"Tag" yn Java - sut i ddatblygu gêm lawn

"Tag" yn Java - sut i ddatblygu gêm lawn

"Pymtheg" neu "Pymtheg" yn enghraifft wych o gêm rhesymeg syml sy'n boblogaidd ar draws y byd. Er mwyn datrys y pos, mae angen i chi drefnu'r sgwariau gyda rhifau yn eu trefn, o'r lleiaf i'r mwyaf. Nid yw'n hawdd, ond mae'n ddiddorol.

Yn y tiwtorial heddiw byddwn yn dangos i chi sut i ddatblygu Fifteen yn Java 8 gydag Eclipse. I ddatblygu'r UI byddwn yn defnyddio'r API Swing.

Rydym yn atgoffa: i holl ddarllenwyr "Habr" - gostyngiad o 10 rubles wrth gofrestru ar unrhyw gwrs Skillbox gan ddefnyddio'r cod hyrwyddo "Habr".

Mae Skillsbox yn argymell: Cwrs addysgol ar-lein "Datblygwr Java Proffesiynol".

Dylunio Gêm

Ar y cam hwn mae angen ichi ddiffinio'r priodweddau:

  • Maint — maint y cae chwarae;
  • nbTiles — nifer y tagiau yn y maes. nbTiles = maint * maint - 1;
  • Mae teils yn dag sy'n amrywiaeth un-dimensiwn o gyfanrifau. Bydd pob un o'r tagiau yn derbyn gwerth unigryw yn yr ystod [0, nbTiles]. Mae sero yn dynodi sgwâr gwag;
  • blankPos — lleoliad y sgwâr gwag.

Rhesymeg gêm

Mae angen i ni ddiffinio dull ailosod a ddefnyddir i gychwyn safle gêm newydd. Fel hyn rydym yn gosod gwerth ar gyfer pob elfen o'r arae tagiau. Wel, yna rydyn ni'n gosod blankPos yn safle olaf yr arae.

Mae angen dull siffrwd arnom hefyd i newid yr amrywiaeth o dagiau. Nid ydym yn cynnwys y tag gwag yn y broses symud er mwyn ei adael yn yr un sefyllfa.

Gan mai dim ond hanner mannau cychwyn posibl y pos sydd â datrysiad, mae angen i chi wirio'r canlyniad siffrwd dilynol i wneud yn siŵr bod y cynllun presennol hyd yn oed yn solvable. I wneud hyn, rydym yn diffinio'r dull isSolvable.

Os oes tag â gwerth uwch o flaen tag penodol, fe'i hystyrir yn wrthdroad. Pan fydd y man gwag yn ei le, rhaid i nifer y gwrthdroadau fod yn gyfartal er mwyn i'r pos fod yn solvable. Felly rydym yn cyfrif nifer y gwrthdroadau ac yn dychwelyd yn wir os yw'r rhif yn eilrif.

Yna mae'n bwysig diffinio'r dull isSolved i wirio a yw ein cynllun Game Of Fifteen wedi'i ddatrys. Yn gyntaf edrychwn ar ble mae'r man gwag. Os yn y sefyllfa gychwynnol, yna mae'r aliniad presennol yn newydd, na phenderfynwyd yn flaenorol. Yna rydyn ni'n ailadrodd trwy'r teils mewn trefn wrthdroi, ac os yw gwerth y tag yn wahanol i'r mynegai cyfatebol +1, rydyn ni'n dychwelyd ffug. Fel arall, ar ddiwedd y dull mae'n bryd dychwelyd yn wir oherwydd bod y pos eisoes wedi'i ddatrys.

Dull arall y mae angen ei ddiffinio yw newGame. Mae'n ofynnol i greu enghraifft newydd o'r gêm. I wneud hyn, rydyn ni'n ailosod y cae chwarae, yna'n ei gymysgu ac yn parhau nes bod modd datrys y sefyllfa chwarae.

Dyma god enghreifftiol gyda rhesymeg allweddol tag:

private void newGame() {
  do {
    reset(); // reset in initial state
    shuffle(); // shuffle
  } while(!isSolvable()); // make it until grid be solvable
 
  gameOver = false;
}
 
private void reset() {
  for (int i = 0; i < tiles.length; i++) {
    tiles[i] = (i + 1) % tiles.length;
  }
 
  // we set blank cell at the last
  blankPos = tiles.length - 1;
}
 
private void shuffle() {
  // don't include the blank tile in the shuffle, leave in the solved position
  int n = nbTiles;
 
  while (n > 1) {
    int r = RANDOM.nextInt(n--);
    int tmp = tiles[r];
    tiles[r] = tiles[n];
    tiles[n] = tmp;
  }
}
 
// Only half permutations of the puzzle are solvable/
// Whenever a tile is preceded by a tile with higher value it counts
// as an inversion. In our case, with the blank tile in the solved position,
// the number of inversions must be even for the puzzle to be solvable
private boolean isSolvable() {
  int countInversions = 0;
 
  for (int i = 0; i < nbTiles; i++) {
    for (int j = 0; j < i; j++) {
      if (tiles[j] > tiles[i])
        countInversions++;
    }
  }
 
  return countInversions % 2 == 0;
}
 
private boolean isSolved() {
  if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Yn olaf, mae angen i chi raglennu symudiad y tagiau yn yr arae. Bydd y cod hwn yn cael ei alw'n ddiweddarach trwy alwad yn ôl i ymateb i symudiad cyrchwr. Bydd ein gêm yn cefnogi symudiadau teils lluosog ar yr un pryd. Felly, ar ôl i ni drosi'r safle gwasgu ar y sgrin yn dag, rydyn ni'n cael lleoliad y tag gwag ac yn edrych am gyfeiriad symud i gefnogi nifer o'i symudiadau ar yr un pryd.

Dyma enghraifft cod:

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

Rydym yn datblygu UI gan ddefnyddio Swing API

Mae'n bryd gweithio ar y rhyngwyneb. Yn gyntaf cymerwn y dosbarth Jpanel. Yna rydyn ni'n tynnu tagiau ar y cae - i gyfrifo meintiau pob un, byddwn ni'n defnyddio'r data a nodir ym mharamedr lluniwr gêm:

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

Mae ymyl hefyd yn baramedr a osodwyd yn y lluniwr gêm.

Nawr mae angen i ni ddiffinio'r dull drawGrid i dynnu'r grid a'r smotiau ar y sgrin. Rydym yn dadansoddi'r amrywiaeth o dagiau ac yn trosi'r cyfesurynnau yn gyfesurynnau rhyngwyneb defnyddiwr. Yna tynnwch lun pob smotyn gyda'r rhif cyfatebol yn y canol:

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

Yn olaf, gadewch i ni ddiystyru'r dull Cydran paent, sy'n deillio o'r dosbarth JPane. Yna rydyn ni'n defnyddio'r dull drawGrid, ac yna'r dull drawStartMessage i arddangos neges yn ein hannog i glicio i gychwyn y gêm:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Click to start new game";
    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);
}

Ymateb i weithredoedd defnyddwyr yn yr UI

Er mwyn i'r gêm redeg ei chwrs, mae angen prosesu gweithredoedd defnyddwyr yn yr UI. I wneud hyn, ychwanegwch weithrediad MouseListener ar Jpanel a'r cod ar gyfer symud mannau, a ddangosir eisoes uchod:

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 < 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;
      }
 
      // we check if game is solved
      gameOver = isSolved();
    }
 
    // we repaint panel
    repaint();
  }
});

Rydyn ni'n gosod y cod yn adeiladwr y dosbarth GameOfFifteen. Ar y diwedd, rydyn ni'n galw'r dull newGame i ddechrau gêm newydd.

Cod gêm llawn

Y cam olaf cyn gweld y gêm ar waith yw rhoi'r holl elfennau cod at ei gilydd. Dyma beth sy'n digwydd:

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;
 
// We are going to create a Game of 15 Puzzle with Java 8 and Swing
// If you have some questions, feel free to read comments ;)
public class GameOfFifteen extends JPanel { // our grid will be drawn in a dedicated Panel
 
  // Size of our Game of Fifteen instance
  private int size;
  // Number of tiles
  private int nbTiles;
  // Grid UI Dimension
  private int dimension;
  // Foreground Color
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // we use arbitrary color
  // Random object to shuffle tiles
  private static final Random RANDOM = new Random();
  // Storing the tiles in a 1D Array of integers
  private int[] tiles;
  // Size of tile on UI
  private int tileSize;
  // Position of the blank tile
  private int blankPos;
  // Margin for the grid on the frame
  private int margin;
  // Grid UI Size
  private int gridSize;
  private boolean gameOver; // true if game over, false otherwise
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // init tiles
    nbTiles = size * size - 1; // -1 because we don't count blank tile
    tiles = new int[size * size];
    
    // calculate grid size and tile 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) {
        // 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 < 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;
          }
          
          // we check if game is solved
          gameOver = isSolved();
        }
        
        // we repaint panel
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // reset in intial state
      shuffle(); // shuffle
    } while(!isSolvable()); // make it until grid be solvable
    
    gameOver = false;
  }
 
  private void reset() {
    for (int i = 0; i < tiles.length; i++) {
      tiles[i] = (i + 1) % tiles.length;
    }
    
    // we set blank cell at the last
    blankPos = tiles.length - 1;
  }
 
  private void shuffle() {
    // don't include the blank tile in the shuffle, leave in the solved position
    int n = nbTiles;
    
    while (n > 1) {
      int r = RANDOM.nextInt(n--);
      int tmp = tiles[r];
      tiles[r] = tiles[n];
      tiles[n] = tmp;
    }
  }
 
  // Only half permutations of the puzzle are solvable.
  // Whenever a tile is preceded by a tile with higher value it counts
  // as an inversion. In our case, with the blank tile in the solved position,
  // the number of inversions must be even for the puzzle to be solvable
  private boolean isSolvable() {
    int countInversions = 0;
    
    for (int i = 0; i < nbTiles; i++) {
      for (int j = 0; j < i; j++) {
        if (tiles[j] > tiles[i])
          countInversions++;
      }
    }
    
    return countInversions % 2 == 0;
  }
 
  private boolean isSolved() {
    if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved
      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 < 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);
    }
  }
 
  private void drawStartMessage(Graphics2D g) {
    if (gameOver) {
      g.setFont(getFont().deriveFont(Font.BOLD, 18));
      g.setColor(FOREGROUND_COLOR);
      String s = "Click to start new game";
      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);
  }
 
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setTitle("Game of Fifteen");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // center on the screen
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Yn olaf, gadewch i ni chwarae!

Mae'n bryd lansio'r gêm a'i phrofi ar waith. Dylai'r maes edrych fel hyn:

"Tag" yn Java - sut i ddatblygu gêm lawn

Gadewch i ni geisio datrys y pos. Os aeth popeth yn iawn, rydyn ni'n cael hyn:

"Tag" yn Java - sut i ddatblygu gêm lawn

Dyna i gyd. Oeddech chi'n disgwyl mwy? 🙂

Mae Skillsbox yn argymell:

Ffynhonnell: hab.com

Ychwanegu sylw