Ocean Battle — Game Design Specification

Design specification · rev D · for firmware

OceanBattle

Two consoles, one umpire. For the nerd in us all, here are more details than anyone could ever want. Well except someone that want's to build their own. I have that stuff too.

Consoles 2 × ESP32-S3
Grid 12 × 12 NeoTrellis
Panel 3.5″ 480×320 IPS
Control Grid + encoder
Authority Brain
01

Architecture

The brain is the umpire and the sole owner of game state. Consoles are terminals: they report presses and encoder motion, and they render exactly what they are told to render. No console computes a hit, decides a turn, or knows anything about the opponent's fleet.

This costs a little protocol verbosity and buys three things that are otherwise expensive. Desync between the two consoles becomes structurally impossible, since there is only one copy of the truth. A console can be yanked and replugged mid-game and recover completely, because the brain simply re-pushes the full board. And information leaks become impossible by construction — a console cannot reveal what it was never sent.

The leak rule

When Announce Ship Sunk is off, the attacker's console is never told which ship went down. Not told and instructed to hide it — never told. The same principle governs every fact in the protocol: each console receives only what its player is entitled to know.

Host

Both consoles show the title screen at power-up. Whoever presses the dial first becomes the host — the brain honours the first packet it receives and the other console falls to standby. There is no port-order privilege, so the two players never have to swap seats to swap roles.

The host chooses board size and rules, and fires first. There is no coin flip and no turn-order prompt.

StateOwnerShared?Notes
Board sizeHostYesPushed to guest before placement
RulesHostYesDisplayed on the guest's standby screen
Turn orderBrainYesHost fires first, always
Placement methodEach playerNoOne may auto-place while the other places by hand
VolumeBrainYesOne speaker, so one volume. Either console may set it; last write wins
BrightnessConsoleNoLocal setting

Brightness is per-console because each console has its own panel. Volume isn't a console setting at all — the single speaker lives in the brain, and so does its control, on the brain's own screen and dial. Board size and rules are the only shared settings, and they're host-owned, which reduces the whole "first entry wins" problem to one arbitration at the title screen.

02

Mode state machine

Every transition below is caused by a brain message. A console never changes mode on its own initiative — it asks, and the brain answers.

BOOT TITLE MENU_MAIN MENU_SIZE host only STANDBY_WAIT guest — “P1 is choosing” PLACE_METHOD — per console PLACE_AUTO PLACE_MANUAL READY — “Stand by for other player” PLAY_MY_TURN PLAY_THEIR_TURN brain alternates · host first GAME_OVER — full fleet reveal, both consoles MENU_RULES MENU_IN_GAME LINK_LOST entered from any state PRACTICE one console detected lost the bid resume rematch · new game · back to title
Fig. 02.1 — Console modes. Yellow marks the two states where the grid accepts input.
ModeGridEncoderPanel
TITLEAttract mode — rainbow snakePress bids for hostTitle, “Press Dial to begin”
MENU_MAINDarkRotate to move, press to selectMenu list
MENU_SIZEDarkRotate, pressFour sizes + back
STANDBY_WAITDarkVolume / brightness only“Player 1 is choosing…”
PLACE_AUTOShows the generated fleetRotate: Shuffle / OK. Press to selectFleet list, Shuffle / OK
PLACE_MANUALCell selection, live validationRotate to pick ship, press to confirmCurrent ship, remaining, refusal reason
READYOwn fleet, staticVolume / brightness“Stand by for other player”
PLAY_MY_TURNFiring view. Arm, fireRotate: volume. Press: menu. (Salvo: press commits)Own fleet + status
PLAY_THEIR_TURNOwn fleet. InertRotate: volume. Press: menuFiring view + status
GAME_OVEROwn fleet, full revealRotate, pressResult, opponent's fleet, options
LINK_LOSTDarkInert“Link lost — check cable”
The surface swap

The grid and the panel trade roles every turn: the grid is your firing board on your turn and your own fleet on theirs. Two invariants keep this legible. White never appears on the firing view — so a glance at the grid tells you whose turn it is without reading a word. And the panel carries a permanent header, YOUR FLEET or YOUR SHOTS, naming what it is showing.

During PLAY_THEIR_TURN the grid is displaying your own ships while your hand is resting on it, so presses must be dead. Not silently dead — a brief dim-and-restore on the pressed cell acknowledges the press and communicates “not now” better than nothing happening at all.

03

Menus

One encoder means one gesture vocabulary: rotate, press. Keep every list two levels deep at most, and end every list with an explicit way back — nobody discovers a long-press on their own.

Out of game

OCEAN BATTLE
  Start Game
      6 x 6
      8 x 8
      10 x 10
      12 x 12
      < back >
  Rules
      Hit — Fire Again      [NO]
      Salvo                 [NO]
      One-Hit Sink          [NO]
      Ships Can Touch       [YES]
      Announce Ship Sunk    [YES]
      < back >
  Brightness: 6            this console's panel only
  Back to Title

Rules is top-level, not a step in the start flow. Most games use the same rules as the last one, and burying them inside Start Game makes them friction every single session.

Placement method is not in this menu. It is a per-console prompt that appears after the host commits a board size, so one player can auto-place while the other places by hand.

Volume is not on the console. The single speaker lives in the brain, and so does its control — a 0–5 dial on the brain's own screen, where 0 renders as OFF. Nothing about volume crosses the link, so the console menus carry only Brightness, which dims that console's own panel. This deletes the old two-controls-for-one-thing problem entirely — there is no volume on the console to contradict anything.

In game

Brightness: 6              this console's panel only
End Game
    Restart        same size and rules, re-place fleets
    Back to Title
    < back >
Resume

Ending the game takes effect immediately on both consoles — no cross-console confirmation, no permission request. The two players are sitting at the same table and can speak to each other. The opponent's console does get told why it left the game, so it shows PLAYER 1 ENDED THE GAME rather than dropping to the title with no explanation.

Likewise, a player wandering into settings on their own turn is not a state the firmware needs to broadcast. They will get told to hurry up by the person opposite.

04

Rules

Five toggles, host-owned, shared, persisted. All five are available at every board size.

RuleDefaultEffect
Hit — Fire AgainNOA hit grants another shot, repeating while you keep hitting. Shortens games; a hot streak can end a 6×6 almost immediately.
SalvoNOFire one shot per surviving ship, all declared before any result returns. Accelerates the winner and handicaps the loser, which is what fixes long games on big boards.
One-Hit SinkNOAny hit sinks the whole ship. Much faster, much lower skill ceiling — length becomes target size and nothing else.
Ships Can TouchYESOff forbids contact including diagonals, shrinking the legal placement space and making the hunt harder.
Announce Ship SunkYESOff tells the attacker only that something sank. Withholding the name is meaningfully harder — you cannot infer what lengths remain.

Interactions the firmware must handle

Salvo greys out Hit — Fire Again. An extra full volley per hit compounds catastrophically: five shots landing three hits would spiral immediately. When Salvo is on, the Hit — Fire Again row is shown greyed with the reason on the panel rather than silently ignored.

No-touch needs a robust auto-placer. With contact forbidden, three ships plus their exclusion halos nearly fill a 6×6, and incremental placement can paint itself into a corner. Implement as full restart on failure with an attempt cap, not backtracking — simpler and faster in practice.

Sunk display is gated per player, not globally. The defender always knows their own ship died, so their fleet view goes dark regardless of the Announce setting. Only the attacker's view is gated: with Announce off, sunk cells stay red on the firing view because the attacker has not been told.

Where rules are shown

The guest never chooses the rules, so the guest must be told them before placing a fleet. The standby and placement screens both carry a rules strip: Rules: Salvo · No-Touch, listing only non-default settings. A player placing ships without knowing whether contact is legal is being set up to fail.

05

Fleets

Eight hulls, each with a distinct root word. Same-length pairs — the two 4s and the two 3s — carry no disambiguating hit-count, so their names do all the work and are deliberately far apart.

