"Petnaestica" na Javi — kako razviti punopravnu igru

"Petnaestica" na Javi — kako razviti punopravnu igru

«Petnaest», ili «Petnaestica» — izvrstan primjer jednostavne logičke igre, popularne diljem svijeta. Da biste riješili zagonetku, potrebno je posložiti kvadrate s brojevima prema redu, od manjeg prema većem. To nije lako, ali je zanimljivo.

U današnjem tutorialu pokazujemo kako razviti «Petnaesticu» u Javi 8 koristeći Eclipse. Za razvoj UI-a koristit ćemo Swing API.

Podsjećamo: za sve čitatelje "Habra" — popust od 10 000 rubalja prilikom upisa na bilo koji tečaj Skillbox uz promo kod "Habr".

Skillbox preporučuje: Online edukacijski tečaj «Zanimanje Java programer».

Projektiranje igre

U ovoj fazi trebate definirati svojstva:

  • Size — veličina igrališta;
  • nbTiles — broj pločica na polju. nbTiles = size*size — 1;
  • Tiles — pločica, koja predstavlja jednodimenzionalni niz cijelih brojeva. Svaka od pločica će dobiti jedinstvenu vrijednost u rasponu [0, nbTiles]. Nula označava prazni kvadrat;
  • blankPos — pozicija praznog kvadrata.

Logika igre

Trebate definirati metodu reset, koja se koristi za inicijalizaciju nove pozicije igre. Tako postavljamo vrijednost za svaki element niza pločica. A zatim stavljamo blankPos u posljednju poziciju niza.

Također je potrebna metoda shuffle za miješanje niza pločica. Ne uključujemo praznu pločicu u proces miješanja, kako bi ostala na istom mjestu.

Budući da samo polovica mogućih početnih pozicija zagonetke ima rješenje, potrebno je provjeriti rezultat miješanja kako bismo se uvjerili da je trenutna postavka uopće rješiva. Da bismo to učinili, definiramo metodu isSolvable.

Ako specifikanoj pločici prethodi pločica s višom vrijednošću, to se smatra inverzijom. Kada je prazna pločica na svom mjestu, broj inverzija mora biti paran kako bi zagonetka bila rješiva. Tako prebrojavamo broj inverzija i vraćamo true, ako je broj paran.

Zatim je važno definirati metodu isSolved, kako bismo provjerili je li naš raspored Game Of Fifteen riješen. Prvo gledamo gdje se nalazi prazna pločica. Ako je na početnoj poziciji, tada je trenutni raspored nov, nikada ranije nije bio riješen. Zatim prolazimo kroz pločice unatrag, i ako se vrijednost pločice razlikuje od odgovarajućeg indeksa +1, vraćamo false. U suprotnom, na kraju metode vrijeme je da vratimo true, jer je zagonetka već riješena.

Još jedna metoda koju treba definirati je newGame. Ona je potrebna za stvaranje nove instance igre. Za to ćemo resetirati polje igre, zatim ga promiješati i nastaviti sve dok pozicija igre ne bude rješiva.

Evo primjera koda s ključnom logikom za 15-puzzle:

private void newGame() {
  do {
    reset(); // resetiraj na početno stanje
    shuffle(); // promiješaj
  } while(!isSolvable()); // ponavljaj dok mreža ne postane rješiva

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

// Samo polovica permutacija slagalice je rješiva
// Kad god je pločica prethodila pločici s višom vrijednošću, to se broji
// kao obrnuta situacija. U našem slučaju, s praznom pločicom u rješenom položaju,
// broj obrnutih situacija mora biti paran da bi slagalica bila rješiva
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) // ako prazna pločica nije na rješenom položaju ==> nije rješena
    return false;

  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }

  return true;
}

Napokon, potrebno je programirati kretanje pločica u nizu. Ovaj kod će se kasnije pozivati putem povratne funkcije (callback), kako bi reagirao na kretanje pokazivača. Naša igra će podržavati više pomicanja pločica istovremeno. Nakon što pretvorimo pritisnutu poziciju na ekranu u 15-puzzle, dobivamo poziciju prazne pločice i tražimo smjer kretanja za podršku višestrukim pomicanjima.

Evo primjera koda:

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

Razvijamo UI koristeći Swing API

Vrijeme je za rad na sučelju. Prvo uzimamo klasu Jpanel. Zatim na polju crtamo pločice — za izračun veličine svake koristićemo podatke definirane u parametru konstruktora igre:

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

Margin je također parametar definiran u konstruktoru igre.

Sada trebamo definirati metodu drawGrid za crtanje mreže i pločica na ekranu. Analiziramo niz pločica i pretvaramo koordinate u koordinate korisničkog sučelja. Zatim ćemo nacrtati svaku pločicu sa odgovarajućim brojem u središtu:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // pretvaramo 1D koordinate u 2D koordinate na temelju veličine 2D niza
    int r = i / size;
    int c = i % size;
    // pretvaramo u koordinate na UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // provjeravamo posebni slučaj za praznu pločicu
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // za ostale pločice
    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);
  }
}

Na kraju, overridden metode paintComponent, koja je izvedena iz klase JPane. Zatim koristimo metodu drawGrid, a zatim metodu drawStartMessage za prikaz poruke koja predlaže klik na pokretanje igre:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Kliknite za pokretanje nove igre";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centriraj string s za danu pločicu (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);
}

Reagiramo na korisničke akcije u UI

Kako bi igra napredovala, potrebno je obraditi korisničke akcije u UI. Za to dodajemo implementaciju MouseListener na Jpanel i kod za pomicanje pločica, već prikazanog ranije:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // korišteno da korisnicima omogući interakciju na mreži klikom
    // vrijeme je za implementaciju interakcije s korisnicima za pomicanje pločica kako bi se riješila igra!
    if (gameOver) {
      newGame();
    } else {
      // dobijemo poziciju klika
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // klik u mreži?
      if (ex  gridSize  || ey  gridSize)
        return;

      // dobijemo poziciju u mreži
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // dobijemo poziciju prazne ćelije
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // pretvaramo u 1D koordinatu
      int clickPos = r1 * size + c1;

      int dir = 0;

      // tražimo smjer za više pomaka pločica odjednom
      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) {
        // pomičemo pločice u smjeru
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // provjeravamo je li igra riješena
      gameOver = isSolved();
    }

    // ponovno slikamo panel
    repaint();
  }
});

Kod stavljamo u konstruktor klase GameOfFifteen. Na kraju pozivamo metodu newGame za započinjanje nove igre.

Cijeli kod igre

Posljednji korak prije nego što vidimo igru u akciji je sastaviti sve elemente koda zajedno. Evo što će proizać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;

// Kreiramo igru Puzzle od 15 s pomoću Java 8 i Swing
// Ako imate pitanja, slobodno pročitajte komentare ;)
public class GameOfFifteen extends JPanel { // Naša mreža će biti nacrtana u posebnoj Paneli

  // Veličina naše instance igre Puzzle od 15
  private int size;
  // Broj pločica
  private int nbTiles;
  // Dimenzije UI mreže
  private int dimension;
  // Boja prednjeg plana
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // koristimo proizvoljnu boju
  // Random objekt za miješanje pločica
  private static final Random RANDOM = new Random();
  // Pohranjujemo pločice u 1D niz cijelih brojeva
  private int[] tiles;
  // Veličina pločice na sučelju
  private int tileSize;
  // Pozicija prazne pločice
  private int blankPos;
  // Margina za mrežu na okviru
  private int margin;
  // Veličina UI mreže
  private int gridSize;
  private boolean gameOver; // true ako je igra gotova, false inače

  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;

    // inicijalizacija pločica
    nbTiles = size * size - 1; // -1 jer ne brojimo praznu pločicu
    tiles = new int[size * size];

    // izračunaj veličinu mreže i veličinu pločice
    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) {
        // koristi se za omogućavanje korisnicima interakciju na mreži klikom
        // vrijeme je za implementaciju interakcije s korisnicima kako bi se pločice pomicale za rješavanje igre!
        if (gameOver) {
          newGame();
        } else {
          // dobivanje pozicije klika
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;

          // klik u mreži?
          if (ex  gridSize || ey  gridSize)
            return;

          // dobivanje pozicije u mreži
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;

          // dobivanje pozicije prazne ćelije
          int c2 = blankPos % size;
          int r2 = blankPos / size;

          // pretvaramo u 1D koordinate
          int clickPos = r1 * size + c1;

          int dir = 0;

          // tražimo smjer za višestruke pomake pločica odjednom
          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) {
            // pomičemo pločice u tom smjeru
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);

            tiles[blankPos] = 0;
          }

          // provjeravamo je li igra riješena
          gameOver = isSolved();
        }

        // ponovo crtamo panel
        repaint();
      }
    });

    newGame();
  }

  private void newGame() {
    do {
      reset(); // reset u početno stanje
      shuffle(); // miješanje
    } while(!isSolvable()); // ponavlja dok mreža ne bude rješiva

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

  // Samo polovina permutacija puzzla je rješiva.
  // Kad god je pločica prethodila pločici s višom vrijednosti, to se računa
  // kao obrnuto. U našem slučaju, s praznom pločicom u rješenju,
  // broj obrnutih mora biti paran da bi se problem mogao riješiti.
  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) // ako prazna pločica nije u rješenju => nije rješena
      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("Igra od 15");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centriraj na ekranu
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }

}

Napokon, igramo!

Vrijeme je da pokrenemo igru i vidimo kako funkcionira. Polje bi trebalo izgledati ovako:

"Petnaestica" na Javi — kako razviti punopravnu igru

Pokušavamo riješiti zagonetku. Ako je sve prošlo uspješno, dobijamo ovo:

"Petnaestica" na Javi — kako razviti punopravnu igru

I to je to. Jeste li očekivali više? 🙂

Skillbox preporučuje:

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster