«KĂŒmme» Java-s — kuidas arendada tĂ€isvÀÀrtuslikku mĂ€ngu

«KĂŒmme» Java-s — kuidas arendada tĂ€isvÀÀrtuslikku mĂ€ngu

«Viisakus» vĂ”i «Viiekas» on suurepĂ€rane nĂ€ide lihtsast loogika mĂ€ngust, mis on populaarsuselt ĂŒle kogu maailma. Probleemi lahendamiseks tuleb numbrikastid paigutada jĂ€rjestikku, alates vĂ€iksemast kuni suuremani. See ei ole lihtne, aga huvitav.

KĂ€esolevas Ă”petuses nĂ€itame, kuidas luua «Viiekas» Java 8-ga Eclipse’is. Kasutame kasutajaliidese arendamiseks Swing API-d.

Tuletame meelde: kĂ”igile «Habra» lugejatele — 10 000 rubla soodustus, kui registreerite end Skillboxi mis tahes kursusele promokoodi «Habr» abil.

Skillbox soovitab: Hariduslik veebikursus „Java-arendaja amet“.

MĂ€ngu disainimine

Selles etapis tuleb mÀÀratleda omadused:

  • Size — mĂ€nguvĂ€lja suurus;
  • nbTiles — mĂ€ngunuppude arv mĂ€nguvĂ€ljal. nbTiles = size*size — 1;
  • Tiles — nupp, mis kujutab endast ĂŒhemÔÔtmelist tĂ€isarvude massiivi. Iga nupp saab ainulaadse vÀÀrtuse vahemikus [0, nbTiles]. Null tĂ€histab tĂŒhja kasti;
  • blankPos — tĂŒhja kasti positsioon.

MĂ€ngu loogika

Tuleb mÀÀratleda nullimismeetod (reset), mida kasutatakse uue mÀngupositsiooni algatamiseks. Nii paneme vÀÀrtuse igasse mÀngunupu massiivi elemendisse. Siis paigutame blankPos massiivi viimasesse positsiooni.

Samuti on vajalik shuffle meetod mĂ€ngunuppude massiivi segamiseks. Me ei kaasata tĂŒhja nuppu segamisse, et hoida see endises positsioonis.

Kuna vaid pool vĂ”imalikest mĂ”istatuste algpositsioonidest on lahendatavad, tuleb segunemise tulemus kontrollida, et veenduda, kas praegune asetuse olukord on ĂŒleĂŒldse lahendatav. Selleks mÀÀratleme meetodi isSolvable.

Kui konkreetse viiese enne on viies, mille vÀÀrtus on suurem, loetakse seda inversiooniks. Kui tĂŒhi viies on oma kohas, peab inversioonide arv olema paarisarv, et mĂ”istatus oleks lahendatav. Seega loendame inversioonide arvu ja tagastame true, kui arv on paarisarv.

SeejĂ€rel on oluline mÀÀratleda meetod isSolved, et kontrollida, kas meie mĂ€ngu Game Of Fifteen asetuse olek on lahendatud. Esiteks vaatame, kus asub tĂŒhi viies. Kui algasendis, siis on praegune asetuse olek uus, varem lahendamata. SeejĂ€rel lĂ€bime plaadid vastupidises jĂ€rjestuses ja kui viie vÀÀrtus erineb vastava indeksi +1-st, tagastame false. Kui mitte, on lĂ”puks meetodis aeg tagastada true, sest mĂ”istatus on juba lahendatud.

Teine meetod, mille mÀÀratlemine on vajalik, on newGame. See on vajalik uue mÀngu eksemplari loomiseks. Selleks lÀhtestame mÀnguvÀlja, seejÀrel segame selle ja jÀtkame, kuni mÀngupositsioon on lahendatav.

Siin on nÀide koodist, kus on peamine loogika viiekesi mÀngus:

private void newGame() {
  do {
    reset(); // lÀhtesta algses olekus
    shuffle(); // sega
  } while(!isSolvable()); // tee, kuni ruudustik on lahendatav
 
  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;
  }
}
 
// Ainult pool pĂŒsivast mĂ€ngust on lahendatavad/
// Iga kord, kui plaat eelneb kÔrgema vÀÀrtusega plaadile, loetakse see
// pöördeks. Meie juhul, kui tĂŒhja plaat on lahendatud positsioonis,
// peab pöördepunkte olema paarisarv, et mÀng oleks lahendatav
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) // kui tĂŒhja plaat ei ole lahendatud positsioonis ==>
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

LĂ”puks tuleb programmeerida klotside liikumine massiivis. Seda koodi kutsutakse hiljem vĂ€lja tagasikutsumise (callback) teel, et reageerida hiirekursori liikumisele. Meie mĂ€ng toetab mitme klotsi liikumist samaaegselt. Seega, pĂ€rast seda, kui oleme muundanud ekraanil vajutatud positsiooni klotsiks, saame tĂŒhja klotsi positsiooni ja otsime liikumissuunda, et toetada selle korraga mitut liikumist.

Siin on koodi nÀide:

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

Arendame UI-d Swing API abil

On aeg tegeleda liidese kujundamisega. Esiteks vĂ”etakse klass Jpanel. SeejĂ€rel joonistame klotse mĂ€nguvĂ€ljakule — iga klotsi mÔÔtmete arvutamiseks kasutame mĂ€ngu konstruktori parameetrites mÀÀratletud andmeid:

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

Margin on samuti parameeter, mis on mÀÀratud mÀngu konstruktori kaudu.

NĂŒĂŒd tuleb mÀÀrata meetod drawGrid, et joonistada vĂ”rk ja klotsid ekraanile. AnalĂŒĂŒsime klotside massiivi ja muundame koordinaadid kasutajaliidese koordinaatideks. SeejĂ€rel joonistame iga klotsi keskmesse vastava numbri:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // me konverteerime 1D koordinadid 2D koordinatideks vastavalt 2D massiivi suurusele
    int r = i / size;
    int c = i % size;
    // me konverteerime sisendkoordinadid UI-s
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // kontrollime erijuhtumit tĂŒhja plaadi jaoks
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // teiste plaatide jaoks
    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);
  }
}

LĂ”puks ĂŒle kirjutame paintComponent meetodi, mis pĂ€rineb JPane klassist. SeejĂ€rel kasutame meetodit drawGrid ja seejĂ€rel meetodit drawStartMessage, et kuvada sĂ”num, mis kutsub klikkima mĂ€ngu alustamiseks:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Kliki, et alustada uut mÀngu";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // keskenda string s antud ruudule (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);
}

Kasutaja tegevusele reageerimine kasutajaliideses

Kuna mĂ€ng peab sujuvalt kulgema, on vajalik kasutaja tegevuste töötlemine kasutajaliideses. Selleks lisame Jpanelile MouseListener'i ja koodi viie tĂŒkki liigutamiseks, nagu on juba ĂŒlal nĂ€idatud:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // kasulik kasutaja liikumiseks ruudustikul hiireklikkide kaudu
    // on aeg rakendada kasutajate interaktsiooni, et liikuda plaatide vahel mÀngu lahendamiseks!
    if (gameOver) {
      newGame();
    } else {
      // vÔtame kliki positsiooni
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // klikk ruudustikus?
      if (ex  gridSize || ey  gridSize)
        return;
 
      // vÔtame positsiooni ruudustikus
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;
 
      // vĂ”tame tĂŒhja raku positsiooni
      int c2 = blankPos % size;
      int r2 = blankPos / size;
 
      // muundame 1D koordinaatideks
      int clickPos = r1 * size + c1;
 
      int dir = 0;
 
      // otsime suunda mitme plaadi liigutamiseks korraga
      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) {
        // liigume plaatidega suunas
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);
 
        tiles[blankPos] = 0;
      }
 
      // kontrollime, kas mÀng on lahendatud
      gameOver = isSolved();
    }
 
    // uuesti joonistame paneeli
    repaint();
  }
});

Kood asetatakse klassi GameOfFifteen konstruktorisse. LÔpus kutsume vÀlja meetodi newGame, et alustada uut mÀngu.

MÀngu tÀiskood

Viimane samm enne mÀngu nÀgemist tegevuses on kÔik koodi elemendid kokku panna. Siin on, mida saate:

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;
 
// Loome 15 Pusle MĂ€ngu Java 8 ja Swingiga
// KĂŒsimuste korral lugege julgelt kommentaare ;)
public class GameOfFifteen extends JPanel { // meie ruudustik joonistatakse spetsiaalses paneelis
 
  // Meie 15 mÀngu suurus
  private int size;
  // Plaatide arv
  private int nbTiles;
  // Ruudustiku UI mÔÔtmed
  private int dimension;
  // Esimene vÀrv
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // kasutame erivÀrvi
  // Plaatide segamiseks mÔeldud juhuslik objekt
  private static final Random RANDOM = new Random();
  // Plaatide salvestamine 1D tÀisarvude massiivi
  private int[] tiles;
  // Plaadi suurus UI-s
  private int tileSize;
  // TĂŒhja plaadi asukoht
  private int blankPos;
  // Ruudustiku serv
  private int margin;
  // Ruudustiku UI suurus
  private int gridSize;
  private boolean gameOver; // tÔene, kui mÀng on lÀbi, vale vastupidiselt
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // initsialiseerime plaadid
    nbTiles = size * size - 1; // -1, sest me ei arvesta tĂŒhja plaati
    tiles = new int[size * size];
    
    // arvutame ruudustiku ja plaadi suuruse
    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) {
        // kasutatakse, et lasta kasutajatel ruudustikku interakteeruda hiireklÔpsudega
        // on aeg rakendada kasutajatega interaktsiooni, et liikuda plaatidega mÀngu lahendamiseks!
        if (gameOver) {
          newGame();
        } else {
          // saame klÔpsuse asukoha
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // klÔps ruudustikus?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // saame asukoha ruudustikus
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // saame tĂŒhja ruudu asukoha
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // konverteerime 1D koordinaatides
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // otsime suunda mitme plaadi liigutamiseks korraga
          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) {
            // liigutame plaate suunas
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // kontrollime, kas mÀng on lahendatud
          gameOver = isSolved();
        }
        
        // joonistame paneeli uuesti
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // lÀhtestame algolekusse
      shuffle(); // segame
    } while(!isSolvable()); // teeme seda seni, kuni ruudustik on lahendatav
    
    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;
    }
  }
 
  // Ainult pooled pusle permutatsioonid on lahendatavad.
  // Iga kord, kui plaat on eelneva suurema vÀÀrtusega plaadi ees, loetakse see
  // tagasi pöördumisel. Meie juhul, kui tĂŒhja plaat on lahendatud positsioonis,
  // peavad inversioonide arv olema paaris, et pusle oleks lahendatav
  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) // kui tĂŒhja plaat ei ole lahendatud positsioonis == false
      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("15 Pusle MĂ€ng");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // keskele ekraanile
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

LÔpuks saame mÀngida!

NĂŒĂŒd on Ă”ige aeg mĂ€ng kĂ€ivitada ja vaadata, kuidas see toimib. MĂ€nguvĂ€li peaks vĂ€lja nĂ€gema jĂ€rgmiselt:

«KĂŒmme» Java-s — kuidas arendada tĂ€isvÀÀrtuslikku mĂ€ngu

Proovime mÔistatust lahendada. Kui kÔik lÀheb hÀsti, siis saame jÀrgmise tulemuse:

«KĂŒmme» Java-s — kuidas arendada tĂ€isvÀÀrtuslikku mĂ€ngu

Ja kĂ”ik! Kas te ootasite rohkem? 🙂

Skillbox soovitab:

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster