Skip to content
 
 

Latest commit

 

History

16 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Claude\Zombies

A round-based 3D zombie-survival FPS that runs entirely in the browser — with zero binary assets. No models, no textures, no audio files. Every polygon is authored in code, every texture is drawn to a <canvas> at boot, and every sound is synthesised from oscillators and noise.

CI Play the live demo License: MIT

Play it in your browser — no install, no download, ~200 KB gzipped.

Hold a facility called Blackline Facility against escalating rounds of the dead. Earn Scrip by killing them, spend it on doors, wall guns, ammo, buff stations, the Cache Roulette weapon box, and the Anvil Station upgrade rig. Round 13 is where zombie speed reaches 4.4 m/s — faster than you can walk. Most runs end within a few rounds of it.

Original work inspired by the genre as a whole. No third-party publisher, studio, map, character or weapon names appear anywhere in this repository; that is enforced by a test, not a promise (see IP hygiene).


Contents


Architecture

A fixed-timestep simulation core drives nine systems that communicate exclusively through a typed event bus and a service registry. The renderer sits at the end of the chain, not in the middle of it — which is what lets the entire game run headlessly in Node with Three.js absent from the module graph.

The inline SVG this diagram was originally drawn as is stripped by GitHub's Markdown sanitiser, so Mermaid is the only form that actually renders here.

flowchart TD
    M["src/main.ts → src/game/bootstrap.ts"] --> L["src/core/loop.ts<br/>fixed 60 Hz accumulator"]
    L --> B["src/core/bus.ts<br/>EventBus&lt;GameEventMap&gt;"]
    L --> R["src/core/registry.ts<br/>ServiceRegistry"]
    B --> S
    R --> S
    subgraph S["nine systems"]
        LV[level] --- PL[player] --- WP[weapons]
        AI[ai] --- DR[director] --- EC[economy]
        AU[audio] --- UI[ui] --- EN[engine]
    end
    EN --> RN["src/engine/renderer.ts<br/>three.js 0.169 · WebGL2"]
    RN --> CV["canvas#game"]
    T["tests/sim · tests/unit"] --> H["src/game/headless.ts<br/>createHeadlessGame()"]
    H -. "no WebGL · no WebAudio · no DOM" .-> S
    H -. "engine · audio · ui bypassed" .-x RN
Loading

Why the shape matters. Systems never import each other. They read the frozen interfaces in src/types/**, resolve peers through the ServiceRegistry, and communicate through a single typed GameEventMap. That is what makes createHeadlessGame() possible: the same gameplay code that runs in a browser runs in Node with no DOM, no WebGL and no WebAudio, which is how 15 full 30-round games are simulated in the balance suite.


Quick start

git clone https://github.com/justinw916-sketch/claude-zombies.git
cd claude-zombies
npm ci
npm run dev          # http://localhost:5173

Production build and local preview:

npm run build        # tsc --noEmit && vite build  →  dist/
npm run preview      # http://localhost:4173

Everything at once (the full gate ladder):

npm run verify       # typecheck → lint → unit → sim → build

Node 20 or newer (engines.node: ">=20"). CI runs Node 20; local development is on Node 24.


Controls

Every action below is remappable in Settings → Controls. Values are KeyboardEvent.code tokens, plus the synthetic tokens Mouse0Mouse4. Defaults live in src/ui/settings.ts (DEFAULT_KEY_BINDINGS).

Action Default Alternate
Move Forward W
Move Back S
Move Left A
Move Right D
Jump Space
Crouch Left Ctrl C
Sprint Left Shift
Fire Mouse 0 (left)
Aim (ADS) Mouse 1 (right)
Reload R
Interact / repair barrier F
Melee V
Frag Grenade G
Pulse Grenade H
Weapon Slot 1 1
Weapon Slot 2 2
Swap Weapon Q
Pause Esc
Dev Console ` (backquote)
Toggle FPS overlay F3
Scoreboard Tab

Movement: walk 4.2 m/s, sprint 6.6 m/s for 5 s with a 3 s recovery, crouch 2.1 m/s. Health 100 base, regen begins 4 s after the last hit at 25 hp/s. Downed is a 12 s bleed-out — Second Wind revives you automatically once, then is consumed.

Interact is edge-triggered (one purchase per press), but held interact repairs the nearest damaged barrier within 2.5 m. An intact barrier is not a repair candidate, so a held key cannot farm Scrip.


Balance

Round curve, rounds 1–30

Frozen math from src/systems/director/curves.tsnot tunable, exported as standalone pure functions so tests can import them without constructing the director. Health is displayed rounded; the curve itself is health(r-1) × 1.10 anchored at health(9) = 950. Maximum concurrent zombies is 24 at all times.

Round Zombies Health Speed m/s Spawn interval s Ether Hounds Rendmen
1 6 150 1.2 1.94
2 8 250 1.2 1.88
3 10 350 1.2 1.82
4 12 450 1.2 1.76
5 14 550 2.2 1.70
6 16 650 2.2 1.64 9
7 18 750 2.2 1.58
8 20 850 2.2 1.52
9 22 950 3.4 1.46
10 26 1045 3.4 1.40
11 30 1150 3.4 1.34 11
12 33 1264 3.4 1.28
13 37 1391 4.4 1.22
14 42 1530 4.4 1.16
15 47 1683 4.4 1.10 1
16 53 1851 4.4 1.04 14 1
17 59 2036 4.4 0.98 1
18 66 2240 4.4 0.92 1
19 74 2464 4.4 0.86 1
20 83 2710 4.4 0.80 1
21 93 2982 4.4 0.74 16 1
22 104 3280 4.4 0.68 1
23 117 3608 4.4 0.62 1
24 131 3968 4.4 0.56 1
25 147 4365 4.4 0.50 2
26 164 4802 4.4 0.44 19 2
27 184 5282 4.4 0.38 2
28 206 5810 4.4 0.35 2
29 231 6391 4.4 0.35 2
30 259 7030 4.4 0.35 2

Two steps decide the shape of a run. Round 5 is where zombie speed first outpaces early-round DPS growth. Round 13 is the wall: 4.4 m/s is faster than the 4.2 m/s player walk, and slower only than a 5-second sprint. Every measured death in the balance matrix landed at rounds 10–18.

Per-zombie speed carries a seeded ±10 % jitter applied once, by the AI system.

Economy

Canonical source: src/systems/economy/balance.ts. The tables below are a snapshot of that file for readers; if the two ever disagree, the file wins.

Doors — priced by destination zone. Only these four are purchasable; Z5 loops back to Z1 for free, so the map is a closed kiting ring.

Door Cost
Z1 Spawn Bay → Z2 Machine Shop 1 000
Z2 Machine Shop → Z3 Cold Storage 1 600
Z3 Cold Storage → Z4 Reactor Corridor 2 300
Z4 Reactor Corridor → Z5 Anvil Chamber 3 300

Buff stations — max 4 held at once, all lost on death. The power switch is in Z4 and is free; every station is inert until it is thrown.

Buff Cost Zone Effect
Steady Aim 3 500 Z4 Recoil ×0.65, head hit-zone ×1.15
Ironhide 4 000 Z3 Max health 100 → 250
Second Wind 7 000 Z5 One automatic self-revive, consumed on use
Quickhands 7 500 Z5 Reload duration ×0.5
Overclock 13 000 Z4 Fire rate ×1.33

Fixtures

Fixture Cost Notes
Power switch (Z4) 0 Gates all five buff stations and the Anvil Station
Cache Roulette 950 / pull 6 s spin, 10 s grab window, 12 % relocation event, 3 anchors
Anvil Station (Z5) 8 200 Damage ×2.2, magazine ×1.5, penetration +1, generated name suffix

WeaponswallBuy 0 means the weapon has no wall anchor and is reachable only through the Cache Roulette. ammoBuy 0 means ammo is unbuyable.

Weapon Display name Wall buy Ammo refill
sidearm Mark I Sidearm — (starting weapon) 1 500
smg Vector SMG 1 400 3 200
ar Lattice AR 1 800 3 900
pumpShotgun Breaker 12 2 200 3 900
lmg Drumfire LMG 2 700 4 600
boltSniper Longline Bolt Cache only 4 600
autoShotgun Cyclone Auto Cache only 4 300
burstRifle Tercet Burst Cache only 4 100
arcLance Arc Lance Cache only

Scrip awards — spec values from the design contract, restored after a tuning pass cut them; do not re-cut them.

Event Scrip
Starting balance 500
Non-lethal hit 10
Kill 60
Headshot kill 130
Melee kill 130
Barrier plank repaired 10
Full round survived +50

Measured difficulty

From the balance simulation matrix — 30 rounds × 3 skill levels × 5 seeds, 15 full headless games, bot-driven through player.setInput() only. These are the shipped figures, matching the final matrix in QA_REPORT.md §LOOP 2 CONTINUED:

Bot skill (headshot intent) Mean death round Ironhide affordable Anvil Station affordable
low — 35 % 10.2 round 6 (4 of 5 seeds) round 9 (3 of 5 seeds)
mid — 55 % 12.8 round 6 (all 5 seeds) round 9 (all 5 seeds)
high — 75 % 14.6 round 6 (all 5 seeds) round 9 (all 5 seeds)

Skill separates monotonically: +2.6 rounds from low to mid, +1.8 from mid to high. The mid bot opens the Z5 door in 5 of 5 seeds and buys the Anvil Station in 1 of 5 (round 14); high skill buys it in 2 of 5. Time-to-clear rises from round 10 onward. Every completed round across the matrix spawned exactly zombiesInRound(r) zombies — hard equality, not a tolerance.


Module map

Directory Owns One line
src/types/ frozen contracts Every interface in the game. Imports nothing at runtime and never imports three.js — which is what keeps the sim headless.
src/core/ primitives Seeded Rng (mulberry32 + FNV-1a), typed EventBus, ServiceRegistry, fixed-timestep loop, math helpers, object pools, the one sanctioned log wrapper.
src/engine/ rendering Three.js boundary: code-generated texture atlas and materials, instancing manager, fixed-capacity GPU pools, frustum/LOD culling, lighting + fitted shadow frustum, single-pass post FX, F3 debug overlay.
src/level/ Blackline Facility Five-zone room graph, procedural geometry and props, AABB BVH collision, 192×116 nav grid with door/barrier gating, 24 barriers × 6 planks, 20 interactable anchors.
src/systems/player/ the player Capsule sweep movement, stamina and sprint, mantle, crouch, health / downed / bleed-out, camera FX and recoil kick, hardware input capture and pointer lock.
src/systems/weapons/ gunplay Nine data-driven weapons, hitscan ballistics with penetration, spread and recoil, pooled grenades, melee, procedural viewmodels, buff and Anvil multipliers.
src/systems/ai/ the dead Struct-of-arrays arena (24 live / 32 slots), Dijkstra flow field with amortised rebuild, 7-state FSM, zero-allocation hit-zone raycast, Ether Hound and Rendman variants.
src/systems/director/ pacing The frozen round curves, round phase machine, biased spawn scheduling, Scrip awards, soft-lock watchdogs.
src/systems/economy/ Scrip BALANCE (the single tuning surface), ledger, power gate, five buffs, four doors, Cache Roulette, Anvil Station, wall buys, allocation-free interaction raycast with per-anchor cooldown.
src/audio/ sound Raw WebAudio only. Oscillators, noise buffers, biquads and feedback-delay reverb; positional voices with zone occlusion; adaptive music bed; tab-blur resume.
src/ui/ screens DOM-over-canvas HUD, main menu, settings (the whole accessibility surface), pause, round transition, game over, localStorage high scores, dev console. No React.
src/game/ assembly Game context and reseedable RNG tree, bootstrap ordering, the state machine that joins systems together, the rAF loop, and createHeadlessGame().
tests/unit/ correctness Vitest. Curve math, hit-zone resolution, purchase atomicity, buff stacking, the weapon state machine, flow-field correctness, RNG determinism, the Cache Roulette draw distribution, the IP scan.
tests/sim/ fairness Vitest. The 30 × 3 × 5 headless matrix plus a determinism fingerprint, driven by a scripted bot in tests/sim/harness/.
tests/e2e/ the real thing Playwright. Boot, pointer lock, shooting, round advance, pause, every accessibility control, game over, restart cleanliness, AudioContext resume, a 3-minute memory soak.
tests/perf/ budgets Playwright. Draw calls, CPU frame time, hot-loop allocations, shadow-frustum fit, bundle size.

Tuning guide

Change the economy

One file: src/systems/economy/balance.ts. Every door price, buff cost, Cache Roulette cost, Anvil cost, Scrip award and per-weapon price lives in the exported BALANCE object, and every purchase path reads it. Nothing else under src/systems/economy/** is permitted to hard-code a cost. Each field carries a comment saying what raising or lowering it feels like in play.

Then re-measure:

npm run test:sim

Two cautions from the measured record, both real:

  • Ammo price is the sharpest lever in the table, and the game sits on a cliff. Moving the AR refill from 3 600 to 3 900 — 8 % — moved the mid-skill mean death round by 4.4 rounds, because a run either sustains fire long enough to bank the Anvil Station or it does not, and the Anvil then decides the next six rounds.
  • Per-seed spread at n=5 is ±3 rounds, so the standard error on any mean from this matrix is roughly ±1 round. Two configurations whose means differ by less than that are not distinguishable. Do not tune against a single run.

Add a weapon

Two edits, in this order:

  1. src/systems/weapons/defs.ts — add the WeaponDef: damage, RPM, magazine, reload timings, spread, the deterministic per-shot recoil table, penetration, cacheWeight, and a ViewmodelSpec (a procedural geometry recipe — there is no model file to author).
  2. src/systems/economy/balance.ts — add the matching entry to BALANCE.weaponCosts (wallBuy / ammoBuy).

defs.ts projects the contract's wallBuyCost / ammoBuyCost fields out of BALANCE.weaponCosts, so pricing is written exactly once. A unit test pins that projection for every weapon, which means forgetting step 2 fails a test rather than silently shipping a free gun. Add the WeaponId to the union in src/types/weapons.ts and the compiler will find every remaining site.

The new weapon also needs an acquisition route — a wall anchor in src/level/anchors.ts with a non-zero wallBuy, a positive cacheWeight, or both. tests/unit/cache-draw-distribution.test.ts asserts that the union of the two routes covers every WeaponId, so a weapon with neither fails the suite instead of shipping as dead content.

Change round pacing

src/systems/director/curves.ts — deliberately outside the balance table. Zombie count, health, speed, spawn interval, hound rounds and Rendman counts are frozen exact math with standalone exported functions, and unit tests assert them value-by-value. They are excluded from BALANCE on purpose: the economy is meant to be tuned freely, and the difficulty curve is not. Changing anything here means updating tests/unit/director-curves.test.ts in the same commit, and every function must stay total (non-finite and out-of-range inputs clamp through sanitizeRound).

Run each suite

Command Gate What it proves
npm run typecheck G1 tsc --noEmit, strict, zero any in src/systems/**
npm run lint G2 ESLint 9 flat config, --max-warnings 0. Also enforces the Math.random ban and the console ban.
npm run test:unit G3 Vitest + v8 coverage; ≥85 % lines on src/systems/**
npm run test:e2e G4 Playwright, real Chromium, zero console errors
npm run test:sim G5 The 30 × 3 × 5 headless balance matrix
npm run test:perf G6 Draw calls, CPU frame time, allocations, shadow fit, bundle
npm run build G7 tsc --noEmit && vite build into dist/
npm run verify typecheck + lint + unit + sim + build
npm run format Prettier over the claude-zombies

Playwright browsers are not installed by npm ci. Before the first test:e2e or test:perf run:

npx playwright install --with-deps chromium

Accessibility

Every option below is on the Settings screen, persisted to localStorage under claude-zombies:settings:v1, and covered by end-to-end tests. Corrupt or out-of-range stored values fall back to the default field rather than propagating into gameplay.

Option Range / values Notes
Remappable keys all 21 actions Every action in the Controls table above, primary and alternate slots. Press-to-bind; Esc cancels.
Field of view 70–110° Clamped on load. Applied before the camera initialises, so a persisted value is honoured from the first frame.
Mouse sensitivity slider Radians per pixel of raw movement.
Invert Y on / off
Reduce motion on / off Kills camera shake and head bob.
Colourblind-safe hitmarkers off · protanopia · deuteranopia · tritanopia Recolours hitmarkers and damage cues for safe contrast.
Subtitles on / off Text for audio cues — the game is fully playable with sound off.
Master volume 0–1
SFX volume 0–1
Music volume 0–1
FPS overlay on / off Also bound to F3.

Zero assets

There is not a single binary media file in this repository, or in dist/. No .glb, .fbx, .png, .jpg, .jpeg, .mp3, .wav, .ogg, .webp, .hdr or .ktx2 — anywhere. That is asserted by a standing test, not by convention.

  • All geometry is authored in code. The facility, its props, the zombies and every weapon viewmodel are built from parametric recipes at boot. ViewmodelSpec is a geometry recipe, not a file path.
  • All textures are generated to <canvas> at boot and packed into a single atlas, which is why the entire 92.8 × 54.8 m facility renders in 38 draw calls.
  • All audio is synthesised through raw WebAudio — oscillators, noise buffers, biquad filters and a feedback-delay reverb. No Howler, no audio files, no samples.

The measured result: the whole of dist/ is about 200 KB gzipped against a 900 KB budget — roughly 22 % of budget used and 700 KB of headroom. (Last measured build, the root-path Cloudflare build serving live: 200.52 KB across 6 files — 121.17 KB Three.js, 73.92 KB game, 3.32 KB visual chunks, 1.44 KB HTML, 0.66 KB favicon.) Sizes here are KiB at zlib level 6, matching what the budget test prints; Vite's own build log reports a slightly larger figure because it counts in 1000-byte kB and omits public/ files it merely copies. The Three.js chunk alone is ~121 KB gz, so the entire game — level, nine weapons, AI, economy, UI, audio synthesis — costs under 80 KB gzipped.

Bundle size is a standing regression test (tests/perf/bundle-budget.spec.ts), measured at zlib level 6 because that is what a CDN actually serves.

The social card is generated too

Paste the link into Slack, iMessage, Discord or Facebook and it unfurls with a full-bleed 1200 × 630 preview card — which is, of course, not a file either.

og:image has to be a raster format (Facebook and iMessage will not render an SVG), and a committed .png would violate the zero-asset rule and fail two tests. So the card is synthesised at the edge on request by a Cloudflare Pages Function, functions/og.png.ts: satori lays the card out from a flexbox tree, resvg rasterises it to PNG, and both run as WASM inside the Worker. Typefaces come from the Google Fonts API on a cold isolate and are then held in module scope. The response is cached for a day in the browser and a week at the edge, so the render itself is rare.

Nothing about this lands in the repository or in dist/functions/ is deployed alongside the static bundle, never bundled into it, so the asset ban and the 900 KB budget are both untouched. The same discipline extends to the icon: public/favicon.svg is hand-authored vector (SVG is a text format), and /favicon.ico is a 301 redirect to it served by functions/favicon.ico.ts rather than a binary .ico.

If every typeface fetch fails, the Function falls back to a hand-rolled PNG encoder — raw IHDR/IDAT/IEND chunks, CRC-32 by hand, CompressionStream('deflate') for the zlib stream — so a scraper is never served a 5xx it will cache.


Testing

Suite Result
Unit (Vitest) 292 / 292, 13 files, 93.25 % lines on src/systems/** (threshold 85)
End-to-end (Playwright) 39 / 39, 0 console errors, incl. a 3-minute memory soak with 0.0 MB heap growth
Simulation (Vitest, headless) 79 / 79
Performance (Playwright) 8 / 8 — 43 draw calls at 24 zombies (budget 1 500); p95 game CPU frame 0.5–1.6 ms (budget 8 ms)
Bundle 200.52 KB gz across 6 dist/ files (budget 900 KB), 0 binary media files
Live smoke (Playwright) 2 / 2 against the deployed live URL on a real GPU — see Known limitations

Every suite except the live smoke runs on every push, on Node 20, via .github/workflows/ci.yml. The live smoke is opt-in (LIVE_URL) because it targets a deployed site rather than a preview server and needs a GPU no CI runner has.

IP hygiene

Claude\Zombies is original work inspired by the round-based zombie-survival genre as a whole. A unit test scans every file git tracks or would track — case-insensitively, as substrings — against a denylist of publisher, studio, title, map, character, perk and enemy names from commercial franchises. Latest full run: 0 matches for 134 denylisted terms across 165 enumerated files (164 scanned — see the single exclusion below).

The scan excludes exactly one path — the fixture that defines the denylist, because a denylist necessarily contains the strings it forbids. Nothing under tests/** is reachable from index.html or src/main.ts, so that file never enters dist/. Four meta-assertions stop the scan passing vacuously, including a planted-term detection.


Deployment

Live now at https://zombies.jwhitton.com/, hosted on Cloudflare Pages (project claude-zombies, also reachable at https://claude-zombies.pages.dev/). The running build serves /assets/index-BeQyZDjN.js from the domain root. See DEPLOY.md for why the bundle hash, not the commit SHA, is the thing worth quoting.

The Pages project is direct-upload, not Git-connected. Pushing to main does not redeploy the site. .github/workflows/ci.yml still runs the full gate ladder on every push, but publishing is a deliberate, manual step. This is a real operational caveat: main can be arbitrarily far ahead of what is actually served, and nothing will warn you.

Redeploying is one command, from a clean working tree:

npm run deploy:cf

That is build:cf (which pins --base=/) followed by wrangler pages deploy dist --project-name=claude-zombies --branch=main. It needs wrangler on PATH (npm i -g wrangler) and an authenticated Cloudflare session (wrangler login).

Run it from the repository root. Pages Functions are discovered in ./functions relative to the working directory, not relative to the dist argument. Deploy from anywhere else and the static bundle still uploads perfectly — but /og.png and /favicon.ico silently vanish. A successful deploy prints ✨ Uploading Functions bundle; if that line is missing, the Functions did not ship.

The other targets in the table below serve the static bundle only. functions/ is Cloudflare-specific, so on Vercel, Netlify, Docker or any plain web server the social card and the /favicon.ico redirect are simply absent — the game itself is unaffected.

The repository still ships ready-to-use configuration for the other targets. Full copy-pasteable instructions, including the required BASE_PATH for each, are in DEPLOY.md.

Target Config BASE_PATH
Cloudflare Pages (live) npm run deploy:cf — see DEPLOY.md / (pinned by --base=/)
Vercel vercel.json unset (/)
Netlify netlify.toml unset (/)
Docker + nginx Dockerfile, nginx.conf unset (/), or a build arg
GitHub Pages — retired workflow deleted; kept as reference /claude-zombies/

The output is plain static files. Any web server will do — there is no backend, no API, and the game makes zero runtime network requests after load.


Browser requirements

  • WebGL 2 is required. There is no WebGL 1 fallback and no software path.
  • Pointer Lock API for mouse look.
  • Web Audio API for all sound. The AudioContext is resumed on visibilitychange and on the next user gesture, so audio survives tab blur.
  • localStorage for settings and high scores. The game runs without it, losing only persistence.
  • ES2022 output — current Chrome, Edge, Firefox and Safari.

Desktop, keyboard and mouse. There are no touch controls.


Known limitations

Stated plainly rather than omitted.

  • Real-GPU frame time was measured exactly once — on one machine, against the live site. Every automated browser timing in this repository comes from ANGLE/SwiftShader software rasterisation in headless Chromium, because no CI runner has a GPU. Draw calls, triangle counts, bundle size and allocation behaviour are exact and hardware-independent under SwiftShader; frame time is not. In one demonstration, renderFrame measured p95 1.0 ms in one run and p95 41.2 ms in another on the same build, purely because the host was busy — a figure that moves 40× with unrelated CPU load is measuring the rasteriser, not the game. The performance gate is therefore asserted against the game's own JavaScript (fixedStep + frameStep), which is identical on any hardware and measured at p95 0.5–1.6 ms.

    The live Playwright project (tests/e2e/live-smoke.spec.ts) closes the gap once, by hand, against the deployed live URL with the SwiftShader flags removed. Measured at 1920 × 1080, backbuffer confirmed in-page, on a Qualcomm Adreno X2‑90 integrated GPU through ANGLE/D3D11, WebGL 2:

    Scene Draw calls Triangles Median frame p95 Max fps
    Round 1, 6 zombies 46 11 833 8.30 ms 9.10 ms 10.50 ms 120.5
    24 walkers at round-13 stats 45 15 961 8.30 ms 9.20 ms 11.20 ms 120.5

    The run above was then repeated independently against the same deployment, and the finding reproduces: 46 draw calls / 11 917 triangles organic and 46 / 16 081 loaded, median 8.30 ms in both scenes (p95 9.10 / 9.00 ms, max 10.80 / 10.20 ms), 120.5 fps. Draw calls and triangle counts move only with view-dependent culling; the frame cadence does not move at all.

    Quadrupling the zombie count moved the median frame interval by 0.00 ms: the cadence is pinned to the display's 120 Hz vsync in both scenes, so the true headroom is larger than these numbers can express and the measurement is an upper bound on delivered fps, not on capability. That is twice the 60 fps target, at the stated resolution, on integrated graphics — so the §2 constraint is met on this hardware. It remains a single sample from a single GPU vendor, and no other machine has been tested. See QA_REPORT.md risk R4.

  • The Anvil Station is a late, narrow reward, and that is a forced trade. It is affordable on the income curve at round 9 in all 15 simulated runs, but it is actually purchased in only 1 of 5 mid-skill runs (round 14) and 2 of 5 high-skill runs. Measured across fourteen tuned configurations, the mid mean death round runs 12.0 / 13.0 / 14.4 / 15.6 / 17.4 as Anvil purchases go 0 / 1 / 2 / 3 / 4 of five seeds — the upgrade is worth four to six rounds of survival, so "a mid player buys the Anvil" and "a mid player dies around round 12" cannot both be true. The shipped configuration takes the pacing target and accepts the narrow purchase rate. Full reasoning in QA_REPORT.md §LOOP 2 CONTINUED.

  • The audio has never been listened to by a human. It is synthesised, tested for correct cue routing and AudioContext lifecycle, and measured — but nobody has judged whether it sounds good.

  • Single-player only. No networking of any kind, by design.

  • Desktop only. No touch input, no mobile layout, no gamepad support.

  • arcLance ammo cannot be refilled. It has no wall anchor and ammoBuy: 0. That is a deliberate design property of the Cache Roulette gamble, not an oversight — a pull can hand you a weapon you cannot resupply.


License

MIT © 2026 Claude\Zombies contributors.

Three.js is used under its own MIT license.

About

Stateloop publishing fork of justinw916-sketch/claude-zombies. Original game and MIT license by its upstream authors.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages