"""Regression tests for the shipped campaign content (``levels/``, task 04). These pin what campaign 02 ships, exactly as the phase task specifies: the recipe book (ingredients + four named dishes with PLAN §5 step pipelines and fixed base scores), the first two levels' tunables (pool, concurrency, spawn interval, deadline, time limit, target), the blurb floor, campaign ordering, and the level-1 → level-2 escalation contract (≥2 axes up). They read the real files from ``levels/`` — not a copy — so editing shipped content that a test pins fails here, in review, instead of at a player's startup. Startup behaviour and the HTTP surface live in ``test_content_startup.py`` and ``tests/integration/test_content_api.py``; the generic loader and validator have their own unit suites. """ from __future__ import annotations from pathlib import Path import pytest from undercooked.sim.content import ContentBundle, load_content from undercooked.sim.level import Level from undercooked.sim.recipe import RecipeBook, load_recipe_book from undercooked.sim.validation import escalation_axes, validate_bundle REPO_ROOT = Path(__file__).resolve().parents[2] LEVELS_DIR = REPO_ROOT / "levels" RECIPES_PATH = LEVELS_DIR / "recipes.yaml" LEVEL_FILES = sorted( path for path in LEVELS_DIR.glob("*.yaml") if path.name != "recipes.yaml" ) #: The campaign order this task ships (PLAN §8: soup tutorial, then salad). SHIPPED_IDS = ["01_first_kitchen", "02_double_shift"] #: PLAN §5 station pipelines as (interaction, station) pairs: #: pick at the shelf -> chop at the board -> cook on the stove -> plate at #: prep -> serve at the window; a dish skips the stages its recipe lacks. EXPECTED_STEPS = { "soup": [ ("pick", "spawn_shelf"), ("chop", "chop_board"), ("cook", "stove"), ("plate", "prep"), ("serve", "serve"), ], "salad": [ ("pick", "spawn_shelf"), ("chop", "chop_board"), ("plate", "prep"), ("serve", "serve"), ], "fried_rice": [ ("pick", "spawn_shelf"), ("cook", "stove"), ("plate", "prep"), ("serve", "serve"), ], "burger": [ ("pick", "spawn_shelf"), ("chop", "chop_board"), ("cook", "stove"), ("plate", "prep"), ("serve", "serve"), ], } @pytest.fixture(scope="module") def book() -> RecipeBook: return load_recipe_book(RECIPES_PATH) @pytest.fixture(scope="module") def bundle() -> ContentBundle: return load_content(LEVELS_DIR, RECIPES_PATH) def level_of(bundle: ContentBundle, level_id: str) -> Level: return bundle.by_id[level_id] # --- the recipe book --------------------------------------------------------- def test_recipe_book_ships_the_named_ingredient_catalog(book: RecipeBook) -> None: assert set(book.ingredients) == { "tomato", "onion", "lettuce", "dough", "patty", "rice", "fish", "plate", } def test_recipe_book_ships_exactly_the_four_dishes(book: RecipeBook) -> None: assert set(book.recipes) == {"soup", "salad", "fried_rice", "burger"} @pytest.mark.parametrize( ("recipe_id", "base_score"), [("soup", 40), ("salad", 30), ("fried_rice", 50), ("burger", 70)], ) def test_dishes_carry_their_pinned_base_scores( book: RecipeBook, recipe_id: str, base_score: int ) -> None: assert book.get(recipe_id).base_score == base_score @pytest.mark.parametrize("recipe_id", sorted(EXPECTED_STEPS)) def test_dishes_follow_the_plan_station_pipelines( book: RecipeBook, recipe_id: str ) -> None: recipe = book.get(recipe_id) assert [(step.kind, step.station.value) for step in recipe.steps] == ( EXPECTED_STEPS[recipe_id] ) @pytest.mark.parametrize("recipe_id", sorted(EXPECTED_STEPS)) def test_every_dish_consumes_catalogued_ingredients( book: RecipeBook, recipe_id: str ) -> None: recipe = book.get(recipe_id) assert recipe.ingredients, recipe_id assert {item.kind for item in recipe.ingredients} <= set(book.ingredients) # --- the level files ---------------------------------------------------------- def test_every_yaml_file_in_levels_is_a_shipped_level(bundle: ContentBundle) -> None: stems = {path.stem for path in LEVEL_FILES} assert stems, "levels/ must ship at least one level" assert stems == set(bundle.by_id) def test_shipped_campaign_validates_without_warnings(bundle: ContentBundle) -> None: report = validate_bundle(bundle) assert report.ok, report.render() assert not report.warnings, report.render() def test_campaign_order_is_unique_and_starts_at_one(bundle: ContentBundle) -> None: orders = [level.order for level in bundle.levels] assert orders == list(range(1, len(bundle.levels) + 1)) def test_campaign_ships_the_first_two_levels(bundle: ContentBundle) -> None: assert [level.id for level in bundle.levels] == SHIPPED_IDS @pytest.mark.parametrize("level_id", SHIPPED_IDS) def test_each_level_has_a_readable_premise(bundle: ContentBundle, level_id: str) -> None: blurb = level_of(bundle, level_id).blurb assert len(blurb) >= 20 @pytest.mark.parametrize("level_id", SHIPPED_IDS) def test_difficulty_is_within_the_declared_range( bundle: ContentBundle, level_id: str ) -> None: assert 1 <= level_of(bundle, level_id).difficulty <= 10 @pytest.mark.parametrize("level_id", SHIPPED_IDS) def test_pool_recipes_exist_in_the_book( bundle: ContentBundle, book: RecipeBook, level_id: str ) -> None: recipe_ids = level_of(bundle, level_id).orders.pool assert recipe_ids for recipe_id in recipe_ids: assert recipe_id in book.recipes # --- the two levels' pinned tunables (task 04) -------------------------------- def test_level_one_is_the_soup_tutorial(bundle: ContentBundle) -> None: level = level_of(bundle, "01_first_kitchen") assert level.order == 1 assert level.difficulty == 1 assert level.time_limit == 150 assert level.target_score == 45 assert level.orders.pool == ["soup"] assert level.orders.concurrent == 1 assert tuple(level.orders.spawn_interval) == (16, 20) assert level.orders.deadline == 60 assert level.hazards == [] def test_level_two_adds_salad_and_pressure(bundle: ContentBundle) -> None: level = level_of(bundle, "02_double_shift") assert level.order == 2 assert level.difficulty == 2 assert level.time_limit == 130 assert level.target_score == 80 assert level.orders.pool == ["soup", "salad"] assert level.orders.concurrent == 2 assert tuple(level.orders.spawn_interval) == (12, 16) assert level.orders.deadline == 52 assert level.hazards == [] def test_level_two_escalates_at_least_two_axes(bundle: ContentBundle) -> None: before = escalation_axes(level_of(bundle, "01_first_kitchen")) after = escalation_axes(level_of(bundle, "02_double_shift")) moved_up = sorted( name for name, value in after.items() if value > before[name] + 1e-9 ) assert len(moved_up) >= 2, f"only these axes moved: {moved_up}"