Implement Battleship for human players
This commit is contained in:
121
battleship.py
121
battleship.py
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from os import system
|
|
||||||
from typing import Tuple, List, Set, Optional
|
|
||||||
from enum import Enum, auto
|
|
||||||
|
|
||||||
|
|
||||||
class ShipOrientation(Enum):
|
|
||||||
RIGHT = auto()
|
|
||||||
DOWN = auto()
|
|
||||||
|
|
||||||
|
|
||||||
class ShipType(Enum):
|
|
||||||
PATROL_BOAT = (2, "P")
|
|
||||||
DESTROYER = (3, "D")
|
|
||||||
SUBMARINE = (3, "S")
|
|
||||||
BATTLESHIP = (4, "B")
|
|
||||||
CARRIER = (5, "C")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def size(self) -> int:
|
|
||||||
return self.value[0]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def symbol(self) -> str:
|
|
||||||
return self.value[1]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Ship:
|
|
||||||
x: int
|
|
||||||
y: int
|
|
||||||
ship_type: ShipType
|
|
||||||
orientation: ShipOrientation
|
|
||||||
|
|
||||||
def __init__(self, x: int, y: int, ship_type: ShipType, orientation: ShipOrientation):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.ship_type = ship_type
|
|
||||||
self.orientation = orientation
|
|
||||||
|
|
||||||
if self.orientation == ShipOrientation.RIGHT:
|
|
||||||
self.squares = {(x_i, self.y) for x_i in range(self.x, self.x+self.ship_type.size)}
|
|
||||||
else:
|
|
||||||
self.squares = {(self.x, y_i) for y_i in range(self.y, self.y+self.ship_type.size)}
|
|
||||||
|
|
||||||
|
|
||||||
def sunk(self, guesses: Set[Tuple[int, int]]) -> bool:
|
|
||||||
return self.squares.issubset(guesses)
|
|
||||||
|
|
||||||
def hit(self, guess) -> bool:
|
|
||||||
return guess in self.squares
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
|
||||||
def __init__(self, name: str, board_shape: Tuple[int, int]):
|
|
||||||
self.name = name
|
|
||||||
self.board_shape = board_shape
|
|
||||||
self.guesses = set()
|
|
||||||
self.ships: List[Ship] = []
|
|
||||||
|
|
||||||
def place_ships(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def symbol_at(self, position: Tuple[int, int]) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def guess(self, opponent: Player) -> Tuple[int, int]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def defeated(self, guesses: Set[Tuple[int, int]]):
|
|
||||||
return all(ship.sunk(guesses) for ship in self.ships)
|
|
||||||
|
|
||||||
def hit(self, position: Tuple[int, int]) -> Optional[Ship]:
|
|
||||||
for ship in self.ships:
|
|
||||||
if ship.hit(position):
|
|
||||||
return ship
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HumanPlayer(Player):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AIPlayer(Player):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def grid_input(prompt: str, board_shape: Tuple[int, int]) -> Tuple[int, int]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def format_board(viewer: Player, opponent: Player) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def play_turn(player: Player, opponent: Player):
|
|
||||||
print(format_board(player, opponent))
|
|
||||||
player.guesses.add(player.guess(opponent))
|
|
||||||
window if isinstance(player, HumanPlayer) and isinstance(opponent, HumanPlayer):
|
|
||||||
system("clear")
|
|
||||||
|
|
||||||
|
|
||||||
def play_battleship(player_1: Player, player_2: Player):
|
|
||||||
player_1.place_ships()
|
|
||||||
player_2.place_ships()
|
|
||||||
|
|
||||||
while not (player_1.defeated(player_2.guesses) or player_2.defeated(player_1.guesses)):
|
|
||||||
play_turn(player_1, player_2)
|
|
||||||
play_turn(player_2, player_1)
|
|
||||||
|
|
||||||
if player_1.defeated(player_2.guesses) and player_2.defeated(player_1.guesses):
|
|
||||||
print("It's a draw!")
|
|
||||||
elif player_1.defeated(player_2.guesses):
|
|
||||||
print(f"{player_2.name} wins!")
|
|
||||||
else:
|
|
||||||
print(f"{player_1.name} wins!")
|
|
||||||
0
battleship/__init__.py
Normal file
0
battleship/__init__.py
Normal file
12
battleship/__main__.py
Normal file
12
battleship/__main__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from .battleship import play_battleship
|
||||||
|
from .board import Board
|
||||||
|
from .human import HumanPlayer
|
||||||
|
|
||||||
|
board = Board(10, 10)
|
||||||
|
p1 = HumanPlayer("Player 1", board)
|
||||||
|
p2 = HumanPlayer("Player 2", board)
|
||||||
|
|
||||||
|
try:
|
||||||
|
play_battleship(p1, p2)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n")
|
||||||
7
battleship/ai.py
Normal file
7
battleship/ai.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .player import Player
|
||||||
|
|
||||||
|
|
||||||
|
class AIPlayer(Player):
|
||||||
|
pass
|
||||||
47
battleship/battleship.py
Normal file
47
battleship/battleship.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .player import Player
|
||||||
|
from .ship import ShipType
|
||||||
|
from .ui import clear_screen, format_board, opponent_cell_fn
|
||||||
|
|
||||||
|
|
||||||
|
def play_turn(player: Player, opponent: Player):
|
||||||
|
guess = player.guess(opponent)
|
||||||
|
player.guesses.add(guess)
|
||||||
|
|
||||||
|
ship = opponent.ship_at(guess)
|
||||||
|
|
||||||
|
clear_screen()
|
||||||
|
print(format_board(opponent_cell_fn(player, opponent), opponent.board))
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not ship:
|
||||||
|
print("Miss!")
|
||||||
|
elif ship.sunk(player.guesses):
|
||||||
|
print(f"{player.name} sunk the enemy {ship.name}")
|
||||||
|
else:
|
||||||
|
print("Hit!")
|
||||||
|
|
||||||
|
print("\n")
|
||||||
|
input("Press ENTER to continue.")
|
||||||
|
|
||||||
|
|
||||||
|
def play_battleship(player_1: Player, player_2: Player):
|
||||||
|
clear_screen()
|
||||||
|
ship_types = list(sorted(ShipType, key=lambda t: t.size, reverse=True))[-1:]
|
||||||
|
player_1.place_ships(ship_types)
|
||||||
|
player_2.place_ships(ship_types)
|
||||||
|
|
||||||
|
while not player_1.defeated(player_2.guesses) and not player_2.defeated(player_1.guesses):
|
||||||
|
play_turn(player_1, player_2)
|
||||||
|
play_turn(player_2, player_1)
|
||||||
|
|
||||||
|
clear_screen()
|
||||||
|
if player_1.defeated(player_2.guesses) and player_2.defeated(player_1.guesses):
|
||||||
|
print("It's a draw!")
|
||||||
|
elif player_1.defeated(player_2.guesses):
|
||||||
|
print(f"{player_2.name} wins!")
|
||||||
|
else:
|
||||||
|
print(f"{player_1.name} wins!")
|
||||||
|
|
||||||
|
input("Press ENTER to exit.")
|
||||||
22
battleship/board.py
Normal file
22
battleship/board.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from .bs_types import Coordinate
|
||||||
|
|
||||||
|
|
||||||
|
class Board:
|
||||||
|
"""
|
||||||
|
Represents the shape of a board and provides utility functions related its coordinate space.
|
||||||
|
"""
|
||||||
|
def __init__(self, width, height):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rows(self):
|
||||||
|
return range(self.height)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def columns(self):
|
||||||
|
return range(self.width)
|
||||||
|
|
||||||
|
def valid(self, coordinate: Coordinate) -> bool:
|
||||||
|
x, y = coordinate
|
||||||
|
return x in self.columns and y in self.rows
|
||||||
4
battleship/bs_types.py
Normal file
4
battleship/bs_types.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from typing import Tuple, Callable
|
||||||
|
|
||||||
|
Coordinate = Tuple[int, int]
|
||||||
|
CellFn = Callable[[Coordinate], str]
|
||||||
54
battleship/human.py
Normal file
54
battleship/human.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .bs_types import Coordinate
|
||||||
|
from .player import Player
|
||||||
|
from .ship import Ship, ShipType
|
||||||
|
from .ui import read_coordinate, format_board, player_cell_fn, read_orientation, opponent_cell_fn, \
|
||||||
|
clear_screen
|
||||||
|
|
||||||
|
|
||||||
|
class HumanPlayer(Player):
|
||||||
|
def place_ships(self, ship_types: List[ShipType]):
|
||||||
|
print(f"{self.name}, time to place your ships. Press ENTER to continue.")
|
||||||
|
input()
|
||||||
|
super().place_ships(ship_types)
|
||||||
|
clear_screen()
|
||||||
|
|
||||||
|
def place_ship(self, ship_type: ShipType) -> Ship:
|
||||||
|
current_board = format_board(player_cell_fn(self), self.board)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
print(current_board)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"Please place your {ship_type.name} (size {ship_type.size}).")
|
||||||
|
coord = read_coordinate(f"What is the top left coordinate of the ship? ", self.board)
|
||||||
|
orientation = read_orientation("Does the ship extend [D]own or to the [R]ight? ")
|
||||||
|
print()
|
||||||
|
|
||||||
|
ship = Ship(coord, ship_type, orientation)
|
||||||
|
|
||||||
|
if self.validate_placement(ship):
|
||||||
|
return ship
|
||||||
|
else:
|
||||||
|
print("Press ENTER to try again.\n")
|
||||||
|
input()
|
||||||
|
|
||||||
|
def guess(self, opponent: Player) -> Coordinate:
|
||||||
|
current_board = format_board(opponent_cell_fn(self, opponent), opponent.board)
|
||||||
|
while True:
|
||||||
|
clear_screen()
|
||||||
|
print(current_board)
|
||||||
|
print()
|
||||||
|
|
||||||
|
coord = read_coordinate(f"{self.name}, where would you like to aim your cannons? ", opponent.board)
|
||||||
|
|
||||||
|
if coord not in self.guesses:
|
||||||
|
return coord
|
||||||
|
|
||||||
|
print("You've already tried that coordinate!")
|
||||||
|
print("Press ENTER to try again.\n")
|
||||||
|
input()
|
||||||
58
battleship/player.py
Normal file
58
battleship/player.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Set, Optional
|
||||||
|
|
||||||
|
from .board import Board
|
||||||
|
from .ship import Ship, ShipType
|
||||||
|
from .bs_types import Coordinate
|
||||||
|
from .ui import format_coordinate
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
def __init__(self, name: str, board: Board):
|
||||||
|
self.name = name
|
||||||
|
self.board = board
|
||||||
|
self.guesses: Set[Coordinate] = set()
|
||||||
|
self.ships: List[Ship] = []
|
||||||
|
|
||||||
|
def place_ships(self, ship_types: List[ShipType]):
|
||||||
|
for ship_type in ship_types:
|
||||||
|
self.ships.append(self.place_ship(ship_type))
|
||||||
|
|
||||||
|
def place_ship(self, ship_type: ShipType) -> Ship:
|
||||||
|
"""
|
||||||
|
Return a new ship of the given type which does not collide with any existing ships.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def validate_placement(self, ship: Ship) -> bool:
|
||||||
|
if not all(self.board.valid(coord) for coord in ship.coordinates):
|
||||||
|
print("Ship does not fit on the board.\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
for other in self.ships:
|
||||||
|
if ship.coordinates.intersection(other.coordinates):
|
||||||
|
print(f"Ship collides with {other.name} at {format_coordinate(other.origin)}.\n")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def guess(self, opponent: Player) -> Coordinate:
|
||||||
|
"""
|
||||||
|
Subclasses should override this with a method that returns a coordinate the player will try to attack.
|
||||||
|
:param opponent: The opponent being attacked.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def ship_cells(self) -> Set[Coordinate]:
|
||||||
|
cells = set()
|
||||||
|
for ship in self.ships:
|
||||||
|
cells = cells.union(ship.coordinates)
|
||||||
|
return cells
|
||||||
|
|
||||||
|
def ship_at(self, coordinate: Coordinate) -> Optional[Ship]:
|
||||||
|
for ship in self.ships:
|
||||||
|
if ship.hit(coordinate):
|
||||||
|
return ship
|
||||||
|
return None
|
||||||
|
|
||||||
|
def defeated(self, guesses: Set[Coordinate]):
|
||||||
|
return all(ship.sunk(guesses) for ship in self.ships)
|
||||||
55
battleship/ship.py
Normal file
55
battleship/ship.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum, auto
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
from .bs_types import Coordinate
|
||||||
|
|
||||||
|
|
||||||
|
class ShipOrientation(Enum):
|
||||||
|
RIGHT = auto()
|
||||||
|
DOWN = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class ShipType(Enum):
|
||||||
|
PATROL_BOAT = (2, "P")
|
||||||
|
DESTROYER = (3, "D")
|
||||||
|
SUBMARINE = (3, "S")
|
||||||
|
BATTLESHIP = (4, "B")
|
||||||
|
CARRIER = (5, "C")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def size(self) -> int:
|
||||||
|
return self.value[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def symbol(self) -> str:
|
||||||
|
return self.value[1]
|
||||||
|
|
||||||
|
|
||||||
|
class Ship:
|
||||||
|
origin: Coordinate
|
||||||
|
ship_type: ShipType
|
||||||
|
orientation: ShipOrientation
|
||||||
|
coordinates: Set[Coordinate]
|
||||||
|
|
||||||
|
def __init__(self, origin: Coordinate, ship_type: ShipType, orientation: ShipOrientation):
|
||||||
|
self.origin = origin
|
||||||
|
self.ship_type = ship_type
|
||||||
|
self.orientation = orientation
|
||||||
|
|
||||||
|
x, y = origin
|
||||||
|
if self.orientation == ShipOrientation.RIGHT:
|
||||||
|
self.coordinates = {(x_i, y) for x_i in range(x, x + self.ship_type.size)}
|
||||||
|
else:
|
||||||
|
self.coordinates = {(x, y_i) for y_i in range(y, y + self.ship_type.size)}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.ship_type.name.capitalize()
|
||||||
|
|
||||||
|
def sunk(self, guesses: Set[Coordinate]) -> bool:
|
||||||
|
return self.coordinates.issubset(guesses)
|
||||||
|
|
||||||
|
def hit(self, guess: Coordinate) -> bool:
|
||||||
|
return guess in self.coordinates
|
||||||
152
battleship/ui.py
Normal file
152
battleship/ui.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from string import ascii_uppercase
|
||||||
|
|
||||||
|
from .board import Board
|
||||||
|
from .ship import Ship, ShipOrientation, ShipType
|
||||||
|
from .bs_types import Coordinate, CellFn
|
||||||
|
|
||||||
|
GUESS_REGEX = re.compile(r"(?P<x>[A-Z])(?P<y>\d+)")
|
||||||
|
|
||||||
|
|
||||||
|
def clear_screen():
|
||||||
|
if os.name == "nt":
|
||||||
|
os.system("cls")
|
||||||
|
else:
|
||||||
|
os.system("clear")
|
||||||
|
|
||||||
|
|
||||||
|
def read_coordinate(prompt: str, board: Board) -> Coordinate:
|
||||||
|
while True:
|
||||||
|
guess = input(prompt)
|
||||||
|
match: re.Match = GUESS_REGEX.match(guess.strip().upper())
|
||||||
|
|
||||||
|
if not match:
|
||||||
|
print("Coordinates should be of the form [LETTER][NUMBER], for example: B5.\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
x, y = match.groups()
|
||||||
|
x = ascii_uppercase.find(x)
|
||||||
|
y = int(y) - 1
|
||||||
|
coordinate = (x, y)
|
||||||
|
|
||||||
|
if not board.valid(coordinate):
|
||||||
|
print(f"Coordinate {guess} is outside the board.\n")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return coordinate
|
||||||
|
|
||||||
|
|
||||||
|
def format_coordinate(coordinate: Coordinate) -> str:
|
||||||
|
x, y = coordinate
|
||||||
|
return f"{ascii_uppercase[x]}{y+1}"
|
||||||
|
|
||||||
|
|
||||||
|
def read_orientation(prompt: str) -> ShipOrientation:
|
||||||
|
while True:
|
||||||
|
orientation = input(prompt).strip().upper()[:1]
|
||||||
|
|
||||||
|
if orientation == "D":
|
||||||
|
return ShipOrientation.DOWN
|
||||||
|
elif orientation == "R":
|
||||||
|
return ShipOrientation.RIGHT
|
||||||
|
else:
|
||||||
|
print("Please answer D or R.\n")
|
||||||
|
|
||||||
|
|
||||||
|
def player_cell_fn(player) -> CellFn:
|
||||||
|
"""
|
||||||
|
Creates a cell renderer for the player's view of their own board.
|
||||||
|
"""
|
||||||
|
def _cell(coordinate: Coordinate) -> str:
|
||||||
|
ship = player.ship_at(coordinate)
|
||||||
|
if not ship:
|
||||||
|
return " "
|
||||||
|
return ship.ship_type.symbol
|
||||||
|
|
||||||
|
return _cell
|
||||||
|
|
||||||
|
|
||||||
|
def opponent_cell_fn(player, opponent) -> CellFn:
|
||||||
|
"""
|
||||||
|
Creates a cell renderer for a player's view of their opponent's board.
|
||||||
|
"""
|
||||||
|
def _cell(coordinate: Coordinate) -> str:
|
||||||
|
if coordinate not in player.guesses:
|
||||||
|
return " "
|
||||||
|
else:
|
||||||
|
ship = opponent.ship_at(coordinate)
|
||||||
|
|
||||||
|
if not ship:
|
||||||
|
return "x"
|
||||||
|
|
||||||
|
if ship.sunk(player.guesses):
|
||||||
|
return ship.ship_type.symbol
|
||||||
|
else:
|
||||||
|
return "*"
|
||||||
|
|
||||||
|
return _cell
|
||||||
|
|
||||||
|
|
||||||
|
def format_board(cell_fn: CellFn, board: Board, row_separator="-", column_separator="|", intersection="+") -> str:
|
||||||
|
"""
|
||||||
|
Formats the current board state as a string.
|
||||||
|
|
||||||
|
:param cell_fn: A rendering function that takes a cell coordinate and returns a string.
|
||||||
|
:param board: The board we're rendering.
|
||||||
|
:param row_separator: A character to render between rows.
|
||||||
|
:param column_separator: A character to render between columns.
|
||||||
|
:param intersection: A character to render at intersections.
|
||||||
|
:return: A string representing the current board state.
|
||||||
|
"""
|
||||||
|
y_label_width = len(str(board.width+1))
|
||||||
|
cell_width = 3
|
||||||
|
|
||||||
|
rendered_board = ""
|
||||||
|
divider = " " * y_label_width + column_separator + (row_separator*cell_width + intersection) * board.width + "\n"
|
||||||
|
|
||||||
|
# Render the header with tickmarks for the X axis
|
||||||
|
# We use letters to represent the X axis so that each header will always be a single cell
|
||||||
|
rendered_board += " " * y_label_width + column_separator
|
||||||
|
for tick in ascii_uppercase[:board.width]:
|
||||||
|
rendered_board += tick.center(cell_width) + column_separator
|
||||||
|
rendered_board += "\n"
|
||||||
|
|
||||||
|
for y in board.rows:
|
||||||
|
rendered_board += divider
|
||||||
|
row_label = str(y+1).rjust(y_label_width)
|
||||||
|
|
||||||
|
rendered_board += f"{row_label}{column_separator}"
|
||||||
|
|
||||||
|
for x in board.columns:
|
||||||
|
coordinate = (x, y)
|
||||||
|
cell = cell_fn(coordinate)
|
||||||
|
rendered_board += cell.center(cell_width) + column_separator
|
||||||
|
|
||||||
|
rendered_board += "\n"
|
||||||
|
|
||||||
|
rendered_board += divider
|
||||||
|
|
||||||
|
return rendered_board
|
||||||
|
|
||||||
|
|
||||||
|
def print_test_board():
|
||||||
|
from .player import Player
|
||||||
|
board = Board(7, 7)
|
||||||
|
p1 = Player("P1", board)
|
||||||
|
p1.guesses.add((0, 0))
|
||||||
|
p1.guesses.add((2, 0))
|
||||||
|
p2 = Player("P2", board)
|
||||||
|
p2.ships.append(Ship((0, 0), ShipType.DESTROYER, ShipOrientation.RIGHT))
|
||||||
|
|
||||||
|
status_fn = opponent_cell_fn(p1, p2)
|
||||||
|
player_cell = player_cell_fn(p2)
|
||||||
|
|
||||||
|
print(format_board(status_fn, board))
|
||||||
|
print(format_board(player_cell, board))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print_test_board()
|
||||||
Reference in New Issue
Block a user