""" --------------------------------- project: Platform puzzler Standard: 91896 School: Tauranga Boys' College Author: Noah Burrell Date: June 2024 Python: 3.11.9 --------------------------------- """ import pygame from pygame.locals import * from pygame import mixer import pickle from os import path pygame.mixer.pre_init(44100, -16, 2, 512) mixer.init() pygame.init() clock = pygame.time.Clock() fps = 60 screen_width = 32*28 screen_height = 32*28 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption('Platformer') #define font font = pygame.font.SysFont('Bauhaus 93', 70) font_score = pygame.font.SysFont('Bauhaus 93', 30) #define game variables tile_size = 32 game_over = 0 main_menu = True level = 1 max_levels = 7 score = 0 restart_key_pressed = False #define colours white = (255, 255, 255) blue = (0, 0, 255) #load images sun_img = pygame.image.load('img/sun.png') bg_img = pygame.image.load('img/sky.png') restart_img = pygame.image.load('img/restart_btn.png') start_img = pygame.image.load('img/start_btn.png') exit_img = pygame.image.load('img/exit_btn.png') #load sounds pygame.mixer.music.load('img/music.wav') pygame.mixer.music.play(-1, 0.0, 5000) coin_fx = pygame.mixer.Sound('img/coin.wav') coin_fx.set_volume(0.5) jump_fx = pygame.mixer.Sound('img/jump.wav') jump_fx.set_volume(0.5) game_over_fx = pygame.mixer.Sound('img/game_over.wav') game_over_fx.set_volume(0.5) def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) screen.blit(img, (x, y)) #function to reset level def reset_level(level): player.reset(100, screen_height - 130) platform_group.empty() coin_group.empty() lava_group.empty() door_group.empty() pressure_group.empty() exit_group.empty() box_group.empty() #load in level data and create world if path.exists(f'level{level}_data'): pickle_in = open(f'level{level}_data', 'rb') world_data = pickle.load(pickle_in) world = World(world_data) #create dummy coin for showing the score score_coin = Coin(tile_size // 2, tile_size // 2) coin_group.add(score_coin) return world class Button(): def __init__(self, x, y, image): self.image = image self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.clicked = False def draw(self): action = False #get mouse position pos = pygame.mouse.get_pos() #check mouseover and clicked conditions if self.rect.collidepoint(pos): if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False: action = True self.clicked = True if pygame.mouse.get_pressed()[0] == 0: self.clicked = False #draw button screen.blit(self.image, self.rect) return action class Player(): def __init__(self, x, y): self.reset(x, y) def update(self, game_over): dx = 0 dy = 0 walk_cooldown = 5 col_thresh = 20 if game_over == 0: # get keypresses key = pygame.key.get_pressed() if key[pygame.K_SPACE] and not self.jumped and not self.in_air: jump_fx.play() self.vel_y = -15 self.jumped = True if not key[pygame.K_SPACE]: self.jumped = False if key[pygame.K_a]: dx -= 5 self.counter += 1 self.direction = -1 if key[pygame.K_d]: dx += 5 self.counter += 1 self.direction = 1 if not key[pygame.K_a] and not key[pygame.K_d]: self.counter = 0 self.index = 0 if self.direction == 1: self.image = self.images_right[self.index] if self.direction == -1: self.image = self.images_left[self.index] # handle animation if self.counter > walk_cooldown: self.counter = 0 self.index += 1 if self.index >= len(self.images_right): self.index = 0 if self.direction == 1: self.image = self.images_right[self.index] if self.direction == -1: self.image = self.images_left[self.index] # add gravity self.vel_y += 1 if self.vel_y > 10: self.vel_y = 10 dy += self.vel_y # check for collision self.in_air = True for tile in world.tile_list: # check for collision in x direction if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # check for collision in y direction if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): # check if below the ground i.e. jumping if self.vel_y < 0: dy = tile[1].bottom - self.rect.top self.vel_y = 0 # check if above the ground i.e. falling elif self.vel_y >= 0: dy = tile[1].top - self.rect.bottom self.vel_y = 0 self.in_air = False # check for collision with lava if pygame.sprite.spritecollide(self, lava_group, False): game_over = -1 game_over_fx.play() # check for collision with exit if pygame.sprite.spritecollide(self, exit_group, False): game_over = 1 # check for collision with platforms for platform in platform_group: # collision in the x direction if platform.rect.colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # collision in the y direction if platform.rect.colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): # check if below platform if abs((self.rect.top + dy) - platform.rect.bottom) < col_thresh: self.vel_y = 0 dy = platform.rect.bottom - self.rect.top # check if above platform elif abs((self.rect.bottom + dy) - platform.rect.top) < col_thresh: self.rect.bottom = platform.rect.top - 1 self.in_air = False dy = 0 # move sideways with the platform if platform.move_x != 0: self.rect.x += platform.move_direction # Check for collision with doors for door in door_group: # Check horizontal collision if door.rect.colliderect(self.rect.x + dx, self.rect.y, self.rect.width, self.rect.height): if dx > 0: self.rect.right = door.rect.left elif dx < 0: self.rect.left = door.rect.right dx = 0 # Stop horizontal movement # Check vertical collision if door.rect.colliderect(self.rect.x, self.rect.y + dy, self.rect.width, self.rect.height): if dy > 0: self.rect.bottom = door.rect.top elif dy < 0: self.rect.top = door.rect.bottom dy = 0 # Stop vertical movement # Check for collision with boxes for box in box_group: # check for collision in x direction if box.rect.colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): if dx > 0: box.dx = 5 # Push the box to the right self.rect.right = box.rect.left elif dx < 0: box.dx = -5 # Push the box to the left self.rect.left = box.rect.right dx = 0 # Stop horizontal movement # check for collision in y direction if box.rect.colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): # check if below the ground i.e. jumping if self.vel_y < 0: dy = box.rect.bottom - self.rect.top self.vel_y = 0 # check if above the ground i.e. falling elif self.vel_y >= 0: dy = box.rect.top - self.rect.bottom self.vel_y = 0 self.in_air = False # update player coordinates self.rect.x += dx self.rect.y += dy elif game_over == -1: self.image = self.dead_image draw_text('Try Again!', font, blue, (screen_width // 2) - 160, screen_height // 2) if self.rect.y > 200: self.rect.y -= 5 # draw player onto screen screen.blit(self.image, self.rect) return game_over def reset(self, x, y): self.images_right = [] self.images_left = [] self.index = 0 self.counter = 0 for num in range(1, 5): img_right = pygame.image.load(f'img/guy{num}.png') img_right = pygame.transform.scale(img_right, (32, 48)) img_left = pygame.transform.flip(img_right, True, False) self.images_right.append(img_right) self.images_left.append(img_left) self.dead_image = pygame.image.load('img/ghost.png') self.image = self.images_right[self.index] self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.width = self.image.get_width() self.height = self.image.get_height() self.vel_y = 0 self.jumped = False self.direction = 0 self.in_air = True class World(): def __init__(self, data): self.tile_list = [] #load images dirt_img = pygame.image.load('img/dirt.png') grass_img = pygame.image.load('img/grass.png') row_count = 0 for row in data: col_count = 0 for tile in row: if tile == 1:# BASE PLATFORMS img = pygame.transform.scale(dirt_img, (tile_size, tile_size)) img_rect = img.get_rect() img_rect.x = col_count * tile_size img_rect.y = row_count * tile_size tile = (img, img_rect) self.tile_list.append(tile) if tile == 2: img = pygame.transform.scale(grass_img, (tile_size, tile_size)) img_rect = img.get_rect() img_rect.x = col_count * tile_size img_rect.y = row_count * tile_size tile = (img, img_rect) self.tile_list.append(tile) if tile == 3: # x moving platforms platform = Platform(col_count * tile_size, row_count * tile_size, 1, 0) platform_group.add(platform) if tile == 4: # y moving platforms platform = Platform(col_count * tile_size, row_count * tile_size, 0, 1) platform_group.add(platform) if tile == 5: # lava lava = Lava(col_count * tile_size, row_count * tile_size + (tile_size // 2)) lava_group.add(lava) if tile == 6: # coins coin = Coin(col_count * tile_size + (tile_size // 2), row_count * tile_size + (tile_size // 2)) coin_group.add(coin) if tile == 7: # exit exit = Exit(col_count * tile_size, row_count * tile_size - (tile_size // 2)) exit_group.add(exit) if tile == 8: # doors - red door = Door(col_count * tile_size, row_count * tile_size, "r") door_group.add(door) if tile == 9: # blue door = Door(col_count * tile_size, row_count * tile_size, "b") door_group.add(door) if tile == 10: # green door = Door(col_count * tile_size, row_count * tile_size, "g") door_group.add(door) if tile == 11: # pressure plates - red pressure = Pressure(col_count * tile_size, row_count * tile_size-(tile_size//2), "r") pressure_group.add(pressure) if tile == 12: # blue pressure = Pressure(col_count * tile_size, row_count * tile_size-(tile_size//2), "b") pressure_group.add(pressure) if tile == 13: # green pressure = Pressure(col_count * tile_size, row_count * tile_size-(tile_size//2), "g") pressure_group.add(pressure) if tile == 14: # box box = Box(col_count * tile_size, row_count * tile_size) box_group.add(box) col_count += 1 row_count += 1 def draw(self): for tile in self.tile_list: screen.blit(tile[0], tile[1]) # Door class class Door(pygame.sprite.Sprite): def __init__(self, x, y, col): super().__init__() self.images = [] self.index = 0 self.count = 0 self.col = col self.open = False self.original_bottom = y # Store the initial bottom position self.rect = pygame.Rect(x, y, tile_size, 4 * tile_size) # Initialize the door's rect self.rect.y -= 3*tile_size # Load and scale images for the door animation for num in range(1, 7): img = pygame.image.load(f'img/door{num}{col}.png') img = pygame.transform.scale(img, (tile_size, 4 * tile_size)) # Ensure the same height for all door images self.images.append(img) def update(self): # If the door is open and not fully opened, increment the count if self.open and self.count < 30: self.count += 1 # If the door is closed and not fully closed, decrement the count elif not self.open and self.count > 0: self.count -= 1 # Calculate the difference in height caused by opening or closing the door height_difference = 3 * tile_size * (1 - (self.count / 30)) self.rect = pygame.Rect(self.rect.x, self.original_bottom, tile_size, tile_size + height_difference) self.rect.y -= 3*tile_size # Update the animation frame based on the count if self.count > 0: self.index = min(self.count // 5, len(self.images) - 1) else: self.index = 0 self.image = self.images[self.index] # Pressure plate class class Pressure(pygame.sprite.Sprite): def __init__(self, x, y, col): super().__init__() self.images = [] self.index = 0 self.count = 0 self.col = col # Load and scale images for the pressure plate animation for num in range(1, 6): img = pygame.image.load(f'img/pressure{num}{col}.png') img = pygame.transform.scale(img, (tile_size, tile_size*1.5)) #image height *1.5 self.images.append(img) # Set the initial image and rect attributes self.image = self.images[self.index] # Set the rect attributes with a height of only 1 tile self.rect = pygame.Rect(x, y, tile_size, tile_size//2) def update(self): # Check for collision with the player or box on the top side collision_occurred = False # Flag variable to track collision player_collision = player.rect.colliderect(self.rect.x, self.rect.y, self.rect.width, self.rect.height) if player_collision: collision_occurred = True player.rect.bottom = self.rect.top player.in_air = False for box in box_group: box_collision = box.rect.colliderect(self.rect.x, self.rect.y, self.rect.width, self.rect.height) if box_collision: collision_occurred = True box.rect.bottom = self.rect.top - 10 if collision_occurred: if self.count < 30: self.count += 1 # Open the doors that match the pressure plate's color when count reaches 30 for door in door_group: if door.col == self.col and self.count > 0: door.open = True elif not collision_occurred: if self.count > 0: self.count -= 1 # Close the doors that match the pressure plate's color for door in door_group: if door.col == self.col: door.open = False # Update the animation frame based on the count if self.count > 0: self.index = min(self.count // 6, len(self.images) - 1) else: self.index = 0 self.image = self.images[self.index] class Box(pygame.sprite.Sprite): def __init__(self, x, y): """ Initializes the Box object. Args: x (int): The x-coordinate of the box. y (int): The y-coordinate of the box. """ pygame.sprite.Sprite.__init__(self) # Load the box image and scale it to the tile size img = pygame.image.load('img/box.png') self.image = pygame.transform.scale(img, (tile_size, tile_size)) # Get the rectangle of the image and set its position self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y # Initialize vertical and horizontal velocities self.vel_y = 0 self.dx = 0 self.collided = False #collison helpd self.collide_count = 0 def update(self): """ Updates the position and behavior of the box. """ dy = 0 # Initialize change in y-coordinate col_thresh = 20 # Collision threshold # Apply gravity self.vel_y += 1 if self.vel_y > 10: self.vel_y = 10 dy += self.vel_y # Check for collision with the world tiles for tile in world.tile_list: # Check for collision in the x direction if tile[1].colliderect(self.rect.x + self.dx, self.rect.y, self.rect.width, self.rect.height): self.collided = True if self.dx > 0: # Moving right self.rect.right = tile[1].left elif self.dx < 0: # Moving left self.rect.left = tile[1].right self.dx = 0 # Check for collision in the y direction if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.rect.width, self.rect.height): if self.vel_y > 0: # Falling dy = tile[1].top - self.rect.bottom self.vel_y = 0 elif self.vel_y < 0: # Jumping dy = tile[1].bottom - self.rect.top self.vel_y = 0 # Check for collision with platforms for platform in platform_group: # Check for collision in the x direction if platform.rect.colliderect(self.rect.x + self.dx, self.rect.y, self.rect.width, self.rect.height): self.collided = True if self.dx > 0: # Moving right self.rect.right = platform.rect.left elif self.dx < 0: # Moving left self.rect.left = platform.rect.right self.dx = 0 # Check for collision in the y direction if platform.rect.colliderect(self.rect.x, self.rect.y + dy, self.rect.width, self.rect.height): if self.vel_y > 0: # Falling dy = platform.rect.top - self.rect.bottom self.vel_y = 0 elif self.vel_y < 0: # Jumping dy = platform.rect.bottom - self.rect.top self.vel_y = 0 # Ensure the box does not climb walls for tile in world.tile_list: if tile[1].colliderect(self.rect.x + self.dx, self.rect.y, self.rect.width, self.rect.height): self.collided = True self.dx = 0 # Check for collision with doors for door in door_group: # Check horizontal collision if door.rect.colliderect(self.rect.x + self.dx, self.rect.y, self.rect.width, self.rect.height): self.collided = True if self.dx > 0: self.rect.right = door.rect.left elif self.dx < 0: self.rect.left = door.rect.right self.dx = 0 # Stop horizontal movement # Check vertical collision if door.rect.colliderect(self.rect.x, self.rect.y + dy, self.rect.width, self.rect.height): self.rect.bottom = door.rect.top dy = 0 # Stop vertical movement if not door.open: dy -= 32 if self.collided and self.collide_count < 100: self.collide_count += 1 draw_text("If needed press R to restart", font_score, white, screen_width//2 - 200, screen_height//2) # Update box position self.rect.x += self.dx self.rect.y += dy self.dx = 0 # Check for collision with moving platforms for platform in platform_group: if platform.move_x != 0 and platform.rect.colliderect(self.rect.x, self.rect.y + 1, self.rect.width, self.rect.height): self.rect.x += platform.move_direction * platform.move_x self.dx = 0 class Platform(pygame.sprite.Sprite): def __init__(self, x, y, move_x, move_y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('img/platform.png') self.image = pygame.transform.scale(img, (tile_size, tile_size // 2)) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.move_counter = 0 self.move_direction = 1 self.move_x = move_x self.move_y = move_y def update(self): self.rect.x += self.move_direction * self.move_x self.rect.y += self.move_direction * self.move_y self.move_counter += 1 if abs(self.move_counter) > 50: self.move_direction *= -1 self.move_counter *= -1 class Lava(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('img/lava.png') self.image = pygame.transform.scale(img, (tile_size, tile_size // 2)) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y class Coin(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('img/coin.png') self.image = pygame.transform.scale(img, (tile_size // 2, tile_size // 2)) self.rect = self.image.get_rect() self.rect.center = (x, y) class Exit(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('img/exit.png') self.image = pygame.transform.scale(img, (tile_size, int(tile_size * 1.5))) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y player = Player(100, screen_height - 130) door_group = pygame.sprite.Group() pressure_group = pygame.sprite.Group() lever_group = pygame.sprite.Group() box_group = pygame.sprite.Group() platform_group = pygame.sprite.Group() lava_group = pygame.sprite.Group() coin_group = pygame.sprite.Group() exit_group = pygame.sprite.Group() #create dummy coin for showing the score score_coin = Coin(tile_size // 2, tile_size // 2) coin_group.add(score_coin) #load in level data and create world if path.exists(f'level{level}_data'): pickle_in = open(f'level{level}_data', 'rb') world_data = pickle.load(pickle_in) world = World(world_data) #create buttons restart_button = Button(screen_width // 2 - 50, screen_height // 2 + 100, restart_img) start_button = Button(screen_width // 2 - 350, screen_height // 2, start_img) exit_button = Button(screen_width // 2 + 150, screen_height // 2, exit_img) run = True while run: clock.tick(fps) screen.blit(bg_img, (0, 0)) screen.blit(sun_img, (100, 100)) if main_menu == True: if exit_button.draw(): run = False if start_button.draw(): main_menu = False else: world.draw() if game_over == 0: platform_group.update() box_group.update() #update score #check if a coin has been collected if pygame.sprite.spritecollide(player, coin_group, True): score += 1 coin_fx.play() draw_text('X ' + str(score), font_score, white, tile_size - 10, 10) platform_group.draw(screen) lava_group.draw(screen) coin_group.draw(screen) exit_group.draw(screen) door_group.draw(screen) box_group.draw(screen) pressure_group.draw(screen) game_over = player.update(game_over) key = pygame.key.get_pressed() restart_key_pressed = False if key[pygame.K_r] and not restart_key_pressed: restart_key_pressed = True game_over = -1 #if player has died if game_over == -1: if restart_button.draw(): world_data = [] world = reset_level(level) game_over = 0 score = 0 #if player has completed the level if game_over == 1: #reset game and go to next level level += 1 if level <= max_levels: #reset level world_data = [] world = reset_level(level) game_over = 0 else: world = reset_level(0) draw_text('YOU WIN!', font, blue, (screen_width // 2) - 140, screen_height // 2) if restart_button.draw(): level = 1 #reset level world_data = [] world = reset_level(level) game_over = 0 score = 0 for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pressure_group.update() door_group.update() box_group.update() pygame.display.update() pygame.quit()