{"_id":"@duyquangnvx/state-machine","_rev":"3-1d47b9f3a9fbaee534438c6acd5311ab","name":"@duyquangnvx/state-machine","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@duyquangnvx/state-machine","version":"0.1.0","keywords":["state-machine","fsm","typescript","game","state"],"license":"ISC","_id":"@duyquangnvx/state-machine@0.1.0","maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"dist":{"shasum":"20137ec6fa5b2a45d95c9a3e3e51d8bd30cdad5a","tarball":"https://registry.npmjs.org/@duyquangnvx/state-machine/-/state-machine-0.1.0.tgz","fileCount":8,"integrity":"sha512-Uzq0ZE8ytmcq1hYavV4vvoz4X8rqQAyECVoPFcKdUILl0oqtC4nfPakDsHE4K/h381vNgxEvqx5adEDzigJhtw==","signatures":[{"sig":"MEUCIQDy7A+/SDvpywlq+d3e9FUQmcft0LIhbzryRrLaOKN0FgIgQXeIvC5WdavNn+8q+zwrFANpgpHY/cwEsJBVkr6tS80=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":49261},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","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":"c7b029d8020a0251c0d685de463b16099d2c1d94","scripts":{"demo":"npm run build:demo && node dist/demo/main.js","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsup","clean":"rimraf dist","demo:slot":"npm run build:demo && node dist/demo/slot-main.js","build:demo":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"},"_npmVersion":"10.9.4","description":"Generic, type-safe finite state machine library for TypeScript","directories":{},"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.5.1","ts-jest":"^29.2.0","typescript":"^5.7.0","@types/jest":"^29.5.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/state-machine_0.1.0_1770459095383_0.7488291817413613","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@duyquangnvx/state-machine","version":"0.1.1","keywords":["state-machine","fsm","typescript","game","state"],"license":"ISC","_id":"@duyquangnvx/state-machine@0.1.1","maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"dist":{"shasum":"b568e7226ed453bc361f65adada108a7b9eac289","tarball":"https://registry.npmjs.org/@duyquangnvx/state-machine/-/state-machine-0.1.1.tgz","fileCount":8,"integrity":"sha512-lzCt0Yesha8ft1Dk3GIOhwrDoukmY4I1+X5yaW1Iosrt5073jcVAtXoNbmBmfbwgpg2mVX4hpwhKhyjrGmOfLQ==","signatures":[{"sig":"MEUCIQCTFGPLK5/xMW1H7LFMR9l2qxiAbDfSJ8I1i3bBlUoCsQIgRr8Jm61psg3iv4gCOCU3n1NyHFiXZuwfAbOTBIaFfnQ=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":49298},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","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":"7fe85005c5732d7db319e01edd236a64b08348d6","scripts":{"demo":"npm run build:demo && node dist/demo/main.js","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsup","clean":"rimraf dist","demo:slot":"npm run build:demo && node dist/demo/slot-main.js","build:demo":"tsc","prepublishOnly":"npm run build"},"_npmUser":{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"},"_npmVersion":"10.9.4","description":"Generic, type-safe finite state machine library for TypeScript","directories":{},"_nodeVersion":"22.21.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tsup":"^8.5.1","ts-jest":"^29.2.0","typescript":"^5.7.0","@types/jest":"^29.5.0","@types/node":"^22.0.0"},"_npmOperationalInternal":{"tmp":"tmp/state-machine_0.1.1_1770485152618_0.08019968299364355","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@duyquangnvx/state-machine","version":"0.1.2","description":"Generic, type-safe finite state machine library for TypeScript","type":"module","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}}},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","publishConfig":{"access":"public"},"license":"ISC","keywords":["state-machine","fsm","typescript","game","state"],"scripts":{"build":"tsup","build:demo":"tsc","demo":"npm run build:demo && node dist/demo/main.js","demo:slot":"npm run build:demo && node dist/demo/slot-main.js","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","lint":"biome check .","lint:fix":"biome check --write .","format":"biome format --write .","clean":"rimraf dist","prepublishOnly":"npm run build"},"devDependencies":{"@biomejs/biome":"^2.3.13","@types/jest":"^29.5.0","@types/node":"^22.0.0","jest":"^29.7.0","ts-jest":"^29.2.0","tsup":"^8.5.1","typescript":"^5.7.0"},"_id":"@duyquangnvx/state-machine@0.1.2","gitHead":"cde473d700abad21c65697839a830ea0f7214a49","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-MEYz96DW/DYRHSzjmCQ+sdbaOGeApZ9e5K8t7SOGDCoytf7/5Vd1LSJ2D9KP44Q3tc6dH6LHhcdhfF99yfT/Zg==","shasum":"7ac690821a1397ff43ef28e50a3d05afe3f4787f","tarball":"https://registry.npmjs.org/@duyquangnvx/state-machine/-/state-machine-0.1.2.tgz","fileCount":8,"unpackedSize":50339,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDIUYEwNiTUwFpdNQERy6QmcPllgo+iYy/eEaU7LkoaIwIhAPQiwSrsU4dt2xin0Om45uhzTqCSF4H5plOpMemDIqxi"}]},"_npmUser":{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"},"directories":{},"maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/state-machine_0.1.2_1770824389399_0.5224561110779973"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-07T10:11:35.216Z","modified":"2026-02-11T15:39:49.656Z","0.1.0":"2026-02-07T10:11:35.518Z","0.1.1":"2026-02-07T17:25:52.765Z","0.1.2":"2026-02-11T15:39:49.537Z"},"license":"ISC","keywords":["state-machine","fsm","typescript","game","state"],"description":"Generic, type-safe finite state machine library for TypeScript","maintainers":[{"name":"duyquangnvx","email":"duyquangnvx@gmail.com"}],"readme":"# state-machine\n\nA generic, type-safe finite state machine library for TypeScript with synchronous lifecycle hooks, transition guards, hierarchical states, event history, and a tick-based update loop.\n\n## Features\n\n- **Fully generic** — `StateMachine<TContext, TStateId>` works with any context and state ID types\n- **Synchronous lifecycle** — `onEnter` / `onExit` / `onUpdate` are all sync for deterministic state at every moment\n- **Transition guards** — `canTransitionTo(target, ctx)` lets each state control allowed transitions\n- **Tick-based updates** — `onUpdate(ctx, dt)` runs every frame/tick; return a state ID to auto-transition\n- **Hierarchical states** — `HierarchicalState` embeds a nested state machine inside a parent state\n- **Event system** — Subscribe to state changes with `on(listener)`, unsubscribe with the returned function\n- **Bounded history** — Configurable history buffer for debugging and replay\n\n## Quick start\n\n```bash\nnpm install\nnpm run build\n```\n\n### Define states\n\n```ts\nimport { BaseState } from \"state-machine\";\n\ntype MyStateId = \"idle\" | \"loading\" | \"ready\";\n\ninterface MyContext {\n  data: string | null;\n}\n\nclass IdleState extends BaseState<MyContext, MyStateId> {\n  readonly id = \"idle\" as const;\n\n  override onEnter(ctx: MyContext, prevState: MyStateId | null): void {\n    console.log(\"Entered idle\");\n  }\n\n  override onUpdate(ctx: MyContext, dt: number): MyStateId | undefined {\n    return \"loading\"; // auto-transition on next tick\n  }\n}\n\nclass LoadingState extends BaseState<MyContext, MyStateId> {\n  readonly id = \"loading\" as const;\n\n  override onUpdate(): MyStateId | undefined {\n    return \"ready\";\n  }\n}\n\nclass ReadyState extends BaseState<MyContext, MyStateId> {\n  readonly id = \"ready\" as const;\n}\n```\n\n### Create and run the machine\n\n```ts\nimport { StateMachine } from \"state-machine\";\n\nconst sm = new StateMachine<MyContext, MyStateId>({\n  states: [new IdleState(), new LoadingState(), new ReadyState()],\n  initialState: \"idle\",\n  context: { data: null },\n  historySize: 50, // optional, defaults to 100\n});\n\nsm.start();            // enters \"idle\", calls onEnter\nsm.update(0.016);      // calls onUpdate, may auto-transition\nsm.transitionTo(\"ready\"); // explicit transition\nsm.stop();             // exits current state, calls onExit\n```\n\n### Transition guards\n\nOverride `canTransitionTo` to block transitions conditionally:\n\n```ts\nclass IdleState extends BaseState<MyContext, MyStateId> {\n  readonly id = \"idle\" as const;\n\n  override canTransitionTo(target: MyStateId, ctx: MyContext): boolean {\n    return ctx.data !== null; // only allow transitions when data is loaded\n  }\n}\n```\n\nBlocked transitions throw `TransitionDeniedError`.\n\n### Events and history\n\n```ts\nconst unsub = sm.on((event) => {\n  console.log(`${event.from} -> ${event.to} at ${event.timestamp}`);\n});\n\nsm.getHistory(); // ReadonlyArray<StateChangeEvent<MyStateId>>\nunsub();         // stop listening\n```\n\n### Async work as a state\n\nThe state machine is fully synchronous — `onEnter`, `onExit`, and `onUpdate` all return `void`. This ensures the machine is always in exactly one definite state at any moment, which is critical for game loops and real-time systems.\n\nTo handle async operations (API calls, loading, etc.), **model the async work as its own state** that polls for completion:\n\n```ts\ntype MyStateId = \"IDLE\" | \"LOADING\" | \"READY\";\n\ninterface MyContext {\n  loading: boolean;\n  data: string | null;\n  fetchData: () => Promise<string>;\n}\n\nclass LoadingState extends BaseState<MyContext, MyStateId> {\n  readonly id = \"LOADING\" as const;\n\n  override onEnter(ctx: MyContext): void {\n    ctx.loading = true;\n    ctx.fetchData().then((data) => {\n      ctx.data = data;\n      ctx.loading = false;\n    });\n  }\n\n  override onUpdate(ctx: MyContext): MyStateId | undefined {\n    if (!ctx.loading) return \"READY\";\n    return undefined;\n  }\n}\n```\n\nThis pattern keeps the state machine synchronous while still supporting async operations. The state machine ticks on each frame/update, and the loading state simply polls until the async work completes.\n\n## API\n\n### `StateMachine<TContext, TStateId>`\n\n| Method / Property | Description |\n|---|---|\n| `start(): void` | Enter the initial state. Idempotent. |\n| `stop(): void` | Exit the current state and shut down. |\n| `transitionTo(stateId): void` | Transition to a specific state (checks guard). |\n| `update(dt): void` | Tick the current state; auto-transitions if `onUpdate` returns a state ID. |\n| `on(listener)` | Subscribe to state changes. Returns unsubscribe function. |\n| `getHistory()` | Get the bounded transition history. |\n| `currentStateId` | The active state's ID. Throws if not started. |\n| `isStarted` | Whether the machine is running. |\n| `context` | The shared mutable context object. |\n\n### `BaseState<TContext, TStateId>` (lifecycle hooks)\n\n| Hook | Signature | Notes |\n|---|---|---|\n| `canTransitionTo` | `(target, ctx) => boolean` | Sync guard. Default: `true`. |\n| `onEnter` | `(ctx, prevState) => void` | Called when entering. `prevState` is `null` on `start()`. |\n| `onUpdate` | `(ctx, dt) => TStateId \\| undefined` | Return a state ID to auto-transition. |\n| `onExit` | `(ctx, nextState) => void` | Called when leaving. `nextState` is `null` on `stop()`. |\n\n### `HierarchicalState<TContext, TParentId, TChildId>`\n\nA composite state that runs a nested `StateMachine`. Override `createChildConfig(ctx)` to define the child machine. The child starts/stops automatically with the parent state.\n\n### Errors\n\n| Error | Thrown when |\n|---|---|\n| `StateNotFoundError` | Transitioning to an unknown state ID. |\n| `MachineNotStartedError` | Calling `transitionTo`, `update`, or `currentStateId` before `start()`. |\n| `TransitionDeniedError` | `canTransitionTo` returns `false`. |\n\n## Demos\n\n### Tower Defense\n\nThree interlocking state machines (tower, enemy, wave) simulating a tower defense game loop.\n\n```\nTower:  BUILDING -> IDLE -> TARGETING -> ATTACKING -> IDLE\nEnemy:  SPAWNING -> MOVING -> ATTACKING -> DYING -> DEAD\nWave:   PREPARING -> WAVE_ACTIVE -> WAVE_COMPLETE -> ... -> GAME_OVER\n```\n\n```bash\nnpm run build && npm run demo\n```\n\n### Slot Machine\n\nDemonstrates the \"async work as a state\" pattern — API calls (bet deduction, payout crediting) are modeled as dedicated states that poll for completion.\n\n```\nIDLE -> DEDUCTING_BET -> SPINNING -> STOPPING -> EVALUATING -> CREDITING_WIN -> IDLE\n                                                     |\n                                                     +--> IDLE (no win)\n```\n\n```bash\nnpm run build && npm run demo:slot\n```\n\n## Testing\n\n```bash\nnpm test\n```\n\n67 tests covering the core library (construction, start/stop, transitions, guards, updates, events, history, hierarchical states) and both demos.\n\n## Project structure\n\n```\nsrc/\n  lib/                    # Core library\n    State.ts              # BaseState abstract class\n    StateMachine.ts       # StateMachine engine\n    HierarchicalState.ts  # Composite state with nested machine\n    StateEvent.ts         # Event emitter with bounded history\n    interfaces.ts         # IState, IStateMachine, config types\n    errors.ts             # StateNotFoundError, MachineNotStartedError, TransitionDeniedError\n    index.ts              # Public exports\n  demo/\n    tower/                # Tower defense FSM (tower states)\n    enemy/                # Tower defense FSM (enemy states)\n    wave/                 # Tower defense FSM (wave management)\n    slot/                 # Slot machine FSM\n    main.ts               # Tower defense entry point\n    slot-main.ts          # Slot machine entry point\n    GameLoop.ts           # Tick-based game loop utility\ntests/\n  lib/                    # Core library tests\n  demo/                   # Demo-specific tests\n```\n\n## License\n\nISC\n","readmeFilename":"README.md"}