3

I'm trying to implement the MinMax algorithm for four in a row (or connect4 or connect four) game.

I think I got the idea of it, it should build a tree of possible boards up to a certain depth, evaluate them and return their score, then we just take the max of those scores.

So, aiChooseCol() checks the score of every possible column by calling MinMax() and returns the column with the max score.

Now I wasn't sure, is this the right way to call MinMax()?

Is it right to check temp = Math.Max(temp, 1000);?

I still haven't made the heuristic function but this should at least recognize a winning column and choose it, but currently it just choose the first free column from the left... I can't figure out what am I doing wrong.

private int AiChooseCol()
{
    int best = -1000;
    int col=0;
    for (int i = 0; i < m_Board.Cols; i++)
    {
        if (m_Board.CheckIfColHasRoom(i))
        {
            m_Board.FillSignInBoardAccordingToCol(i, m_Sign);
            int t = MinMax(5, m_Board, board.GetOtherPlayerSign(m_Sign));
            if (t > best)
            {
                best = t;
                col = i;
            }
            m_Board.RemoveTopCoinFromCol(i);
        }

    }
    return col;
}


private int MinMax(int Depth, board Board, char PlayerSign)
{
    int temp=0;
    if (Depth <= 0)
    {
        // return from heurisitic function
        return temp;
    }
    char otherPlayerSign = board.GetOtherPlayerSign(PlayerSign);

    char checkBoard = Board.CheckBoardForWin();
    if (checkBoard == PlayerSign)
    {
        return 1000;
    }
    else if (checkBoard == otherPlayerSign)
    {
        return -1000;
    }
    else if (!Board.CheckIfBoardIsNotFull())
    {
        return 0;   // tie
    }


    if (PlayerSign == m_Sign)   // maximizing Player is myself
    {
        temp = -1000;
        for (int i = 0; i < Board.Cols; i++)
        {
            if (Board.FillSignInBoardAccordingToCol(i, PlayerSign)) // so we don't open another branch in a full column
            {
                var v = MinMax(Depth - 1, Board, otherPlayerSign);
                temp = Math.Max(temp, v);
                Board.RemoveTopCoinFromCol(i);
            }
        }
    }
    else
    {
        temp = 1000;
        for (int i = 0; i < Board.Cols; i++)
        {
            if (Board.FillSignInBoardAccordingToCol(i, PlayerSign)) // so we don't open another branch in a full column
            {
                var v = MinMax(Depth - 1, Board, otherPlayerSign);
                temp = Math.Min(temp, v);
                Board.RemoveTopCoinFromCol(i);
            }
        }
    }
    return temp;
}

Some notes:

FillSignInBoardAccordingToCol() returns a boolean if it was successful.

The board type has a char[,] array with the actual board and signs of the players.

This code is in the AI Player class.

shinzou
  • 5,850
  • 10
  • 60
  • 124
  • In `AiChooseCol` you are not passing the column `i` into `MinMax`, so how does it know which column you are asking it to evaluate? – juharr Apr 22 '16 at 11:41
  • Oh right maybe I should already place a coin in column `i` before calling `MinMax()`? @juharr – shinzou Apr 22 '16 at 11:46
  • Yeah I know it can be refactored down. It still doesn't work with fixing `AiChooseCol`. @juharr – shinzou Apr 22 '16 at 11:50
  • You should be taking the `Min` value in the `else` branch, not `Max`. And since that's the only difference that's really the part you should put the `if`-`else` around. – juharr Apr 22 '16 at 11:52
  • @juharr still not working, I tried to switch around the `1000` and `-1000` between them as well. – shinzou Apr 22 '16 at 11:57
  • Wait your `Min` and `Max` calls should be comparing the previuos `temp` to the returned value of the recursive call, not to your max and min values. So you need to assign it to a variable `var v = MinMax(...);` then do `temp = Math.Max(temp, v);`. And `temp` should be initialized to either `1000` or `-1000` based on whether it's the maximizing player or not. – juharr Apr 22 '16 at 12:03
  • It should be inialized based on whether you are looking for the Max or Min (or based on which players turn). For Max it shoudl initialize to -1000 and for Min to 1000. – juharr Apr 22 '16 at 12:12
  • Alright, I edited those changed in the question, it still doesn't work... @juharr – shinzou Apr 22 '16 at 12:18
  • @juharr thank you, I found the problem: `if (checkBoard == PlayerSign)` should have been `if (checkBoard == m_sign)` – shinzou Apr 22 '16 at 16:58

1 Answers1

5

So I decided to write my own MinMax Connect 4. I used the depth to determine the value of a win or loss so that a move that gets you closer to winning or blocking a loss will take precedence. I also decide that I will randomly pick the move if more than one has the same heuristic. Finally I stretched out the depth to 6 as that's how many moves are required to find possible win paths from the start.

private static void Main(string[] args)
{
    var board = new Board(8,7);
    var random = new Random();

    while (true)
    {
        Console.WriteLine("Pick a column 1 -8");
        int move;
        if (!int.TryParse(Console.ReadLine(), out move) || move < 1 || move > 8)
        {
            Console.WriteLine("Must enter a number 1-8.");
            continue;
        }

        if (!board.DropCoin(1, move-1))
        {
            Console.WriteLine("That column is full, pick another one");
            continue;
        }

        if (board.Winner == 1)
        {
            Console.WriteLine(board);
            Console.WriteLine("You win!");
            break;
        }

        if (board.IsFull)
        {
            Console.WriteLine(board);
            Console.WriteLine("Tie!");
            break;
        }

        var moves = new List<Tuple<int, int>>();
        for (int i = 0; i < board.Columns; i++)
        {
            if (!board.DropCoin(2, i))
                continue;
            moves.Add(Tuple.Create(i, MinMax(6, board, false)));
            board.RemoveTopCoin(i);
        }

        int maxMoveScore = moves.Max(t => t.Item2);
        var bestMoves = moves.Where(t => t.Item2 == maxMoveScore).ToList();
        board.DropCoin(2, bestMoves[random.Next(0,bestMoves.Count)].Item1);
        Console.WriteLine(board);

        if (board.Winner == 2)
        {
            Console.WriteLine("You lost!");
            break;
        }

        if (board.IsFull)
        {
            Console.WriteLine("Tie!");
            break;
        }
    }

    Console.WriteLine("DONE");
    Console.ReadKey();
}

private static int MinMax(int depth, Board board, bool maximizingPlayer)
{
    if (depth <= 0)
        return 0;

    var winner = board.Winner;
    if (winner == 2)
        return depth;
    if (winner == 1)
        return -depth;
    if (board.IsFull)
        return 0;


    int bestValue = maximizingPlayer ? -1 : 1;
    for (int i = 0; i < board.Columns; i++)
    {
        if (!board.DropCoin(maximizingPlayer ? 2 : 1, i))
            continue;
        int v = MinMax(depth - 1, board, !maximizingPlayer);
        bestValue = maximizingPlayer ? Math.Max(bestValue, v) : Math.Min(bestValue, v);
        board.RemoveTopCoin(i);
    }

    return bestValue;
}

public class Board
{
    private readonly int?[,] _board;

    private int? _winner;

    private bool _changed;

    public Board(int cols, int rows)
    {
        Columns = cols;
        Rows = rows;
        _board = new int?[cols, rows];
    }

    public int Columns { get; }
    public int Rows { get; }

    public bool ColumnFree(int column)
    {
        return !_board[column, 0].HasValue;
    }

    public bool DropCoin(int playerId, int column)
    {
        int row = 0;
        while (row < Rows && !_board[column,row].HasValue)
        {
            row++;
        }

        if (row == 0)
            return false;
        _board[column, row - 1] = playerId;
        _changed = true;
        return true;
    }

    public bool RemoveTopCoin(int column)
    {
        int row = 0;
        while (row < Rows && !_board[column, row].HasValue)
        {
            row++;
        }

        if (row == Rows)
            return false;
        _board[column, row] = null;
        _changed = true;
        return true;
    }

    public int? Winner
    {
        get
        {
            if (!_changed)
                return _winner;

            _changed = false;
            for (int i = 0; i < Columns; i++)
            {
                for (int j = 0; j < Rows; j++)
                {
                    if (!_board[i, j].HasValue)
                        continue;

                    bool horizontal = i + 3 < Columns;
                    bool vertical = j + 3 < Rows;

                    if (!horizontal && !vertical)
                        continue;

                    bool forwardDiagonal = horizontal && vertical;
                    bool backwardDiagonal = vertical && i - 3 >= 0;

                    for (int k = 1; k < 4; k++)
                    {
                        horizontal = horizontal && _board[i, j] == _board[i + k, j];
                        vertical = vertical && _board[i, j] == _board[i, j + k];
                        forwardDiagonal = forwardDiagonal && _board[i, j] == _board[i + k, j + k];
                        backwardDiagonal = backwardDiagonal && _board[i, j] == _board[i - k, j + k];
                        if (!horizontal && !vertical && !forwardDiagonal && !backwardDiagonal)
                            break;
                    }

                    if (horizontal || vertical || forwardDiagonal || backwardDiagonal)
                    {
                        _winner = _board[i, j];
                        return _winner;
                    }
                }
            }

            _winner = null;
            return _winner;
        }
    }

    public bool IsFull
    {
        get
        {
            for (int i = 0; i < Columns; i++)
            {
                if (!_board[i, 0].HasValue)
                    return false;
            }

            return true;
        }
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        for (int j = 0; j < Rows; j++)
        {
            builder.Append('|');
            for (int i = 0; i < Columns; i++)
            {
                builder.Append(_board[i, j].HasValue ? _board[i,j].Value.ToString() : " ").Append('|');
            }
            builder.AppendLine();
        }

        return builder.ToString();
    }
}
juharr
  • 31,741
  • 4
  • 58
  • 93
  • Yours is really fast at depth 6, mine is really slow at that depth even without calling the heuristic function. Depth 5 takes about 1 seconds in the early game, but yours is a lot faster, how come? – shinzou Apr 22 '16 at 21:32
  • You turned the `isFull` to a property, very clever. Doesn't the `winner` method check many coordinates multiple times? (even in the same direction) – shinzou Apr 22 '16 at 21:37
  • The `Winner` loops through each position considering it as a starting point of a connect four going down, to the right, down diagonally to the right, or down diagonally to the left. Then it might look at the up to 12 positions in those directions, so In a way it does look at positions more than once, but that's because one position can be part of up to 16 different possible connect 4 scenarios (on a board that is at least 7x7). – juharr Apr 25 '16 at 11:43
  • It helped me a lot to complete my assignment. Thanks – t4taurus Aug 26 '17 at 18:21