Java хэл дээрх "Таг" - бүрэн хэмжээний тоглоомыг хэрхэн хөгжүүлэх

Java хэл дээрх "Таг" - бүрэн хэмжээний тоглоомыг хэрхэн хөгжүүлэх

"Арван тав" эсвэл "арван тав" бол дэлхий даяар алдартай энгийн логик тоглоомын гайхалтай жишээ юм. Оньсого тайлахын тулд та дөрвөлжин тоонуудыг жижигээс том хүртэл дарааллаар нь байрлуулах хэрэгтэй. Энэ нь амар биш, гэхдээ сонирхолтой юм.

Өнөөдрийн хичээлээр бид Eclipse-ийн тусламжтайгаар Java 8-д Fifteen-ийг хэрхэн хөгжүүлэхийг харуулах болно. UI-г хөгжүүлэхийн тулд бид Swing API ашиглах болно.

Бид танд сануулж байна: "Хабр" -ын бүх уншигчдад - "Habr" сурталчилгааны кодыг ашиглан Skillbox-ын аль ч курст бүртгүүлэхдээ 10 рублийн хөнгөлөлт.

Skillbox зөвлөж байна: Боловсролын онлайн курс "Мэргэжил Java хөгжүүлэгч".

Тоглоомын дизайн

Энэ үе шатанд та шинж чанаруудыг тодорхойлох хэрэгтэй:

  • Хэмжээ - тоглоомын талбайн хэмжээ;
  • nbTiles - талбар дахь хаягуудын тоо. nbTiles = хэмжээ*хэмжээ - 1;
  • Хавтанцар нь бүхэл тоонуудын нэг хэмжээст массив болох шошго юм. Шошго бүр нь [0, nbTiles] мужид өвөрмөц утгыг хүлээн авах болно. Тэг нь хоосон квадратыг заана;
  • blankPos — хоосон квадратын байрлал.

Тоглоомын логик

Тоглоомын шинэ байрлалыг эхлүүлэхийн тулд бид дахин тохируулах аргыг тодорхойлох хэрэгтэй. Ингэснээр бид tags массивын элемент бүрийн утгыг тохируулдаг. За тэгвэл бид массивын сүүлчийн байрлалд blankPos-ыг байрлуулна.

Мөн хаягуудын массивыг холихын тулд бидэнд холих арга хэрэгтэй. Бид хоосон шошгыг ижил байрлалд үлдээхийн тулд холих процесст оруулдаггүй.

Тааврын боломжит эхлэлийн байрлалын тал хувь нь л шийдэлтэй байдаг тул та одоогийн зохион байгуулалтыг шийдвэрлэх боломжтой эсэхийг шалгахын тулд холих үр дүнг шалгах хэрэгтэй. Үүний тулд бид isSolvable аргыг тодорхойлно.

Хэрэв тодорхой шошгоны өмнө илүү өндөр утгатай шошго байгаа бол үүнийг урвуу гэж үзнэ. Хоосон цэг байгаа үед оньсого шийдвэрлэх боломжтой байхын тулд урвуу тоо нь тэгш байх ёстой. Тиймээс бид урвуу тоог тоолж, хэрэв тоо нь тэгш байвал үнэнийг буцаана.

Дараа нь манай Game Of Fifteen зохион байгуулалт шийдэгдсэн эсэхийг шалгахын тулд isSolved аргыг тодорхойлох нь чухал юм. Эхлээд бид хоосон газар хаана байгааг харна. Хэрэв эхний байрлалд байгаа бол одоогийн тохируулга нь урьд өмнө шийдэгдээгүй шинэ байна. Дараа нь бид хавтангуудыг урвуу дарааллаар давтаж, хэрэв шошгоны утга харгалзах индекс +1-ээс өөр байвал бид худал буцаана. Үгүй бол аргын төгсгөлд таавар аль хэдийн шийдэгдсэн тул үнэнийг буцаах цаг болжээ.

Тодорхойлох шаардлагатай өөр нэг арга бол newGame юм. Тоглоомын шинэ жишээ үүсгэх шаардлагатай. Үүнийг хийхийн тулд бид тоглоомын талбарыг дахин тохируулж, дараа нь хольж, тоглох байрлалыг шийдвэрлэх хүртэл үргэлжлүүлнэ.

Тагийн гол логик бүхий жишээ код энд байна:

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

Эцэст нь та массив дахь хаягуудын хөдөлгөөнийг програмчлах хэрэгтэй. Энэ кодыг курсорын хөдөлгөөнд хариу өгөхийн тулд дараа нь буцааж залгах замаар дуудах болно. Манай тоглоом нэгэн зэрэг хэд хэдэн хавтангийн хөдөлгөөнийг дэмжих болно. Тиймээс бид дэлгэцэн дээрх дарагдсан байрлалыг шошго болгон хувиргасны дараа хоосон тагны байрлалыг олж, түүний хэд хэдэн хөдөлгөөнийг нэгэн зэрэг дэмжих хөдөлгөөний чиглэлийг хайж байна.

Энд жишээ код байна:

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

Бид Swing API ашиглан UI хөгжүүлдэг

Интерфэйс дээр ажиллах цаг болжээ. Эхлээд бид Jpanel ангид ордог. Дараа нь бид талбар дээр шошго зурдаг - тус бүрийн хэмжээг тооцоолохын тулд бид тоглоомын бүтээгчийн параметрт заасан өгөгдлийг ашиглана.

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

Margin нь мөн тоглоомын бүтээгчид тохируулсан параметр юм.

Одоо бид дэлгэцэн дээрх тор болон цэгүүдийг зурах drawGrid аргыг тодорхойлох хэрэгтэй. Бид хаягуудын массивыг шинжилж, координатыг хэрэглэгчийн интерфэйсийн координат болгон хөрвүүлдэг. Дараа нь цэг бүрийг төвд нь харгалзах тоогоор зур.

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

Эцэст нь JPane ангиас гаралтай paintComponent аргыг хүчингүй болгоё. Дараа нь бид drawGrid аргыг хэрэглэж, дараа нь drawStartMessage аргыг ашиглан тоглоомыг эхлүүлэхийн тулд товшихыг сануулсан мессежийг харуулна.

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

UI дахь хэрэглэгчийн үйлдэлд хариу үйлдэл үзүүлж байна

Тоглоомыг ажиллуулахын тулд хэрэглэгчийн үйлдлийг UI дээр боловсруулах шаардлагатай. Үүнийг хийхийн тулд Jpanel дээр MouseListener-ийн хэрэгжилт болон дээр үзүүлсэн толбуудыг зөөх кодыг нэмнэ үү.

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

Бид кодыг GameOfFifteen ангийн бүтээгчид байрлуулна. Эцэст нь бид шинэ тоглоом эхлүүлэхийн тулд newGame аргыг дууддаг.

Тоглоомын бүрэн код

Тоглоомыг ажиллуулахын өмнөх хамгийн сүүлийн алхам бол бүх кодын элементүүдийг нэгтгэх явдал юм. Энд юу болох вэ:

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

Эцэст нь тоглоцгооё!

Тоглоомыг эхлүүлж, үйл ажиллагаандаа туршиж үзэх цаг болжээ. Талбар нь дараах байдлаар харагдах ёстой.

Java хэл дээрх "Таг" - бүрэн хэмжээний тоглоомыг хэрхэн хөгжүүлэх

Тааварыг шийдэхийг хичээцгээе. Хэрэв бүх зүйл сайн болсон бол бид дараахь зүйлийг авна.

Java хэл дээрх "Таг" - бүрэн хэмжээний тоглоомыг хэрхэн хөгжүүлэх

Тэгээд л болоо. Та илүү ихийг хүлээж байсан уу? 🙂

Skillbox зөвлөж байна:

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх