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.
Leave a reply to BattleShip Project Review – Level up SE Cancel reply