1. 程式人生 > 其它 >JAVA實現2048小遊戲

JAVA實現2048小遊戲

技術標籤:Javajava遊戲guiswing2408

目錄

一、效果

二、教程

三、程式碼


一、效果

2048小遊戲是一款比較流行的數字遊戲,遊戲規則如下:

每次可以選擇上下左右其中一個方向去滑動,每滑動一次,所有的數字方塊都會往滑動的方向靠攏外,系統也會在空白的地方亂數出現一個數字方塊,相同數字的方塊在靠攏、相撞時會相加。不斷的疊加最終拼湊出2048這個數字就算成功。

ps: 博主就沒有新增成功的圖片了,實在是因為技術不行,試完了幾次均沒有湊成 2048 ...

二、教程

1、使用IDEA搭建一個專案,專案名稱:Game2048_java(可根據自己的喜好)

具體搭建過程可看博文

用IDEA構建一個簡單的Java程式範例,這裡就不詳細說了。

2、Data.interface

(1)匯入包

import java.awt.Font;

(2)介面的定義

public interface Data {}

(3)資料型別的定義

  • title: Font型,視窗的標題字型型別和大小,即為 ‘2048’;
  • score:Font型,分數的字型型別和大小,即為‘得分’;
  • tips:Font型,說明文字的字型型別和大小,即為‘操作: ↑ ↓ ← →, 按esc鍵重新開始’;
  • font1: 表格中數字 2、4、8字型型別和大小
  • font2: 表格中數字 16、32、64字型型別和大小
  • font3: 表格中數字128、256、512
    字型型別和大小
  • font4: 表格中數字1024、2048字型型別和大小
  • CHART_GAP: int型,表格與表格之前的空隙距離;
  • CHART_ARC: int型,表格的弧度值;
  • CHART_SIZE: int型,表格的大小。
Font title = new Font("微軟雅黑", Font.BOLD, 50); 
Font score = new Font("微軟雅黑", Font.BOLD, 28); 
Font tips = new Font("宋體", Font.PLAIN, 20); 

Font font1 = new Font("宋體", Font.BOLD, 46);
Font font2 = new Font("宋體", Font.BOLD, 40);
Font font3 = new Font("宋體", Font.BOLD, 34);
Font font4 = new Font("宋體", Font.BOLD, 28);

int CHART_GAP = 10;
int CHART_ARC = 20;
int CHART_SIZE = 86;

3、Form.class

(1)匯入包

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.KeyListener;

(2)介面的實現

public class Form implements Data{}

(3)建構函式 —— 遊戲窗體的搭建

A.視窗設定

JFrame frame = new JFrame("Game 2048");
frame.setSize(400, 530); // width * height
frame.setResizable(false); // 視窗大小不可調整
frame.setVisible(true); // true視窗可見
frame.setLocationRelativeTo(null); // 視窗居中
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE); // 視窗的關閉
frame.setLayout(null); // 設定使用者介面上的螢幕元件的格式佈局,預設為流式佈局

B.新增標籤

  • 新增標題:2048
JLabel ltitle = new JLabel("2048", JLabel.CENTER);
frame.add(ltitle);
ltitle.setFont(Data.title); // 標題字型/大小/位置
ltitle.setForeground(Color.BLACK);
ltitle.setBounds(50, 0, 150, 60);
  • 新增分數:得分:0
JLabel lscorename = new JLabel("得 分", JLabel.CENTER);
frame.add(lscorename);
lscorename.setFont(Data.score); // "得分"字型/大小/位置
lscorename.setForeground(Color.WHITE);
lscorename.setOpaque(true); // 設定控制元件是否透明.true: 控制元件不透明; false: 控制元件透明.
lscorename.setBackground(Color.GRAY);
lscorename.setBounds(250, 0, 120, 30);

lscore = new JLabel("0", JLabel.CENTER);
frame.add(lscore);
lscore.setFont(Data.score); // "得分"字型/大小/位置
lscore.setForeground(Color.WHITE);
lscore.setOpaque(true); // 設定控制元件是否透明.true: 控制元件不透明; false: 控制元件透明.
lscore.setBackground(Color.GRAY);
lscore.setBounds(250, 30, 120, 30);
  • 遊戲說明
JLabel ltips = new JLabel("操作: ↑ ↓ ← →, 按esc鍵重新開始  ", JLabel.CENTER);
frame.add(ltips);
ltips.setFont(Data.tips); // "說明"字型/大小/位置
ltips.setForeground(Color.DARK_GRAY);
ltips.setBounds(0, 60, 400, 40);

C.遊戲面板

JPanel panel = new Game2048Panel();
frame.add(panel);
panel.setBounds(0, 100, 400, 400); // 面板繪製區域
panel.setBackground(Color.GRAY);
panel.setFocusable(true); //setFocusable設定元件是否可被選中
// FlowLayout(流式佈局): 元件按照加入的先後順序按照設定的對齊方式從左向右排列,一行排滿到下一行開始繼續排列
panel.setLayout(new FlowLayout());
frame.addKeyListener((KeyListener) panel);  // 鍵盤監聽

(4)主函式

public static void main(String[] args) {
        new Form();
    }

4、Chart.class —— 表格

(1)匯入包

import java.awt.Color;
import java.awt.Font;

(2)介面的實現

public class Chart implements Data{}

(3) 資料型別的定義

  • value: int型,表格中的數值

(4)基本方法

A. 建構函式

 public Chart(){
        clear();
    }

B. 清除面板

public void clear(){
        value = 0;
    }

C. 設定數字字型顏色

對應數字的字型顏色

  • 0: 0xcdc1b4, 同背景顏色相同,因此不會顯示
  • 2, 4: BLACK, 黑色
  • 其他: WHITE, 白色
public Color getForeground(){
        return switch (value) {
            case 0 -> new Color(0xcdc1b4);
            case 2, 4 -> Color.BLACK;
            default -> Color.WHITE;
        };
    }

D. 設定數字背景顏色

public Color getBackground(){
        return switch (value){
            case 0 -> new Color(0xcdc1b4);
            case 2 -> new Color(0xeee4da);
            case 4 -> new Color(0xede0c8);
            case 8 -> new Color(0xf2b179);
            case 16 -> new Color(0xf59563);
            case 32 -> new Color(0xf67c5f);
            case 64 -> new Color(0xf65e3b);
            case 128 -> new Color(0xedcf72);
            case 256 -> new Color(0xedcc61);
            case 512 -> new Color(0xedc850);
            case 1024 -> new Color(0xedc53f);
            case 2048 -> new Color(0xedc22e);
            default -> new Color(0x248c51);
        };
    }

E. 設定數字字型大小

public Font getChartFont(){
        return switch (value){
            case 0, 2, 4, 8 -> font1;
            case 16, 32, 64 -> font2;
            case 128, 256, 512 -> font3;
            default -> font4;
        };
    }

5、Game2048Panel —— 面板

(1)匯入包

import javax.swing.JPanel;

import java.awt.Graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.FontMetrics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

(2)類的繼承,介面的實現

  • JPanel類:面板元件,非頂層容器
  • KeyListener:鍵盤監聽介面
public class Game2048Panel extends JPanel implements Data, KeyListener {}

(3)資料型別的定義

  • scores: int 型,記錄遊戲得分
  • charts: Chart型,方格陣列
  • isadd: boolean型,是否新增數字

(4)構造方法

 public Game2048Panel() {
        initGame();  // 初始化
    }

(5)按鍵函式KeyPressed()

  • getKeyCode():鍵盤上每一個按鈕都有對應碼(Code),可用來查知使用者按了什麼鍵,返回當前按鈕的數值
  • getKeyChar():處理的是比較高層的事件,返回的是每欠敲擊鍵盤後得到的字元(中文輸入法下就是漢字)
  • getKeyText():返回與此事件中的鍵關聯的字元。比如getKeyText(e.getKeyCode())就返回你所按下的鍵盤
  • VK_ESCAPE: Esc
  • VK_UP: ↑
  • VK_DOWN: ↓
  • VK_LEFT: ←
  • VK_RIGHT: →
  • paint()方法用來繪製圖形
  • repaint()方法用來重新繪製圖像
public void keyPressed(KeyEvent e) {
    switch (e.getKeyCode()){
        // 重新開始遊戲
        case KeyEvent.VK_ESCAPE:
            initGame(); // 重新開始遊戲,遊戲初始化
            break;
        // ↑
        case KeyEvent.VK_UP:
            MoveUp();  // 向左移動
            creatChart();  // 隨機生成數字
            break;
        // ↓
        case KeyEvent.VK_DOWN:
            MoveDown();  // 向右移動
            creatChart();  // 隨機生成數字
            break;
        // ←
        case KeyEvent.VK_LEFT:
            MoveLeft();  // 向左移動
            creatChart();  // 隨機生成數字
            break;
        // →
        case KeyEvent.VK_RIGHT:
            MoveRight();  // 向右移動
            creatChart();  // 隨機生成數字
            break;
        // others
        default:
            break;
    }
    repaint();
}

(6)遊戲初始化

private void initGame(){
    scores = 0;
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            charts[row][col] = new Chart();  // from Chart.java
        }
    }
    // 隨機生成兩個數
    for (int i = 0; i < 2; i++){
        isadd = true;
        creatChart();  // 隨機生成一個數字
    }
}

(7)隨機生成一個數字

2, 4出現概率3:1

  • random.nextInt(int n) 是引數 [0, n) 的隨機數
  • random.nextInt(4): 隨機生成 0, 1, 2, 3;
private void creatChart(){
    List<Chart> list = getEmptyCharts();  // list 為空白方格

    if (!list.isEmpty() && isadd) {  // 在空白方格出隨機生成一個數字
        Random random = new Random();
        int index = random.nextInt(list.size());
        Chart chart = list.get(index);
        chart.value = (random.nextInt(4) % 3 == 0) ? 2 : 4;
        System.out.println(chart.value);
        isadd = false;
    }
}

(8)獲取空白方格

private List<Chart> getEmptyCharts(){
    List<Chart> chartList = new ArrayList<>();
    for (int i = 0; i < 4; i++){
        for (int j = 0; j < 4; j++){
            if (charts[i][j].value == 0){
                chartList.add(charts[i][j]);  //新增元素到 ArrayList 可以使用 add() 方法
            }
        }
    }
    return chartList;
}

(9)移動函式

A. 向上移動

private boolean MoveUp() {
    /* 向上移動,只需考慮第二行到第四行
       共分為兩種情況:
       1、當前數字上邊無空格,即上邊值不為 0
          a. 當前數字與上邊數字相等,合併
          b. 當前數字與上邊數字不相等,continue
       2、當前數字上邊有空格,即上邊值為 0, 上移 */
    for (int j = 0; j < 4; j++) {
        for (int i = 1, index = 0; i < 4; i++) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[index][j].value) {
                    // 當前數字 == 上邊數字
                /* 分數: 當前數字 + 上邊數字
                   數值: 上邊數字 = 上邊數字 + 當前數字, 當前數字 = 0 */
                    scores += charts[i][j].value + charts[index][j].value;
                    charts[index][j].value = charts[i][j].value + charts[index][j].value;
                    charts[i][j].value = 0;
                    index += 1;
                    isadd = true;
                }
                // 當前數字與上邊數字不相等,continue 可以省略不寫
                else if (charts[index][j].value == 0) {
                    // 當前數字上邊有0
                /* 分數: 不變
                   數值: 上邊數字 = 當前數字, 當前數字 = 0 */
                   charts[index][j].value = charts[i][j].value;
                   charts[i][j].value = 0;
                   isadd = true;
                }
                else if (charts[++index][j].value == 0) {
                   // index 相當於慢指標,j 相當於快指標
                   // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                   // 上邊數字 = 0
                /* 分數: 不變
                   數值: 上邊數字 = 當前數字, 當前數字 = 0 */
                   charts[index][j].value = charts[i][j].value;
                   charts[i][j].value = 0;
                   isadd = true;
                }
            }
        }
    }
    return isadd;
}

B. 向下移動

private boolean MoveDown(){
    /* 向下移動,只需考慮第一列到第三列
       共分為兩種情況:
       1、當前數字下邊無空格,即下邊值不為 0
          a. 當前數字與下邊數字相等,合併
          b. 當前數字與下邊數字不相等,continue
       2、當前數字下邊有空格,即下邊值為 0, 下移 */
    for (int j = 0; j < 4; j++) {
        for (int i = 2, index = 3; i >= 0; i--) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[index][j].value) {
                    // 當前數字 == 下邊數字
                    /* 分數: 當前數字 + 下邊數字
                       數值: 下邊數字 = 下邊數字 + 當前數字, 當前數字 = 0 */
                    scores += charts[i][j].value + charts[index][j].value;
                    charts[index][j].value = charts[i][j].value + charts[index][j].value;
                    charts[i][j].value = 0;
                    index -= 1;
                    isadd = true;
                }
                // 當前數字與下邊數字不相等,continue 可以省略不寫
                else if (charts[index][j].value == 0) {
                    // 當前數字下邊有0
                    /* 分數: 不變
                       數值: 下邊數字 = 當前數字, 當前數字 = 0 */
                    charts[index][j].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
                else if (charts[--index][j].value == 0) {
                    // index 相當於慢指標,j 相當於快指標
                    // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                    // 下邊數字 = 0
                    /* 分數: 不變
                       數值: 下邊數字 = 當前數字, 當前數字 = 0 */
                    charts[index][j].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
            }
        }
    }
    return isadd;
}

C. 向左移動

private boolean MoveLeft(){
    /* 向左移動,只需考慮第二列到第四列
       共分為兩種情況:
       1、當前數字左邊無空格,即左邊值不為 0
          a. 當前數字與左邊數字相等,合併
          b. 當前數字與左邊數字不相等,continue
       2、當前數字左邊有空格,即左邊值為 0, 左移 */
    for (int i = 0; i < 4; i++) {
        for (int j = 1, index = 0; j < 4; j++) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[i][index].value) {
                    // 當前數字 == 左邊數字
                    /* 分數: 當前數字 + 左邊數字
                       數值: 左邊數字 = 左邊數字 + 當前數字, 當前數字 = 0 */
                    scores += charts[i][j].value + charts[i][index].value;
                    charts[i][index].value = charts[i][index].value + charts[i][j].value;
                    charts[i][j].value = 0;
                    index += 1;
                    isadd = true;
                }
                // 當前數字與左邊數字不相等,continue 可以省略不寫
                else if (charts[i][index].value == 0) {
                    // 當前數字左邊有0
                    /* 分數: 不變
                       數值: 左邊數字 = 當前數字, 當前數字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
                else if (charts[i][++index].value == 0) {
                    // index 相當於慢指標,j 相當於快指標
                    // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                    // 左邊數字 = 0
                    /* 分數: 不變
                       數值: 左邊數字 = 當前數字, 當前數字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
            }
        }
    }
    return isadd;
}

D. 向右移動

private boolean MoveRight(){
  /* 向右移動,只需考慮第一列到第三列
     共分為兩種情況:
     1、當前數字右邊無空格,即右邊值不為 0
        a. 當前數字與右邊數字相等,合併
        b. 當前數字與右邊數字不相等,continue
     2、當前數字右邊有空格,即右邊值為 0, 右移 */
    for (int i = 0; i < 4; i++) {
        for (int j = 2, index = 3; j >= 0; j--) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[i][index].value) {
                    // 當前數字 == 右邊數字
                    /* 分數: 當前數字 + 右邊數字
                       數值: 右邊數字 = 右邊數字 + 當前數字, 當前數字 = 0 */
                    scores += charts[i][j].value + charts[i][index].value;
                    charts[i][index].value = charts[i][j].value + charts[i][index].value;
                    charts[i][j].value = 0;
                    index -= 1;
                    isadd = true;
                }
                // 當前數字與左邊數字不相等,continue 可以省略不寫
                else if (charts[i][index].value == 0) {
                    // 當前數字右邊有0
                    /* 分數: 不變
                       數值: 右邊數字 = 當前數字, 當前數字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
                else if (charts[i][--index].value == 0) {
                    // index 相當於慢指標,j 相當於快指標
                    // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                    // 右邊數字 = 0
                    /* 分數: 不變
                       數值: 右邊數字 = 當前數字, 當前數字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
            }
        }
    }
    return isadd;
}

(10)判斷遊戲是否結束

private boolean judgeGameOver(){
    // 將lscore標籤內容設定為 scores + ""
    Form.lscore.setText(scores + "");

    // 當空白空格不為空時,即遊戲未結束
    if (!getEmptyCharts().isEmpty()){
        return false;
    }

    // 當空白方格為空時,判斷是否存在可合併的方格
    for (int i = 0; i < 3; i++){
        for (int j = 0; j < 3; j++){
            if (charts[i][j].value == charts[i][j + 1].value
                || charts[i][j].value == charts[i + 1][j].value){
                return false;
            }
        }
    }
    // 若不滿足以上兩種情況,則遊戲結束
    return true;
}

(11)判斷遊戲是否成功

private boolean judgeGameSuccess() {
    // 檢查是否有2048
    for (int i = 0; i< 4; i++) {
        for (int j = 0; j < 4; j++) {
            if (charts[i][j].value == 2048) {
                return true;
            }
        }
    }
    return false;
}

(12)畫筆函式

public void paint(Graphics g){
    super.paint(g);
    for (int i = 0; i < 4; i++){
        for (int j = 0; j < 4; j++){
            drawChart(g, i, j);
        }
    }

    // 如果遊戲結束
    if (judgeGameOver()){
        g.setColor(new Color(64, 64, 64, 150));
        g.fillRect(0, 0, getWidth(), getHeight());  // 畫矩形
        g.setColor(Color.WHITE);  // 畫筆顏色為白色
        g.setFont(title);
        FontMetrics fm = getFontMetrics(title);
        String value = "Game Over!";  // 內容: Game Over!
        g.drawString(value,
                (getWidth() - fm.stringWidth(value)) / 2,
                getHeight() / 2);
    }  // 位置

    // 如果遊戲成功
    if (judgeGameSuccess()) {
        g.setColor(new Color(64, 64, 64, 150));
        g.fillRect(0, 0, getWidth(), getHeight());  // 畫矩形
        g.setColor(Color.RED);  // 畫筆顏色為紅色
        g.setFont(title);
        FontMetrics fm = getFontMetrics(title);
        String value = "Successful!";  // 內容: Successful!
        g.drawString(value,
                (getWidth() - fm.stringWidth(value)) / 2,
                getHeight() / 2);
      // 位置
    }
}

(13)繪製方格

A. Java語言在Graphics類提供繪製各種基本的幾何圖形的基礎上, 擴充套件Graphics類提供一個Graphics2D類, 它擁用更強大的二維圖形處理能力,提供、座標轉換、顏色管理以及文字佈局等更精確的控制。

B. setRenderingHint() 方法的引數是一個鍵以及對應的鍵值。

a. KEY_ANTIALIASING: 抗鋸齒提示鍵。

物件的幾何形狀呈現方法是否將嘗試沿形狀的邊緣減少鋸齒現象 此提示允許的值有:

  • -- VALUE_ANTIALIAS_ON:使用抗鋸齒模式完成呈現
  • -- VALUE_ANTIALIAS_OFF:在不使用抗鋸齒模式的情況下完成呈現
  • -- VALUE_ANTIALIAS_DEFAULT:使用由實現選擇的預設抗鋸齒模式完成呈現

b. KEY_STROKE_CONTROL: 筆劃規範化控制提示鍵。

c. STROKE_CONTROL 提示鍵控制呈現實現是否應該或允許出於各種目的而修改所呈現輪廓的幾何形狀。

此提示允許的值有:

  • -- VALUE_STROKE_NORMALIZE:幾何形狀應當規範化,以提高均勻性或直線間隔和整體美觀。
  • -- VALUE_STROKE_PURE:幾何形狀應該保持不變並使用子畫素精確度呈現
  • -- VALUE_STROKE_DEFAULT:根據給定實現的權衡,可以修改幾何形狀或保留原來的幾何形狀。

C. 繪製圓角

  • -- x: 填充矩形的 x 座標
  • -- y: 填充矩形的 y 座標
  • -- width: 填充矩形的寬度
  • -- height: 填充矩形的高度
  • -- arcwidth: 4個弧度的水平直徑
  • -- archeight: 4個弧度的垂直直徑

D. FontMetrics 字型屬性類

  • GetAscent(): Ascent表示字型從基線到頂端的距離
  • getDescent(): Descent表示字型從基線到下降字元底端的距離
  • getLeading(): Leading 表示本文行之間的距離
  • getheight(): 字型高度 Ascent + Descent + Leading
  • StringWidth(String): 字串寬度
private void drawChart(Graphics g, int i, int j){
    Graphics2D g2d = (Graphics2D) g;
    
    // 消除鋸齒
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    // 幾何形狀規範化
    g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
            RenderingHints.VALUE_STROKE_NORMALIZE);

    Chart chart = charts[i][j];
    g2d.setColor(chart.getBackground());  // 表格背景顏色
    g2d.fillRoundRect(CHART_GAP + (CHART_GAP + CHART_SIZE) * j,
            CHART_GAP + (CHART_GAP + CHART_SIZE) * i,
            CHART_SIZE, CHART_SIZE, CHART_ARC, CHART_ARC);
    g2d.setColor(chart.getForeground());  // 表格前景顏色
    g2d.setFont(chart.getChartFont());   // 設定字型

    // 文字設定
    FontMetrics fm = getFontMetrics(chart.getChartFont());
    String value = String.valueOf(chart.value);  // int型轉換為String字串
    g2d.drawString(value,
            CHART_GAP + (CHART_GAP + CHART_SIZE) * j +
                    (CHART_SIZE - fm.stringWidth(value)) / 2,
            CHART_GAP + (CHART_GAP + CHART_SIZE) * i +
                    (CHART_SIZE - fm.getAscent() - fm.getDescent()) / 2
                    + fm.getAscent());

}

三、程式碼

1、Data.interface

package Game2048_java;

import java.awt.Font;

public interface Data {
    Font title = new Font("微軟雅黑", Font.BOLD, 50); // 視窗標題
    Font score = new Font("微軟雅黑", Font.BOLD, 28); // 分數
    Font tips = new Font("宋體", Font.PLAIN, 20); // 說明

    /* font1: 數字2, 4, 8
    font2: 數字16, 32, 64
    font3: 數字128, 256, 512
    font4: 數字1024, 2048, 4096, 8192
    * */
    Font font1 = new Font("宋體", Font.BOLD, 46);
    Font font2 = new Font("宋體", Font.BOLD, 40);
    Font font3 = new Font("宋體", Font.BOLD, 34);
    Font font4 = new Font("宋體", Font.BOLD, 28);

    int CHART_GAP = 10;
    int CHART_ARC = 20;
    int CHART_SIZE = 86;
}

2、Form.class

package Game2048_java;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.KeyListener;

public class Form implements Data{
    public static JLabel lscore;

    /** 構造窗體 */
    public Form() {
        /*
        setSize(x, y): 視窗大小 x * y
        setVisible(true): true視窗可見
        setLocationRelativeTo((Component)null)設定視窗相對於指定元件的位置,null表示視窗在螢幕中央
        點選視窗右上角關閉,四種關閉方式:
        DO_NOTHING_ON_CLOSE,不執行任何操作。
        HIDE_ON_CLOSE,只隱藏介面,setVisible(false)。
        DISPOSE_ON_CLOSE,隱藏並釋放窗體,dispose(),當最後一個視窗被釋放後,則程式也隨之執行結束。
        EXIT_ON_CLOSE,直接關閉應用程式,System.exit(0)。一個main函式對應一整個程式。
         */
        /* 視窗設定 */
        JFrame frame = new JFrame("Game 2048");
        frame.setSize(400, 530); // width * height
        frame.setResizable(false); // 視窗大小不可調整
        frame.setVisible(true); // true視窗可見
        frame.setLocationRelativeTo(null); // 視窗居中
        frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE); // 視窗的關閉
        frame.setLayout(null); // 設定使用者介面上的螢幕元件的格式佈局,預設為流式佈局

        /* 新增標籤 */
        // 新增標題: 2048
        JLabel ltitle = new JLabel("2048", JLabel.CENTER);
        frame.add(ltitle);
        ltitle.setFont(Data.title); // 標題字型/大小/位置
        ltitle.setForeground(Color.BLACK);
        ltitle.setBounds(50, 0, 150, 60);

        // 新增分數: 得分: 0
        JLabel lscorename = new JLabel("得 分", JLabel.CENTER);
        frame.add(lscorename);
        lscorename.setFont(Data.score); // "得分"字型/大小/位置
        lscorename.setForeground(Color.WHITE);
        lscorename.setOpaque(true); // 設定控制元件是否透明.true: 控制元件不透明; false: 控制元件透明.
        lscorename.setBackground(Color.GRAY);
        lscorename.setBounds(250, 0, 120, 30);

        lscore = new JLabel("0", JLabel.CENTER);
        frame.add(lscore);
        lscore.setFont(Data.score); // "得分"字型/大小/位置
        lscore.setForeground(Color.WHITE);
        lscore.setOpaque(true); // 設定控制元件是否透明.true: 控制元件不透明; false: 控制元件透明.
        lscore.setBackground(Color.GRAY);
        lscore.setBounds(250, 30, 120, 30);

        // 遊戲說明:
        JLabel ltips = new JLabel("操作: ↑ ↓ ← →, 按esc鍵重新開始  ", JLabel.CENTER);
        frame.add(ltips);
        ltips.setFont(Data.tips); // "說明"字型/大小/位置
        ltips.setForeground(Color.DARK_GRAY);
        ltips.setBounds(0, 60, 400, 40);

        // 遊戲面板:
        JPanel panel = new Game2048Panel();
        frame.add(panel);
        panel.setBounds(0, 100, 400, 400); // 面板繪製區域
        panel.setBackground(Color.GRAY);
        panel.setFocusable(true); //setFocusable設定元件是否可被選中
        // FlowLayout(流式佈局): 元件按照加入的先後順序按照設定的對齊方式從左向右排列,一行排滿到下一行開始繼續排列
        panel.setLayout(new FlowLayout());
        // 鍵盤監聽
        frame.addKeyListener((KeyListener) panel);
    }

    public static void main(String[] args) {
        new Form();
    }
}

3、Chart.class

package Game2048_java;

import java.awt.Color;
import java.awt.Font;

public class Chart implements Data{
    /**
     * 方格類
     * @param value: 面板上的數字值
     */
    public int value;

    public Chart(){
        clear();
    }

    /** 清除面板 */
    public void clear(){
        value = 0;
    }

    /** 對應數字的字型顏色 */
    public Color getForeground(){
        /* 對應數字的字型顏色
        0: 0xcdc1b4, 同背景顏色相同,因此不會顯示
        2, 4: BLACK, 黑色
        其他: WHITE, 白色 */
        return switch (value) {
            case 0 -> new Color(0xcdc1b4);
            case 2, 4 -> Color.BLACK;
            default -> Color.WHITE;
        };
    }

    /** 對應數字的背景顏色 */
    public Color getBackground(){
        /* 對應數字的背景顏色 */
        return switch (value){
            case 0 -> new Color(0xcdc1b4);
            case 2 -> new Color(0xeee4da);
            case 4 -> new Color(0xede0c8);
            case 8 -> new Color(0xf2b179);
            case 16 -> new Color(0xf59563);
            case 32 -> new Color(0xf67c5f);
            case 64 -> new Color(0xf65e3b);
            case 128 -> new Color(0xedcf72);
            case 256 -> new Color(0xedcc61);
            case 512 -> new Color(0xedc850);
            case 1024 -> new Color(0xedc53f);
            case 2048 -> new Color(0xedc22e);
            default -> new Color(0x248c51);
        };
    }

    /** 對應數字的字型大小 */
    public Font getChartFont(){
        return switch (value){
            case 0, 2, 4, 8 -> font1;
            case 16, 32, 64 -> font2;
            case 128, 256, 512 -> font3;
            default -> font4;
        };
    }
}

4、Game2048Panel

package Game2048_java;

import javax.swing.JPanel;

import java.awt.Graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.FontMetrics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

// 遊戲面板需要對鍵盤進行監聽,因此需要實現介面 KeyListener 中的 keyPressed 方法
public class Game2048Panel extends JPanel implements Data, KeyListener {
    /* 變數定義 */
    private static int scores = 0;
    private Chart[][] charts = new Chart[4][4]; // 遊戲面板
    private boolean isadd = true;  // 是否新增數字

    /** 構造方法 */
    public Game2048Panel() {
        initGame();  // 初始化
    }

    /** 按下某個鍵時呼叫此方法 */
    @Override
    public void keyPressed(KeyEvent e) {
        /* getKeyCode():鍵盤上每一個按鈕都有對應碼(Code),可用來查知使用者按了什麼鍵,返回當前按鈕的數值
           getKeyChar():處理的是比較高層的事件,返回的是每欠敲擊鍵盤後得到的字元(中文輸入法下就是漢字)
           getKeyText():返回與此事件中的鍵關聯的字元。比如getKeyText(e.getKeyCode())就返回你所按下的鍵盤*/

        switch (e.getKeyCode()){
            /* VK_ESCAPE: Esc
               VK_UP: ↑
               VK_DOWN: ↓
               VK_LEFT: ←
               VK_RIGHT: →
            */
            // 重新開始遊戲
            case KeyEvent.VK_ESCAPE:
                System.out.println("esc");
                initGame(); // 重新開始遊戲,遊戲初始化
                break;
            // ↑
            case KeyEvent.VK_UP:
//                System.out.println("up");
                MoveUp();  // 向左移動
                creatChart();  // 隨機生成數字
//                judgeGameOver(); // 判斷遊戲是否結束
                break;
            // ↓
            case KeyEvent.VK_DOWN:
//                System.out.println("down");
                MoveDown();  // 向右移動
                creatChart();  // 隨機生成數字
//                judgeGameOver(); // 判斷遊戲是否結束
                break;
            // ←
            case KeyEvent.VK_LEFT:
//                System.out.println("left");
                MoveLeft();  // 向左移動
                creatChart();  // 隨機生成數字
//                judgeGameOver(); // 判斷遊戲是否結束
                break;
            // →
            case KeyEvent.VK_RIGHT:
//                System.out.println("right");
                MoveRight();  // 向右移動
                creatChart();  // 隨機生成數字
//                judgeGameOver(); // 判斷遊戲是否結束
                break;
            // others
            default:
                break;
        }
        // paint()方法用來繪製圖形,repaint()方法用來重新繪製圖像
        repaint();
    }

    /** 遊戲初始化 */
    private void initGame(){
        scores = 0;
        System.out.println("initGame");
        for (int row = 0; row < 4; row++) {
            for (int col = 0; col < 4; col++) {
                charts[row][col] = new Chart();  // from Chart.java
            }
        }
        // 隨機生成兩個數
        for (int i = 0; i < 2; i++){
            isadd = true;
            creatChart();  // 隨機生成一個數字
        }
    }

    /** 隨機在一個位置生成一個數 */
    private void creatChart(){
        List<Chart> list = getEmptyCharts();  // list 為空白方格

        if (!list.isEmpty() && isadd) {  // 在空白方格出隨機生成一個數字
            Random random = new Random();
            int index = random.nextInt(list.size());
            Chart chart = list.get(index);
            // 2, 4出現概率3:1
            /* random.nextInt(int n) 是引數 [0, n) 的隨機數 */
            /* random.nextInt(4): 隨機生成 0, 1, 2, 3; */
            chart.value = (random.nextInt(4) % 3 == 0) ? 2 : 4;
            System.out.println(chart.value);
            isadd = false;
        }
    }

    /** 獲取空白方格 */
    private List<Chart> getEmptyCharts(){
        List<Chart> chartList = new ArrayList<>();
        for (int i = 0; i < 4; i++){
            for (int j = 0; j < 4; j++){
                if (charts[i][j].value == 0){
                    chartList.add(charts[i][j]);  //新增元素到 ArrayList 可以使用 add() 方法
                }
            }
        }
        return chartList;
    }

    /** 向上移動 */
    private boolean MoveUp() {
//        System.out.println("MoveUp");
        /* 向上移動,只需考慮第二行到第四行
           共分為兩種情況:
           1、當前數字上邊無空格,即上邊值不為 0
              a. 當前數字與上邊數字相等,合併
              b. 當前數字與上邊數字不相等,continue
           2、當前數字上邊有空格,即上邊值為 0, 上移 */
        for (int j = 0; j < 4; j++) {
            for (int i = 1, index = 0; i < 4; i++) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[index][j].value) {
                        // 當前數字 == 上邊數字
                    /* 分數: 當前數字 + 上邊數字
                       數值: 上邊數字 = 上邊數字 + 當前數字, 當前數字 = 0 */
                        scores += charts[i][j].value + charts[index][j].value;
                        charts[index][j].value = charts[i][j].value + charts[index][j].value;
                        charts[i][j].value = 0;
                        index += 1;
                        isadd = true;
                    }
                    // 當前數字與上邊數字不相等,continue 可以省略不寫
                    else if (charts[index][j].value == 0) {
                        // 當前數字上邊有0
                    /* 分數: 不變
                       數值: 上邊數字 = 當前數字, 當前數字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[++index][j].value == 0) {
                        // index 相當於慢指標,j 相當於快指標
                        // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                        // 上邊數字 = 0
                    /* 分數: 不變
                       數值: 上邊數字 = 當前數字, 當前數字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }
        return isadd;
    }

    /** 向下移動 */
    private boolean MoveDown(){
        System.out.println("MoveDown");
        /* 向下移動,只需考慮第一列到第三列
           共分為兩種情況:
           1、當前數字下邊無空格,即下邊值不為 0
              a. 當前數字與下邊數字相等,合併
              b. 當前數字與下邊數字不相等,continue
           2、當前數字下邊有空格,即下邊值為 0, 下移 */
        for (int j = 0; j < 4; j++) {
            for (int i = 2, index = 3; i >= 0; i--) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[index][j].value) {
                        // 當前數字 == 下邊數字
                        /* 分數: 當前數字 + 下邊數字
                           數值: 下邊數字 = 下邊數字 + 當前數字, 當前數字 = 0 */
                        scores += charts[i][j].value + charts[index][j].value;
                        charts[index][j].value = charts[i][j].value + charts[index][j].value;
                        charts[i][j].value = 0;
                        index -= 1;
                        isadd = true;
                    }
                    // 當前數字與下邊數字不相等,continue 可以省略不寫
                    else if (charts[index][j].value == 0) {
                        // 當前數字下邊有0
                        /* 分數: 不變
                           數值: 下邊數字 = 當前數字, 當前數字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[--index][j].value == 0) {
                        // index 相當於慢指標,j 相當於快指標
                        // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                        // 下邊數字 = 0
                        /* 分數: 不變
                           數值: 下邊數字 = 當前數字, 當前數字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }
        return isadd;
    }

    /** 向左移動 */
    private boolean MoveLeft(){
    //        System.out.println("MoveLeft");
        /* 向左移動,只需考慮第二列到第四列
           共分為兩種情況:
           1、當前數字左邊無空格,即左邊值不為 0
              a. 當前數字與左邊數字相等,合併
              b. 當前數字與左邊數字不相等,continue
           2、當前數字左邊有空格,即左邊值為 0, 左移 */
        for (int i = 0; i < 4; i++) {
            for (int j = 1, index = 0; j < 4; j++) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[i][index].value) {
                        // 當前數字 == 左邊數字
                        /* 分數: 當前數字 + 左邊數字
                           數值: 左邊數字 = 左邊數字 + 當前數字, 當前數字 = 0 */
                        scores += charts[i][j].value + charts[i][index].value;
                        charts[i][index].value = charts[i][index].value + charts[i][j].value;
                        charts[i][j].value = 0;
                        index += 1;
                        isadd = true;
                    }
                    // 當前數字與左邊數字不相等,continue 可以省略不寫
                    else if (charts[i][index].value == 0) {
                        // 當前數字左邊有0
                        /* 分數: 不變
                           數值: 左邊數字 = 當前數字, 當前數字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[i][++index].value == 0) {
                        // index 相當於慢指標,j 相當於快指標
                        // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                        // 左邊數字 = 0
                        /* 分數: 不變
                           數值: 左邊數字 = 當前數字, 當前數字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }

        return isadd;
    }

    /** 向右移動 */
    private boolean MoveRight(){
    //        System.out.println("MoveRight");
      /* 向右移動,只需考慮第一列到第三列
         共分為兩種情況:
         1、當前數字右邊無空格,即右邊值不為 0
            a. 當前數字與右邊數字相等,合併
            b. 當前數字與右邊數字不相等,continue
         2、當前數字右邊有空格,即右邊值為 0, 右移 */
        for (int i = 0; i < 4; i++) {
            for (int j = 2, index = 3; j >= 0; j--) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[i][index].value) {
                        // 當前數字 == 右邊數字
                        /* 分數: 當前數字 + 右邊數字
                           數值: 右邊數字 = 右邊數字 + 當前數字, 當前數字 = 0 */
                        scores += charts[i][j].value + charts[i][index].value;
                        charts[i][index].value = charts[i][j].value + charts[i][index].value;
                        charts[i][j].value = 0;
                        index -= 1;
                        isadd = true;
                    }
                    // 當前數字與左邊數字不相等,continue 可以省略不寫
                    else if (charts[i][index].value == 0) {
                        // 當前數字右邊有0
                        /* 分數: 不變
                           數值: 右邊數字 = 當前數字, 當前數字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[i][--index].value == 0) {
                        // index 相當於慢指標,j 相當於快指標
                        // 也就是說快指標和慢指標中間可能存在一個以上的空格,或者index和j並未相鄰
                        // 右邊數字 = 0
                        /* 分數: 不變
                           數值: 右邊數字 = 當前數字, 當前數字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }
        return isadd;
    }

    /** 判斷遊戲是否結束 */
    private boolean judgeGameOver(){
        // 將lscore標籤內容設定為 scores + ""
        Form.lscore.setText(scores + "");

        // 當空白空格不為空時,即遊戲未結束
        if (!getEmptyCharts().isEmpty()){
            return false;
        }

        // 當空白方格為空時,判斷是否存在可合併的方格
        for (int i = 0; i < 3; i++){
            for (int j = 0; j < 3; j++){
                if (charts[i][j].value == charts[i][j + 1].value
                    || charts[i][j].value == charts[i + 1][j].value){
                    return false;
                }
            }
        }
        // 若不滿足以上兩種情況,則遊戲結束
        return true;
    }

    /** 判斷遊戲是否成功 */
    private boolean judgeGameSuccess() {
        // 檢查是否有2048
        for (int i = 0; i< 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (charts[i][j].value == 2048) {
                    return true;
                }
            }
        }
        return false;
    }

    /** 畫筆函式 */
    @Override
    public void paint(Graphics g){
        super.paint(g);
        System.out.println("paint");
        for (int i = 0; i < 4; i++){
            for (int j = 0; j < 4; j++){
                drawChart(g, i, j);
            }
        }

        // 如果遊戲結束
        if (judgeGameOver()){
            g.setColor(new Color(64, 64, 64, 150));
            g.fillRect(0, 0, getWidth(), getHeight());  // 畫矩形
            g.setColor(Color.WHITE);  // 畫筆顏色為白色
            g.setFont(title);
            FontMetrics fm = getFontMetrics(title);
            String value = "Game Over!";  // 內容: Game Over!
            g.drawString(value,
                    (getWidth() - fm.stringWidth(value)) / 2,
                    getHeight() / 2);
        }  // 位置

        // 如果遊戲成功
        if (judgeGameSuccess()) {
            g.setColor(new Color(64, 64, 64, 150));
            g.fillRect(0, 0, getWidth(), getHeight());  // 畫矩形
            g.setColor(Color.RED);  // 畫筆顏色為紅色
            g.setFont(title);
            FontMetrics fm = getFontMetrics(title);
            String value = "Successful!";  // 內容: Successful!
            g.drawString(value,
                    (getWidth() - fm.stringWidth(value)) / 2,
                    getHeight() / 2);
          // 位置
        }
    }

    /** 繪製方格 */
    private void drawChart(Graphics g, int i, int j){
        /* Java語言在Graphics類提供繪製各種基本的幾何圖形的基礎上,
           擴充套件Graphics類提供一個Graphics2D類,
           它擁用更強大的二維圖形處理能力,提供、座標轉換、顏色管理以及文字佈局等更精確的控制。*/
        Graphics2D g2d = (Graphics2D) g;
        /* setRenderingHint() 方法的引數是一個鍵以及對應的鍵值。
           KEY_ANTIALIASING: 抗鋸齒提示鍵。物件的幾何形狀呈現方法是否將嘗試沿形狀的邊緣減少鋸齒現象
           此提示允許的值有:
           -- VALUE_ANTIALIAS_ON:使用抗鋸齒模式完成呈現
           -- VALUE_ANTIALIAS_OFF:在不使用抗鋸齒模式的情況下完成呈現
           -- VALUE_ANTIALIAS_DEFAULT:使用由實現選擇的預設抗鋸齒模式完成呈現
           KEY_STROKE_CONTROL: 筆劃規範化控制提示鍵。STROKE_CONTROL 提示鍵控制呈現實現是否應該或允許出於各種目的而修改所呈現輪廓的幾何形狀。
           此提示允許的值有
           -- VALUE_STROKE_NORMALIZE:幾何形狀應當規範化,以提高均勻性或直線間隔和整體美觀。
           -- VALUE_STROKE_PURE:幾何形狀應該保持不變並使用子畫素精確度呈現
           -- VALUE_STROKE_DEFAULT:根據給定實現的權衡,可以修改幾何形狀或保留原來的幾何形狀。
           */
        // 消除鋸齒
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        // 幾何形狀規範化
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_NORMALIZE);

        Chart chart = charts[i][j];
        g2d.setColor(chart.getBackground());  // 表格背景顏色
        /* 繪製圓角
           -- x: 填充矩形的 x 座標
           -- y: 填充矩形的 y 座標
           -- width: 填充矩形的寬度
           -- height: 填充矩形的高度
           -- arcwidth: 4個弧度的水平直徑
           -- archeight: 4個弧度的垂直直徑 */
        g2d.fillRoundRect(CHART_GAP + (CHART_GAP + CHART_SIZE) * j,
                CHART_GAP + (CHART_GAP + CHART_SIZE) * i,
                CHART_SIZE, CHART_SIZE, CHART_ARC, CHART_ARC);
        g2d.setColor(chart.getForeground());  // 表格前景顏色
        g2d.setFont(chart.getChartFont());   // 設定字型

        // 文字設定
        /* FontMetrics 字型屬性類
           GetAscent(): Ascent表示字型從基線到頂端的距離
           getDescent(): Descent表示字型從基線到下降字元底端的距離
           getLeading(): Leading 表示本文行之間的距離
           getheight(): 字型高度  Ascent + Descent + Leading
           StringWidth(String): 字串寬度 */
        FontMetrics fm = getFontMetrics(chart.getChartFont());
        String value = String.valueOf(chart.value);  // int型轉換為String字串
        g2d.drawString(value,
                CHART_GAP + (CHART_GAP + CHART_SIZE) * j +
                        (CHART_SIZE - fm.stringWidth(value)) / 2,
                CHART_GAP + (CHART_GAP + CHART_SIZE) * i +
                        (CHART_SIZE - fm.getAscent() - fm.getDescent()) / 2
                        + fm.getAscent());

    }

    /** 釋放某個鍵時呼叫此方法 */
    @Override
    public void keyReleased(KeyEvent e) {

    }

    /** 鍵入某個鍵時呼叫此方法 */
    @Override
    public void keyTyped(KeyEvent e) {

    }
}

參考: