Who has never drew two grids on a paper and played Battleship with a friend? It’s a childhood memory of almost everyone and the rules are known all over the world! This board game has its origins between the end of the 19th century and the beginning of the 20th century, with signs of having been played even before World War I.

The rules are simple. Each one of the two players has a grid of equal size and has to define the secret position of the ships in the grid, knowing that these can’t overlap nor, sometimes, be attached to one another. Each ship corresponds to a consecutive line of n squares, where n is the size of the vessel (normally between 2 and 5). In turns, each player has to choose a cell to drop a bomb on (for example, A4), and the opponent says if it hit the water or a part of a ship. The winner is the first player to destroy all ships of the opponent.

Battleship started as a simple paper and pencil game, later appearing in three-dimensional plastic versions and, finally, it was one of the first games to be adapted for a computer version. To honor this last part, let’s try to build a simple interactive Battleship game with Python!
Making the board
The first thing to think about must be the board game, and there are several requirements we have to meet:
- The board must be a square 2D array. Let’s decide for a 9×9 matrix!
- The ships must be placed randomly, so each time we generate a new board the layout of the fleet must be random and different.
- The ships cannot overlap or lean against each other. To make the job easier, let’s start with only 3-cell boats.
The first thing to do is to start the general class of the game with the initial necessary methods: a constructor, a ship generator and some methods to return attributes the user:
class Battleship():
def __init__(self, boats=3):
#class constructor with the number of
#boats as a parameter (default of 3 boats)
def add_boat(self):
#add a 3-cell boat to the board
def get_board(self):
#return the board 2D array
def get_boats(self):
#return a list with the ships locations
def show_board(self):
#show the board as an image
The idea is to generate the board in the constructor and add each ship at a time calling the method add_boat inside a for loop. So, before that, let’s see the ship generator method. As we will only create 3-cell ships, the function can be divided into three main parts:
- Add the fist ship cell in a random cell of the board that is not part of another ship or in the direct neighborhood of a ship.
- Add the second cell (consecutive) in one of the possible directions (randomly chosen).
- Add the third and last cell in one of the possible directions (randomly chosen).
Check the code below:
def add_boat(self):
#add a 3 cell boat to the board
done = False
while done == False:
#create temporary board
board = np.zeros((9,9))
boat = []
#check if initial boat cell is valid
invalid = True
while invalid:
row = random.randint(0, 8)
col = random.randint(0, 8)
if self.main_board[row, col] == 0:
invalid = False
#get initial cell
board[row, col] = 1
boat.append((row, col))
#possible second cell
possible = [(boat[-1][0]-1, boat[-1][1]), (boat[-1][0]+1, boat[-1][1]), (boat[-1][0], boat[-1][1]-1), (boat[-1][0], boat[-1][1]+1)]
#remove the invalid second cells
possible = [coordinates for coordinates in possible if -1 not in coordinates and self.main_board.shape[0] not in coordinates and coordinates not in self.not_possible]
#get the random second cell
row, col = random.sample(possible, 1)[0]
board[row, col] = 1
boat.append((row, col))
#possible third cell
if boat[-1][0] == boat[0][0]:
if boat[-1][1] > boat[0][1]:
possible = [(boat[-1][0], boat[0][1]-1), (boat[-1][0], boat[-1][1]+1)]
else:
possible = [(boat[-1][0], boat[0][1]+1), (boat[-1][0], boat[-1][1]-1)]
else:
if boat[-1][0] > boat[0][0]:
possible = [(boat[0][0]-1, boat[-1][1]), (boat[-1][0]+1, boat[-1][1])]
else:
possible = [(boat[0][0]+1, boat[-1][1]), (boat[-1][0]-1, boat[-1][1])]
#validate possibilities
possible = [coordinates for coordinates in possible if -1 not in coordinates and self.main_board.shape[0] not in coordinates and coordinates not in boat and coordinates not in self.not_possible]
#add third cell
try:
row, col = random.sample(possible, 1)[0]
except:
continue
board[row, col] = 1
boat.append((row, col))
#get neighbors of the boat
footprint = np.array([[1,1,1],
[1,0,1],
[1,1,1]])
board = ndimage.generic_filter(board, get_neighbors, footprint=footprint)
#define the neighbors and boats as -1
board = np.where(board!=0, -1, board)
#define boat cells as 1
for row, col in boat:
board[row, col] = 1
#join the temporary board to the main board
self.main_board = self.main_board + board
done = True
return boat
During the board creation, a ship cell is represented by a 1 in the matrix, the water is 0 and the neighborhood of a ship -1. At the end of the creation, the -1 cells are changed to 0, leaving only two types of cell: ship (1) and water (0). By calling the ship generator inside a for loop, we end up with a complete random board:
def __init__(self, boats = 3):
self.num_boats = boats
#the board starts as 2D array with 9 rows, 9 columns and all values set to zero
self.main_board = np.zeros((9,9))
#initialization of some variables of the game
self.win = False
self.hits = []
self.attempts = []
self.boats_destroyed = []
#create the board, getting the list of boats and the list of blocked cells (not possible to insert a boat cell)
self.boat_list = []
self.not_possible = []
for _ in range(self.num_boats):
self.boat_list.append(self.add_boat())
rows, cols = np.where(self.main_board == -1)
self.not_possible = [(rows[i], cols[i]) for i in range(len(rows))]
#sort the cells of each boat
for boat in self.boat_list:
boat = boat.sort()
#replace all -1 in the board by 0, so now a boat cell is represented by one and the rest is 0
self.main_board = np.where(self.main_board!=1, 0, self.main_board)
By the end of the constructor execution, we get 2 main attributes: the board and the list of boats, which is a list of lists of tuples, where each tuple has the row and the column of the boat cell (for example, [ [ (1,2), (1,3), (1,4) ], [ (5,6), (6,6), (7,6) ], [ (9,1), (9,2), (9,3) ] ]).
We can return these attributes to the user using two different methods of the class:
def get_board(self):
return self.main_board
def get_boats(self):
return self.boat_list
And, finally, we can show the board with the boats by using the matplotlib package and use a dictionary to transform the 9×9 array into a 9x9x3 RGB array with colors:
def show_board(self):
#show board as image with matplotlib
colors = { 0: [90, 155, 255],
1: [88, 88, 88]}
image = np.array([[colors[val] for val in row] for row in self.main_board], dtype='B')
plt.imshow(image)
plt.axis('off')
plt.show()
And that’s it! Now we have a simple Battleship class that can generate random game boards and everything was done with hard code! By creating an object and calling the show function, we can see the board with the chosen colors:
def main():
game = Battleship()
game.show_board()
if __name__ == '__main__':
main()
Creating the game
Now that we have the boards, let’s create an interactive game using only the python console.
The player has to choose cells to drop bombs, so we have to create a board specific for the player. This board has to show the labels of the rows / columns (letters / numbers) and must be updated each time the player tries to hit a ship. Let’s had 5 rows at the end of the class constructor, to build this second board:
self.player_cols = [str(n) for n in range(1, 10)]
self.player_rows = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
self.player_board = [list(" ") + self.player_cols]
for i in range(len(self.player_rows)):
self.player_board.append(list(self.player_rows[i]) + [" "]*9)
Now let’s add a class method to print the player’s board.
def print_player_board(self):
print()
for x in self.player_board:
print(*x, sep=' ')
Regarding the playing itself, I’ll show the three methods that make up the entire game process, and then we can talk about the code:
def play(self):
#top level method
print("Welcome to Battleship!")
print("You have 5 ships to destroy (all with 3 cells of size). Good luck!")
self.print_player_board()
while self.win==False:
#ask for a guess
guess = input("\n{} - Insert your guess (ROWCOL) or 'exit': ".format(len(self.attempts) + 1))
#let the user insert lower or upper letters
guess = guess.upper()
#check for exit
if guess == "EXIT":
sys.exit()
else:
#add guess
valid = self.add_guess(guess)
#check if guess was valid
if valid == "yes":
self.print_player_board()
else:
print(valid)
#won game
print("\n\nCongratulations, you won with {} attempts!".format(len(self.attempts)))
def add_guess(self, guess):
#insert a guess
#validate the guess
if len(guess) != 2:
return ">> Invalid guess!"
if not guess[0].isalpha() or not guess[1].isdigit():
return ">> Invalid guess!"
if guess[0] not in self.player_rows or int(guess[1]) < 0 or int(guess[1]) > 9:
return ">> Invalid guess!"
#get the row and column of the guess (correspondent to the game board, not the player board)
guess_row = self.player_rows.index(guess[0])
guess_col = int(guess[1])-1
#check if is repeated guess
if (guess_row, guess_col) in self.attempts:
return ">> Repeated guess!"
self.attempts.append((guess_row, guess_col))
#check if the guess hit a boat
is_boat = any((guess_row, guess_col) in sublist for sublist in self.boat_list)
if is_boat:
self.player_board[guess_row+1][guess_col+1] = "x"
self.hits.append((guess_row, guess_col))
print(">> You hit a boat!")
self.check_boats_destroyed()
else:
self.player_board[guess_row+1][guess_col+1] = "o"
print(">> Oops...whater!")
#check if game was won
self.win = self.check_win()
return "yes"
def check_boats_destroyed(self):
#check for destroyed boats
for i in range(len(self.boat_list)):
if all(i in self.hits for i in self.boat_list[i]) and i not in self.boats_destroyed:
self.boats_destroyed.append(i)
print(">> {} boat(s) destroyed!".format(len(self.boats_destroyed)))
def check_win(self):
#check if game was won
win = False
if self.num_boats == len(self.boats_destroyed):
win = True
return win
In short, the play method is a loop that repeatedly asks the player for a guess and shows the updated player’s board until all ships are destroyed. Each guess is passed to the add_guess method, which has several jobs:
- Validate the guess (two character string, letter and number between the ranges, not repeated, etc)
- Check if the valid guess hit a ship or not
- Update the player’s board, where a hit on water is represented as an o and a hit on ship is represented as an x.
- Check the number of boats already destroyed (calling the method check_boats_destroyed)
- Check if the game was won (calling the method check_win)
And that’s it! Now let’s check a video of the game in action, with the corresponding board on the right:

You can check the complete code on my Github!
Future work
Now that we can play battleship in the Python console, why not try to create a GUI just for this game?