Learning Objectives
GUI Application Development
Event Handling
Data Structures and Algorithms
Timer Implementation
Graphic Interface Design
Overview
1. Game Window and Interface Design
We begin by creating a game window class PuzzleFrame that extends JFrame. This class is responsible for displaying the game interface and handling game logic. The interface design utilizes JPanel components to arrange puzzle pieces and buttons, while JLabel components are used to display the game title and background images.
2. Game Data and Logic
The puzzle state is represented by a 2D array gameBoard, where 0 represents the empty space. We define a corresponding victory state array victoryBoard to determine if the game has been won. The core game logic includes moving puzzle pieces, checking for victory conditions, and shuffling the puzzle.
3. User Interaction and Event Handling
User interactions are handled by adding event listeners to buttons and the window. Players can move puzzle pieces by clicking buttons or using keyboard keys. Additional features include displaying helpful messages and resetting the game.
4. Timer and Interface Updates
To enhance gameplay, we implement a timer to track game duration. The timer updates every second and displays the current time in the window title. After each puzzle piece movement, the interface is redrawn to reflect the updated game state.
Key Code Examples
1. Timer Implementation
private Timer gameTimer;
private int elapsedTime;
/**
* Start the timer and update elapsed time
*/
private void startTimer() {
// Create timer that triggers actionPerformed method every second
gameTimer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
elapsedTime++; // Increment seconds each timer trigger
setTitle("Anime Puzzle - Time: " + elapsedTime + " seconds"); // Update window title with current time
}
});
elapsedTime = 0; // Initialize seconds to 0
gameTimer.start(); // Start the timer
}
First, we declare a Timer object gameTimer and an integer variable elapsedTime to track seconds. Then, we define a startTimer method to initiate the timer. In this method:
- Create a Timer object with a 1000-millisecond trigger interval and an anonymous ActionListener that implements the actionPerformed method.
- In the actionPerformed method, increment the seconds count and update the window title with the current elapsed time.
- Initialize seconds to 0 and start the timer.
2. 2D Array Manipulation
/**
* Randomly shuffle the puzzle pieces
*/
private void shufflePuzzle() {
// Create a Random object for generating random numbers
Random randomGenerator = new Random();
// Use nested loops to traverse the 2D array
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard.length; j++) {
// Generate two random indices for swapping
int rowIndex = randomGenerator.nextInt(gameBoard.length);
int colIndex = randomGenerator.nextInt(gameBoard[rowIndex].length);
// Swap values between current position and random position
int temporary = gameBoard[i][j];
gameBoard[i][j] = gameBoard[rowIndex][colIndex];
gameBoard[rowIndex][colIndex] = temporary;
}
}
// Locate the empty space (value 0) in the array
findEmptyPosition();
}
/**
* Find and record the position of the empty space
*/
private void findEmptyPosition() {
exitLoop:
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard.length; j++) {
if (gameBoard[i][j] == 0) {
emptyRow = i; // Record empty space row index
emptyCol = j; // Record empty space column index
break exitLoop; // Exit the loop
}
}
}
}
- Create a Random object for generating random numbers.
- Use nested for loops to traverse the 2D array and swap values at each position with a randomly selected position.
- After shuffling, locate the empty space (value 0) and record its position.
3. Movement Logic
/**
* Move the empty space upward
*/
private void moveUp() {
// If empty space is at the top, don't move
if (emptyRow == 0) {
return;
}
// If empty space is not at the top, move it up
if (emptyRow > 0) {
// Swap empty space with the piece above it
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow - 1][emptyCol];
gameBoard[emptyRow - 1][emptyCol] = 0;
// Update empty space position
emptyRow--;
}
// Check if puzzle is solved
if (isVictory()) {
victory();
}
// Redraw the game interface
refreshView();
}
/**
* Move the empty space downward
*/
private void moveDown() {
// If empty space is at the bottom, don't move
if (emptyRow == 3) {
return;
}
// If empty space is not at the bottom, move it down
if (emptyRow < 3) {
// Swap empty space with the piece below it
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow + 1][emptyCol];
gameBoard[emptyRow + 1][emptyCol] = 0;
// Update empty space position
emptyRow++;
}
// Check if puzzle is solved
if (isVictory()) {
victory();
}
// Redraw the game interface
refreshView();
}
/**
* Move the empty space leftward
*/
private void moveLeft() {
// If empty space is at the left edge, don't move
if (emptyCol == 0) {
return;
}
// If empty space is not at the left edge, move it left
if (emptyCol > 0) {
// Swap empty space with the piece to its left
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow][emptyCol - 1];
gameBoard[emptyRow][emptyCol - 1] = 0;
// Update empty space position
emptyCol--;
}
// Check if puzzle is solved
if (isVictory()) {
victory();
}
// Redraw the game interface
refreshView();
}
/**
* Move the empty space rightward
*/
private void moveRight() {
// If empty space is at the right edge, don't move
if (emptyCol == 3) {
return;
}
// If empty space is not at the right edge, move it right
if (emptyCol < 3) {
// Swap empty space with the piece to its right
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow][emptyCol + 1];
gameBoard[emptyRow][emptyCol + 1] = 0;
// Update empty space position
emptyCol++;
}
// Check if puzzle is solved
if (isVictory()) {
victory();
}
// Redraw the game interface
refreshView();
}
- moveUp() method: Moves the empty space upward if possible, checks for victory, and refreshes the view.
- moveDown() method: Moves the empty space downward if possible, checks for victory, and refreshes the view.
- moveLeft() method: Moves the empty space leftward if possible, checks for victory, and refreshes the view.
- moveRight() method: Moves the empty space rightward if possible, checks for victory, and refreshes the view.
Complete Code Implementation
1. Main File: PuzzleFrame
package com.puzzle;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyAdapter;
import java.util.Random;
/**
* Game window class for displaying the game interface and handling game logic
*/
public class PuzzleFrame extends JFrame {
// Game board array representing puzzle state
private int[][] gameBoard = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// Victory state array for comparison
private int[][] victoryBoard = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 0}
};
// Row and column of the empty space
private int emptyRow;
private int emptyCol;
// Button and panel components
private JButton upButton;
private JButton downButton;
private JButton leftButton;
private JButton rightButton;
private JButton helpButton;
private JButton resetButton;
private JPanel gamePanel;
// Timer and elapsed time
private Timer gameTimer;
private int elapsedTime;
// Time display label
private JLabel timeLabel;
/**
* Constructor to initialize game interface and components
*/
public PuzzleFrame() {
initializeFrame();
shufflePuzzle();
paintView();
addButtonListeners();
addKeyboardListener();
// Start the timer
startTimer();
this.setVisible(true);
this.setFocusable(true);
}
/**
* Actions to perform when game is won
*/
private void victory() {
gameBoard = new int[][]{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
// Disable movement buttons
upButton.setEnabled(false);
rightButton.setEnabled(false);
downButton.setEnabled(false);
leftButton.setEnabled(false);
}
/**
* Check if current game state is victorious
*
* @return true if game is won
*/
public boolean isVictory() {
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard[i].length; j++) {
if (gameBoard[i][j] != victoryBoard[i][j]) {
return false;
}
}
}
return true;
}
/**
* Redraw the game interface
*/
private void refreshView() {
gamePanel.removeAll();
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard[i].length; j++) {
JLabel imageLabel = new JLabel(new ImageIcon("images/" + gameBoard[i][j] + ".png"));
imageLabel.setBounds(j * 90, i * 90, 90, 90);
gamePanel.add(imageLabel);
gamePanel.repaint();
}
}
}
/**
* Add event listeners to buttons
*/
private void addButtonListeners() {
upButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveUp();
requestFocusInWindow();
}
});
downButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveDown();
requestFocusInWindow();
}
});
leftButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveLeft();
requestFocusInWindow();
}
});
rightButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
moveRight();
requestFocusInWindow();
}
});
helpButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(PuzzleFrame.this, "Contact support", "Help", JOptionPane.INFORMATION_MESSAGE);
}
});
resetButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
shufflePuzzle();
refreshView();
}
});
}
/**
* Add keyboard event listener
*/
private void addKeyboardListener() {
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
switch (keyCode) {
case KeyEvent.VK_UP:
moveUp();
break;
case KeyEvent.VK_DOWN:
moveDown();
break;
case KeyEvent.VK_LEFT:
moveLeft();
break;
case KeyEvent.VK_RIGHT:
moveRight();
break;
}
}
});
}
/**
* Move the empty space upward
*/
private void moveUp() {
if (emptyRow == 0) {
return;
}
if (emptyRow > 0) {
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow - 1][emptyCol];
gameBoard[emptyRow - 1][emptyCol] = 0;
emptyRow--;
}
if (isVictory()) {
victory();
}
refreshView();
}
/**
* Move the empty space downward
*/
private void moveDown() {
if (emptyRow == 3) {
return;
}
if (emptyRow < 3) {
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow + 1][emptyCol];
gameBoard[emptyRow + 1][emptyCol] = 0;
emptyRow++;
}
if (isVictory()) {
victory();
}
refreshView();
}
/**
* Move the empty space leftward
*/
private void moveLeft() {
if (emptyCol == 0) {
return;
}
if (emptyCol > 0) {
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow][emptyCol - 1];
gameBoard[emptyRow][emptyCol - 1] = 0;
emptyCol--;
}
if (isVictory()) {
victory();
}
refreshView();
}
/**
* Move the empty space rightward
*/
private void moveRight() {
if (emptyCol == 3) {
return;
}
if (emptyCol < 3) {
gameBoard[emptyRow][emptyCol] = gameBoard[emptyRow][emptyCol + 1];
gameBoard[emptyRow][emptyCol + 1] = 0;
emptyCol++;
}
if (isVictory()) {
victory();
}
refreshView();
}
/**
* Randomly shuffle the puzzle
*/
private void shufflePuzzle() {
Random randomGenerator = new Random();
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard.length; j++) {
int rowIndex = randomGenerator.nextInt(gameBoard.length);
int colIndex = randomGenerator.nextInt(gameBoard[rowIndex].length);
int temporary = gameBoard[i][j];
gameBoard[i][j] = gameBoard[rowIndex][colIndex];
gameBoard[rowIndex][colIndex] = temporary;
}
}
findEmptyPosition();
}
/**
* Find the position of the empty space
*/
private void findEmptyPosition() {
exitLoop:
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard.length; j++) {
if (gameBoard[i][j] == 0) {
emptyRow = i;
emptyCol = j;
break exitLoop;
}
}
}
}
/**
* Draw the game interface
*/
private void paintView() {
JLabel title = new JLabel(new ImageIcon("images/title.png"));
title.setBounds(354, 27, 232, 57);
this.add(title);
gamePanel = new JPanel();
gamePanel.setBounds(150, 114, 360, 360);
gamePanel.setLayout(null);
for (int i = 0; i < gameBoard.length; i++) {
for (int j = 0; j < gameBoard[i].length; j++) {
JLabel imageLabel = new JLabel(new ImageIcon("images/" + gameBoard[i][j] + ".png"));
imageLabel.setBounds(90 * j, 90 * i, 90, 90);
gamePanel.add(imageLabel);
}
}
this.add(gamePanel);
JLabel referenceImage = new JLabel(new ImageIcon("images/reference.png"));
referenceImage.setBounds(574, 114, 122, 121);
this.add(referenceImage);
upButton = new JButton(new ImageIcon("images/up.png"));
downButton = new JButton(new ImageIcon("images/down.png"));
leftButton = new JButton(new ImageIcon("images/left.png"));
rightButton = new JButton(new ImageIcon("images/right.png"));
helpButton = new JButton(new ImageIcon("images/help.png"));
resetButton = new JButton(new ImageIcon("images/reset.png"));
upButton.setBounds(732, 265, 57, 57);
downButton.setBounds(732, 347, 57, 57);
leftButton.setBounds(650, 347, 57, 57);
rightButton.setBounds(813, 347, 57, 57);
helpButton.setBounds(626, 444, 108, 45);
resetButton.setBounds(786, 444, 108, 45);
this.add(upButton);
this.add(downButton);
this.add(leftButton);
this.add(rightButton);
this.add(helpButton);
this.add(resetButton);
JLabel background = new JLabel(new ImageIcon("images/background.png"));
background.setBounds(0, 0, 960, 530);
this.add(background);
}
/**
* Initialize the game window
*/
private void initializeFrame() {
this.setSize(960, 565);
this.setTitle("Anime Puzzle");
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setLayout(null);
}
/**
* Start the timer and update elapsed time
*/
private void startTimer() {
gameTimer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
elapsedTime++;
setTitle("Anime Puzzle - Time: " + elapsedTime + " seconds");
}
});
elapsedTime = 0;
gameTimer.start();
}
}
2. Program Entry Point: App
package com.puzzle;
public class App {
public static void main(String[] args) {
PuzzleFrame gameWindow = new PuzzleFrame();
}
}