Java-da "o'n besh" - to'liq huquqli o'yinni qanday ishlab chiqish kerak

Java-da "o'n besh" - to'liq huquqli o'yinni qanday ishlab chiqish kerak

"O'n besh" yoki "O'n besh" butun dunyoda mashhur bo'lgan oddiy mantiqiy o'yinning ajoyib namunasidir. Jumboqni yechish uchun siz kvadratchalarni raqamlar bilan tartiblashtirib, kichikdan kattagacha tartiblashingiz kerak. Bu oson emas, lekin qiziqarli.

Bugungi qo'llanmada biz sizga Eclipse yordamida Java 8 da Fifteenni qanday ishlab chiqishni ko'rsatamiz. UI ni ishlab chiqish uchun biz Swing API dan foydalanamiz.

Sizga eslatib o'tamiz: "Habr" ning barcha o'quvchilari uchun - "Habr" promo-kodidan foydalangan holda har qanday Skillbox kursiga yozilishda 10 000 rubl chegirma.

Skillbox tavsiya qiladi: Ta'lim onlayn kurs "Java dasturchi kasbi".

O'yin dizayni

Ushbu bosqichda siz xususiyatlarni aniqlashingiz kerak:

  • Hajmi - o'yin maydonining o'lchami;
  • nbTiles - maydondagi teglar soni. nbTiles = o'lcham*o'lcham - 1;
  • Plitkalar butun sonlarning bir o'lchovli massivi bo'lgan tegdir. Teglarning har biri [0, nbTiles] oralig'ida noyob qiymatga ega bo'ladi. Nol bo'sh kvadratni bildiradi;
  • blankPos - bo'sh kvadratning pozitsiyasi.

O'yin mantiq

Biz yangi o'yin pozitsiyasini ishga tushirish uchun ishlatiladigan qayta o'rnatish usulini aniqlashimiz kerak. Teg massivining har bir elementi uchun qiymatni shu tarzda o'rnatamiz. Xo'sh, keyin biz blankPos ni massivning oxirgi holatiga qo'yamiz.

Teglar qatorini aralashtirish uchun bizga aralashish usuli ham kerak. Biz aralashtirish jarayonida bo'sh tegni asl holatida qoldirish uchun kiritmaymiz.

Jumboqning mumkin bo'lgan boshlang'ich pozitsiyalarining faqat yarmi yechimga ega bo'lganligi sababli, joriy tartibning umuman echilishi mumkinligiga ishonch hosil qilish uchun aralashish natijasi tekshirilishi kerak. Buning uchun biz isSolvable usulini aniqlaymiz.

Agar ma'lum bir dog'dan oldin yuqoriroq qiymatga ega bo'lgan dog' bo'lsa, bu inversiya hisoblanadi. Bo'sh teg o'rnatilganda, jumboq yechilishi uchun inversiyalar soni teng bo'lishi kerak. Shunday qilib, biz inversiya sonini hisoblaymiz va agar raqam juft bo'lsa, rostini qaytaramiz.

Keyin, bizning Game Of Fifteen qo'limiz hal qilinganligini tekshirish uchun isSolved usulini aniqlash muhim. Avval biz bo'sh teg qaerda joylashganiga qaraymiz. Agar boshlang'ich holatda bo'lsa, u holda joriy tartib yangi, ilgari hal qilinmagan. Keyin biz plitkalarni teskari tartibda aylantiramiz va teg qiymati mos keladigan +1 indeksidan farq qilsa, biz noto'g'ri qaytaramiz. Aks holda, usul oxirida true ni qaytarish vaqti keldi, chunki jumboq allaqachon echilgan.

Aniqlanishi kerak bo'lgan yana bir usul - bu newGame. O'yinning yangi nusxasini yaratish talab qilinadi. Buni amalga oshirish uchun biz o'yin taxtasini qayta o'rnatamiz, keyin uni aralashtiramiz va o'yin pozitsiyasi hal qilinmaguncha davom etamiz.

Teglarning asosiy mantig'iga ega kod misoli:

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

Nihoyat, massivdagi teglar harakatini dasturlash kerak. Kursor harakatiga javob berish uchun bu kod keyinchalik qayta qo'ng'iroq orqali chaqiriladi. Bizning o'yinimiz bir vaqtning o'zida bir nechta plitka harakatlarini qo'llab-quvvatlaydi. Shunday qilib, biz ekrandagi bosilgan pozitsiyani tegga aylantirganimizdan so'ng, biz bo'sh teg o'rnini olamiz va uning bir nechta harakatlarini bir vaqtning o'zida qo'llab-quvvatlash uchun harakat yo'nalishini qidiramiz.

Mana misol kod:

// 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 yordamida foydalanuvchi interfeysini ishlab chiqish

Interfeys vaqti keldi. Avval Jpanel sinfini olamiz. Keyin biz maydonga teglarni chizamiz - har birining o'lchamini hisoblash uchun biz o'yin konstruktorining parametrida ko'rsatilgan ma'lumotlardan foydalanamiz:

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

Margin ham o'yin konstruktorida o'rnatilgan parametrdir.

Endi biz ekrandagi panjara va dog'larni chizish uchun drawGrid usulini aniqlashimiz kerak. Biz teglar qatorini tahlil qilamiz va koordinatalarni foydalanuvchi interfeysi koordinatalariga aylantiramiz. Keyin biz har bir tegni markazga tegishli raqam bilan chizamiz:

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

Nihoyat, biz JPane sinfidan olingan paintComponent usulini bekor qilamiz. Keyin biz o'yinni boshlash uchun bosishni so'ragan xabarni ko'rsatish uchun drawGrid usulini, so'ngra drawStartMessage usulidan foydalanamiz:

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

UIda foydalanuvchi harakatlariga javob berish

O'yin o'z yo'nalishida ishlashi uchun foydalanuvchi interfeysida foydalanuvchi harakatlarini qayta ishlash kerak. Buning uchun biz Jpanel-da MouseListener dasturini va yuqorida ko'rsatilgan teglarni ko'chirish kodini qo'shamiz:

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

Kod GameOfFifteen sinfining konstruktoriga joylashtirilgan. Oxirida biz yangi o'yinni boshlash uchun newGame usulini chaqiramiz.

To'liq o'yin kodi

O'yinni amalda ko'rishdan oldingi oxirgi qadam barcha kod qismlarini bir joyga qo'yishdir. Mana nima sodir bo'ladi:

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

Nihoyat, o'ynaymiz!

O'yinni ishga tushirish va uni amalda sinab ko'rish vaqti keldi. Maydon quyidagicha ko'rinishi kerak:

Java-da "o'n besh" - to'liq huquqli o'yinni qanday ishlab chiqish kerak

Keling, jumboqni hal qilishga harakat qilaylik. Agar hamma narsa yaxshi bo'lsa, biz buni olamiz:

Java-da "o'n besh" - to'liq huquqli o'yinni qanday ishlab chiqish kerak

Ana xolos. Ko'proq kutganmidingiz? 🙂

Skillbox tavsiya qiladi:

Manba: www.habr.com

a Izoh qo'shish