npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@capylabs-io/todaycode-base-game-engine

v0.3.0

Published

Game-agnostic, deterministic engine base (Engine contract, FrameDriver, seeded RNG, integer vec2, replay envelope). Games implement this contract in their own repos.

Readme

todaycode-base-game-engine

A TypeScript library that simulates a game frame by frame, as pure logic. No rendering, no I/O, fully deterministic — the same input always produces the same result, whether it runs in a browser, on a server, or in CI.

You give it:

  • a world — a list of entities (hero, enemies, coins, walls)
  • a plan — the sequence of actions a player wants to perform (move, attack, pick up)
  • the rules — what each action does, and what happens automatically every frame
  • a goal — the win/loss condition

It gives you back:

  • whether the plan succeeded or failed
  • a frame-by-frame recording of everything that happened, so a client can play it back as an animation without running the engine itself
         INPUT                          SIMULATION                      OUTPUT
┌────────────────────────┐   ┌───────────────────────────┐   ┌─────────────────────────┐
│ WorldState (entities)  │   │  WorldRunner.run()        │   │ WorldRunnerResult       │
│ Plan (list of Actions) │   │  each frame:              │   │  ├ success: boolean     │
│ Systems (auto rules)   │ → │   1. run systems          │ → │  ├ frames: number       │
│ Action handlers        │   │   2. advance the plan     │   │  ├ events[]             │
│ Objectives (win/loss)  │   │   3. snapshot the world   │   │  └ worldFrames[]        │
│ seed                   │   │   4. check objectives     │   │        │ buildReplay()  │
└────────────────────────┘   │   5. frame++              │   │        ▼                │
                             └───────────────────────────┘   │  Replay (JSON)          │
                                                             └─────────────────────────┘

Architecture: ECS + Actions/Plans + Objectives

The engine is built on the Entity–Component–System (ECS) pattern, with two extra layers on top: Actions/Plans ("what a character does") and Objectives ("what counts as winning").

New to ECS? A two-minute primer

The classic OOP way to model a game is one class per thing:

class Hero  { hp; x; y; move() {...} attack() {...} }
class Wall  { x; y; }
class Coin  { x; y; pickUp() {...} }

This falls apart quickly. What about a chest that can be attacked but can't move? A ghost that moves but can't be hit? A coin that expires? You end up with deep inheritance trees, duplicated logic, and classes full of methods that don't apply. ECS solves this by splitting a "game object" into three separate ideas:

| | What it holds | What it does NOT hold | |---|---|---| | 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. | | 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. | | 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. |

The mental shift: what something is = which components it carries; what happens = which systems run. Three entities, three different component mixes:

// A hero: can be somewhere, can walk, takes damage
{ id: 'hero', kind: 'hero', components: {
    position: { x: 10, y: 10 },
    movement: { speed: 1, stepDistance: 5 },
    health:   { current: 20, max: 20 },
} }

// A wall: has a place and blocks movement — nothing else
{ id: 'wall-1', kind: 'wall', components: {
    position: { x: 15, y: 10 },
    collision: { radius: 1 },
    blocking:  { blocksMovement: true },
} }

// A coin: has a place and can be picked up — it can't move or die
{ id: 'coin-1', kind: 'coin', components: {
    position:    { x: 12, y: 10 },
    collectible: { kind: 'gold' },
} }

A system then selects by component, never by class. MovementSystem is literally just:

every frame:
  for each entity (in fixed order):
    does it have position + motion + movement?   ← "motion" = a destination someone gave it
      yes → move it `speed` units toward motion.target
            arrived? remove the motion component (the move is finished)
      no  → skip it

It moves heroes, enemies, and arrows with the same 20 lines of code — anything carrying those three components. Want a new kind of thing? Don't write a class — compose components: an attackable chest is position + health (no movement, so it can never walk); a patrolling ghost is position + movement (no health and no collision, so it can't be hit and walks through walls).

