# Battleship Part 1: Local Battles

> Last updated: 2017-07-13

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](https://en.wikipedia.org/wiki/Battleship_(game)), 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.

```
class Board
  def initialize
    self.height = 10
    self.width = 10
    @board = Hash.new(".")
  end
end

class TestBoard < Minitest::Test
  def test_new
    b = Board.new

    assert_equal 10, b.height
    assert_equal 10, b.width

    assert_equal ".", b["C7"]
  end
end
```

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.

```
class Board
  def [] location
    @board[location]
  end

  def []= location, value
    @board[location] = value
  end
end

class TestBoard < Minitest::Test
  def test_set_cell
    b = Board.new

    b["B7"] = :submarine

    assert_equal :submarine, b["B7"]
  end
end
```

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.

```
def to_s
    str = ""

    str = ('A'..'J').map { |l|
      (1..10).map { |n| @board["#{l}#{n}"][0] }.join
    }.join("\n")
    str << "\n"

    str
  end

  def test_to_s
    b = Board.new

    expected = ""
    10.times do
      expected << "..........\n"
    end

    str = b.to_s

    assert_equal expected, b.to_s
  end
```

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

```
class Client
  def initialize game_id, fleet
    self.game_id = game_id
    self.fleet = fleet
    self.my_board = Board.new
    self.their_board = Board.new
  end
end

def test_initialize
  c = Client.new("gameID", [[:battleship, 5]])

  assert_equal "gameID", c.game_id
  assert_equal [[:battleship, 5]], c.fleet

  assert c.my_board
  assert c.their_board
end
```

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.

```
class Board
  LETTERS = ('A'..'J').to_a
  NUMBERS = ('1'..'10').to_a

  def in_range? location
    m = /([A-J])(\d+)/.match(location)

    return false unless m

    letter, number = /([A-J])(\d+)/.match(location).captures

    LETTERS.include?(letter) and NUMBERS.include?(number)
  end
end

def test_in_range?
  b = Board.new

  assert b.in_range?("B7")
  assert b.in_range?("A1")
  assert b.in_range?("A10")
  assert b.in_range?("J1")
  assert b.in_range?("J10")

  refute b.in_range?("X7")
  refute b.in_range?("B0")
  refute b.in_range?("B11")
  refute b.in_range?("")
  refute b.in_range?("142342")
end
```

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.

```
class Client
  DIRECTIONS = [:up, :down, :left, :right]
  LETTERS = ('A'..'J').to_a

  def place_ship name, length
    loop do
      dir = DIRECTIONS.sample
      letter = LETTERS.sample
      number = Random.rand(10)

      locations = []

      length.times do
        locations << "#{letter}#{number}"

        case dir
        when :right
          number += 1
        when :left
          number -= 1
        when :up
          letter = (letter.ord - 1).chr
        when :down
          letter = letter.next
        end
      end

      if locations.all? { |l| self.my_board.in_range?(l) }
        locations.each do |l|
          self.my_board[l] = name
        end
        return
      end
    end
  end
end

def test_place_ship
  c = Client.new("gameID", [])

  c.place_ship :cruiser, 3

  assert_equal 3, c.my_board.to_s.each_char.count { |l| l == "c" }
end
```

To place the entire fleet you just loop over the fleet, placing each ship in turn.

```
def place_ships
  self.fleet.each do |name, length|
    place_ship name, length
  end
end

def test_place_ships
  c = Client.new("gameID", [[:battleship, 5], [:cruiser, 3]])

  c.place_ships

  assert_equal 5, c.my_board.to_s.each_char.count { |l| l == "b" }
  assert_equal 3, c.my_board.to_s.each_char.count { |l| l == "c" }
end
```

Finally, I need a method to make guesses. I took the lazy way out here and just picked a random letter and number.

```
def guess
    "#{LETTERS.sample}#{Random.rand(10)}"
  end

  def test_guess
    c = Client.new()

    g = c.guess

    assert c.their_board.in_range?(g)
  end
```

## Game

The final class in my system is Game. When I instantiate a game object, it creates two clients and a game ID.

```
class Game
  FLEET = [[:battleship, 5],
           [:cruiser, 4],
           [:submarine, 3],
           [:frigate, 3],
           [:destroyer, 2]]

  attr_accessor :id, :client_a, :client_b

  def initialize
    @id = SecureRandom.uuid
    @client_a = Client.new(@id, FLEET)
    @client_b = Client.new(@id, FLEET)
  end
end

class TestGame < Minitest::Test
  def test_initialize
    g = Game.new

    assert g.id

    assert g.client_a
    assert g.client_b
  end
end
```

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.

Please enable JavaScript to view the [comments powered by Disqus.](https://disqus.com/?ref_noscript)

 ![Aja Hammerly](/img/traditionalheadshot.jpg)

Aja Hammerly

Seattle, WA

### library\_books Series Index 

- [Developer Relations](/series#developer-relations)4
- [Machine Learning Basics](/series#machine-learning-basics)3
- [Battleship](/series#battleship)5
- [Talks I Love](/series#talks-i-love)6
- [Algorithms with Auntie Aja](/series#algorithms-with-auntie-aja)3
- [Ruby and GCP](/series#ruby-and-gcp)5
- [Playing with Data](/series#playing-with-data)3
- [Kubernetes & Rails](/series#kubernetes-rails)3
- [Learn To Code Resources](/series#learn-to-code-resources)3
- [Testing Anti-Patterns](/series#testing-anti-patterns)5
