BattleShip Part 11: Include columns and rows in display

Planning on wrapping this up by implementing one more feature around displaying the row and column labels. The project isn’t “perfect” but I feel like it’s in a good place (imo).

Previously, I was only showing the coordinates and the statuses but not the labels. i.e. (A-J or 1-10). This made figuring out the coordinates I wanted to attack harder.

The primary logic for this should live in the Game class since it knows about how many columns and rows there are.

@dataclass                        
class BoardLayout:    
    coord_info: List[List[CoordInfo]]                                                              
    row_labels: List[str]                          
    column_labels: List[str]

I’ll define a new class that will return the row and column labels.

Since this impacts the Game class’ “public” API, sometimes what I like to do is create a new version.

    def get_board_layout_v2(self):
        coord_info = [
            [self._get_coord_status(row_index, col_index) for col_index in range(self.col_length) ]
            for row_index in range(self.row_length)
        ]

        row_labels = [index_to_letter(i) for i in range(self.row_length)]
        col_labels = [str(i + 1) for i in range(self.col_length)]

        return BoardLayout(coord_info, row_labels, col_labels)

This lets your work on the functionality independently without changing the GameCli. Let’s add a unit test for it too.

    def test_get_board_layout_v2_with_no_hits(self):
        ship_coords = {
            (0,0): 'A',            
        }
        game = Game(ship_coords, 2, 2)

        layout_without_hit = game.get_board_layout_v2()
        empty_coord = CoordInfo(CoordStatus.EMPTY, None)
        expected_coords = [
          [empty_coord, empty_coord],
          [empty_coord, empty_coord]
        ]
        expected_layout = BoardLayout(expected_coords, ['A', 'B'], ['1', '2'])
        self.assertEqual(expected_layout, layout_without_hit)

Later when I’m done, I can update the caller to use the new one.

    def _display_board_layout(self):
        board_v2 = self.game.get_board_layout_v2()

        col_header = '  ' + ','.join(board_v2.column_labels) + '\n'
        rows = '\n'.join([ board_v2.row_labels[idx] + ' ' + ','.join([str(coord) for coord in row]) for idx, row in enumerate(board_v2.coord_info)])
        self.view_manager.display(col_header + rows)

When the old method is no longer being used, we can remove it.

We should also update our tests.

    def test_game_cli_show(self):
        game = Mock()
        view_manager = Mock()
        user_inputs = ['show']

        cli = GameCli(user_inputs, game, view_manager)

        hit = CoordInfo(CoordStatus.SHIP_HIT, 'A')
        missed = CoordInfo(CoordStatus.SHIP_MISSED, None)
        empty = CoordInfo(CoordStatus.EMPTY, None)

        coords_info = [
            [hit, missed],
            [empty, empty]
        ]
        layout = BoardLayout(coords_info, ['A', 'B'], ['1', '2'])
        game.get_board_layout_v2.return_value = layout

        cli.run()

        expected_board_text = '  1,2\nA A,x\nB  , '
        view_manager.display.assert_has_calls([call(expected_board_text), call(expected_board_text), call('Game is over')])

The logic for converting the board into a string is a little more complicated now. If we wanted to we could extract this functionality into a separate function if we wanted to.

For some of my other tests, I’m not actually looking to test the board rendering logic i.e. in my test_game_cli_attack test I can use an ANY helper which basically checks we attempted to display anything.

    def test_game_cli_attack(self):
        game = Mock()
        view_manager = Mock()
        user_inputs = ['a1']

        cli = GameCli(user_inputs, game, view_manager)

        layout = BoardLayout([], [], [])
        game.get_board_layout_v2.return_value = layout

        game.attack.return_value = AttackResult.SUCCESS
        cli.run()

        game.attack.assert_called_with('a', '1')
        view_manager.display.assert_has_calls([call('Ship hit!'), call(ANY), call('Game is over')])

Project changeset.

Anyway, our project is working now. It handles bad inputs and shows us the state of the board.

  1,2,3,4,5,6,7,8,9,10
A x,x,x,x,x,x,x,x,x,x
B  , , , ,x, , , , , 
C x,x,x,x,x,x,x,x,x,x
D  , , , ,x,x,x, , , 
E x,x,x,x,x,x,x,x,x,x
F  ,B, , ,B, , , , , 
G  , , ,C,C, , , , , 
H  , , , , , , , , , 
I x,x,x,x,x,x,x,x,x,x
J  , , , ,x, , , , , 

We also have unit tests that verify a lot of the functionality.

$ python3 -m unittest test/test* -v
test_attacks_return_correct_result (test.test_game.TestGame) ... ok
test_game_with_all_ships_hit_game_over (test.test_game.TestGame) ... ok
test_game_with_invalid_input_throws_error (test.test_game.TestGame) ... ok
test_game_with_unhit_ships_not_over (test.test_game.TestGame) ... ok
test_get_board_layout_v2_with_no_hits (test.test_game.TestGame) ... ok
test_get_board_layout_with_hits (test.test_game.TestGame) ... ok
test_get_board_layout_with_no_hits (test.test_game.TestGame) ... ok
test_coordinate_returns_attack_action_and_coordinate (test.test_game_cli.TestGameCli) ... ok
test_exit_returns_exit_action (test.test_game_cli.TestGameCli) ... ok
test_game_cli_attack (test.test_game_cli.TestGameCli) ... ok
test_game_cli_show (test.test_game_cli.TestGameCli) ... ok
test_invalid_input (test.test_game_cli.TestGameCli) ... ok
test_invalid_input_returns_invalid_action (test.test_game_cli.TestGameCli) ... ok
test_show_returns_show_action (test.test_game_cli.TestGameCli) ... ok
test_get_valid_ship_placements (test.test_ship_placer.TestShipPlacer) ... ok
test_no_valid_placements_throws_exception (test.test_ship_placer.TestShipPlacer) ... ok
test_placed_ships_position_matches_ship_sizes (test.test_ship_placer.TestShipPlacer) ... ok
test_ship_too_large_cannot_be_placed (test.test_ship_placer.TestShipPlacer) ... ok
test_valid_placment_sets_coordinates (test.test_ship_placer.TestShipPlacer) ... ok

----------------------------------------------------------------------
Ran 19 tests in 0.029s

Anyway, hope this was helpful.

One response to “BattleShip Part 11: Include columns and rows in display”

  1. […] BattleShip Part 11: Include columns and rows in display […]

    Like

Leave a comment