CellsName6×68×810×1012×12
6Mega Carrier
5AntiSub Cruiser
4Battleship
4Frigate
3Submarine
3Destroyer
2Patrol Boat
2Torpedo Boat
BoardCellsShipsShip cellsDensityOpening salvo
6 × 6363719%3
8 × 86441219%4
10 × 1010051717%5
12 × 1214482920%8

The 10×10 fleet is the classic complement at the classic density. The 12×12 is deliberately denser — 144 cells hunting a sparse fleet is a twenty-minute game — and it is the board where Salvo earns its keep.

06

Setup & placement

Auto

The brain generates a legal fleet and pushes it to the console, which lights it on the grid in white while the panel offers Shuffle and OK. Shuffle requests a fresh layout; OK commits.

An auto-placed fleet is editable — press any ship to lift it back into selection and reposition it. This costs nothing once the manual validator exists, and it turns Shuffle from take-it-or-leave-it into a good starting point.

Manual

Rotate to choose a ship. Press the cells it occupies. Press a selected cell again to drop it. Encoder press confirms, and stays inert until the selection is legal.

Validity is three conditions, and between them they reject bends, gaps and wrong lengths:

  • Every cell shares a row, or every cell shares a column
  • Span from lowest to highest equals the number of cells selected — no gaps
  • That count equals the ship's length

Plus, when Ships Can Touch is off, no selected cell may be orthogonally or diagonally adjacent to a committed ship.

Refusals are explained, never silent. The panel says which condition failed — SHIPS MUST BE IN A STRAIGHT LINE, 3 OF 5 PLACED, SHIPS MAY NOT TOUCH — while the encoder simply does not respond to a press. An inert control with a stated reason beats a beep.

There is no “Replace ship?” dialog. Pressing a placed ship lifts it. Direct manipulation handles what a modal would otherwise interrupt for.

Committing

When every ship is placed, the panel offers Ready. Confirming sends the fleet to the brain and the console enters READY: STAND BY FOR OTHER PLAYER, with the grid holding the fleet in white so it can still be reviewed. When both fleets arrive, the brain starts the game with the host firing first.

07

The play loop

Standard fire

Press a cell to arm it — flashing yellow. Press the same cell again within the window to fire. Because the window is a full five seconds, that first press is genuine aiming, and the cursor is a real UI element with its own rules:

  • A press on any other cell only disarms. Whatever you press within the window — fired or unfired, adjacent or across the board — clears the armed cell and arms nothing. That press is spent entirely on clearing, exactly as if the 5 seconds had elapsed. To arm the new cell you press it again.
  • So the cursor has exactly two ways to end: press it a second time to fire, or clear it (any other press, or the timeout) and start over. There is no way to move an armed cursor — arming is always a fresh press on an idle cell.
  • The trade is deliberate: a stray brush can cost you your aim, but it can never cost you a shot.
  • Expiry is visible — the cursor fades over the final second rather than vanishing mid-decision.
  • Encoder press cancels the cursor, since the encoder is otherwise idle during your turn.
  • Already-fired cells are blocked as targets. Pressing one clears any armed cursor like any other cell, but it cannot itself be armed — brief flash, ALREADY FIRED on the panel, nothing armed.

On the second press the cell fast-flashes yellow, then resolves to red or dim green. Results are computed by the brain and pushed to both consoles simultaneously — the attacker sees a hit appear on their firing view, the defender sees the same cell go red on their fleet at the same instant.

Salvo fire

Salvo declares all shots before any results return — that is the entire point, and it means the double-tap-resolve gesture cannot be used. Salvo gets its own interaction:

  1. Press cells to arm them, each flashing yellow, up to your surviving-ship count
  2. Press an armed cell again to disarm it
  3. Encoder press commits the whole volley
  4. One FIRE tone plays at commit. Then results resolve one cell at a time, each with its own tone and animation — the tone and the button light in lockstep, so the sound always matches the cell currently resolving

The panel carries the count: VOLLEY: 5 OF 7 ARMED. A partial volley is permitted but the panel says so before committing, so nobody throws away shots by accident.

With Announce Ship Sunk on, a single volley can sink more than one ship, so the sink message must handle a list rather than one name.

Turn end

After the dwell expires the brain advances the turn and both consoles swap surfaces. With Hit — Fire Again on and a hit landed, the turn does not advance and no swap occurs — the panel says HIT — FIRE AGAIN so the reason for the missing swap is explicit.

Game over

When a player's last ship sinks the brain declares the result and pushes both fleets to both consoles. Each grid shows its own fleet in final state; each panel shows the opponent's complete layout — including the ships that were never found, which is the most interesting information in the game and the reason to reveal at all.

Options, on both consoles, host selects:

  • Rematch — same fleets, same rules, straight back into play
  • New Game — same rules, re-place fleets
  • Back to Title — host bidding starts over
08

Colour language

This is what players actually read, and it is the spec most likely to be got wrong by eye later. Two principles govern the whole table.

Brightness carries meaning independently of hue. Hits are bright, misses and water are dim. This keeps hit-and-miss separable for a colourblind player — red and green being the classic collision — and it makes the interesting cells pop out of the board. It also keeps 144 pixels inside the current budget, since the dominant colour is dim.

White appears only on your own fleet. The firing view is blue, green, red and yellow, never white. That single rule is what makes the two views distinguishable at a glance through diffusing silicone.

Waterdeep blue, low bright
Ship intactbright white · own fleet only
Hitbright red · both views
Sunkdark, dim red retained
Missdim green · both views
Cursor armedflashing yellow
Firingfast flash → result
Selected, not legalpulsing white
Placedsolid white
Invalid pressred flash → off
Inertoff
Sunk keeps its history

A sunk ship going fully dark would destroy the record of where you hit — which is exactly what you use to infer what is left afloat. Sunk cells go dark but retain a very dim red, so the blackout still reads as a kill while the plot stays legible.

Two pairs were deliberately separated. Water is deep and dim rather than light blue, because a pale blue through silicone lands very near white and those are the two colours that must be told apart most often. The cursor is yellow rather than orange, for a wider gap from red — the cursor must never read as a result.

Attract mode

At the title screen the grid runs a rainbow snake — a short trail, eight to twelve cells long, wandering the board with hue advancing along its body and cycling slowly over time. It never reverses into itself and it bounces off the edges; there is no collision to detect because nothing is at stake.

Two reasons this is the right attract mode rather than an ambient wash of the whole board. It draws a fraction of the current, since only a dozen pixels are lit at once. And a moving object reads as alive from across a room in a way that a slow fade does not, which is the entire job of an object sitting on a shelf.

The snake is drawn by the console, not the brain — the brain says MODE: TITLE and the console owns every frame. The two consoles run independent snakes and will drift out of step, which is correct: they are two separate objects, not one display.

What each surface shows

Your fleet

Grid on their turn · panel on yours

Your shots

Grid on your turn · panel on theirs

Fig. 08.1 — Mid-game, 10×10. Note the absence of white on the right.
09

Sound

Six sounds, one speaker, and the speaker lives in the brain. No menu ticks, no key clicks, no turn chimes, no confirmation blips. The grid and the panel already confirm every press visually; a sound that only says “you pressed a thing” is noise.

SoundFires whenNotes
FIREA shot is committedPlays on the fast-flash, before the result
MISSResult resolves to water
HITResult resolves to a hull
DESTROYEDA ship's last cell is hitReplaces HIT on the killing shot — never both. Suppressed entirely when Announce Ship Sunk is off
WINNERLast ship of a fleet sinksPlays once, heard by both
VOLUMEVolume value changesPlays at the new level. The one permitted menu sound

The volume tone

Stripping out menu sounds leaves the volume control with nothing to be judged against — you turn the dial and hear silence until somebody fires. So volume gets its own tone, played at the level being set. It lives entirely on the brain, on the same screen and dial as the speaker it controls.

This is the single exception to the no-menu-sounds rule, and it holds because it is not confirming a press. It is the setting, made audible.

  • Constant pitch, varying amplitude. If the pitch rose with the level too, you could not tell which of the two you were hearing — and higher pitches already read as louder. Only loudness should change, or the tone is not an honest test of loudness.
  • One tone when the dial settles. Turning tracks the level silently; a beat after the knob stops (~120 ms), a single tone plays at the level you landed on. That keeps a fast spin from machine-gunning the module while still telling you exactly where you ended up.
  • Level 0 plays nothing, which is the correct demonstration of OFF.
  • No cross-link message. Volume lives on the brain, which owns the speaker, so it sets the level and plays the tone locally. Nothing about volume travels over the link, and the protocol carries no volume message at all.

What one shared speaker changes

Sounds become events in the world, not feedback to a player. A single speaker in the middle of the table cannot say “you were hit” — it can only say that something was hit. That is thematically stronger than two consoles chirping at their owners, and it costs nothing, because the panel already names ownership in words. The speaker carries the event; the screen carries whose it was.

Volume lives entirely in the brain. One speaker, one volume, one place to set it — the brain's own screen and dial, saved in brain NVS. It never becomes shared console state, so there is no cross-console sync, no last-write-wins, and no volume in the console menus at all. Brightness stays per-console, since each console has its own panel and grid.

Announce off silences the defender too

A shared speaker cannot tell one player something and not the other. So when Announce Ship Sunk is off, DESTROYED is suppressed for everyone — including the defender, who is entitled to know and will still see their own cells go dark. That is the price of one speaker, and it is a small one, but it is a real asymmetry with the panel, which still tells the defender by name.

Sequencing

  • Salvo fires once. One FIRE tone at volley commit, not one per shot — eight in a row is a stutter, not a broadside.
  • Then each cell resolves in turn, tone locked to its own animation. A cell's HIT, MISS or DESTROYED tone plays while that cell runs its animation; the next cell does not begin until the current one's clip finishes. The audio is therefore always describing the button that is lit — never a cell ahead or behind.
  • Resolution order is the brain's to choose and need not match arming order. Whatever sequence it picks, the tone follows the light. A natural choice is misses first and the killing blow last, so a volley builds rather than fizzles — but that is a flavour decision, not a correctness one.
  • A volley may sink more than one ship, so DESTROYED can play more than once in a single resolution sequence.
  • WINNER waits for the final DESTROYED to finish rather than overlapping it.

Hardware consequence

Audio moves off both consoles and onto the brain: one I2S amplifier and one speaker instead of two of each. Both ESP32-S3s get their audio pins back, and the brain gains an I2S peripheral it did not previously need. This is a change to the wiring document, not just this one.

10

Screen copy

Strings are IDs in the protocol, not literals — the brain sends STR_SANK_THEIRS plus a ship index, and the console composes. That keeps the wire narrow and makes wording changes a console-side edit.

IDTextWhen
STR_TITLEOCEAN BATTLE / Press Dial to beginTitle
STR_WAIT_HOSTPLAYER 1 IS CHOOSING…Guest, host in menus
STR_STANDBYSTAND BY FOR OTHER PLAYERFleet committed, opponent still placing
STR_SETTINGS_HINTPress Dial for SettingsSmall, bottom corner, standby and play
STR_PLACEPLACE SHIPSManual placement header
STR_PLACE_COUNT%d OF %d PLACEDPartial selection
STR_ERR_LINESHIPS MUST BE IN A STRAIGHT LINEBent or gapped selection
STR_ERR_TOUCHSHIPS MAY NOT TOUCHNo-touch rule violated
STR_READYREADY?All ships placed
STR_YOUR_FLEETYOUR FLEETPermanent panel header
STR_YOUR_SHOTSYOUR SHOTSPermanent panel header
STR_YOUR_TURNYOUR TURNTurn start
STR_THEIR_TURNTHEIR TURNTurn start
STR_ALREADYALREADY FIREDBlocked repeat shot
STR_HITHITAttacker, hit
STR_MISSMISSAttacker, miss
STR_HIT_TAKENTHEY HIT YOUR %sDefender, hit taken
STR_SANK_THEIRSYOU SANK THEIR %sAttacker, Announce ON
STR_SANK_ANONSHIP SUNKAttacker, Announce OFF
STR_SANK_YOURSTHEY SANK YOUR %sDefender — always named
STR_FIRE_AGAINHIT — FIRE AGAINHit—Fire Again, turn not advancing
STR_VOLLEYVOLLEY: %d OF %d ARMEDSalvo arming
STR_VOLLEY_PARTCOMMIT %d SHOTS?Salvo, under-armed
STR_WINFLEET DESTROYED — YOU WINGame over, winner
STR_LOSEYOUR FLEET IS LOSTGame over, loser
STR_ENDEDPLAYER %d ENDED THE GAMEOpponent quit
STR_LINKLINK LOST — CHECK CABLEHeartbeat timeout
STR_PRACTICEPRACTICE — ONE CONSOLESingle console detected

Note that STR_SANK_YOURS is always named while STR_SANK_THEIRS is gated. The defender knows their own fleet; only the attacker's knowledge is rationed.

11

Message set

UART at a deliberately low baud (19 200) with generous filtering, brain-authoritative. Consoles report physical events; the brain returns render instructions.

Cell state enum

Eleven states, so four bits. A dirty-cell update is one byte of index — 144 cells fits comfortably — plus a nibble of state, packed two cells to three bytes.

ValNameColour
0WATERDeep blue, dim
1SHIPWhite — own fleet only
2HITBright red
3SUNKVery dim red, keeps the plot legible
4MISSDim green
5ARMEDSlow-blink yellow (reserved — aiming is console-local)
6LAUNCHINGFast-blink yellow — brain-commanded during the launch phase
7OFFInert / dark

Animation is console-side. The brain sends a state, not a frame — LAUNCHING means “fast-blink this yellow until told otherwise,” and the console owns the blink rate. This is what keeps the link at a few hundred bytes a second.

Aiming and placement visuals are console-local and never cross the link. The slow-blink armed cursor, the pulsing “pending” ship during manual placement, the confirmed-white and the red reject-flash are all rendered by the console itself — the brain isn't told about them and carries no state for them. The brain only hears the committed result (a FIRE, or a finished placement). That's why ARMED sits in the enum as reserved: the launch phase uses LAUNCHING, while the armed cursor is drawn locally.

Console → Brain

MessagePayloadMeaning
HELLOfw version, player idConsole booted; player id from the ID resistor
BID_HOSTDial pressed at title
KEYindex, down/upGrid press
ENCsigned deltaEncoder rotation
ENC_PRESSEncoder pressed
SET_SIZEboard sizeHost committed a board
SET_RULErule id, valueHost toggled a rule
SHUFFLERequest a fresh auto fleet
FLEET_OKship list, cellsPlacement committed
FIREcellStandard shot
VOLLEYcell listSalvo, committed
END_GAMErestart / titlePlayer ended the game
AGAINrematch / new / titleGame-over choice
PINGHeartbeat

Brain → Console

MessagePayloadMeaning
ROLEhost/guest, player no.Result of the host bid
CONFIGsize, rule flagsShared settings — pushed to both
MODEmode idEnter this mode
FLEETship list, cellsAuto-generated layout to display
CELLSdirty list: index + stateThe core render message
CELLS_ALLfull boardRecovery after link loss
PANELscreen id, argsSelects a fixed console string; args fill its blanks
TURNmine / theirs, shotsTurn ownership and salvo count
RESULTcell, outcome (miss/hit/sink), ship id or noneShot outcome, already gated
OVERwin/lose, both fleetsGame over and reveal
PONGHeartbeat reply

There is no audio message in either direction. The brain owns the speaker and plays its own sounds as it resolves each shot, so the consoles never learn that a sound occurred — which is one more thing they cannot leak.

