Battleships
Attribution
Licensed under GNU GENERAL PUBLIC LICENSE Version 3.
Original Python code
import math
import random
import pygame
import pgzrun
from pgzero.actor import Actor
class Grid:
# Grid dimensions are in terms of screen pixels
# Tools to convert between different values are
# included as static methods
def __init__ (self, start_grid, grid_size):
self.start_grid = start_grid
self.grid_size = grid_size
# Does co-ordinates match this grid
def check_in_grid (self, screen_pos):
if (screen_pos[0] < self.start_grid[0] or
screen_pos[1] < self.start_grid[1] or
screen_pos[0] > self.start_grid[0] + (self.grid_size[0] * 10) or
screen_pos[1] > self.start_grid[1] + (self.grid_size[1] * 10)):
return False
else:
return True
def get_grid_pos (self, screen_pos):
x_offset = screen_pos[0] - self.start_grid[0]
x = math.floor(x_offset / self.grid_size[0])
y_offset = screen_pos[1] - self.start_grid[1]
y = math.floor(y_offset / self.grid_size[1])
if (x < 0 or y < 0 or x > 9 or y > 9):
return None
return (x,y)
# Gets top left of a grid position - returns as screen position
def grid_pos_to_screen_pos (self, grid_pos):
x = self.start_grid[0] + (grid_pos[0] * self.grid_size[0])
y = self.start_grid[1] + (grid_pos[1] * self.grid_size[1])
return (x,y)
# Ship is referred to using an x,y position
class Ship (Actor):
def __init__ (self, ship_type, grid, grid_pos, direction, img_txt="", grid_size=(38,28), hidden=False):
Actor.__init__(self, ship_type, (10,10))
self.grid_size = grid_size
self.ship_type = ship_type
self.grid = grid
self.image = ship_type+img_txt
self.grid_pos = grid_pos
self.topleft = self.grid.grid_pos_to_screen_pos((grid_pos))
# Set the actor anchor position to centre of the first square
self.anchor = (grid_size[0]/2, grid_size[1]/2)
self.direction = direction
if (direction == 'vertical'):
self.angle = -90
self.hidden = hidden
if (ship_type == "destroyer"):
self.ship_size = 2
self.hits = [False, False]
elif (ship_type == "cruiser"):
self.ship_size = 3
self.hits = [False, False, False]
elif (ship_type == "submarine"):
self.ship_size = 3
self.hits = [False, False, False]
elif (ship_type == "battleship"):
self.ship_size = 4
self.hits = [False, False, False, False]
elif (ship_type == "carrier"):
self.ship_size = 5
self.hits = [False, False, False, False, False]
def draw(self):
if (self.hidden):
return
Actor.draw(self)
def is_sunk (self):
if (False in self.hits):
return False
return True
def fire (self, fire_grid_pos):
if self.direction == 'horizontal':
if (fire_grid_pos[0] >= self.grid_pos[0] and
fire_grid_pos[0] < self.grid_pos[0]+self.ship_size and
fire_grid_pos[1] == self.grid_pos[1]):
self.hits[fire_grid_pos[0]-self.grid_pos[0]] = True
return True
else:
if (fire_grid_pos[0] == self.grid_pos[0] and
fire_grid_pos[1] >= self.grid_pos[1] and
fire_grid_pos[1] < self.grid_pos[1]+self.ship_size):
self.hits[fire_grid_pos[1]-self.grid_pos[1]] = True
return True
return False
# Does this ship cover this grid_position
def includes_grid_pos (self, check_grid_pos):
# If first pos then return True
if (self.grid_pos == check_grid_pos):
return True
# check x axis
elif (self.direction == 'horizontal' and
self.grid_pos[1] == check_grid_pos[1] and
check_grid_pos[0] >= self.grid_pos[0] and
check_grid_pos[0] < self.grid_pos[0] + self.ship_size):
return True
elif (self.direction == 'vertical' and
self.grid_pos[0] == check_grid_pos[0] and
check_grid_pos[1] >= self.grid_pos[1] and
check_grid_pos[1] < self.grid_pos[1] + self.ship_size):
return True
else :
return False
class Shot(Actor):
def __init__ (self, hit, pos):
Actor.__init__(self,hit)
self.topleft=pos
class Fleet:
def __init__ (self, start_grid, grid_size, img_txt=""):
self.start_grid = start_grid
self.grid_size = grid_size
self.ships = []
self.grid = Grid(start_grid, grid_size)
self.shots = []
self.img_txt = img_txt
def change_grid (self, start_grid, grid_size, img_txt=""):
self.start_grid = start_grid
self.grid_size = grid_size
self.grid.start_grid = self.start_grid
self.grid.grid_size = self.grid_size
self.img_txt = img_txt
# Is there a ship at this position that has sunk
def is_ship_sunk_grid_pos (self, check_grid_pos):
# find ship at that position
for this_ship in self.ships:
if (this_ship.includes_grid_pos(check_grid_pos)):
return this_ship.is_sunk()
# If there is no ship at this position then return False
return False
def add_ship (self, type, position, direction, img_txt="", grid_size=(38,38), hidden=False):
self.ships.append(Ship(type, self.grid, position, direction, img_txt, grid_size, hidden))
# check through ships to see if any still floating
def all_sunk (self):
for this_ship in self.ships:
if not this_ship.is_sunk():
return False
return True
# Draws entire fleet (each of the ships)
def draw(self):
for this_ship in self.ships:
this_ship.draw()
for this_shot in self.shots:
this_shot.draw()
def fire (self, pos):
# Is this a hit
for this_ship in self.ships:
if (this_ship.fire(pos)):
# Hit
self.shots.append(Shot("hit"+self.img_txt,self.grid.grid_pos_to_screen_pos(pos)))
#check if this ship sunk
if this_ship.is_sunk():
# Ship sunk so make it visible
this_ship.hidden = False
return True
self.shots.append(Shot("miss"+self.img_txt,self.grid.grid_pos_to_screen_pos(pos)))
return False
def reset(self):
self.ships = []
self.shots = []
# Provides Ai Player
class Player:
NA = 0
MISS = 1
HIT = 2
def __init__ (self):
# Own grid for positioning own ships
# Set to hit where a ship is positioned
self.owngrid = [ [Player.NA for y in range(10)] for x in range(10) ]
def check_ship_fit (self, ship_size, direction, start_pos):
#print ("Checking {} {} {}".format(ship_size, direction, start_pos))
if (direction == "horizontal"):
# Check if it won't fit on the grid
# -1 as start_pos is included in the size
if ((start_pos[0] + ship_size -1) > 9):
return False
# check that there are no ships in the way
# range goes to one less than max size - so no need for the -1
for x_pos in range(start_pos[0],start_pos[0]+ship_size):
if (self.owngrid[x_pos][start_pos[1]] == Player.HIT):
return False
return True
# Otherwise vertical
else:
# Check if it won't fit on the grid
# -1 as start_pos is included in the size
if ((start_pos[1] + ship_size -1) > 9):
return False
# check that there are no ships in the way
# range goes to one less than max size - so no need for the -1
for y_pos in range (start_pos[1],start_pos[1]+ship_size):
if (self.owngrid[start_pos[0]][y_pos] == Player.HIT):
return False
return True
# Place ship is used during ship placement
# Updates grid with location of ship
def place_ship (self, ship_size, direction, start_pos):
if (direction == "horizontal"):
for x_pos in range (start_pos[0],start_pos[0]+ship_size):
self.owngrid[x_pos][start_pos[1]] = Player.HIT
# otherwise vertical
else:
for y_pos in range (start_pos[1],start_pos[1]+ship_size):
self.owngrid[start_pos[0]][y_pos] = Player.HIT
def reset(self):
self.owngrid = [ [Player.NA for y in range(10)] for x in range(10) ]
# Provides Ai Player
class PlayerAi(Player):
def __init__ (self):
# Create 2 dimension list with no shots fired
# access using [x value][y value]
# Pre-populate with not checked
self.shots = [ [Player.NA for y in range(10)] for x in range(10) ]
# Hit ship is the position of the first successful hit on a ship
self.hit_ship = None
Player.__init__(self)
def fire_shot(self):
# If not targetting hit ship
if (self.hit_ship == None):
return (self.get_random())
else:
# Have scored a hit - so find neighbouring positions
# copy hit_ship into separate values to make easier to follow
hit_x = self.hit_ship[0]
hit_y = self.hit_ship[1]
# Try horizontal if not at edge
if (hit_x < 9):
for x in range (hit_x+1,10):
if (self.shots[x][hit_y] == Player.NA):
return (x,hit_y)
if (self.shots[x][hit_y] == Player.MISS):
break
if (hit_x > 0):
for x in range (hit_x-1,-1, -1):
if (self.shots[x][hit_y] == Player.NA):
return (x,hit_y)
if (self.shots[x][hit_y] == Player.MISS):
break
if (hit_y < 9):
for y in range (hit_y+1,10):
if (self.shots[hit_x][y] == Player.NA):
return (hit_x,y)
if (self.shots[hit_x][y] == Player.MISS):
break
if (hit_y > 0):
for y in range (hit_y-1,-1, -1):
if (self.shots[hit_x][y] == Player.NA):
return (hit_x,y)
if (self.shots[hit_x][y] == Player.MISS):
break
# Catch all - shouldn't get this, but just in case guess random
return (self.get_random())
def fire_result(self, grid_pos, result):
x_pos = grid_pos[0]
y_pos = grid_pos[1]
if (result == True):
result_value = Player.HIT
if (self.hit_ship == None):
self.hit_ship = grid_pos
else:
result_value = Player.MISS
self.shots[x_pos][y_pos] = result_value
def get_random(self):
# Copy only non used positions into a temporary list
non_shots = []
for x_pos in range (0,10):
for y_pos in range (0,10):
if self.shots[x_pos][y_pos] == Player.NA:
non_shots.append((x_pos,y_pos))
return random.choice(non_shots)
# Let Ai know that the last shot sunk a ship
# list_pos is provided, but not currently used
def ship_sunk(self, grid_pos):
# reset hit ship
self.hit_ship = None
# Find a position for the ship -
def position_ship (self, ship_size):
# determine if horizontal or vertical
direction = random.choice(["horizontal","vertical"])
# Position where the ship starts
grid_pos = [None, None]
# Keep trying to find a place until successful
while (grid_pos[0] == None):
possible_positions = []
# if horizontal first choose y axis
if (direction == "horizontal"):
y_pos = random.randint (0,9)
# Find positions that the ship will fit
for x_pos in range (0, 9-ship_size):
if (self.check_ship_fit(ship_size, direction, (x_pos, y_pos))):
possible_positions.append((x_pos,y_pos))
else:
x_pos = random.randint (0,9)
# Find positions that the ship will fit
for y_pos in range (0, 9-ship_size):
if (self.check_ship_fit(ship_size, direction, (x_pos, y_pos))):
possible_positions.append((x_pos,y_pos))
# Did we find any possible positions?
if (len(possible_positions)>0):
position = random.choice(possible_positions)
self.place_ship (ship_size, direction, position)
return (direction, position)
# if didn't get a match then try again
else:
continue
def reset(self):
self.shots = [ [Player.NA for y in range(10)] for x in range(10) ]
Player.reset(self)
# Provides Ai Player
class PlayerHuman(Player):
def __init__ (self):
Player.__init__(self)
# Default screen size - can be changed by config
WIDTH = 1280
HEIGHT = 720
TITLE = "Battleships"
# Set SIZE_SML to True for small size (800 x 480)
SIZE_SML = True
# Set fullscreen
FULLSCREEN = False
GRID_SIZE = (38,38)
# suffix for image
img_txt = ""
# Track if fullscreen
fullscreen_status = False
player = "player1setup"
grid_img_1 = Actor ("grid", topleft=(50,150))
grid_img_2 = Actor ("grid", topleft=(500,150))
# Uses start of grid (after grid labels)
own_fleet = Fleet((94,179), GRID_SIZE)
enemy_fleet = Fleet((544,179), GRID_SIZE)
player1=PlayerHuman()
# Player 2 represents the AI player
player2=PlayerAi()
mouse_position = (0,0)
key_position = (1000, 150)
your_fleet_txt_pos = (100,100)
enemy_fleet_txt_pos = (550,100)
# list of different ship types
ship_list = [
Actor("carrier", topleft=(key_position[0], key_position[1]+70)),
Actor("battleship", topleft=(key_position[0], key_position[1]+150)),
Actor("submarine", topleft=(key_position[0], key_position[1]+230)),
Actor("cruiser", topleft=(key_position[0], key_position[1]+310)),
Actor("destroyer", topleft=(key_position[0], key_position[1]+390))
]
ship_list_text = [
("Carrier (5)", (key_position[0], key_position[1]+40)),
("Battleship (4)", (key_position[0], key_position[1]+120)),
("Submarine (3)", (key_position[0], key_position[1]+200)),
("Cruiser (3)", (key_position[0], key_position[1]+280)),
("Destroyer (2)", (key_position[0], key_position[1]+360))
]
def setup ():
global player_ships, placing_ship, placing_ship_direction
# configure is used to read config
# after this will know screen size
configure()
# Add Ai ships - start with largest
# position ship takes ship size and returns direction, position
hide_ship = True
this_ship = player2.position_ship(5)
enemy_fleet.add_ship("carrier",this_ship[1],this_ship[0], img_txt, GRID_SIZE, hide_ship)
this_ship = player2.position_ship(4)
enemy_fleet.add_ship("battleship",this_ship[1],this_ship[0], img_txt, GRID_SIZE, hide_ship)
this_ship = player2.position_ship(3)
enemy_fleet.add_ship("submarine",this_ship[1],this_ship[0], img_txt, GRID_SIZE, hide_ship)
this_ship = player2.position_ship(3)
enemy_fleet.add_ship("cruiser",this_ship[1],this_ship[0], img_txt, GRID_SIZE, hide_ship)
this_ship = player2.position_ship(2)
enemy_fleet.add_ship("destroyer",this_ship[1],this_ship[0], img_txt, GRID_SIZE, hide_ship)
player_ships = {
"carrier" : 5,
"battleship" : 4,
"cruiser" : 3,
"submarine" : 3,
"destroyer": 2 }
placing_ship = "carrier"
placing_ship_direction = "horizontal"
def configure():
global WIDTH, HEIGHT, GRID_SIZE, img_txt, grid_img_1, grid_img_2, your_fleet_txt_pos, enemy_fleet_txt_pos
if (SIZE_SML == False):
return
WIDTH=800
HEIGHT=480
GRID_SIZE=(25,25)
img_txt = "_sml"
grid_img_1.image = "grid"+img_txt
grid_img_2.image = "grid"+img_txt
grid_img_1.topleft = (60, 140)
grid_img_2.topleft = (420, 140)
your_fleet_txt_pos = (80, 90)
enemy_fleet_txt_pos = (440, 90)
own_fleet.change_grid((91, 163), GRID_SIZE, img_txt)
enemy_fleet.change_grid((451, 163), GRID_SIZE, img_txt)
def draw():
global fullscreen_status
if (FULLSCREEN == True and fullscreen_status == False):
screen.surface = pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
fullscreen_status = True
screen.fill((192,192,192))
grid_img_1.draw()
grid_img_2.draw()
screen.draw.text("Battleships", fontsize=60, center=(WIDTH/2,50), shadow=(1,1), color=(255,255,255), scolor=(32,32,32))
screen.draw.text("Your fleet", fontsize=40, topleft=your_fleet_txt_pos, color=(255,255,255))
screen.draw.text("The enemy fleet", fontsize=40, topleft=enemy_fleet_txt_pos, color=(255,255,255))
own_fleet.draw()
enemy_fleet.draw()
if (player == "gameover1"):
screen.draw.text("Game Over\nYou won", fontsize=60, center=(WIDTH/2,HEIGHT/2), shadow=(1,1), color=(255,255,255), scolor=(32,32,32))
elif (player == "gameover2"):
screen.draw.text("Game Over\nYou lost!", fontsize=60, center=(WIDTH/2,HEIGHT/2), shadow=(1,1), color=(255,255,255), scolor=(32,32,32))
elif (player == "player1setup"):
screen.draw.text("Place your ships", fontsize=40, center=(WIDTH/2,650), color=(255,255,255))
if (own_fleet.grid.check_in_grid(mouse_position)):
if (placing_ship_direction == "horizontal"):
preview_width = (GRID_SIZE[0] * player_ships[placing_ship]) - 4
preview_height = GRID_SIZE[1] - 4
else:
preview_width = GRID_SIZE[0] - 4
preview_height = (GRID_SIZE[1] * player_ships[placing_ship]) - 4
# Show approx position of ship (slightly offset to top left)
screen.draw.rect(Rect((mouse_position[0]-2, mouse_position[1]-2),(preview_width,preview_height)), (128,128,128))
else:
screen.draw.text("Aim and fire", fontsize=40, center=(WIDTH/2,650), color=(255,255,255))
# Only show key if screen is wider than 1200
if (WIDTH >= 1200):
for ship_key_img in ship_list:
ship_key_img.draw()
screen.draw.text("Ships", topleft=key_position, fontsize=38)
for ship_text in ship_list_text:
screen.draw.text(ship_text[0], topleft=ship_text[1])
def update():
global player
if keyboard.q:
exit()
if (player == "player1setup"):
pass
if (player == "player2"):
grid_pos = player2.fire_shot()
# Ai uses list position - but grid uses grid
result = own_fleet.fire(grid_pos)
player2.fire_result (grid_pos, result)
# If ship sunk then inform Ai player
if (result == True):
if (own_fleet.is_ship_sunk_grid_pos(grid_pos)):
player2.ship_sunk(grid_pos)
# As a ship is sunk - check to see if all ships are sunk
if own_fleet.all_sunk():
player = "gameover2"
return
# If reach here then not gameover, so switch back to main player
player = "player1"
# Track position of mouse, needed during ship placement
def on_mouse_move(pos):
global mouse_position
mouse_position = (pos)
def on_mouse_down(pos, button):
global player, player_ships, placing_ship, placing_ship_direction
if (player == "player1setup"):
if (button == mouse.RIGHT):
if (placing_ship_direction == "horizontal"):
placing_ship_direction = "vertical"
else:
placing_ship_direction = "horizontal"
elif (button == mouse.LEFT):
# Click to place_ship
# Check if no grid
if (own_fleet.grid.check_in_grid(mouse_position)):
# convert to grid position
grid_pos = own_fleet.grid.get_grid_pos(mouse_position)
# check it fits and reserve space
if (player1.check_ship_fit(player_ships[placing_ship], placing_ship_direction, grid_pos)):
player1.place_ship(player_ships[placing_ship], placing_ship_direction, grid_pos)
# Create the ship object
own_fleet.add_ship(placing_ship,grid_pos,placing_ship_direction, img_txt, GRID_SIZE)
# Remove from list of ships to add_ship
player_ships.pop(placing_ship)
# If more ships to place_ship
if (len (player_ships) > 0):
# Get next ship to add
placing_ship = next(iter(player_ships))
placing_ship_direction = "horizontal"
else:
# When completed adding ships switch to play
player="player1"
if (button != mouse.LEFT):
return
if (player == "player1"):
if (enemy_fleet.grid.check_in_grid(pos)):
grid_location = enemy_fleet.grid.get_grid_pos(pos)
enemy_fleet.fire(grid_location)
if enemy_fleet.all_sunk():
player = "gameover1"
else:
# switch to player 2
player = "player2"
elif (player == "gameover1" or player == "gameover2"):
own_fleet.reset()
enemy_fleet.reset()
player1.reset()
player2.reset()
setup()
player = "player1setup"
setup()
pgzrun.go()