make this multplayer game send player movement every 24 freames to not cause lag in high ping and simlaute other freames
game.py
# game.py
import time
import pygame
import socket
import pickle
import threading
import struct
import sys

# Initialize Pygame
pygame.init()

# Set up the screen
SCREEN_WIDTH, SCREEN_HEIGHT = 1280, 720
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('2D Fight Game')

# Constants
TILE_SIZE = 72  # Tile size after scaling
GRAVITY = 1
JUMP_STRENGTH = 15
SPEED = 11
FRICTION = 0.05  # Adjusted friction for better movement
FONT = pygame.font.Font(None, 48)
SMALL_FONT = pygame.font.Font(None, 36)
WHITE = (255, 255, 255)
BLACK = (46, 46, 46)
BLUE = (135, 206, 235)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

# Bullet settings
MAX_BULLETS = 1          # Maximum number of bullets allowed on screen
BULLET_COOLDOWN = 300    # Cooldown time in milliseconds between shots

# Input boxes
player_name_input_box = pygame.Rect(400, 200, 400, 50)
server_ip_input_box = pygame.Rect(400, 300, 400, 50)

# Text variables
player_name = 'Player'
server_ip = 'localhost:25566'
input_active = None

# Connecting flag
connecting = False

# Function to display text
def draw_text(text, font, color, surface, x, y):
    textobj = font.render(text, True, color)
    textrect = textobj.get_rect(center=(x, y))
    surface.blit(textobj, textrect)

# Connecting screen
def show_connecting_screen():
    screen.fill(BLUE)
    draw_text("Connecting...", FONT, BLACK, screen, SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
    pygame.display.flip()

# Function to handle user input for name and server IP in Pygame
def get_user_input_pygame():
    global player_name, server_ip, input_active, connecting
    while True:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if player_name_input_box.collidepoint(event.pos):
                    input_active = 'name'
                elif server_ip_input_box.collidepoint(event.pos):
                    input_active = 'server_ip'
                else:
                    input_active = None
            if event.type == pygame.KEYDOWN:
                if input_active == 'name':
                    if event.key == pygame.K_RETURN:
                        input_active = 'server_ip'
                    elif event.key == pygame.K_BACKSPACE:
                        player_name = player_name[:-1]
                    else:
                        player_name += event.unicode
                elif input_active == 'server_ip':
                    if event.key == pygame.K_RETURN:
                        connecting = True
                        show_connecting_screen()
                        return player_name, server_ip
                    elif event.key == pygame.K_BACKSPACE:
                        server_ip = server_ip[:-1]
                    else:
                        server_ip += event.unicode
                elif event.key == pygame.K_ESCAPE:
                    pygame.quit()
                    sys.exit()

        screen.fill(BLUE)
        draw_text("Enter Player Name:", FONT, BLACK, screen, SCREEN_WIDTH // 2, 160)
        draw_text("Enter Server IP (e.g., localhost:25566):", FONT, BLACK, screen, SCREEN_WIDTH // 2, 260)

        pygame.draw.rect(screen, WHITE, player_name_input_box, 2)
        pygame.draw.rect(screen, WHITE, server_ip_input_box, 2)

        draw_text(player_name, SMALL_FONT, BLACK, screen, player_name_input_box.centerx, player_name_input_box.centery)
        draw_text(server_ip, SMALL_FONT, BLACK, screen, server_ip_input_box.centerx, server_ip_input_box.centery)

        pygame.display.flip()
        pygame.time.Clock().tick(60)  # Control input loop frame rate

# Function to send data
def send_data(data):
    serialized_data = pickle.dumps(data)
    length = len(serialized_data)
    try:
        client.sendall(struct.pack('>I', length) + serialized_data)
    except socket.error as e:
        print(f"Failed to send data to server: {e}")

# Function to receive messages
def receive_message(conn):
    length_data = conn.recv(4)
    if len(length_data) < 4:
        raise Exception("Received incomplete length data")

    data_length = struct.unpack('>I', length_data)[0]
    data = b''
    while len(data) < data_length:
        packet = conn.recv(data_length - len(data))
        if not packet:
            raise Exception("Received incomplete data")
        data += packet
    return pickle.loads(data)

# Set up the character
character = pygame.Rect(375, 375, 50, 50)  # Initial position
character_velocity_x = 0
character_velocity_y = 0
is_jumping = False

# Initialize player data and map layout
players = {}
map_layout = []

own_player_id = None  # To store the player's unique ID assigned by the server

# Load tile images
tile_images = {
    0: pygame.Surface((TILE_SIZE, TILE_SIZE)),  # Empty space
    1: pygame.image.load('./assets/grass.png').convert_alpha(),  # Grass tile (solid)
    2: pygame.image.load('./assets/grass.png').convert_alpha(),  # Grass tile (jump-through)
}

# Scale the grass tile image
tile_images[1] = pygame.transform.scale(tile_images[1], (TILE_SIZE, TILE_SIZE))
tile_images[2] = pygame.transform.scale(tile_images[2], (TILE_SIZE, TILE_SIZE))

# Set color for transparent tile (matching the screen background)
tile_images[0].fill(BLUE)  # Light blue to blend with background

# Function to check if the character is on the ground (on a grass tile)
def is_on_ground(rect):
    rect.bottom += 1  # Temporarily move the rect down by 1 pixel
    on_ground = False
    for y in range(max(0, rect.bottom // TILE_SIZE), min(len(map_layout), (rect.bottom // TILE_SIZE) + 1)):
        for x in range(max(0, rect.left // TILE_SIZE), min(len(map_layout[y]), (rect.right // TILE_SIZE) + 1)):
            if map_layout[y][x] == 1 or map_layout[y][x] == 2:
                tile_rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)
                if rect.colliderect(tile_rect):
                    rect.bottom -= 1  # Move the rect back up
                    on_ground = True
    rect.bottom -= 1  # Move the rect back up
    return on_ground

# Function to check and handle general collision with tiles
def handle_tile_collisions(rect):
    global character_velocity_y, character_velocity_x, is_jumping

    # Check vertical collisions first (falling or jumping)
    for y in range(max(0, rect.top // TILE_SIZE), min(len(map_layout), (rect.bottom // TILE_SIZE) + 1)):
        for x in range(max(0, rect.left // TILE_SIZE), min(len(map_layout[y]), (rect.right // TILE_SIZE) + 1)):
            tile_id = map_layout[y][x]
            tile_rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)

            if rect.colliderect(tile_rect):
                if tile_id == 1:  # Solid tile
                    # Handle vertical collisions
                    if character_velocity_y > 0:  # Falling down
                        if rect.bottom > tile_rect.top and rect.bottom <= tile_rect.bottom:
                            rect.bottom = tile_rect.top
                            character_velocity_y = 0
                            is_jumping = False
                    elif character_velocity_y < 0:  # Jumping up
                        if rect.top < tile_rect.bottom and rect.top >= tile_rect.top:
                            rect.top = tile_rect.bottom
                            character_velocity_y = 0

                elif tile_id == 2:  # Jump-through tile
                    # Handle collision if falling onto the tile
                    if character_velocity_y > 0 and rect.bottom > tile_rect.top and rect.bottom <= tile_rect.bottom:
                        rect.bottom = tile_rect.top
                        character_velocity_y = 0
                        is_jumping = False

    # Check horizontal collisions after vertical collisions are resolved
    for y in range(max(0, rect.top // TILE_SIZE), min(len(map_layout), (rect.bottom // TILE_SIZE) + 1)):
        for x in range(max(0, rect.left // TILE_SIZE), min(len(map_layout[y]), (rect.right // TILE_SIZE) + 1)):
            tile_id = map_layout[y][x]
            tile_rect = pygame.Rect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE)

            if rect.colliderect(tile_rect):
                if tile_id == 1:  # Solid tile
                    # Handle horizontal collisions
                    if character_velocity_x > 0:  # Moving right
                        if rect.right > tile_rect.left and rect.right <= tile_rect.right:
                            rect.right = tile_rect.left
                            character_velocity_x = 0
                    elif character_velocity_x < 0:  # Moving left
                        if rect.left < tile_rect.right and rect.left >= tile_rect.left:
                            rect.left = tile_rect.right
                            character_velocity_x = 0

# Networking setup
player_name, server_ip = get_user_input_pygame()

try:
    if ':' in server_ip:
        HOST, PORT = server_ip.split(':')
        PORT = int(PORT)
    else:
        HOST = server_ip
        PORT = 25566
except ValueError:
    HOST = 'localhost'
    PORT = 25566
except TypeError:
    HOST = 'localhost'
    PORT = 25566
except Exception as e:
    print(f"An error occurred: {e}")
    HOST = 'localhost'
    PORT = 25566

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    client.connect((HOST, PORT))
    # Send initial player data to the server
    send_data({'name': player_name, 'position': (character.x, character.y)})
except socket.error as e:
    print(f"Unable to connect to the server: {e}")
    sys.exit()

def receive_data():
    global players, map_layout, running, own_player_id
    while running:
        try:
            data = receive_message(client)
            if 'player_id' in data:
                own_player_id = data['player_id']
            players = data.get('players', {})
            map_layout = data.get('map_layout', [])
        except Exception as e:
            print(f"Disconnected from server: {e}")
            running = False
            break

# Start a thread to receive data from the server
running = True
threading.Thread(target=receive_data, daemon=True).start()

# Bullet class to manage bullets
class Bullet:
    def __init__(self, owner_id, position, direction):
        self.owner_id = owner_id  # Unique Player ID
        self.x, self.y = position
        self.direction = direction  # 1 for right, -1 for left
        self.speed = 15
        self.rect = pygame.Rect(self.x, self.y, 10, 5)

    def move(self):
        self.x += self.speed * self.direction
        self.rect.x = self.x

    def draw(self, surface):
        pygame.draw.rect(surface, RED, self.rect)

# List to store local bullets
bullets = []

# Variables for bullet cooldown and firing
last_shot_time = 0  # Time when the last shot was fired
is_firing = False   # Track if the fire key is being held down

# Respawn variables
is_dead = False
respawn_time = 0  # Timestamp when respawn should occur

# Main game loop
clock = pygame.time.Clock()
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

        # Track firing state
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_f and not is_firing:
                is_firing = True
                current_time = pygame.time.get_ticks()
                if (current_time - last_shot_time >= BULLET_COOLDOWN) and (len(bullets) < MAX_BULLETS):
                    # Determine shooting direction based on facing direction
                    # Assuming facing right by default
                    direction = 1
                    if character_velocity_x < 0:
                        direction = -1  # Left
                    # Create a bullet at the front of the character
                    bullet_x = character.x + character.width if direction == 1 else character.x - 10
                    bullet_y = character.y + character.height // 2
                    new_bullet = Bullet(own_player_id, (bullet_x, bullet_y), direction)
                    bullets.append(new_bullet)
                    # Update the last shot time
                    last_shot_time = current_time

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_f:
                is_firing = False

    # Get keys pressed
    keys = pygame.key.get_pressed()

    # Handle horizontal movement
    if not is_dead and own_player_id in players and not players[own_player_id]['is_dead']:
        if keys[pygame.K_LEFT]:
            character_velocity_x = -SPEED
        elif keys[pygame.K_RIGHT]:
            character_velocity_x = SPEED
        else:
            character_velocity_x *= FRICTION  # Apply friction when no key is pressed
            # To prevent drifting due to very low velocity
            if abs(character_velocity_x) < 0.1:
                character_velocity_x = 0

        # Handle jumping
        if (keys[pygame.K_SPACE] or keys[pygame.K_UP] or keys[pygame.K_w]) and not is_jumping and is_on_ground(character):
            character_velocity_y = -JUMP_STRENGTH
            is_jumping = True

        # Apply gravity
        character_velocity_y += GRAVITY
        character.x += character_velocity_x
        character.y += character_velocity_y

        # Check and handle collisions with tiles
        handle_tile_collisions(character)

        # Ensure character stays within screen bounds
        if character.left < 0:
            character.left = 0
        if character.right > SCREEN_WIDTH:
            character.right = SCREEN_WIDTH
        if character.top < 0:
            character.top = 0
        if character.bottom > SCREEN_HEIGHT:
            character.bottom = SCREEN_HEIGHT
            character_velocity_y = 0
            is_jumping = False

        # Update bullets
        for bullet in bullets[:]:
            bullet.move()
            # Remove bullets that are off-screen
            if bullet.x < 0 or bullet.x > SCREEN_WIDTH:
                bullets.remove(bullet)

        # Send character position and bullets to server
        try:
            # Serialize bullet data
            bullet_data = [{'x': b.x, 'y': b.y, 'direction': b.direction} for b in bullets]
            send_data({'name': player_name, 'position': (character.x, character.y), 'bullets': bullet_data})
        except socket.error as e:
            print(f"Failed to send data to server: {e}")
            running = False

    # Handle respawn
    if is_dead and pygame.time.get_ticks() >= respawn_time:
        is_dead = False
        if own_player_id in players:
            spawn_pos = players[own_player_id]['position']
            character.x, character.y = spawn_pos
        character_velocity_x, character_velocity_y = 0, 0
        is_jumping = False
        bullets.clear()
        # Inform server about respawn
        send_data({'name': player_name, 'position': (character.x, character.y), 'respawn': True})

    # Draw everything
    screen.fill(BLUE)

    # Draw the map layout
    for y, row in enumerate(map_layout):
        for x, tile_id in enumerate(row):
            tile_image = tile_images.get(tile_id)
            if tile_image:
                screen.blit(tile_image, (x * TILE_SIZE, y * TILE_SIZE))

    # Draw all players
    for pid, info in players.items():
        pos = info['position']
        color = info['color']
        health = info.get('health', 100)
        player_rect = pygame.Rect(pos[0], pos[1], 50, 50)
        pygame.draw.rect(screen, color, player_rect)
        name = info.get('name', 'Unknown')
        text_surface = SMALL_FONT.render(name, True, BLACK)
        screen.blit(text_surface, (pos[0], pos[1] - 20))  # Display name above player

        # Draw health bar
        health_bar_width = 50
        health_bar_height = 5
        health_ratio = max(health / 100, 0)
        pygame.draw.rect(screen, RED, (pos[0], pos[1] - 10, health_bar_width, health_bar_height))
        pygame.draw.rect(screen, GREEN, (pos[0], pos[1] - 10, health_bar_width * health_ratio, health_bar_height))

    # Draw all bullets from other players
    for pid, info in players.items():
        for b in info.get('bullets', []):
            bullet_rect = pygame.Rect(b['x'], b['y'], 10, 5)
            pygame.draw.rect(screen, RED, bullet_rect)

    # Draw local bullets
    for bullet in bullets:
        bullet.draw(screen)

    # Update the display
    pygame.display.flip()

    # Cap the frame rate
    clock.tick(60)

    # Handle death and respawn if necessary
    if own_player_id in players:
        if players[own_player_id].get('health', 100) <= 0 and not is_dead:
            is_dead = True
            respawn_time = pygame.time.get_ticks() + 3000  # 3 seconds cooldown
            print("You have been eliminated! Respawning in 3 seconds...")
            # Reset local character position to server's value for consistency
            spawn_pos = players[own_player_id].get('position', (375, 375))
            character.x, character.y = spawn_pos
            character_velocity_x, character_velocity_y = 0, 0
            is_jumping = False

# Close the client socket and quit Pygame
client.close()
pygame.quit()
sys.exit()

server.py
# server.py
import random
import socket
import threading
import pickle
import struct
import time
import pygame

# Define SCREEN_WIDTH and SCREEN_HEIGHT to match client
SCREEN_WIDTH, SCREEN_HEIGHT = 1280, 720

# Server settings
HOST = '0.0.0.0'  # Listen on all interfaces
PORT = 25566

# Store player positions, colors, health, bullets, and respawn info
players = {}

# List to keep track of connections
connections = []

# Tile size
TILE_SIZE = 72  # Updated to match client

# Initialize Pygame for collision rects (optional, can be removed if not needed)
pygame.init()

# Define the map layouts
map_layouts = [
    [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 1, 1, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 1, 1, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]  # Ground layer
    ],
    # Add additional predefined map layouts here
    [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 1, 1, 0, 1, 0, 0, 0, 0, 2, 0, 2, 0, 1, 1, 1, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]  # Ground layer
    ]
]

current_map_layout = map_layouts[0]

# Networking functions
def send_data(conn, data):
    """Serialize and send data to a client."""
    try:
        serialized_data = pickle.dumps(data)
        length = len(serialized_data)
        conn.sendall(struct.pack('>I', length) + serialized_data)
    except Exception as e:
        print(f"Error sending data: {e}")
        close_connection(conn)

def receive_message(conn):
    """Receive and deserialize a message from a client."""
    try:
        length_data = conn.recv(4)
        if len(length_data) < 4:
            raise Exception("Received incomplete length data")

        data_length = struct.unpack('>I', length_data)[0]
        data = b''
        while len(data) < data_length:
            packet = conn.recv(data_length - len(data))
            if not packet:
                raise Exception("Received incomplete data")
            data += packet
        return pickle.loads(data)
    except Exception as e:
        print(f"Error receiving message: {e}")
        close_connection(conn)
        raise

def handle_client(conn, addr, player_id):
    print(f"Connected by {addr} with Player ID: {player_id}")
    try:
        initial_data = receive_message(conn)
        player_name = initial_data.get('name', f'Player{player_id}')
        spawn_position = get_random_spawn_position()
        players[player_id] = {
            'id': player_id,
            'name': player_name, 
            'position': spawn_position, 
            'color': generate_color(),
            'health': 100,
            'bullets': [],
            'is_dead': False,
            'respawn_time': 0
        }

        connections.append(conn)

        # Send initial data to the new client, including their player_id
        send_data(conn, {
            'player_id': player_id,
            'players': players, 
            'map_layout': current_map_layout
        })

        # Broadcast to all clients about the new player
        broadcast({'players': players, 'map_layout': current_map_layout}, exclude=None)

        while True:
            data = receive_message(conn)
            if not data:
                break

            player = players.get(player_id)
            if not player:
                continue  # Player might have been disconnected

            # Handle respawn request
            if data.get('respawn'):
                player['is_dead'] = False
                player['health'] = 100
                player['position'] = get_random_spawn_position()
                player['bullets'] = []
                send_data(conn, {'respawned': True, 'position': player['position']})
                broadcast({'players': players, 'map_layout': current_map_layout}, exclude=None)
                continue

            # Update player position if not dead
            if not player['is_dead']:
                player['position'] = data.get('position', player['position'])

            # Handle bullets from this player
            incoming_bullets = data.get('bullets', [])
            for b in incoming_bullets:
                bullet = {
                    'owner_id': player_id,
                    'x': b['x'],
                    'y': b['y'],
                    'direction': b['direction']
                }
                player['bullets'].append(bullet)

            # Update bullets positions and handle collisions
            update_bullets()

            # Update all clients
            broadcast({'players': players, 'map_layout': current_map_layout}, exclude=None)

    except Exception as e:
        print(f"Exception in handle_client: {e}")
    finally:
        disconnect_client(conn, player_id)

def broadcast(data, exclude=None):
    """Send data to all clients except the specified one."""
    for player_conn in connections[:]:
        if player_conn != exclude:
            try:
                send_data(player_conn, data)
            except Exception as e:
                print(f"Error broadcasting data: {e}")
                close_connection(player_conn)

def get_random_spawn_position():
    x = random.randint(0, len(current_map_layout[0]) - 2)
    y = len(current_map_layout) - 2  # Just above the ground
    return x * TILE_SIZE, y * TILE_SIZE

def generate_color():
    return (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))

player_id_counter = 1
player_id_lock = threading.Lock()

def change_map_layout_periodically():
    """Change the map layout every 60 seconds."""
    global current_map_layout
    map_index = 0
    while True:
        time.sleep(60)
        current_map_layout = map_layouts[map_index]
        map_index = (map_index + 1) % len(map_layouts)
        print("Map layout changed.")
        broadcast({'players': players, 'map_layout': current_map_layout})

# Connection handling
def close_connection(conn):
    """Close the connection and remove it from the active list."""
    if conn in connections:
        connections.remove(conn)
    conn.close()

def disconnect_client(conn, player_id):
    """Remove a client from the player list and notify others."""
    print(f"Disconnected Player ID: {player_id}")
    if player_id in players:
        del players[player_id]
    close_connection(conn)
    broadcast({'players': players, 'map_layout': current_map_layout})

# Core game functions
def update_bullets():
    """Update bullet positions and handle collisions."""
    global players
    for player_id, info in players.items():
        if info['is_dead']:
            continue  # Dead players can't shoot

        bullets_to_remove = []
        for bullet in info['bullets']:
            # Move bullet
            bullet['x'] += bullet['direction'] * 15

            # Check if bullet is out of bounds
            if bullet['x'] < 0 or bullet['x'] > SCREEN_WIDTH:
                bullets_to_remove.append(bullet)
                continue

            # Check collision with tiles
            bullet_rect = pygame.Rect(bullet['x'], bullet['y'], 10, 5)
            tile_x = bullet_rect.x // TILE_SIZE
            tile_y = bullet_rect.y // TILE_SIZE
            if 0 <= tile_y < len(current_map_layout) and 0 <= tile_x < len(current_map_layout[0]):
                tile_id = current_map_layout[tile_y][tile_x]
                if tile_id != 0:  # Non-empty tile
                    bullets_to_remove.append(bullet)
                    continue

            # Check collision with other players
            for other_id, other_info in players.items():
                if other_id == player_id or other_info['is_dead']:
                    continue
                other_rect = pygame.Rect(other_info['position'][0], other_info['position'][1], 50, 50)
                if bullet_rect.colliderect(other_rect):
                    # Decrease health
                    other_info['health'] -= 10
                    print(f"{other_info['name']} was hit by {info['name']}! Health: {other_info['health']}")
                    # Remove the bullet
                    bullets_to_remove.append(bullet)
                    # Check if player is dead
                    if other_info['health'] <= 0:
                        print(f"{other_info['name']} has been eliminated!")
                        # Set player as dead and set respawn time
                        other_info['is_dead'] = True
                        other_info['respawn_time'] = time.time() + 3  # 3 seconds cooldown
                        # Reset player bullets
                        other_info['bullets'] = []
                        # Inform the player to respawn
                        for conn in connections:
                            try:
                                send_data(conn, {'players': players, 'map_layout': current_map_layout})
                            except:
                                pass
                    break  # Bullet can hit only one player

        # Remove bullets that have hit, collided, or are out of bounds
        for bullet in bullets_to_remove:
            if bullet in info['bullets']:
                info['bullets'].remove(bullet)

    # Handle respawning
    current_time = time.time()
    for player_id, info in players.items():
        if info['is_dead'] and current_time >= info['respawn_time']:
            info['is_dead'] = False
            info['health'] = 100
            info['position'] = get_random_spawn_position()
            print(f"{info['name']} has respawned.")
            # Notify all clients
            broadcast({'players': players, 'map_layout': current_map_layout}, exclude=None)

# Server setup
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((HOST, PORT))
server.listen()
print("Server started, waiting for connections...")

# Start the map layout change thread
threading.Thread(target=change_map_layout_periodically, daemon=True).start()

# Start the bullet update loop in a separate thread
def bullet_update_loop():
    while True:
        update_bullets()
        time.sleep(0.05)  # 20 times per second

threading.Thread(target=bullet_update_loop, daemon=True).start()

while True:
    conn, addr = server.accept()
    with player_id_lock:
        player_id = player_id_counter
        player_id_counter += 1
    threading.Thread(target=handle_client, args=(conn, addr, player_id), daemon=True).start()