«Pitanja» na Javi — kako razviti potpunu igru

«Pitanja» na Javi — kako razviti potpunu igru

»Petnajst«, oziroma »Petnajška« — odličen primer preproste logične igre, ki je priljubljena po vsem svetu. Za rešitev uganke je potrebno razporediti kvadratke s številkami v pravem vrstnem redu, od najmanjšega do največjega. Nije lahko, a zanimivo.

V današnjem tutorialu prikazujemo, kako razviti »Petnajst« na Javi 8 z uporabo Eclipse. Za razvoj uporabniškega vmesnika bomo uporabili Swing API.

Opominjamo: za vse bralce »Habra« — popust 10 000 rubljev ob vpisu na kateri koli tečaj Skillbox s promo kodo »Habr«.

Skillbox priporoča: Образовательный онлайн-курс «Профессия Java-разработчик».

Oblikovanje igre

V tej fazi je treba določiti lastnosti:

  • Size — velikost igralne površine;
  • nbTiles — število petnajstk v polju. nbTiles = size*size — 1;
  • Tiles — petnajstka, ki predstavlja enodimenzionalno tabelo celih števil. Vsaka od petnajstk bo dobila edinstveno vrednost v razponu [0, nbTiles]. Ničla označuje prazni kvadrat;
  • blankPos — položaj praznega kvadrata.

Logika igre

Treba je določiti metodo reset, ki se uporablja za inicializacijo novega igralnega položaja. Tako nastavimo vrednost za vsak element tabele petnajstk. Nato prazno petnajstko postavimo na zadnji položaj tabele.

Potrebna je tudi metoda shuffle za premešanje tabele petnajstk. Ne vključujemo prazne petnajstke v proces premeščanja, da jo obdržimo na prejšnjem mestu.

Ker ima le polovica možnih začetnih položajev uganke rešitev, je potrebno preveriti nastali rezultat premešanja, da se prepričamo, da je trenutni položaj sploh rešljiv. Da bi to naredili, določimo metodo isSolvable.

Če določeni petnajstki prehaja petnajstka z višjo vrednostjo, to velja za obrat. Ko je prazna petnajstka na svojem mestu, mora biti število obratov sodo, da je uganka rešena. Tako preštejemo število obratov in vrnemo true, če je število sodo.

Nato je pomembno določiti metodo isSolved, da preverimo, ali je naš položaj igre Game Of Fifteen rešen. Najprej si ogledamo, kje se nahaja prazna petnajstka. Če je na začetni poziciji, je trenutni položaj nov, pred tem še nerešen. Nato pregledujemo ploščice v obratnem vrstnem redu, in če se vrednost petnajstke razlikuje od ustreznega indeksa +1, vrnemo false. V nasprotnem primeru pa je na koncu metode čas, da vrnemo true, ker je uganka že rešena.

Drugi način, ki ga je treba določiti, je newGame. Potreben je za ustvarjanje novega izvodu igre. Za to ponastavimo igralno polje, ga premešamo in nadaljujemo, dokler igralna pozicija ni rešena.

Tukaj je primer kode s ključnimi logikami petice:

private void newGame() {
  do {
    reset(); // ponastavi v začetno stanje
    shuffle(); // premešaj
  } while(!isSolvable()); // nadaljuj, dokler mreža ne postane rešena
 
  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;
  }
}
 
// Le polovica permutacij uganke je rešljiva/
// Kadar koli je plošček pred ploščkom z višjo vrednostjo, se šteje
// kot obrat. V našem primeru, s praznim ploščkom na rešenem mestu,
// mora biti število obratov sode, da je uganka rešena.
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) // če prazni plošček ni na rešenem mestu == ne rešen
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Nazadnje je treba programirati premikanje ploščkov v polju. Ta koda se bo pozneje klicala preko povratnega klica, da bi se odzvala na gibanje kurzorja. Naša igra bo podpirala več premikov ploščkov hkrati. Ko prenesemo pritisnjeno pozicijo na zaslonu na plošček, dobimo pozicijo praznega ploščka in poiščemo smer gibanja za podporo več premikom hkrati.

Tukaj je primer kode:

// 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 na Swing API

Zdaj je čas, da se posvetimo vmesniku. Najprej vzamemo razred Jpanel. Nato na polju narišemo ploščke – za izračun velikosti vsakega uporabimo podatke, določene v parametru konstruktorja igre:

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

Margin je prav tako parameter, določen v konstruktorju igre.

Zdaj je treba določiti metodo drawGrid za risanje mreže in ploščkov na zaslonu. Analiziramo polje ploščkov in pretvorimo koordinate v koordinate uporabniškega vmesnika. Nato narišemo vsak plošček s pripadajočim številko v središču:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // we convert 1D coords to 2D coords given the size of the 2D Array
    int r = i / size;
    int c = i % size;
    // we convert in coords on the UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;
 
    // check special case for blank tile
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }
 
      continue;
    }
 
    // for other tiles
    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);
  }
}

Najprej bomo nadomestili metodo paintComponent, ki je izpeljana iz razreda JPane. Potem bomo uporabili metodo drawGrid in nato metodo drawStartMessage za prikaz sporočila, ki spodbuja klik, da začnete igro:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Kliknite za začetek nove igre";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // center string s for the given tile (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 dejanja uporabnika v uporabniškem vmesniku.

Da igra poteka gladko, je potrebno obravnavati dejanja uporabnika v uporabniškem vmesniku. Za to dodamo implementacijo MouseListener na Jpanel in kodo za premikanje ploščic, ki je bila že prikazana zgoraj:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // used to let users to interact on the grid by clicking
    // it's time to implement interaction with users to move tiles to solve the game !
    if (gameOver) {
      newGame();
    } else {
      // get position of the click
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // click in the grid ?
      if (ex  gridSize  || 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;
      }
 
      // we check if game is solved
      gameOver = isSolved();
    }
 
    // we repaint panel
    repaint();
  }
});

Koda umestimo v konstruktor razreda GameOfFifteen. Na koncu pokličemo metodo newGame za začetek nove igre.

Poln koda igre

Zadnji korak, preden vidimo igro v akciji, je, da združimo vse elemente kode. Tukaj je, kaj dobimo:

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;
 
// Создаём игру Пятнашки на Java 8 и Swing
// Если у вас есть вопросы, не стесняйтесь читать комментарии ;)
public class GameOfFifteen extends JPanel { // наша сетка будет отображаться в отдельной панели
 
  // Размер нашего экземпляра Пятнашек
  private int size;
  // Количество плиток
  private int nbTiles;
  // Размеры графического интерфейса сетки
  private int dimension;
  // Цвет переднего плана
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // используем произвольный цвет
  // Объект Random для перемешивания плиток
  private static final Random RANDOM = new Random();
  // Храним плитки в одномерном массиве целых чисел
  private int[] tiles;
  // Размер плитки на графическом интерфейсе
  private int tileSize;
  // Позиция пустой плитки
  private int blankPos;
  // Отступ для сетки на фрейме
  private int margin;
  // Размер сетки интерфейса
  private int gridSize;
  private boolean gameOver; // true, если игра окончена, false — в противном случае
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // Инициализация плиток
    nbTiles = size * size - 1; // - 1, потому что не учитываем пустую плитку
    tiles = new int[size * size];
    
    // Расчёт размеров сетки и плитки
    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) {
        // позволяет пользователям взаимодействовать с сеткой, нажимая на неё
        // настало время реализовать взаимодействие с пользователями для перемещения плиток и решения игры!
        if (gameOver) {
          newGame();
        } else {
          // получаем позицию клика
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // кликнули в сетке?
          if (ex  gridSize || ey  gridSize)
            return;
          
          // получаем позицию в сетке
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // получаем позицию пустой клетки
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // переводим в одномерные координаты
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // ищем направление для множественных перемещений плиток одновременно
          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) {
            // перемещаем плитки в указанном направлении
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // проверяем, решена ли игра
          gameOver = isSolved();
        }
        
        // перерисовываем панель
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // сброс в начальное состояние
      shuffle(); // перемешивание
    } while(!isSolvable()); // продолжаем, пока сетка не станет решаемой
    
    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;
    }
  }
 
  // Только половина перестановок головоломки являются решаемыми.
  // Каждый раз, когда плитка предшествует плитке с большим значением, это считается
  // как инверсия. В нашем случае, при пустой плитке в решённом положении,
  // количество инверсий должно быть чётным, чтобы головоломка была решаемой
  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) // если пустая плитка не в решённом положении == не решена
      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("Игра в Пятнашки");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // центрируем на экране
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Nazad, igrajmo!

Pravi trenutak je da pokrenemo igru i proverimo je u akciji. Polje bi trebalo da izgleda ovako:

«Pitanja» na Javi — kako razviti potpunu igru

Pokušavamo da rešimo zagonetku. Ako sve prođe uspešno, dobijamo ovo:

«Pitanja» na Javi — kako razviti potpunu igru

To je to. Da li ste očekivali više? 🙂

Skillbox priporoča:

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster