| """The encounter author: bounded composition, validated choice, sim gate.""" |
|
|
| from __future__ import annotations |
|
|
| import random |
|
|
| import pytest |
|
|
| from scrypt.data import load_content |
| from scrypt.engine.combat import ScriptedPlay |
| from scrypt.warden import author |
|
|
|
|
| @pytest.fixture(scope="module") |
| def content(): |
| return load_content() |
|
|
|
|
| REAL_ENCOUNTERS = ["first_blood", "audit_sweep", "swap_storm", "scheduled_doom", "init_zero"] |
|
|
|
|
| def test_every_offered_variant_is_inside_the_bands(content): |
| for enc_id in REAL_ENCOUNTERS: |
| base = content.encounters[enc_id]["script"] |
| options = author.variants(content, base, random.Random(7)) |
| assert options[0].label == author.BASE_LABEL |
| for v in options[1:]: |
| assert author.within_bands(v.script, base), f"{enc_id}/{v.label}" |
| assert v.script != base |
|
|
|
|
| def test_real_encounters_offer_real_choices(content): |
| """If mutations silently stop building, the author dies quietly — |
| make that loud: every non-tutorial encounter must offer ≥2 variants.""" |
| for enc_id in ["audit_sweep", "swap_storm", "scheduled_doom", "init_zero"]: |
| base = content.encounters[enc_id]["script"] |
| assert len(author.variants(content, base, random.Random(7))) >= 3, enc_id |
|
|
|
|
| def test_bands_reject_inflation_and_frontloading(content): |
| base = content.encounters["audit_sweep"]["script"] |
| init = content.card("init") |
| inflated = [list(t) for t in base] |
| inflated[0] = [ScriptedPlay(lane=0, card=init), ScriptedPlay(lane=1, card=init)] |
| assert not author.within_bands(inflated, base) |
|
|
| frontloaded = [list(t) for t in base] |
| |
| frontloaded[0] = [p for t in base for p in t][:3] |
| assert not author.within_bands(frontloaded, base) |
|
|
| wrong_length = [list(t) for t in base][:-1] |
| assert not author.within_bands(wrong_length, base) |
|
|
|
|
| def test_smoke_gate_passes_base_against_itself(content): |
| base = content.encounters["first_blood"]["script"] |
| deck = list(content.starter_decks["vanilla"]["cards"]) |
| assert author.smoke_gate(content, base, base, deck, seeds=8) |
|
|
|
|
| class ComposeBackend: |
| """Answers the harness with a fixed tool call.""" |
|
|
| def __init__(self, variant: str): |
| self.reply = '{"tool": "compose", "args": {"variant": "%s"}}' % variant |
|
|
| async def stream(self, messages, **kw): |
| yield self.reply |
|
|
|
|
| async def test_compose_honors_a_valid_model_pick(content): |
| base = content.encounters["audit_sweep"]["script"] |
| labels = [v.label for v in author.variants(content, base, random.Random(7))] |
| assert "the swarm" in labels |
| deck = list(content.starter_decks["vanilla"]["cards"]) |
| script, label = await author.compose( |
| ComposeBackend("the swarm"), content, "audit_sweep", |
| deck, "- leans on firewall", random.Random(7), |
| ) |
| assert label == "the swarm" |
| assert script != base |
| assert author.within_bands(script, base) |
|
|
|
|
| async def test_compose_survives_a_rambling_model(content): |
| class Rambler: |
| async def stream(self, messages, **kw): |
| yield "I think, given the circumstances, we should talk about feelings." |
|
|
| deck = list(content.starter_decks["vanilla"]["cards"]) |
| script, label = await author.compose( |
| Rambler(), content, "audit_sweep", deck, "", random.Random(3), |
| ) |
| |
| base = content.encounters["audit_sweep"]["script"] |
| assert label is None or author.within_bands(script, base) |
|
|
|
|
| async def test_compose_offline_still_varies_but_stays_legal(content): |
| base = content.encounters["scheduled_doom"]["script"] |
| deck = list(content.starter_decks["graveyard"]["cards"]) |
| seen = set() |
| for seed in range(6): |
| script, label = await author.compose( |
| None, content, "scheduled_doom", deck, "", random.Random(seed), |
| ) |
| seen.add(label) |
| if label is not None: |
| assert author.within_bands(script, base) |
| assert len(seen) > 1 |
|
|