mirror of
https://codeberg.org/andyscott/ShipGame.git
synced 2024-11-09 06:00:50 -05:00
469 lines
14 KiB
Python
469 lines
14 KiB
Python
|
# Author: Andrew Scott
|
||
|
# Date: 2022-03-04
|
||
|
# Description: Allows two people to play the ShipGame. Each player has their own
|
||
|
# 10x10 grid on which they place their ships. On their turn, each
|
||
|
# player can fire a torpedo at a square on the enemy's grid. Player
|
||
|
# 'first' gets the first turn to fire a torpedo, after which
|
||
|
# players alternate. A ship is sunk when all of its squares have
|
||
|
# been hit. When a player sinks their opponent's final ship, they
|
||
|
# win.
|
||
|
|
||
|
class Ship():
|
||
|
"""Represents a ship in the ShipGame."""
|
||
|
|
||
|
def __init__(self, player, length, coords, orientation):
|
||
|
"""Constructor for the Ship class. All data members are private.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
player : obj
|
||
|
The player object that owns the ship ('first' or 'second')
|
||
|
length : int
|
||
|
The length of the ship
|
||
|
coords : str
|
||
|
Coordinates of the square closest to A1 that the ship will occupy
|
||
|
orientation : str
|
||
|
'R' if the ships squares occupy the same row, or 'C' if its squares
|
||
|
occupy the same column
|
||
|
"""
|
||
|
|
||
|
self._player = player
|
||
|
self._length = length
|
||
|
self._coords = coords
|
||
|
self._orientation = orientation
|
||
|
self._squares = []
|
||
|
self._hits = set()
|
||
|
|
||
|
if coords[0] == 'A':
|
||
|
self._indices = '0' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'B':
|
||
|
self._indices = '1' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'C':
|
||
|
self._indices = '2' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'D':
|
||
|
self._indices = '3' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'E':
|
||
|
self._indices = '4' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'F':
|
||
|
self._indices = '5' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'G':
|
||
|
self._indices = '6' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'H':
|
||
|
self._indices = '7' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'I':
|
||
|
self._indices = '8' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'J':
|
||
|
self._indices = '9' + str(int(coords[1]) - 1)
|
||
|
|
||
|
|
||
|
def get_player(self):
|
||
|
"""Get the player who owns the ship.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
Either 'first' or 'second'
|
||
|
"""
|
||
|
|
||
|
return self._player
|
||
|
|
||
|
def get_length(self):
|
||
|
"""Get the ships length.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
int
|
||
|
The length of the ship
|
||
|
"""
|
||
|
|
||
|
return self._length
|
||
|
|
||
|
def get_indices(self):
|
||
|
"""Get the square occupied by the ship closes to A1 as a 2-digit string.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
2-digit string representing the square the ship occupies on the grid
|
||
|
closest to A1 (e.g 'A1' = '00')
|
||
|
"""
|
||
|
|
||
|
return self._indices
|
||
|
|
||
|
def get_coords(self):
|
||
|
"""Get the coordinates of the square occupied by the ship closest to A1.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
The coordinates (e.g. 'B7')
|
||
|
"""
|
||
|
|
||
|
return self._coords
|
||
|
|
||
|
def get_orientation(self):
|
||
|
"""Get the ships orientation.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
'R' if the ships squares occupy the same row, or 'C' if its squares
|
||
|
occupy the same column
|
||
|
"""
|
||
|
|
||
|
return self._orientation
|
||
|
|
||
|
def get_hits(self):
|
||
|
"""Get the number of hits recorded against a ship.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
int
|
||
|
The number of times the ships has been hit
|
||
|
"""
|
||
|
|
||
|
return self._hits
|
||
|
|
||
|
def get_squares(self):
|
||
|
"""Get the squares occupied by the ship.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
list
|
||
|
List of 2-digit strings representing squares occupied by the ship
|
||
|
(e.g 'A1' = '00')
|
||
|
"""
|
||
|
|
||
|
return self._squares
|
||
|
|
||
|
def set_squares(self, squares):
|
||
|
"""Set the squares occupied by the ship.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
squares : list
|
||
|
List of 2-digit strings representing squares occupied by the ship
|
||
|
(e.g 'A1' = '00')
|
||
|
"""
|
||
|
|
||
|
self._squares = squares
|
||
|
|
||
|
def add_hit(self, index):
|
||
|
"""Records when the ship is hit. A hit only counts once per square.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
index : str
|
||
|
2-digit string representing the square on the grid to add
|
||
|
(e.g coordinate 'A1' = index '00')
|
||
|
"""
|
||
|
|
||
|
self._hits.add(index)
|
||
|
|
||
|
def is_sunk(self):
|
||
|
"""Checks if the ship was sunk.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
bool
|
||
|
True if the ship was sunk, otherwise False
|
||
|
"""
|
||
|
|
||
|
if len(self._squares) == len(self._hits):
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
class Board():
|
||
|
"""Represents each players board for the ShipGame class."""
|
||
|
|
||
|
def __init__(self, player):
|
||
|
"""Constructor for the Board class. All data members are private.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
player : str
|
||
|
Either 'first' or 'second'
|
||
|
"""
|
||
|
|
||
|
self._player = player
|
||
|
self._rows = 10
|
||
|
self._columns = 10
|
||
|
self._grid = [[' ' for r in range(self._rows)] for c in range(self._columns)]
|
||
|
|
||
|
def render(self):
|
||
|
"""Prints the current state of the board to the terminal"""
|
||
|
|
||
|
print('\n 1 2 3 4 5 6 7 8 9 10')
|
||
|
|
||
|
column_char = 'A'
|
||
|
for column in self._grid:
|
||
|
output = str()
|
||
|
for item in column:
|
||
|
output += item
|
||
|
print(column_char, ' '.join(output))
|
||
|
column_char = chr(ord(column_char) + 1)
|
||
|
|
||
|
def check_hit(self, index):
|
||
|
"""Checks the grid for a hit.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
index : str
|
||
|
2-digit string representing the square on the grid to check for a hit
|
||
|
(e.g coordinate 'A1' = index '00')
|
||
|
"""
|
||
|
|
||
|
try:
|
||
|
location = self._grid[int(index[0])][int(index[1])]
|
||
|
except:
|
||
|
return False
|
||
|
if location != ' ':
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def is_valid(self, ship):
|
||
|
"""Determines whether a position requested for ship placement is valid.
|
||
|
|
||
|
Uses the current state of the board to determine whether or not the
|
||
|
coordinates and orientation passed to the ShipGame's place_ship method
|
||
|
represent a valid move.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
ship : obj
|
||
|
The ship that needs to be validated
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
bool
|
||
|
True if coordinates are valid, otherwise false
|
||
|
"""
|
||
|
|
||
|
length = ship.get_length()
|
||
|
orientation = ship.get_orientation()
|
||
|
location = ship.get_indices()
|
||
|
column = int(location[0])
|
||
|
row = int(location[1])
|
||
|
|
||
|
try:
|
||
|
if orientation == 'R':
|
||
|
for index in range(length):
|
||
|
if self._grid[column][row] != ' ':
|
||
|
return False
|
||
|
row += 1
|
||
|
return True
|
||
|
if orientation == 'C':
|
||
|
for index in range(length):
|
||
|
if self._grid[column][row] != ' ':
|
||
|
return False
|
||
|
column += 1
|
||
|
return True
|
||
|
except:
|
||
|
return False
|
||
|
|
||
|
def add_ship(self, ship):
|
||
|
"""Called by ShipGame's place_ship method to add a ship to the grid.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
ship : obj
|
||
|
A Ship object
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
ship_squares : list
|
||
|
List of 2-digit strings representing squares occupied by the ship
|
||
|
(e.g 'A1' = '00')
|
||
|
"""
|
||
|
|
||
|
length = ship.get_length()
|
||
|
orientation = ship.get_orientation()
|
||
|
location = ship.get_indices()
|
||
|
column = int(location[0])
|
||
|
row = int(location[1])
|
||
|
|
||
|
if orientation == 'R':
|
||
|
ship_squares = []
|
||
|
for index in range(length):
|
||
|
self._grid[column][row] = str(length)
|
||
|
ship_squares.append(str(column) + str(row))
|
||
|
row += 1
|
||
|
return ship_squares
|
||
|
if orientation == 'C':
|
||
|
ship_squares = []
|
||
|
for index in range(length):
|
||
|
self._grid[column][row] = str(length)
|
||
|
ship_squares.append(str(column) + str(row))
|
||
|
column += 1
|
||
|
return ship_squares
|
||
|
|
||
|
class ShipGame():
|
||
|
"""Represents the game ShipGame. Uses Board and Player classes."""
|
||
|
|
||
|
def __init__(self):
|
||
|
"""Constructor for the ShipGame class. All data members are private."""
|
||
|
|
||
|
self._player1 = 'first'
|
||
|
self._player2 = 'second'
|
||
|
self._board1 = Board(self._player1)
|
||
|
self._board2 = Board(self._player2)
|
||
|
self._player1_ships_dict = dict()
|
||
|
self._player2_ships_dict = dict()
|
||
|
self._current_state = 'UNFINISHED'
|
||
|
self._current_turn = self._player1
|
||
|
|
||
|
def place_ship(self, player, length, coords, orientation):
|
||
|
"""Places a ship on the player's board.
|
||
|
|
||
|
If a ship would not fit entirely on that player's board, if it would
|
||
|
overlap any previously placed ships, or if the length of the ship is
|
||
|
less than 2 the ship will not be added.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
player : str
|
||
|
Either 'first' or 'second'
|
||
|
length : int
|
||
|
The length of the ship
|
||
|
coords : str
|
||
|
Coordinates of the square closest to A1 that the ship will occupy
|
||
|
orientation : str
|
||
|
'R' if the ships squares occupy the same row, or 'C' if its squares
|
||
|
occupy the same column
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
bool
|
||
|
True if the ship is added, False if it cannot be added
|
||
|
"""
|
||
|
|
||
|
if length < 2:
|
||
|
return False
|
||
|
if length > 10:
|
||
|
return False
|
||
|
|
||
|
ship = Ship(player, length, coords, orientation)
|
||
|
if player == 'first':
|
||
|
board = self._board1
|
||
|
if board.is_valid(ship):
|
||
|
squares = board.add_ship(ship)
|
||
|
self._player1_ships_dict[coords] = ship
|
||
|
ship.set_squares(squares)
|
||
|
return True
|
||
|
return False
|
||
|
if player == 'second':
|
||
|
board = self._board2
|
||
|
if board.is_valid(ship):
|
||
|
squares = board.add_ship(ship)
|
||
|
self._player2_ships_dict[coords] = ship
|
||
|
ship.set_squares(squares)
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
def get_current_state(self):
|
||
|
"""Get the current state of the game.
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
str
|
||
|
'FIRST_WON', 'SECOND_WON', or 'UNFINISHED'
|
||
|
"""
|
||
|
|
||
|
return self._current_state
|
||
|
|
||
|
def fire_torpedo(self, player, coords):
|
||
|
"""Fires a torpedo at the enemy ships.
|
||
|
|
||
|
Records the move, updates whose turn it is, and updates the current
|
||
|
state of the game if the move resulted in a win.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
player : str
|
||
|
Either 'first' or 'second'
|
||
|
coords : str
|
||
|
Coordinates of the target square (e.g. 'B7')
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
bool
|
||
|
False when the game is already over or it's not the players turn,
|
||
|
otherwise True
|
||
|
"""
|
||
|
|
||
|
if self._current_state != 'UNFINISHED' or self._current_turn != player:
|
||
|
return False
|
||
|
|
||
|
index = ''
|
||
|
if coords[0] == 'A':
|
||
|
index = '0' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'B':
|
||
|
index = '1' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'C':
|
||
|
index = '2' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'D':
|
||
|
index = '3' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'E':
|
||
|
index = '4' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'F':
|
||
|
index = '5' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'G':
|
||
|
index = '6' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'H':
|
||
|
index = '7' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'I':
|
||
|
index = '8' + str(int(coords[1]) - 1)
|
||
|
elif coords[0] == 'J':
|
||
|
index = '9' + str(int(coords[1]) - 1)
|
||
|
|
||
|
if player == 'first':
|
||
|
board = self._board2
|
||
|
ships_dict = self._player2_ships_dict
|
||
|
if board.check_hit(index):
|
||
|
for key in ships_dict:
|
||
|
ship = ships_dict[key]
|
||
|
if index in ship.get_squares():
|
||
|
ship.add_hit(index)
|
||
|
if ship.is_sunk():
|
||
|
del ships_dict[key]
|
||
|
ships_remaining = self.get_num_ships_remaining('second')
|
||
|
if ships_remaining == 0:
|
||
|
self._current_state = 'FIRST_WON'
|
||
|
return True
|
||
|
self._current_turn = 'second'
|
||
|
if player == 'second':
|
||
|
board = self._board1
|
||
|
ships_dict = self._player1_ships_dict
|
||
|
if board.check_hit(index):
|
||
|
for key in ships_dict:
|
||
|
ship = ships_dict[key]
|
||
|
if index in ship.get_squares():
|
||
|
ship.add_hit(index)
|
||
|
if ship.is_sunk():
|
||
|
del ships_dict[key]
|
||
|
ships_remaining = self.get_num_ships_remaining('first')
|
||
|
if ships_remaining == 0:
|
||
|
self._current_state = 'SECOND_WON'
|
||
|
return True
|
||
|
self._current_turn = 'first'
|
||
|
return True
|
||
|
|
||
|
def get_num_ships_remaining(self, player):
|
||
|
"""Get how many ships a player has left.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
player : str
|
||
|
Either 'first' or 'second'
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
int
|
||
|
How many ships the specified player has left
|
||
|
"""
|
||
|
|
||
|
if player == 'first':
|
||
|
return len(self._player1_ships_dict.keys())
|
||
|
if player == 'second':
|
||
|
return len(self._player2_ships_dict.keys())
|