連連看 是經典益智遊戲:在網格中找出兩張相同圖案,且可用不超過三條直線連接 (即最多兩個轉彎) 且路徑無阻擋,即可消除。本文提供基於 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 持久化。clearHor/Ver 結果或預計算連通性矩陣。updateUI()。