Spaces:
Sleeping
Sleeping
File size: 9,272 Bytes
1c9c13f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | """νμ΄μ§ λ³ν©(Pager) νκ· β νν°μ
λΆλ³μ Β· μ¨μ μν λ±κ°μ± Β· 볡μ λ λ.
ν΅μ¬ νκ·: λ³ν©ν΄λ μΉ΄λλΉ λΆμν¨κ³Ό(μ μ½λ/μ λ’°μΆ)λ μ νν 1ν β μΉ΄λ λ¨μ μμ κ±·κΈ°μ
μ΅μ’
μ¨μ μνΒ·μλ©μ΄ μμ ν μΌμΉν΄μΌ νλ€.
"""
import pytest
from engine import router
from engine.repositories.content import ContentRepository
from engine.repositories.play_session import PlaySessionRepository
from engine.services.paging import Pager
from engine.services.story import StoryEngine
@pytest.fixture(scope="module")
def repo():
return ContentRepository("content")
@pytest.fixture(scope="module")
def pager(repo):
cuts = {cid for cid in router.IMAGE_MAP if repo.has_card(cid)}
solo = cuts | {c.id for c in repo.all_cards() if c.can_chat}
return Pager(repo, solo_ids=solo, cut_ids=cuts)
def pick(choices, policy):
idx = next((i for i, c in enumerate(choices) if c.axis == policy), None)
if idx is None:
idx = next((i for i, c in enumerate(choices) if c.axis in (None, "neutral")), 0)
return idx
def play_paged(repo, pager, policy):
"""λΌμ°ν°μ λμΌν νμ΄μ§ κ±·κΈ°. (engine, [PageViewβ¦]) λ°ν."""
engine = StoryEngine(repo)
pages = [pager.complete(engine, engine.start())]
while pages[-1].ending is None:
page = pages[-1]
if page.choices:
_, step = engine.choose(pick(page.choices, policy))
else:
step = engine.advance()
pages.append(pager.complete(engine, step))
return engine, pages
def play_cards(repo, policy):
"""μΉ΄λ λ¨μ μμ κ±·κΈ° (κΈ°μ€ λμ‘°κ΅°)."""
engine = StoryEngine(repo)
step = engine.start()
while step.ending is None:
if step.choices:
_, step = engine.choose(pick(step.choices, policy))
else:
step = engine.advance()
return engine
# ββ νν°μ
λΆλ³μ βββββββββββββββββββββββββββββββββββββββββββ
def test_partitions_cover_all_cards_once_in_order(repo, pager):
flat = [cid for part in pager.partitions for cid in part]
assert flat == [c.id for c in repo.all_cards()]
def test_partition_break_invariants(repo, pager):
for part in pager.partitions:
cards = [repo.get_card(cid) for cid in part]
for a, b in zip(cards, cards[1:]):
# νμ΄μ§ λ΄λΆλ μ ν λ§ν¬ + κ²°μ μ§μ /μνΌμλ κ²½κ³ μμ
assert a.next == b.id
assert not a.choices and not a.next_options
assert a.episode_id == b.episode_id
# solo(μ°μΆ μ»·Β·μ±ν
μ΅μ»€)λ λ¨λ
νμ΄μ§
for c in cards:
if c.id in pager.solo:
assert len(part) == 1
def test_balanced_density_band(repo, pager):
"""κ· ν λΆν β λ³ν©(λ©ν°μΉ΄λ) νμ΄μ§λ λͺ©ν λλΉ κ³Όμ /λΉμ½ μμ΄ λ°΄λ μμ λ λ€."""
def chars(part):
return sum(len(b.text) for cid in part for b in repo.get_card(cid).narration)
multi = [chars(p) for p in pager.partitions if len(p) > 1]
assert multi, "λ³ν© νμ΄μ§κ° μμ΄μΌ νλ€"
assert max(multi) <= pager.target * 1.7 # 그리λ κ³Όμ (κΈ°μ‘΄ 964μ) μ¬λ° λ°©μ§
assert min(multi) >= pager.target * 0.55 # ννΈ λ³ν© μ€ν¨ λ°©μ§
def test_merging_actually_reduces_pages(repo, pager):
assert len(pager.partitions) < repo.total_cards # 61λ³΄λ€ μ μ΄μΌ λ³ν© μλ―Έκ° μλ€
assert pager.display_total == len(pager.partitions) + len(pager.cuts & set(pager.part_of))
# ββ μ¨μ μν λ±κ°μ± (ν΅μ¬ νκ·) ββββββββββββββββββββββββββββ
@pytest.mark.parametrize("policy", ["trust", "doubt", "neutral"])
def test_paged_walk_equals_card_walk(repo, pager, policy):
paged, _ = play_paged(repo, pager, policy)
plain = play_cards(repo, policy)
assert paged.ending.id == plain.ending.id
assert paged.state.snapshot() == plain.state.snapshot() # λΆμν¨κ³Ό μ νν 1ν
assert paged.visited == plain.visited
# ββ μ§νλ νμ βββββββββββββββββββββββββββββββββββββββββββββ
def test_display_index_monotonic_over_playthrough(repo, pager):
engine, pages = play_paged(repo, pager, "trust")
seen = [pager.display_index(p.last.id) for p in pages if p.ending is None]
assert seen == sorted(seen) # μ§νλλ μ λ λ€λ‘ κ°μ§ μλλ€
assert seen[0] == 1
assert seen[-1] <= pager.display_total
# ββ 볡μ λ λ βββββββββββββββββββββββββββββββββββββββββββββββ
def test_render_current_matches_played_page(repo, pager):
mid = StoryEngine(repo)
mid_pages = [pager.complete(mid, mid.start())]
for _ in range(3):
mid_pages.append(pager.complete(mid, mid.advance() if not mid_pages[-1].choices
else mid.choose(pick(mid_pages[-1].choices, "trust"))[1]))
restored = StoryEngine.restore(repo, mid.snapshot())
view = pager.render_current(restored)
last_seen = mid_pages[-1]
assert [b.text for b in view.narration] == [b.text for b in last_seen.narration]
assert view.first.id == last_seen.first.id and view.last.id == last_seen.last.id
# ββ λΈλ‘ μν μ λ (lead_in) ββββββββββββββββββββββββββββββββ
def test_lead_in_roles_derived_on_load(repo):
"""λ°ν μ§μ μ μ§§μ proseλ lead_in β E04-10 'ν λ¬Έμ₯ νμ΄μ§' νκ·."""
card = repo.get_card("E04-10")
assert card.narration[0].type == "prose"
assert card.narration[0].role == "lead_in" # 39μ 리λμΈ
assert card.narration[2].role is None # μ¬ν proseλ λ―Έμ§μ
# κΈ΄ prose(140μ μ΄κ³Ό)κ° λ°ν μμ μμ΄λ 리λμΈμ΄ μλλ€ β E01-03 첫 λΈλ‘
long_case = repo.get_card("E01-03")
assert long_case.narration[0].type == "prose"
assert len(long_case.narration[0].text) > 140
assert long_case.narration[1].type == "character_dialogue"
assert long_case.narration[0].role is None
def test_thin_card_warnings():
"""Cμ β νλ©΄ μ΅μ λ°λ λ―Έλ¬ μΉ΄λλ validate κ²½κ³ μ μ‘νλ€."""
from pathlib import Path
from engine.schemas.validate import check_links, load_content
episodes, r_cards, chapter, *_ = load_content(Path("content"))
_, warnings = check_links(episodes, chapter, r_cards)
flagged = [w for w in warnings if "λ―Έλ¬" in w]
assert any(w.startswith("E05-05") for w in flagged) # 96μ μ±ν
μΉ΄λ
assert any(w.startswith("E06-10C") for w in flagged) # 154μ λΆκΈ° λ³ν
# ββ λΌμ°ν° λ 벨 βββββββββββββββββββββββββββββββββββββββββββββ
@pytest.mark.asyncio
async def test_background_follows_place_tags_and_meta_has_intro(tmp_path, monkeypatch):
"""μ₯μ νκ·Έ β λ°°κ²½ λ§€νμ΄ μμ£Ό λμ λ°μλκ³ , μΈνΈλ‘ μνμ€κ° λ©νμ μ€λ¦°λ€."""
monkeypatch.setattr(router, "_playdb", PlaySessionRepository(tmp_path / "s.db"))
monkeypatch.setattr(router, "_sessions", {})
meta = await router.meta()
assert meta["intro_images"] == router.INTRO_IMAGES and len(meta["intro_images"]) == 3
data = await router.create_session()
sid, step = data["session_id"], data["step"]
seen = {step["background"]}
portraits = set()
while step["ending"] is None:
portraits |= {n["portrait"] for n in step["narration"]
if n["type"] == "character_dialogue" and n["portrait"]}
if step["choices"]:
step = (await router.choose(sid, router.ChooseBody(index=0)))["step"]
else:
step = (await router.advance(sid))["step"]
seen.add(step["background"])
# μΉ¨μ€Β·λ§μ°¬ν(μλΉ)Β·μ²κΈΈ(μκ° μ²)μ μ΄λ κ²½λ‘λ μ§λκ°λ€ + μλ©μ λ°°κ²½ μμ
assert {"/assets/λ°°κ²½/μΉ¨μ€.png", "/assets/λ°°κ²½/μλΉ.png", "/assets/λ°°κ²½/μκ° μ².png"} <= seen
assert step["background"] is None
# νμ μ΄μ β μΉ΄λ₯΄λ°λΌΒ·μλ²μ§λ μ΄λ κ²½λ‘λ λ°ννλ€ (VN λ°ν μ°μΆ)
assert {"/assets/μΈλ¬Ό/μΉ΄λ₯΄λ°λΌ.png", "/assets/μΈλ¬Ό/μλ²μ§.png"} <= portraits
@pytest.mark.asyncio
async def test_router_serves_merged_pages(tmp_path, monkeypatch):
monkeypatch.setattr(router, "_playdb", PlaySessionRepository(tmp_path / "s.db"))
monkeypatch.setattr(router, "_sessions", {})
data = await router.create_session()
sid, step = data["session_id"], data["step"]
total = step["progress"]["total"]
assert total == router._pager.display_total
clicks = 1
while step["ending"] is None:
if step["choices"]:
step = (await router.choose(sid, router.ChooseBody(index=0)))["step"]
else:
step = (await router.advance(sid))["step"]
clicks += 1
assert clicks < 61 # μΉ΄λ μλ³΄λ€ μ μ λκΉμΌλ‘ μμ£Ό = λ³ν© λμ
|