„Cifrul” în Java - cum să dezvolți un joc complet

„Cifrul” în Java - cum să dezvolți un joc complet

„Cincisprezece”, sau „Cincime” — este un exemplu excelent de joc logic simplu, popular în întreaga lume. Pentru a rezolva puzzle-ul, este necesar să aranjezi căsuțele cu numere în ordinea crescătoare. Nu este simplu, dar este interesant.

În tutorialul de astăzi, vom arăta cum să dezvoltăm „Cincisprezece” în Java 8 cu Eclipse. Pentru dezvoltarea UI, vom folosi Swing API.

Vă reamintim: pentru toți cititorii „Habr” — reducere de 10.000 de ruble la înscrierea la orice curs Skillbox cu codul de promovare „Habr”.

Skillbox recomandă: Curs online educațional «Profesie Java Developer».

Proiectarea jocului

La acest pas trebuie să definim proprietățile:

  • Size — dimensiunea câmpului de joc;
  • nbTiles — numărul de căsuțe în câmp. nbTiles = size*size — 1;
  • Tiles — căsuța, care reprezintă un array unidimensional de numere întregi. Fiecare căsuță va primi o valoare unică în intervalul [0, nbTiles]. Zero indică căsuța goală;
  • blankPos — poziția căsuței goale.

Logica jocului

Trebuie să definim metoda reset, folosită pentru a inițializa o nouă poziție de joc. Astfel, stabilim valoarea pentru fiecare element al array-ului de căsuțe. Apoi, plasăm blankPos în ultima poziție a array-ului.

De asemenea, este necesară o metodă shuffle pentru a amesteca array-ul de căsuțe. Nu includem căsuța goală în procesul de amestecare, pentru a o menține în aceeași poziție.

Deoarece doar jumătate din posibilele poziții de start ale puzzle-ului au soluție, trebuie să verificăm rezultatul obținut prin amestecare pentru a ne asigura că aranjamentul curent este, în general, rezolvabil. Pentru a face asta, definim metoda isSolvable.

Dacă o căsuță specifică este precedată de o căsuță cu o valoare mai mare, aceasta este considerată o inversare. Când căsuța goală este la locul ei, numărul inversărilor trebuie să fie par pentru ca puzzle-ul să fie rezolvabil. Așadar, numărăm inversările și returnăm true dacă numărul este par.

Apoi, este important să definim metoda isSolved, pentru a verifica dacă aranjamentul nostru Game Of Fifteen este rezolvat. Mai întâi, ne uităm unde se află căsuța goală. Dacă este în poziția inițială, atunci aranjamentul curent este nou, nerezolvat anterior. Apoi, verificăm căsuțele în ordine inversă, iar dacă valoarea căsuței diferă de indicele corespunzător +1, returnăm false. În caz contrar, la finalul metodei putem returna true, deoarece puzzle-ul este deja rezolvat.

O altă metodă care trebuie definită este newGame. Aceasta este folosită pentru a crea o nouă instanță a jocului. Pentru aceasta, resetăm tabla de joc, apoi o amestecăm și continuăm până când poziția de joc devine solvabilă.

Iată un exemplu de cod cu logica cheie a jocului 15:

private void newGame() {
  do {
    reset(); // resetare în starea inițială
    shuffle(); // amestecare
  } while(!isSolvable()); // continuăm până când tabla devine solvabilă
 
  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;
  }
}
 
// Numai jumătate din permutările puzzle-ului sunt solvabile/
// Ori de câte ori o placă este precedată de o placă cu o valoare mai mare, aceasta contează
// ca și o inversare. În cazul nostru, cu placa goală în poziția rezolvată,
// numărul inversărilor trebuie să fie par pentru ca puzzle-ul să fie solvabil
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) // dacă placa goală nu este în poziția rezolvată => nu este rezolvat
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

În cele din urmă, trebuie să programăm mișcarea plăcilor în tablou. Acest cod va fi apelat mai târziu printr-un callback pentru a reacționa la mișcarea cursorului. Jocul nostru va suporta mai multe mișcări ale plăcilor simultan. Astfel, după ce am convertit poziția apăsată pe ecran în placa corespunzătoare, obținem poziția plăcii goale și căutăm direcția de mișcare pentru a suporta mai multe mișcări simultan.

Iată un exemplu de cod:

// 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;

Dezvoltăm UI folosind Swing API

Este momentul să ne ocupăm de interfață. Mai întâi, luăm clasa Jpanel. Apoi desenăm plăcile pe tablă—pentru a calcula dimensiunile fiecărei plăci, vom folosi datele specificate în parametrul constructorului jocului:

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

Marginea este de asemenea un parametru specificat în constructorul jocului.

Acum trebuie să definim metoda drawGrid pentru a desena grila și plăcile pe ecran. Analizăm tabloul plăcilor și convertim coordonatele în coordonatele interfeței utilizatorului. Apoi vom desena fiecare placă cu numărul corespunzător în centru:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // convertim coordonatele 1D în coordonate 2D, având în vedere dimensiunea matricei 2D
    int r = i / size;
    int c = i % size;
    // convertim în coordonate pentru UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // verificăm cazul special pentru placa goală
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // pentru celelalte plăci
    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);
  }
}

