Getting this error when pressing Enter on the game over screen on one of the games. Then another error pops up on the other leftover screen when pressing enter. Output the entire approx. 1200 lines of code.

First:

[NetworkThread] Networking error: [WinError 10053] An established connection was aborted by the software in your host machine

Second:

[NetworkThread] Networking error: [WinError 10054] An existing connection was forcibly closed by the remote host
[NetworkThread] Connection closed.
[NetworkThread] Socket closed.




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):
        super().__init__()
        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:
                try:
                    self.sock.bind((self.host, self.port))
                    self.sock.listen(1)
                    print(f"[Server] Listening on {self.host}:{self.port}")
                except socket.error as e:
                    print(f"[Server] Bind/Listen failed: {e}")
                    self.running = False
                    return

                try:
                    self.connection, addr = self.sock.accept()
                    print(f"[Server] Client connected from {addr}")
                    self.connected.set()  # Signal that the connection is established
                except socket.error as e:
                    print(f"[Server] Accept failed: {e}")
                    self.running = False
                    return
            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(1194).decode('utf-8')  # Use utf-8 decoding
                    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.")
                                            # Handle game restart request
                                            if remote_game_state.get('restart_game'):
                                                self.game_state['restart_game'] = True
                                        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 socket.error as e:
                    print(f"[NetworkThread] Networking error: {e}")
                    self.running = False
        except Exception as e:
            print(f"[NetworkThread] Exception: {e}")
        finally:
            if self.connection:
                try:
                    self.connection.close()
                    print("[NetworkThread] Connection closed.")
                except Exception as e:
                    print(f"[NetworkThread] Error closing connection: {e}")
            if self.sock:
                try:
                    self.sock.close()
                    print("[NetworkThread] Socket closed.")
                except Exception as e:
                    print(f"[NetworkThread] Error closing socket: {e}")
            self.running = False  # Ensure running is set to False

    def send_data(self, data):
        try:
            if self.connection and self.running:
                message = json.dumps(data) + '\n'  # Append newline delimiter
                self.connection.sendall(message.encode('utf-8'))  # Use utf-8 encoding
                print(f"[NetworkThread] Sent data: {data}")
        except socket.error as e:
            print(f"[NetworkThread] Socket error while sending data: {e}")
            self.running = False
        except Exception as e:
            print(f"[NetworkThread] Unexpected error while sending data: {e}")
            self.running = False

    def stop(self):
        self.running = False
        if self.connection:
            try:
                self.connection.close()
                print("[NetworkThread] Connection closed via stop().")
            except Exception as e:
                print(f"[NetworkThread] Error closing connection in stop(): {e}")
        if self.sock:
            try:
                self.sock.close()
                print("[NetworkThread] Socket closed via stop().")
            except Exception as e:
                print(f"[NetworkThread] Error closing socket in stop(): {e}")

# 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
    mixer.music.load('title_screen.mp3')
    mixer.music.set_volume(0.25)
    mixer.music.play(-1)  # The '-1' makes the music play in a loop

    # Splash screen
    surface.fill(BLACK)
    font = pygame.font.Font(font_name, 50)
    text = 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

    # Maze settings
    maze_width, maze_height = 201, 201  # 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

    running = True
    maze_alpha = 0  # Initial alpha for the maze
    text_alpha = 0  # Initial alpha for the text

    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)

        # Maze Surface for fading effect
        maze_surface = pygame.Surface((maze_width * TILE_SIZE, HEIGHT))
        maze_surface.set_alpha(maze_alpha)
        maze_surface.fill(BLACK)

        # Draw the scrolling maze on the maze surface
        for y, row in enumerate(maze):
            for x, tile in enumerate(row):
                if tile == 1:
                    pygame.draw.rect(
                        maze_surface,
                        WHITE,
                        ((x - 1) * TILE_SIZE + maze_offset_x,
                         (y - 1) * TILE_SIZE,
                         TILE_SIZE,
                         TILE_SIZE)
                    )
                    pygame.draw.rect(
                        maze_surface,
                        WHITE,
                        ((x - 1) * TILE_SIZE + maze_offset_x + maze_width * TILE_SIZE,
                         (y - 1) * TILE_SIZE,
                         TILE_SIZE,
                         TILE_SIZE)
                    )
        # Blit maze surface onto the main surface
        surface.blit(maze_surface, (0, 0))

        # Update maze offset for scrolling
        maze_offset_x -= scroll_speed
        if maze_offset_x <= -maze_width * TILE_SIZE:
            maze_offset_x += maze_width * TILE_SIZE

        # Text Surface for fading effect
        text_surface = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
        text_surface.set_alpha(text_alpha)
        text_surface.fill((0, 0, 0, 0))  # Transparent background

        # Title
        title_font = pygame.font.Font(font_name, 70)
        title_text = "Maze Explorers Online"
        title_text_render = title_font.render(title_text, True, GOLDEN)
        title_text_rect = title_text_render.get_rect(center=(WIDTH // 2, HEIGHT // 3))
        outline_text(text_surface, title_text, title_font, GOLDEN, BLACK, title_text_rect.centerx, title_text_rect.centery)

        # Press Enter to Start
        start_font = pygame.font.Font(font_name, 30)
        start_text = "Press Enter"
        start_text_render = start_font.render(start_text, True, GOLDEN)
        start_text_rect = start_text_render.get_rect(center=(WIDTH // 2, HEIGHT // 2))
        outline_text(text_surface, start_text, start_font, GOLDEN, BLACK, start_text_rect.centerx, start_text_rect.centery)

        # Blit text surface onto the main surface
        surface.blit(text_surface, (0, 0))

        # Increment alpha values for fade-in effect
        maze_alpha = min(maze_alpha + 5, 255)
        text_alpha = min(text_alpha + 5, 255)

        pygame.display.flip()

def outline_text(surface, text, font, color, outline_color, x, y, alpha=255):
    # Create a text surface with the primary color
    text_render = font.render(text, True, color)
    text_render.set_alpha(alpha)
    text_rect = text_render.get_rect(centerx=x, centery=y)

    # Create an outline
    outline = font.render(text, True, outline_color)
    outline.set_alpha(alpha)
    outline_rect = outline.get_rect(centerx=x, centery=y)

    # Blit the outline first
    surface.blit(outline, outline_rect.move(2, 2))

    # Then blit the text on top of the outline
    surface.blit(text_render, text_rect)

def display_game_over_screen(surface, winner, winner_score):
    # Generate a maze for the game over screen
    maze_width, maze_height = 201, 201
    maze = generate_maze(maze_width, maze_height)
    maze_offset_x, maze_offset_y = 0, 0
    scroll_speed = 3.0  # Adjust scroll speed as needed

    # Start with a solid color surface for fade-in
    fade_surface_screen = pygame.Surface((WIDTH, HEIGHT))
    fade_surface_screen.fill(WHITE)  # Start with a white surface

    mixer.music.load('game_over.mp3')
    mixer.music.set_volume(0.25)
    mixer.music.play(-1)  # The '-1' makes the music play in a loop

    # Perform initial fade-in effect
    fade_duration = 2000  # Adjust as needed
    for alpha in range(255, -1, -5):  # Decrease alpha to create fade-in effect
        fade_surface_screen.set_alpha(alpha)

        # Draw the game over content beneath the fade surface
        surface.fill(WHITE)  # Clear the screen

        # 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 + maze_offset_y
                    pygame.draw.rect(surface, BLACK, (tile_x, tile_y, TILE_SIZE, TILE_SIZE))

        # Update maze offset for diagonal scrolling
        maze_offset_x -= scroll_speed
        maze_offset_y -= scroll_speed
        if maze_offset_x <= -maze_width * TILE_SIZE or maze_offset_y <= -maze_height * TILE_SIZE:
            maze_offset_x = maze_width * TILE_SIZE
            maze_offset_y = maze_height * TILE_SIZE

        # Render 'Game Over' and winner text with outline
        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))
        outline_text(surface, game_over_text, game_over_font, GOLDEN, BLACK, game_over_rect.centerx, game_over_rect.centery)

        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, WHITE).get_rect(center=(WIDTH // 2, HEIGHT // 2))
        outline_text(surface, winner_text, winner_font, WHITE, BLACK, winner_rect.centerx, winner_rect.centery)

        surface.blit(fade_surface_screen, (0, 0))

        pygame.display.flip()
        pygame.time.delay(fade_duration // 51)

    # Press Enter text settings
    press_enter_font = pygame.font.Font(font_name, 30)
    press_enter_text = "Press Enter to Restart"
    press_enter_color = WHITE
    press_enter_outline_color = BLACK
    alpha = 0  # Starting alpha value for 'Press Enter' text

    # Game over loop
    game_over = True
    confirmed_restart = False  # Flag to confirm both players are ready to restart
    while game_over:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_over = False
                pygame.quit()
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RETURN:
                    mixer.music.stop()
                    # Send restart signal to the other player
                    with game_state_lock:
                        game_state['restart_game'] = True
                    network_thread.send_data({'restart_game': True})
                    confirmed_restart = True

        # Check if both players are ready to restart
        with game_state_lock:
            if game_state.get('restart_game') and confirmed_restart:
                game_over = False  # Exit the game over loop
                # Reset the restart signals
                game_state['restart_game'] = False
                confirmed_restart = False

        # Clear the screen
        surface.fill(WHITE)

        # Update and redraw the scrolling maze
        maze_offset_x -= scroll_speed
        maze_offset_y -= scroll_speed
        if maze_offset_x <= -maze_width * TILE_SIZE or maze_offset_y <= -maze_height * TILE_SIZE:
            maze_offset_x = maze_width * TILE_SIZE
            maze_offset_y = maze_height * TILE_SIZE

        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 + maze_offset_y
                    pygame.draw.rect(surface, BLACK, (tile_x, tile_y, TILE_SIZE, TILE_SIZE))

        # Redraw 'Game Over' and winner text
        game_over_rect = game_over_font.render(game_over_text, True, GOLDEN).get_rect(center=(WIDTH // 2, HEIGHT // 3))
        outline_text(surface, game_over_text, game_over_font, GOLDEN, BLACK, game_over_rect.centerx, game_over_rect.centery)
        winner_rect = winner_font.render(winner_text, True, WHITE).get_rect(center=(WIDTH // 2, HEIGHT // 2))
        outline_text(surface, winner_text, winner_font, WHITE, BLACK, winner_rect.centerx, winner_rect.centery)

        # Fade-in 'Press Enter' text
        if alpha < 255:
            outline_text(surface, press_enter_text, press_enter_font, press_enter_color, press_enter_outline_color, WIDTH // 2, HEIGHT // 2 + 50, alpha)
            alpha += 10  # Increase alpha value for fade-in effect
        else:
            # Ensure 'Press Enter' text remains visible with full opacity
            outline_text(surface, press_enter_text, press_enter_font, press_enter_color, press_enter_outline_color, WIDTH // 2, HEIGHT // 2 + 50, 255)

        pygame.display.flip()
        pygame.time.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()
        with game_state_lock:
            game_state['game_over'] = True  # Signal game over to both players
        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
    global max_level, level_increment

    max_level = 5  # Maximum level
    level_increment = 4  # Increase in map size per level

    first_run = True  # Flag to check if it's the first run (to skip splash screen)

    while True:
        level_start_time = pygame.time.get_ticks()
        level_advancement_requested = False  # Reset the flag
        goal_collected = False  # Reset the flag
        player_score = 0
        opponent_score = 0
        game_over = False  # Flag to indicate game over

        if first_run:
            # Networking setup
            is_server = False
            host = 'localhost'
            port = 1194  # 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 1194): ")
                try:
                    port = int(port_input)
                    if not (0 < port < 65536):
                        raise ValueError
                except ValueError:
                    print("Invalid port number. Using default port 1194.")
                    port = 1194
            else:
                host = input("Enter the server's IP address: ")
                port_input = input("Enter the server's port number (default 1194): ")
                try:
                    port = int(port_input)
                    if not (0 < port < 65536):
                        raise ValueError
                except ValueError:
                    print("Invalid port number. Using default port 1194.")
                    port = 1194

            first_run = False  # Set first_run to False after initial setup

        # Reset game variables for a new game
        current_level = 5
        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}")

            # 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,
                'game_over': False,
                'restart_game': False
            }
        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,
                'game_over': False,
                'restart_game': False
            }

        # 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,
                'game_over': False,
                'restart_game': False
            }
            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)  # Wait 100 ms before checking again
            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,
                'goal_collected': goal_collected,
                'game_over': False,
                'restart_game': False
            }
            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()

                # Check for game over
                with game_state_lock:
                    if game_state.get('game_over'):
                        game_over = True

                # Handle game over
                if game_over:
                    # 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)

                    # Wait for both players to confirm restart
                    # This is handled inside display_game_over_screen

                    # Stop the network thread and restart it
                    network_thread.stop()
                    pygame.time.wait(1000)  # Wait a moment to ensure the thread has stopped

                    # Break out of the game loop to restart
                    running = False
                    break

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

                # Local Player Movement
                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)

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

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

                    # Update game state
                    with game_state_lock:
                        game_state['player_pos'] = player_pos
                        game_state['player_score'] = player_score
                        game_state['goal_collected'] = goal_collected
                        game_state['level_advanced'] = False  # Reset the flag

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

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

        # Prepare for next game iteration
        # The while True loop will restart the game
        # Reset variables as needed and continue

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