2048小游戏的java实现

发布于 2021-07-31  38 次阅读


做这个主要是为了熟悉一下java的图形界面编程。废话不多说了,先看看效果。

2048

下面直接上代码,说明见注释,懒得单独提出来了,有兴趣的小伙伴可以根据自己的喜好改一改数据。

import java.awt.*;
import java.awt.event.*;
import java.util.Random;
import javax.swing.*;

public class Game2048 extends JPanel {

  //枚举游戏的所有状态
  enum State {
    start, won, running, over
  }

  //数字合成之后的变色存储
  final Color[] colorTable = {
      new Color(0x701710), new Color(0xFFE4C3), new Color(0xfff4d3),
      new Color(0xffdac3), new Color(0xe7b08e), new Color(0xe7bf8e),
      new Color(0xffc4c3), new Color(0xE7948e), new Color(0xbe7e56),
      new Color(0xbe5e56), new Color(0x9c3931), new Color(0x701710)};

  final static int target = 2048;

  static int highest;
  static int score;

  private Color gridColor = new Color(0xBBADA0);//格子边框线条颜色及开始主页面标题颜色
  private Color emptyColor = new Color(0xCDC1B4);//游戏中的空格颜色
  private Color startColor = new Color(0x8C8C8C);//开始主页面游戏背景颜色

  private Random rand = new Random();//new一个随机对象

  private Tile[][] tiles;
  private int side = 4;//游戏界面大小,这里为4*4
  private State gamestate = State.start;//将游戏状态置为开始
  private boolean checkingAvailableMoves;//默认为false

  public Game2048() {
    setPreferredSize(new Dimension(900, 700));//设置窗口大小
    setBackground(new Color(0xFAF8EF));//设置窗口背景色
    setFont(new Font("SansSerif", Font.BOLD,48));//设置游戏界面字体"SansSerif",粗体,48号
    setFocusable(true);//设置游戏可聚焦,若不设置,无法通过键盘操作,默认值为false

    //添加鼠标监听器
    addMouseListener(new MouseAdapter() {
      @Override
      public void mousePressed(MouseEvent e) {
        startGame();//鼠标点击便开始游戏
        repaint();//重新绘制组件
      }
    });

    //添加键盘监听器
    addKeyListener(new KeyAdapter() {
      @Override
      public void keyPressed(KeyEvent e) {//键盘上下左右按键操作游戏
        switch (e.getKeyCode()) {
          case KeyEvent.VK_UP:
            moveUp();
            break;
          case KeyEvent.VK_DOWN:
            moveDown();
            break;
          case KeyEvent.VK_LEFT:
            moveLeft();
            break;
          case KeyEvent.VK_RIGHT:
            moveRight();
            break;
        }
        repaint();
      }
    });
  }

  @Override
  public void paintComponent(Graphics gg) {
    super.paintComponent(gg);
    Graphics2D g = (Graphics2D) gg;
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON);
    drawGrid(g);
  }

  void startGame() {
    //如果游戏状态不是running,就重置分数与最高分并将游戏状态置为running,同时重置游戏画布,并随机生成两个数
    if (gamestate != State.running) {
      score = 0;
      highest = 0;
      gamestate = State.running;
      tiles = new Tile[side][side];
      addRandomTile();
      addRandomTile();
    }
  }

  //画格子方法
  void drawGrid(Graphics2D g) {
    g.setColor(gridColor);
    g.fillRoundRect(200, 100, 499, 499, 15, 15);

    if (gamestate == State.running) {

      for (int r = 0; r < side; r++) {
        for (int c = 0; c < side; c++) {
          //如果格子内没有数字,就画空格子
          if (tiles[r][c] == null) {
            g.setColor(emptyColor);
            g.fillRoundRect(215 + c * 121, 115 + r * 121, 106, 106, 7, 7);
          } else {//否则画数字
            drawTile(g, r, c);
          }
        }
      }
    } else {//如果游戏状态不是running,就重置所有
      g.setColor(startColor);
      g.fillRoundRect(215, 115, 469, 469, 7, 7);

      g.setColor(gridColor.darker());
      g.setFont(new Font("SansSerif", Font.BOLD, 128));
      g.drawString("2048", 310, 270);//这里是游戏开始界面的文字及其位置

      g.setFont(new Font("SansSerif", Font.BOLD, 20));

      //根据游戏状态显示相应文字
      if (gamestate == State.won) {
        g.drawString("you made it!", 390, 350);

      } else if (gamestate == State.over)
        g.drawString("game over", 400, 350);

      g.setColor(gridColor);
      g.drawString("click to start a new game", 330, 470);
      g.drawString("(use arrow keys to move tiles)", 310, 530);
    }
  }

  //画内容(包含数字的格子上层的容器)方法
    void drawTile(Graphics2D g, int r, int c) {
    int value = tiles[r][c].getValue();//获取要画的容器中的值

    g.setColor(colorTable[(int) (Math.log(value) / Math.log(2)) + 1]);//根据数值选取相应颜色
    g.fillRoundRect(215 + c * 121, 115 + r * 121, 106, 106, 7, 7);
    String s = String.valueOf(value);//将数字转为字符串

    g.setColor(value < 128 ? colorTable[0] : colorTable[1]);

    FontMetrics fm = g.getFontMetrics();
    int asc = fm.getAscent();
    int dec = fm.getDescent();

    //获取要画的容器的中心位置即要画的数字的位置
    int x = 215 + c * 121 + (106 - fm.stringWidth(s)) / 2;
    int y = 115 + r * 121 + (asc + (106 - (asc + dec)) / 2);

    g.drawString(s, x, y);//画出数字
  }


  //添加随机数字
  private void addRandomTile() {
    int pos = rand.nextInt(side * side);
    int row, col;
    //找到一个为null的位置
    do {
      pos = (pos + 1) % (side * side);
      row = pos / side;
      col = pos % side;
    } while (tiles[row][col] != null);

    int val = rand.nextInt(10) == 0 ? 4 : 2;//随机出现的新数字是4或2, 4的概率是0.1, 2的概率是0.9
    tiles[row][col] = new Tile(val);//将随机产生的新数字放到找到的null位置中
  }

  private boolean move(int countDownFrom, int yIncr, int xIncr) {
    //每次开始重置moved为false
    boolean moved = false;

    for (int i = 0; i < side * side; i++) {
      int j = Math.abs(countDownFrom - i);

      int r = j / side;
      int c = j % side;

      if (tiles[r][c] == null)
        continue;

      int nextR = r + yIncr;
      int nextC = c + xIncr;

      while (nextR >= 0 && nextR < side && nextC >= 0 && nextC < side) {

        Tile next = tiles[nextR][nextC];
        Tile curr = tiles[r][c];

        if (next == null) {

          if (checkingAvailableMoves)//因为默认为false,这条语句一开始不会执行
            return true;

          tiles[nextR][nextC] = curr;
          tiles[r][c] = null;
          r = nextR;
          c = nextC;
          nextR += yIncr;
          nextC += xIncr;
          moved = true;//成功移动一次后, moved变为true

        } else if (next.canMergeWith(curr)) {

          if (checkingAvailableMoves)//因为默认为false,这条语句一开始不会执行
            return true;

          int value = next.mergeWith(curr);
          if (value > highest)
            highest = value;
          score += value;
          tiles[r][c] = null;
          moved = true;//成功移动一次后, moved变为true
          break;
        } else
          break;
      }
    }

    //每次成功移动一次后要进行的操作
    if (moved) {
      if (highest < target) {//最高分小于2048
        clearMerged();
        addRandomTile();//每次合并完就产生下一个新的随机数
        if (!movesAvailable()) {//判断游戏结束
          gamestate = State.over;
        }
      } else if (highest == target)//最高分等于2048,游戏胜利
        gamestate = State.won;
    }

    return moved;
  }

  boolean moveUp() {
    return move(0, -1, 0);
  }

  boolean moveDown() {
    return move(side * side - 1, 1, 0);
  }

  boolean moveLeft() {
    return move(0, 0, -1);
  }

  boolean moveRight() {
    return move(side * side - 1, 0, 1);
  }

  void clearMerged() {
    for (Tile[] row : tiles)
      for (Tile tile : row)
        if (tile != null)
          tile.setMerged(false);
  }

  //游戏结束的判定
  boolean movesAvailable() {
    checkingAvailableMoves = true;//
    boolean hasMoves = moveUp() || moveDown() || moveLeft() || moveRight();
    checkingAvailableMoves = false;
    return hasMoves;
  }

  //游戏入口
  public static void main(String[] args) {
    /*从任何线程中调用invokeLater来请求事件分发线程以运行某段代码,你必须将这段代码放入一个Runnable对象的run方法中,
    并将该指定Runnable对象作为参数传递给invokeLater。 invokeLater函数会立即返回,不会等到事件分发线程执行完这段代码。*/
    SwingUtilities.invokeLater(() -> {
      JFrame f = new JFrame();//new一个窗口对象
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//实现鼠标点击右上角×结束游戏
      f.setTitle("2048啦啦啦");//窗口左上角的字
      f.setResizable(true);//实现窗口可变尺寸 (默认为true)
      f.add(new Game2048(), BorderLayout.CENTER);//参数comp - 要添加的组件constraints - 表示此组件的布局约束的对象
      f.pack();//窗口大小自适应
      f.setLocationRelativeTo(null);//窗口位置,为null默认屏幕中间
      f.setVisible(true);//显示窗口,默认为false
    });
  }
}

//Tile类
class Tile {
  private boolean merged;
  private int value;

  Tile(int val) {
    value = val;
  }

  int getValue() {
    return value;
  }

  void setMerged(boolean m) {
    merged = m;
  }

  boolean canMergeWith(Tile other) {
    return !merged && other != null && !other.merged && value == other.getValue();
  }

  int mergeWith(Tile other) {
    if (canMergeWith(other)) {
      value *= 2;
      merged = true;
      return value;
    }
    return -1;
  }
}