"""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] # Move every late play to turn 0 (worth >2 plays -> also rejected). 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), ) # Harness fallback -> seeded rng pick; either way the result is legal. 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 # the offline rng actually exercises the menu