"""The two-player carry script against its committed golden hash. `tests/goldens/carry_two_player.golden.json` pins the state hash of a fixed 12-tick script: one pass (a set-down picked up inside the hand-off window, twice — out and back) and one contested pick (the full tie broken by seat, the loser told `contested_lost`). The event sequence is asserted in code, so a golden drift is diagnosable in one read: if the events match but the hash does not, a constant or a subsystem order moved; if the events differ, a rule changed shape. The golden is committed, not computed: a mismatch fails, and this test prints the diff hint. Regeneration is deliberate — an intentional sim-contract change recorded in the phase task, never a side effect of a failing run (task 05's replay harness formalises the re-bless path and the input-script format this script foreshadows). """ from __future__ import annotations import json from pathlib import Path import pytest from undercooked.sim.grid import Grid from undercooked.sim.model import ( Facing, Item, ItemForm, ItemKind, KitchenState, Player, Station, StationKind, TileKind, Vec2, ) from undercooked.sim.step import STEP_DT, PlayerInput, step pytestmark = pytest.mark.unit GOLDEN_PATH: Path = ( Path(__file__).resolve().parent.parent / "goldens" / "carry_two_player.golden.json" ) WIDTH, HEIGHT = 13, 9 def build_state() -> KitchenState: """Tick 0: A at the prep holding a lettuce, B at its left diagonal, both facing it.""" grid = Grid(width=WIDTH, height=HEIGHT, tiles=(TileKind.FLOOR,) * (WIDTH * HEIGHT)) prep = Station(id="prep-9-4", kind=StationKind.PREP, x=9, y=4) a = Player( id="cook-a", pos=Vec2(9.5, 5.5), facing=Facing.UP, carrying=Item(kind=ItemKind.LETTUCE, form=ItemForm.RAW), seat=0, ) b = Player(id="cook-b", pos=Vec2(8.5, 5.5), facing=Facing.UP, seat=1) return KitchenState( level_id="03_replay_carry", seed=99, grid=grid, stations=(prep,), players=(a, b), target_score=40, time_left=90.0, ) def frame(player_id: str, seq: int, action: bool, tick: int) -> PlayerInput: return PlayerInput(player_id=player_id, seq=seq, action=action, received_at_tick=tick) # The 12-tick script: (tick, one or two frames). The pass is t0 -> t2 (a sets down, # b picks: HANDED_OFF), the contest is t6 (both press the prep's item: a, seat 0, # wins; b is told contested_lost), and the return pass is t8 -> t10. SCRIPT: tuple[tuple[int, tuple[PlayerInput, ...]], ...] = ( (0, (frame("cook-a", 0, True, 0),)), (1, (frame("cook-a", 1, False, 1),)), (2, (frame("cook-b", 2, True, 2),)), (3, (frame("cook-b", 3, False, 3),)), (4, (frame("cook-b", 4, True, 4),)), (5, (frame("cook-b", 5, False, 5),)), (6, (frame("cook-a", 6, True, 6), frame("cook-b", 6, True, 6))), (7, (frame("cook-a", 7, False, 7),)), (8, (frame("cook-a", 8, True, 8),)), (9, (frame("cook-a", 9, False, 9),)), (10, (frame("cook-b", 10, True, 10),)), (11, (frame("cook-b", 11, False, 11),)), ) # The event the script must produce, tick by tick (kinds and the load-bearing payload). EXPECTED_EVENTS: tuple[tuple[str, dict[str, str]], ...] = ( ("dropped", {"player_id": "cook-a", "station": "prep-9-4"}), ("handed_off", {"from": "cook-a", "to": "cook-b"}), ("dropped", {"player_id": "cook-b", "station": "prep-9-4"}), ("handed_off", {"from": "cook-b", "to": "cook-a"}), ("denied", {"player_id": "cook-b", "reason": "contested_lost"}), ("dropped", {"player_id": "cook-a", "station": "prep-9-4"}), ("handed_off", {"from": "cook-a", "to": "cook-b"}), ) def run_script() -> tuple[KitchenState, tuple[tuple[str, dict[str, object]], ...]]: """Run the 12-tick script; return the final state and the events, keyed by kind.""" state = build_state() events: list[tuple[str, dict[str, object]]] = [] for _tick, frames in SCRIPT: state, produced = step(state, STEP_DT, frames) events.extend((event.kind, dict(event.payload)) for event in produced) return state, tuple(events) def test_the_script_produces_the_expected_event_sequence() -> None: _state, events = run_script() assert len(events) == len(EXPECTED_EVENTS) for (got_kind, got_payload), (want_kind, want_payload) in zip( events, EXPECTED_EVENTS, strict=True ): assert got_kind == want_kind for key, value in want_payload.items(): assert got_payload.get(key) == value, (got_kind, got_payload) def test_the_script_reaches_the_committed_golden_hash() -> None: golden = json.loads(GOLDEN_PATH.read_text(encoding="utf-8")) state, _events = run_script() assert state.tick == golden["tick"] if state.state_hash() != golden["hash"]: # The diff hint the phase promises: name both hashes so the commit shows the drift. print( f"golden mismatch for {GOLDEN_PATH.name}:\n" f" expected {golden['hash']}\n" f" actual {state.state_hash()}\n" "Regenerate only on a deliberate sim-contract change recorded in the phase " "task (docs/SIMULATION.md, 'Goldens'); never to silence a failure." ) assert state.state_hash() == golden["hash"] def test_the_golden_file_is_committed_and_well_formed() -> None: golden = json.loads(GOLDEN_PATH.read_text(encoding="utf-8")) assert golden["name"] == "carry_two_player" assert golden["tick"] == 12 assert len(golden["hash"]) == 64 # a sha256 hex digest, nothing else def test_rerunning_the_script_is_bit_identical() -> None: """No hidden global state: the same script twice in one process hashes the same.""" first, first_events = run_script() second, second_events = run_script() assert first.state_hash() == second.state_hash() assert first_events == second_events