QuanAI只读快照

共享会话

改進

分享于 2026年9月19日 05:39
提问者

改進 最新遊戲規則為:

遊戲規則

拚數棋是一款二人弈棋,即二人抽象策略遊戲。

目標:

雙方比拚多個區域內的數字,數字之差將作為得分。雙方互換先後手玩兩局,總分高者獲勝。

棋子:

本棋共有五種棋子,每方每種棋子有五個。

這五種棋子分別為「單一、雙二、角二、網三、叉零」:

「單一」是一個正方形有一條實邊和一個三角形; 「雙二」是一個正方形有兩條分開的實邊和兩個三角形; 「角二」是一個正方形有兩條相連實邊和兩個三角形; 「圍三」是一個正方形有三條實邊和三個三角形; 「零叉」是一個正方形內部兩條對角線交叉。

棋盤:

七乘七方格,中間的格子上有圓點標記。

棋規:

落子規則:

先手第一手棋一定要用「角二」下到棋盤中心位置,此後雙方輪流落子。要求落下的棋子要跟場上已有的棋子相鄰,但實邊不能相鄰。不能形成空洞。先手方必須落完二十五個棋子,後手方要落下二十四個棋子,最後剩下的棋子不能是「叉零」。

提子規則:

落子結束後開始提子,由後手方先提子,任意選取一個己方棋子,按它上面的三角形指向行動,單一則走一步,雙二則選擇兩個方向的其中一個走一步,角二則選擇斜向一步或兩步,圍三則三個方向選一個走一步,一直走到一個己方的「叉零」。不能走到沒有棋子的空位上,也不能走出棋盤邊界。如果選取的是「叉零」則直接完成本次提子。把這些棋子拿在手上,之後輪到先手方提子。雙方輪流提完五個「叉零」,則本局結束。

計分規則:

當落子階段結束,實邊圍成的一個區域內,一方的三角形數量減去另一方的三角形數,結果即為這一方在本區域的得分。當一個區域只有一方棋子,則不能計分,必須是比拚過雙方三角形數量的區域才可計分。如果是通向棋盤外的區域,則視為同一區域。在計完落子階段的分數後,才能開始提子,然後再計提子分數。一方每回合所提的棋子中,己方棋子的三角形數量減去對方的棋子的三角形數量,則是本回合提子得分,如果只包含本方棋子則不能得分。

勝負判定:

雙方玩兩局,最終分數為正整數者獲勝。

在任意一局中,如果一方不能按規則落子或提子,則直接判負。

規則介紹完畢,下面說明這版規則跟上一版的區別:

在落子階段,後手方最後剩下的棋子不能是「叉零」。去掉「在落子階段結束時,後手方會剩下一個棋子,這個棋子也將計到提子階段的得分」這條規則,這個棋子將不再參與計分。

在提子階段,每方可以任意選擇一個己方棋子,包括「叉零」。去掉「當走出邊界則會從另一邊回來,上下、左右是打通的」這條規則,邊界將不再打通。之前的提子只有一輪,現在要進行五輪,雙方要把己方所有的「叉零」都提走。每回合的提子都獨立計分,並且要包含對方的棋子,否則不能計分。

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

battlenumber.htmltext/plain · 118.3 KB预览下载
QuanAI

我會先檢查 HTML 中與落子結束、剩餘棋子、提子移動、邊界處理、提子輪次及計分相關的程式,再逐項給出可直接替換的代碼。

已確認舊版邏輯仍包含「棋盤邊界環回、剩餘棋子加分、只提兩次」;接著讀取其餘腳本,找出主遊戲、AI、復原與規則文字的所有對應點。

主程式也已定位到相同舊規則,且目前禁止以「叉零」直接提子。最後讀取後段,以涵蓋提子提交、人工點擊、AI 狀態轉換、結果顯示與棋譜輸出。

以下修改可讓 battlenumber.html 完整符合新版規則。CSS 和棋盤 SVG 不需要修改;主要要同步修改兩套邏輯:

  1. <script id="mctsWorkerSource"> 內的 AI 模擬規則。
  2. 後面的主遊戲 <script>

若只修改主程式而不修改 Worker,AI 仍會按舊規則計算,因此兩邊都必須改。


一、加入提子總回合常數

1. 修改 MCTS Worker

在 Worker 內找到:

js
const TYPE_CROSS = 4;

替換為:

js
const TYPE_CROSS = 4;

const CROSS_COUNT_PER_PLAYER = 5;
const TOTAL_EXTRACTIONS = CROSS_COUNT_PER_PLAYER * 2;

2. 修改主遊戲程式

在後面的主程式內同樣找到:

js
const TYPE_CROSS = 4;

替換為:

js
const TYPE_CROSS = 4;

const CROSS_COUNT_PER_PLAYER = 5;
const TOTAL_EXTRACTIONS = CROSS_COUNT_PER_PLAYER * 2;

這會把提子階段由原本的兩次,改為雙方各提五次,共十次。


二、落子時禁止後手最後剩下「叉零」

後手第 24 次落子前,手上只剩兩枚棋子。該次落子後,如果仍有「叉零」留在手上,這步便不合法。

1. 修改 Worker 的 canPlace()

在 Worker 的 canPlace() 中找到:

js
const total = s.placeCount[0] + s.placeCount[1];

if (total === 0) {
  return player === s.first && type === TYPE_CORNER && index === 24;
}

替換為:

js
const total = s.placeCount[0] + s.placeCount[1];

if (total === 0) {
  return player === s.first && type === TYPE_CORNER && index === 24;
}

// 棋盤已有 47 枚時,輪到後手放第 24 枚。
// 這一步落下後,後手剩餘的最後一枚不能是「叉零」。
if (
  total === SIZE - 2 &&
  player !== s.first &&
  s.inv[player][TYPE_CROSS] -
    (type === TYPE_CROSS ? 1 : 0) > 0
) {
  return false;
}

2. 修改主程式的 validatePlacement()

找到:

js
if (gameState.inventories[player][type] <= 0) return "noStock";
if (gameState.board[index]) return "occupied";

if (total === 0) return null;

替換為:

js
if (gameState.inventories[player][type] <= 0) return "noStock";
if (gameState.board[index]) return "occupied";

// 棋盤已有 47 枚時,後手正準備放第 24 枚。
// 落下這枚後,最後剩下的棋子不能是「叉零」。
if (
  total === SIZE - 2 &&
  player !== gameState.first &&
  gameState.inventories[player][TYPE_CROSS] -
    (type === TYPE_CROSS ? 1 : 0) > 0
) {
  return "leftoverCross";
}

if (total === 0) return null;

三、移除後手剩餘棋子的提子加分

1. 修改 Worker 的落子結束邏輯

在 Worker 的 applyAction() 中找到:

js
if (total === SIZE) {
  next.phase = "extract";
  next.extractIndex = 0;
  next.turn = 1 - next.first;

  let leftover = 0;
  for (let type = 0; type < 5; type++) {
    leftover += next.inv[next.turn][type] * TRI[type];
  }
  next.raw[next.turn] += leftover;
} else {
  next.turn = 1 - p;
}

替換為:

js
if (total === SIZE) {
  next.phase = "extract";
  next.extractIndex = 0;

  // 落子完成後由後手先提子。
  next.turn = 1 - next.first;
} else {
  next.turn = 1 - p;
}

2. 修改主程式的 commitPlacement()

commitPlacement() 中找到整個:

js
if (total >= SIZE) {
  state.placementFinal = state.placementScore;
  state.phase = "extract";
  state.extractIndex = 0;
  state.turn = 1 - state.first;
  state.leftover = [];

  let leftoverScore = 0;

  for (let type = 0; type < 5; type++) {
    const quantity = state.inventories[state.turn][type];

    if (quantity > 0) {
      state.leftover.push({
        player: state.turn,
        type,
        quantity
      });

      leftoverScore += quantity * TRIANGLES[type];
    }
  }

  state.extractRaw[state.turn] += leftoverScore;

  state.record.push({
    kind: "extractStart",
    round: state.round,
    firstExtractor: state.turn + 1,
    leftover: clone(state.leftover),
    leftoverScore
  });
} else {
  state.turn = 1 - player;
}

替換為:

js
if (total >= SIZE) {
  state.placementFinal = state.placementScore;
  state.phase = "extract";
  state.extractIndex = 0;

  // 後手先提子。
  state.turn = 1 - state.first;

  // 僅在棋譜中記錄未落下的棋子,不參與任何計分。
  const unplacedPieces = [];

  for (let type = 0; type < 5; type++) {
    const quantity = state.inventories[state.turn][type];

    if (quantity > 0) {
      unplacedPieces.push({
        player: state.turn,
        type,
        quantity
      });
    }
  }

  state.record.push({
    kind: "extractStart",
    round: state.round,
    firstExtractor: state.turn + 1,
    unplacedPieces
  });
} else {
  state.turn = 1 - player;
}

3. 從初始狀態移除舊的 leftover

makeFreshState() 中找到:

js
extractRaw: [0, 0],
extractIndex: 0,
leftover: [],
phase: "place",

替換為:

js
extractRaw: [0, 0],
extractIndex: 0,
phase: "place",

四、取消棋盤邊界環回

必須同時修改 Worker 和主程式的 movementDestinations()

1. 替換 Worker 的 movementDestinations()

將 Worker 內整個函數:

js
function movementDestinations(board, index) {

到其結束的 },完整替換為:

js
function movementDestinations(board, index) {
  const tile = board[index];

  if (!tile || tile[1] === TYPE_CROSS) return [];

  const type = tile[1];
  const rot = tile[2];
  const r = Math.floor(index / N);
  const c = index % N;
  const raw = [];

  if (type === TYPE_SINGLE) {
    raw.push([-1, 0, 1]);
  } else if (type === TYPE_DOUBLE) {
    raw.push([0, -1, 1], [0, 1, 1]);
  } else if (type === TYPE_CORNER) {
    raw.push([-1, -1, 1], [-1, -1, 2]);
  } else if (type === TYPE_SURROUND) {
    raw.push([-1, 0, 1], [0, -1, 1], [0, 1, 1]);
  }

  const result = [];
  const used = new Set();

  for (const [baseDr, baseDc, distance] of raw) {
    const [dr, dc] = rotateVector(baseDr, baseDc, rot);
    const nr = r + dr * distance;
    const nc = c + dc * distance;

    // 新規則:不能走出棋盤,不再從另一邊回來。
    if (nr < 0 || nr >= N || nc < 0 || nc >= N) {
      continue;
    }

    const ni = nr * N + nc;

    if (!used.has(ni)) {
      used.add(ni);
      result.push(ni);
    }
  }

  return result;
}

2. 替換主程式的 movementDestinations()

將主程式內整個同名函數替換為:

js
function movementDestinations(board, index) {
  const tile = board[index];

  if (!tile || tile.type === TYPE_CROSS) return [];

  const row = Math.floor(index / N);
  const col = index % N;
  const movement = [];

  if (tile.type === TYPE_SINGLE) {
    movement.push([-1, 0, 1]);
  } else if (tile.type === TYPE_DOUBLE) {
    movement.push([0, -1, 1], [0, 1, 1]);
  } else if (tile.type === TYPE_CORNER) {
    movement.push([-1, -1, 1], [-1, -1, 2]);
  } else if (tile.type === TYPE_SURROUND) {
    movement.push([-1, 0, 1], [0, -1, 1], [0, 1, 1]);
  }

  const destinations = [];
  const used = new Set();

  for (const [baseDr, baseDc, distance] of movement) {
    const [dr, dc] = rotateVector(baseDr, baseDc, tile.rot);
    const nextRow = row + dr * distance;
    const nextCol = col + dc * distance;

    // 新規則:超出棋盤的方向直接視為不可走。
    if (
      nextRow < 0 ||
      nextRow >= N ||
      nextCol < 0 ||
      nextCol >= N
    ) {
      continue;
    }

    const next = nextRow * N + nextCol;

    if (!used.has(next)) {
      used.add(next);
      destinations.push(next);
    }
  }

  return destinations;
}

五、允許直接選擇自己的「叉零」完成提子

路徑只包含一枚自己的叉零也是合法提子,分數為零。

1. 替換 Worker 的 extractionActions()

將 Worker 內整個 extractionActions() 替換為:

js
function extractionActions(s, cap) {
  const p = s.turn;
  const actions = [];
  const board = s.board;

  for (let start = 0; start < SIZE && actions.length < cap; start++) {
    const startTile = board[start];

    if (!startTile || startTile[0] !== p) {
      continue;
    }

    // 可以直接選取自己的「叉零」,立即完成本次提子。
    if (startTile[1] === TYPE_CROSS) {
      actions.push({
        kind: "extract",
        path: [start]
      });
      continue;
    }

    const seen = new Uint8Array(SIZE);
    const path = [start];
    seen[start] = 1;

    const dfs = current => {
      if (actions.length >= cap) return;

      const destinations = movementDestinations(board, current);
      shuffle(destinations);

      for (const next of destinations) {
        if (actions.length >= cap || seen[next]) continue;

        const tile = board[next];
        if (!tile) continue;

        if (tile[1] === TYPE_CROSS) {
          if (tile[0] === p) {
            actions.push({
              kind: "extract",
              path: path.concat(next)
            });
          }

          continue;
        }

        seen[next] = 1;
        path.push(next);
        dfs(next);
        path.pop();
        seen[next] = 0;
      }
    };

    dfs(start);
  }

  return actions;
}

2. 替換主程式的 findOneExtractionPath()

js
function findOneExtractionPath(gameState, player, start) {
  const board = gameState.board;
  const startTile = board[start];

  if (!startTile || startTile.player !== player) {
    return null;
  }

  // 自己的叉零可直接完成本次提子。
  if (startTile.type === TYPE_CROSS) {
    return [start];
  }

  const seen = new Uint8Array(SIZE);
  const path = [start];
  seen[start] = 1;

  function dfs(current) {
    for (const next of movementDestinations(board, current)) {
      if (seen[next]) continue;

      const tile = board[next];
      if (!tile) continue;

      if (tile.type === TYPE_CROSS) {
        if (tile.player === player) {
          return path.concat(next);
        }

        continue;
      }

      seen[next] = 1;
      path.push(next);

      const result = dfs(next);
      if (result) return result;

      path.pop();
      seen[next] = 0;
    }

    return null;
  }

  return dfs(start);
}

3. 替換主程式的 getExtractionActions()

js
function getExtractionActions(gameState, player, limit = 500) {
  const actions = [];
  const board = gameState.board;

  for (let start = 0; start < SIZE && actions.length < limit; start++) {
    const startTile = board[start];

    if (!startTile || startTile.player !== player) {
      continue;
    }

    // 直接提走自己的叉零。
    if (startTile.type === TYPE_CROSS) {
      actions.push({
        kind: "extract",
        path: [start]
      });
      continue;
    }

    const seen = new Uint8Array(SIZE);
    const path = [start];
    seen[start] = 1;

    function dfs(current) {
      if (actions.length >= limit) return;

      for (const next of movementDestinations(board, current)) {
        if (actions.length >= limit || seen[next]) continue;

        const tile = board[next];
        if (!tile) continue;

        if (tile.type === TYPE_CROSS) {
          if (tile.player === player) {
            actions.push({
              kind: "extract",
              path: path.concat(next)
            });
          }

          continue;
        }

        seen[next] = 1;
        path.push(next);
        dfs(next);
        path.pop();
        seen[next] = 0;
      }
    }

    dfs(start);
  }

  return actions;
}

4. 替換主程式的 validateExtractionPath()

js
function validateExtractionPath(gameState, player, path) {
  if (!Array.isArray(path) || path.length < 1) {
    return "invalidPath";
  }

  const startTile = gameState.board[path[0]];

  if (!startTile || startTile.player !== player) {
    return "ownStart";
  }

  // 路徑只有一枚自己的叉零時,直接合法。
  if (startTile.type === TYPE_CROSS) {
    return path.length === 1 ? null : "invalidPath";
  }

  // 非叉零起點必須至少再走到一枚己方叉零。
  if (path.length < 2) {
    return "invalidPath";
  }

  const seen = new Set([path[0]]);

  for (let step = 1; step < path.length; step++) {
    const previous = path[step - 1];
    const current = path[step];

    if (seen.has(current)) return "revisited";

    const tile = gameState.board[current];
    if (!tile) return "emptyExtraction";

    if (!movementDestinations(gameState.board, previous).includes(current)) {
      return "illegalDirection";
    }

    // 叉零只能作為路徑終點,不能再從叉零繼續行動。
    if (step < path.length - 1 && tile.type === TYPE_CROSS) {
      return "invalidPath";
    }

    seen.add(current);
  }

  const finalTile = gameState.board[path[path.length - 1]];

  if (
    !finalTile ||
    finalTile.type !== TYPE_CROSS ||
    finalTile.player !== player
  ) {
    return "opponentCross";
  }

  return null;
}

