1

NOTE: Please comment if you think this post looks to you not having adequate details e.g. codes, results and other stuff; I will edit the post accordingly.

NOTE 2: I wrote this program by hand myself.

I have a negamax implementation the result of which looked very wrong to me i have tried many ways of debugging it but I still can't seem to get the crux of it.

First of all this is a negamax implementation for Tic Tac Toe, which has board of 3X3.

following codes are the full set in order to replicate the error I had wit this algorithm. please comment below if I missed anything.

An Example could be doing this main:

int main {

Board board;
board.startGameNage(0,0);


}

I would expect the game ended in a draw because this is computer (perfect player) vs computer (perfect player), however,Using the following set of functions I got a game ending like below:

current max move is: 0,0, current score is: -inf current max move is: 0,2, current score is: 3 current max move is: 0,1, current score is: -3 current max move is: 1,1, current score is: 3 current max move is: 2,0, current score is: -3 current max move is: 1,2, current score is: 3 current max move is: 2,1, current score is: -3 current max move is: 1,0, current score is: 3 current max move is: 1,0, current score is: -3

X X O
X O O
X X ---

the " - " there means no move is made in that cell, which looked obviously wrong.

I implemented my minimax first and this negamax was in a way evolving based on my minimax implementation, which might be the reason that I can't see my error.

I got that minimax makes moves from 2 player's perspective and evaluate scores the same as well, whereas negamax make moves from 2 player's perspective but evaluate score only from current player's perspective.

I guess this is the bit that confused me. I can't seem to see how my implementation went wrong here.

I start my game by the following function in 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
}

here is the 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;
};

some key elements listed here:

print:

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;

}

evaluate the score:

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

here's the 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;

    };

This is the actual NegaMax Function:

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;

}

I evaluate the game state using following Function:

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;
}
stucash
  • 1,078
  • 1
  • 12
  • 23
  • maybe `_depth == 8` makes it stop after 8 entries – M.M May 01 '16 at 21:17
  • If you want someone to debug your code you will need to post a [MCVE](http://stackoverflow.com/help/mcve). But you should try debugging first. – M.M May 01 '16 at 21:17
  • I have debugged more than 20 times I am here really just because I couldn't see what was wrong. I am very new on StackOverflow, how can I post a MCVE? – stucash May 01 '16 at 21:20
  • Sorry forgot to mention this is a negamax for tic tac toe. index starts from 0 therefore depth could only be as far as 8. please correct me if I am wrong though. – stucash May 01 '16 at 21:21
  • I have checked the MCVE definition from your link. thanks, M.M. However I am not sure what do I need to post to make MCVE. the barebones are already here and I guess applyMove and generateMove methods belongs to the part that won't matter that much. but I am happy to get them on here as well. – stucash May 01 '16 at 21:25
  • post something that someone else can copy and paste into their compiler and get the same output as you – M.M May 01 '16 at 21:56
  • @M.M ok I will post full set on here thanks a lot ! – stucash May 01 '16 at 22:05
  • @stucash *I have debugged more than 20 times* -- You did no debugging in the sense that we're asking you to do. Did you write the program? If so, did you single-step through the program using a known data set, to see which line, function, etc. causes your program to go awry and against the plan(s) you had laid out? You didn't post any of these results -- you just dumped a lot of code, asking **us** to debug it for you. – PaulMcKenzie May 01 '16 at 22:09
  • @PaulMcKenzie Hi Paul, yes I wrote this program, I and put break points in Visual Studio going through the recursion and other bit that I found matters as well. To be honest I do not even know what data set I could give you. I started with coordinate (0,0) and stepped in, out and through. the result I got was the ending I posted at the very beginning, let me know if you want anything more. By the way, I didn't mean to just post it here without any work and just lay back. I might miss something explanation here but I am happy to justify anything you are in doubt. – stucash May 01 '16 at 22:29
  • @M.M Hi I just added the full set here please let me know if I missed anything. Thanks a lot! – stucash May 01 '16 at 22:35
  • @stucash Yes, but your question relies on us to step through the program and we really don't have such a complete program where we can just copy and paste it, compile it, and run it. There is no way someone (unless that person has supernatural powers) could take your code, eyeball it, and say "your error is here on line x". – PaulMcKenzie May 01 '16 at 22:35
  • @PaulMcKenzie I have just edited the post again to include some more results. please let me know. – stucash May 01 '16 at 22:36
  • We still don't know what `Moves` is. – PaulMcKenzie May 01 '16 at 22:37
  • @PaulMcKenzie yes Paul agreed. I have added the full set of codes here and people whoever wants to should be able to do copy and paste into their IDE and run it, getting the same result. – stucash May 01 '16 at 22:37
  • @PaulMcKenzie Hi Paul, Thanks, I have just added the Extra bit for Moves. it is just a typedef of vector. I really wanted to know the reason why this didn't work, please let me know I missed anything else. Thanks a lot, for pointing out errors in my post. And Paul, trust me, I have gone line by line for 20 times. :) – stucash May 01 '16 at 22:41
  • You didn't post the `Board::print` function. – PaulMcKenzie May 01 '16 at 22:52
  • Thanks @PaulMcKenzie just added it. – stucash May 01 '16 at 22:57
  • Using your main program, [this is what is the result](http://ideone.com/f0JPHH). I used `vector` instead of the array, just to ensure that there are no boundary access issues. However, the answer given there is different than what you posted. – PaulMcKenzie May 01 '16 at 23:13
  • @PaulMcKenzie thanks Paul that is at least one step closer to the problem it seems. It indeed is a better result i just don't see how this could happen I will run the code again to see. with the copy your provided(although it's my code..) but the result you get still has one issue there, which is my issue: this is computer vs computer it should result in a draw. My minimax Did so is I would expect it to work. I could figure out why this didn't work... – stucash May 01 '16 at 23:20
  • @PaulMcKenzie this strikes me as something to do with vector vs array ? Really just can't see why.. Paul this is a big chunk of codes I really want to thank you for trying. I really mean it. Thanks – stucash May 01 '16 at 23:28
  • The vector (using `at()`) is used to ensure that there were no out-of-bounds accesses. An array doesn't give you this handy debugging feature -- with an array, if you have an index out-of-bounds, the code may run without you knowing there is an error. However, there were no faulty indices being used, at least with the data posted. If there were, the program would have halted with an `out_of_range` exception. – PaulMcKenzie May 01 '16 at 23:32
  • @PaulMcKenzie right gotcha, I can now see why I got my result. I will start from here again and run through your version, but any chance you see what the reason is that isn't a draw ( your result shows x won) I will edit my post as well – stucash May 01 '16 at 23:43
  • That first line of output is suspicious. Are you accessing a variable that isn't initialized? You are. You should initialize **all** of your `Board` and `Move` members, regardless of the constructor being called. You have `Move` with two constructors that do not initialize all members. Having uninitialized variables leads to erratic running code. – PaulMcKenzie May 02 '16 at 00:04
  • @PaulMcKenzie yes I was actually accessing the score field on Move which wasn't initialized. I did not know what was an appropriate score to give to the first move. It's hard for me to see if this has anything to do with the result though; I set it later once the negamax function was run through. I guess I didn't understand the algorithm properly. – stucash May 02 '16 at 00:08
  • @PaulMcKenzie ok thanks Paul I will make note of that and improve Move and Board. – stucash May 02 '16 at 00:12
  • @stucash The problem with uninitialized variables is that you're counting on "business logic" to set them. You should not rely on this, as many times, the logic may skip over (for whatever reason) the code to assign a value to these variables. Also, many times those uninitialized variables have "good" uninitialized values, thus you never knew that they were uninitialized. The bottom line is this -- when an object is constructed, all members should have a known, initial state. – PaulMcKenzie May 02 '16 at 00:16
  • @PaulMcKenzie thanks very much Paul! had some good lessons learned :) I will reimplement my algorithm and run through and post the new version and new result again here. I will see how far I can go. – stucash May 02 '16 at 00:29

