Battleship Ship-Finding Game — Free HTML CSS JS Snippet

Battleship Ship-Finding Game · Games · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Randomised fleet placement with real collision and boundary checking via canPlace(), retrying on failed attempts
Both ship orientations (horizontal and vertical) chosen independently at random per ship
O(1) hit lookup via a 2D board array storing ship index per cell, no linear search per shot
Per-ship hit tracking with a Set, guaranteeing accurate sunk detection with no double-counting
Derived rendering: cell visual state (unknown/hit/miss/sunk) computed purely from shotLog and ship data
Named "Ship sunk!" notification the moment a specific ship's every cell has been hit
Live shots-fired, hits, and accuracy percentage stats updating after every shot
placeFleetSafe() retry wrapper guards against the rare case of a full fleet placement failure

About this UI Snippet

Battleship Ship-Finding Game — Randomised Fleet Placement, Hit Detection & Accuracy Tracking

Screenshot of the Battleship Ship-Finding Game snippet rendered live

Battleship is one of the oldest "search and destroy" grid games, and building a working single-player version is a genuinely useful exercise in constraint-based random placement, coordinate-based state lookup, and win-condition detection. This snippet implements a complete, playable version entirely in vanilla JavaScript — a hidden fleet is randomly placed on an 8x8 grid with real collision and boundary checking, and the player fires at cells to locate and sink every ship.

Randomised fleet placement with retry-on-collision

The fleet consists of four ships defined in SHIP_DEFS — a 4-cell Carrier, two 3-cell ships (Cruiser and Submarine), and a 2-cell Destroyer — mirroring a simplified version of the classic fleet composition. placeFleet() places each ship in turn: it picks a random starting row and column, a random orientation (horizontal or vertical), computes the full list of cells the ship would occupy, and calls canPlace() to verify every one of those cells is within the 8x8 boundary and currently unoccupied on the board array. If the random placement collides with another ship or runs off the grid, the loop simply tries again with a fresh random position and orientation, up to 200 attempts per ship. Because ship placement order and position are both randomised independently for every new game, no two games have the same fleet layout. placeFleetSafe() wraps the whole process in a retry loop that restarts fleet placement entirely from scratch if any single ship exhausts its attempts (extremely rare on an 8x8 grid with this fleet size, but a real safeguard against ever leaving the game unplayable).

Board representation and coordinate lookup

The hidden board is a 2D array, board[row][col], where each cell holds either null (empty water) or the numeric index of the ship occupying it. This gives instant O(1) lookup of "what ship, if any, lives at this cell" via findShipAt(), which is the core operation every fired shot needs. Each ship object separately tracks its own cells array (its full list of occupied coordinates) and a hits Set of coordinate keys that have been successfully struck — using a Set rather than a counter means the sunk check, ship.hits.size === ship.length, is both correct and cannot be thrown off by a duplicate hit being counted twice.

Firing, hit/miss rendering, and the shot log

Every fired cell is recorded in a shotLog object keyed by a "row-col" string, storing either 'hit' or 'miss' — this both prevents firing at the same cell twice and drives the entire visual re-render. cellState() derives what a cell should look like purely from shotLog and the ship data: a miss renders as a pale dot on a darker water tile, a hit renders as a red cross, and a hit whose owning ship is now fully sunk gets an additional sunk class for a darker red treatment, visually distinguishing "you hit something" from "you finished it off." This derive-from-source-of-truth approach means the render function never needs its own separate tracking state — it always reflects shotLog and ships exactly.

Sunk detection and win condition

After every shot, isShipSunk() compares a ship's accumulated hit count against its length; when they match, a "Ship sunk!" toast names the specific ship. checkWin() runs after every shot and checks whether ships.every(isShipSunk) — only when every ship in the fleet independently satisfies the sunk condition does the game declare victory, at which point it reports the final accuracy percentage, calculated as hits / shotsFired, rounded to the nearest whole percent.

Why this is a strong constraint-placement exercise

The interesting engineering problem in Battleship isn't the UI, it's guaranteeing a valid random layout under real constraints (no overlaps, no out-of-bounds cells, both orientations supported) without ever falling into an infinite loop or producing an invalid board. The retry-with-attempt-cap pattern used here — try random placement, validate, retry on failure, cap total attempts — is broadly reusable any time you need procedurally generated content that must satisfy hard non-overlap constraints, from level generation to seating charts.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS and JS into an AI coding assistant like Claude and ask it to explain exactly how placeFleet() avoids overlapping ships and what happens in the rare case a placement keeps failing — tracing through canPlace() and the retry-with-attempt-cap logic is a good way to understand defensive procedural generation. You could also ask it to add a simple "hunt and target" AI opponent that fires back at your own hidden fleet between turns, add a visual ship-placement phase where the player manually places their own fleet with drag-and-drop before the computer's fleet is generated, or extend the accuracy stats with a "shots remaining until guaranteed sink" probability hint based on which cells are already ruled out. Each is a natural next step once the core hit-detection and placement logic already works correctly.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a single-player Battleship-style grid game in plain HTML, CSS, and JavaScript with genuine randomised fleet placement — no frameworks, no libraries.