5. 修改人工點擊叉零的處理

handleExtractionCell() 中找到:

js
if (tile.type === TYPE_CROSS) {
  showNotice("errors.nonCrossStart");
  return;
}

if (!findOneExtractionPath(state, state.turn, index)) {

替換為:

js
// 選取自己的叉零時,直接完成本次提子。
if (tile.type === TYPE_CROSS) {
  commitExtraction([index]);
  return;
}

if (!findOneExtractionPath(state, state.turn, index)) {

這樣玩家點擊自己的叉零後會立即提走,不需要再選下一枚棋子。


六、提子改為十次,並且只有包含對方棋子才得分

1. 替換 Worker applyAction() 的提子部分

在 Worker 的 applyAction() 中找到:

js
} else {
  let value = 0;

  for (const index of action.path) {
    const tile = next.board[index];
    value += tile[0] === p ? TRI[tile[1]] : -TRI[tile[1]];
  }

  next.raw[p] += value;

  for (const index of action.path) {
    next.board[index] = null;
  }

  next.extractIndex++;

  if (next.extractIndex >= 2) {
    next.done = true;
  } else {
    next.turn = next.first;
  }
}

替換為:

js
} else {
  let value = 0;
  let hasOpponentPiece = false;

  for (const index of action.path) {
    const tile = next.board[index];

    if (tile[0] === p) {
      value += TRI[tile[1]];
    } else {
      value -= TRI[tile[1]];
      hasOpponentPiece = true;
    }
  }

  // 路徑只有本方棋子時,本回合不計分。
  if (hasOpponentPiece) {
    next.raw[p] += value;
  }

  for (const index of action.path) {
    next.board[index] = null;
  }

  next.extractIndex++;

  if (next.extractIndex >= TOTAL_EXTRACTIONS) {
    next.done = true;
  } else {
    // 雙方輪流提子。
    next.turn = 1 - p;
  }
}

2. 替換主程式的 commitExtraction()

將整個函數替換為:

js
function commitExtraction(path, delayBeforeAI = 100) {
  const player = state.turn;
  const error = validateExtractionPath(state, player, path);

  if (error) {
    showNotice(`errors.${error}`);
    return false;
  }

  let triangleDifference = 0;
  let hasOpponentPiece = false;

  for (const index of path) {
    const tile = state.board[index];

    if (tile.player === player) {
      triangleDifference += TRIANGLES[tile.type];
    } else {
      triangleDifference -= TRIANGLES[tile.type];
      hasOpponentPiece = true;
    }
  }

  // 本回合若沒有提到對方棋子,得分為零。
  const awardedScore = hasOpponentPiece
    ? triangleDifference
    : 0;

  const pathRecord = path.map(index => ({
    square: coord(index),
    index,
    owner: state.board[index].player + 1,
    type: state.board[index].type
  }));

  state.extractRaw[player] += awardedScore;

  for (const index of path) {
    state.board[index] = null;
  }

  state.record.push({
    kind: "extract",
    round: state.round,
    player: player + 1,
    extractionNumber: state.extractIndex + 1,
    path: pathRecord,
    containsOpponent: hasOpponentPiece,
    triangleDifference,
    rawScore: awardedScore
  });

  state.extractIndex++;

  if (state.extractIndex >= TOTAL_EXTRACTIONS) {
    finalizeRound(state);
  } else {
    // 後手、先手交替,直到雙方各提完五個叉零。
    state.turn = 1 - player;
    resolveNoLegalAction(state);
  }

  pushHistory();
  onStateChanged(delayBeforeAI);
  return true;
}

七、修改提子進度顯示

1. 中文訊息

找到:

js
extract: "第 {round} 局 · 提子 {step}/2 · 輪到{player}({role})",

替換為:

js
extract: "第 {round} 局 · 提子 {step}/{total} · 輪到{player}({role})",

2. 英文訊息

找到:

js
extract: "Game {round} · Extraction {step}/2 · {player} to act ({role})",

替換為:

js
extract: "Game {round} · Extraction {step}/{total} · {player} to act ({role})",

3. 修改 normalMessage()

找到:

js
main: tr("messages.extract", {
  round: state.round,
  step: state.extractIndex + 1,
  player: playerName(state.turn),
  role: roleName(state.turn)
}),

替換為:

js
main: tr("messages.extract", {
  round: state.round,
  step: state.extractIndex + 1,
  total: TOTAL_EXTRACTIONS,
  player: playerName(state.turn),
  role: roleName(state.turn)
}),

八、修改錯誤訊息

1. 中文

在中文 errors 中刪除:

js
nonCrossStart: "不能以「叉零」作為提子起點。",

並在 noStock 附近加入:

js
leftoverCross: "後手最後剩下的棋子不能是「叉零」。",

修改後相關部分應為:

js
noStock: "這一種棋子已經用完。",
leftoverCross: "後手最後剩下的棋子不能是「叉零」。",
adjacent: "每一手棋都要跟場上已有棋子貼合。",

2. 英文

刪除:

js
nonCrossStart: "A Cross Zero cannot be the starting piece.",

加入:

js
leftoverCross: "The second player's final unplaced piece cannot be a Cross Zero.",

九、移除結果視窗中的「剩餘棋子已計分」

1. 修改中文結果文字

找到:

js
rawExtraction: "提子原始分:玩家一 {p1},玩家二 {p2};表中顯示雙方的零和淨分。",
leftoverIncluded: "後手剩餘的一枚棋子已計入提子原始分。",

替換為:

js
rawExtraction: "提子累計分:玩家一 {p1},玩家二 {p2}。每回合只有提子路徑包含對方棋子時才計分;表中顯示雙方的零和淨分。",

2. 修改英文結果文字

找到:

js
rawExtraction: "Raw extraction scores: Player One {p1}, Player Two {p2}. The table shows the zero-sum net score.",
leftoverIncluded: "The second player's unplaced piece is included in the raw extraction score.",

替換為:

js
rawExtraction: "Accumulated extraction scores: Player One {p1}, Player Two {p2}. A turn scores only if its path contains an opposing piece. The table shows the zero-sum net score.",

3. 修改 roundScoreTable()

找到:

js
<p class="result-note">
  ${tr("result.rawExtraction", {
    p1: formatSigned(summary.extractRaw[0]),
    p2: formatSigned(summary.extractRaw[1])
  })}
  ${tr("result.leftoverIncluded")}
</p>

替換為:

js
<p class="result-note">
  ${tr("result.rawExtraction", {
    p1: formatSigned(summary.extractRaw[0]),
    p2: formatSigned(summary.extractRaw[1])
  })}
</p>

十、修改回退功能

舊版 undo() 會在提子後一次回退到整個提子階段開始。新版有十個提子回合,應改成每次只回退一手。

將整個 undo() 替換為:

js
function undo() {
  if (manualPath.length) {
    manualPath = [];
    renderBoard();
    renderMessage();
    renderToolbar();
    return;
  }

  if (historyIndex <= 0) return;

  restoreHistory(historyIndex - 1);
}

十一、更新規則視窗

RULE_SECTIONS.zh 中修改以下段落。

1. 中文「目標」

替換為:

js
{
  title: "目標",
  html: `
    <p>雙方在由實邊分隔的區域中比較三角形數量,數字之差作為得分。</p>
    <p>落子後雙方輪流提子,直到各自提完五個「叉零」。雙方互換先後手玩兩局,兩局總分較高的一方獲勝。</p>
  `
},

2. 中文「落子」

替換為:

js
{
  title: "落子",
  html: `
    <ol>
      <li>先手第一手必須把「角二」放在中心格。</li>
      <li>此後雙方輪流落子,新棋子必須以邊貼合場上至少一枚棋子。</li>
      <li>相鄰棋子的兩條實邊不能在同一條格線上重合。</li>
      <li>落子後不能令尚未佔用的格子形成封閉空洞。</li>
      <li>先手放完二十五枚棋子,後手放二十四枚。</li>
      <li>後手最後剩下的一枚棋子不能是「叉零」,而且不參與任何計分。</li>
    </ol>
  `
},

3. 中文「提子」

替換為:

js
{
  title: "提子",
  html: `
    <p>落子完畢後,由後手先提子,之後雙方輪流提子,直到各自提完五個「叉零」。</p>
    <ol>
      <li>每回合任意選擇一枚己方棋子作為起點,包括「叉零」。</li>
      <li>如果選取的是己方「叉零」,直接提走該棋子並完成本回合。</li>
      <li>如果選取其他棋子,按照目前棋子的三角形方向移動:單一走一步;雙二選一個方向走一步;角二沿斜向走一步或兩步;圍三選一個方向走一步。</li>
      <li>每次必須落到仍在棋盤上的棋子,不能走到空位,不能走出棋盤邊界,也不能重複經過同一枚棋子。</li>
      <li>提子路徑必須終止於己方的「叉零」。完成後,移除整條路徑上的所有棋子。</li>
    </ol>
  `
},

4. 中文「計分」

替換為:

js
{
  title: "計分",
  html: `
    <p><strong>落子分:</strong>實邊把棋盤分成多個區域。通向棋盤外的部分視為同一區域。只有同時含有雙方棋子的區域才計分;玩家一三角形數減去玩家二三角形數,就是玩家一在該區域的淨分,玩家二取得相反分數。</p>
    <p><strong>提子分:</strong>每一個提子回合獨立計分。己方棋子的三角形數量減去對方棋子的三角形數量,就是該回合的提子分。如果該回合只提到本方棋子,則該回合得零分。</p>
  `
},

RULE_SECTIONS.en 中對應替換。

5. 英文「Objective」

js
{
  title: "Objective",
  html: `
    <p>Players compare the numbers of triangles inside areas separated by solid edges. The difference becomes the score.</p>
    <p>After placement, the players alternate extraction turns until each has removed all five of their Cross Zero pieces. Two games are played with the first-player roles swapped, and the higher combined score wins.</p>
  `
},

6. 英文「Placement」

js
{
  title: "Placement",
  html: `
    <ol>
      <li>The first player must place a Corner Two on the centre square.</li>
      <li>Every later piece must share a side with at least one piece already on the board.</li>
      <li>Two solid edges may not overlap along the same grid line.</li>
      <li>A placement may not create an enclosed hole of empty squares.</li>
      <li>The first player places twenty-five pieces and the second player places twenty-four.</li>
      <li>The second player's final unplaced piece cannot be a Cross Zero and does not take part in scoring.</li>
    </ol>
  `
},

7. 英文「Extraction」

js
{
  title: "Extraction",
  html: `
    <p>After placement, the second player extracts first. The players then alternate until each has removed all five of their Cross Zero pieces.</p>
    <ol>
      <li>On each turn, choose any friendly piece as the starting piece, including a Cross Zero.</li>
      <li>If a friendly Cross Zero is selected, it is removed immediately and the extraction turn is complete.</li>
      <li>Otherwise, move according to the triangles on the current piece: Single One moves one step; Double Two chooses one direction and moves one step; Corner Two moves one or two diagonal steps; Surround Three chooses one direction and moves one step.</li>
      <li>Every landing square must contain a piece. A move cannot leave the board, land on an empty square, or visit the same piece twice.</li>
      <li>The path must finish on the acting player's own Cross Zero. All pieces in the completed path are removed.</li>
    </ol>
  `
},

8. 英文「Scoring」

js
{
  title: "Scoring",
  html: `
    <p><strong>Placement:</strong> solid edges divide the board into regions. All openings leading outside the board belong to one outside region. A region scores only when it contains pieces belonging to both players. Player One's triangles minus Player Two's triangles is Player One's net score; Player Two receives the opposite score.</p>
    <p><strong>Extraction:</strong> every extraction turn is scored separately. Friendly triangles minus opposing triangles is the score for that turn. If the extracted path contains only friendly pieces, that turn scores zero.</p>
  `
},

十二、建議更新棋譜版本

exportRecord() 中找到:

js
version: 1,

替換為:

js
version: 2,

因為新版棋譜的提子回合、計分方式及 extractStart 資料結構已經改變。


修改後的關鍵行為

完成以上替換後,遊戲會按以下方式運作:

  • 後手第 24 次落子後,剩餘棋子不能是叉零。
  • 後手未落下的棋子不再加分。
  • 棋子不能跨越棋盤邊界。
  • 玩家可以直接選擇自己的叉零並提走。
  • 提子由後手開始,雙方交替,共十個提子回合。
  • 每方恰好提走五個自己的叉零。
  • 每回合獨立計算提子分。
  • 路徑只有本方棋子時,該回合得零分。
  • 人類玩家、備用 AI 和 MCTS AI 均使用相同的新規則。
  • 回退按鈕每次只回退一個提子回合,而不是退回整個提子階段。