În cele din urmă, vom suprascrie metoda paintComponent, care este derivată din clasa JPane. Apoi vom folosi metoda drawGrid și, ulterior, metoda drawStartMessage pentru a afișa un mesaj care invită să facem clic pentru a începe jocul:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Clic pentru a începe un joc nou";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centram șirul s pentru placa dată (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);
}

Reacționăm la acțiunile utilizatorului în UI

Pentru ca jocul să decurgă bine, este necesar să procesăm acțiunile utilizatorului în UI. Pentru aceasta, adăugăm implementarea MouseListener pe Jpanel și codul pentru mutarea plăcilor, deja arătat mai sus:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // folosit pentru a permite utilizatorilor să interacționeze cu tabla prin clic
    // este timpul să implementăm interacțiunea cu utilizatorii pentru a muta plăcile pentru a rezolva jocul!
    if (gameOver) {
      newGame();
    } else {
      // obțineți poziția clicului
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // clic în tablă?
      if (ex  gridSize  || ey  gridSize)
        return;

      // obțineți poziția în tablă
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // obțineți poziția celulei goale
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // convertim în coordonata 1D
      int clickPos = r1 * size + c1;

      int dir = 0;

      // căutăm direcția pentru mișcările multiple ale plăcilor
      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) {
        // mutăm plăcile în direcția specificată
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // verificăm dacă jocul este rezolvat
      gameOver = isSolved();
    }

    // repaintăm panelul
    repaint();
  }
});

Codul este plasat în constructorul clasei GameOfFifteen. La sfârșit, apelăm metoda newGame pentru a începe un joc nou.

Codul complet al jocului

Ultimul pas, înainte de a vedea jocul în acțiune, este să adunăm toate elementele codului împreună. Iată ce va ieși:

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;
 
// Vom crea un joc de puzzle 15 cu Java 8 și Swing
// Dacă aveți întrebări, nu ezitați să citiți comentariile ;)
public class GameOfFifteen extends JPanel { // grila noastră va fi desenată într-un panou dedicat
 
  // Dimensiunea instanței noastre de joc 15
  private int size;
  // Numărul de plăci
  private int nbTiles;
  // Dimensiunea UI a grilei
  private int dimension;
  // Culoarea de prim-plan
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // folosim o culoare arbitrară
  // Obiect aleator pentru amestecarea plăcilor
  private static final Random RANDOM = new Random();
  // Stocarea plăcilor într-un tablou 1D de întregi
  private int[] tiles;
  // Dimensiunea plăcii în UI
  private int tileSize;
  // Poziția plăcii goale
  private int blankPos;
  // Marginea pentru grilă pe cadru
  private int margin;
  // Dimensiunea UI a grilei
  private int gridSize;
  private boolean gameOver; // adevărat dacă jocul s-a Terminat, fals în caz contrar
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inițializăm plăcile
    nbTiles = size * size - 1; // -1 pentru că nu numărăm placa goală
    tiles = new int[size * size];
    
    // calculăm dimensiunea grilei și dimensiunea plăcii
    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) {
        // folosit pentru a permite utilizatorilor să interacționeze cu grila prin clic
        // e timpul să implementăm interacțiunea cu utilizatorii pentru a muta plăcile și a rezolva jocul!
        if (gameOver) {
          newGame();
        } else {
          // obținem poziția clicului
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // clic în grilă? 
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // obținem poziția în grilă
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // obținem poziția celulei goale
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // convertim în coordonatele 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // căutăm direcția pentru mai multe mutări de plăci simultan
          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) {
            // mutăm plăcile în direcție
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // verificăm dacă jocul este rezolvat
          gameOver = isSolved();
        }
        
        // repaintăm panelul
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // resetăm în starea inițială
      shuffle(); // amestecăm
    } while(!isSolvable()); // continuăm până când grila devine soluționabilă
    
    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;
    }
  }
 
  // Doar jumătate din permutațiile puzzle-ului sunt soluționabile.
  // Oricând o placă este precedată de o placă cu o valoare mai mare, este considerată
  // o inversare. În cazul nostru, cu placa goală în poziția rezolvată,
  // numărul inversărilor trebuie să fie par pentru ca puzzle-ul să fie soluționabil.
  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) // dacă placa goală nu este în poziția rezolvată => nu este rezolvat
      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("Game of Fifteen");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centrat pe ecran
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 

}

În sfârșit, să jucăm!

Este momentul perfect să lansăm jocul și să-i vedem acțiunea. Terenul ar trebui să arate astfel:

„Cifrul” în Java - cum să dezvolți un joc complet

Încercăm să rezolvăm puzzle-ul. Dacă totul a mers bine, obținem următoarele rezultate:

„Cifrul” în Java - cum să dezvolți un joc complet

Asta e tot. Te așteptai la mai mult? 🙂

Skillbox recomandă:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster