Я попытался закодировать минимаксный алгоритм для крестики-нолики, приведенный в книге Рассела Норвига об искусственном интеллекте. У него было все, кроме того, что способ вернуть bestMove пользователю. Я стараюсь вернуть bestMove, но не могу решить, когда выбрать bestMove. Помогите кому-нибудь?
moveT MiniMax(stateT state)
{
moveT bestMove;
max_move(state,bestMove);
return bestMove;
}
int max_move(stateT state,int & bestMove)
{
int v = -10000;
if(GameIsOver(state))
{
return EvaluateStaticPosition(state);
}
vector<moveT> moveList;
GenerateMoveList(state, moveList);
int nMoves = moveList.size();
for(int i = 0 ; i < nMoves ; i++)
{
moveT move = moveList[i];
MakeMove(state, move);
int curValue = min_move(state,bestMove);
if(curValue > v)
{
v = curValue;
bestMove = move;
}
RetractMove(state, move);
}
return v;
}
int min_move(stateT state, int &bestMove)
{
int v = 10000;
if(GameIsOver(state))
{
return EvaluateStaticPosition(state);
}
vector<moveT> moveList;
GenerateMoveList(state, moveList);
int nMoves = moveList.size();
for(int i = 0 ; i < nMoves; i++)
{
moveT move = moveList[i];
MakeMove(state, move);
int curValue = max_move(state,depth+1,bestMove);
if(curValue < v)
{
curValue = v;
}
RetractMove(state, move);
}
return v;
}
P.S .: Есть другой псевдокод для определения минимального значения. Тем не менее, они ориентированы только на крестики-нолики, я пытаюсь распространить его на другие игры. Благодарю.
Обновление: весь код можно найти здесь: http://ideone.com/XPswCl
В простейшей версии минимакса первый игрок хочет максимизировать свой счет, а второй игрок хочет минимизировать счет первого игрока.
Поскольку как первый, так и второй игрок заботятся только о счете первого игрока, EvaluateStaticPosition
должен вернуть значение, указывающее, насколько хорошим является состояние доски для первого игрока. Чья это очередь не имеет значения.
int EvaluateStaticPosition(stateT state)
{
if(CheckForWin(state, FIRST_PLAYER))
{
return WINNING_POSITION;
}
if(CheckForWin(state, Opponent(FIRST_PLAYER)))
{
return LOSING_POSITION;
}
return NEUTRAL_POSITION;
}
Теперь, когда вы хотите ход, который лучше всего подходит для первого игрока, вызывайте MaxMove. Если вы хотите ход, который лучше для второго игрока, позвоните в MinMove.
moveT MiniMax(stateT state)
{
moveT bestMove;
int i = 0;
if (state.whoseTurn == FIRST_PLAYER){
i = MaxMove(state, bestMove);
}
else{
i = MinMove(state,bestMove);
}
cout<<"i is "<<i<<endl;
return bestMove;
}
Наконец, у вас есть некоторые проблемы внутри MinMove
а также MaxMove
, когда вы назначаете curRating
ни в одном из них вы не должны проходить bestMove
в качестве второго аргумента MaxMove
или же MinMove
, Затем он положит противника лучший шаг в bestMove
, который не имеет смысла. Вместо этого объявите opponentsBestMove
возразить и передать это в качестве второго аргумента. (На самом деле вы не будете использовать объект или даже смотреть на его значение, но это нормально). С этим изменением вы никогда ничего не назначаете bestMove
в MinMove
так что вы должны сделать это внутри if(curRating < v)
блок.
int MaxMove(stateT state, moveT &bestMove)
{
if(GameIsOver(state))
{
return EvaluateStaticPosition(state);
}
vector<moveT> moveList;
GenerateMoveList(state, moveList);
int nMoves = moveList.size();
int v = -1000;
for(int i = 0 ;i<nMoves; i++)
{
moveT move = moveList[i];
MakeMove(state, move);
moveT opponentsBestMove;
int curRating = MinMove(state, opponentsBestMove);
if (curRating > v)
{
v = curRating;
bestMove = move;
}
RetractMove(state, move);
}
return v;
}
int MinMove(stateT state, moveT &bestMove)
{
if(GameIsOver(state))
{
return EvaluateStaticPosition(state);
}
vector<moveT>moveList;
GenerateMoveList(state, moveList);
int nMoves = moveList.size();
int v = 1000;
for(int i = 0 ; i<nMoves; i++)
{
moveT move = moveList[i];
MakeMove(state , move);
moveT opponentsBestMove;
int curRating = MaxMove(state,opponentsBestMove);
if(curRating < v)
{
v = curRating;
bestMove = move;
}
RetractMove(state, move);
}
return v;
}
На данный момент у вас должен быть непобедимый AI!
The final position looks like this:
O | O | X
---+---+---
X | X | O
---+---+---
O | X | X
Cat's game.
Альтернативный метод использует тот факт, что крестики-нолики — игра с нулевой суммой. Другими словами, в конце игры сумма очков игроков будет равна нулю. Для игры с двумя игроками это означает, что оценка одного игрока всегда будет отрицательной оценкой другого игрока. Это удобно для нас, поскольку минимизация счета другого игрока идентична максимизации собственного счета. Таким образом, вместо одного игрока, максимизирующего свой счет, и одного игрока, минимизирующего счет другого игрока, мы можем просто заставить обоих игроков попытаться максимизировать свой собственный счет.
+ Изменить EvaluateStaticPosition
вернуться к своей первоначальной форме, так что он дает оценку, основанную на том, насколько хорошо состояние доски для текущего игрока.
int EvaluateStaticPosition(stateT state)
{
if(CheckForWin(state, state.whoseTurn))
{
return WINNING_POSITION;
}
if(CheckForWin(state, Opponent(state.whoseTurn)))
{
return LOSING_POSITION;
}
return NEUTRAL_POSITION;
}
удалять MinMove
, так как мы заботимся только о максимизации.
перезапись MaxMove
так что он выбирает ход, который дает противнику наихудший возможный счет. Счет за лучший ход является отрицательным результатом худшего результата другого игрока.
int MaxMove(stateT state, moveT &bestMove)
{
if(GameIsOver(state))
{
return EvaluateStaticPosition(state);
}
vector<moveT> moveList;
GenerateMoveList(state, moveList);
int nMoves = moveList.size();
int v = -1000;
for(int i = 0 ;i<nMoves; i++)
{
moveT move = moveList[i];
MakeMove(state, move);
moveT opponentsBestMove;
int curRating = -MaxMove(state, opponentsBestMove);
if (curRating > v)
{
v = curRating;
bestMove = move;
}
RetractMove(state, move);
}
return v;
}
поскольку MaxMove
используется для обоих игроков, нам больше не нужно различать игроков в MiniMax
функция.
moveT MiniMax(stateT state)
{
moveT bestMove;
int i = 0;
i = MaxMove(state, bestMove);
cout<<"i is "<<i<<endl;
return bestMove;
}
Ну похоже MiniMax
правильно выберет его для вас, просто назовите его с начальным состоянием и глубиной. (Если первый игрок в соответствии с состоянием не является вторым игроком, то вы должны вызвать min_move в MiniMax.)
РЕДАКТИРОВАТЬ:
да, я что-то упустил, bestMove в настоящее время не имеет особого смысла. В программе max_move вы меняете цикл следующим образом:
for(int i = 0 ; i < nMoves ; i++)
{
moveT move = moveList[i];
MakeMove(state, move);
int new_value = min_move(state, depth+1);
if(new_value > v)
{
v=new_value;
}
RetractMove(state, move);
}
После этого вы можете думать о себе, что означает bestMove? Моя идея заключается в том, что вы заинтересованы в поиске одной из «наилучших возможных» серий движений для крестики-нолики. Для этого вам нужен вектор или даже лучше стек. Но это также означает наличие std::stack<int>* best_moves
как последний параметр.
Для реализации стека в min_move вы возвращаете следующие ходы, и если их значение является лучшим, вы будете выдвигать move
на вершине best_moves
стек. Конечно, в конце игры вы просто возвращаете пустой стек. Требуется ООП подход, чтобы осуществить это должным образом, я сделаю это, когда у меня будет время.
Если все, что вам нужно, это просто лучший следующий ход тогда я предлагаю вам изменить типы возвращаемых значений min_move и max_moe на такую структуру, как эта:
struct Value_move{
int value;
moveT best_move;
};
Тогда новая реализация max_move выглядит следующим образом:
const int MOVE_INVALID = -12345;
const int MOVE_NOTHING = -12346;
Value_move max_move(stateT state, int depth)
{
Value_move best;
best.value = -10000; best.best_move = MOVE_INVALID;
if(GameIsOver(state))
{
best.value = EvaluateStaticPosition(state);
best.best_move = MOVE_NOTHING;
return best;
}
vector<moveT> moveList;
GenerateMoveList(state, moveList);
int nMoves = moveList.size();
for(int i = 0 ; i < nMoves ; i++)
{
moveT move = moveList[i];
MakeMove(state, move);
Value_move curr = min_move(state, depth+1);
if(curr.value > best.value)
{
best.value = curr.value;
best.best_move = move;
}
RetractMove(state, move);
}
return v;
}
Вам нужно только выбрать поле best_move в возвращенной структуре в функции MiniMax.
Замечание:
Вы должны признать, что это во многом напоминает не программу на С ++, а скорее программу на с. В противном случае все функции в CapitalCamelCase должны быть методами класса, вы должны передавать состояния с помощью (const) ref вместо значения — но весь этот код имеет смысл, только если состояние действительно является указателем за typedef.
Ваш код находит правильное значение, но затем перезаписывает его, передавая ту же ссылку вниз.
int curValue = min_move(state,bestMove);
должен стать
moveT nextMove; // No need to actually do anything with this value
int curValue = min_move(state,nextMove);
Вам также необходимо внести такие же изменения в функцию min_move.
NB: в min_move
ваш код звонков max_move
с большим количеством аргументов, чем вы определили для функции.