add bullet limt system and add bullet delay to disable counties firing

game.py
import time
import pygame
import socket
import pickle
import threading
import struct
import sys
import json

# Initialize Pygame
pygame.init()

# Set up the screen
screen = pygame.display.set_mode((1280, 720))
pygame.display.set_caption('2dFight Game')

# Constants
tile_size = 72  # Tile size after scaling
GRAVITY = 1
JUMP_STRENGTH = 15
SPEED = 11
FRICTION = 0.0001
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)
# 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'
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, 640, 360)
    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

        screen.fill(BLUE)
        draw_text("Enter Player Name:", FONT, BLACK, screen, 640, 160)
        draw_text("Enter Server IP:", FONT, BLACK, screen, 640, 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, 640, 225)
        draw_text(server_ip, SMALL_FONT, BLACK, screen, 640, 325)

        pygame.display.flip()

# Function to send data
def send_data(data):
    serialized_data = pickle.dumps(data)
    length = len(serialized_data)
    client.sendall(struct.pack('>I', length) + serialized_data)

# 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 = []

# 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
    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
                    return True
    rect.bottom -= 1  # Move the rect back up
    return False

# 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

# Networking setup
player_name, server_ip = get_user_input_pygame()

try:
    HOST, PORT = server_ip.split(':')
    PORT = int(PORT) if PORT else 25566
except ValueError:
    HOST = server_ip
    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
    while running:
        try:
            data = receive_message(client)
            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, position, direction):
        self.owner = owner  # Tuple (IP, PORT)
        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 bullets
bullets = []

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

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

    # Handle horizontal movement
    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

    # 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

    # Handle shooting
    if keys[pygame.K_f]:
        # Determine shooting direction based on movement
        if character_velocity_x >= 0:
            direction = 1  # Right
        else:
            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
        bullets.append(Bullet((client.getsockname()[0], client.getsockname()[1]), (bullet_x, bullet_y), direction))
        # Prevent continuous firing by adding a short delay
        pygame.time.delay(10)

    # 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 > 1280:
        character.right = 1280
    if character.top < 0:
        character.top = 0
    if character.bottom > 720:
        character.bottom = 720

    # Update bullets
    for bullet in bullets[:]:
        bullet.move()
        # Remove bullets that are off-screen
        if bullet.x < 0 or bullet.x > 1280:
            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

    # 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 addr, 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)
        font = pygame.font.Font(None, 24)
        text_surface = font.render(info['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
    server_bullets = players.get((client.getsockname()[0], client.getsockname()[1]), {}).get('bullets', [])
    for addr, 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
    pygame.time.Clock().tick(60)

pygame.quit()


server.py

import random
import socket
import threading
import pickle
import struct
import time

import pygame

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

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

# List to keep track of connections
connections = []

# Tile size
tile_size = 72  # Updated to match client

# 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):
    print(f"Connected by {addr}")
    try:
        initial_data = receive_message(conn)
        player_name = initial_data.get('name', 'Unknown')
        spawn_position = get_random_spawn_position()
        players[addr] = {
            'name': player_name, 
            'position': spawn_position, 
            'color': generate_color(),
            'health': 100,
            'bullets': []
        }

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

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

        while True:
            data = receive_message(conn)
            if not data:
                break
            # Update player position
            players[addr]['position'] = data['position']

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

            # Update bullets positions and handle collisions
            update_bullets()

            # Remove bullets that have hit or are out of bounds
            for player_addr in players:
                bullets_to_remove = []
                for bullet in players[player_addr]['bullets']:
                    # Move bullet
                    bullet['x'] += bullet['direction'] * 15
                    # Check if bullet is out of bounds
                    if bullet['x'] < 0 or bullet['x'] > 1280:
                        bullets_to_remove.append(bullet)
                        continue
                    # Check collision with other players
                    for other_addr, info in players.items():
                        if other_addr != player_addr:
                            other_rect = pygame.Rect(info['position'][0], info['position'][1], 50, 50)
                            bullet_rect = pygame.Rect(bullet['x'], bullet['y'], 10, 5)
                            if bullet_rect.colliderect(other_rect):
                                # Decrease health
                                info['health'] -= 10
                                print(f"{info['name']} was hit by {players[player_addr]['name']}! Health: {info['health']}")
                                # Remove the bullet
                                bullets_to_remove.append(bullet)
                                # Check if player is dead
                                if info['health'] <= 0:
                                    print(f"{info['name']} has been eliminated!")
                                    # Reset player position and health
                                    info['position'] = get_random_spawn_position()
                                    info['health'] = 100
                                break
                # Remove bullets
                for bullet in bullets_to_remove:
                    players[player_addr]['bullets'].remove(bullet)

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

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]) - 1)
    y = random.randint(0, len(current_map_layout) - 1)
    return x * tile_size, y * tile_size

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

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, addr):
    """Remove a client from the player list and notify others."""
    print(f"Disconnected by {addr}")
    if addr in players:
        del players[addr]
    close_connection(conn)
    broadcast({'players': players, 'map_layout': current_map_layout})

# Core game functions
def update_bullets():
    """Update bullet positions and handle collisions (optional if handled differently)."""
    pass  # Already handled in handle_client

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

while True:
    conn, addr = server.accept()
    connections.append(conn)
    threading.Thread(target=handle_client, args=(conn, addr), daemon=True).start()