Морской бой (navy-fleet-battle)

July 30, 2026 · View on GitHub

Version 1.3.2

An Android implementation of Морской бой, the classic Russian pen-and-paper sea battle game. It speaks Russian and English, and is designed to be operated comfortably by children: large boards, generous touch targets, high contrast, and a confirmation step before every shot.

Rules

The game implements the classic Russian rules strictly, with no variants and no options screen.

Field. 10x10. Rows are labelled with the Russian letters А Б В Г Д Е Ж З И К (Ё and Й are skipped), or with A to J when the game is in English; columns are numbered 1 to 10.

Fleet. Ten ships, twenty cells:

ShipSizeCount
Линкор41
Крейсер32
Эсминец23
Катер14

Placement. Ships are straight lines, horizontal or vertical, never bent. Ships may not touch each other, including diagonally — every ship is surrounded by empty water. This is the main structural difference from the Western version of the game, where ships may be adjacent.

Play. Players alternate shots. A hit grants another shot: the shooter keeps firing until they miss, and the turn passes only on a miss. This is the second major difference from the Western version, where a turn is exactly one shot.

Sinking. Because ships cannot touch, every cell surrounding a sunk ship is guaranteed to be empty, and those cells are marked automatically on the shooter's tracking board.

Victory. The first player to sink all ten enemy ships wins.

Features

  • Single player against the computer, and pass-and-play for two people on one device.
  • Manual ship placement with a live legality preview, orientation toggle, random fill and clear; «Готов» is enabled only once the whole fleet is legally placed.
  • Portrait and landscape layouts. Game state survives rotation.
  • Aim-then-fire confirmation: the first tap aims and shows a crosshair, the second tap or the «Огонь!» button fires. This prevents accidental shots from small fingers.
  • An animated sequence for the computer's shot: a reticle converges on the target cell while its row letter and column number light up on the board axes, followed by an impact and a distinct result — a cold splash for a miss, a hot burst for a hit.
  • A sinking plays as two beats: the hit lands first, with its burst and its own sound, and only then does the hull flash gold and go down. The shot that sinks a ship is a hit before it is a sinking, and it is shown and heard that way.
  • An end-of-game overlay drawn over the final boards, carrying the result as a picture as well as text so that a child who cannot yet read still understands the outcome.
  • A hand-off screen between turns in two-player mode, leading with a large player number so the device changes hands without anyone reading a word.
  • Sound throughout, with a mute control on every screen that remembers its setting.
  • Russian and English, with a language control next to the mute control on every screen.

Languages

The game is written in Russian and translated into English. Which one it opens in follows the device the first time it is run — Russian on a Russian device, English on any other — and after that the button in the corner decides. The choice is remembered.

The button shows the language it will switch to, so a player looking for English presses EN. Both codes are written in Latin whichever way round the game is, so neither of them is a word the reader has to already know.

Switching is immediate and can be done at any point, including in the middle of a battle: the language is applied by re-providing the resources the interface reads from rather than by restarting the activity, so nothing on screen is lost and there is no flash. Nothing the view model holds is text, so there is no game state to convert.

The board is labelled in the alphabet of the language in use: А Б В Г Д Е Ж З И К in Russian, which skips Ё and Й as a paper field does and therefore ends at К, and A to J in English. The rules engine works in indices and is not affected by either.

One limitation worth stating: the button governs the text inside the game. The name under the launcher icon is chosen by Android from the device language — «Морской бой» on a Russian device, "Navy Fleet Battle" elsewhere — and no in-app setting can change it.

Sound

Twelve effects, tied to the moments the animation already marks so that picture and sound stay in step: the sonar ping on aiming, the gun on firing, then a splash or an explosion as the shot resolves, and a breaking hull after the explosion when a ship goes down. Because the result is deliberately hidden until the animation finishes, the outcome sound is held back with it - nothing gives a hit away before it is shown.

A shot is played out as a sequence of stages, and its sounds are the sounds of those stages in order. A miss is one stage, an ordinary hit is one, a sinking is two. Both the picture and the audio read that same sequence, so they cannot tell different stories.

Eight of them are recordings, held in app/src/main/res/raw as 16-bit mono PCM at 48 kHz: the sonar, the gun, the splash, the explosion, the breaking hull, the ship's bell and the two end-of-game stingers. The remaining four - the tap, the tick, the thunk and the buzz - are synthesised in Kotlin, because a recording would not improve a click.