1 Answers1

0

thanks @PaulMckenzie for pointing out some of my code issues.

But they were nothing to do with errors I made on core logic of Negamax.

One by one, i will list them all out here and hope it could also help others who want to learn Negamax as well. if I miss anything just comment, I will edit afterwards.

*

DO remember to initialise all your new fields to a value, don't leave them for the logic to decide what is the initial value. this helps in debugging and it is just a good code practice. Thanks to @PaulMcKenzie

*

  • Issue 1: deleteMoveNega() && applyMoveNega()

all they do is deleting a proposed move/applying a proposed move; they don't give back/pass on the turn to current player. Because the move is proposed as the best move for the opponent of current player, once we finish calculating the score for this proposed move (A) and we want to test the next proposed move(B), we will need to delete A and give the turn back to current player. (or, to some people it's better understood as previous player.) the same applies when we apply a proposed move.

it should therefore be:

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

this is the most important error I made because with the old codes I would just propose move and calculate scores as whoever started the game all the way through; because I manually set the player to the opponent in startGameNage(), I then played the game as opponent proposing moves and calculating scores only as the opponent all the way through (whereas I should really be switching context and be in two players' positions). And this happened in each iteration of the negamax function. this didn't enforce the concept of thinking as current player because when I am supposed to play as current player, I however played as opponent.

This is fundamentally wrong in negamax.

Once we fix this, we then don't need to manually set the turn in startGameNage() therefore:

player = -player;

should be deleted and:

int player = 1;

will be changed to:

m_player = 1;
  • Issue 2: negaMax()

with deleteMove() and applyMove() sorted out, we can now have a look at our negamax engine.

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

First, I don't need the current player parameter. I have private m_player which I can make use of.

Second, and more importantly, with old deleteMove() and applyMove() and setting turn manually in startGameNega(), this negation for player here (-_curplayer) is just so wrong.

for example, we apply/make a move for -_curplayer; the proposed move happens next should be for the opponent, which in our case, should be _curplayer. i am still passing -_curplayer, this will then generate moves for the wrong player right from the very beginning.

a new core negamax would be like:

    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;
}
  • Issue 3: clean up a bit

Yes I just have to admit this piece of algorithm was horribly written only to rush out the logic in my head and only to be prone to errors later. As we progress, we should all be diligent enough to prevent this. However sometimes we still need to get the logic going first :)

I will post clean-ups just to make it work, but not all the clean-ups which are to make it perfect. happy to accept comment.

  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;
    

    }

now I realised I didn't have to check for both players; that was wrong, I shall only be checking for current player; and the code is cleaner to read with only one if statement. the result was completely unnecessary. delete them. I was just confusing myself by complicating matters.

  1. evaluateGameStateNega()

following the change in isWon() we would accordingly change the implementation of evaluateGameStateNega() as well:

    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()

Above changes suffice to make it work with all other parts being untouched. this one therefore, is to add value.

   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;

}

obviously I wrote redundant codes. we don't need to check if the cell was occupied or not; we just need to generate moves for all empty cells!

stucash
  • 1,078
  • 1
  • 12
  • 23