import pygame
import sys
import math

# Initialize Pygame
pygame.init()

# Window dimensions
WINDOW_WIDTH = 1300
WINDOW_HEIGHT = 800
SIMULATION_HEIGHT = 650
UI_HEIGHT = 150

# Colors
LIGHT_GRAY = (211, 211, 211)
DARK_GRAY = (169, 169, 169)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
LIGHT_BLUE = (173, 216, 230)
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

# Screen setup
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Ball Trajectory Simulation Game")

# Clock
clock = pygame.time.Clock()
FPS = 60

# Fonts
font = pygame.font.SysFont(None, 24)
large_font = pygame.font.SysFont(None, 36)

# Physics constants
GRAVITY = 650  # units per second squared

# Ball properties
BALL_RADIUS = 96.3881  # units
BALL_COLOR = RED

# Wall properties
WALL_THICKNESS = BALL_RADIUS
WALL_COLOR = DARK_GRAY
WALL_POSITION_Y = 1474  # units

# Boundaries
LEFT_BOUNDARY = -3581
RIGHT_BOUNDARY = 3581
GROUND_LEVEL = 0
CEILING_LEVEL = 1820

# Conversion factor from units to pixels (arbitrary scaling)
SCALE = SIMULATION_HEIGHT / (CEILING_LEVEL + BALL_RADIUS * 2)

# Initial positions (convert from units to pixels)
def units_to_pixels_y(y_units):
    return WINDOW_WIDTH / 2 + y_units * SCALE

def units_to_pixels_z(z_units):
    return SIMULATION_HEIGHT - z_units * SCALE

def pixels_to_units_y(y_pixels):
    return (y_pixels - WINDOW_WIDTH / 2) / SCALE

def pixels_to_units_z(z_pixels):
    return (SIMULATION_HEIGHT - z_pixels) / SCALE

class Slider:
    def __init__(self, x, y, w, h, min_val, max_val, default, label):
        self.rect = pygame.Rect(x, y, w, h)
        self.min_val = min_val
        self.max_val = max_val
        self.value = default
        self.grabbed = False
        self.label = label
        self.slider_pos = x + (default - min_val) / (max_val - min_val) * w

    def draw(self, surface):
        # Draw line
        pygame.draw.line(surface, BLACK, (self.rect.x, self.rect.centery),
                         (self.rect.x + self.rect.width, self.rect.centery), 2)
        # Draw slider
        pygame.draw.circle(surface, BLACK, (int(self.slider_pos), self.rect.centery), self.rect.height // 2)
        # Draw label
        label_surface = font.render(f"{self.label}: {round(self.value, 2)}", True, BLACK)
        surface.blit(label_surface, (self.rect.x, self.rect.y - 20))

    def update(self, mouse_pos):
        if self.grabbed:
            self.slider_pos = max(self.rect.x, min(mouse_pos[0], self.rect.x + self.rect.width))
            self.value = self.min_val + (self.slider_pos - self.rect.x) / self.rect.width * (self.max_val - self.min_val)

    def handle_event(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            if pygame.Rect(self.slider_pos - self.rect.height // 2, self.rect.y,
                           self.rect.height, self.rect.height).collidepoint(event.pos):
                self.grabbed = True
        elif event.type == pygame.MOUSEBUTTONUP:
            self.grabbed = False

class Button:
    def __init__(self, x, y, w, h, label, action):
        self.rect = pygame.Rect(x, y, w, h)
        self.label = label
        self.action = action

    def draw(self, surface):
        pygame.draw.rect(surface, DARK_GRAY, self.rect)
        label_surface = font.render(self.label, True, WHITE)
        surface.blit(label_surface, (self.rect.x + (self.rect.width - label_surface.get_width()) // 2,
                                     self.rect.y + (self.rect.height - label_surface.get_height()) // 2))

    def handle_event(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            if self.rect.collidepoint(event.pos):
                self.action()

class Ball:
    def __init__(self):
        self.reset()
        self.dragging = False

    def reset(self):
        self.y = 0
        self.z = BALL_RADIUS
        self.vy = 0
        self.vz = 0
        self.in_motion = False

    def update(self, dt):
        if self.in_motion:
            # Update velocities
            self.vz -= GRAVITY * dt
            # Update positions
            self.y += self.vy * dt
            self.z += self.vz * dt

            # Check for collisions with boundaries
            if self.y - BALL_RADIUS < LEFT_BOUNDARY:
                self.y = LEFT_BOUNDARY + BALL_RADIUS
                self.vy *= -1
            if self.y + BALL_RADIUS > RIGHT_BOUNDARY:
                self.y = RIGHT_BOUNDARY - BALL_RADIUS
                self.vy *= -1
            # Ceiling collision
            if self.z + BALL_RADIUS > CEILING_LEVEL:
                self.z = CEILING_LEVEL - BALL_RADIUS
                self.vz *= -1
            # Ground collision
            if self.z - BALL_RADIUS < GROUND_LEVEL:
                self.z = GROUND_LEVEL + BALL_RADIUS
                self.in_motion = False
                self.vz = 0
                self.vy = 0
                self.z = GROUND_LEVEL + BALL_RADIUS

            # Wall collision
            wall_top = WALL_HEIGHT_SLIDER.value
            wall_left = WALL_POSITION_Y - WALL_THICKNESS / 2
            wall_right = WALL_POSITION_Y + WALL_THICKNESS / 2
            if self.y + BALL_RADIUS > wall_left and self.y - BALL_RADIUS < wall_right and self.z - BALL_RADIUS < wall_top:
                # Collision with wall
                if self.vy > 0:
                    self.y = wall_left - BALL_RADIUS
                else:
                    self.y = wall_right + BALL_RADIUS
                self.vy *= -1

    def draw(self, surface):
        # Convert positions to pixels
        x = units_to_pixels_y(self.y)
        y = units_to_pixels_z(self.z)
        pygame.draw.circle(surface, BALL_COLOR, (int(x), int(y)), int(BALL_RADIUS * SCALE))

    def handle_event(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            x = units_to_pixels_y(self.y)
            y = units_to_pixels_z(self.z)
            if (event.pos[0] - x) ** 2 + (event.pos[1] - y) ** 2 <= (BALL_RADIUS * SCALE) ** 2:
                self.dragging = True
        elif event.type == pygame.MOUSEBUTTONUP:
            self.dragging = False
        elif event.type == pygame.MOUSEMOTION:
            if self.dragging:
                new_y = pixels_to_units_y(event.pos[0])
                new_z = pixels_to_units_z(event.pos[1])
                # Enforce constraints
                self.y = max(LEFT_BOUNDARY + BALL_RADIUS, min(RIGHT_BOUNDARY - BALL_RADIUS, new_y))
                self.z = max(GROUND_LEVEL + BALL_RADIUS, min(CEILING_LEVEL - BALL_RADIUS, new_z))
                # Prevent intersection with wall
                wall_top = WALL_HEIGHT_SLIDER.value
                wall_left = WALL_POSITION_Y - WALL_THICKNESS / 2 - BALL_RADIUS
                wall_right = WALL_POSITION_Y + WALL_THICKNESS / 2 + BALL_RADIUS
                if self.y > wall_left and self.y < wall_right and self.z - BALL_RADIUS < wall_top:
                    if abs(self.y - wall_left) < abs(self.y - wall_right):
                        self.y = wall_left
                    else:
                        self.y = wall_right
                # Prevent intersection with ground and ceiling
                self.z = max(GROUND_LEVEL + BALL_RADIUS, min(CEILING_LEVEL - BALL_RADIUS, self.z))

# Instantiate sliders and buttons
INITIAL_SPEED_SLIDER = Slider(50, SIMULATION_HEIGHT + 20, 200, 20, 0, 6000, 1000, "Initial Speed")
LAUNCH_ANGLE_SLIDER = Slider(300, SIMULATION_HEIGHT + 20, 200, 20, -180, 180, 45, "Launch Angle")
WALL_HEIGHT_SLIDER = Slider(550, SIMULATION_HEIGHT + 20, 200, 20, 0, 1820, 400, "Wall Height")

def launch_ball():
    if not ball.in_motion:
        angle_rad = math.radians(LAUNCH_ANGLE_SLIDER.value)
        speed = INITIAL_SPEED_SLIDER.value
        ball.vy = speed * math.cos(angle_rad)
        ball.vz = speed * math.sin(angle_rad)
        ball.in_motion = True

def reset_ball():
    ball.reset()

def reset_wall():
    WALL_HEIGHT_SLIDER.value = 400
    WALL_HEIGHT_SLIDER.slider_pos = WALL_HEIGHT_SLIDER.rect.x + (WALL_HEIGHT_SLIDER.value - WALL_HEIGHT_SLIDER.min_val) / \
                                    (WALL_HEIGHT_SLIDER.max_val - WALL_HEIGHT_SLIDER.min_val) * WALL_HEIGHT_SLIDER.rect.width

LAUNCH_BUTTON = Button(800, SIMULATION_HEIGHT + 20, 100, 30, "Launch", launch_ball)
RESET_BALL_BUTTON = Button(920, SIMULATION_HEIGHT + 20, 100, 30, "Reset Ball", reset_ball)
RESET_WALL_BUTTON = Button(1040, SIMULATION_HEIGHT + 20, 100, 30, "Reset Wall", reset_wall)

ball = Ball()

def predict_trajectory():
    prediction = []
    temp_ball = Ball()
    temp_ball.y = ball.y
    temp_ball.z = ball.z
    angle_rad = math.radians(LAUNCH_ANGLE_SLIDER.value)
    speed = INITIAL_SPEED_SLIDER.value
    temp_ball.vy = speed * math.cos(angle_rad)
    temp_ball.vz = speed * math.sin(angle_rad)
    dt = 0.01
    for _ in range(10000):
        temp_ball.vz -= GRAVITY * dt
        temp_ball.y += temp_ball.vy * dt
        temp_ball.z += temp_ball.vz * dt
        if temp_ball.y - BALL_RADIUS < LEFT_BOUNDARY:
            temp_ball.y = LEFT_BOUNDARY + BALL_RADIUS
            temp_ball.vy *= -1
        if temp_ball.y + BALL_RADIUS > RIGHT_BOUNDARY:
            temp_ball.y = RIGHT_BOUNDARY - BALL_RADIUS
            temp_ball.vy *= -1
        # Ceiling collision
        if temp_ball.z + BALL_RADIUS > CEILING_LEVEL:
            temp_ball.z = CEILING_LEVEL - BALL_RADIUS
            temp_ball.vz *= -1
        # Ground collision
        if temp_ball.z - BALL_RADIUS < GROUND_LEVEL:
            break  # Stop prediction at ground collision
        prediction.append((temp_ball.y, temp_ball.z))

        # Wall collision
        wall_top = WALL_HEIGHT_SLIDER.value
        wall_left = WALL_POSITION_Y - WALL_THICKNESS / 2
        wall_right = WALL_POSITION_Y + WALL_THICKNESS / 2
        if temp_ball.y + BALL_RADIUS > wall_left and temp_ball.y - BALL_RADIUS < wall_right and temp_ball.z - BALL_RADIUS < wall_top:
            # Collision with wall
            # Determine if ball will make it over the wall
            return False, prediction

    return True, prediction

# Main game loop
run = True
while run:
    dt = clock.tick(FPS) / 1000  # Delta time in seconds

    # Event handling
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

        INITIAL_SPEED_SLIDER.handle_event(event)
        LAUNCH_ANGLE_SLIDER.handle_event(event)
        WALL_HEIGHT_SLIDER.handle_event(event)
        LAUNCH_BUTTON.handle_event(event)
        RESET_BALL_BUTTON.handle_event(event)
        RESET_WALL_BUTTON.handle_event(event)
        ball.handle_event(event)

    mouse_pos = pygame.mouse.get_pos()
    INITIAL_SPEED_SLIDER.update(mouse_pos)
    LAUNCH_ANGLE_SLIDER.update(mouse_pos)
    WALL_HEIGHT_SLIDER.update(mouse_pos)

    # Update ball physics
    ball.update(dt)

    # Drawing
    screen.fill(LIGHT_GRAY)

    # Draw grid lines
    for i in range(int(LEFT_BOUNDARY / 200), int(RIGHT_BOUNDARY / 200) + 1):
        x = units_to_pixels_y(i * 200)
        pygame.draw.line(screen, DARK_GRAY, (x, 0), (x, SIMULATION_HEIGHT))
    for i in range(int(GROUND_LEVEL / 200), int(CEILING_LEVEL / 200) + 1):
        y = units_to_pixels_z(i * 200)
        pygame.draw.line(screen, DARK_GRAY, (0, y), (WINDOW_WIDTH, y))

    # Draw wall
    wall_top_z = WALL_HEIGHT_SLIDER.value
    wall_left_x = units_to_pixels_y(WALL_POSITION_Y - WALL_THICKNESS / 2)
    wall_right_x = units_to_pixels_y(WALL_POSITION_Y + WALL_THICKNESS / 2)
    wall_top_y = units_to_pixels_z(wall_top_z)
    pygame.draw.rect(screen, WALL_COLOR, (wall_left_x, wall_top_y, wall_right_x - wall_left_x, SIMULATION_HEIGHT - wall_top_y))

    # Draw ball
    ball.draw(screen)

    # Draw trajectory prediction
    success, trajectory = predict_trajectory()
    if trajectory:
        points = [(units_to_pixels_y(pos[0]), units_to_pixels_z(pos[1])) for pos in trajectory]
        pygame.draw.lines(screen, LIGHT_BLUE, False, points)

    # Information display
    info_y = SIMULATION_HEIGHT + 60
    initial_position_text = font.render(f"Initial Position: y = {round(ball.y, 2)}, z = {round(ball.z, 2)}", True, BLACK)
    screen.blit(initial_position_text, (50, info_y))

    current_position_text = font.render(f"Current Position: y = {round(ball.y, 2)}, z = {round(ball.z, 2)}", True, BLACK)
    screen.blit(current_position_text, (50, info_y + 20))

    initial_velocity_text = font.render(f"Initial Velocity: Vy = {round(ball.vy, 2)}, Vz = {round(ball.vz, 2)}", True, BLACK)
    screen.blit(initial_velocity_text, (300, info_y))

    current_velocity_text = font.render(f"Current Velocity: Vy = {round(ball.vy, 2)}, Vz = {round(ball.vz, 2)}", True, BLACK)
    screen.blit(current_velocity_text, (300, info_y + 20))

    wall_height_text = font.render(f"Current Wall Height: {round(WALL_HEIGHT_SLIDER.value, 2)}", True, BLACK)
    screen.blit(wall_height_text, (550, info_y))

    prediction_result_text = large_font.render("Will the ball make it over the wall?", True, BLACK)
    screen.blit(prediction_result_text, (800, info_y))

    result_color = GREEN if success else RED
    result_text = large_font.render("YES" if success else "NO", True, result_color)
    screen.blit(result_text, (800, info_y + 30))

    # Draw UI elements
    INITIAL_SPEED_SLIDER.draw(screen)
    LAUNCH_ANGLE_SLIDER.draw(screen)
    WALL_HEIGHT_SLIDER.draw(screen)
    LAUNCH_BUTTON.draw(screen)
    RESET_BALL_BUTTON.draw(screen)
    RESET_WALL_BUTTON.draw(screen)

    # Update the display
    pygame.display.flip()

# Quit Pygame
pygame.quit()
sys.exit()


Make the prediction path easier to see, the current colour and thickness is really hard to see. Make sure to draw all four boundaries, the floor at z = 0, the ceiling at z =1820 and the boundary walls at 3581. All the walls are bouncy except the floor which stops the ball. Add padding around the boundaries so that there is some space around the window, make the window size adjustable scaling everything accordingly. The text should never overlap, currently the initial position and velocities sometimes overlap when the numbers are larger. There is a bug where the initial position shows the current position when the ball is being launched, the initial position the position before the ball is launched. The current wall height should be removed since it's redundant with the wall height displayed from the slider. The prediction is based on the initial position, so should not update while the ball is being launched. The launch button should become return button after the ball is launched, the button returns the ball to the initial position with zero velocity, once back to the initial position it should go back to being the launch button. the regular reset button should also return it to being a launch button if it isn't already.
Since the background is a graph, add marking to show the height and horizontal position like a graph. Important markings are 0,0 , the boundaries, the adjustable wall position. 
there is a major bug which crashes the game when the ball touches the adjustable wall: Traceback (most recent call last):
  File "C:\Users\Usuario\Documents\CodingProjects\Ball&WallSim\run.py", line 322, in <module>
    pygame.draw.lines(screen, LIGHT_BLUE, False, points)
ValueError: points argument must contain 2 or more points

fix this bug.
The boundaries should all be perfectly bouncy except the ground which stops the ball. The adjustable wall is also bouncy.