{"_id":"@amas.nghia/todaycode-game-engine","_rev":"6-6d9783ff169ae1b8078adbe5dde8f4b6","name":"@amas.nghia/todaycode-game-engine","dist-tags":{"latest":"0.3.0"},"versions":{"0.2.3":{"name":"@amas.nghia/todaycode-game-engine","version":"0.2.3","license":"UNLICENSED","_id":"@amas.nghia/todaycode-game-engine@0.2.3","maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"dist":{"shasum":"a0accf5863fb27fcc5544fb368be2acc284b05d2","tarball":"https://registry.npmjs.org/@amas.nghia/todaycode-game-engine/-/todaycode-game-engine-0.2.3.tgz","fileCount":8,"integrity":"sha512-x5i3Pyunm/yfZiJh1WqaZrgbg2W1pDT499fkdTUJovr1B4MhwzPec1TWzvEHewaofElSgU0gqWdMXtwyUaczgg==","signatures":[{"sig":"MEUCIQDi9Cn3ASJUZLLnzbB0lhmqdaN9H1fbzSDmT1i0voz0nQIgLNtdlr+c2IPUahjYzvlr3Qlu6wJBftewFi+19hVYI78=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":303087},"jest":{"preset":"ts-jest","testMatch":["**/__tests__/**/*.spec.ts"],"testEnvironment":"node","moduleFileExtensions":["ts","js","json"]},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.cts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"e37158b631dd8490f555927526d793d662412c8b","scripts":{"lint":"tsc -b --noEmit","test":"jest","build":"tsup","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"amas.nghia","email":"amasnghia@gmail.com"},"_npmVersion":"11.12.1","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","tsup":"^8.0.0","ts-jest":"^29.0.0","typescript":"^5.6.0","@types/jest":"^30.0.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/todaycode-game-engine_0.2.3_1784028650978_0.588589499499965","host":"s3://npm-registry-packages-npm-production"}},"0.2.6":{"name":"@amas.nghia/todaycode-game-engine","version":"0.2.6","license":"UNLICENSED","_id":"@amas.nghia/todaycode-game-engine@0.2.6","maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"dist":{"shasum":"ae701c30a0659c46a8c6946533410f2456acb955","tarball":"https://registry.npmjs.org/@amas.nghia/todaycode-game-engine/-/todaycode-game-engine-0.2.6.tgz","fileCount":8,"integrity":"sha512-2AByLawIw5/F/cPpQotKjCVv0ppMWzV4eel2lLSWw+DvE1riXLgBGpKJkCdgeUEHkHVIMaWE87m2Hrx4ttjtsA==","signatures":[{"sig":"MEUCIGDEw8Q3kbAXtSGLgY8qcyGMr5Dn2Ko8TyorU+bnVHdTAiEAjBmGLYvhSj+wIKTNyEYfldPJ1qUZpMNREfQUYWiByHg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":307443},"jest":{"preset":"ts-jest","testMatch":["**/__tests__/**/*.spec.ts"],"testEnvironment":"node","moduleFileExtensions":["ts","js","json"]},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.cts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"d4417ae63eb01e4a3d1c00aa12de8dd8c6b074d5","scripts":{"lint":"tsc -b --noEmit","test":"jest","build":"tsup","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"amas.nghia","email":"amasnghia@gmail.com"},"_npmVersion":"11.12.1","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","tsup":"^8.0.0","ts-jest":"^29.0.0","typescript":"^5.6.0","@types/jest":"^30.0.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/todaycode-game-engine_0.2.6_1784097977623_0.39000427729348464","host":"s3://npm-registry-packages-npm-production"}},"0.2.7":{"name":"@amas.nghia/todaycode-game-engine","version":"0.2.7","license":"UNLICENSED","_id":"@amas.nghia/todaycode-game-engine@0.2.7","maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"dist":{"shasum":"8905978ba047cde0baa975cd0e84bab67e0c27a9","tarball":"https://registry.npmjs.org/@amas.nghia/todaycode-game-engine/-/todaycode-game-engine-0.2.7.tgz","fileCount":8,"integrity":"sha512-ey1SIY/G6Af2vy0a31LwJZdgYfItTUBuOBMASBre+tWc6NuAzZv5hwxAg9NHvwe+2m7tuEFoRmrEZWmHPrtNug==","signatures":[{"sig":"MEYCIQClmIJuIEm+dd5lw8yOoLx9PO8hNZxi4YLfUQtp+tYhqQIhAPDIsTMJ9PU4be2ek0OIe9znhXyA7xpud68QqtLZOrGT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":307155},"jest":{"preset":"ts-jest","testMatch":["**/__tests__/**/*.spec.ts"],"testEnvironment":"node","moduleFileExtensions":["ts","js","json"]},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.cts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"d4417ae63eb01e4a3d1c00aa12de8dd8c6b074d5","scripts":{"lint":"tsc -b --noEmit","test":"jest","build":"tsup","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"amas.nghia","email":"amasnghia@gmail.com"},"_npmVersion":"11.12.1","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","tsup":"^8.0.0","ts-jest":"^29.0.0","typescript":"^5.6.0","@types/jest":"^30.0.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/todaycode-game-engine_0.2.7_1784100979293_0.1806056850060893","host":"s3://npm-registry-packages-npm-production"}},"0.2.8":{"name":"@amas.nghia/todaycode-game-engine","version":"0.2.8","license":"UNLICENSED","_id":"@amas.nghia/todaycode-game-engine@0.2.8","maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"dist":{"shasum":"886ec3da436c0e32b079fa31f8f13d623d004539","tarball":"https://registry.npmjs.org/@amas.nghia/todaycode-game-engine/-/todaycode-game-engine-0.2.8.tgz","fileCount":8,"integrity":"sha512-zlq67ZJf5Hf/sANinaRGvv//hzo/4hY6/1e50+vZqgg2keJc6/DxfL81nkLdPzy43cm95hC1JGt+GdVvW5cdhA==","signatures":[{"sig":"MEQCIG7oLLLe1abbgBUPjU44Xmho6G4JNG3w7nia10gkkaDMAiB4etFX1+g7e4D6F58fc3o1SsvlJ7I7XzI9EQpByJJW2A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":314415},"jest":{"preset":"ts-jest","testMatch":["**/__tests__/**/*.spec.ts"],"testEnvironment":"node","moduleFileExtensions":["ts","js","json"]},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.cts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"d4417ae63eb01e4a3d1c00aa12de8dd8c6b074d5","scripts":{"lint":"tsc -b --noEmit","test":"jest","build":"tsup","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"amas.nghia","email":"amasnghia@gmail.com"},"_npmVersion":"11.12.1","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","tsup":"^8.0.0","ts-jest":"^29.0.0","typescript":"^5.6.0","@types/jest":"^30.0.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/todaycode-game-engine_0.2.8_1784106578776_0.44778494204589325","host":"s3://npm-registry-packages-npm-production"}},"0.2.9":{"name":"@amas.nghia/todaycode-game-engine","version":"0.2.9","license":"UNLICENSED","_id":"@amas.nghia/todaycode-game-engine@0.2.9","maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"dist":{"shasum":"7af26ffc1650d10d8f10388fb4ff8efd856d8aa1","tarball":"https://registry.npmjs.org/@amas.nghia/todaycode-game-engine/-/todaycode-game-engine-0.2.9.tgz","fileCount":8,"integrity":"sha512-3irigLjmWHLqs4rFCYUU6JCiADGHWw6iKnDai2EM5lBtcscm5AJRpBC/cS1js4V+G42Q3VRanBAYkaIopsb3bQ==","signatures":[{"sig":"MEUCIQDCMotGcaDXGxOc3CVEjGbJU0D1451mJGcr1L86mpkN8QIgHRk9b8Hppo18T9caqQkFF6TPJnHqM/H3ef4D5Q03mfk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":359822},"jest":{"preset":"ts-jest","testMatch":["**/__tests__/**/*.spec.ts"],"testEnvironment":"node","moduleFileExtensions":["ts","js","json"]},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.cts","module":"./dist/index.js","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"gitHead":"6ef5c071b3dd3485b9fb152f2859f0006360ee72","scripts":{"lint":"tsc -b --noEmit","test":"jest","build":"tsup","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"amas.nghia","email":"amasnghia@gmail.com"},"_npmVersion":"11.12.1","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","directories":{},"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","tsup":"^8.0.0","ts-jest":"^29.0.0","typescript":"^5.6.0","@types/jest":"^30.0.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/todaycode-game-engine_0.2.9_1784114010602_0.8504405430180468","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@amas.nghia/todaycode-game-engine","version":"0.3.0","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","license":"UNLICENSED","publishConfig":{"access":"public"},"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.cts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"scripts":{"build":"tsup","test":"jest","lint":"tsc -b --noEmit","prepublishOnly":"pnpm run build"},"devDependencies":{"@types/jest":"^30.0.0","@types/node":"^22.0.0","jest":"^30.0.0","ts-jest":"^29.0.0","tsup":"^8.0.0","typescript":"^5.6.0"},"jest":{"preset":"ts-jest","testEnvironment":"node","testMatch":["**/__tests__/**/*.spec.ts"],"moduleFileExtensions":["ts","js","json"]},"gitHead":"07873de29c22f931e2a89f4760d25a289de19f3c","_id":"@amas.nghia/todaycode-game-engine@0.3.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-LRHgOGZ8i/MzpbdhlT6WMJyDUoo46xTXYpjJAcWamk177jnH1h0d+ldWxcsr4TEexiEtOzkTUIt9uqSWoYqXFg==","shasum":"c622caf5336f0139ab58749dd2dbc0ba42fcbf9b","tarball":"https://registry.npmjs.org/@amas.nghia/todaycode-game-engine/-/todaycode-game-engine-0.3.0.tgz","fileCount":8,"unpackedSize":348486,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD+CChN03gQUObMbK3Y0C4NDhGKZNAzUHcW99Eqnm/7DAIgKc1ScmikrEKcL2HwHe2i5hBGobVMggFRt/h3OmhhDL4="}]},"_npmUser":{"name":"amas.nghia","email":"amasnghia@gmail.com"},"directories":{},"maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/todaycode-game-engine_0.3.0_1784133716888_0.3003170432399478"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-14T11:30:50.796Z","modified":"2026-07-15T16:41:57.145Z","0.2.3":"2026-07-14T11:30:51.115Z","0.2.6":"2026-07-15T06:46:17.775Z","0.2.7":"2026-07-15T07:36:19.500Z","0.2.8":"2026-07-15T09:09:38.940Z","0.2.9":"2026-07-15T11:13:30.730Z","0.3.0":"2026-07-15T16:41:57.037Z"},"license":"UNLICENSED","description":"Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.","maintainers":[{"name":"amas.nghia","email":"amasnghia@gmail.com"}],"readme":"# todaycode-game-engine\n\nA TypeScript library that simulates a game **frame by frame, as pure logic**.\nNo rendering, no I/O, fully deterministic — the same input always produces the\nsame result, whether it runs in a browser, on a server, or in CI.\n\n**You give it:**\n\n- a **world** — a list of entities (hero, enemies, coins, walls)\n- a **plan** — the sequence of actions a player wants to perform (`move`, `attack`, `pick up`)\n- the **rules** — what each action does, and what happens automatically every frame\n- a **goal** — the win/loss condition\n\n**It gives you back:**\n\n- whether the plan **succeeded or failed**\n- a **frame-by-frame recording** of everything that happened, so a client can\n  play it back as an animation without running the engine itself\n\n```\n         INPUT                          SIMULATION                      OUTPUT\n┌────────────────────────┐   ┌───────────────────────────┐   ┌─────────────────────────┐\n│ WorldState (entities)  │   │  WorldRunner.run()        │   │ WorldRunnerResult       │\n│ Plan (list of Actions) │   │  each frame:              │   │  ├ success: boolean     │\n│ Systems (auto rules)   │ → │   1. run systems          │ → │  ├ frames: number       │\n│ Action handlers        │   │   2. advance the plan     │   │  ├ events[]             │\n│ Objectives (win/loss)  │   │   3. snapshot the world   │   │  └ worldFrames[]        │\n│ seed                   │   │   4. check objectives     │   │        │ buildReplay()  │\n└────────────────────────┘   │   5. frame++              │   │        ▼                │\n                             └───────────────────────────┘   │  Replay (JSON)          │\n                                                             └─────────────────────────┘\n```\n\n## Architecture: ECS + Actions/Plans + Objectives\n\nThe engine is built on the **Entity–Component–System (ECS)** pattern, with two\nextra layers on top: **Actions/Plans** (\"what a character does\") and\n**Objectives** (\"what counts as winning\").\n\n### New to ECS? A two-minute primer\n\nThe classic OOP way to model a game is one class per thing:\n\n```ts\nclass Hero  { hp; x; y; move() {...} attack() {...} }\nclass Wall  { x; y; }\nclass Coin  { x; y; pickUp() {...} }\n```\n\nThis falls apart quickly. What about a chest that can be attacked but can't\nmove? A ghost that moves but can't be hit? A coin that expires? You end up with\ndeep inheritance trees, duplicated logic, and classes full of methods that\ndon't apply. ECS solves this by splitting a \"game object\" into three separate\nideas:\n\n| | What it holds | What it does NOT hold |\n|---|---|---|\n| **Entity** | Only an identity: an `id`, a `kind` label, and a bag of components. | No methods. No class hierarchy. A hero and a wall are the *same type*. |\n| **Component** | One facet of plain data: `position {x,y}`, `health {current,max}`, `movement {speed}`. | No logic. Just data you can attach or remove at runtime. |\n| **System** | Behavior: a function that runs every frame, scans all entities, and processes the ones that **have the components it cares about**. | No per-entity state. It doesn't know what a \"hero\" is. |\n\nThe mental shift: **what something *is* = which components it carries; what\n*happens* = which systems run.** Three entities, three different component mixes:\n\n```ts\n// A hero: can be somewhere, can walk, takes damage\n{ id: 'hero', kind: 'hero', components: {\n    position: { x: 10, y: 10 },\n    movement: { speed: 1, stepDistance: 5 },\n    health:   { current: 20, max: 20 },\n} }\n\n// A wall: has a place and blocks movement — nothing else\n{ id: 'wall-1', kind: 'wall', components: {\n    position: { x: 15, y: 10 },\n    collision: { radius: 1 },\n    blocking:  { blocksMovement: true },\n} }\n\n// A coin: has a place and can be picked up — it can't move or die\n{ id: 'coin-1', kind: 'coin', components: {\n    position:    { x: 12, y: 10 },\n    collectible: { kind: 'gold' },\n} }\n```\n\nA system then selects by component, never by class. `MovementSystem` is\nliterally just:\n\n```\nevery frame:\n  for each entity (in fixed order):\n    does it have position + motion + movement?   ← \"motion\" = a destination someone gave it\n      yes → move it `speed` units toward motion.target\n            arrived? remove the motion component (the move is finished)\n      no  → skip it\n```\n\nIt moves heroes, enemies, and arrows with the same 20 lines of code — anything\ncarrying those three components. Want a new kind of thing? **Don't write a\nclass — compose components**: an attackable chest is `position + health` (no\n`movement`, so it can never walk); a patrolling ghost is `position + movement`\n(no `health` and no `collision`, so it can't be hit and walks through walls).\n\nWhy this engine uses ECS specifically:\n\n1. **Composition over inheritance** — every game level is just a list of\n   entity literals like the ones above; new gameplay = new component mixes,\n   no new classes.\n2. **Free serialization** — the whole world is plain JSON data, so snapshotting\n   it every frame (`worldFrames`) and replaying matches later costs nothing.\n3. **Determinism** — behavior lives in systems that iterate one fixed `order`\n   array, so two machines always process entities in the same sequence.\n\n### The six concepts and where they live\n\nOn top of raw ECS, the engine adds Actions/Plans and Objectives. Six concepts,\none file each:\n\n| Concept | What it is | File |\n|---|---|---|\n| **Entity + Component** | Everything in the game is an entity: `{ id, kind, components }`. Components are plain data — `position {x,y}`, `health {current,max}`, `movement {speed}`. Entities have **no methods**; all behavior lives in Systems and Actions. | [entity.ts](src/entity.ts), [components.ts](src/components.ts) |\n| **WorldState** | The container holding all entities, the current `frame`, the `seed`, optional map `bounds`, and an `order` array that fixes iteration order (this is what makes the simulation deterministic). | [world.ts](src/world.ts) |\n| **Action** | One command from a character: `{ type: 'MOVE_DIRECTION', actorId: 'hero', payload: {...} }`. Each `type` has a **handler** that defines how the command plays out — possibly across **many frames** (walking 5 units takes 5 frames). | [actions.ts](src/actions.ts), [multi-frame-action.ts](src/multi-frame-action.ts) |\n| **Plan** | A sequence of actions that runs **one at a time**: action 1 must finish before action 2 starts — exactly like a student writing `move(); attack();`. Written as a generator (`function*`) that yields actions. | [plan-runner.ts](src/plan-runner.ts) |\n| **System** | A rule that runs **automatically every frame**, with no command needed: `MovementSystem` slides entities toward their destination, a trap deals damage, gravity pulls things down. | [systems.ts](src/systems.ts), [systems/movement.ts](src/systems/movement.ts) |\n| **Objective** | Win/loss conditions declared as plain JSON objects: `{ type: 'reach_position', ... }`, composable with `all` / `any`. | [objectives.ts](src/objectives.ts) |\n\n**[`WorldRunner`](src/world-runner.ts)** ties everything together. Every frame, in this exact order:\n\n1. Run every System — e.g. `MovementSystem` moves each entity `speed` units toward its target.\n2. Advance the Plan one step — the current action gets a `step()` call; if it finished, the next action starts.\n3. Snapshot the world (position/health/collected of every entity) plus this frame's events → `worldFrames[]`.\n4. Check `winCondition` / `lossCondition` — if met, stop and return `success`.\n5. If the plan ran out of actions (or failed) without winning → stop with `success: false`. If `maxFrames` is reached → timeout, `success: false`.\n\nThe single most important design decision — **Actions and Systems are\ndecoupled**: a `MOVE_DIRECTION` action does *not* move the entity itself. Its\nhandler only attaches a `motion` component (the destination). `MovementSystem`\nis what actually moves the entity a little each frame. When the entity arrives,\nthe system removes `motion`, the handler sees that and reports `done`, and the\nplan moves on to the next action. This is how one command like \"walk 12 units\"\nplays out smoothly over 12 frames while the plan code stays simple and sequential.\n\n## Install\n\nThe package is published publicly on [npm](https://www.npmjs.com/package/@amas.nghia/todaycode-game-engine) —\nno registry config or token needed:\n\n```bash\npnpm add @amas.nghia/todaycode-game-engine\n```\n\n## Getting started — a complete example\n\nThe task: a hero stands at (10,10) and must reach (15,15). The player submits\nthe plan \"move right, then move up\". Copy this block and it runs as-is:\n\n```ts\nimport {\n  createWorld, addEntity,                          // step 1\n  SystemRunner, MovementSystem,                    // step 2\n  MultiFrameActionRunner, MoveDirectionHandler,    // step 3\n  YieldPlanRunner, type PlanIterator,              // step 4\n  ObjectiveSystem, WorldRunner,                    // step 5\n  buildReplay, toReplayJSON,                       // step 6\n} from '@amas.nghia/todaycode-game-engine'\n\n// ── Step 1: create a world and drop entities into it ─────────────────\nconst world = createWorld(/* seed */ 123)\nworld.bounds = { minX: 0, maxX: 100, minY: 0, maxY: 100 }\n\naddEntity(world, {\n  id: 'hero',\n  kind: 'hero',\n  components: {\n    position: { x: 10, y: 10 },\n    movement: { speed: 1, stepDistance: 5 },  // moves 1 unit/frame; each move command covers 5 units by default\n    collision: { radius: 0.5 },\n  },\n})\n\n// ── Step 2: register systems (rules that run automatically) ──────────\nconst systems = new SystemRunner()\nsystems.addSystem(new MovementSystem())\n\n// ── Step 3: register action handlers (which commands exist) ──────────\nconst actions = new MultiFrameActionRunner()\nactions.registerHandler(new MoveDirectionHandler())\n\n// ── Step 4: the plan — the player's/bot's script ─────────────────────\nfunction* heroPlan(): PlanIterator {\n  yield { type: 'MOVE_DIRECTION', actorId: 'hero', payload: { direction: 'right' } }\n  yield { type: 'MOVE_DIRECTION', actorId: 'hero', payload: { direction: 'up' } }\n}\nconst plan = new YieldPlanRunner(actions, heroPlan())\n\n// ── Step 5: win condition + run ──────────────────────────────────────\nconst runner = new WorldRunner(world, systems, actions, new ObjectiveSystem(), plan, {\n  maxFrames: 100,\n  winCondition: { type: 'reach_position', actorId: 'hero', position: { x: 15, y: 15 }, radius: 0.1 },\n})\nconst result = runner.run()   // ← the whole simulation happens here, synchronously\n\n// ── Step 6: read the result ──────────────────────────────────────────\nresult.success       // true — the hero reached (15,15)\nresult.frames        // number of frames simulated\nresult.worldFrames   // per-frame snapshots — what a client uses to draw the animation\n\nconst replayJSON = toReplayJSON(buildReplay(result, {\n  levelSlug: 'demo-01', levelVersion: 1, seed: 123,\n}))  // JSON you can store in a DB / send to a client for playback\n```\n\n### What the output looks like\n\n`result.worldFrames` is an array with one element per frame:\n\n```jsonc\n{\n  \"frame\": 3,\n  \"tracked\": {                       // snapshot of every entity that has position/health/collectible\n    \"hero\": { \"position\": { \"x\": 13, \"y\": 10 } }\n  },\n  \"events\": [                        // what happened during this frame\n    { \"type\": \"custom\", \"subtype\": \"move_progress\", \"frame\": 3,\n      \"payload\": { \"actorId\": \"hero\", \"position\": { \"x\": 13, \"y\": 10 } } }\n  ]\n}\n```\n\nA client just reads `worldFrames` in order to replay the whole match — **it\nnever needs to run the engine**. `buildReplay()` wraps the result with metadata\n(seed, level, version) into the standard `Replay` envelope ([replay.ts](src/replay.ts))\nfor storage.\n\n## Writing your own game rules — the 3 extension points\n\nThe engine contains no game-specific rules. Your game = the kernel + three\nthings you write yourself, plugged in via `registerHandler` / `addSystem` /\n`registerEvaluator`:\n\n### 1) A new action handler — give characters a new command\n\nImplement `MultiFrameActionHandler`: `validate` (is the command legal?),\n`start` (runs once when the action begins), `step` (runs every frame and\nreturns `running` / `done` / `failed`).\n\n```ts\nimport {\n  type MultiFrameActionHandler, type MultiFrameActionState, type MultiFrameActionStepResult,\n  type Action, type ActionContext, type ValidationResult,\n  getComponent, type HealthComponent,\n} from '@amas.nghia/todaycode-game-engine'\n\n// A HEAL command: restores 5 HP per frame, for `frames` frames\nexport class HealHandler\n  implements MultiFrameActionHandler<{ frames: number }, { remaining: number }> {\n  type = 'HEAL'\n\n  validate(ctx: ActionContext, a: Action<{ frames: number }>): ValidationResult {\n    const actor = ctx.world.entities[a.actorId]\n    if (!actor) return { valid: false, reason: 'Actor not found' }\n    if (!getComponent<HealthComponent>(actor, 'health')) return { valid: false, reason: 'No health' }\n    return { valid: true }\n  }\n\n  start(_ctx: ActionContext, a: Action<{ frames: number }>) {\n    return { localState: { remaining: a.payload!.frames } }\n  }\n\n  step(ctx: ActionContext, s: MultiFrameActionState<{ frames: number }, { remaining: number }>)\n    : MultiFrameActionStepResult<{ remaining: number }> {\n    const actor = ctx.world.entities[s.action.actorId]\n    const hp = getComponent<HealthComponent>(actor, 'health')!\n    hp.current = Math.min(hp.max, hp.current + 5)\n    const remaining = s.localState!.remaining - 1\n    if (remaining <= 0 || hp.current >= hp.max) return { status: 'done', events: [] }\n    return { status: 'running', events: [], localState: { remaining } }\n  }\n}\n\n// actions.registerHandler(new HealHandler())\n// in a plan:  yield { type: 'HEAL', actorId: 'hero', payload: { frames: 3 } }\n```\n\n### 2) A new system — add an automatic rule\n\nImplement `System` with a `tick(world)` method (runs every frame, before\nactions). Always iterate entities through `world.order` to stay deterministic:\n\n```ts\nimport { type System, type WorldState, type GameEvent, getComponent } from '@amas.nghia/todaycode-game-engine'\n\n// A spike trap: anything standing on (5,5) loses 1 HP per frame\nexport class SpikeTrapSystem implements System {\n  name = 'spike-trap'\n  tick(world: WorldState): GameEvent[] {\n    const events: GameEvent[] = []\n    for (const id of world.order) {\n      const e = world.entities[id]\n      const pos = getComponent<{ x: number; y: number }>(e, 'position')\n      const hp = getComponent<{ current: number; max: number }>(e, 'health')\n      if (pos && hp && pos.x === 5 && pos.y === 5 && hp.current > 0) {\n        hp.current -= 1\n        events.push({ type: 'custom', subtype: 'spike', frame: world.frame, payload: { id } })\n      }\n    }\n    return events\n  }\n}\n```\n\n### 3) A new objective — add a win/loss condition type\n\n```ts\nconst objectives = new ObjectiveSystem()\nobjectives.registerEvaluator('hero_alive', (world) => {\n  const hp = world.entities['hero']?.components['health'] as { current: number } | undefined\n  return !!hp && hp.current > 0\n})\n// use it like any built-in: winCondition: { type: 'hero_alive' }\n```\n\n## What's in the box\n\n**Components** ([components.ts](src/components.ts)) — plain TS interfaces; an entity declares only what it needs:\n`position` · `movement` · `motion` · `collision` · `blocking` · `health` ·\n`combat` · `collectible` · `inventory` · `team` · `cooldown` · `programmable` · `objectiveTarget`\n\n**Action handlers** ([actions/handlers.ts](src/actions/handlers.ts)):\n\n| `type` | payload | Required components | Behavior |\n|---|---|---|---|\n| `MOVE_DIRECTION` | `{ direction: 'up'\\|'down'\\|'left'\\|'right', distance? }` | `position`, `movement` | Walks in a direction; stopped by map bounds / blockers → `move_blocked` event, keeping whatever progress was made |\n| `ATTACK` | `{ targetId }` | actor: `combat`; target: `health` | Deals damage if the target is within `range`; emits `damage` and `death` events |\n| `PICK_UP` | `{ targetId }` | target: `collectible` | Picks up an item within radius 1.0 → adds it to `inventory` |\n| `WAIT` | `{ frames }` | — | Stands still for n frames |\n\n**Objectives** ([objectives.ts](src/objectives.ts)):\n`all` / `any` (nest via `conditions[]`) · `defeat_all` · `reach_position` ·\n`reach_entity` · `collect_count` · `metric_comparison`\n\n**Events** ([events.ts](src/events.ts)) — what clients use to drive animations:\n`move` · `move_blocked` · `attack` · `damage` · `death` · `pickup` · `score` ·\n`objective_completed` · `log` · `custom {subtype, payload}`\n\n**Other utilities**: `findNearest` / `isPathClear` / `isOccupied` ([selectors.ts](src/selectors.ts)) —\nworld queries for use inside handlers/systems; `distance` / `isSegmentClear` ([physics.ts](src/physics.ts));\nseeded `RNG` ([rng.ts](src/rng.ts)); fixed-point `vec2` ([vec2.ts](src/vec2.ts)) for games that need integer math.\n\n## Low-level API: the `Engine` contract\n\nBesides the World Kernel above, the package ships a minimal contract for games\nthat want to **manage their entire state themselves** instead of using ECS:\nthe [`Engine`](src/gamecore.interface.ts) interface — five methods,\n`init / applyOrders / tick / isOver / result` — where state, orders, and events\nare all opaque (`unknown`). The infrastructure can run any such engine without\nknowing its rules. [`FrameDriver`](src/frame-driver.ts) is the reference loop\nfor this layer: `new FrameDriver(engine, commanders).run(level, seed)` → `Replay`.\n\nUse this layer when your game isn't a \"character on a map\" (card games, number\ngames, turn-based duels). Both layers produce the same `Replay` envelope — the\n`Engine` layer fills `frames[]` (events only), while the World Kernel fills\n`worldFrames[]` (events + snapshots). If you ever want to wrap the World Kernel\nbehind an `Engine`, the bridge interfaces `WorldEngine` / `ActionEngine` in\n[gamecore.interface.ts](src/gamecore.interface.ts) exist for exactly that.\n\n## Determinism rules — mandatory for all code plugged into the engine\n\nThe engine's promise: **the same `(seed, level, plan)` produces the same\n`worldFrames[]` and outcome on every machine**. Replays must stay playable\nforever, and a client–server mismatch means either a bug or cheating. So inside\nevery handler/system/objective:\n\n- ❌ No I/O (network/fs/DB), no reading the clock (`Date.now`), no spawning processes.\n- ❌ No `Math.random()` — if you need randomness, use the seeded `RNG` derived from `world.seed`.\n- ❌ Never iterate `Object.keys(world.entities)` or a `Map` in arbitrary order —\n  **always iterate `world.order`** (every built-in system/selector already does).\n- ✅ Plain JS `number` coordinates are fine, as long as updates are pure and run\n  in a fixed order. Verify by running the simulation twice and deep-comparing\n  results (see [determinism.spec.ts](src/__tests__/determinism.spec.ts)).\n- Player/bot code never runs *inside* the engine — it runs in the app's sandbox\n  and only submits a plan (or orders).\n\n## Developing this repo\n\n```bash\npnpm install\npnpm test      # jest — src/__tests__/ ; integration.spec.ts is the live version of the examples above\npnpm build     # tsup → dist/ (ESM + CJS + .d.ts)\npnpm lint      # tsc -b --noEmit\n```\n\n**`dist/` is committed.** Release flow: edit `src` → `pnpm build` → commit\n`dist/` too → bump `version` in `package.json` → `npm publish` (goes to the\npublic npm registry; `publishConfig.access` is already `public`). Skipping the\nbuild means consumers get stale compiled code on the next version bump (this\nhas caused a real bug before).\nAlso keep the default `engineVersion` in `buildReplay`\n([world-runner.ts](src/world-runner.ts)) in sync with `package.json`.\n","readmeFilename":"README.md"}