Battleship Part 2: Terminal Forever
Last time I wrote about a set of data structures and objects to implement battleship. I promised that this time I’d show how to implement the turn taking and running the game locally against itself.
Turn Taking
I decided that my battleship server would have two endpoints: new_game
and take_turn
. The new_game
endpoint returns a game ID to the client. The take_turn
endpoint receives a move from the client, responds to that move with “hit,” “miss,” or “you sunk my __,” and supplies a move of its own. If it helps, here’s the JSON I scribbled in my notebook as I was figuring out my API.
To implement this my client needs a method to make a move and a method to respond to a move. Last time I showed code that makes a move by randomly guessing. This time I’ll show the code that responds to those moves.
Updating The Data Structures
When I went to implement code that responded to my opponent’s guesses I realized the data structure made it hard to detect when a ship was sunk. To get around this I added an instance variable, called fleet
, to the client. The fleet is an associative array. The keys are ship names and the values are arrays that show the location of that ship. For example:
With this structure I can replace the location with :hit
when the ship is hit. To detect if the ship is sunk I can fleet[:ship_name].all? { |l| l == :hit }
.
With that in mind I wrote process_move
.
process_move
takes a string that represents the opponent’s move. To start the method, I declare a results object and initialize :hit
to false since most guesses will be misses. Then I iterate through the fleet using Array#index
to see if the opponent’s move is in the array of locations. If it is I set the results[:hit]
to true and set the location in the array to :hit
. Finally, I check if all of the locations are now hit
. If so the ship is sunk, and I add that to the results hash. Then I return the results. I’m pretty sure there’s a shorter way to write this, but I’ll be porting this code to other languages, and I wanted something without too much Ruby magic (except for the ever useful all?
).
The last thing I need to do is record the guess on my board. When I play battleship in person, I usually don’t bother to record my opponent’s guesses, and I don’t have to record them here. But when I was debugging my implementation I found it useful to be able to print out the board state. I ran into an issue where my tests were passing but actually running the game wasn’t working correctly. It turned out half of my code used the letters to represent the columns, and the other half had letters representing the rows. Being able to see the board state made it obvious where I’d messed up.
Running The Game
The last thing I wanted to do was have my two clients play each other. Each client needed to place their ships, then one client guesses, the other processes the guess and makes their own guess, and then the first client processes that guess. Here’s the code:
The first time I ran this I didn’t have a lost?
method and got caught in an infinite loop. To address that I added Client#lost?
.
With all that in place running the game is easy: Game.new.run
. The game will run against itself and print out the moves. The first couple times I ran it I used a single ship in the fleet and ran the moves by hand to double check that everything worked correctly. The next stop is moving this code to Sinatra so that others can play battleship against my client.