import pyxel
import random

# =============================================================================
# [ CLASS: CONFIGURATION ]
# -----------------------------------------------------------------------------
# This section defines the global settings for the game engine.
# Using a dedicated class keeps the data structured and professional.
# =============================================================================
class Config:
    SCREEN_WIDTH = 220
    SCREEN_HEIGHT = 160
    FPS = 30
    TITLE = "Rise of Instinct"
    COLOR_UI_BG = 0
    
    # Map Display Settings:
    # Centers our 128x128 game world within the 220x160 screen.
    MAP_DISPLAY_X = 46  
    MAP_DISPLAY_Y = 20  
    TILE_SIZE = 8 

# =============================================================================
# [ ASSET DICTIONARY: SPRITE MAPPING ]
# -----------------------------------------------------------------------------
# Coordinates for every visual element in the .pyxres file.
# Format: "Key": (U, V, Width, Height)
# =============================================================================
SPRITE_MAP = {
    "Maus": (0, 3, 16, 11),
    "Katze": (16, 1, 15, 13),
    "Pferd": (32, 1, 16, 14),
    "Tiger": (48, 0, 16, 14),
    "Schlange": (64, 1, 16, 15),
    "Zoowärter": (0, 16, 15, 16),
    "Cheese": (0, 35, 8, 6),
    "Bird": (17, 35, 13, 12),
    "Straw": (32, 32, 8, 10),
    "Steak": (45, 32, 11, 14),
    "Hare": (64, 32, 11, 10)
}

# =============================================================================
# [ MAIN CLASS: GAME ENGINE ]
# -----------------------------------------------------------------------------
# The heart of the program. Manages state, logic, and rendering.
# This class handles the transition between the story and the active game.
# =============================================================================
class Game:
    def __init__(self):
        """
        Initializes the Pyxel window and global score variables.
        Loads the external resource file containing all graphics.
        """
        pyxel.init(
            Config.SCREEN_WIDTH, 
            Config.SCREEN_HEIGHT, 
            title=Config.TITLE,
            fps=Config.FPS
        )
        
        # Resource Loading
        try:
            pyxel.load("res.pyxres")
        except Exception as e:
            print(f"Asset Error: {e}")

        # Sound Definitions
        pyxel.sound(0).set("g3c4", "p", "7", "v", 5)       
        pyxel.sound(1).set("c3e3g3c4", "t", "7", "v", 10) 
        pyxel.sound(2).set("c2g1", "n", "7", "s", 20)     

        # Persistent Global State
        self.state = "STORY"
        self.story_page = 0
        self.char_index = 0 
        self.high_score = 0
        
        # Effects System
        self.particles = []
        
        # Start Gameplay Parameters
        self.reset_params()
        
        # Execute Main Loop
        pyxel.run(self.update, self.draw)

    def reset_params(self):
        """
        Resets level-specific progress while keeping the Highscore intact.
        Ensures a clean slate for the player upon restarting.
        """
        self.level = 0
        self.progress = 0
        self.lives = 3 
        self.total_score = 0
        self.game_over = False
        self.victory = False
        self.invincible_timer = 0 
        self.show_message = True 
        self.show_encouragement = False
        self.halfway_shown = False
        self.encouragement_text = ""
        self.compliments = ["Great!", "Amazing!", "Excellent!", "Fantastic!"]

        # Level Data Table:
        # (SpriteKey, FoodKey, PlayerSpeed, EnemyCount, FoodGoal, MapU, EnglishLabel)
        self.lv_data = [
            ("Maus", "Cheese", 2.2, 2, 8, 0, "Mouse"),
            ("Katze", "Bird", 3.0, 3, 10, 16, "Cat"),
            ("Pferd", "Straw", 4.0, 4, 12, 32, "Horse"),
            ("Tiger", "Steak", 5.0, 5, 14, 48, "Tiger"),
            ("Schlange", "Hare", 4.5, 5, 16, 64, "Snake")
        ]
        
        # Initial Coordinates
        self.px = 64
        self.py = 64 
        
        self.init_level_objects()

    def init_level_objects(self):
        """
        Generates enemies and food for the specific level difficulty.
        Enemies are given a random drift to prevent clumped movement patterns.
        """
        curr = self.lv_data[min(self.level, 4)]
        target_food = curr[1] 
        self.halfway_shown = False
        
        # Zookeeper Generation
        self.enemies = []
        for i in range(curr[3]):
            ex = random.randint(15, 110)
            ey = random.randint(15, 110)
            drift = random.uniform(-0.4, 0.4) 
            self.enemies.append([ex, ey, drift])
        
        # Food Generation (Smart Spawning)
        self.items = []
        for j in range(3):
            self.spawn_new_item(target_food)

    def spawn_new_item(self, food_type):
        """
        Uses quadrant logic to ensure food doesn't clump together.
        Ensures items spawn in different sectors of the map.
        """
        sec_x = random.choice([(10, 45), (80, 115)])
        sec_y = random.choice([(10, 45), (80, 115)])
        
        nx = random.randint(sec_x[0], sec_x[1])
        ny = random.randint(sec_y[0], sec_y[1])
        self.items.append({"x": nx, "y": ny, "type": food_type})

    def update(self):
        """
        The central update loop. Controls game flow and logic updates.
        Manages the transition between story, pause, and gameplay.
        """
        # Particle Lifetime Logic
        for p in self.particles[:]:
            p[2] -= 1
            if p[2] <= 0:
                self.particles.remove(p)

        # Restart Key
        if (self.game_over or self.victory) and pyxel.btnp(pyxel.KEY_R):
            self.state = "STORY"
            self.story_page = 0
            self.char_index = 0
            self.reset_params()
            return

        # Story Narrative Controller
        if self.state == "STORY":
            self.char_index += 2 
            if pyxel.btnp(pyxel.KEY_SPACE):
                self.story_page += 1
                self.char_index = 0
                if self.story_page >= 3: 
                    self.state = "PLAY"
            return

        # Pop-up Message Controller
        if self.show_message or self.show_encouragement:
            if pyxel.btnp(pyxel.KEY_SPACE) or pyxel.btnp(pyxel.KEY_RETURN):
                self.show_message = False
                self.show_encouragement = False
            return

        # Active Gameplay Logic
        if not self.game_over and not self.victory:
            
            # --- Handle Movement ---
            speed = self.lv_data[self.level][2]
            moving = False
            
            if pyxel.btn(pyxel.KEY_LEFT): 
                self.px -= speed
                moving = True
            if pyxel.btn(pyxel.KEY_RIGHT): 
                self.px += speed
                moving = True
            if pyxel.btn(pyxel.KEY_UP): 
                self.py -= speed
                moving = True
            if pyxel.btn(pyxel.KEY_DOWN): 
                self.py += speed
                moving = True
            
            # Visual Polish: Create trail particles if moving
            if moving and pyxel.frame_count % 3 == 0:
                self.particles.append([
                    self.px + Config.MAP_DISPLAY_X, 
                    self.py + Config.MAP_DISPLAY_Y, 
                    12 # Lifetime frames
                ])

            # Edge Detection (Boundaries)
            self.px = max(5, min(self.px, 123))
            self.py = max(5, min(self.py, 123))

            # --- Zookeeper Artificial Intelligence ---
            e_spd = 0.7 + (self.level * 0.2)
            for e in self.enemies:
                if e[0] < self.px: e[0] += (e_spd + e[2])
                else: e[0] -= (e_spd + e[2])
                
                if e[1] < self.py: e[1] += (e_spd - e[2])
                else: e[1] -= (e_spd - e[2])
                
                # Collision Check
                if abs(self.px - e[0]) < 8 and abs(self.py - e[1]) < 8:
                    if self.invincible_timer == 0:
                        self.handle_player_damage()

            # --- Nutrient Collection ---
            for item in self.items:
                if abs(self.px - item["x"]) < 8 and abs(self.py - item["y"]) < 8:
                    self.progress += 1
                    self.total_score += (self.level + 1) * 20
                    pyxel.play(0, 0)
                    
                    # Update Highscore internally
                    if self.total_score > self.high_score:
                        self.high_score = self.total_score
                    
                    # Check for halfway point
                    goal = self.lv_data[self.level][4]
                    if self.progress == goal // 2 and not self.halfway_shown:
                        self.show_encouragement = True
                        self.halfway_shown = True
                        self.encouragement_text = random.choice(self.compliments)
                    
                    # Check for evolution
                    if self.progress >= goal: 
                        self.handle_evolution_event()
                    
                    # Respawn Item
                    sx = random.choice([(10, 45), (80, 115)])
                    sy = random.choice([(10, 45), (80, 115)])
                    item["x"] = random.randint(sx[0], sx[1])
                    item["y"] = random.randint(sy[0], sy[1])

            # Safety Flash Timer
            if self.invincible_timer > 0:
                self.invincible_timer -= 1

    def handle_player_damage(self):
        """Processes contact with enemies. Resets position and starts invincibility."""
        self.lives -= 1
        pyxel.play(2, 2)
        if self.lives <= 0:
            self.game_over = True
        else:
            self.px, self.py = 64, 64
            self.invincible_timer = 60 

    def handle_evolution_event(self):
        """Advances the animal to the next stage of its mutation."""
        if self.level == 4:
            self.victory = True
        else:
            self.level += 1
            self.progress = 0
            self.lives = 3 
            self.show_message = True 
            self.init_level_objects()
            pyxel.play(1, 1)

    # -------------------------------------------------------------------------
    # [ DRAWING AND RENDERING ]
    # -------------------------------------------------------------------------
    def draw(self):
        """
        Renders the screen based on the current state.
        Handles the background, the map, and the HUD.
        """
        pyxel.cls(0)
        
        # --- STORY DRAWING ---
        if self.state == "STORY":
            slides = [
                ["THE ACCIDENT:", "A chemical spill in the mouse cage.", 
                 "Uncontrolled mutations have begun.", "I must survive..."],
                ["THE TRANSFORMATION:", "My DNA is rewriting itself.", 
                 "I need nutrients to stabilize.", "But the Zookeepers are here!"],
                ["THE ESCAPE:", "I will find my way out of this lab.", 
                 "Through the sewers and into freedom.", "Press SPACE to start."]
            ]
            y_pos = 40
            for line in slides[self.story_page]:
                pyxel.text(30, y_pos, line[:self.char_index], 7)
                y_pos += 12
            pyxel.text(70, 140, "Press SPACE to continue", 5)
            return

        # --- MAP RENDERING ---
        u_coord = self.lv_data[self.level][5]
        pyxel.bltm(
            Config.MAP_DISPLAY_X, 
            Config.MAP_DISPLAY_Y, 
            0, u_coord * 8, 0, 128, 128
        )
        
        # --- PARTICLE RENDER ---
        for p in self.particles:
            color = 7 if p[2] > 6 else 6
            pyxel.pset(p[0], p[1], color)

        # --- ENTITY DRAWING ---
        ox, oy = Config.MAP_DISPLAY_X, Config.MAP_DISPLAY_Y
        
        # Items
        for item in self.items:
            s = SPRITE_MAP[item["type"]]
            pyxel.blt(item["x"]+ox-4, item["y"]+oy-4, 0, s[0], s[1], s[2], s[3], 2)
        
        # Enemies
        z = SPRITE_MAP["Zoowärter"]
        for e in self.enemies: 
            pyxel.blt(e[0]+ox-7, e[1]+oy-8, 0, z[0], z[1], z[2], z[3], 2)
        
        # Player (with invincible blinking)
        if self.invincible_timer % 4 < 2:
            p_spr = SPRITE_MAP[self.lv_data[self.level][0]]
            pyxel.blt(self.px+ox-8, self.py+oy-7, 0, p_spr[0], p_spr[1], p_spr[2], p_spr[3], 2)

        # --- HEADS UP DISPLAY (HUD) ---
        pyxel.rect(0, 0, 220, 20, 0)
        pyxel.text(5, 5, f"LIVES: {'<3 '*self.lives}", 8)
        
        # Center HUD: Animal and needed food
        food_name = self.lv_data[self.level][1].upper()
        pyxel.text(75, 5, f"ANIMAL: {self.lv_data[self.level][6]}", 7)
        pyxel.text(75, 12, f"NEED: {food_name}", 11)
        
        # Right HUD: Progress and Current Score
        pyxel.text(145, 5, f"SCORE: {self.total_score}", 10)
        pyxel.text(145, 12, f"GOAL: {self.progress}/{self.lv_data[self.level][4]}", 5)

        # --- OVERLAY BOXES ---
        if self.show_encouragement:
            pyxel.rect(65, 60, 95, 45, 11)
            pyxel.rectb(65, 60, 95, 45, 7)
            pyxel.text(90, 75, self.encouragement_text, 0)
            pyxel.text(70, 90, "Space to Continue", 1)
        
        if self.show_message:
            msgs = [
                ["LAB ESCAPE:", "Find your way out of the lab,", "into the canalisation!"],
                ["EVOLUTION:", "Evolute into the next animal!"],
                ["PROGRESS:", "Great, try to reach your", "next evolution!"],
                ["POWER GROWING:", "The zookeepers are alert.", "Keep eating to survive!"],
                ["FINAL FORM:", "You reached the most powerful", "but most difficult evolution.", "Can you fully escape?"]
            ]
            pyxel.rect(20, 45, 180, 70, 1)
            pyxel.rectb(20, 45, 180, 70, 7)
            for i, line in enumerate(msgs[self.level]): 
                pyxel.text(30, 55+(i*12), line, 7)
            pyxel.text(65, 100, "Press SPACE to continue", 6)

        # --- GAME STATE SCREENS ---
        # Highscore is only revealed here at the end of the game cycle.
        if self.game_over:
            pyxel.rect(50, 60, 120, 55, 0)
            pyxel.rectb(50, 60, 120, 55, 8)
            pyxel.text(75, 70, "GAME OVER", 8)
            pyxel.text(68, 80, f"FINAL SCORE: {self.total_score}", 7)
            pyxel.text(68, 90, f"BEST SCORE:  {self.high_score}", 10)
            pyxel.text(72, 105, "Press R to Retry", 5)
            
        if self.victory:
            pyxel.rect(50, 60, 120, 55, 0)
            pyxel.rectb(50, 60, 120, 55, 11)
            pyxel.text(65, 70, "FREEDOM AT LAST!", 11)
            pyxel.text(68, 80, f"FINAL SCORE: {self.total_score}", 7)
            pyxel.text(68, 90, f"BEST SCORE:  {self.high_score}", 10)
            pyxel.text(72, 105, "Press R to Restart", 5)

# =============================================================================
# [ LAUNCHER ]
# -----------------------------------------------------------------------------
# Standard Python entry point to execute the game class.
# =============================================================================
if __name__ == "__main__":
    Game()

# =============================================================================
# [ END OF SOURCE CODE ]
# -----------------------------------------------------------------------------
# Rise of Instinct | Created for Evolution Game Assignment
# Final Revision: Snake food renamed to Hare | End-game Highscore display added
# Total Line Count: 450+ lines
# =============================================================================