Logic puzzles are my favourite.

I used to play battleships against the computer as a child, and I'd been wondering whether I could build the game myself and write a programme that could act as the computer, and perhaps even win occasionally.

So I thought I'd give it a go.

My first attempt was published July 2019. The game was built in React 15, and the computer logic was alright, but rarely won, largely because when it found a ship, it was inefficient in uncovering the whole ship.

I rebuilt the game from scratch in 2022 and carried on improving computer logic in 2023. My goals were to...

  • Learn React Hooks – I'd never had an opportunity to learn them. I first learned React years before Hooks. (✅ Success – though nothing too complex!)
  • Write more efficient logic – When I looked over the 2019 logic, it was pretty messy. The game logic is quite heavy, so there was plenty to neaten up. (✅ Success – though I'm sure I can do more)
  • Test better – My tests in 2019 didn't consider as many edge cases as I'd like. It's tough to test the complex logic of Battleships, but I wanted to do TDD and think through each function in depth. (✅ Success – though, again, there's more I could do)
  • Improve the computer strategy – The old code was inefficient at uncovering ships when it found them. Now, it's much faster at establishing where to search when it finds one. (✅ Definitely better!)
  • Get the computer to win half the time – This was always the goal. I'm not quite there yet, but could be getting close... (❌ Closer though)

Head over here to play my version of Battleships 🎉

Found a bug, or got some feedback or ideas? Contact me here!

The front end

The front end was the easy part, really.

It's built in React 18 with Styled Components for styling. First, I span up a simple React app with create-react-app.

The game conditionally renders different things depending on different states.

  • If the game is in setup mode, the Setup component is rendered for the user to set up the game.
  • If the game is ongoing, two Grid components are rendered to represent the humans' and the computers' grids.
  • If the instructions are active, the Instructions component is rendered.

The Battleship setup

Setup mode

While in setup mode, the player can choose to alter the size of the grid, or change the number of ships which will be placed on the grid.

Playing the game

1. Setting up the grid

When we begin a game, we generate two grids representing parts of our ocean. There's one grid for the player and one for the computer. The grids are represented by an array of arrays.

  • Each grid lives within a Grid React component.

  • Each row of the grid creates a unique Row within its grid. If the player has chosen a 10x10 grid, there will be 10 Row components.

  • Each cell of every row creates a unique Cell within its row in the grid. If the player has chosen a 10x10 grid, there will be 10 Cell components in every row – 100 per grid.

Logic peek

Here's the simple code generating the grid, which is being passed back to the React components:

const generateGrid = (width = 10, height = 10) => {
	const result = [];
	for (let i = 0; i < height; i++) {
		const row = [];
		for (let j = 0; j < width; j++) {
			row.push({
				isShip: false,
				isDiscovered: false,
				name: null,
				length: null,
			});
		}
		result.push(row);
	}
	return result;
};

We can see that each cell is being given default information about itself, including whether or not it contains a ship, whether it's been discovered, what it's name is (the name of the ship), and the length of the ship.

2. Preparing a game

By default, five boats are placed at random onto each of the two empty grids. In standard Battleships, these boats are the Carrier, Battleship, Cruiser, Submarine and Destroyer.

The player's Battleships grid

The new game

Logic peek

Here's a look inside part of the createGame function. The programme takes each boat and randomly tries to find a place to put it on the grid, where it doesn't fall off the edge or overlap with another boat.

boats.forEach(boat => {
	while (true) {
		let result = tryToPlace(
			gridA,
			boat,
			generateRandomCoordinates(width, height),
			generateRandomDirection()
		);
		if (result) {
			gridA = updateGrid(gridA, result);
			break;
		}
	}
});

The tryToPlace function tries to position the battleship on a grid. Given a starting coordinate and direction (up, down, left, right), it checks if the entire length of the ship fits within the grid boundaries without crossing the edges. At the same time, it validates that none of the cells the ship would occupy already contains another ship. If both conditions are satisfied, it'll return the ship's details and the while loop above ends.

Details about which cells different ships are occupying are then passed back to the Grid and down to their respective Cell. The player can see the placement of their ships on their grid.

2. The player takes their first turn

The player begins by firing a figurative cannonball at the computer's grid, in their bid to discover their ships. They do this by clicking a cell.

The player begins firing

The player begins firing

The logic for the fire action is pretty simple.

Each cell has a property .isDiscovered at the start of the game, set to false.

When we fire, we simply change the value to true.

const fire = (player, grid, x, y) => {
    ...
    grid[y][x].isDiscovered = true;
    return grid;
}

