""" ----------------------------------------- Project: Culinary Quest Standard: 91896 School: Tauranga Boys' College Author: Leighton Simmons Date: 29th April 2024 Python: 3.13.3 ----------------------------------------- """ # Import Libraries import pygame from pygame import mixer import os import csv import button import random # Initialize Libraries mixer.init() pygame.init() # Setup Screen --------------------------------------------------------------------------------- SCREEN_WIDTH = 800 SCREEN_HEIGHT = int(SCREEN_WIDTH * 0.8) screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption('Culinary Quest') icon_image = pygame.image.load('img/game_icon.png') pygame.display.set_icon(icon_image) #Set Framerate --------------------------------------------------------------------------------- clock = pygame.time.Clock() FPS = 60 #Define Game Variables ------------------------------------------------------------------------- GRAVITY = 0.75 ROWS = 16 COLS = 150 TILE_SIZE = SCREEN_HEIGHT // ROWS TILE_TYPES = 3 BUTTON_RESIZE_WIDTH = pygame.image.load('img/button/start_btn.png').get_width() // 12 BUTTON_RESIZE_HEIGHT = pygame.image.load('img/button/start_btn.png').get_height() // 12 food_interval = 2 item_gravity = 0 food_timer = 0 start_timer = None final_time = 0 high_score_time = 0 level = 1 start_game = False start_intro = False game_win = False elapsed_time = 0 food_amount = 0 poison_amount = 0 outline_enabled = False #Define Player Action Variables ---------------------------------------------------------------- moving_left = False moving_right = False # Load Audio ----------------------------------------------------------------------------------- jump_fx = pygame.mixer.Sound('audio/jump.wav') jump_fx.set_volume(0.05) # Load Images ---------------------------------------------------------------------------------- # Button Images start_img = pygame.transform.scale(pygame.image.load('img/button/start_btn.png'), (BUTTON_RESIZE_WIDTH, BUTTON_RESIZE_HEIGHT)).convert_alpha() exit_img = pygame.transform.scale(pygame.image.load('img/button/exit_btn.png'), (BUTTON_RESIZE_WIDTH, BUTTON_RESIZE_HEIGHT)).convert_alpha() restart_img = pygame.transform.scale(pygame.image.load('img/button/restart_btn.png'), (BUTTON_RESIZE_WIDTH, BUTTON_RESIZE_HEIGHT)).convert_alpha() # Background Images layer_1 = pygame.transform.scale(pygame.image.load('img/Background/1.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_2 = pygame.transform.scale(pygame.image.load('img/Background/2.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_3 = pygame.transform.scale(pygame.image.load('img/Background/3.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_4 = pygame.transform.scale(pygame.image.load('img/Background/4.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_5 = pygame.transform.scale(pygame.image.load('img/Background/5.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_6 = pygame.transform.scale(pygame.image.load('img/Background/6.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_7 = pygame.transform.scale(pygame.image.load('img/Background/7.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() layer_8 = pygame.transform.scale(pygame.image.load('img/Background/8.png'), (SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha() # Store Tiles In A List ----------------------------------------------------------------------- img_list = [] for x in range(TILE_TYPES): img = pygame.image.load(f'img/tile/{x}.png') img = pygame.transform.scale(img, (TILE_SIZE, TILE_SIZE)) img_list.append(img) # Define Colours ------------------------------------------------------------------------------ bg_image = pygame.image.load('img/background/bg.png').convert_alpha() bg_image = pygame.transform.scale(bg_image, (SCREEN_WIDTH, SCREEN_HEIGHT)) WHITE = (255, 255, 255) BLACK = (0, 0, 0) TEXT_FILL = (77, 0, 0) GRAY = (192, 192, 192) # Font & Title -------------------------------------------------------------------------------- font = pygame.font.Font('font/Ziplock.ttf', 64) small_font = pygame.font.Font('font/Ziplock.ttf', 16) text = 'Culinary Quest' win_text = 'You Win!' text_width = font.render(text, True, (WHITE)).get_width() win_text_width = font.render(win_text, True, (WHITE)).get_width() win_text_height = font.render(win_text, True, (WHITE)).get_height() win_text_x = (SCREEN_WIDTH // 2) - (win_text_width // 2) win_text_y = (SCREEN_HEIGHT // 2) - (win_text_height // 2) text_position = (SCREEN_WIDTH // 2) - (text_width // 2) _circle_cache = {} def _circlepoints(r): r = int(round(r)) if r in _circle_cache: return _circle_cache[r] x, y, e = r, 0, 1 - r _circle_cache[r] = points = [] while x >= y: points.append((x, y)) y += 1 if e < 0: e += 2 * y - 1 else: x -= 1 e += 2 * (y - x) - 1 points += [(y, x) for x, y in points if x > y] points += [(-x, y) for x, y in points if x] points += [(x, -y) for x, y in points if y] points.sort() return points # Outline Text Function --------------------------------------------------------------------- def render(text, font, gfcolor=(TEXT_FILL), ocolor=(BLACK), opx=3): textsurface = font.render(text, True, gfcolor).convert_alpha() w = textsurface.get_width() + 2 * opx h = font.get_height() osurf = pygame.Surface((w, h + 2 * opx)).convert_alpha() osurf.fill((0, 0, 0, 0)) surf = osurf.copy() osurf.blit(font.render(text, True, ocolor).convert_alpha(), (0, 0)) for dx, dy in _circlepoints(opx): surf.blit(osurf, (dx + opx, dy + opx)) surf.blit(textsurface, (opx, opx)) return surf def render_win_text(win_text, font, gfcolor=(TEXT_FILL), ocolor=(BLACK), opx=3): textsurface = font.render(text, True, gfcolor).convert_alpha() w = textsurface.get_width() + 2 * opx h = font.get_height() osurf = pygame.Surface((w, h + 2 * opx)).convert_alpha() osurf.fill((0, 0, 0, 0)) surf = osurf.copy() osurf.blit(font.render(win_text, True, ocolor).convert_alpha(), (0, 0)) for dx, dy in _circlepoints(opx): surf.blit(osurf, (dx + opx, dy + opx)) surf.blit(textsurface, (opx, opx)) return surf # Manage High Score Function ------------------------------------------------------------- def load_high_score(): if os.path.exists('high_score.txt'): # Open the high score .txt file with open('high_score.txt', 'r') as file: # Read the file high_score_str = file.read() if high_score_str.isdigit(): # If it is a number then return that number return int(high_score_str) else: return float('inf') # If there is no number than return infinity return float('inf') # If nothing is there then return infinity def save_high_score(final_time): with open('high_score.txt', 'w') as f: # Open the high score .txt file with the ability to write f.write(str(final_time)) # Write the high score # Draw Background ------------------------------------------------------------------------ def draw_bg(): screen.blit(bg_image, (0, 0)) width = SCREEN_WIDTH for x in range(5): screen.blit(layer_1, ((x * width), 0)) screen.blit(layer_2, ((x * width), 0)) screen.blit(layer_3, ((x * width), 0)) screen.blit(layer_4, ((x * width), 0)) screen.blit(layer_5, ((x * width), 0)) screen.blit(layer_6, ((x * width), 0)) screen.blit(layer_7, ((x * width), 0)) screen.blit(layer_8, ((x * width), 0)) # Crop Animation Frames For Transparent Pixels ------------------------------------------ # There are three different methods for cropping transparent pixels in this file, they should work best for their specific purpose def crop_image(image): mask = pygame.mask.from_surface(image) non_transparent_points = mask.outline() if non_transparent_points: # Keep visible pixels min_x, min_y = min(non_transparent_points, key=lambda p: p[0]), min(non_transparent_points, key=lambda p: p[1]) max_x, max_y = max(non_transparent_points, key=lambda p: p[0]), max(non_transparent_points, key=lambda p: p[1]) width = max_x[0] - min_x[0] + 1 height = max_y[1] - min_y[1] + 1 # Check if width and height are valid if width > 0 and height > 0: cropped_image = image.subsurface((min_x[0], min_y[1], width, height)).copy() return cropped_image else: return image else: return image # Function To Reset Level -------------------------------------------------------------- def reset_level(): global level enemy_group.empty() exit_group.empty() food_group.empty() poison_group.empty() #create empty tile list data = [] for row in range(ROWS): r = [-1] * COLS data.append(r) level = 1 return data # Player Class -------------------------------------------------------------------------- class Player(pygame.sprite.Sprite): def __init__(self, x, y, scale, speed, health=1): pygame.sprite.Sprite.__init__(self) self.alive = True self.speed = speed self.health = health self.max_health = self.health self.direction = 1 self.vel_y = 0 self.jump = False self.in_air = True self.flip = False self.animation_list = [] self.frame_index = 0 self.action = 0 self.update_time = pygame.time.get_ticks() self.moving_left = False self.moving_right = False self.death_sound_played = False self.spawned = False self.food_collected = 0 # Load all images for the player animation_types = ['Idle', 'Run', 'Jump', 'Death', 'Win'] # Load the five animation types (Only three are used but leaving room for expansion) for animation in animation_types: temp_list = [] num_of_frames = len(os.listdir(f'img/player/{animation}')) # Get the amount of frames for each type for i in range(num_of_frames): # Load, scale, and crop each image frame img = pygame.image.load(f'img/player/{animation}/{i}.png').convert_alpha() img = pygame.transform.scale(img, (int(img.get_width() * scale), int(img.get_height() * scale))) cropped_img = crop_image(img) temp_list.append(cropped_img) self.animation_list.append(temp_list) # Create player rect self.image = self.animation_list[self.action][self.frame_index] self.rect = self.image.get_rect() self.rect.center = (x, y) self.width = self.image.get_width() self.height = self.image.get_height() def update(self): # Animation function self.update_animation() # Call the update_animation method self.check_alive() # Check if the player is alive self.level_finish() # Check if the level has been completed food_collisions = pygame.sprite.spritecollide(self, food_group, True) # Detect food collisions poison_collisions = pygame.sprite.spritecollide(self, poison_group, True) # Detect poison collisions for food in food_collisions: self.food_collected += 1 # Increase food collected by one for poison in poison_collisions: self.health -= 1 # Decrease health by one / Kill player if outline_enabled: # If hitboxes are enabled then draw the hitbox pygame.draw.rect(screen, (255, 255, 255), self.rect, 1) def level_finish(self): # Detect if the level has been completed global level if self.food_collected >= 10: # Check if player collected 10 food items # Reset level and transition to the next level level += 1 self.food_collected = 0 def update_action(self, new_action): # Check the update for the player animation if new_action != self.action: # Check if the new action is different from the current action self.action = new_action # Update the animation list based on the new action self.frame_index = 0 self.update_time = pygame.time.get_ticks() def update_animation(self): # Update animation ANIMATION_COOLDOWN_IDLE = 200 # Increase ANIMATION_COOLDOWN for idle action so it looks more natural ANIMATION_COOLDOWN_MOVEMENT = 100 # Default value for movement # Update image depending on current action self.image = self.animation_list[self.action][self.frame_index] # Choose appropriate cooldown based on action if self.action == 0: # Idle action animation_cooldown = ANIMATION_COOLDOWN_IDLE else: animation_cooldown = ANIMATION_COOLDOWN_MOVEMENT if self.action == 3: # Death animation self.image = self.animation_list[self.action][self.frame_index] if self.frame_index == len(self.animation_list[self.action]) - 1: # Detect for the final frame of death animation # Update the rect size to 20x15 so it can fall to the floor self.rect.width = 20 self.rect.height = 15 while True: # Make the player fall to the ground until it collides with the ground self.rect.y += 1 for tile in world.obstacle_list: if tile[1].colliderect(self.rect): self.rect.bottom = tile[1].top break else: continue break # Check if enough time has passed since the last update if pygame.time.get_ticks() - self.update_time > animation_cooldown: self.update_time = pygame.time.get_ticks() self.frame_index += 1 # Update frame # If the animation has reached the end, reset it if self.frame_index >= len(self.animation_list[self.action]): if self.action == 3: # Death animation self.frame_index = len(self.animation_list[self.action]) - 1 else: self.frame_index = 0 def check_alive(self): # Check if the player is alive if self.health <= 0: self.health = 0 # Kill player self.speed = 0 # Stop the player from moving self.alive = False self.update_action(3) # Start death animation def move(self, moving_left, moving_right): # The player movement function # Reset movement variables dx = 0 dy = 0 # Assign movement variables if moving left or right if moving_left: dx = -self.speed self.flip = True # Flip the player so they're facing left self.direction = -1 if moving_right: dx = self.speed self.flip = False # Unflip the player so they're facing right self.direction = 1 # Jump if self.jump and not self.in_air: # Only jump if not already in the air self.vel_y = -11 self.jump = False self.in_air = True # Apply gravity self.vel_y += GRAVITY if self.vel_y > 10: self.vel_y = 10 # Cap the maximum vertical velocity dy += self.vel_y # Check for collision in the x direction for tile in world.obstacle_list: if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # Check for collision in the y direction for tile in world.obstacle_list: if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): if self.vel_y < 0: # Jumping self.vel_y = 0 dy = tile[1].bottom - self.rect.top elif self.vel_y >= 0: # Falling self.vel_y = 0 self.in_air = False dy = tile[1].top - self.rect.bottom if dy == 0: self.rect.y += 1 # Move player down slightly to avoid being stuck on the tile (doesn't really need this but for consistency) # Check if fallen off the map if self.rect.bottom > SCREEN_HEIGHT: self.health = 0 # Kill the player # Check if going off the edges of the screen if self.rect.left + dx < 0 or self.rect.right + dx > SCREEN_WIDTH: dx = 0 # Update rect position self.rect.x += dx self.rect.y += dy # Stop jumping if already in the air if self.in_air: self.jump = False def draw(self): # Draw the player if self.action == 3: # Death animation screen.blit(pygame.transform.flip(self.image, self.flip, False), self.rect.topleft) else: # Other animations screen.blit(pygame.transform.flip(self.image, self.flip, False), self.rect.topleft) # Draw The Amount Of Food Collected ----------------------------------------------------- def draw_food_collected(): text = f'Food Collected: {player.food_collected} out of 10' text_surface = small_font.render(text, True, (BLACK)) screen.blit(text_surface, (10, 10)) # Draw the time until more food spawns -------------------------------------------------- def draw_food_timer(): time_until_food = round(food_interval - food_timer, 1) timer_text = small_font.render(f'Next food in: {time_until_food}', True, (BLACK)) screen.blit(timer_text, (SCREEN_WIDTH - 150, 10)) # Draw Whatever Level Player Is On ------------------------------------------------------ def draw_level_text(): level_text = small_font.render(f'Level {level}', True, (BLACK)) text_width = level_text.get_width() screen.blit(level_text, (SCREEN_WIDTH // 2 - text_width // 2, 10)) # Draw The High Score ------------------------------------------------------------------- def draw_high_score(): high_score_time = load_high_score() # Load the high score from the .txt file if high_score_time <= 0 or not isinstance(high_score_time, (int, float)): # If the high score value is 0 or less or not a number high_score_text = small_font.render('High score: Not Available', True, (BLACK)) else: minutes = high_score_time // 60000 # Convert milliseconds to minutes seconds = (high_score_time % 60000) // 1000 # Convert milliseconds to seconds if minutes != minutes or seconds != seconds: # Check for nan high_score_text = small_font.render('High score: Not Available', True, (BLACK)) else: high_score_text = small_font.render(f'High score: {minutes} minutes and {seconds} seconds', True, (BLACK)) # If everything checks out then draw it screen.blit(high_score_text, (SCREEN_WIDTH - high_score_text.get_width() - 10, SCREEN_HEIGHT - 30)) # Draw The Current Score ---------------------------------------------------------------- def display_score(): global final_time if start_timer is not None and not game_win and player.alive: # If game is not won and start_timer is initialized elapsed_time = pygame.time.get_ticks() - start_timer minutes = elapsed_time // 60000 # Convert milliseconds to minutes seconds = (elapsed_time % 60000) // 1000 # Convert milliseconds to seconds score_text = small_font.render(f'Total time: {minutes} minutes and {seconds} seconds', True, BLACK) screen.blit(score_text, (10, SCREEN_HEIGHT - score_text.get_height() - 10)) else: if final_time == 0 and game_win: if start_timer is not None: final_time = pygame.time.get_ticks() - start_timer if final_time < load_high_score(): save_high_score(final_time) final_time_text = small_font.render(f'Total time: {final_time // 60000} minutes and {(final_time % 60000) // 1000} seconds', True, BLACK) screen.blit(final_time_text, (10, SCREEN_HEIGHT - final_time_text.get_height() - 10)) # Draw The Controls On The Main Menu ---------------------------------------------------- def draw_controls(): movement_text = small_font.render('Movement: Left = A, Right = D, Jump = W', True, GRAY) hitbox_text = small_font.render('To see the items clearer, use G to enable hitboxes', True, GRAY) instructions_text = small_font.render('Collect 10 food items to get to the next level, avoid any poison vials', True, GRAY) screen.blit(movement_text, (10, SCREEN_HEIGHT - movement_text.get_height() - 40)) screen.blit(hitbox_text, (10, SCREEN_HEIGHT - hitbox_text.get_height() - 20)) screen.blit(instructions_text, (10, SCREEN_HEIGHT - instructions_text.get_height())) # Check What The Current Level Is And Adjust Food/Poison Amount ------------------------- def check_completion(level): global game_win global item_gravity global food_interval global food_amount global poison_amount if level == 5 and player.food_collected == 10: # If Game Has Been Completed game_win = True if game_win == True: # Stop everything (except food falling because it looks cool) and draw win text screen.blit(render(win_text, font), (win_text_x, win_text_y)) level = 5 player.food_collected = 0 food_interval = 0 player.timer_running = False if level == 1: # Level 1 spawn rates item_gravity = 0.2 food_amount = 10 poison_amount = 0 elif level == 2: # Level 2 spawn rates item_gravity = 0.4 food_amount = 8 poison_amount = 2 else: # Every other level item_gravity = 0.6 food_amount = 5 poison_amount = 5 # Generate World ------------------------------------------------------------------------ class World(): def __init__(self): self.obstacle_list = [] def process_data(self, data): # Load the data from the level sheet self.level_length = len(data[0]) player = None for y, row in enumerate(data): for x, tile in enumerate(row): if tile >= 0 and tile < len(img_list): img = img_list[tile] img_rect = img.get_rect() img_rect.x = x * TILE_SIZE img_rect.y = y * TILE_SIZE tile_data = (img, img_rect) if tile >= 0 and tile <= 2: self.obstacle_list.append(tile_data) elif tile == 3: player = Player(x * TILE_SIZE, y * TILE_SIZE, 2, 5) print('player added') # Debugging: Check if the player is added if player is None: # I had an issue where the code detected the world tiles like ground and edges but not the player and I couldn't find what was wrong player = Player(50, 600, 2, 5) return player def draw(self): # Draw the player as per data sheet coords (fixed but the debugging above) for tile in self.obstacle_list: screen.blit(tile[0], tile[1]) # Create Screen Fade -------------------------------------------------------------------- class ScreenFade: def __init__(self, direction, colour, speed): self.direction = direction self.colour = colour self.speed = speed self.fade_counter = 0 def fade(self): # 0% to 100% fade fade_complete = False self.fade_counter += self.speed fade_surface = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) fade_surface.fill(self.colour) if self.direction == 1: # whole screen fade fade_surface.set_alpha(255 - self.fade_counter) elif self.direction == 2: # vertical screen fade down fade_surface.set_alpha(self.fade_counter) screen.blit(fade_surface, (0, 0)) if self.fade_counter >= 255: fade_complete = True return fade_complete # Create Screen Fades ------------------------------------------------------------------- intro_fade = ScreenFade(1, (BLACK), 4) # Transparent to black fade death_fade = ScreenFade(1, (WHITE), 2.5) # Quick transparent to white fade # Create Buttons ------------------------------------------------------------------------ exit_menu_button = button.Button(SCREEN_WIDTH // 4 * 3 - exit_img.get_width() * 0.5, SCREEN_HEIGHT // 1.4, exit_img, 1) exit_game_button = button.Button(SCREEN_WIDTH // 2 - exit_img.get_width() * 0.5, SCREEN_HEIGHT // 2 + exit_img.get_width() * 0.5, exit_img, 1) start_button = button.Button(SCREEN_WIDTH // 4 - start_img.get_width() * 0.5, SCREEN_HEIGHT // 1.4, start_img, 1) restart_button = button.Button(SCREEN_WIDTH // 2 - restart_img.get_width() * 0.5, SCREEN_HEIGHT // 2 - restart_img.get_width() * 0.5, restart_img, 1) # Create Sprite Groups ------------------------------------------------------------------ enemy_group = pygame.sprite.Group() exit_group = pygame.sprite.Group() food_group = pygame.sprite.Group() poison_group = pygame.sprite.Group() # Create Empty Tile List ---------------------------------------------------------------- world_data = [] for row in range(ROWS): r = [-1] * COLS world_data.append(r) # Load In Level Data And Create World --------------------------------------------------- with open('levels/level_data.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') for x, row in enumerate(reader): for y, tile in enumerate(row): world_data[x][y] = int(tile) world = World() player = world.process_data(world_data) # Falling Food Creation ----------------------------------------------------------------- class Food(pygame.sprite.Sprite): def __init__(self, x, y, item_gravity): pygame.sprite.Sprite.__init__(self) food_num = random.randint(0, 46) self.scale = 1.3 self.image = pygame.image.load(f'img/food/{food_num}.png') # Crop the image for transparent pixels self.mask = pygame.mask.from_surface(self.image) # Get a mask of the food image self.rect = self.mask.get_bounding_rects()[0] # Get the first rect of the list self.rect.center = (x, y) self.vel_y = 0 self.item_gravity = item_gravity def draw(self): # Draw food screen.blit(self.cropped_image, self.rect) def update(self): # Update every frame self.vel_y += self.item_gravity # Add gravity self.rect.y += self.vel_y # Move rect if outline_enabled: # Draw rect pygame.draw.rect(screen, (255, 0, 0), self.rect, 1) # Check collision with tiles in the platform for tile in world.obstacle_list: if tile[1].colliderect(self.rect): self.kill() # Kill the fruit if it collides with a platform tile if self.rect.top > SCREEN_HEIGHT: # If it falls off the map somehow self.kill() # Falling Poison ----------------------------------------------------------------------- class Poison(pygame.sprite.Sprite): def __init__(self, x, y, item_gravity): pygame.sprite.Sprite.__init__(self) poison_num = random.randint(0, 2) self.scale = 0.6 self.image = pygame.image.load(f'img/poison/{poison_num}.png') # Crop the image for transparent pixels self.bounding_rect = self.image.get_bounding_rect() # Get the bounding rect for the poison image self.cropped_image = pygame.Surface((self.bounding_rect.width, self.bounding_rect.height), pygame.SRCALPHA) # Create a new surface and assign it to the cropped image self.cropped_image.blit(self.image, (0, 0), self.bounding_rect) # Draw the new cropped image self.cropped_image = pygame.transform.scale(self.cropped_image, (int(self.cropped_image.get_width()) * self.scale, int(self.cropped_image.get_height()) * self.scale)) # Scale the new image self.rect = self.cropped_image.get_rect() # Create new rect based on the cropped image self.rect.center = (x, y) self.image = self.cropped_image # Assign the cropped image to the main image variable self.vel_y = 0 self.item_gravity = item_gravity def draw(self): # Draw poison screen.blit(self.image, self.rect) def update(self): # Update every frame self.vel_y += self.item_gravity self.rect.y += self.vel_y if outline_enabled: # Draw hitbox pygame.draw.rect(screen, (0, 0, 255), self.rect, 1) for tile in world.obstacle_list: # If collides with floor if tile[1].colliderect(self.rect): self.kill() if self.rect.top > SCREEN_HEIGHT: # If somehow falls of the screen self.kill() # Spawn Food -------------------------------------------------------------------------- def spawn_food(): x = random.randint(10, SCREEN_WIDTH - TILE_SIZE - 10) # Random x position y = random.randint(0, SCREEN_HEIGHT // 3 - TILE_SIZE) # Random y position food = Food(x, y, item_gravity) # Create a Food object food_group.add(food) # Add the food to the group # Spawn Poison ------------------------------------------------------------------------ def spawn_poison(): if not game_win: # Don't spawn food if the game is over x = random.randint(10, SCREEN_WIDTH - TILE_SIZE - 10) # Random x position y = random.randint(0, SCREEN_HEIGHT // 3 - TILE_SIZE) # Random y position poison = Poison(x, y, item_gravity) # Create a Food object poison_group.add(poison) # Add the food to the group # Main Run Loop ----------------------------------------------------------------------- run = True while run: clock.tick(FPS) if not start_game: # If in the main menu screen.blit(bg_image, (0, 0)) # Draw background screen.blit(render(text, font), (text_position, SCREEN_HEIGHT // 5)) # Draw the title text with the outline method draw_controls() if start_button.draw(screen): # If the start button is clicked start_game = True start_intro = True # Play the start fade start_timer = pygame.time.get_ticks() # Start the game timer if exit_menu_button.draw(screen): # If exit button is clicked run = False else: # When the game starts draw_bg() # Draw the background layers (Only needs one layer since it's stationary but if I decide to do something with it) world.draw() # Draw the tiles if player.alive: food_timer += 1 / FPS # Increase the timer if food_timer >= food_interval: # Spawn food and poison every time the timer gets to 2 seconds for i in range(food_amount): spawn_food() for i in range(poison_amount): spawn_poison() food_timer = 0 # Reset the timer food_group.draw(screen) # Draw the food on the main screen food_group.update() # Call the food update method poison_group.draw(screen) # Draw the poison on the main screen poison_group.update() # Call the poison update method player.update() # # Call the player update method player.draw() # Draw the player draw_food_collected() # Draw the food collected draw_food_timer() # Draw the food timer draw_high_score() # Draw the high score draw_level_text() # Draw the current level display_score() # Draw the current score check_completion(level) # Check if the level has been completed exit_group.update() # Check if the exit button has been clicked exit_group.draw(screen) # Draw the exit button on the screen if start_intro: # Only play the start fade once if intro_fade.fade(): start_intro = False intro_fade.fade_counter = 0 if player.alive: # While the player is alive if player.in_air: player.update_action(2) # Play jump animation elif moving_left or moving_right: player.update_action(1) # Play running animation else: player.update_action(0) # Play death animation level_complete = player.move(moving_left, moving_right) else: if death_fade.fade(): # After the death animation plays if exit_game_button.draw(screen): # If the exit button is clicked run = False if restart_button.draw(screen): # If the restart button is clicked death_fade.fade_counter = 0 world_data = reset_level() start_timer = pygame.time.get_ticks() elapsed_time = 0 final_time = 0 food_timer = 0 with open('levels/level_data.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') for x, row in enumerate(reader): for y, tile in enumerate(row): world_data[x][y] = int(tile) world = World() player = world.process_data(world_data) for event in pygame.event.get(): # Detect keyboard or system clicks if event.type == pygame.QUIT: # When the exit button on the game window is clicked run = False if event.type == pygame.KEYDOWN: # Events for key presses if event.key == pygame.K_ESCAPE: # Escape key run = False if event.key == pygame.K_r and not game_win: # R key to restart the game (Reset everything) death_fade.fade_counter = 0 world_data = reset_level() start_timer = pygame.time.get_ticks() elapsed_time = 0 final_time = 0 food_timer = 0 with open(f'levels/level_data.csv', newline='') as csvfile: reader = csv.reader(csvfile, delimiter=',') for x, row in enumerate(reader): for y, tile in enumerate(row): world_data[x][y] = int(tile) world = World() player = world.process_data(world_data) if event.type == pygame.KEYDOWN: # Events for key presses if event.key == pygame.K_a: # A key / Left moving_left = True if event.key == pygame.K_d: # D key / Right moving_right = True if event.key == pygame.K_SPACE and player.alive: # Space key / Jump player.jump = True jump_fx.play() if event.key == pygame.K_f: # For testing but also if you want to cheat / Spawns 10 food for i in range(10): spawn_food() if event.key == pygame.K_p: # For testing but also if you want to die faster / Spawns 3 food for i in range(3): spawn_poison() if event.key == pygame.K_g: # Easy mode / If G key / Enabled outline of player, food, poison outline_enabled = not outline_enabled if event.type == pygame.KEYUP: # Events for key release if event.key == pygame.K_a: # A key / Stop moving left moving_left = False if event.key == pygame.K_d: # D key / Stop moving right moving_right = False pygame.display.update() pygame.quit()