注意:如果您认为此帖子看起来没有足够的详细信息,请发表评论,例如:代码,结果和其他东西;我会相应地编辑帖子。
注2:我自己手工编写了这个程序。
我有一个negamax实现,其结果对我来说看起来非常错误我已经尝试了很多方法来调试它但我仍然无法得到它的关键。
首先,这是Tic Tac Toe的一个negamax实现,它具有3X3的板。
以下代码是完整集,以便复制我对此算法的错误。如果我遗漏了任何内容,请在下面发表评论。
一个例子可以做这个主要的:
int main {
Board board;
board.startGameNage(0,0);
}
我希望游戏以平局结束,因为这是电脑(完美播放器)与电脑(完美播放器),然而,使用以下一组功能,我得到了如下所示的游戏:
当前最大移动为:0,0,当前得分为:-inf 当前最大移动为:0,2,当前得分为:3 当前最大移动为:0,1,当前得分为:-3 当前最大移动为:1,1,当前得分为:3 当前最大移动为:2,0,当前得分为:-3 当前最大移动是:1,2,当前得分为:3 当前最大移动是:2,1,当前得分为:-3 当前最大移动是:1,0,当前得分为:3 当前最大移动为:1,0,当前得分为:-3
X X O
X O O
X X ---
“ - ”表示在该单元格中没有移动,这显然是错误的。
我首先实现了我的minimax,这种negamax在某种程度上基于我的minimax实现而发展,这可能是我无法看到我的错误的原因。
我认为minimax从2名球员的角度进行移动并且评分得分也相同,而negamax从2名球员的角度进行移动,但仅从当前玩家的角度评估得分。
我想这有点困扰我。我似乎无法看到我的实现在这里出错了。
我通过main中的以下功能开始游戏:
// in main I will just give the following function a coordinate, e.g. (0,0)
void Board::startGameNega(const int & row, const int & col){
Move move(row, col);
int player = 1;
for (int depth = 0; depth < 9; depth++){
applyMoveNega(move, player);
Move current_move = move;
move = negaMax(depth, player, move);
player = -player;
cout << "current Max move is: " << current_move.getRow()
<< " , "
<< current_move.getCol()
<< ", Current score is: "
<< current_move.getScore() << endl;
}
print(); // print the end of game board
}
这是board.hpp:
#define LENGTH 3
#define WIDTH 3
#define CROSS 1
#define NOUGHT -1
#include <iostream>
#include <vector>
#include <array>
#include <map>
#include "Move.hpp"
using namespace std;
#pragma once
typedef vector<Move> Moves;
struct Board {
// constructors;
Board(int width, int length) :m_width(width), m_length(width){};
Board(){};
// destructor;
~Board(){};
// negamax;
Move negaMax(const int & depth, const int & player, const Move & initialMove);
void startGameNega(const int & row, const int & col);
void applyMoveNega(const Move & move, const int & player);
bool isWon(const int & player);
bool isGameComplete();
int evaluateGameStateNega(const int & depth, const int & player);
// share;
int getOpponent(const int & player);
void deleteMove(const Move & move);
void deleteMoves(const Move & initialMove);
// utilities;
static int defaultBoard[WIDTH][LENGTH];
int getWidth() const { return m_width; }
int getLength() const { return m_length; }
void setWidth(int width){ m_width = width; }
void setLength(int length){ m_length = length; }
void print();
int getCurrentPlayer();
private:
int m_width;
int m_length;
enum isWin{ yes, no, draw };
int result;
int m_player;
};
此处列出的一些关键要素:
打印:
void Board::print(){
for (int i = 0; i < WIDTH; i++) {
for (int j = 0; j < LENGTH; j++) {
switch (defaultBoard[i][j]) {
case CROSS:
cout << "X";
break;
case NOUGHT:
cout << "O";
break;
default:
cout << "-";
break;
}
cout << " ";
}
cout << endl;
}
}
generateMoves:
Moves Board::generateMoves(const int &rowIndex, const int &colIndex){
Moves Moves;
if (defaultBoard){
for (int i = 0; i < WIDTH; i++)
{
for (int j = 0; j < LENGTH; j++)
{
if (i == rowIndex && j == colIndex)
{
continue;
}
else if (defaultBoard[i][j] == 1 || defaultBoard[i][j] == 4)
{
continue;
}
else if (defaultBoard[i][j] == 0)
{
Move move(i, j);
Moves.push_back(move);
}
}
}
}
return Moves;
}
applyMovesNega:
void Board::applyMoveNega(const Move & move, const int & player){
if (player == 1){
defaultBoard[move.getRow()][move.getCol()] = CROSS;
}
else if (player == -1)
{
defaultBoard[move.getRow()][move.getCol()] = NOUGHT;
}
}
isGameComplete:
bool Board::isGameComplete(){
if (defaultBoard[0][0] == defaultBoard[0][1] && defaultBoard[0][1] == defaultBoard[0][2] && defaultBoard[0][0] != 0 ||
defaultBoard[1][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[1][2] && defaultBoard[1][0] != 0 ||
defaultBoard[2][0] == defaultBoard[2][1] && defaultBoard[2][1] == defaultBoard[2][2] && defaultBoard[2][0] != 0 ||
defaultBoard[0][0] == defaultBoard[1][0] && defaultBoard[1][0] == defaultBoard[2][0] && defaultBoard[0][0] != 0 ||
defaultBoard[0][1] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][1] && defaultBoard[0][1] != 0 ||
defaultBoard[0][2] == defaultBoard[1][2] && defaultBoard[1][2] == defaultBoard[2][2] && defaultBoard[0][2] != 0 ||
defaultBoard[0][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][2] && defaultBoard[0][0] != 0 ||
defaultBoard[2][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[0][2] && defaultBoard[2][0] != 0){
return true;
}
return false;
}
评估得分:
int Board::evaluateGameStateNega(const int & depth, const int & player){
int new_score;
isWon(player);
if (result == isWin::yes)
new_score = 10 - depth;
else if (result == isWin::no)
new_score = depth - 10;
else
new_score = 0;
return new_score;
}
deleteMove:
void Board::deleteMove(const Move & move){
defaultBoard[move.getRow()][move.getCol()] = 0;}
这是move.hpp:
struct Move{
Move(){};
Move(const int & index) :m_rowIndex(index / 3),m_colIndex(index % 3){};
Move(const int & row, const int & col) :m_rowIndex(row), m_colIndex(col){};
Move(const int & row, const int & col, const int & score):m_rowIndex(row), m_colIndex(col), m_score(score){};
~Move(){};
//member functions;
int getRow() const { return m_rowIndex; };
int getCol() const { return m_colIndex; };
void setRow(const int & row){ m_rowIndex = row; };
void setCol(const int & col){ m_colIndex = col; };
void setScore(const int & score){ m_score = score; };
int getScore() const { return m_score; }
private:
int m_rowIndex;
int m_colIndex;
int m_score;
};
这是实际的NegaMax功能:
Move Board::negaMax(const int & depth, const int & curPlayer, const Move & initialMove){
int row = initialMove.getRow();
int col = initialMove.getCol();
int _depth = depth;
int _curplayer = curPlayer;
Moves moves = generateMoves(row, col);
Move bestMove;
Move proposedNextMove;
//change to isGameComplete as of 15/10;
if (_depth == 8 || isGameComplete())
{
int score = evaluateGameStateNega(_depth, _curplayer);
bestMove.setScore(score);
bestMove.setRow(initialMove.getRow());
bestMove.setCol(initialMove.getCol());
}
else{
_depth += 1;
int bestScore = -1000;
for (auto move : moves){
applyMoveNega(move, -_curplayer);
proposedNextMove = negaMax(_depth, -_curplayer, move);
int tScore = -proposedNextMove.getScore();
proposedNextMove.setScore(tScore);
if (proposedNextMove.getScore() > bestScore){
bestScore = proposedNextMove.getScore();
bestMove.setScore(bestScore);
bestMove.setRow(move.getRow());
bestMove.setCol(move.getCol());
}
deleteMove(move);
}
}
return bestMove;
}
我使用以下功能评估游戏状态:
bool Board::isWon(const int & player){
if (defaultBoard[0][0] == defaultBoard[0][1] && defaultBoard[0][1] == defaultBoard[0][2] && defaultBoard[0][0] == player ||
defaultBoard[1][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[1][2] && defaultBoard[1][0] == player ||
defaultBoard[2][0] == defaultBoard[2][1] && defaultBoard[2][1] == defaultBoard[2][2] && defaultBoard[2][0] == player ||
defaultBoard[0][0] == defaultBoard[1][0] && defaultBoard[1][0] == defaultBoard[2][0] && defaultBoard[0][0] == player ||
defaultBoard[0][1] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][1] && defaultBoard[0][1] == player ||
defaultBoard[0][2] == defaultBoard[1][2] && defaultBoard[1][2] == defaultBoard[2][2] && defaultBoard[0][2] == player ||
defaultBoard[0][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][2] && defaultBoard[0][0] == player ||
defaultBoard[2][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[0][2] && defaultBoard[2][0] == player){
result = isWin::yes;
return true;
}
else if (defaultBoard[0][0] == defaultBoard[0][1] && defaultBoard[0][1] == defaultBoard[0][2] && defaultBoard[0][0] == -player ||
defaultBoard[1][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[1][2] && defaultBoard[1][0] == -player ||
defaultBoard[2][0] == defaultBoard[2][1] && defaultBoard[2][1] == defaultBoard[2][2] && defaultBoard[2][0] == -player ||
defaultBoard[0][0] == defaultBoard[1][0] && defaultBoard[1][0] == defaultBoard[2][0] && defaultBoard[0][0] == -player ||
defaultBoard[0][1] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][1] && defaultBoard[0][1] == -player ||
defaultBoard[0][2] == defaultBoard[1][2] && defaultBoard[1][2] == defaultBoard[2][2] && defaultBoard[0][2] == -player ||
defaultBoard[0][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][2] && defaultBoard[0][0] == -player ||
defaultBoard[2][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[0][2] && defaultBoard[2][0] == -player)
{
result = isWin::no;
return true;
}
result = isWin::draw;
return false;
}
答案 0 :(得分:0)
感谢@PaulMckenzie指出我的一些代码问题。
但它们与我在Negamax的核心逻辑上所犯的错误毫无关系。
一个接一个,我会把它们全部列在这里,并希望它也可以帮助那些想要学习Negamax的人。如果我想念任何评论,我会在之后进行编辑。
*
请记住将所有新字段初始化为值,不要离开 他们为逻辑决定什么是初始值。这有助于 调试,这只是一个很好的代码实践。感谢@PaulMcKenzie
*
他们所做的只是删除建议的移动/应用建议的移动;他们不回馈/转牌给当前的球员。因为移动被认为是当前玩家的对手的最佳移动,所以一旦我们完成计算此提议移动的得分(A)并且我们想要测试下一个提议的移动(B),我们将需要删除A并给出转回当前的球员。 (或者,对某些人来说,它更好地被理解为以前的玩家。)当我们应用提议的移动时,这同样适用。
因此,它应该是: void Board::deleteMoveNega(const Move & move){
defaultBoard[move.getRow()][move.getCol()] = EMPTY;
m_player = getOpponent(m_player); // give turn back to current player;
}
void Board::applyMoveNega(const Move & move){
defaultBoard[move.getRow()][move.getCol()] = m_player;
m_player = getOpponent(m_player); // pass on the turn to current player;
}
这是我犯的最重要的错误,因为旧的代码我会建议移动并计算得分,因为无论谁开始游戏都是如此;因为我在 startGameNage()中手动将玩家设置为对手,然后我作为对手提出移动并且仅仅作为对手计算得分而玩游戏(而我应该真正切换上下文和在两个球员的位置)。这发生在negamax函数的每次迭代中。这并没有强化作为当前球员的思维概念,因为当我应该扮演现在的球员时,我却扮演了对手的角色。
这在negamax中根本就是错误的。
一旦我们解决了这个问题,我们就不需要在 startGameNage()中手动设置转弯了:
player = -player;
应该删除并且:
int player = 1;
将更改为:
m_player = 1;
deleteMove()和 applyMove()整理后,我们现在可以查看一下我们的negamax引擎了。
applyMoveNega(move, -_curplayer);
proposedNextMove = negaMax(_depth, -_curplayer, move);
首先,我不需要当前的播放器参数。我有私人 m_player 我可以利用它。
其次,更重要的是,使用旧的 deleteMove()和 applyMove()并在startGameNega()中手动设置,这里对玩家的否定(-_curplayer)是错的。
例如,我们为 -_ curplayer 申请/移动;建议的移动接下来应该是对手,在我们的情况下,应该 _curplayer 。我仍在传递 -_ curplayer ,这将从一开始就为错误的玩家生成动作。
一个新的核心negamax将是:
Move Board::negaMax(const int & depth, const Move & initialMove){
int row = initialMove.getRow();
int col = initialMove.getCol();
int _depth = depth;
Move bestMove;
Move proposedNextMove;
//change to isGameComplete as of 15/10;
if (_depth == 8 || isGameComplete())
{
int score = evaluateGameStateNega(_depth);
bestMove.setScore(score);
bestMove.setRow(initialMove.getRow());
bestMove.setCol(initialMove.getCol());
}
else{
Moves moves = generateMoves(row, col);
_depth += 1;
int bestScore = -1000;
for (auto move : moves){
applyMoveNega(move);
proposedNextMove = negaMax(_depth, move);
int tScore = -proposedNextMove.getScore();
proposedNextMove.setScore(tScore);
if (proposedNextMove.getScore() > bestScore){
bestScore = proposedNextMove.getScore();
bestMove.setScore(bestScore);
bestMove.setRow(move.getRow());
bestMove.setCol(move.getCol());
}
deleteMoveNega(move);
}
}
return bestMove;
}
是的,我只需要承认这段算法的写法非常糟糕,只是为了匆匆解决我脑子里的逻辑而只是为了以后容易出错。随着我们的进步,我们都应该努力防止这种情况发生。但是有时候我们仍然需要首先获得逻辑:)
我会发布清理只是为了让它起作用,但不是所有的清理工作都是为了让它变得完美。很高兴接受评论。
bool Board :: isWon(const int&amp; currentPlayer){
if (defaultBoard[0][0] == defaultBoard[0][1] && defaultBoard[0][1] == defaultBoard[0][2] && defaultBoard[0][0] == currentPlayer ||
defaultBoard[1][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[1][2] && defaultBoard[1][0] == currentPlayer ||
defaultBoard[2][0] == defaultBoard[2][1] && defaultBoard[2][1] == defaultBoard[2][2] && defaultBoard[2][0] == currentPlayer ||
defaultBoard[0][0] == defaultBoard[1][0] && defaultBoard[1][0] == defaultBoard[2][0] && defaultBoard[0][0] == currentPlayer ||
defaultBoard[0][1] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][1] && defaultBoard[0][1] == currentPlayer ||
defaultBoard[0][2] == defaultBoard[1][2] && defaultBoard[1][2] == defaultBoard[2][2] && defaultBoard[0][2] == currentPlayer ||
defaultBoard[0][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[2][2] && defaultBoard[0][0] == currentPlayer ||
defaultBoard[2][0] == defaultBoard[1][1] && defaultBoard[1][1] == defaultBoard[0][2] && defaultBoard[2][0] == currentPlayer){
return true;
}
return false;
}
现在我意识到我不需要检查两个球员;这是错的,我只会检查当前的球员;只有一个if语句,代码更清晰。 结果完全没必要。删除它们。我只是因为事情的复杂而让自己感到困惑。
在 isWon()更改后,我们也会相应地更改 evaluateGameStateNega()的实现:
int Board::evaluateGameStateNega(const int & depth){
if (isWon(m_player))
return 10 - depth;
if (isWon(getOpponent(m_player)))
return depth - 10;
else
return 0;
}
以上更改足以使其与未触及的所有其他部分一起使用。因此,这是为了增加价值。
Moves Board::generateMoves(const int &rowIndex, const int &colIndex){
Moves Moves;
if (defaultBoard){
for (int i = 0; i < WIDTH; i++)
{
for (int j = 0; j < LENGTH; j++)
{
if (defaultBoard[i][j] == 0)
{
Move move(i, j);
Moves.push_back(move);
}
}
}
}
return Moves;
}
显然我写了多余的代码。我们不需要检查细胞是否被占用;我们只需要为所有空单元格生成移动!