«Pjatanca» nĂ« Java — si tĂ« zhvillosh njĂ« lojĂ« tĂ« plotĂ«

«Pjatanca» nĂ« Java — si tĂ« zhvillosh njĂ« lojĂ« tĂ« plotĂ«

«Pesëmbëdhjetë», ose «Pesëmbëdhjetshi» është një shembull i shkëlqyer i një loje logjike të thjeshtë, e njohur në të gjithë botën. Për të zgjidhur enigmën, duhet të rregulloni katrorët me numra në rendin e duhur, nga më i vogli te më i madhi. Kjo nuk është e lehtë, por është interesante.

Në këtë tutorial, do të tregojmë se si të zhvilloni «Pesëmbëdhjetëshin» në Java 8 me Eclipse. Për zhvillimin e UI do të përdorim API-në Swing.

KujtojmĂ«: pĂ«r tĂ« gjithĂ« lexuesit e "Habra" — zbritje prej 10,000 rublej pĂ«r regjistrimin nĂ« çdo kurs Skillbox me kodin promocional "Habr".

Skillbox rekomandon: Kurs online edukativ «Profesioni Java-developer».

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 katrorĂ«ve nĂ« fushĂ«. nbTiles = size * size — 1;
  • Tiles — pesĂ«mbĂ«dhjetĂ«shja, e cila pĂ«rbĂ«n njĂ« array njĂ«-dimensional tĂ« numrave tĂ« plotĂ«. Çdo katror do tĂ« marrĂ« njĂ« vlerĂ« unike nĂ« intervalin [0, nbTiles]. Zero tregon katrorin bosh;
  • blankPos — pozita e katrorit bosh.

Logjika e lojës

Duhet të përcaktojmë një metodë reset që përdoret për inicializimin e një pozite të re të lojës. Kështu vendosim vlerën për çdo element të array-t të pesëmbëdhjetëshes. Pastaj, vendosim blankPos në pozitat e fundit të array-t.

Na nevojitet gjithashtu një metodë shuffle për të përzier array-n e pesëmbëdhjetëshes. Ne nuk e përfshimë pesëmbëdhjetëshën bosh në procesin e përzierjes për ta mbajtur atë në vendin e saj të mëparshëm.

Duke vetëm gjysma e pozita të mundshme fillestare të enigmës ka një zgjidhje, është e nevojshme të kontrolloni rezultatin e arritur nga përzierja, për t'u siguruar që rregullimi aktual është në të vërtetë i zgjidhshëm. Për ta bërë këtë, ne përcaktojmë metodën isSolvable.

Nëse një dytë para një tjetër me vlerë më të lartë, kjo konsiderohet një inversim. Kur dytë e zbrazët ndodhet në vendin e saj, numri i inversimeve duhet të jetë çift për të qenë enigmatike e zgjidhshme. Kështu, ne numërojmë inversimet dhe kthejmë true nëse numri është çift.

Më pas, është e rëndësishme të përcaktojmë metodën isSolved për të kontrolluar nëse rregullimi ynë i Game Of Fifteen është zgjidhur. Fillimisht, shikojmë se ku ndodhet dytë e zbrazët. Nëse është në pozitat fillestare, atëherë rregullimi aktual është i ri, i pa zgjidhur më parë. Më pas, ne kalojmë nëpër pllakat në rendin e kundërt, dhe nëse vlera e dytës ndryshon nga indeksi përkatës +1, kthejmë false. Përndryshe, në fund të metodës është koha për të kthyer true, sepse enigmatika ka përfunduar.

Një metodë tjetër që duhet të përcaktohet është newGame. Kjo është e nevojshme për të krijuar një ekzemplar të ri të lojës. Për këtë, ne rimarrim tabelën e lojës, pastaj e përzieni dhe vazhdojmë derisa pozita e lojës të jetë e zgjidhshme.

Ja një shembull kodi me logjikën kryesore të lojës së pesë të ndara:

private void newGame() {
  do {
    reset(); // reset në gjendje fillestare
    shuffle(); // përzieni
  } while(!isSolvable()); // bëjeni deri sa grila 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ë paraprihet nga një pllakë me vlerë më të lartë, ajo numëron
// si një inversim. Në rastin tonë, me pllakën bosh në poziten e zgjidhur,
// numri i inversimeve 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 pllakë bosh nuk është në poziton e zgjidhur => jo e zgjidhur
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Së fundi, duhet të programojmë lëvizjen e flakave në një array. Ky kod do të thirret më vonë nëpërmjet një skedari kthyes (callback) për të reaguar ndaj lëvizjes së kursorit. Loja jonë do të mbështesë disa lëvizje të pllakave në të njëjtën kohë. Pra, pasi ta kemi konvertuar pozitat e prekura në ekran në flakë, ne marrim pozitat e flakës së zbrazët dhe kërkojmë drejtimin e lëvizjes për të suportuar disa lëvizje të saj në të njëjtën kohë.

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 në Swing API

Ka ardhur koha për të punuar mbi ndërfaqen. Fillimisht marrim klasën Jpanel. Pastaj vizatojmë flakët në fushë - për të llogaritur masat e secilës do të përdorim të dhënat e vendosura në parametrin e konstruktorit të lojës:

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

Margin është gjithashtu një parameter i vendosur në konstruktorin e lojës.

Tani duhet të përcaktojmë metodën drawGrid për të vizatuar gridin dhe flakët në ekran. Analizojmë arrayn e flakëve dhe konvertojmë koordinatat në koordinata të përdoruesit. Pastaj vizatojmë çdo flakë me numrin përkatës në qendër:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // ne konvertojmë koordinatat 1D në koordinatat 2D duke pasur parasysh madhësinë e Array-t 2D
    int r = i / size;
    int c = i % size;
    // ne konvertojmë 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);
  }
}

