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.

{ game_id: <id>,
  response: { hit: [true|false],
              sunk: <ship name>,
              turn_id: i },
  guess:    { guess: <A7, B4, E1, etc>,
              turn_id: i + 1 }
}              

The Models

I created two models to store game state. The game model is simple: it keeps track of what games have been started.

class Game < ActiveRecord::Base
  has_many :turns
end

class CreateGames < ActiveRecord::Migration[5.1]
  def change
    create_table :games do |t|
      t.string :game_id
      t.timestamps
    end
  end
end

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.

class Turn < ActiveRecord::Base
  belongs_to :game
end

class CreateTurns < ActiveRecord::Migration[5.1]
  def change
    create_table :turns do |t|
      t.belongs_to :game, index: true
      t.integer :turn_id, index: true
      t.text :body      
      t.binary :state
      t.timestamps
    end
  end
end

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:

    @board  = Hash.new { |h, k| h[k] = [] }

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.

  def marshal_dump
    @board.to_a
  end

To recreate the hash in marshal_load I simply iterated over the dumped array to recreate the hash.

  def marshal_load data
    @board  = Hash.new { |h, k| h[k] = [] }

    data.each do |location, value|
      @board[location] = value
    end
  end

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.

  def marshal_load data
    @board = Hash[data]
    @board.default_proc = proc { |h, k| h[k] = [] }
  end

Actual Server Code

The new_game endpoint is super simple. It just creates a new game record.

get '/new_game' do
  g = Game.create!

  content_type :json
  { game_id: g.id }.to_json
end

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.

post '/turn' do
  body = request.body.read
  params = JSON.parse(body)

  response = {}

  g = Game.find( params["game_id"] )

  turns = Turn.where(game_id: g.id)
  last = turns.last

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.

  # Load in state from the previous turns and create a new client object.
  if last
    c = Marshal.load(last.state)
  else
    c = Client.new
    c.place_ships
  end

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.

  response[:response] = c.process_move params["guess"]["guess"]
  response[:response][:turn_id] = params["guess"]["turn_id"]
  response[:response][:lost] = c.lost?

  t = Turn.new
  t.game_id = g.id
  t.state = Marshal.dump(c)
  t.body = params
  t.turn_id = params["guess"]["turn_id"]
  t.save!

Then I need to create my guess and record that in the turns table as well.

  guess = c.guess
  t = Turn.new
  t.game_id = g.id
  t.state = Marshal.dump(c)
  t.turn_id = params["guess"]["turn_id"] + 1
  t.save!

  response[:guess] = { guess: guess, turn_id: t.turn_id}

Finally, I can send the response to the client.

  response[:game_id] = g.id

  content_type :json
  response.to_json

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.