换位表导致测试失败(但在游戏中运行良好)

Transposition Table is causing tests to fail(but runs fine in game)

本文关键字:游戏 运行 测试 失败 换位      更新时间:2023-10-16

我在我的TicTacToe minmax算法中添加了一个换位表

int AI::findBestMove()
{
hash = tTable->recalculateHash();
int bestMove = minMax().second;
return bestMove;
}
std::pair<int, int> AI::minMax(int reverseDepth, std::pair<int, int> bestScoreMove, player currentPlayer, int alpha, int beta, int lastPlay)
{
Entry e = (*tTable)[hash];
if (e && e.depth == reverseDepth)
return e.scoreMove;
if (reverseDepth == 0)
return { 0, -2 };
else if (field->canDrawOrWin() && lastPlay != -1)
{
if (field->hasWon(lastPlay))
return { evaluateScore(currentPlayer), -1 };
else if (field->isDraw())
return { 0, -1 };
}
bestScoreMove.first = currentPlayer == player::AI ? INT_MIN : INT_MAX;
for (int i = 0; i < field->size(); i++)
{
if ((*field)[i] == player::None && field->isCoordWorthChecking(i))
{
(*field)[i] = currentPlayer;
hash = tTable->calculateHash(hash, i);
std::pair<int, int> scoreMove = minMax(reverseDepth - 1, bestScoreMove, getOpponent(currentPlayer), alpha, beta, i);
if (currentPlayer == player::AI)
{
alpha = std::max(alpha, scoreMove.first);
if (bestScoreMove.first < scoreMove.first)
bestScoreMove = { scoreMove.first, i };
}
else
{
beta = std::min(beta, scoreMove.first);
if (bestScoreMove.first > scoreMove.first)
bestScoreMove = { scoreMove.first, i };
}
hash = tTable->calculateHash(hash, i);
(*field)[i] = player::None;
if (beta <= alpha)
break;
}
}
tTable->placeEntry(hash, bestScoreMove, reverseDepth);
return bestScoreMove;
}

为了测试它,我做了一个验收测试,播放所有可能的棋盘并检查人类是否赢得

TEST(AcceptanceTest, EveryBoard)
{
int winstate = 0;
std::shared_ptr<Field> field = std::make_shared<Field>(4);
AI ai(field);
playEveryBoard(ai, field, winstate);
std::cout <<"Human wins: " << winstate << std::endl;
}
void playEveryBoard(AI& ai, std::shared_ptr<Field> f, int& winstate)
{
int bestMove = 0;
auto it = f->begin();
while (true)
{
it = std::find(it, f->end(), player::None);
if (it == f->end())
break;
*it = player::Human;
if (f->hasWon())
winstate++;
EXPECT_TRUE(!f->hasWon());
bestMove = ai.findBestMove();
if (bestMove == -1)//TIE
{
*it = player::None;
break;
}
(*f)[bestMove] = player::AI;
if (f->hasWon())//AI WIN
{
*it = player::None;
(*f)[bestMove] = player::None;
break;
}
playEveryBoard(ai, f, winstate);
*it = player::None;
(*f)[bestMove] = player::None;
if (it == f->end())
break;
it++;
}
}

在我添加换位表之前,测试从未返回任何松动状态,为了测试松动状态何时出现,我做了一个测试,播放松动字段的每个排列,但它从未发现松动状态,是什么导致AI只在EveryBoard测试中松动?

TEST(LoosePossible, AllPermutations)
{
std::vector<int> loosingField = { 2, 3, 7, 11, 12, 13, 15 };
do{
std::shared_ptr<Field> field = std::make_shared<Field>(4);
AI *ai = new AI(field);
for (auto i : loosingField)
{
if ((*field)[i] != player::None || field->hasWon())
break;
(*field)[i] = player::Human;
EXPECT_TRUE(!field->hasWon());
(*field)[ai->findBestMove()] = player::AI;
}
delete ai;
} while (next_permutation(loosingField.begin(), loosingField.end()));
}

我看到至少有两个地方可能会出现这些错误。

一个潜在的问题就在这条线上:

Entry e = (*tTable)[hash];
if (e && e.depth == reverseDepth)
return e.scoreMove;

除了检查换位表是否存储了相同深度的搜索结果外,还需要检查表中存储的边界是否与表中的边界兼容。

我在回答另一个问题时提到了这一点:

在换位表中存储值时,还需要存储搜索过程中使用的alpha和beta边界。当您在搜索中期从节点处获取值时,它要么是真值的上界(因为值=β(,要么是真数值的下界(因为值=alpha(,要么就是节点的实际值(alpha<value<beta(。您需要将其存储在换位表中。然后,当你想重用该值时,你必须检查你是否可以使用给定当前阿尔法和贝塔边界的值。(在换位表中找到值后,您可以通过实际搜索来验证这一点,看看您是否从搜索中获得了与表中相同的值。(

对此进行测试的方法是修改AI::minMax。当从换位表返回值时,将标志设置为true。然后,每次返回值时,如果换位表标志为true,请将要返回的值与在换位表中找到的值进行比较。如果它们不一样,那么就有问题了。

此外,minimax通常与零和游戏一起使用,这意味着两个玩家的分数之和应该加成0。我不知道所有返回的值在您的代码中意味着什么,但有时您返回{0, -1},有时返回{0, -2}。这是有问题的,因为现在你有一个非零和游戏,很多理论都崩溃了。

特别地,最大玩家可以相同地对待{0, -1}{0, -2},但是最小玩家不会。因此,如果移动顺序以任何方式发生变化,您可能会以不同的顺序看到这些,因此树根部的值将不稳定。

顺便说一句,这是多人游戏中的一个基本问题。实际上,当一个玩家是造王者时,就会出现这种情况。他们自己不能赢得比赛,但他们可以决定谁赢。