Së fundi, do të ritaksojmë metodën paintComponent, e cila është një metodë e klasës JPane. Pastaj do të përdorim metodën drawGrid, dhe më pas metodën drawStartMessage për të treguar mesazhin që kërkon të klikoni për të nisur lojën:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Klikoni për të filluar një lojë të re";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // qendroni fjalinë s për kutinë 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 të vazhdojë normalisht, nevojitet të trajtohen veprimet e përdoruesit në UI. Për këtë, shtojmë implementimin e MouseListener në JPanel dhe kodin për lëvizjen e pjesëve, që është treguar më sipër:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // përdoret për të lejuar përdoruesit të ndërveprojnë me grid-in duke klikuar
    // është koha të implementohet ndërveprimi me përdoruesit për të zhvendosur pllakat për të zgjidhur lojën!
    if (gameOver) {
      newGame();
    } else {
      // merr pozitën e klikimit
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // klikim në grid?
      if (ex  gridSize || ey  gridSize)
        return;
 
      // merr pozitën 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;
 
      // konvertojmë në koordinatën 1D
      int clickPos = r1 * size + c1;
 
      int dir = 0;
 
      // kërkojmë drejtimin për lëvizje të shumta pllaka njëherësh
      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ë drejtim
        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();
    }
 
    // ripikturojmë panelin
    repaint();
  }
});

Kodi vendoset në konstruktorin e klasës GameOfFifteen. Në fund 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ë elementët e kodit së bashku. Ja çfarë do të dalë:

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 po ne nje Loje të 15 Pjesëve me Java 8 dhe Swing
// Nëse keni ndonjë pyetje, mos ngurroni të lexoni komentet ;)
public class GameOfFifteen extends JPanel { // gridi ynë do të tërhiqet në një Panel të dedikuar
 
  // Madhësia e instancës sonë të Lojës së Pesëmbëdhjetë
  private int size;
  // Numri i pllaka
  private int nbTiles;
  // Dimensioni i Grid UI
  private int dimension;
  // Ngjyra e Parë
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // ne përdorim ngjyrë arbitrare
  // Objekti Rastësor për të përzier pllakat
  private static final Random RANDOM = new Random();
  // Ruajtja e pllakat në një Array 1D të integerëve
  private int[] tiles;
  // Madhësia e pllakës në UI
  private int tileSize;
  // Pozita e pllakatës bosh
  private int blankPos;
  // Marzhi për griden në kornizë
  private int margin;
  // Madhësia e Grid UI
  private int gridSize;
  private boolean gameOver; // e vërtetë nëse loja ka përfunduar, e falso përndryshe
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // inicializimi i pllakat
    nbTiles = size * size - 1; // -1 sepse nuk e llogarisim pllakatën bosh
    tiles = new int[size * size];
    
    // llogarit madhësinë e grid dhe madhësinë e pllakës
    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ë me gridin 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 pozitën e klikimit
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // klikuar në grid?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // merr pozitën në grid
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // merr pozitën e qelisë bosh
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // ne konvertojmë në koordinata 1D
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // ne kërkojmë drejtimin për lëvizjet e shumë pllakatave njëherësh
          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 e ripikturojmë panelin
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // reset në gjendje fillestare
      shuffle(); // përziej
    } while(!isSolvable()); // bëje derisa grid 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 permutations së puzzle janë të zgjidhshme.
  // Sa herë që një pllakë është e paraprirë nga një pllakë me vlerë më të lartë, ajo numëron
  // si një invers. Në rastin tonë, me pllakatën bosh në pozita të zgjidhura,
  // numri i inversioneve duhet të jetë i barabartë për që puzzle 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 pllaka bosh nuk është në pozita të zgjidhur ==> nuk është 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);
    });
  }
 
 
}

Finalmente, luajmë!

ËshtĂ« koha pĂ«r tĂ« nisur lojĂ«n dhe ta shohim atĂ« nĂ« veprim. Fusha duhet tĂ« duket si nĂ« vijim:

«Pjatanca» nĂ« Java — si tĂ« zhvillosh njĂ« lojĂ« tĂ« plotĂ«

Po përpiqemi të zgjidhim enigmën. Nëse gjithçka shkoi mirë, këtu është ajo që marrim:

«Pjatanca» nĂ« Java — si tĂ« zhvillosh njĂ« lojĂ« tĂ« plotĂ«

KĂ«tu Ă«shtĂ« gjithçka. A prisnit mĂ« shumĂ«? 🙂

Skillbox rekomandon:

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster