A standalone first-person shooter built as an unmodified-engine package that requires no commercial game data: one production map is integrated, runtime-proven and traversed on foot from the main menu to the exit switch, while twenty-two further maps, eight further weapons and the owner's art lock remain ahead of it.
Mission profile
- Ship a standalone package that needs no commercial game data, verified by tests rather than asserted in a readme.
- Author every map as an engine-agnostic contract compiled into geometry, so the design survives an engine decision.
- Model weapons as explicit state graphs where handling, recoil, reload and selector behaviour are authored frames, not tuning constants.
- Bind every imported art pack to a manifest and prove byte-identity through packaging.
- Separate what a machine proved from what a person physically did, and keep the owner's aesthetic verdict unsigned until the owner signs it.
Architecture
Verified state
What exists and has been exercised at the time of writing.
- The engine is an unmodified stable build, and the package is a standalone archive of 1,153 deterministic entries requiring no commercial game data.
- The build is reproducible by construction: entries are sorted, timestamps are fixed, and a manifest records a SHA-256 for every file alongside the package hash.
- Static tests hold the package to its contracts, including a gate that fails the build on any commercial lump name and a gate that fails on any commercial sprite prefix.
- One production map is integrated: 233 sectors, 978 lines, 67 dynamic lights, 2,018 reachable cells, 2 secrets and 47 non-interactive production prop placements.
- Four weapons are implemented as authored state graphs — a 13-round pistol, a 20-round rifle with a semi and automatic selector, a throwable with a four-band blast, and a melee viewmodel.
- The protagonist carries health, separate armour, five portrait states and a rage state, and the portrait selection is a pure function of that state with thresholds carried from the earlier engine contract.
- The route from the main menu through the map to the exit switch has been completed on foot with real input, including both keys, every mandatory door, both remote gates, a save and a reload in a distinct fresh process.
- Imported art packs are byte-identical through packaging: the enemy pack ships 148 production frames and the map's prop pack 369 sprite rotations, each proven against its manifest by test.
Stated limits
What this project is not, written here so nobody has to discover it later.
- This is not released. There is no distribution page, no platform bundle and no public build; the package exists on the machine that builds it.
- One map of twenty-three is integrated. The remaining twenty-two are authored contracts, not playable content, and production on the second has not begun.
- Four weapons of twelve exist. The other eight have not been started.
- The owner's art lock is open. Weapon frames still show repaired edges, a required delivery for the empty rifle state is missing, and the enemy pack passed technical review while its alpha workflow awaits an aesthetic verdict.
- Audio is a deterministic synthesized placeholder bank. Music has not started.
- The automated route proves technical completion. It does not prove that the game is fun, that its difficulty is right, or that anyone other than its author has played it.
- The figures on this page are reported from the project's own status documents and repository at a named commit. This build replayed no gate.
The name, and the codename
The title is RED FLAGS 2: COLOR OF POMEGRANADE. It is the string in the package's own startup identity and in its localization table, and it is what the repository's status documents use. POMEGRENADE is not the title: it is the throwable weapon, the palette name, and the internal historical working name the project carried before the title settled. This page keeps that distinction because a codename quietly promoted to a title is the smallest possible lie and the easiest one to avoid.
The project began on a different engine and migrated to its current one, which removed the two constraints that made the earlier line unshippable: a commercial-use restriction, and a dependency on a non-redistributable commercial data file. The archived implementation is preserved unmodified under a tag. The final product depends on no commercial game data at all.
A deterministic package, or no package
The build walks the game tree in sorted order, writes every entry with a fixed timestamp, and records a SHA-256 per file into a manifest alongside the package hash. Two builds of the same tree therefore produce the same bytes, which is what makes a package hash worth naming in a status document at all. Required lumps are checked by exact name, and a font stored under an organised path is explicitly not allowed to satisfy the bare name the engine looks up internally.
def assemble(out_path):
entries = []
for dirpath, dirnames, filenames in os.walk(GAME):
dirnames.sort()
for fn in sorted(filenames):
full = os.path.join(dirpath, fn)
rel = os.path.relpath(full, GAME).replace(os.sep, "/")
entries.append((rel, full))
entries.sort()
# A FON2 stored only as fonts/BIGFONT is addressable by the explicit path
# "fonts/BIGFONT", but it does not replace the engine's already-registered
# bare "BigFont". That leaks the stock chunky face into protected menus and
# any menu descriptor that the engine constructs internally. Keep the
# organised copies for explicit lookups and add byte-identical root aliases
# so the game's fonts win every bare-name lookup as well.
for name in ROOT_FONT_ALIASES:
source = os.path.join(GAME, "fonts", name)
if os.path.isfile(source) and not any(rel == name for rel, _full in entries):
entries.append((name, source))
entries.sort()
manifest = []
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as z:
for rel, full in entries:
with open(full, "rb") as fh:
data = fh.read()
info = zipfile.ZipInfo(rel, date_time=FIXED_DATE)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o644 << 16
z.writestr(info, data)
manifest.append({"path": rel, "size": len(data), "sha256": sha256_bytes(data)})
return manifestThe comment is a bug report turned into a rule. A font present only under an organised path left the engine's own menus rendering with its stock face, so byte-identical root aliases are added deliberately and the required-lump check refuses to accept the organised copy in their place.
Nothing commercial gets into the package
Standalone is asserted by a test, not by a sentence. One gate refuses any file whose base name matches a commercial data pattern. Another refuses any sprite whose four-letter prefix collides with the engine's ancestral sprite set, and additionally refuses any sprite that does not match the project's own naming grammar. Further gates check that the map definitions, the localization table and the menu definitions expose none of those names to a player.
def test_no_external_iwad_or_commercial_data(pkg):
_, names = pkg
bad = [n for n in names if FORBIDDEN_NAMES.match(os.path.basename(n))]
assert not bad, bad
def test_no_doom_sprite_names_or_lumps(pkg):
_, names = pkg
sprites = {os.path.basename(n)[:4].upper() for n in names if n.startswith("sprites/")}
assert not (sprites & DOOM_SPRITES), sorted(sprites & DOOM_SPRITES)
bad = [n for n in names
if n.startswith("sprites/") and not re.match(r"sprites/[A-Z0-9]{4}[A-Z][0-8]\.png$", n)]
assert not bad, bad[:10]Three assertions, each on the built package rather than on the source tree. The sprite test is the strict one: it is not enough to avoid the ancestral names, every sprite must also match the project's own four-letter grammar, so an accidental import cannot pass by being merely unfamiliar.
Weapons are state graphs, not tuning tables
Each weapon is an explicit graph of named states with authored frame cadences: ready, empty ready, select, deselect, fire, automatic fire, dry fire, selector press and return, and a reload broken into magazine out, shoulder, magazine in, bolt and return. The rifle's two fire modes change cadence and control only — damage per round and ammunition economics are identical in both — and that rule is written in the file as canon rather than left to be discovered by a later balance pass.
AltFire:
FALG A 0
{
invoker.fullAuto = !invoker.fullAuto;
// Keep the engine's Ready-state edge latch enabled in both modes.
// FireAuto performs its own held-trigger loop with A_ReFire, which
// bypasses NoAutoFire; clearing this flag only made a held selector
// key enter AltFire twice and undo the requested mode change.
invoker.bNoAutoFire = true;
invoker.burstRound = 0;
A_StartSound("pom/fal/mode", CHAN_WEAPON);
PomVoice.Tell(StringTable.Localize(
invoker.fullAuto ? "$POM_FAL_AUTO" : "$POM_FAL_SEMI"));
PomClearMotion();
PomBeginMotionStage(4, 0, POM_REST_Y,
1.2, POM_REST_Y + 2.5);
return ResolveState("ModePress");
}The comment records a defect and its cause: the automatic path runs its own held-trigger loop, which bypasses the engine's edge latch, so clearing that flag made a held selector key enter the state twice and undo the mode change it had just requested. The fix is one line, and it is explained where it lives.
The protagonist's face is a function of state
The status bar draws a portrait, health, separate armour, a numeric count for the throwable, the full weapon name and a magazine-and-reserve pair. The portrait is not scripted by events: it is a pure function of the player's current state, evaluated every frame, with a rage state that overrides the injury ladder beneath it.
static clearscope int PortraitState(PlayerPawn pawn)
{
// 4=RAGE overrides all, then 3=NEAR-DEATH <11, 2=CRITICAL <31,
// 1=HURT <61, 0=NORMAL. Thresholds carried exactly from the
// EDuke32 bootstrap contract.
let pp = PomPlayer(pawn);
if (pp && pp.pomRageTics > 0) return 4;
int h = pawn ? pawn.health : 0;
if (h < 11) return 3;
if (h < 31) return 2;
if (h < 61) return 1;
return 0;
}Five states, four thresholds, one override, no history. Because the selection is pure, the HUD cannot drift out of sync with the simulation, and the thresholds are carried verbatim from the project's earlier engine contract rather than re-invented during the migration.
Machine-proven, physically proven, humanly accepted
The project keeps three separate vocabularies and refuses to collapse them. Implemented means code exists. Integrated means it is in the standalone package. Runtime-proven means the engine exercised it. Physically proven means real keyboard and mouse input reached it without a cheat or a state-changing console command. None of those means owner-accepted, and none of them means art-locked.
- Static suite
- 58 of 58, as reported by the project's status document.
- Runtime slice
- 77 of 77, reported.
- Controls gate
- 10 of 10, reported.
- Physical input
- 8 of 8, reported.
- Physical route
- Accepted: 45 legs, 977 samples, no discontinuity beyond the sampling threshold.
- Fresh-process load
- An exact field-by-field match after reload in a distinct process.
- Owner art lock
- Open. Automation may not sign it and this page does not imply it.