BattleShip Part 6: Ship Placement

The next major part of the project involves randomly placing ships on a board.

I decided to construct a new component that handles the placement of ships and builds a set of ship coordinates which is then used by the Game class.’

This component will be responsible for making sure ships are accurately placed on a board and we’ll assume Game receives a valid board.

    config = {
        'columns': 10,
        'rows': 10,
        'ships': [
            {'name': 'B', 'length': 2},
            {'name': 'C', 'length': 3}
        ]
    }

    ship_placer = ShipPlacer(config)
    ship_coords = ship_placer.build_ship_coords()
    game = Game(ship_coords)

I’ll define a high-level configuration that would allow for specifying a different row or column counts and different ships.

Ship Placer Class

class ShipPlacer:                                              
                                         
    def __init__(self, config):                                
        self.ship_configs = config['ships']
        self.columns = config['columns']                  
        self.rows = config['rows']
        self.ship_coords = {}
                             
    def build_ship_coords(self):  
        # create a copy of the list                         
        ships_to_place = self.ship_configs[:]
                                                                       
        while ships_to_place:             
            current_ship = ships_to_place.pop()              
            placed = self._attempt_placement(current_ship)

            if not placed:
                raise Exception('Could not place all ships on board')

        # transform to coords expected by game e.g. A1 -> A10
        return {        
            (index_to_letter(row), str(col + 1)): ship_name
            for (row, col), ship_name in self.ship_coords.items()
        }

The high-level logic involves going through each ship and trying to place it on the grid. For whatever reason, if we couldn’t, we’d throw an error. Afterward, we return the ship coordinates as the game expects them. e.g. (‘a’, ‘2’).

For each ship, I first get all free coordinates on the board. I remove a random coordinate and try to place the ship on that coordinate. For that coordinate, I try a random direction until I’ve run out of options.

    def _attempt_placement(self, ship_config):
        available_coords = [
            coord                 
            for coord in get_coords(self.rows, self.columns)
            # exclude positions already placed
            if coord not in self.ship_coords
        ]

        if not available_coords:
            return False

        while available_coords:
            random_coord = available_coords.pop(random.randrange(len(available_coords)))

            directions = [direction for direction in Direction]
            while directions:
                random_direction = directions.pop(random.randrange(len(directions)))
                placed = self._place_ship(ship_config, random_coord, random_direction)
                if placed:
                    return True

        return False

When I attempt to place a ship, I calculate the coordinates the ship would be made up of based on the direction and actually store them in the ship_coords if they are all valid coordinates.

    def _place_ship(self, ship_config, coord, direction):
        '''Places ship on coordinate in specified direction if able to.'''

        ship_size = ship_config['length']

        # next_coord = lambda row, col, idx: None
        if direction == Direction.UP:
            next_coord = lambda row, col, idx: (row - idx, col)
        elif direction == Direction.DOWN:
            next_coord = lambda row, col, idx: (row + idx, col)
        elif direction == Direction.RIGHT:
            next_coord = lambda row, col, idx: (row, col + idx)
        elif direction == Direction.LEFT:
            next_coord = lambda row, col, idx: (row, col - idx)
        else:
            raise Exception('No valid direction provided')

        row, col = coord
        potential_coords = []
        for i in range(ship_size):
            potential_coords.append(next_coord(row, col, i))

        if all(self._valid_coord(coord) for coord in potential_coords):
            for coord in potential_coords:
                self.ship_coords[coord] = ship_config['name']
            return True

        return False

I ran this and at a glance, it seems like it’s working. There might be some bugs, but next step will be writing some more tests to verify some scenarios.

Project changeset.

One response to “BattleShip Part 6: Ship Placement”

  1. […] BattleShip Part 6: Ship Placement […]

    Like

Leave a comment