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