Files
basic-computer-games/77_Salvo/python/salvo.py
2024-08-19 03:46:27 +03:00

471 lines
13 KiB
Python

import random
import re
from typing import List, Optional, Tuple
BoardType = List[List[Optional[int]]]
CoordinateType = Tuple[int, int]
BOARD_WIDTH = 10
BOARD_HEIGHT = 10
# 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})"
# array of BOARD_HEIGHT arrays, BOARD_WIDTH in length,
# representing the human player and computer
player_board: BoardType = []
computer_board: BoardType = []
# array representing the coordinates
# for each ship for player and computer
# array is in the same order as SHIPS
computer_ship_coords: List[List[CoordinateType]] = []
####################################
#
# 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 = False
PLAYER = True
active_turn = COMPUTER
####################
#
# game functions
#
####################
# random number functions
#
# seed the random number generator
random.seed()
# random_x_y
#
def random_x_y() -> CoordinateType:
"""Generate a valid x,y coordinate on the board"""
x = random.randrange(1, BOARD_WIDTH + 1)
y = random.randrange(1, BOARD_HEIGHT + 1)
return (x, y)
def input_coord() -> CoordinateType:
"""
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.
"""
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
def generate_ship_coordinates(ship: int) -> List[CoordinateType]:
"""
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)
"""
# 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 _ 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
def create_blank_board() -> BoardType:
"""Create a blank game board"""
return [[None for _y in range(BOARD_WIDTH)] for _x in range(BOARD_HEIGHT)]
def print_board(board: BoardType) -> None:
"""Print out the game board for testing purposes"""
# 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()
def place_ship(board: BoardType, coords: List[CoordinateType], ship: int) -> None:
"""
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 representing the type of ship (given in SHIPS)
"""
for coord in coords:
board[coord[0] - 1][coord[1] - 1] = ship
def generate_board() -> Tuple[BoardType, List[List[CoordinateType]]]:
"""
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)]
"""
board = create_blank_board()
ship_coords = []
for ship in range(len(SHIPS)):
placed = False
coords = []
while not placed:
coords = generate_ship_coordinates(ship)
clear = all(board[coord[0] - 1][coord[1] - 1] is None for coord in coords)
if clear:
placed = True
place_ship(board, coords, ship)
ship_coords.append(coords)
return board, ship_coords
def execute_shot(
turn: bool, board: BoardType, x: int, y: int, current_turn: int
) -> int:
"""
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
def calculate_shots(board: BoardType) -> int:
"""Examine each board and determine how many shots remaining"""
ships_found = [0 for _ 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
return sum(
SHIPS[ship][2]
for ship in range(len(ships_found))
if ships_found[ship] == 1
)
def initialize_game() -> None:
# 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_index in range(len(SHIPS)):
place_ship(player_board, ship_coords[ship_index], ship_index)
# 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_index in range(len(SHIPS)):
print(SHIPS[ship_index][0])
coords = computer_ship_coords[ship_index]
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
if player_start.lower() != "yes":
first_turn = COMPUTER
# calculate the initial number of shots for each
global num_computer_shots, 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
def execute_turn(turn: bool, current_turn: int) -> int:
global num_computer_shots, num_player_shots
# print out the number of shots the current player has
board = None
num_shots = 0
if turn == COMPUTER:
print(f"I HAVE {num_computer_shots} SHOTS.")
board = player_board
num_shots = num_computer_shots
else:
print(f"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:
x, y = random_x_y() if turn == COMPUTER else 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:
current_turn = 0
initialize_game()
# execute turns until someone wins or we run
# out of squares to shoot
game_over = False
while not game_over:
current_turn += 1
print("\n")
print("TURN", current_turn)
if (
execute_turn(first_turn, current_turn) == 0
or execute_turn(not first_turn, current_turn) == 0
):
game_over = True
continue
if __name__ == "__main__":
main()