Reusable components and bootstrap for vibe-coded games and apps. Self-contained TypeScript modules used across multiple projects in this workspace (VibeRacer, Flatline, VibeCity, VibeGear2, FrackingAsteroids). Each module is:
- zero-dependency at runtime (apart from
zodand, for./server,@upstash/redis+node:crypto), - framework-agnostic on the default entry (no React, no Next.js, no DOM-only globals beyond what the module's domain needs),
- pure TypeScript that compiles with
tscalone, - documented at the top of its file.
The kit has no build step. Consume it by either copy-pasting a .ts file into your project, or by adding this repo as a file: dependency:
{
"dependencies": {
"@randroids-dojo/vibekit": "file:../VibeKit"
}
}Releases are automated via release-please. Every push to main opens or updates a Release PR titled chore(main): release X.Y.Z with a CHANGELOG entry and a version bump. Merging that PR creates the git tag vX.Y.Z.
Use Conventional Commits in PR titles / squash-merge messages so release-please picks the right semver level:
fix:orchore:→ patch (0.1.0 → 0.1.1)feat:→ minor pre-1.0; once we cut 1.0,feat:→ minor and breaking → majorfeat!:orBREAKING CHANGE:in the body → major
Pre-1.0 the kit treats every feat: as a patch via bump-patch-for-minor-pre-major: true, so daily authoring stays at 0.1.x until we cut 1.0.0 deliberately.
Float-where-you-tap touch joystick state. createJoystick / beginJoystick / moveJoystick / endJoystick mutate state from pointer events; readJoystick returns a [-1, 1] deflection vector clamped at JOYSTICK_RADIUS. The consumer applies JOYSTICK_DEADZONE itself so different consumers can pick different thresholds.
Generic EditorHistory<T> undo / redo stack. createHistory / pushHistory / undoHistory / redoHistory / replacePresent / resetHistory plus canUndo / canRedo flags. Reference-equal pushes are no-ops so an idempotent setter does not pollute the past stack with duplicates. The past stack caps at EDITOR_HISTORY_MAX_PAST = 100.
Pure particle simulation for celebration overlays. spawnConfettiBatch (seeded RNG, count, palette, burst origin), stepConfetti (one physics frame), confettiAlpha (per-particle fade), isBatchExpired, makeRng (re-exported here too for convenience). Coordinates are normalized 0-1 viewport space; the renderer multiplies by canvas pixel size at draw time.
Tiny seeded Mulberry32 PRNG. makeRng(seed) returns a deterministic float-in-[0, 1) generator; range(rng, lo, hi), pick(rng, items), and gauss(rng) are convenience helpers. Use these in any system that needs reproducible randomness (replays, deterministic spawns, ghost integrity).
TAU, clamp, lerp, inverseLerp, remap, smoothstep, wrapAngle. Pure helpers re-implemented in many game projects; pulling them here so the same definition is shared and tested once.
Defensive localStorage helpers validated with zod schemas. Every function is SSR-safe (returns sensibly when window is missing), JSON-safe (catches parse failure), schema-safe (rejects values that fail the supplied zod schema), and quota-safe (writes return false on quota errors instead of throwing).
readStorage<T>(key, schema)returnsT | null.writeStorage(key, value)returnsfalseon SSR / quota / cyclic value, fires a same-tab change event on success.removeStorage(key)clears and fires the change event.updateStorage<T>(key, schema, prev => next)is read-modify-write convenience.listenStorage(key, onChange)subscribes to BOTH cross-tabstorageevents AND same-tabgamekit:storageCustomEvents (the standardstorageevent does not fire in the window that wrote the change). Returns an unsubscribe function.notifyStorageChange(key)dispatches the same-tab event without writing, useful when state is reconstructed from another source.
Zod is the only runtime dependency in the kit; consumers pass a z.ZodSchema<T> directly. There is no React hook layer; project-side hooks compose readStorage + listenStorage for the hydrate-after-mount pattern.
Engine-agnostic ordered basketball scoring gate. createRimScoringSensorState() creates per-ball state, and updateRimScoringSensor(state, params) returns 'swish', 'score', 'rim', or null. A make only counts after a descending ball enters the above-rim shaft and then crosses the lower gate inside the rim cylinder, which prevents net-from-below false positives.
Pure tap / upward-swipe classifier for canvas and touch games. classifySwipeGesture(start, end, bounds, config?) returns tap, up-swipe, or none plus dragPowerNorm and lateralAngle; dragPowerNorm(start, current, bounds, config?) gives the same power value for live previews.
Server-only helpers that import @upstash/redis and node:crypto. Import path is the ./server subpath, never the root, so a stray client import errors loudly:
import { getKv, readKv, writeKv, signToken, verifyToken, incrementWithExpiry } from '@randroid/game-kit/server'getKv() returns an @upstash/redis Redis client when either KV_REST_API_URL / KV_REST_API_TOKEN or UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN are populated, or null otherwise. Returning null lets routes degrade gracefully in local dev or preview deploys without a KV binding.
readKv<T>(kv, key, schema) issues GET, validates the parsed value against a zod schema, and returns T | null. Upstash auto-parses JSON on reads. writeKv(kv, key, value, { ttlSec? }) issues SET with optional TTL; returns false on thrown access. removeKv(kv, key) issues DEL swallowing failures.
HMAC-SHA256 signed tokens for race-start / replay / admin flows. The encoding is intentionally simpler than JWT (no algorithm header, no expiration parsing): a <base64url(json)>.<base64url(hmac)> string. verifyToken<T>(token, secret, schema?) returns the parsed payload on success or null on any failure (signature mismatch, malformed token, JSON parse failure, schema rejection). Comparison is constant-time via timingSafeEqual.
Fixed-window rate-limit primitive. incrementWithExpiry(kv, key, windowSec) issues INCR then sets EXPIRE on the first hit. Returns the post-increment count. Caller picks the policy (compare to a limit, return 429 when over). Returns null only when both INCR and EXPIRE throw; pick a fail-open or fail-closed policy explicitly when this happens.
A module qualifies for this kit when it satisfies all of:
- zero project imports (
grep "from '@/" <file>returns empty); - no React, Vue, Next.js, or any framework imports;
- pure TypeScript that compiles with
tsc --noEmitalone; - tests that pin behavior with Vitest;
- documented public API at the top of the file.
If a module is almost portable but reaches into a project type or a settings store, refactor it in its origin project to accept that as a parameter or interface, then move it here.
pnpm install
pnpm type-check
pnpm test