Why this engine uses ECS specifically:

  1. Composition over inheritance — every game level is just a list of entity literals like the ones above; new gameplay = new component mixes, no new classes.
  2. Free serialization — the whole world is plain JSON data, so snapshotting it every frame (worldFrames) and replaying matches later costs nothing.
  3. Determinism — behavior lives in systems that iterate one fixed order array, so two machines always process entities in the same sequence.

The six concepts and where they live

On top of raw ECS, the engine adds Actions/Plans and Objectives. Six concepts, one file each:

| Concept | What it is | File | |---|---|---| | 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, components.ts | | 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 | | 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, multi-frame-action.ts | | 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 | | 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, systems/movement.ts | | Objective | Win/loss conditions declared as plain JSON objects: { type: 'reach_position', ... }, composable with all / any. | objectives.ts |

WorldRunner ties everything together. Every frame, in this exact order:

  1. Run every System — e.g. MovementSystem moves each entity speed units toward its target.
  2. Advance the Plan one step — the current action gets a step() call; if it finished, the next action starts.
  3. Snapshot the world (position/health/collected of every entity) plus this frame's events → worldFrames[].
  4. Check winCondition / lossCondition — if met, stop and return success.
  5. If the plan ran out of actions (or failed) without winning → stop with success: false. If maxFrames is reached → timeout, success: false.

The single most important design decision — Actions and Systems are decoupled: a MOVE_DIRECTION action does not move the entity itself. Its handler only attaches a motion component (the destination). MovementSystem is what actually moves the entity a little each frame. When the entity arrives, the system removes motion, the handler sees that and reports done, and the plan moves on to the next action. This is how one command like "walk 12 units" plays out smoothly over 12 frames while the plan code stays simple and sequential.

Install

The package is published publicly on npm — no registry config or token needed:

pnpm add @capylabs-io/todaycode-base-game-engine

Getting started — a complete example

The task: a hero stands at (10,10) and must reach (15,15). The player submits the plan "move right, then move up". Copy this block and it runs as-is:

import {
  createWorld, addEntity,                          // step 1
  SystemRunner, MovementSystem,                    // step 2
  MultiFrameActionRunner, MoveDirectionHandler,    // step 3
  YieldPlanRunner, type PlanIterator,              // step 4
  ObjectiveSystem, WorldRunner,                    // step 5
  buildReplay, toReplayJSON,                       // step 6
} from '@capylabs-io/todaycode-base-game-engine'

// ── Step 1: create a world and drop entities into it ─────────────────
const world = createWorld(/* seed */ 123)
world.bounds = { minX: 0, maxX: 100, minY: 0, maxY: 100 }

addEntity(world, {
  id: 'hero',
  kind: 'hero',
  components: {
    position: { x: 10, y: 10 },
    movement: { speed: 1, stepDistance: 5 },  // moves 1 unit/frame; each move command covers 5 units by default
    collision: { radius: 0.5 },
  },
})

// ── Step 2: register systems (rules that run automatically) ──────────
const systems = new SystemRunner()
systems.addSystem(new MovementSystem())

// ── Step 3: register action handlers (which commands exist) ──────────
const actions = new MultiFrameActionRunner()
actions.registerHandler(new MoveDirectionHandler())

// ── Step 4: the plan — the player's/bot's script ─────────────────────
function* heroPlan(): PlanIterator {
  yield { type: 'MOVE_DIRECTION', actorId: 'hero', payload: { direction: 'right' } }
  yield { type: 'MOVE_DIRECTION', actorId: 'hero', payload: { direction: 'up' } }
}
const plan = new YieldPlanRunner(actions, heroPlan())

// ── Step 5: win condition + run ──────────────────────────────────────
const runner = new WorldRunner(world, systems, actions, new ObjectiveSystem(), plan, {
  maxFrames: 100,
  winCondition: { type: 'reach_position', actorId: 'hero', position: { x: 15, y: 15 }, radius: 0.1 },
})
const result = runner.run()   // ← the whole simulation happens here, synchronously

// ── Step 6: read the result ──────────────────────────────────────────
result.success       // true — the hero reached (15,15)
result.frames        // number of frames simulated
result.worldFrames   // per-frame snapshots — what a client uses to draw the animation

const replayJSON = toReplayJSON(buildReplay(result, {
  levelSlug: 'demo-01', levelVersion: 1, seed: 123,
}))  // JSON you can store in a DB / send to a client for playback

What the output looks like

result.worldFrames is an array with one element per frame:

{
  "frame": 3,
  "tracked": {                       // snapshot of every entity that has position/health/collectible
    "hero": { "position": { "x": 13, "y": 10 } }
  },
  "events": [                        // what happened during this frame
    { "type": "custom", "subtype": "move_progress", "frame": 3,
      "payload": { "actorId": "hero", "position": { "x": 13, "y": 10 } } }
  ]
}

A client just reads worldFrames in order to replay the whole match — it never needs to run the engine. buildReplay() wraps the result with metadata (seed, level, version) into the standard Replay envelope (replay.ts) for storage.

Writing your own game rules — the 3 extension points

The engine contains no game-specific rules. Your game = the kernel + three things you write yourself, plugged in via registerHandler / addSystem / registerEvaluator:

1) A new action handler — give characters a new command

Implement MultiFrameActionHandler: validate (is the command legal?), start (runs once when the action begins), step (runs every frame and returns running / done / failed).

import {
  type MultiFrameActionHandler, type MultiFrameActionState, type MultiFrameActionStepResult,
  type Action, type ActionContext, type ValidationResult,
  getComponent, type HealthComponent,
} from '@capylabs-io/todaycode-base-game-engine'

// A HEAL command: restores 5 HP per frame, for `frames` frames
export class HealHandler
  implements MultiFrameActionHandler<{ frames: number }, { remaining: number }> {
  type = 'HEAL'

  validate(ctx: ActionContext, a: Action<{ frames: number }>): ValidationResult {
    const actor = ctx.world.entities[a.actorId]
    if (!actor) return { valid: false, reason: 'Actor not found' }
    if (!getComponent<HealthComponent>(actor, 'health')) return { valid: false, reason: 'No health' }
    return { valid: true }
  }

  start(_ctx: ActionContext, a: Action<{ frames: number }>) {
    return { localState: { remaining: a.payload!.frames } }
  }

  step(ctx: ActionContext, s: MultiFrameActionState<{ frames: number }, { remaining: number }>)
    : MultiFrameActionStepResult<{ remaining: number }> {
    const actor = ctx.world.entities[s.action.actorId]
    const hp = getComponent<HealthComponent>(actor, 'health')!
    hp.current = Math.min(hp.max, hp.current + 5)
    const remaining = s.localState!.remaining - 1
    if (remaining <= 0 || hp.current >= hp.max) return { status: 'done', events: [] }
    return { status: 'running', events: [], localState: { remaining } }
  }
}

// actions.registerHandler(new HealHandler())
// in a plan:  yield { type: 'HEAL', actorId: 'hero', payload: { frames: 3 } }

2) A new system — add an automatic rule

Implement System with a tick(world) method (runs every frame, before actions). Always iterate entities through world.order to stay deterministic:

import { type System, type WorldState, type GameEvent, getComponent } from '@capylabs-io/todaycode-base-game-engine'

// A spike trap: anything standing on (5,5) loses 1 HP per frame
export class SpikeTrapSystem implements System {
  name = 'spike-trap'
  tick(world: WorldState): GameEvent[] {
    const events: GameEvent[] = []
    for (const id of world.order) {
      const e = world.entities[id]
      const pos = getComponent<{ x: number; y: number }>(e, 'position')
      const hp = getComponent<{ current: number; max: number }>(e, 'health')
      if (pos && hp && pos.x === 5 && pos.y === 5 && hp.current > 0) {
        hp.current -= 1
        events.push({ type: 'custom', subtype: 'spike', frame: world.frame, payload: { id } })
      }
    }
    return events
  }
}

3) A new objective — add a win/loss condition type

const objectives = new ObjectiveSystem()
objectives.registerEvaluator('hero_alive', (world) => {
  const hp = world.entities['hero']?.components['health'] as { current: number } | undefined
  return !!hp && hp.current > 0
})
// use it like any built-in: winCondition: { type: 'hero_alive' }

What's in the box

Components (components.ts) — plain TS interfaces; an entity declares only what it needs: position · movement · motion · collision · blocking · health · combat · collectible · inventory · team · cooldown · programmable · objectiveTarget

Action handlers (actions/handlers.ts):

| type | payload | Required components | Behavior | |---|---|---|---| | 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 | | ATTACK | { targetId } | actor: combat; target: health | Deals damage if the target is within range; emits damage and death events | | PICK_UP | { targetId } | target: collectible | Picks up an item within radius 1.0 → adds it to inventory | | WAIT | { frames } | — | Stands still for n frames |

Objectives (objectives.ts): all / any (nest via conditions[]) · defeat_all · reach_position · reach_entity · collect_count · metric_comparison

Events (events.ts) — what clients use to drive animations: move · move_blocked · attack · damage · death · pickup · score · objective_completed · log · custom {subtype, payload}

Other utilities: findNearest / isPathClear / isOccupied (selectors.ts) — world queries for use inside handlers/systems; distance / isSegmentClear (physics.ts); seeded RNG (rng.ts); fixed-point vec2 (vec2.ts) for games that need integer math.

Low-level API: the Engine contract

Besides the World Kernel above, the package ships a minimal contract for games that want to manage their entire state themselves instead of using ECS: the Engine interface — five methods, init / applyOrders / tick / isOver / result — where state, orders, and events are all opaque (unknown). The infrastructure can run any such engine without knowing its rules. FrameDriver is the reference loop for this layer: new FrameDriver(engine, commanders).run(level, seed) → Replay.

Use this layer when your game isn't a "character on a map" (card games, number games, turn-based duels). Both layers produce the same Replay envelope — the Engine layer fills frames[] (events only), while the World Kernel fills worldFrames[] (events + snapshots). If you ever want to wrap the World Kernel behind an Engine, the bridge interfaces WorldEngine / ActionEngine in gamecore.interface.ts exist for exactly that.

Determinism rules — mandatory for all code plugged into the engine

The engine's promise: the same (seed, level, plan) produces the same worldFrames[] and outcome on every machine. Replays must stay playable forever, and a client–server mismatch means either a bug or cheating. So inside every handler/system/objective:

  • ❌ No I/O (network/fs/DB), no reading the clock (Date.now), no spawning processes.
  • ❌ No Math.random() — if you need randomness, use the seeded RNG derived from world.seed.
  • ❌ Never iterate Object.keys(world.entities) or a Map in arbitrary order — always iterate world.order (every built-in system/selector already does).
  • ✅ Plain JS number coordinates are fine, as long as updates are pure and run in a fixed order. Verify by running the simulation twice and deep-comparing results (see determinism.spec.ts).
  • Player/bot code never runs inside the engine — it runs in the app's sandbox and only submits a plan (or orders).

Developing this repo

pnpm install
pnpm test      # jest — src/__tests__/ ; integration.spec.ts is the live version of the examples above
pnpm build     # tsup → dist/ (ESM + CJS + .d.ts)
pnpm lint      # tsc -b --noEmit

dist/ is committed. Release flow: edit src → pnpm build → commit dist/ too → bump version in package.json → npm publish (goes to the public npm registry; publishConfig.access is already public). Skipping the build means consumers get stale compiled code on the next version bump (this has caused a real bug before). Also keep the default engineVersion in buildReplay (world-runner.ts) in sync with package.json.