aicraft-engine
v0.24.0
Published
Ultra-minimalist procedural rendering + algorithmic cosmetics + IAP bridge for indie and procedural games. Zero runtime dependencies.
Maintainers
Readme
aicraft-engine
Ultra-minimalist procedural rendering + algorithmic cosmetics + IAP bridge for indie and procedural games. Zero runtime dependencies. Canvas2D-only. Strict TypeScript. Vitest.
Part of the Create Games with AI community.
What this is
A small TypeScript library of procedural rendering primitives, algorithmic cosmetics, and an IAP bridge, generalised for reuse across many games. Three principles drive every design decision:
- Procedural-first rendering — characters, worlds, and effects are drawn from vector primitives in code. Inspired by Sokpop Collective's ~100-title catalog and their public fake-3D demo. CC0/PD pixel art is also a first-class citizen: the
src/spritespipeline plays Aseprite-JSON sprite sheets (one.json+ one.png= a whole game's cast), and the two can be mixed per-entity in one scene. - Algorithmic cosmetics — skins, themes, and content variants are parameter presets, not art files. The algorithm IS the art. This gives a full IAP cosmetics surface with zero asset pipeline.
- Determinism everywhere — the same inputs always produce the same outputs. No
Math.randomin deterministic code (renderers may use it only when the result never feeds back into the simulation). Saves, replays, and recorded footage all reproduce exactly.
What it provides (when complete)
| Pillar | Modules | Status |
|---|---|---|
| 1. Primitives | outlineRect, shade, color guards, device-pixel camera snapping, motion probe, surface ripple (waveDisplacement, generateWaveLine), hit-stop (freeze-frame game-feel), additive glow (drawGlow), parallax scroll (parallaxOffset, drawTiledParallax) | Shipped |
| 1. RNG | Seeded mulberry32, distribution helpers, stateless typed visual-address folding | Shipped |
| 1. Particles | Deterministic spawn / advance / cull, region/cone sampling, continuous emitters, heterogeneous physics | Shipped |
| 1. Animation | Skeletal rig, IK (limb/CCD/FABRIK), procedural locomotion, squash/stretch, Verlet springs, foot-lock, oscillators, procedural spider enemy, and seeded visual-only humanoid body plan | Shipped |
| 1. Character body plans | Typed body-plan registry plus deterministic humanoid config, displacement-driven visual state, signed-gravity poses, and Canvas2D renderer. 0.5.0 ships the idle pose polished; grounded-locomotion (H3), airborne + landing (H4), and ceiling-gravity support are deferred to 0.6.0 | Shipped |
| 1. Sprite animation | Aseprite-JSON-superset pipeline: one .json + one .png defines a whole game's cast. Defensive never-throws parser, grid + explicit-rect frames, named animations (frameTags, with authored loop/duration/durations extensions for one-shot clips and per-clip pacing), multi-character characters[], deterministic per-frame-duration player (loop/reverse/pingpong), facing mirror + silhouette tint, and a character-agnostic anim-state deriver (idle/walk/climb/airborne) shared by player and enemies | Shipped |
| 1. Collision | AABB overlap, per-axis move-and-resolve, tile-grid collision (one-way platforms), moving-gap platforms, capped supercover line of sight | Shipped |
| 1. Camera | Follow camera (lerp, clamp, snap-to-target) | Shipped |
| 1. Input | Edge accumulator, keyboard adapter, touch-button adapter, multi-touch button set, gamepad adapter (Standard Gamepad, axial deadzone), OR-merge, multi-device merge (mergeEdges, mergePolledEdgeMaps), frozen-map extenders (extendKeyboardMap, extendGamepadMap) | Shipped |
| 1. Game loop | Fixed-step accumulator, defensive RAF adapter, spiral-of-death guard, visibilitychange pause | Shipped |
| 1. Game state | Pure dt-driven FSM reducer (reduceGameState, createGameState, isLegalTransition), declarative adjacency table (DEFAULT_GAME_STATE_ADJACENCY); menu/playing/paused/gameover/levelComplete; menu navigation (createMenuNav, advanceMenuNav) — wrapped selection, confirm edge, open-grace window | Shipped |
| 1. Audio | WebAudio synthesized SFX adapter (oscillator tones, filtered noise, sustained noise loops with stop handles, de-correlated noise retriggers), defensive lazy-unlock, mute/volume | Shipped |
| 1. Music | Procedural step-sequencer — pure theory, seeded patterns, boundary-correct fixed-step advanceSequencer, autonomous createSequencer, and external-event createNoteFirePlayer adapters | Shipped |
| 1. Save | Defensive localStorage/memory backends, JSON load/write helpers (SaveStorage, loadSave, writeSave) | Shipped |
| 1. Replay | Replay record/playback — fluent createReplayRecorder (mutable renderer-output buffer), pure playReplay re-sim driver (calls consumer's step), deterministic 32-bit replayHash fingerprint via canonicalize + fnv1a (share-codes / CI verification); zero new determinism work — reuses stepPlatformer kernel | Shipped |
| 1. Blend | Pose-interpolation primitives (Pose2D, blendPose, blendPoses) — independent of animation pillar | Shipped |
| 1. Easing & tween | Penner easing curves (linear, easeOutCubic, easeOutBack, easeOutElastic, easeOutBounce, …), generic powOut, inversion helpers (easeIn, easeInOut), fixed-step tween driver (createTweenState, advanceTween) | Shipped |
| 1. Bitmap text | Asset-less 5×7 pixel font (full printable ASCII, MIT-sourced glyph data), pure measureText, drawText / drawTextOutlined, font registry (createFont, addGlyph, DEFAULT_FONT) | Shipped |
| 1. Platformer kernel | Composable character controller + ability pipeline, signed gravity with gravity-relative support/carry, stable physical contact identity, deterministic tick; Celeste FallingBlock recipe (collectFallingBlocks, advanceFallingBlocks) over LDtk trigger entities with authored fields | Shipped |
| 1. Enemy archetypes | Enemy compile/step pipeline + behavior registry (spinny, turret, spider, telegraphed charger), projectiles, renderer, custom-handler dispatch via EnemyBehaviorHandler | Shipped |
| 2. Palette | OKLCH substitution, harmonic generation, WCAG AA contrast repair | Shipped |
| 2. Cosmetics | Versioned manifest, seeded variant generation, multi-slot ownership | Shipped |
| 2. Level schema | Versioned platformer levels, migration and validation, standalone tile queries, and unified compileLevel output combining captured tile collision with entity geometry | Shipped |
| 2. Level generation | Procedural level generator — route-graph generation, rhythm/pacing planning, motif-based geometry, physics-constrained realization, candidate search with quality scoring, targeted repair, diversity/novelty tracking, and difficulty calibration. Entry point: generateLevel(seed, config) → GeneratedLevel | Shipped |
| 2. Level authoring | Native LDtk .ldtk parse/edit/render/write pipeline (src/ldtk/) — defensive parse, LevelData translation, tile-layer + rule-driven IntGrid rendering, pure editing ops, and byte-faithful write-back. runLdtkAutoLayer re-runs LDtk's own auto-layer rules, proven exact against LDtk's output by a 20,046-tile oracle (src/tests/ldtk-rules-oracle.test.ts); createLdtkLevelSurfaceCache bakes each level once at native resolution for seam-free rendering at fractional camera zooms. Additive to src/terrain-art/, not a replacement: LDtk serves tile-asset games, terrain-art serves zero-asset games | Shipped |
| 2. Terrain rendering | Prepared connectivity and exposure, normalized materials, seamless connected tiles, deterministic exposed-edge treatments, and role-aware rectangle rendering (src/terrain/); plus src/terrain-art/ procedural dual-grid authoring — versioned source-art projects, dual-grid resolution, rule-atlas composition, manual paint + variant/transition/occurrence editing, imported-tileset bridging, and runtime gutter-aware rendering. Editor subpath: aicraft-engine/terrain-art/editor | Shipped |
| 2. Terrain pieces | A terrain fragment rendered as a finished object rather than a sliced rectangle (src/terrain-art/piece.ts, piece-render.ts) — rect→grid rasterizer, bonded/free neighbour sampling (resolveTerrainPiece), bake-once with fingerprint-invalidated caching (createTerrainPieceCache), and two shrinking readings: drawClippedTerrainPiece slides a piece into a wall, drawMaskedTerrainPiece erodes it in place with an optional cap strip riding the cut. The rendering sibling of collision/moving-gap — that module decides which solids exist, this one decides how the newly-exposed ends are drawn | Shipped |
| 3. Level themes | Two-stage theme/level preparation, ordered render passes, resolved runtime entities, and Ruins/Cavern/Mechanical/Outdoor examples (src/platformer/level-theme.ts); renders authored src/terrain-art/ output via the theme.terrainArt layer override | Shipped |
| 4. Semantic level art | Themed exits, coin/gem/key silhouettes, warning-form traps, edit-only markers, and stateless reduced-motion atmosphere recipes (src/platformer/themed-entity-renderer.ts, atmosphere-recipes.ts) | Shipped |
| 5. Visual authoring | Art/Collision editor previews, consumer-supplied theme selection and fallback, structural re-preparation, and deterministic thumbnails (src/platformer/theme-preview.ts) | Shipped |
| 2. Collectibles | First-class 'collectible' EntityKind, pure-progression-ops CollectibleSave mirroring cosmetics/ownership.ts (collect/hasCollected), pure derivePickups (deterministic AABB collision — kernel unaware, zero replay impact); per-level scoping is consumer-owned via Record<levelId, CollectibleSave> | Shipped |
| 2. Editor core | Headless level-editor operations: serializable ops + snapshot undo/redo, transactions, multi-select, grid + edge snapping, in-memory clipboard, sandbox playtest boundary, prefab catalog. applyOp, undo, beginTransaction, selectInRect, snapToEdges, enterPlaytest, DEFAULT_CATALOG | Shipped |
| 2. Level testing | Platformer level verification — jump-arc trajectory sampling, static reachability BFS, platformer simulation adapter, three bot policies (cautious/direct/collector), win conditions, and tri-state verifyLevel/verifyCompiledLevel (proven-beatable / proven-unreachable / inconclusive) | Shipped |
| Cross-cutting simulation testing | Generic deterministic simulation-test core — fixed-tick orchestration, policy execution, trace recording/playback, and honest verifyScenario / playSimulationTrace / simulationTraceHash. Zero imports from platformer/level/editor; leveltest is the platformer adapter | Shipped |
| 3. IAP | Bridge adapter interface, entitlement store, pure progression ops, memory + localStorage dev adapters (native platform SDKs deferred to Phase 5) | Shipped |
| RPG module | Zero-asset top-down monster-tamer vertical slice (src/rpg/) — serializable RNG streams, tick-counted grid movement, never-throw content compilation with path diagnostics, seeded world generation + BFS verification, procedural species (five body-plan grammars), pure 1v1 battle kernel with exact legal-command exposure and versioned fixed RNG draw budgets, XP/level/move learning, dialogue graphs with terminal-effect ordering, save/restore bound to content fingerprints, themed Canvas2D renderers + synthesized audio cues, and a createRpgController facade over a discriminated activity union. Starter game: games/rpg-starter/ | Shipped |
| 4. Fake-3D | Billboarding, isometric tiles, orthographic cube, heightmap | Phase 4 |
| 5. Platform adapters | Platform SDKs (Poki, StoreKit, Play Billing) | Phase 5 (on-demand) |
Install (submodule)
The library is consumed as a git submodule to preserve consumer-side zero-runtime-deps invariants and keep source greppable for AI agents.
# From your game repo
git submodule add <aicraft-engine-url> src/lib/aicraft-engine
git commit -m "Add aicraft-engine submodule"Then import from a relative path:
import { outlineRect, shade } from './lib/aicraft-engine/src/primitives';
import { mulberry32 } from './lib/aicraft-engine/src/rng';Vite + moduleResolution: "bundler" handle relative imports transparently — no build plumbing changes in the consumer. See docs/integration.md for full details.
Install (npm)
The library is also published as compiled ESM + .d.ts (no source ships). The emitted entry is dist/index.js with full type declarations; consumers resolve it through package.json exports:
npm install aicraft-engineimport { outlineRect, mulberry32, spawn } from 'aicraft-engine';sideEffects: false is set, so bundlers tree-shake aggressively. Only dist/ + README.md + package.json ship — src/, tests/, showcase/, docs/, and benchmarks/ are excluded. The tarball carries no runtime dependencies.
Works in plain Node ESM — no bundler required. The
build:diststep runsscripts/fix-esm-specifiers.mjs, which rewrites the dist's internal extensionless imports to explicit.jsspecifiers, so the package imports directly from plain Node (node --input-type=module) as well as through Vite/esbuild/webpack. This is verified every publish byrelease:smoke's Node-ESM + NodeNext-typecheck + Vite consumer gates against the packed tarball.
Usage
import { outlineRect, shade } from './aicraft-engine/src/primitives';
import { mulberry32 } from './aicraft-engine/src/rng';
import { spawn, step } from './aicraft-engine/src/particles';
// Deterministic PRNG — same seed, same sequence, forever
const rng = mulberry32(12345);
// Flat-fill rect with 1px dark outline (matches GDD §11.3 art rules)
outlineRect(ctx, 100, 100, 32, 32, '#FE5701');
// Deterministic particle burst — 8 particles, evenly distributed
let particles = spawn(player.x, player.y, {
count: 8,
speed: 3,
life: 24,
size: 4,
});
particles = step(particles, 1); // advance + cullRPG starter
A complete, runnable monster-tamer built only from the public barrel lives in
games/rpg-starter/ — zero asset files, one
seed driving species/world/encounters, and headless full-loop determinism
tests driving the same game object the browser runs:
import {
compileRpgContent, createStarterContentBundle, createRpgState, createRpgController,
} from 'aicraft-engine';
const compiled = compileRpgContent(createStarterContentBundle(2026));
if (!compiled.ok) throw new Error(compiled.diagnostics[0].message);
const game = createRpgController(compiled.content);
let state = createRpgState(compiled.content, 2026);
state = game.step(state, { direction: 'right', confirm: false, cancel: false, menu: false, battleCommand: null }, 1 / 60).state;Author games as data: swap the bundle's types/moves/species/dialogues/maps (yours or generated), compile, and the same facade runs them.
Recipes
The package ships a recipes/ directory — compiled,
unit-tested copy-in wiring modules for the glue every game rebuilds by
hand (audio unlock, the reduced-motion-gated loop boot, particle systems,
LDtk draw/entity-art/hot-reload wiring, sprite-sheet boot, room-slide
letterboxing). Copy the files you need into src/recipes/ and import them
locally; they are typechecked against this package's source on every commit,
so they cannot drift from the shipped API. See
recipes/README.md for the full catalog and the
governance ladder (brief sketch → recipe → first-class export).
Showcase
The showcase/ directory is a standalone Vite app that demos the library end-to-end. It is not shipped to consumers -- it is a reference and visual-validation tool that lives inside the repo. Run it with:
npm run showcase:dev # dev server (prints a localhost URL)
npm run showcase:build # production build to showcase/dist/Nine sections, each an independent canvas:
| Section | What it shows |
|---|---|
| Hero | Seeded slime-knight character (rng, animation, IK, locomotion, jump) |
| Lava pool | Gerstner wave surface + heterogeneous particle emitters |
| Playground | Playable platformer (input, collision, camera, hit-stop, game-loop) |
| Parallax | 4-layer IMP underworld background with AI-generated raster art (drawTiledParallax) |
| Spider | Multi-legged IK gait anchored to sampled terrain |
| Tile room | Generated/topology rooms: seamless terrain art, semantic entities, compiled tile collision |
| LDtk editor | Supplied .ldtk level end-to-end: parse, auto-tile rules, room transitions, camera brain |
| Sprite demo | Sprite-sheet pipeline (parseSpriteSheet → compileSpriteSheet → drawSprite) |
| Camera brain | Per-room virtual cameras, deadzone bands, cover-fit zoom (createCameraBrain) |
The parallax section is the first to consume raster art, validating that the library's drawTile callback is asset-agnostic. See showcase/README.md for full details including the art regeneration pipeline.
Conventions
The library follows strict conventions; see docs/conventions.md for the full list. Highlights:
- Zero runtime dependencies. Adding one is a breaking change.
- Strict TypeScript.
noUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitch,isolatedModules. - File naming: lowercase-kebab (
outline-rect.ts, notOutlineRect.ts). - Defensive adapters: lazy host-API resolution, swallow errors, in-memory test fallback, never-throw public APIs.
- Pure progression/ownership ops: immutable returns, JSON-clone, never mutate input.
- No
Math.randomin deterministic code. Usesrc/rnginstead. prefers-reduced-motionrespected via a cached-at-module-load probe.- Extensive JSDoc + inline rationale. Terse code feels foreign here.
Development
npm install # devDependencies only (typescript, vite, vitest, canvas for headless benchmark rendering)
npm test # vitest run
npm run test:watch # vitest watch mode
npm run build # tsc --noEmit (typecheck gate)
npm run visual:sheets # deterministic level-art review gallery
npm run bench:level-visual # same-host fallback/production benchmark gate
npm run check:terrain-tree-shaking # leaf renderer bundle-isolation gateThe npm publish build is separate from the typecheck gate:
npm run build:dist # tsc -p tsconfig.build.json → emits dist/ (.js + .d.ts)
npm run check:level-visual-size # Phase 0-relative dist ceiling
npm publish # prepack hook auto-runs build:dist, so the tarball is always freshAgent team
This repository ships with a 6-agent team configured in opencode.json. The team is research-first rather than feature-driven, because the hard problem in a library is finding the right abstraction across many future games, not making a single feature work.
| Agent | Role | Mode |
|---|---|---|
| @team | Primary orchestrator. Coordinates research → API design → architect critique → prototype → benchmark → decide → implement → document. | primary |
| @researcher | Surveys prior art (Sokpop catalog, JS13k, demoscene, papers). Writes docs/research/<technique>.md. Read-only on src/. | subagent |
| @api-designer | Proposes 2-3 TypeScript API approaches per technique with trade-offs. Maintains docs/api-surface.md. | subagent |
| @architect | Read-only adversarial critic of API/architecture changes. Catches determinism violations, layering leaks, convention drift. | subagent |
| @coder | TDD implementer for deterministic helpers; clean implementation for renderers; prototype spikes in src/_prototype/. | subagent |
| @benchmarker | Headless node-canvas rendering. Sample galleries, variant sheets, stress tests. Writes to benchmarks/. Vision-capable. | subagent |
To start a session with the team agent as primary, run opencode in this directory. Prompts live in prompts/. Always-loaded instructions (tech stack, project structure) live in .opencode/instructions/.
Workflow shape (different from feature-driven teams)
For each non-trivial technique:
- Research →
@researcherwritesdocs/research/<technique>.md - API design →
@api-designerproposes 2-3 approaches indocs/design/<technique>-proposal.md - Architect critique →
@architectreturns APPROVED or NEEDS REVISION - Prototype (conditional) →
@coderspikes 1-2 approaches insrc/_prototype/ - Benchmark →
@benchmarkerrenders sample PNGs for comparison - Decide →
@teampicks the winner, writesdocs/design/<technique>-decision.md - Implement →
@coderdoes proper TDD implementation - Document →
@api-designerfinalizesdocs/api-surface.md;@benchmarkeradds the gallery
Multiple techniques can be in flight at different stages simultaneously. Bug fixes and trivial utilities skip the full loop.
See prompts/team.md for the full orchestrator instructions.
License
MIT — see LICENSE.
