首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Swing 實作連連看遊戲完整原始碼與核心邏輯解析

     編輯:Java 相關知識

專案簡介

連連看 是經典益智遊戲:在網格中找出兩張相同圖案,且可用不超過三條直線連接 (即最多兩個轉彎) 且路徑無阻擋,即可消除。本文提供基於 Swing 的完整實作,包含 UI 建立、隨機佈局、連線判定演算法、分數計算與重新開始功能。

核心資料結構

// 8x7 邏輯網格 (含邊界緩衝),實際可見 6x5
int[][] grid = new int[8][7]; 

// JButton 陣列對應視覺層
JButton[][] buttons = new JButton[6][5];

// 雙向鏈結節點 (可選,本範例用陣列座標)
class Node { int x, y; Node next, prev; }

連線判定核心演算法

採用 BFS 搜尋最短轉彎路徑 或 四方向延伸法。本實作使用四方向延伸:從起點向四方向延伸直到牆壁/障礙,記錄轉彎點,再從轉彎點延伸,最多兩次轉彎。


/**
 * 判斷兩點是否可連線 (最多 2 轉彎)
 * @return 可連線傳回 true,並可選擇性記錄路徑座標供繪製連線動畫
 */
public boolean canConnect(int x1, int y1, int x2, int y2) {
    if (x1 == x2 && y1 == y2) return false; // 同一格
    if (grid[x1][y1] != grid[x2][y2]) return false; // 圖案不同
    
    // 0 轉彎:同行/同列且中間無阻擋
    if (x1 == x2 && isClearHorizontal(x1, y1, y2)) return true;
    if (y1 == y2 && isClearVertical(y1, x1, x2)) return true;
    
    // 1 轉彎:嘗試 (x1, y2) 和 (x2, y1) 為轉彎點
    if (grid[x1][y2] == 0 && isClearHorizontal(x1, y1, y2) && isClearVertical(y2, x1, x2)) return true;
    if (grid[x2][y1] == 0 && isClearVertical(y1, x1, x2) && isClearHorizontal(x2, y1, y2)) return true;
    
    // 2 轉彎:向四方向延伸尋找中介點
    // 向上延伸
    for (int x = x1 - 1; x >= 0; x--) {
        if (grid[x][y1] != 0) break;
        if (canConnectOneTurn(x, y1, x2, y2)) return true;
    }
    // 向下、向左、向右 同理...
    return false;
}

private boolean isClearHorizontal(int row, int y1, int y2) {
    int from = Math.min(y1, y2) + 1;
    int to = Math.max(y1, y2) - 1;
    for (int y = from; y <= to; y++) if (grid[row][y] != 0) return false;
    return true;
}
// isClearVertical 類似

完整可執行原始碼 (單一檔案版)

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

public class LinkLinkGame extends JFrame {
    private static final int ROWS = 6, COLS = 5, TYPES = 15;
    private static final int CELL_SIZE = 60;
    
    private JButton[][] btns = new JButton[ROWS][COLS];
    private int[][] grid = new int[ROWS + 2][COLS + 2]; // 含邊界 0
    private Point firstClick = null;
    private JLabel scoreLabel;
    private int score = 0;
    private Timer hintTimer;

    public LinkLinkGame() {
        setTitle("連連看 Java Swing 版");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        initUI();
        newGame();
        pack();
        setLocationRelativeTo(null);
        setResizable(false);
    }

    private void initUI() {
        // 頂部分數列
        JPanel top = new JPanel(new FlowLayout(FlowLayout.CENTER));
        scoreLabel = new JLabel("分數: 0");
        scoreLabel.setFont(new Font("微軟正黑體", Font.BOLD, 20));
        JButton btnNew = new JButton("新遊戲");
        JButton btnShuffle = new JButton("重排");
        JButton btnHint = new JButton("提示");
        btnNew.addActionListener(e -> newGame());
        btnShuffle.addActionListener(e -> shuffle());
        btnHint.addActionListener(e -> showHint());
        top.add(scoreLabel);
        top.add(btnNew);
        top.add(btnShuffle);
        top.add(btnHint);
        add(top, BorderLayout.NORTH);

        // 中央遊戲區
        JPanel center = new JPanel(new GridLayout(ROWS, COLS, 2, 2));
        center.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                JButton b = new JButton();
                b.setFont(new Font("微軟正黑體", Font.BOLD, 24));
                b.setFocusPainted(false);
                b.setMargin(new Insets(0,0,0,0));
                final int fr = r, fc = c;
                b.addActionListener(e -> onCellClick(fr, fc));
                btns[r][c] = b;
                center.add(b);
            }
        }
        add(center, BorderLayout.CENTER);
    }

    private void newGame() {
        // 產生配對資料
        List pool = new ArrayList<>();
        for (int i = 1; i <= TYPES; i++) {
            pool.add(i); pool.add(i); // 各兩張
        }
        Collections.shuffle(pool);
        // 填入 grid (1~ROWS, 1~COLS)
        Iterator it = pool.iterator();
        for (int r = 1; r <= ROWS; r++) {
            for (int c = 1; c <= COLS; c++) {
                grid[r][c] = it.next();
            }
        }
        // 邊界保持 0
        firstClick = null;
        score = 0;
        updateUI();
    }

    private void shuffle() {
        List remaining = new ArrayList<>();
        for (int r = 1; r <= ROWS; r++) {
            for (int c = 1; c <= COLS; c++) {
                if (grid[r][c] != 0) remaining.add(grid[r][c]);
            }
        }
        Collections.shuffle(remaining);
        Iterator it = remaining.iterator();
        for (int r = 1; r <= ROWS; r++) {
            for (int c = 1; c <= COLS; c++) {
                if (grid[r][c] != 0) grid[r][c] = it.next();
            }
        }
        firstClick = null;
        updateUI();
    }

    private void onCellClick(int r, int c) {
        if (grid[r+1][c+1] == 0) return; // 已消除
        JButton clicked = btns[r][c];
        
        if (firstClick == null) {
            // 第一次點擊
            firstClick = new Point(r, c);
            clicked.setBorder(BorderFactory.createLineBorder(Color.RED, 3));
        } else {
            // 第二次點擊
            Point p1 = firstClick;
            Point p2 = new Point(r, c);
            btns[p1.x][p1.y].setBorder(null); // 清除紅框
            
            if (p1.equals(p2)) {
                firstClick = null; // 取消選取
                return;
            }
            
            if (canConnect(p1.x+1, p1.y+1, p2.x+1, p2.y+1)) {
                // 消除成功
                grid[p1.x+1][p1.y+1] = 0;
                grid[p2.x+1][p2.y+1] = 0;
                score += 100;
                scoreLabel.setText("分數: " + score);
                // 檢查勝利
                if (isGameClear()) {
                    JOptionPane.showMessageDialog(this, "恭喜通關! 最終分數: " + score);
                }
            } else {
                // 失敗視覺回饋
                clicked.setBackground(Color.PINK);
                Timer t = new Timer(200, e -> clicked.setBackground(null));
                t.setRepeats(false);
                t.start();
            }
            firstClick = null;
            updateUI();
        }
    }

    private boolean canConnect(int x1, int y1, int x2, int y2) {
        if (grid[x1][y1] != grid[x2][y2]) return false;
        
        // 0 轉
        if (x1 == x2 && clearHor(x1, y1, y2)) return true;
        if (y1 == y2 && clearVer(y1, x1, x2)) return true;
        
        // 1 轉
        if (grid[x1][y2] == 0 && clearHor(x1, y1, y2) && clearVer(y2, x1, x2)) return true;
        if (grid[x2][y1] == 0 && clearVer(y1, x1, x2) && clearHor(x2, y1, y2)) return true;
        
        // 2 轉:向四方向延伸
        // 上
        for (int x = x1 - 1; x >= 0; x--) {
            if (grid[x][y1] != 0) break;
            if (canConnectOneTurn(x, y1, x2, y2)) return true;
        }
        // 下
        for (int x = x1 + 1; x < grid.length; x++) {
            if (grid[x][y1] != 0) break;
            if (canConnectOneTurn(x, y1, x2, y2)) return true;
        }
        // 左
        for (int y = y1 - 1; y >= 0; y--) {
            if (grid[x1][y] != 0) break;
            if (canConnectOneTurn(x1, y, x2, y2)) return true;
        }
        // 右
        for (int y = y1 + 1; y < grid[0].length; y++) {
            if (grid[x1][y] != 0) break;
            if (canConnectOneTurn(x1, y, x2, y2)) return true;
        }
        return false;
    }

    private boolean canConnectOneTurn(int mx, int my, int x2, int y2) {
        // 從中介點 (mx,my) 到目標 (x2,y2) 只需 0 或 1 轉
        if (mx == x2 && clearHor(mx, my, y2)) return true;
        if (my == y2 && clearVer(my, mx, x2)) return true;
        if (grid[mx][y2] == 0 && clearHor(mx, my, y2) && clearVer(y2, mx, x2)) return true;
        if (grid[x2][my] == 0 && clearVer(my, mx, x2) && clearHor(x2, my, y2)) return true;
        return false;
    }

    private boolean clearHor(int row, int y1, int y2) {
        int from = Math.min(y1, y2) + 1;
        int to = Math.max(y1, y2) - 1;
        for (int y = from; y <= to; y++) if (grid[row][y] != 0) return false;
        return true;
    }
    private boolean clearVer(int col, int x1, int x2) {
        int from = Math.min(x1, x2) + 1;
        int to = Math.max(x1, x2) - 1;
        for (int x = from; x <= to; x++) if (grid[x][col] != 0) return false;
        return true;
    }

    private void updateUI() {
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                int val = grid[r+1][c+1];
                JButton b = btns[r][c];
                if (val == 0) {
                    b.setText("");
                    b.setEnabled(false);
                    b.setBackground(null);
                } else {
                    b.setText(String.valueOf(val));
                    b.setEnabled(true);
                    b.setBackground(null);
                }
            }
        }
    }

    private boolean isGameClear() {
        for (int r = 1; r <= ROWS; r++)
            for (int c = 1; c <= COLS; c++)
                if (grid[r][c] != 0) return false;
        return true;
    }

    private void showHint() {
        // 簡易提示:找出第一組可消除配對並閃爍
        for (int r1 = 1; r1 <= ROWS; r1++) {
            for (int c1 = 1; c1 <= COLS; c1++) {
                if (grid[r1][c1] == 0) continue;
                for (int r2 = 1; r2 <= ROWS; r2++) {
                    for (int c2 = 1; c2 <= COLS; c2++) {
                        if (grid[r2][c2] == 0) continue;
                        if (r1 == r2 && c1 == c2) continue;
                        if (canConnect(r1, c1, r2, c2)) {
                            flashHint(r1-1, c1-1, r2-1, c2-1);
                            return;
                        }
                    }
                }
            }
        }
        JOptionPane.showMessageDialog(this, "無可消除配對,請重排!");
    }

    private void flashHint(int r1, int c1, int r2, int c2) {
        JButton b1 = btns[r1][c1], b2 = btns[r2][c2];
        Color orig1 = b1.getBackground(), orig2 = b2.getBackground();
        b1.setBackground(Color.YELLOW);
        b2.setBackground(Color.YELLOW);
        new Timer(1500, e -> { b1.setBackground(orig1); b2.setBackground(orig2); }).start();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new LinkLinkGame().setVisible(true));
    }
}

擴充建議

  • 圖片資源:將數字改為 ImageIcon,準備 15 張圖片對應類型。
  • 連線動畫:在 canConnect 回傳路徑座標列表,用 Timer 逐步繪製連線。
  • 關卡系統:增加網格大小、圖案種類、時間限制、道具 (炸彈、提示)。
  • 高分紀錄:使用 Preferences 或 SQLite 持久化。
  • 撤銷功能:紀錄每步狀態快照,支援 Ctrl+Z。

效能優化提示

  • 連線判定頻繁呼叫,可快取 clearHor/Ver 結果或預計算連通性矩陣。
  • 大網格 (10x10 以上) 建議改用 BFS 尋找最短轉彎路徑,時間複雜度 O(V+E)。
  • UI 更新僅重繪變更格子,避免全盤 updateUI()。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved