"Fifteen" på Java - hvordan man udvikler et fuldgyldigt spil

"Fifteen" på Java - hvordan man udvikler et fuldgyldigt spil

«Femten», eller «Fifteen» — et glimrende eksempel på et simpelt logisk spil, der er populært over hele verden. For at løse puslespillet skal man arrangere firkanterne med tal i rækkefølge fra lavest til højest. Det er ikke let, men det er interessant.

I dagens tutorial viser vi, hvordan man udvikler «Fifteen» i Java 8 med Eclipse. Vi vil bruge Swing API til at udvikle UI.

Vi minder om: til alle læsere af 'Habr' — rabat på 10.000 rubler ved tilmelding til et hvilket som helst Skillbox-kursus med rabatkoden 'Habr'.

Skillbox anbefaler: Online uddannelseskursus «Java-udvikler erhverv».

Spildesign

I dette trin skal vi definere egenskaberne:

  • Size — størrelsen på spillefeltet;
  • nbTiles — antallet af femtens i feltet. nbTiles = size * size — 1;
  • Tiles — femten, der er et endimensionelt array af heltal. Hver af femtens vil få en unik værdi i intervallet [0, nbTiles]. Nul angiver det tomme felt;
  • blankPos — positionen for det tomme felt.

Spillets logik

Det er nødvendigt at definere en reset-metode, der bruges til at initialisere en ny spilleposition. Her sætter vi værdien for hvert element i femten-arrayet. Derefter placerer vi blankPos i den sidste position i arrayet.

Derudover er der brug for en shuffle-metode til at blande femten-arrayet. Vi inkluderer ikke den tomme femten i blandingsprocessen for at holde den i sin nuværende position.

Da kun halvdelen af de mulige startpositioner for puslespillet er løsbare, skal vi kontrollere det resulterende blandingsresultat for at sikre, at den aktuelle opstilling overhovedet er løsbar. For at gøre dette definerer vi en isSolvable-metode.

Hvis en bestemt femten har en femten med en højere værdi foran sig, betragtes det som en inversion. Når den tomme femten er på sin plads, skal antallet af inversioner være lige for at puslespillet kan være løsbar. Så vi tæller antallet af inversioner og returnerer true, hvis tallet er lige.

Derefter er det vigtigt at definere en isSolved-metode for at kontrollere, om vores opstilling af Game Of Fifteen er løst. Først ser vi, hvor den tomme femten er. Hvis den er i startpositionen, er den aktuelle opstilling ny og ikke tidligere løst. derefter gennemgår vi fliserne i omvendt rækkefølge, og hvis værdien af femten er forskellig fra den tilsvarende indeks +1, returnerer vi false. Ellers, i slutningen af metoden, er det tid til at returnere true, fordi puslespillet allerede er løst.

Endnu en metode, der skal defineres, er newGame. Den er nødvendig for at oprette et nyt spilindhold. Til dette nulstiller vi spillefeltet, derefter blander vi det og fortsætter, indtil spilpositionen er løsbar.

Her er et kodeeksempel med nøglelogikken for femt eller titern:

private void newGame() {
  do {
    reset(); // nulstille til starten
    shuffle(); // blande
  } while(!isSolvable()); // fortsæt indtil rutinen er løsbar
 
  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;
  }
}
 
// Kun halvdelen af permutationerne af puslespillet er løsbare/
// Når en flise efterfølges af en flise med højere værdi, tæller det
// som en inversion. I vores tilfælde, med den blanke flise i den løste position,
// skal antallet af inversioner være lige for at puslespillet kan være løsbar
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) // hvis den blanke flise ikke er i den løste position => ikke løst
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Endelig skal bevægelsen af femt eller titern programmeres i arrayet. Denne kode vil blive kaldt senere via en callback for at reagere på cursorbevægelser. Vores spil vil understøtte flere placeringer af fliser samtidig. Så efter at have konverteret den trykkede position på skærmen til en femt eller titern, finder vi positionen af den blanke flise og søger efter bevægelsesretningen for at understøtte flere bevægelser samtidigt.

Her er et kodeeksempel:

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

Vi udvikler UI med Swing API

Det er tid til at arbejde på brugerfladen. Først tager vi klassen Jpanel. Derefter tegner vi fliserne på feltet - for at beregne størrelsen af hver vil vi bruge data givet i konstructorens parametre:

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

Margin er også en parameter givet i konstructoren.

Nu skal metoden drawGrid defineres for at tegne rutenettet og fliserne på skærmen. Vi analyserer arrayet af fliser og konverterer koordinaterne til UI-koordinater. Derefter tegner vi hver flise med det relevante nummer i midten:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // vi konverterer 1D koordinater til 2D koordinater givet størrelsen på 2D Array
    int r = i / size;
    int c = i % size;
    // vi konverterer til koordinater i UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // tjekker specialtilfælde for blank flise
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // for andre fliser
    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);
  }
}

Endelig skal vi redefinere metoden paintComponent, som er en afledt metode fra klassen JPane. Derefter bruger vi metoden drawGrid, og efterfølgende — metoden drawStartMessage til at vise en besked, der opfordrer til at klikke for at starte spillet:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Klik for at starte et nyt spil";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centrerer strengen s til den givne flise (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);
}

Reagerer på brugerhandlinger i UI

For at sikre at spillet fortsætter, er det nødvendigt at håndtere brugerhandlinger i UI. Derfor tilføjer vi implementeringen af MouseListener på JPanel og koden til at flytte fliserne, som allerede er vist ovenfor:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // bruges til at lade brugerne interagere med rutenet ved at klikke
    // det er tid til at implementere interaktion med brugerne for at flytte fliser for at løse spillet!
    if (gameOver) {
      newGame();
    } else {
      // få positionen for klikket
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // klikker i rutenettet?
      if (ex  gridSize  || ey  gridSize)
        return;

      // få position i rutenettet
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // få positionen af den tomme celle
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // vi konverterer til 1D koordinat
      int clickPos = r1 * size + c1;

      int dir = 0;

      // vi søger retning for flere flisemoves ad gangen
      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) {
        // vi flytter fliser i retningen
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // vi tjekker om spillet er løst
      gameOver = isSolved();
    }

    // vi maler panelet igen
    repaint();
  }
});

Koden placeres i klassen GameOfFifteen's konstruktor. Til sidst kalder vi metoden newGame for at starte et nyt spil.

Den fulde kode til spillet

Det sidste skridt, før du ser spillet i aktion, er at samle alle kodeelementerne sammen. Her er hvad der kommer ud:

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;
 
// Vi skal lave et 15-spils spil med Java 8 og Swing
// Hvis du har spørgsmål, er du velkommen til at læse kommentarerne ;)
public class GameOfFifteen extends JPanel { // vores gitter vil blive tegnet i en dedikeret panel
 
  // Størrelsen af vores Game of Fifteen instans
  private int size;
  // Antal fliser
  private int nbTiles;
  // Grid UI Dimension
  private int dimension;
  // Forgrundsfarve
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // vi bruger en vilkårlig farve
  // Random objekt til at blande fliser
  private static final Random RANDOM = new Random();
  // Opbevaring af fliser i et 1D array af heltal
  private int[] tiles;
  // Størrelse af fliser i UI
  private int tileSize;
  // Position af den blanke flise
  private int blankPos;
  // Margin for gitteret på rammen
  private int margin;
  // Grid UI Størrelse
  private int gridSize;
  private boolean gameOver; // sandt hvis spillet er slut, falsk ellers
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // init fliser
    nbTiles = size * size - 1; // -1 fordi vi ikke tæller den blanke flise
    tiles = new int[size * size];
    
    // beregn gitterstørrelse og flisestørrelse
    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) {
        // bruges til at lade brugerne interagere med gitteret ved at klikke
        // det er tid til at implementere interaktionen med brugerne for at flytte fliserne og løse spillet!
        if (gameOver) {
          newGame();
        } else {
          // få positionen af klikket
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // klik i gitteret ?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // få positionen i gitteret
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // få positionen af den blanke celle
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // vi konverterer til 1D koordinater
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // vi søger retning for flere flisebevægelser på én gang
          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) {
            // vi flytter fliserne i retningen
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // vi tjekker om spillet er løst
          gameOver = isSolved();
        }
        
        // vi maler panelet igen
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // nulstil til initial tilstand
      shuffle(); // bland
    } while(!isSolvable()); // gør det, indtil gitteret er løseligt
    
    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;
    }
  }
 
  // Kun halvdelen af permutationerne af puslespillet er løselige.
  // Hver gang en flise er forudgået af en flise med højere værdi, tæller det
  // som en inversion. I vores tilfælde, med den blanke flise i løst position,
  // skal antallet af inversioner være lige for puslespillet at være løseligt
  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) // hvis den blanke flise ikke er i den løste position => ikke løst
      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("Game of Fifteen");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // center på skærmen
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Endelig, lad os spille!

Det er tid til at starte spillet og se det i aktion. Spillefeltet skal se således ud:

"Fifteen" på Java - hvordan man udvikler et fuldgyldigt spil

Lad os forsøge at løse puzzlen. Hvis alt gik som planlagt, får vi dette:

"Fifteen" på Java - hvordan man udvikler et fuldgyldigt spil

Det var det hele. Forventede du mere? 🙂

Skillbox anbefaler:

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster