# Sample Game — Pinefield Valley A complete, small, playable farm game built **entirely from engine content** (YAML) plus **one mod**. It exists to prove the nEngine's data-driven contract: no engine code was touched to make this game; every capability is exercised through content and the MCP surface. ## What the game is Spring Day 1. You are a new farmer. The game's arc: 1. **Start** — `Spring Day 1`, 0 gold, empty inventory. 2. **Starter gift** — on the first `day-start`, the `starter-gift` trigger grants 3 parsnip seeds + 2 carrot seeds (single-shot via a flag). 3. **Quest** — the `starter-quest` state machine activates on Day 2: *harvest 3 parsnips*. 4. **Grow** — till, sow, and water plots; `time.tick` drives growth day by day (parsnip = spring crop, 2/2/1 days; carrot = summer crop, 2/3/1 days). 5. **Harvest & sell** — harvest mature crops, sell them at the general store. 6. **Buy** — spend gold on tools/seeds. 7. **Build** — place a 3×3 coop (and fences) on the grass. 8. **Chicken** — acquire a chicken, force it to laying, feed it daily; the `chicken-egg` state machine turns `animal-produce` events into eggs. 9. **Talk to the Mayor** — a dialogue with choices, a condition-gated branch (only after the starter quest completes), and give-item actions on choices and nodes. 10. **Quest chain (D29)** — once the starter quest completes, the `carrot-quest` machine activates on the next day-start (*harvest a carrot*); completing it activates `mayor-talk-quest` (*finish a mayor dialogue*), which pays an egg + gold and unlocks a new "fishing tip" branch in the mayor dialogue. 11. **Fishing (D29)** — stamina/skills are plain player fields; the fishing **Mod** hooks the player's intent: interact with a pond `water` tile to cast (costs 5 stamina, `skills.fishing` +1, grants a fish), and every third catch pays a free egg via the `fishing-treat` machine. Exhaust your stamina below 5 and the Mod tells you you're too tired. Along the way, weather rolls every day-start, and the **mod** spawns the Mayor NPC (NPCs have no content kind — they are runtime entities), walks him according to the `mayor-routine` state machine, reports the weather, and emits `mod:sample-day-start`. ## Content inventory | File | Demonstrates | |---|---| | `content/maps/farm.yaml` | 16×16 map, variety (pond `water`, pre-tilled bed, open `grass`), spawn point | | `content/time.yaml` | 4 seasons × 28 days, 480 ticks/day, Spring Day 1 start | | `content/weather.yaml` | 4-season weighted weather table (sunny/rain/wind/storm) | | `content/items/hoe.yaml` | tool, non-stackable | | `content/items/watering-can.yaml` | tool (decorative use in this slice) | | `content/items/parsnip-seed.yaml` / `carrot-seed.yaml` | seeds | | `content/items/parsnip.yaml` / `carrot.yaml` / `egg.yaml` / `fish.yaml` | harvest materials + produce + the fishing catch (granted by the Mod, D29) | | `content/crops/parsnip.yaml` | spring crop, 3 stages, water-gated, drops parsnip | | `content/crops/carrot.yaml` | summer crop, different days (2/3/1) + same water gating | | `content/animals/chicken.yaml` | chick→grown→laying, `feedRequired` from grown, produces egg | | `content/sprites/*.yaml` | procedural sprite draw instructions (D26) **+ image layers (D31)**: player + chicken stages stay programmatic; parsnip/carrot stages, the coop, and the fence draw Kenney assets from `content/assets/` (`image` layers with `src`), some with programmatic root/leaf accents layered on top | | `content/sounds/*.yaml` | sound specs (D26): till/water/harvest/coin now play **audio assets** (`src: drop-1`/`pluck-1`/`confirm-1`/`click-1`, D31) | | `content/assets/` | **binary asset table (D31)**: 13 Kenney Tiny Farm tiles (`.png`) + 8 Kenney interface sounds (`.ogg`), CC0 — self-descriptive ids (`grass-1`, `water-1`, `tilled-1`, `plant-sprout`, `plant-grown`, `plant-mature`, `carrot-sprout`, `carrot-grown`, `carrot-mature`, `coop-1`, `coop-2`, `fence-1`, `grass-2`; `click-1`, `confirm-1`, `error-1`, `back-1`, `drop-1`, `pluck-1`, `switch-1`, `bong-1`). NOT a content kind — the loader builds the asset table (`engine.assets`); YAML references assets by id. Keep assets small: the table is base64-inlined in `bundle.json` and every WS snapshot frame | | `content/buildings/coop.yaml` | 3×3 footprint, grass-only, blocks movement | | `content/buildings/fence.yaml` | 1×1 footprint, grass-only, blocks movement | | `content/shops/general-store.yaml` | buys parsnip/carrot/egg; sells tools + seeds | | `content/behaviors/starter-gift.yaml` | single-shot trigger (flag re-arm pattern) with give-item + set-state | | `content/statemachines/starter-quest.yaml` | quest state machine (day-start activate, crop-harvest complete, reward) | | `content/statemachines/carrot-quest.yaml` | quest chain 2 (D29): activates when the starter quest completes, completes on a carrot harvest | | `content/statemachines/mayor-talk-quest.yaml` | quest chain 3 (D29): activates when the carrot quest completes, completes on a mayor `dialogue-end`, pays egg + gold | | `content/statemachines/chicken-egg.yaml` | production rule: animal-produce event → egg grant | | `content/statemachines/mayor-routine.yaml` | NPC daily routine via `onSchedule` day-start entries | | `content/statemachines/daily-stamina.yaml` | stamina rules (D29): a condition on `player.stamina` + a day-start restore | | `content/statemachines/fishing-treat.yaml` | Mod→content bridge (D29): matches the Mod's `mod:fish-caught` event, every third catch pays an egg | | `content/dialogues/mayor.yaml` | choices, condition-gated branches (incl. the D29 quest-gated fishing tip), give-item actions on choice + node | | `mods/sample-game-mod/index.mjs` | the Mod escape hatch (D6/D9/D29): spawns + walks the Mayor NPC, reports weather, emits an event, and settles the fishing minigame on `onPlayerIntent` (fish grant, stamina drain, `skills.fishing`, `mod:fish-caught`/`mod:fish-too-tired`) | ## Run the MCP server against this game Build first (the entry ships from `dist`): ```sh pnpm -r build ``` Then boot the stdio server pointing at this game's content/mods: ```sh node apps/mcp-server/dist/index.js \ --content-dir examples/sample-game/content \ --mods-dir examples/sample-game/mods ``` Equivalent via environment variables (CLI still wins if both are set): ```sh NENGINE_CONTENT_DIR=examples/sample-game/content \ NENGINE_MODS_DIR=examples/sample-game/mods \ node apps/mcp-server/dist/index.js ``` Zero-arg invocation keeps serving the monorepo's own `content/`/`mods/`. ## Drive the game with an MCP client Connect any MCP client over stdio. Key tool calls for the full loop: 1. `server.info` — confirm the loaded game content (map `farm`, crops `carrot`+`parsnip`, animals `chicken`, buildings `coop`+`fence`, dialogue `mayor`, behaviors `starter-gift`, statemachines `carrot-quest`/`chicken-egg`/`daily-stamina`/`fishing-treat`/`mayor-routine`/`mayor-talk-quest`/`starter-quest`, shop `general-store`). 2. `time.advance {ticks: 480}` — first day-start: starter gift lands (3 parsnip-seed + 2 carrot-seed), quest activates, weather rolls, mod spawns `npc:mayor`. 3. `item.query_inventory` — see the gift. 4. `npc.list` — see `npc:mayor` (spawned by the mod, not by the engine). 5. `weather.get` — see the rolled weather. 6. Till/sow/water plots — `crop.till {x, y}` then `crop.sow {x, y, seedItemId: parsnip-seed}` (consumes 1 seed from the player inventory), then `crop.water {x, y}` (sets the plot watered; mirrors `PlantingSystem.water`, safe to call repeatedly — watering an already-watered plot keeps it watered, so a `waterRequired` stage never flips back mid-loop). 7. `time.tick {ticks: 480}` — grow the crop one day (repeat + re-water). 8. `crop.harvest {x, y}` once mature (`crop.stage {x, y, stageIndex: 2}` or `time.tick` growth) → quest completes (`behavior.statemachine.status {machineId: starter-quest}`). 9. `shop.sell {shopId: general-store, itemId: parsnip, qty: 3}` → gold. 10. `shop.buy {shopId: general-store, itemId: carrot-seed, qty: 1}`. 11. `placement.place {buildingId: coop, x: 10, y: 4}` (+ `fence`). 12. `animal.spawn {animalId: chicken, x: 9, y: 3}` — chickens are runtime entities; the shop sells items only. 13. `animal.stage {id: animal:chicken:1, stageIndex: 2}` + `animal.feed` (feed each day). 14. `time.tick {ticks: 480}` → `animal-produce` fires → the `chicken-egg` machine grants an egg; see it in `item.query_inventory`. 15. `dialogue.start {dialogueId: mayor}` → `dialogue.choose {index: 2}` ("Got anything for me?", only present after the quest) → +2 eggs; then `dialogue.choose {index: 0}` to leave. 16. **Quest chain (D29)** — after the starter quest completes, advance a day (`time.tick {ticks: 480}`) to activate `carrot-quest`; harvest a carrot (grow `carrot-seed` — 2/3/1 days — then `crop.harvest`) → +2 carrot seeds. Advance a day again → `mayor-talk-quest` activates; start + finish the mayor dialogue (`dialogue.choose` to the terminal node) → +1 egg + gold, and a new "Tell me about fishing." branch appears. 17. **Fishing (D29)** — the pond is the top-left 4×3 `water` block. Use `player.intent {action: interact, tile: {x: 3, y: 2}}` (or the dev app's canvas) to cast: +1 fish (sellable at the general store), −5 stamina, `player.skills.fishing` +1. Every third catch pays a free egg (`fishing-treat`). Fish with `player.stamina < 5` and the Mod emits `mod:fish-too-tired` instead of a catch. ## Run the dev render app against this game The dev render app (`apps/dev`) is parameterized via URL query — it boots an engine in the browser for its own content by default, or renders a headless core's world over WebSocket. To view THIS game's world, start the headless server with a WS channel and open the app in ws mode: ```sh # Terminal 1 — headless core + MCP + WS broadcast on 8787 pnpm -r build node apps/mcp-server/dist/index.js \ --content-dir examples/sample-game/content \ --mods-dir examples/sample-game/mods \ --ws-port 8787 # Terminal 2 — the dev app pnpm dev ``` Then open `http://localhost:5173/apps/dev/?sync=ws&url=ws://localhost:8787`. The browser renders snapshots from the headless core; the HUD shows day/season, seed counts, and gold. ## Package this game (D30) The engine ships a packaging CLI (`apps/pack`, `nengine-pack`) that compiles a game's content (YAML) into a **self-contained static site**: `bundle.json` (the content **plus the asset table** — base64-inlined `content/assets/` binaries; the single source of truth for packaged output, no YAML files and no separate binary files ship) plus `index.html` + `assets/` (the built dev app in `?bundle=` mode). The packaged game runs entirely in the browser via `createEngine({bundle})` + LocalSync — no dev server, no headless core. ```sh # build first (the CLI ships from dist) pnpm -r build # package this game pnpm pack:example # equivalent: node apps/pack/dist/index.js \ --content examples/sample-game/content \ --out examples/sample-game/dist-pack ``` Then serve the output folder statically and open it — **no URL params needed** (the built index.html defaults to the bundle): ```sh npx serve examples/sample-game/dist-pack # or: python3 -m http.server 8000 --directory examples/sample-game/dist-pack # open http://localhost:3000 (or :8000) ``` The packaged game keeps the full dev-app playability (Interact/Move toolbar, arrow keys, HUD with day/season + inventory + gold, sprite image layers + map tile textures + `src` audio sounds). Since **D36** you can package the Mod too with `--mods` — the Mayor NPC and the fishing minigame then run in the packaged game exactly as in dev (the engine executes the bundled Mod source via a data-URL import, browser + desktop): ```sh pnpm pack:example:full # equivalent: node apps/pack/dist/index.js \ --content examples/sample-game/content \ --mods examples/sample-game/mods \ --out examples/sample-game/dist-pack ``` `pack:example` (no `--mods`) still packages the content-only game, unchanged. Desktop (D32) + installers (D36): see below. **Split-assets packaging (D37)**: by default the 21 binary assets are base64-inlined in `bundle.json` (self-contained single file). Pass `--split-assets` to write them as separate files instead — the packaged site then serves `out/nengine-assets/.png`/`.ogg` (one file per asset) with a metadata-only `bundle.json` (the renderer fetches the bytes on demand): ```sh pnpm pack:example:split # equivalent: node apps/pack/dist/index.js \ --content examples/sample-game/content \ --out examples/sample-game/dist-pack \ --split-assets ``` The game renders identically in both modes (the renderer fetches from the sibling `nengine-assets/` dir when entries carry `file` instead of inline base64); inline stays the default (zero-regression). The dev-app `?bundle=` mode derives the fetch base from the bundle's directory, so pointing it at a split `dist-pack/bundle.json` also works. To try the same bundle in the dev app (over the vite dev server), pass `?bundle=` with a repo-relative path: ``` http://localhost:5173/apps/dev/?bundle=examples/sample-game/dist-pack/bundle.json ``` ## Run this game as a desktop app (D32) `nengine-pack --desktop` writes a **runnable Electron app** into `out/desktop/` next to the static site. It serves the folder over a local loopback HTTP server and opens the same bundle-mode page in a 960×640 window. ```sh # package with the desktop shell (window title "Pinefield Valley") pnpm pack:example:desktop # equivalent: node apps/pack/dist/index.js \ --content examples/sample-game/content \ --out examples/sample-game/dist-pack \ --desktop --name "Pinefield Valley" # run it (installs Electron once, ~100 MB) cd examples/sample-game/dist-pack/desktop npm install npm start ``` The game window is fully playable exactly like the browser package: Interact/Move toolbar, arrow-key movement, HUD, textures and sounds. Electron was chosen over Tauri because it needs no system packages (npm-only, bundles Chromium); Tauri requires `webkit2gtk-4.1` system deps. v1 ships no installer (`electron-builder` is not a dependency), uses the default Electron icon, and does not bundle a sidecar headless core (server-side Mods + multiplayer stay a documented future path) — see `docs/decisions.md` D32. **Desktop + Mods + installers (D36)**: pass `--mods` and `--installer` to package the Mod into the game and generate installer config: ## Build installers (D36) `nengine-pack --installer` (implies `--desktop`) writes an `electron-builder.yml` into `out/desktop/` (and copies the site files in so the builder can package them), plus `dist`/`dist:win` scripts: ```sh pnpm pack:example:full # equivalent: node apps/pack/dist/index.js \ --content examples/sample-game/content \ --mods examples/sample-game/mods \ --out examples/sample-game/dist-pack \ --desktop --installer --name "Pinefield Valley" # build the installer (installs electron-builder once) cd examples/sample-game/dist-pack/desktop npm install npm run dist # builds dist/*.AppImage (Linux) npm run dist:win # builds a Windows NSIS installer (run on Windows) ``` The Linux AppImage build is verified end-to-end in this repo (`dist/Pinefield Valley-0.1.0.AppImage`); the Windows NSIS target is documented config (needs a Windows host). The packaged game includes the Mod (`--mods`), so the Mayor and the fishing minigame work in the installed app too. ## How to play The dev app is playable in **both** sync modes (`?sync=ws&url=ws://...` connects to a headless core; the default local mode boots the engine in the browser — use `?contentDir=examples/sample-game/content&modsDir=examples/sample-game/mods` to boot THIS game's content locally). 1. **Choose a tool** from the HUD toolbar: **Interact** (default) or **Move**. 2. **Use the arrow keys** (↑ ↓ ← →) to walk the player one tile in that direction — works in both sync modes: ws mode sends the raw `{"type":"input","input":"key:Arrow<…>"}` message (the headless core maps it to a player move), local mode maps the same arrow client-side. Holding a key repeats. 3. **Click a tile on the map** — the canvas click becomes an intent: - **Move** — walk the player to the clicked tile (grass only; water and buildings block). - **Interact** — a smart context action on the clicked plot tile, in priority order: - a **water** tile is **fished** by the sample-game Mod (`onPlayerIntent`): +1 fish, −5 stamina, `skills.fishing` +1 (the engine's default till-on-interact is overridden on the pond; with `stamina < 5` the Mod reports "too tired" instead), - a mature crop is **harvested** (the HUD feedback shows `harvested `), - a planted, un-watered plot is **watered**, - a tilled, empty plot is **sown** with the **selected seed** — the HUD seed picker (D35) sets it via the `select-seed` intent (`sowed `; falls back to the first seed in inventory; `no seed in inventory` otherwise), - an un-tilled tile is **tilled** (creates the plot), - anything else reports an error message (e.g. `crop parsnip is not ready to harvest`). 4. **Read the HUD** — day/season, gold, stamina, every inventory item, the selected seed, and the last intent's feedback message (auto-clears after a few seconds). The **inventory bar** (bottom) lists items with counts. 5. **Advance time** — in local mode the HUD **Advance day** button drives a full day (480 ticks); in ws mode time is driven by the headless core (via MCP `time.advance`/`time.tick` or auto-tick), so the button is disabled. ### In-game UI panels (D35) The dev app adds four DOM panels driven through the same intent mechanism: - **Seed picker** (HUD): one button per owned seed (local mode filters to `seed`-kind items via the registry; ws mode offers every owned item — the snapshot carries no item defs — and the engine validates). Clicking sends `player.intent select-seed {seedItemId}`; the selected button is highlighted and `interact`'s sow prefers that seed while it is owned. - **Shop panel**: clicking a building that carries a `shop` field (the sample game's `shop` building def → `shop: general-store`; place it with `placement.place {buildingId:"shop", x, y}`) opens a panel with the shop's buy rows (Shop sells — click **Buy**) and sell rows (Shop buys — click **Sell**, priced from your owned stock). Rows come from the shop def (registry in local mode, the WS snapshot's `shops` catalog in ws mode) and owned counts refresh on each snapshot. - **Dialogue panel**: talking to an NPC (the mayor Mod spawn carries `dialogue: "mayor"`) — via `player.intent talk {npcId:"npc:mayor"}` or by clicking the mayor's tile — opens a panel with the current node text and choice buttons. Choosing sends `player.intent choose {index}`; the panel hides when the dialogue ends (the `dialogue:active` entity leaves the world). - **Inventory bar** (bottom): all inventory items with counts, gold, stamina, and the current seed selection — updated from every snapshot. ### Presentation (D26/D27/D31) The world is drawn from the game's **sprite content** (`content/sprites/`): the player, crop stages (parsnip/carrot × sprout/grown/mature), chicken stages, and the coop/fence buildings each render as their sprite def (buildings scaled to their footprint); entities without a sprite ref fall back to the engine's shape drawing. Since **D31**, crop-stage/coop/fence sprites use **image layers** that draw Kenney assets from `content/assets/` as PixiJS textures (the map's `tiles` mapping draws grass/water/tilled from the same assets), while the player and chickens stay programmatic — both paths coexist. Game events play **sound** (`content/sounds/`): the four defs now decode **audio assets** (`src`), e.g. the starter-gift day-start plays the `coin` ping from `click-1.ogg`; audio starts on the first keypress/click (browser autoplay policy). Game loop reminder: the `starter-gift` trigger fires on the first day-start (3 parsnip-seed + 2 carrot-seed). Till → sow → water the central bed, advance days to grow (parsnips 2/2/1 in spring), harvest, sell at the general store, and follow the quest. ### Agents can play too The same intent mechanism is exposed as MCP `player.intent` — an agent drives the identical `IntentSystem` a human drives from the canvas, with the same validation and events (D25): `move`/`interact` take `{action, tile}`; since **D35** `buy`/`sell` take `{action, shopId, itemId, qty?}`, `talk` takes `{action, npcId|tile}`, `choose` takes `{action, index}`, and `select-seed` takes `{action, seedItemId?}`. ## Test coverage `apps/mcp-server/test/sample-game.test.ts` boots this exact game through the parameterized entry (`resolveDirs` → `createEngine`), asserts every content kind loads, the mod spawned the Mayor, the starter gift fires on the first day-start, the full gameplay arc runs (till/sow/water/tick/harvest/ sell/buy/place/feed/egg + quest + dialogue), the **asset table loads (D31 — 21 assets: 13 tiles + 8 sounds, referenced by the map `tiles` mapping, sprite image layers, and sound `src` ids)** and — since D29 — the Tier 3 round: the chained quests (carrot-quest + mayor-talk-quest complete with rewards), the seeded stamina/skills fields, and the fishing minigame flow (water-tile interact → fish + stamina drain + `skills.fishing` + `mod:fish-caught` → fishing-treat egg on the third catch → too-tired gate).