Do not change variable names, code logic and basically anything. But do improve my ball with paddle collision logic. I want the ball to bounce of from angles according to how it hits the paddle.

import acm.graphics.*;
import acm.program.*;
import acm.util.*;

import java.applet.AudioClip;
import java.awt.*;
import java.awt.event.*;

public class Breakout extends GraphicsProgram {
	public static void main(String[] args) {
		Breakout breakout = new Breakout();
		breakout.start(args);
	}

	/** Width and height of application window in pixels */
	public static final int APPLICATION_WIDTH = 400;
	public static final int APPLICATION_HEIGHT = 500;

	/** Dimensions of game board (usually the same) */
	private static final int WIDTH = APPLICATION_WIDTH;
	private static final int HEIGHT = APPLICATION_HEIGHT;

	/** Dimensions of the paddle */
	private static final int PADDLE_WIDTH = 60;
	private static final int PADDLE_HEIGHT = 10;

	/** Offset of the paddle up from the bottom */
	private static final int PADDLE_Y_OFFSET = 30;

	/** Number of bricks per row */
	private static final int NBRICKS_PER_ROW = 10;

	/** Number of rows of bricks */
	private static final int NBRICK_ROWS = 10;

	/** Separation between bricks */
	private static final int BRICK_SEP = 4;

	/** Width of a brick */
	private static final int BRICK_WIDTH = (WIDTH - (NBRICKS_PER_ROW - 1) * BRICK_SEP) / NBRICKS_PER_ROW;

	/** Height of a brick */
	private static final int BRICK_HEIGHT = 8;

	/** Diameter of the ball in pixels */
	private static final int BALL_DIAMETER = 15; // Reduced from 30 to 15

	/** Offset of the top brick row from the top */
	private static final int BRICK_Y_OFFSET = 70;

	/** Number of lives */
	private static final int MAX_LIVES = 3;

	/** Maximum vertical velocity */
	private static final double MAX_VY = 12.0;

	/** Declaring brick as instant variable */
	private GRect brick;

	/** Declaring paddle as instant variable */
	private GRect paddle;

	/** Declaring Ball instance */
	private GOval ball;

	/** Ball coordinate tracking */
	private double ballX, ballY;

	/** Ball directional velocities */
	private double vx, vy;

	/** RandomGenerator instance for ball speed velocity variation */
	private RandomGenerator rspeed = RandomGenerator.getInstance();

	/** Collision counter for velocity increase */
	private int collisionCounts = 0;

	/** Score tracking */
	private int score = 0;
	private GLabel scoreLabel;

	/** Lives tracking */
	private int lives = 0;
	private GLabel livesLabel;

	/** Bricks remaining */
	private int bricksRemaining;

	/** Initializing the audio for the game */
	AudioClip bounceClip = MediaTools.loadAudioClip("bounce.au");

	/*
	 * Here the game runs
	 */
	public void run() {
		awaitUserInput();
		initializeTheGame();
		addMouseListeners();
		playGame();
	}

	/*
	 * Drawing all necessary game elements to the screen
	 */
	private void initializeTheGame() {
		drawBricks();
		drawPaddle();
		drawBall();
		initializeScore();
		initializeLives();
	}

	/*
	 * Let's user click the screen to actually initialize the game
	 */
	private void awaitUserInput() {
		GLabel startLabel = new GLabel("Click Anywhere to Start the Game!");
		startLabel.setFont("SansSerif-18");
		add(startLabel, (getWidth() - startLabel.getWidth()) / 2, getHeight() / 2);
		waitForClick();
		remove(startLabel);
	}

	/*
	 * Draws the current score on the top-right side of the screen.
	 * Initial score is set to 0.
	 */
	private void initializeScore() {
		score = 0;
		scoreLabel = new GLabel("Score: " + score);
		scoreLabel.setFont("Serif-14");
		add(scoreLabel, 10, 20);
	}
	
	/*
	 * Draws the current remaining lives on the top-left side of the screen.
	 * Initial live count is 3.
	 */
	private void initializeLives() {
		lives = MAX_LIVES;
		livesLabel = new GLabel("Lives: " + lives);
		livesLabel.setFont("Serif-14");
		// Positioning the lives label with proper calculation
		add(livesLabel, getWidth() - livesLabel.getWidth() - 10, 20);
	}

	/*
	 * Draws the game bricks using given brick-related variables. The nested
	 * loops draws the rows and columns of bricks together and assigns them colors
	 * via fillBrickColor(rows) method. bricksRemaining variable is used to count
	 * all the bricks left totally in the game. Once the count reaches 0, the variable
	 * is used to end the game.
	 */
	private void drawBricks() {
		bricksRemaining = NBRICK_ROWS * NBRICKS_PER_ROW;
		for (int rows = 0; rows < NBRICK_ROWS; rows++) {
			for (int cols = 0; cols < NBRICKS_PER_ROW; cols++) {
				int xPos = cols * (BRICK_WIDTH + BRICK_SEP);
				int yPos = BRICK_Y_OFFSET + rows * (BRICK_HEIGHT + BRICK_SEP);

				brick = new GRect(xPos, yPos, BRICK_WIDTH, BRICK_HEIGHT);
				brick.setFilled(true);
				brick.setFillColor(fillBrickColor(rows));
				add(brick);
			}
		}
	}

	/*
	* Simple color filling pattern method. For every two rows we have fill
	* the same colors alternating between assignment required colors.
	*/
	private Color fillBrickColor(int row) {
		if (row == 0 || row == 1) {
			return Color.RED;
		} else if (row == 2 || row == 3) {
			return Color.ORANGE;
		} else if (row == 4 || row == 5) {
			return Color.YELLOW;
		} else if (row == 6 || row == 7) {
			return Color.GREEN;
		} else if (row == 8 || row == 9) {
			return Color.CYAN;
		}
		return Color.GRAY;
	}

	// Method to draw the paddle on the screen
	private void drawPaddle() {
		// Create a new GRect (paddle) with specified width and height
		paddle = new GRect(PADDLE_WIDTH, PADDLE_HEIGHT);

		// Set the paddle to be filled and assign it a black color
		paddle.setFilled(true);
		paddle.setFillColor(Color.BLACK);

		double paddleX = getWidth() / 2 - PADDLE_WIDTH / 2;
		double paddleY = getHeight() - PADDLE_Y_OFFSET;

		// Add the paddle to the screen at the calculated position (x, y)
		add(paddle, paddleX, paddleY);
	}

	/*
	 * Similar to paddle, but the method this time draws the ball
	 * to the center of the screen.
	 */
	private void drawBall() {
		ball = new GOval(BALL_DIAMETER, BALL_DIAMETER);
		ballX = getWidth() / 2 - BALL_DIAMETER / 2;
		ballY = getHeight() / 2 - BALL_DIAMETER / 2;

		ball.setFilled(true);
		ball.setFillColor(Color.BLACK);

		add(ball);
		ball.setLocation(ballX, ballY);
	}

	/* The MAIN part of the game. It assigns velocity to the ball which we drew
	 * previously. It keeps count of the lives and reduces them when ball goes below
	 * the screen. It ends the game via announceGameOver() method once lives = 0.
	 * It also includes collision handling logic, the animation via pause() and
	 * announceVictory() method similar to announceGameOver() if the game is actually won
	 * Game win state is checked by looking at bricksRemaining variable and seeing if it
	 * is 0 or not. 
	 */
	private void playGame() {
		initializeVelocity();

		while (lives > 0) {
			// Update the ball's position first
			updateBallPos();

			// Check if the ball is lost
			if (ballY + BALL_DIAMETER >= getHeight()) {
				lives -= 1;
				livesLabel.setLabel("Lives: " + lives);
				if (lives > 0) {
					resetBall();
					initializeVelocity();
				} else {
					announceGameOver();
				}
				continue;
			}

			// Handle boundary collisions
			handleBoundaryCollision();

			// Handle collisions with other objects
			handleCollisions();

			pause(13); // Control the speed of the game loop

			// Check for victory condition
			if (bricksRemaining == 0) {
				announceVictory();
				return;
			}
		}

		// Display Game Over message if lives run out
		announceGameOver();
	}

	private void announceVictory() {
		GLabel win = new GLabel("You Win! Click to Restart.");
		win.setFont("SansSerif-18");
		add(win, (getWidth() - win.getWidth()) / 2, getHeight() / 2);
		waitForClick();
		removeAll();
		resetGame();
	}

	private void announceGameOver() {
		GLabel gameOver = new GLabel("Game Over! Click to Restart.");
		gameOver.setFont("SansSerif-18");
		add(gameOver, (getWidth() - gameOver.getWidth()) / 2, getHeight() / 2);
		waitForClick();
		removeAll();
		resetGame();
	}

	/* This method resets the ball position to the center if:
	 * lives > 0 and if the current ball Y position is below the paddle
	 * and the display getHeight().
	 */
	private void resetBall() {
		ballX = getWidth() / 2 - BALL_DIAMETER / 2;
		ballY = getHeight() / 2 - BALL_DIAMETER / 2;
		ball.setLocation(ballX, ballY);
		collisionCounts = 0;
	}

	/*
	* Exactly what the method name says. Initializing ball's 
	* velocity in both x and y axis. This is the default
	* state of the ball's movement until a collision happens.
	*/	
	private void initializeVelocity() {
		vy = 3.0;
		vx = rspeed.nextDouble(1.0, 3.0);
		if (rspeed.nextBoolean(0.5)) {
			vx = -vx;
		}
	}

	/*
	* This method is used to make the paddle and the mouse
	* positions on x axis the same. It essentially allows
	* paddle to gain the same x value as the mouse, so that it
	* can simulate movement on said axis.
	*/
	public void mouseMoved(MouseEvent e) {
		// Get the x-coordinate of the mouse, adjusted for paddle width (center alignment)
		double xPos = e.getX() - PADDLE_WIDTH / 2;

		// If the paddle goes beyond the left edge, set it to the leftmost position
		if (xPos < 0) {
			xPos = 0;
		}
		// If the paddle goes beyond the right edge, set it to the rightmost position
		else if (xPos > getWidth() - PADDLE_WIDTH) {
			xPos = getWidth() - PADDLE_WIDTH;
		}

		// Update the paddle's position to the calculated x position, at a fixed y-coordinate
		paddle.setLocation(xPos, getHeight() - PADDLE_Y_OFFSET);
	}

	/*
	 * Collision logic. We track 4 different positions of the ball and check
	 * if collider hit any GObject at specific x and y coordinates. We also
	 * make sure to exclude ball from the calculation because ball is the object
	 * we are checking collision at those x,y values with.
	 */
	private GObject getCollidingObject() {
		double x = ball.getX();
		double y = ball.getY();

		GObject collider = null;

		// Top-left corner
		collider = getElementAt(x, y);
		if (collider != null && collider != ball) {
			return collider;
		} 

		// Top-right corner
		collider = getElementAt(x + BALL_DIAMETER, y);
		if (collider != null && collider != ball) {
			return collider;
		} 

		// Bottom-left corner
		collider = getElementAt(x, y + BALL_DIAMETER);
		if (collider != null && collider != ball) {
			return collider;
		} 

		// Bottom-right corner
		collider = getElementAt(x + BALL_DIAMETER, y + BALL_DIAMETER);
		if (collider != null && collider != ball) {
			return collider;
		} 

		return null;
	}

	/*
	 * This method uses the upper getCollidingObject() method to actually
	 * implement collisions. If the collider object is the paddle, meaning
	 * that ball has called with the paddle, then we reverse its vy velocity
	 * and create a "bounce" effect. 
	 * 
	 * If the collider object is the brick then we remove the subsequent
	 * collider(the brick) and also decrease the bricksRemaining count.
	 * 
	 * Simple scoring system is also implemented. Each broken brick is +10 score
	 * and collisionCounts also checks how many collisions(with bricks) has happened.
	 * Every 7th collision icreases the vy velocity by random 1.2-2.0 range until it gets
	 * capped by MAX_VY velocity amount.
	 */
	private void handleCollisions() {
		GObject collider = getCollidingObject();

		if (collider != null) {
			if (collider == paddle) {
				vy = -Math.abs(vy); // Ensure the ball moves upwards
				bounceClip.play();
			} else if (collider instanceof GRect) {
				vy = -vy;
				remove(collider);
				bounceClip.play();
				bricksRemaining--;

				score += 10; // Each brick is worth 10 points
				scoreLabel.setLabel("Score: " + score);

				collisionCounts += 1;
				/*
				* For every 7th broken brick, bounce speed increases anywhere from
				* 1.2 to 2x of the current velocity
				*/
				if (collisionCounts % 7 == 0) {
					vy *= rspeed.nextDouble(1.2, 2.0);
					vy = capVelocity(vy, MAX_VY);
				}
			}
		}
	}

	/* Capping velocity increase to maxVelocity variable. At really high
	 * speeds checking only four ball positions is not enough for proper collison 
	 * handling and it also breaks paddle collision logic which sometimes makes 
	 * the ball "noclip" through the paddle when speed is too high. The game itself 
	 * becomes absurdly difficult to play if velocity doubling is left unchecked. 
	 * This both fixes noclip error and makes the game playable.
	 */
	private double capVelocity(double velocity, double maxVelocity) {
		if (Math.abs(velocity) > maxVelocity) {
			velocity = maxVelocity;
			return velocity;
		}
		return velocity;
	}

	// Animating the ball's movement.
	private void updateBallPos() {
		ballX += vx;
		ballY += vy;
		ball.setLocation(ballX, ballY);
	}

	/* This method makes sure to reset ball position once it hits
	 * the screen/display boundaries. It also decreases the live count
	 * if the boundary is bottom side of the screen.
	 */
	private void handleBoundaryCollision() {
		if (ballY <= 0) {
			vy = Math.abs(vy);
			lives -= 1; 
			livesLabel.setLabel("Lives: " + lives); // Update the label

			if (lives <= 0) {
				announceGameOver();
			} else {
				resetBall();
				initializeVelocity();
			}
		}

		if (ballX <= 0) {
			ballX = 0;
			vx = Math.abs(vx); 
		}

		if (ballX + BALL_DIAMETER >= getWidth()) {
			ballX = getWidth() - BALL_DIAMETER;
			vx = -Math.abs(vx); 
		}

		ball.setLocation(ballX, ballY);
	}

	/* This method ensures that resetting the whole game to its default
	 * starting position (when necessary) is simple.
	 */
	private void resetGame() {
		initializeTheGame();
		playGame();
	}
}