共享会话
改進當前程式的minimax搜尋
分享于 2026年8月19日 00:21改進當前程式的minimax搜尋
- 節點共享 alpha
當前程式的根節點沒有共享 alpha,這是最直接、最值得先修的效能問題。
目前每個根候選都這樣搜尋:
alphaBetaState(child, depth - 1, -Infinity, Infinity)
也就是說:即使前面已經找到值為 10 的根走法,下一個根走法仍然以完整窗 [-∞, +∞] 搜尋。
這不影響正確性,但浪費了大量剪枝機會。
- 上一迭代最佳走法優先(PV ordering)
應使用上一輪的最佳走法作為 Principal Variation 優先走法。
每完成一層後:
將 committedBestMove 放到下一層 rootMoves 最前; 在每個非根節點,可儲存該局面的最佳走法,下一次優先嘗試。
- 將「最後單子」改成專用的精確求值
當前程式最後一子仍走一般 minimax 流程:
generateLegalTurns → orderMovesForSearch → analyseSearchMove → applySearchTurnToState → alphaBetaState → evaluateSearchState
其中 orderMovesForSearch() 對每個單子候選會做一次模擬:
const analysis = analyseSearchMove(state, move);
而真正往下遞迴時,又會再做一次:
const child = applySearchTurnToState(state, move, { validate: false }).state;
也就是最後一子的一個候選,可能被「模擬/計分」至少兩次。
現在改成:如果是全局最後一手棋,則加入專用分支。比較所有的本方的「單接」,選擇本方得分最高的(分差最大)。如果不存在本方的單接,則只要存在一個該棋子種類的活法則立即返回。
- 消息框應實時顯示當前搜搜尋進度
目前的消息框沒能實時顯示當前搜尋次數等,應實時給出分支數量,已搜尋分支和次數等信息。
回答要求:
不要影響當前正確的遊戲流程,明確給出所有要修改的地方和所要替換的代碼。
QuanAI我會先定位附件中的 minimax/alpha-beta、走法排序、最後一子判定、根迭代與消息框更新程式,再依原有資料結構給出可直接替換的完整代碼。
以下修改只涉及 minimax 搜尋、最後一子精確求值與進度顯示,不改變正式落子、合法性判定、計分、仲裁、MCTS、棋譜及 UI 操作流程。
一、修改 minimax 進度文字
在 i18n 中找到:
'ai-minimax-progress': {
zh: '極小化極大搜尋 | 深度: {depth} | 用時: {time}s',
en: 'Minimax | Depth: {depth} | Time: {time}s'
},替換成:
'ai-minimax-progress': {
zh: '極小化極大搜尋 | 深度: {depth} | 分支: {done}/{total} | 搜尋次數: {count} | 用時: {time}s',
en: 'Minimax | Depth: {depth} | Branches: {done}/{total} | Searches: {count} | Time: {time}s'
},這裡的資訊含義是:
depth:目前迭代深度done / total:已完成的根分支/根分支總數count:目前搜尋節點次數time:minimax 已使用時間
二、加入 PV 排序、局面鍵及最後一子精確求值函式
找到:
function orderMovesForSearch(state, moves) {保留整個 orderMovesForSearch() 不變。
在它結束後、runStateMinimax() 之前,加入以下完整程式:
/*
* Minimax 專用穩定走法簽名。
*
* 不使用假想棋子的 id、targetId 或 parentId,因為同一個局面在不同
* iterative-deepening 層次中重新建立時,負數假想 ID 可能不同。
*
* 幾何位置相同、棋子種類相同、翻轉狀態相同,即視為同一個搜尋走法。
*/
function minimaxPieceGeometrySignature(piece) {
return [
piece.owner,
piece.svgId,
piece.isFlipped ? 1 : 0,
piece.vertices.map(vertex => vertex.join(',')).join('/')
].join('|');
}
function minimaxTurnGeometrySignature(move) {
return move.map(minimaxPieceGeometrySignature).sort().join('||');
}
/*
* 將 preferredMove 或 preferredSignature 所代表的走法移到最前面。
*
* 此函式只改變搜尋順序,不刪除任何候選,因此不影響 minimax 正確性。
*/
function prioritizeSearchMoves(moves, preferredMoveOrSignature) {
if (!preferredMoveOrSignature || moves.length <= 1) {
return moves;
}
const preferredSignature =
typeof preferredMoveOrSignature === 'string'
? preferredMoveOrSignature
: minimaxTurnGeometrySignature(preferredMoveOrSignature);
const preferredIndex = moves.findIndex(
move => minimaxTurnGeometrySignature(move) === preferredSignature
);
if (preferredIndex <= 0) {
return moves;
}
const result = moves.slice();
const [preferredMove] = result.splice(preferredIndex, 1);
result.unshift(preferredMove);
return result;
}
/*
* 建立非根節點的 PV ordering 局面鍵。
*
* scoredVictims 原本包含棋子 ID;這裡將 victim ID 轉成棋子幾何簽名,
* 避免不同迭代中假想負 ID 不同而導致上一輪 PV 無法重用。
*
* 這個鍵只用於走法排序提示,不參與局面求值或合法性判定。
*/
function getMinimaxPVStateKey(state) {
if (state._minimaxPVStateKey) {
return state._minimaxPVStateKey;
}
const pieceRows = state.boardPieces.map(piece => ({
id: String(piece.id),
signature: minimaxPieceGeometrySignature(piece)
}));
const idToGeometry = new Map(
pieceRows.map(row => [row.id, row.signature])
);
const boardKey = pieceRows
.map(row => row.signature)
.sort()
.join('##');
const countsKey = Object.keys(state.counts)
.sort()
.map(tileId => tileId + ':' + (state.counts[tileId] || 0))
.join(',');
const scoredKey = Array.from(state.scoredVictims)
.map(rawKey => {
const key = String(rawKey);
const separatorIndex = key.indexOf('_');
if (separatorIndex < 0) {
return key;
}
const victimId = key.slice(0, separatorIndex);
const victimGeometry = idToGeometry.get(victimId);
return (victimGeometry || victimId) + key.slice(separatorIndex);
})
.sort()
.join('##');
state._minimaxPVStateKey = [
'mover=' + state.mover,
'score=' + state.scores[1] + ':' + state.scores[2],
'counts=' + countsKey,
'board=' + boardKey,
'scored=' + scoredKey
].join('|||');
return state._minimaxPVStateKey;
}
/*
* 判斷是否為全局最後一手:
*
* 1. 盤面不能是空盤,排除先手第一手;
* 2. 雙方庫存合計只剩一顆;
* 3. 這一顆屬於目前行動方。
*/
function isGlobalFinalSingleTurn(state) {
if (state.boardPieces.length === 0) {
return false;
}
const totalRemaining = Object.values(state.counts).reduce(
(sum, count) => sum + Math.max(0, count || 0),
0
);
return (
totalRemaining === 1 &&
getStateRemainingTotal(state, state.mover) === 1
);
}
/*
* 全局最後一手的專用精確求值。
*
* 不經過:
*
* generateLegalTurns
* -> orderMovesForSearch
* -> analyseSearchMove
* -> applySearchTurnToState
* -> alphaBetaState
*
* 而是直接使用持久增量索引內已計算好的:
*
* index.singleScoring[player]
* index.methods
* method.gain
*
* 選擇規則:
*
* 1. 比較所有能讓行動方實際得分的單接;
* 2. 優先選行動方分差 gain[mover] - gain[opponent] 最大者;
* 3. 分差相同時選行動方得分較高者;
* 4. 若不存在本方得分的單接,找到該剩餘棋種的第一個活法即返回;
* 5. 回傳值仍然轉換為 rootPlayer 的視角。
*/
function solveExactFinalSingleTurn(state, rootPlayer, rootBaseDiff) {
if (!isGlobalFinalSingleTurn(state)) {
return null;
}
const mover = state.mover;
const opponent = mover === 1 ? 2 : 1;
const rootOpponent = rootPlayer === 1 ? 2 : 1;
const remainingTileId = getPlayerTileIds(mover).find(
tileId => (state.counts[tileId] || 0) > 0
);
if (!remainingTileId) {
return {
move: null,
value: evaluateArbitrationTerminal(
state,
rootPlayer,
rootBaseDiff
),
examinedCount: 0
};
}
const index = ensureStateIncrementalSync(state);
let examinedCount = 0;
let bestMethod = null;
let bestMoverNetGain = -Infinity;
let bestMoverOwnGain = -Infinity;
/*
* 先只掃描單接索引。
*
* method.gain 已在建立/更新增量索引時精確計算,
* 此處不再模擬落子。
*/
const scoringKeys =
index.singleScoring[mover] || new Set();
for (const methodKey of scoringKeys) {
const method = index.methods.get(methodKey);
if (!method) continue;
if (method.ghost.svgId !== remainingTileId) continue;
if ((state.counts[method.ghost.svgId] || 0) <= 0) continue;
examinedCount++;
const moverOwnGain = method.gain[mover] || 0;
const opponentGain = method.gain[opponent] || 0;
/*
* 「本方的單接」必須讓目前行動方實際得到分數。
* 只替對手得分的不算本方單接,留到下面的活法保底分支。
*/
if (moverOwnGain <= 0) {
continue;
}
const moverNetGain = moverOwnGain - opponentGain;
if (
moverNetGain > bestMoverNetGain ||
(moverNetGain === bestMoverNetGain &&
moverOwnGain > bestMoverOwnGain)
) {
bestMethod = method;
bestMoverNetGain = moverNetGain;
bestMoverOwnGain = moverOwnGain;
}
}
/*
* 沒有本方單接時,不再比較所有普通活法。
* 只要找到該剩餘棋種的一個活法便立即返回。
*/
if (!bestMethod) {
const liveMethodKeys = index.byPlayer[mover] || new Set();
for (const methodKey of liveMethodKeys) {
const method = index.methods.get(methodKey);
if (!method) continue;
if (method.ghost.svgId !== remainingTileId) continue;
if ((state.counts[method.ghost.svgId] || 0) <= 0) continue;
examinedCount++;
bestMethod = method;
break;
}
}
/*
* 找不到任何活法,等同目前行動方可仲裁的終局。
*/
if (!bestMethod) {
return {
move: null,
value: evaluateArbitrationTerminal(
state,
rootPlayer,
rootBaseDiff
),
examinedCount
};
}
const gain = bestMethod.gain || { 1: 0, 2: 0 };
/*
* evaluateSearchState(state) 是落子前的 root 分差;
* 再加上最後一手對 root 分差造成的精確變化。
*/
const value =
evaluateSearchState(state, rootPlayer, rootBaseDiff) +
(gain[rootPlayer] || 0) -
(gain[rootOpponent] || 0);
return {
move: [bestMethod.ghost],
value,
examinedCount
};
}三、完整替換 runStateMinimax()
找到目前整個:
async function runStateMinimax(rootState, rootMoves, maxDepth, deadline) {
...
}將整個函式完整替換成以下版本:
async function runStateMinimax(
rootState,
rootMoves,
maxDepth,
deadline
) {
const minimaxStartTime = performance.now();
const rootPlayer = rootState.mover;
const opponent = rootPlayer === 1 ? 2 : 1;
const rootBaseDiff =
rootState.scores[rootPlayer] - rootState.scores[opponent];
let searchCount = 0;
let committedBestMove =
rootMoves && rootMoves.length > 0 ? rootMoves[0] : null;
let committedBestValue = 0;
/*
* 保存上一個已完成迭代中,各非根局面的最佳走法簽名。
*
* 下一個 iterative-deepening 深度重新遇到同一局面時,
* 會把該走法移到 orderMovesForSearch() 結果的最前面。
*/
const pvMoveByState = new Map();
let progressDepth = 0;
let progressRootDone = 0;
let progressRootTotal =
rootMoves && rootMoves.length > 0 ? rootMoves.length : 0;
let lastMinimaxProgressTime = 0;
const MINIMAX_PROGRESS_INTERVAL = 100;
/*
* 更新消息框並讓出主執行緒。
*
* 即使目前仍在同一個大型根分支內,
* searchCount 也會持續增加並每約 100ms 重繪一次。
*/
async function publishMinimaxProgress(force = false) {
const now = performance.now();
if (
!force &&
now - lastMinimaxProgressTime <
MINIMAX_PROGRESS_INTERVAL
) {
return;
}
showMessage(
t('ai-minimax-progress', {
depth: progressDepth,
done: progressRootDone,
total: progressRootTotal,
count: searchCount,
time: (
(now - minimaxStartTime) /
1000
).toFixed(1)
}),
0,
true
);
/*
* 必須真正讓出事件迴圈,否則 DOM 雖已修改,
* 瀏覽器仍無法重繪消息框,也不能處理取消 AI 的點擊。
*/
await new Promise(resolve => setTimeout(resolve, 0));
lastMinimaxProgressTime = performance.now();
}
/*
* 保險處理:若直接以全局最後一手呼叫本函式,
* 仍走專用精確分支,不進入一般 alpha-beta。
*/
const exactRootResult = solveExactFinalSingleTurn(
rootState,
rootPlayer,
rootBaseDiff
);
if (exactRootResult) {
return {
bestMove: exactRootResult.move,
value: exactRootResult.value,
searchCount: Math.max(
1,
exactRootResult.examinedCount || 0
)
};
}
if (!rootMoves || rootMoves.length === 0) {
return {
bestMove: null,
value: evaluateArbitrationTerminal(
rootState,
rootPlayer,
rootBaseDiff
),
searchCount
};
}
async function alphaBetaState(
state,
depth,
alpha,
beta
) {
searchCount++;
if (cancelAi || performance.now() >= deadline) {
throw new SearchTimeout();
}
/*
* 定時更新搜尋次數。
*
* alphaBetaState 雖然改為 async,但只有超過節流間隔時
* 才真正 setTimeout(0),避免每個節點都強制重繪。
*/
if (
searchCount === 1 ||
performance.now() - lastMinimaxProgressTime >=
MINIMAX_PROGRESS_INTERVAL
) {
await publishMinimaxProgress(false);
if (cancelAi || performance.now() >= deadline) {
throw new SearchTimeout();
}
}
/* 棋子全部下完是正常終局。 */
if (isSearchGameComplete(state)) {
return evaluateSearchState(
state,
rootPlayer,
rootBaseDiff
);
}
/*
* 全局最後一手直接精確求值。
*
* 必須放在 hasArbitrationDefence() 及 depth 截斷之前,
* 否則最後一手仍會生成全部合法回合或被當作普通葉節點。
*/
const exactFinalResult = solveExactFinalSingleTurn(
state,
rootPlayer,
rootBaseDiff
);
if (exactFinalResult) {
if (exactFinalResult.move) {
pvMoveByState.set(
getMinimaxPVStateKey(state),
minimaxTurnGeometrySignature(
exactFinalResult.move
)
);
}
return exactFinalResult.value;
}
/*
* 上一方若沒有留下至少一組共活,
* 行動方可立即仲裁獲勝。
*/
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
);
}
const statePVKey = getMinimaxPVStateKey(state);
const previousPVMoveSignature =
pvMoveByState.get(statePVKey);
/*
* 先執行原本的戰術排序,再把上一輪 PV 最佳走法
* 移到最前面。
*/
legalMoves = orderMovesForSearch(
state,
legalMoves
);
legalMoves = prioritizeSearchMoves(
legalMoves,
previousPVMoveSignature
);
if (cancelAi || performance.now() >= deadline) {
throw new SearchTimeout();
}
const maximizing = state.mover === rootPlayer;
if (maximizing) {
let best = -Infinity;
let bestMove = null;
for (const move of legalMoves) {
const child = applySearchTurnToState(
state,
move,
{
validate: false
}
).state;
const value = await alphaBetaState(
child,
depth - 1,
alpha,
beta
);
if (value > best) {
best = value;
bestMove = move;
}
alpha = Math.max(alpha, best);
if (beta <= alpha) {
break;
}
}
if (bestMove) {
pvMoveByState.set(
statePVKey,
minimaxTurnGeometrySignature(bestMove)
);
}
return best;
}
let best = Infinity;
let bestMove = null;
for (const move of legalMoves) {
const child = applySearchTurnToState(
state,
move,
{
validate: false
}
).state;
const value = await alphaBetaState(
child,
depth - 1,
alpha,
beta
);
if (value < best) {
best = value;
bestMove = move;
}
beta = Math.min(beta, best);
if (beta <= alpha) {
break;
}
}
if (bestMove) {
pvMoveByState.set(
statePVKey,
minimaxTurnGeometrySignature(bestMove)
);
}
return best;
}
/*
* orderedRootMoves 會在每個完整迭代結束後,
* 把 committedBestMove 放到最前面。
*/
let orderedRootMoves = rootMoves.slice();
for (let depth = 1; depth <= maxDepth; depth++) {
/*
* 根節點 PV ordering:
* 上一層完整搜尋得到的最佳走法優先。
*/
orderedRootMoves = prioritizeSearchMoves(
orderedRootMoves,
committedBestMove
);
let iterationBestMove =
committedBestMove || orderedRootMoves[0];
let iterationBestValue = -Infinity;
let iterationCompleted = true;
/*
* 根節點共享 alpha。
*
* 前一個根候選取得的最佳值會傳給下一個根候選,
* 不再讓每個候選都使用 [-Infinity, Infinity]。
*/
let rootAlpha = -Infinity;
progressDepth = depth;
progressRootDone = 0;
progressRootTotal = orderedRootMoves.length;
await publishMinimaxProgress(true);
try {
for (const move of orderedRootMoves) {
if (
cancelAi ||
performance.now() >= deadline
) {
throw new SearchTimeout();
}
const child = applySearchTurnToState(
rootState,
move,
{
validate: false
}
).state;
/*
* 關鍵修改:
*
* 原本:
* alphaBetaState(
* child,
* depth - 1,
* -Infinity,
* Infinity
* )
*
* 現在:
* 所有根候選共享 rootAlpha。
*/
const value = await alphaBetaState(
child,
depth - 1,
rootAlpha,
Infinity
);
if (value > iterationBestValue) {
iterationBestValue = value;
iterationBestMove = move;
}
/*
* 將目前根節點最佳值共享給下一個根候選。
*/
rootAlpha = Math.max(
rootAlpha,
iterationBestValue
);
progressRootDone++;
/*
* 分支完成後嘗試更新一次。
* 若距離上次更新不足 100ms,會被節流。
*/
await publishMinimaxProgress(false);
}
} catch (error) {
if (!(error instanceof SearchTimeout)) {
throw error;
}
iterationCompleted = false;
}
/*
* 只提交完整搜尋完所有根候選的深度。
* 超時的未完成深度不覆蓋上一個可靠結果。
*/
if (!iterationCompleted) {
break;
}
committedBestMove = iterationBestMove;
committedBestValue = iterationBestValue;
/*
* 將本層最佳走法放到下一層根候選最前面。
*/
orderedRootMoves = prioritizeSearchMoves(
orderedRootMoves,
committedBestMove
);
await publishMinimaxProgress(true);
}
return {
bestMove: committedBestMove,
value: committedBestValue,
searchCount
};
}這個替換完成了三件事
1. 根節點共享 alpha
原本:
const value = alphaBetaState(
child,
depth - 1,
-Infinity,
Infinity
);現在:
const value = await alphaBetaState(
child,
depth - 1,
rootAlpha,
Infinity
);每個根候選完成後:
rootAlpha = Math.max(
rootAlpha,
iterationBestValue
);2. 根節點與非根節點 PV ordering
根節點:
orderedRootMoves = prioritizeSearchMoves(
orderedRootMoves,
committedBestMove
);非根節點:
const previousPVMoveSignature =
pvMoveByState.get(statePVKey);
legalMoves = orderMovesForSearch(state, legalMoves);
legalMoves = prioritizeSearchMoves(
legalMoves,
previousPVMoveSignature
);3. 搜尋過程定時讓出主執行緒
alphaBetaState() 改為非同步,每約 100ms:
await publishMinimaxProgress(false);因此消息框可以在同一個大型根分支尚未完成時,繼續顯示增加中的搜尋次數。
四、替換 startAI() 中建立根候選及選擇搜尋器的部分
在 startAI() 中,找到從:
const arbitrationDefences = generateArbitrationDefenceTurns(rootState);開始,一直到原本這一整段結束:
if (useMinimax) {
...
} else {
...
}也就是在:
searchCount = result.searchCount || 0;之前的整段程式。
完整替換成:
const turnsLeft = getRemainingTurnCount(rootState);
const useMinimax =
turnsLeft <= ENDGAME_MINIMAX_TURNS;
let rootMoves = [];
let result;
/*
* 全局最後一手直接使用專用精確求值。
*
* 此分支不呼叫:
*
* generateArbitrationDefenceTurns
* generateLegalTurns
* orderMovesForSearch
* analyseSearchMove
* applySearchTurnToState
* runStateMinimax
*
* 因此不會再對最後一子的每個候選重複模擬。
*/
if (isGlobalFinalSingleTurn(rootState)) {
const rootOpponent =
aiPlayer === 1 ? 2 : 1;
const rootBaseDiff =
rootState.scores[aiPlayer] -
rootState.scores[rootOpponent];
const exactFinalResult =
solveExactFinalSingleTurn(
rootState,
aiPlayer,
rootBaseDiff
);
if (
!exactFinalResult ||
!exactFinalResult.move
) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}
rootMoves = [exactFinalResult.move];
result = {
bestMove: exactFinalResult.move,
value: exactFinalResult.value,
searchCount: Math.max(
1,
exactFinalResult.examinedCount || 0
)
};
showMessage(
t('ai-minimax-progress', {
depth: 1,
done: 1,
total: 1,
count: result.searchCount,
time: (
(performance.now() - totalStartTime) /
1000
).toFixed(1)
}),
0,
true
);
/*
* 讓最後一手的精確搜尋結果也能實際顯示,
* 並讓取消事件有機會被處理。
*/
await new Promise(resolve =>
setTimeout(resolve, 0)
);
if (cancelAi) {
aiThinking = false;
return;
}
} else {
/*
* 非最後一手維持原本仲裁流程。
*/
const arbitrationDefences =
generateArbitrationDefenceTurns(rootState);
if (
!isSearchGameComplete(rootState) &&
arbitrationDefences.length === 0
) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}
rootMoves = generateLegalTurns(rootState);
if (rootMoves.length === 0) {
aiThinking = false;
aiTriggerArbitrationSuccess();
return;
}
if (cancelAi) {
aiThinking = false;
return;
}
rootMoves = shuffledCopy(rootMoves);
/*
* 保留原本的戰術排序。
* minimax 進入下一層迭代後,再由 runStateMinimax()
* 把上一輪 PV 最佳走法移到最前面。
*/
rootMoves = orderMovesForSearch(
rootState,
rootMoves
);
if (cancelAi) {
aiThinking = false;
return;
}
const searchStartTime = performance.now();
const searchDeadline =
searchStartTime + timeLimit;
if (useMinimax) {
result = await runStateMinimax(
rootState,
rootMoves,
turnsLeft,
searchDeadline
);
} else {
result = await runStateMCTS(
rootState,
rootMoves,
searchDeadline,
MCTS_C
);
}
}後面的原程式保持不變,從以下這一行繼續:
searchCount = result.searchCount || 0;五、替換舊的 placeAISingleMove()
目前這個函式雖然沒有直接被新的 startAI() 最後一手流程呼叫,但它仍然保留了舊的「逐一 simulateScoreGain()」實作。
為避免將來其他入口呼叫它時又走回舊流程,建議完整替換。
找到:
function placeAISingleMove(player) {
...
}替換成:
/*
* 最後一子相容入口。
*
* startAI() 現在會直接呼叫 solveExactFinalSingleTurn(),
* 此函式保留給其它可能的舊入口使用。
*/
function placeAISingleMove(player) {
const state =
createSearchStateFromGlobals(player);
if (!isGlobalFinalSingleTurn(state)) {
return false;
}
const opponent =
player === 1 ? 2 : 1;
const rootBaseDiff =
state.scores[player] -
state.scores[opponent];
const exactResult =
solveExactFinalSingleTurn(
state,
player,
rootBaseDiff
);
if (!exactResult || !exactResult.move) {
return false;
}
const source = exactResult.move[0];
const piece =
JSON.parse(JSON.stringify(source));
piece.id = nextPieceId++;
piecesCount[piece.svgId]--;
tempPieces = [piece];
updateUI();
renderBoard();
setTimeout(() => {
actionCheck();
}, 600);
return true;
}六、修改後的搜尋流程
七、正確性及遊戲流程影響
這些修改不會改變以下流程:
validateTurnOnState()正式合法性判定applySearchTurnToState()正式計分actionCheck()正式落子- 仲裁只承認共活的規則
- MCTS 的選擇、展開及回傳
- 人類落子流程
- 棋譜匯入及匯出
- undo/redo
committedSearchState的持久增量索引- 最後實際落子仍會重新經過正式合法性與計分流程
根節點共享 alpha 只會讓「已經不可能超越目前最佳根走法」的候選更早剪枝,不會改變最佳走法。
PV ordering 也只改變候選嘗試順序,不會刪除候選,不會影響 minimax 結果。
最後一子則直接使用增量索引中已經由 adjudicateSearchTurn() 計算並持續維護的 method.gain,避免原本的:
排序時模擬一次
→ 遞迴前再套用一次
→ 葉節點再評估同時根節點及非根節點遇到全局最後一手時,都會進入同一個專用精確分支。