„Patnástka“ v Jave – ako vytvoriť plnohodnotnú hru

„Patnástka“ v Jave – ako vytvoriť plnohodnotnú hru

„Pätnásť“, alebo „Pätnástačka“ — je skvelý príklad jednoduchej logickej hry, populárnej po celom svete. Na vyriešenie hádanky je potrebné usporiadať štvrtky s číslami v poradí od najmenšieho po najväčšie. Nie je to jednoduché, ale je to zaujímavé.

V dnešnom tutoriáli ukážeme, ako vyvinúť „Pätnástačku“ v Jave 8 s Eclipse. Na vývoj UI použijeme Swing API.

Pripomíname: pre všetkých čitateľov „Habr“ — zľava 10 000 rubľov pri zapísaní na akýkoľvek kurz Skillbox s promo kódom „Habr“.

Skillbox odporúča: Vzdelávací online kurz „Povolanie Java vývojár“.

Návrh hry

V tejto fáze je potrebné určiť vlastnosti:

  • Size — veľkosť herného poľa;
  • nbTiles — počet pätnástok na poli. nbTiles = size * size — 1;
  • Tiles — pätnástka, ktorá je jednorozmerným poľom celých čísel. Každá z pätnástok dostane jedinečnú hodnotu v rozsahu [0, nbTiles]. Nula označuje prázdny štvorček;
  • blankPos — pozícia prázdneho štvorčeka.

Logika hry

Je potrebné definovať metódu na resetovanie (reset), ktorá sa používa na inicializáciu novej hernej pozície. Takto nastavujeme hodnotu pre každý prvok poľa pätnástok. A potom umiestnime blankPos na poslednú pozíciu poľa.

Tiež potrebujeme metódu shuffle na premiešanie poľa pätnástok. Nezahŕňame prázdnu pätnástku do procesu premiešania, aby sme ju nechali na jej pôvodnom mieste.

Keďže len polovica možných štartovacích pozícií hádanky má riešenie, je potrebné overiť výsledok premiešania, aby sme sa uistili, že aktuálne rozloženie je vôbec riešiteľné. Na to definujeme metódu isSolvable.

Ak konkrétnej pätnástke predchádza pätnástka s vyššou hodnotou, považuje sa to za inverziu. Keď je prázdna pätnástka na svojom mieste, počet inverzií musí byť párny, aby bola hádanka riešiteľná. Takže počítame počet inverzií a vraciame true, ak je číslo párne.

Potom je dôležité definovať metódu isSolved, aby sme zistili, či je naše rozloženie Game Of Fifteen vyriešené. Najprv sa pozrieme, kde sa nachádza prázdna pätnástka. Ak je na počiatočnej pozícii, aktuálne rozloženie je nové, doteraz nevyriešené. Potom prechádzame cez dlaždice v opačnom poradí a ak hodnota pätnástky nezodpovedá príslušnému indexu + 1, vraciame false. V opačnom prípade, na konci metódy, môžeme vrátiť true, pretože hádanka je už vyriešená.

Ďalšou metódou, ktorú je potrebné definovať, je newGame. Tento metóda je potrebná na vytvorenie nového inštancie hry. Na to resetujeme hernú plochu, potom ju zamiešame a pokračujeme, kým herná pozícia nebude riešiteľná.

Tu je príklad kódu s kľúčovou logikou pre patnástky:

private void newGame() {
  do {
    reset(); // reset v počiatočnom stave
    shuffle(); // zamiešať
  } while(!isSolvable()); // pokračovať, kým nebude mriežka riešiteľná
 
  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;
  }
}
 
// Iba polovica permutácií puzzle je riešiteľná/
// Kedykoľvek je dlaždica predchádzaná dlaždicou s vyššou hodnotou, počíta sa
// ako inverzia. V našom prípade, keď je prázdna dlaždica na vyriešenej pozícii,
// počet inverzií musí byť párny, aby bolo puzzle riešiteľné
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) // ak prázdna dlaždica nie je na vyriešenej pozícii == > nie je vyriešené
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Nakoniec je potrebné naprogramovať pohyb dlaždíc v poli. Tento kód bude vyvolaný neskôr pomocou zpětného volania (callback), aby reagoval na pohyb kurzora. Naša hra bude podporovať viacero pohybov dlaždíc súčasne. Po tom, ako prekonvertujeme stlačenú pozíciu na obrazovke na dlaždicu, získame pozíciu prázdnej dlaždice a vyhľadáme smer pohybu na podporu viacerých jej pohybov súčasne.

Tu je príklad kódu:

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

Vyvíjame UI na Swing API

Je čas zamerať sa na rozhranie. Najprv vezmime triedu JPanel. Potom na plochu nakreslíme dlaždice – na výpočet rozmerov každej využijeme údaje zadané v konštruktore hry:

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

Margin je tiež parameter zadaný v konštruktore hry.

Teraz je potrebné definovať metódu drawGrid na vykreslenie mriežky a dlaždíc na obrazovke. Analyzujeme pole dlaždíc a konvertujeme súradnice na súradnice používateľského rozhrania. Potom vykreslíme každú dlaždicu s príslušným číslom v strede:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // prevádzame 1D súradnice na 2D súradnice vzhľadom na veľkosť 2D poľa
    int r = i / size;
    int c = i % size;
    // prevádzame súradnice na UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // kontrolujeme špeciálny prípad prázdneho dlaždice
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // pre ostatné dlaždice
    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);
  }
}

Nakoniec predefinujeme metódu paintComponent, ktorá je odvodená z triedy JPane. Potom použijeme metódu drawGrid a následne metódu drawStartMessage na zobrazenie správy, ktorá ponúka kliknúť na spustenie hry:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Kliknite na začiatok novej hry";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centrovať reťazec s pre danú dlaždicu (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);
}

Reagujeme na akcie používateľa v UI.

Pre to, aby hra plynula svojím vlastným spôsobom, je potrebné spracovávať akcie používateľa v UI. Na to pridáme implementáciu MouseListener na Jpanel a kód na posun dlaždíc, ktorý bol už vyššie znázornený:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // použitý na umožnenie interakcie používateľov s mriežkou kliknutím
    // je čas implementovať interakciu používateľov na presun dlaždíc na vyriešenie hry!
    if (gameOver) {
      newGame();
    } else {
      // získanie pozície kliknutia
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // kliknutím v mriežke?
      if (ex  gridSize || ey  gridSize)
        return;

      // získanie pozície v mriežke
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // získanie pozície prázdnej bunky
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // prevádzame do 1D súradnice
      int clickPos = r1 * size + c1;

      int dir = 0;

      // hľadáme smer pre viacnásobný presun dlaždíc naraz
      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) {
        // presunieme dlaždice v smere
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // kontrolujeme, či je hra vyriešená
      gameOver = isSolved();
    }

    // prekreslíme panel
    repaint();
  }
});

Kód umiestnite do konštruktora triedy GameOfFifteen. Na konci zavoláme metódu newGame na spustenie novej hry.

Úplný kód hry

Posledný krok predtým, ako uvidíte hru v akcii, je potrebné poskladať všetky prvky kódu dohromady. Tu je, čo dostanete:

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;
 
// Vytvoríme Hru s 15 Puzzle v Jave 8 a Swing
// Ak máte nejaké otázky, neváhajte si prečítať komentáre ;)
public class GameOfFifteen extends JPanel { // náš mriežka bude vykreslená v prednej paneli
 
  // Veľkosť našej inštancie hry s pätnástimi
  private int size;
  // Počet dlaždíc
  private int nbTiles;
  // Rozmer UI mriežky
  private int dimension;
  // Predná farba
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // používame ľubovoľnú farbu
  // Náhodný objekt na premiešanie dlaždíc
  private static final Random RANDOM = new Random();
  // Ukladanie dlaždíc v 1D poli celých čísel
  private int[] tiles;
  // Veľkosť dlaždice na UI
  private int tileSize;
  // Poloha prázdnej dlaždice
  private int blankPos;
  // Okraj pre mriežku na ráme
  private int margin;
  // Veľkosť UI mriežky
  private int gridSize;
  private boolean gameOver; // true ak je hra dokončená, false inak
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inicializácia dlaždíc
    nbTiles = size * size - 1; // -1, pretože nepočítame prázdnu dlaždicu
    tiles = new int[size * size];
    
    // výpočet veľkosti mriežky a veľkosti dlaždice
    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) {
        // použité na interakciu užívateľov s mriežkou kliknutím
        // je čas implementovať interakciu s užívateľmi na presunutie dlaždíc na vyriešenie hry!
        if (gameOver) {
          newGame();
        } else {
          // získať pozíciu kliknutia
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // kliknutie v mriežke?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // získať pozíciu v mriežke
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // získať pozíciu prázdnej bunky
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // konvertujeme na 1D súradnice
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // hľadáme smer pre viacnásobné pohyby dlaždíc súčasne
          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) {
            // posúvame dlaždice v smere
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // kontrolujeme, či je hra vyriešená
          gameOver = isSolved();
        }
        
        // znovu vykreslíme panel
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // reset na počiatočný stav
      shuffle(); // premiešať
    } while(!isSolvable()); // iba ak bude mriežka riešiteľná
    
    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;
    }
  }
 
  // Iba polovica permutácií puzzle je riešiteľná.
  // Kedykoľvek je dlaždica predchádzaná dlaždicou s vyššou hodnotou, počíta sa
  // ako inverzia. V našom prípade, s prázdnou dlaždicou na vyriešenom mieste,
  // počet inverzií musí byť párny na to, aby bolo puzzle riešiteľné
  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) // ak nie je prázdna dlaždica na vyriešenom mieste ==> nie je vyriešená
      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("Hra s pätnástimi");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centrum na obrazovke
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Konečne, hra je tu!

Teraz je ten správny čas spustiť hru a otestovať ju v akcii. Pole by malo vyzerať nasledovne:

„Patnástka“ v Jave – ako vytvoriť plnohodnotnú hru

Snažíme sa vyriešiť rébus. Ak všetko prebehlo úspešne, dostávame toto:

„Patnástka“ v Jave – ako vytvoriť plnohodnotnú hru

A to je všetko. Očakávali ste viac? 🙂

Skillbox odporúča:

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster