new RPG system
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from services.rpg_story import (
|
||||
migrate_beats_to_steps,
|
||||
normalize_story_arc,
|
||||
get_current_step,
|
||||
step_progress,
|
||||
is_arc_completed,
|
||||
sync_quest_to_current_step,
|
||||
apply_step_advance,
|
||||
apply_story_post,
|
||||
is_new_arc_request,
|
||||
normalize_new_arc_first,
|
||||
new_arc_roll_choice,
|
||||
append_new_arc_roll_choice,
|
||||
format_step_guidance_for_character,
|
||||
format_new_arc_opening,
|
||||
normalize_step,
|
||||
resolve_step_injection,
|
||||
roll_next_arc,
|
||||
)
|
||||
|
||||
|
||||
def test_migrate_beats_to_steps():
|
||||
arc = {
|
||||
"beats": [
|
||||
{"id": "b1", "title": "Meet Yuki", "injection": "Door opens.", "choices": []},
|
||||
]
|
||||
}
|
||||
out = migrate_beats_to_steps(arc)
|
||||
assert len(out["steps"]) == 1
|
||||
assert "beats" not in out
|
||||
assert out["steps"][0]["title"] == "Meet Yuki"
|
||||
|
||||
|
||||
def test_step_progress():
|
||||
arc = normalize_story_arc({
|
||||
"steps": [{"id": "s1", "title": "A"}, {"id": "s2", "title": "B"}],
|
||||
"current_step_index": 1,
|
||||
})
|
||||
assert step_progress(arc) == (2, 2)
|
||||
|
||||
|
||||
def test_is_arc_completed():
|
||||
arc = normalize_story_arc({
|
||||
"steps": [{"id": "s1", "title": "A"}],
|
||||
"current_step_index": 1,
|
||||
"status": "active",
|
||||
})
|
||||
assert is_arc_completed(arc)
|
||||
|
||||
|
||||
def test_new_arc_request_detected():
|
||||
assert is_new_arc_request("Начать новую арку")
|
||||
assert is_new_arc_request("Начать новый квест: 'Тайна плазменных шаров'")
|
||||
assert is_new_arc_request("Start new quest")
|
||||
assert not is_new_arc_request("привет")
|
||||
|
||||
|
||||
def test_normalize_new_arc_first():
|
||||
assert normalize_new_arc_first("user") == "user"
|
||||
assert normalize_new_arc_first("Character") == "character"
|
||||
assert normalize_new_arc_first("other") is None
|
||||
|
||||
|
||||
def test_new_arc_roll_choice_and_filter():
|
||||
base = new_arc_roll_choice()
|
||||
assert base["type"] == "new_arc_roll"
|
||||
merged = append_new_arc_roll_choice([
|
||||
{"id": "x", "label": "Начать новый квест: Foo", "source": "narrator"},
|
||||
{"id": "y", "label": "Пойти спать", "source": "narrator"},
|
||||
])
|
||||
assert len(merged) == 2
|
||||
assert merged[-1]["type"] == "new_arc_roll"
|
||||
assert not any("новый квест" in (c.get("label") or "").lower() for c in merged[:-1])
|
||||
|
||||
|
||||
def test_normalize_step_repairs_legacy_beat_shape():
|
||||
step = normalize_step({
|
||||
"status_quo": "Сара и Григо прибывают к лаборатории.",
|
||||
"choices": [{"label": "Войти", "injection": "*Дверь скрипит*"}],
|
||||
}, 0)
|
||||
assert step["title"]
|
||||
assert step["completion_criteria"]
|
||||
assert resolve_step_injection(step) == "*Дверь скрипит*"
|
||||
|
||||
|
||||
def test_format_new_arc_opening():
|
||||
arc = {"title": "Тайна", "global_story": "Расследование шаров."}
|
||||
step = {"status_quo": "У ворот лаборатории тихо.", "choices": []}
|
||||
text = format_new_arc_opening(arc, step, lang="ru")
|
||||
assert "Тайна" in text
|
||||
assert "лаборатории" in text or "Расследование" in text
|
||||
|
||||
|
||||
def test_guidance_block_ru():
|
||||
step = {"title": "Приютить", "goal": "Впустить внутрь", "character_guidance": "Робость"}
|
||||
block = format_step_guidance_for_character(step, {"global_story": "x", "steps": [step]}, lang="ru")
|
||||
assert "Приютить" in block
|
||||
assert "ОБЯЗАТЕЛЬНОЕ" in block
|
||||
|
||||
|
||||
def test_apply_step_advance():
|
||||
arc = normalize_story_arc({
|
||||
"title": "Test",
|
||||
"steps": [
|
||||
{"id": "s1", "title": "Step One", "choices": []},
|
||||
{"id": "s2", "title": "Step Two", "injection": "Next scene.", "choices": []},
|
||||
],
|
||||
"current_step_index": 0,
|
||||
})
|
||||
|
||||
async def run():
|
||||
with patch("services.memory.upsert_quest", new_callable=AsyncMock) as uq:
|
||||
with patch("services.memory.update_session_plot_arc", new_callable=AsyncMock):
|
||||
with patch(
|
||||
"services.rpg_story.sync_quest_to_current_step",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
return await apply_step_advance("sess", arc), uq
|
||||
|
||||
result, mock_uq = asyncio.run(run())
|
||||
assert result["advanced"] is True
|
||||
assert result["injection"] == "Next scene."
|
||||
mock_uq.assert_called()
|
||||
|
||||
|
||||
def test_apply_story_post_step_complete_fallback_via_note():
|
||||
arc = normalize_story_arc({
|
||||
"title": "Test",
|
||||
"steps": [
|
||||
{"id": "s1", "title": "Step One", "choices": []},
|
||||
{"id": "s2", "title": "Step Two", "choices": []},
|
||||
],
|
||||
"current_step_index": 0,
|
||||
})
|
||||
|
||||
post = {
|
||||
"step_complete": False,
|
||||
"step_completion_note": "The scene clearly satisfies the completion criteria.",
|
||||
}
|
||||
|
||||
async def run():
|
||||
with patch("services.memory.get_session", new_callable=AsyncMock) as gs:
|
||||
gs.return_value = {"plot_arc_json": json.dumps(arc, ensure_ascii=False)}
|
||||
with patch(
|
||||
"services.rpg_story.apply_step_advance",
|
||||
new_callable=AsyncMock,
|
||||
) as asa:
|
||||
asa.return_value = {"advanced": True, "arc_completed": False, "new_step": arc["steps"][1], "injection": ""}
|
||||
r = await apply_story_post("sess", post, arc, {"quests": True})
|
||||
return r, asa
|
||||
|
||||
r, asa = asyncio.run(run())
|
||||
assert r["step_advanced"] is True
|
||||
assert asa.await_count == 1
|
||||
|
||||
|
||||
def test_roll_next_arc_combines_context_for_generate_next_arc():
|
||||
old_arc = normalize_story_arc({
|
||||
"title": "Arc One",
|
||||
"steps": [{"id": "s1", "title": "Done"}],
|
||||
"current_step_index": 1,
|
||||
"status": "completed",
|
||||
})
|
||||
new_arc = normalize_story_arc({
|
||||
"title": "Arc Two",
|
||||
"steps": [{"id": "s1", "title": "Start"}],
|
||||
"current_step_index": 0,
|
||||
})
|
||||
|
||||
async def run():
|
||||
with patch("services.memory.get_session", new_callable=AsyncMock) as gs:
|
||||
gs.return_value = {"plot_arc_json": json.dumps(old_arc, ensure_ascii=False)}
|
||||
with patch("services.rpg_plot.generate_next_arc", new_callable=AsyncMock) as gna:
|
||||
gna.return_value = new_arc
|
||||
with patch("services.memory.update_session_plot_arc", new_callable=AsyncMock):
|
||||
with patch(
|
||||
"services.rpg_story.sync_quest_to_current_step",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
return await roll_next_arc(
|
||||
"sess",
|
||||
{"name": "Sarah", "description": "", "scenario": ""},
|
||||
"Начать новую арку",
|
||||
"adventure",
|
||||
recent_context="assistant: goodbye",
|
||||
), gna
|
||||
|
||||
result, gna = asyncio.run(run())
|
||||
assert result["title"] == "Arc Two"
|
||||
gna.assert_awaited_once()
|
||||
assert gna.await_args.args[3] == "assistant: goodbye\nНачать новую арку"
|
||||
Reference in New Issue
Block a user