""" Features added/changes made: - Redrew the vast majority of sprites - Coins reset to your amount at the start of the level - Speedrun timer at the top - Keeps track of individual level times (as well as cumulative time when entering said level) - "End screen" with all stats about your playthrough - Death counter that shows at the aformentioned end screen - Custom starting and ending levels - Multiple keybind support (WASD as well as arrow keys + space) - Moving blobs replaced with static spikes - R key as a quick restart """ # Import libraries import pygame, time from pygame.locals import * from pygame import mixer import pickle from os import path # Starting level (prompted before initating the game) while True: try: startinglevel = int(input("\nSelect a starting level between 0 and 7.\n")) if startinglevel < 0 or startinglevel > 7: print("\nPlease select a number between 0 and 7.") else: break except: print("\nNot an integer, try again.") # Ending level (prompted before initating the game) while True: try: endinglevel = int(input("\nSelect an ending level between 0 and 7.\n")) if endinglevel < 0 or endinglevel > 7 or startinglevel > endinglevel: print("\nPlease select a number between 0 and 7 greater than your starting level.") else: break except: print("\nNot an integer, try again.") # Load music pygame.mixer.pre_init(44100, -16, 2, 512) mixer.init() # Set up game window, fps, size, etc clock = pygame.time.Clock() fps = 60 screen_width = 1000 screen_height = 1000 screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption('Platformer Project') pygame.init() # Define fonts normalpath = "assets/Outfit-Medium.ttf" boldpath = "assets/Outfit-Bold.ttf" font = pygame.font.Font(boldpath, 70) font_score = pygame.font.Font(normalpath, 30) # Define variables, constants, and colors game_over = 0 main_menu = True level = startinglevel score = 0 prevscore = 0 timer = 0 # recorded in ms displaytime = '' currenttimes = [0] * (endinglevel + 2) deaths = 0 TILE_SIZE = 50 white = (255, 255, 255) blue = (0, 0, 255) # Load images bg_img = pygame.image.load('assets/bg.png') restart_img = pygame.image.load('assets/restart_btn.png') start_img = pygame.image.load('assets/start_btn.png') exit_img = pygame.image.load('assets/exit_btn.png') # Load sounds pygame.mixer.music.load('assets/music.wav') pygame.mixer.music.play(-1, 0.0, 5000) coin_fx = pygame.mixer.Sound('assets/coin.wav') coin_fx.set_volume(0.5) jump_fx = pygame.mixer.Sound('assets/jump.wav') jump_fx.set_volume(0.5) game_over_fx = pygame.mixer.Sound('assets/game_over.wav') game_over_fx.set_volume(0.5) # Text function def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) screen.blit(img, (x, y)) # Reset level function def reset_level(level): player.reset(100, screen_height - 130) spike_group.empty() platform_group.empty() coin_group.empty() lava_group.empty() exit_group.empty() # Load 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 fake coin for score display score_coin = Coin(TILE_SIZE // 2, TILE_SIZE // 2) coin_group.add(score_coin) return world # Button 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 if hovered and clicked 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 # Player 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() # Jump logic if (key[pygame.K_SPACE] or key[pygame.K_w]) and self.jumped == False and self.in_air == False: jump_fx.play() self.vel_y = -16 # Jump strength self.jumped = True if key[pygame.K_SPACE] or key[pygame.K_w] == False: self.jumped = False # Left movement if key[pygame.K_LEFT] or key[pygame.K_a]: dx -= 5 self.counter += 1 self.direction = -1 # Right movement if key[pygame.K_RIGHT] or key[pygame.K_d]: dx += 5 self.counter += 1 self.direction = 1 # Check if at rest if key[pygame.K_LEFT] == False and key[pygame.K_RIGHT] == False and key[pygame.K_a] == False and key[pygame.K_d] == False: # Reset animation 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 self.direction = dx / 5 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: # X direction if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 # Y direction if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): # Check if below the ground; jumping if self.vel_y < 0: dy = tile[1].bottom - self.rect.top self.vel_y = 0 # Check if above the ground; 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 enemies if pygame.sprite.spritecollide(self, spike_group, False): game_over = -1 game_over_fx.play() # 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 # Update player coordinates according to movement self.rect.x += dx self.rect.y += dy # Make boundaries so that the player does not walk off screen if self.rect.x > 960: self.rect.x = 960 elif self.rect.x < 0: self.rect.x = 0 # Game over logic elif game_over == -1: self.image = self.dead_image draw_text('GAME OVER!', font, (255, 0, 0), (screen_width // 2) - 200, 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 # Reset animations, velocity, etc 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'assets/guy{num}.png') img_right = pygame.transform.scale(img_right, (40, 80)) 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('assets/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 # World class World(): def __init__(self, data): self.tile_list = [] # Load images dirt_img = pygame.image.load('assets/dirt.png') grass_img = pygame.image.load('assets/grass.png') # Define tiles row_count = 0 for row in data: col_count = 0 for tile in row: if tile == 1: # Dirt 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: # Grass 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: # Enemy spike = Enemy(col_count * TILE_SIZE, row_count * TILE_SIZE + 15) spike_group.add(spike) if tile == 4: # Horizontal platform platform = Platform(col_count * TILE_SIZE, row_count * TILE_SIZE, 1, 0) platform_group.add(platform) if tile == 5: # Vertical platform platform = Platform(col_count * TILE_SIZE, row_count * TILE_SIZE, 0, 1) platform_group.add(platform) if tile == 6: # Lava lava = Lava(col_count * TILE_SIZE, row_count * TILE_SIZE + (TILE_SIZE // 2)) lava_group.add(lava) if tile == 7: # Coin coin = Coin(col_count * TILE_SIZE + (TILE_SIZE // 2), row_count * TILE_SIZE + (TILE_SIZE // 2)) coin_group.add(coin) if tile == 8: # Exit exit = Exit(col_count * TILE_SIZE, row_count * TILE_SIZE - (TILE_SIZE // 2)) exit_group.add(exit) col_count += 1 row_count += 1 def draw(self): for tile in self.tile_list: screen.blit(tile[0], tile[1]) # Enemy class Enemy(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load('assets/spike.png') self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y # Platform class Platform(pygame.sprite.Sprite): def __init__(self, x, y, move_x, move_y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('assets/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 # Lava class Lava(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('assets/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 # Coin class Coin(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('assets/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) # Exit class Exit(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('assets/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 # Define player and object groups player = Player(100, screen_height - 130) spike_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 fake coin for score display 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 # Game loop while run: clock.tick(fps) screen.blit(bg_img, (0, 0)) if main_menu == True: if exit_button.draw(): run = False if start_button.draw(): main_menu = False timer = 0 else: world.draw() if game_over == 0: # Update platform groups spike_group.update() platform_group.update() # Update score if a coin has been collected if pygame.sprite.spritecollide(player, coin_group, True): #game_over = -1 score += 1 coin_fx.play() draw_text('x ' + str(score), font_score, white, TILE_SIZE - 5, 6) draw_text(str(round(timer/1000, 3)), font_score, white, TILE_SIZE + 400, 6) # Draw platform groups spike_group.draw(screen) platform_group.draw(screen) lava_group.draw(screen) coin_group.draw(screen) exit_group.draw(screen) game_over = player.update(game_over) # If player has died if game_over == -1: if restart_button.draw(): world_data = [] world = reset_level(level) score = prevscore game_over = 0 deaths += 1 # If player has completed the level if game_over == 1: # Takes note of the time when exiting the level if not the last level (game completion) if level <= endinglevel: currenttimes[level + 1] = round(timer/1000, 3) # Increases the level by one, takes note of the coin amount when exiting the level (as it resets to that amount on death) level += 1 prevscore = score if level <= endinglevel: # Reset level data world_data = [] world = reset_level(level) game_over = 0 else: # Win screen screen.blit(pygame.image.load('assets/bg.png'), (0, 0)) draw_text('YOU WIN!', font, (0, 255, 0), (screen_width // 2) - 155, screen_height // 2) if displaytime == '': displaytime = str(round(timer/1000, 3)) + 's' for i in range(startinglevel, endinglevel + 1): splittext = f"Level {i}: {currenttimes[i+1]}s ({round(currenttimes[i+1] - currenttimes[i], 3)}s)" draw_text(splittext, font_score, white, 20, (i - startinglevel)*40 + 20) draw_text(f"Level {i}: {currenttimes[i+1]}s", font_score, (128, 255, 128), 20, (i - startinglevel)*40 + 20) # Make win text green draw_text(f"Coins collected: {score}", font_score, (255, 240, 128), 20, (endinglevel - startinglevel + 2)*40 + 20) # Coins collected draw_text(f"Deaths: {deaths}", font_score, (128, 128, 128), 20, (endinglevel - startinglevel + 3)*40 + 20) # Death counter if deaths == 0: draw_text("0", font_score, (255, 180, 255), 130, (endinglevel - startinglevel + 3)*40 + 20) # Make death text pink if deathless completion if restart_button.draw(): # Reset level currenttimes = [0] * (endinglevel + 2) level = startinglevel world_data = [] world = reset_level(level) deaths = 0 game_over = 0 score = 0 timer = 0 displaytime = '' for event in pygame.event.get(): # Quit game if quit if event.type == pygame.QUIT: run = False if event.type == pygame.KEYDOWN: # Restart with R key if event.key == pygame.K_r: currenttimes = [0] * (endinglevel + 2) level = startinglevel world_data = [] world = reset_level(level) deaths = 0 game_over = 0 score = 0 timer = 0 displaytime = '' # Update visuals and tick timer pygame.display.update() timer += 1000/fps # Quit pygame.quit()