Battleship Part 1: Local Battles
One of my goals for this summer is to get out of my Ruby rut. I love Ruby. I like how the language feels. I like the principle of least surprise. I appreciate that it has both strong object oriented and functional roots. And of course, I love the Ruby community. As much as I love Ruby, I believe most successful programmers are polyglots and can be productive in multiple languages. So I came up with what I’m calling the battleship project. My goal for the next few months is to implement an API for the game Battleship, deploy it on a server, and then build clients in a variety of languages. If I do it right, I can have other folks play their clients against my clients. There could be battleship tournaments. I can use it for tutorials on deployment and monitoring. I hope it will be awesome.
I’m starting with the server. For the first version, I plan to use Sinatra with a Postgres backend. For something this simple I don’t need the full power of Rails. Before I can dig into the web side of the implementation, I need to have the game working locally. My local version has three classes, Board, Game, and Client. Board is responsible for representing and maintaining the state of a Battleship board. Client is where the logic for placing ships and making moves lives. Game keeps track of which clients are playing and how they interact. I expect that many of these classes will end up as models in the online, multi-player version of the game but I’m not sure yet.
Board
I’ve implemented parts of Battleship before. While it is tempting to use nested arrays to represent the board (it is a grid after all) a hash is actually easier. To represent an empty cell I’m using “.” and to represent hits, misses, and ships I use symbols. Here’s the new method for my board class with its tests.
After initialization, the next thing to implement was getters and setters for each cell. I tried using Java-style set_cell
and get_cell
methods, but that didn’t feel right. Once I started implementing Client#place_ships
, I realized I wanted to use square brackets to access cells of the board. Here’s what iteration three of board cell accessors looks like.
The other thing I know from previous experience was that I need a way to visualize the board. In Ruby, we do this with to to_s
. I’m not thrilled with this implementation. It feels a bit too clever with the nested loops and joins. The line adding the last newline, in particular, offends my sensibilities. But it works, and I think it is readable to the average Rubyist, so I’m leaving it alone.
Client
The Client class has three responsibilities: placing ships, making guesses, and handling guesses. When the Game class initializes a client, it provides a game ID and a fleet of ships. The client then creates two board objects. One represents my board (the bottom one in the Milton Bradley version of the game), and the other represents the opponent’s board.
Placing ships is a little bit tricky. I broke it up into two methods. place_ship
puts a single ship on the board. place_ships
puts the entire fleet on the board, one ship at a time. Writing the code to place the ships is easier if there’s some way to detect out of range errors. Adding in_range?
to Board solves this problem.
The code to actually place a ship is complicated. I start by getting a random direction and location. Then using that starting point and direction, I generate the cells that the ship is going to occupy (lines 13 - 26). If all the locations are in the range allowed I place the ship thereby setting the cells equal to the ships name. If some of the cells are out of range, I generate a new starting point and direction. Here is the implementation and the test I wrote.
To place the entire fleet you just loop over the fleet, placing each ship in turn.
Finally, I need a method to make guesses. I took the lazy way out here and just picked a random letter and number.
Game
The final class in my system is Game. When I instantiate a game object, it creates two clients and a game ID.
This is most of the logic necessary to run my battleship server. In the next post, I’ll implement Game#run
to actually run the game and Client#take_turn
to respond to the other player’s guess and produce my own guess.