You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Negamax实现中置换表引发错误落子的问题排查

国际象棋AI置换表实现引发Alpha-Beta剪枝错误

我用TypeScript开发了一款国际象棋AI,核心采用带Alpha-Beta剪枝的Negamax算法搜索可行走法。AI配置了两种启发式函数:

  • 主启发式函数:用于评估Negamax树遍历中的叶节点
  • 轻量启发式函数:用于对走法排序,减少搜索节点数量

为了节省计算时间,我参考维基百科Negamax页面的伪代码实现了置换表(transposition table)。虽然速度提升明显,但AI出现了错误落子的情况。我怀疑是置换表的实现导致Alpha-Beta剪枝逻辑异常,使得AI误判已剪枝的其他走法,但无法准确定位问题。

/**
 * Depth first search: negamax with a/b pruning
 */
private lookAheadAtMove(
    boardState: ChessBoardState,
    player: ChessPlayer,
    enemy: ChessPlayer,
    depthRemaining: number,
    alphaPrune: number,
    betaPrune: number,
    negateMult: number,
    //pathMoves: ChessBoardSingleMove[],
    transpositionTable: Map<string, iTranspositionTableEntry>
): iLookahedResponse {
    let bestMoveH: iChessAiHeuristicEvaluation = {
        score: -Infinity,
        data: {},
    };
    //let bestMovePath = pathMoves;
    let bestMove!: ChessBoardSingleMove;

    const transpositionTableKey = boardState.toString();

    if (depthRemaining === 0) {
        // base case, depth is 0
        bestMoveH = this.heuristic.getScore(boardState);
        bestMoveH.score *= negateMult;
    } else {
        // default, keep looking
        const possibleMoves: {
            move: ChessBoardSingleMove;
            score: number;
        }[] = [];
        // sort based on initial board state analysis
        for (const move of boardState
            .getPossibleMovesForPlayer(player)
            .getMoves()) {
            const moveIsGood = this.tryMakeMove(boardState, move);
            if (moveIsGood) {
                const score =
                    this.sortHeuristic.getScore(boardState).score *
                    negateMult;

                possibleMoves.push({
                    move,
                    score,
                });
                boardState.undoLastMove();
            }
        }

        // sort the moves based on initial heuristic estimate
        possibleMoves.sort((a, b) => b.score - a.score);

        let alphaOriginal = alphaPrune;

        for (const { move } of possibleMoves) {
            let thisMoveH!: iLookahedResponse;
            if (transpositionTable.has(transpositionTableKey)) {
                const tableResult = transpositionTable.get(transpositionTableKey)!;
                if (tableResult.depthRemaining <= depthRemaining) {
                    switch (tableResult.type) {
                        case 'exact':
                            thisMoveH = {...tableResult};
                            thisMoveH.hScore.score *= negateMult;
                            break;
                        case 'upperbound':
                            alphaPrune = Math.max(alphaPrune, tableResult.hScore.score * negateMult);
                            break;
                        case 'lowerbound':
                            betaPrune = Math.min(betaPrune, tableResult.hScore.score * negateMult);
                            break;
                    }
                }
            }

            // if we didn't grab from the transposition table, make the move now
            if (!thisMoveH) {
                boardState.setPiecesFromMove(move, "");
                thisMoveH = this.lookAheadAtMove(
                    boardState,
                    enemy,
                    player,
                    depthRemaining - 1,
                    -betaPrune,
                    -alphaPrune,
                    -negateMult,
                    transpositionTable
                );
                // cleanup for next iteration
                boardState.undoLastMove();
                thisMoveH.hScore.score *= -1;
            }


            // compare scores
            if (thisMoveH.hScore.score >= bestMoveH.score) {
                bestMoveH = thisMoveH.hScore;
            }

            // add to transposition table
            let type: 'exact' | 'lowerbound' | 'upperbound';
            if (bestMoveH.score <= alphaOriginal) {
                type = 'upperbound';
            } else if (bestMoveH.score >= betaPrune) {
                type = 'lowerbound';
            } else {
                type = 'exact';
            }
            // multiply back by negate multi to put the score to to absolute value
            const tableScore = { ...thisMoveH.hScore };
            tableScore.score *= negateMult;
            transpositionTable.set(transpositionTableKey, {
                depthRemaining,
                move,
                hScore: tableScore,
                type
            });

            if (bestMoveH.score > alphaPrune) {
                alphaPrune = bestMoveH.score;
                bestMove = move;

            }


            if (alphaPrune >= betaPrune) {
                break;
            }
        }
    }

    const returnValue = { hScore: bestMoveH, move: bestMove };

    return returnValue;
}

我已验证算法其余部分正常,仅在启用置换表时出现落子错误。尝试过的解决方法:

  • 仅存储启发式分数,不保存上下界类型
  • 修改正负值处理的数学逻辑
  • 仅在特定深度使用置换表
  • 调换置换表使用判断中的<=与>=(按逻辑应采用<=以仅使用更深搜索的分数,但两种方式均无效)

内容的提问来源于stack exchange,提问作者byte-this

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 07:11:07