Java Swing Puzzle game
last modified January 10, 2023
In this chapter, we will create a simple puzzle game in Java Swing. The sources are available at the author's Github repository.
The goal of this little game is to form a picture. Buttons containing images are moved by clicking on them. Only buttons adjacent to the empty button can be moved.
In this game, among other things, we learn how to crop an image into parts.
package com.zetcode; import javax.imageio.ImageIO; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Color; import java.awt.EventQueue; import java.awt.GridLayout; import java.awt.Image; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.awt.image.CropImageFilter; import java.awt.image.FilteredImageSource; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; class MyButton extends JButton { private boolean isLastButton; public MyButton() { super(); initUI(); } public MyButton(Image image) { super(new ImageIcon(image)); initUI(); } private void initUI() { isLastButton = false; BorderFactory.createLineBorder(Color.gray); addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { setBorder(BorderFactory.createLineBorder(Color.yellow)); } @Override public void mouseExited(MouseEvent e) { setBorder(BorderFactory.createLineBorder(Color.gray)); } }); } public void setLastButton() { isLastButton = true; } public boolean isLastButton() { return isLastButton; } } public class PuzzleEx extends JFrame { private JPanel panel; private BufferedImage source; private BufferedImage resized; private Image image; private MyButton lastButton; private int width, height; private List<MyButton> buttons; private List<Point> solution; private final int NUMBER_OF_BUTTONS = 12; private final int DESIRED_WIDTH = 300; public PuzzleEx() { initUI(); } private void initUI() { solution = new ArrayList<>(); solution.add(new Point(0, 0)); solution.add(new Point(0, 1)); solution.add(new Point(0, 2)); solution.add(new Point(1, 0)); solution.add(new Point(1, 1)); solution.add(new Point(1, 2)); solution.add(new Point(2, 0)); solution.add(new Point(2, 1)); solution.add(new Point(2, 2)); solution.add(new Point(3, 0)); solution.add(new Point(3, 1)); solution.add(new Point(3, 2)); buttons = new ArrayList<>(); panel = new JPanel(); panel.setBorder(BorderFactory.createLineBorder(Color.gray)); panel.setLayout(new GridLayout(4, 3, 0, 0)); try { source = loadImage(); int h = getNewHeight(source.getWidth(), source.getHeight()); resized = resizeImage(source, DESIRED_WIDTH, h, BufferedImage.TYPE_INT_ARGB); } catch (IOException ex) { JOptionPane.showMessageDialog(this, "Could not load image", "Error", JOptionPane.ERROR_MESSAGE); } width = resized.getWidth(null); height = resized.getHeight(null); add(panel, BorderLayout.CENTER); for (int i = 0; i < 4; i++) { for (int j = 0; j < 3; j++) { image = createImage(new FilteredImageSource(resized.getSource(), new CropImageFilter(j * width / 3, i * height / 4, (width / 3), height / 4))); var button = new MyButton(image); button.putClientProperty("position", new Point(i, j)); if (i == 3 && j == 2) { lastButton = new MyButton(); lastButton.setBorderPainted(false); lastButton.setContentAreaFilled(false); lastButton.setLastButton(); lastButton.putClientProperty("position", new Point(i, j)); } else { buttons.add(button); } } } Collections.shuffle(buttons); buttons.add(lastButton); for (int i = 0; i < NUMBER_OF_BUTTONS; i++) { var btn = buttons.get(i); panel.add(btn); btn.setBorder(BorderFactory.createLineBorder(Color.gray)); btn.addActionListener(new ClickAction()); } pack(); setTitle("Puzzle"); setResizable(false); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); } private int getNewHeight(int w, int h) { double ratio = DESIRED_WIDTH / (double) w; int newHeight = (int) (h * ratio); return newHeight; } private BufferedImage loadImage() throws IOException { var bimg = ImageIO.read(new File("src/resources/icesid.jpg")); return bimg; } private BufferedImage resizeImage(BufferedImage originalImage, int width, int height, int type) { var resizedImage = new BufferedImage(width, height, type); var g = resizedImage.createGraphics(); g.drawImage(originalImage, 0, 0, width, height, null); g.dispose(); return resizedImage; } private class ClickAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { checkButton(e); checkSolution(); } private void checkButton(ActionEvent e) { int lidx = 0; for (MyButton button : buttons) { if (button.isLastButton()) { lidx = buttons.indexOf(button); } } var button = (JButton) e.getSource(); int bidx = buttons.indexOf(button); if ((bidx - 1 == lidx) || (bidx + 1 == lidx) || (bidx - 3 == lidx) || (bidx + 3 == lidx)) { Collections.swap(buttons, bidx, lidx); updateButtons(); } } private void updateButtons() { panel.removeAll(); for (JComponent btn : buttons) { panel.add(btn); } panel.validate(); } } private void checkSolution() { var current = new ArrayList<Point>(); for (JComponent btn : buttons) { current.add((Point) btn.getClientProperty("position")); } if (compareList(solution, current)) { JOptionPane.showMessageDialog(panel, "Finished", "Congratulation", JOptionPane.INFORMATION_MESSAGE); } } public static boolean compareList(List ls1, List ls2) { return ls1.toString().contentEquals(ls2.toString()); } public static void main(String[] args) { EventQueue.invokeLater(() -> { var puzzle = new PuzzleEx(); puzzle.setVisible(true); }); } }
We use an image of a Sid character from the Ice Age movie.
We scale the image and cut it into 12 pieces. These pieces
are used by JButton
components. The last piece
is not used; we have an empty button instead. You can
download some reasonably large picture and use it in this game.
addMouseListener(new MouseAdapter() { @Override public void mouseEntered(MouseEvent e) { setBorder(BorderFactory.createLineBorder(Color.yellow)); } @Override public void mouseExited(MouseEvent e) { setBorder(BorderFactory.createLineBorder(Color.gray)); } });
When we hover a mouse pointer over the button, its border changes to yellow colour.
public boolean isLastButton() { return isLastButton; }
There is one button that we call the last button. It is a button that does not have an image. Other buttons swap space with this one.
private final int DESIRED_WIDTH = 300;
The image that we use to form is scaled to have the desired width. With the
getNewHeight()
method we calculate the new height, keeping the
image's ratio.
solution.add(new Point(0, 0)); solution.add(new Point(0, 1)); solution.add(new Point(0, 2)); solution.add(new Point(1, 0)); ...
The solution array list stores the correct order of buttons which forms
the image. Each button is identified by one Point
.
panel.setLayout(new GridLayout(4, 3, 0, 0));
We use a GridLayout
to store our components. The layout consists
of 4 rows and 3 columns.
image = createImage(new FilteredImageSource(resized.getSource(), new CropImageFilter(j * width / 3, i * height / 4, (width / 3), height / 4)));
CropImageFilter
is used to cut a rectangular shape from
the already resized image source. It is meant to be used in conjunction
with a FilteredImageSource
object to produce cropped
versions of existing images.
button.putClientProperty("position", new Point(i, j));
Buttons are identified by their position
client property.
It is a point containing the button's correct row and colum position
in the picture. These properties are used to find out if we have the
correct order of buttons in the window.
if (i == 3 && j == 2) { lastButton = new MyButton(); lastButton.setBorderPainted(false); lastButton.setContentAreaFilled(false); lastButton.setLastButton(); lastButton.putClientProperty("position", new Point(i, j)); } else { buttons.add(button); }
The button with no image is called the last button; it is placed at the
end of the grid in the bottom-right corner. It is the button that swaps
its position with the adjacent button that is being clicked. We set its isLastButton
flag with the setLastButton()
method.
Collections.shuffle(buttons); buttons.add(lastButton);
We randomly reorder the elements of the buttons
list.
The last button, i.e. the button with no image, is inserted at
the end of the list. It is not supposed to be shuffled, it always
goes at the end when we start the Puzzle game.
for (int i = 0; i < NUMBER_OF_BUTTONS; i++) { var btn = buttons.get(i); panel.add(btn); btn.setBorder(BorderFactory.createLineBorder(Color.gray)); btn.addActionListener(new ClickAction()); }
All the components from the buttons
list are placed
on the panel. We create some gray border around the buttons and
add a click action listener.
private int getNewHeight(int w, int h) { double ratio = DESIRED_WIDTH / (double) w; int newHeight = (int) (h * ratio); return newHeight; }
The getNewHeight()
method calculates the height of the image
based on the desired width. The image's ratio is kept. We scale the image using these values.
private BufferedImage loadImage() throws IOException { var bimg = ImageIO.read(new File("src/resources/icesid.jpg")); return bimg; }
A JPG image is loaded from the disk. ImageIO's
read()
method returns a BufferedImage
, which is Swing's important class
for manipulating images.
private BufferedImage resizeImage(BufferedImage originalImage, int width, int height, int type) throws IOException { var resizedImage = new BufferedImage(width, height, type); var g = resizedImage.createGraphics(); g.drawImage(originalImage, 0, 0, width, height, null); g.dispose(); return resizedImage; }
The original image is resized by creating a new BufferedImage
with
new dimensions. We paint from the original image into this new buffered image.
private void checkButton(ActionEvent e) { int lidx = 0; for (MyButton button : buttons) { if (button.isLastButton()) { lidx = buttons.indexOf(button); } } var button = (JButton) e.getSource(); int bidx = buttons.indexOf(button); if ((bidx - 1 == lidx) || (bidx + 1 == lidx) || (bidx - 3 == lidx) || (bidx + 3 == lidx)) { Collections.swap(buttons, bidx, lidx); updateButtons(); } }
Buttons are stored in an array list. This list is then mapped to the
grid of the panel. We get the indexes of the last button and the clicked
button. They are swapped using the Collections.swap()
if they
are adjacent.
private void updateButtons() { panel.removeAll(); for (JComponent btn : buttons) { panel.add(btn); } panel.validate(); }
The updateButtons()
method maps the list to the
panel's grid. First, all components are removed with the removeAll()
method. A for loop is used to go trough the buttons
list
to add the reordered buttons back to the panel's layout manager.
Finally, the validate()
method implements the new layout.
private void checkSolution() { var current = new ArrayList<Point>(); for (JComponent btn : buttons) { current.add((Point) btn.getClientProperty("position")); } if (compareList(solution, current)) { JOptionPane.showMessageDialog(panel, "Finished", "Congratulation", JOptionPane.INFORMATION_MESSAGE); } }
Solution checking is done by comparing the list of points of the correctly ordered buttons with the current list containg the order of buttons from the window. A message dialog is shown in case the solution is reached.
This was the Puzzle game.