File size: 2,796 Bytes
4be6a52 | 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 | import copy
import json
import pytest
from stackcraft.engine import legal_actions, new_game, step
from stackcraft.replay import make_replay, replay_states
def episode(seed: int = 123) -> tuple[list[str], list]:
states = [new_game(seed)]
actions = []
while not states[-1].terminal:
action = legal_actions(states[-1])[0].id
actions.append(action)
states.append(step(states[-1], action).state)
return actions, states
def test_json_roundtrip_recreates_every_state_and_terminal_outcome() -> None:
actions, states = episode()
artifact = make_replay(123, actions)
assert replay_states(json.loads(json.dumps(artifact))) == states
assert artifact["final"]["terminal"] is True
assert artifact["final"]["pieces"] == len(actions)
actions.clear()
assert artifact["actions"]
def test_empty_and_partial_games_are_valid_replays() -> None:
assert replay_states(make_replay(1, [])) == [new_game(1)]
actions, states = episode()
assert replay_states(make_replay(123, actions[:3])) == states[:4]
@pytest.mark.parametrize(
"key,value",
[
("schema_version", 2),
("schema_version", True),
("rules_version", "unknown"),
("seed", True),
("seed", "123"),
("actions", "r0x0"),
("actions", [4]),
("actions", ["invalid"]),
("final", {}),
("final", None),
],
)
def test_malformed_or_tampered_metadata_is_rejected(key: str, value: object) -> None:
artifact = make_replay(123, [])
artifact[key] = value
with pytest.raises(ValueError):
replay_states(artifact)
@pytest.mark.parametrize("field", ["score", "lines", "pieces", "terminal"])
def test_tampered_final_outcomes_are_rejected(field: str) -> None:
actions, _ = episode()
artifact = make_replay(123, actions)
artifact["final"][field] = not artifact["final"][field] if field == "terminal" else 999
with pytest.raises(ValueError, match="summary"):
replay_states(artifact)
def test_boolean_cannot_impersonate_numeric_summary() -> None:
artifact = make_replay(123, [])
artifact["final"]["score"] = False
with pytest.raises(ValueError, match="summary"):
replay_states(artifact)
def test_actions_after_game_over_are_rejected() -> None:
actions, _ = episode()
artifact = make_replay(123, actions)
artifact["actions"].append("r0x0")
with pytest.raises(ValueError, match="invalid replay move"):
replay_states(artifact)
def test_summary_optional_and_validation_does_not_mutate_artifact() -> None:
actions, states = episode()
artifact = make_replay(123, actions)
del artifact["final"]
before = copy.deepcopy(artifact)
assert replay_states(artifact) == states
assert artifact == before
|