Hello there. I am working on an online game. It works on localhost and LAN but it won't connect over the actual internet. 


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()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        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):
    start_time = pygame.time.get_ticks()
    while pygame.time.get_ticks() - start_time < freeze_duration:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
        pygame.time.delay(10)  # Small delay to prevent CPU hogging
    fade_surface(surface, fade_duration, fade_in=True, fade_color=fade_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.")

game_state_lock = threading.Lock()

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, game_state_lock

    # 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()

    level_advancement_requested = False  # Flag for asynchronous level advancement

    # 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
        }

    # Start the network thread
    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.")

        # 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)

    if in_game_music:
        mixer.music.load(in_game_music)
        mixer.music.set_volume(0.5)
        mixer.music.play(-1)  # Loop the music

    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 is_server:
                # Server uses WASD keys
                if keys[pygame.K_w]:
                    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_s]:
                    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_a]:
                    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_d]:
                    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
            else:
                # Client uses Arrow keys
                if keys[pygame.K_w]:
                    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_s]:
                    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_a]:
                    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_d]:
                    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:
                    level_advancement_requested = True
                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.")
                    level_advancement_requested = True
                    game_state['level_advance_requested'] = False  # Reset the request flag

            # Handle level advancement
            if level_advancement_requested:
                increase_level()
                level_advancement_requested = False

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

                # 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
                network_thread.send_data(game_state)
                print(f"[Server] Level advanced to {current_level}, 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()