"Pesëmbëdhjetë" në Java — si të zhvillosh një lojë të plotë

"Pesëmbëdhjetë" në Java — si të zhvillosh një lojë të plotë

"Pesëmbëdhjetë", ose "Pesëmbëdhjetëka" është një shembull i shkëlqyer i një loje logjike të thjeshtë, e njohur në të gjithë botën. Për të zgjidhur enigmat, është e nevojshme të vendosen katrorët me numra në rend, nga më i vogli në më të madhin. Nuk është e thjeshtë, por është interesante.

Në këtë tutorial po tregojmë se si të zhvillojmë "Pesëmbëdhjetë" në Java 8 me Eclipse. Për zhvillimin e UI do të përdorim Swing API.

Kujtojmë: për të gjithë lexuesit e «Habra» — zbritje prej 10,000 rublesh për regjistrimin në çdo kurs Skillbox me kodin promovues «Habr».

Skillbox rekomandon: Kurs online edukativ «Profesia Java-zhvillues».

Projektimi i lojës

Në këtë fazë duhet të përcaktojmë pronat:

  • Size — madhësia e fushës së lojës;
  • nbTiles — numri i pesëmbëdhjetëve në fushë. nbTiles = size*size — 1;
  • Tiles — pesëmbëdhjete, e cila përfaqëson një varg një dimensionale të numrave të plotë. Çdo pesëmbëdhjetë do të marrë një vlerë unike brenda gamës [0, nbTiles]. Nula përfaqëson katrorin bosh;
  • blankPos — pozita e katrorit bosh.

Logjika e lojës

Duhet të përcaktojmë metodën reset, e cila përdoret për të inicializuar një pozicion të ri në lojë. Kështu vendosim vlerën për çdo element të vargut të pesëmbëdhjetëve. Pastaj vendosim blankPos në pozitat e fundit të vargut.

Përveç kësaj, duhet një metodë shuffle për të përzier vargun e pesëmbëdhjetëve. Ne nuk e përfshijmë pesëmbëdhjetën bosh në procesin e përzierjes, për të mbajtur atë në pozitat e saj të mëparshme.

Duke qenë se vetëm gjysma e pozitat fillestare të mundshme të enigmes ka një zgjidhje, duhet të kontrollojmë rezultatin e përzierjes për të siguruar se ngarkesa aktuale është e zgjidhshme. Për ta bërë këtë, përcaktojmë metodën isSolvable.

Nëse një pesëmbëdhjete e caktuar paraprihet nga një pesëmbëdhjete me një vlerë më të lartë, kjo konsiderohet një inversion. Kur pesëmbëdhjeta bosh është në vendin e saj, numri i inversioneve duhet të jetë çift për të bërë enigmat të zgjidhshme. Pra, ne numërojmë numrin e inversioneve dhe kthejmë true nëse numri është çift.

Pastaj është e rëndësishme të përcaktojmë metodën isSolved, për të kontrolluar nëse ngarkesa jonë e Game Of Fifteen është zgjidhur. Para se gjithash, shohim se ku ndodhet pesëmbëdhjeta bosh. Nëse është në pozita fillestare, atëherë ngarkesa aktuale është një e re, e pa zgjidhur më parë. Pastaj kalojmë përmes pllakatave në rendin e kundërt, dhe, nëse vlera e pesëmbëdhjetës ndryshon nga indeksi përkatës +1, kthejmë false. Në të kundërt, në fund të metodës është koha të kthejmë true, sepse enigmat tashmë janë zgjidhur.

Një metodë tjetër që duhet të definoni është newGame. Kjo kërkohet për të krijuar një instancë të re të lojës. Për këtë, ne rikthejmë fushën e lojës, pastaj e përzjejmë atë dhe vazhdojmë derisa pozita e lojës të jetë e zgjidhshme.

Ja një shembull kodi me logjikën kyçe të puzzle-it më të madh.

private void newGame() {
  do {
    reset(); // rikthe në gjendjen fillestare
    shuffle(); // përzjej
  } while(!isSolvable()); // bëje derisa grilja të jetë e zgjidhshme

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

// Vetëm gjysma e permutimeve të puzzles janë të zgjidhshme/
// Sa herë që një pllakë i paraprin një pllakë me vlerë më të lartë, kjo llogaritet
// si një inversim. Në rastin tonë, me pllakën bosh në pozita të zgjidhura,
// numri i inversimeve duhet të jetë çift për puzzle-n që të jetë e zgjidhshme
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) // nëse plaka bosh nuk është në pozita të zgjidhura ==>
    return false;

  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }

  return true;
}

Më në fund, duhet të programojmë lëvizjen e pllakatave në array. Ky kod do të thirret më vonë përmes një thirrjeje (callback) për të reaguar ndaj lëvizjes së kursorit. Loja jonë do të mbështesë disa lëvizje të pllakatave njëkohësisht. Kështu, pasi ta kemi konvertuar pozita e prekur në ekran në një pllakë, ne marrim pozita e pllakatës bosh dhe kërkojmë drejtimin e lëvizjes për të mbështetur disa lëvizje të saj njëkohësisht.

Ja një shembull kodi:

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

Po zhvillojmë UI me Swing API

Është koha për të punuar mbi ndërfaqen. Fillimisht marim klasën Jpanel. Pastaj do të vizatojmë pllakatat në fushë - për të llogaritur dimensionet e secilës do të shfrytëzojmë të dhënat e caktuara në parametrin e konstruktuesit të lojës:

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

Margin gjithashtu është një parametër i caktuar në konstruktuesin e lojës.

Tani duhet të përcaktojmë metodën drawGrid për të vizatuar gridin dhe pllakatat në ekran. Anlizojmë array-n e pllakatave dhe i konvertojmë koordinatat në koordinatat e ndërfaqes. Pastaj do të vizatojmë secilën pllakë me numrin përkatës në qendër:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // ne convertojmë koordinatat 1D në koordinata 2D duke pasur parasysh madhësinë e Array 2D
    int r = i / size;
    int c = i % size;
    // ne konvertojmë në koordinatat në UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // kontrollojmë rastin special për pllakën bosh
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // për pllakat e tjera
    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);
  }
}

Në fund, do të tejkaluam metodën paintComponent, e cila është një klasë e nxjerrë nga JPane. Më pas do të përdorim metodën drawGrid, dhe pas saj metodën drawStartMessage për të shfaqur mesazhin që sugjeron të klikoni për të filluar lojën:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Kliko për të filluar lojën e re";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // qendron string s për pllaken e dhënë (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);
}

Reagojmë ndaj veprimeve të përdoruesit në UI

Për të siguruar që loja ecën normalisht, është e nevojshme të trajtojme veprimet e përdoruesit në UI. Për këtë, shtojmë implementimin e MouseListener në Jpanel dhe kodin për të lëvizur pllakat, siç është treguar më lart:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // përdoret për të lejuar përdoruesit të interaktojnë në grid duke klikuar
    // tani është koha të implementojmë interaksionin me përdoruesit për të lëvizur pllakat për të zgjidhur lojën!
    if (gameOver) {
      newGame();
    } else {
      // merr pozita e klikimit
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // kliko në grid?
      if (ex  gridSize || ey  gridSize)
        return;

      // merr poziten në grid
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // merr pozitën e qelizës bosh
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // ne konvertojmë në koordinatën 1D
      int clickPos = r1 * size + c1;

      int dir = 0;

      // kërkojmë drejtimin për lëvizje shumëplakjesh njëherazi
      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) {
        // ne lëvizim pllakat në drejtimin e duhur
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // kontrollojmë nëse loja është zgjidhur
      gameOver = isSolved();
    }

    // ne ripikturojmë panelin
    repaint();
  }
});

Kodi e vendosim në konstruktorin e klasës GameOfFifteen. Në fund të fundit, thërrasim metodën newGame për të filluar një lojë të re.

Kodi i plotë i lojës

Hapi i fundit, para se të shihni lojën në veprim, është të bashkoni të gjithë elementet e kodit. Këto janë rezultatet:

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;
 
// Ne do të krijojmë një lojë Puzzle 15 me Java 8 dhe Swing
// Nëse keni ndonjë pyetje, mos hezitoni të lexoni komentet ;)
public class GameOfFifteen extends JPanel { // gridi ynë do të vizatohet në një Panel të dedikuar
 
  // Madhësia e instancës sonë të Lojës së Pesëmbëdhjetë
  private int size;
  // Numri i pllakatave
  private int nbTiles;
  // Dimensioni i UI të Grid-it
  private int dimension;
  // Ngjyra e Parë
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // ne përdorim një ngjyrë arbitrare
  // Objekt i rastësishëm për të përzier pllakat
  private static final Random RANDOM = new Random();
  // Ruajtja e pllakatave në një Array 1D të integerëve
  private int[] tiles;
  // Madhësia e pllakës në UI
  private int tileSize;
  // Pozita e pllakës bosh
  private int blankPos;
  // Margin për gridin në frame
  private int margin;
  // Madhësia e UI të Grid-it
  private int gridSize;
  private boolean gameOver; // e vërtetë nëse loja ka përfunduar, e falsë për ndryshe
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // init pllakat
    nbTiles = size * size - 1; // -1 sepse nuk e numërojmë pllakën bosh
    tiles = new int[size * size];
    
    // llogarit madhësinë e grid-it dhe madhësinë e pllakave
    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) {
        // përdoret për të lejuar përdoruesit të ndërveprojnë në grid duke klikuar
        // është koha të implementojmë ndërveprimin me përdoruesit për të lëvizur pllakat për të zgjidhur lojën!
        if (gameOver) {
          newGame();
        } else {
          // merr pozitat e klikimit
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // klikimi në grid?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // merr pozitat në grid
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // merr pozitat e qelizës bosh
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // ne konvertojmë në koordinatat 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // ne kërkojmë drejtimin për lëvizje të shumëfishta të pllakatave
          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) {
            // ne lëvizim pllakatat në drejtim
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // ne kontrollojmë nëse loja është zgjidhur
          gameOver = isSolved();
        }
        
        // ne ripikturojmë panelin
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // rivendos në gjendjen fillestare
      shuffle(); // përzihet
    } while(!isSolvable()); // bëj deri sa grid duhet të jetë i zgjidhshëm
    
    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;
    }
  }
 
  // Vetëm gjysma e permutimeve të puzzles janë të zgjidhshme.
  // Sa herë që një pllakë paraprihet nga një pllakë me vlerë më të lartë, ajo llogaritet
  // si një inversion. Në rastin tonë, me pllakën bosh në pozitat e zgjidhura,
  // numri i inversioneve duhet të jetë çift për të qenë puzzle i zgjidhshëm
  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) // nëse plaka bosh nuk është në pozitat e zgjidhura => nuk është e zgjidhur
      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("Lojë e Pesëmbëdhjetë");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // centroni në ekran
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
}

Më në fund, luajmë!

Është koha për të nisur lojën dhe për ta provuar atë në veprim. Fusha duhet të duket si më poshtë:

"Pesëmbëdhjetë" në Java — si të zhvillosh një lojë të plotë

Të provosh të zgjidhesh enigmat. Nëse gjithçka shkoi mirë, ne marrim këtë:

"Pesëmbëdhjetë" në Java — si të zhvillosh një lojë të plotë

Kjo është gjithçka. A prisnit më shumë? 🙂

Skillbox rekomandon:

Burimi: habr.com

Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS 🔥 Blini hostim të besueshëm për faqe interneti me mbrojtje DDoS, serverë VPS VDS - ProHoster