# # CPSC 217 W26 Assignment 3: A Dr. Mario clone # from SimpleGraphics import * from random import randrange, sample from time import time, sleep from pprint import pprint from inspect import signature from copy import deepcopy # Size of the board. BOARD_WIDTH = 8 BOARD_HEIGHT = 16 CELL_SIZE = 32 # The representation of an empty cell in the board. EMPTY = "0" # How many bottom rows of the board can contain randomly positioned viruses? VIRUS_ROWS = 10 # The user interface will attempt to maintain this framerate when drawing. FRAME_RATE = 30 # Colors used to draw the capsules. COLORS = {1: (128, 192, 96), 2: (192, 0, 0), 3: (64, 128, 255)} # Green virus virus_1_data = [(28, 28), \ ("#000000",0xe), ("#9bc35a",0x3), ("#000000",0x18), ("#9bc35a",0x5), \ ("#000000",0xe), ("#6cae5a",0x3), ("#000000",0x6), ("#9bc35a",0x5), \ ("#000000",0xd), ("#6cae5a",0x5), ("#000000",0x5), ("#9bc35a",0x5), \ ("#000000",0xd), ("#6cae5a",0x5), ("#000000",0x6), ("#9bc35a",0x3), \ ("#000000",0xe), ("#6cae5a",0x5), ("#000000",0x6), ("#9bc35a",0x2), \ ("#000000",0x7), ("#6cae5a",0x3), ("#000000",0x6), ("#6cae5a",0x5), \ ("#000000",0x4), ("#9bc35a",0x3), ("#000000",0x6), ("#6cae5a",0x5), \ ("#000000",0x8), ("#6cae5a",0x3), ("#000000",0x1), ("#6cae5a",0x2), \ ("#9bc35a",0x2), ("#6cae5a",0x2), ("#000000",0x4), ("#6cae5a",0x6), \ ("#000000",0x9), ("#6cae5a",0xa), ("#000000",0x1), ("#6cae5a",0x8), \ ("#000000",0xa), ("#6cae5a",0xd), ("#000000",0x1), ("#6cae5a",0x3), \ ("#000000",0xa), ("#6cae5a",0x3), ("#cde1ac",0x2), ("#6cae5a",0x8), \ ("#000000",0x8), ("#487c39",0x3), ("#000000",0x3), ("#6cae5a",0x3), \ ("#cde1ac",0x4), ("#6cae5a",0x7), ("#000000",0x7), ("#487c39",0x5), \ ("#000000",0x2), ("#487c39",0x1), ("#6cae5a",0x2), ("#cde1ac",0x4), \ ("#6cae5a",0x7), ("#000000",0x7), ("#487c39",0x8), ("#6cae5a",0x3), \ ("#cde1ac",0x2), ("#6cae5a",0x8), ("#000000",0x7), ("#487c39",0x7), \ ("#6cae5a",0xd), ("#487c39",0x1), ("#000000",0x3), ("#487c39",0x3), \ ("#000000",0x2), ("#487c39",0x3), ("#000000",0x3), ("#6cae5a",0x9), \ ("#f3f8eb",0x2), ("#6cae5a",0x2), ("#487c39",0x8), ("#000000",0x7), \ ("#6cae5a",0x9), ("#f3f8eb",0x2), ("#6cae5a",0x3), ("#487c39",0x7), \ ("#000000",0x6), ("#6cae5a",0x6), ("#f3f8eb",0x1), ("#6cae5a",0x7), \ ("#000000",0x3), ("#487c39",0x5), ("#000000",0x5), ("#6cae5a",0xe), \ ("#000000",0x5), ("#487c39",0x3), ("#000000",0x3), ("#6cae5a",0x6), \ ("#000000",0x2), ("#6cae5a",0x6), ("#487c39",0x2), ("#000000",0xb), \ ("#6cae5a",0x6), ("#000000",0x4), ("#6cae5a",0x1), ("#9bc35a",0x2), \ ("#6cae5a",0x2), ("#487c39",0x3), ("#000000",0xa), ("#6cae5a",0x5), \ ("#000000",0x5), ("#9bc35a",0x2), ("#000000",0x4), ("#487c39",0x3), \ ("#000000",0x9), ("#6cae5a",0x5), ("#000000",0x5), ("#9bc35a",0x2), \ ("#000000",0x5), ("#487c39",0x4), ("#000000",0x8), ("#6cae5a",0x3), \ ("#000000",0x5), ("#9bc35a",0x3), ("#000000",0x5), ("#487c39",0x5), \ ("#000000",0xe), ("#9bc35a",0x5), ("#000000",0x4), ("#487c39",0x5), \ ("#000000",0xe), ("#9bc35a",0x5), ("#000000",0x4), ("#487c39",0x5), \ ("#000000",0xe), ("#9bc35a",0x5), ("#000000",0x5), ("#487c39",0x3), \ ("#000000",0x10), ("#9bc35a",0x3), ("#000000",0xf)] # Red virus virus_2_data = [(28, 28), \ ("#000000",0xf), ("#dd0000",0x3), ("#000000",0x10), ("#ff6e6e",0x3), \ ("#000000",0x5), ("#dd0000",0x5), ("#000000",0xe), ("#ff6e6e",0x5), \ ("#000000",0x4), ("#dd0000",0x5), ("#000000",0xe), ("#ff6e6e",0x5), \ ("#000000",0x4), ("#dd0000",0x5), ("#000000",0xe), ("#ff6e6e",0x5), \ ("#000000",0x5), ("#dd0000",0x3), ("#000000",0x10), ("#ff6e6e",0x3), \ ("#000000",0x6), ("#dd0000",0x2), ("#000000",0x6), ("#dd0000",0x3), \ ("#000000",0x9), ("#ff6e6e",0x3), ("#000000",0x5), ("#dd0000",0x2), \ ("#000000",0x5), ("#dd0000",0x5), ("#000000",0x9), ("#ff6e6e",0x6), \ ("#dd0000",0x2), ("#ff6e6e",0x1), ("#000000",0x5), ("#dd0000",0x5), \ ("#000000",0x9), ("#ff6e6e",0xa), ("#000000",0x3), ("#dd0000",0x6), \ ("#000000",0xa), ("#ff6e6e",0xa), ("#000000",0x1), ("#dd0000",0x6), \ ("#000000",0x3), ("#ff9999",0x3), ("#000000",0x4), ("#ff6e6e",0x7), \ ("#fff4f4",0x2), ("#ff6e6e",0x2), ("#dd0000",0x3), ("#000000",0x6), \ ("#ff9999",0x5), ("#000000",0x2), ("#ff6e6e",0x8), ("#fff4f4",0x2), \ ("#ff6e6e",0x2), ("#dd0000",0x2), ("#000000",0x7), ("#ff9999",0x7), \ ("#ff6e6e",0xe), ("#000000",0x7), ("#ff9999",0x8), ("#ff6e6e",0xd), \ ("#000000",0x8), ("#ff9999",0x3), ("#000000",0x2), ("#ff9999",0x2), \ ("#ff6e6e",0x3), ("#ffcece",0x2), ("#ff6e6e",0x7), ("#ff9999",0x1), \ ("#000000",0x3), ("#ff9999",0x3), ("#000000",0x8), ("#ff6e6e",0x3), \ ("#ffcece",0x4), ("#ff6e6e",0x3), ("#fff4f4",0x1), ("#ff6e6e",0x2), \ ("#ff9999",0x8), ("#000000",0x7), ("#ff6e6e",0x3), ("#ffcece",0x4), \ ("#ff6e6e",0x7), ("#ff9999",0x7), ("#000000",0x8), ("#ff6e6e",0x3), \ ("#ffcece",0x2), ("#ff6e6e",0x7), ("#000000",0x3), ("#ff9999",0x5), \ ("#000000",0x7), ("#ff6e6e",0xc), ("#000000",0x5), ("#ff9999",0x3), \ ("#000000",0x7), ("#ff6e6e",0x3), ("#000000",0x1), ("#ff6e6e",0x9), \ ("#000000",0xc), ("#ff6e6e",0x5), ("#000000",0x3), ("#ff6e6e",0x1), \ ("#dd0000",0x2), ("#ff6e6e",0x6), ("#000000",0xa), ("#ff6e6e",0x5), \ ("#000000",0x6), ("#dd0000",0x2), ("#000000",0x2), ("#ff6e6e",0x4), \ ("#000000",0x9), ("#ff6e6e",0x5), ("#000000",0x6), ("#dd0000",0x2), \ ("#000000",0x3), ("#ff6e6e",0x5), ("#000000",0x7), ("#ff6e6e",0x5), \ ("#000000",0x5), ("#dd0000",0x3), ("#000000",0x4), ("#ff6e6e",0x5), \ ("#000000",0x7), ("#ff6e6e",0x3), ("#000000",0x5), ("#dd0000",0x5), \ ("#000000",0x3), ("#ff6e6e",0x5), ("#000000",0xf), ("#dd0000",0x5), \ ("#000000",0x3), ("#ff6e6e",0x5), ("#000000",0xf), ("#dd0000",0x5), \ ("#000000",0x4), ("#ff6e6e",0x3), ("#000000",0x11), ("#dd0000",0x3), \ ("#000000",0xd) ] # Blue virus virus_3_data = [(28, 28), \ ("#000000",0xf), ("#81bcff",0x3), ("#000000",0x10), ("#006df0",0x3), \ ("#000000",0x5), ("#81bcff",0x5), ("#000000",0xe), ("#006df0",0x5), \ ("#000000",0x4), ("#81bcff",0x5), ("#000000",0xe), ("#006df0",0x5), \ ("#000000",0x4), ("#81bcff",0x5), ("#000000",0xe), ("#006df0",0x5), \ ("#000000",0x5), ("#81bcff",0x3), ("#000000",0x5), ("#55a3ff",0x3), \ ("#000000",0x8), ("#006df0",0x4), ("#000000",0x5), ("#81bcff",0x2), \ ("#000000",0x5), ("#55a3ff",0x5), ("#000000",0x9), ("#006df0",0x3), \ ("#000000",0x4), ("#81bcff",0x2), ("#000000",0x5), ("#55a3ff",0x5), \ ("#000000",0xa), ("#006df0",0x3), ("#55a3ff",0x2), ("#81bcff",0x2), \ ("#55a3ff",0x1), ("#000000",0x4), ("#55a3ff",0x6), ("#000000",0xb), \ ("#006df0",0x2), ("#55a3ff",0x6), ("#000000",0x2), ("#55a3ff",0x6), \ ("#000000",0x3), ("#006df0",0x3), ("#000000",0x5), ("#55a3ff",0xe), \ ("#000000",0x5), ("#006df0",0x5), ("#000000",0x3), ("#55a3ff",0x7), \ ("#f1f8ff",0x1), ("#55a3ff",0x6), ("#000000",0x6), ("#006df0",0x7), \ ("#55a3ff",0x3), ("#f1f8ff",0x2), ("#55a3ff",0x9), ("#000000",0x7), \ ("#006df0",0x8), ("#55a3ff",0x2), ("#f1f8ff",0x2), ("#55a3ff",0x9), \ ("#000000",0x3), ("#006df0",0x3), ("#000000",0x2), ("#006df0",0x3), \ ("#000000",0x3), ("#006df0",0x1), ("#55a3ff",0xd), ("#006df0",0x7), \ ("#000000",0x7), ("#55a3ff",0x8), ("#c0deff",0x2), ("#55a3ff",0x3), \ ("#006df0",0x8), ("#000000",0x7), ("#55a3ff",0x7), ("#c0deff",0x4), \ ("#55a3ff",0x2), ("#006df0",0x1), ("#000000",0x2), ("#006df0",0x5), \ ("#000000",0x7), ("#55a3ff",0x7), ("#c0deff",0x4), ("#55a3ff",0x3), \ ("#000000",0x3), ("#006df0",0x3), ("#000000",0x8), ("#55a3ff",0x8), \ ("#c0deff",0x2), ("#55a3ff",0x3), ("#000000",0xa), ("#55a3ff",0x3), \ ("#000000",0x1), ("#55a3ff",0xd), ("#000000",0xa), ("#55a3ff",0x8), \ ("#000000",0x1), ("#55a3ff",0xa), ("#000000",0x9), ("#55a3ff",0x6), \ ("#000000",0x4), ("#55a3ff",0x2), ("#81bcff",0x2), ("#55a3ff",0x2), \ ("#000000",0x1), ("#55a3ff",0x3), ("#000000",0x8), ("#55a3ff",0x5), \ ("#000000",0x6), ("#81bcff",0x3), ("#000000",0x4), ("#55a3ff",0x5), \ ("#000000",0x6), ("#55a3ff",0x3), ("#000000",0x7), ("#81bcff",0x2), \ ("#000000",0x6), ("#55a3ff",0x5), ("#000000",0xe), ("#81bcff",0x3), \ ("#000000",0x6), ("#55a3ff",0x5), ("#000000",0xd), ("#81bcff",0x5), \ ("#000000",0x5), ("#55a3ff",0x5), ("#000000",0xd), ("#81bcff",0x5), \ ("#000000",0x6), ("#55a3ff",0x3), ("#000000",0xe), ("#81bcff",0x5), \ ("#000000",0x18), ("#81bcff",0x3), ("#000000",0xe)] ############################################################################### # # Insert your code below this comment. Do not modify the code above this # comment. # ############################################################################### ############################################################################### # # Insert your code above this comment. Do not modify the code below this # comment. # ############################################################################### # # Decode a run-length encoded image, returning an image object that can be # drawn by SimpleGraphics. # # Parameters: # data: The run-length encoded data. The first element in the list is the # width and height of the image. Each subsequent element in the list # is a tuple consisting of a color and the number of pixels of that # color. # # Returns: The image represented by the run-length encoded data. # def decodeImage(data): # Create a blank image of the correct size. w, h = data[0] img = createImage(w, h) # Decode the image data. x = 0 y = 0 for i in range(1, len(data)): # Extract the values from the current tuple. col, count = data[i] r = int(col[1:3], 16) g = int(col[3:5], 16) b = int(col[5:7], 16) # Draw the correct number of copies of the pixel. for j in range(count): putPixel(img, x, y, r, g, b) x = x + 1 if x == w: x = 0 y = y + 1 # Return the decoded image. return img # # Draw the board on the screen. # # Parameters: # board: the board, stored in a 2D list, to draw # v1, v2, v3: The images to draw for each virus # # Returns: None # def drawBoard(board, v1, v2, v3): # Verify that the board has the correct size assert(board is not None) assert(len(board) == 16) assert(len(board[0]) == 8) # Map the integers 1, 2 and 3 to the image for each virus. vs = {1: v1, 2: v2, 3: v3} # Draw the rectangles where the capsule starts at the top of the board. setOutline(64, 64, 64) setFill(0, 0, 0) rect(getWidth() / 2 - CELL_SIZE, getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2), CELL_SIZE, -CELL_SIZE) rect(getWidth() / 2, getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2), CELL_SIZE, -CELL_SIZE) # Draw the rest of the squares for the board, and any viruses or capsules # that occupy those squares. for row in range(len(board)): for col in range(len(board[row])): # Draw the square. setOutline(64, 64, 64) setFill(0, 0, 0) rect(getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE, CELL_SIZE) # Fill the square with the appropriate virus image if it is occupied # by a virus. if board[row][col] == "1" or board[row][col] == "2" or board[row][col] == "3": drawImage(vs[int(board[row][col])], 2 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 2 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE) # Fill the square with a round capsule if it is occupied by such. elif board[row][col] == "1c" or board[row][col] == "2c" or board[row][col] == "3c": setColor(*COLORS[int(board[row][col][0])]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) # Fill the square with a half capsule with a square right edge. elif board[row][col] == "1r" or board[row][col] == "2r" or board[row][col] == "3r": setColor(*COLORS[int(board[row][col][0])]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) rect(CELL_SIZE / 2 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE / 2, CELL_SIZE - 6) # Fill the square with a half capsule with a square left edge. elif board[row][col] == "1l" or board[row][col] == "2l" or board[row][col] == "3l": setColor(*COLORS[int(board[row][col][0])]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) rect(getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE / 2, CELL_SIZE - 6) # Fill the square with a half capsule with a square top edge. elif board[row][col] == "1u" or board[row][col] == "2u" or board[row][col] == "3u": setColor(*COLORS[int(board[row][col][0])]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) rect(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE / 2) # Fill the square with a half capsule with a square bottom edge. elif board[row][col] == "1d" or board[row][col] == "2d" or board[row][col] == "3d": setColor(*COLORS[int(board[row][col][0])]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) rect(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, CELL_SIZE / 2 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE / 2) # Fill the square with a half capsule with an open circle because it has # been marked for deletion. elif board[row][col] == "1x" or board[row][col] == "2x" or board[row][col] == "3x": setFill("Black") setOutline(*COLORS[int(board[row][col][0])]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) # Do nothing for spaces that are empty. (This case is necessary so that # else can be used to catch an unexpected value on the board.) elif board[row][col] == EMPTY: pass # The location on the board was populated with something unexpected. else: print(f"Error: {board[row][col]=}") close() quit() # # "Fix" any broken capsules by replacing an unmatched half with a circular # capsule piece. Only left and right edges are handled by this function. # # Parameters: # board: The board that is fixed. This parameter is modified as the # function executes. # # Returns: None # def fix_lr(board): for row in range(len(board)): # # Replace unmatched right edges with a circular capsule piece. # for col in range(len(board[row])-1): if len(board[row][col]) >= 2 and \ board[row][col][1] == "r": if len(board[row][col+1]) == 1: board[row][col] = board[row][col][0] + "c" if len(board[row][col+1]) == 2 and \ board[row][col+1][1] != "l": board[row][col] = board[row][col][0] + "c" # # Replace unmatched left edges with a circular capsule piece. # for col in range(1, len(board[row])): if len(board[row][col]) >= 2 and \ board[row][col][1] == "l": if len(board[row][col-1]) == 1: board[row][col] = board[row][col][0] + "c" if len(board[row][col-1]) == 2 and \ board[row][col-1][1] != "r": board[row][col] = board[row][col][0] + "c" # # "Fix" any broken capsules by replacing an unmatched half with a circular # capsule piece. Only tob and bottom edges are handled by this function. # # Parameters: # board: The board that is fixed. This parameter is modified as the # function executes. # # Returns: None # def fix_tb(board): for col in range(len(board[0])): # # Replace unmatched down (bottom) edges with a circular capsule piece. # for row in range(len(board) - 1): if len(board[row][col]) >= 2 and \ board[row][col][1] == "d": if len(board[row+1][col]) == 1: board[row][col] = board[row][col][0] + "c" if len(board[row+1][col]) == 2 and \ board[row+1][col][1] != "u": board[row][col] = board[row][col][0] + "c" # # Replace unmatched up (top) edges with a circular capsule piece. # for row in range(1, len(board)): if len(board[row][col]) >= 2 and \ board[row][col][1] == "u": if len(board[row-1][col]) == 1: board[row][col] = board[row][col][0] + "c" if len(board[row-1][col]) == 2 and \ board[row-1][col][1] != "d": board[row][col] = board[row][col][0] + "c" # # Drop any unsupported pieces down one space on the board. # # Parameters: # board: The current game board with viruses, capsules and blank spaces. # This parameter is modified as the function executes. # # Returns: True if any changes were made to the board. False otherwise. # def dropBoardPieces(board): changed = False # Process the board from bottom to top so that pieces at the bottom of the # board move first, making space for pieces above them to move during the # same function call. for row in range(len(board) - 2, -1, -1): for col in range(0, len(board[0])): if len(board[row][col]) == 2: # # If it is a circular or up-down (top-bottom) connected piece, then # it can move down if the space immediately below it is open. # if (board[row][col][1] == "c" or board[row][col][1] == "u" or board[row][col][1] == "d") and board[row+1][col] == EMPTY: board[row+1][col] = board[row][col] board[row][col] = EMPTY changed = True # # If it is a sideways connect block and both of the spaces below it # are empty # if col < len(board[row]) - 1 and \ (len(board[row][col]) >= 2) and (len(board[row][col+1]) >= 2) and \ (board[row][col][1] == "r") and (board[row][col+1][1] == "l") and \ (board[row+1][col] == EMPTY) and (board[row+1][col+1]) == EMPTY: board[row+1][col] = board[row][col] board[row][col] = EMPTY board[row+1][col+1] = board[row][col+1] board[row][col+1] = EMPTY changed = True # Return True if the board changed. False otherwise. return changed # # Draw one capsule on the screen at the indicated location on the board. # # Parameters: # row, col: The location of the capsule on the board # c1, c2: The colors of the two halves of the capsule # rot: The rotation of the capsule # # Returns: None # def drawCapsule(row, col, c1, c2, rot): # Draw the ellipse for the first side of the capsule. setColor(*COLORS[c1]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) # Draw the rectangles that are needed to make the capsule look connected. # Where these rectangles are drawn depends on the rotation of the capsule. if rot == 0: # Horizontal capsule rotated 0 degrees. row_off = 0 col_off = 1 rect(CELL_SIZE / 2 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE / 2, CELL_SIZE - 6) setColor(*COLORS[c2]) rect(CELL_SIZE + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE / 2, CELL_SIZE - 6) elif rot == 90: # Vertical capsule rotated 90 degrees. row_off = -1 col_off = 0 rect(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE / 2) setColor(*COLORS[c2]) rect(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, -CELL_SIZE / 2 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE / 2) elif rot == 180: # Horizontal capsule rotated 180 degrees. row_off = 0 col_off = -1 rect(getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE / 2, CELL_SIZE - 6) setColor(*COLORS[c2]) rect(-CELL_SIZE / 2 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE / 2, CELL_SIZE - 6) elif rot == 270: # Vertical capsule rotated 270 degrees. row_off = 1 col_off = 0 rect(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, CELL_SIZE / 2 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE / 2) setColor(*COLORS[c2]) rect(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, CELL_SIZE + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE / 2) # Draw the anchor dot in the middle of the anchor side of the block. setColor(64, 64, 64) ellipse(12 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + col * CELL_SIZE, 12 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + row * CELL_SIZE, CELL_SIZE - 24, CELL_SIZE - 24) # Draw the circle for the second half of the capsule. setColor(*COLORS[c2]) ellipse(3 + getWidth() / 2 - (CELL_SIZE * BOARD_WIDTH / 2) + (col + col_off) * CELL_SIZE, 3 + getHeight() / 2 - (CELL_SIZE * BOARD_HEIGHT / 2) + (row + row_off) * CELL_SIZE, CELL_SIZE - 6, CELL_SIZE - 6) # # Create a board of the stated size where every location is empty. # # Parameters: # rows: The number of rows in the board # cols: The number of columns in the board # # Returns: A 2D list of the stated size where every element is EMPTY # def createBoard(rows, cols): # Create the empty board. board = [] for j in range(rows): board.append([]) for i in range(cols): board[j].append(EMPTY) # Return the empty board. return board # # Create a board that is populated in an alternating checkerboard pattern of # occupied and open spaces. This function isn't need for the core game # functionality, but the checkerboard pattern is used for testing some # functions. # # Parameters: # rows: The number of rows in the board # cols: The number of columns in the board # # Returns: A 2D list of the stated size where the elements alternate between # being occupied and empty. The occupied elements may be a virus # or half of a capsule. # def createCheckerBoard(rows, cols): # Create a new, empty board. board = createBoard(rows, cols) # # Fill the empty board in with checkboard pattern of viruses and capsule # pieces. # color = 1 vc = 0 for r in range(len(board)): for c in range(r % 2, len(board[r]), 2): board[r][c] = str(color) + ("" if vc == 0 else "c") # Alternate viruses and capsules. vc = (vc + 1) % 2 # Cycle through the colors 1, 2, and 3. color = color + 1 if color % 4 == 0: color = 1 # Return the checkerboard. return board # # Test the populateBoard function, reporting warnings for any tests that # fail. An error is reported (and the game closes) if the function does # not exist, or if it takes the wrong number of parameters. # # Parameters: (None) # Returns: (None) # def testPopulateBoard(): print("Testing populateBoard...") # Verify that the function exists. if "populateBoard" not in globals() or not callable(globals()["populateBoard"]): print("Warning: The populateBoard function does not appear to exist.") print() global populateBoard populateBoard = lambda board, num_viruses: None return # Verify that it takes exactly 2 parameters. num_params = len(signature(populateBoard).parameters) if num_params != 2: print(f"Error: The populateBoard function is supposed to take 2 parameters, but it currently takes {num_params}.") print() close() quit() # Verify that populateBoard adds the correct number of viruses to the # board. warned = 0 target = 1 while target <= 40 and warned < 5: board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) current_warned = False # Verify that the size of the board is correct. rows = len(board) if rows != BOARD_HEIGHT: print(f" Warning: The board had {rows} rows when it was supposed to have {BOARD_HEIGHT} rows.") warned = warned + 1 current_warned = True # Verify that each row has the correct number of columns. if all([len(row) == BOARD_WIDTH for row in board]) == False: print(f" Warning: Each row in the board is supposed to have {BOARD_WIDTH} columns, but there is at least one column that does not.") warned = warned + 1 current_warned = True # Create a board with the target number of viruses and verify that the # board has that many. populateBoard(board, target) #nv = len([e for e in [item for r in board for item in r] if e != EMPTY]) nv = len([e for e in [item for r in board for item in r] if (e == "1" or e == "2" or e == "3")]) if nv != target: print(f" Warning: Tried to populate the board with {target} viruses, but the board actually had {nv} viruses.") if warned == 0: print("The board that was returned by populateBoard is:") pprint(board) warned = warned + 1 current_warned = True # Verify that all of the viruses are in the bottom VIRUS_ROWS rows of the # board. tv = len([e for e in [item for r in board[:len(board)-VIRUS_ROWS] for item in r] if e != EMPTY]) if tv != 0: print(f" Warning: The board had {nv} viruses, {tv} of which {'was' if tv == 1 else 'were'} not in the bottom {VIRUS_ROWS} rows of the board.") warned = warned + 1 current_warned = True if current_warned == False: print(f"Testing a board with {target} viruses... SUCCESS!") # Move to the next target value. target = target + 1 # Advise the user if additional warnings may not be present but were not # displayed. if warned >= 5: print("Warning limit reached. There may be additional warnings that were not displayed.") # Advise the user that all of the tests passed if they did. if warned == 0: print("All tests passed for populateBoard.") print() # # Can a piece be rotated counter-clockwise? # # Parameters: # board: The board where the piece will be rotated # row, col: The location of the piece # rot: The current rotation of the piece (must be 0, 90, 180 or 270) # # Returns: True if the piece can be rotated counter-clockwise. False # otherwise. # def canRotateCCW(board, row, col, rot): # The piece cannot be rotated if the other half of it will end up off the # left edge of the board. if col == 0 and rot == 90: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if col > 0 and rot == 90 and board[row][col - 1] != EMPTY: return False # The piece cannot be rotated if the other half of it will end up off the # right edge of the board. if row == len(board) - 1 and rot == 180: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if row < len(board) - 1 and rot == 180 and board[row + 1][col] != EMPTY: return False # The piece cannot be rotated if the other half of it will end up off the # bottom of the board. if col == len(board[row]) - 1 and rot == 270: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if col < len(board[row]) - 1 and rot == 270 and board[row][col + 1] != EMPTY: return False # The piece cannot be rotated if the other half of it will end up off the # top of the board. if row <= 0 and rot == 0: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if row > 0 and rot == 0 and board[row-1][col] != EMPTY: return False # In all other cases, the piece can be rotated. return True # # Can a piece be rotated clockwise? # # Parameters: # board: The board where the piece will be rotated # row, col: The location of the piece # rot: The current rotation of the piece (must be 0, 90, 180 or 270) # # Returns: True if the piece can be rotated clockwise. False otherwise. # def canRotateCW(board, row, col, rot): # The piece cannot be rotated if the other half of it will end up off the # left edge of the board. if col == 0 and rot == 270: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if col > 0 and rot == 270 and board[row][col - 1] != EMPTY: return False # The piece cannot be rotated if the other half of it will end up off the # right edge of the board. if row == len(board) - 1 and rot == 0: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if row < len(board) - 1 and rot == 0 and board[row + 1][col] != EMPTY: return False # The piece cannot be rotated if the other half of it will end up off the # bottom of the board. if col == len(board[row]) - 1 and rot == 90: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if col < len(board[row]) - 1 and rot == 90 and board[row][col + 1] != EMPTY: return False # The piece cannot be rotated if the other half of it will end up off the # top of the board. if row <= 0 and rot == 180: return False # The piece cannot be rotated if the space that it will be rotated into # is not empty. if row > 0 and rot == 180 and board[row][col - 1] != EMPTY: return False return True # # Can the capsule be moved on column to the left? # # Parameters: # board: The game board on which the capsule is being moved. # row, col: The location of the capsule # rot: The rotation of the capsule # # Returns: # True if the capsule can be moved to the left. False otherwise. # def canMoveLeft(board, row, col, rot): # If the capsule is above the top of the board it cannot be moved. if row < 0: return False # If the capsule is at the left edge of the board it cannot be moved. if col == 0: return False # If the capsule is rotated by 180 degrees, then the anchor point is to the # right of its other half, so it can't move left if it is in column 1. if col == 1 and rot == 180: return False # The capsule can't move to the left if the space(s) to its left is/are # already occupied. if rot == 0 and col > 0 and board[row][col-1] != EMPTY: return False if rot == 90 and col > 0 and row >= 1 and (board[row][col-1] != EMPTY or board[row-1][col-1] != EMPTY): return False if rot == 180 and col > 1 and board[row][col-2] != EMPTY: return False if rot == 270 and col > 0 and row <= BOARD_HEIGHT - 2 and (board[row][col-1] != EMPTY or board[row+1][col-1] != EMPTY): return False # In all other cases, the capsule can move to its left. return True # # Can the capsule be moved on column to the right? # # Parameters: # board: The game board on which the capsule is being moved. # row, col: The location of the capsule # rot: The rotation of the capsule # # Returns: # True if the capsule can be moved to the right. False otherwise. # def canMoveRight(board, row, col, rot): # If the capsule is above the top of the board it cannot be moved. if row < 0: return False # If the capsule is at the right edge of the board it cannot be moved. if col == BOARD_WIDTH - 1: return False # If the capsule is rotated by 0 degrees, then the anchor point is to the # left of its other half, so it can't move right if it is in the second # last column. if col == BOARD_WIDTH - 2 and rot == 0: return False # The capsule can't move to the right if the space(s) to its right is/are # already occupied. if rot == 0 and col < BOARD_WIDTH - 2 and board[row][col+2] != EMPTY: return False if rot == 90 and col < BOARD_WIDTH - 1 and row >= 1 and (board[row][col+1] != EMPTY or board[row-1][col+1] != EMPTY): return False if rot == 180 and col < BOARD_WIDTH - 1 and board[row][col+1] != EMPTY: return False if rot == 270 and col < BOARD_WIDTH - 1 and row < BOARD_HEIGHT - 1 and (board[row][col+1] != EMPTY or board[row+1][col+1] != EMPTY): return False # In all other cases, the capsule can move to its right. return True # # Display the framerate as part of the user interface. # # Parameters: # app_start_time: The result returned by time.time when the program was # started. # frames: The number of frames that have been displayed since the program # started. # def displayFramerate(app_start_time, frames): setColor("black") text(0, getHeight() - 36, f"Frames: {frames}", "w") text(0, getHeight() - 24, f"Seconds: {time() - app_start_time}", "w") if frames > 0: text(0, getHeight() - 12, f"Frames / second: {1 / ((time() - app_start_time) / frames)}", "w") # # Display information about the level for the user. # # Parameters: # board: The game board # level: The current level number # cleared: The number of viruses that have been cleared by the player # during the game (not just the current level) # future: The queue of future capsules that the player will use in the game # # Returns: (None) # def displayLevelInformation(board, level, cleared, future): # Display the level number. setColor("Black") text(getWidth() - (getWidth() / 2 - CELL_SIZE * (BOARD_WIDTH / 2)) / 2, 100, f"Level {level}") # Display the number of viruses remaining. text(getWidth() - (getWidth() / 2 - CELL_SIZE * (BOARD_WIDTH / 2)) / 2, 200, f"Viruses Remaining:") text(getWidth() - (getWidth() / 2 - CELL_SIZE * (BOARD_WIDTH / 2)) / 2, 250, numViruses(board)) # Display the total number of viruses that have been cleared. text(getWidth() - (getWidth() / 2 - CELL_SIZE * (BOARD_WIDTH / 2)) / 2, 300, f"Total Cleared:") text(getWidth() - (getWidth() / 2 - CELL_SIZE * (BOARD_WIDTH / 2)) / 2, 350, cleared) # Display the queue of future capsules that the player will use. text((getWidth() / 2 - CELL_SIZE * (BOARD_WIDTH / 2)) / 2, 75, "Next") setOutline("black") setFill(96, 96, 96) polygon(135, 100, 200, 165, \ 170, 165, \ 170, 475, \ 100, 475, \ 100, 165, 70, 165) # # Draw the capsules. # for i in range(len(future)): drawCapsule(i+3, -5.25, int(future[i][0]), int(future[i][1]), 0) # # Test the numViruses function. Warnings are displayed for any tests that # fail. # # Parameters: (None) # # Returns: (None) # # TODO: The tests should probably verify that call numViruses doesn't change # the board. # def testNumViruses(): print("Testing numViruses...") # Verify that the function exists. if "numViruses" not in globals() or not callable(globals()["numViruses"]): print("Warning: The numViruses function does not appear to exist.") print() #close() #quit() global numViruses numViruses = lambda board: -1 return # Verify that it takes exactly 1 parameter. num_params = len(signature(numViruses).parameters) if num_params != 1: print(f"Error: The numViruses function is supposed to take 1 parameter, but it currently takes {num_params}.") print() close() quit() # locs is tuples of row, column, string to place at that location, and # expected result when this item and all previous items have been added # to the board. locs = [(1, 2, "1", 1), (1, 3, "2x", 1), (8, 7, "2", 2), (1, 0, "2c", 2), (15, 0, "2c", 2), (15, 1, "3c", 2), (15, 2, "3", 3), (15, 3, "3", 4), (0, 0, "1", 5), (7, 0, "1", 6), (6, 0, "1r", 6), (6, 1, "2l", 6), (7, 1, "3d", 6), (8, 1, "1u", 6)] # Create a new, empty board. board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) # Add each location to the board in sequence, verifying that the updated # board has the expected number of viruses. warnings = 0 for (r, c, item, expected) in locs: # Add the current element to the board. board[r][c] = item # Count the number of viruses and compare it to the expecte value. nv = numViruses(board) if nv != expected: print(f"Warning: numVirsuses returned {nv} when {expected} was expected.") if warnings == 0: print("The board that was evaluated is:") pprint(board) print() warnings = warnings + 1 else: print(f" Testing a board with {expected} viruses... SUCCESS!") # Display an appropriate message if all of the test cases passed. if warnings == 0: print("numViruses passed all of the test cases.") print() # # Test the findHorizontal function. # # Parameters: (None) # # Returns: (None) # def testFindHorizontal(): print("Testing findHorizontal...") # Verify that the function exists. if "findHorizontal" not in globals() or not callable(globals()["findHorizontal"]): print("Warning: The findHorizontal function does not appear to exist.") print() global findHorizontal findHorizontal = lambda board: (-1, -1) return # Verify that it takes exactly 1 parameter. num_params = len(signature(findHorizontal).parameters) if num_params != 1: print(f"Error: The findHorizontal function is supposed to take 1 parameter, but it currently takes {num_params}.") print() close() quit() # # Test an empty board and verify that no horizontal lines are found. # orig_board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) warnings = 0 r, c = findHorizontal(board) if r != -1 or c != -1: print(f" Warning: Attempted to find a horizontal line on an empty board and it returned {r}, {c} when it should have returned -1, -1.") warnings = warnings + 1 if board != orig_board: print(f" Warning: findHorizontal changed the game board while performing its work when it shouldn't have.") warnings = warnings + 1 # # Test a checkerboard, and verify that no horizontal lines are found. # orig_board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) r, c = findHorizontal(board) if r != -1 or c != -1: print(f" Warning: Attempted to find a horizontal line on an checkerboard and it returned {r}, {c} when it should have returned -1, -1.") warnings = warnings + 1 if board != orig_board: print(f" Warning: findHorizontal changed the game board while performing its work when it shouldn't have.") warnings = warnings + 1 # # Test several cases where lines should be found # warnings = warnings + testOneHorizontal(10, 4, 10, 4, "1", "1", "1", "1", warnings) warnings = warnings + testOneHorizontal(0, 0, 0, 0, "2", "2", "2", "2", warnings) warnings = warnings + testOneHorizontal(0, 4, 0, 4, "3", "3", "3", "3", warnings) warnings = warnings + testOneHorizontal(15, 0, 15, 0, "3", "3", "3", "3", warnings) warnings = warnings + testOneHorizontal(15, 4, 15, 4, "3", "3", "3", "3", warnings) warnings = warnings + testOneHorizontal(10, 4, 10, 4, "1c", "1c", "1c", "1c", warnings) warnings = warnings + testOneHorizontal(0, 0, 0, 0, "2u", "2d", "2l", "2r", warnings) warnings = warnings + testOneHorizontal(0, 4, 0, 4, "3", "3r", "3", "3", warnings) warnings = warnings + testOneHorizontal(15, 0, 15, 0, "3", "3", "3c", "3", warnings) warnings = warnings + testOneHorizontal(15, 4, 15, 4, "3d", "3", "3", "3d", warnings) # # Test several cases where lines should not be found # warnings = warnings + testOneHorizontal(10, 4, -1, -1, "1", "1", "2", "1", warnings) warnings = warnings + testOneHorizontal(0, 0, -1, -1, "2", "2", "2", "1", warnings) warnings = warnings + testOneHorizontal(0, 4, -1, -1, "3", "1", "3", "3", warnings) warnings = warnings + testOneHorizontal(15, 0, -1, -1, "2", "3", "3", "3", warnings) warnings = warnings + testOneHorizontal(15, 4, -1, -1, "3", "3", "3", "1", warnings) warnings = warnings + testOneHorizontal(10, 4, -1, -1, "2c", "1c", "1c", "1c", warnings) warnings = warnings + testOneHorizontal(0, 0, -1, -1, "2u", "2d", "2l", "3r", warnings) warnings = warnings + testOneHorizontal(0, 4, -1, -1, "3", "3r", "1", "3", warnings) warnings = warnings + testOneHorizontal(15, 0, -1, -1, "2", "3", "3c", "3", warnings) warnings = warnings + testOneHorizontal(15, 4, -1, -1, "3d", "3", "3", "1d", warnings) # # Make sure that the left-most edge is found for a longer line. # board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) board[5][1:8] = ["1", "1", "1", "1", "1", "1", "1"] r, c = findHorizontal(board) if r != 5 or c != 1: print(f" Warning: Attempted to find a horizontal line and it returned {r}, {c} when it should have returned 5, 1.") if warnings == 0: print("The board that was evaluated is:") pprint(board) print() warnings = warnings + 1 else: print(f" Looking for a horizontal line at {r, c}: SUCCESS!") if warnings == 0: print("findHorizontal passed all of the test cases.") print() def testOneHorizontal(lr, lc, er, ec, s1, s2, s3, s4, warnings): board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) board[lr][lc:lc+4] = [s1, s2, s3, s4] r, c = findHorizontal(board) if r != er or c != ec: print(f" Warning: Attempted to find a horizontal line and it returned {r}, {c} when it should have returned {er}, {ec}.") if warnings == 0: print("The board that was evaluated is:") pprint(board) print() return 1 else: print(f" Looking for a horizontal line at {er, ec}: SUCCESS!") return 0 # # Test the findVertical function. # # Parameters: (None) # # Returns: (None) # def testFindVertical(): print("Testing findVertical...") # Verify that the function exists. if "findVertical" not in globals() or not callable(globals()["findVertical"]): print("Warning: The findVertical function does not appear to exist.") print() #close() #quit() global findVertical findVertical = lambda board: (-1, -1) return # Verify that it takes exactly 1 parameter. num_params = len(signature(findVertical).parameters) if num_params != 1: print(f"Error: The findVertical function is supposed to take 1 parameter, but it currently takes {num_params}.") print() close() quit() # # Test an empty board and verify that no vertical lines are found. # orig_board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) warnings = 0 r, c = findVertical(board) if r != -1 or c != -1: print(f" Warning: Attempted to find a vertical line on an empty board and it returned {r}, {c} when it should have returned -1, -1.") warnings = warnings + 1 if board != orig_board: print(f" Warning: findVertical changed the game board while performing its work when it shouldn't have.") warnings = warnings + 1 # # Test a checkerboard, and verify that no vertical lines are found. # orig_board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) r, c = findVertical(board) if r != -1 or c != -1: print(f" Warning: Attempted to find a vertical line on an checkerboard and it returned {r}, {c} when it should have returned -1, -1.") warnings = warnings + 1 if board != orig_board: print(f" Warning: findVertical changed the game board while performing its work when it shouldn't have.") warnings = warnings + 1 # # Test several cases where lines should be found # warnings = warnings + testOneVertical(10, 4, 10, 4, "1", "1", "1", "1", warnings) warnings = warnings + testOneVertical(0, 0, 0, 0, "2", "2", "2", "2", warnings) warnings = warnings + testOneVertical(0, 7, 0, 7, "3", "3", "3", "3", warnings) warnings = warnings + testOneVertical(12, 0, 12, 0, "3", "3", "3", "3", warnings) warnings = warnings + testOneVertical(12, 7, 12, 7, "2", "2", "2", "2", warnings) warnings = warnings + testOneVertical(10, 4, 10, 4, "1c", "1c", "1c", "1c", warnings) warnings = warnings + testOneVertical(0, 0, 0, 0, "2u", "2d", "2l", "2r", warnings) warnings = warnings + testOneVertical(0, 7, 0, 7, "3", "3r", "3", "3", warnings) warnings = warnings + testOneVertical(12, 0, 12, 0, "3", "3", "3c", "3", warnings) warnings = warnings + testOneVertical(12, 7, 12, 7, "2d", "2", "2", "2d", warnings) # # Test several cases where lines should not be found # warnings = warnings + testOneVertical(10, 4, -1, -1, "1", "1", "2", "1", warnings) warnings = warnings + testOneVertical(0, 0, -1, -1, "2", "2", "2", "1", warnings) warnings = warnings + testOneVertical(0, 7, -1, -1, "3", "1", "3", "3", warnings) warnings = warnings + testOneVertical(12, 0, -1, -1, "2", "3", "3", "3", warnings) warnings = warnings + testOneVertical(12, 7, -1, -1, "1", "1", "1", "3", warnings) warnings = warnings + testOneVertical(10, 4, -1, -1, "2c", "1c", "1c", "1c", warnings) warnings = warnings + testOneVertical(0, 0, -1, -1, "2u", "2d", "2l", "3r", warnings) warnings = warnings + testOneVertical(0, 7, -1, -1, "3", "3r", "1", "3", warnings) warnings = warnings + testOneVertical(12, 0, -1, -1, "2", "3", "3c", "3", warnings) warnings = warnings + testOneVertical(12, 7, -1, -1, "1d", "1", "1", "3d", warnings) # # Make sure that the top edge is found for longer lines. # board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) board[5][1] = "1" board[6][1] = "1" board[7][1] = "1" board[8][1] = "1" board[9][1] = "1" board[10][1] = "1" board[11][1] = "1" r, c = findVertical(board) if r != 5 or c != 1: print(f" Warning: Attempted to find a vertical line and it returned {r}, {c} when it should have returned 5, 1.") if warnings == 0: print("The board that was evaluated is:") pprint(board) print() warnings = warnings + 1 else: print(f" Looking for a vertical line at {r, c}: SUCCESS!") if warnings == 0: print("findVertical passed all of the test cases.") print() # # Perform one test of the findVertical function. # # Parameters: # lr, lc: The position on the board where items will be stored on the board # er, ec: The expected result from findVertical # s1, s2, s3, s4: The items that are stored on the board # warnings: The number of warnings that have been reported so far while # testing findVertical # # Returns: The new number of warnings reported while testing findVertical. # def testOneVertical(lr, lc, er, ec, s1, s2, s3, s4, warnings): board = createCheckerBoard(BOARD_HEIGHT, BOARD_WIDTH) board[lr][lc] = s1 board[lr+1][lc] = s2 board[lr+2][lc] = s3 board[lr+3][lc] = s4 r, c = findVertical(board) if r != er or c != ec: print(f" Warning: Attempted to find a vertical line and it returned {r}, {c} when it should have returned {er}, {ec}.") if warnings == 0: print("The board that was evaluated is:") pprint(board) print() return 1 else: print(f" Looking for a vertical line at {er, ec}: SUCCESS!") return 0 # # Test the canDrop function. # # Parameters: (None) # # Returns: (None) # def testCanDrop(): print("Testing the canDrop function...") # Verify that the function exists. if "canDrop" not in globals() or not callable(globals()["canDrop"]): print("Warning: The canDrop function does not exist.") print() global canDrop canDrop = lambda board, r, c, rotation: False return # Verify that it takes exactly 2 parameters. num_params = len(signature(populateBoard).parameters) if num_params != 2: print(f"Error: The populateBoard function is supposed to take 2 parameters, but it currently takes {num_params}.") print() close() quit() warnings = 0 # # Test several cases where the rotation is 0 # board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) warnings = warnings + testOneCanDrop(board, -1, 3, 0, True, warnings) warnings = warnings + testOneCanDrop(board, 0, 0, 0, True, warnings) warnings = warnings + testOneCanDrop(board, 0, BOARD_WIDTH - 2, 0, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 2, 0, 0, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 2, BOARD_WIDTH - 2, 0, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 1, 0, 0, False, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 1, BOARD_WIDTH - 2, 0, False, warnings) # Only the left side is supported board[5][5] = "1" warnings = warnings + testOneCanDrop(board, 4, 5, 0, False, warnings) # Both sides are supported board[5][6] = "2c" warnings = warnings + testOneCanDrop(board, 4, 5, 0, False, warnings) # Only the right side is supported board[5][5] = EMPTY warnings = warnings + testOneCanDrop(board, 4, 5, 0, False, warnings) # Test some cases where it can drop because there is something that is # below it on the board (but not immediately below it). warnings = warnings + testOneCanDrop(board, 3, 4, 0, True, warnings) warnings = warnings + testOneCanDrop(board, 3, 5, 0, True, warnings) warnings = warnings + testOneCanDrop(board, 3, 6, 0, True, warnings) # # Test several cases where the capsule is rotated by 180 degrees # board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) warnings = warnings + testOneCanDrop(board, 0, 0, 180, True, warnings) warnings = warnings + testOneCanDrop(board, 0, BOARD_WIDTH - 2, 180, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 2, 0, 180, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 2, BOARD_WIDTH - 2, 180, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 1, 0, 180, False, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 1, BOARD_WIDTH - 2, 180, False, warnings) # Only the left side is supported board[5][5] = "1" warnings = warnings + testOneCanDrop(board, 4, 6, 180, False, warnings) # Both sides are supported board[5][6] = "2c" warnings = warnings + testOneCanDrop(board, 4, 6, 180, False, warnings) # Only the right side is supported board[5][5] = EMPTY warnings = warnings + testOneCanDrop(board, 4, 6, 180, False, warnings) # Test some cases where it can drop because there is something that is # below it on the board (but not immediately below it). warnings = warnings + testOneCanDrop(board, 3, 5, 180, True, warnings) warnings = warnings + testOneCanDrop(board, 3, 6, 180, True, warnings) warnings = warnings + testOneCanDrop(board, 3, 7, 180, True, warnings) # # Test several cases where the capsule is rotated by 90 degrees # board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) warnings = warnings + testOneCanDrop(board, 1, 0, 90, True, warnings) warnings = warnings + testOneCanDrop(board, 1, BOARD_WIDTH - 1, 90, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 1, 0, 90, False, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 1, BOARD_WIDTH - 1, 90, False, warnings) # Can't drop due to something immediately below it. board[11][6] = "1" warnings = warnings + testOneCanDrop(board, 10, 6, 90, False, warnings) board[11][6] = "1u" warnings = warnings + testOneCanDrop(board, 10, 6, 90, False, warnings) board[11][6] = "2c" warnings = warnings + testOneCanDrop(board, 10, 6, 90, False, warnings) board[11][6] = "2r" warnings = warnings + testOneCanDrop(board, 10, 6, 90, False, warnings) board[11][6] = "3l" warnings = warnings + testOneCanDrop(board, 10, 6, 90, False, warnings) board[11][6] = "3d" warnings = warnings + testOneCanDrop(board, 10, 6, 90, False, warnings) # # Test several cases where the capsule is rotated by 270 degrees # board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) warnings = warnings + testOneCanDrop(board, 0, 0, 270, True, warnings) warnings = warnings + testOneCanDrop(board, 0, BOARD_WIDTH - 1, 270, True, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 2, 0, 270, False, warnings) warnings = warnings + testOneCanDrop(board, BOARD_HEIGHT - 2, BOARD_WIDTH - 1, 270, False, warnings) # Can't drop due to something immediately below it. board[12][6] = "1c" warnings = warnings + testOneCanDrop(board, 10, 6, 270, False, warnings) board[12][6] = "1r" warnings = warnings + testOneCanDrop(board, 10, 6, 270, False, warnings) board[12][6] = "2l" warnings = warnings + testOneCanDrop(board, 10, 6, 270, False, warnings) board[12][6] = "2d" warnings = warnings + testOneCanDrop(board, 10, 6, 270, False, warnings) board[12][6] = "3" warnings = warnings + testOneCanDrop(board, 10, 6, 270, False, warnings) board[12][6] = "3u" warnings = warnings + testOneCanDrop(board, 10, 6, 270, False, warnings) if warnings == 0: print("canDrop passed all of the test cases.") print() # # Test the canDrop function for one particular input case. # # Parameters: # board: The board used during testing # row, col: The position of the capsule that is being tested # rot: The rotation of the capsule being tested # expected: The result that canDrop is expected to return # warnings: The number of warnings that have been reported so far while # testing canDrop # # Returns: The new number of warnings reported while testing canDrop # # def testOneCanDrop(board, row, col, rot, expected, warnings): result = canDrop(board, row, col, rot) if result != expected: print(f" Warning: Called canDrop(, {row}, {col}, {rot} and it returned {result} when it should have returned {expected}.") if warnings == 0: print("The board that was evaluated is:") pprint(board) print() return 1 else: print(f" Testing canDrop(, {row}, {col}, {rot}): SUCCESS!") return 0 # # Is a capsule fully on the board? # # Parameters: # row, col: The position of the anchor point for the capsule # rot: The rotation of the capsule # # Returns: True if the capsule is fully on the board. False otherwise. # def isOnBoard(row, col, rot): # Check capsules with a rotation of 0 degrees. if rot == 0 and col >= 0 and col < BOARD_WIDTH - 1 and row >= 0 and row < BOARD_HEIGHT: return True # Check capsules that are rotated 180 degrees. if rot == 180 and col >= 1 and col < BOARD_WIDTH and row >= 0 and row < BOARD_HEIGHT: return True # Check capsules that are rotated 90 degrees. if rot == 90 and col >= 0 and col < BOARD_WIDTH and row >= 1 and row < BOARD_HEIGHT - 1: return True # Check capsules that are rotated 270 degrees. if rot == 270 and col >= 0 and col < BOARD_WIDTH and row >= 1 and row < BOARD_HEIGHT - 2: return True # Any other capsules are at least partially off the board. return False def main(): # # Test the functions that the students write. # testPopulateBoard() testNumViruses() testCanDrop() testFindHorizontal() testFindVertical() # # Display the controls in the console # print() print("Instructions:") print() print("Build lines of 4 or more elements of the same color to remove them.") print() print("Controls:") print(" Move capsule left / right: Arrows") print(" Move capsule down: Down arrow") print(" Rotate capsule clockwise: x") print(" Rotate capsule counter-clockwise: z") print(" Quit: Esc") print() # Get the images of the virus that the player is trying to eliminate. v1 = decodeImage(virus_1_data) v2 = decodeImage(virus_2_data) v3 = decodeImage(virus_3_data) # Set up the initial game state. level = 1 cleared = 0 future = [] row = -1 col = BOARD_WIDTH // 2 - 1 c1 = randrange(1, 4) c2 = randrange(1, 4) rot = 0 # Create and populate the board board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) populateBoard(board, 10) for r in range(len(board)): for c in range(len(board[r])): if type(board[r][c]) != str: print("Error: The board contains at least one element that is not a string.") print("The board is:") pprint(board) close() quit() # Populate the list of future blocks. Each entry in this list is a string # of two charactesr, where the first is the color of the left part of the # block, and the second is the color of the right part of the block. for i in range(10): future.append(str(randrange(1, 4)) + str(randrange(1, 4))) # Set up the counters for tracking the frame rate setAutoUpdate(False) start_time = time() app_start_time = start_time frames = 0 frames_since_drop = 0 # Draw the initial board. clear() drawBoard(board, v1, v2, v3) drawCapsule(row, col, c1, c2, rot) displayLevelInformation(board, level, cleared, future) # Wait for the user to press a key to begin the game. setColor("Black") text(getWidth() / 2, 575, "Press any key to begin...") update() keys = getKeys() while len(keys) == 0: sleep(1/ 30) update() keys = getKeys() if closed(): quit() # Main game loop. game_over = False while not closed() and game_over == False: # Read key presses from the user. keys = getKeys() if isOnBoard(row, col, rot): # Rotate the capsule. if 'z' in keys or 'Z' in keys: if canRotateCCW(board, row, col, rot): rot = (rot + 90) % 360 if 'x' in keys or 'X' in keys: if canRotateCW(board, row, col, rot): rot = (rot - 90) % 360 # Move the capsule. if 'Left' in keys and canMoveLeft(board, row, col, rot): col = col - 1 if 'Right' in keys and canMoveRight(board, row, col, rot): col = col + 1 if 'Down' in keys and canDrop(board, row, col, rot): row = row + 1 elif 'Down' in keys and canDrop(board, row, col, rot) == False: # Force the item to store at this location immediately frames_since_drop = 15 if frames_since_drop == 15: # Drop the capsule if possible if canDrop(board, row, col, rot): row = row + 1 frames_since_drop = 0 # Ensure that the capsule is on the board. if row >= BOARD_HEIGHT or (row >= BOARD_HEIGHT - 1 and rot == 270): print("Error: The capsule went off the bottom of the board. Perhaps there is an error in canDrop?") close() quit() else: # Ensure that the capsule is on the board before it is shored. if row >= BOARD_HEIGHT: print("Error: The capsule went off the bottom of the board. Perhaps there is an error in canDrop?") close() quit() # Store the current item into the board with the appropriate rotation. if row > -1: if rot == 0: board[row][col] = "%dr" % c1 elif rot == 90: board[row][col] = "%du" % c1 elif rot == 180: board[row][col] = "%dl" % c1 elif rot == 270: board[row][col] = "%dd" % c1 else: board[row][col] = "%dc" % c1 if rot == 0: board[row][col+1] = "%dl" % c2 if rot == 90: board[row-1][col] = "%dd" % c2 if rot == 180: board[row][col-1] = "%dr" % c2 if rot == 270: board[row+1][col] = "%du" % c2 # Set up the next capsule at the top of the board. row = -1 col = BOARD_WIDTH // 2 - 1 rot = 0 # Add a new capsule to the list of future capsules. c1 = int(future[0][0]) c2 = int(future[0][1]) future.pop(0) future.append(str(randrange(1, 4)) + str(randrange(1, 4))) else: # Game over -- the block can't drop from its initial location print("Game Over!") game_over = True else: frames_since_drop = frames_since_drop + 1 # # Use the student's findVertical function to find all of the vertical # lines on the board, building a list of positions that identify all of # squares on the board that need to be marked for deletion. # temp_board = deepcopy(board) found = [] r, c = findVertical(temp_board) while (r, c) != (-1, -1): color = temp_board[r][c][0] while r < len(temp_board) and temp_board[r][c][0] == color: found.append((r, c)) temp_board[r][c] = EMPTY r = r + 1 r, c = findVertical(temp_board) # # Use the student's findHorizontal function to find all of the horizontal # lines on the board, adding them to the previous list of positions for # vertical lines that identify all of squares on the board that need to # be marked for deletion. Waiting to do any of the deletions until both # vertical and horizontal lines have been identified ensures that T # shaped constructs are also processed correctly. # temp_board = deepcopy(board) r, c = findHorizontal(temp_board) while (r, c) != (-1, -1): color = temp_board[r][c][0] while c < len(temp_board[r]) and temp_board[r][c][0] == color: found.append((r, c)) temp_board[r][c] = EMPTY c = c + 1 r, c = findHorizontal(temp_board) # Mark all of the identified squares for deletion. for (r, c) in found: # Count the viruses that have been cleared if len(board[r][c]) == 1: cleared = cleared + 1 # Mark the location for deletion board[r][c] = board[r][c][0] + "x" # If there were any items marked for deletion, then the game pauses # briefly so that the player can see which items are about to be # deleted. if len(found) > 0: drawBoard(board, v1, v2, v3) update() sleep(0.3) frames = frames + 9 # After the pause, any items marked for deletion are actually deleted # from the board. for r in range(len(board)): for c in range(len(board[r])): if len(board[r][c]) >= 2 and board[r][c][1] == "x": board[r][c] = EMPTY # During the deletion process, some capsules may be broken. These function # calls replace any broken half capsules with their circular equivalents. fix_lr(board) fix_tb(board) # Drop the pieces until everything unsupported has dropped as far as # possible. while dropBoardPieces(board) == True: clear() drawBoard(board, v1, v2, v3) displayFramerate(app_start_time, frames) displayLevelInformation(board, level, cleared, future) update() sleep(0.1) frames = frames + 3 frames_since_drop = 0 row = -1 # Draw the game board. clear() drawBoard(board, v1, v2, v3) # Draw the current capsule that the player is using. drawCapsule(row, col, c1, c2, rot) # Update the framerate counter and level information. displayFramerate(app_start_time, frames) displayLevelInformation(board, level, cleared, future) # Force the updates to be displayed. update() # If the number of viruses is 0, then the level is done and the next level # needs to be created. if numViruses(board) == 0: level = level + 1 # Create a new board for the next level. board = createBoard(BOARD_HEIGHT, BOARD_WIDTH) populateBoard(board, min(level * 2 + 8, VIRUS_ROWS * BOARD_WIDTH)) for r in range(len(board)): for c in range(len(board[r])): if type(board[r][c]) != str: print("Error: The board contains at least one element that is not a string.") print("The board is:") pprint(board) close() quit() # Display the next level. clear() drawBoard(board, v1, v2, v3) drawCapsule(row, col, c1, c2, rot) displayFramerate(app_start_time, frames) displayLevelInformation(board, level, cleared, future) update() # Pause briefly before the next level begins. sleep(1) frames = frames + 30 # Keep the game running at approximately 30 frames per second. while time() < start_time + 1/30: sleep(0.00001) start_time = time() frames = frames + 1 # Display the game over message. setColor("red") setFont("Arial", 96) text(getWidth() / 2, getHeight() / 2, "Game Over!", "c") while not closed(): update() sleep(1/30) main()