Battleship Part 2: Terminal Forever

This is part of the Battleship series.

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.

1
2
3
4
5
6
7
{ gameID: id,
  response: { turnID: x,
              hit: true,
              sunk: ship_name},
  turn: { turnID: x + 1,
          move: "F3" }
}

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:

1
2
3
4
5
{:battleship => ["E7", "E6", "E5", "E4", "E3"],
 :cruiser    => ["I8", "I7", "I6", "I5"],
 :submarine  => ["E8", "E9", "E10"],
 :frigate    => ["H9", "H8", "H7"],
 :destroyer  => ["C3", "B3"]}

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Client
  def process_move move
    results = { :hit => false }

    self.fleet.each do |ship, locations|
      i = locations.index(move)

      if i
        results[:hit] = true
        locations[i] = :hit
        results[:sunk] = ship if locations.all? { |l| l == :hit }
        self.my_board[move] = :hit
      else
        self.my_board[move] = :miss
      end
    end

    results
  end
end

class TestClient < Minitest::Test
  def test_process_move
    c = Client.new("gameID")
    c.fleet = {destroyer: ["A7", "A8"]}

    assert_equal false, c.process_move("F3")[:hit]

    assert_equal true, c.process_move("A7")[:hit]

    expected = {:hit => true, :sunk => :destroyer }

    assert_equal expected, c.process_move("A8")
  end
end

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.

 ABCDEFGHIJ
1..........
2..........
3..........
4..........
5..........
6..........
7..........
8..........
9..........
0..........

or 

 1234567890
A..........
B..........
C..........
D..........
E..........
F..........
G..........
H..........
I..........
J..........

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Game
  def run
    @client_a.place_ships
    @client_b.place_ships

    puts "Client A Board"
    puts @client_a.my_board
    puts

    puts "Client B Board"
    puts @client_b.my_board
    puts

    loop do
      g = @client_a.guess
      r = @client_b.process_move g
      puts "A #{g}: B #{r}"

      if @client_b.lost?
        puts "B Lost"
        break
      end

      g = @client_b.guess
      r = @client_a.process_move g
      puts "B: #{g} A: #{r}"

      if @client_a.lost?
        puts "A Lost"
        break
      end
    end
  end
end

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?.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Client
  def lost?
    self.fleet.all? { |ship, locations| locations.all? { |l| l == :hit}}
  end
end

class TestClient < Minitest:Test
  def test_lost?
    c = Client.new("gameID")
    c.fleet = {destroyer: ["A7", "A8"]}

    c.process_move("A7")
    c.process_move("A8")

    assert c.lost?
  end
end

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.