共享会话
🔧 修復仲裁邏輯錯誤
分享于 2026年8月16日 16:50
QuanAI以下修改只調整「仲裁成立條件」以及 MCTS/minimax 對仲裁終局的理解,不改動目前正常落子、單接、雙接、跨目標頂鑫例外與計分流程。
一、修正後的判定原則
一般兩子回合中:
-
「共活」只能是兩個棋子:
- 落在同一個對方棋子上;
- 落在該棋子的兩條不同邊;
- 兩子彼此相容;
- 棋子庫存足夠。
-
即使存在:
- 單接;
- 雙接;
- 跨不同目標、依頂鑫例外可以實際落下的合法回合;
只要不存在至少一組上述「共活」,當前方就能仲裁成功。
-
搜尋中,某一方完成回合後,如果沒有給下一方留下共活,該局面必須立即視為「下一方仲裁獲勝」。
-
最後只剩一子的特殊回合仍沿用目前正確流程:存在任一合法單子即可,不要求兩子共活。
-
棋子已全部下完時是正常終局,不再進行仲裁判定。
二、新增「仲裁防禦/共活」專用生成函式
修改位置
找到:
js
function generateLegalTurns(state) {保留這個函式不變。
在整個 generateLegalTurns(state) 函式結束後、下面這個函式之前:
js
function evaluateSearchState(state, rootPlayer, rootBaseDiff) {插入以下程式碼:
js
/*
* 仲裁專用判定。
*
* 這裡與一般合法回合 generateLegalTurns 不同:
*
* 1. 一般兩子回合只承認「共活」:
* 兩子必須落在同一個對方棋子的兩條不同邊上。
*
* 2. 單接、雙接或跨目標頂鑫例外雖然仍可作為正常落子,
* 但不能用來阻止仲裁成功。
*
* 3. 若行動方只剩最後一子,沿用現有最後一子規則:
* 存在任一合法單子即可。
*/
function generateArbitrationDefenceTurns(state) {
const required = getRequiredPiecesForState(state);
/*
* 最後只剩一子的特殊回合:
* 仲裁防禦條件為至少存在一個合法單子。
*/
if (required === 1) {
if (state.boardPieces.length === 0) return [];
return generateLegalSinglePlacements(state, state.mover).map(ghost => [ghost]);
}
const singles = generateLegalSinglePlacements(state, state.mover);
if (singles.length < 2) return [];
const moves = [];
const seen = new Set();
/*
* 只組合同一 targetId 的兩個落點。
* 不收錄任何跨目標單接/雙接/頂鑫例外。
*/
const targetMap = new Map();
for (const ghost of singles) {
if (!targetMap.has(ghost.targetId)) {
targetMap.set(ghost.targetId, []);
}
targetMap.get(ghost.targetId).push(ghost);
}
for (const targetGhosts of targetMap.values()) {
for (let i = 0; i < targetGhosts.length; i++) {
for (let j = i + 1; j < targetGhosts.length; j++) {
const first = targetGhosts[i];
const second = targetGhosts[j];
if (!pairFitsInventory(state, first, second)) {
continue;
}
const firstTargetEdge =
first.targetEdge !== undefined ? first.targetEdge : first.edgeOnOpp;
const secondTargetEdge =
second.targetEdge !== undefined ? second.targetEdge : second.edgeOnOpp;
/*
* 共活要求落在同一個棋子的兩條不同邊。
*/
if (firstTargetEdge === secondTargetEdge) {
continue;
}
if (!twoGhostsCompatible(first, second)) {
continue;
}
const move = [first, second];
/*
* 仍使用正式合法性引擎做最後驗證,
* 避免仲裁判定繞過庫存或幾何規則。
*/
const validation = validateTurnOnState(state, move);
if (!validation.ok) {
continue;
}
const signature = turnSignature(move);
if (seen.has(signature)) {
continue;
}
seen.add(signature);
moves.push(move);
}
}
}
return moves;
}
/*
* 判斷 state.mover 是否有阻止仲裁成立的下法。
*
* 已正常完局時不再判定仲裁,因此回傳 true。
*/
function hasArbitrationDefence(state) {
if (isSearchGameComplete(state)) {
return true;
}
return generateArbitrationDefenceTurns(state).length > 0;
}這是本次修正的核心:把「一般合法回合」和「能阻止仲裁的共活」分離。
三、修改 minimax/MCTS 的仲裁終局評估函式
修改位置
找到原本的:
js
function evaluateNoMoveTerminal(state, rootPlayer, rootBaseDiff) {
/*
* 棋子全部下完是正常終局,以分數判定。
* 尚未完局但行動方沒有合法回合,行動方可仲裁獲勝。
*/
if (isSearchGameComplete(state)) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
return state.mover === rootPlayer ? SEARCH_TERMINAL_VALUE : -SEARCH_TERMINAL_VALUE;
}整段替換為
js
/*
* 仲裁終局評估。
*
* 呼叫此函式代表:
* - 棋局尚未正常結束;
* - 輪到 state.mover;
* - 但上一方沒有給 state.mover 留下至少一組共活。
*
* 因此 state.mover 可以仲裁成功並直接獲勝。
*/
function evaluateArbitrationTerminal(state, rootPlayer, rootBaseDiff) {
if (isSearchGameComplete(state)) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
return state.mover === rootPlayer
? SEARCH_TERMINAL_VALUE
: -SEARCH_TERMINAL_VALUE;
}四、修改戰術 alpha-beta 的終局判定順序
仲裁判定必須早於 depth <= 0。
否則剛好搜索到深度邊界時,即使上一手沒有留下共活,也只會被當成普通分數葉節點。
修改位置
在:
js
function tacticalAlphaBeta(state, depth, alpha, beta, rootPlayer, rootBaseDiff, deadline) {找到原本:
js
if (isSearchGameComplete(state) || depth <= 0) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
const legalMoves = generateLegalTurns(state);
if (legalMoves.length === 0) {
return evaluateNoMoveTerminal(state, rootPlayer, rootBaseDiff);
}替換為
js
/*
* 正常完局優先按分數判定。
*/
if (isSearchGameComplete(state)) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
/*
* 即使存在單接、雙接或跨目標合法下法,
* 只要沒有共活,行動方即可仲裁獲勝。
*
* 此判定必須放在 depth <= 0 之前。
*/
if (!hasArbitrationDefence(state)) {
return evaluateArbitrationTerminal(state, rootPlayer, rootBaseDiff);
}
if (depth <= 0) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
const legalMoves = generateLegalTurns(state);
/*
* 理論上有仲裁防禦就一定至少有一個合法回合。
* 保留此保險判定,避免異常資料造成空分支。
*/
if (legalMoves.length === 0) {
return evaluateArbitrationTerminal(state, rootPlayer, rootBaseDiff);
}五、修改 minimax 主 alpha-beta 的終局判定
修改位置
在:
js
async function runStateMinimax(...)內部的:
js
function alphaBetaState(state, depth, alpha, beta) {找到原本:
js
if (isSearchGameComplete(state) || depth <= 0) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
let legalMoves = generateLegalTurns(state);
if (legalMoves.length === 0) {
return evaluateNoMoveTerminal(state, rootPlayer, rootBaseDiff);
}替換為
js
/*
* 棋子全部下完是正常終局。
*/
if (isSearchGameComplete(state)) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
/*
* 上一方若沒有留下至少一組共活,
* 則目前行動方立即可以仲裁獲勝。
*
* 單接、雙接及跨目標合法下法不能阻止仲裁。
* 必須在深度截斷之前檢查。
*/
if (!hasArbitrationDefence(state)) {
return evaluateArbitrationTerminal(state, rootPlayer, rootBaseDiff);
}
if (depth <= 0) {
return evaluateSearchState(state, rootPlayer, rootBaseDiff);
}
let legalMoves = generateLegalTurns(state);
if (legalMoves.length === 0) {
return evaluateArbitrationTerminal(state, rootPlayer, rootBaseDiff);
}後面的:
js
legalMoves = orderMovesForSearch(state, legalMoves);以及其餘 minimax 流程全部保留不變。
六、修改 MCTS 節點終局判定
修改位置
在:
js
async function runStateMCTS(...)內部找到:
js
function ensureNodeMoves(node, isRoot = false) {原本是:
js
function ensureNodeMoves(node, isRoot = false) {
if (node.orderedMoves !== null) return;
if (isSearchGameComplete(node.state)) {
node.orderedMoves = [];
node.terminalValue = evaluateSearchState(node.state, rootPlayer, rootBaseDiff);
return;
}
let moves = isRoot ? rootMoves.slice() : generateLegalTurns(node.state);
if (moves.length === 0) {
node.orderedMoves = [];
node.terminalValue = evaluateNoMoveTerminal(node.state, rootPlayer, rootBaseDiff);
return;
}
node.orderedMoves = orderMovesForSearch(node.state, moves, isRoot ? tacticalScores : null);
}整段替換為
js
function ensureNodeMoves(node, isRoot = false) {
if (node.orderedMoves !== null) return;
/*
* 棋子全部下完:正常按分數終局。
*/
if (isSearchGameComplete(node.state)) {
node.orderedMoves = [];
node.terminalValue = evaluateSearchState(
node.state,
rootPlayer,
rootBaseDiff
);
return;
}
/*
* 仲裁終局:
* 不論是否還存在單接、雙接或跨目標合法回合,
* 只要沒有至少一組共活,node.state.mover 即可仲裁獲勝。
*/
if (!hasArbitrationDefence(node.state)) {
node.orderedMoves = [];
node.terminalValue = evaluateArbitrationTerminal(
node.state,
rootPlayer,
rootBaseDiff
);
return;
}
let moves = isRoot
? rootMoves.slice()
: generateLegalTurns(node.state);
/*
* 保險處理:正常情況下,共活本身就是合法回合,
* 因此通過 hasArbitrationDefence 後不應為空。
*/
if (moves.length === 0) {
node.orderedMoves = [];
node.terminalValue = evaluateArbitrationTerminal(
node.state,
rootPlayer,
rootBaseDiff
);
return;
}
node.orderedMoves = orderMovesForSearch(
node.state,
moves,
isRoot ? tacticalScores : null
);
}七、修改 AI 根節點的仲裁判定
目前 AI 使用:
js
let rootMoves = generateLegalTurns(rootState);
if (rootMoves.length === 0) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}這會把單接、雙接、跨目標頂鑫例外也當成可以阻止仲裁,因此需要先獨立檢查共活。
修改位置
在 startAI() 中找到:
js
/*
* 根節點使用全部合法回合,不再先用單接、雙接、
* 破壞對方或共活規則刪除候選。
*/
let rootMoves = generateLegalTurns(rootState);
if (rootMoves.length === 0) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}替換為
js
/*
* 仲裁判定只看「共活」。
*
* 即使目前存在單接、雙接或跨目標頂鑫例外下法,
* 只要上一方沒有留下至少一組共活,
* AI 仍應立即申請仲裁並獲勝。
*/
const arbitrationDefences =
generateArbitrationDefenceTurns(rootState);
if (
!isSearchGameComplete(rootState) &&
arbitrationDefences.length === 0
) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}
/*
* 通過仲裁檢查後,AI 實際搜索仍使用所有正常合法回合。
* 單接、雙接及跨目標頂鑫例外仍然可以正常成為候選。
*/
let rootMoves = generateLegalTurns(rootState);
if (rootMoves.length === 0) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}因此:
- 仲裁檢查只認共活;
- 通過仲裁檢查後,AI 仍可選擇單接、雙接或跨目標合法回合;
- 但搜尋會正確發現:如果選擇某一步後沒有給對手留下共活,對手下一節點將仲裁獲勝。
八、修改 getAllValidMoves(),使人工仲裁只列出共活
修改位置
在檔案後段找到原本:
js
function getAllValidMoves(player) {
const state = createSearchStateFromGlobals(player);
return generateLegalTurns(state);
}整段替換為
js
/*
* 此函式目前只供仲裁及舊仲裁相容流程使用。
*
* 仲裁只承認共活,不承認單接、雙接或跨目標頂鑫例外。
*/
function getAllValidMoves(player) {
const state = createSearchStateFromGlobals(player);
return generateArbitrationDefenceTurns(state);
}這一處替換會同步修正以下現有呼叫:
js
arbValidMoves = getAllValidMoves(currentPlayer);包括:
executeArbitration()aiTriggerArbitrationSuccess()- 原有仲裁預覽
arbValidMoves的數量顯示
因此人工仲裁結果將變成:
- 有共活:仲裁失敗,預覽共活組合;
- 沒有共活:仲裁成功;
- 即使有單接/雙接/跨目標頂鑫下法,也不會導致仲裁失敗。
九、建議同步修正註解,避免未來再次混淆
找到 validateTurnOnState 上方原註解中的:
js
* 「是否讓對方下一回合無棋可下」不在此硬刪除。
* 搜尋會把它評估為對方仲裁獲勝,而不是用啟發式直接刪除候選。替換為:
js
* 「是否給對方留下至少一組共活」不屬於本回合本身的落子合法性,
* 因此不在此處刪除候選。
*
* 某回合即使符合單接、雙接或跨目標頂鑫例外,
* 若完成後沒有給對方留下共活,對方下一回合仍可仲裁獲勝。
* 此結果由仲裁及 minimax/MCTS 終局判定處理。十、修改後的完整邏輯關係
正在绘制流程图…
其中「仲裁防禦」在一般回合嚴格等於:
text
同一個對方棋子
+ 兩條不同邊
+ 兩個合法棋子
+ 兩子彼此相容
+ 庫存足夠不包含單接、雙接或跨目標下法。
最終需要實際修改的程式位置摘要
- 在
generateLegalTurns()後新增:generateArbitrationDefenceTurns()hasArbitrationDefence()
- 將
evaluateNoMoveTerminal()替換成:evaluateArbitrationTerminal()
- 修改
tacticalAlphaBeta()的終局判定順序。 - 修改
runStateMinimax()內alphaBetaState()的終局判定順序。 - 修改
runStateMCTS()內ensureNodeMoves()。 - 修改
startAI()根節點的仲裁檢查。 - 將
getAllValidMoves()改為返回generateArbitrationDefenceTurns(state)。 - 其餘正常落子、計分、單接/雙接與跨目標例外流程不修改。