代码: 全选
這是一個非常精彩且具有深度的棋盤遊戲專案!你對記分規則(尤其是責任傳遞機制)的設計非常有創意。針對你的四項需求,我已經整理了相應的代碼修復與改進方案。
以下是需要修改和替換的代碼,請按照指示覆蓋原有的部分:
### 一、 頁面樣式修復與導入/導出窗口美化 (CSS & HTML)
**1. 替換 `<style>` 標籤中的相關 CSS:**
請找到並替換 `.piece-count`, `@media (max-width: 768px)`, `#export-modal` 等相關樣式,並加入關閉按鈕的新樣式:
```css
/* 替換或新增以下 CSS */
.piece-count {
position: static;
font-size: 26px; /* 調大字體 */
font-weight: 900;
color: #333;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.15); /* 增加立體感 */
}
@media (max-width: 768px) {
.piece-count {
font-size: 22px; /* 手機端稍小但依然清晰 */
}
.piece-area {
flex-direction: row;
align-items: center;
justify-content: space-evenly;
padding: 0;
width: 100%;
}
#opp-control .piece-wrapper,
#my-control .piece-wrapper {
flex-direction: row; /* 強制水平分布 */
align-items: center;
justify-content: center;
gap: 8px;
}
}
/* 彈出視窗現代化樣式 */
#export-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(3px);
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: #fff;
padding: 25px;
border-radius: 16px;
width: 380px;
max-width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
position: relative;
font-family: inherit;
}
.modal-content h3 {
margin-top: 0;
color: #333;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
}
.modal-section {
margin-top: 15px;
}
.modal-section label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
.textarea-group {
display: flex;
gap: 10px;
height: 70px; /* 統一高度 */
}
.textarea-group textarea {
flex: 1;
resize: none;
border: 1px solid #ccc;
border-radius: 6px;
padding: 8px;
font-family: monospace;
}
.textarea-group button {
cursor: pointer;
padding: 0 15px;
border: none;
background: var(--p1-color);
color: white;
border-radius: 6px;
font-weight: bold;
transition: background 0.2s;
}
.textarea-group button:hover {
background: #003d82;
}
.close-btn {
position: absolute;
top: 15px;
right: 15px;
width: 24px;
height: 24px;
cursor: pointer;
fill: #888;
transition: fill 0.2s;
}
.close-btn:hover {
fill: #333;
}
```
**2. 替換 `#floating-tools` 與 `#export-modal` 的 HTML 結構:**
將工具欄(新增 Debug 按鈕)與彈出視窗的 HTML 替換為以下內容:
```html
<div id="floating-tools">
<svg class="tool-btn" onclick="downloadBoardSVG()" viewBox="0 0 24 24" title="下載棋盤">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" fill="#333" />
</svg>
<svg class="tool-btn" onclick="openExportModal()" viewBox="0 0 24 24" title="導出/導入">
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z" fill="#333" />
</svg>
<svg class="tool-btn tool-btn-undo" onclick="undoMove()" viewBox="0 0 24 24" title="後退">
<path d="M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z" fill="#333" />
</svg>
<svg class="tool-btn tool-btn-redo" onclick="redoMove()" viewBox="0 0 24 24" title="前進" transform="scale(-1,1)">
<path d="M11.5 8C16.15 8 20.08 11.03 21.43 15.22l-2.37.78c-1.05-3.19-4.06-5.5-7.6-5.5-1.95 0-3.73.72-5.12 1.88L10 16H1V7l3.6 3.6C6.45 8.99 8.85 8 11.5 8z" fill="#333" />
</svg>
<svg class="tool-btn" id="debug-btn" onclick="toggleDebug()" viewBox="0 0 24 24" title="Debug Blue">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" fill="#333" />
</svg>
</div>
<div id="export-modal" style="display: none">
<div class="modal-content">
<svg class="close-btn" onclick="document.getElementById('export-modal').style.display = 'none'" viewBox="0 0 24 24">
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<h3>棋譜 導出 / 導入</h3>
<div class="modal-section">
<label>導出當前棋譜:</label>
<div class="textarea-group">
<textarea id="export-text" readonly></textarea>
<button onclick="copyExport()">複製</button>
</div>
</div>
<div class="modal-section">
<label>導入棋譜:</label>
<div class="textarea-group">
<textarea id="import-text"></textarea>
<button onclick="submitImport()">導入</button>
</div>
</div>
</div>
</div>
```
---
### 二、 記譜格式改進與坐標保留 (JavaScript)
請在 `<script>` 中替換原有的 `getIdxChar`,擴大 `findValidMove` 的搜尋範圍,並新增 `getOldFormat` 函數:
```javascript
// 支持無限擴展的字母坐標 (A, B... Z, AA, AB...)
function getIdxChar(i) {
if (i === 0) return '';
let isUpper = i > 0;
let n = Math.abs(i);
let colName = '';
while (n > 0) {
let rem = (n - 1) % 26;
colName = String.fromCharCode((isUpper ? 65 : 97) + rem) + colName;
n = Math.floor((n - 1) / 26);
}
return colName;
}
// 產生舊版坐標格式以供驗證
function getOldFormat(t1, t2) {
return `[(${getIdxChar(t1.idx)},${t1.N}),(${getIdxChar(t2.idx)},${t2.N})]`;
}
// 替換 commitMove 函式中的打印部分
function commitMove(pid, t1, t2, px, py, player) {
occupied.add(`${t1.idx},${t1.N}`);
occupied.add(`${t2.idx},${t2.N}`);
piecesLeft[player][pid]--;
isFirstMove = false;
let notation = encodeNotation(pid, t1, t2);
moveHistory = moveHistory.slice(0, historyIndex + 1);
moveHistory.push({ notation, pid, t1, t2, px, py, player });
historyIndex++;
// 同時顯示新格式與舊格式坐標
document.getElementById('test-box').innerText = `落子: ${notation} ${getOldFormat(t1, t2)}`;
}
// 替換 findValidMove 函式,擴大搜尋範圍以支持超出 Z 的坐標
function findValidMove(notationStr, player) {
for (let pid = 0; pid < 3; pid++) {
if (piecesLeft[player][pid] <= 0) continue;
// 範圍擴大到 80,確保能解析 AA, AB 等坐標
for (let n = -80; n <= 80; n++) {
for (let idx = -80; idx <= 80; idx++) {
if (idx === 0) continue;
let t1 = { idx, N: n, isRight: isRightPointing(idx, n) };
let t2 = getPairTri(t1, pid);
if (occupied.has(`${t1.idx},${t1.N}`) || occupied.has(`${t2.idx},${t2.N}`)) continue;
let nbs = [
...getNeighbors(t1.idx, t1.N, t1.isRight),
...getNeighbors(t2.idx, t2.N, isRightPointing(t2.idx, t2.N))
];
if (nbs.some(nb => occupied.has(`${nb.idx},${nb.N}`))) {
if (encodeNotation(pid, t1, t2) === notationStr) {
let cx = t1.isRight
? (t1.idx > 0 ? t1.idx - 1 : t1.idx) * 31.1769 + 10.392
: (t1.idx > 0 ? t1.idx - 1 : t1.idx) * 31.1769 + 20.784;
t1.cx = cx;
t1.cy = n * 18;
return { t1, t2, pid, pos: calcDropPos(t1, pid) };
}
}
}
}
}
return null;
}
```
---
### 三、 遞迴記分系統與 Debug 模式 (JavaScript)
這是整個改進的核心。為了實現「責任傳遞」與層級遞迴,請在 `<script>` 中加入以下邏輯,並更新 UI 計算。
**1. 加入記分核心演算法:**
```javascript
// --- 記分系統與責任傳遞邏輯 ---
function getTriCenter(idx, n, isRight) {
let halfWidth = 31.1769;
let k_left = idx > 0 ? idx - 1 : idx;
return isRight ? k_left * halfWidth + 10.392 : k_left * halfWidth + 20.784;
}
function getPieceTargets(piece, player) {
let t1 = piece.t1, t2 = piece.t2;
t1.cx = getTriCenter(t1.idx, t1.N, t1.isRight);
t2.cx = getTriCenter(t2.idx, t2.N, t2.isRight);
// 比較 cx 區分左右半部
let tL = t1.cx < t2.cx ? t1 : t2;
let tR = t1.cx < t2.cx ? t2 : t1;
// P1 向下 (N+1),P2 向上 (N-1)
let dir = player === 1 ? 1 : -1;
return {
leftTarget: { idx: tL.idx, N: tL.N + dir },
rightTarget: { idx: tR.idx, N: tR.N + dir }
};
}
function getPieceAt(idx, n) {
return moveHistory.find(m => (m.t1.idx === idx && m.t1.N === n) || (m.t2.idx === idx && m.t2.N === n));
}
// 遞迴解析目標三角形是否滿足所需層級與顏色
function resolveTarget(targetTri, expectedPid, player, layer, visited) {
let key = `${targetTri.idx},${targetTri.N},${expectedPid},${layer}`;
if (visited.has(key)) return false; // 防止循環
visited.add(key);
let p = getPieceAt(targetTri.idx, targetTri.N);
if (!p) return false;
if (p.pid === 0) {
// 若遇到藍色棋子,則它必須承接並獨自完成這個層級的任務
return checkBluePieceLayer(p, player, layer, visited);
} else if (p.pid === expectedPid) {
// 顏色正確。若是第1層,直接達成。
if (layer === 1) return true;
// 若層級 > 1,紅/橙棋子將責任繼續往下傳遞(只要其下方任一分支完成即可)
let targets = getPieceTargets(p, player);
return resolveTarget(targets.leftTarget, expectedPid, player, layer - 1, visited) ||
resolveTarget(targets.rightTarget, expectedPid, player, layer - 1, visited);
}
return false;
}
// 檢查藍色棋子是否能完成指定層級
function checkBluePieceLayer(bluePiece, player, layer, visited) {
let targets = getPieceTargets(bluePiece, player);
// 左下需要橙(2)或紅(1),右下需要紅(1)或橙(2)
let reqLeft = player === 1 ? 2 : 1;
let reqRight = player === 1 ? 1 : 2;
let okL = resolveTarget(targets.leftTarget, reqLeft, player, layer, visited);
let okR = resolveTarget(targets.rightTarget, reqRight, player, layer, visited);
return okL && okR; // 必須左右皆滿足
}
function getBluePieceScore(bluePiece, player) {
let score = 0;
for (let k = 1; k <= 20; k++) { // 避免無限遞迴,上限設20層
let visited = new Set();
if (checkBluePieceLayer(bluePiece, player, k, visited)) {
score++;
} else {
break;
}
}
return score;
}
function calculateScores() {
let s1 = 0, s2 = 0;
for (let m of moveHistory) {
if (m.pid === 0) {
if (m.player === 1) s1 += getBluePieceScore(m, 1);
if (m.player === 2) s2 += getBluePieceScore(m, 2);
}
}
return { s1, s2 };
}
```
**2. 更新 `updateUI` 函式:**
使用新寫的計分系統替換原有的寫法。
```javascript
function updateUI() {
let scores = calculateScores();
document.getElementById('score-1').innerText = scores.s1;
document.getElementById('score-2').innerText = scores.s2;
for (let p = 1; p <= 2; p++) {
for (let c = 0; c < 3; c++) {
let countEl = document.getElementById(`count-${p}-${c}`);
if (countEl) countEl.innerText = piecesLeft[p][c];
let box = document.getElementById(`p${p}-${c}`);
if (piecesLeft[p][c] === 0 || currentPlayer !== p) {
box.style.opacity = '0.5';
box.style.cursor = 'not-allowed';
} else {
box.style.opacity = '1';
box.style.cursor = 'pointer';
}
}
}
}
```
**3. Debug 模式實作 (JavaScript):**
```javascript
let isDebugMode = false;
function toggleDebug() {
isDebugMode = !isDebugMode;
const btn = document.getElementById('debug-btn');
btn.style.background = isDebugMode ? '#ffcc00' : 'rgba(255, 255, 255, 0.8)';
document.getElementById('test-box').innerText = isDebugMode ? 'Debug Mode ON: 點擊藍色棋子查看詳細' : 'Debug Mode OFF';
}
```
最後,請在 `setupBoardClick` 函式內的 `svg.addEventListener('click', function (e) { ... }` 頂部加入攔截邏輯:
```javascript
function setupBoardClick() {
const svg = document.getElementById('etani');
const dropG = document.getElementById('etanidrop');
const pt = svg.createSVGPoint();
svg.addEventListener('click', function (e) {
if (hasDragged) {
hasDragged = false;
return;
}
// --- 新增:Debug 模式攔截點擊事件 ---
if (isDebugMode) {
pt.x = e.clientX; pt.y = e.clientY;
const svgP = pt.matrixTransform(dropG.getScreenCTM().inverse());
const halfWidth = 31.1769;
let k = Math.floor(svgP.x / halfWidth);
let idx = svgP.x > 0 ? k + 1 : k;
if (svgP.x === 0) idx = 1;
let approxN = Math.round(svgP.y / 18);
let bestDist = Infinity, bestTri = null;
for (let n = approxN - 2; n <= approxN + 2; n++) {
let isRight = isRightPointing(idx, n);
let k_left = idx > 0 ? idx - 1 : idx;
let cx = isRight ? k_left * halfWidth + 10.392 : k_left * halfWidth + 20.784;
let cy = n * 18;
let dist = Math.hypot(svgP.x - cx, svgP.y - cy);
if (dist < bestDist) { bestDist = dist; bestTri = { idx, N: n, isRight, cx, cy }; }
}
if (bestTri) {
let p = getPieceAt(bestTri.idx, bestTri.N);
if (p && p.pid === 0) {
let score = getBluePieceScore(p, p.player);
let targets = getPieceTargets(p, p.player);
let pL = getPieceAt(targets.leftTarget.idx, targets.leftTarget.N);
let pR = getPieceAt(targets.rightTarget.idx, targets.rightTarget.N);
let info = `P${p.player} 藍棋分數: ${score} | 左下: ${pL ? (pL.pid===0?'藍':pL.pid===1?'紅':'橙') : '無'} | 右下: ${pR ? (pR.pid===0?'藍':pR.pid===1?'紅':'橙') : '無'}`;
document.getElementById('test-box').innerText = info;
} else {
document.getElementById('test-box').innerText = '這不是藍色棋子';
}
}
return;
}
// --- Debug 結束 ---
if (currentSelectedPiece === null) {
const ctrl = document.getElementById('board-controls');
ctrl.style.display = ctrl.style.display === 'none' ? 'block' : 'none';
return;
}
document.getElementById('board-controls').style.display = 'none';
pt.x = e.clientX;
pt.y = e.clientY;
const svgP = pt.matrixTransform(dropG.getScreenCTM().inverse());
processBoardClick(svgP.x, svgP.y);
});
}
```