連連看是看似簡單的小遊戲,但核心邏輯有趣 — 兩個圖塊能消除的條件是「用最多 3 條線(最多兩個轉折)連起來且路徑上沒有其他圖塊」。這個約束讓「能否連線」的判斷不只是 BFS,要考慮路徑形狀。這篇拆解整套實作。
規則回顧
- 玩家點兩個相同圖塊
- 系統檢查能否用 1、2 或 3 條直線連起來
- 路徑可以走出棋盤外圍(重要!)
- 路徑不能穿越其他未消除的圖塊
資料結構
// 棋盤: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×6 | 8 種 |
| 普通 | 10×8 | 12 種 |
| 困難 | 12×10 | 20 種 |
結語
連連看的演算法看似簡單,但「3 條線判斷」+「死局偵測」+「路徑視覺化」三個元素湊起來才完整。整套核心邏輯約 200 行 JavaScript,是適合 1 個下午寫完的小專案。
有想要完整實作的,可以寄信 lo246179268@gmail.com。