""" -------------------------------------------- Project: Puzzle Game Standard: 91892 (AS 2.7) V10 School: Tauranga Boys' College Author: Tyler Bunning Date: Jun 2024| -------------------------------------------- """ import pygame import pickle import json import time from os import path # Initialize Pygame pygame.init() # Set up the clock for managing the frame rate clock = pygame.time.Clock() fps = 60 # Define color WHITE = (255, 255, 255) # Define screen dimensions and tile size screen_width = 1000 screen_height = 1000 tile_size = 50 # Initialize game variables level = 1 run = True game_over = 0 main_menu = True max_levels = 3 start_time = time.time() score = 0 # Set up font for displaying text font = pygame.font.Font(None, 36) # Function to load high score for a specific level def load_highscore(level): try: with open(f'hs_{level}.json', 'r') as f: highscore = json.load(f) if isinstance(highscore, dict): return float(highscore.get('highscore', 100.0)) except (FileNotFoundError, json.JSONDecodeError): return 100.0 # Load high score for the current level highscore = load_highscore(level) # Function to save high score for a specific level def save_highscore(level, highscore): with open(f'hs_{level}.json', 'w') as f: json.dump({"highscore": highscore}, f) # Set up the game screen screen = pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption('Puzzle Game') # Load background image and scale it to fit the screen bg_img = pygame.image.load('sprites/bg.jpg') bg_img = pygame.transform.scale(bg_img, (screen_width, screen_height)) # Load and scale the restart button image restart_image = pygame.image.load('sprites/restart_button.png') restart_image = pygame.transform.scale(restart_image, (200, 50)) # Load and scale level start images start_img = pygame.image.load('sprites/level1.png') start_img = pygame.transform.scale(start_img, (215, 70)) level_2_start_img = pygame.image.load('sprites/level2.png') level_2_start_img = pygame.transform.scale(level_2_start_img, (215, 70)) level_3_start_img = pygame.image.load('sprites/level3.png') level_3_start_img = pygame.transform.scale(level_3_start_img, (215, 70)) # 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 reset the level def reset_level(level): global max_coins global score global highscore player.reset(100, screen_height - 500) crate_group.empty() portal_group.empty() coin_group.empty() button_group.empty() saw_group.empty() max_coins = 0 score = 0 highscore = load_highscore(level) 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, player) max_coins = len(coin_group) player.coin_counter = 0 return world # Class to handle the reset button class Reset(): 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 pos = pygame.mouse.get_pos() 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 screen.blit(self.image, self.rect) return action # Player class class Player(): def __init__(self, x, y): self.reset(x,y) #Update method def update(self, game_over): dx = 0 dy = 0 walk_cooldown = 10 if game_over == 0: key = pygame.key.get_pressed() if key[pygame.K_a]: dx -= 2 self.direction = -1 self.counter += 1 if key[pygame.K_d]: dx += 2 self.direction = 1 self.counter += 1 if key[pygame.K_w]: dy -= 2 self.direction = 2 self.counter += 1 if key[pygame.K_s]: dy += 2 self.direction = -2 self.counter += 1 if key[pygame.K_a] and key[pygame.K_w]: self.direction = -1 if key[pygame.K_a] and key[pygame.K_s]: self.direction = -1 if key[pygame.K_d] and key[pygame.K_w]: self.direction = 1 if key[pygame.K_d] and key[pygame.K_s]: self.direction = 1 if not key[pygame.K_a] and not key[pygame.K_d] and not key[pygame.K_w] and not key[pygame.K_s]: 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] if self.direction == 2: self.image = self.images_up[self.index] if self.direction == -2: self.image = self.images_down[self.index] 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] if self.direction == 2: self.image = self.images_up[self.index] if self.direction == -2: self.image = self.images_down[self.index] for tile in world.tile_list: if tile[1].colliderect(self.rect.x + dx, self.rect.y, self.width, self.height): dx = 0 if tile[1].colliderect(self.rect.x, self.rect.y + dy, self.width, self.height): if dy < 0: dy = tile[1].bottom - self.rect.top if dy > 0: dy = tile[1].top - self.rect.bottom for crate in crate_group: player_rect_next_x = self.rect.copy() player_rect_next_x.x += dx player_rect_next_y = self.rect.copy() player_rect_next_y.y += dy if crate.rect.colliderect(player_rect_next_x): if dx > 0: if crate.push(2, 0): crate.rect.x += 2 else: dx = 0 elif dx < 0: if crate.push(-2, 0): crate.rect.x -= 2 else: dx = 0 if crate.rect.colliderect(player_rect_next_y): if dy > 0: if crate.push(0, 2): crate.rect.y += 2 else: dy = 0 elif dy < 0: if crate.push(0, -2): crate.rect.y -= 2 else: dy = 0 coins_collected = pygame.sprite.spritecollide(self, coin_group, True) for coin in coins_collected: self.coin_counter += 1 if pygame.sprite.spritecollide(self, saw_group, False): game_over = -1 for portal in portal_group: if portal.visible == True: if self.rect.colliderect(portal.rect): game_over = 1 for button in button_group: if self.rect.colliderect(button.rect): button.activate() self.rect.x += dx self.rect.y += dy elif game_over == -1: self.image = self.dead_image if self.rect.y > 200: self.rect.y -= 5 screen.blit(self.image, self.rect) self.draw_coin_counter() return game_over def draw_coin_counter(self): font = pygame.font.Font(None, 36) text_surface = font.render(f'Coins: {self.coin_counter}/{max_coins}', True, (255, 255, 255)) screen.blit(text_surface, (screen_width - 150, 20)) def reset(self, x, y): self.images_right = [] self.images_left = [] self.images_up = [] self.images_down = [] self.index = 0 self.counter = 0 for num in range(1, 5): img_right = pygame.image.load(f'sprites/right{num}.png') img_right = pygame.transform.scale(img_right, (40, 40)) img_left = pygame.transform.flip(img_right, True, False) self.images_right.append(img_right) self.images_left.append(img_left) for num in range(1, 5): img_up = pygame.image.load(f'sprites/up{num}.png') img_up = pygame.transform.scale(img_up, (50, 40)) self.images_up.append(img_up) for num in range(1, 5): img_down = pygame.image.load(f'sprites/down{num}.png') img_down = pygame.transform.scale(img_down, (40, 40)) self.images_down.append(img_down) 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.direction = 0 self.coin_counter = 0 self.dead_image = pygame.image.load('sprites/ghost.png') self.dead_image = pygame.transform.scale(self.dead_image, (50,40)) class Crate(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) img = pygame.image.load('sprites/crate1.jpg') self.image = pygame.transform.scale(img, (tile_size, tile_size)) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y def update(self): screen.blit(self.image, self.rect) for button in button_group: if self.rect.colliderect(button.rect): button.activate() def check_collision(self, tiles, x, y): for tile in tiles: if tile[1].colliderect(pygame.Rect(x, y, self.rect.width, self.rect.height)): return True return False def check_crate_collision(self, x, y): for crate in crate_group: if crate != self and crate.rect.colliderect(pygame.Rect(x, y, self.rect.width, self.rect.height)): return crate return None def push(self, dx, dy): crates_to_push = [(self, dx, dy)] visited = set() while crates_to_push: crate, dx, dy = crates_to_push.pop(0) next_x = crate.rect.x + dx next_y = crate.rect.y + dy if (crate, dx, dy) in visited: continue # Check for tile collisions tile_id = world.get_tile_id_at(next_x, next_y) if tile_id == 1: # Specific tile ID # Move back slightly crate.rect.x -= dx crate.rect.y -= dy return False if crate.check_collision(world.tile_list, next_x, next_y): return False next_crate = crate.check_crate_collision(next_x, next_y) if next_crate: crates_to_push.append((next_crate, dx, dy)) else: crate.rect.x = next_x crate.rect.y = next_y visited.add((crate, dx, dy)) return True class Saw(pygame.sprite.Sprite): def __init__(self, x, y, tile_size): super().__init__() self.animation_images = [] for i in range(1, 3): img = pygame.image.load(f'sprites/saw{i}.png') img = pygame.transform.scale(img, (tile_size * 1.5, tile_size * 1.5)) self.animation_images.append(img) self.current_frame = 0 self.image = self.animation_images[self.current_frame] self.rect = self.image.get_rect() self.rect.center = (x, y) self.animation_delay = 2 self.frame_count = 0 self.move_direction = 15 self.move_counter = 0 def update(self): self.rect.y += self.move_direction self.move_counter += 1 crate_collision = any(self.rect.colliderect(crate.rect) for crate in crate_group) wall_collision = any(tile[1].colliderect(self.rect) for tile in world.tile_list) if crate_collision or wall_collision: self.move_direction *= -1 self.rect.y += self.move_direction self.frame_count += 1 if self.frame_count >= self.animation_delay: self.frame_count = 0 self.current_frame = (self.current_frame + 1) % len(self.animation_images) self.image = self.animation_images[self.current_frame] class Coin(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) self.animation_images = [] for i in range(1, 7): img = pygame.image.load(f'sprites/coin{i}.png') img = pygame.transform.scale(img, (tile_size // 1.5, tile_size // 1.5)) self.animation_images.append(img) self.current_frame = 0 self.image = self.animation_images[self.current_frame] self.rect = self.image.get_rect() self.rect.center = (x, y) self.animation_delay = 11 self.frame_count = 0 def update(self): self.frame_count += 1 if self.frame_count >= self.animation_delay: self.frame_count = 0 self.current_frame = (self.current_frame + 1) % len(self.animation_images) self.image = self.animation_images[self.current_frame] class Button(pygame.sprite.Sprite): def __init__(self, x, y, player, portal): pygame.sprite.Sprite.__init__(self) self.image_off = pygame.image.load('sprites/button1.png') self.image_on = pygame.image.load('sprites/button2.png') self.image_off = pygame.transform.scale(self.image_off, (tile_size * 2, int(tile_size * 2))) self.image_on = pygame.transform.scale(self.image_on, (tile_size * 2, int(tile_size * 2))) self.image = self.image_off self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.active = False self.crate_group = crate_group self.player = player self.portal = portal def activate(self): if not self.active: self.active = True self.image = self.image_on self.portal.set_visible(True) def deactivate(self): if self.active: self.active = False self.image = self.image_off self.portal.set_visible(False) def update(self): crate_collision = any(self.rect.colliderect(crate.rect) for crate in self.crate_group) player_collision = self.rect.colliderect(self.player.rect) if crate_collision or player_collision: self.activate() else: self.deactivate() class Portal(pygame.sprite.Sprite): def __init__(self, x, y): pygame.sprite.Sprite.__init__(self) self.animation_images = [] for i in range(1, 5): img = pygame.image.load(f'sprites/p{i}.png') img = pygame.transform.scale(img, (tile_size * 2, tile_size * 2)) self.animation_images.append(img) self.current_frame = 0 self.image = self.animation_images[self.current_frame] self.rect = self.image.get_rect() self.rect.center = (x, y) self.animation_delay = 10 self.frame_count = 0 self.visible = False def update(self): if not self.visible: return self.frame_count += 1 if self.frame_count >= self.animation_delay: self.frame_count = 0 self.current_frame = (self.current_frame + 1) % len(self.animation_images) self.image = self.animation_images[self.current_frame] def set_visible(self, visible): self.visible = visible if not visible: portal_group.remove(portal) else: portal_group.add(self) def draw(self, surface): for button in button_group: if button.active: surface.blit(self.image, self.rect) class World: def __init__(self, data, player): self.tile_list = [] block_img = pygame.image.load('sprites/block.jpg') row_count = 0 portal_positions = {} for row in data: col_count = 0 for tile in row: if tile == 1: img = pygame.transform.scale(block_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, 1) self.tile_list.append(tile) elif tile == 2: crate = Crate(col_count * tile_size, row_count * tile_size) crate_group.add(crate) elif tile == 3: coin = Coin(col_count * tile_size, row_count * tile_size + 25) coin_group.add(coin) elif tile == 4: portal = Portal(col_count * tile_size + 50 , row_count * tile_size) portal_group.add(portal) portal_positions[(col_count, row_count)] = portal elif tile == 6: saw = Saw(col_count * tile_size, row_count * tile_size, tile_size) saw_group.add(saw) col_count += 1 row_count += 1 row_count = 0 for row in data: col_count = 0 for tile in row: if tile == 5: nearest_portal = self.find_nearest_portal(col_count, row_count, portal_positions) if nearest_portal: button_x = col_count * tile_size button_y = row_count * tile_size - 50 button = Button(button_x, button_y, player, nearest_portal) button_group.add(button) col_count += 1 row_count += 1 def find_nearest_portal(self, col, row, portal_positions): nearest_portal = None min_distance = float('inf') for (p_col, p_row), portal in portal_positions.items(): distance = abs(col - p_col) + abs(row - p_row) if distance < min_distance: nearest_portal = portal min_distance = distance return nearest_portal def draw(self): for tile in self.tile_list: screen.blit(tile[0], tile[1]) def get_tile_id_at(self, x, y): for tile in self.tile_list: if tile[1].colliderect(pygame.Rect(x, y, tile_size, tile_size)): return tile[2] return None crate_group = pygame.sprite.Group() coin_group = pygame.sprite.Group() portal_group = pygame.sprite.Group() button_group = pygame.sprite.Group() saw_group = pygame.sprite.Group() def load_world(level): if path.exists(f'level{level}_data'): pickle_in = open(f'level{level}_data', 'rb') world_data = pickle.load(pickle_in) return world_data return [] world_data = load_world(level) player = Player(100, screen_height - 500) world = World(world_data, player) restart_button = Reset(screen_width // 2 -100 , screen_height // 2 , restart_image) start_button = Reset(screen_width // 2 - 100, screen_height // 2 - 50, start_img) level_2_start_button = Reset(screen_width // 2 - 100, screen_height // 2 - 50, level_2_start_img) level_3_start_button = Reset(screen_width // 2 - 100, screen_height // 2 - 50, level_3_start_img) while run: clock.tick(fps) screen.blit(bg_img, (0, 0)) score = time.time() - start_time if main_menu: if start_button.draw(): main_menu = False game_over = 0 start_time = time.time() world = reset_level(level) elif game_over == 1: if score < highscore: highscore = score save_highscore(level, highscore) if level == 1 and level_2_start_button.draw(): main_menu = False start_time = time.time() game_over = 0 level = 2 world = reset_level(level) elif level == 2 and level_3_start_button.draw(): main_menu = False start_time = time.time() game_over = 0 level = 3 world = reset_level(level) elif level == 3 and restart_button.draw(): main_menu = False start_time = time.time() level = 1 game_over = 0 world = reset_level(level) else: world.draw() button_group.update() button_group.draw(screen) saw_group.draw crate_group.update() crate_group.draw(screen) if game_over == -1: start_time = time.time() if restart_button.draw(): world_data = [] world = reset_level(level) game_over = 0 if game_over == 0: coin_group.update() coin_group.draw(screen) saw_group.update() saw_group.draw(screen) draw_text(f'TIME: {int(score)}', font, WHITE, 610, 10) draw_text(f'BEST TIME: {int(highscore)}', font, WHITE, 610, 40) draw_text('Controls: Movement W,A,S,D',font, WHITE, 20,20 ) for portal in portal_group: if portal.visible: portal.update() portal.draw(screen) game_over = player.update(game_over) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pygame.display.update() pygame.quit()