""" -------------------------------------------- Project: Platformer Game Game Standard: 91896 (AS2.7) School: Tauranga Boys' College Author: Tama Colomer de Beer Date: April 2024 Python: 3.9.13 -------------------------------------------- """ #import statements import os import json import random import time import math import pygame from os import listdir from os.path import isfile, join import button #Initilize pygame pygame.init() #Set caption for the game pygame.display.set_caption("Platformer") #Define global variables WIDTH, HEIGHT = 1000, 800 FPS = 60 PLAYER_VEL = 5 BLOCK_SIZE = 96 FIRE_WIDTH = 16 FIRE_HEIGHT = 32 #set the window width and height window = pygame.display.set_mode((WIDTH, HEIGHT)) # Load jump sound JUMP_SOUND = pygame.mixer.Sound(join("assets", "jump.mp3")) # Load button images start_img = pygame.image.load(join("assets", 'start_btn.png')).convert_alpha() exit_img = pygame.image.load(join("assets", 'exit.png')).convert_alpha() # Create button instances start_button = button.Button(150, 400, start_img, 0.9) exit_button = button.Button(600, 405, exit_img, 0.9) # Load button images for restart and quit restart_img = pygame.image.load(join("assets", 'restart_btn.png')).convert_alpha() quit_img = pygame.image.load(join("assets", 'exit_btn.png')).convert_alpha() # Create button instances restart_button = button.Button(WIDTH // 2 - 150, HEIGHT // 2, restart_img, 0.9) quit_button = button.Button(WIDTH // 2 + 50, HEIGHT // 2, quit_img, 0.9) # Load fonts custom_font_path = join("assets", "font.ttf") score_font = pygame.font.Font(custom_font_path, 25) level_font = pygame.font.Font(custom_font_path, 25) timer_font = pygame.font.Font(custom_font_path, 25) def flip(sprites): # Flips the sprite images for directional animation return [pygame.transform.flip(sprite, True, False) for sprite in sprites] def load_sprite_sheets(dir1, dir2, width, height, direction=False): # Loads sprite sheets and organizes them into a dictionary path = join("assets", dir1, dir2) images = [f for f in listdir(path) if isfile(join(path, f))] all_sprites = {} for image in images: sprite_sheet = pygame.image.load(join(path, image)).convert_alpha() sprites = [] for i in range(sprite_sheet.get_width() // width): surface = pygame.Surface((width, height), pygame.SRCALPHA, 32) rect = pygame.Rect(i * width, 0, width, height) surface.blit(sprite_sheet, (0, 0), rect) sprites.append(pygame.transform.scale2x(surface)) if direction: all_sprites[image.replace(".png", "") + "_right"] = sprites all_sprites[image.replace(".png", "") + "_left"] = flip(sprites) else: all_sprites[image.replace(".png", "")] = sprites return all_sprites def get_block(size): # Returns a block image of the specified size path = join("assets", "Terrain", "Terrain.png") image = pygame.image.load(path).convert_alpha() surface = pygame.Surface((size, size), pygame.SRCALPHA, 32) rect = pygame.Rect(96, 0, size, size) surface.blit(image, (0, 0), rect) return pygame.transform.scale2x(surface) def save_high_score(elapsed_time): high_score_file = 'high_score.json' try: with open(high_score_file, 'r') as file: high_score_data = json.load(file) high_score = high_score_data.get('high_score', float('inf')) except FileNotFoundError: high_score = float('inf') if elapsed_time < high_score: with open(high_score_file, 'w') as file: json.dump({'high_score': elapsed_time}, file) # Function that creates the ends screen window def end_screen(window, elapsed_time): save_high_score(elapsed_time) custom_font_path = join("assets", "font.ttf") end_font = pygame.font.Font(custom_font_path, 50) end_text = end_font.render("Congratulations!", True, (255, 255, 0)) complete_text = end_font.render("You Win!", True, (255, 255, 255)) time_text = end_font.render(f"Time: {int(elapsed_time)}s", True, (255, 255, 255)) # Display elapsed time end_bg_image = pygame.image.load(join("assets", "end_bg.png")).convert_alpha() while True: window.blit(end_bg_image, (0, 0)) # Draw the background image # Calculate the vertical center for each text and adjust the position directly window.blit(end_text, ((WIDTH - end_text.get_width()) // 2, (HEIGHT - end_text.get_height()) // 2 - 60)) window.blit(complete_text, ((WIDTH - complete_text.get_width()) // 2, (HEIGHT - complete_text.get_height()) // 2 - 120)) window.blit(time_text, ((WIDTH - time_text.get_width()) // 2, (HEIGHT - time_text.get_height()) // 2 - 180)) # Draw buttons if restart_button.draw(window): return "restart" if quit_button.draw(window): pygame.quit() quit() pygame.display.update() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() def die_screen(window): custom_font_path = join("assets", "font.ttf") die_font = pygame.font.Font(custom_font_path, 75) die_text = die_font.render("You Died!", True, (255, 0, 0)) while True: window.fill((0, 0, 0)) # Fill the screen with black window.blit(die_text, (WIDTH // 2 - die_text.get_width() // 2, HEIGHT // 3)) # Draw buttons if restart_button.draw(window): return "restart" if quit_button.draw(window): pygame.quit() quit() pygame.display.update() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() class Player(pygame.sprite.Sprite): # Player class representing the main character COLOR = (255, 0, 0) GRAVITY = 1 SPRITES = load_sprite_sheets("MainCharacters", "VirtualGuy", 32, 32, True) ANIMATION_DELAY = 3 def __init__(self, x, y, width, height): super().__init__() self.rect = pygame.Rect(x, y, width, height) self.x_vel = 0 self.y_vel = 0 self.mask = None self.direction = "left" self.animation_count = 0 self.fall_count = 0 self.jump_count = 0 self.hit = False self.hit_count = 0 self.score = 0 self.sprite = self.SPRITES['idle_right'][0] # Initialize with a default sprite print(f"Player initialized at ({self.rect.x}, {self.rect.y})") def jump(self): # Handles jumping logic self.y_vel = -self.GRAVITY * 8 self.animation_count = 0 self.jump_count += 1 if self.jump_count == 1: self.fall_count = 0 # Play jump sound JUMP_SOUND.play() def move(self, dx, dy): # Updates the player's position self.rect.x += dx self.rect.y += dy def make_hit(self): # Flags the player as hit self.hit = True def is_off_screen(self): return self.rect.top > HEIGHT # Adjust as needed for your game logic def move_left(self, vel): # Moves the player to the left self.x_vel = -vel if self.direction != "left": self.direction = "left" self.animation_count = 0 def move_right(self, vel): # Moves the player to the right self.x_vel = vel if self.direction != "right": self.direction = "right" self.animation_count = 0 def loop(self, fps): # Updates the player's state every frame self.y_vel += min(1, (self.fall_count / fps) * self.GRAVITY) self.move(self.x_vel, self.y_vel) if self.hit: self.hit_count += 1 if self.hit_count > fps * 2: self.hit = False self.hit_count = 0 self.fall_count += 1 self.update_sprite() def landed(self): # Resets fall and jump counters when the player lands self.fall_count = 0 self.y_vel = 0 self.jump_count = 0 def hit_head(self): # Reverses velocity when hitting the head self.count = 0 self.y_vel *= -1 def update_sprite(self): # Updates the current sprite for animation sprite_sheet = "idle" if self.hit: sprite_sheet = "hit" elif self.y_vel < 0: if self.jump_count == 1: sprite_sheet = "jump" elif self.jump_count == 2: sprite_sheet = "double_jump" elif self.y_vel > self.GRAVITY * 2: sprite_sheet = "fall" elif self.x_vel != 0: sprite_sheet = "run" sprite_sheet_name = sprite_sheet + "_" + self.direction sprites = self.SPRITES[sprite_sheet_name] sprite_index = (self.animation_count // self.ANIMATION_DELAY) % len(sprites) self.sprite = sprites[sprite_index] self.animation_count += 1 self.update() def update(self): # Updates the player's rect and mask for collision self.rect = self.sprite.get_rect(topleft=(self.rect.x, self.rect.y)) self.mask = pygame.mask.from_surface(self.sprite) def draw(self, win, offset_x): # Draws the player on the screen win.blit(self.sprite, (self.rect.x - offset_x, self.rect.y)) class Object(pygame.sprite.Sprite): # Base class for all objects in the game def __init__(self, x, y, width, height, name=None): super().__init__() self.rect = pygame.Rect(x, y, width, height) self.image = pygame.Surface((width, height), pygame.SRCALPHA) self.width = width self.height = height self.name = name def draw(self, win, offset_x): # Draws the object on the screen win.blit(self.image, (self.rect.x - offset_x, self.rect.y)) class Block(Object): # Block class representing terrain blocks def __init__(self, x, y, size): super().__init__(x, y, size, size) block = get_block(size) self.image.blit(block, (0, 0)) self.mask = pygame.mask.from_surface(self.image) class Fire(Object): # Fire class representing fire traps ANIMATION_DELAY = 3 def __init__(self, x, y, width, height): super().__init__(x, y, width, height, "fire") self.fire = load_sprite_sheets("Traps", "Fire", width, height) self.image = self.fire["on"][0] self.mask = pygame.mask.from_surface(self.image) self.animation_count = 0 self.animation_name = "on" def loop(self): # Updates the fire animation sprites = self.fire[self.animation_name] sprite_index = (self.animation_count // self.ANIMATION_DELAY) % len(sprites) self.image = sprites[sprite_index] self.animation_count += 1 self.rect = self.image.get_rect(topleft=(self.rect.x, self.rect.y)) self.mask = pygame.mask.from_surface(self.image) if self.animation_count // self.ANIMATION_DELAY > len(sprites): self.animation_count = 0 class Coin(Object): def __init__(self, x, y, size): super().__init__(x, y, size, size) coin_image = pygame.image.load(join("assets", "coin.png")).convert_alpha() self.image = pygame.transform.scale(coin_image, (size, size)) self.mask = pygame.mask.from_surface(self.image) class Portal(Object): def __init__(self, x, y, size): super().__init__(x, y, size, size) portal_image = pygame.image.load(join("assets", "portal.png")).convert_alpha() self.image = pygame.transform.scale(portal_image, (size, size)) self.mask = pygame.mask.from_surface(self.image) def get_background(name): # Loads the background image image = pygame.image.load(join("assets", "Background", name)) _, _, width, height = image.get_rect() tiles = [] for i in range(WIDTH // width + 1): for j in range(HEIGHT // height + 1): pos = (i * width, j * height) tiles.append(pos) return tiles, image def parse_level(level_data): objects = [] # Default spawn position in case 'S' is not found in the level spawn_x, spawn_y = 1800, 300 for y, row in enumerate(level_data): for x, char in enumerate(row): if char == "F": fire = Fire(x * BLOCK_SIZE, y * BLOCK_SIZE, FIRE_WIDTH, FIRE_HEIGHT) objects.append(fire) elif char == "B": block = Block(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE) objects.append(block) elif char == "C": coin = Coin(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE // 2) objects.append(coin) elif char == "P": portal = Portal(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE) objects.append(portal) elif char == "S": spawn_x = x * BLOCK_SIZE spawn_y = y * BLOCK_SIZE return objects, (spawn_x, spawn_y) def draw(window, background, bg_image, objects, offset_x, current_level, player, elapsed_time): for tile in background: window.blit(bg_image, tile) for obj in objects: obj.draw(window, offset_x) player.draw(window, offset_x) # Ensure player is being drawn # Draw the score score_text = score_font.render(f"Coins: {player.score}", True, (255, 255, 255)) window.blit(score_text, (10, 10)) # Draw level level_text = level_font.render(f"Level: {current_level}", True, (255, 255, 255)) window.blit(level_text, (10, 50)) # Draw timer # Draw timer timer_text = timer_font.render(f"Time: {int(elapsed_time)}s", True, (255, 255, 255)) window.blit(timer_text, (10, 90)) # Adjust position as needed pygame.display.update() def handle_vertical_collision(player, objects, dy): collided_objects = [] for obj in objects: if pygame.sprite.collide_mask(player, obj): if dy > 0: player.rect.bottom = obj.rect.top player.landed() elif dy < 0: player.rect.top = obj.rect.bottom player.hit_head() collided_objects.append(obj) return collided_objects def collide(player, objects, dx): # Checks for collisions when the player moves horizontally player.move(dx, 0) player.update() collided_object = None for obj in objects: if pygame.sprite.collide_mask(player, obj): collided_object = obj break player.move(-dx, 0) player.update() return collided_object def handle_move(player, objects): keys = pygame.key.get_pressed() player.x_vel = 0 collide_left = collide(player, objects, -PLAYER_VEL * 2) collide_right = collide(player, objects, PLAYER_VEL * 2) if keys[pygame.K_LEFT] and not collide_left: player.move_left(PLAYER_VEL) if keys[pygame.K_RIGHT] and not collide_right: player.move_right(PLAYER_VEL) vertical_collide = handle_vertical_collision(player, objects, player.y_vel) to_check = [collide_left, collide_right, *vertical_collide] objects_to_remove = [] # List to keep track of objects to be removed for obj in to_check: if obj: if obj.name == "fire": player.make_hit() elif isinstance(obj, Coin) and obj not in objects_to_remove: objects_to_remove.append(obj) # Add coin to removal list player.score += 1 elif isinstance(obj, Portal) and player.score >= 4: return True # Player has collected enough coins and reached the portal # Remove collected coins from objects list for obj in objects_to_remove: if obj in objects: objects.remove(obj) return False def load_high_score(): high_score_file = 'high_score.json' try: with open(high_score_file, 'r') as file: high_score_data = json.load(file) return high_score_data.get('high_score', float('inf')) except FileNotFoundError: return float('inf') def main_menu(window): # Display the main menu with background and buttons bg_image = pygame.image.load(join("assets", "sky1.png")).convert_alpha() # Load a custom font custom_font_path = join("assets", "font.ttf") # Replace with your font file menu_font = pygame.font.Font(custom_font_path, 50) title_text = menu_font.render("Platformer Game", True, (255, 255, 255)) high_score = load_high_score() if high_score == float('inf'): high_score_text = menu_font.render("High Score: None", True, (255, 255, 255)) else: high_score_text = menu_font.render(f"High Score: {int(high_score)}s", True, (255, 255, 255)) while True: window.blit(bg_image, (0, 0)) window.blit(title_text, (WIDTH // 2 - title_text.get_width() // 2, HEIGHT // 3)) window.blit(high_score_text, (WIDTH // 2 - high_score_text.get_width() // 2, HEIGHT // 3 + 60)) # Draw buttons if start_button.draw(window): return if exit_button.draw(window): pygame.quit() quit() pygame.display.update() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() quit() def main(window): name = "music.mp3" pygame.mixer.music.load(join("assets", name)) pygame.mixer.music.play(-1) clock = pygame.time.Clock() background, bg_image = get_background("Gray.png") level_1 = [ " C P", " BB B BB", " ", " C B B C", " BB BB ", " B BBBBBB ", " S F ", " BBB BB C ", "BBBBBBBBBBBBBBBBBBBBB", " ", ] level_2 = [ " ", "C C ", "BBB BB B C ", " F BB", "P C BBBBB BBB", "BBB BBBB ", " S ", "BBBBBBBBB BBBBBBBBBBB", ] level_3 = [ " F", " C C BB", "BBBBB B BBB", " B FC P", " C B F BB F BBBBB", "BB S BBB BBBBBB", " BBB F BBB CF BBBB", "BBBBBBBBBBB B BBBBBBB", ] levels = [level_1, level_2, level_3] current_level = 0 def reset_player(spawn_pos): player = Player(spawn_pos[0], spawn_pos[1], 50, 50) # Adjust the starting position if necessary player.score = 0 # Reset the player's score return player objects, spawn_pos = parse_level(levels[current_level]) player = reset_player(spawn_pos) offset_x = 0 scroll_area_width = 200 main_menu(window) # Show the main menu before starting the game start_time = time.time() # Record the start time run = True while run: clock.tick(FPS) # Calculate elapsed time elapsed_time = time.time() - start_time for event in pygame.event.get(): if event.type == pygame.QUIT: run = False break if event.type == pygame.KEYDOWN: if event.key == pygame.K_SPACE and player.jump_count < 2: player.jump() player.loop(FPS) for obj in objects: if isinstance(obj, Fire): obj.loop() if handle_move(player, objects): current_level += 1 if current_level >= len(levels): result = end_screen(window, elapsed_time) if result == "restart": current_level = 0 objects, spawn_pos = parse_level(levels[current_level]) player = reset_player(spawn_pos) player.score = 0 offset_x = 0 start_time = time.time() else: run = False else: objects, spawn_pos = parse_level(levels[current_level]) player = reset_player(spawn_pos) # Reset the player for the new level player.score = 0 # Reset the player's score offset_x = 0 if player.is_off_screen() or player.hit: result = die_screen(window) # Show the die screen if result == "restart": objects, spawn_pos = parse_level(levels[current_level]) # Reload the current level player = reset_player(spawn_pos) # Reset the player offset_x = 0 draw(window, background, bg_image, objects, offset_x, current_level + 1, player, elapsed_time) if ((player.rect.right - offset_x >= WIDTH - scroll_area_width) and player.x_vel > 0) or ( (player.rect.left - offset_x <= scroll_area_width) and player.x_vel < 0): offset_x += player.x_vel pygame.quit() quit() if __name__ == "__main__": main(window)