import operator import logging import random # Logging to console logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) # Set loglevel to INFO or higher to prevent console spam. """ Helper functions """ # Return some hex colour """ Waiting for Stanford to fix a bug in the environment CIP bug: https://codeinplace.stanford.edu/cip6/report?post=ac7c4ffd-1f90-475a-b4ee-2e5b73c5348f from matplotlib import colors import decimal def get_random_colour(): return decimal.Decimal(random.randrange(0,10)/10), decimal.Decimal(random.randrange(0,10)/10), decimal.Decimal(random.randrange(0,10)/10) colour = colors.rgb2hex((get_random_colour())) background = colors.rgb2hex((get_random_colour())) """ def generate_random_colour(): # Generate random RGB values rgb_values = [random.randrange(256), random.randrange(256), random.randrange(256)] # Convert to HEX values hex_colours = [hex(value)[2:] for value in rgb_values] # Strip the '0x' prefix # Add leading "0" for single digit values for i in range(len(hex_colours)): if len(str(hex_colours[i])) == 1: hex_colours[i] = f"0{hex_colours[i]}" # Add leading "#" to hex values colour = '#' + ''.join(hex_colours) logger.debug(f"Generated random colour: {colour}") return colour # Base the size on the shortest pane def determine_max_size(canvas): if canvas.width < canvas.height: size = random.randint(1, canvas.width) else: size = random.randint(1, canvas.height) return size """ Asset functions """ # Draw a background with random colour on the canvas def generate_random_background(canvas): background = canvas.create_rectangle( 1, 1, canvas.width, canvas.height, generate_random_colour(), generate_random_colour() ) return background # Erase a passed Karel from the canvas def erase_asset(asset): canvas = asset[1][0] # Erase each shape of the asset for shape in asset[0].values(): canvas.delete(shape) logger.debug(f"Erased {asset[2]} asset: {asset}") # Move a passed Karel on the canvas """ Waiting for Stanford to fix a bug in the environment CIP bug: https://codeinplace.stanford.edu/cip6/report?post=7043ece9-7653-4e39-8a63-5f4544b1d74b def move_karel(canvas, karel, x, y): for shape in karel[0].values(): canvas.move(shape, x, y) logger.debug(f"Moved: {karel} by {x} horizontally, {y} vertically") """ # Move a passed asset relative to previous position def relative_move_asset(asset, x:int=0, y:int=0): # Update coordinates in list asset[1][1] += x asset[1][2] += y # Replace asset erase_asset(asset) match asset[2]: case "karel": new_asset = draw_karel(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) case "beeper": new_asset = draw_beeper(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) logger.debug(f"Moved {asset[2]} by {x} horizontally, {y} vertically: {new_asset}") return new_asset # Move a passed asset in relation to her orientation def orientation_move_asset(asset, direction:str, amount:int): # Direction translation if asset[1][4].startswith("east"): # East if direction.lower() == "forward" or direction.lower() == "front": asset[1][1] += amount if direction.lower() == "left": asset[1][2] -= amount if direction.lower() == "right": asset[1][2] += amount if direction.lower() == "backward" or direction.lower() == "back": asset[1][1] -= amount if asset[1][4].startswith("north"): # North if direction.lower() == "forward" or direction.lower() == "front": asset[1][2] -= amount if direction.lower() == "left": asset[1][1] -= amount if direction.lower() == "right": asset[1][1] += amount if direction.lower() == "backward" or direction.lower() == "back": asset[1][2] += amount if asset[1][4].startswith("west"): # West if direction.lower() == "forward" or direction.lower() == "front": asset[1][1] -= amount if direction.lower() == "left": asset[1][2] += amount if direction.lower() == "right": asset[1][2] -= amount if direction.lower() == "backward" or direction.lower() == "back": asset[1][1] += amount if asset[1][4].startswith("south"): # South if direction.lower() == "forward" or direction.lower() == "front": asset[1][2] += amount if direction.lower() == "left": asset[1][1] += amount if direction.lower() == "right": asset[1][1] -= amount if direction.lower() == "backward" or direction.lower() == "back": asset[1][2] -= amount # Replace asset erase_asset(asset) match asset[2]: case "karel": new_asset = draw_karel(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) case "beeper": new_asset = draw_beeper(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) logger.debug(f"Moved: {asset[2]} {direction} by {amount}: {new_asset}") return new_asset # Move a passed asset to new coordinates def absolute_move_asset(asset, x:int, y:int): # Update coordinates in list asset[1][1] = x asset[1][2] = y # Replace asset erase_asset(asset) match asset[2]: case "karel": new_asset = draw_karel(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) case "beeper": new_asset = draw_beeper(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) logger.debug(f"Moved: {asset[2]} by {x} horizontally, {y} vertically: {new_asset}") return new_asset # Change the orientation of a passed asset def rotate_asset(asset, direction): # Relative turn if direction == "right" or direction == "left": if asset[1][4] == "east": # East if direction == "right": asset[1][4] = "south" if direction == "left": asset[1][4] = "north" elif asset[1][4] == "east-flipped": if direction == "right": asset[1][4] = "south-flipped" if direction == "left": asset[1][4] = "north-flipped" elif asset[1][4] == "north": # North if direction == "right": asset[1][4] = "east" if direction == "left": asset[1][4] = "west" elif asset[1][4] == "north-flipped": if direction == "right": asset[1][4] = "east-flipped" if direction == "left": asset[1][4] = "west-flipped" elif asset[1][4] == "west": # West if direction == "right": asset[1][4] = "north" if direction == "left": asset[1][4] = "south" elif asset[1][4] == "west-flipped": if direction == "right": asset[1][4] = "north-flipped" if direction == "left": asset[1][4] = "south-flipped" elif asset[1][4] == "south": # South if direction == "right": asset[1][4] = "west" if direction == "left": asset[1][4] = "east" elif asset[1][4] == "south-flipped": if direction == "right": asset[1][4] = "west-flipped" if direction == "left": asset[1][4] = "east-flipped" # Absolute rotation elif direction == "east" or direction == "east-flipped" or direction == "north" or direction == "north-flipped" or direction == "west" or direction == "west-flipped" or direction == "south" or direction == "south-flipped": asset[1][4] = direction # Syntax error else: logger.error(f"Invalid rotation direction: {direction}") # Repalce asset erase_asset(asset) match asset[2]: case "karel": new_asset = draw_karel(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) case "beeper": new_asset = draw_beeper(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) logger.debug(f"Rotated {asset[2]} by {direction} as {asset[1][4]}: {new_asset}") return new_asset # Recolour a passed asset on the canvas """ Waiting for Stanford to fix a bug in the environment CIP bug: https://codeinplace.stanford.edu/cip6/report?post=c36ed931-3019-4830-8ba6-655c1a513471 def recolour_karel(canvas, karel, colour:str="black", background:str="white"): for name, shape in karel[0].items(): # Loop over Karel dict if name.endswith("_fill"): # Background shape canvas.set_color(shape, background) canvas.set_outline_color(shape, background) else: # Foreground shape if name.endswith("_line") or name.endswith("_corner") or name == "mouth": canvas.set_color(shape, colour) elif name == "eye": #canvas.set_color(shape, "transparent") canvas.set_outline_color(shape, colour) else: # Legs and feet canvas.set_color(shape, colour) canvas.set_outline_color(shape, colour) """ def recolour_asset(asset, colour:str="black", background:str="white"): # Random colours if colour == "random": colour = generate_random_colour() if background == "random": background = generate_random_colour() # Update colours in list asset[1][5] = colour asset[1][6] = background # Replace asset erase_asset(asset) match asset[2]: case "karel": new_asset = draw_karel(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) case "beeper": new_asset = draw_beeper(asset[1][0], asset[1][1], asset[1][2], asset[1][3], asset[1][4], asset[1][5], asset[1][6], asset[1][7]) logger.debug(f"Re-coloured Karel {asset[2]} with {colour} and {background}: {new_asset}") return new_asset # Draw a random asset on the canvas def generate_random_asset(canvas, karel_prevelance:int="3"): asset_type = random.randint(1, karel_prevelance) match asset_type: case 1: asset = generate_random_beeper(canvas) case _: asset = generate_random_karel(canvas) return asset # Draw a random asset outside the canvas def generate_outofbounds_random_asset(canvas, side:str, karel_prevelance:int=6): asset_type = random.randint(1, karel_prevelance) match asset_type: case 1: asset = generate_outofbounds_random_beeper(canvas, side) case _: asset = generate_outofbounds_random_karel(canvas, side) return asset """ Karel functions """ # Draw a random Karel outside the canvas def generate_outofbounds_random_karel(canvas, side:str): size = determine_max_size(canvas) # Generate random coordinates (Outside the canvas geometry) match side: case "left": centre_x = int(0 - size / 2 - 1) centre_y = random.randint(int(size / 2), int(canvas.height - size / 2)) case "top": centre_x = random.randint(int(size / 2), int(canvas.height - size / 2)) centre_y = int(0 - size / 2 - 1) case "right": centre_x = int(0 + size / 2 + 1) centre_y = random.randint(int(size / 2), int(canvas.height - size / 2)) case "bottom": centre_x = random.randint(int(size / 2), int(canvas.height - size / 2)) centre_y = int(0 + size / 2 + 1) # Generate random values orientation = random.choice(["east", "east-flipped", "north", "north-flipped", "west", "west-flipped", "south", "south-flipped"]) transparent = random.choice((True, False, False)) return draw_karel(canvas, centre_x, centre_y, size, orientation, "random", "random", transparent) # Draw a random Karel on the canvas def generate_random_karel(canvas): size = determine_max_size(canvas) # Generate random values (Inside the canvas geometry) centre_x = random.randint(int(size / 2), int(canvas.width - size / 2)) centre_y = random.randint(int(size / 2), int(canvas.height - size / 2)) orientation = random.choice(["east", "east-flipped", "north", "north-flipped", "west", "west-flipped", "south", "south-flipped"]) transparent = random.choice((True, False, False, False)) return draw_karel(canvas, centre_x, centre_y, size, orientation, "random", "random", transparent) # Draw a Karel def draw_karel( canvas, centre_x:int=25, centre_y:int=25, size:int=50, orientation:str="east", colour:str="black", background:str="white", transparent:bool=False ): # Body constants MARGIN = size / 8 APPENDAGE_MULTIPLIER = MARGIN * 0.6 # Random colours if colour == "random": colour = generate_random_colour() if background == "random": background = generate_random_colour() ''' Flipper case In order to be able to flip Karel, the operands in the forumlas must be able to switch around. Orientations are in relation to the centre of Karel. ''' match orientation.lower(): case "east" | "south-flipped": left_operand = operator.sub # Left / Top top_operand = operator.sub # Top / Left right_operand = operator.add # Right / Bottom bottom_operand = operator.add # Bottom / Right case "east-flipped" | "south": left_operand = operator.sub # Left / Top top_operand = operator.add # Bottom / Right right_operand = operator.add # Right / Bottom bottom_operand = operator.sub # Top / Left case "west" | "north-flipped": left_operand = operator.add # Right top_operand = operator.add # Bottom right_operand = operator.sub # Left bottom_operand = operator.sub # Top case "west-flipped" | "north": left_operand = operator.add # Right / Bottom top_operand = operator.sub # Top / Left right_operand = operator.sub # Left / Top bottom_operand = operator.add # Bottom / Right """ Coords case X and Y need to be swapped in case of a North or South facing Karel, yet the original vlaues have to be returned. (To prevent diagonal relative translation) """ if orientation.lower() == "north" or orientation.lower() == "north-flipped" or orientation.lower() == "south" or orientation.lower() == "south-flipped": x = centre_y y = centre_x else: x = centre_x y = centre_y # Borders left = left_operand(x, size / 2) top = top_operand(y, size / 2) right = right_operand(x, size / 2) bottom = bottom_operand(y, size / 2) margin = size / 8 appendage_multiplier = margin * 0.6 # Body borders body_left = left_operand(x, size / 3) body_top = top body_right = right_operand(x, size / 3) body_bottom = bottom_operand(y, size / 2.6) eye_left = right_operand(body_left, margin) eye_top = bottom_operand(body_top, margin) eye_right = left_operand(body_right, margin) eye_bottom = top_operand(body_bottom, margin * 2) # Body coordinates top_left_corner = body_left, body_top left_diagonal_top = body_left, top_operand(body_bottom, margin) left_diagonal_bottom = eye_left, body_bottom right_diagonal_top = eye_right, body_top right_diagonal_bottom = body_right, bottom_operand(body_top, margin) bottom_right_corner = body_right, body_bottom mouth_left_corner = x, bottom_operand(eye_bottom, margin) mouth_right_corner = eye_right, mouth_left_corner[1] # Left appendage borders leftLeg_left = left_operand(body_left, margin) leftLeg_top = eye_bottom leftLeg_right = body_left leftLeg_bottom = bottom_operand(leftLeg_top, appendage_multiplier) leftFoot_left = leftLeg_left leftFoot_top = leftLeg_bottom leftFoot_right = right_operand(leftLeg_left, appendage_multiplier) leftFoot_bottom = bottom_operand(leftFoot_top, appendage_multiplier) # Right appendage borders rightLeg_left = x rightLeg_top = body_bottom rightLeg_right = right_operand(rightLeg_left, appendage_multiplier) rightLeg_bottom = bottom rightFoot_left = rightLeg_right rightFoot_top = top_operand(bottom, appendage_multiplier) rigthFoot_right = right_operand(rightLeg_right, appendage_multiplier) rightFoot_bottom = bottom # Draw Karel match orientation.lower(): case "east" | "east-flipped" | "west" | "west-flipped": if not transparent: # Draw meat filler = canvas.create_polygon( top_left_corner[0], top_left_corner[1], right_diagonal_top[0], right_diagonal_top[1], right_diagonal_bottom[0], right_diagonal_bottom[1], bottom_right_corner[0], bottom_right_corner[1], left_diagonal_bottom[0], left_diagonal_bottom[1], left_diagonal_top[0], left_diagonal_top[1], top_left_corner[0], eye_top, eye_left, eye_top, eye_left, eye_bottom, eye_left, eye_bottom, eye_right, eye_bottom, eye_right, eye_top, top_left_corner[0], eye_top, color = background, outline = background ) # Draw outlines top_line = canvas.create_line( top_left_corner[0], top_left_corner[1], right_diagonal_top[0], right_diagonal_top[1], colour ) top_corner = canvas.create_line( right_diagonal_top[0], right_diagonal_top[1], right_diagonal_bottom[0], right_diagonal_bottom[1], colour ) left_line = canvas.create_line( top_left_corner[0], top_left_corner[1], left_diagonal_top[0], left_diagonal_top[1], colour ) bottom_corner = canvas.create_line( left_diagonal_top[0], left_diagonal_top[1], left_diagonal_bottom[0], left_diagonal_bottom[1], colour ) bottom_line = canvas.create_line( left_diagonal_bottom[0], left_diagonal_bottom[1], bottom_right_corner[0], bottom_right_corner[1], colour ) right_line = canvas.create_line( right_diagonal_bottom[0], right_diagonal_bottom[1], bottom_right_corner[0], bottom_right_corner[1], colour ) left_appendage = canvas.create_polygon( leftLeg_left, leftLeg_top, leftLeg_right, leftLeg_top, leftLeg_right, leftLeg_bottom, leftFoot_right, leftFoot_top, leftFoot_right, leftFoot_bottom, leftFoot_left, leftFoot_bottom, color = colour, outline = colour ) right_appendage = canvas.create_polygon( rightLeg_left, rightLeg_top, rightLeg_right, rightLeg_top, rightFoot_left, rightFoot_top, rigthFoot_right, rightFoot_top, rigthFoot_right, rightFoot_bottom, rightLeg_left, rightFoot_bottom, color = colour, outline = colour ) eye = canvas.create_rectangle( eye_left, eye_top, eye_right, eye_bottom, "transparent", colour, ) mouth = canvas.create_line( mouth_left_corner[0], mouth_left_corner[1], mouth_right_corner[0], mouth_right_corner[1], colour ) # X and Y coordinates swapped for these orientations case "north" | "north-flipped" | "south" | "south-flipped": if not transparent: # Draw meat filler = canvas.create_polygon( top_left_corner[1], top_left_corner[0], right_diagonal_top[1], right_diagonal_top[0], right_diagonal_bottom[1], right_diagonal_bottom[0], #eye_bottom, right_diagonal_bottom[0], bottom_right_corner[1], bottom_right_corner[0], left_diagonal_bottom[1], left_diagonal_bottom[0], left_diagonal_top[1], left_diagonal_top[0], eye_top, top_left_corner[0], eye_top, eye_left, eye_bottom, eye_left, eye_bottom, eye_right, eye_top, eye_right, eye_top, top_left_corner[0], color = background, outline = background ) # Draw outlines top_line = canvas.create_line( top_left_corner[1], top_left_corner[0], right_diagonal_top[1], right_diagonal_top[0], colour ) top_corner = canvas.create_line( right_diagonal_top[1], right_diagonal_top[0], right_diagonal_bottom[1], right_diagonal_bottom[0], colour ) left_line = canvas.create_line( top_left_corner[1], top_left_corner[0], left_diagonal_top[1], left_diagonal_top[0], colour ) bottom_corner = canvas.create_line( left_diagonal_top[1], left_diagonal_top[0], left_diagonal_bottom[1], left_diagonal_bottom[0], colour ) bottom_line = canvas.create_line( left_diagonal_bottom[1], left_diagonal_bottom[0], bottom_right_corner[1], bottom_right_corner[0], colour ) right_line = canvas.create_line( right_diagonal_bottom[1], right_diagonal_bottom[0], bottom_right_corner[1], bottom_right_corner[0], colour ) left_appendage = canvas.create_polygon( leftLeg_top, leftLeg_left, leftLeg_top, leftLeg_right, leftLeg_bottom, leftLeg_right, leftFoot_top, leftFoot_right, leftFoot_bottom, leftFoot_right, leftFoot_bottom, leftFoot_left, color = colour, outline = colour ) right_appendage = canvas.create_polygon( rightLeg_top, rightLeg_left, rightLeg_top, rightLeg_right, rightFoot_top, rightFoot_left, rightFoot_top, rigthFoot_right, rightFoot_bottom, rigthFoot_right, rightFoot_bottom, rightLeg_left, color = colour, outline = colour ) eye = canvas.create_rectangle( eye_top, eye_right, eye_bottom, eye_left, "transparent", colour, ) mouth = canvas.create_line( mouth_left_corner[1], mouth_left_corner[0], mouth_right_corner[1], mouth_right_corner[0], colour ) # Return each object so it can later be altered/destroyed if transparent: shapes = { "top_line": top_line, "top_corner": top_corner, "left_line": left_line, "bottom_corner": bottom_corner, "bottom_line": bottom_line, "right_line": right_line, "left_appendage": left_appendage, "right_appendage": right_appendage, "eye": eye, "mouth": mouth } else: # Not transparent shapes = { "filler": filler, "top_line": top_line, "top_corner": top_corner, "left_line": left_line, "bottom_corner": bottom_corner, "bottom_line": bottom_line, "right_line": right_line, "left_appendage": left_appendage, "right_appendage": right_appendage, "eye": eye, "mouth": mouth } arguments = [canvas, centre_x, centre_y, size, orientation.lower(), colour, background, transparent] logger.debug(f"Created Karel: {shapes, arguments}") return [shapes, arguments, "karel"] """ Beeper functions Even though the beeper is symmetrical in two directions and rotating wont matter, it may be useful in case of moving in relation to orientation, perhaps in coordinated group movements with Karels. """ # Draw a random Karel outside the canvas def generate_outofbounds_random_beeper(canvas, side:str): size = determine_max_size(canvas) # Generate random coordinates (Outside the canvas geometry) match side: case "left": centre_x = int(0 - size / 2 - 1) centre_y = random.randint(int(size / 2), int(canvas.height - size / 2)) case "top": centre_x = random.randint(int(size / 2), int(canvas.height - size / 2)) centre_y = int(0 - size / 2 - 1) case "right": centre_x = int(0 + size / 2 + 1) centre_y = random.randint(int(size / 2), int(canvas.height - size / 2)) case "bottom": centre_x = random.randint(int(size / 2), int(canvas.height - size / 2)) centre_y = int(0 + size / 2 + 1) # Generate random values orientation = random.choice(["east", "east-flipped", "north", "north-flipped", "west", "west-flipped", "south", "south-flipped"]) transparent = random.choice((True, False, False)) return draw_beeper(canvas, centre_x, centre_y, size, orientation, "random", "random", transparent) def generate_random_beeper(canvas): size = determine_max_size(canvas) # Generate random values (Inside the canvas geometry) centre_x = random.randint(int(size / 2), int(canvas.width - size / 2)) centre_y = random.randint(int(size / 2), int(canvas.height - size / 2)) orientation = random.choice(["east", "east-flipped", "north", "north-flipped", "west", "west-flipped", "south", "south-flipped"]) transparent = random.choice((True, False, False, False)) return draw_beeper(canvas, centre_x, centre_y, size, orientation, "random", "random", transparent) def draw_beeper( canvas, centre_x:int=25, centre_y:int=25, size:int=50, orientation:str="east", colour:str="black", background:str="cyan", transparent:bool=False ): # Random colours if colour == "random": colour = generate_random_colour() if background == "random": background = generate_random_colour() # Borders left = centre_x - size / 2 top = centre_y - size / 2 right = centre_x + size / 2 bottom = centre_y + size / 2 # Body coordinates top_corner = centre_x, top left_corner = left, centre_y bottom_corner = centre_x, bottom right_corner = right, centre_y """ # Draw outlines north_west_line = canvas.create_line( top_corner[0], top_corner[1], left_corner[0], left_corner[1], colour ) south_west_line = canvas.create_line( left_corner[0], left_corner[1], bottom_corner[0], bottom_corner[1], colour ) south_east_line = canvas.create_line( right_corner[0], right_corner[1], bottom_corner[0], bottom_corner[1], colour ) north_east_line = canvas.create_line( top_corner[0], top_corner[1], right_corner[0], right_corner[1], colour ) """ # Draw insides filler = canvas.create_polygon( top_corner[0] + 1, top_corner[1] + 1, right_corner[0] - 1, right_corner[1] - 1, bottom_corner[0] - 1, bottom_corner[1] - 1, left_corner[0] + 1, left_corner[1] + 1, color = background, outline = colour ) # Return each object so it can later be altered/destroyed """ shapes = { "north_west_line": north_west_line, "south_west_line": south_west_line, "south_east_line": south_east_line, "north_east_line": north_east_line, "filler": filler } """ shapes = { "filler": filler } arguments = [canvas, centre_x, centre_y, size, orientation.lower(), colour, background, transparent] logger.debug(f"Created beeper: {shapes, arguments}") return [shapes, arguments, "beeper"]