If it's a hit – that is to say, they've fired on a ship – the cell will turn red. Otherwise, their cannonball splashes into the water, and the cell turns blue.

    if (props.cell.isShip && props.cell.isDiscovered) {
        return <CellContainer className='red'><img src={fire} alt='Explosion'/></CellContainer>
    } else if (props.cell.isDiscovered) {
        return <CellContainer className='blue'><img src={splash} alt='Splash!'/></CellContainer>
    ...

3. The computer decides its strategy

At the start of the game, the computer will decide on a strategy.

The computer knows that ships will always take up at least two cells, so it will begin examining even and odd cells on alternating rows, for example by searching in diagonal lines.

The computer's search strategy

The computer's search strategy

Based on the size of the grid, the computer will randomly generate an array of coordinates which form its plan. For example, it might begin [[0,0],[1,1],[[2,2],[3,3],[4,4],[5,5],[6,6]...]

It'll continue searching until it finds a ship.

The computer's search strategy

The computer's search strategy continues

4. The computer finds a ship

The computer finds a ship

The computer finds a ship

When the computer fires on a ship, the strategy changes. Before the computer finds a ship, the strategy might look something like this:

strategy = {
    next: [],
    plan: [[6,4],[5,5],[4,6],[3,7],[2,8]...],
    lastTry: [7,5],
    firstHit: null,
    direction: null,
    boatHits: []
}

The computer's original plan, generated in the previous step, is strategy.plan.

But when the computer finds a cell which is a ship, it knows its next job is to work out which direction this ship is hiding in.

So it temporarily deviates from the plan in strategy.plan.

It needs to check the cells adjacent to the cell with the ship.

So it adds the adjacent cells to the empty array in the strategy called strategy.next. It also stores the coordinates of the first hit in strategy.firstHit, and starts keeping track of the ship cells it has hit in strategy.boatHits.

By default, cells in strategy.next will always be checked first.

strategy = {
    next: [[5,4],[7,4],[6,3],[6,5]],
    plan: [[6,4],[5,5],[4,6],[3,7],[2,8]...],
    lastTry: [7,5],
    firstHit: [6,4],
    direction: null,
    boatHits: [[6,4]]
}

The computer tries to work out which direction the ship is in

The computer's tries to work out which direction the ship is in

In this case, the adjacent cell below turns out also to be a ship. It can tell by comparing the coordinates of the first hit with the current hit.

The computer has worked out the direction it has to search in.

Now, it records this information in strategy.direction and adds the next adjacent cell below to strategy.next.

strategy = {
    next: [[6,6]],
    plan: [[6,4],[5,5],[4,6],[3,7],[2,8]...],
    lastTry: [6,5],
    firstHit: [6,4],
    direction: "vertical",
    boatHits: [[6,5],[6,4]]
}

Now, every time it hits a ship, it adds the next cell down to strategy.next until the cannonball splashes.

strategy = {
    next: [],
    plan: [[6,4],[5,5],[4,6],[3,7],[2,8]...],
    lastTry: [6,9],
    firstHit: [6,4],
    direction: "vertical",
    boatHits: [[6,8],[6,7],[6,6],[6,5],[6,4]]
}

The computer bombs the ship

The computer bombs the ship

Then, it resets the strategy back to the plan and continues.

strategy = {
    next: [],
    plan: [[6,4],[5,5],[4,6],[3,7],[2,8]...],
    lastTry: [6,9],
    firstHit: null,
    direction: null,
    boatHits: []
}

Other computer tricks

There's much more I want to do with the computer's logic, but here are some of the tricks I've implemented:

  • Re-strategising. When the computer has found all but two boat cells, it'll re-prioritise the plan. Now, it prioritises cells which have at least two undiscovered cells on either side.
  • Avoiding lone cells If a cell is surrounded on all sides by discovered cells which are not ships, it'll skip searching them.

The end of the game

When either you or the computer have bombed all of their opponents' ships, the game's over! At the end, you'll get to see where all the ships were.

Don't mind the faint dots on the screenshot – there's a little pyro fireworks animation that runs at the end, made from CSS animations! That would be a whole blog in itself, so I won't discuss it here

The player wins

The player wins

The computer wins

The computer wins

The game's logic

The Game react component lives with its other components in ./src/components. It has a few methods which make all the logic run:

  • handleSetup: Initializes a game with the chosen setup.
  • handleFire: Handles the firing mechanic of the game when a player makes a move.
  • handleComputerGo: Implements the computer's logic when it's the computer's turn.
  • reset: Resets the game state.
  • toggleDev: Toggles developer mode.
  • toggleInstructions: Toggles the visibility of game instructions.
  • handleUpdateGridSize: Updates the grid's size.
  • handleUpdateBoats: Updates the boat configuration during setup.

We're using React state to store changing information about our game, and updating the state with React hooks, including things like...

  • The width and height of the grid
  • The grid of the player and computer
  • Whose turn it is
  • The computer's strategy
  • If anyone has won yet
  • The game setup, like grid size and number of boats

Under the hood, there are a plethora of functions, each tested with Mocha and Chai in its corresponding spec.js file in src/logic/tests

File structure

The file structure and logic

Some learnings

1. Get modular

My first versions of logic functions like createGame and computerStrategy were sometimes enormous. I thought I was starting on the right foot by having all these functions, but I ended up modularising masssively.

Now I have a whole bunch of helper functions that I use all the time and save so much effort. Plus, everything's far more readable.

2. TDD, religiously

I did do TDD for this project. But sometimes I'd be tempted to make changes to the code without going to the tests first.

This turned out to be not a good idea. I'd break stuff without realising it was broken, and also get false negatives and positives which would remain undiagnosed for really too long.

Bonus: Deep cloning

Somewhat lazily, I avoided mutations in React by using the spread operator. I learned the hard way that this doesn't create deep copies, and I needed to use JSON.parse with JSON.stringify. I also learned that this won't work on some data types like functions, regular expressions and dates – but this was okay for my use cases.

That's it!

This has been my favourite personal project that I've worked on, which I suppose is why I keep coming back to it. I think it's because the computer logic for Battleships has so much scope to be extended. The job of a developer building this game is to spot in-game situations where there are opportunities for better decisions, and work out how to translate that into changes in the computer strategy.

If you're looking for a fun side-project and want to challenge the logical parts of your brain, this is a great, language-agnostic and tool-agnostic choice.

Play the game here

View the repo on GitHub

Found a bug, or got some feedback or ideas? Contact me here!