Implement AI
This commit is contained in:
@@ -1,12 +1,17 @@
|
|||||||
|
from .ai import AIPlayer
|
||||||
from .battleship import play_battleship
|
from .battleship import play_battleship
|
||||||
from .board import Board
|
from .board import Board
|
||||||
from .human import HumanPlayer
|
from .human import HumanPlayer
|
||||||
|
from .ship import ShipType
|
||||||
|
|
||||||
board = Board(10, 10)
|
board = Board(7, 7)
|
||||||
p1 = HumanPlayer("Player 1", board)
|
# p1 = HumanPlayer("Player 1", board)
|
||||||
p2 = HumanPlayer("Player 2", board)
|
# p2 = HumanPlayer("Player 2", board)
|
||||||
|
p1 = AIPlayer("Player 1", board)
|
||||||
|
p2 = AIPlayer("Player 2", board)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
play_battleship(p1, p2)
|
ship_types = list(sorted(ShipType, key=lambda t: t.size, reverse=True))
|
||||||
|
play_battleship(p1, p2, ship_types)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|||||||
124
battleship/ai.py
124
battleship/ai.py
@@ -1,7 +1,129 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from .player import Player
|
from enum import Enum, auto
|
||||||
|
import random
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .bs_types import Coordinate
|
||||||
|
from .player import Player, PlacementError
|
||||||
|
from .ship import ShipType, Ship, ShipOrientation
|
||||||
|
|
||||||
|
|
||||||
|
class CoordStatus(Enum):
|
||||||
|
UNKNOWN = auto()
|
||||||
|
HIT = auto()
|
||||||
|
SUNK = auto()
|
||||||
|
MISS = auto()
|
||||||
|
|
||||||
|
|
||||||
|
class SearchDirection(Enum):
|
||||||
|
UP = (0, -1)
|
||||||
|
DOWN = (0, 1)
|
||||||
|
LEFT = (-1, 0)
|
||||||
|
RIGHT = (1, 0)
|
||||||
|
|
||||||
|
def move(self, coordinate: Coordinate, distance=1) -> Coordinate:
|
||||||
|
x, y = coordinate
|
||||||
|
dx, dy = self.value
|
||||||
|
dx *= distance
|
||||||
|
dy *= distance
|
||||||
|
return x + dx, y + dy
|
||||||
|
|
||||||
|
def inverse(self) -> SearchDirection:
|
||||||
|
return {
|
||||||
|
SearchDirection.UP: SearchDirection.DOWN,
|
||||||
|
SearchDirection.DOWN: SearchDirection.UP,
|
||||||
|
SearchDirection.LEFT: SearchDirection.RIGHT,
|
||||||
|
SearchDirection.RIGHT: SearchDirection.LEFT,
|
||||||
|
}[self]
|
||||||
|
|
||||||
|
|
||||||
class AIPlayer(Player):
|
class AIPlayer(Player):
|
||||||
|
def place_ship(self, ship_type: ShipType) -> Ship:
|
||||||
|
while True:
|
||||||
|
orientation = random.choice(list(ShipOrientation))
|
||||||
|
max_x, max_y = self.board.width, self.board.height
|
||||||
|
if orientation == ShipOrientation.RIGHT:
|
||||||
|
max_x -= ship_type.size
|
||||||
|
else:
|
||||||
|
max_y -= ship_type.size
|
||||||
|
|
||||||
|
coords = random.randrange(0, max_x), random.randrange(0, max_y)
|
||||||
|
ship = Ship(coords, ship_type, orientation)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.validate_placement(ship)
|
||||||
|
return ship
|
||||||
|
except PlacementError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def guess(self, opponent: Player) -> Coordinate:
|
||||||
|
for guess in self.guesses:
|
||||||
|
if self.coord_status(guess, opponent) == CoordStatus.HIT:
|
||||||
|
return self.guess_near(guess, opponent)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
guess = random.choice(opponent.board.columns), random.choice(opponent.board.rows)
|
||||||
|
if guess in self.guesses:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return guess
|
||||||
|
|
||||||
|
def coord_status(self, coordinate: Coordinate, opponent: Player) -> CoordStatus:
|
||||||
|
if coordinate not in self.guesses:
|
||||||
|
return CoordStatus.UNKNOWN
|
||||||
|
|
||||||
|
ship = opponent.ship_at(coordinate)
|
||||||
|
|
||||||
|
if not ship:
|
||||||
|
return CoordStatus.MISS
|
||||||
|
elif ship.sunk(self.guesses):
|
||||||
|
return CoordStatus.SUNK
|
||||||
|
else:
|
||||||
|
return CoordStatus.HIT
|
||||||
|
|
||||||
|
def guess_near(self, coordinate: Coordinate, opponent: Player) -> Coordinate:
|
||||||
|
# First, look for the directions in which we've found a hit one cell away
|
||||||
|
promising_directions = [
|
||||||
|
direction for direction in SearchDirection
|
||||||
|
if self.coord_status(direction.move(coordinate, 1), opponent) == CoordStatus.HIT
|
||||||
|
]
|
||||||
|
# Failing that, search in the opposite direction of found hits
|
||||||
|
reverse_directions = [direction.inverse() for direction in promising_directions
|
||||||
|
if direction.inverse() not in promising_directions]
|
||||||
|
|
||||||
|
# As a last resort, search in other directions in random order
|
||||||
|
remaining_directions = [direction for direction in SearchDirection
|
||||||
|
if direction not in promising_directions + reverse_directions]
|
||||||
|
random.shuffle(remaining_directions)
|
||||||
|
|
||||||
|
search_order = promising_directions + reverse_directions + remaining_directions
|
||||||
|
|
||||||
|
for direction in search_order:
|
||||||
|
candidate = self.try_direction(coordinate, direction, opponent)
|
||||||
|
if candidate:
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
def try_direction(
|
||||||
|
self,
|
||||||
|
coordinate: Coordinate,
|
||||||
|
direction: SearchDirection,
|
||||||
|
opponent: Player
|
||||||
|
) -> Optional[Coordinate]:
|
||||||
|
"""
|
||||||
|
Search for candidate cells in a given direction from a known hit.
|
||||||
|
"""
|
||||||
|
for distance in range(1, 5):
|
||||||
|
candidate = direction.move(coordinate, distance)
|
||||||
|
status = self.coord_status(candidate, opponent)
|
||||||
|
|
||||||
|
# We went off the board! Time to stop.
|
||||||
|
if not opponent.board.valid(candidate):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# We've hit a dead end in this direction
|
||||||
|
if status in [CoordStatus.MISS, CoordStatus.SUNK]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if status == CoordStatus.UNKNOWN:
|
||||||
|
return candidate
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from .player import Player
|
from .player import Player
|
||||||
from .ship import ShipType
|
from .ship import ShipType
|
||||||
from .ui import clear_screen, format_board, opponent_cell_fn
|
from .ui import clear_screen, format_board, opponent_cell_fn
|
||||||
@@ -9,26 +11,24 @@ def play_turn(player: Player, opponent: Player):
|
|||||||
guess = player.guess(opponent)
|
guess = player.guess(opponent)
|
||||||
player.guesses.add(guess)
|
player.guesses.add(guess)
|
||||||
|
|
||||||
ship = opponent.ship_at(guess)
|
|
||||||
|
|
||||||
clear_screen()
|
clear_screen()
|
||||||
print(format_board(opponent_cell_fn(player, opponent), opponent.board))
|
print(format_board(opponent_cell_fn(player, opponent, guess), opponent.board))
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
ship = opponent.ship_at(guess)
|
||||||
if not ship:
|
if not ship:
|
||||||
print("Miss!")
|
print(f"{player.name}: Miss!")
|
||||||
elif ship.sunk(player.guesses):
|
elif ship.sunk(player.guesses):
|
||||||
print(f"{player.name} sunk the enemy {ship.name}")
|
print(f"{player.name} sunk the enemy {ship.name}!")
|
||||||
else:
|
else:
|
||||||
print("Hit!")
|
print(f"{player.name}: Hit!")
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
input("Press ENTER to continue.")
|
input("Press ENTER to continue.")
|
||||||
|
|
||||||
|
|
||||||
def play_battleship(player_1: Player, player_2: Player):
|
def play_battleship(player_1: Player, player_2: Player, ship_types: List[ShipType]):
|
||||||
clear_screen()
|
clear_screen()
|
||||||
ship_types = list(sorted(ShipType, key=lambda t: t.size, reverse=True))[-1:]
|
|
||||||
player_1.place_ships(ship_types)
|
player_1.place_ships(ship_types)
|
||||||
player_2.place_ships(ship_types)
|
player_2.place_ships(ship_types)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from .bs_types import Coordinate
|
from .bs_types import Coordinate
|
||||||
from .player import Player
|
from .player import Player, PlacementError
|
||||||
from .ship import Ship, ShipType
|
from .ship import Ship, ShipType
|
||||||
from .ui import read_coordinate, format_board, player_cell_fn, read_orientation, opponent_cell_fn, \
|
from .ui import read_coordinate, format_board, player_cell_fn, read_orientation, opponent_cell_fn, \
|
||||||
clear_screen
|
clear_screen
|
||||||
@@ -31,9 +31,11 @@ class HumanPlayer(Player):
|
|||||||
|
|
||||||
ship = Ship(coord, ship_type, orientation)
|
ship = Ship(coord, ship_type, orientation)
|
||||||
|
|
||||||
if self.validate_placement(ship):
|
try:
|
||||||
|
self.validate_placement(ship)
|
||||||
return ship
|
return ship
|
||||||
else:
|
except PlacementError as e:
|
||||||
|
print(e.message + "\n")
|
||||||
print("Press ENTER to try again.\n")
|
print("Press ENTER to try again.\n")
|
||||||
input()
|
input()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ from .bs_types import Coordinate
|
|||||||
from .ui import format_coordinate
|
from .ui import format_coordinate
|
||||||
|
|
||||||
|
|
||||||
|
class PlacementError(Exception):
|
||||||
|
@property
|
||||||
|
def message(self) -> str:
|
||||||
|
return self.args[0]
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
class Player:
|
||||||
def __init__(self, name: str, board: Board):
|
def __init__(self, name: str, board: Board):
|
||||||
self.name = name
|
self.name = name
|
||||||
@@ -24,17 +30,13 @@ class Player:
|
|||||||
Return a new ship of the given type which does not collide with any existing ships.
|
Return a new ship of the given type which does not collide with any existing ships.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def validate_placement(self, ship: Ship) -> bool:
|
def validate_placement(self, ship: Ship):
|
||||||
if not all(self.board.valid(coord) for coord in ship.coordinates):
|
if not all(self.board.valid(coord) for coord in ship.coordinates):
|
||||||
print("Ship does not fit on the board.\n")
|
raise PlacementError("Ship does not fit on the board.")
|
||||||
return False
|
|
||||||
|
|
||||||
for other in self.ships:
|
for other in self.ships:
|
||||||
if ship.coordinates.intersection(other.coordinates):
|
if ship.coordinates.intersection(other.coordinates):
|
||||||
print(f"Ship collides with {other.name} at {format_coordinate(other.origin)}.\n")
|
raise PlacementError(f"Ship collides with {other.name} at {format_coordinate(other.origin)}.")
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def guess(self, opponent: Player) -> Coordinate:
|
def guess(self, opponent: Player) -> Coordinate:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from string import ascii_uppercase
|
from string import ascii_uppercase
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .board import Board
|
from .board import Board
|
||||||
from .ship import Ship, ShipOrientation, ShipType
|
from .ship import Ship, ShipOrientation, ShipType
|
||||||
@@ -69,7 +70,7 @@ def player_cell_fn(player) -> CellFn:
|
|||||||
return _cell
|
return _cell
|
||||||
|
|
||||||
|
|
||||||
def opponent_cell_fn(player, opponent) -> CellFn:
|
def opponent_cell_fn(player, opponent, latest: Optional[Coordinate] = None) -> CellFn:
|
||||||
"""
|
"""
|
||||||
Creates a cell renderer for a player's view of their opponent's board.
|
Creates a cell renderer for a player's view of their opponent's board.
|
||||||
"""
|
"""
|
||||||
@@ -80,12 +81,16 @@ def opponent_cell_fn(player, opponent) -> CellFn:
|
|||||||
ship = opponent.ship_at(coordinate)
|
ship = opponent.ship_at(coordinate)
|
||||||
|
|
||||||
if not ship:
|
if not ship:
|
||||||
return "x"
|
symbol = "x"
|
||||||
|
elif ship.sunk(player.guesses):
|
||||||
if ship.sunk(player.guesses):
|
symbol = ship.ship_type.symbol
|
||||||
return ship.ship_type.symbol
|
|
||||||
else:
|
else:
|
||||||
return "*"
|
symbol = "*"
|
||||||
|
|
||||||
|
if coordinate == latest:
|
||||||
|
symbol = f"({symbol})"
|
||||||
|
|
||||||
|
return symbol
|
||||||
|
|
||||||
return _cell
|
return _cell
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user