BattleShip Part 5: Game Mechanics

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.

One response to “BattleShip Part 5: Game Mechanics”

  1. […] BattleShip Part 5: Game Mechanics […]

    Like

Leave a comment