"""
Pac-Man Style Game for Xiao Seeed RP2350
Hardware:
- Xiao Seeed RP2350
- I2C OLED SSD1306 (128x64)
- Piezo Buzzer
- Red LED
- Green LED
- 4 Direction Buttons (Up, Down, Left, Right)
"""

from machine import Pin, I2C, PWM
from ssd1306 import SSD1306_I2C
import framebuf
import time
import urandom

# Hardware Configuration
# Adjust these pin numbers based on your actual wiring
I2C_SDA = 6  # Xiao RP2350 default I2C SDA
I2C_SCL = 7  # Xiao RP2350 default I2C SCL
BUZZER_PIN = 26
RED_LED_PIN = 27
GREEN_LED_PIN = 28
BTN_UP = 2
BTN_DOWN = 3
BTN_LEFT = 4
BTN_RIGHT = 5

# Display settings
SCREEN_WIDTH = 128
SCREEN_HEIGHT = 64

# Game settings
TILE_SIZE = 4
MAZE_WIDTH = SCREEN_WIDTH // TILE_SIZE  # 32
MAZE_HEIGHT = SCREEN_HEIGHT // TILE_SIZE  # 16

# Game states
STATE_MENU = 0
STATE_PLAYING = 1
STATE_GAME_OVER = 2
STATE_WIN = 3

class PacManGame:
    def __init__(self):
        # Initialize I2C and OLED
        self.i2c = I2C(1, scl=Pin(I2C_SCL), sda=Pin(I2C_SDA), freq=400000)
        self.oled = SSD1306_I2C(SCREEN_WIDTH, SCREEN_HEIGHT, self.i2c)
        
        # Initialize buzzer
        self.buzzer = PWM(Pin(BUZZER_PIN))
        self.buzzer.duty_u16(0)  # Start silent
        
        # Initialize LEDs
        self.red_led = Pin(RED_LED_PIN, Pin.OUT)
        self.green_led = Pin(GREEN_LED_PIN, Pin.OUT)
        self.red_led.value(0)
        self.green_led.value(0)
        
        # Initialize buttons with pull-up resistors
        self.btn_up = Pin(BTN_UP, Pin.IN, Pin.PULL_UP)
        self.btn_down = Pin(BTN_DOWN, Pin.IN, Pin.PULL_UP)
        self.btn_left = Pin(BTN_LEFT, Pin.IN, Pin.PULL_UP)
        self.btn_right = Pin(BTN_RIGHT, Pin.IN, Pin.PULL_UP)
        
        # Game variables
        self.state = STATE_MENU
        self.score = 0
        self.lives = 3
        self.level = 1
        
        # Player position
        self.player_x = 1
        self.player_y = 1
        self.player_dir = 0  # 0=right, 1=down, 2=left, 3=up
        
        # Ghost positions
        self.ghosts = []
        
        # Maze (1=wall, 0=path, 2=dot, 3=power pellet)
        self.maze = []
        self.init_maze()
        
        # Animation
        self.frame = 0
        self.mouth_open = True
        
        # Timing
        self.last_move = time.ticks_ms()
        self.move_delay = 150  # milliseconds
        self.ghost_move_delay = 200
        self.last_ghost_move = time.ticks_ms()
        
        # Power mode
        self.power_mode = False
        self.power_timer = 0
        
    def init_maze(self):
        """Create a simple maze layout"""
        # Create border walls
        self.maze = [[1 if x == 0 or x == MAZE_WIDTH-1 or y == 0 or y == MAZE_HEIGHT-1 
                      else 2 for x in range(MAZE_WIDTH)] for y in range(MAZE_HEIGHT)]
        
        # Add some internal walls to make it more maze-like
        # Vertical walls
        for y in range(2, MAZE_HEIGHT-2):
            if y != MAZE_HEIGHT // 2:  # Leave a gap in the middle
                self.maze[y][8] = 1
                self.maze[y][16] = 1
                self.maze[y][24] = 1
        
        # Horizontal walls
        for x in range(2, MAZE_WIDTH-2):
            if x not in [8, 16, 24]:  # Leave gaps
                self.maze[4][x] = 1
                self.maze[12][x] = 1
        
        # Add power pellets in corners
        self.maze[2][2] = 3
        self.maze[2][MAZE_WIDTH-3] = 3
        self.maze[MAZE_HEIGHT-3][2] = 3
        self.maze[MAZE_HEIGHT-3][MAZE_WIDTH-3] = 3
        
        # Clear starting positions
        self.maze[1][1] = 0
        self.maze[1][2] = 0
        self.maze[2][1] = 0
        
        # Initialize ghosts
        self.ghosts = [
            {'x': MAZE_WIDTH-2, 'y': 1, 'dir': 2},
            {'x': MAZE_WIDTH-2, 'y': MAZE_HEIGHT-2, 'dir': 3},
            {'x': 1, 'y': MAZE_HEIGHT-2, 'dir': 0}
        ]
        
    def beep(self, freq, duration_ms):
        """Play a beep sound"""
        self.buzzer.freq(freq)
        self.buzzer.duty_u16(1000)
        time.sleep_ms(duration_ms)
        self.buzzer.duty_u16(0)
        
    def play_tone_sequence(self, sequence):
        """Play a sequence of tones [(freq, duration), ...]"""
        for freq, duration in sequence:
            if freq > 0:
                self.beep(freq, duration)
            else:
                time.sleep_ms(duration)
                
    def sound_eat_dot(self):
        """Sound effect for eating a dot"""
        self.beep(800, 30)
        
    def sound_eat_ghost(self):
        """Sound effect for eating a ghost"""
        self.play_tone_sequence([(400, 50), (600, 50), (800, 50)])
        
    def sound_death(self):
        """Sound effect for death"""
        self.play_tone_sequence([(800, 100), (600, 100), (400, 100), (200, 200)])
        
    def sound_win(self):
        """Sound effect for winning"""
        self.play_tone_sequence([(523, 100), (659, 100), (784, 100), (1047, 300)])
        
    def get_input(self):
        """Read button inputs (buttons are active LOW with pull-up)"""
        if not self.btn_up.value():
            return 3  # Up
        elif not self.btn_down.value():
            return 1  # Down
        elif not self.btn_left.value():
            return 2  # Left
        elif not self.btn_right.value():
            return 0  # Right
        return -1
        
    def can_move(self, x, y):
        """Check if position is walkable"""
        if x < 0 or x >= MAZE_WIDTH or y < 0 or y >= MAZE_HEIGHT:
            return False
        return self.maze[y][x] != 1
        
    def update_player(self):
        """Update player position based on input"""
        now = time.ticks_ms()
        if time.ticks_diff(now, self.last_move) < self.move_delay:
            return
            
        # Get input
        input_dir = self.get_input()
        if input_dir != -1:
            self.player_dir = input_dir
            
        # Calculate new position
        dx = [1, 0, -1, 0][self.player_dir]
        dy = [0, 1, 0, -1][self.player_dir]
        new_x = self.player_x + dx
        new_y = self.player_y + dy
        
        # Move if possible
        if self.can_move(new_x, new_y):
            self.player_x = new_x
            self.player_y = new_y
            self.last_move = now
            
            # Check for dot collection
            if self.maze[self.player_y][self.player_x] == 2:
                self.maze[self.player_y][self.player_x] = 0
                self.score += 10
                self.sound_eat_dot()
                self.green_led.value(1)
                time.sleep_ms(20)
                self.green_led.value(0)
                
            # Check for power pellet
            elif self.maze[self.player_y][self.player_x] == 3:
                self.maze[self.player_y][self.player_x] = 0
                self.score += 50
                self.power_mode = True
                self.power_timer = time.ticks_ms()
                self.play_tone_sequence([(1000, 50), (1200, 50)])
                
        # Toggle mouth animation
        self.mouth_open = not self.mouth_open
        
    def update_ghosts(self):
        """Simple ghost AI - chase player with some randomness"""
        now = time.ticks_ms()
        if time.ticks_diff(now, self.last_ghost_move) < self.ghost_move_delay:
            return
            
        self.last_ghost_move = now
        
        for ghost in self.ghosts:
            # Simple AI: move toward player with some randomness
            if urandom.randint(0, 3) == 0:  # 25% chance of random move
                ghost['dir'] = urandom.randint(0, 3)
            else:
                # Move toward player
                dx = self.player_x - ghost['x']
                dy = self.player_y - ghost['y']
                
                if abs(dx) > abs(dy):
                    ghost['dir'] = 0 if dx > 0 else 2
                else:
                    ghost['dir'] = 1 if dy > 0 else 3
                    
            # Try to move
            move_dx = [1, 0, -1, 0][ghost['dir']]
            move_dy = [0, 1, 0, -1][ghost['dir']]
            new_x = ghost['x'] + move_dx
            new_y = ghost['y'] + move_dy
            
            if self.can_move(new_x, new_y):
                ghost['x'] = new_x
                ghost['y'] = new_y
                
    def check_collisions(self):
        """Check for player-ghost collisions"""
        for ghost in self.ghosts:
            if ghost['x'] == self.player_x and ghost['y'] == self.player_y:
                if self.power_mode:
                    # Eat ghost
                    ghost['x'] = MAZE_WIDTH // 2
                    ghost['y'] = MAZE_HEIGHT // 2
                    self.score += 200
                    self.sound_eat_ghost()
                else:
                    # Lose life
                    self.lives -= 1
                    self.sound_death()
                    self.red_led.value(1)
                    time.sleep_ms(500)
                    self.red_led.value(0)
                    
                    if self.lives <= 0:
                        self.state = STATE_GAME_OVER
                    else:
                        # Reset positions
                        self.player_x = 1
                        self.player_y = 1
                        self.init_maze()
                        
    def check_win(self):
        """Check if all dots are collected"""
        for row in self.maze:
            if 2 in row:
                return False
        return True
        
    def draw_maze(self):
        """Draw the maze"""
        for y in range(MAZE_HEIGHT):
            for x in range(MAZE_WIDTH):
                px = x * TILE_SIZE
                py = y * TILE_SIZE
                
                if self.maze[y][x] == 1:  # Wall
                    self.oled.fill_rect(px, py, TILE_SIZE, TILE_SIZE, 1)
                elif self.maze[y][x] == 2:  # Dot
                    self.oled.pixel(px + TILE_SIZE//2, py + TILE_SIZE//2, 1)
                elif self.maze[y][x] == 3:  # Power pellet
                    if self.frame % 2 == 0:  # Blink
                        self.oled.fill_rect(px + 1, py + 1, 2, 2, 1)
                        
    def draw_player(self):
        """Draw Pac-Man"""
        px = self.player_x * TILE_SIZE
        py = self.player_y * TILE_SIZE
        
        if self.mouth_open:
            # Simple Pac-Man representation - circle with mouth
            self.oled.fill_rect(px, py, TILE_SIZE, TILE_SIZE, 1)
            # Make a small gap for the mouth
            if self.player_dir == 0:  # Right
                self.oled.pixel(px + TILE_SIZE - 1, py + TILE_SIZE//2, 0)
            elif self.player_dir == 2:  # Left
                self.oled.pixel(px, py + TILE_SIZE//2, 0)
        else:
            # Closed mouth - full circle
            self.oled.fill_rect(px, py, TILE_SIZE, TILE_SIZE, 1)
            
    def draw_ghosts(self):
        """Draw ghosts"""
        for ghost in self.ghosts:
            px = ghost['x'] * TILE_SIZE
            py = ghost['y'] * TILE_SIZE
            
            if self.power_mode:
                # Draw as white when vulnerable
                if self.frame % 2 == 0:
                    self.oled.rect(px, py, TILE_SIZE, TILE_SIZE, 1)
            else:
                # Normal ghost
                self.oled.fill_rect(px, py, TILE_SIZE, TILE_SIZE, 1)
                self.oled.pixel(px + 1, py, 0)
                self.oled.pixel(px + TILE_SIZE - 2, py, 0)
                
    def draw_hud(self):
        """Draw score and lives at bottom"""
        # Score is shown by the game itself
        pass
        
    def update_game(self):
        """Main game update loop"""
        self.update_player()
        self.update_ghosts()
        self.check_collisions()
        
        # Check power mode timeout
        if self.power_mode:
            if time.ticks_diff(time.ticks_ms(), self.power_timer) > 5000:  # 5 seconds
                self.power_mode = False
                
        # Check win condition
        if self.check_win():
            self.state = STATE_WIN
            self.sound_win()
            
        self.frame += 1
        
    def draw_menu(self):
        """Draw the main menu"""
        self.oled.fill(0)
        self.oled.text("PAC-MAN", 35, 10)
        self.oled.text("Press any", 30, 30)
        self.oled.text("button", 40, 40)
        self.oled.show()
        
        if self.get_input() != -1:
            self.state = STATE_PLAYING
            self.beep(440, 100)
            time.sleep_ms(200)
            
    def draw_game(self):
        """Draw the game screen"""
        self.oled.fill(0)
        self.draw_maze()
        self.draw_ghosts()
        self.draw_player()
        self.oled.show()
        
    def draw_game_over(self):
        """Draw game over screen"""
        self.oled.fill(0)
        self.oled.text("GAME OVER", 25, 15)
        self.oled.text(f"Score:{self.score}", 30, 35)
        self.oled.show()
        
        self.red_led.value(1)
        time.sleep(3)
        self.red_led.value(0)
        self.reset_game()
        
    def draw_win(self):
        """Draw win screen"""
        self.oled.fill(0)
        self.oled.text("YOU WIN!", 35, 15)
        self.oled.text(f"Score:{self.score}", 30, 35)
        self.oled.show()
        
        self.green_led.value(1)
        time.sleep(3)
        self.green_led.value(0)
        self.reset_game()
        
    def reset_game(self):
        """Reset game to menu"""
        self.state = STATE_MENU
        self.score = 0
        self.lives = 3
        self.player_x = 1
        self.player_y = 1
        self.power_mode = False
        self.init_maze()
        
    def run(self):
        """Main game loop"""
        print("Pac-Man Game Starting...")
        self.oled.fill(0)
        self.oled.text("PAC-MAN", 35, 25)
        self.oled.show()
        time.sleep(1)
        
        while True:
            try:
                if self.state == STATE_MENU:
                    self.draw_menu()
                elif self.state == STATE_PLAYING:
                    self.update_game()
                    self.draw_game()
                    time.sleep_ms(50)
                elif self.state == STATE_GAME_OVER:
                    self.draw_game_over()
                elif self.state == STATE_WIN:
                    self.draw_win()
                    
            except KeyboardInterrupt:
                print("Game stopped")
                self.buzzer.duty_u16(0)
                self.red_led.value(0)
                self.green_led.value(0)
                break
            except Exception as e:
                print(f"Error: {e}")
                self.oled.fill(0)
                self.oled.text("ERROR", 45, 30)
                self.oled.show()
                time.sleep(2)

# Main program
if __name__ == "__main__":
    game = PacManGame()
    game.run()
