具體算法是,把開始點擊的坐標記下,當拖動時,開始點擊的坐標與原點的連線,跟拖動後坐標與原點的連線的夾角是多少度,則棋子的rotate就改變多少度。只能是「1度」的整數倍。
如果是先手首回合,當手機觸屏雙指或電腦鼠標滾輪縮放時,應能保持棋盤中心不變,也就是說在改變g.etdrop的scale時,也要配合translate以使中心不變。原始中心是translate(240,240)。
當進入第二局時,不應交換雙方分數,也不應仍由粉色方先下,而是讓後手方變成先手方,也就是由綠方先下,綠方將成為第二局的先手方,他的第二局首回合的第一手棋也要實現上面所講的改進。
代码: 全选
這是一套完整的修復與改進方案,完全符合您的四項需求。我們將採用「動態計算首子中心並整體偏移」的策略,以確保所有代碼中的代數坐標系(Exact Geometry)不受破壞,同時在視覺上完美達到中心點對齊與整數度數旋轉的效果。
請依序尋找並替換以下代碼片段:
### 一、 新增遊戲結束對話框與多語系字典
**1. 尋找 HTML 中的 `<div id="arb-result-dialog" ...> ... </div>`,在它的下方加入新的 `end-game-dialog` HTML:**
```html
<div id="arb-result-dialog" class="custom-dialog">
</div>
<div id="end-game-dialog" class="custom-dialog">
<div class="dialog-box">
<h2 id="ui-end-title">遊戲結束</h2>
<p id="ui-end-msg"></p>
<div class="dialog-btn-row">
<button class="menu-btn" style="background: #43b581" onclick="nextGame()" id="ui-end-yes">
再來一局
</button>
<button class="menu-btn" style="background: #555" onclick="document.getElementById('end-game-dialog').style.display = 'none'" id="ui-end-no">
取消
</button>
</div>
</div>
</div>
```
**2. 在 `<script>` 開頭的 `i18n` 字典中,新增對應的多語系文本:**
尋找 `'arb-ways': { zh: '當前局面共有 {n} 種合規下法組合。', en: 'There are {n} valid move combinations.' }`,將其替換為:
```javascript
'arb-ways': { zh: '當前局面共有 {n} 種合規下法組合。', en: 'There are {n} valid move combinations.' },
'ui-end-title': { zh: '遊戲結束', en: 'Game Over' },
'ui-end-yes': { zh: '再來一局', en: 'Play Again' },
'ui-end-no': { zh: '取消', en: 'Cancel' }
```
---
### 二、 更新全局變數
尋找全局變數定義區(約在 `let msgTimeout;` 下方),加入 `startingPlayer` 並修改 `boardTransform`:
**將這段代碼:**
```javascript
let selectedTile = null;
let targetOpponentPieceId = null;
let boardTransform = { tx: 240, ty: 240, scale: 1 };
let lastTurnPieces = [];
```
**替換為:**
```javascript
let selectedTile = null;
let targetOpponentPieceId = null;
let boardTransform = { tx: 240, ty: 240, scale: 1, angle: 0 };
let startingPlayer = 1; // 記錄當前局的先手方
let lastTurnPieces = [];
```
---
### 三、 重寫 RenderBoard 實現棋子完美居中對齊
為了讓第一手棋完美置中,我們將在 SVG 內動態建立一個 `<g id="etanicontent">`,根據第一手棋的幾何中心將整個畫面偏移至 `(0,0)`。這不會影響後台精準的代數計算。
**將整個 `function renderBoard() { ... }` 函數(包含其內部所有代碼)替換為:**
```javascript
function renderBoard() {
etanidrop.innerHTML = '';
// 建立內容群組,用以抵消第一手棋的中心點,使其完美居於(0,0)
const etanicontent = document.createElementNS('http://www.w3.org/2000/svg', 'g');
etanicontent.setAttribute('id', 'etanicontent');
let offset = { x: 0, y: 0 };
let firstPiece = pieces[0] || tempPieces[0];
if (firstPiece) {
const centerExact = [
firstPiece.vertices.reduce((s, v) => s + v[0], 0) / 4,
firstPiece.vertices.reduce((s, v) => s + v[1], 0) / 4,
firstPiece.vertices.reduce((s, v) => s + v[2], 0) / 4,
firstPiece.vertices.reduce((s, v) => s + v[3], 0) / 4
];
offset = exactToScreen(centerExact);
}
etanicontent.setAttribute('transform', `translate(${-offset.x.toFixed(3)}, ${-offset.y.toFixed(3)})`);
etanidrop.appendChild(etanicontent);
pieces.concat(tempPieces).forEach(p => {
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#' + p.svgId);
use.setAttribute('transform', getTransformString(p.svgId, p.vertices));
use.classList.add('placed-piece');
if (
p.owner !== currentPlayer &&
tempPieces.length < (turnNumber === 1 || pieces.length === N_PIECES * 6 - 1 ? 1 : 2)
) {
use.classList.remove('placed-piece');
use.classList.add('board-target');
use.onclick = e => {
e.stopPropagation();
handleTargetClick(p.id);
};
}
etanicontent.appendChild(use);
});
ghosts.forEach((g, i) => {
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', '#' + g.svgId);
use.setAttribute('transform', getTransformString(g.svgId, g.vertices));
use.classList.add('ghost-piece');
use.setAttribute('fill-opacity', '0.36');
use.onclick = e => {
e.stopPropagation();
commitGhost(i);
};
etanicontent.appendChild(use);
});
if (targetOpponentPieceId) {
let tp = pieces.find(x => x.id === targetOpponentPieceId);
if (tp) {
const centerExact = [
tp.vertices.reduce((s, v) => s + v[0], 0) / 4,
tp.vertices.reduce((s, v) => s + v[1], 0) / 4,
tp.vertices.reduce((s, v) => s + v[2], 0) / 4,
tp.vertices.reduce((s, v) => s + v[3], 0) / 4
];
const c = exactToScreen(centerExact);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', c.x.toFixed(3));
circle.setAttribute('cy', c.y.toFixed(3));
circle.setAttribute('r', '3');
circle.setAttribute('fill', '#00bfff');
circle.setAttribute('stroke', 'none');
etanicontent.appendChild(circle);
}
}
if (tempPieces.length > 0) {
tempPieces.forEach(tp => {
const centerExact = [
tp.vertices.reduce((s, v) => s + v[0], 0) / 4,
tp.vertices.reduce((s, v) => s + v[1], 0) / 4,
tp.vertices.reduce((s, v) => s + v[2], 0) / 4,
tp.vertices.reduce((s, v) => s + v[3], 0) / 4
];
const c = exactToScreen(centerExact);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', c.x.toFixed(3));
circle.setAttribute('cy', c.y.toFixed(3));
circle.setAttribute('r', '5');
circle.setAttribute('fill', '#ffffff');
circle.setAttribute('stroke', '#ff0000');
circle.setAttribute('stroke-width', '1');
circle.style.cursor = 'pointer';
circle.style.pointerEvents = 'all';
circle.onclick = e => {
e.stopPropagation();
pickupTempPiece(tp.id);
};
etanicontent.appendChild(circle);
const pieceUse = Array.from(etanicontent.children).find(
el =>
el.getAttribute('href') === '#' + tp.svgId &&
el.getAttribute('transform') === getTransformString(tp.svgId, tp.vertices)
);
if (pieceUse) {
pieceUse.style.pointerEvents = 'all';
pieceUse.style.cursor = 'pointer';
pieceUse.onclick = e => {
e.stopPropagation();
pickupTempPiece(tp.id);
};
}
});
} else if (!selectedTile && !targetOpponentPieceId) {
lastTurnPieces.forEach(tp => {
const centerExact = [
tp.vertices.reduce((s, v) => s + v[0], 0) / 4,
tp.vertices.reduce((s, v) => s + v[1], 0) / 4,
tp.vertices.reduce((s, v) => s + v[2], 0) / 4,
tp.vertices.reduce((s, v) => s + v[3], 0) / 4
];
const c = exactToScreen(centerExact);
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', c.x.toFixed(3));
circle.setAttribute('cy', c.y.toFixed(3));
circle.setAttribute('r', '3');
circle.setAttribute('fill', '#ffffff');
circle.setAttribute('stroke', 'none');
etanicontent.appendChild(circle);
});
}
allRings.forEach(ring => {
let dStr = ring.path.map((p, index) => {
const centerExact = [
p.vertices.reduce((s, v) => s + v[0], 0) / 4,
p.vertices.reduce((s, v) => s + v[1], 0) / 4,
p.vertices.reduce((s, v) => s + v[2], 0) / 4,
p.vertices.reduce((s, v) => s + v[3], 0) / 4
];
const c = exactToScreen(centerExact);
return (index === 0 ? 'M ' : 'L ') + c.x.toFixed(3) + ' ' + c.y.toFixed(3);
}).join(' ') + ' Z';
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
pathEl.setAttribute('d', dStr);
pathEl.setAttribute('fill', 'none');
pathEl.setAttribute('stroke', ring.scorer === 1 ? '#8b0000' : '#006400');
pathEl.setAttribute('stroke-width', '3');
pathEl.setAttribute('stroke-linejoin', 'round');
pathEl.setAttribute('stroke-linecap', 'round');
pathEl.style.pointerEvents = 'none';
etanicontent.appendChild(pathEl);
});
}
```
---
### 四、 完善棋盤互動:加入旋轉與優化縮放
在代碼最下方,尋找 `// Pan & Zoom` 到 `function updateTransform()` 的部分,**將整個互動監聽事件替換為包含旋轉機制的代碼:**
**將這段代碼:**
```javascript
// Pan & Zoom
let isDragging = false;
let startX, startY;
boardSvg.addEventListener('pointerdown', e => {
//...略
function updateTransform() {
etanidrop.setAttribute(
'transform',
`translate(${boardTransform.tx.toFixed(3)},${boardTransform.ty.toFixed(3)}) scale(${boardTransform.scale.toFixed(3)})`
);
}
```
**替換為:**
```javascript
// Pan, Zoom & Rotate
let isDragging = false;
let startX, startY;
let rotating = false;
let startAngle = 0;
let baseAngle = 0;
boardSvg.addEventListener('pointerdown', e => {
if (pieces.length === 0 && tempPieces.length === 0) return;
// 如果是首回合的第一子,單點拖動改為旋轉
if (turnNumber === 1 && pieces.length === 0 && tempPieces.length === 1) {
rotating = true;
const rect = boardSvg.getBoundingClientRect();
const cx = rect.left + boardTransform.tx;
const cy = rect.top + boardTransform.ty;
startAngle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI);
baseAngle = boardTransform.angle || 0;
return;
}
isDragging = true;
startX = e.clientX - boardTransform.tx;
startY = e.clientY - boardTransform.ty;
});
boardSvg.addEventListener('pointermove', e => {
if (rotating) {
const rect = boardSvg.getBoundingClientRect();
const cx = rect.left + boardTransform.tx;
const cy = rect.top + boardTransform.ty;
let currentMouseAngle = Math.atan2(e.clientY - cy, e.clientX - cx) * (180 / Math.PI);
let deltaAngle = currentMouseAngle - startAngle;
// 若棋子已被翻轉,修正旋轉方向對應
let tp = tempPieces[0];
if (tp && tp.isFlipped) {
deltaAngle = -deltaAngle;
}
let newAngle = Math.round(baseAngle + deltaAngle);
boardTransform.angle = newAngle;
updateTransform();
// 實時顯示整數角度
let box = document.getElementById('message-box');
box.innerText = (currentLang === 'zh' ? '旋轉角度 / Rotate: ' : 'Rotate: ') + newAngle + '°';
box.style.display = 'block';
clearTimeout(msgTimeout);
return;
}
if (!isDragging) return;
boardTransform.tx = e.clientX - startX;
boardTransform.ty = e.clientY - startY;
updateTransform();
});
boardSvg.addEventListener('pointerup', () => {
if (rotating) {
rotating = false;
document.getElementById('message-box').style.display = 'none';
}
isDragging = false;
});
boardSvg.addEventListener('pointerleave', () => {
if (rotating) {
rotating = false;
document.getElementById('message-box').style.display = 'none';
}
isDragging = false;
});
// 電腦滑鼠滾輪縮放
boardSvg.addEventListener('wheel', e => {
e.preventDefault();
if (pieces.length === 0 && tempPieces.length === 0) return;
const ds = e.deltaY > 0 ? 0.9 : 1.1;
const nextScale = boardTransform.scale * ds;
if (nextScale < 0.2 || nextScale > 5) return;
// 若為首回合,保持畫面中心 (240, 240) 為縮放基準,不隨滑鼠位置偏移
if (turnNumber === 1 && pieces.length === 0 && tempPieces.length === 1) {
const cx = 240;
const cy = 240;
boardTransform.tx = cx - (cx - boardTransform.tx) * ds;
boardTransform.ty = cy - (cy - boardTransform.ty) * ds;
} else {
const rect = boardSvg.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
boardTransform.tx = mx - (mx - boardTransform.tx) * ds;
boardTransform.ty = my - (my - boardTransform.ty) * ds;
}
boardTransform.scale = nextScale;
updateTransform();
});
// 手機觸控雙指縮放
let pinchStartDist = 0;
boardSvg.addEventListener('touchstart', e => {
if (e.touches.length === 2 && (pieces.length > 0 || tempPieces.length > 0)) {
isDragging = false;
rotating = false;
pinchStartDist = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
}
});
boardSvg.addEventListener('touchmove', e => {
if (e.touches.length === 2 && (pieces.length > 0 || tempPieces.length > 0)) {
e.preventDefault();
const dist = Math.hypot(
e.touches[0].clientX - e.touches[1].clientX,
e.touches[0].clientY - e.touches[1].clientY
);
const ds = dist / pinchStartDist;
const nextScale = boardTransform.scale * ds;
if (nextScale > 0.2 && nextScale < 5) {
if (turnNumber === 1 && pieces.length === 0 && tempPieces.length === 1) {
const cx = 240;
const cy = 240;
boardTransform.tx = cx - (cx - boardTransform.tx) * ds;
boardTransform.ty = cy - (cy - boardTransform.ty) * ds;
} else {
const rect = boardSvg.getBoundingClientRect();
const mx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left;
const my = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top;
boardTransform.tx = mx - (mx - boardTransform.tx) * ds;
boardTransform.ty = my - (my - boardTransform.ty) * ds;
}
boardTransform.scale = nextScale;
updateTransform();
}
pinchStartDist = dist;
}
},
{ passive: false }
);
function updateTransform() {
etanidrop.setAttribute(
'transform',
`translate(${boardTransform.tx.toFixed(3)},${boardTransform.ty.toFixed(3)}) scale(${boardTransform.scale.toFixed(3)}) rotate(${boardTransform.angle || 0})`
);
}
```
---
### 五、 更新遊戲結束彈窗與切換先手方邏輯
尋找並替換 `checkEndGame()` 與 `resetGame(swap)` 函數。並加入新的 `nextGame()` 函數處理新局。
**將這段代碼:**
```javascript
function checkEndGame() {
if (pieces.length === N_PIECES * 6) {
let msg = document.body.classList.contains('en')
? `Game Over!\nScore: P1 ${scores[1]} - P2 ${scores[2]}\n${scores[1] > scores[2] ? 'P1 Wins!' : scores[2] > scores[1] ? 'P2 Wins!' : 'Draw!'}\n\nSwap turns and play again?`
: `遊戲結束!\n總分: 先手方 ${scores[1]} - 後手方 ${scores[2]}\n${scores[1] > scores[2] ? '先手方贏了!' : scores[2] > scores[1] ? '後手方贏了!' : '平局!'}\n\n是否換先再來一局?`;
if (confirm(msg)) {
resetGame(true);
}
}
}
function resetGame(swap) {
pieces = [];
tempPieces = [];
ghosts = [];
piecesCount = {
tile0: N_PIECES,
tile1: N_PIECES,
tile2: N_PIECES,
tile3: N_PIECES,
tile4: N_PIECES,
tile5: N_PIECES
};
isFlipped = { tile0: false, tile1: false, tile2: false, tile3: true, tile4: true, tile5: true };
turnNumber = 1;
scoredVictims.clear();
lastTurnPieces = [];
allRings = []; // 清空之前的頂鑫環
if (swap) {
scores = { 1: scores[2], 2: scores[1] };
} else {
scores = { 1: 0, 2: 0 };
}
currentPlayer = 1;
applyRotation();
boardTransform = { tx: 240, ty: 240, scale: 1 };
updateTransform();
updateUI();
renderBoard();
}
```
**替換為:**
```javascript
function checkEndGame() {
if (pieces.length === N_PIECES * 6) {
let msg = currentLang === 'en'
? `Score: P1 ${scores[1]} - P2 ${scores[2]}<br/><br/>${scores[1] > scores[2] ? 'P1 Wins!' : scores[2] > scores[1] ? 'P2 Wins!' : 'Draw!'}`
: `總分: 先手方 ${scores[1]} - 後手方 ${scores[2]}<br/><br/>${scores[1] > scores[2] ? '先手方贏了!' : scores[2] > scores[1] ? '後手方贏了!' : '平局!'}`;
document.getElementById('ui-end-msg').innerHTML = msg;
document.getElementById('end-game-dialog').style.display = 'flex';
}
}
function nextGame() {
document.getElementById('end-game-dialog').style.display = 'none';
startingPlayer = startingPlayer === 1 ? 2 : 1; // 交換先手方
resetGame(false);
saveState(); // 紀錄新局初始狀態
}
function resetGame(swapScore) {
pieces = [];
tempPieces = [];
ghosts = [];
piecesCount = { tile0: N_PIECES, tile1: N_PIECES, tile2: N_PIECES, tile3: N_PIECES, tile4: N_PIECES, tile5: N_PIECES };
isFlipped = { tile0: false, tile1: false, tile2: false, tile3: true, tile4: true, tile5: true };
turnNumber = 1;
scoredVictims.clear();
lastTurnPieces = [];
allRings = [];
// 需求:不應交換雙方分數(新的一局將清零重新計算)
scores = { 1: 0, 2: 0 };
currentPlayer = startingPlayer;
applyRotation();
boardTransform = { tx: 240, ty: 240, scale: 1, angle: 0 }; // 確保旋轉角度歸零
updateTransform();
updateUI();
renderBoard();
}
```