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

indie-arcade-craft

v0.1.0

Published

Deterministic engine parts and a gate framework for small web arcade games that play like well-reviewed indie games: fixed-step loop, seeded streams, state hashing, input sampling, juice budgets, human-limit bots, and gates proven by mutants.

Readme

indie-arcade-craft

Deterministic engine parts and a gate framework for small web arcade games that should play like well-reviewed indie games, not like ad units.

Zero dependencies. ES modules. Node 18 or newer, and any browser that runs modules. No browser globals are touched at import time, so everything below runs the same in a test as in a page.

npm install indie-arcade-craft

The idea

A small game feels good when a few things are true, and each of them can be checked by a machine:

  • The game is deterministic. The same seed and the same inputs give the same state at every tick, at 30, 60 or 144 fps. Then you can prove it with a hash, record golden replays, and replay a prize draw on a server.
  • Input is judged where the finger was, not where the frame drew. Touching down never moves the thing you drag.
  • Effects have budgets. Hitstop, haptics and shake are capped, so the loud moments stay loud.
  • Difficulty is a table. The spawner, the fairness checks and the test bots all read one phase table.
  • Bots play with human limits. A bot that reacts in one frame proves nothing about a thumb.
  • Gates can fail. Every gate is tied to a deliberate bug (a mutant). If a gate stays green on its own bug, the proof run says so.

This package is those parts. It is not a renderer, an audio engine or a game framework: you bring the drawing.

Quickstart

A complete headless game built from the package is in examples/mini-catch. Run its gates, and prove they can fail:

npx arcade-gates node_modules/indie-arcade-craft/examples/mini-catch/gates.mjs --prove
PASS  D1       must   same seed, same run: identical state hash at tick 600
PASS  D2       must   frame-rate independent: 60, 144 and 30 fps pacing give the same hash
PASS  B1       must   the reward resolves exactly once (and does resolve, so this gate is not vacuous)
PASS  P1       should the difficulty curve is a sawtooth with a calm start and a rush peak
PASS  F1       must   a median human can reach the reward on at least 60% of seeds

5 passed, 0 failed, score 100/100, musts 4/4

mutant proofs: 5/5 caught

The five mutants are real bugs: unseeded randomness, spawning that depends on the frame count, a reward that resolves twice, a difficulty curve with no breathers, and a game too fast for a human to react to.

A game in six lines

import { createClock, createLoop, createBus, createStreams, rngNext, hashState } from 'indie-arcade-craft';

const clock = createClock();
const streams = createStreams(42);                       // sim and cosmetic randomness, kept apart
const S = { tick: 0, x: 0, sim: streams.sim };           // all gameplay state, plain data
const bus = createBus({ stamp: () => ({ tick: S.tick }) });

const loop = createLoop({
  clock,
  step: () => { S.tick++; S.x += rngNext(S.sim) - 0.5; }, // one tick of the simulation
  tick: () => S.tick,
  hash: () => hashState(S),
  render: (alpha) => { /* draw S, interpolating by alpha */ },
  isActive: () => true,
});

loop.start();                                             // requestAnimationFrame in a page
// in a test:  loop.setManual(true); loop.advance(3000, 1000 / 144); loop.hashAt(150)

Modules

Every module is also a subpath import, for example indie-arcade-craft/loop.

| Module | What it gives you | |---|---| | rng | mulberry32, rngNext over an Int32Array(1) so the stream position lives in hashed state, createStreams (sim and fx streams from one seed), mix32, noise1, hashSeed, pickWeighted, intRange | | hash | hashState (canonical walk: sorted keys, Float64 bits, _-prefixed keys skipped, throws on functions and cycles), buildRules (a frozen copy of your design numbers stamped with a cfgHash), canonical, fnv1a, deepFreeze | | clock | One clock. Timers fire in order, errors are contained, fromWall maps DOM timestamps onto game time. Nothing in the game uses setTimeout | | bus | A typed event ring (emit, on, entries, count). Gates assert on it; end screens rebuild their stats from it | | pool | A fixed-capacity pool that reports overflow | | loop | Fixed 60 Hz step on an accumulator. At most 5 steps per frame, a long gap pauses instead of catching up, a throwing step is rolled back and halts the loop, manual stepping for tests, and a ring of per-tick hashes | | input | createSampler (timestamped samples, interpolated at the step time), createRelativeDrag (no jump on touch-down, slop, tap detection, wall re-anchoring), attachPointer (pointer events, coalesced events, capture) | | juice | createTrauma (shake is trauma squared), createHitstop (rolling-window budget), createHapticBudget (per run and per second), createLadder (a pentatonic pitch ladder that resets on a miss) | | pacing | phaseAt, lerpField, checkSawtooth (contiguity, breather ratio, calm start, expected peak, longest stretch), readableTime | | bots | PROFILES (pro, median, casual), createHuman (reaction, hand speed, aim error, misreads, attention lapses, all seeded), and profilesHash to lock the table | | gates | defineGate, runGates, proveMutants | | qa | installQaHook (a production stub, or a dev surface with stepping, hash lookup and a live tainted flag) and canReport (tainted, manual and local runs never reach analytics) |

Gates and mutants

import { defineGate, proveMutants } from 'indie-arcade-craft';

export const gates = [
  defineGate({ id: 'B1', title: 'reward resolves once', severity: 'must',
    run: (ctx) => ({ pass: ctx.resolves === 1, detail: `${ctx.resolves} resolves` }) }),
];
export const mutants = [
  { id: 'double-resolve', breaks: ['B1'], apply: (ctx) => ({ ...ctx, resolves: 2 }) },
];
export const makeCtx = () => ({ resolves: 1 });

arcade-gates <module> [--only ID,ID] [--prove] [--json] exits 0 when every must gate passes and, with --prove, every mutant is caught. It exits 1 otherwise and 2 on bad input. Gates are plain async functions over a context you build, so they work with any runner, including a headless browser.

A gate should also not pass for the wrong reason. If a gate checks "resolves at most once" and nothing ever resolves, it is vacuously green. The example's B1 requires at least one resolve for exactly that reason.

Contracts

  • Hash stability. What hashState and buildRules return for the same input is a contract, because golden replays store it. A change is a major version. Strings are hashed over both bytes of each UTF-16 unit.
  • Two streams. Anything cosmetic must use the fx stream (or none), never the sim stream, and cosmetic state must stay out of the hashed state (or under a _ key). Otherwise a visual tweak moves your goldens.
  • One clock. Schedule through clock.after. Direct timers make manual stepping lie.
  • loop.advance(ms) moves a whole number of frames, so the time moved is ms rounded up to a multiple of the frame length.

Not in 0.1

No renderer, audio engine, UI kit or browser gate runner. Those depend on a game's art and shell, and are better left to the game. A headless-browser runner for gates (driving a page through its QA hook) is the likely next piece.

Where the numbers come from

Defaults such as the 60 Hz step, 5-step cap, 250 ms gap, the 14-ticks-in-120 hitstop budget, the haptic budget and the bot reaction times (0.20, 0.26 and 0.35 s) come from tuning a series of small games on low-end phones. Hand speed, aim error, lapses and misread rates are starting values: refit them from your own playtests, and update the pinned profile hash when you do.

The game-feel ideas have public sources: Steve Swink, Game Feel (2008); Jan Willem Nijman, "The Art of Screenshake" (2013); Dominic Kao, "The effects of juiciness in an action RPG", Entertainment Computing 34 (2020).

License

MIT. See LICENSE.