Spaces:
Running on Zero
Running on Zero
| """The encounter author: the Warden re-composes the next fight — in bands. | |
| The cardinal rule holds. Composition is deterministic: a small set of | |
| mutations of the YAML-authored base script, each REJECTED at build time | |
| unless it stays inside threat-budget bands (total cost and a cumulative | |
| front-load cap). The LLM's entire authority is choosing WHICH pre-proven | |
| variant will sting this player hardest, informed by its memory shards. | |
| A bot smoke-sim guards the winner; any failure anywhere returns the base | |
| script exactly as authored. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| from dataclasses import dataclass | |
| from scrypt.engine.bots import simulate | |
| from scrypt.engine.cards import Card | |
| from scrypt.engine.combat import LANES, EncounterScript, Result, ScriptedPlay | |
| from .context import build_messages | |
| from .harness import Harness, Tool | |
| BASE_LABEL = "as scheduled" | |
| # Threat bands: a variant may shave a fifth or add 15% — never more. | |
| TOTAL_LOW, TOTAL_HIGH = 0.80, 1.15 | |
| FRONTLOAD_SLACK = 7 # cumulative threat may lead the base by at most this | |
| GATE_DROP = 0.25 # bot winrate may fall at most this far below base | |
| GATE_SEEDS = 24 | |
| def card_threat(card: Card) -> int: | |
| return card.power * 2 + card.health + 2 * len(card.sigils) | |
| def script_cost(script: EncounterScript) -> int: | |
| return sum(card_threat(p.card) for turn in script for p in turn) | |
| def _cumulative(script: EncounterScript) -> list[int]: | |
| total, out = 0, [] | |
| for turn in script: | |
| total += sum(card_threat(p.card) for p in turn) | |
| out.append(total) | |
| return out | |
| def within_bands(candidate: EncounterScript, base: EncounterScript) -> bool: | |
| if len(candidate) != len(base): | |
| return False | |
| if any(len(turn) > 2 for turn in candidate): | |
| return False | |
| if any(not (0 <= p.lane < LANES) for turn in candidate for p in turn): | |
| return False | |
| base_cost = script_cost(base) | |
| cost = script_cost(candidate) | |
| if not (base_cost * TOTAL_LOW <= cost <= base_cost * TOTAL_HIGH): | |
| return False | |
| for cand_cum, base_cum in zip(_cumulative(candidate), _cumulative(base)): | |
| if cand_cum > base_cum + FRONTLOAD_SLACK: | |
| return False | |
| return True | |
| # ----------------------------------------------------------- mutations | |
| def _copy(script: EncounterScript) -> list[list[ScriptedPlay]]: | |
| return [list(turn) for turn in script] | |
| def _flat(script) -> list[tuple[int, int, ScriptedPlay]]: | |
| return [(t, i, p) for t, turn in enumerate(script) for i, p in enumerate(turn)] | |
| def _free_lane(turn: list[ScriptedPlay], rng: random.Random) -> int | None: | |
| free = sorted(set(range(LANES)) - {p.lane for p in turn}) | |
| return rng.choice(free) if free else None | |
| def _swarm(script, content, rng): | |
| """Trade the heaviest scheduled play for cheap early pressure.""" | |
| out = _copy(script) | |
| plays = _flat(out) | |
| if not plays: | |
| return None | |
| t, i, _ = max(plays, key=lambda x: card_threat(x[2].card)) | |
| del out[t][i] | |
| reaper = content.card("reaper") | |
| placed = 0 | |
| for turn_idx in range(1, len(out)): | |
| if placed == 2: | |
| break | |
| if len(out[turn_idx]) < 2: | |
| lane = _free_lane(out[turn_idx], rng) | |
| if lane is not None: | |
| out[turn_idx].append(ScriptedPlay(lane=lane, card=reaper)) | |
| placed += 1 | |
| return out if placed == 2 else None | |
| def _dead_air(script, content, rng): | |
| """The ground forces stand down; everything comes in over the wall.""" | |
| out = _copy(script) | |
| kill = content.card("kill-signal") | |
| grounded = [ | |
| (t, i) for t, i, p in _flat(out) | |
| if not p.card.has("tunneling") and card_threat(p.card) >= card_threat(kill) | |
| ] | |
| if len(grounded) < 2: | |
| return None | |
| for t, i in rng.sample(grounded, 2): | |
| out[t][i] = ScriptedPlay(lane=out[t][i].lane, card=kill) | |
| return out | |
| def _panes(script, content, rng): | |
| """Cheap bodies become multiplexers: adjacency stops being safe.""" | |
| out = _copy(script) | |
| mux = content.card("multiplexer") | |
| cheap = sorted( | |
| ( | |
| (t, i) for t, i, p in _flat(out) | |
| if p.card.id != "multiplexer" and card_threat(p.card) <= card_threat(mux) | |
| ), | |
| key=lambda x: card_threat(out[x[0]][x[1]].card), | |
| ) | |
| if len(cheap) < 2: | |
| return None | |
| for t, i in cheap[:2]: | |
| out[t][i] = ScriptedPlay(lane=out[t][i].lane, card=mux) | |
| return out | |
| def _wall(script, content, rng): | |
| """Two small plays are recalled; something large is scheduled instead.""" | |
| out = _copy(script) | |
| plays = sorted(_flat(out), key=lambda x: card_threat(x[2].card)) | |
| if len(plays) < 3: | |
| return None | |
| for t, i, _ in sorted(plays[:2], key=lambda x: (x[0], -x[1])): | |
| del out[t][i] | |
| mid = len(out) // 2 | |
| for turn_idx in (mid, mid + 1, mid - 1): | |
| if 0 <= turn_idx < len(out) and len(out[turn_idx]) < 2: | |
| lane = _free_lane(out[turn_idx], rng) | |
| if lane is not None: | |
| out[turn_idx].append( | |
| ScriptedPlay(lane=lane, card=content.card("cron-golem")) | |
| ) | |
| return out | |
| return None | |
| MUTATIONS = [ | |
| ("the swarm", "many cheap processes, early — the board fills before they breathe", _swarm), | |
| ("dead air", "ground forces swapped for tunneling kill-signals; blockers stop mattering", _dead_air), | |
| ("panes within panes", "small bodies become multiplexers; standing next to anything hurts", _panes), | |
| ("the wall", "small plays recalled; a cron-golem is scheduled in their place", _wall), | |
| ] | |
| class Variant: | |
| label: str | |
| description: str | |
| script: EncounterScript | |
| def variants(content, base: EncounterScript, rng: random.Random) -> list[Variant]: | |
| """Every pre-proven option, base first. Mutations that fail to build | |
| or fall outside the bands simply don't make the menu.""" | |
| out = [Variant(BASE_LABEL, "the encounter exactly as scheduled", base)] | |
| for label, desc, fn in MUTATIONS: | |
| candidate = fn(base, content, rng) | |
| if candidate is not None and within_bands(candidate, base): | |
| out.append(Variant(label, desc, candidate)) | |
| return out | |
| # ----------------------------------------------------------- the choice | |
| def choice_frame(encounter_name: str, shards: str, options: list[Variant]) -> str: | |
| menu = "\n".join(f"- {v.label}: {v.description}" for v in options) | |
| memory = shards.strip() or "- (no observations on file yet)" | |
| return ( | |
| f"You are re-composing the encounter '{encounter_name}' for this " | |
| "specific player.\nWhat you remember of them:\n" | |
| f"{memory}\n\nPre-approved variants:\n{menu}\n\n" | |
| "Choose the variant that punishes their habits hardest by calling the tool." | |
| ) | |
| async def _choose(backend, encounter_name: str, shards: str, | |
| options: list[Variant], rng: random.Random) -> str: | |
| labels = [v.label for v in options] | |
| if backend is None: | |
| return rng.choice(labels) | |
| picked: list[str] = [] | |
| tool = Tool( | |
| name="compose", | |
| description="pick one pre-approved encounter variant", | |
| schema={"properties": {"variant": {"enum": labels}}, "required": ["variant"]}, | |
| handler=lambda args: picked.append(args["variant"]) or "composed", | |
| ) | |
| harness = Harness(backend, [tool], max_steps=2, max_tokens=120) | |
| await harness.run(build_messages(choice_frame(encounter_name, shards, options))) | |
| return picked[0] if picked else rng.choice(labels) | |
| def smoke_gate(content, candidate: EncounterScript, base: EncounterScript, | |
| deck: list[Card], seeds: int = GATE_SEEDS) -> bool: | |
| """The floor bot plays the player's own deck against both scripts. | |
| A variant that craters the floor winrate is too hot to ship.""" | |
| side = [content.card("bit")] * 20 | |
| def rate(script) -> float: | |
| wins = sum( | |
| simulate(list(deck), side, script, seed).result is Result.PLAYER_WIN | |
| for seed in range(seeds) | |
| ) | |
| return wins / seeds | |
| return rate(candidate) >= rate(base) - GATE_DROP | |
| async def compose( | |
| backend, | |
| content, | |
| encounter_id: str, | |
| deck: list[Card], | |
| shards: str, | |
| rng: random.Random, | |
| ) -> tuple[EncounterScript, str | None]: | |
| """(script, variant_label-or-None). None means: exactly as authored.""" | |
| base = content.encounters[encounter_id]["script"] | |
| options = variants(content, base, rng) | |
| if len(options) <= 1: | |
| return base, None | |
| try: | |
| label = await _choose( | |
| backend, content.encounters[encounter_id]["name"], shards, options, rng | |
| ) | |
| except Exception: | |
| return base, None | |
| if label == BASE_LABEL: | |
| return base, None | |
| chosen = next(v for v in options if v.label == label) | |
| if not smoke_gate(content, chosen.script, base, deck): | |
| return base, None | |
| return chosen.script, chosen.label | |