QuanAI只读快照

共享会话

改進

分享于 2026年9月11日 07:31
提问者

改進 在「工具欄」中,「導出棋譜」改成「導出」,「下載局面」改成「下載」。把「導出棋譜」的icon改成「文本文件」的svg圖標。

取消島的點擊事件,島將不可點擊。當有一個棋子選中時,只有點到正確的空位,它才會飛過去。當沒有點擊到正確的位置,則消息欄會給出提示「當棋子飛到另一個島時,必須在島的同一位置」,2s後恢復之前的文字。當點到另一個島的己方棋子,則是選中那個棋子,之前的棋子取消選中。

回退或前進,AI都會自動切換成人類。

棋盤線改為正三角形,以當前的弧線端點為正三角形的端點,中間的內切圓也要相應縮小。線要加粗一些。

當勝利同時形成多條線時,就顯示多條勝利線。

回答要求:明確給出所要修改的地方和所要替換的代碼。

ifif2425.htmltext/plain · 105.1 KB预览下载
QuanAI

下面按原文件中的位置逐項修改。所有代碼均可直接搜尋原代碼後替換。


1. 工具欄文字及「導出」圖標

1.1 將導出圖標改為文本文件 SVG

搜尋:

html
<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>

替換為:

html
<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 修改工具欄預設文字

搜尋:

html
<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>

替換為:

html
<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 中搜尋:

javascript
exportRecord: '導出棋譜',
downloadPosition: '下載局面',

替換為:

javascript
exportRecord: '導出',
downloadPosition: '下載',

再在:

javascript
chooseDestination: '已選棋子,請選擇空位',

後面加入:

javascript
invalidIslandPosition: '當棋子飛到另一個島時,必須在島的同一位置',

即:

javascript
chooseDestination: '已選棋子,請選擇空位',
invalidIslandPosition: '當棋子飛到另一個島時,必須在島的同一位置',
recordSaved: '棋譜已導出',

I18N.en 的:

javascript
chooseDestination: 'Piece selected — choose an empty destination',

後面加入:

javascript
invalidIslandPosition: 'When a piece flies to another island, it must land on the same position.',

2. 取消島本身的點擊事件

修改後只有島上的七個位置可以點擊,島的其他區域和棋盤空白均不會觸發取消選中或棋子移動。

2.1 修改鼠標樣式

搜尋:

css
#gameBoard .island-hit,
#gameBoard .hit-slot {
  cursor: pointer;
}

#gameBoard.input-locked .island-hit,
#gameBoard.input-locked .hit-slot {
  cursor: default;
}

替換為:

css
#gameBoard .hit-slot {
  cursor: pointer;
}

#gameBoard.input-locked .hit-slot {
  cursor: default;
}

2.2 替換 buildBoard()

搜尋整個:

javascript
function buildBoard() {

一直到該函數結束,完整替換為:

javascript
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 刪除三個不再需要的函數

完整刪除:

javascript
function tryFlySelectedPieceToIsland(targetIsland) {
  ...
}

完整刪除:

javascript
function onIslandClick(island) {
  ...
}

完整刪除:

javascript
function onBoardEmptyClick() {
  ...
}

2.4 修改棋盤點擊事件

bindEvents() 中搜尋:

javascript
$('#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();
});

替換為:

javascript
$('#gameBoard').addEventListener('click', event => {
  const cell = event.target.closest?.('.hit-slot');

  if (!cell) return;

  onCellClick(Number(cell.dataset.cell));
});

這樣點擊島的空白區域或棋盤背景時不再有任何反應。


3. 修正跨島移動及選中邏輯

目前的問題是:選中棋子後,只要點擊另一個島的任意位置,代碼就會自動嘗試飛到該島的相同位置。

應改為:

  1. 點擊己方棋子時,永遠優先選中該棋子。
  2. 點擊確切的合法空位時才移動。
  3. 點擊另一個島的錯誤位置時顯示提示。
  4. 提示顯示 2 秒。
  5. 點擊另一個島的己方棋子不顯示錯誤,而是切換選中棋子。

3.1 替換 onCellClick()

搜尋整個:

javascript
function onCellClick(index) {

將該函數完整替換為:

javascript
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 秒

搜尋:

javascript
function showNotice(message) {
  clearTimeout(noticeTimer);
  noticeText = message;
  renderStatus();

  noticeTimer = setTimeout(() => {
    noticeText = '';
    renderStatus();
  }, 1800);
}

替換為:

javascript
function showNotice(message, duration = 2000) {
  clearTimeout(noticeTimer);
  noticeText = message;
  renderStatus();

  noticeTimer = setTimeout(() => {
    noticeText = '';
    renderStatus();
  }, duration);
}

提示消失後,renderStatus() 會重新按照當前回合、選中棋子或 AI 統計恢復之前應顯示的狀態文字。


4. 回退或前進時,所有 AI 自動改成人類

這裡建議把火方和冰方中所有非人類控制器全部切換為人類,並取消正在進行的 AI 思考。回退或前進後不再自動重新啟動 AI。

4.1 在 undo() 前加入輔助函數

加入:

javascript
function switchAllControllersToHuman() {
  if (controllers[1] !== 'human') {
    controllers[1] = 'human';
  }

  if (controllers[-1] !== 'human') {
    controllers[-1] = 'human';
  }
}

4.2 替換 undo()redo()

搜尋:

javascript
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);
}

替換為:

javascript
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. 將大型棋盤弧線改為正三角形

目前三條外側弧線的端點為:

text
頂點:     240, 73.625
左下頂點: 56.403, 391.625
右下頂點: 423.597, 391.625

這三點本身構成正三角形:

  • 高度為 318
  • 內切圓半徑為 318 / 3 = 106
  • 內切圓圓心為 240, 285.625

5.1 替換大型棋盤線 SVG

在:

html
<g id="largeFano" aria-hidden="true" fill="none" stroke-linecap="round" stroke-linejoin="round">

內,將原來的兩組 <g> 完整替換為:

html
<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 更新大型法諾平面的邏輯座標

搜尋:

javascript
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:右下角島
];

替換為:

javascript
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 更新內切圓常量

搜尋:

javascript
const META_CIRCLE_RADIUS = 136.5;
const META_ARC_RADIUS = 567.84;

替換為:

javascript
const META_CIRCLE_RADIUS = 106;

META_ARC_RADIUS 已經不再需要,可以刪除。

再搜尋:

javascript
const META_INCIRCLE = {
  cx: 240,
  cy: 277.846,
  r: 103.923
};

替換為:

javascript
const META_INCIRCLE = {
  cx: 240,
  cy: 285.625,
  r: 106
};

雖然目前 META_INCIRCLE 沒有直接參與繪製,但同步更新可以避免後續功能使用錯誤的舊座標。


6. 同時形成多條勝利線時顯示全部勝利線

6.1 將單條查找改成多條查找

搜尋:

javascript
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;
}

替換為:

javascript
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() 中搜尋:

javascript
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;

替換為:

javascript
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()

搜尋整個:

javascript
function renderVictory() {

將該函數完整替換為:

javascript
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 元素和動畫。


修改後的點擊行為

修改完成後,棋盤點擊規則為:

  1. 點擊未選中的己方棋子:選中該棋子。
  2. 點擊已選中的己方棋子:取消選中。
  3. 點擊另一個島的己方棋子:切換選中到新棋子。
  4. 點擊同島合法空位:正常移動。
  5. 點擊另一島相同編號的空位:正常飛過去。
  6. 點擊另一島錯誤編號的位置:顯示
    「當棋子飛到另一個島時,必須在島的同一位置」
  7. 點擊另一島被對方佔用的位置:顯示相同提示。
  8. 點擊島的非棋位區域:不發生任何事情。
  9. 點擊棋盤背景:不發生任何事情。
  10. 回退或前進:停止 AI,火方和冰方的 AI 均切換為人類。
  11. 同時形成兩條或更多勝利線:全部顯示。