For the next part, I’m going to build out the game mechanics. I initially thought I might want some kind of Game and Board class. Where the GameCli would interact with the Game and the Game would control the Board.
Now that I’m thinking a bit more about how I would want both of these to work, I think having a Game and a Board class might be redundant. The Board class would basically have all the functionality of the game and the Game class basically wouldn’t do anything else.
It might still make sense to break these out, but for now, I’ll just define a single Game class. It will be responsible for keeping track of the state of the game and returning information that can be used to show the board.
Tracking Attacks
I decided that I don’t really need a matrix to track where ships are and what position has been attacked.
All attacks can be tracked in a set, attacked_coords. Also, I decided to just keep track of where ships are using a dictionary, ship_coords. In the original project, it would show different letters per ship, so I’ll keep an association of ship position to ship name.
This keeps things pretty simple and simple is usually good.
class Game:
def __init__(self, ship_coords, col_length = 10, row_length = 10):
self.attacked_coords = set()
self.ship_coords = ship_coords
self.row_length = row_length
self.col_length = col_length
def attack(self, row, col):
coord = (row, col)
# already attacked that position
if coord in self.attacked_coords:
return False
self.attacked_coords.add(coord)
return coord in self.ship_coords
def game_over(self):
attacked_ship_coords = self.attacked_coords.intersection(set(self.ship_coords.keys()))
return len(attacked_ship_coords) == len(self.ship_coords)
Displaying the board
For the show command, I need to provide information that can be used to render the board.
I’ll define a dataclass that the game can use to describe the state of the board.
class CoordStatus(Enum):
'''Status of a board coordinate'''
SHIP_HIT = 1
SHIP_MISSED = 2
EMPTY = 3
def __repr__(self):
return self.name
@dataclass
class CoordInfo:
status: CoordStatus
ship_name: str
def __repr__(self):
if self.status == CoordStatus.SHIP_HIT:
return self.ship_name
return self.status.name
Then in the Game class, I’ll add the following methods.
def get_board_layout(self):
return [
[self._get_coord_status(index_to_letter(row_index), str(col_index)) for row_index in range(self.row_length) ]
for col_index in range(1, self.col_length + 1)
]
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)
Updating Game Cli
Next, I’ll update the CLI to actually interact with the game.
class GameCli:
'''Manages interactions between the user and the game.'''
def __init__(self, user_inputs, game):
self.user_inputs = user_inputs
self.game = game
def run(self):
'''Parses and applies user provided commands to the game'''
for user_input in self.user_inputs:
(action_type, action_info) = parse_user_input(user_input)
if action_type == UserActionType.SHOW:
self._display_board_layout()
elif action_type == UserActionType.ATTACK:
(row, col) = action_info
self._attack(row, col)
else:
self._display_invalid_user_input_message()
if self.game.game_over():
break
self._display_board_layout()
print('Game is over')
def _display_board_layout(self):
board = self.game.get_board_layout()
for row in board:
print(row)
def _attack(self, row, col):
hit = self.game.attack(row, col)
if hit:
print('Ship hit!')
else:
print('Ship missed!')
def _display_invalid_user_input_message(self):
print('TODO: display invalid message')
I can initialize the game w/ a test board configuration and run the game.
game = Game({
('a', '1'): 'B',
('a', '2'): 'B',
('c', '3'): 'D',
('c', '4'): 'D',
('c', '5'): 'D',
})
cli = GameCli(get_user_inputs(), game)
cli.run()
$ python3 main.py
Your Move > a1
Ship hit!
Your Move > a2
Ship hit!
Your Move > c3
Ship hit!
Your Move > c4
Ship hit!
Your Move > c5
Ship hit!
[B, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[B, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, D, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, D, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, D, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
[EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY]
Game is over
I’ll hold off on the tests for now. There’s still some functionality around creating a game board and placing ships on it that I still need to incorporate and that could affect how I organize the game class.
Project changeset.
Leave a reply to BattleShip Project Review – Level up SE Cancel reply