«Piecīte» Java — kā izstrādāt pilnvērtīgu spēli

«Piecīte» Java — kā izstrādāt pilnvērtīgu spēli

«Piektošu», vai «Piekta» — lielisks piemērs vienkāršai loģiskai spēlei, kas ir populāra visā pasaulē. Lai atrisinātu puzli, ir jāizkārto kvadrāti ar cipariem secībā no mazāka uz lielāku. Tas nav viegli, bet ir interesanti.

Šodienas pamācībā mēs parādīsim, kā izstrādāt «Piektošu» Java 8 ar Eclipse. Lai izstrādātu saskarni, mēs izmantosim Swing API.

Atgādinām: visiem «Habr» lasītājiem — atlaide 10 000 rubļu reģistrējoties jebkurā Skillbox kursā ar promo kodu «Habr».

Skillbox iesaka: Izglītības tiešsaistes kurss «Profesionāls Java izstrādātājs».

Spēles projektēšana

Šajā posmā jānosaka īpašības:

  • Size — spēles lauka izmērs;
  • nbTiles — lauka piektošu skaits. nbTiles = size*size — 1;
  • Tiles — piektoša, kas ir vien dimensionāls veselu skaitļu masīvs. Katrai piektošai tiks piešķirta unikāla vērtība intervālā [0, nbTiles]. Nulles vērtība apzīmē tukšo kvadrātu;
  • blankPos — tukšā kvadrāta pozīcija.

Spēles loģika

Jānosaka reset metode, ko izmanto, lai inicializētu jauno spēles pozīciju. Mēs iestata vērtību katram piektošu masīva elementam. Pēc tam mēs ievietojam blankPos pēdējā masīva pozīcijā.

Tāpat nepieciešama shuffle metode, lai sajauktu piektošu masīvu. Mēs neiekļaujam tukšo piektošu sajaukšanas procesā, lai atstātu to iepriekšējā stāvoklī.

Tā kā tikai puse no iespējamajām sākuma pozīcijām ir risināma, ir nepieciešams pārbaudīt rezultātu, lai pārliecinātos, ka pašreizējais izkārtojums ir vispār risināms. Lai to izdarītu, nosakām isSolvable metodi.

Ja konkrētai piektošai priekšā ir piektoša ar augstāku vērtību, to uzskata par inversiju. Kad tukšā piektoša atrodas savā vietā, inversiju skaitam ir jābūt pāra skaitlim, lai puzle būtu risināma. Tātad mēs skaitām inversiju skaitu un atgriežam true, ja skaits ir pāra.

Pēc tam ir svarīgi definēt isSolved metodi, lai pārbaudītu, vai mūsu izkārtojums Game Of Fifteen ir risināts. Vispirms mēs aplūkojam, kur atrodas tukšā piektoša. Ja sākotnējā pozīcijā, tad pašreizējais izkārtojums ir jauns, iepriekš nerisināts. Pēc tam mēs pārskatām flīzes pretējā secībā, un, ja piektošās vērtība atšķiras no attiecīgā indeksa +1, atgriežam false. Pretējā gadījumā metodes beigās ir jāatgriež true, jo puzle jau ir atrisināta.

Vēl viens metods, kas jānosaka, ir newGame. Tas nepieciešams, lai izveidotu jaunu spēles instanci. Tādēļ mēs atiestata spēles laukumu, pēc tam to sajaucam un turpinām, līdz spēles pozīcija ir atrisināma.

Šeit ir koda piemērs ar galveno loģiku piecšķiedros:

private void newGame() {
  do {
    reset(); // atiestata sākotnējā stāvoklī
    shuffle(); // sajauc
  } while(!isSolvable()); // turpinām, līdz tīkls ir atrisināms
 
  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;
  }
}
 
// Tikai pusē permutācijas mīklas ir atrisināmas/
// Kad flīze ir iepriekš flīzei ar augstāku vērtību, tā tiek skaitīta
// kā inversija. Mūsu gadījumā, kad tukšā flīze ir atrisinātajā pozīcijā,
// inversiju skaitam ir jābūt pāra skaitlim, lai mīkla būtu atrisināma
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) // ja tukšā flīze nav atrisinātajā pozīcijā ==> nav atrisināta
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Visbeidzot, jāprogrammē piecšķiedru kustība masīvā. Šis kods tiks izsauktas vēlāk, izmantojot atgriešanas izsaukumu (callback), lai reaģētu uz kursora kustību. Mūsu spēle atbalstīs vairākus flīžu pārvietojumus vienlaicīgi. Tādējādi, pēc tam, kad esam konvertējuši nospiesto pozīciju ekrānā uz piecšķiedra, iegūstam tukšās piecšķiedra pozīciju un meklējam kustības virzienu, lai nodrošinātu vairākus tajā pašā laikā.

Šeit ir koda piemērs:

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

Izstrādājam UI uz Swing API

Ir īstais brīdis piedarboties saskarnei. Sākumā ņemam klasi Jpanel. Pēc tam uz laukuma zīmējam piecšķiedrus — lai aprēķinātu katra izmērus, izmantojam konstruktorā spēlei norādītās vērtības:

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

Margin arī ir parametrs, kas norādīts spēles konstruktorā.

Tagad jānosaka metode drawGrid, lai attēlotu tīklu un piecšķiedrus uz ekrāna. Analizējam piecšķiedru masīvu un konvertējam koordinātas uz lietotāja interfeisa koordinātām. Pēc tam attēlosim katru piecšķiedru ar atbilstošu numuru centrā:

privāta void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // mēs pārvēršam 1D koordinātas 2D koordinātās, ņemot vērā 2D masīva izmēru
    int r = i / size;
    int c = i % size;
    // mēs pārvēršam UI koordinātās
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // pārbaudām īpašu gadījumu tukšai flīzei
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // pārējām flīzēm
    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);
  }
}

Visbeidzot, pārdefinēsim paintComponent metodi, kas ir JPane klases atvasinājums. Pēc tam izmantojam drawGrid metodi, un pēc tam — drawStartMessage metodi, lai parādītu ziņu, kas aicina klikšķināt, lai uzsāktu spēli:

privāta void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Klikšķiniet, lai uzsāktu jaunu spēli";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

privāta void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centrējam string s dotajā flīzē (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);
}

Reaģējam uz lietotāja darbībām UI

Lai spēle ritētu uz priekšu, ir nepieciešams apstrādāt lietotāja darbības UI. Tādēļ pievienojam MouseListener implemantu uz Jpanel un kodu flīžu pārvietošanai, kā jau iepriekš bija parādīts:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // ļauj lietotājiem mijiedarboties ar režģi, klikšķinot
    // ir pienācis laiks īstenot mijiedarbību ar lietotājiem, lai pārvietotu flīzes, lai atrisinātu spēli!
    if (gameOver) {
      newGame();
    } else {
      // iegūst klikšķa pozīciju
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // klikšķis režģī?
      if (ex  gridSize || ey  gridSize)
        return;

      // iegūst pozīciju režģī
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // iegūst tukšās šūnas pozīciju
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // pārvēršam 1D koordinātē
      int clickPos = r1 * size + c1;

      int dir = 0;

      // meklējam virzienu vairāku flīžu pārvietošanai vienlaicīgi
      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) {
        // mēs pārvietojam flīzes norādītajā virzienā
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // pārbaudām, vai spēle ir atrisināta
      gameOver = isSolved();
    }

    // atjaunojam paneli
    repaint();
  }
});

Kodu ievietojam klases GameOfFifteen konstruktorā. Nobeigumā izsaucam metodi newGame, lai uzsāktu jaunu spēli.

Pilnais spēles kods

Pēdējais solis, pirms redzam spēli darbībā, ir apvienot visus koda elementus. Lūk, ko mēs iegūstam:

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;
 
// Mēs izveidosim 15 puzli spēli, izmantojot Java 8 un Swing
// Ja jums ir kādi jautājumi, droši lasiet komentārus ;)
public class GameOfFifteen extends JPanel { // mūsu tīkls tiks attēlots atsevišķā panelī
 
  // Mūsu 15 puzles instance izmērs
  private int size;
  // Flīžu skaits
  private int nbTiles;
  // Tīkla UI dimensija
  private int dimension;
  // Priekšplāna krāsa
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // mēs izmantojam patvaļīgu krāsu
  // Nejaušo objektu, lai sajauktu flīzes
  private static final Random RANDOM = new Random();
  // Flīžu glabāšana 1D veselu skaitļu masīvā
  private int[] tiles;
  // Flīzes izmērs UI
  private int tileSize;
  // Tukšās flīzes pozīcija
  private int blankPos;
  // Margin tīkliem uz rāmja
  private int margin;
  // Tīkla UI izmērs
  private int gridSize;
  private boolean gameOver; // patiesi, ja spēle beigusies, nevis
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inicializēt flīzes
    nbTiles = size * size - 1; // -1, jo mēs nesaskaitām tukšo flīzi
    tiles = new int[size * size];
    
    // aprēķināt tīkla izmēru un flīzes izmēru
    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) {
        // izmantots, lai ļautu lietotājiem mijiedarboties tīklā, nospiežot
        // ir laiks īstenot mijiedarbību ar lietotājiem, lai pārvietotu flīzes un atrisinātu spēli!
        if (gameOver) {
          newGame();
        } else {
          // iegūst klikšķa pozīciju
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // klikšķis tīklā?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // iegūst pozīciju tīklā
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // iegūst tukšas šūnas pozīciju
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // mēs pārvēršam 1D koordinātēs
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // mēs meklējam virzienu vairāku flīžu pārvietošanai vienlaicīgi
          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) {
            // mēs pārvietojam flīzes attiecīgajā virzienā
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // mēs pārbaudām, vai spēle ir atrisināta
          gameOver = isSolved();
        }
        
        // mēs atkārtoti attēlojam paneli
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // atgriezt sākotnējā stāvoklī
      shuffle(); // sajaukt
    } while(!isSolvable()); // turpināt līdz tīkla ir risināma
    
    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;
    }
  }
 
  // Tikai puse puzles permutāciju ir risināmas.
  // Kad flīze ir iepriekšēja flīze ar augstāku vērtību, tā tiek skaitīta
  // kā inversija. Mūsu gadījumā, ar tukšo flīzi atrisinātā pozīcijā,
  // inversiju skaitam ir jābūt pāra skaitlim, lai puzle būtu risināma
  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) // ja tukšā flīze nav atrisinātajā pozīcijā == neatrisināta
      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("15 puzle spēle");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centrēt uz ekrāna
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Beidzot, spēlējam!

Ir īstais laiks uzsākt spēli un pārbaudīt to darbībā. Laukam jāizskatās šādi:

«Piecīte» Java — kā izstrādāt pilnvērtīgu spēli

Mēģinām atrisināt mīklu. Ja viss izdevās, saņemam sekojošo:

«Piecīte» Java — kā izstrādāt pilnvērtīgu spēli

Un viss. Vai jūs gaidījāt ko vairāk? 🙂

Skillbox iesaka:

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster