共享会话
改進
分享于 2026年9月11日 07:31改進 在「工具欄」中,「導出棋譜」改成「導出」,「下載局面」改成「下載」。把「導出棋譜」的icon改成「文本文件」的svg圖標。
取消島的點擊事件,島將不可點擊。當有一個棋子選中時,只有點到正確的空位,它才會飛過去。當沒有點擊到正確的位置,則消息欄會給出提示「當棋子飛到另一個島時,必須在島的同一位置」,2s後恢復之前的文字。當點到另一個島的己方棋子,則是選中那個棋子,之前的棋子取消選中。
回退或前進,AI都會自動切換成人類。
棋盤線改為正三角形,以當前的弧線端點為正三角形的端點,中間的內切圓也要相應縮小。線要加粗一些。
當勝利同時形成多條線時,就顯示多條勝利線。
回答要求:明確給出所要修改的地方和所要替換的代碼。
QuanAI下面按原文件中的位置逐項修改。所有代碼均可直接搜尋原代碼後替換。
1. 工具欄文字及「導出」圖標
1.1 將導出圖標改為文本文件 SVG
搜尋:
<symbol id="icon-export" viewBox="0 0 24 24">
<path d="M7 3h7l4 4v14H7V3Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" />
<path
d="M14 3v5h5M12 11v7m0 0-3-3m3 3 3-3"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</symbol>替換為:
<symbol id="icon-export" viewBox="0 0 24 24">
<path
d="M6 2.5h8l4 4v15H6v-19Z"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
/>
<path
d="M14 2.5v5h5M9 11h6M9 15h6M9 19h4"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</symbol>這個圖標為帶有多行文字的文本文件,不再包含下載箭頭。
1.2 修改工具欄預設文字
搜尋:
<button id="exportButton" class="tool-button" data-i18n-title="exportRecord">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#icon-export"></use></svg>
<span data-i18n="exportRecord">導出棋譜</span>
</button>
<button id="downloadButton" class="tool-button" data-i18n-title="downloadPosition">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#icon-download"></use></svg>
<span data-i18n="downloadPosition">下載局面</span>
</button>替換為:
<button id="exportButton" class="tool-button" data-i18n-title="exportRecord">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#icon-export"></use></svg>
<span data-i18n="exportRecord">導出</span>
</button>
<button id="downloadButton" class="tool-button" data-i18n-title="downloadPosition">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#icon-download"></use></svg>
<span data-i18n="downloadPosition">下載</span>
</button>1.3 修改中文翻譯並加入錯誤提示文字
在 I18N.zh 中搜尋:
exportRecord: '導出棋譜',
downloadPosition: '下載局面',替換為:
exportRecord: '導出',
downloadPosition: '下載',再在:
chooseDestination: '已選棋子,請選擇空位',後面加入:
invalidIslandPosition: '當棋子飛到另一個島時,必須在島的同一位置',即:
chooseDestination: '已選棋子,請選擇空位',
invalidIslandPosition: '當棋子飛到另一個島時,必須在島的同一位置',
recordSaved: '棋譜已導出',在 I18N.en 的:
chooseDestination: 'Piece selected — choose an empty destination',後面加入:
invalidIslandPosition: 'When a piece flies to another island, it must land on the same position.',2. 取消島本身的點擊事件
修改後只有島上的七個位置可以點擊,島的其他區域和棋盤空白均不會觸發取消選中或棋子移動。
2.1 修改鼠標樣式
搜尋:
#gameBoard .island-hit,
#gameBoard .hit-slot {
cursor: pointer;
}
#gameBoard.input-locked .island-hit,
#gameBoard.input-locked .hit-slot {
cursor: default;
}替換為:
#gameBoard .hit-slot {
cursor: pointer;
}
#gameBoard.input-locked .hit-slot {
cursor: default;
}2.2 替換 buildBoard()
搜尋整個:
function buildBoard() {一直到該函數結束,完整替換為:
function buildBoard() {
const islandLayer = $('#islandLayer');
const hitLayer = $('#hitLayer');
DISPLAY_ISLAND_CENTERS.forEach(([x, y], island) => {
const use = createSVGElement('use', {
href: '#island-symbol',
transform: `translate(${x} ${y})`,
fill: '#fff0c5',
filter: 'url(#islandShadow)',
'pointer-events': 'none',
'data-island': island
});
islandLayer.appendChild(use);
islandUses.push(use);
/*
* 命中層只保留七個棋位。
* 不再建立覆蓋整個島的 island-hit 元素。
*/
const hitGroup = createSVGElement('g', {
fill: 'transparent',
stroke: 'none',
'pointer-events': 'all',
'data-hit-island': island
});
for (let point = 0; point < 7; point++) {
const index = island * 7 + point;
const [cellX, cellY] = cellCoordinates(index);
const circle = createSVGElement('circle', {
cx: cellX,
cy: cellY,
r: '17.25',
class: 'hit-slot',
'data-cell': index,
'data-island': island,
'aria-hidden': 'true'
});
hitGroup.appendChild(circle);
hitSlots[index] = circle;
}
hitLayer.appendChild(hitGroup);
islandHitGroups.push(hitGroup);
});
}關鍵變化:
- 刪除
.island-hit。 - 棋盤可視島嶼增加
pointer-events="none"。 - 點擊區域只保留七個
.hit-slot。
2.3 刪除三個不再需要的函數
完整刪除:
function tryFlySelectedPieceToIsland(targetIsland) {
...
}完整刪除:
function onIslandClick(island) {
...
}完整刪除:
function onBoardEmptyClick() {
...
}2.4 修改棋盤點擊事件
在 bindEvents() 中搜尋:
$('#gameBoard').addEventListener('click', event => {
const target = event.target;
const cell = target.closest?.('.hit-slot');
if (cell) {
onCellClick(Number(cell.dataset.cell));
return;
}
const island = target.closest?.('.island-hit');
if (island) {
onIslandClick(Number(island.dataset.island));
return;
}
onBoardEmptyClick();
});替換為:
$('#gameBoard').addEventListener('click', event => {
const cell = event.target.closest?.('.hit-slot');
if (!cell) return;
onCellClick(Number(cell.dataset.cell));
});這樣點擊島的空白區域或棋盤背景時不再有任何反應。
3. 修正跨島移動及選中邏輯
目前的問題是:選中棋子後,只要點擊另一個島的任意位置,代碼就會自動嘗試飛到該島的相同位置。
應改為:
- 點擊己方棋子時,永遠優先選中該棋子。
- 點擊確切的合法空位時才移動。
- 點擊另一個島的錯誤位置時顯示提示。
- 提示顯示 2 秒。
- 點擊另一個島的己方棋子不顯示錯誤,而是切換選中棋子。
3.1 替換 onCellClick()
搜尋整個:
function onCellClick(index) {將該函數完整替換為:
function onCellClick(index) {
if (!Number.isInteger(index) || index < 0 || index >= 49 || isBoardInteractionLocked()) {
return;
}
const occupant = state.board[index];
/*
* 點擊己方棋子時,優先切換選中對象。
* 即使該棋子位於另一個島,也不會讓之前選中的棋子飛過去。
*/
if (occupant === state.turn) {
lastAIStats = null;
if (selectedCell === index) {
selectedCell = -1;
} else {
selectedCell = index;
}
renderBoard();
renderStatus();
return;
}
if (!hasSelectedOwnPiece()) return;
const from = selectedCell;
const fromIsland = (from / 7) | 0;
const clickedIsland = (index / 7) | 0;
const move = {
from,
to: index
};
/*
* 只有玩家實際點擊的棋位是合法空位時才移動。
* 不再根據「點擊到哪個島」自動修正目的棋位。
*/
if (occupant === 0 && isLegalMove(state, move)) {
commitMove(move, 'human', null);
return;
}
/*
* 已選中棋子,但點擊了另一個島的錯誤位置,
* 包括錯誤編號的空位或被對方棋子佔用的位置。
*/
if (clickedIsland !== fromIsland) {
showNotice(I18N[language].invalidIslandPosition, 2000);
}
}3.2 將提示時間改成精確的 2 秒
搜尋:
function showNotice(message) {
clearTimeout(noticeTimer);
noticeText = message;
renderStatus();
noticeTimer = setTimeout(() => {
noticeText = '';
renderStatus();
}, 1800);
}替換為:
function showNotice(message, duration = 2000) {
clearTimeout(noticeTimer);
noticeText = message;
renderStatus();
noticeTimer = setTimeout(() => {
noticeText = '';
renderStatus();
}, duration);
}提示消失後,renderStatus() 會重新按照當前回合、選中棋子或 AI 統計恢復之前應顯示的狀態文字。
4. 回退或前進時,所有 AI 自動改成人類
這裡建議把火方和冰方中所有非人類控制器全部切換為人類,並取消正在進行的 AI 思考。回退或前進後不再自動重新啟動 AI。
4.1 在 undo() 前加入輔助函數
加入:
function switchAllControllersToHuman() {
if (controllers[1] !== 'human') {
controllers[1] = 'human';
}
if (controllers[-1] !== 'human') {
controllers[-1] = 'human';
}
}4.2 替換 undo() 和 redo()
搜尋:
function undo() {
if (historyIndex <= 0) return;
cancelAI();
historyIndex--;
restoreHistory(historyIndex);
if (gameStarted && !state.winner) scheduleAI(850);
}
function redo() {
if (historyIndex >= history.length - 1) return;
cancelAI();
historyIndex++;
restoreHistory(historyIndex);
if (gameStarted && !state.winner) scheduleAI(850);
}替換為:
function undo() {
if (historyIndex <= 0) return;
cancelAI();
switchAllControllersToHuman();
historyIndex--;
restoreHistory(historyIndex);
}
function redo() {
if (historyIndex >= history.length - 1) return;
cancelAI();
switchAllControllersToHuman();
historyIndex++;
restoreHistory(historyIndex);
}restoreHistory() 內部會呼叫 renderAll(),因此左右兩側的控制器按鈕會立即顯示為「人類」。
5. 將大型棋盤弧線改為正三角形
目前三條外側弧線的端點為:
頂點: 240, 73.625
左下頂點: 56.403, 391.625
右下頂點: 423.597, 391.625這三點本身構成正三角形:
- 高度為
318 - 內切圓半徑為
318 / 3 = 106 - 內切圓圓心為
240, 285.625
5.1 替換大型棋盤線 SVG
在:
<g id="largeFano" aria-hidden="true" fill="none" stroke-linecap="round" stroke-linejoin="round">內,將原來的兩組 <g> 完整替換為:
<g stroke="#fff7cc" stroke-width="8" opacity=".18">
<!-- 正三角形外框 -->
<path d="M 240 73.625 L 56.403 391.625 L 423.597 391.625 Z" />
<!-- 三條穿過中心的直線 -->
<path
d="
M 240 73.625 L 240 391.625
M 56.403 391.625 L 331.7985 232.625
M 423.597 391.625 L 148.2015 232.625
"
/>
<!-- 正三角形內切圓 -->
<circle cx="240" cy="285.625" r="106" />
</g>
<g stroke="#705e46" stroke-width="3.2" opacity=".62">
<!-- 正三角形外框 -->
<path d="M 240 73.625 L 56.403 391.625 L 423.597 391.625 Z" />
<!-- 三條穿過中心的直線 -->
<path
d="
M 240 73.625 L 240 391.625
M 56.403 391.625 L 331.7985 232.625
M 423.597 391.625 L 148.2015 232.625
"
/>
<!-- 正三角形內切圓 -->
<circle cx="240" cy="285.625" r="106" />
</g>修改後:
- 外框由三條弧線變成正三角形。
- 三角形頂點完全使用原弧線端點。
- 內切圓由
136.5縮小至106。 - 主線寬度由
2.15增加至3.2。 - 高光線寬度由
6增加至8。
5.2 更新大型法諾平面的邏輯座標
搜尋:
const ISLAND_CENTERS = [
[240, 73.625], // 0:上方角島
[121.788, 217.375], // 1:左側圓弧上的島
[358.212, 217.375], // 2:右側圓弧上的島
[240, 285.625], // 3:中心島
[56.403, 391.625], // 4:左下角島
[240, 422.125], // 5:底部圓弧上的島
[423.597, 391.625] // 6:右下角島
];替換為:
const ISLAND_CENTERS = [
[240, 73.625], // 0:上方頂點
[148.2015, 232.625], // 1:左邊中點
[331.7985, 232.625], // 2:右邊中點
[240, 285.625], // 3:中心
[56.403, 391.625], // 4:左下頂點
[240, 391.625], // 5:底邊中點
[423.597, 391.625] // 6:右下頂點
];注意:DISPLAY_ISLAND_CENTERS 不需要修改。它包含為了避免七個島互相重疊而使用的顯示偏移;ISLAND_CENTERS 則用於大型棋盤線及勝利線的幾何位置。
5.3 更新內切圓常量
搜尋:
const META_CIRCLE_RADIUS = 136.5;
const META_ARC_RADIUS = 567.84;替換為:
const META_CIRCLE_RADIUS = 106;META_ARC_RADIUS 已經不再需要,可以刪除。
再搜尋:
const META_INCIRCLE = {
cx: 240,
cy: 277.846,
r: 103.923
};替換為:
const META_INCIRCLE = {
cx: 240,
cy: 285.625,
r: 106
};雖然目前 META_INCIRCLE 沒有直接參與繪製,但同步更新可以避免後續功能使用錯誤的舊座標。
6. 同時形成多條勝利線時顯示全部勝利線
6.1 將單條查找改成多條查找
搜尋:
function findMetaLine(claims, player) {
for (let i = 0; i < LOCAL_LINES.length; i++) {
const line = LOCAL_LINES[i];
if (claims[line[0]] === player && claims[line[1]] === player && claims[line[2]] === player)
return { index: i, line };
}
return null;
}替換為:
function findMetaLines(claims, player) {
const results = [];
for (let i = 0; i < LOCAL_LINES.length; i++) {
const line = LOCAL_LINES[i];
if (
claims[line[0]] === player &&
claims[line[1]] === player &&
claims[line[2]] === player
) {
results.push({
index: i,
line
});
}
}
return results;
}6.2 修改 applyMove() 中的勝利判定
在 applyMove() 中搜尋:
const playerLine = findMetaLine(next.claims, player);
const opponentLine = findMetaLine(next.claims, -player);
if (playerLine) next.winner = player;
else if (opponentLine) next.winner = -player;
else if (!hasAnyLegalMove(next)) next.winner = 2;
else next.winner = 0;替換為:
const playerLines = findMetaLines(next.claims, player);
const opponentLines = findMetaLines(next.claims, -player);
if (playerLines.length > 0) next.winner = player;
else if (opponentLines.length > 0) next.winner = -player;
else if (!hasAnyLegalMove(next)) next.winner = 2;
else next.winner = 0;6.3 完整替換 renderVictory()
搜尋整個:
function renderVictory() {將該函數完整替換為:
function renderVictory() {
const layer = $('#victoryLayer');
layer.replaceChildren();
if (state.winner !== 1 && state.winner !== -1) return;
const results = findMetaLines(state.claims, state.winner);
if (!results.length) return;
const color = state.winner === 1 ? '#ffe06c' : '#e8fdff';
layer.setAttribute('stroke', color);
results.forEach(result => {
let element;
/*
* LOCAL_LINES 的第 7 條線,即索引 6,
* 是由三個邊中點構成的內切圓。
*/
if (result.index === 6) {
element = createSVGElement('circle', {
cx: META_CENTER.x,
cy: META_CENTER.y,
r: META_CIRCLE_RADIUS
});
} else {
/*
* 其餘六條線現在全部是直線:
* 三條正三角形邊線及三條穿過中心的線。
*/
const points = result.line.map(index => ISLAND_CENTERS[index]);
element = createSVGElement('path', {
d: `
M ${points[0][0]} ${points[0][1]}
L ${points[1][0]} ${points[1][1]}
L ${points[2][0]} ${points[2][1]}
`
});
}
appendSVGAnimation(element, {
attributeName: 'opacity',
values: '.65;1;.65',
dur: '1.6s',
repeatCount: 'indefinite'
});
appendSVGAnimation(element, {
attributeName: 'stroke-width',
values: '8;10;8',
dur: '1.6s',
repeatCount: 'indefinite'
});
layer.appendChild(element);
});
}原來的 renderVictory() 只呼叫一次 findMetaLine(),所以最多顯示第一條勝利線。新的版本會遍歷 findMetaLines() 的全部結果,為每條勝利線分別建立 SVG 元素和動畫。
修改後的點擊行為
修改完成後,棋盤點擊規則為:
- 點擊未選中的己方棋子:選中該棋子。
- 點擊已選中的己方棋子:取消選中。
- 點擊另一個島的己方棋子:切換選中到新棋子。
- 點擊同島合法空位:正常移動。
- 點擊另一島相同編號的空位:正常飛過去。
- 點擊另一島錯誤編號的位置:顯示
「當棋子飛到另一個島時,必須在島的同一位置」 - 點擊另一島被對方佔用的位置:顯示相同提示。
- 點擊島的非棋位區域:不發生任何事情。
- 點擊棋盤背景:不發生任何事情。
- 回退或前進:停止 AI,火方和冰方的 AI 均切換為人類。
- 同時形成兩條或更多勝利線:全部顯示。