One build flag, two games - how I shipped a Halloween spin-off from the same codebase
Playgama is running a weekly challenge called "A Little Spooky": cozy, slightly eerie browser games, no jump scares. I already had a bullet-reflecting boss rush, Void Parry, and a week is not enough to write a second eng
Playgama is running a weekly challenge called "A Little Spooky": cozy, slightly eerie browser games, no jump scares. I already had a bullet-reflecting boss rush, Void Parry, and a week is not enough to write a second engine. So the spin-off, Hollow Night, is the same code with one flag switched on.
Here is what that looked like in practice.
The flag
The whole switch is one global, set by the build script before the game loads:
<script>window.VP_EVENT = "hollow";</script>
<script src="bosses_hollow.js"></script>
and one line at the top of the game:
const HN = window.VP_EVENT === "hollow";
Everything that differs hangs off HN: the title, the boss list, the arena backgrounds, the music table, the save key and which menus exist. Hollow Night has five bosses and a shop. The main game's other modes are simply not in the zip.
Swap data, not code paths
The rule I kept: HN may pick which data the engine uses, but it should not add new behaviour.
const BG = HN ? ["art/hollow/hollow1_wood.jpg", /* ... */]
: ["art/bg1_nebula.jpg", /* ... */];
The five new bosses (a lantern wisp, a candle spider, a scarecrow, a grave moth and a smiling moon) re-use bullet pattern families that the main game's tests already cover: spirals, rains, fans, telegraphed lines. Each gets its own palette and one signature move. That way a bot that can beat the main game can also play the spin-off, and I did not need a second test suite.
The boss drawings are a separate file with the same draw API as the main game's. The spin-off's file registers itself under the same name (window.VP_VEC = window.VP_HOLLOW), so the engine's VP_VEC.draw(...) call does not know which file answered.
The build is a short shell script
No bundler. The script copies the files the spin-off needs into a temp folder, rewrites index.html with sed, and zips it:
sed -i -e 's|<script src="bosses_vec.js"></script>|<script>window.VP_EVENT = "hollow";</script>\n<script src="bosses_hollow.js"></script>|' \
-e 's|<title>Void Parry</title>|<title>Hollow Night</title>|' "$D/index.html"
grep -q 'VP_EVENT = "hollow"' "$D/index.html" && ! grep -q "bosses_vec" "$D/index.html"
The last line matters more than it looks. sed fails silently when its pattern no longer matches. If someone edits the script tag in the main index.html, the build would happily produce a zip of the wrong game. The grep after it turns that into a failed build.
The one thing that must not be shared: the save
Both builds read and write localStorage. On the portal each game gets its own origin, so shared key names would be harmless there. On a test site where both games sit under one origin, the spin-off would load a save with fifteen cleared bosses and menus it does not have.
So the flag also picks the key prefix: the spin-off's saves all start with hn_. When one codebase becomes two products, the save belongs to the product, not to the engine.
What I would do again
- One flag, read once, at the top. If you can grep for it, you can count how different the two games are.
- Let the variant choose data, not logic. New content rides on tested systems.
-
Assert after every
sed. A text-replace build needs a check that the replace happened. - Separate saves from day one.
Hollow Night is free to play in the browser: playgama.ai/play/qwpaherohe. Five spirits, fog, lanterns, and one button that sends their bullets back.
Made with AI (Claude Code) and published through #PlaygamaMCP; the art is drawn in code, not generated images. #playgama
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.