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