A top-down, real-time, 8-bit-styled survival action-RPG for up to 25 concurrent players. Wake up in an open prison cell at the start of an extraterrestrial zombie outbreak, escape the prison, traverse a hostile forest, find a boat on the coast, and row out to a rescue ship. Persistent world, PvP everywhere, infection that spreads NPC-to-NPC in real time so the world degrades whether or not you play.
The look is deliberate: strict 256-colour indexed graphics at 480×270 internal resolution, scaled up to whatever display you're on, scored by an atmospheric soundtrack. SNES-era visual discipline applied to a modern dystopian apocalypse.
Named NPCs remember every choice — and the shore is never as empty as it looks.
Pre-built Linux x86_64 binaries: https://parasite.mittonvillage.com
The client downloads its asset bundle (~24 MB) on first launch over HTTPS, verified by SHA-256. After that, runs are offline-safe.
System requirements: Ubuntu 22.04 LTS or newer plus the Allegro 5.2 + libcurl
runtime packages — see the in-tarball README.md or run ./check_deps.sh
before launching.
You spawn in a cell. You craft a rope from the bedding, you scavenge the prison for food / medicine / a weapon, you climb out into the forest, you fight or sneak through hand-authored zones (treeline, logging road, a forester's camp, a river crossing, a gas station, coastal pines, the beach), you find an intact dinghy, you row out to the rescue ship, you board it, the run is over. Total run length: 1–3 hours of focused play. Die at any stage and you respawn back in a cell — same character, vitals reset, inventory mostly intact, one anchor item dropped at the death site for whoever finds it first.
| Input | Action |
|---|---|
| W A S D | Move (8-directional) |
| Left Shift | Sprint (drains stamina; no regen while held) |
| Mouse left | Melee swing (cone hitbox) — or fire while aiming |
| Mouse right | Aim (precision reticle for ranged weapons) |
| E | Interact — talk to NPCs, open doors, pick up loot, board the rescue ship, advance dialogue |
| Tab | Open / close inventory grid |
| M | Open map |
| Enter | Open chat input (commit with Enter, cancel with Esc) |
| V | Push-to-talk for proximity voice |
| F1 / F2 / 3 / F4 | Quick emotes (wave, point, nod, etc.) — slot 3 lives on the digit 3 key because F3 is reserved |
| F3 | Toggle debug overlay (frame profiler) |
| C | Craft cure from antiseptic + antibiotic + bandage |
| Esc | Pause / back out of menus |
Reloading is automatic: when the equipped firearm's magazine empties, a 5-second reload begins if you're carrying compatible ammo. There is no manual reload key.
- Health (100 base). Bleeding from open wounds ticks until bandaged.
- Hunger drops over time; below 25% your maximum HP is capped. Eating restores it.
- Stamina drains while sprinting. Regenerates while walking, and 1.5× faster while standing still.
- Infection is the central pressure. A bite from a zombie has a 15% chance to expose you. Once exposed, a 2-minute countdown starts (visible as a corrupting palette tint on the HUD). To cure: collect antiseptic + antibiotic + a bandage, press C to craft them into a single cure item, then use that cure from your inventory. (The three ingredients aren't applied individually — they're combined first, then the combined cure is consumed.) Inside the prison, all three ingredients live in the locked guard room, which needs a
cell_key. The key drops from yard zombies at a 40% chance per kill, and the first kill in a fresh prison run is guaranteed — so if you clear the yard methodically you will always come out with at least one. If the timer expires you respawn — but you keep climbing only while exposed; once you cross into incubating, the parasite shuts down higher motor control and you cannot climb anymore. The 2-minute window is your chance to escape vertically before that happens.
Melee is the default. Bullets are scarce, loud (gunshots create a 30-tile alarm event), and reserved for emergencies. Weapons divide into:
- Improvised melee — shanks, broken bottles, mop handles. Low damage, no durability cost.
- Solid melee — batons, fire axes, machetes. Medium damage, durability.
- Pistols, shotguns, rifles — found in the armoury and forest set-pieces. Each has its own audible signature.
- Special — Molotovs and flares. Fire kills the parasite outright; flares distract.
Zombies have a 90° sight cone (~6 tiles), hear gunfire from 30 tiles and footsteps from 4 (sprinting). They cannot climb, cannot jump gaps, and move at ~25% of their base speed while idle, snapping to 100% on contact — so an idle horde looks dormant until you give it a reason.
PvP is always active everywhere. No safe zones, no opt-in, no mechanical immunity inside the prison. What stops the prison from devolving into a deathmatch is the Marked status: kill a named NPC (anywhere a named settlement exists — the prison, the forester's camp, the gas station) and your character is flagged. While Marked, guard NPCs across the map attack you on sight, and every named NPC refuses to talk or trade — the server replies INTERACT_DENY(MARKED) and the client renders a "they won't deal with you" cue. Killing anonymous infected or wandering zombies does not Mark you. The status persists for the rest of that character's life and is only cleared by death and respawn. Permadeath is the redemption mechanic.
A trade-window exploit lock prevents the "shoot them while they're confirming the trade" trick: the moment either party opens the trade window, a 10-second corpse-lock timer arms — and the items locked into the trade UI stay flagged on the corpse even if the dead party closes the window early.
- Global text chat —
/sayreaches everyone on the server./mefor actions,/w <name>for whisper. Up to 256 chars per/say//me, 64 per whisper; client retains the last 32 entries with 6 visible. - Local emotes — fanned out to every player in the sender's 3×3 chunk interest window (so effectively everyone nearby on screen).
- Proximity voice — Opus-encoded, 48 kHz, 20 ms frames, server-mixed. Client sends mono at 24 kbps; the server returns the spatial mix as stereo (24 kbps/channel, 48 kbps total). Full gain inside 2 tiles, linear falloff to silence at 16. Walls drop blocked talkers to 25% gain. Voice persists through death and respawn — being killed doesn't silence your radio.
Get to the beach, find a dinghy that's intact AND has oars (some are stove in, some don't), row ~2–3 minutes out to the ship, board the ladder. The run ends. The "You Survived" screen shows your final score, a leaderboard, and your per-run stats. Press Enter to return to title or Esc to exit. Your character is retired into a Survivors gallery and the next time you connect, you start fresh in a new cell.
Each completed run produces a score. Higher is better. The formula is integer-clamped to [0, INT32_MAX]:
Positive contributions
| Action | Points |
|---|---|
| Zombie killed | +10 each |
| Melee kill (any kind, on top of the kind-specific bonus) | +25 each |
| Player killed (PvP) | +50 each |
| Net items picked up minus dropped (clamped at zero) | +5 each |
| NPC conversation initiated | +20 each |
| Quest unlocked | +100 each (the formula reserves this term but v1 ships with no quest registry, so the field always reads zero in practice) |
| Accuracy bonus | +5 per integer percent (max +500 at 100%) |
| Damage dealt to other players | +2 per HP |
Negative contributions
| Action | Penalty |
|---|---|
| Damage taken from any source | −1 per HP |
| Damage taken from other players | −3 per HP (in addition to the above) |
| Shots fired (encourages efficiency) | −1 each |
| Food eaten (encourages frugality) | −2 each |
| Named NPC killed (this is what Marked tracks) | −30 each |
| Time spent on the run | −1 per 10 seconds |
Killing zombies efficiently with melee and finishing fast rewards more than spraying ammo and grinding. Killing named NPCs is heavily penalised — that's the design lever that keeps the prison social rather than a free-for-all. The leaderboard shows the top 5 server-wide; if you don't appear in the top 5, your own row is appended below the cutoff with your actual score so you can see where you stand.
A C11 client (Allegro 5.2) talks to a C11 headless Linux server over ENet on UDP 7777 (game) and a separate raw UDP socket on 7778 (Opus voice). Up to 25 concurrent players. The server is a single multi-threaded process with four threads — Net I/O, Sim (20 Hz), Voice Mix, DB Write — sharing world state under a mutex with an RCU-style 4-slot snapshot pool for the voice thread. Persistence is SQLite (WAL) plus a periodic world.bin snapshot. Auth is Ed25519 identity + X25519 ECDH → HKDF-SHA256 session keys + HMAC-SHA256 (16-byte truncated) per packet. No payload encryption in v1 — only authentication. Rendering is strict 256-colour indexed at 480×270 internal, upscaled by a fragment shader.
┌─────────────────────────────────────────────┐
│ parasite_server │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────┐ │
UDP 7777 (ENet, reliable + unrel) │ │ Net I/O│ │ Sim │ │ Voice │ │ DB │ │
┌─────────────────────────────────►│ │ │◄┤ 20 Hz ├►│ Mix │ │ Write│ │
│ │ │ ch0/1/2│ │ phases │ │ Opus │ │ WAL │ │
│ ┌─────────────────────────────► │ └────────┘ └────┬───┘ └────────┘ └───┬──┘ │
│ │ UDP 7778 (raw, Opus voice) │ │ │ │
│ │ │ atomic 4-slot snapshot pool ────┘ │
parasite_client × N │ (sim publish → voice acquire) │
( ≤25 concurrent ) │ │
│ SQLite (WAL) + world.bin snapshot ◄──┘
└─────────────────────────────────────────────┘
| Layer | Technology |
|---|---|
| Language | C11 (no compiler extensions, _Static_assert for layout invariants) |
| Build | CMake 3.20+, out-of-tree in build/ |
| Client framework | Allegro 5.2.x (locked to 5.2) |
| Networking | ENet 1.3.18 for game, raw UDP for voice |
| Crypto | libsodium 1.0.20 (Ed25519 / X25519 / HKDF-SHA256 / HMAC-SHA256) |
| Voice codec | libopus 1.5.2, 48 kHz, 20 ms frames, server-mixed |
| Persistence | SQLite (vendored amalgamation), WAL, numbered SQL migrations |
| Config | cJSON for asset tables; TOML-ish for server / client configs |
| Tests | µunit (vendored), ctest at the top level, libFuzzer harnesses under tests/fuzz/ |
| Asset pipeline | Python codegen (tools/gen_*.py) emits paletted PNGs + C sources |
libparasite_core is a static library linked by both client and server. Anything that must agree on both sides — protocol structs, infection state machine, inventory grid rules, item registry, vec2 math, RNG, session-key derivation, replay window — lives there. core/ is forbidden from including Allegro, ENet, libsodium, SQLite, Opus, or cJSON headers, and from including anything under client/, server/, or platform/. The pre-commit hook enforces this with a grep. If it blocks an include you think is legitimate, the code is wrong, not the hook.
The infection state machine, inventory grid rules, and trade-lock timer exist in exactly one place — core/. Client-side prediction and server-side authority link the same implementation. Duplication of these in client/ or server/ is a bug.
- C11 strictly. No GCC/Clang extensions unless guarded by feature macros. No VLAs in structs, no flexible array members in public headers, no computed gotos.
_Static_assertis used liberally for compile-time invariants on struct sizes and protocol field offsets. - CMake 3.20+ at the top level. Out-of-tree builds in
build/(gitignored). Top-level targets:parasite_client,parasite_server,parasite_core(static lib),parasite_tests. - Vendored (
third_party/, pinned):enet1.3.18,libsodium1.0.20,libopus1.5.x,cjson, thesqlite3amalgamation, µunit for tests. - System dep: Allegro 5.2.x. Locked to 5.2; do not adopt 5.3.
Install Allegro 5.2 dev packages (Ubuntu 22.04+):
sudo apt-get install liballegro5-dev liballegro-image5-dev \
liballegro-audio5-dev liballegro-acodec5-dev liballegro-ttf5-dev \
liballegro-dialog5-dev(Primitives / color / font ship inside liballegro5-dev on Ubuntu 24+; no separate packages needed.)
macOS: brew install allegro. Windows: vcpkg install allegro:x64-windows and pass -DCMAKE_TOOLCHAIN_FILE=... at configure.
First-time setup — vendored sources are fetched out-of-band, not committed:
./tools/fetch_third_party.sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build --parallel
./tools/hooks/install.sh # opt-in pre-commit (build + ctest + boundary grep)Targeted builds (skip the client when Allegro is unavailable; skip tests for a faster server-only iteration):
cmake -S . -B build -DPARASITE_BUILD_CLIENT=OFF # server + tests only
cmake --build build --target parasite_server # one targetRun the binaries:
./build/server/parasite_server --config server/server.toml.example
./build/client/parasite_client --server 127.0.0.1:7777ctest is enabled at the top level. µunit provides the framework.
ctest --test-dir build --output-on-failure # full suite
ctest --test-dir build --output-on-failure -R <regex> # subset by test name
./build/tests/parasite_tests --help # µunit flags
./build/tests/parasite_tests /core/infection # single suite directlyUse separate build trees — TSan is incompatible with ASan.
# ASan + UBSan
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DPARASITE_SANITIZE=address,undefined
cmake --build build-asan --parallel
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ctest --test-dir build-asan --output-on-failure
# TSan
cmake -S . -B build-tsan -DCMAKE_BUILD_TYPE=Debug -DPARASITE_SANITIZE=thread
cmake --build build-tsan --parallel
./tools/run_tsan_tests.sh # wraps ctest with setarch x86_64 -RTSan needs an ASLR workaround on Linux x86-64. ThreadSanitizer uses a fixed shadow-memory layout that conflicts with the kernel's PIE + ASLR address randomization. Use tools/run_tsan_tests.sh instead of calling ctest directly; cmake/ParasiteHardening.cmake also disables -fPIE/-pie when PARASITE_SANITIZE=thread so the shadow region can be reserved at the expected address.
MSan (Clang only, experimental):
cmake -S . -B build-msan -DCMAKE_BUILD_TYPE=Debug -DPARASITE_SANITIZE=memory \
-DCMAKE_C_COMPILER=clang
cmake --build build-msan --parallelcmake -S . -B build-fuzz \
-DPARASITE_BUILD_FUZZ=ON \
-DCMAKE_C_COMPILER=clang \
-DPARASITE_SANITIZE=address,undefined
cmake --build build-fuzz --parallel
./build-fuzz/tests/fuzz/fuzz_protocol_auth -max_total_time=60See tests/fuzz/README.md for per-harness details.
# scan-build (Clang static analyser)
scan-build cmake --build build 2>&1 | tee scan-build-report.txt
# clang-tidy (requires compile_commands.json)
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
run-clang-tidy -p build 'client/|server/|core/|tests/'The .clang-tidy config at the repo root enables cert-*, bugprone-*, clang-analyzer-security-*, and readability-non-const-parameter check groups.
Verification runs on the developer's machine; there is no GitHub Actions setup. ./tools/hooks/install.sh installs a pre-commit hook that runs cmake --build build, ctest, and the core/ include-boundary grep. The hook is opt-in. If you genuinely need to bypass it (rare), git commit --no-verify works — don't make a habit of it.
These are easy to violate by accident; reviewers will catch them, but knowing them up front saves a round trip.
- Wire format is little-endian everywhere, including the on-disk
world.binand SQLite blobs. No struct-cast shortcuts; use thepst_write_*_le/pst_read_*_lehelpers incore/protocol.h. htonl/htonsare forbidden for wire-format encoding. They are only legitimate in POSIX socket-struct setup (sockaddr_in.sin_port) where the kernel API contract requires them.- Floats and doubles are forbidden on the wire AND in server-local persistence. Positions and continuous quantities cross the wire as
i16at 1/16-tile (the_q15suffix convention).world.binand SQLite both store Q15 integers for player/NPC/item coordinates. Float varies by FPU rounding and breaks deterministic replay/reconciliation. - Sequence numbers compare via signed difference, never
>or<directly. Direct unsigned comparison silently misbehaves at wraparound and breaks replay windows, prediction ring-buffer lookups, and acked-input-tick checks. - Replay protection is per-channel. Reliable channels (ch0) enforce strict monotonic
seq; unreliable channels (ch1, ch2) use a 64-bit sliding bitmap. The INPUT-redundancy mechanism (3 backup commands per packet) depends on the bitmap — a strict monotonic check would defeat it. - HMAC and key comparison uses
sodium_memcmp, notmemcmp.memcmpleaks timing and is a forgery oracle. - Unpredictable values come from
randombytes_buf(libsodium), neverrand()/random()/srand(). Session IDs, challenge nonces, voice session IDs — anything an attacker must not predict. - Attacker-supplied strings are sanitized before logging. Player display names, chat text, BYE reason strings, and any other wire-sourced text have control bytes (
0x00–0x1F,0x7F) stripped or escaped before they reach the logger. Raw bytes enable ANSI-escape terminal hijacking and log-line forgery.
- Game state mutates only on the sim thread. Other threads read via the RCU snapshot pattern documented in
docs/SERVER_ARCHITECTURE.md §3.3— atomic pointer exchange, old snapshots freed after a 2-tick (~100 ms) grace period. This is the canonical pub/sub pattern; use it whenever sim publishes read-mostly state. - Identity-key memory protection. The Ed25519 secret lives in
sodium_malloc-allocated memory markedsodium_mprotect_noaccessbetween uses.sodium_mprotect_readonlybrackets every sign call, after which the region returns tonoaccess. Never store the secret in a stack buffer or normal heap.
All sprites, tilesheets, fonts, dialogue tables, item tables, SFX, and .pmap map files are generated by Python scripts in tools/ (gen_*.py). CMake wires them in and re-runs them on input change. The generators emit C source or paletted PNG output as appropriate, all keyed to a single master palette at assets/palette/master.gpl. Patch the JSON sources or the generator, never the emitted file. gen_*.py output under core/generated/ carries a "do not edit" banner.
The palette discipline is strict: every paletted PNG uses indices from the master palette, and the entire UI renders into the same indexed framebuffer — there is no separate RGB compositing layer. Day/night blending, infection tint, open-water tinting, water-cycle animation, and fire flicker are all palette LUT operations, not per-pixel shading.
| Subsystem | Budget |
|---|---|
| Client frame rate | 60 fps locked at 480×270 internal (any display res) |
| Client memory ceiling | 256 MB resident |
| Server tick | 20 Hz game sim |
| Snapshot rate per client | 10 Hz |
| Server CPU per tick | 50 ms total across input apply, AI, infection sim, snapshot encode, DB queue, voice mix |
| Voice mix | ≤ 5 ms per 20 ms tick for 25 concurrent talkers (target = 25% of one core) |
| Server memory ceiling | 1 GB resident on a 2 GB VPS |
| Bandwidth per client | ≤ 64 kbps game + ≤ 32 kbps voice (one talker burst) |
world.binand the wire protocol both serialise field-by-field. Any struct-layout change bumps the relevant version field (PST_PROTOCOL_VERSIONincore/protocol.h;schema_versioninworld.bin). The changelog comment at the top ofcore/protocol.his the source of truth.- SQLite migrations are plain SQL under
server/migrations/NNN_description.sql, applied in order on startup and tracked by aschema_versiontable. - Wire-format correctness tests live under
tests/core/test_protocol_*.cand gate the pre-commit hook. JSON test vectors undertests/protocol/vectors/drive a language-agnostic vector runner. Any change to a packet's byte layout MUST update the matching_Static_assert, the C test, and the JSON vector — pre-commit catches a missed one.
This repository includes project-scoped subagent definitions under .claude/agents/. They pre-load the relevant docs sections and are preferred over general-purpose whenever the work matches their domain:
c-developer— C11 hygiene, build system, threading conventions, cross-platform portability.game-developer— game loop, entity model, AI, world sim, palette pipeline, content pipeline.network-engineer— ENet, packet protocol, handshake, snapshot delta encoding, prediction/reconciliation, security.voice-engineer— Opus encode/decode, voice UDP transport, jitter buffer, server-side spatial mixing.security-auditor— read-only vulnerability audits for the networking code (memory safety, integer arithmetic, crypto misuse, auth/replay, concurrency).
These are optional; you don't need to use them to contribute. They exist because the codebase has wide cross-cutting invariants and pre-loading the right docs section is the single biggest accuracy win for AI-assisted work.
| Document | Owns |
|---|---|
docs/GAME_DESIGN.md |
Gameplay rules, infection model, PvP/Marked semantics, canonical constants |
docs/TECHNICAL_REQUIREMENTS.md |
Language/toolchain, deps, palette pipeline, perf budgets, security surface |
docs/SERVER_ARCHITECTURE.md |
Server thread model, world sim, SQLite schema, deploy/systemd |
docs/CLIENT_ARCHITECTURE.md |
Client modules, render pipeline, state machine, input, audio |
docs/NETWORK_PROTOCOL.md |
Wire format, packet types, handshake, snapshots, voice |
docs/DEPLOYMENT.md |
VPS provisioning, deploy scripts, backup, admin socket |
docs/ROADMAP.md |
Milestone schedule and parallel-track guidance |
When code and docs disagree, fix one or the other in the same PR — never leave them in conflict.
v1 is shipped (current release: see VERSION). See docs/ROADMAP.md for the per-milestone closure list and the small post-v1 backlog.
assets/ is not tracked in git. To work on the game locally, get a copy of
assets/ by one of:
-
Regenerate from source. Every authored asset has a
tools/gen_*.pygenerator that runs as part of the CMake build:cmake --build build --target parasite_assets
-
Download the prebuilt bundle.
curl -fLO https://parasite.mittonvillage.com/downloads/parasite-assets-v$(cat VERSION).tar.gz tar xzf parasite-assets-v$(cat VERSION).tar.gz
See CONTRIBUTING.md for the dev setup, commit conventions,
and the rules around the core/ boundary.
zlib. See LICENSE.



