1

注: この投稿にコード、結果、その他の情報が不足していると思われる場合は、コメントしてください。それに応じて投稿を編集します。

注 2: このプログラムは自分で手で書きました。

私はnegamaxの実装を持っていますが、その結果は私には非常に間違っているように見えました。デバッグする多くの方法を試しましたが、まだ核心をつかむことができないようです.

まず、これは 3X3 のボードを持つ Tic Tac Toe のネガマックス実装です。

次のコードは、このアルゴリズムで発生したエラーを再現するための完全なセットです。何か見逃した場合は、以下にコメントしてください。

例では、このメインを実行できます。

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

XXO
XOO
XX ---

「 - 」は、そのセルで移動が行われないことを意味しますが、これは明らかに間違っているように見えます。

最初にミニマックスを実装しましたが、このネガマックスはミニマックスの実装に基づいて進化していたため、エラーが表示されない可能性があります。

ミニマックスは 2 人のプレイヤーの視点から動き、スコアも同様に評価するのに対し、ネガマックスは 2 人のプレイヤーの視点から動きますが、現在のプレイヤーの視点からのみスコアを評価します。

これが私を混乱させたビットだと思います。ここで私の実装がどのようにうまくいかなかったのかわかりません。

メインの次の関数でゲームを開始します。

// 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;
}
}

生成する移動:

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;
}

削除移動:

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;
}
4

1 に答える 1

0

私のコードの問題のいくつかを指摘してくれた@PaulMckenzieに感謝します。

しかし、それは私がネガマックスのコア ロジックで犯したエラーとは関係がありませんでした。

ネガマックスを学びたい他の人にも役立つことを願っています. コメントだけで何か見逃した場合は、後で編集します。

*

すべての新しいフィールドを値に初期化することを忘れないでください。ロジックが初期値を決定するためにそれらを放置しないでください。これはデバッグに役立ち、コードの実践に役立ちます。@PaulMcKenzieに感謝

*

  • 問題 1: deleteMoveNega() && applyMoveNega()

彼らがすることは、提案された動きを削除する/提案された動きを適用することだけです。彼らは現在のプレイヤーにターンでギブバック/パスしません。この手は現在のプレイヤーの対戦相手にとって最良の手として提案されているため、この提案された手 (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()でプレーヤーを手動で対戦相手に設定したため、対戦相手としてゲームをプレイし、動きを提案し、対戦相手としてのみスコアを計算しました (実際にはコンテキストを切り替えて、2 人のプレーヤーの位置にいる必要があります)。 . これは negamax 関数の各反復で発生しました。これは、現在のプレーヤーとしてプレーすることになっているときに、対戦相手としてプレーしたため、現在のプレーヤーとして考えるという概念を強制しませんでした。

これは negamax では根本的に間違っています。

これを修正すると、 startGameNage()でターンを手動で設定する必要がなくなります。

player = -player;

削除する必要があります。

int player = 1;

次のように変更されます。

m_player = 1;
  • 問題 2: negaMax()

deleteMove()applyMove()が整理されたので、negamax エンジンを確認できます。

applyMoveNega(move, -_curplayer);
proposedNextMove = negaMax(_depth, -_curplayer, move);

まず、現在のプレーヤーのパラメーターは必要ありません。私は利用できるプライベートm_playerを持ってい ます。

2 つ目は、さらに重要なことですが、古いdeleteMove()applyMove()を使用し、startGameNega()でターンを手動で設定すると、ここでのプレーヤーの否定 (-_curplayer) はまったく間違っています。

たとえば、-_curplayerを適用/移動します。次に発生する提案された動きは、対戦相手のためのものである必要があり、この場合は_curplayerである必要があります。私はまだ-_curplayerを渡しています。これにより、最初から間違ったプレーヤーの動きが生成されます。

新しいコア ネガマックスは次のようになります。

    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;
}
  • 問題 3: 少し整理する

はい、このアルゴリズムの一部は、頭の中でロジックを急いで実行し、後でエラーが発生しやすくするためだけに恐ろしく書かれたことを認めなければなりません。私たちが進歩するにつれて、私たちは皆、これを防ぐために十分に勤勉でなければなりません. ただし、最初にロジックを実行する必要がある場合もあります:)

動作させるためだけにクリーンアップを投稿しますが、完璧にするためのすべてのクリーンアップを投稿するわけではありません. コメントを喜んで受け入れます。

  1. isWon()

    bool Board::isWon(const int & 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 ステートメントを 1 つだけ使用すると、コードが読みやすくなります。結果は完全に不必要でした。それらを削除します。私は物事を複雑にすることで自分自身を混乱させていました。

  1. evaluateGameStateNega()

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;
}
  1. 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 (defaultBoard[i][j] == 0)
            {
                Move move(i, j);
                Moves.push_back(move);
            }

        }
    }

}

return Moves;

}

明らかに冗長なコードを書きました。セルが占有されているかどうかを確認する必要はありません。すべての空のセルに対して移動を生成する必要があるだけです!

于 2016-05-14T00:13:00.440 に答える