用classtri.js中的類和方法,全部替換掉trirhombus2453claude.html中的方法,點擊棋盤時的坐標改為classtri.js的方式,更改坐標與棋譜的轉換方法,[(0,0),(1,0)]將是第一手藍色棋子所放置的坐標。同時更改相應方法:記分方法、判定是否存在空洞的方法、給出所選棋子可能可以落子的坐標方法等。
代码: 全选
這是一個非常棒的重構計畫!將原本基於 UI 狀態的邏輯抽離,換成 `classtri.js` 中極致優化的純數據結構(`Int32Array`、位元運算與一維陣列 Flood-fill),能大幅提升效能並讓代碼更整潔。
為了實現坐標系統的轉換(原本的 `idx, n` 轉為 `c, r`,即 $x, y$),並將第一手藍棋定義為 `[(0,0), (1,0)]`,我們需要更新幾何映射,並將原本的判定方法替換為 `gameLogic` 實例的方法。
以下是明確的修改步驟與替換代碼:
---
### 1. 引入核心類別 (頂部插入)
在 `<script>` 標籤的最上方(`const I18N = { ... }` 之前或之後皆可),**直接貼上** `classtri.js` 裡的 `UltraFastTriangleGrid` 與 `TriangleStrategyGame` 兩個完整的 Class。
### 2. 替換全域遊戲狀態變數
找到定義 `let occupied = new Set();` 的地方,將其替換為新的引擎實例:
**【找到代碼】**
```javascript
let isFirstMove = true;
let occupied = new Set();
```
**【替換為】**
```javascript
let isFirstMove = true;
let gameLogic = new TriangleStrategyGame(); // 替換 occupied
let bluePieceRecords = []; // 供新版計分系統追蹤藍色棋子
```
並在 `initBoardState` 函數中,重置狀態:
**【找到代碼】**
```javascript
occupied = new Set();
```
**【替換為】**
```javascript
gameLogic = new TriangleStrategyGame();
bluePieceRecords = [];
```
---
### 3. 重寫幾何坐標與點擊映射 (核心轉換)
原本的 `getPairTri`、`calcDropPos`、`processBoardClick` 是基於 `idx` 與 `N`。現在我們將其替換為基於 `r (Row, Y軸)` 與 `c (Column, X軸)` 的映射。
**【刪除舊方法】**
請將原本的 `getPairTri`、`calcDropPos`、`processBoardClick`、`isRightPointing`、`getNeighborIdx`、`getNeighbors`、`formatCoord` **全部刪除**。
**【替換為以下新方法】**
```javascript
// --- 新版幾何與坐標轉換 ---
// (c, r) 相當於 (x, y)。為了符合 classtri.js 邏輯:
// c 為水平軸,間距為 10.3923;r 為垂直軸,間距為 18。
const H_SPACING = 10.3923; // 31.1769 / 3
const V_SPACING = 18;
function isRightPointing(r, c) {
return ((r + c) & 1) === 0; // Parity 0 指向右/上
}
// 獲取另外半個三角形的坐標 (對接 classtri.js)
function getPairTri(c, r, pieceId) {
let parity = (r + c) & 1;
if (pieceId === 0) { // 藍色 (水平)
return parity === 0 ? { c: c + 1, r: r } : { c: c - 1, r: r };
} else if (pieceId === 1) { // 紅色 (垂直,偶數在上方)
return parity === 0 ? { c: c, r: r + 1 } : { c: c, r: r - 1 };
} else { // 橙色 (垂直,奇數在上方)
return parity === 1 ? { c: c, r: r + 1 } : { c: c, r: r - 1 };
}
}
// 根據 r, c 計算 SVG 像素中心點
function getTriCenter(c, r) {
let isRight = isRightPointing(r, c);
let cx = c * H_SPACING;
let cy = r * V_SPACING;
// 微調視覺中心偏移
cx += isRight ? 10.392 : 20.784;
return { cx, cy };
}
// 計算落子動畫與渲染的像素位置
function calcDropPos(c, r, pieceId) {
let { cx, cy } = getTriCenter(c, r);
let isRight = isRightPointing(r, c);
if (pieceId === 0) return { px: isRight ? cx - 10.392 : cx + 10.392, py: cy };
if (pieceId === 1) return { px: isRight ? cx + 5.196 : cx - 5.196, py: isRight ? cy - 9 : cy + 9 };
return { px: isRight ? cx + 5.196 : cx - 5.196, py: isRight ? cy + 9 : cy - 9 };
}
// 點擊事件轉為 r, c 坐標
function processBoardClick(tx, ty) {
let approxC = Math.floor((tx - 10.392) / H_SPACING);
let approxR = Math.round(ty / V_SPACING);
let bestDist = Infinity, bestTri = null;
for (let r = approxR - 2; r <= approxR + 2; r++) {
for (let c = approxC - 2; c <= approxC + 2; c++) {
let { cx, cy } = getTriCenter(c, r);
let dist = Math.hypot(tx - cx, ty - cy);
if (dist < bestDist) {
bestDist = dist;
bestTri = { c, r, cx, cy };
}
}
}
if (bestTri && currentSelectedPiece !== null) attemptDrop(bestTri);
}
// 新版棋譜坐標格式化 [(x1,y1),(x2,y2)]
function formatCoord(c1, r1, c2, r2) {
return `[(${c1},${r1}),(${c2},${r2})]`;
}
```
---
### 4. 落子驗證與空洞偵測 (`attemptDrop`)
刪除原本巨大的 `attemptDrop`,改用 `classtri.js` 提供的 `gameLogic.getValidMoves` 與 `gameLogic._formsHole` 進行嚴格的資料層驗證。
**【刪除舊版】** `detectHole`、`drawHoleOverlay`、`clearHoleOverlay` 函數(不需要了,新演算法直接拒絕無效步)。
**【將 `attemptDrop` 替換為】**
```javascript
function attemptDrop(tri) {
let pair = getPairTri(tri.c, tri.r, currentSelectedPiece);
let c1 = tri.c, r1 = tri.r;
let c2 = pair.c, r2 = pair.r;
// 1. 檢查是否已被佔用
if (gameLogic.board.has(r1, c1) || gameLogic.board.has(r2, c2)) return;
// 2. 第二手紅棋限制
let roundStart = currentRound === 2 ? 36 : 0;
let movesInRound = historyIndex - roundStart + 1;
if (movesInRound === 1 && currentSelectedPiece !== 1) {
showNotification(getText('secondRedPrompt'));
return;
}
// 3. 合法性與空洞驗證 (對接引擎)
let p1Coord = gameLogic._packCoord(r1, c1);
let p2Coord = gameLogic._packCoord(r2, c2);
if (movesInRound > 0) {
// 利用引擎獲取所有合法步,檢查玩家選的這步是否在其中
let validMoves = gameLogic.getValidMoves(currentSelectedPiece, currentPlayer);
let isValid = validMoves.some(m =>
(m[0] === p1Coord && m[1] === p2Coord) || (m[0] === p2Coord && m[1] === p1Coord)
);
if (!isValid) {
// 細分錯誤提示:相鄰或空洞
if (gameLogic._formsHole(p1Coord, p2Coord)) {
showNotification(getText('noHole'));
} else {
showNotification(getText('mustConnect') + " / " + getText('invalidDrop'));
}
return;
}
}
// 4. 動畫與落子執行
let { px, py } = calcDropPos(c1, r1, currentSelectedPiece);
const useNode = document.createElementNS('http://www.w3.org/2000/svg', 'use');
useNode.setAttribute('href', `#tile${currentSelectedPiece}`);
useNode.setAttribute('class', 'tiledropped');
useNode.setAttribute('fill', TILE_COLORS[currentSelectedPiece]);
document.getElementById('etanidrop').appendChild(useNode);
let startY = currentPlayer === 1 ? (480 - panY) / currentScale + 100 : -panY / currentScale - 100;
let startTime = performance.now();
let pid = currentSelectedPiece;
function dropAnim(time) {
let progress = (time - startTime) / 777;
if (progress > 1) progress = 1;
useNode.setAttribute('transform', `translate(${px.toFixed(3)}, ${(startY + (py - startY) * progress).toFixed(3)})`);
if (progress < 1) {
requestAnimationFrame(dropAnim);
} else {
// 寫入底層引擎
let val = gameLogic._encodeCell(currentPlayer, pid, ++gameLogic.pieceCount);
gameLogic.board.set(r1, c1, val);
gameLogic.board.set(r2, c2, val);
if (pid === 0) bluePieceRecords.push({ r: r1, c: c1, player: currentPlayer });
commitMove(pid, {c: c1, r: r1}, {c: c2, r: r2}, px, py, currentPlayer);
cleanUpSelection();
checkGameEnd();
}
}
requestAnimationFrame(dropAnim);
}
```
---
### 5. 第一手自動落子與 `commitMove` 的格式更新
將坐標格式強制變更為題目要求的 `[(0,0), (1,0)]`(對應 `c=0, r=0` 與 `c=1, r=0`)。
**【找到 `autoPlayFirstBlue` 並替換為】**
```javascript
function autoPlayFirstBlue(player) {
let t1 = { c: 0, r: 0 }; // 坐標 (0,0)
let t2 = { c: 1, r: 0 }; // 坐標 (1,0)
const useNode = document.createElementNS('http://www.w3.org/2000/svg', 'use');
useNode.setAttribute('href', '#tile0');
useNode.setAttribute('class', 'tiledropped');
useNode.setAttribute('fill', '#00BFFF');
document.getElementById('etanidrop').appendChild(useNode);
let startY = player === 1 ? (480 - panY) / currentScale + 100 : -panY / currentScale - 100;
let startTime = performance.now();
function dropAnim(time) {
let progress = (time - startTime) / 777;
if (progress > 1) progress = 1;
useNode.setAttribute('transform', `translate(0, ${(startY * (1 - progress)).toFixed(3)})`);
if (progress < 1) requestAnimationFrame(dropAnim);
else {
// 寫入新引擎
let val = gameLogic._encodeCell(player, 0, ++gameLogic.pieceCount);
gameLogic.board.set(t1.r, t1.c, val);
gameLogic.board.set(t2.r, t2.c, val);
bluePieceRecords.push({ r: t1.r, c: t1.c, player: player });
piecesLeft[player][0]--;
isFirstMove = false;
let notation = formatCoord(t1.c, t1.r, t2.c, t2.r); // [(0,0),(1,0)]
moveHistory.push({ notation, pid: 0, t1, t2, px: 0, py: 0, player });
historyIndex++;
cleanUpSelection();
showNotification(getText('secondRedPrompt'));
}
}
requestAnimationFrame(dropAnim);
}
```
**【找到 `commitMove` 替換 notation 顯示】**
```javascript
function commitMove(pid, t1, t2, px, py, player) {
piecesLeft[player][pid]--;
isFirstMove = false;
// 使用新版坐標系統字串
let notation = formatCoord(t1.c, t1.r, t2.c, t2.r);
moveHistory = moveHistory.slice(0, historyIndex + 1);
moveHistory.push({ notation, pid, t1, t2, px, py, player });
historyIndex++;
showNotification(`落子: ${notation}`);
}
```
---
### 6. 計分系統對接 (`calculateScores`)
將 UI 依賴的計分與新版引擎對接。原本的樹狀檢測很耗效能,現在轉交給引擎。
**【刪除舊版計分代碼】** 刪除 `evaluateBluePieceTree`、`evaluateSideTree`、`getBluePieceScore`、`getBPLeftCol`、`getBPRightCol`。
**【找到 `calculateScores` 並替換為】**
```javascript
function calculateScores() {
// 假設 classtri.js 中的 calculateScores 已經可以正確運算:
// (需確保 classtri.js 內藍色棋子清單有與 bluePieceRecords 介接)
// 若你採用 classtri.js 內部掃描,可以直接呼叫:
let scores = gameLogic.calculateScores();
return { s1: scores[0], s2: scores[1] };
}
```
*(注意:因為 `classtri.js` 裡的 `calculateScores` 標註了 `// scores[0] += checkLayer(bluePiece.r, bluePiece.c, 0, 1);` 為概念展示,如果你需要它真實運作,需在 `classtri.js` 中將 `bluePieces` 陣列開放並在 `attemptDrop` 時 `push` 紀錄。上方步驟 4 已為你加入了 `bluePieceRecords` 清單以供擴充。)*