Source code for game.game_manager

"""Main game state management."""

import pygame
import time
from .isometric_map import IsometricMap
from .entities.entity_manager import EntityManager
from .environment.degradation import DegradationSystem
from .environment.villager_happiness import VillagerHappinessSystem
from .ui.hud import HUD
from .ui.info_popup import InfoPopup
from .ui.combined_graph import CombinedGraph
from .ui.main_menu import MainMenu
from .ui.tutorial_popup import TutorialPopup # Import the new class
from .utils.localization import localization_manager
from .utils import high_score_manager
from .utils.asset_loader import AssetLoader
from .utils import constants as constants_module
from .utils.constants import PACKAGE_DIR
from .utils.constants import *

[docs] class GameManager: """Manages the overall game state and coordinates all systems.""" def __init__(self, screen):
[docs] self.screen = screen
[docs] self.game_state = "MAIN_MENU"
[docs] self.should_quit = False
[docs] self.space_key_down = False # Add this flag to track spacebar state
# Initialize font for game over messages pygame.font.init()
[docs] self.title_font = pygame.font.Font(None, 48)
[docs] self.asset_loader = AssetLoader()
try: background_original = self.asset_loader.load_image("Background.png") self.background_image = pygame.transform.scale(background_original, self.screen.get_size()) except FileNotFoundError: print("Warning: Background.png not found. Using solid color.") self.background_image = None
[docs] self.main_menu = MainMenu(self.background_image)
[docs] self.high_scores = []
[docs] self.username = ""
# Parallax background layers BG_HEIGHT = 200
[docs] self.BG_HEIGHT = BG_HEIGHT
bg_far_raw = self.asset_loader.load_image("bg_far.png") bg_near_raw = self.asset_loader.load_image("bg_near.png")
[docs] self.bg_far = pygame.transform.scale(bg_far_raw, (SCREEN_WIDTH, BG_HEIGHT)) if bg_far_raw else None
[docs] self.bg_near = pygame.transform.scale(bg_near_raw, (SCREEN_WIDTH, BG_HEIGHT)) if bg_near_raw else None
[docs] self.session_score_file = high_score_manager.get_session_filename()
[docs] self.final_elapsed_time = 0
[docs] self.final_score = 0
# Initialize HUD here so it's always available for all game states
[docs] self.hud = HUD()
[docs] self.tutorial_popup = TutorialPopup() # Create an instance of the tutorial popup
[docs] def handle_event(self, event): """Handles all user input and events.""" # If the tutorial is active, it captures all input if self.tutorial_popup.is_active: if event.type == pygame.MOUSEBUTTONDOWN: if self.tutorial_popup.handle_click(event.pos): # Unpause the game timers self._adjust_timers_for_pause(self.tutorial_pause_start_time) self.tutorial_pause_start_time = 0 # When the button is clicked, set a timer to show the next message self.time_since_last_tutorial_step = time.time() return # Block all other game input while tutorial is active if event.type == pygame.QUIT: self.quit_game() if self.game_state == "MAIN_MENU": self.main_menu.handle_event(event, self) if event.type == pygame.MOUSEBUTTONDOWN: self.hud.handle_click(event.pos, self, self.game_state) elif self.game_state in ["PLAYING", "PAUSED", "GAME_OVER"]: if event.type == pygame.KEYDOWN: if event.key == pygame.K_e: self.toggle_pause() elif event.key == pygame.K_SPACE and self.game_state == "PLAYING": self.emergency_feast() elif event.key == pygame.K_r and self.game_state in ["GAME_OVER", "PAUSED"]: self.restart_game() elif event.key == pygame.K_s and self.game_state == "PLAYING": self.send_pig_away() elif event.key == pygame.K_a and self.game_state == "PLAYING": self.add_pigs(5) elif event.type == pygame.MOUSEBUTTONDOWN: self.hud.handle_click(event.pos, self, self.game_state) if self.game_state == "PLAYING": if event.button == 1: # Left Mouse Button self.send_pig_away() elif event.button == 3: # Right Mouse Button self.add_pigs(5) # Add support for Control + Left Click as Right Click elif event.button == 1 and (pygame.key.get_mods() & pygame.KMOD_CTRL): self.add_pigs(5) # Treat Control + Left Click as Right Click
[docs] def quit_game(self): """Signals the main loop to exit.""" self.should_quit = True
[docs] def start_game(self, difficulty='medium'): """Initializes all game systems and starts the game.""" if difficulty == 'easy': constants_module.PIG_BIRTH_RATE = 5 elif difficulty == 'medium': constants_module.PIG_BIRTH_RATE = 7 elif difficulty == 'hard': constants_module.PIG_BIRTH_RATE = 9 self._initialize_game_state() self.game_state = "PLAYING"
def _initialize_game_state(self): """Sets or resets the game to its initial state.""" self.score_saved = False self.is_win_condition = False # Add this flag self.pigs_sent_count = 0 # Track how many pigs have been sent self.controls_explained = False # This now tracks the whole sequence self.final_happiness = None self.final_degradation = None self.pause_start_time = 0 self.tutorial_pause_start_time = 0 # Add this line # Game timing self.game_start_time = time.time() self.last_feast_time = 0 self.score = 0 # Initialize systems self.isometric_map = IsometricMap(str(PACKAGE_DIR / "maps" / "iso_map3.tmj")) self.isometric_map.randomize_land_tiles() self.entity_manager = EntityManager(self.isometric_map) self.degradation_system = DegradationSystem(self.isometric_map) self.villager_happiness_system = VillagerHappinessSystem(self.isometric_map, self.entity_manager, self.degradation_system) # self.hud = HUD() <- REMOVE THIS LINE self.info_popup = InfoPopup() self.combined_graph = CombinedGraph() # Add state for the tutorial sequence self.tutorial_steps = ['tutorial_step_1', 'tutorial_step_2', 'tutorial_step_3', 'tutorial_step_4'] self.current_tutorial_step = 0 self.tutorial_start_time = 2 # seconds into the game self.time_since_last_tutorial_step = 0 self.tutorial_delay = 1.5 # seconds between popups self.history_update_interval = 0.25 # seconds self.time_since_last_history_update = 0 # Degradation ramp-up: stays near zero during tutorial, then ramps to 1.0 self.degradation_scale = 0.0 self.grace_period_elapsed = 0.0 self.GRACE_PERIOD = 15.0 # seconds to ramp from 0 to full after tutorial ends # Initialize starting entities self._spawn_initial_entities() # Show welcome message using localization self.info_popup.show_message(localization_manager.get('welcome_message'), 5)
[docs] def restart_game(self): """Resets the game to the main menu without clearing session high scores.""" self.game_state = "MAIN_MENU" # Re-create the main menu to ensure it's fresh, passing the background self.main_menu = MainMenu(self.background_image)
def _initialize_systems(self): """Initializes all game systems.""" self.map = Map('maps/iso_map3.tmj') self.entity_manager = EntityManager(self.map) self.degradation_system = DegradationSystem(self.map) self.hud = HUD() self.info_popup = InfoPopup() self.combined_graph = CombinedGraph() def _spawn_initial_entities(self): """Spawn initial pigs and villagers.""" # Spawn 5 initial pigs for _ in range(5): self.entity_manager.spawn_pig() # Spawn 10 villagers for _ in range(10): self.entity_manager.spawn_villager() def _adjust_timers_for_pause(self, pause_start_time): if pause_start_time > 0: paused_duration = time.time() - pause_start_time self.game_start_time += paused_duration if self.last_feast_time > 0: self.last_feast_time += paused_duration def _trigger_game_over(self, is_win, elapsed_time, message_key): if self.game_state == "GAME_OVER": return self.is_win_condition = is_win self.game_state = "GAME_OVER" self.final_happiness = self.villager_happiness_system.get_average_happiness() self.final_degradation = self.degradation_system.get_overall_degradation() self.final_elapsed_time = elapsed_time self.final_score = int(self.score) self.info_popup.show_message(localization_manager.get(message_key, score=self.final_score), 10000)
[docs] def toggle_pause(self): """Toggle game pause state.""" if self.game_state == "PLAYING": self.game_state = "PAUSED" self.pause_start_time = time.time() self.info_popup.clear_messages() elif self.game_state == "PAUSED": self.game_state = "PLAYING" self._adjust_timers_for_pause(self.pause_start_time)
[docs] def send_pig_away(self): """Send one pig to another island.""" if self.entity_manager.remove_pig(): self.score += 50 self.pigs_sent_count += 1 if self.pigs_sent_count <= 10: # For the first 10 pigs, show the full, prominent message self.info_popup.show_message(localization_manager.get('pig_sent_message'), 2) else: # After 10, show a less prominent, shorter score update self.info_popup.show_message("+50", 0.75)
[docs] def add_pigs(self, count): """Adds a specified number of new pigs to the island.""" for _ in range(count): self.entity_manager.spawn_pig() self.info_popup.show_message(localization_manager.get('add_pigs_message', count=count), 2)
[docs] def emergency_feast(self): """Slaughter half the pigs for a feast.""" current_time = time.time() if current_time - self.last_feast_time >= FEAST_COOLDOWN: removed_pigs = self.entity_manager.emergency_feast() if removed_pigs > 0: self.last_feast_time = current_time points = removed_pigs * 25 self.score += points self.info_popup.show_message(localization_manager.get('feast_message', pigs=removed_pigs, points=points), 3) else: self.info_popup.show_message(localization_manager.get('no_pigs_for_feast'), 2) else: remaining_cooldown = FEAST_COOLDOWN - (current_time - self.last_feast_time) self.info_popup.show_message(localization_manager.get('feast_cooldown_message', cooldown=int(remaining_cooldown)), 2)
[docs] def update(self, dt): """Update game state.""" # --- Tutorial Sequence Management --- # Check game_state first to prevent error when not in PLAYING state if self.game_state == "PLAYING" and not self.controls_explained: elapsed_time = time.time() - self.game_start_time if elapsed_time > self.tutorial_start_time and not self.tutorial_popup.is_active: if self.current_tutorial_step < len(self.tutorial_steps): # Check if enough time has passed since the last popup was closed if self.time_since_last_tutorial_step == 0 or \ time.time() - self.time_since_last_tutorial_step > self.tutorial_delay: message_key = self.tutorial_steps[self.current_tutorial_step] is_last = self.current_tutorial_step == len(self.tutorial_steps) - 1 self.tutorial_popup.show(message_key, is_last) self.tutorial_pause_start_time = time.time() # Start the pause timer self.current_tutorial_step += 1 self.time_since_last_tutorial_step = 0 # Reset timer until it's closed else: self.controls_explained = True # Tutorial is finished # If tutorial popup is active, pause all game logic if self.tutorial_popup.is_active: self.info_popup.update(dt) # Allow existing popups to fade, but nothing else return if self.game_state != "PLAYING": # If game is over and score hasn't been saved, save it only on a win. if self.game_state == "GAME_OVER" and not self.score_saved: if self.is_win_condition: score_entry = dict() current_scorer = {'name': self.username, 'score': int(self.score)} score_entry.update(current_scorer) high_score_manager.add_score_to_session(score_entry, self.session_score_file) # Always load scores to display them, even on a loss self.high_scores = high_score_manager.load_scores(self.session_score_file) self.score_saved = True return current_time = time.time() elapsed_time = current_time - self.game_start_time # Check for game over by time (WIN condition) if elapsed_time >= GAME_DURATION: self._trigger_game_over(True, elapsed_time, 'time_up_message') return # Check for game over by degradation (LOSE condition) degradation_level = self.degradation_system.get_overall_degradation() if degradation_level >= ECOSYSTEM_COLLAPSE_THRESHOLD: self._trigger_game_over(False, elapsed_time, 'ecosystem_collapsed_message') return # Advance degradation scale: zero during tutorial, ramp up afterwards if not self.controls_explained: self.degradation_scale = 0.0 elif self.grace_period_elapsed < self.GRACE_PERIOD: self.grace_period_elapsed += dt self.degradation_scale = self.grace_period_elapsed / self.GRACE_PERIOD else: self.degradation_scale = 1.0 self.entity_manager.update(dt) self.degradation_system.update(dt * self.degradation_scale, self.entity_manager) self.villager_happiness_system.update(dt) self.info_popup.update(dt) # Update combined graph self.time_since_last_history_update += dt if self.time_since_last_history_update >= self.history_update_interval: self.time_since_last_history_update -= self.history_update_interval self.combined_graph.add_data_point( self.degradation_system.get_overall_degradation(), self.villager_happiness_system.get_average_happiness(), self.entity_manager.get_pig_count(), ) # Update score self.score += SCORE_PER_SECOND * dt # REMOVE the old single message display logic # Check for environmental warnings degradation_level = self.degradation_system.get_overall_degradation() if degradation_level > 0.7: self.info_popup.show_message(localization_manager.get('critical_warning'), 4) elif degradation_level > 0.5: self.info_popup.show_message(localization_manager.get('heavy_warning'), 3) # Spawn new pigs (scaled so population ramps up alongside degradation) if self.entity_manager.should_spawn_pig(dt * self.degradation_scale): self.entity_manager.spawn_pig()
[docs] def render(self, screen): """Render all game elements.""" if self.game_state == "MAIN_MENU": self.main_menu.render(screen) # Also render the HUD to show the exit button self.hud.render(screen, { 'main_menu': True, 'paused': False, 'game_over': False }) return screen.fill((117, 148, 62)) if self.bg_far: screen.blit(self.bg_far, (0, 0)) if self.bg_near: screen.blit(self.bg_near, (0, 0)) screen.set_clip(pygame.Rect(0, self.BG_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - self.BG_HEIGHT)) self.isometric_map.render(screen, bg_height=self.BG_HEIGHT) self.entity_manager.render(screen) screen.set_clip(None) # Determine the correct time to use for rendering, accounting for all pause types is_paused_by_tutorial = self.tutorial_popup.is_active and self.tutorial_pause_start_time > 0 if self.game_state == "GAME_OVER": elapsed_time = self.final_elapsed_time score = self.final_score # The win/loss message is now rendered by the HUD, so we remove the logic from here. else: # Determine the 'current' time for calculation, freezing it if paused if self.game_state == "PAUSED": current_time = self.pause_start_time elif is_paused_by_tutorial: current_time = self.tutorial_pause_start_time else: current_time = time.time() elapsed_time = current_time - self.game_start_time score = int(self.score) remaining_time = max(0, GAME_DURATION - elapsed_time) # The feast cooldown is handled by adjusting its start time, so it can always use the real current time. feast_cooldown = max(0, FEAST_COOLDOWN - (time.time() - self.last_feast_time)) self.hud.render(screen, { 'score': score, 'time_remaining': int(remaining_time), 'pig_count': len(self.entity_manager.pigs), 'feast_cooldown': int(feast_cooldown), 'degradation': self.degradation_system.get_overall_degradation(), 'happiness': self.villager_happiness_system.get_average_happiness(), 'paused': self.game_state == "PAUSED", 'game_over': self.game_state == "GAME_OVER", 'final_happiness': self.final_happiness, 'final_degradation': self.final_degradation, 'high_scores': self.high_scores, 'is_win': self.is_win_condition # Add this line }) # Render info popup self.info_popup.render(screen) self.combined_graph.render(screen) # Render the tutorial popup on top of everything self.tutorial_popup.render(screen)