Game Devlog

連連看遊戲架構:圖塊匹配演算法

2026 年 5 月 5 日約 10 分鐘閱讀作者:Hao0321 Studio

連連看是看似簡單的小遊戲,但核心邏輯有趣 — 兩個圖塊能消除的條件是「用最多 3 條線(最多兩個轉折)連起來且路徑上沒有其他圖塊」。這個約束讓「能否連線」的判斷不只是 BFS,要考慮路徑形狀。這篇拆解整套實作。

規則回顧

  1. 玩家點兩個相同圖塊
  2. 系統檢查能否用 1、2 或 3 條直線連起來
  3. 路徑可以走出棋盤外圍(重要!)
  4. 路徑不能穿越其他未消除的圖塊

資料結構

// 棋盤:N+2 × M+2 的二維陣列
// 邊界一圈是 0(empty),讓路徑可繞外圍
const board = [
  [0,0,0,0,0,0,0,0,0,0],
  [0,1,2,3,4,5,6,7,8,0],
  [0,2,3,4,5,6,7,8,1,0],
  [0,3,4,5,6,7,8,1,2,0],
  [0,0,0,0,0,0,0,0,0,0],
];
// 0 = 空, 1-N = 圖塊類型

核心:3 條線判斷

能用 1 條線連 → 顯然可以連 2 條 → 顯然可以連 3 條。所以由簡到繁判斷:

1 條線(直連)

function canDirectConnect(a, b) {
  if (a.x === b.x) {
    // 垂直
    const [y1, y2] = [Math.min(a.y, b.y), Math.max(a.y, b.y)];
    for (let y = y1 + 1; y < y2; y++) {
      if (board[y][a.x] !== 0) return false;
    }
    return true;
  }
  if (a.y === b.y) {
    // 水平
    const [x1, x2] = [Math.min(a.x, b.x), Math.max(a.x, b.x)];
    for (let x = x1 + 1; x < x2; x++) {
      if (board[a.y][x] !== 0) return false;
    }
    return true;
  }
  return false;
}

2 條線(一個轉折點)

兩個轉折候選點:(a.x, b.y) 跟 (b.x, a.y):

function canTwoLineConnect(a, b) {
  const corner1 = { x: a.x, y: b.y };
  const corner2 = { x: b.x, y: a.y };

  for (const c of [corner1, corner2]) {
    if (board[c.y][c.x] !== 0) continue;
    if (canDirectConnect(a, c) && canDirectConnect(c, b)) {
      return [a, c, b];
    }
  }
  return null;
}

3 條線(兩個轉折點)

掃所有可能的中間「直線」:

function canThreeLineConnect(a, b) {
  // 嘗試所有水平中間線
  for (let y = 0; y < rows; y++) {
    if (y === a.y || y === b.y) continue;
    const c1 = { x: a.x, y };
    const c2 = { x: b.x, y };
    if (board[c1.y][c1.x] === 0 && board[c2.y][c2.x] === 0) {
      if (canDirectConnect(a, c1)
          && canDirectConnect(c1, c2)
          && canDirectConnect(c2, b)) {
        return [a, c1, c2, b];
      }
    }
  }
  // 同樣嘗試所有垂直中間線
  for (let x = 0; x < cols; x++) {
    /* ... 對稱的實作 ... */
  }
  return null;
}

主判斷函式

function findPath(a, b) {
  if (board[a.y][a.x] !== board[b.y][b.x]) return null;
  if (board[a.y][a.x] === 0) return null;

  if (canDirectConnect(a, b)) return [a, b];
  const two = canTwoLineConnect(a, b);
  if (two) return two;
  return canThreeLineConnect(a, b);
}

提示(Hint)功能

掃所有圖塊組合,找第一對能連的:

function findHint() {
  const tiles = [];
  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      if (board[y][x] !== 0) tiles.push({ x, y });
    }
  }
  for (let i = 0; i < tiles.length; i++) {
    for (let j = i + 1; j < tiles.length; j++) {
      if (findPath(tiles[i], tiles[j])) {
        return [tiles[i], tiles[j]];
      }
    }
  }
  return null;  // 沒有可消的組合 → 死局
}

對 100 個圖塊的盤面這個複雜度約 5,000 次 findPath 呼叫,但每次很快,總時間 < 50ms。

洗牌(Shuffle)邏輯

當沒有可消組合時自動洗牌:

function shuffle() {
  // 收集所有非空圖塊
  const types = [];
  const positions = [];
  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      if (board[y][x] !== 0) {
        types.push(board[y][x]);
        positions.push({ x, y });
      }
    }
  }
  // Fisher-Yates 洗牌
  for (let i = types.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [types[i], types[j]] = [types[j], types[i]];
  }
  // 重新放置
  positions.forEach((p, i) => {
    board[p.y][p.x] = types[i];
  });
  // 確認洗完還有可消組合,沒有就再洗
  if (!findHint()) shuffle();
}

路徑視覺化

連線時畫出實際路徑(不是直接消除):

function drawPath(path) {
  ctx.strokeStyle = '#4A7BF5';
  ctx.lineWidth = 4;
  ctx.lineCap = 'round';
  ctx.beginPath();
  path.forEach((p, i) => {
    const px = p.x * tileSize + tileSize/2;
    const py = p.y * tileSize + tileSize/2;
    if (i === 0) ctx.moveTo(px, py);
    else ctx.lineTo(px, py);
  });
  ctx.stroke();
  // 200ms 後消失,圖塊隨即消除
  setTimeout(() => {
    clearPath();
    removeTiles(path[0], path[path.length-1]);
  }, 200);
}

難度設計

難度盤面圖塊類型數
新手8×68 種
普通10×812 種
困難12×1020 種

結語

連連看的演算法看似簡單,但「3 條線判斷」+「死局偵測」+「路徑視覺化」三個元素湊起來才完整。整套核心邏輯約 200 行 JavaScript,是適合 1 個下午寫完的小專案。

有想要完整實作的,可以寄信 lo246179268@gmail.com