Play it: dads-on-duty.netlify.app
— runs in any modern browser and installs as a PWA.

Mom has gone shopping. You are Dad. There is a baby, a six-room house, and a cry-o-meter that
fills whenever you get something wrong.
Baby Care: Dad’s on Duty is an isometric 3D browser game I built to see how far plain
three.js and ES modules get you with no bundler, no framework, and no build step at all. The
answer is: considerably further than I expected — far enough to include co-op multiplayer and a
global leaderboard.
No build step, on purpose
The whole game is index.html plus a folder of ES modules the browser imports directly.
three.js is vendored into vendor/three.min.js; Firebase arrives as two <script> tags from
gstatic.com. There is no bundler, no transpiler, and no dist/.
That second detail is the one worth dwelling on, because it is where “no build step” usually
dies. The moment a project needs a backend SDK, the reflex is to npm install it — and once
one dependency is in node_modules, you need a bundler to get it into the browser, and now you
have a build. Loading the compat build from a CDN sidesteps the whole chain: the game gets a
real-time database and a leaderboard, and the deploy is still a file copy.
This is not nostalgia. It buys three concrete things:
- The thing you edit is the thing that runs. No source maps, no “works in dev, breaks in
prod”, no waiting on a rebuild to see a tuning change.
- It cannot rot. There is no toolchain to have a breaking major version next year.
- Deploy is a file copy. The Netlify config is
publish = "." and an empty build command.
The cost is real and worth naming: no tree-shaking, no minification, and you are responsible
for your own module boundaries. For a game that vendors one large dependency and ships a dozen
small modules, that trade came out clearly ahead. npm is still there — but only for the test
runner and Playwright, never for the thing that ships.
The house is generated, and that is the hard part
Every run generates a new floor plan. That sentence hides where all the difficulty is.
Random rooms are easy. Random rooms that make sense are not. The rules that ended up
mattering:
- Every room opens onto a central hallway through exactly one door. No mazes, no bedroom
that connects straight into the bathroom.
- Rooms have plausible relative sizes. The living room is always bigger than the bathroom.
- Furniture is appropriate to the room type. The bathroom gets a toilet, a sink, a
shower-or-tub and a mirror. The bedroom gets a bed, a wardrobe, a dresser.
- The hallway itself varies — usually a straight spine, sometimes bending 90° into an
L-shaped house, sometimes branching into a T with a stem off the middle.
That last one is the one I would flag to anyone attempting this. It is tempting to “vary the
layout” by resizing rooms, which produces houses that are all subtly the same. A real corner or
junction in the corridor changes how you move, and movement is the game.
Generation is deterministic from a seed, which is what makes the Daily Challenge possible:
every player gets the same house, the same hazard timing and the same difficulty for a whole
UTC day. Same seed, same run.
Everything telegraphs
The game is a stack of hazards — the baby throws up, climbs into the oven, chokes on a small
toy, trips while walking. A hazard that fires with no warning is not difficulty, it is noise.
So there is one rule the whole design obeys: nothing fires with zero warning. The baby goes
visibly queasy before it is sick. It coughs before it chokes. A persistent banner appears the
moment it starts heading for the oven.
This is a small commitment that pays out everywhere, because it converts every hazard from
punishment into a decision. You always had a chance to act. If you didn’t, that was a choice
about what to do first — and “what to do first” is the entire game.
The same logic drives the preventative actions: tidy toys near the baby to push back choking
risk, baby-proof the oven to remove that hazard for the rest of the run, stretch at the bed to
reset your own back-pain clock. You are not only reacting.
Personality traits, or: variance with a memory
Each baby rolls one or two traits per run. Clingy frets if you wander too far. Climber is
riskier around the oven. Picky Eater refuses solid food in stretches — milk still works.
Favorite Toy means one specific toy soothes twice as well. Diaper Machine dirties faster but
pays out more. Cuddle Bug builds happiness just from being held.
Traits are rolled from the same seed as the house, which matters more than it sounds: it means
a daily challenge is fair. Two players comparing scores played the same game, not two
different ones.
Camera problems nobody warns you about
Isometric 3D in a building has one specific, immediate problem: walls get between the camera
and the player.
The fix that worked is to fade any wall between the camera and Dad or the baby to
semi-transparent. It sounds obvious written down; what was not obvious is that it changes what
zoom level is usable. The game now starts zoomed in on Dad rather than showing a full-house
overview, because the zoomed-in view no longer hides anything. You can scroll out for the
bird’s-eye whenever you want.
Co-op, on a database that already had to be there
Once a leaderboard exists, so does a Realtime Database — and a Realtime Database is most of a
multiplayer game.
Rooms are a five-character code. One player is host, and the host is authoritative: it owns the
simulation and publishes world snapshots, while guests send actions rather than state. That
split is the same call as in Island Doodle Adventure and for
the same reason — the alternative is every client asserting its own version of the truth and
the room disagreeing about whether the baby is still in the playpen.
The part that took the longest was not the sync. It was that a room can play several rounds in
a row. Attaching listeners on round start seems fine until someone hits Back to Lobby and
plays again, at which point every action is processed twice by a second set of listeners
nobody detached. The fix is unglamorous and worth writing down: make listen*() idempotent
with explicit guards, rather than trusting that setup runs once.
Multiplayer also raised the trait ceiling. With up to eight babies in a room, “roll one or two
traits each” has to guarantee no duplicate personality — so the twenty-one possible one- and
two-trait combinations are precomputed once and dealt out, shuffled by the seeded RNG.

Four modes and three difficulties
Normal is a timed run until Mom is back. Endless drops the timer and plays for score.
Chores adds a household checklist on top of the babysitting. Daily Challenge is the
seeded one — same house, same hazards, same difficulty for everyone that day, one attempt.
Difficulty is a table, not a multiplier: First Born, Been There, Done That and King each
carry their own decay rates, cry-meter fill and drain, hazard windows, oven odds, back-pain
timers and starting needs. Tuning by hand is more work than scaling one number, but it lets
easy mode be gentler rather than merely slower — a beginner gets a longer choke window, not
just a baby that gets hungry less often.
Offline, because a browser game should be
manifest.json plus a service worker make it an installable PWA. Load it once over http(s)
and a solo run works with no network at all — only multiplayer and the leaderboard need one,
and both degrade to “unavailable” rather than breaking the game.
One detail worth stealing, from the Netlify config:
[[headers]]
for = "/sw.js"
[headers.values]
Cache-Control = "public, max-age=0, must-revalidate"
A CDN-cached service worker keeps an old worker controlling the page — and since the worker is
what decides freshness for everything else, that failure is unrecoverable from the client.
Netlify already defaults to this, but it is far too load-bearing to leave riding on a platform
default.
What the tests are for
There is a Playwright suite and a set of node --test unit tests. On a hobby game that can
look like ceremony, but the unit tests all cover the same category of thing: rules that are
easy to get subtly wrong and impossible to eyeball. Score multipliers. Need drain rates per
day phase. Whether a generated house is actually connected.
You cannot verify “every room reaches the hallway” by playing. You can verify it in
milliseconds, a thousand seeds at a time.
Play it
dads-on-duty.netlify.app
It runs in any modern browser and installs as a PWA. WASD to move, E to interact, Space
to pick up the baby, Esc to pause — the pause screen has a live map of the house showing
exactly where you and the baby are. Bring a friend for co-op, or take the Daily Challenge and
argue about the leaderboard.