Spaces:
Running on Zero
Running on Zero
File size: 14,821 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 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 | """Branching paths, the legacy system, card art, and the newer puzzles."""
from __future__ import annotations
import pytest
from scrypt.data import load_content
from scrypt.engine import legacy as legacy_store
from scrypt.engine.run import NodeKind, new_run
from scrypt.sandbox.fabricate import fabricate_home
from scrypt.sandbox.inheritance import plant_legacy, warden_quarters
from scrypt.sandbox.puzzles import plant_all
from scrypt.sandbox.shell import Shell
from scrypt.sandbox.vfs import VFS
@pytest.fixture
def content():
return load_content()
# ----------------------------------------------------------------- forks
def test_fork_ids_become_fork_nodes(content):
deck = content.starter_decks["vanilla"]["cards"]
run = new_run(deck, ["first_blood", "crossroads_audit"], fork_ids=frozenset(content.forks))
kinds = [n.kind for n in run.nodes]
assert kinds[0] is NodeKind.BATTLE
assert kinds[-1] is NodeKind.FORK
def test_resolve_fork_swaps_in_the_chosen_battle(content):
deck = content.starter_decks["vanilla"]["cards"]
run = new_run(deck, ["crossroads_audit"], fork_ids=frozenset(content.forks))
run.resolve_fork("swap_storm")
assert run.current.kind is NodeKind.BATTLE
assert run.current.payload == "swap_storm"
def test_resolve_fork_requires_standing_at_one(content):
deck = content.starter_decks["vanilla"]["cards"]
run = new_run(deck, ["first_blood"])
with pytest.raises(ValueError):
run.resolve_fork("swap_storm")
def test_fork_options_reference_real_encounters(content):
for fork in content.forks.values():
assert len(fork["options"]) >= 2
for opt in fork["options"]:
assert opt["encounter"] in content.encounters
if opt["bounty"]:
assert opt["bounty"]["kind"] in ("cycles", "draft")
# ---------------------------------------------------------------- legacy
def _crash(data, statement="no regrets", strongest=None, cycles=12):
legacy_store.record_crash(
data,
encounter="audit_sweep",
turn=6,
deck_ids=["daemon", "tarpit"],
strongest=strongest,
statement=statement,
cycles=cycles,
estate_card="oom-killer",
password="stoat",
)
def test_clean_statement_is_printable_and_bounded():
cleaned = legacy_store.clean_statement("\x1b[31mEVIL\x07 " + "x" * 500)
assert "\x1b" not in cleaned and "\x07" not in cleaned
assert len(cleaned) <= legacy_store.MAX_STATEMENT
assert legacy_store.clean_statement(" ") == "(they said nothing)"
def test_record_crash_caps_history_and_seals_estate(tmp_path, monkeypatch):
monkeypatch.setenv("SCRYPT_HOME", str(tmp_path))
data = legacy_store.load()
for i in range(legacy_store.MAX_CRASHES + 2):
_crash(data, statement=f"death {i}")
legacy_store.save(data)
again = legacy_store.load()
assert len(again["crashes"]) == legacy_store.MAX_CRASHES
assert again["runs"] == legacy_store.MAX_CRASHES + 2
assert again["estate"] == {"cycles": 12, "card": "oom-killer", "password": "stoat"}
def test_legacy_survives_a_corrupt_store(tmp_path, monkeypatch):
monkeypatch.setenv("SCRYPT_HOME", str(tmp_path))
(tmp_path / "legacy.json").write_text("not even json")
assert legacy_store.load() == legacy_store.EMPTY
(tmp_path / "legacy.json").write_text('{"crashes": "what", "wins": 3}')
data = legacy_store.load()
assert data["crashes"] == [] and data["wins"] == 3
def test_audit_level_spares_the_first_win():
data = dict(legacy_store.EMPTY)
assert legacy_store.audit_level(data) == 0
legacy_store.record_win(data, most_played="daemon", diary_password="moth-orbit")
assert legacy_store.audit_level(data) == 0 # first win: pure payoff
legacy_store.record_win(data, most_played="daemon", diary_password="moth-orbit")
assert legacy_store.audit_level(data) == 1
data["wins"] = 99
assert legacy_store.audit_level(data) == 2 # hard cap
def test_conscript_is_clamped_no_matter_the_json():
card = legacy_store.conscript(
{"strongest": {"name": "Root", "power": 99, "health": -3, "sigils": ["privileged", "fake_sigil"]}}
)
assert card.power == 4 and card.health == 1
assert card.sigils == ("privileged",)
assert legacy_store.conscript({"strongest": None}) is None
assert legacy_store.conscript({"strongest": {"power": "lots"}}) is None
# ----------------------------------------------- inheritance in the sandbox
def _legacy_with_everything():
data = dict(legacy_store.EMPTY)
_crash(data, statement="see you in the logs", strongest={"name": "OOM Killer"})
data["daemon"] = "fork-bomb"
return data
def test_crash_dump_leaks_the_estate_key():
sh = _shell()
plant_legacy(sh.vfs, _legacy_with_everything())
dump = sh.run("cat /var/crash/run-1.core").out
assert 'ESTATE_KEY="stoat"' in dump
assert "see you in the logs" in dump
assert "reassigned" in dump # the conscription notice
def test_estate_puzzle_pays_out_with_the_dump_password():
sh = _shell()
puzzles = {p.id: p for p in plant_legacy(sh.vfs, _legacy_with_everything())}
sh.run("cd /home/drifter")
assert sh.run("unzip estate.zip wrongpass").err
assert puzzles["estate"].poll(sh) is None
assert "inflating" in sh.run("unzip estate.zip stoat").out
sh.run("cat estate.txt")
reward = puzzles["estate"].poll(sh)
assert reward is not None and reward.kind == "estate"
assert reward.value == {"cycles": 12, "card": "oom-killer"}
def test_daemon_arms_via_chmod():
sh = _shell()
puzzles = {p.id: p for p in plant_legacy(sh.vfs, _legacy_with_everything())}
assert puzzles["daemon"].poll(sh) is None
assert sh.run("chmod +x /etc/init.d/missing").err
sh.run("chmod +x /etc/init.d/fork-bomb")
reward = puzzles["daemon"].poll(sh)
assert reward is not None and reward.kind == "daemon" and reward.value == "fork-bomb"
def test_warden_quarters_leak_the_diary_password():
data = _legacy_with_everything()
vfs = warden_quarters(data, diary_password="lanternfish")
sh = Shell(vfs)
assert sh.vfs.cwd_path == "/root"
diary = sh.run("cat diary.log").out
assert '"lanternfish"' in diary
record = sh.run("cat drifters/run-1.txt").out
assert "see you in the logs" in record
def test_authored_artifacts_surface_in_future_runs():
data = _legacy_with_everything()
data["crashes"][-1]["note"] = "lane two was open the whole time."
data["diary"] = ["a drifter escaped tonight. unacceptable; logged."]
sh = _shell()
plant_legacy(sh.vfs, data)
dump = sh.run("cat /var/crash/run-1.core").out
assert "warden annotation: lane two was open the whole time." in dump
quarters = Shell(warden_quarters(data, diary_password="stoat"))
diary = quarters.run("cat /root/diary.log").out
assert "a drifter escaped tonight" in diary
def test_legacy_load_coerces_a_broken_diary(tmp_path, monkeypatch):
monkeypatch.setenv("SCRYPT_HOME", str(tmp_path))
(tmp_path / "legacy.json").write_text('{"diary": "not a list", "wins": 1}')
data = legacy_store.load()
assert data["diary"] == [] and data["wins"] == 1
def test_intel_forces_next_runs_archive_password():
sh = _shell()
plant_all(sh.vfs, seed=3, forced_password="lanternfish")
sh.run("cd ~/downloads")
assert "inflating" in sh.run("unzip severance.zip lanternfish").out
# -------------------------------------------------------------- card art
def test_every_card_has_compact_art(content):
for card in content.pool.values():
assert card.art, f"{card.id} has no art"
lines = card.art.splitlines()
assert len(lines) <= 3, f"{card.id} art too tall"
indent = min(len(l) - len(l.lstrip(" ")) for l in lines if l.strip())
assert all(len(l.rstrip()) - indent <= 9 for l in lines), f"{card.id} art too wide"
# ------------------------------------------------------------- card info
def test_every_engine_sigil_has_rules_text():
from scrypt.engine.cards import KNOWN_SIGILS
from scrypt.ui.render import SIGIL_EXPLAIN, SIGIL_GLYPHS
assert set(SIGIL_EXPLAIN) == KNOWN_SIGILS
assert set(SIGIL_GLYPHS) == KNOWN_SIGILS
def test_card_info_states_cost_and_every_sigil(content):
from scrypt.engine.cards import CardInstance
from scrypt.ui.render import card_info
info = card_info(CardInstance(spec=content.card("fork-bomb"))).plain
assert "costs ♦" in info
assert "copy of it is added to your hand" in info
info = card_info(CardInstance(spec=content.card("zombie-process"))).plain
assert "4⊙" in info and "core dumps" in info
info = card_info(CardInstance(spec=content.card("bit"))).plain
assert "free" in info
def test_card_info_for_a_foe_skips_costs(content):
from scrypt.engine.cards import CardInstance
from scrypt.ui.render import card_info
info = card_info(CardInstance(spec=content.card("fork-bomb")), foe=True).plain
assert "the warden's" in info
assert "costs" not in info # the foe never pays
assert "copy of it is added" in info # but its sigils are still explained
# ------------------------------------------------------- table furniture
def _combat(content, main=None, side=None):
from scrypt.engine.combat import CombatState
return CombatState(
main_deck=main if main is not None else list(
content.starter_decks["vanilla"]["cards"]
),
side_deck=side if side is not None else [content.card("bit")] * 5,
script=[[]],
seed=3,
)
def test_deck_stacks_show_counts_and_shout_when_empty(content):
from rich.console import Console
from scrypt.ui.render import deck_stacks
console = Console(width=40, force_terminal=False)
state = _combat(content)
with console.capture() as cap:
console.print(deck_stacks(state))
out = cap.get()
assert "your deck" in out and "bits" in out
assert f"{len(state._draw_pile)} left" in out
drained = _combat(content, side=[])
drained._draw_pile.clear()
with console.capture() as cap:
console.print(deck_stacks(drained))
assert cap.get().count("EMPTY") == 2
def test_scoreboard_carries_the_win_condition(content):
from rich.console import Console
from scrypt.ui.render import scoreboard
console = Console(width=80, force_terminal=False)
with console.capture() as cap:
console.print(scoreboard(2))
out = cap.get()
assert "+5 you escape" in out and "−5 you are reaped" in out
assert "██" in out # the pips themselves
def test_preview_bell_predicts_without_moving_the_table(content):
from scrypt.engine.cards import CardInstance, make_card
from scrypt.engine.combat import preview_bell
state = _combat(content) # still in DRAW; preview must work anyway
striker = make_card("striker", power=2, health=1)
state.player_row[0] = CardInstance(spec=striker) # open lane -> face
foe = make_card("foe", power=3, health=9)
state.foe_row[1] = CardInstance(spec=foe) # your lane 1 is open -> face
snapshot = (state.scale, state.turn, len(state.events), state.phase)
events = preview_bell(state)
kinds = {e.kind for e in events}
assert "face_damage" in kinds
mine = next(e for e in events if e.kind == "face_damage" and e.data["player"])
assert mine.data["amount"] == 2 and mine.data["lane"] == 0
theirs = next(e for e in events if e.kind == "face_damage" and not e.data["player"])
assert theirs.data["amount"] == 3 and theirs.data["lane"] == 1
# The ghost did all the work; the real table never moved.
assert (state.scale, state.turn, len(state.events), state.phase) == snapshot
def test_preview_marks_digest_the_event_log(content):
from scrypt.engine.cards import CardInstance, make_card
from scrypt.engine.combat import preview_bell
from scrypt.ui.render import preview_marks
state = _combat(content)
state.player_row[0] = CardInstance(spec=make_card("hit", power=2, health=1))
state.foe_row[2] = CardInstance(spec=make_card("wall", power=1, health=1))
state.player_row[2] = CardInstance(spec=make_card("axe", power=4, health=2))
state.foe_queue[3] = CardInstance(spec=make_card("next", power=1, health=1))
adv, out, inc, verdict = preview_marks(preview_bell(state))
assert "▲2" in out[0].plain # open lane: face damage
assert "⚔4" in out[2].plain and "☠" in out[2].plain # kills the wall
assert 3 in adv # the queued card will drop in
assert verdict is None
def test_preview_warns_when_the_bell_would_end_you(content):
from scrypt.engine.cards import CardInstance, make_card
from scrypt.engine.combat import preview_bell
from scrypt.ui.render import preview_marks
state = _combat(content)
state.scale = -3
state.foe_row[0] = CardInstance(spec=make_card("reaper", power=2, health=9))
_, _, inc, verdict = preview_marks(preview_bell(state))
assert verdict == "player_loss"
assert "▼2" in inc[0].plain
def test_run_track_marks_where_you_stand(content):
from scrypt.engine.run import new_run
from scrypt.ui.render import run_track
run = new_run(
list(content.starter_decks["vanilla"]["cards"]),
["first_blood", "crossroads_audit", "crossroads_final"],
fork_ids=frozenset(content.forks),
)
track = run_track(run.nodes, run.position).plain
assert track.startswith("path ")
assert "[⚔]" in track # the first battle is bracketed
assert "⑂" in track and "$" in track and "†" in track and "+" in track
run.advance()
later = run_track(run.nodes, run.position).plain
assert "[$]" in later and "[⚔]" not in later
# --------------------------------------------------------------- puzzles
def _shell() -> Shell:
vfs = VFS()
fabricate_home(vfs, seed=11)
return Shell(vfs)
def test_log_trail_pays_when_the_spool_is_read():
sh = _shell()
puzzles = {p.id: p for p in plant_all(sh.vfs, seed=11)}
assert puzzles["log_trail"].poll(sh) is None
out = sh.run("grep CYCLES /var/log/warden.log").out
assert "/var/spool/cycles.dat" in out
sh.run("cat /var/spool/cycles.dat")
reward = puzzles["log_trail"].poll(sh)
assert reward is not None and reward.kind == "cycles" and reward.value == 8
def test_zombie_reap_rewards_the_unlinker():
sh = _shell()
puzzles = {p.id: p for p in plant_all(sh.vfs, seed=11)}
assert puzzles["zombie_reap"].poll(sh) is None
pidfile = sh.run("find /run -name *.pid").out.strip().splitlines()[0]
sh.run(f"rm {pidfile}")
reward = puzzles["zombie_reap"].poll(sh)
assert reward is not None and reward.kind == "card" and reward.value == "zombie-process"
|