testsAndMisc/reverse_survivors
Krzysztof kuhy Rudnicki 43b63ac266 reverse_survivors: survivor status effects (slow, suppress, bleed)
The warden mires, the artillery stifles, the leech unknits. All three need
contact, so a survivor that kites cleanly takes none of them — debuffs
punish positional failure rather than taxing existence.

isOn/factor are written as arithmetic rather than comparisons:
Math.min(1, Math.ceil(t)) is 1 while a timer is live and 0 once it expires,
so consumers multiply instead of branching. The whole status module
therefore contributes zero branches, which is what makes the 100% gate
affordable for a system this cross-cutting. Statuses are applied
unconditionally too; a 0-duration spec is a provable no-op.

applyStatus uses Math.max, so re-applying refreshes rather than stacks.
That caps a horde's total lockdown at one duration by construction — a
structural guarantee, not a tuned number.

123 tests, 100% coverage. Branches 117 -> 119: the draw halo gate and the
snapshot chip filter. The status logic itself adds none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFyXexNjMQG9V7K8de17fc
2026-08-09 23:08:51 +02:00
..
src reverse_survivors: survivor status effects (slow, suppress, bleed) 2026-08-09 23:08:51 +02:00
.gitignore feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
eslint.config.js feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
index.html feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
package-lock.json feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
package.json feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
README.md feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
tsconfig.json feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00
vite.config.ts feat(reverse_survivors): migrate Reverse Survivors game into the monorepo 2026-08-03 10:45:12 +02:00

Reverse Survivors

Vampire Survivors, inverted: you are the horde. An AI-controlled survivor kites, auto-fires, and levels up on its own. You spend a regenerating pool of souls to spawn grunts, call escalating ring waves, and — once the seals break — summon bosses. Kill the intruder before the 5:00 clock runs out, or watch dawn break on your defeat.

Play

npm install
npm run dev
  • Rusher / Stalker / Tank / Bomber — single spawns at a random arena edge.
  • Wave N — a ring formation around the survivor; each call grows and costs more.
  • Colossus (unlocks 1:00) and Hivemind (unlocks 2:00) — expensive, on cooldown. The Hivemind births free rushers while it lives.
  • The survivor gains XP per kill and rolls an upgrade per level (damage, fire rate, speed, vitality, regen, multishot). Time favors it. Pressure early.

Where enemies appear is deliberately not yours to choose — the director picks what and when; the seeded RNG picks where.

Quality gates

npm run check = typecheck + lint + tests with coverage. All three must pass; CI runs the same.

Gate Enforced by
100 % lines / branches / functions / statements Vitest coverage.thresholds hard-set to 100 — the run fails below it
Strictest typed linting ESLint 10 flat config: typescript-eslint strictTypeChecked + stylisticTypeChecked, @eslint-react strict-type-checked, react-hooks (compiler-backed rules), react-refresh, vitest plugin, --max-warnings 0
Strictest compiler TS strict + noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax, erasableSyntaxOnly, noUnusedLocals/Parameters, and friends

Pinned stack (live-latest at build time): React 19.2.8 · Vite 8.2.0 · Vitest 4.1.10 · ESLint 10.8.0 · typescript-eslint 8.65.0 · TypeScript 6.0.3.

Why not TypeScript 7? TS 7 is the native (Go) compiler and does not expose the JS compiler API that typed linting needs — typescript-eslint@8.65 caps its peer range at <6.1.0. TS 6.0.x is the newest compiler that still type-lints.

Architecture (what makes 100 % honest)

  • src/core/ — the whole game as pure-ish functions over a plain state object. Deterministic: seeded mulberry32 RNG lives in the state; same seed + same action sequence ⇒ identical run. Behavior dispatch uses records instead of switches, and clamping uses Math.min/max instead of if, so there are no unreachable defensive branches to fake-cover.
  • src/render/draw.ts — draws onto a minimal Ctx2D structural interface; tests assert against a typed stub. (This layer's tests are mock-assertion ritual, as warned — the assurance lives in the core suite.)
  • src/ui/ — thin React shell. The rAF loop clamps dt to 100 ms and holds its callback in a ref. App accepts a boot prop as an explicit test seam to reach both endings without simulating five minutes.
  • src/main.tsx is covered too, including the missing-#root throw, via vi.resetModules() + dynamic import.

Scripts

dev · build (tsc + vite) · preview · typecheck · lint · test · coverage · check (all gates)