Iteration 2 - The Board

Back to Battleship Home Back to Requirements

Test Driven Development

In this iteration, you are required to use TDD to create your classes. Use the interaction pattern to determine what a method should do and write one or more tests to verify that expected behavior. Then you can implement the method. You should always write code with the purpose of making a test pass.

Board

The Board class is responsible for keeping track of cells, validating coordinates, validating ship placements, placing ships, and rendering a visual representation of itself.

The Cells

The board is responsible for keeping track of all the cells. Since our board is 4 x 4, it will have 16 Cell objects. It will keep track of these cells in a hash where the coordinates of the cell are the keys that point to Cell objects:

pry(main)> require './lib/board'
# => true

pry(main)> board = Board.new
# => #<Board:0x00007ff0728c8010...>

pry(main)> board.cells
# =>
{
 "A1" => #<Cell:0x00007ff0728a3f58...>,
 "A2" => #<Cell:0x00007ff0728a3ee0...>,
 "A3" => #<Cell:0x00007ff0728a3e68...>,
 "A4" => #<Cell:0x00007ff0728a3df0...>,
 "B1" => #<Cell:0x00007ff0728a3d78...>,
 "B2" => #<Cell:0x00007ff0728a3d00...>,
 "B3" => #<Cell:0x00007ff0728a3c88...>,
 "B4" => #<Cell:0x00007ff0728a3c10...>,
 "C1" => #<Cell:0x00007ff0728a3b98...>,
 "C2" => #<Cell:0x00007ff0728a3b20...>,
 "C3" => #<Cell:0x00007ff0728a3aa8...>,
 "C4" => #<Cell:0x00007ff0728a3a30...>,
 "D1" => #<Cell:0x00007ff0728a39b8...>,
 "D2" => #<Cell:0x00007ff0728a3940...>,
 "D3" => #<Cell:0x00007ff0728a38c8...>,
 "D4" => #<Cell:0x00007ff0728a3850...>
}

Testing the #cells method is a bit tricky because the Cell objects are created in the Board class and not in our tests. We can’t specify exactly what the return value should be because we don’t have reference to the exact cell objects we expect in the hash. Instead, we can assert what we do know about this hash. It’s a hash, it should have 16 key/value pairs, and those keys point to cell objects.

Validating Coordinates

Our board should be able to tell us if a coordinate is on the board or not:

pry(main)> board.valid_coordinate?("A1")
# => true

pry(main)> board.valid_coordinate?("D4")
# => true

pry(main)> board.valid_coordinate?("A5")
# => false

pry(main)> board.valid_coordinate?("E1")
# => false

pry(main)> board.valid_coordinate?("A22")
# => false

Validating Placements

Additionally, a Board should be able to tell us if a placement for a ship is valid or not. Our Board should have a method called valid_placement? that takes two arguments: a Ship object and an array of Coordinates.

There are many things we need to check for validating ship placement. Let’s use this setup:

pry(main)> require './lib/board'
# => true

pry(main)> require './lib/ship'
# => true

pry(main)> board = Board.new
# => #<Board:0x00007fcb0d9db478...>

pry(main)> cruiser = Ship.new("Cruiser", 3)
# => #<Ship:0x00007fcb0d989510...>

pry(main)> submarine = Ship.new("Submarine", 2)    
# => #<Ship:0x00007fcb0e8402c0...>

First, the number of coordinates in the array should be the same as the length of the ship:

pry(main)> board.valid_placement?(cruiser, ["A1", "A2"])
# => false

pry(main)> board.valid_placement?(submarine, ["A2", "A3", "A4"])
# => false

Next, make sure the coordinates are consecutive:

pry(main)> board.valid_placement?(cruiser, ["A1", "A2", "A4"])
# => false

pry(main)> board.valid_placement?(submarine, ["A1", "C1"])
# => false

pry(main)> board.valid_placement?(cruiser, ["A3", "A2", "A1"])
# => false

pry(main)> board.valid_placement?(submarine, ["C1", "B1"])
# => false

Finally, coordinates can’t be diagonal:

pry(main)> board.valid_placement?(cruiser, ["A1", "B2", "C3"])
# => false

pry(main)> board.valid_placement?(submarine, ["C2", "D3"])
# => false

If all the previous checks pass then the placement should be valid:

pry(main)> board.valid_placement?(submarine, ["A1", "A2"])
# => true

pry(main)> board.valid_placement?(cruiser, ["B1", "C1", "D1"])
# => true

Note that all of the different cases listed above should be their own tests. This will help you break this problem down into small steps and working on them one at a time. You should not have one big test for validating placement.

Tools for validation

There are many ways to go about the validation. Here are some ideas to help you get started:

Ranges

Ruby has a built in Class called Range. It has a special syntax:

pry(main)> range = 3..8
# => 3..8

pry(main)> range.class
# => Range

The first element (in this case 3) is the start of the range and the second is the end of the range.

You can turn a Range into an Array quite easily and then do Array stuff:

pry(main)> range = 3..8
# => 3..8

pry(main)> array = range.to_a
# => [3, 4, 5, 6, 7, 8]

pry(main)> array.length
# => 6

pry(main)> array[3]
# => 6

This also works with Strings:

pry(main)> range = "A".."D"
# => "A".."D"

pry(main)> range.to_a
# => ["A", "B", "C", "D"]

Ordinal Values

The Range with Strings works because each character has an implicit value that tells us in what order the characters should be. This is called the Ordinal Value, and you can access it with the ord method for Strings:

pry(main)> "A".ord
# => 65

pry(main)> "D".ord
# => 68

Helpful Enumerables

Ruby has an enumerable method called each_cons that allows you to access consecutive elements in a collection. See the Ruby Docs for more details.

Also consider if the enumerables any?, all?, none? would be helpful.

Placing Ships

The board should be able to place a ship in its cells. Because a Ship occupies more than one cell, multiple Cells will contain the same ship. This is a little brain bendy at first, but it is a very important concept. This is Object Oriented Programming in a nutshell.

pry(main)> require './lib/board'
# => true

pry(main)> require './lib/ship'
# => true

pry(main)> board = Board.new
# => #<Board:0x00007fcb0e1f6720...>

pry(main)> cruiser = Ship.new("Cruiser", 3)    
# => #<Ship:0x00007fcb0e1ffa28...>

pry(main)> board.place(cruiser, ["A1", "A2", "A3"])    

pry(main)> cell_1 = board.cells["A1"]    
# => #<Cell:0x00007fcb0e1f66a8...>

pry(main)> cell_2 = board.cells["A2"]
# => #<Cell:0x00007fcb0e1f6630...>

pry(main)> cell_3 = board.cells["A3"]    
# => #<Cell:0x00007fcb0e1f65b8...>

pry(main)> cell_1.ship
# => #<Ship:0x00007fcb0e1ffa28...>

pry(main)> cell_2.ship
# => #<Ship:0x00007fcb0e1ffa28...>

pry(main)> cell_3.ship
# => #<Ship:0x00007fcb0e1ffa28...>

pry(main)> cell_3.ship == cell_2.ship
# => true

Overlapping Ships

When you are able to place ships, you need to add another check to your validation of ship placements so that ships don’t overlap:

pry(main)> require './lib/board'
# => true

pry(main)> require './lib/ship'
# => true

pry(main)> board = Board.new
# => #<Board:0x00007fcb0e1f6720...>

pry(main)> cruiser = Ship.new("Cruiser", 3)
# => #<Ship:0x00007fcb0d92b5f0...>

pry(main)> board.place(cruiser, ["A1", "A2", "A3"])

pry(main)> submarine = Ship.new("Submarine", 2)    
# => #<Ship:0x00007fcb0dace9c0...>

pry(main)> board.valid_placement?(submarine, ["A1", "B1"])
# => false

Rendering the Board

Our board needs to be able to render a String representation of itself to display to the user all of its cells in a formatted grid. Each Cell should be displayed using the rules from Iteration 1:

  • ”.” if the cell has not been fired upon.
  • “M” if the cell has been fired upon and it does not contain a ship (the shot was a miss).
  • “H” if the cell has been fired upon and it contains a ship (the shot was a hit).
  • “X” if the cell has been fired upon and its ship has been sunk. Note that all of the cells that contain that sunken ship should render as an “X”, not just the cell that resulted in the ship being sunk.

And just like with cells, we will include an optional argument to indicate whether we want to show hidden ships.

pry(main)> require './lib/board'
# => true

pry(main)> require './lib/ship'
# => true

pry(main)> board = Board.new
# => #<Board:0x00007fcb0f056860...>

pry(main)> cruiser = Ship.new("Cruiser", 3)    
# => #<Ship:0x00007fcb0f0573f0...>

pry(main)> board.place(cruiser, ["A1", "A2", "A3"])    

pry(main)> board.render
# => "  1 2 3 4 \nA . . . . \nB . . . . \nC . . . . \nD . . . . \n"

pry(main)> board.render(true)
# => "  1 2 3 4 \nA S S S . \nB . . . . \nC . . . . \nD . . . . \n"

You can format strings on multiple lines using concatenation to make them more readable. The two return values from above can be written as:

"  1 2 3 4 \n" +
"A . . . . \n" +
"B . . . . \n" +
"C . . . . \n" +
"D . . . . \n"

"  1 2 3 4 \n" +
"A S S S . \n" +
"B . . . . \n" +
"C . . . . \n" +
"D . . . . \n"

As you move forward, you will need to add functionality to your game so that you can fire on Cells and damage their Ships. When you do this, you should also add new tests for your render method that it can render with Hits, Misses, and Sunken Ships. A Board in the middle of a game might be rendered as something like this:

"  1 2 3 4 \n" +
"A H . . . \n" +
"B . . . M \n" +
"C X . . . \n" +
"D X . . . \n"

"  1 2 3 4 \n" +
"A H S S . \n" +
"B . . . M \n" +
"C X . . . \n" +
"D X . . . \n"

Lesson Search Results

Showing top 10 results