# Engine / Game Boundary Where the engine ends and the game begins. Produced by a grilling session over the question: *"What should a game engine do, and what should be left to the game itself?"* > **Source of truth for WHY a feature is (or is not) engine-built.** Complements `decisions.md` (recorded as D19). Changes to this file must be reflected in `decisions.md` in the same change (D-op2). ## The Criterion A feature is built into the engine **iff** all three of these hold simultaneously: 1. **Dataization value** — the feature is meaningfully expressible as content data (YAML), so creators can author it without code. 2. **Mod hook demand** — gameplay-relevant subsystems need typed hook points Mods can register against. 3. **MCP manipulability** — agents/creators need tools to introspect or drive the feature. **Failure of any one → push the feature down** (to fields + behavior language, or to the render layer / Mods entirely). ## Ownership Model | Layer | Contents | Owner | |---|---|---| | **L0 Infrastructure** | Content pipeline, world/entity store, system host, event bus, Mod loader, MCP gateway, save serialization | Engine (built) | | **L1 Simulation mechanisms** | General mechanisms passable by the criterion — dialogue, weather, placement, movement primitives | Engine (judged case-by-case) | | **L2 Expression layer** | Behavior data language (conditions v1, state machines v2) — the escape hatch for most game logic | Engine (built) | | **L3 Game strategy** | What to build, when to save, what prices to charge, what quests exist, minigames | Creators (100%) | ## Feature Verdicts | Feature | Verdict | Where it lives | Rationale | |---|---|---|---| | NPC | Movement primitive **in engine**; schedules → behavior language v2 | `npc` entity + `moveTo(x,y)`; schedules = time-triggered state machines | Schedules are state machines; a dedicated schedule system re-invents the state machine | | Behavior language v2 (state machines) | **Engine, Tier 2 lead item** | v2 = v1 triggers + state machine syntax + `onTime`/`onSchedule` primitives | Critical path for NPC schedules, quests, events | | Dialogue/story | **Engine system** | `dialogue` content (nodes/options/conditions), `DialogueSystem`, MCP `dialogue.*`, hooks `onDialogueStart`/`onDialogueEnd` | Dialogue is a content document, not a state machine; AI-authored dialogue is a core creation scenario (all three criteria met) | | Friendship/affection | **Field-level, data layer** | `npc.affection` entity field; read/write via behavior language | Deterministic numeric rule — zero new engine code | | Quests/events | **Behavior language v2** | State = quest stage; transitions = events/conditions; actions = rewards (`give-item`); progress in `player.questProgress.`; authoring via `behavior.*` MCP | Quests are isomorphic to state machines; a `QuestSystem` re-invents the state machine | | Weather | **Lightweight engine system** | `world.weather` field, `content/weather.yaml` (season→weather probability table), roll on day-start, `weather-change` event, hook `onWeatherChange` | "Roll a probability table" is a mechanism; the behavior language has no randomness | | Stamina/fatigue | **Field-level BUILT + demo'd (D29)** | `player.stamina` field **seeded by the engine** (`seedWorld`: `stamina: 100`); rules via behavior language + Mods — the sample-game's `daily-stamina` machine restores stamina on day-start (`set-state`) and alerts on a **condition** (`player.stamina lte 30`, matching the fishing Mod's drain); the fishing Mod deducts stamina per cast | "Insufficient → block" is strategy; mechanism-izing it is brittle. No `StaminaSystem` | | Buildings/placement | **Placement primitive in engine** | `PlacementSystem`: grid occupancy (footprint, overlap checks, buildability read from map data), place/remove `type:"building"` entities; building definitions (footprint/appearance/function) are data | Grid occupancy + overlap validation is geometric mechanism the behavior language can't express | | Animals | **Reuse existing mechanisms + data** | `type:"animal"` entity + `animal` schema (growth cycle/output table/feed needs); GrowthSystem generalized to `age` field; output/feed rules via behavior language | Animals are isomorphic to crops — no new system | | UI/HUD | Engine exposes **UI logic state**, render layer draws | Engine tracks current dialogue node / inventory / shop prices / quest progress, exposed via MCP + WebSocket; render layer draws freely | Zero UI-framework code in engine | | Presentation data (sprites/sounds) | **BUILT (D26 data/protocol + D27 render half + D31 asset pipeline)** | `sprite` content kind (`content/sprites/`, programmatic draw instructions — rect/circle layers + the D31 `image` layer `{src: assetId, x?, y?, w?, h?}` referencing `content/assets/` binaries) + `sound` content kind (`content/sounds/`, synth specs + D31 `src` audio asset references) + MCP `sprite.*`/`sound.*` + optional `spriteRef` appearance hints on crop/animal stages, buildings, and the player; **binary assets (`content/assets/`) are NOT a content kind — a plain file table (`registry.assets`/`engine.assets`, base64) that YAML references by id** (D31); both catalogs + the asset table ride the WS snapshot; **render layer consumes them (D27/D31)** — `packages/render` draws `spriteRef` frames (pure `sprite-draw.ts`: `durationTicks`-weighted animation clock at a documented default 10 fps, building footprint scaling, `image` commands with source-crop), draws `map.tiles` image textures (palette `tileColor` fallback), and plays `sound` defs on `sound-play` (synth via Web Audio OR `src` via `decodeAudioData`, lazy AudioContext) | Presentation is content-as-data (D4); the render layer consumes the draw instructions / synth specs / asset table / `sound-play` events and plays. Assets are filesystem content referenced by id — no schema, no MCP namespace (D31) | | Audio | **Event mechanism BUILT (D26) + playback BUILT (D27/D31)** | `play-sound` action emits the **`sound-play`** bus event `{soundId}` (behavior-language-matchable, broadcast as an event frame); `sound` defs (`content/sounds/`) carry the synth spec and/or a D31 `src` audio asset id; the render layer's `createSoundPlayer` (`packages/render/src/sound.ts`) synthesizes the `synth` spec (OscillatorNode wave/freq/durationMs/gain/`slideTo` ramp) or decodes the `src` asset (base64 → `decodeAudioData` → `AudioBufferSourceNode`, buffers cached by id, `src` takes precedence over `synth`; AudioContext created lazily on the first user gesture per the autoplay policy) | Zero audio-library code in engine; playback is render-layer freedom (D1) | | Multiplayer | **Multi-viewport mechanism BUILT (D8/D24)**; multiplayer gameplay blank | N WebSocket clients subscribe to world state (`render/sync` BUILT 2026-08-12: `apps/mcp-server --ws-port` broadcast channel + `connectWs` browser client + snapshot-driven viewport drawing); multiplayer gameplay (multiple player entities, trading, permissions) left to creators | Multi-viewport sync is a mechanism; multiplayer *rules* are game content | | Skills/proficiency | **Field-level BUILT + demo'd (D29)** | `player.skills.{mining,farming,...}` fields **seeded by the engine** (`skills: {}`); accumulation via `set-state`/Mod mutation + `gte` threshold unlocks — the sample-game's fishing Mod increments `player.skills.fishing` per catch | Same shape as stamina/affection; the D29 fishing Mod is the accumulation demonstration | | Movement primitives | **Grid movement + blocking check** | `moveTo` tile-to-tile, simple blocking (non-walkable tiles, occupying entities), `npc-move` event with from/to | Pathfinding (A*/BFS) and collision systems stay out for now | | Player input | **Intent primitive BUILT (D25) + keyboard BUILT (D27) + `onPlayerIntent` hook (D29)** | `IntentSystem` (id `"intent"`): `handle(action, args)` — `move` (player to a walkable tile, shared map-grid/placement helpers) and `interact` (smart plot action: harvest mature → water planted → sow first seed → till → error), `player-intent` bus event per call (success AND failure). Channels: MCP `player.intent`, WS `{"type":"intent"}`, `connectLocal.intent` — all thin adapters over the same mechanism. **Hook (D29)**: `player-intent` → `onPlayerIntent` (16th hook; reverses D25.4's bus-only stance — Mods can't subscribe to raw bus events, and the intent payload is transient, so the "Mod hook demand" arm holds; the fishing Mod is the consumer). **Keyboard (D27)**: the dev app's arrow-key `keydown` → `sync.input("key:Arrow<…>")` — ws mode sends the D26 raw `input` message (server maps arrows → `IntentSystem.move`), local mode maps the same `arrowDirection` delta client-side | Interact priority order is the v1 engine default (content-izable later); a seed picker is future UI; the browser toolbar UI is render-layer freedom (D24) | | Math / randomness | **Seeded RNG mechanism BUILT (D28)** | `RandomSource` (`packages/core/src/random.ts`, mulberry32-style mirroring LÖVE's `love.math.RandomGenerator`): `seedRandom(seed)` / `randomSeed()`; engine-owned `engine.rng` (seeded via `createEngine({seed?})`), injected into systems via `SystemInitCtx.rng` — WeatherSystem day-start rolls and PlantingSystem harvest drop chances draw from it; MCP `rng.*` debug surface (`rng.get`/`rng.set`/`rng://state`); `save.*` carries `rngState {seed, calls}` and restore re-anchors the seed | Determinism is a mechanism (replay/tests/Mods); "draw from the engine RNG" replaces scattered `Math.random` calls. Declined LÖVE gaps (D28 adjudication): text rendering / particles → render-layer freedom; **image-atlas + audio files → ADOPTED (D31)** — the asset pipeline builds them as `content/assets/` binaries referenced by id (sprite `image` layer, `map.tiles`, `sound.src`); physics/threading/compression/window/video/sensor/joystick → not applicable; Tauri → deferred | | Minigames (fishing/mining/festivals) | **Mod / render layer — fishing demo'd (D29)** | Engine guarantees trigger conditions + reward settlement (`give-item`) + progress (entity fields) via data + language; the minigame **body lives in the Mod** — the sample-game's fishing Mod hooks `onPlayerIntent`, settles the catch on water tiles (fish grant, stamina cost, `skills.fishing`, `mod:fish-caught`/`mod:fish-too-tired` events the language matches), and the fishing-treat machine turns `mod:fish-caught` into a reward | No built-in minigame framework; D29 is the escape-hatch demonstration (D5/D19) | ## Pattern Cheat-Sheet | Feature kind | Destination | |---|---| | Content document (dialogue, shops, crops) | Engine system + content schema | | Random mechanism (weather rolls, drop chances) | Engine system + **shared seeded RNG (D28)** — `engine.rng` via `SystemInitCtx.rng` | | Geometry/spatial (placement, movement) | Engine primitive | | Temporal state (schedules, quests) | Behavior language v2 (state machines) | | Deterministic numeric + rules (affection, stamina, skills) | Entity fields + behavior language | ## Tier 2 Build List (derived from this session) 1. ~~**Behavior language v2** — state machine syntax + `onTime`/`onSchedule` triggers (lead item; critical path for NPC schedules/quests/events)~~ **BUILT (2026-08-12)**: `statemachine` content kind + `behavior.statemachine.*` MCP tools + `onSchedule` time primitive + `onStateChange` hook. See `decisions.md` D20, `mcp.md` §behavior, `api.md` §4/§7. 2. ~~**Dialogue system** — `dialogue` schema + `DialogueSystem` + MCP `dialogue.*` + `onDialogueStart`/`onDialogueEnd` hooks~~ **BUILT (2026-08-12)**: `dialogue` content kind (`content/dialogues/.yaml`) + `DialogueSystem` (active state on the `dialogue:active` world entity) + MCP `dialogue.*` (7 tools incl. debug `start`/`choose` + 2 resources + 1 prompt) + `onDialogueStart`/`onDialogueEnd` hooks. See `decisions.md` D21, `mcp.md` §dialogue, `api.md` §4/§7. 3. ~~**WeatherSystem** — `weather.yaml` + `weather-change` event + `onWeatherChange` hook~~ **BUILT (2026-08-12)**: `weather` single-file content kind (`content/weather.yaml`) + `WeatherSystem` (weighted roll on day-start, injectable RNG, state on the `weather` world entity) + `weather-change` event + `onWeatherChange` hook + MCP `weather.*` (2 debug tools + 1 resource). See `decisions.md` D21, `mcp.md` §weather, `api.md` §4/§7. 4. ~~**PlacementSystem** — grid occupancy (footprint/overlap/buildability)~~ **BUILT (2026-08-12)**: `building` content kind (`content/buildings/.yaml`) + `PlacementSystem` (grid footprint/overlap/buildability-from-map-data, place/remove `type:"building"` entities) + `placement-change` event + `onPlacementChange` hook + MCP `placement.*` (5 building lifecycle tools + 4 debug tools + 3 resources + 1 prompt). See `decisions.md` D22, `mcp.md` §placement, `api.md` §4/§7. 5. ~~**NPC grid-movement primitive** — `moveTo` + blocking check + `npc-move` event~~ **BUILT (2026-08-12)**: `MovementSystem` (id `movement`) — `moveTo` tile-to-tile with a simple blocking check (unwalkable "water" tiles, `blocksMovement` buildings via `PlacementSystem.occupies`) + `npc-move` event with from/to + MCP `npc.*` (5 debug tools + 1 resource, no prompts). No pathfinding (A*/BFS), no collision system, no NPC schedules (those are behavior language v2 content). See `decisions.md` D22, `mcp.md` §npc, `api.md` §4. 6. ~~Reuse/generalization: GrowthSystem generalized to animal `age`~~ **BUILT (2026-08-12)**: `animal` content kind (`content/animals/.yaml` — growth cycle + produce table, data only) + GrowthSystem generalized to advance BOTH `type:"plot"` (CropDef) and `type:"animal"` entities (AnimalDef) — animal entities carry `{animalId, stageIndex?, daysInStage?, fed?}`, `feedRequired` mirrors `waterRequired`, `feed` mirrors `water`; feed/produce RULES are behavior-language content hooking the `animal-grow`/`animal-produce`/`animal-fed` bus events; hooks `onAnimalGrow`/`onAnimalProduce`; MCP `animal.*` (9 tools + 2 resources + 1 prompt). See `decisions.md` D23, `mcp.md` §animal, `api.md` §4/§7. **Tier 2 core is now complete** (items 1–6 with the tick loop from Task A). 7. ~~**Multi-viewport sync (`render/sync`)** — optional render-layer extension~~ **BUILT (2026-08-12, D24)**: the final Tier 2 item. `apps/mcp-server` gains an optional `--ws-port `/`FARM_WS_PORT` WebSocket broadcast channel in the same process (snapshot on connect + on `{"type":"sync"}`, event frames per bus emission, defensive JSON serialization); `packages/render/src/sync/ws.ts` `connectWs` (browser-native `WebSocket`, same handle shape as `connectLocal`); `viewport.render(snapshot?)` draws map + basic entity shapes (pure `shapes.ts` geometry — plots/buildings/dots, no art); `apps/dev` parameterized (`?contentDir=`/`?modsDir=`/`?sync=ws&url=ws://...`). MCP surface unchanged. See `decisions.md` D24, `mcp.md` transport section, `api.md` §1. **Tier 2 is now fully complete.** ## Change Log | Date | Change | Files | |---|---|---| | 2026-08-13 | **Asset pipeline BUILT (D31) — the D28 "image-atlas + audio files" gap is adopted, closing the D26-reserved atlas/audio-file capability.** Presentation data row: `sprite` gains the `image` layer (`src` = asset id), `sound.src` becomes a real audio asset id, `map` gains `tiles` (tile-id → asset id); **binary assets (`content/assets/`, `.png`/`.ogg`) are NOT a content kind** — the loader builds an asset table (`registry.assets`/`engine.assets`, base64) that YAML references by id; the table rides the bundle and the WS snapshot. Render: image layers draw via PixiJS textures (data: URLs, cache by id, async + redraw-on-ready), map tiles draw from `map.tiles`, `sound.src` plays via `decodeAudioData` (buffers cached). Audio row: src playback added (`src` takes precedence over `synth`). Math/randomness row: the D28 declined "image-atlas + audio files" verdict flips to **ADOPTED (D31)** — no atlas format (single-tile assets by id, sprite-sheet atlas documented as future). MCP surface unchanged — no `assets.*` namespace (D31.8). | `boundaries.md`, `decisions.md` (D31), `mcp.md`, `api.md`, `AGENTS.md`, `examples/sample-game/README.md` | | 2026-08-12 | **Tier 3 content round (D29): stamina/skills field-level rows marked BUILT + demo'd, minigame row demo'd, `onPlayerIntent` hook adjudicated.** Stamina/fatigue and Skills/proficiency verdict rows flip from "Field-level, data layer" to **Field-level BUILT + demo'd (D29)**: the engine seeds `player.stamina: 100` and `player.skills: {}` (data, not systems); the sample-game's `daily-stamina` machine demonstrates both a **condition** on `player.stamina` (low-stamina alert) and a day-start restore (`set-state`), and the fishing Mod accumulates `player.skills.fishing`. Minigames row flips to **Mod / render layer — fishing demo'd (D29)**: the fishing Mod hooks the new `onPlayerIntent` (16th hook — see the player-input row) and settles catches on water tiles (fish grant, stamina cost, `mod:fish-caught`/`mod:fish-too-tired` custom events the behavior language matches via `fishing-treat`). Player-input row gains the D29 hook adjudication. | `boundaries.md`, `decisions.md` (D29), `mcp.md`, `api.md`, `AGENTS.md`, `examples/sample-game/README.md` | | 2026-08-12 | **Deterministic seeded RNG BUILT (D28) — the ONE gap adopted from the engine-vs-LÖVE grill; all other LÖVE gaps consciously declined and recorded.** New "Math / randomness" verdict row (mechanism BUILT): `RandomSource` (`packages/core/src/random.ts`, mulberry32-style mirroring `love.math.RandomGenerator` — `seedRandom(seed)`/`randomSeed()`, `next()`/`nextInt(max)`/`setSeed`/`state(): {seed, calls}`); engine-owned `engine.rng` (`createEngine({seed?})`) injected into systems via `SystemInitCtx.rng` — WeatherSystem day-start rolls and PlantingSystem harvest drop chances now draw from it (the only two randomness consumers); MCP `rng.*` debug namespace (18th: `rng.get`/`rng.set`/`rng://state`, no prompts); `save.*` snapshot carries `rngState {seed, calls}` and restore re-anchors the seed (`calls` reset to 0, draw replay not built). The cheat-sheet's random-mechanism row now points at the shared seeded RNG. Declined gaps (adjudicated in D28, NOT built): text rendering + particles → render-layer freedom (D1/D6); image/atlas + audio files → reserved Tier 3 (D26 maintained); physics → grid-blocking covers collision; threading/compression/window/video/sensors/joystick → not applicable; Tauri → deferred. | `boundaries.md`, `decisions.md` (D28), `mcp.md`, `api.md`, `AGENTS.md` | | 2026-08-12 | **Player input mechanism BUILT (D25)** — the engine's first direct player-input primitive (previously input was MCP-debug-only per D3's "runtime MCP is debug-only"; D25 adds the *mechanism* for human play while keeping MCP as one debug adapter). `IntentSystem` (id `"intent"`, auto-booted): `handle(action, args)` for `move {tile}` (walkability + blocking via the shared map-grid/placement helpers, NOT `MovementSystem.moveTo` — npc-move untouched) and `interact {tile}` (v1 priority order harvest → water → sow-first-seed → till → error, delegating to `PlantingSystem`); every call emits `player-intent` (success AND failure). Three thin channels over the same mechanism: MCP `player.intent` + `player://state` (15th namespace), WS `{"type":"intent"}` message + snapshot gains `time`, `connectLocal.intent`. Sync handles are now uniform (`intent`/`setTimeLabel`/`refresh`/`destroy`); `viewport.tileAt` + an Interact/Move toolbar make the dev app playable in both local and ws modes. The interact priority order is the v1 hardcoded engine default (content-izable later); the browser toolbar/HUD is render-layer freedom (D24 — the engine exposes the intent result + world state, the render layer draws). | `boundaries.md`, `decisions.md` (D25), `mcp.md`, `api.md`, `architecture.md`, `AGENTS.md` | | 2026-08-12 | **Tier 2 item 7 built: multi-viewport sync (`render/sync`)** — Tier 2 fully complete. Item 7 (previously "optional render-layer extension") is now BUILT per D24: `apps/mcp-server` hosts an optional `--ws-port `/`FARM_WS_PORT` WebSocket broadcast channel in the same process (snapshot on connect/`sync` request; event frames broadcast to all clients per bus emission; defensive JSON serialization — non-serializable payloads skipped, never a crash); `packages/render/src/sync/ws.ts` `connectWs` implements the `LocalSyncHandle` shape client-side (browser-native `WebSocket`, auto-reconnect, fresh-snapshot re-sync per event frame, time label from `day-start`/`time-tick`-style payloads); `viewport.render(snapshot?)` now draws map + basic entity shapes (pure `shapes.ts` geometry: plots tilled/watered/crop-stage, buildings as footprint blocks, npc/player/animal dots — no art); `apps/dev` parameterized via `?contentDir=`/`?modsDir=`/`?sync=ws&url=ws://...`. The engine event bus is exposed as `engine.events` (readonly) for the broadcast subscriber — the only core change. MCP surface unchanged (85 tools / 22 resources / 10 prompts / 14 namespaces); the WS channel is NOT a namespace. Multiplayer gameplay (multiple players, trading, permissions) remains creator content. | `boundaries.md`, `decisions.md` (D24), `mcp.md`, `api.md`, `architecture.md`, `AGENTS.md` | | 2026-08-12 | **Tier 2 items 5 & 6 (tick loop + animal age) built** — Tier 2 core complete. Item 5's "fine-grained intra-day timing awaits a tick loop" limitation is now resolved: `engine.tick(dt)` (dt in ticks) is the authoritative explicit driver (D8 — no render loop), driving TimeSystem (boundary events + new `time-tick` `{day, season, tickInDay}` per step) and every system's `step` in registration order (GrowthSystem accumulates day-fractions); `onSchedule` gains `at: {tickInDay: n}` evaluated on `time-tick`; optional auto-tick via `createEngine({tickIntervalMs})` (OFF by default). Item 6's animal generalization: `animal` content kind + GrowthSystem advancing `type:"animal"` entities through AnimalDef stages with a `fed` flag mirroring `watered`; feed/produce rules remain behavior-language content (decision #9 — engine emits `animal-grow`/`animal-produce`/`animal-fed`, grants nothing); MCP `animal.*` namespace + hooks `onAnimalGrow`/`onAnimalProduce`. The last remaining Tier 2 item is the optional multi-viewport sync (`render/sync`), which is a render-layer concern. | `boundaries.md`, `decisions.md` (D23), `mcp.md`, `api.md` | | 2026-08-12 | Initial draft (grilling session: engine vs game boundary) | `boundaries.md`, `decisions.md` (D19), `AGENTS.md` | | 2026-08-12 | **Tier 2 item 1 built: behavior language v2** (state machine syntax + `onSchedule` time primitive). The "temporal state → behavior language v2" row of the pattern cheat-sheet now has a concrete mechanism: schedules/quests/events are content state machines. NPC schedules and quests remain creator content built on this language (no dedicated schedule/quest system — per D19, a dedicated system would re-invent the state machine). Fine-grained intra-day timing still awaits a tick loop (documented limitation in `mcp.md`/`api.md`). | `boundaries.md`, `decisions.md` (D20), `mcp.md`, `api.md` | | 2026-08-12 | **Tier 2 items 2 & 3 built: dialogue + weather.** Dialogue is a content-document system (nodes/choices/conditions — a document, not a state machine, per D19), confirming the "content document → engine system" cheat-sheet row; weather is a lightweight probability-roll system ("roll from a probability table" is a mechanism; the behavior language has no randomness). Both snapshot via world entities (`dialogue:active`, `weather`). | `boundaries.md`, `decisions.md` (D21), `mcp.md`, `api.md` | | 2026-08-12 | **Tier 2 items 4 & 5 built: placement + NPC grid movement.** Both confirm the "geometry/spatial → engine primitive" cheat-sheet row — they are the two final Tier 2 engine subsystems. Placement: `building` content kind (footprint/`buildable`/`blocksMovement` are data), `PlacementSystem` (grid footprint, overlap, buildability from map data, place/remove building entities), `placement-change` event + `onPlacementChange` hook. Movement: `MovementSystem` `moveTo` + simple blocking (unwalkable tiles + `blocksMovement` buildings) + `npc-move` event. No pathfinding/collision systems, no NPC schedules (state-machine content). Tier 2 remains open only for item 6 (GrowthSystem→animal `age` generalization + multi-viewport sync). | `boundaries.md`, `decisions.md` (D22), `mcp.md`, `api.md` |