Requirements:
- An 8x8 (or similar) grid and a fleet of at least 4 ships of varying lengths (e.g. 4, 3, 3, 2 cells), placed with real random position and orientation (horizontal or vertical) selection per ship.
- A placement algorithm with real collision and boundary detection: no two ships may overlap, no ship may extend off the grid, and failed placement attempts must retry with a new random position/orientation rather than silently allowing an invalid placement.
- Click-to-fire interaction where each cell can only be fired at once; a hit renders with a distinct visual marker from a miss, and already-fired cells become non-interactive.
- Per-ship hit tracking so that when every cell of a specific ship has been hit, a "Ship sunk!" notification appears naming that specific ship (not just a generic hit message).
- Live stats showing total shots fired, total hits, and a calculated accuracy percentage (hits divided by shots fired), updating after every shot.
- Win detection that fires only when every ship in the fleet is fully sunk, showing a final summary with shot count and accuracy.
- A "New Game" action that re-places the entire fleet randomly and resets all stats and grid state cleanly.
- Ensure the game state (which ship occupies which cell, which cells have been hit) is the single source of truth, with all rendering derived from it rather than tracked separately.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Fire at a cellClick any cell on the 8x8 grid. If it hits part of the hidden fleet, it turns red with a cross marker and the ship's hit count updates internally; if it misses, it darkens with a small dot marker and cannot be clicked again.
  2. 2
    Watch for the sunk notificationWhen every cell belonging to a specific ship has been hit, a toast message names that ship (e.g. "Destroyer sunk!") and its hit cells switch to a darker red sunk styling so you can visually distinguish finished ships from ones still in play.
  3. 3
    Track your shots and hitsThe HUD keeps a live count of Shots fired and Hits landed, with Accuracy calculated as hits divided by shots fired as a rounded percentage, updating after every single click.
  4. 4
    Sink the entire fleet to wincheckWin() runs after every shot and compares every ship's hit count against its length; once all four ships are fully sunk, a win toast reports your final shot count and accuracy percentage.
  5. 5
    Start a new gameClick "New Game" to call placeFleetSafe() again, which randomly re-places all four ships with fresh positions and orientations and clears the shot log, hit count, and accuracy back to zero.
  6. 6
    Adjust the fleet compositionEdit the SHIP_DEFS array to add, remove, or resize ships, and change GRID_SIZE for a larger or smaller board — both the placement algorithm and the render loop read from these constants directly.

Real-world uses

Common Use Cases

Lightweight embedded arcade game for a web product
A fully self-contained, dependency-free Battleship implementation drops cleanly into a games hub, waiting-room screen, or 404 page as a genuinely playable distraction. Because it has no external assets or libraries, it loads instantly and works offline once cached.
Teaching constrained random placement and retry-on-failure logic
placeFleet() is a clean, minimal example of the generate-validate-retry pattern used throughout procedural content generation — from maze generation to non-overlapping UI layout algorithms. Studying how it caps attempts and falls back to a full restart is a practical lesson in defensive randomised generation.
Grid-based hit detection reference for other search games
The shotLog-plus-derived-render pattern (store only the minimal fact of what happened, compute all visual state from it) generalises to Minesweeper-style reveal games, memory-match grids, or any other cell-based game where render state should never drift out of sync with game state.
Nautical colour palette and grid styling reference
The deep-blue water tiles, bright cyan unexplored cells, and red hit markers form a cohesive nautical palette that can be restyled for other grid-based games — swap the ocean blues for a different theme while keeping the same hit/miss/sunk visual hierarchy intact.
Coordinate-key state management pattern
Using a "row-col" string as an object key (cellKey()) to track per-cell state is a lightweight alternative to nested 2D arrays for sparse or shot-only data, and is a pattern worth reusing anywhere you need to record facts about specific grid coordinates without preallocating a full grid, as seen alongside the stack-based tube state in the Color Sort Water Puzzle.
Quick logic-puzzle break in an onboarding or loading flow
Because a game typically resolves in well under a minute on an 8x8 board with a four-ship fleet, this component works well as a short engagement moment during an otherwise idle wait, similar to how airlines and apps use mini-games during loading screens.

Got questions?

Frequently Asked Questions

canPlace() checks every single cell a candidate ship placement would occupy against two conditions: the coordinates must fall within 0 to GRID_SIZE - 1 on both axes, and the corresponding board cell must currently be null (unoccupied). If either check fails for any cell in the candidate placement, the whole placement is rejected and placeFleet() tries a new random position and orientation, up to 200 attempts per ship before placeFleetSafe() restarts the entire fleet.

A Set of coordinate keys guarantees that firing at the same already-hit cell twice (which fireAt() actually prevents via the shotLog check) can never inflate a ship's hit count past its true length. It also lets isShipSunk() do an exact comparison, ship.hits.size === ship.length, which is more robust than an incrementing counter that could theoretically drift if hit-recording logic changed elsewhere in the code.

Yes. Increase GRID_SIZE for a larger board (the CSS grid-template-columns and cell sizing adapt automatically since they use repeat() and aspect-ratio), and add more entries to the SHIP_DEFS array with a name and length for each additional ship. The placement algorithm and rendering both read these constants directly with no hard-coded assumptions about fleet size.

Accuracy is hitsCount divided by shotsFired, converted to a percentage and rounded to the nearest whole number with Math.round(). It recalculates after every single shot via updateStats(), so it is always current, and the same calculation is reused for the final win-toast accuracy figure shown when the whole fleet is sunk.

fireAt() checks whether the cell's coordinate key already exists in shotLog and returns immediately without incrementing shotsFired or changing any state if it does. Already-fired cells also have no click behaviour visually since hit and miss cells lose the hover/pointer cursor styling in CSS, making it clear they are no longer interactive.