The Game of Battleships in Python

A couple of weeks ago we talked about the game of Tic Tac Toe. I thought it would be fit to talk about another game, Battleships. Both games are excellent beginner games for programming and Python makes it very easy to implement them without a lot of hassle. You will notice that this article is a little different from my regular work. Normally I have the output of the function embedded. Since this is a game, like Tic Tac Toe, it is hard to capture the game in a single screen shoot. I will have some screen shoots toward the end of the article when I talk about printing out the board. Before we begin let us lay some ground rules for the game:

  1. This is a modified version of the game Battleships.
  2. The coordinates used for this game are all numeric.
  3. The game will be played against a random computer player.
  4. The computer will place random ships that are hidden from the user.
  5. The turns alternate between player regards of hit or miss.

Let us begin by laying out our main logic for the game. Here is some pseudo code that will help keep us inline:

  1. Setup the boards for both computer and user
  2. Place ships
  3. Alternate turns asking for coordinates and check win condition.

That is just a very rough outline. Of course the game itself is a lot more complex than this. So let’s get started. First we need to think about data representation. We need to have 2 boards that are 10 X 10. That is easy, it is just a list of lists with 10 lists. We will say that a cell is empty if it contains a “-1” value. So we just need to initiate this list of lists like this:


#setup blank 10x10 board
	board = []
	for i in range(10):
		board_row = []
		for j in range(10):
			board_row.append(-1)
		board.append(board_row)

	#setup user and computer boards
	user_board = copy.deepcopy(board)
	comp_board = copy.deepcopy(board)

Notice that we had to use the Python copy module to make sure we are not pointing to the same memory. There are other ways to this, like having the code duplicated. This is a topic for another day, in the mean time, here is a nice article about Deep and Shallow copies in Python. You may want to note that some of this principles are actually global and have parallels in other languages. So we have the boards, but what about this ships? Well, that is easy, what if we have a dictionaries attached to our list of list as the final element. The dictionary will look like this:


ships = {"Aircraft Carrier":5,
		     "Battleship":4,
 		     "Submarine":3,
		     "Destroyer":3,
		     "Patrol Boat":2}

Ok. So we have the easy part taken away. Now we have to look at how to place the ships. That is a little more complex. Lets first think about our logic. We need to figure out first that the coordinates entered by the user are valid. Then we have to ask orientation of the ship. After that we need to make sure that a ship can be actually placed. For example, what if we are trying to place a Patrol Boat at row: 10 col: 8, vertically? We cannot place the ship there. Horizontally we can, but that is another entry. So we will need to validate the request. If all is good, we will use the first letter of the ship name as a place holder on the board. Seeing the code might hep a little, so here it is:


def user_place_ships(board,ships):

	for ship in ships.keys():

		#get coordinates from user and vlidate the postion
		valid = False
		while(not valid):

			print_board("u",board)
			print "Placing a/an " + ship
			x,y = get_coor()
			ori = v_or_h()
			valid = validate(board,ships[ship],x,y,ori)
			if not valid:
				print "Cannot place a ship there.\nPlease take a look at the board and try again."
				raw_input("Hit ENTER to continue")

		#place the ship
		board = place_ship(board,ships[ship],ship[0],ori,x,y)
		print_board("u",board)

	raw_input("Done placing user ships. Hit ENTER to continue")
	return board

You will notice that we call 5 functions we did not talk about yet. So let us talk about them. We will talk about the printing function in a little bit. We will take a look first at the get_coor() function. Before you get cold feet I do warn you that this function is lengthy. Keep in mind that the function accounts for user errors and will make sure that the only input it will accept and return is 2 values that are between 1 - 10. Nothing more, nothing less.


def get_coor():

	while (True):
		user_input = raw_input("Please enter coordinates (row,col) ? ")
		try:
			#see that user entered 2 values seprated by comma
			coor = user_input.split(",")
			if len(coor) != 2:
				raise Exception("Invalid entry, too few/many coordinates.");

			#check that 2 values are integers
			coor[0] = int(coor[0])-1
			coor[1] = int(coor[1])-1

			#check that values of integers are between 1 and 10 for both coordinates
			if coor[0] > 9 or coor[0] < 0 or coor[1] > 9 or coor[1] < 0:
				raise Exception("Invalid entry. Please use values between 1 to 10 only.")

			#if everything is ok, return coordinates
			return coor

		except ValueError:
			print "Invalid entry. Please enter only numeric values for coordinates"
		except Exception as e:
			print e

Ok. So that was a mouth full. How about the next one, the v_or_h()? That is a little more straight forward, it only return V or H. This should be simple:


def v_or_h():

	#get ship orientation from user
	while(True):
		user_input = raw_input("vertical or horizontal (v,h) ? ")
		if user_input == "v" or user_input == "h":
			return user_input
		else:
			print "Invalid input. Please only enter v or h"

Recall that we are doing all this to place the user ships. So at this point we have a valid coordinate from the user and an orientation. Now we go back to our placement issue. We need to make sue that the type of ship can be placed where the user wants it. To do this we just need to check that it can physically be placed within the board limits and that the cells are empty (-1).


def validate(board,ship,x,y,ori):

	#validate the ship can be placed at given coordinates
	if ori == "v" and x+ship > 10:
		return False
	elif ori == "h" and y+ship > 10:
		return False
	else:
		if ori == "v":
			for i in range(ship):
				if board[x+i][y] != -1:
					return False
		elif ori == "h":
			for i in range(ship):
				if board[x][y+i] != -1:
					return False

You should note that this function returns a boolean value of True or False. This is so we can use it when we write the computer version of place ships, we can use the validate() function without writing a new one. Now all we have look at, excluding the print function is the actual placement of the ships. Recall that all we need to do at this point is put the intial character of the ships name.


def place_ship(board,ship,s,ori,x,y):

	#place ship based on orientation
	if ori == "v":
		for i in range(ship):
			board[x+i][y] = s
	elif ori == "h":
		for i in range(ship):
			board[x][y+i] = s

	return board

You will notice that this looks very much like a function that we can reuse, just like the validate() function. Perhaps now is a good time to look at the ship placement for the computer agent.


def computer_place_ships(board,ships):

	for ship in ships.keys():

		#genreate random coordinates and vlidate the postion
		valid = False
		while(not valid):

			x = random.randint(1,10)-1
			y = random.randint(1,10)-1
			o = random.randint(0,1)
			if o == 0:
				ori = "v"
			else:
				ori = "h"
			valid = validate(board,ships[ship],x,y,ori)

		#place the ship
		print "Computer placing a/an " + ship
		board = place_ship(board,ships[ship],ship[0],ori,x,y)

	return board

The main difference between the user and computer ship placement is that the computer uses randomization to place the ships while the user enters the information. At this point we have our setup complete and can play the game. The 2 functions that will handle the game are user_move and computer_move. Let us take a look at both:


def user_move(board):

	#get coordinates from the user and try to make move
	#if move is a hit, check ship sunk and win condition
	while(True):
		x,y = get_coor()
		res = make_move(board,x,y)
		if res == "hit":
			print "Hit at " + str(x+1) + "," + str(y+1)
			check_sink(board,x,y)
			board[x][y] = '$'
			if check_win(board):
				return "WIN"
		elif res == "miss":
			print "Sorry, " + str(x+1) + "," + str(y+1) + " is a miss."
			board[x][y] = "*"
		elif res == "try again":
			print "Sorry, that coordinate was already hit. Please try again"

		if res != "try again":
			return board

def computer_move(board):

	#generate user coordinates from the user and try to make move
	#if move is a hit, check ship sunk and win condition
	while(True):
		x = random.randint(1,10)-1
		y = random.randint(1,10)-1
		res = make_move(board,x,y)
		if res == "hit":
			print "Hit at " + str(x+1) + "," + str(y+1)
			check_sink(board,x,y)
			board[x][y] = '$'
			if check_win(board):
				return "WIN"
		elif res == "miss":
			print "Sorry, " + str(x+1) + "," + str(y+1) + " is a miss."
			board[x][y] = "*"

		if res != "try again":

			return board

Fist thing you will notice is the infomus function get_coor(). We have used it before and it will work for us again. Now we have 3 more function we need to look at in order to understand how this works. We will start with make_move() this function looks like this:


def make_move(board,x,y):

	#make a move on the board and return the result, hit, miss or try again for repeat hit
	if board[x][y] == -1:
		return "miss"
	elif board[x][y] == '*' or board[x][y] == '$':
		return "try again"
	else:
		return "hit"

All this function really does is looks at the board in a particular cell and returns a value at that cell. Based of the result the board will be updated if needed. Now we need to check if a ship got sunk. This is easy. If you recall we have a dictionary containing the ships and the amount of hits they can take. This is the last element in the board data representation. So this is not as bad as you might think. We just need to figure out what ship got hit and if it has more damage points.


def check_sink(board,x,y):

	#figure out what ship was hit
	if board[x][y] == "A":
		ship = "Aircraft Carrier"
	elif board[x][y] == "B":
		ship = "Battleship"
	elif board[x][y] == "S":
		ship = "Submarine"
	elif board[x][y] == "D":
		ship = "Destroyer"
	elif board[x][y] == "P":
		ship = "Patrol Boat"

	#mark cell as hit and check if sunk
	board[-1][ship] -= 1
	if board[-1][ship] == 0:
		print ship + " Sunk"

Cool. Now for the last function, check_win(). Now this can be done better, but I was tired at this point so I went for simple. If you think about it, a board is in a win condition if all the ships are gone. That means that in our representation, all the cells must conation a -1 for empty, a $ for a hit or a * for a miss. Any other character is a ship and there fore not a win. So the function can look like this:


def check_win(board):

	#simple for loop to check all cells in 2d board
	#if any cell contains a char that is not a hit or a miss return false
	for i in range(10):
		for j in range(10):
			if board[i][j] != -1 and board[i][j] != '*' and board[i][j] != '$':
				return False
	return True

So now we have our puzzle almost complete. The last pice we need is a good print function. This function is built upon the Tic Tac Toe function, but is a little more complex. Recall that there are 2 printing functions that we need. When the users sees the computer board, the ships should be hidden. However, when the user is presented with their own board, they should see all the ships. In wither case, the hits and misses should be displayed and the ‘-1’ should not be printed. This took me a while to get working properly. Mostly because I wanted it to look good, for me. Here is the printing function:


def print_board(s,board):

	# WARNING: This function was crafted with a lot of attention. Please be aware that any
	#          modifications to this function will result in a poor output of the board
	#          layout. You have been warn.

	#find out if you are printing the computer or user board
	player = "Computer"
	if s == "u":
		player = "User"

	print "The " + player + "'s board look like this: \n"

	#print the horizontal numbers
	print " ",
	for i in range(10):
		print "  " + str(i+1) + "  ",
	print "\n"

	for i in range(10):

		#print the vertical line number
		if i != 9:
			print str(i+1) + "  ",
		else:
			print str(i+1) + " ",

		#print the board values, and cell dividers
		for j in range(10):
			if board[i][j] == -1:
				print ' ',
			elif s == "u":
				print board[i][j],
			elif s == "c":
				if board[i][j] == "*" or board[i][j] == "$":
					print board[i][j],
				else:
					print " ",

			if j != 9:
				print " | ",
		print

		#print a horizontal line
		if i != 9:
			print "   ----------------------------------------------------------"
		else:
			print

So to sum it all up, here is a link to the complete code Battleships in Python Command Line. Here are some screen shoots I took from the game play:

wpid-python_battleships_placing_ships-2014-02-16-19-17.png

wpid-python_battleships_user_ships-2014-02-16-19-17.png
wpid-python_battleships_computer_placing_ships-2014-02-16-19-17.png

wpid-python_battleships_user_move-2014-02-16-19-17.png

wpid-python_battleships_computer_hitship-2014-02-16-19-17.png

wpid-python_battleships_user_move-2014-02-16-19-17.png
wpid-python_battleships_userwon-2014-02-16-19-17.png

Have Fun!

If you enjoyed this post, please consider leaving a comment or subscribing to the RSS feed to have future articles delivered to your feed reader.