"Tag" am Java - wéi e vollwäertegt Spill z'entwéckelen
"Fofzéng" oder "Fofzéng" ass en exzellent Beispill vun engem einfache Logik-Spill dat op der ganzer Welt populär ass. Fir de Puzzel ze léisen, musst Dir d'Plaze mat Zuelen an Uerdnung arrangéieren, vu klengst bis gréissten. Et ass net einfach, awer et ass interessant.
Am Tutorial vun haut wäerte mir Iech weisen wéi Dir Fifteen am Java 8 mat Eclipse entwéckelt. Fir den UI z'entwéckelen benotze mir d'Swing API.
Mir erënneren Iech:fir all Habr Lieser - eng Remise vun 10 Rubel wann Dir Iech an all Skillbox Cours aschreift mat dem Habr Promo Code.
Op dëser Etapp musst Dir d'Eegeschafte definéieren:
Gréisst - Gréisst vum Spillfeld;
nbTiles - Zuel vun Tags am Feld. nbTiles = Gréisst * Gréisst - 1;
Fliesen ass en Tag deen eng eendimensional Array vun ganz Zuelen ass. Jiddereng vun den Tags kritt en eenzegaartege Wäert am Beräich [0, nbTiles]. Null weist en eidele Quadrat un;
blankPos - Positioun vum eidele Quadrat.
Spill Logik
Mir mussen e Reset Method definéieren benotzt fir eng nei Spill Positioun initialize. Op dës Manéier setzen mir e Wäert fir all Element vun der Tags-Array. Gutt, da setzen mir blankPos an der leschter Positioun vun der Array.
Mir brauchen och eng Shuffle-Methode fir d'Array vun Tags ze shuffle. Mir enthalen net den eidelen Tag am Shufflingprozess fir en an der selwechter Positioun ze loossen.
Well nëmmen d'Halschent vun de méigleche Startpositioune vum Puzzel eng Léisung hunn, musst Dir dat resultéierend Shuffle-Resultat kontrolléieren fir sécher ze stellen datt den aktuellen Layout souguer léisbar ass. Fir dëst ze maachen, definéiere mir d'isSolvable Method.
Wann e bestëmmte Tag vun engem Tag mat engem méi héije Wäert viraus ass, gëtt et als Inversioun ugesinn. Wann déi eidel Plaz op der Plaz ass, muss d'Zuel vun den Inversioune souguer sinn fir datt d'Puzzel léisbar ass. Also zielen mir d'Zuel vun den Inversioune a ginn richteg zréck wann d'Zuel gläich ass.
Et ass dann wichteg d'isSolved Method ze definéieren fir ze kontrolléieren ob eise Game Of Fifteen Layout geléist ass. Als éischt kucke mir wou déi eidel Plaz ass. Wann an der éischter Positioun, dann ass déi aktuell Ausriichtung nei, net virdru decidéiert. Mir iteréieren dann duerch d'Fliesen an ëmgedréint Uerdnung, a wann de Wäert vum Tag anescht ass wéi den entspriechende Index +1, gi mir falsch zréck. Soss ass et um Enn vun der Method Zäit fir richteg zréckzekommen, well d'Puzzel ass scho geléist.
Eng aner Method déi muss definéiert ginn ass newGame. Et ass néideg fir eng nei Instanz vum Spill ze kreéieren. Fir dëst ze maachen, setzen mir d'Spillfeld zréck, shuffle se dann a fuere weider bis d'Spillpositioun opléisbar ass.
Hei ass e Beispillcode mat der Schlëssellogik vum Tag:
private void newGame() {
do {
reset(); // reset in initial state
shuffle(); // shuffle
} while(!isSolvable()); // make it until grid be solvable
gameOver = false;
}
private void reset() {
for (int i = 0; i < tiles.length; i++) {
tiles[i] = (i + 1) % tiles.length;
}
// we set blank cell at the last
blankPos = tiles.length - 1;
}
private void shuffle() {
// don't include the blank tile in the shuffle, leave in the solved position
int n = nbTiles;
while (n > 1) {
int r = RANDOM.nextInt(n--);
int tmp = tiles[r];
tiles[r] = tiles[n];
tiles[n] = tmp;
}
}
// Only half 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 tile in the solved position,
// the number of inversions must be even for the puzzle to be solvable
private boolean isSolvable() {
int countInversions = 0;
for (int i = 0; i < nbTiles; 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) // if blank tile is not in the solved position ==> not solved
return false;
for (int i = nbTiles - 1; i >= 0; i--) {
if (tiles[i] != i + 1)
return false;
}
return true;
}
Schlussendlech musst Dir d'Bewegung vun den Tags an der Array programméieren. Dëse Code gëtt spéider iwwer e Réckruff genannt fir op d'Cursorbewegung ze reagéieren. Eist Spill ënnerstëtzt verschidde Fliesbewegungen zur selwechter Zäit. Also, nodeems mir déi gedréckt Positioun um Bildschierm an e Tag ëmgewandelt hunn, kréie mir d'Positioun vum eidelen Tag a sichen no enger Bewegungsrichtung fir e puer vu senge Bewegungen zur selwechter Zäit z'ënnerstëtzen.
Hei ass e Beispill Code:
// get position of the click
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// click in the grid ?
if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize)
return;
// get position in the grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// get position of the blank cell
int c2 = blankPos % size;
int r2 = blankPos / size;
// we convert in the 1D coord
int clickPos = r1 * size + c1;
int dir = 0;
// we search direction for multiple tile moves at once
if (c1 == c2 && Math.abs(r1 - r2) > 0)
dir = (r1 - r2) > 0 ? size : -size;
else if (r1 == r2 && Math.abs(c1 - c2) > 0)
dir = (c1 - c2) > 0 ? 1 : -1;
if (dir != 0) {
// we move tiles in the direction
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
Mir entwéckelen UI mat Swing API
Et ass Zäit un der Interface ze schaffen. Als éischt huelen mir d'Jpanel Klass. Da zéie mir Tags um Feld - fir d'Gréisst vun all ze berechnen, benotze mir d'Donnéeën, déi am Spillkonstruktorparameter spezifizéiert sinn:
Margin ass och e Parameter, deen am Spillkonstruktor gesat gëtt.
Elo musse mir d'DrawGrid Method definéieren fir d'Gitter a Flecken um Bildschierm ze zéien. Mir analyséieren d'Array vun Tags a konvertéieren d'Koordinaten an d'Benotzerinterface Koordinaten. Dann molen all Plaz mat der entspriechender Zuel am Zentrum:
private void drawGrid(Graphics2D g) {
for (int i = 0; i < tiles.length; i++) {
// we convert 1D coords to 2D coords given the size of the 2D Array
int r = i / size;
int c = i % size;
// we convert in coords on the UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// check special case for blank tile
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// for other tiles
g.setColor(getForeground());
g.fillRoundRect(x, y, tileSize, tileSize, 25, 25);
g.setColor(Color.BLACK);
g.drawRoundRect(x, y, tileSize, tileSize, 25, 25);
g.setColor(Color.WHITE);
drawCenteredString(g, String.valueOf(tiles[i]), x , y);
}
}
Schlussendlech, loosst eis d'PaintComponent Method iwwerschreiden, déi aus der JPane Klass ofgeleet. Mir benotzen dann d'DrawGrid Method, gefollegt vun der DrawStartMessage Method fir e Message ze weisen deen eis freet ze klickt fir d'Spill ze starten:
private void drawStartMessage(Graphics2D g) {
if (gameOver) {
g.setFont(getFont().deriveFont(Font.BOLD, 18));
g.setColor(FOREGROUND_COLOR);
String s = "Click to start new game";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// center string s for the given tile (x,y)
FontMetrics fm = g.getFontMetrics();
int asc = fm.getAscent();
int desc = fm.getDescent();
g.drawString(s, x + (tileSize - fm.stringWidth(s)) / 2,
y + (asc + (tileSize - (asc + desc)) / 2));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2D = (Graphics2D) g;
g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(g2D);
drawStartMessage(g2D);
}
Reagéiert op Benotzeraktiounen am UI
Fir datt d'Spill säi Laf leeft, ass et néideg Benotzeraktiounen an der UI ze veraarbecht. Fir dëst ze maachen, füügt d'Implementatioun vu MouseListener op Jpanel an de Code fir d'Bewegungsplazen un, scho gewisen:
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// used to let users to interact on the grid by clicking
// it's time to implement interaction with users to move tiles to solve the game !
if (gameOver) {
newGame();
} else {
// get position of the click
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// click in the grid ?
if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize)
return;
// get position in the grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// get position of the blank cell
int c2 = blankPos % size;
int r2 = blankPos / size;
// we convert in the 1D coord
int clickPos = r1 * size + c1;
int dir = 0;
// we search direction for multiple tile moves at once
if (c1 == c2 && Math.abs(r1 - r2) > 0)
dir = (r1 - r2) > 0 ? size : -size;
else if (r1 == r2 && Math.abs(c1 - c2) > 0)
dir = (c1 - c2) > 0 ? 1 : -1;
if (dir != 0) {
// we move tiles in the direction
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// we check if game is solved
gameOver = isSolved();
}
// we repaint panel
repaint();
}
});
Mir setzen de Code am Konstruktor vun der GameOfFifteen Klass. Um Enn ruffe mir d'NewGame Method fir en neit Spill unzefänken.
Voll Spill Code
De leschte Schrëtt ier Dir d'Spill an Aktioun gesinn ass all Code Elementer zesummen ze setzen. Hei ass wat geschitt:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
// We are going to create a Game of 15 Puzzle with Java 8 and Swing
// If you have some questions, feel free to read comments ;)
public class GameOfFifteen extends JPanel { // our grid will be drawn in a dedicated Panel
// Size of our Game of Fifteen instance
private int size;
// Number of tiles
private int nbTiles;
// Grid UI Dimension
private int dimension;
// Foreground Color
private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // we use arbitrary color
// Random object to shuffle tiles
private static final Random RANDOM = new Random();
// Storing the tiles in a 1D Array of integers
private int[] tiles;
// Size of tile on UI
private int tileSize;
// Position of the blank tile
private int blankPos;
// Margin for the grid on the frame
private int margin;
// Grid UI Size
private int gridSize;
private boolean gameOver; // true if game over, false otherwise
public GameOfFifteen(int size, int dim, int mar) {
this.size = size;
dimension = dim;
margin = mar;
// init tiles
nbTiles = size * size - 1; // -1 because we don't count blank tile
tiles = new int[size * size];
// calculate grid size and tile size
gridSize = (dim - 2 * margin);
tileSize = gridSize / size;
setPreferredSize(new Dimension(dimension, dimension + margin));
setBackground(Color.WHITE);
setForeground(FOREGROUND_COLOR);
setFont(new Font("SansSerif", Font.BOLD, 60));
gameOver = true;
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// used to let users to interact on the grid by clicking
// it's time to implement interaction with users to move tiles to solve the game !
if (gameOver) {
newGame();
} else {
// get position of the click
int ex = e.getX() - margin;
int ey = e.getY() - margin;
// click in the grid ?
if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize)
return;
// get position in the grid
int c1 = ex / tileSize;
int r1 = ey / tileSize;
// get position of the blank cell
int c2 = blankPos % size;
int r2 = blankPos / size;
// we convert in the 1D coord
int clickPos = r1 * size + c1;
int dir = 0;
// we search direction for multiple tile moves at once
if (c1 == c2 && Math.abs(r1 - r2) > 0)
dir = (r1 - r2) > 0 ? size : -size;
else if (r1 == r2 && Math.abs(c1 - c2) > 0)
dir = (c1 - c2) > 0 ? 1 : -1;
if (dir != 0) {
// we move tiles in the direction
do {
int newBlankPos = blankPos + dir;
tiles[blankPos] = tiles[newBlankPos];
blankPos = newBlankPos;
} while(blankPos != clickPos);
tiles[blankPos] = 0;
}
// we check if game is solved
gameOver = isSolved();
}
// we repaint panel
repaint();
}
});
newGame();
}
private void newGame() {
do {
reset(); // reset in intial state
shuffle(); // shuffle
} while(!isSolvable()); // make it until grid be solvable
gameOver = false;
}
private void reset() {
for (int i = 0; i < tiles.length; i++) {
tiles[i] = (i + 1) % tiles.length;
}
// we set blank cell at the last
blankPos = tiles.length - 1;
}
private void shuffle() {
// don't include the blank tile in the shuffle, leave in the solved position
int n = nbTiles;
while (n > 1) {
int r = RANDOM.nextInt(n--);
int tmp = tiles[r];
tiles[r] = tiles[n];
tiles[n] = tmp;
}
}
// Only half 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 tile in the solved position,
// the number of inversions must be even for the puzzle to be solvable
private boolean isSolvable() {
int countInversions = 0;
for (int i = 0; i < nbTiles; 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) // if blank tile is not in the solved position ==> not solved
return false;
for (int i = nbTiles - 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++) {
// we convert 1D coords to 2D coords given the size of the 2D Array
int r = i / size;
int c = i % size;
// we convert in coords on the UI
int x = margin + c * tileSize;
int y = margin + r * tileSize;
// check special case for blank tile
if(tiles[i] == 0) {
if (gameOver) {
g.setColor(FOREGROUND_COLOR);
drawCenteredString(g, "u2713", x, y);
}
continue;
}
// for other tiles
g.setColor(getForeground());
g.fillRoundRect(x, y, tileSize, tileSize, 25, 25);
g.setColor(Color.BLACK);
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(FOREGROUND_COLOR);
String s = "Click to start new game";
g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
getHeight() - margin);
}
}
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
// center string s for the given tile (x,y)
FontMetrics fm = g.getFontMetrics();
int asc = fm.getAscent();
int desc = fm.getDescent();
g.drawString(s, x + (tileSize - fm.stringWidth(s)) / 2,
y + (asc + (tileSize - (asc + desc)) / 2));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2D = (Graphics2D) g;
g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
drawGrid(g2D);
drawStartMessage(g2D);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setTitle("Game of Fifteen");
frame.setResizable(false);
frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
frame.pack();
// center on the screen
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
Endlech, loosst eis spillen!
Et ass Zäit d'Spill ze starten an et an Aktioun ze testen. D'Feld soll esou ausgesinn:
Loosst eis probéieren de Puzzel ze léisen. Wann alles gutt gaang ass, kréie mir dëst: