mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-12 07:40:50 -08:00
print_with_tab / print_with_whitespace is trivial with Python string formatting and was mostly used in only 2 lines.
523 lines
14 KiB
Python
523 lines
14 KiB
Python
import random
|
|
import re
|
|
|
|
###################
|
|
#
|
|
# static variables
|
|
#
|
|
###################
|
|
|
|
BOARD_WIDTH = 10
|
|
BOARD_HEIGHT = 10
|
|
|
|
# game ships
|
|
#
|
|
# data structure keeping track of information
|
|
# about the ships in the game. for each ship,
|
|
# the following information is provided:
|
|
#
|
|
# name - string representation of the ship
|
|
# length - number of "parts" on the ship that
|
|
# can be shot
|
|
# shots - number of shots the ship counts for
|
|
SHIPS = [
|
|
("BATTLESHIP", 5, 3),
|
|
("CRUISER", 3, 2),
|
|
("DESTROYER<A>", 2, 1),
|
|
("DESTROYER<B>", 2, 1),
|
|
]
|
|
|
|
VALID_MOVES = [
|
|
[-1, 0], # North
|
|
[-1, 1], # North East
|
|
[0, 1], # East
|
|
[1, 1], # South East
|
|
[1, 0], # South
|
|
[1, -1], # South West
|
|
[0, -1], # West
|
|
[-1, -1],
|
|
] # North West
|
|
|
|
COORD_REGEX = "[ \t]{0,}(-?[0-9]{1,3})[ \t]{0,},[ \t]{0,}(-?[0-9]{1,2})"
|
|
|
|
####################
|
|
#
|
|
# global variables
|
|
#
|
|
####################
|
|
|
|
# array of BOARD_HEIGHT arrays, BOARD_WIDTH in length,
|
|
# representing the human player and computer
|
|
player_board = []
|
|
computer_board = []
|
|
|
|
# array representing the coordinates
|
|
# for each ship for player and computer
|
|
# array is in the same order as SHIPS
|
|
computer_ship_coords = []
|
|
|
|
|
|
####################################
|
|
#
|
|
# SHOTS
|
|
#
|
|
# The number of shots computer/player
|
|
# has is determined by the shot "worth"
|
|
# of each ship the computer/player
|
|
# possesses. As long as the ship has one
|
|
# part not hit (i.e., ship was not
|
|
# sunk), the player gets all the shots
|
|
# from that ship.
|
|
|
|
# flag indicating if computer's shots are
|
|
# printed out during computer's turn
|
|
print_computer_shots = False
|
|
|
|
# keep track of the number
|
|
# of available computer shots
|
|
# inital shots are 7
|
|
num_computer_shots = 7
|
|
|
|
# keep track of the number
|
|
# of available player shots
|
|
# initial shots are 7
|
|
num_player_shots = 7
|
|
|
|
#
|
|
# SHOTS
|
|
#
|
|
####################################
|
|
|
|
# flag indicating whose turn
|
|
# it currently is
|
|
COMPUTER = 0
|
|
PLAYER = 1
|
|
active_turn = COMPUTER
|
|
|
|
####################
|
|
#
|
|
# game functions
|
|
#
|
|
####################
|
|
|
|
# random number functions
|
|
#
|
|
# seed the random number generator
|
|
random.seed()
|
|
|
|
|
|
# random_x_y
|
|
#
|
|
# generate a valid x,y coordinate on the board
|
|
# returns: x,y
|
|
# x: integer between 1 and BOARD_HEIGHT
|
|
# y: integer between 1 and BOARD WIDTH
|
|
def random_x_y():
|
|
x = random.randrange(1, BOARD_WIDTH + 1)
|
|
y = random.randrange(1, BOARD_HEIGHT + 1)
|
|
return (x, y)
|
|
|
|
|
|
# input_coord
|
|
#
|
|
# ask user for single (x,y) coordinate
|
|
# validate the coordinates are within the bounds
|
|
# of the board width and height. mimic the behavior
|
|
# of the original program which exited with error
|
|
# messages if coordinates where outside of array bounds.
|
|
# if input is not numeric, print error out to user and
|
|
# let them try again.
|
|
def input_coord():
|
|
match = None
|
|
while not match:
|
|
coords = input("? ")
|
|
match = re.match(COORD_REGEX, coords)
|
|
if not match:
|
|
print("!NUMBER EXPECTED - RETRY INPUT LINE")
|
|
x = int(match.group(1))
|
|
y = int(match.group(2))
|
|
|
|
if x > BOARD_HEIGHT or y > BOARD_WIDTH:
|
|
print("!OUT OF ARRAY BOUNDS IN LINE 1540")
|
|
exit()
|
|
|
|
if x <= 0 or y <= 0:
|
|
print("!NEGATIVE ARRAY DIM IN LINE 1540")
|
|
exit()
|
|
|
|
return x, y
|
|
|
|
|
|
# generate_ship_coordinates
|
|
#
|
|
# given a ship from the SHIPS array, generate
|
|
# the coordinates of the ship. the starting point
|
|
# of the ship's first coordinate is generated randomly.
|
|
# once the starting coordinates are determined, the
|
|
# possible directions of the ship, accounting for the
|
|
# edges of the board, are determined. once possible
|
|
# directions are found, a direction is randomly
|
|
# determined and the remaining coordinates are
|
|
# generated by adding or substraction from the starting
|
|
# coordinates as determined by direction.
|
|
#
|
|
# arguments:
|
|
# ship - index into the SHIPS array
|
|
#
|
|
# returns:
|
|
# array of sets of coordinates (x,y)
|
|
def generate_ship_coordinates(ship):
|
|
# randomly generate starting x,y coordinates
|
|
start_x, start_y = random_x_y()
|
|
|
|
# using starting coordinates and the ship type,
|
|
# generate a vector of possible directions the ship
|
|
# could be placed. directions are numbered 0-7 along
|
|
# points of the compass (N, NE, E, SE, S, SW, W, NW)
|
|
# clockwise. a vector of valid directions where the
|
|
# ship does not go off the board is determined
|
|
ship_len = SHIPS[ship][1] - 1
|
|
dirs = [False for x in range(8)]
|
|
dirs[0] = (start_x - ship_len) >= 1
|
|
dirs[2] = (start_y + ship_len) <= BOARD_WIDTH
|
|
dirs[1] = dirs[0] and dirs[2]
|
|
dirs[4] = (start_x + ship_len) <= BOARD_HEIGHT
|
|
dirs[3] = dirs[2] and dirs[4]
|
|
dirs[6] = (start_y - ship_len) >= 1
|
|
dirs[5] = dirs[4] and dirs[6]
|
|
dirs[7] = dirs[6] and dirs[0]
|
|
directions = [p for p in range(len(dirs)) if dirs[p]]
|
|
|
|
# using the vector of valid directions, pick a
|
|
# random direction to place the ship
|
|
dir_idx = random.randrange(len(directions))
|
|
direction = directions[dir_idx]
|
|
|
|
# using the starting x,y, direction and ship
|
|
# type, return the coordinates of each point
|
|
# of the ship. VALID_MOVES is a staic array
|
|
# of coordinate offsets to walk from starting
|
|
# coordinate to the end coordinate in the
|
|
# chosen direction
|
|
ship_len = SHIPS[ship][1] - 1
|
|
d_x = VALID_MOVES[direction][0]
|
|
d_y = VALID_MOVES[direction][1]
|
|
|
|
coords = [(start_x, start_y)]
|
|
x_coord = start_x
|
|
y_coord = start_y
|
|
for _ in range(ship_len):
|
|
x_coord = x_coord + d_x
|
|
y_coord = y_coord + d_y
|
|
coords.append((x_coord, y_coord))
|
|
return coords
|
|
|
|
|
|
# create_blank_board
|
|
#
|
|
# helper function to create a game board
|
|
# that is blank
|
|
def create_blank_board():
|
|
return [[None for y in range(BOARD_WIDTH)] for x in range(BOARD_HEIGHT)]
|
|
|
|
|
|
# print_board
|
|
#
|
|
# print out the game board for testing
|
|
# purposes
|
|
def print_board(board) -> None:
|
|
|
|
# print board header (column numbers)
|
|
print(" ", end="")
|
|
for z in range(BOARD_WIDTH):
|
|
print(f"{z+1:3}", end="")
|
|
print()
|
|
|
|
for x in range(len(board)):
|
|
print(f"{x+1:2}", end="")
|
|
for y in range(len(board[x])):
|
|
if board[x][y] is None:
|
|
print(f"{' ':3}", end="")
|
|
else:
|
|
print(f"{board[x][y]:3}", end="")
|
|
print()
|
|
|
|
|
|
# place_ship
|
|
#
|
|
# place a ship on a given board. updates
|
|
# the board's row,column value at the given
|
|
# coordinates to indicate where a ship is
|
|
# on the board.
|
|
#
|
|
# inputs: board - array of BOARD_HEIGHT by BOARD_WIDTH
|
|
# coords - array of sets of (x,y) coordinates of each
|
|
# part of the given ship
|
|
# ship - integer repreesnting the type of ship (given in SHIPS)
|
|
def place_ship(board, coords, ship):
|
|
for coord in coords:
|
|
board[coord[0] - 1][coord[1] - 1] = ship
|
|
|
|
|
|
# NOTE: A little quirk that exists here and in the orginal
|
|
# game: Ships are allowed to cross each other!
|
|
# For example: 2 destroyers, length 2, one at
|
|
# [(1,1),(2,2)] and other at [(2,1),(1,2)]
|
|
def generate_board():
|
|
board = create_blank_board()
|
|
|
|
ship_coords = []
|
|
for ship in range(len(SHIPS)):
|
|
placed = False
|
|
coords = []
|
|
while not placed:
|
|
coords = generate_ship_coordinates(ship)
|
|
clear = True
|
|
for coord in coords:
|
|
if board[coord[0] - 1][coord[1] - 1] is not None:
|
|
clear = False
|
|
break
|
|
if clear:
|
|
placed = True
|
|
place_ship(board, coords, ship)
|
|
ship_coords.append(coords)
|
|
return board, ship_coords
|
|
|
|
|
|
def execute_shot(turn, board, x, y, current_turn):
|
|
"""
|
|
given a board and x, y coordinates,
|
|
execute a shot. returns True if the shot
|
|
is valid, False if not
|
|
"""
|
|
square = board[x - 1][y - 1]
|
|
ship_hit = -1
|
|
if square is not None and square >= 0 and square < len(SHIPS):
|
|
ship_hit = square
|
|
board[x - 1][y - 1] = 10 + current_turn
|
|
return ship_hit
|
|
|
|
|
|
# calculate_shots
|
|
#
|
|
# function to examine each board
|
|
# and determine how many shots remaining
|
|
def calculate_shots(board):
|
|
|
|
ships_found = [0 for x in range(len(SHIPS))]
|
|
for x in range(BOARD_HEIGHT):
|
|
for y in range(BOARD_WIDTH):
|
|
square = board[x - 1][y - 1]
|
|
if square is not None and square >= 0 and square < len(SHIPS):
|
|
ships_found[square] = 1
|
|
shots = 0
|
|
for ship in range(len(ships_found)):
|
|
if ships_found[ship] == 1:
|
|
shots += SHIPS[ship][2]
|
|
|
|
return shots
|
|
|
|
|
|
# initialize
|
|
#
|
|
# function to initialize global variables used
|
|
# during game play.
|
|
def initialize_game():
|
|
|
|
# initialize the global player and computer
|
|
# boards
|
|
global player_board
|
|
player_board = create_blank_board()
|
|
|
|
# generate the ships for the computer's
|
|
# board
|
|
global computer_board
|
|
global computer_ship_coords
|
|
computer_board, computer_ship_coords = generate_board()
|
|
|
|
# print out the title 'screen'
|
|
print("{:>38}".format("SALVO"))
|
|
print("{:>57s}".format("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"))
|
|
print()
|
|
print("{:>52s}".format("ORIGINAL BY LAWRENCE SIEGEL, 1973"))
|
|
print("{:>56s}".format("PYTHON 3 PORT BY TODD KAISER, MARCH 2021"))
|
|
print("\n")
|
|
|
|
# ask the player for ship coordinates
|
|
print("ENTER COORDINATES FOR...")
|
|
ship_coords = []
|
|
for ship in SHIPS:
|
|
print(ship[0])
|
|
list = []
|
|
for _ in range(ship[1]):
|
|
x, y = input_coord()
|
|
list.append((x, y))
|
|
ship_coords.append(list)
|
|
|
|
# add ships to the user's board
|
|
for ship in range(len(SHIPS)):
|
|
place_ship(player_board, ship_coords[ship], ship)
|
|
|
|
# see if the player wants the computer's ship
|
|
# locations printed out and if the player wants to
|
|
# start
|
|
input_loop = True
|
|
player_start = "YES"
|
|
while input_loop:
|
|
player_start = input("DO YOU WANT TO START? ")
|
|
if player_start == "WHERE ARE YOUR SHIPS?":
|
|
for ship in range(len(SHIPS)):
|
|
print(SHIPS[ship][0])
|
|
coords = computer_ship_coords[ship]
|
|
for coord in coords:
|
|
x = coord[0]
|
|
y = coord[1]
|
|
print(f"{x:2}", f"{y:2}")
|
|
else:
|
|
input_loop = False
|
|
|
|
# ask the player if they want the computer's shots
|
|
# printed out each turn
|
|
global print_computer_shots
|
|
see_computer_shots = input("DO YOU WANT TO SEE MY SHOTS? ")
|
|
if see_computer_shots.lower() == "yes":
|
|
print_computer_shots = True
|
|
|
|
global first_turn
|
|
global second_turn
|
|
if player_start.lower() != "yes":
|
|
first_turn = COMPUTER
|
|
second_turn = PLAYER
|
|
|
|
# calculate the initial number of shots for each
|
|
global num_computer_shots
|
|
global num_player_shots
|
|
num_player_shots = calculate_shots(player_board)
|
|
num_computer_shots = calculate_shots(computer_board)
|
|
|
|
|
|
####################################
|
|
#
|
|
# Turn Control
|
|
#
|
|
# define functions for executing the turns for
|
|
# the player and the computer. By defining this as
|
|
# functions, we can easily start the game with
|
|
# either computer or player and alternate back and
|
|
# forth, replicating the gotos in the original game
|
|
|
|
|
|
# initialize the first_turn function to the
|
|
# player's turn
|
|
first_turn = PLAYER
|
|
|
|
|
|
# initialize the second_turn to the computer's
|
|
# turn
|
|
second_turn = COMPUTER
|
|
|
|
|
|
def execute_turn(turn, current_turn):
|
|
|
|
global num_computer_shots
|
|
global num_player_shots
|
|
|
|
# print out the number of shots the current
|
|
# player has
|
|
board = None
|
|
num_shots = 0
|
|
if turn == COMPUTER:
|
|
print("I HAVE", num_computer_shots, "SHOTS.")
|
|
board = player_board
|
|
num_shots = num_computer_shots
|
|
else:
|
|
print("YOU HAVE", num_player_shots, "SHOTS.")
|
|
board = computer_board
|
|
num_shots = num_player_shots
|
|
|
|
shots = []
|
|
for _shot in range(num_shots):
|
|
valid_shot = False
|
|
x = -1
|
|
y = -1
|
|
|
|
# loop until we have a valid shot. for the
|
|
# computer, we randomly pick a shot. for the
|
|
# player we request shots
|
|
while not valid_shot:
|
|
if turn == COMPUTER:
|
|
x, y = random_x_y()
|
|
else:
|
|
x, y = input_coord()
|
|
square = board[x - 1][y - 1]
|
|
if square is not None and square > 10:
|
|
if turn == PLAYER:
|
|
print("YOU SHOT THERE BEFORE ON TURN", square - 10)
|
|
continue
|
|
shots.append((x, y))
|
|
valid_shot = True
|
|
|
|
hits = []
|
|
for shot in shots:
|
|
hit = execute_shot(turn, board, shot[0], shot[1], current_turn)
|
|
if hit >= 0:
|
|
hits.append(hit)
|
|
if turn == COMPUTER and print_computer_shots:
|
|
print(shot[0], shot[1])
|
|
|
|
for hit in hits:
|
|
if turn == COMPUTER:
|
|
print("I HIT YOUR", SHIPS[hit][0])
|
|
else:
|
|
print("YOU HIT MY", SHIPS[hit][0])
|
|
|
|
if turn == COMPUTER:
|
|
num_player_shots = calculate_shots(board)
|
|
return num_player_shots
|
|
else:
|
|
num_computer_shots = calculate_shots(board)
|
|
return num_computer_shots
|
|
|
|
|
|
#
|
|
# Turn Control
|
|
#
|
|
######################################
|
|
|
|
|
|
def main() -> None:
|
|
# keep track of the turn
|
|
current_turn = 0
|
|
|
|
# initialize the player and computer
|
|
# boards
|
|
initialize_game()
|
|
|
|
# execute turns until someone wins or we run
|
|
# out of squares to shoot
|
|
|
|
game_over = False
|
|
while not game_over:
|
|
|
|
# increment the turn
|
|
current_turn = current_turn + 1
|
|
|
|
print("\n")
|
|
print("TURN", current_turn)
|
|
|
|
# print("computer")
|
|
# print_board(computer_board)
|
|
# print("player")
|
|
# print_board(player_board)
|
|
|
|
if execute_turn(first_turn, current_turn) == 0:
|
|
game_over = True
|
|
continue
|
|
if execute_turn(second_turn, current_turn) == 0:
|
|
game_over = True
|
|
continue
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|