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 """ 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: {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) 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]) logger.debug(f"Moved {asset} by {x} horizontally, {y} vertically, to {new_asset}") return new_asset # Move a passed asset in relation to her orientation def orientation_move_asset(asset, direction, amount): # Direction translation # East if asset[1][4].startswith("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 # North if asset[1][4].startswith("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 # West if asset[1][4].startswith("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 # South if asset[1][4].startswith("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) new_karel = 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]) logger.debug(f"Moved: {asset} {direction} by {amount} as {new_karel}") return new_karel # Move a passed asset to new coordinates def absolute_move_asset(asset, x, y): # Update coordinates in list asset[1][1] = x asset[1][2] = y # Replace asset erase_asset(asset) new_karel = 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]) logger.debug(f"Moved: {asset} by {x} horizontally, {y} vertically, to {new_karel}") return new_karel # Change the orientation of a passed asset def rotate_asset(asset, direction): # Relative turn if direction == "right" or direction == "left": # East if asset[1][4] == "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" # North elif asset[1][4] == "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" # West elif asset[1][4] == "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" # South elif asset[1][4] == "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) new_karel = 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]) logger.debug(f"Rotated Karel: {asset} by {direction} as {asset[1][4]} to {new_karel}") return new_karel # 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) new_karel = 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]) logger.debug(f"Re-coloured Karel: {asset} with {colour} and {background} to {new_karel}") return new_karel # Draw a random asset on the canvas def generate_random_asset(canvas): pass """ Karel functions """ # Draw a random Karel outside the canvas def generate_outofbounds_random_karel(canvas, side): # Base the size on the shortest pane if canvas.width < canvas.height: size = random.randint(1, canvas.width) else: size = random.randint(1, canvas.height) # 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): # Base the size on the shortest pane if canvas.width < canvas.height: size = random.randint(1, canvas.width) else: size = random.randint(1, canvas.height) # 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: # Meat top_fill = canvas.create_rectangle( top_left_corner[0], top_left_corner[1], right_diagonal_top[0], eye_top, background, background ) top_corner_fill = canvas.create_polygon( right_diagonal_top[0] , right_diagonal_top[1], right_diagonal_bottom[0], right_diagonal_bottom[1], eye_right, eye_top, color = background, outline = background ) left_fill = canvas.create_rectangle( top_left_corner[0], eye_top, eye_left, left_diagonal_top[1], background, background ) right_fill = canvas.create_rectangle( eye_right, eye_top, right_diagonal_bottom[0], eye_bottom, background, background ) bottom_fill = canvas.create_rectangle( eye_left, eye_bottom, bottom_right_corner[0], bottom_right_corner[1], background, background ) bottom_corner_fill = canvas.create_polygon( left_diagonal_top[0], left_diagonal_top[1], eye_left, left_diagonal_top[1], left_diagonal_bottom[0], left_diagonal_bottom[1], color = background, outline = background ) # 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_leg = canvas.create_rectangle( leftLeg_left, leftLeg_top, leftLeg_right, leftLeg_bottom, colour, colour ) left_foot = canvas.create_rectangle( leftFoot_left, leftFoot_top, leftFoot_right, leftFoot_bottom, colour, colour ) right_leg = canvas.create_rectangle( rightLeg_left, rightLeg_top, rightLeg_right, rightLeg_bottom, colour, colour ) right_foot = canvas.create_rectangle( rightFoot_left, rightFoot_top, rigthFoot_right, rightFoot_bottom, colour, 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 ) case "north" | "north-flipped" | "south" | "south-flipped": if not transparent: # Meat top_fill = canvas.create_rectangle( top_left_corner[1], # Top Y right_diagonal_top[0], # Right X eye_top, # Bottom Y top_left_corner[0], # Left X background, background ) top_corner_fill = canvas.create_polygon( right_diagonal_top[1] , right_diagonal_top[0], right_diagonal_bottom[1], right_diagonal_bottom[0], eye_top, eye_right, color = background, outline = background ) left_fill = canvas.create_rectangle( eye_top, eye_left, left_diagonal_top[1], top_left_corner[0], background, background ) right_fill = canvas.create_rectangle( eye_top, right_diagonal_bottom[0], eye_bottom, eye_right, background, background ) bottom_fill = canvas.create_rectangle( eye_bottom, bottom_right_corner[0], bottom_right_corner[1], eye_left, background, background ) bottom_corner_fill = canvas.create_polygon( left_diagonal_top[1], left_diagonal_top[0], left_diagonal_top[1], eye_left, left_diagonal_bottom[1], left_diagonal_bottom[0], color = background, outline = background ) # 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_leg = canvas.create_rectangle( leftLeg_top, leftLeg_right, leftLeg_bottom, leftLeg_left, colour, colour ) left_foot = canvas.create_rectangle( leftFoot_top, leftFoot_right, leftFoot_bottom, leftFoot_left, colour, colour ) right_leg = canvas.create_rectangle( rightLeg_top, rightLeg_right, rightLeg_bottom, rightLeg_left, colour, colour ) right_foot = canvas.create_rectangle( rightFoot_top, rigthFoot_right, rightFoot_bottom, rightFoot_left, colour, 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_leg": left_leg, "left_foot": left_foot, "right_foot": right_foot, "right_leg": right_leg, "right_foot": right_foot, "eye": eye, "mouth": mouth } else: # Not transparent shapes = { "top_fill": top_fill, "top_corner_fill": top_corner_fill, "left_fill": left_fill, "right_fill": right_fill, "bottom_fill": bottom_fill, "bottom_corner_fill": bottom_corner_fill, "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_leg": left_leg, "left_foot": left_foot, "right_foot": right_foot, "right_leg": right_leg, "right_foot": right_foot, "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]