Scrypt / tests /test_author.py
IMJONEZZ's picture
SCRYPT: initial commit — game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
4.11 kB
"""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