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 indecisions.mdin the same change (D-op2).
A feature is built into the engine iff all three of these hold simultaneously:
Failure of any one → push the feature down (to fields + behavior language, or to the render layer / Mods entirely).
| 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 | 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.<id>; 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) |
| 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 |
onTime/onSchedule triggers (lead item; critical path for NPC schedules/quests/events)statemachine content kind + behavior.statemachine.* MCP tools + onSchedule time primitive + onStateChange hook. See decisions.md D20, mcp.md §behavior, api.md §4/§7.dialogue schema + DialogueSystem + MCP dialogue.* + onDialogueStart/onDialogueEnd hooksdialogue content kind (content/dialogues/<id>.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.weather.yaml + weather-change event + onWeatherChange hookweather 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.building content kind (content/buildings/<id>.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.moveTo + blocking check + npc-move eventMovementSystem (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.ageanimal content kind (content/animals/<id>.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).render/sync) — optional render-layer extensionapps/mcp-server gains an optional --ws-port <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.| 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 <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 |