Jak vytvořit plnohodnotnou hru ‚Pětky‘ na Javě

Jak vytvořit plnohodnotnou hru ‚Pětky‘ na Javě

„Patnáct“, nebo „Patnáctka“ — skvělý příklad jednoduché logické hry, která je populární po celém světě. Abyste vyřešili tuto hádanku, musíte uspořádat čtverce s čísly v pořadí od nejmenšího po největší. Není to snadné, ale je to zajímavé.

V dnešním tutoriálu ukážeme, jak vyvinout „Patnáctku“ v Javě 8 s Eclipse. Pro vývoj UI použijeme Swing API.

Připomínáme: pro všechny čtenáře "Habra" – sleva 10 000 korun při registraci na jakýkoli kurz Skillbox s promokódem "Habr".

Skillbox doporučuje: Vzdělávací online kurz „Profese Java vývojář“.

Navrhování hry

V této fázi je třeba určit vlastnosti:

  • Size — velikost herního pole;
  • nbTiles — počet patnáctek na poli. nbTiles = size*size — 1;
  • Tiles — patnáctka, která představuje jednorozměrné pole celých čísel. Každá patnáctka obdrží unikátní hodnotu v rozsahu [0, nbTiles]. Nula označuje prázdný čtverec;
  • blankPos — pozice prázdného čtverce.

Logika hry

Je třeba definovat metodu reset, která se používá k inicializaci nové herní pozice. Tím nastavíme hodnotu pro každý prvek pole patnáctek. Potom umístíme blankPos na poslední pozici pole.

Také potřebujeme metodu shuffle pro zamíchání pole patnáctek. Nezapojujeme prázdnou patnáctku do procesu míchání, abychom ji ponechali ve stejné pozici.

Vzhledem k tomu, že pouze polovina možných výchozích pozic hádanky má řešení, je třeba zkontrolovat výsledný stav zamíchání, aby se ujistilo, že aktuální uspořádání je vůbec řešitelné. K tomu definujeme metodu isSolvable.

Pokud konkrétní patnáctku předchází patnáctka s vyšší hodnotou, považuje se to za inverzi. Když je prázdná patnáctka na svém místě, musí být počet inverzí sudý, aby byla hádanka řešitelná. Takže počítáme počet inverzí a vracíme true, pokud je počet sudý.

Pak je důležité definovat metodu isSolved, abychom zkontrolovali, zda je naše uspořádání Game Of Fifteen vyřešeno. Nejprve se podíváme, kde se nachází prázdná patnáctka. Pokud je na výchozí pozici, je aktuální uspořádání nové, dosud nevyřešené. Poté procházíme dlaždice v opačném pořadí a pokud se hodnota patnáctky liší od odpovídajícího indexu +1, vrátíme false. Jinak na konci metody vrátíme true, protože hádanka je už vyřešena.

Další metodou, kterou je třeba určit, je newGame. Ta je potřebná pro vytvoření nové instance hry. K tomu resetujeme hrací pole, poté ho zamícháme a pokračujeme, dokud není hrací pozice řešitelná.

Zde je příklad kódu s klíčovou logikou patnáctek:

private void newGame() {
  do {
    reset(); // resetovat do výchozího stavu
    shuffle(); // zamíchat
  } while (!isSolvable()); // pokračovat, dokud nebude mřížka řešitelná
 
  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;
  }
}
 
// Pouze polovina permutací puzzle je řešitelná/
// Kdykoli je dlaždice předcházená dlaždicí s vyšší hodnotou, počítá se
// jako inverze. V našem případě, s prázdnou dlaždicí v řešené pozici,
// počet inverzí musí být sudý, aby bylo puzzle řešitelné
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) // pokud prázdná dlaždice není v řešené pozici => není vyřešeno
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Nakonec je třeba naprogramovat pohyb patnáctek v poli. Tento kód bude později vyvolán pomocí zpětného volání (callback), aby reagoval na pohyb kurzoru. Naše hra bude podporovat více pohybů dlaždic zároveň. Jakmile převedeme stisknutou pozici na obrazovce na patnáctku, získáme pozici prázdné dlaždice a hledáme směr pohybu, abychom podpořili její více pohybů současně.

Zde je pří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íjíme UI na Swing API

Je čas zabývat se uživatelským rozhraním. Nejprve vezmeme třídu JPanel. Poté na poli vykreslíme patnáctky — pro výpočet rozměrů každé použijeme údaje zadané v parametru konstruktora hry:

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

Margin je také parametrem zadaným v konstruktoru hry.

Nyní je třeba definovat metodu drawGrid pro vykreslení sítě a patnáctek na obrazovce. Analyzujeme pole patnáctek a převádíme souřadnice na souřadnice uživatelského rozhraní. Poté vykreslíme každou patnáctku s odpovídajícím číslem uprostřed:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // převádíme 1D souřadnice na 2D souřadnice podle velikosti 2D pole
    int r = i / size;
    int c = i % size;
    // převádíme souřadnice na UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // kontrolujeme speciální případ pro prázdný dlaždici
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // pro 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);
  }
}

Nakonec přepíšeme metodu paintComponent, která je odvozená od třídy JPane. Poté použijeme metodu drawGrid a po ní metodu drawStartMessage pro zobrazení zprávy, která vyzývá k kliknutí pro spuštění hry:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Klikněte pro zahájení nové hry";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // středíme text s pro danou dlaždici (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 akce uživatele v UI

Aby hra probíhala bez problémů, je třeba zpracovávat akce uživatele v UI. K tomu přidáme implementaci MouseListener na Jpanel a kód pro pohyb dlaždic, jak bylo již uvedeno výše:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // používané k tomu, aby uživatelé mohli interagovat s mřížkou kliknutím
    // je čas implementovat interakci s uživateli pro pohyb dlaždic k vyřešení hry!
    if (gameOver) {
      newGame();
    } else {
      // zjistíme pozici kliknutí
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

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

      // zjistíme pozici v mřížce
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // zjistíme pozici prázdné buňky
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // převádíme na 1D souřadnici
      int clickPos = r1 * size + c1;

      int dir = 0;

      // hledáme směr pro vícenásobné pohyby dlaždic
      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) {
        // posuneme dlaždice ve směru
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // kontrolujeme, jestli je hra vyřešena
      gameOver = isSolved();
    }

    // překreslíme panel
    repaint();
  }
});

Kód umisťujeme do konstruktoru třídy GameOfFifteen. Na konci voláme metodu newGame pro zahájení nové hry.

Úplný kód hry

Poslední krok, než uvidíte hru v akci, je spojit všechny části kódu dohromady. Toto je výsledek:

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;
 
// Vytvoříme Hru Dvaceti Pěti Puzzle s Javam 8 a Swingem
// Pokud máte nějaké dotazy, neváhejte si přečíst komentáře ;)
public class GameOfFifteen extends JPanel { // naši mřížku nakreslíme na dedikované Pane
 
  // Velikost naší instance Hry Dvaceti Pěti
  private int size;
  // Počet dlaždic
  private int nbTiles;
  // Rozměr UI mřížky
  private int dimension;
  // Barva popředí
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // používáme libovolnou barvu
  // Náhodný objekt pro zamíchání dlaždic
  private static final Random RANDOM = new Random();
  // Ukládání dlaždic v jednorozměrném poli celých čísel
  private int[] tiles;
  // Velikost dlaždice na UI
  private int tileSize;
  // Pozice prázdné dlaždice
  private int blankPos;
  // Okraj pro mřížku na rámečku
  private int margin;
  // Velikost UI mřížky
  private int gridSize;
  private boolean gameOver; // true pokud je hra u konce, false jinak
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inicializace dlaždic
    nbTiles = size * size - 1; // -1 protože nepočítáme prázdnou dlaždici
    tiles = new int[size * size];
    
    // výpočet velikosti mřížky a dlaždic
    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žívá se k tomu, aby uživatelé mohli interagovat s mřížkou kliknutím
        // je čas implementovat interakci s uživateli pro posunutí dlaždic a vyřešení hry !
        if (gameOver) {
          newGame();
        } else {
          // zjistit pozici kliknutí
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // kliknutí v mřížce ?
          if (ex  gridSize || ey  gridSize)
            return;
          
          // zjistit pozici v mřížce
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // zjistit pozici prázdné buňky
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // převedeme na 1D souřadnici
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // hledáme směr pro více pohybů dlaždic najednou
          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) {
            // posuneme dlaždice ve směru
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // zkontrolujeme, zda je hra vyřešena
          gameOver = isSolved();
        }
        
        // repaintujeme panel
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // reset do počátečního stavu
      shuffle(); // zamíchání
    } while(!isSolvable()); // děláme to dokud není mřížka řešitelná
    
    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;
    }
  }
 
  // Pouze polovina permutací puzzle je řešitelná.
  // Kdykoli je dlaždice předcházená dlaždicí s vyšší hodnotou, počítá se to
  // jako inverze. V našem případě, kdy je prázdná dlaždice v vyřešené pozici,
  // musí být počet inverzí sudý, aby bylo puzzle řešitelné
  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) // pokud prázdná dlaždice není na vyřešené pozici ==> není vyřešeno
      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 Dvaceti Pěti");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centrovat na obrazovku
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Konečně hrajeme!

Je nejvyšší čas spustit hru a vyzkoušet ji v akci. Pole by mělo vypadat takto:

Jak vytvořit plnohodnotnou hru ‚Pětky‘ na Javě

Snažíme se vyřešit hádanku. Pokud to vše proběhlo úspěšně, dostaneme následující:

Jak vytvořit plnohodnotnou hru ‚Pětky‘ na Javě

A to je vše. Očekávali jste víc? 🙂

Skillbox doporučuje:

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster