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", 2, 1), ("DESTROYER", 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 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 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 = 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: 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 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 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: 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: 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()