- Published on
Naval Zone - Rebuilding Battleship
- Authors

- Name
- Emir Mujcevic
- @em____97
Introduction
Battleship is a game everyone already knows. Two grids, a fleet you hide, and a slow guessing match until someone runs out of ships. That familiarity is exactly why I wanted to build it.
When the rules are given to you, nothing hides behind them. You can't excuse a clumsy interface by saying the concept is complicated, because the concept is a piece of paper with dots on it. Everything that feels good or bad about the result comes down to design and craft.
So I built Naval Zone: a modernized, real-time two-player Battleship. No login, no profiles, no matchmaking queue. One player creates a room, shares a short code or an invite link, and the match starts.

The shape of a match
The whole game is four screens, and each one only does one thing.
You create or join from the home screen with a short room code. The lobby shows the code and a presence indicator, so you can actually see the moment your opponent arrives instead of staring at a static screen wondering if the link worked. In placement, you drag your fleet onto the grid, rotate ships, and mark yourself ready. Then the battle: alternating turns on a 3+ blitz clock, hit / miss / sunk resolution, power-ups, and a live combat log running down the side.
At the end there's a result screen with an isometric recap of the final board and the full match statistics: hits, misses, accuracy, ships destroyed, longest hit streak, fastest and slowest turn.
The fleet is five ships on a 10×10 board: Dreadnought (5), Titan (4), Phanton (3), Cobra (3), Reaper (2). The two sides are styled as Agent and Nemesis, which was mostly an excuse to avoid the usual naval-grey palette and build something closer to a tactical interface.

The part that was actually hard
Drawing the grid was the easy half. The difficult half was that two browsers, on two devices, have to agree on the same board without ever contradicting each other.
The naive version of this breaks quickly. If both clients independently resolve the same shot, you eventually get a state where one player sees a hit and the other sees a miss, and from that point on the game is quietly broken for both of them.
The rule I landed on is shot ownership. The player who fires resolves the shot locally, then writes the complete result into the shared state. The receiver trusts that result and never re-resolves it. There is exactly one place where a shot becomes true, and it's the shooter's client.
Underneath that, persistence is the source of truth. The full game state is written into a Supabase rooms.game_state JSONB column on every change, and the opponent syncs off that single Postgres UPDATE stream. There's no separate broadcast channel running alongside it, which means a move physically cannot be applied twice. The same column rehydrates state when you join or rejoin, so closing a tab mid-match isn't fatal.
That single decision solved reconnection, desync, and spectating-your-own-refresh all at once, which is usually the sign that it was the right one.

Keeping the code honest
The rest of the architecture follows from wanting the game to stay readable.
All the actual game logic lives in src/game/ as plain TypeScript with no React imports at all: board construction, placement rules, shot resolution, statistics. It's pure, so it's testable without mounting a single component. React never gets to own a rule.
State is split in two. GameContext owns all game state and the actions that change it. RealtimeContext is transport only, persistence and presence, nothing else. They only meet in the view layer.
There are also only two routes in the entire app, / and /room/:code. The screen you see inside a room is driven by gamePhase (lobby → placement → playing → result), not by the URL. That keeps the invite link stable: you send someone one link, and it works no matter what stage the match is in when they open it.

Tools I Used
React 19 with Vite and TypeScript, CSS Modules and SCSS with design tokens as CSS variables, React Router v7, and Supabase for realtime and anonymous auth. Hosting is Vercel via GitHub.
Anonymous auth was worth the setup. Every client signs in silently, and that uid becomes the player identity that row-level security keys off, so only the two people actually in a room can write to it and a third visitor with the code is sealed out. The player never sees a login screen.
Design happened in Figma first, as always. Build happened with Claude Code.

Next Steps
- Real accounts (if the game gets some traction, no more anonymous-only rooms).
- User profiles, persistent stats, and a ranked ladder.
- More power-ups, and a better sense of when to spend them.
- A proper mobile mode.
Conclusion
The interesting thing about rebuilding a game this old is that you can't improve it by adding rules. Every decision I made was about the space around the rules: how fast the room fills, how clearly a hit reads, whether the recap makes you want to hit rematch.
The game underneath is the same one you played on paper as a kid. Everything else is design.
You can play it at naval.zone.
Thank you for reading.
Yours truly,
Emir