Each sound owns one AudioTrack in MODE_STATIC, so overlapping sounds are simply different tracks and nothing is mixed by hand. Recordings play at their own 48 kHz; the synthesised sounds are generated at whatever rate the device reports as native, so the platform resamples nothing.

The mute control sits in the top corner of every screen with a touch target sized for a child, and the setting is stored in SharedPreferences alongside the chosen language. Those two are the only things this app persists; a game in progress is not among them.

Sound credits

Every recording is CC0 - public domain dedication, no attribution required, no restriction on use or redistribution. That keeps the whole project uniformly licensed with no exceptions to document and nothing a downstream user has to check.

The clips that ship have been trimmed to a usable segment, faded at both ends, converted to mono at 48 kHz and levelled against each other.

Computer opponent

The computer plays deliberately simply. It fires uniformly at random among the cells it has not yet tried, and after a hit it hunts the rest of that ship in the orthogonally adjacent cells, restricting itself to the known line once two hits are lined up. When a ship sinks it writes off the surrounding water, which the no-touch rule guarantees to be empty. It has no access to the player's board and cannot see ship positions.

Requirements

  • Android 8.0 (API 26) or newer
  • JDK 21 to build
  • Android SDK with platform 36 and build-tools 36.0.0

Building

The project uses its own Gradle wrapper; do not use a system Gradle installation.

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export ANDROID_HOME=/path/to/android-sdk

./gradlew assembleDebug        # debug APK
./gradlew testDebugUnitTest    # unit tests
./gradlew assembleRelease      # release APK

The debug APK is written to app/build/outputs/apk/debug/.

A release build is signed only if the signing keystore is configured and reachable; without it the build succeeds and produces an unsigned APK, which is what a fresh clone gets. See RELEASING.md for the release procedure, including the two build settings that keep the published binary reproducible from source.

Installing on a connected device:

adb install -r app/build/outputs/apk/debug/navy-fleet-battle-1.3.2-debug.apk

Project layout

app/src/main/java/net/tigr/navyfleetbattle/
  game/          rules engine — pure Kotlin, no Android dependencies
    Coordinates.kt   board geometry and Russian coordinate labels
    Ship.kt          ship classes, sizes and orientation
    Board.kt         placement legality, shot resolution, board views
    FleetPlacer.kt   random legal fleet generation
    Match.kt         turn management and win detection
  ai/
    ComputerPlayer.kt  random search with adjacent hunting after a hit
  ui/
    theme/         fixed naval colour scheme and typography
    game/          UiState and GameViewModel
    audio/         sound sources, WAV reader, PCM synthesis, AudioTrack engine
    locale/        the two languages, the stored choice, and the resource override
    settings/      the name of the one preferences file both settings live in
    components/    board renderer, fleet panels, buttons, mute and language controls
    screens/       menu, rules, placement, hand-off, battle, result
app/src/main/res/values/     Russian text, and the Cyrillic row alphabet
app/src/main/res/values-en/  English text, and the Latin row alphabet
app/src/main/res/raw/        the eight recorded effects

The rules engine and the computer opponent contain no Android imports and run on the plain JVM, which is what makes them directly unit-testable.

Tests

./gradlew testDebugUnitTest

The suite covers fleet composition, placement legality including diagonal contact, shot resolution and auto-marking around sunk ships, turn rules, win detection, random fleet generation over hundreds of seeds, the computer opponent over hundreds of complete games, the view model's state transitions, which sound each game event produces and that muting silences all of them, the WAV reader against malformed files, and the synthesised sounds at more than one sample rate.

The two translations are also checked against each other: that every translatable string is translated and none is invented, that both languages take the same format arguments — a mismatch there throws when the string is shown rather than when it is built — and that each alphabet labels all ten rows. That test reads the two strings.xml files directly, so they are declared as inputs of the test task in app/build.gradle.kts; without that Gradle would consider the task up to date after a translation changed and serve it from the cache.

The four synthesised effects can be rendered to files for listening:

./gradlew :app:testDebugUnitTest --tests '*SoundPreviewTest'

They are written to app/build/sound-preview/.

Technical notes

  • Kotlin with Jetpack Compose and Material 3
  • compileSdk 36, targetSdk 35, minSdk 26
  • Only Russian and English resources are bundled; Russian is the base locale and therefore the fallback for anything a translation does not carry
  • No third-party dependencies beyond AndroidX and Compose

Licence

MIT. See LICENSE. Every source file carries an SPDX identifier.

The bundled sound recordings are CC0, a public domain dedication, so the whole project is free to use, modify and redistribute with no conditions beyond the MIT notice.

Author

Albert "Tigr" Zenkoff