""" ---------------------------------------------------------- Project: Underground Knight game Standerd: 91896 (AS2.7) v2.5 School: Tauranga Boys' College Author: Jamie.clarksonwall Date: jun 9th 2024 Python3.11.9 64 bit ---------------------------------------------------------- """ import pygame from pygame.locals import * from pygame import mixer import pickle from os import path import time import os # Function to load the high score from a file def load_high_score(): if os.path.exists('high_score.txt'): with open('high_score.txt', 'r') as file: return int(file.read()) return 0 # Function to save the high score to a file def save_high_score(high_score): with open('high_score.txt', 'w') as file: file.write(str(high_score)) # Initialize Pygame mixer pygame.mixer.pre_init(44100, -16, 2, 512) mixer.init() pygame.init() clock = pygame.time.Clock() fps = 60 # Set screen dimensions screen_width = 800 screen_height = 800 # Create screen object screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption('Platformer') # define font font_path = "font/Ziplock.ttf" font_size_large = 65 font_size_medium = 40 font_size_small = 24 font = pygame.font.Font(font_path, font_size_large) font_medium = pygame.font.Font(font_path, font_size_medium) font_small = pygame.font.Font(font_path, font_size_small) font_score = pygame.font.Font(font_path, font_size_small) # define game variables tile_size = 40 game_over = 0 main_menu = True level = 3 max_levels = 4 score = 0 high_score = 5 # Load the high score at the start high_score = load_high_score() # define colours white = (255, 255, 255) red = (255, 0, 0) # load images bg_img = pygame.image.load('img/background_cave.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('sound/music.wav.ogg') 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) # Load coin images coin_images = [pygame.image.load(f'coins/coin{num}.png') for num in range(1, 25)] # Function to draw text on the screen def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) screen.blit(img, (x, y)) # Function to get data from a file and parse it into a list of lists def get_data_from_file(file): world_data = [] for l in file: temp = [] s_temp = l.strip().split(',') temp = [int(s) for s in s_temp] world_data.append(temp) return world_data # function to reset level def reset_level(level): player.reset(100, screen_height - 130) blob_group.empty() platform_group.empty() coin_group.empty() lava_group.empty() exit_group.empty() # load in level data and create world if path.exists(f'levels/level{level}.txt'): pickle_in = open(f'levels/level{level}.txt', 'r') world_data = get_data_from_file(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 # Function to crop an image based on its non-transparent pixels def crop_image(image): mask = pygame.mask.from_surface(image) non_transparent_points = mask.outline() if non_transparent_points: 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 calculate the number of coins needed for a level def coins_needed_for_level(level): return level * 3 # Class for buttons in the game 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 not self.clicked: 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 for the player character---------------------------------------------------------------------- class Player(): def __init__(self, x, y): self.reset(x, y) self.idle_images_right = [pygame.image.load(f'sprite cut/idle{num}.png') for num in range(1, 5)] self.idle_images_left = [pygame.transform.flip(image, True, False) for image in self.idle_images_right] self.damage_images = [pygame.transform.scale(pygame.image.load(f'sprite cut/death{num}.png'), (28, 38)) for num in range(1, 5)] self.idle_index = 0 self.damage_index = 0 self.idle_cooldown = 15 self.damage_cooldown = 10 self.idle_counter = 0 self.damage_counter = 0 self.direction = 0 self.jumped = False self.in_air = True self.vel_y = 0 self.width = 28 self.height = 38 self.counter = 0 self.mask = pygame.mask.from_surface(self.image) self.idle = True self.walk_cooldown = 5 # Adjust the walk cooldown for smoother animation def update(self, game_over): dx = 0 dy = 0 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.direction = -1 self.idle = False if key[pygame.K_d]: dx += 5 self.direction = 1 self.idle = False if not key[pygame.K_LEFT] and not key[pygame.K_RIGHT]: self.idle = True # handle animation if not self.idle: if self.counter > self.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] self.mask = pygame.mask.from_surface(self.image) # Update mask else: self.counter += 1 else: # handle idle animation if self.idle_counter >= self.idle_cooldown: self.idle_counter = 0 if self.direction == 1: self.idle_index = (self.idle_index + 1) % len(self.idle_images_right) self.image = pygame.transform.scale(self.idle_images_right[self.idle_index], (28, 38)) elif self.direction == -1: self.idle_index = (self.idle_index + 1) % len(self.idle_images_left) self.image = pygame.transform.scale(self.idle_images_left[self.idle_index], (28, 38)) self.mask = pygame.mask.from_surface(self.image) # Update mask else: self.idle_counter += 1 # 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): if self.vel_y < 0: dy = tile[1].bottom - self.rect.top self.vel_y = 0 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 for blob in blob_group: if pygame.sprite.collide_mask(self, blob): game_over = -1 game_over_fx.play() # check for collision with lava for lava in lava_group: if pygame.sprite.collide_mask(self, lava): game_over = -1 game_over_fx.play() # check for collision with exit for exit in exit_group: if pygame.sprite.collide_mask(self, exit): if score > 2: game_over = 1 # check for collision with platforms for platform in platform_group: if platform.rect.colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 if platform.rect.colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): if abs((self.rect.top + dy) - platform.rect.bottom) < col_thresh: self.vel_y = 0 dy = platform.rect.bottom - self.rect.top elif abs((self.rect.bottom + dy) - platform.rect.top) < col_thresh: self.rect.bottom = platform.rect.top - 1 self.in_air = False dy = 0 if platform.move_x != 0: self.rect.x += platform.move_direction # update player coordinates self.rect.x += dx self.rect.y += dy if game_over == -1: if self.damage_index < len(self.damage_images) - 1: if self.damage_counter >= self.damage_cooldown: self.damage_counter = 0 self.damage_index += 1 self.image = self.damage_images[self.damage_index] else: self.damage_counter += 1 else: self.image = self.damage_images[-1] # Keep the last frame of the death animation draw_text('GAME OVER!', font, red, (screen_width // 2) - 200, screen_height // 2) # draw player on 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'sprite cut/run{num}.png') img_right = pygame.transform.scale(img_right, (28, 38)) img_left = pygame.transform.flip(img_right, True, False) self.images_right.append(img_right) self.images_left.append(img_left) 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 self.mask = pygame.mask.from_surface(self.image) # Create a mask from the initial image 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: 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: blob = Enemy(col_count * tile_size, row_count * tile_size + 15) blob_group.add(blob) if tile == 4: platform = Platform(col_count * tile_size, row_count * tile_size, 1, 0) platform_group.add(platform) if tile == 5: platform = Platform(col_count * tile_size, row_count * tile_size, 0, 1) platform_group.add(platform) if tile == 6: lava = Lava(col_count * tile_size, row_count * tile_size + (tile_size // 2)) lava_group.add(lava) if tile == 7: 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(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]) class Enemy(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) self.images_right = [pygame.image.load(f'slime/walk{num}.png') for num in range(1, 5)] self.images_left = [pygame.transform.flip(image, True, False) for image in self.images_right] self.index = 0 self.image = pygame.transform.scale(self.images_right[self.index], (tile_size, int(tile_size * 0.75))) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.move_direction = 1 self.move_counter = 0 self.animation_counter = 0 self.animation_speed = 5 # Adjust the speed of the animation def update(self): # Move the enemy self.rect.x += self.move_direction self.move_counter += 1 if abs(self.move_counter) > 50: self.move_direction *= -1 self.move_counter *= -1 # Update animation frame self.animation_counter += 1 if self.animation_counter >= self.animation_speed: self.animation_counter = 0 self.index += 1 if self.index >= len(self.images_right): self.index = 0 if self.move_direction > 0: self.image = pygame.transform.scale(self.images_right[self.index], (tile_size, int(tile_size * 0.75))) else: self.image = pygame.transform.scale(self.images_left[self.index], (tile_size, int(tile_size * 0.75))) 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) self.images = [pygame.transform.scale(image, (tile_size // 2, tile_size // 2)) for image in coin_images] self.index = 0 self.image = self.images[self.index] self.rect = self.image.get_rect() self.rect.center = (x, y) self.animation_counter = 0 self.animation_speed = 5 # Adjust this value to control the speed of the animation def update(self): # Update animation frame self.animation_counter += 1 if self.animation_counter >= self.animation_speed: self.animation_counter = 0 self.index = (self.index + 1) % len(self.images) self.image = self.images[self.index] 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) blob_group = pygame.sprite.Group() platform_group = pygame.sprite.Group() lava_group = pygame.sprite.Group() coin_group = pygame.sprite.Group() exit_group = pygame.sprite.Group() if path.exists(f'levels/level{level}.txt'): with open(f'levels/level{level}.txt', 'r') as pickle_in: world_data = get_data_from_file(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) # 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 + 200, start_img) exit_button = Button(screen_width // 2 + 100, screen_height // 2 + 200, exit_img) example_img = pygame.image.load('img/keyboard controles.png') run = True while run: clock.tick(fps) screen.blit(bg_img, (0, 0)) if main_menu: # Draw the intro text draw_text('Underground Knight', font, red, screen_width // 2 - 350, screen_height // 2 - 300) draw_text('In a forgotten underground kingdom, you must navigate ', font_small, white, screen_width // 2 - 350, screen_height // 2 - 210) draw_text('treacherous tunnels filled with deadly spikes and ', font_small, white, screen_width // 2 - 350, screen_height // 2 - 180) draw_text('fearsome enemies. Gather as much gold as possible to ', font_small, white, screen_width // 2 - 350, screen_height // 2 - 150) draw_text('restore glory to your kingdom. Once you have the gold,', font_small, white, screen_width // 2 - 350, screen_height // 2 - 120) draw_text('find the door to proceed to the next level.', font_small, white, screen_width // 2 - 350, screen_height // 2 - 90) # Draw the image screen.blit(example_img, (screen_width // 2 - example_img.get_width() // 2, screen_height // 2 - 50)) if exit_button.draw(): run = False if start_button.draw(): main_menu = False else: world.draw() if game_over == 0: blob_group.update() platform_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('Score: ' + str(score), font_score, white, tile_size - 1, 10) draw_text('High Score: ' + str(high_score), font_score, white, tile_size - 1, 40) # Update high score if current score is greater if score > high_score: high_score = score save_high_score(high_score) blob_group.draw(screen) platform_group.draw(screen) lava_group.draw(screen) exit_group.draw(screen) # Update and draw coin group coin_group.update() coin_group.draw(screen) game_over = player.update(game_over) if game_over == -1: if restart_button.draw(): world_data = [] world = reset_level(level) game_over = 0 score = 0 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: draw_text('YOU WIN!', font, red, (screen_width // 2) - 140, screen_height // 2) if restart_button.draw(): level = 1 world_data = [] world = reset_level(level) game_over = 0 score = 0 for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pygame.display.update() pygame.quit()