"""`sim/orders.py` fuzz: twenty randomized order specs (fixed seed) each run for 300 simulated seconds, re-checking the rail's invariants on every tick — no negative times, no duplicate ids, no unbounded order count, no exceptions. The specs come from the sim's own splitmix64 (a fixed seed makes the fuzz a property of the script, not of the machine): pool size 1-4 over four recipe ids, a concurrency cap 1-4, a spawn interval with `min <= max` and a span up to 300 ticks, a deadline of 10-1000 ticks, a penalty 0-30. Every spec is validated by construction, so an exception from the stage is a stage bug, not a content bug. """ from __future__ import annotations from dataclasses import replace import pytest from undercooked.sim.grid import Grid from undercooked.sim.model import KitchenState, TileKind from undercooked.sim.orders import FIRST_ORDER_DELAY_TICKS, step_orders from undercooked.sim.recipe import RecipeBook from undercooked.sim.rng import Rng from undercooked.sim.step import STEP_DT pytestmark = pytest.mark.unit #: The fixed seed the spec generator draws from: the fuzz is a property of #: the script, not of the machine. FUZZ_SEED = 0xF00D_2042 #: Twenty level-worths of specs, each run this many substeps (300 s at 20 Hz). SPECS = 20 TICKS = 300 * 20 #: The recipe ids a spec may put in its pool; the book carries all of them. RECIPE_IDS: tuple[str, ...] = ("fz_a", "fz_b", "fz_c", "fz_d") def book() -> RecipeBook: return RecipeBook.model_validate( { "recipes": { recipe_id: { "id": recipe_id, "name": recipe_id.title(), "base_score": 10 * (index + 1), "ingredients": [{"kind": "tomato", "form": "raw"}], "steps": [{"kind": "serve", "station": "serve"}], } for index, recipe_id in enumerate(RECIPE_IDS) } } ) def make_spec(rng: Rng) -> dict[str, int | tuple[str, ...]]: """One randomized `OrdersSpec` worth of scalars, valid by construction.""" pool = tuple(rng.choice(RECIPE_IDS) for _ in range(rng.range_int(1, 4))) concurrent = rng.range_int(1, 4) spawn_min = rng.range_int(5, 200) spawn_max = spawn_min + rng.range_int(0, 300) deadline = rng.range_int(10, 1000) penalty = rng.range_int(0, 30) return { "pool": pool, "concurrent": concurrent, "spawn_min": spawn_min, "spawn_max": spawn_max, "deadline": deadline, "penalty": penalty, } def make_state(spec: dict[str, int | tuple[str, ...]]) -> KitchenState: return KitchenState( level_id="04_orders_fuzz", seed=FUZZ_SEED, grid=Grid(width=4, height=4, tiles=(TileKind.FLOOR,) * 16), orders=(), order_pool=tuple(spec["pool"]), # type: ignore[arg-type] order_concurrent=spec["concurrent"], # type: ignore[arg-type] order_spawn_min=spec["spawn_min"], # type: ignore[arg-type] order_spawn_max=spec["spawn_max"], # type: ignore[arg-type] order_deadline=spec["deadline"], # type: ignore[arg-type] order_penalty=spec["penalty"], # type: ignore[arg-type] next_order_at=FIRST_ORDER_DELAY_TICKS, next_order_id=1, rng_state=FUZZ_SEED, time_left=float(TICKS) / 20.0, ) def check_invariants(state: KitchenState, spec: dict[str, int | tuple[str, ...]]) -> None: """The rail's invariants, one tick at a time.""" assert len(state.orders) <= spec["concurrent"], "unbounded order count" # type: ignore[arg-type] seen_ids: set[int] = set() for order in state.orders: assert order.time_left >= 0.0, "a negative clock" assert order.id not in seen_ids, "duplicate order id" seen_ids.add(order.id) assert state.counters.get("missed", 0) >= 0 assert state.next_order_id >= 1 assert state.next_order_at >= 0 def test_twenty_randomized_specs_hold_the_invariants_for_300_seconds_each() -> None: value_book = book() rng = Rng(FUZZ_SEED) total_spawns = 0 for _ in range(SPECS): spec = make_spec(rng) state = make_state(spec) first_spawn_tick: int | None = None for _ in range(TICKS): due_tick = state.tick # the pre-increment tick this substep runs on before_id = state.next_order_id state, _events = step_orders(state, STEP_DT, value_book) state = replace(state, tick=state.tick + 1) check_invariants(state, spec) if state.next_order_id != before_id: total_spawns += 1 if first_spawn_tick is None: first_spawn_tick = due_tick # the spec ran to the end; a non-empty pool spawns from the delay # (every spec has a pool of 1-4), and the ids stayed a contiguous run. if state.next_order_id > 1: assert first_spawn_tick == FIRST_ORDER_DELAY_TICKS, spec assert state.tick == TICKS assert total_spawns >= SPECS, "300 s per spec must spawn at least one order each" def test_the_fuzz_is_reproducible_from_its_seed() -> None: """Two runs of the whole fuzz from `FUZZ_SEED` see the same specs and the same final `rng_state` per spec: the fuzz is a property of the script.""" def run() -> list[int]: rng = Rng(FUZZ_SEED) finals: list[int] = [] value_book = book() for _ in range(SPECS): spec = make_spec(rng) state = make_state(spec) for _ in range(TICKS): state, _events = step_orders(state, STEP_DT, value_book) state = replace(state, tick=state.tick + 1) finals.append(state.rng_state) return finals assert run() == run()