Gating happens at the brain

RESULT carries a ship id only when the receiving player is entitled to it. With Announce Ship Sunk off, the attacker's RESULT has no ship id at all — there is nothing on the console to leak.

12

Timings

EventDurationNotes
Cursor window5 sLong enough that the first press is real aiming
Cursor fadefinal 1 sExpiry must be visible, not sudden
Launch — sound on= FIRE clipNeutral fast-yellow, held for the launch clip (via the BUSY pin)
Launch — sound off0.4 sFixed snappy beat when volume = 0
Result — sound on= result clipMISS / HIT / DESTROYED clip length (via BUSY)
Result — sound off0.6 sFixed snappy beat when volume = 0
Salvo reveal1 launch + result / cellOne launch for the whole volley, then each cell resolves in turn
Rejected press flash0.3 sRed, then off
Heartbeat500 ms
Link-lost threshold2 sFour missed beats
Idle timeout10 minAttract mode only, never in-game; then grid dark, panel asleep, wake on any press
Sound owns the clock

When sound is on, the hold is however long the clip runs — a 1.5 s splash gives a 1.5 s miss animation, a three-second explosion gives a three-second sink. The brain doesn't need to know the clip's length: it watches the DFPlayer's BUSY pin and holds the render state until the clip finishes, then advances. Swap a sound file for a longer one and the animation stretches to match with no code change.

When sound is off (volume 0) the clip still plays silently, so the brain ignores BUSY and uses short fixed beats instead — launch ~0.4 s, result ~0.6 s — keeping a muted game snappy. There is no skip: the shot sequence is uninterruptible, and a press during resolution does nothing. Turning the volume off is the fast path, so there's no separate skip control to collide with the fire button.

The pairing is a constraint, not just a convenience

Tying the two together means the sound files now set the pace of the game. That is fine for hit and miss, but FIRE plays before the result and must stay short — a long launch sound is dead air on every single shot, sixty times a game. Keep FIRE well under a second; spend the length budget on DESTROYED, which is rare. And in Salvo, FIRE plays once at commit while each cell still waits for its own clip, so a slow HIT clip multiplies across a volley — worth hearing on hardware before locking the clip lengths.

13

Persistence

Settings survive a power cycle, stored where they are owned.

SettingStored onStore
VolumeBrainNVS — one speaker, one setting
BrightnessConsoleNVS
Rules (all five)BrainNVS
Last board sizeBrainNVS — pre-selects in the size menu

Nothing about an in-progress game is persisted. A power cut ends the game, which is the honest behaviour for a tabletop object.

14

Practice & link loss

Practice mode

If the brain sees only one HELLO, it offers practice: the single console plays against the brain, which places a fleet and takes turns. Every rule, board size and interaction works identically — the brain is already the umpire, so the only new code is an opponent that chooses cells.

That opponent is hunt-and-target. It fires at random until it lands a hit, then switches to working the four orthogonal neighbours of that hit; once two hits line up it follows the axis until the ship sinks or the trail runs dry, then returns to random. Perhaps thirty lines, and the difference between a test harness and something worth playing.

This is worth building early. It exercises the entire play loop with half the hardware, which matters a great deal while the second console is still a pile of parts.

Link loss

Four missed heartbeats and the console enters LINK_LOST: grid dark, LINK LOST — CHECK CABLE on the panel, all input inert. Freezing on a stale board while quietly doing nothing is the failure mode to avoid — a dead grid is honest and a frozen one is not.

On reconnection the brain pushes CONFIG, MODE and CELLS_ALL, and play resumes exactly where it stopped. Brain-authoritative state is what makes this a non-event rather than a lost game.

15

Still open

Tone character

Six slots are defined; what actually plays is not. DESTROYED is the one worth spending effort on — it is the moment the whole game is built around, and it is rare enough to be allowed some length. VOLUME wants the opposite: short, neutral, and deliberately unlike any game sound, so it never reads as an event on the board.

Ocean Battle · game design rev D · companion to the console and brain wiring document