Battleship: Building the Server
I’ve been enjoying the battleship project. It seemed so simple, but I’m finding tons of little things that are tricky in ways I didn’t anticipate. That has taken it from the level of “toy project” to “actually interesting but small engineering challenge.” This post is about taking the classes I had written for playing battleship locally and creating a server that allows others to play against my client.
My Tech Stack
I’m a Rubyist at heart. Even though this project is designed to get me to branch out a bit, it is fastest for me to start with what I know. This project feels too small for something as heavy as Rails, so I’m using Sinatra. For the database layer, I’m using ActiveRecord since I like ActiveRecord Query Interface and I assume many folks don’t know you can use AR outside of Rails. Since this project is focused on computers talking to other computers, I’m not worrying about a templating tool for my views. Embedded Ruby - ERB and HTML are more than sufficient. I’m using minitest and ZenTest for testing.
In summary:
- Ruby
- Sinatra
- ActiveRecord
- ERB & HTML
- Minitest
The Endpoints
My initial design called for two endpoints, /new_game
and /turn
. I added a third endpoint at /
that displays the rules and the format for messages to the other endpoints. One assumption I made is that the server always goes second. This isn’t necessary but made figuring out the logic much simpler.
/new_game
starts a game with the server and returns a game ID. The endpoint accepts a get
request and returns a JSON object of this form: { game_id: <id> }
.
/turn
takes a guess/move/turn posted from the client, processes it, and responds with a guess of its own. Here’s the message format for both the request and the response. The first move will have no response and is left empty. The last move will have no guess and is also left empty.
The Models
I created two models to store game state. The game model is simple: it keeps track of what games have been started.
The game model has a one-to-many relationship with the turn model. Turn keeps track of what happens each time the /turn
endpoint is hit. Turn records a game ID and a turn ID. It also records the message from the client. The last field, state, records the client state at the end of the turn so that the next time a message for this game comes in the server can rebuild the appropriate client object.
Marshaling
I chose to use marshaling to save the client state and then reconstitute it. In Ruby, you can marshal most objects without having to do anything special. The board object, however, has a Hash that runs some code on key misses and this can’t be marshaled as is. Since my client object has a board object, I had to solve this before I could finish implementing my server.
The board object’s board instance variable is defined as a hash with array values, like this:
To marshal this object I had to write my own marshal_dump
and marshal_load
methods. Hashes in Ruby decompose nicely to nested arrays of the form [[key, value], [key, value]]
so I implemented marshal_dump
by converting the hash to an array.
To recreate the hash in marshal_load
I simply iterated over the dumped array to recreate the hash.
I chose this method because it seemed a more idiomatic way to set the default_proc on a hash. I could have also used Hash::[]
and Hash.default_proc=
. But I find many people don’t know Hash::[]
so I chose the more obvious solution. For completeness though, here’s the other definition I tried.
Actual Server Code
The new_game
endpoint is super simple. It just creates a new game record.
The turn
endpoint is more complicated. The first bit is just processing the incoming message and then retrieving the game and last turn from the database.
After that, I have to recreate a client, or if this is the first move, create a new one. This is where I use Marshal.load
.
The next step is to process the user’s last move and create the response section of the response. I also record this move as a turn in the turns table.
Then I need to create my guess and record that in the turns table as well.
Finally, I can send the response to the client.
Stylistically I’m breaking a lot of rules of good design. This method is about 50 lines long. It has five distinct phases that could conceivably be refactored into their own methods. Perhaps the turn class should have a client object and be able to reconstitute it on its own. I’m creating turn objects in six or more lines instead of using ActiveRecord hash syntax to create them in a single line. I may come back to this at some point to try out different refactorings based on design patterns just to see if I can clean this up. Suggestions welcome.
Mistakes and Uncertainties
As I coded this up, I realized I hadn’t included any way for someone to say that they had lost. So I’ve added that to the protocol. I’m not confident in my choice to include turn_ids manually specified by the client and the server. I can order the turns via timestamps just as quickly so I may end up removing that in the future. In early September, Seattle.rb will be putting my server to the test at our semi-regular workshop meetup. I’m confident that I will learn a lot about the weaknesses in my design that evening. I’m looking forward to it.
The code for this post is located here.