0% found this document useful (0 votes)
33 views

Bubble Burst

Uploaded by

Sharath Reddy
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
33 views

Bubble Burst

Uploaded by

Sharath Reddy
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 15

PROJECT

MCIS 5103: Advanced Programming


Concepts
Instructor: Dr. Justin L. Rice
Due Date: 4/3/2024: Requirements I, II, and III (Round 1), IV: 30 points
Due Date: 4/10/2024: Requirements III (Round 2 - 10), VI, VII: 35 points
Due Date: 4/17/2024: Requirements V, VIII, IX & Report: 35 points

Develop a Bubble Burst game where the objective is for a player to burst all the bubbles on
the playing field before the timer expires. (100 points)

Requirements:
I. The system shall have two GUIs (JFrames). (20 points)
A. The first GUI shall have two Jbuttons (Start and Restart) and a JSlider. The Start
button shall be used to start the game. Once it’s pressed the second GUI displays
and the game starts. Once the game ends, the Restart button shall be used to
start the game over. The JSlider shall be used for the player to select the game
difficulty: Easy (4 bubbles), Medium (5 bubbles), Hard (6 bubbles).
B. The second GUI should contain the field where the game will be played.
Solution: Designed the user interface with two JFrame windows. The mainFrame
showcases the JSlider and start/restart buttons, while the playAreaFrame
presents the game screen.
II. The Playing Field
A. The playing field shall be defined as the dimensions of the JPanel.
Solution: The second screenshot above showcases the game's playing field, established by a
JPanel component named playAreaPanel that resides within the playAreaFrame JFrame.

III. Reposition_Global (20 points)


A. For Round 1
1. The program shall prompt the user to define the origin for each bubble by
clicking the mouse on the JPanel. If the user defines an origin that will result in a
bubble not fully contained within the JPanel’s dimensions, the program shall
return an error and prompt the user to make another selection. Round 1 shall
start as soon as the number of user selections equals the number of bubbles for
the specified game difficulty.
B. For Rounds 2 – 10
1. Positions or repositions bubbles (circles) by choosing random coordinates from
the entire playing field. A Java random number function/method shall be used
for any operation that requires randomness.
Solution: Round 1 logic resides within the setBubbleOrigins method, called by clicking the
gamePanel. This method utilizes the BubbleUtil.isValidBubblePlacement function to
ensure clicks are within the panel and don't create bubble overlaps. Valid clicks result in a
drawn bubble, while invalid ones (outside or overlapping) throw an error. After successful
bubble placement, the proceedToNextRound method kicks off Round 1.

Starting from round two onwards, the repositionGlobal method is invoked at the
beginning of each round to generate random bubble positions. This method utilizes Java's
Random class to determine appropriate x and y coordinates for each bubble. To ensure
proper placement, bubbles are only drawn if they fall within the panel boundaries and do not
overlap with existing bubbles.

IV. Reposition_Local
A. Repositions bubbles by choosing a random coordinate from each bubble’s local
neighborhood. The local neighborhood is defined as a subset of the playing field where
the bubble is allowed to roam. For example, if a bubble’s center coordinate is (100, 100)
and we define a local neighborhood of 50, then the bubble can take a random hop in
the bounding box (neighborhood) – (50,150), (50, 150), (150, 50), (150,150).
Neighborhoods are drawn as rectangles for each bubble. Bubbles make hops in their
local neighborhoods until they are burst. Bubbles are not allowed to hop outside of the
playing field. The local neighborhood expands as the rounds increase. Assume a
neighborhood of 50 for Round 1, and increase by 18 for each additional round.
Solution: The proceedToNextRound method initiates a new round by repositioning all
bubbles randomly on the gamePanel using repositionGlobal method. This triggers the
repositionLocal method, which launches a separate thread. The RepositionBubbles
thread's run method executes the BubbleUtil.findValidRandomNeighbor function
repeatedly until all bubbles are eliminated. This function repositions each bubble within its
designated neighborhood (defined by neighborhoodSize) every 3 seconds, keeping the
game dynamic.

V. Collision Avoidance (20 points)


A. A bubble is not allowed to overlap or occupy the same space as another bubble.
Solution: Bubbles must adhere to specific placement rules. The
BubbleUtil.isValidBubblePlacement method ensures a proposed bubble position is
within the panel's boundaries and accommodates a size of 50. It further verifies that this
placement wouldn't cause overlap with any existing bubbles.

VI. BubbleBurst(10 points)


A. A bubble is burst once a player makes a mouse click inside any bubble (circle). Bubbles
disappear once they are burst.
Solution: Players interact with bubbles by clicking them within their boundaries. Upon a
successful click, the bubble bursts and is removed from the game. The handleClick
method calculates the distance between the click location and the bubble's center. If the
distance is less than the bubble's radius (50), the bubble is removed from the bubble list, and
the game panel is repainted to reflect the change.

VII. Rounds (10 points)


A. A new round starts when all the bubbles are burst. New bubbles are then respawned
using reposition_global. The round number shall be displayed.
Solution: The game transitions to a new round once all bubbles are eliminated. Fresh bubbles
are strategically repositioned using the repositionGlobal function. The current round
number is displayed for player reference. Logic within the handleClick method detects
when all bubbles have been burst. If the round number is less than 10, the
proceedToNextRound method is triggered, initiating the spawning of new bubbles
according to Requirement III.

VIII. Game Over (10 points)


A. The game is declared over when the player clicks anywhere on the playing field, the
player successfully completes 10 rounds, or the player takes too long to finish a round. A
JOption shall appear once the game is over, and it shall display “Game Over.”
Solution: The code in proceedToNextRound method handles successful completion of 10
rounds where handleGameOver method is called once all bubbles are popped in round 10.
Additionally, the startTimer method triggers the handleGameOver method if the timer
runs out and there's at least one un-popped bubble.

Moreover, handleClick method detects when player clicks elsewhere and not on the
bubble.
IX. Timer(10 points)
A. The timer starts at the beginning of each round. As the rounds increase, the time to
complete each round decreases. The round 1 timer is 15 seconds, and it decreases by 1
second each additional round. The timer shall be displayed.
Solution: The startTimer method establishes a countdown timer for each round. Initially
set to 15 seconds for Round 1, it decrements by 1 second for subsequent rounds. The timer is
then displayed prominently at the top of the panel.

Code
Main.java
public class Main{
public static void main(String[] args){
new GameField();
}
}

GameField.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.IntStream;

public class GameField extends JPanel {


private final JFrame mainFrame;
private JFrame playAreaFrame;
private JPanel playAreaPanel;
private final JSlider difficultySlider;
private int selectedBubbleCount;
private int currentBubbleCount;
private int currentRound;
private JButton startGameButton;
private JButton restartGameButton;
private static final int bubbleSize = 50;
private List<Point> bubbleOrigins;
private List<Point> fixedBubbleOriginsAtRoundStart;
private Timer gameTimer;
private final Random randomGenerator = new Random();
private static Thread animationThread;
private boolean gameIsWon;

public GameField() {

mainFrame = new JFrame("Bubble Burst Game");

startGameButton = new JButton("Start");


restartGameButton = new JButton("Restart");
difficultySlider = createDifficultySlider();

startGameButton.addActionListener(event -> startNewGame());


restartGameButton.addActionListener(event -> restartGame());
JPanel panel1 = new JPanel(new FlowLayout());
panel1.add(startGameButton);
panel1.add(restartGameButton);

JPanel panel2 = new JPanel(new FlowLayout());


panel2.add(difficultySlider);

mainFrame.add(panel1, BorderLayout.NORTH);
mainFrame.add(panel2, BorderLayout.CENTER);
mainFrame.setSize(400, 400);
mainFrame.setVisible(true);
mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

private JSlider createDifficultySlider() {

JSlider slider = new JSlider(SwingConstants.HORIZONTAL, 4, 6, 5);


slider.setToolTipText("Select Difficulty");
slider.setLabelTable(new Hashtable<Integer, JLabel>() {{
put(4, new JLabel("EASY 4"));
put(5, new JLabel("MEDIUM 5"));
put(6, new JLabel("HARD 6"));
}});
slider.setMinorTickSpacing(1);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
return slider;
}

private void startNewGame() {


resetGame();

selectedBubbleCount = difficultySlider.getValue();
playAreaFrame = new JFrame("Bubble Burst Game");
playAreaFrame.setSize(600, 600);
playAreaFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

playAreaPanel = createPanel();
playAreaFrame.add(playAreaPanel);
playAreaFrame.setVisible(true);

showBubblePlacementGuide();
}

private JPanel createPanel() {


JPanel panel = new JPanel() {

@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
drawOrigins(g);
}
};
panel.addMouseListener(new MouseAdapter() {

@Override
public void mouseClicked(MouseEvent event) {
if (currentRound == 0) {
setBubbleOrigins(event);
} else {
handleClick(event);
}
}
});

return panel;
}

private void showBubblePlacementGuide() {


JOptionPane.showMessageDialog(playAreaPanel, "Define the origin
for each bubble");
}

private void drawOrigins(Graphics g) {


for (Point bubble : bubbleOrigins) {
g.setColor(Color.black);
g.fillOval(bubble.x - (bubbleSize/2), bubble.y -
(bubbleSize/2), bubbleSize, bubbleSize);
}
if(fixedBubbleOriginsAtRoundStart != null)
drawNeighborhood(g);
}

public void drawNeighborhood(Graphics g){


for(int i=0; i < fixedBubbleOriginsAtRoundStart.size(); i++){
Point pt = fixedBubbleOriginsAtRoundStart.get(i);
int neighborhoodSize = bubbleSize + 18 * (currentRound == 0 ? 0
: currentRound -1);
int topLeftX = pt.x - neighborhoodSize;
int topLeftY = pt.y - neighborhoodSize;

// Generate random color


Color randomColor = new
Color(Math.abs(randomGenerator.nextInt()) % 255,
Math.abs(randomGenerator.nextInt()) % 255,
Math.abs(randomGenerator.nextInt()) % 255);

//Set a drawing color


g.setColor(randomColor);
g.drawRect(topLeftX, topLeftY, neighborhoodSize * 2,
neighborhoodSize * 2);

}
}

private void setBubbleOrigins(MouseEvent event) {


if (currentBubbleCount < selectedBubbleCount) {
Point clickedPoint = event.getPoint();
if (BubbleUtil.isValidBubblePlacement(clickedPoint,
playAreaPanel, bubbleOrigins, null)) {
currentBubbleCount++;
fixedBubbleOriginsAtRoundStart.add(clickedPoint);
bubbleOrigins.add(clickedPoint);
playAreaPanel.repaint();
if (currentBubbleCount == selectedBubbleCount) {
JOptionPane.showMessageDialog(playAreaPanel, "Lets Play");
proceedToNextRound();
}
}else {
JOptionPane.showMessageDialog(playAreaPanel, "Invalid Origin.
Choose an origin that allows the bubble to bewithin the panel ");
}
}
}

private void proceedToNextRound() {


stopTimer();
currentRound++;
int currentRoundDuration = 15 - currentRound;
if (currentRound <= 10) {
playAreaFrame.setTitle("Round-" + currentRound + ", Time Left:"
+ currentRoundDuration + " seconds");
startTimer(currentRoundDuration);
if (currentRound > 1){
repositionGlobal();
}
if (bubbleOrigins.size() == selectedBubbleCount) {
repositionLocal();
}
} else {
handleGameOver( JOptionPane.INFORMATION_MESSAGE, "You Won,
Congrats!!");
}
}

private void startTimer(final int currentRoundDuration) {


ActionListener listener = new ActionListener() {
int timeLeft = currentRoundDuration;

@Override
public void actionPerformed(ActionEvent e) {
timeLeft--;
playAreaFrame.setTitle("Round-" + currentRound + ", Time
Left:" + timeLeft + " seconds");
if(timeLeft <= 0 && gameTimer.isRunning()) {
handleGameOver(JOptionPane.ERROR_MESSAGE, "Game Over
because Time is up, Try Again!");
}
}
};

gameTimer = new Timer(1000, listener);


gameTimer.start();
}

private void stopTimer() {


if (gameTimer != null)
gameTimer.stop();
}

private void handleGameOver(int messageType, String msg) {


stopTimer();
animationThread.interrupt();
JOptionPane.showMessageDialog(playAreaPanel, msg, "Game Over",
messageType);
playAreaFrame.setVisible(false);
startGameButton.setVisible(true);
restartGameButton.setVisible(true);
mainFrame.setVisible(true);
}

private void repositionLocal() {


if(animationThread != null) animationThread.interrupt();
RepositionBubbles repositionBubbles = new
RepositionBubbles(bubbleSize, currentRound, bubbleOrigins,
playAreaPanel, fixedBubbleOriginsAtRoundStart);
animationThread = new Thread(repositionBubbles);
animationThread.start();
}

private void repositionGlobal() {


IntStream.range(0, selectedBubbleCount)
.mapToObj(index -> createNonOverlappingBubble())
.forEach(origin -> {
bubbleOrigins.add(origin);
fixedBubbleOriginsAtRoundStart.add(origin);
});
playAreaPanel.repaint();
bubbleOrigins = new ArrayList<> (fixedBubbleOriginsAtRoundStart);
}

private Point createNonOverlappingBubble() {


Point bubble;
do {
bubble = new
Point(randomGenerator.nextInt(playAreaPanel.getWidth() - bubbleSize),
randomGenerator.nextInt(playAreaPanel.getHeight() - bubbleSize));

} while (!BubbleUtil.isValidBubblePlacement(bubble,
playAreaPanel, bubbleOrigins, null));
return bubble;
}

private void handleClick(MouseEvent event) {


Point clickedPoint = event.getPoint();
int selectedIndex = -1;
for (int i=0; i < bubbleOrigins.size(); i++) {
Point origin = bubbleOrigins.get(i);
if (origin.distance(clickedPoint) <= bubbleSize) {
selectedIndex = i;
break;
}
}

if (selectedIndex != -1) {
bubbleOrigins.remove(selectedIndex);
fixedBubbleOriginsAtRoundStart.remove(selectedIndex);
playAreaPanel.repaint();

if (bubbleOrigins.isEmpty()) {
if (animationThread != null) {
animationThread.interrupt();
}
if (currentRound < 10) {
proceedToNextRound();
} else {
gameIsWon = true;
handleGameOver(JOptionPane.INFORMATION_MESSAGE, "You Won,
Congrats!");
}
}
}
else {
gameIsWon = false;
handleGameOver(JOptionPane.ERROR_MESSAGE, "You Lost as you
didn't click on bubble, try again");
}
}

private void resetGame() {

gameIsWon = false;
currentRound = 0;
currentBubbleCount = 0;
bubbleOrigins = new CopyOnWriteArrayList<>();
fixedBubbleOriginsAtRoundStart = new ArrayList<>();
}

private void restartGame() {


startNewGame();
}

RepositionBubbles.java
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

class RepositionBubbles implements Runnable {


private final Random random = new Random();
private final int bubbleSize;
private final int CURR_ROUND;
private final List<Point> BUBBLE_ORIGINS;
private final JPanel GAME_PANEL;
private final List<Point> FIXED_ORIGINS;

RepositionBubbles(int sizeOfBubble, int currRound, List<Point>


bubbleOrigins, JPanel gamePanel, List<Point>
fixedBubbleOriginsAtRoundStart) {
bubbleSize = sizeOfBubble;
CURR_ROUND = currRound;
BUBBLE_ORIGINS = bubbleOrigins;
GAME_PANEL = gamePanel;
FIXED_ORIGINS = fixedBubbleOriginsAtRoundStart;
}

@Override
public void run() {
int neighborhoodSize = bubbleSize + 18 * (CURR_ROUND - 1);
List<Point> tempOrigins = new ArrayList<>();
for(Point origin: BUBBLE_ORIGINS) {
tempOrigins.add(new Point(origin));
}
while (!Thread.currentThread().isInterrupted() && !
BUBBLE_ORIGINS.isEmpty()) {

for (int i = 0; i < BUBBLE_ORIGINS.size(); i++) {


Point origin = tempOrigins.get(i);
if ((origin = BubbleUtil.findValidRandomNeighbor(i,
neighborhoodSize, GAME_PANEL, BUBBLE_ORIGINS, FIXED_ORIGINS)) != null
&& !BUBBLE_ORIGINS.isEmpty())
BUBBLE_ORIGINS.set(i, origin);
}

try {
Thread.sleep(3000);
GAME_PANEL.repaint();
} catch (InterruptedException e) {

}
}
}
}

BubbleUtil.java
import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.stream.IntStream;

class BubbleUtil {

private static final int bubbleSize = 50;

public static Point findValidRandomNeighbor(int index, int


neighborhoodSize, JPanel gamePanel, List<Point> bubbleOrigins,
List<Point> fixedOrigins) {
if(index >= bubbleOrigins.size())
return null;
Random random = new Random();
Point origin = fixedOrigins.get(index);
int left = Math.max(0, origin.x - neighborhoodSize + bubbleSize
/2);
int bottom = Math.max(0, origin.y - neighborhoodSize + bubbleSize
/2);
int right = Math.min(origin.x + neighborhoodSize - bubbleSize /2,
gamePanel.getWidth() - bubbleSize);
int top = Math.min(origin.y + neighborhoodSize - bubbleSize /2,
gamePanel.getHeight() - bubbleSize);
Point newOrigin;
do {
int newX = left + random.nextInt(right - left + 1);
int newY = bottom + random.nextInt(top - bottom + 1);
newOrigin = new Point(newX, newY);
} while (!isValidBubblePlacement(newOrigin, gamePanel,
bubbleOrigins, bubbleOrigins.get(index)));
return newOrigin;
}

public static boolean isValidBubblePlacement(Point origin, JPanel


gamePanel, List<Point> bubbleOrigins, Point oldOrigin) {
return bubbleWithinPanel(origin, gamePanel) && !
isBubbleOverlapping(origin, bubbleOrigins, oldOrigin);
}

private static boolean bubbleWithinPanel(Point point, JPanel


gamePanel) {
return point.getX() > bubbleSize / 2
&& point.getY() > bubbleSize / 2
&& point.getX() < gamePanel.getWidth() - bubbleSize / 2
&& point.getY() < gamePanel.getHeight() - bubbleSize / 2;
}

private static boolean isBubbleOverlapping(Point origin, List<Point>


bubbleOrigins, Point oldOrigin) {
for (Point otherOrigin : bubbleOrigins) {
if (otherOrigin != null && (oldOrigin == null ||
otherOrigin.distance(oldOrigin) != 0) && otherOrigin.distance(origin)
< bubbleSize) {
return true;
}
}
return false;
}

You might also like