Skip to content

Instantly share code, notes, and snippets.

@nick-brady
Created April 1, 2018 00:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nick-brady/b5503467616814da7750b6c2c8517bf1 to your computer and use it in GitHub Desktop.
Save nick-brady/b5503467616814da7750b6c2c8517bf1 to your computer and use it in GitHub Desktop.
A terminal battleship implementation to practice object oriented concepts
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