[Server] Exception in game loop: name 'game_state_lock' is not defined


import pygame
import sys
import threading
import socket
import json
import random
from pygame import mixer

# Increase the recursion limit; use with caution
sys.setrecursionlimit(10000)

# Initialize Pygame
pygame.init()
mixer.init()

# Screen dimensions
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Maze Explorers Online")

# Colors
WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
GOLDEN = (255, 215, 0)

# Player settings
player_size = 40
player_speed = 5

# Tile settings
TILE_SIZE = 40  # Size of each tile in pixels
goal_size = 40  # Size of the goal square

# Font settings
font_size = 30
font_name = pygame.font.match_font('arial')  # Change 'arial' to any installed font
font = pygame.font.Font(font_name, font_size)

# Sound settings
# Load the sound (ensure these files exist in your working directory)
try:
    goal_sound = mixer.Sound('goal.wav')
    fireworks_sound = mixer.Sound('fireworks.mp3')
    title_music = 'title_screen.mp3'
    in_game_music = 'in_game.mp3'
    game_over_music = 'game_over.mp3'
except Exception as e:
    print("Error loading sound files:", e)
    goal_sound = None
    fireworks_sound = None
    title_music = None
    in_game_music = None
    game_over_music = None

# Networking settings
class NetworkThread(threading.Thread):
    def __init__(self, is_server, host, port, game_state, level_event, lock):
        threading.Thread.__init__(self)
        self.is_server = is_server
        self.host = host
        self.port = port
        self.game_state = game_state  # Shared game state
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connection = None
        self.running = True
        self.connected = threading.Event()  # Event to signal that the connection is established
        self.level_event = level_event  # Event to signal level advancement
        self.lock = lock  # Shared lock for thread-safe operations

    def run(self):
        buffer = ''
        try:
            if self.is_server:
                self.sock.bind((self.host, self.port))
                self.sock.listen(1)
                print(f"[Server] Listening on {self.host}:{self.port}")
                self.connection, addr = self.sock.accept()
                print(f"[Server] Client connected from {addr}")
                self.connected.set()  # Signal that the connection is established
            else:
                while self.running:
                    try:
                        self.sock.connect((self.host, self.port))
                        self.connection = self.sock
                        print(f"[Client] Connected to server at {self.host}:{self.port}")
                        self.connected.set()  # Signal that the connection is established
                        break
                    except socket.error as e:
                        print(f"[Client] Connection failed: {e}. Retrying in 1 second...")
                        pygame.time.wait(1000)  # Wait 1 second before retrying

            while self.running:
                try:
                    data = self.connection.recv(4096).decode()
                    if data:
                        buffer += data
                        while '\n' in buffer:
                            line, buffer = buffer.split('\n', 1)
                            if line:
                                try:
                                    remote_game_state = json.loads(line)
                                    print(f"[NetworkThread] Received data: {remote_game_state}")
                                    with self.lock:
                                        if self.is_server:
                                            # Server receives data from client
                                            self.game_state['opponent_pos'] = remote_game_state.get('player_pos', self.game_state.get('opponent_pos'))
                                            self.game_state['opponent_score'] = remote_game_state.get('player_score', self.game_state.get('opponent_score', 0))

                                            # Handle level advancement request
                                            if remote_game_state.get('request_level_advance'):
                                                self.game_state['level_advance_requested'] = True
                                                print("[NetworkThread] Level advancement requested by client.")
                                        else:
                                            # Client receives game state updates from server
                                            # Update opponent data
                                            self.game_state['opponent_pos'] = remote_game_state.get('player_pos', self.game_state.get('opponent_pos'))
                                            self.game_state['opponent_score'] = remote_game_state.get('player_score', self.game_state.get('opponent_score', 0))

                                            # Update other keys without overwriting player's own data
                                            for key in remote_game_state:
                                                if key not in ['player_pos', 'player_score']:
                                                    self.game_state[key] = remote_game_state[key]
                                except json.JSONDecodeError as e:
                                    print(f"[NetworkThread] JSON decode error: {e} in line: {line}")
                    else:
                        # Connection closed
                        print("[NetworkThread] Connection closed by the other side.")
                        self.running = False
                except Exception as e:
                    print(f"[NetworkThread] Networking error: {e}")
                    self.running = False
                    break
        except Exception as e:
            print(f"[NetworkThread] Exception: {e}")
        finally:
            if self.connection:
                self.connection.close()
            self.sock.close()
            print("[NetworkThread] Socket closed.")

    def send_data(self, data):
        try:
            if self.connection and self.running:
                message = json.dumps(data) + '\n'
                self.connection.sendall(message.encode())
                print(f"[NetworkThread] Sent data: {data}")
        except Exception as e:
            print(f"[NetworkThread] Error sending data: {e}")
            self.running = False

    def stop(self):
        self.running = False
        if self.connection:
            self.connection.close()
        self.sock.close()

# Function to render text with outline
def draw_text_with_outline(surface, text, text_color, outline_color, x, y):
    # Create the base text surface
    text_surface = font.render(text, True, text_color)
    text_rect = text_surface.get_rect()
    text_rect.topleft = (x, y)

    # Offsets for the outline
    outline_offset = 2

    # Draw the outline by rendering the text in the outline color with slight offsets
    for dx in [-outline_offset, 0, outline_offset]:
        for dy in [-outline_offset, 0, outline_offset]:
            if dx != 0 or dy != 0:  # Avoid redrawing the base text here
                outline_rect = text_surface.get_rect()
                outline_rect.topleft = (x + dx, y + dy)
                surface.blit(font.render(text, True, outline_color), outline_rect)

    # Draw the base text
    surface.blit(text_surface, text_rect)

# Function to fade in/out a surface
def fade_surface(surface, fade_duration, fade_in=True, fade_color=(0, 0, 0)):
    alpha_start = 0 if fade_in else 255
    alpha_end = 255 if fade_in else 0
    step = 5  # Adjust the step for faster or slower fade

    fade_overlay = pygame.Surface((WIDTH, HEIGHT))
    fade_overlay.fill(fade_color)

    if abs(alpha_end - alpha_start) == 0:
        delay = fade_duration
    else:
        delay = int(fade_duration / (abs(alpha_end - alpha_start) / step))

    alpha_range = range(alpha_start, alpha_end + (step if fade_in else -step), step if fade_in else -step)
    for alpha in alpha_range:
        fade_overlay.set_alpha(alpha)
        surface.blit(fade_overlay, (0, 0))
        pygame.display.flip()
        pygame.time.delay(delay)

def splash_and_title_screen(surface):
    # Load and play the background music
    if title_music:
        mixer.music.load(title_music)
        mixer.music.set_volume(0.25)
        mixer.music.play(-1)  # The '-1' makes the music play in a loop

    # Splash screen
    surface.fill(BLACK)
    splash_font = pygame.font.Font(font_name, 50)
    text = splash_font.render("Exploratory Studios Presents", True, WHITE)
    text_rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2))
    surface.blit(text, text_rect)
    pygame.display.flip()
    pygame.time.wait(2000)

    fade_surface(surface, fade_duration=1000, fade_in=True)  # Fade in
    fade_surface(surface, fade_duration=1500, fade_in=True, fade_color=WHITE)  # Fade in

    # Title screen
    running = True

    # Load background maze for the title screen
    maze_width, maze_height = 20, 20  # Adjust these values as needed
    maze = generate_maze(maze_width, maze_height)
    maze_offset_x, maze_offset_y = 0, 0
    scroll_speed = 1.4  # Speed of the maze scrolling

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RETURN:
                    mixer.music.stop()
                    running = False

        surface.fill(WHITE)

        # Draw the scrolling maze
        for y, row in enumerate(maze):
            for x, tile in enumerate(row):
                if tile == 1:
                    tile_x = x * TILE_SIZE + maze_offset_x
                    tile_y = y * TILE_SIZE
                    pygame.draw.rect(surface, BLACK, (tile_x % WIDTH, tile_y % HEIGHT, TILE_SIZE, TILE_SIZE))

        maze_offset_x -= scroll_speed
        if maze_offset_x <= -maze_width * TILE_SIZE:
            maze_offset_x = 0

        # Title text
        title_font = pygame.font.Font(font_name, 70)
        title_text = "Maze Explorers Online"
        title_render = title_font.render(title_text, True, GOLDEN)
        title_rect = title_render.get_rect(center=(WIDTH // 2, HEIGHT // 3))
        draw_text_with_outline(surface, title_text, GOLDEN, BLACK, title_rect.left, title_rect.top)

        # Press Enter to Start
        start_font = pygame.font.Font(font_name, 30)
        start_text = "Press Enter"
        start_render = start_font.render(start_text, True, GOLDEN)
        start_rect = start_render.get_rect(center=(WIDTH // 2, HEIGHT // 2))
        draw_text_with_outline(surface, start_text, GOLDEN, BLACK, start_rect.left, start_rect.top)

        pygame.display.flip()
        pygame.time.Clock().tick(30)

def display_game_over_screen(surface, winner, winner_score):
    # Load and play game over music
    if game_over_music:
        mixer.music.load(game_over_music)
        mixer.music.set_volume(0.25)
        mixer.music.play(-1)

    # Game over screen settings
    running = True
    clock = pygame.time.Clock()

    # Fade in effect
    fade_surface(surface, fade_duration=2000, fade_in=True, fade_color=WHITE)

    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
                pygame.quit()
                sys.exit()
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RETURN:
                    mixer.music.stop()
                    running = False
                    main()

        surface.fill(WHITE)

        # Display Game Over text
        game_over_font = pygame.font.Font(font_name, 60)
        game_over_text = "Game Over"
        game_over_rect = game_over_font.render(game_over_text, True, GOLDEN).get_rect(center=(WIDTH // 2, HEIGHT // 3))
        draw_text_with_outline(surface, game_over_text, GOLDEN, BLACK, game_over_rect.left, game_over_rect.top)

        # Display Winner
        winner_font = pygame.font.Font(font_name, 40)
        winner_text = f"Winner: Player {winner} with {winner_score} points"
        winner_rect = winner_font.render(winner_text, True, BLACK).get_rect(center=(WIDTH // 2, HEIGHT // 2))
        draw_text_with_outline(surface, winner_text, BLACK, WHITE, winner_rect.left, winner_rect.top)

        # Press Enter to Restart
        restart_font = pygame.font.Font(font_name, 30)
        restart_text = "Press Enter to Restart"
        restart_rect = restart_font.render(restart_text, True, BLACK).get_rect(center=(WIDTH // 2, HEIGHT // 2 + 50))
        draw_text_with_outline(surface, restart_text, BLACK, WHITE, restart_rect.left, restart_rect.top)

        pygame.display.flip()
        clock.tick(30)

def generate_maze(width, height):
    # Initialize all cells as walls
    maze = [[1 for _ in range(width)] for _ in range(height)]

    # Starting position
    start_x, start_y = 1, 1
    maze[start_y][start_x] = 0

    # Function to carve out the maze
    def carve(x, y):
        directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        random.shuffle(directions)

        for dx, dy in directions:
            new_x, new_y = x + dx * 2, y + dy * 2
            if 0 <= new_x < width and 0 <= new_y < height and maze[new_y][new_x] == 1:
                maze[new_y][new_x] = 0
                maze[new_y - dy][new_x - dx] = 0
                carve(new_x, new_y)

    carve(start_x, start_y)

    # Add walls on the bottom and right edges
    for x in range(width):
        maze[height - 1][x] = 1
    for y in range(height):
        maze[y][width - 1] = 1

    return maze

def find_center_open_space():
    center_x = NUM_TILES_WIDTH // 2
    center_y = NUM_TILES_HEIGHT // 2

    radius = 0
    max_radius = max(NUM_TILES_WIDTH, NUM_TILES_HEIGHT)  # Maximum possible radius

    while radius <= max_radius:
        for dx in range(-radius, radius + 1):
            for dy in range(-radius, radius + 1):
                x = center_x + dx
                y = center_y + dy
                if 0 <= x < NUM_TILES_WIDTH and 0 <= y < NUM_TILES_HEIGHT:
                    if environment[y][x] == 0:
                        return x * TILE_SIZE, y * TILE_SIZE
        radius += 1

    # If no open space is found, default to starting position
    return center_x * TILE_SIZE, center_y * TILE_SIZE

def find_open_space_near(x, y):
    if 0 <= y < len(environment) and 0 <= x < len(environment[0]) and environment[y][x] == 0:
        return [x * TILE_SIZE, y * TILE_SIZE]
    else:
        radius = 1
        max_radius = max(NUM_TILES_WIDTH, NUM_TILES_HEIGHT)
        while radius <= max_radius:
            for dx in range(-radius, radius + 1):
                for dy in range(-radius, radius + 1):
                    nx = x + dx
                    ny = y + dy
                    if 0 <= ny < len(environment) and 0 <= nx < len(environment[0]):
                        if environment[ny][nx] == 0:
                            return [nx * TILE_SIZE, ny * TILE_SIZE]
            radius += 1
    # Default to the original position if no open space is found
    return [x * TILE_SIZE, y * TILE_SIZE]

def freeze_and_fade_out(surface, freeze_duration, fade_duration, fade_color):
    pygame.time.wait(freeze_duration)  # Freeze the game for a set duration
    fade_surface(surface, fade_duration, fade_in=True, fade_color=fade_color)  # Fade to the specified color

def calculate_distance(pos1, pos2):
    return ((pos1[0] - pos2[0]) ** 2 + (pos1[1] - pos2[1]) ** 2) ** 0.5

def check_goal_collision(player_pos, player_number):
    global player_score, opponent_score, goal_collected

    if goal_collected:
        return False  # Goal already collected in this level

    player_rect = pygame.Rect(player_pos[0], player_pos[1], player_size, player_size)
    goal_rect = pygame.Rect(center_pos[0], center_pos[1], goal_size, goal_size)
    if player_rect.colliderect(goal_rect):
        goal_collected = True  # Set the flag
        if goal_sound:
            goal_sound.play()
        current_time = pygame.time.get_ticks()
        time_taken = current_time - level_start_time
        score = max(1000 - time_taken // 100, 0)  # Example scoring formula

        if player_number == 1:
            player_score += score
            # Calculate score for opponent based on distance
            distance = calculate_distance(opponent_pos, center_pos) if opponent_pos else 0
            opponent_score += int(max(300 - distance // TILE_SIZE, 0))  # Example proximity scoring
            print(f"[check_goal_collision] Player 1 scored {score}. Opponent score updated by {int(max(300 - distance // TILE_SIZE, 0))}")
        else:
            player_score += score
            # Calculate score for opponent based on distance
            distance = calculate_distance(opponent_pos, center_pos) if opponent_pos else 0
            opponent_score += int(max(300 - distance // TILE_SIZE, 0))  # Example proximity scoring
            print(f"[check_goal_collision] Player 2 scored {score}. Opponent score updated by {int(max(300 - distance // TILE_SIZE, 0))}")

        return True
    return False

def is_collision_with_wall(x, y):
    # Convert the position to tile coordinates
    tile_x = int(x // TILE_SIZE)
    tile_y = int(y // TILE_SIZE)

    # Check for out-of-bounds
    if tile_x < 0 or tile_y < 0 or tile_x >= NUM_TILES_WIDTH or tile_y >= NUM_TILES_HEIGHT:
        return True

    # Check for collision with a wall tile
    return environment[tile_y][tile_x] == 1

def draw_environment(surface, player_pos, opponent_pos, player_color, opponent_color):
    # Draw the maze environment relative to the player's position (camera)
    for y, row in enumerate(environment):
        for x, tile in enumerate(row):
            if tile == 1:
                tile_x = x * TILE_SIZE - player_pos[0] + WIDTH // 2
                tile_y = y * TILE_SIZE - player_pos[1] + HEIGHT // 2
                pygame.draw.rect(surface, BLACK, (tile_x, tile_y, TILE_SIZE, TILE_SIZE))

    # Draw the goal square relative to the player's position
    goal_x = center_pos[0] - player_pos[0] + WIDTH // 2
    goal_y = center_pos[1] - player_pos[1] + HEIGHT // 2
    if not goal_collected:
        pygame.draw.rect(surface, GOLDEN, (goal_x, goal_y, goal_size, goal_size))

    # Check if opponent_pos is valid
    if opponent_pos is None:
        print("[Draw] Opponent position is None.")
    else:
        # Calculate opponent's position relative to the player's position (camera)
        opp_x = opponent_pos[0] - player_pos[0] + WIDTH // 2
        opp_y = opponent_pos[1] - player_pos[1] + HEIGHT // 2

        # Debug: Print opponent's relative position
        print(f"[Draw] Opponent relative position: ({opp_x}, {opp_y})")

        # Draw the opponent if within the visible screen
        if -player_size <= opp_x <= WIDTH and -player_size <= opp_y <= HEIGHT:
            pygame.draw.rect(surface, opponent_color, (opp_x, opp_y, player_size, player_size))
        else:
            print(f"[Draw] Opponent is out of bounds: ({opp_x}, {opp_y})")

    # Draw the local player at the center
    pygame.draw.rect(surface, player_color, (WIDTH // 2, HEIGHT // 2, player_size, player_size))

def increase_level():
    global current_level, NUM_TILES_WIDTH, NUM_TILES_HEIGHT, environment, player_pos, center_pos
    global level_start_time, game_state, goal_collected, player_score, game_state_lock

    current_level += 1
    goal_collected = False  # Reset the flag

    # Check if maximum level is exceeded
    if current_level > max_level:
        if fireworks_sound:
            fireworks_sound.play()
        freeze_and_fade_out(screen, 800, 1000, WHITE)
        display_game_over_screen(screen, player_number if player_score > opponent_score else (2 if player_number == 1 else 1), max(player_score, opponent_score))
        pygame.time.wait(5000)  # Display game over screen for 5 seconds
        pygame.quit()
        sys.exit()
        return

    # Freeze and fade out before changing levels
    freeze_and_fade_out(screen, 800, 1000, WHITE)

    # Regenerate the maze for the next level
    NUM_TILES_WIDTH += level_increment
    NUM_TILES_HEIGHT += level_increment

    # Generate the maze on the server
    environment = generate_maze(NUM_TILES_WIDTH, NUM_TILES_HEIGHT)
    print(f"[increase_level] Generated new maze with size {NUM_TILES_WIDTH}x{NUM_TILES_HEIGHT}")

    # Update the location of the goal square
    center_pos = find_center_open_space()
    print(f"[increase_level] New center position: {center_pos}")

    # Reset player positions
    if player_number == 1:
        player_pos = find_open_space_near(1, 1)
    else:
        player_pos = find_open_space_near(NUM_TILES_WIDTH - 2, NUM_TILES_HEIGHT - 2)

    print(f"[increase_level] Player position: {player_pos}")

    # Update game state
    with game_state_lock:
        game_state['environment'] = environment
        game_state['center_pos'] = center_pos
        game_state['level'] = current_level
        game_state['player_pos'] = player_pos  # Update your own position
        game_state['player_score'] = player_score
        game_state['goal_collected'] = goal_collected  # Update the flag
        game_state['level_advanced'] = True  # Indicate that level has advanced

    # Reset level start time for the new level
    level_start_time = pygame.time.get_ticks()

    # Fade in for the new level
    fade_surface(screen, 1000, fade_in=True, fade_color=WHITE)
    print(f"[increase_level] Level {current_level} started.")

def main():
    global player_pos, opponent_pos, player_score, opponent_score, center_pos, current_level, level_start_time
    global NUM_TILES_WIDTH, NUM_TILES_HEIGHT, environment, player_number, is_server, game_state, goal_collected

    # Initialize level settings
    global max_level, level_increment
    max_level = 5  # Maximum level
    level_increment = 4  # Increase in map size per level
    level_start_time = pygame.time.get_ticks()

    # Initialize threading lock
    game_state_lock = threading.Lock()

    # Networking setup
    is_server = False
    host = 'localhost'
    port = 4096  # Default port

    choice = input("Do you want to host the game? (y/n): ").lower()
    if choice == 'y':
        is_server = True
        host = '0.0.0.0'  # Listen on all interfaces
        port_input = input("Enter the port number to host the game (default 4096): ")
        try:
            port = int(port_input)
            if not (0 < port < 65536):
                raise ValueError
        except ValueError:
            print("Invalid port number. Using default port 4096.")
            port = 4096
    else:
        host = input("Enter the server's IP address: ")
        port_input = input("Enter the server's port number (default 4096): ")
        try:
            port = int(port_input)
            if not (0 < port < 65536):
                raise ValueError
        except ValueError:
            print("Invalid port number. Using default port 4096.")
            port = 4096

    # Initialize the game state
    current_level = 1
    NUM_TILES_WIDTH = 15
    NUM_TILES_HEIGHT = 15

    level_event = threading.Event()  # Event to signal level advancement

    goal_collected = False  # Initialize the flag

    player_score = 0
    opponent_score = 0

    if is_server:
        # Generate the maze on the server
        environment = generate_maze(NUM_TILES_WIDTH, NUM_TILES_HEIGHT)
        print(f"[Server] Generated maze with size {NUM_TILES_WIDTH}x{NUM_TILES_HEIGHT}")

        # Set the center position
        center_pos = find_center_open_space()
        print(f"[Server] Center position: {center_pos}")

        # Ensure center_pos is not None
        if center_pos is None:
            center_pos = (NUM_TILES_WIDTH // 2 * TILE_SIZE, NUM_TILES_HEIGHT // 2 * TILE_SIZE)
            print(f"[Server] Center position was None, defaulted to: {center_pos}")

        # Determine player number and starting positions
        player_number = 1
        player_pos = find_open_space_near(1, 1)  # Player 1 near top left corner
        opponent_pos = None  # Will be received from client
        player_color = RED
        opponent_color = BLUE

        # Initialize the game state
        game_state = {
            'level': current_level,
            'center_pos': center_pos,
            'environment': environment,
            'opponent_pos': None,  # Will receive client's player_pos here
            'opponent_score': opponent_score,
            'player_pos': player_pos,  # Server's player position
            'player_score': player_score,
            'level_advanced': False,
            'level_advance_requested': False,
            'goal_collected': goal_collected
        }
    else:
        # Client will receive the maze from the server
        environment = None
        center_pos = None
        # Determine player number and starting positions
        player_number = 2
        player_pos = None  # Will be set after receiving game state
        opponent_pos = None  # Will be set after receiving game state
        player_color = BLUE
        opponent_color = RED
        # Initialize the game state
        game_state = {
            'level': None,
            'center_pos': None,
            'environment': None,
            'opponent_pos': None,  # Will receive server's player_pos here
            'opponent_score': opponent_score,
            'player_pos': None,  # Client's player position
            'player_score': player_score,
            'level_advanced': False,
            'level_advance_requested': False,
            'goal_collected': goal_collected
        }

    network_thread = NetworkThread(is_server, host, port, game_state, level_event, game_state_lock)
    network_thread.start()

    # Wait until the network connection is established
    print("Waiting for network connection...")
    network_thread.connected.wait()
    print("Network connection established.")

    if is_server:
        # Send initial game state to client
        initial_game_state = {
            'level': current_level,
            'center_pos': center_pos,
            'environment': environment,
            'opponent_pos': player_pos,  # Server's player position (opponent to client)
            'opponent_score': player_score,
            'goal_collected': goal_collected
        }
        network_thread.send_data(initial_game_state)
        print(f"[Server] Sent initial game state.")

        # Wait for the client to send its initial data
        print("Waiting for client's initial data...")
        while True:
            with game_state_lock:
                if game_state.get('opponent_pos') is not None:
                    opponent_pos = game_state['opponent_pos']
                    opponent_score = game_state['opponent_score']
                    break
            pygame.time.wait(100)
        print("Client's initial data received.")
    else:
        # Wait for initial game state from the server
        print("Waiting for initial game state from the server...")
        while True:
            with game_state_lock:
                if game_state['environment'] is not None and game_state['center_pos'] is not None and game_state['opponent_pos'] is not None:
                    break
            pygame.time.wait(100)
        print("Initial game state received.")

        # Debug: Print the received game state
        print(f"[Client] Received game state.")

        # Set up the game state based on received data
        with game_state_lock:
            environment = game_state['environment']
            center_pos = tuple(game_state['center_pos'])
            current_level = game_state['level']
            NUM_TILES_WIDTH = len(environment[0])
            NUM_TILES_HEIGHT = len(environment)
            level_start_time = pygame.time.get_ticks()
            goal_collected = game_state['goal_collected']

            # Set player and opponent positions
            player_pos = find_open_space_near(NUM_TILES_WIDTH - 2, NUM_TILES_HEIGHT - 2)
            opponent_pos = game_state['opponent_pos']  # Server's player position

            opponent_score = game_state['opponent_score']

        print(f"[Client] Player position: {player_pos}")
        print(f"[Client] Opponent position: {opponent_pos}")

        # Ensure opponent_pos is not None
        if opponent_pos is None:
            print("[Client] Warning: Opponent position is None after receiving initial game state.")

        # Send initial player position to server
        initial_player_data = {
            'player_pos': player_pos,
            'player_score': player_score
            # Do NOT send 'goal_collected' from client
        }
        network_thread.send_data(initial_player_data)
        print(f"[Client] Sent initial player data.")

    # Fade in at the start of a new game
    fade_surface(screen, 1000, fade_in=True, fade_color=WHITE)  # Adjust fade duration and color as needed

    if in_game_music:
        mixer.music.load(in_game_music)
        mixer.music.set_volume(0.5)
        mixer.music.play(-1)  # The '-1' makes the music play in a loop

    try:
        # Game loop
        running = True
        clock = pygame.time.Clock()
        while running:
            if not network_thread.running:
                print("Network connection lost.")
                running = False
                break

            # Event handling
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                    network_thread.stop()
                    pygame.quit()
                    sys.exit()

            # Player movement
            keys = pygame.key.get_pressed()

            # Local Player Movement (Arrow Keys)
            if keys[pygame.K_UP]:
                new_y = player_pos[1] - player_speed
                if not is_collision_with_wall(player_pos[0], new_y) and not is_collision_with_wall(player_pos[0] + player_size - 1, new_y):
                    player_pos[1] = new_y
            if keys[pygame.K_DOWN]:
                new_y = player_pos[1] + player_speed
                if not is_collision_with_wall(player_pos[0], new_y + player_size - 1) and not is_collision_with_wall(player_pos[0] + player_size - 1, new_y + player_size - 1):
                    player_pos[1] = new_y
            if keys[pygame.K_LEFT]:
                new_x = player_pos[0] - player_speed
                if not is_collision_with_wall(new_x, player_pos[1]) and not is_collision_with_wall(new_x, player_pos[1] + player_size - 1):
                    player_pos[0] = new_x
            if keys[pygame.K_RIGHT]:
                new_x = player_pos[0] + player_speed
                if not is_collision_with_wall(new_x + player_size - 1, player_pos[1]) and not is_collision_with_wall(new_x + player_size - 1, player_pos[1] + player_size - 1):
                    player_pos[0] = new_x

            # Check for collision with goal square
            if check_goal_collision(player_pos, player_number):
                if is_server:
                    # Server handles level advancement
                    increase_level()
                    level_start_time = pygame.time.get_ticks()  # Reset the level start time
                    # Update game state
                    with game_state_lock:
                        game_state['environment'] = environment
                        game_state['center_pos'] = center_pos
                        game_state['level'] = current_level
                        game_state['goal_collected'] = goal_collected

                        # Include updated player_pos and player_score
                        game_state['player_pos'] = player_pos
                        game_state['player_score'] = player_score

                        # Indicate that level has advanced
                        game_state['level_advanced'] = True

                    # Send updated game state to client
                    network_thread.send_data(game_state)
                    print(f"[Server] Level advanced to {current_level}, sent updated game state.")
                else:
                    # Client requests level advancement from server
                    network_thread.send_data({'request_level_advance': True})
                    print("[Client] Requested level advancement.")

            # Update game state and send data
            with game_state_lock:
                game_state['player_pos'] = player_pos
                game_state['player_score'] = player_score

            # Send local player data (excluding 'goal_collected')
            local_player_data = {
                'player_pos': player_pos,
                'player_score': player_score
            }
            network_thread.send_data(local_player_data)
            if is_server:
                print(f"[Server] Sent player data.")
            else:
                print(f"[Client] Sent player data.")

            # Update opponent's data from game state
            with game_state_lock:
                opponent_pos = game_state.get('opponent_pos', opponent_pos)
                opponent_score = game_state.get('opponent_score', opponent_score)
                if is_server:
                    print(f"[Server] Updated opponent_pos: {opponent_pos}, opponent_score: {opponent_score}")
                else:
                    print(f"[Client] Updated opponent_pos: {opponent_pos}, opponent_score: {opponent_score}")

                # Server handles level advancement requests from client
                if is_server and game_state.get('level_advance_requested'):
                    print("[Main] Level advancement requested by client.")
                    increase_level()
                    level_start_time = pygame.time.get_ticks()  # Reset the level start time
                    # Update game state
                    with game_state_lock:
                        game_state['environment'] = environment
                        game_state['center_pos'] = center_pos
                        game_state['level'] = current_level
                        game_state['goal_collected'] = goal_collected
                        game_state['level_advance_requested'] = False  # Reset the request flag

                        # Include updated player_pos and player_score
                        game_state['player_pos'] = player_pos
                        game_state['player_score'] = player_score

                        # Indicate that level has advanced
                        game_state['level_advanced'] = True

                    # Send updated game state to client
                    network_thread.send_data(game_state)
                    print(f"[Server] Level advanced to {current_level} due to client request, sent updated game state.")

            # Check if level advanced
            if game_state.get('level_advanced'):
                # Level has advanced, update game state
                with game_state_lock:
                    current_level = game_state['level']
                    environment = game_state['environment']
                    center_pos = tuple(game_state['center_pos'])
                    NUM_TILES_WIDTH = len(environment[0])
                    NUM_TILES_HEIGHT = len(environment)
                    goal_collected = False  # Reset the flag

                    # Reset player positions
                    if player_number == 1:
                        player_pos = find_open_space_near(1, 1)
                    else:
                        player_pos = find_open_space_near(NUM_TILES_WIDTH - 2, NUM_TILES_HEIGHT - 2)

                    # Reset opponent_pos to None and wait for updated position
                    opponent_pos = None

                    # Reset level advanced flag
                    game_state['level_advanced'] = False

                # After resetting positions, send updated player position
                network_thread.send_data({
                    'player_pos': player_pos,
                    'player_score': player_score
                })
                print(f"[{'Server' if is_server else 'Client'}] Sent updated player data after level advancement.")

                # Wait for opponent's updated position
                print(f"[{'Server' if is_server else 'Client'}] Waiting for opponent's updated position...")
                while True:
                    with game_state_lock:
                        if game_state.get('opponent_pos') is not None:
                            opponent_pos = game_state['opponent_pos']
                            break
                    pygame.time.wait(100)
                print(f"[{'Server' if is_server else 'Client'}] Received opponent's updated position: {opponent_pos}")

                # Fade in for the new level
                fade_surface(screen, 1000, fade_in=True, fade_color=WHITE)
                print(f"[Game] Level {current_level} started.")

            # Drawing
            screen.fill(WHITE)

            # Draw the environment
            draw_environment(screen, player_pos, opponent_pos, player_color, opponent_color)

            # Display scores
            draw_text_with_outline(screen, f"Your Score: {player_score}", BLACK, WHITE, 5, 5)
            draw_text_with_outline(screen, f"Opponent Score: {opponent_score}", BLACK, WHITE, 5, 35)
            draw_text_with_outline(screen, f"Level: {current_level}", BLACK, WHITE, WIDTH - 150, 5)

            # Update the display
            pygame.display.flip()

            # Frame rate
            clock.tick(30)

            # End the game
            if current_level is not None and current_level > max_level:  # If maximum level is exceeded
                # Determine the winner and their score
                winner = player_number if player_score > opponent_score else (2 if player_number == 1 else 1)
                winner_score = player_score if winner == player_number else opponent_score
                mixer.music.stop()
                display_game_over_screen(screen, winner, winner_score)

                # Keep the game over screen for a set amount of time
                pygame.time.wait(5000)  # 5 seconds

                # Wait for user input after the game over screen
                waiting_for_input = True
                while waiting_for_input:
                    for event in pygame.event.get():
                        if event.type == pygame.QUIT:
                            waiting_for_input = False
                            pygame.quit()
                            sys.exit()
                        if event.type == pygame.KEYDOWN:
                            if event.key == pygame.K_ESCAPE:  # Close the game when ESC is pressed
                                waiting_for_input = False

                break  # Break out of the game loop

    except Exception as e:
        print(f"[{'Server' if is_server else 'Client'}] Exception in game loop: {e}")
        network_thread.stop()
        pygame.quit()
        sys.exit()

    # Cleanup
    network_thread.stop()
    pygame.quit()
    sys.exit()

# Run the splash and title screen
if __name__ == "__main__":
    try:
        splash_and_title_screen(screen)
        main()
    except Exception as e:
        print(f"An exception occurred: {e}")
        pygame.quit()
        sys.exit()