My Journey Learning Python: Battleships

Jonathan Singh
5 min readApr 18, 2023

--

I’m currently on a journey to learn more about Data Science and Data Engineering, and thought it might do me some good to try making something I’m pretty familiar with: a game.

In this case, I wanted to make the game Battleships (please don’t sue me Milton Bradley) as it would allow me to experiment with a couple of interesting features of Python that I wasn’t super familiar with.

At it’s core, Battleships is a game where two players, each with their own grid, place ships on their grid and attempt to discover where their opponent placed their ships. The players take turns guessing coordinates on the enemy grid, and their opponent will confirm if the guess results in a “hit” (I.E. there’s a ship in that grid) or a “miss” (there’s nothing in that grid). This continues until one player has no ships left, at which point they’ve lost.

From the above, I started to break down the different classes I would need:

  • A Player class to hold all the information relating to the player:
  • A PlayArea class that contained all the information contained in a player’s grid.
  • A Coordinate class that contains all the information of a specific coordinate in a player’s grid
  • A Ship class, that contains all the information about a player’s available ships.

Once I wrapped my head around that, I started to work out the different operations I might need. Things like “place a ship”, “attack” and “view a player’s grid” for example.

The biggest challenge for me was how to represent the grid. There were several options that I considered:

A 2D List:

play_area =[[Coordinate, Coordinate],[Coordinate, Coordinate]
#reference a specific coordinate with
play_area[0][0]
#I need to do some math to change a player's input of "A1" into 0 0

A Dictionary:

play_area ={"A1":Coordinate, "A2":Coordinate, "B1":Coordinate, "B2":Coordinate}
#reference a specific coordinate with
play_area["A1"]
#No math needed to parse "A1" into a coordinate. I just need some validation of the input

When comparing both of those options, I opted to try using a dictionary for two reasons:

  1. It required less processing to go from a player’s input to referencing the coordinate. If the player types “A1” and I pass that as a string to the dictionary, I get the correct data. I just need to make sure that “A1” is formatted properly.
  2. It takes less mental gymnastics to read and see the data in the dictionary when developing.

Once I settled on this method, I got to working on building out the basic functionality to display the contents of a grid.

I needed to differentiate between looking at my grid (where I have perfect information) vs my opponents grid (where I only know the coordinates I’ve guessed). After that, I needed to determine the different cases for what I’d have to display. By the end of it, I had this list of things to represent:

  • My Board:
    - A case where the coordinate is empty, but has been fired at
    - A case where the coordinate is empty, but has not been fired at
    - A case where the coordinate has a ship, but has been fired at
    - A case where the coordinate has a ship, but has not been fired at
  • Enemy Board:
    - A case where the coordinate is empty, but has been fired at
    - A case where the coordinate has a ship, but has been fired at
    - A case where the coordinate has not been fired at
    - (
    That’s it. There’s no 4th case here since we never reveal what the enemy board contains unless it’s been fired at)

One thing I quickly realized is that I didn’t know a good way to do switch statements in Python, so I ended up with a bunch of nested if statements. It’s not the prettiest code, but it goes through all the potential cases

def show(self, owner_perspective = True):
display_string = ""
for row in range(self.height + 1):
for column in range(self.length + 1):
coordinate = chr(column+64) + str(row)
module = self.modules.get(coordinate)
if row == 0 and column == 0:
display_string += " "
elif row == 0:
display_string += chr(column+64) + " "
elif column == 0:
row_num_length = len(str(row))
for i in range(2 - row_num_length): display_string += " "
display_string += str(row) + " "
else:
if owner_perspective and module.contents != "" and not module.isGuessed: #My board, module contains a ship that hasn't been attacked
display_string += module.contents + " "
elif owner_perspective and module.contents != "" and module.isGuessed: #My board, module contains a ship that has been attacked
display_string += "# "
elif owner_perspective and module.contents == "" and not module.isGuessed: #My board, module contains nothing and hasn't been attacked
display_string += "~ "
elif owner_perspective and module.contents == "" and module.isGuessed: #My board, module contains nothing but has been attacked
display_string += "! "
elif not owner_perspective and module.contents != "" and module.isGuessed: #Enemy board, module contains a ship that has been attacked
display_string += "# "
elif not owner_perspective and module.contents == "" and module.isGuessed: #Enemy board, module contains nothing and has been attacked
display_string += "! "
elif not owner_perspective and not module.isGuessed: #Enemy board, default character that masks contents of the module
display_string += "~ "
display_string += "\n"
return display_string

You might notice that I’ve added some extra rows and columns for headers of each (A-J, 1–10). This was done to make it easier for me to tell which coordinate I was looking at.

Once I could save data to the play area and visualize it, placing ships was a breeze!

GIF of the placement phase of my Battleship game, where it shows a player entering the coordinates and direction of the ship they want to place. The ship is then rendered on the board.

From this point onwards, everything was a much simpler task to solve:

  • Attacks would always set a property on the coordinate to True to indicate it had been attacked.
  • A succesful attack would lower the health of a player by one
  • This would continue until one player had no health left.

So what did I learn going through all this? A couple of my key take-aways:

  1. Python seems to always pass by reference, not by value. I discovered this while trying to figure out why the dictionary I created that contained all the possible ships was being modified for both players.
  2. Dictionaries are pretty handy when working with strings and wanting a string to reference a specific thing (I guess that’s why they call it a dictionary).
  3. Dictionaries are a lot friendlier than lists when you want to update an entry, thanks to the fact that you can’t have multiple keys in a dictionary with the same value (and the .update() method is pretty nice too.
  4. I’m really bad and writing tests before I write code.
  5. I still need to wrap my head around refactoring strategies

If for some reason you’re ever interested in checking out the entire project, you can view it on GitHub here: tacostorm/python-terminal-battleship: Battleship Game played on a terminal (github.com)

If you ever want to critique what I wrote or have comments, feel free to reach out to me on twitter! Until next time!

--

--

Jonathan Singh
Jonathan Singh

Written by Jonathan Singh

0 Followers

I like to play games, watch esports, and make things for tech companies. I also eat and cook A LOT.

No responses yet