import pygame from pygame.locals import * import random import pickle from os import path #Initialize Pygame pygame.init() pygame.mixer.init() clock = pygame.time.Clock() fps = 60 start_ticks = pygame.time.get_ticks() #Used for tracking time #Set up display screen_width = 1000 screen_height = 1000 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption('Platformer') # Music and Effect pygame.mixer.init() #Initializes the mixer module for sound pygame.mixer.music.load('Audio/boom.mp3') #Load background music pygame.mixer.music.set_volume(0.7) #Background music volume (0.0 to 1.0) pygame.mixer.music.play(-1) #Play on loop -1 means infinite coin_sound = pygame.mixer.Sound("Audio/sfx_gem.ogg") #Load coin sound coin_sound.set_volume(0.3) #Lower volume #Game variables tile_size = 50 game_over = 0 main_menu = True level = 0 max_levels = 7 score = 0 #Player's score counter # Fonts font = pygame.font.SysFont(None, 36) #Default font game_over_font = pygame.font.SysFont("comicsansms", 72) #Big font for end screen sub_font = pygame.font.SysFont("comicsansms", 36) #Smaller font for subtext # Button size button_width, button_height = 150, 70 #For consistent button size # Load and scale images bg_img = pygame.image.load('img/actualbackground.png') #Game background restart_img = pygame.image.load('img/restart_btn.png') restart_img = pygame.transform.scale(restart_img, (150, 60)) #Resize to fit button area start_img = pygame.image.load('img/start_btn.png') start_img = pygame.transform.scale(start_img, (150, 60)) exit_img = pygame.image.load('img/exit_btn.png') exit_img = pygame.transform.scale(exit_img, (150, 60)) # Sprite groups blob_group = pygame.sprite.Group() #Enemy blobs lava_group = pygame.sprite.Group() #Deadly lava bubble_group = pygame.sprite.Group() #Bubbles for lava exit_group = pygame.sprite.Group() #Exit doors coin_group = pygame.sprite.Group() #Coins to collect platform_group = pygame.sprite.Group() #Moving/static platforms # Reset level function def reset_level(level): player.reset(100, screen_height - 130) #Respawn player at start blob_group.empty() lava_group.empty() exit_group.empty() coin_group.empty() platform_group.empty() if path.exists(f'level{level}_data'): pickle_in = open(f'level{level}_data', 'rb') world_data = pickle.load(pickle_in) return World(world_data) else: return None #Return nothing if level data doesn't exist # Button class to handle menu etc... class Button(): def __init__(self, x, y, image): self.image = image #Button image self.rect = self.image.get_rect() #Get the rectangle for positioning self.rect.x = x #Set x position self.rect.y = y #Set y position self.clicked = False #Track if button has already been clicked def draw(self): action = False #Default to no action pos = pygame.mouse.get_pos() # et mouse position #Check if mouse is over the button and if it's clicked if self.rect.collidepoint(pos): if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False: action = True # Button is clicked self.clicked = True #Reset click status when mouse button is released if pygame.mouse.get_pressed()[0] == 0: self.clicked = False #Draw the button screen.blit(self.image, self.rect) return action #Return True if button was clicked # Player class class Player: def __init__(self, x, y): # Set up the player at starting position self.reset(x, y) def update(self, game_over): global score dx = 0 # change in x-direction dy = 0 # change in y-direction walk_cooldown = 5 # how many frames before switching walk animation col_thresh = 20 # collision threshold for platforms # - Coin collection # Check if player has collided with any coins coins_collected = pygame.sprite.spritecollide(player, coin_group, True) if coins_collected: score += len(coins_collected) # Add to score coin_sound.play() # Play sound effect # Gameplay active if game_over == 0: # Handle input key = pygame.key.get_pressed() if key[pygame.K_SPACE] and not self.jumped and not self.in_air: self.vel_y = -15 # Jump velocity self.jumped = True # Prevent double jumping if not key[pygame.K_SPACE]: self.jumped = False # Reset jump on key release if key[pygame.K_LEFT]: dx -= 5 self.counter += 1 self.direction = -1 # Facing left if key[pygame.K_RIGHT]: dx += 5 self.counter += 1 self.direction = 1 # Facing right if not key[pygame.K_LEFT] and not key[pygame.K_RIGHT]: # If no horizontal movement, reset animation self.counter = 0 self.index = 0 self.image = self.images_right[self.index] if self.direction == 1 else self.images_left[self.index] # Walk animation if self.counter > walk_cooldown: self.counter = 0 self.index = (self.index + 1) % len(self.images_right) self.image = self.images_right[self.index] if self.direction == 1 else self.images_left[self.index] # Gravity self.vel_y += 1 # Simulate gravity self.vel_y = min(self.vel_y, 10) # Limit fall speed dy += self.vel_y # Apply vertical movement # Collision detection rectangle shrunk slightly for better accuracy player_rect = pygame.Rect(self.rect.x + 6, self.rect.y, self.width - 20, self.height) # Tile collision self.in_air = True for tile in world.tile_list: # Horizontal collision if tile[1].colliderect(player_rect.x + dx, player_rect.y, player_rect.width, player_rect.height): dx = 0 # Vertical collision if tile[1].colliderect(player_rect.x, player_rect.y + dy, player_rect.width, player_rect.height): if self.vel_y < 0: # Head hits ceiling dy = tile[1].bottom - self.rect.top self.vel_y = 0 else: # Feet hit ground dy = tile[1].top - self.rect.bottom self.vel_y = 0 self.in_air = False # Check for enemy or lava collision if pygame.sprite.spritecollide(self, blob_group, False) or pygame.sprite.spritecollide(self, lava_group, False): game_over = -1 # Trigger death # Check for exit collision if pygame.sprite.spritecollide(self, exit_group, False): game_over = 1 # Level complete # Moving platform collision for platform in platform_group: if platform.rect.colliderect(player_rect.x + dx, player_rect.y, player_rect.width, player_rect.height): dx = 0 if platform.rect.colliderect(player_rect.x, player_rect.y + dy, player_rect.width, player_rect.height): if abs((self.rect.top + dy) - platform.rect.bottom) < col_thresh: # Hitting platform from below self.vel_y = 0 dy = platform.rect.bottom - self.rect.top elif abs(self.rect.bottom - platform.rect.top) < col_thresh: # Landing on platform self.rect.bottom = platform.rect.top - 1 self.in_air = False dy = 0 # Move with platform if it's moving horizontally if platform.move_x != 0: self.rect.x += platform.move_direction_x # Update position self.rect.x += dx self.rect.y += dy # Player dead elif game_over == -1: self.image = self.dead_image # Change to dead sprite if self.rect.y > 200: self.rect.y -= 5 # Float upward when dead # Draw player to screen screen.blit(self.image, self.rect) return game_over def reset(self, x, y): # Reset player state and load images self.images_right = [] self.images_left = [] self.index = 0 self.counter = 0 # Load animation frames for num in range(1, 5): img_right = pygame.image.load(f'img/green{num}.png') img_right = pygame.transform.scale(img_right, (55, 60)) img_left = pygame.transform.flip(img_right, True, False) self.images_right.append(img_right) self.images_left.append(img_left) # Load dead image dead_img = pygame.image.load('img/greenhit.png') self.dead_image = pygame.transform.scale(dead_img, (55, 60)) # Set initial image and position self.image = self.images_right[self.index] self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.rect.inflate_ip(-20, 0) # Slightly shrink hitbox # Player physics 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 # World class to manage tiles and level layout class World(): def __init__(self, data): # Store all visible tiles dirt grass etc. self.tile_list = [] # Load all necessary tile images dirt_img = pygame.image.load('img/dirt1.png') grass_img = pygame.image.load('img/grass1.png') platform_img = pygame.image.load('img/platform1.png') # Start tracking which row and column we're on row_count = 0 for row in data: col_count = 0 for tile in row: # Dirt tile if tile == 1: img = pygame.transform.scale(dirt_img, (tile_size, tile_size)) img_rect = img.get_rect() img_rect.x = col_count * tile_size - 5 # Adjust horizontal position slightly img_rect.y = row_count * tile_size - 10 # Adjust vertical position slightly img_rect.inflate_ip(-10, -10) # Shrink hitbox to reduce collision issues tile = (img, img_rect) # Store as tuple (image, rect) self.tile_list.append(tile) # Add to world tile list # Grass 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 - 5 img_rect.y = row_count * tile_size - 10 img_rect.inflate_ip(-10, -10) tile = (img, img_rect) self.tile_list.append(tile) # Enemy blob if tile == 3: blob = Enemy(col_count * tile_size, row_count * tile_size + 15) blob_group.add(blob) # Add enemy to group so it's drawn and updated # Coin if tile == 7: coin = Coin(col_count * tile_size, row_count * tile_size) coin_group.add(coin) # Horizontal moving platform if tile == 4: platform = Platform(col_count * tile_size, row_count * tile_size, 1, 0) platform_group.add(platform) # Vertical moving platform if tile == 5: platform = Platform(col_count * tile_size, row_count * tile_size, 0, 1) platform_group.add(platform) # Lava if tile == 6: lava = Lava(col_count * tile_size, row_count * tile_size + (tile_size // 2)) lava_group.add(lava) # Exit door if tile == 8: exit = Exit(col_count * tile_size, row_count * tile_size - (tile_size // 2)) exit_group.add(exit) # Move to next column col_count += 1 # Move to next row after finishing columns row_count += 1 def draw(self): # Draw all tiles from the tile list (e.g., grass, dirt) for tile in self.tile_list: screen.blit(tile[0], tile[1]) # tile[0] = image, tile[1] = rect # Coin class with animation class Coin(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() # Call parent Sprite class # Load coin animation frames self.images = [] for i in range(6): img = pygame.image.load(f'img/coins/coin_{i}.png').convert_alpha() # Load coin image with transparency img = pygame.transform.scale(img, (tile_size // 2, tile_size // 2)) # Resize to half tile size self.images.append(img) # Set initial animation state self.index = 0 # Frame index for animation self.image = self.images[self.index] # Current coin image # Create a rectangle for positioning and collision self.rect = self.image.get_rect() self.rect.center = (x + tile_size // 2, y + tile_size // 2) # Center the coin in the tile # Counter for animation timing self.counter = 0 def update(self): # Control how fast the animation changes (lower = faster) animation_cooldown = 4 self.counter += 1 # If counter passes cooldown, switch to next frame if self.counter > animation_cooldown: self.counter = 0 self.index += 1 if self.index >= len(self.images): # Loop back to first frame self.index = 0 self.image = self.images[self.index] # Update image to new frame # Enemy class class Enemy(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() # Call the parent Sprite class # Animation setup self.images_right = [] # List of frames for enemy moving right self.images_left = [] # List of flipped frames for enemy moving left for num in range(1, 3): # Load two animation frames img_right = pygame.image.load(f'img/slime{num}.png') # Load image img_right = pygame.transform.scale(img_right, (tile_size, tile_size)) # Scale to match tile size img_left = pygame.transform.flip(img_right, True, False) # Flip for left movement self.images_right.append(img_right) self.images_left.append(img_left) self.index = 0 # Current frame index self.counter = 0 # Animation frame timer self.direction = 1 # Direction enemy is facing 1 = left, -1 = right self.image = self.images_right[self.index] # Set initial image # Set up position and collision box self.rect = self.image.get_rect() self.rect.inflate_ip(-5, 0) # Slightly shrink collision box for better accuracy self.rect.x = x self.rect.y = y - 20 #Raise slightly to match platform visually # Movement setup self.move_direction = 1 # Movement direction: 1 = right, -1 = left self.move_counter = 0 # Count how far it's moved in one direction def update(self): # Move enemy self.rect.x += self.move_direction # Move by 1 pixel/frame in current direction self.move_counter += 1 # Change direction after moving a set distance if abs(self.move_counter) > 50: self.move_direction *= -1 # Flip direction self.move_counter *= -1 # Reset move counter self.direction = self.move_direction # Update facing direction # Handle animation self.counter += 1 animation_speed = 10 # Lower = faster animation if self.counter >= animation_speed: self.counter = 0 self.index = (self.index + 1) % len(self.images_right) # Cycle through animation frames # Set image depending on facing direction if self.direction > 0: self.image = self.images_left[self.index] else: self.image = self.images_right[self.index] # Moving platform class class Platform(pygame.sprite.Sprite): def __init__(self, x, y, move_x, move_y): super().__init__() # Inherit from pygame's Sprite class # Load and scale the platform image img = pygame.image.load('img/platform1.png') self.image = pygame.transform.scale(img, (tile_size * 1, tile_size // 2)) # Scale to be 1 tile wide and half tile tall self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y - 5 # Raise the platform slightly for visual alignment # Movement setup self.move_direction_x = move_x # 1 for horizontal movement, 0 for static self.move_direction_y = move_y # 1 for vertical movement, 0 for static self.move_x = move_x # Speed in x direction (usually 1 or 0) self.move_y = move_y # Speed in y direction (usually 1 or 0) self.move_counter = 0 # Keeps track of movement to reverse direction def update(self): # Move the platform self.rect.x += self.move_direction_x * self.move_x # Move horizontally self.rect.y += self.move_direction_y * self.move_y # Move vertically # Update movement counter self.move_counter += 1 # Reverse direction after a certain distance if abs(self.move_counter) > 50: self.move_direction_x *= -1 # Flip horizontal direction self.move_direction_y *= -1 # Flip vertical direction self.move_counter *= -1 # Reset the counter (negate to keep oscillation centered) # Lava Class class Lava(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() # Inherit from pygame's Sprite class # Load and scale the lava image img = pygame.image.load('img/lava.png') self.image = pygame.transform.scale(img, (tile_size, tile_size // 2)) # Make lava half tile tall # Create and adjust the hitbox self.rect = self.image.get_rect() self.rect.inflate_ip(-5, 0) # Shrink the width slightly for better collision precision self.rect.x = x # Set horizontal position self.rect.y = y - 5 # Adjust vertical placement to align better with tiles def update(self): # Random chance to spawn a bubble (e.g., 1 in 80 frames) if random.randint(1, 80) == 1: bubble = Bubble(self.rect.centerx, self.rect.top) bubble_group.add(bubble) # Bubbles Class for lava class Bubble(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() self.image = pygame.Surface((8, 8), pygame.SRCALPHA) # Transparent background pygame.draw.circle(self.image, (204, 102, 51, 180), (4, 4), 4) # White semi-transparent bubble self.rect = self.image.get_rect(center=(x, y)) self.timer = 30 # Lifetime in frames def update(self): self.rect.y -= 1 # Bubble rises self.timer -= 1 if self.timer <= 0: self.kill() # Remove bubble when timer runs out # Exit door class class Exit(pygame.sprite.Sprite): def __init__(self, x, y): super().__init__() 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 - 5 # Create player and initial world player = Player(100, screen_height - 130) world = reset_level(level) # Button placement restart_button = Button(screen_width // 2 - 75, screen_height // 2, 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) # Main game loop run = True while run: clock.tick(fps) # Control game speed frames per second screen.blit(bg_img, (0, 0)) # Draw background # Main menu screen if main_menu: if start_button.draw(): # If start is clicked start the game main_menu = False if exit_button.draw(): # If exit is clicked quit the game run = False # Game is running not in menu else: world.draw() # Draw the level tiles if game_over == 0: # Update all sprite groups blob_group.update() lava_group.update() bubble_group.update() coin_group.update() platform_group.update() # Check if player collects a coin if pygame.sprite.spritecollide(player, coin_group, True): score += 1 coin_sound.play() # Play coin collection sound # Draw all game elements blob_group.draw(screen) lava_group.draw(screen) bubble_group.draw(screen) coin_group.draw(screen) platform_group.draw(screen) exit_group.draw(screen) # Display score on screen score_text = font.render(f'Score: {score}', True, (255, 255, 255)) screen.blit(score_text, (10, 10)) # Display timer seconds = (pygame.time.get_ticks() - start_ticks) // 1000 timer_text = font.render(f"Time: {seconds}", True, (255, 255, 255)) text_width = timer_text.get_width() screen.blit(timer_text, (screen_width - text_width - 10, 10)) # Right-aligned # Update player and check if game over game_over = player.update(game_over) # If player dies if game_over == -1: # Darken screen with overlay overlay = pygame.Surface((screen_width, screen_height)) overlay.set_alpha(180) overlay.fill((0, 0, 0)) screen.blit(overlay, (0, 0)) # Show Game Over text game_over_text = game_over_font.render("GAME OVER", True, (255, 0, 0)) screen.blit(game_over_text, (screen_width // 2 - game_over_text.get_width() // 2, screen_height // 2 - 150)) sub_text = sub_font.render("Click Restart to try again", True, (255, 255, 255)) screen.blit(sub_text, (screen_width // 2 - sub_text.get_width() // 2, screen_height // 2 - 70)) # Restart the level if restart button is clicked if restart_button.draw(): world = reset_level(level) game_over = 0 score = 0 start_ticks = pygame.time.get_ticks() # If level is completed if game_over == 1: level += 1 if level <= max_levels: # Load next level world = reset_level(level) game_over = 0 else: # Game completed, return to menu main_menu = True level = 0 # If game is completed after final level if game_over == 1 and level > max_levels: overlay = pygame.Surface((screen_width, screen_height)) overlay.set_alpha(180) overlay.fill((0, 0, 0)) screen.blit(overlay, (0, 0)) # Show win message win_text = game_over_font.render("YOU WIN!", True, (0, 255, 0)) screen.blit(win_text, (screen_width // 2 - win_text.get_width() // 2, screen_height // 2 - 150)) sub_text = sub_font.render("Congratulations! You've completed all levels.", True, (255, 255, 255)) screen.blit(sub_text, (screen_width // 2 - sub_text.get_width() // 2, screen_height // 2 - 70)) # Check for quit event for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pygame.display.update() # Refresh the screen pygame.quit() # Exit the game