Created
April 1, 2018 00:44
-
-
Save nick-brady/b5503467616814da7750b6c2c8517bf1 to your computer and use it in GitHub Desktop.
A terminal battleship implementation to practice object oriented concepts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from random import randint | |
from time import sleep | |
# ALG_DEBUG = True | |
ALG_DEBUG = False | |
def blue(string): | |
return '\x1b[0;34;40m' + string + '\x1b[0m' | |
def green(string): | |
return '\x1b[0;32;40m' + string + '\x1b[0m' | |
def grey(string): | |
return '\x1b[1;37;40m' + string + '\x1b[0m' | |
def red(string): | |
return '\x1b[0;31;40m' + string + '\x1b[0m' | |
def yellow(string): | |
return '\x1b[0;33;40m' + string + '\x1b[0m' | |
class GameManager: | |
def __init__(self): | |
computerBoard = Board() | |
userBoard = Board() | |
self.computer = Computer(friendly_board=computerBoard, | |
enemy_board=userBoard) | |
self.user = Player(friendly_board=userBoard, | |
enemy_board=computerBoard) | |
def run_game(self): | |
"""Run the battleship game. Each user takes turns until there is a winner""" | |
print("ENEMY") | |
self.user.enemy_board.print_board(show_ships=False) | |
print("FRIENDLY") | |
self.user.friendly_board.print_board(show_ships=True) | |
while not self.user.has_won and not self.computer.has_won: | |
if not ALG_DEBUG: | |
self.user.run_turn() | |
if self.user.enemy_board.game_over: | |
self.user.won() | |
self.computer.run_turn() | |
if self.user.friendly_board.game_over: | |
self.computer.won() | |
if not ALG_DEBUG: | |
print("ENEMY") | |
self.user.enemy_board.print_board(show_ships=False) | |
if ALG_DEBUG: | |
sleep(.5) | |
print("FRIENDLY") | |
self.user.friendly_board.print_board(show_ships=True) | |
print(green("Game Over!!!")) | |
if self.user.has_won: | |
print(green('Congratulations!')) | |
else: | |
print(green('Congratulations, you\'re bad at Battleship!')) | |
class Player: | |
def __init__(self, friendly_board, enemy_board): | |
self.enemy_board = enemy_board | |
self.friendly_board = friendly_board | |
self.is_computer = False | |
self.has_won = False | |
self.last_hit = None | |
def get_random_xy(self): | |
"""Return random x, y that has not been guessed before""" | |
x = randint(0, self.enemy_board.n - 1) | |
y = randint(0, self.enemy_board.n - 1) | |
if self.enemy_board.has_been_hit(x, y): | |
# TODO: as this gets closer to the game ending, this will get harder and harder for the program. Optimize | |
# Better solution with higher space complexity, when board is initialized, create a list of tuples of ever | |
# possible move. pop off an element for each guess randomly. will avoid this problem and move is guranteed | |
# to be valid | |
return self.get_random_xy() # Try again for a place that hasn't been hit yet | |
else: | |
return (x, y) | |
def get_xy(self): | |
"""Get user input for x and y position on game board, user can choose to random""" | |
positions = input('Please enter x and y position to hit separated by comma: ') | |
if positions == 'random': | |
return self.get_random_xy() | |
positions = positions.replace(' ', '').split(',') # sanitize | |
try: | |
(x, y) = [int(e) for e in positions] | |
maxY = maxX = self.enemy_board.n - 1 | |
if x > maxX or x < 0 or y > maxY or y < 0: | |
print('Please enter numbers within the game bounds') | |
return self.get_xy() | |
return (x, y) | |
except ValueError: | |
return self.get_xy() | |
def run_turn(self): | |
"""Runs turn for computer or player. User may enter random to randomly pick a point""" | |
(x, y) = self.get_xy() | |
self.enemy_board.mark_shot(x, y) | |
def won(self): | |
self.has_won = True | |
class Computer(Player): | |
vicinity = { | |
'left': (-1, 0), | |
'top': (0, 1), | |
'right': (1, 0), | |
'bottom': (0, -1) | |
} | |
def __init__(self, friendly_board, enemy_board): | |
super().__init__(friendly_board, enemy_board) | |
self.is_computer = True | |
self.hitUndestroyedShip = False | |
self.searchingPhase = False | |
self.shipDirection = None | |
self.hit_on_ship = None | |
self.sweepPhase = False | |
def reset_search(self): | |
self.shipDirection = None | |
self.hit_on_ship = None | |
self.searchingPhase = False | |
self.sweepPhase = False | |
self.hitUndestroyedShip = False | |
def change_direction(self): | |
if self.shipDirection == 'horizontal': | |
self.shipDirection = 'vertical' | |
else: | |
self.shipDirection = 'horizontal' | |
def find_next_unhit(self): | |
"""Will get the next un-hit square along an axis (either horizontal or vertical). | |
If they have already been hit, then the axis is switched, and the process is repeated | |
""" | |
x, y = self.hit_on_ship | |
sweep_other_direction = False # Added all the way until was water and a hit, reverse direction until find unhit ship or hit water | |
if self.shipDirection == 'horizontal': | |
# Try sweeping right until we find an uhit square | |
while self.enemy_board.board[y][x].is_hit: | |
x += 1 | |
if x > self.enemy_board.n - 1 or x < 0: | |
sweep_other_direction = True | |
x -= 1 | |
break | |
# Not this way! Lets reverse and see if we can find a valid square | |
if self.enemy_board.has_been_hit(x, y) and not self.enemy_board.board[y][x].is_ship: | |
sweep_other_direction = True | |
break | |
else: # vertical | |
while self.enemy_board.has_been_hit(x, y): | |
y += 1 | |
if y > self.enemy_board.n - 1 or y < 0: | |
sweep_other_direction = True | |
y -= 1 | |
break | |
# Not this way! Lets reverse and see if we can find a valid square | |
if self.enemy_board.has_been_hit(x, y) and not self.enemy_board.board[y][x].is_ship: | |
sweep_other_direction = True | |
break | |
if sweep_other_direction: | |
if self.shipDirection == 'horizontal': | |
while self.enemy_board.has_been_hit(x, y): | |
x -= 1 | |
if x > self.enemy_board.n - 1 or x < 0: | |
raise Exception('This should never occur, investigate') | |
# Not this way! Lets reverse and see if we can find a valid square | |
if self.enemy_board.has_been_hit(x, y) and not self.enemy_board.board[y][x].is_ship: | |
self.change_direction() | |
x, y = self.find_next_unhit() | |
break | |
else: # vertical | |
while self.enemy_board.has_been_hit(x, y): | |
y -= 1 | |
if y > self.enemy_board.n - 1 or y < 0: | |
raise Exception('This should never occur, investigate') | |
# Not this way! Lets reverse and see if we can find a valid square | |
if self.enemy_board.has_been_hit(x, y) and not self.enemy_board.board[y][x].is_ship: | |
self.change_direction() | |
x, y = self.find_next_unhit() | |
break | |
return x, y | |
def run_turn(self): | |
"""Algorithm to run computers turn and make smart decisions""" | |
if self.hitUndestroyedShip: | |
# start hitting squares around you. | |
x, y = self.hit_on_ship | |
if self.searchingPhase: | |
# Guess around the original hit on ship until a ship is hit | |
for (adjx, adjy) in Computer.vicinity.values(): | |
if adjy + y > self.enemy_board.n - 1 or adjy + y < 0 or adjx + x > self.enemy_board.n - 1 or adjx + x < 0: | |
# prevent computer from guessing out of bounds | |
continue | |
if not self.enemy_board.has_been_hit(adjx + x, adjy + y): | |
break # lock in ajdx and adjy at current value | |
x += adjx | |
y += adjy | |
self.enemy_board.mark_shot(x, y) | |
square = self.enemy_board.board[y][x] | |
# If missed, will have to try again, otherwise, gather info on ship direction | |
if square.is_ship: | |
if square.ship.is_sunk: | |
self.reset_search() | |
else: | |
# we know there is a direction at this point, don't need to | |
# keep guessing around the original ship hit | |
self.searchingPhase = False | |
self.sweepPhase = True | |
# Check if ship is horizontal or vertical | |
lx, ly = self.hit_on_ship | |
dx, dy = (x - lx, y - ly) | |
if dx is 0: | |
self.shipDirection = 'vertical' | |
elif dy is 0: | |
self.shipDirection = 'horizontal' | |
else: | |
raise Exception('This should never occur, investigate') | |
# We know the ship direction, lets try sweeping this direction until ship is sunk or miss on both sides | |
elif self.sweepPhase: | |
x, y = self.find_next_unhit() | |
self.enemy_board.mark_shot(x, y) | |
square = self.enemy_board.board[y][x] | |
if square.is_ship: | |
if square.ship.is_sunk: | |
self.reset_search() | |
else: | |
(x, y) = self.get_random_xy() | |
self.enemy_board.mark_shot(x, y) # any square is just as likely as another, guess randomly | |
# if hit a ship square, and that did not sink a ship, should set hitUndestroyed to true | |
square = self.enemy_board.board[y][x] | |
if square.is_ship: | |
if square.ship.is_sunk: | |
self.reset_search() | |
else: | |
# mark the hit on ship | |
self.hit_on_ship = (x, y) | |
# guess around the hit on ship | |
self.searchingPhase = True | |
self.hitUndestroyedShip = True | |
class BoardSquare: | |
"""Generic square on a board that does not belong to a ship""" | |
def __init__(self): | |
self.is_hit = False | |
self.is_ship = False | |
def render(self, show_ships): | |
if self.is_hit: | |
return yellow('O') | |
else: | |
return blue('-') | |
def hit(self): | |
self.is_hit = True | |
class ShipSquare(BoardSquare): | |
"""Generic square on a board that a ship lies on. A ship is composed of ship squares""" | |
def __init__(self, ship): | |
super().__init__() | |
self.ship = ship | |
self.is_ship = True | |
def render(self, show_ships): | |
if self.is_hit: | |
return red('X') | |
else: | |
return grey('@') if show_ships else blue('-') | |
def hit(self): | |
self.is_hit = True | |
self.ship.check_if_sunk() | |
class Ship: | |
"""A collection of ships on a game board""" | |
ships = { | |
'carrier': 5, | |
'battleship': 4, | |
'submarine': 3, | |
'cruiser': 3, | |
'destroyer': 2, | |
} | |
def __init__(self, name, board): | |
self.name = name | |
self.board = board | |
self.is_sunk = False | |
self.squares = [] | |
# TODO: is_sunk could use property decorator instead of setting it this way. | |
def check_if_sunk(self): | |
if all([shipsquare.is_hit for shipsquare in self.squares]): # all shipsquares have been hit | |
print(green('You sunk my %s!!!' % self.name)) | |
self.is_sunk = True | |
self.board.check_if_lose() | |
class Board: | |
"""Grid of BoardSquare or ShipSquare instances. Contains methods to interact | |
with board and to place initial ships""" | |
def __init__(self, n = 10): | |
# will initialize nxn matrix of BoardSquares to which ShipSquares will be added later | |
# n has to be at least 10, but can be greater if specified | |
if n < 10: | |
raise Exception ('Size of board must be at least 10') | |
self.n = n | |
self.ships = [] | |
self.game_over = False | |
self.board = self.generate_board() | |
self.place_ships() | |
def check_if_lose(self): | |
if all([ship.is_sunk for ship in self.ships]): # all ships have been sunk | |
self.game_over = True | |
print('all my ships have been sunk!!') | |
def generate_board(self): | |
new_board = [[BoardSquare() for _ in range(self.n)] for _ in range(self.n)] | |
return new_board | |
def print_board(self, show_ships): | |
"""Function will print out board""" | |
xpos_row = [' '] + [str(i) for i in range(10)] | |
board_with_ypos = [[str(i)] + list(map(lambda x: x.render(show_ships), row)) for i, row in enumerate(self.board)] | |
board_with_ypos.insert(0, xpos_row) | |
print('\n'.join([' '.join(row) for row in board_with_ypos])) | |
def mark_shot(self, xpos, ypos): | |
if xpos >= self.n or xpos < 0 or ypos >= self.n or ypos < 0: | |
raise Exception ("Please input a valid position") | |
self.board[ypos][xpos].hit() | |
def has_been_hit(self, xpos, ypos): | |
return self.board[ypos][xpos].is_hit | |
def place_ship(self, ship_name): | |
shipLength = Ship.ships[ship_name] | |
# Randomly determine ship orientation, zero for false, one for true | |
is_vertical = bool(randint(0,1)) | |
maxX = 9 if is_vertical else 9 - shipLength | |
maxY = 9 - shipLength if is_vertical else 9 | |
start_xpos = randint(0, maxX) | |
start_ypos = randint(0, maxY) | |
ship = Ship(ship_name, self) | |
# This portion checks to see if there is another ship in the same location | |
if is_vertical: | |
for y in range(start_ypos, start_ypos + shipLength): | |
if self.board[y][start_xpos].is_ship: | |
return self.place_ship(ship_name) | |
else: | |
for x in range(start_xpos, start_xpos + shipLength): | |
if self.board[start_ypos][x].is_ship: | |
return self.place_ship(ship_name) | |
# Now that we know the spots are clear, we can place our ships | |
if is_vertical: | |
for y in range(start_ypos, start_ypos + shipLength): | |
ss = ShipSquare(ship) | |
ship.squares.append(ss) | |
self.board[y][start_xpos] = ss | |
else: | |
for x in range(start_xpos, start_xpos + shipLength): | |
ss = ShipSquare(ship) | |
ship.squares.append(ss) | |
self.board[start_ypos][x] = ss | |
self.ships.append(ship) | |
def place_ships(self): | |
for ship in Ship.ships.keys(): | |
self.place_ship(ship) | |
game = GameManager() | |
game.run_game() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment