Running the project as-is, I noticed a problem with the output.
$ python3 main.py
Your Move > show
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
There are 10 rows but only 9 columns.
Also, my validation check is too strict because it only allows a single letter for the attack command. 1-9. So I’ll relax this to allow any numeric ending.
valid_attack_pattern = r'(^[a-z]{1})([0-9]+$)'
This causes a new issue since now I can enter coordinates outside the board.
$ python3 main.py
Your Move > show
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
[ , , , , , , , , ]
Your Move > a10
Ship missed!
Your Move > a11
Ship missed!
I’ll update the attack method of the game to return a more structured type instead of just a boolean.
class AttackResult(Enum):
SUCCESS = 1
MISSED = 2
INVALID = 3
Then I’ll update the attack method to return the attack result.
def attack(self, row, col):
coord = (row, col)
if not self._on_board(row, col):
return AttackResult.INVALID
# already attacked that position
if coord in self.attacked_coords:
return AttackResult.MISSED
self.attacked_coords.add(coord)
if coord in self.ship_coords:
return AttackResult.SUCCESS
return AttackResult.MISSED
def _on_board(self, row, col):
row_index = letter_to_index(row) + 1
col_index = int(col)
return (
col_index >= 1 and \
col_index <= self.col_length and \
row_index >= 1 and \
row_index <= self.row_length
)
Now the _attack method in GameCli can properly handle the different cases.
def _attack(self, row, col):
result = self.game.attack(row, col)
if result == AttackResult.SUCCESS:
print('Ship hit!')
elif result == AttackResult.MISSED:
print('Ship missed!')
elif result == AttackResult.INVALID:
print('Invalid coordinates!')
else:
raise Exception('There is an unhandled attack type')
One thing I found a little painful about doing this is in Game, I’ve been using raw user-provided input for keeping track of things like coordinates i.e. (“A”, “1”). It also requires the ship_placer to have to translate its output into this type of format.
I’ll refactor Game to track things with a zero-based indexing instead of these raw values which will keep things a bit more consistent and I don’t have to translate between 0-based and 1-based indexing in a bunch of places.
I’ll define a function that will translate the user input into integer coordinates.
def to_coordinates(row, col):
'''Translates user provided input to zero index based matrix coordates: e.g. A1 -> (0, 0).'''
row_index = letter_to_index(row)
col_index = int(col) - 1
return (row_index, col_index)
Then I can remove all this +1 business and keep things simpler.
def get_board_layout(self):
return [
[self._get_coord_status(row_index, col_index) for col_index in range(self.col_length) ]
for row_index in range(self.row_length)
]
def _get_coord_status(self, row, col):
coord = (row, col)
if coord in self.attacked_coords and coord in self.ship_coords:
return CoordInfo(CoordStatus.SHIP_HIT, self.ship_coords[coord])
if coord in self.attacked_coords:
return CoordInfo(CoordStatus.SHIP_MISSED, None)
return CoordInfo(CoordStatus.EMPTY, None)
def _on_board(self, coord):
(row_index, col_index) = coord
return (
col_index >= 0 and \
col_index < self.col_length and \
row_index >= 0 and \
row_index < self.row_length
)
Finally I update ship_placer to just return the integer-based coordinates. Since I changed the return type, I’ll also need to update the test.
Project changeset.
Leave a comment