«La 15» en Java — comment développer un jeu complet

«La 15» en Java — comment développer un jeu complet

«Quinze», ou «Quinze Casse-Tête» — un excellent exemple de jeu logique simple, populaire dans le monde entier. Pour résoudre l'énigme, il faut ranger les carrés avec des chiffres dans l'ordre, du plus petit au plus grand. Ce n'est pas facile, mais c'est intéressant.

Dans ce tutoriel, nous allons montrer comment développer «Quinze» en Java 8 avec Eclipse. Pour le développement de l'interface utilisateur, nous utiliserons l'API Swing.

Rappelons-le : pour tous les lecteurs de « Habr » — une réduction de 10 000 roubles lors de l'inscription à tout cours Skillbox avec le code promo « Habr ».

Skillbox recommande : Cours en ligne éducatif «Profession Développeur Java».

Conception du jeu

À ce stade, il faut définir les propriétés :

  • Size — la taille du plateau de jeu;
  • nbTiles — le nombre de tuiles dans le plateau. nbTiles = size*size — 1;
  • Tiles — les tuiles, qui représentent un tableau unidimensionnel d'entiers. Chaque tuile recevra une valeur unique dans la plage [0, nbTiles]. Zéro représente le carré vide;
  • blankPos — la position du carré vide.

Logique du jeu

Il faut définir la méthode reset, utilisée pour initialiser une nouvelle position de jeu. Ainsi, nous définissons une valeur pour chaque élément du tableau des tuiles. Ensuite, nous plaçons blankPos à la dernière position du tableau.

Nous avons également besoin d'une méthode shuffle pour mélanger le tableau des tuiles. Nous n'incluons pas la tuile vide dans le processus de mélange pour la laisser à sa place.

Comme seules la moitié des positions de départ possibles de l'énigme ont une solution, il faut vérifier le résultat du mélange pour s'assurer que la configuration actuelle est vraiment résoluble. Pour cela, nous définissons la méthode isSolvable.

Si une tuile précède une tuile avec une valeur plus élevée, cela est considéré comme une inversion. Lorsque la tuile vide est à sa place, le nombre d'inversions doit être pair pour que l'énigme soit résoluble. Ainsi, nous comptons le nombre d'inversions et retournons true si le nombre est pair.

Ensuite, il est important de définir la méthode isSolved pour vérifier si notre configuration Game Of Fifteen est résolue. D'abord, nous vérifions où se trouve la tuile vide. Si elle est à la position de départ, alors la configuration actuelle est nouvelle et non encore résolue. Ensuite, nous parcourons les tuiles à l'envers, et si la valeur d'une tuile diffère de l'indice correspondant +1, nous retournons false. Sinon, à la fin de la méthode, il est temps de retourner true, car l'énigme est déjà résolue.

Une autre méthode à définir est newGame. Elle est nécessaire pour créer une nouvelle instance du jeu. Pour cela, nous réinitialisons le plateau de jeu, puis le mélangeons et continuons jusqu'à ce que la position du jeu soit solvable.

Voici un exemple de code avec la logique clé du taquin :

private void newGame() {
  do {
    reset(); // réinitialiser à l'état initial
    shuffle(); // mélanger
  } while(!isSolvable()); // faire en sorte que la grille soit solvable
 
  gameOver = false;
}
 
private void reset() {
  for (int i = 0; i  1) {
    int r = RANDOM.nextInt(n--);
    int tmp = tiles[r];
    tiles[r] = tiles[n];
    tiles[n] = tmp;
  }
}
 
// Seules la moitié des permutations du puzzle sont solvables/
// Chaque fois qu'une case est précédée d'une case avec une valeur plus élevée, cela compte
// comme une inversion. Dans notre cas, avec la case vide en position résolue,
// le nombre d'inversions doit être pair pour que le puzzle soit solvable
private boolean isSolvable() {
  int countInversions = 0;
 
  for (int i = 0; i < nbTiles; i++) {
    for (int j = 0; j  tiles[i])
        countInversions++;
    }
  }
 
  return countInversions % 2 == 0;
}
 
private boolean isSolved() {
  if (tiles[tiles.length - 1] != 0) // si la case vide n'est pas en position résolue ==> non résolu
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Enfin, il faut programmer le mouvement des tuiles dans le tableau. Ce code sera appelé plus tard par un rappel (callback) pour réagir au mouvement du curseur. Notre jeu prendra en charge plusieurs mouvements de tuiles simultanément. Ainsi, après avoir converti la position cliquée sur l'écran en tuile, nous obtenons la position de la tuile vide et cherchons la direction du mouvement pour supporter plusieurs de ses déplacements simultanément.

Voici un exemple de 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;

Développement de l'interface utilisateur avec Swing API

Il est temps de s'occuper de l'interface. Nous commençons par la classe JPanel. Ensuite, nous dessinons les tuiles sur le plateau — pour calculer les dimensions de chacune, nous utiliserons les données fournies dans les paramètres du constructeur du jeu :

gridSize = (dim - 2 * margin);
tileSize = gridSize / size;

Le marge est également un paramètre fourni dans le constructeur du jeu.

Nous devons maintenant définir la méthode drawGrid pour dessiner la grille et les tuiles à l'écran. Nous analysons le tableau des tuiles et convertissons les coordonnées en coordonnées d'interface utilisateur. Ensuite, nous dessinons chaque tuile avec le numéro correspondant au centre :

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // nous convertissons les coordonnées 1D en coordonnées 2D en fonction de la taille du tableau 2D
    int r = i / size;
    int c = i % size;
    // nous convertissons en coordonnées sur l'UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // vérifiez le cas particulier de la tuile vide
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // pour les autres tuiles
    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);
  }
}

Enfin, redéfinissons la méthode paintComponent, issue de la classe JPane. Ensuite, utilisons la méthode drawGrid, puis la méthode drawStartMessage pour afficher le message suggérant de cliquer pour commencer le jeu :

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Cliquez pour commencer un nouveau jeu";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centrer la chaîne s pour la tuile donnée (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);
}

Réagissons aux actions de l'utilisateur dans l'UI

Pour que le jeu se déroule correctement, il est nécessaire de traiter les actions de l'utilisateur dans l'UI. Pour cela, nous ajoutons une implémentation de MouseListener sur Jpanel et le code pour déplacer les tuiles, déjà montré ci-dessus :

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // utilisé pour permettre aux utilisateurs d'interagir sur la grille en cliquant
    // il est temps de mettre en œuvre l'interaction avec les utilisateurs pour déplacer les tuiles et résoudre le jeu !
    if (gameOver) {
      newGame();
    } else {
      // obtenir la position du clic
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // clic dans la grille ?
      if (ex  gridSize  || ey  gridSize)
        return;

      // obtenir la position dans la grille
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // obtenir la position de la cellule vide
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // nous convertissons en la coordonnée 1D
      int clickPos = r1 * size + c1;

      int dir = 0;

      // nous recherchons la direction pour plusieurs mouvements de tuiles à la fois
      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) {
        // nous déplaçons les tuiles dans la direction
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // nous vérifions si le jeu est résolu
      gameOver = isSolved();
    }

    // nous redessinons le panneau
    repaint();
  }
});

Le code est placé dans le constructeur de la classe GameOfFifteen. À la toute fin, nous appelons la méthode newGame pour commencer une nouvelle partie.

Code complet du jeu

La dernière étape, avant de voir le jeu en action, consiste à assembler tous les éléments du code. Voici ce que cela donnera :

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;
 
// Nous allons créer un jeu du Puzzle de 15 avec Java 8 et Swing
// Si vous avez des questions, n'hésitez pas à lire les commentaires ;)
public class GameOfFifteen extends JPanel { // notre grille sera dessinée dans un JPanel dédié
 
  // Taille de notre instance du Jeu de Quinze
  private int size;
  // Nombre de tuiles
  private int nbTiles;
  // Dimension UI de la grille
  private int dimension;
  // Couleur de premier plan
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // nous utilisons une couleur arbitraire
  // Objet Random pour mélanger les tuiles
  private static final Random RANDOM = new Random();
  // Stockage des tuiles dans un tableau 1D d'entiers
  private int[] tiles;
  // Taille de la tuile sur l'UI
  private int tileSize;
  // Position de la tuile vide
  private int blankPos;
  // Marge pour la grille sur le cadre
  private int margin;
  // Taille de l'UI de la grille
  private int gridSize;
  private boolean gameOver; // vrai si le jeu est terminé, faux sinon
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // init des tuiles
    nbTiles = size * size - 1; // -1 car nous ne comptons pas la tuile vide
    tiles = new int[size * size];
    
    // calculer la taille de la grille et la taille de la tuile
    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) {
        // utilisé pour permettre aux utilisateurs d'interagir sur la grille par un clic
        // il est temps de mettre en œuvre l'interaction avec les utilisateurs pour déplacer les tuiles afin de résoudre le jeu !
        if (gameOver) {
          newGame();
        } else {
          // obtenir la position du clic
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // clic dans la grille ?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // obtenir la position dans la grille
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // obtenir la position de la case vide
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // nous convertissons en coordonnées 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // nous recherchons la direction pour plusieurs mouvements de tuiles à la fois
          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) {
            // nous déplaçons les tuiles dans la direction
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // nous vérifions si le jeu est résolu
          gameOver = isSolved();
        }
        
        // nous redessinons le panneau
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // réinitialiser dans l'état initial
      shuffle(); // mélanger
    } while(!isSolvable()); // continuer jusqu'à ce que la grille soit soluble
    
    gameOver = false;
  }
 
  private void reset() {
    for (int i = 0; i  1) {
      int r = RANDOM.nextInt(n--);
      int tmp = tiles[r];
      tiles[r] = tiles[n];
      tiles[n] = tmp;
    }
  }
 
  // Seulement la moitié des permutations du puzzle sont solubles.
  // Chaque fois qu'une tuile est précédée d'une tuile avec une valeur plus élevée, cela compte
  // comme une inversion. Dans notre cas, avec la tuile vide dans la position résolue,
  // le nombre d'inversions doit être pair pour que le puzzle soit soluble
  private boolean isSolvable() {
    int countInversions = 0;
    
    for (int i = 0; i < nbTiles; i++) {
      for (int j = 0; j  tiles[i])
          countInversions++;
      }
    }
    
    return countInversions % 2 == 0;
  }
 
  private boolean isSolved() {
    if (tiles[tiles.length - 1] != 0) // si la tuile vide n'est pas dans la position résolue ==> pas résolu
      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  {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setTitle("Jeu des Quinze");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centrer à l'écran
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Enfin, jouons !

C'est le moment de lancer le jeu et de le tester en action. Le terrain doit ressembler à ceci :

«La 15» en Java — comment développer un jeu complet

Essayons de résoudre l'énigme. Si tout se passe bien, nous obtenons ceci :

«La 15» en Java — comment développer un jeu complet

Voilà, c'est tout. Vous attendiez quelque chose de plus ? 🙂

Skillbox recommande :

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster