215 lines
6.2 KiB
Java
215 lines
6.2 KiB
Java
package fifteenpuzzle;
|
|
|
|
import java.awt.*;
|
|
import java.awt.event.*;
|
|
import java.util.Random;
|
|
import javax.swing.*;
|
|
|
|
class FifteenPuzzle extends JPanel {
|
|
|
|
private final int side = 4;
|
|
private final int numTiles = side * side - 1;
|
|
|
|
private final Random rand = new Random();
|
|
private final int[] tiles = new int[numTiles + 1];
|
|
private final int tileSize;
|
|
private int blankPos;
|
|
private final int margin;
|
|
private final int gridSize;
|
|
private boolean gameOver;
|
|
|
|
private FifteenPuzzle() {
|
|
final int dim = 640;
|
|
|
|
margin = 80;
|
|
tileSize = (dim - 2 * margin) / side;
|
|
gridSize = tileSize * side;
|
|
|
|
setPreferredSize(new Dimension(dim, dim + margin));
|
|
setBackground(Color.WHITE);
|
|
setForeground(new Color(0x6495ED)); // cornflowerblue
|
|
setFont(new Font("SansSerif", Font.BOLD, 60));
|
|
|
|
gameOver = true;
|
|
|
|
addMouseListener(new MouseAdapter() {
|
|
@Override
|
|
public void mousePressed(MouseEvent e) {
|
|
if (gameOver) {
|
|
newGame();
|
|
|
|
} else {
|
|
|
|
int ex = e.getX() - margin;
|
|
int ey = e.getY() - margin;
|
|
|
|
if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize) {
|
|
return;
|
|
}
|
|
|
|
int c1 = ex / tileSize;
|
|
int r1 = ey / tileSize;
|
|
int c2 = blankPos % side;
|
|
int r2 = blankPos / side;
|
|
|
|
int clickPos = r1 * side + c1;
|
|
|
|
int dir = 0;
|
|
if (c1 == c2 && Math.abs(r1 - r2) > 0) {
|
|
dir = (r1 - r2) > 0 ? 4 : -4;
|
|
|
|
} else if (r1 == r2 && Math.abs(c1 - c2) > 0) {
|
|
dir = (c1 - c2) > 0 ? 1 : -1;
|
|
}
|
|
|
|
if (dir != 0) {
|
|
do {
|
|
int newBlankPos = blankPos + dir;
|
|
tiles[blankPos] = tiles[newBlankPos];
|
|
blankPos = newBlankPos;
|
|
} while (blankPos != clickPos);
|
|
tiles[blankPos] = 0;
|
|
}
|
|
|
|
gameOver = isSolved();
|
|
}
|
|
repaint();
|
|
}
|
|
});
|
|
|
|
newGame();
|
|
}
|
|
|
|
private void newGame() {
|
|
do {
|
|
reset();
|
|
shuffle();
|
|
} while (!isSolvable());
|
|
gameOver = false;
|
|
}
|
|
|
|
private void reset() {
|
|
for (int i = 0; i < tiles.length; i++) {
|
|
tiles[i] = (i + 1) % tiles.length;
|
|
}
|
|
blankPos = tiles.length - 1;
|
|
}
|
|
|
|
private void shuffle() {
|
|
// don't include the blank space in the shuffle, leave it
|
|
// in the home position
|
|
int n = numTiles;
|
|
while (n > 1) {
|
|
int r = rand.nextInt(n--);
|
|
int tmp = tiles[r];
|
|
tiles[r] = tiles[n];
|
|
tiles[n] = tmp;
|
|
}
|
|
}
|
|
|
|
/* Only half the permutations of the puzzle are solvable.
|
|
|
|
Whenever a tile is preceded by a tile with higher value it counts
|
|
as an inversion. In our case, with the blank space in the home
|
|
position, the number of inversions must be even for the puzzle
|
|
to be solvable.
|
|
|
|
See also:
|
|
www.cs.bham.ac.uk/~mdr/teaching/modules04/java2/TilesSolvability.html
|
|
*/
|
|
private boolean isSolvable() {
|
|
int countInversions = 0;
|
|
for (int i = 0; i < numTiles; i++) {
|
|
for (int j = 0; j < i; j++) {
|
|
if (tiles[j] > tiles[i]) {
|
|
countInversions++;
|
|
}
|
|
}
|
|
}
|
|
return countInversions % 2 == 0;
|
|
}
|
|
|
|
private boolean isSolved() {
|
|
if (tiles[tiles.length - 1] != 0) {
|
|
return false;
|
|
}
|
|
for (int i = numTiles - 1; i >= 0; i--) {
|
|
if (tiles[i] != i + 1) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void drawGrid(Graphics2D g) {
|
|
for (int i = 0; i < tiles.length; i++) {
|
|
int r = i / side;
|
|
int c = i % side;
|
|
int x = margin + c * tileSize;
|
|
int y = margin + r * tileSize;
|
|
|
|
if (tiles[i] == 0) {
|
|
if (gameOver) {
|
|
g.setColor(Color.GREEN);
|
|
drawCenteredString(g, "\u2713", x, y);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
g.setColor(getForeground());
|
|
g.fillRoundRect(x, y, tileSize, tileSize, 25, 25);
|
|
g.setColor(Color.blue.darker());
|
|
g.drawRoundRect(x, y, tileSize, tileSize, 25, 25);
|
|
g.setColor(Color.WHITE);
|
|
|
|
drawCenteredString(g, String.valueOf(tiles[i]), x, y);
|
|
}
|
|
}
|
|
|
|
private void drawStartMessage(Graphics2D g) {
|
|
if (gameOver) {
|
|
g.setFont(getFont().deriveFont(Font.BOLD, 18));
|
|
g.setColor(getForeground());
|
|
String s = "click to start a new game";
|
|
int x = (getWidth() - g.getFontMetrics().stringWidth(s)) / 2;
|
|
int y = getHeight() - margin;
|
|
g.drawString(s, x, y);
|
|
}
|
|
}
|
|
|
|
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
|
|
FontMetrics fm = g.getFontMetrics();
|
|
int asc = fm.getAscent();
|
|
int des = fm.getDescent();
|
|
|
|
x = x + (tileSize - fm.stringWidth(s)) / 2;
|
|
y = y + (asc + (tileSize - (asc + des)) / 2);
|
|
|
|
g.drawString(s, x, y);
|
|
}
|
|
|
|
@Override
|
|
public void paintComponent(Graphics gg) {
|
|
super.paintComponent(gg);
|
|
Graphics2D g = (Graphics2D) gg;
|
|
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
|
RenderingHints.VALUE_ANTIALIAS_ON);
|
|
|
|
drawGrid(g);
|
|
drawStartMessage(g);
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
SwingUtilities.invokeLater(() -> {
|
|
JFrame f = new JFrame();
|
|
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
f.setTitle("Fifteen Puzzle");
|
|
f.setResizable(false);
|
|
f.add(new FifteenPuzzle(), BorderLayout.CENTER);
|
|
f.pack();
|
|
f.setLocationRelativeTo(null);
|
|
f.setVisible(true);
|
|
});
|
|
}
|
|
}
|