File size: 4,105 Bytes
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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