Domdhi logoDOMDHI.OS
TECH

Monster Mash League: 98 Monsters, Five Days

Monster Mash League is live: browser baseball with 98 original Halloween monsters, solo or head-to-head online. First commit October 25, done October 30, launched on Halloween. No runtime dependencies, and a simulation that two browsers keep in lockstep by comparing hashes.

DDominic BacaSOLO ENGINEER · DOMDHI.OS2026.11.013 MIN READ
TECH// FIG.01

Monster Mash League is live at mash.domdhi.com. Browser baseball with 98 original Halloween monsters. No account, no install, free.

First commit was October 25. It was done October 30: 121 commits in five days. I launched it on Halloween.

Home Run Derby: a horned knight at the plate under a full moon, a ghost on the mound

What you can play

Four ways in:

  • Home Run Derby. Hold to swing, release to hit. Solo, or a duel against a friend online.
  • Perfect Game. You pitch. Aim, speed and accuracy meters. Keep every batter off base. Solo or head-to-head.
  • Play Ball. A full game against the CPU, a two-player room with a code, or Quick Match to get paired with a stranger.
  • League Challenge. Take your club through a championship run and unlock The Legends.

The main menu: Play Ball, Watch, League Challenge, Home Run Derby, Perfect Game, and the online modes

There's a tutorial you can skip, a Practice mode, a CPU-vs-CPU Watch mode, custom clubs, badges, and career records that stay on your device. Touch, keyboard, mouse and gamepad all work. Portrait and landscape both work. Reduced motion is supported. The commentary is Howlin' Hank and Gravely Graves, and the music is generated in the browser.

Every monster has its own power and contact ratings, so picking a slugger is a real decision:

Pick your slugger: Wrap Battle, Dead Weight, Bonafide Second, Designated Lich and more, each with PWR and CON bars

It's two players, max. I built a party mode for more and then cut it on October 29. Two people, one ball, nothing to arbitrate.

The part I care about: nobody sends the ball

Online baseball has a problem. The ball's flight is physics, and physics sent over a network is lag, rubber-banding and arguments about who was safe.

So I don't send the ball. I send decisions.

The whole game is a pure function of three things: (setup, seed, decisions[]). Pitch type and meter results. Swing, take or bunt, with timing in integer milliseconds. That's it. Each browser runs the full simulation locally, at 120 ticks per second, and gets the same game. The server (a Cloudflare Worker and a SQLite-backed Durable Object per room) doesn't simulate anything. It orders the decisions, stamps them, and relays them.

Frames are JSON, one kilobyte max. The room mints the seed, so neither player picks it.

Same game on both ends, on purpose

That only works if the simulation is deterministic down to the bit. So the headless code has rules, and the linter enforces them:

  • No Date, no performance, no setTimeout, no Math.random.
  • No sin, cos, pow or exp. Trig comes from a 0.1-degree table built from integer series, because different JS engines can disagree about the last digit of a float.
  • Every outcome roll draws a fixed number of random values. Draw first, then branch.
  • Nine separate seeded RNG streams, keyed by (seed, pitchNo), so a change in one system can't shift another's dice.

After every pitch, both clients send a 32-bit FNV-1a hash of the game state: count, score, bases, pitcher pitch counts, RNG positions, tick. The room compares them. Match means play continues. Mismatch means the room asks for six section hashes, writes one log line, and tells both clients to throw their state away and replay from the seed through the decision log.

If the rebuilt hashes still differ, the match ends with no result. Nothing is credited. I'd rather void a game than record a wrong score.

A desync_recovered line in the logs counts as a bug, not a feature.

Receipts

As of today:

  • 75,928 lines of TypeScript, not counting tests.
  • 457 test files, plus Playwright end-to-end suites that include WebKit.
  • 15 architecture decision records.
  • 32 epics and 138 stories planned before the build, with the plan run wave by wave.
  • No runtime dependencies. TypeScript, esbuild, WebGL2 rendering, procedural Web Audio, installable as a PWA with offline support.

Any change to the simulation code or sim-relevant data changes an ENGINE_BUILD hash. The WebSocket URL carries it, and the room refuses to pair two clients on different builds. A deploy can't quietly split two players onto different rules.

Play it

Swing at mash.domdhi.com. Send a friend a room code or a challenge link and try to beat their result on the same seed.

If the hashes ever disagree, you'll find out first.

GET_TRANSMISSIONS

Raw signal on finance, freedom, fitness, and tech. No spam. Unsubscribe anytime.

D
Dominic Baca

One engineer building Domdhi.OS in public — money, freedom, and the occasional 2am migration. Every number live, every commit public.

FOLLOW THE LOGS →
// NEXT IN THE LOGS