2

I'm trying to build A.I. for Tic-tac-toe using minimax algorithm but it returns weird moves - sometimes it just returns what I think is first available move (top left, top mid, top right...), but other times it just returns what seems like "random" move. But as far as I can tell the moves aren't random and the A.I. plays always the same under the same conditions.

The game works when there are 2 human players. The whole game is big so I created AITest class which should show the important part of behavior. It lets two A.I. players play and prints the board. Please note that this is separate class and isn't written according to best practices and is heavily simplified (doesn't check for win...). But it should be obvious that the A.I. is stupid.

Thank you for any advice.

package tic_tac_toe;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AITest {

    public enum Tile {
        EMPTY, O, X;
    }

    // [row][column]
    private static Tile[][] board;
    private static int moveCount = 0;

    private Tile mySeed;
    private Tile opponentSeed;

    public static void main(String[] args) {
        board = new Tile[3][3];
        for (int y = 0; y < board.length; y++) {
            for (int x = 0; x < board.length; x++) {
                board[y][x] = Tile.EMPTY;
            }
        }

        AITest ai = new AITest(Tile.X);
        AITest opponent = new AITest(Tile.O);
        // simulate some moves
        for (int i = 0; i < 15; i++) {
            checkReset();
            int[] move = ai.getNextMove(board);
            board[move[0]][move[1]] = Tile.X;
            printBoard();

            checkReset();
            int[] opponentMove = opponent.getNextMove(board);
            board[opponentMove[0]][opponentMove[1]] = Tile.O;
            printBoard();
        }
    }

    private static void checkReset() {
        moveCount++;
        if (moveCount == 9) {
            for (int y = 0; y < board.length; y++) {
                for (int x = 0; x < board.length; x++) {
                    board[y][x] = Tile.EMPTY;
                }
            }
            moveCount = 0;
        }

    }

    private static void printBoard() {
        StringBuilder sb = new StringBuilder();
        sb.append("-----------------------\n");
        Map<Tile, String> map = new HashMap<>();
        map.put(Tile.EMPTY, "e");
        map.put(Tile.O, "O");
        map.put(Tile.X, "X");
        for (Tile[] row : board) {
            for (Tile t : row) {

                sb.append(map.get(t) + "|");
            }
            sb.append("\n");
        }
        System.out.println(sb.toString());
    }

    public AITest(Tile seed) {
        mySeed = seed;
        opponentSeed = (mySeed == Tile.X) ? Tile.O : Tile.X;
    }

    public int[] getNextMove(Tile[][] board) {
        int[] result = minimax(board, mySeed);
        // row, column
        return new int[] { result[1], result[2] };
    }

    private int[] minimax(Tile[][] board, Tile player) {
        // mySeed is maximizing, opponentSeed is minimizing
        int bestScore = (player == mySeed) ? Integer.MIN_VALUE
                : Integer.MAX_VALUE;
        int currentScore;
        int bestRow = -1;
        int bestCol = -1;

        List<Point> possibleMoves = getPossibleMoves(Arrays.copyOf(board,
                board.length));
        if (possibleMoves.isEmpty()) {
            bestScore = getScore(board, player);
        } else {
            for (Point move : possibleMoves) {
                // try the move
                board[move.y][move.x] = player;
                if (player == mySeed) {
                    currentScore = minimax(board, opponentSeed)[0];
                    if (currentScore > bestScore) {
                        bestScore = currentScore;
                        bestRow = move.y;
                        bestCol = move.x;
                    }
                } else {
                    currentScore = minimax(board, mySeed)[0];
                    if (currentScore < bestScore) {
                        bestScore = currentScore;
                        bestRow = move.y;
                        bestCol = move.x;
                    }
                }
                // undo the move
                board[move.y][move.x] = Tile.EMPTY;
            }
        }
        return new int[] { bestScore, bestRow, bestCol };
    }

    private List<Point> getPossibleMoves(Tile[][] board) {
        List<Point> possibleMoves = new ArrayList<>();
        for (int y = 0; y < board.length; y++) {
            for (int x = 0; x < board.length; x++) {
                if (board[y][x] == Tile.EMPTY) {
                    possibleMoves.add(new Point(x, y));
                }
            }
        }
        return possibleMoves;
    }

    private int getScore(Tile[][] board, Tile player) {
        if (isWinner(board, mySeed)) {
            return 1;
        }
        if (isWinner(board, opponentSeed)) {
            return -1;
        }
        return 0;
    }

    private boolean isWinner(Tile[][] board, Tile player) {
        // rows
        for (int i = 0; i < board.length; i++) {
            if (checkLine(player, board[i])) {
                return true;
            }
        }
        // columns
        for (int i = 0; i < board.length; i++) {
            if (checkLine(player, board[0][i], board[1][i], board[2][i])) {
                return true;
            }
        }
        // diagonals
        return checkLine(player, board[0][0], board[1][1], board[2][2])
                || checkLine(player, board[0][2], board[1][1], board[2][0]);
    }

    private boolean checkLine(Tile player, Tile... line) {
        for (Tile tile : line) {
            if (tile != player) {
                return false;
            }
        }
        return true;
    }
}
David Mašek
  • 913
  • 8
  • 23
  • Please can you be more specific than "returns weird moves"? – Andy Turner Jun 28 '15 at 12:31
  • @AndyTurner Edited - see first paragraph. – David Mašek Jun 28 '15 at 12:43
  • Can you give some examples of the "random" moves? What do you expect, what does it do instead? – Andy Turner Jun 28 '15 at 12:44
  • @AndyTurner I start in (mid, mid) A.I. goes in (top, left) - so far probably ok. Then I move (mid, left) and A.I. moves (top, right) while it should block - i. e. go (mid, right). – David Mašek Jun 28 '15 at 12:50
  • have you ever been able to win against the A.I. you implemented? – Gianmarco Jun 28 '15 at 12:57
  • @Gianmarco I can win most games and I don't lose unless I want. – David Mašek Jun 28 '15 at 13:00
  • Is it not possible that `possibleMoves` is non-empty, but somebody has already won? – Andy Turner Jun 28 '15 at 13:06
  • @AndyTurner It's possible. [tucuxi's answer](http://stackoverflow.com/a/31100172/3936732) has more about this. – David Mašek Jun 28 '15 at 13:15
  • if the A.I. is working you shouldn't be able to win. You always lose or at most draw. I suggest you to work with trees and to compute the best move looking few steps ahead and giving it a score... I did that for one of my examination during my degree course. If I find it I will tell you how – Gianmarco Jun 28 '15 at 14:03
  • Adding this check saved the day: `int score = getScore(board); if (score != NOBODY_WON) { return new int[] { score, bestRow, bestCol }; }` Thanks [tucuxi](http://stackoverflow.com/users/15472/tucuxi) – David Mašek Jun 28 '15 at 16:40
  • I should probably note that altrought the check (see above) helped a lot the A.I. still isn't perfect so evaluation really needs to be improved. – David Mašek Jun 28 '15 at 17:41

1 Answers1

3

MinMax is very dependent on having a good evaluation function (the function that "scores" a board, to tell you how good it is). Your evaluation function should be as follows:

  1. If the game is won, lots of points for the winner. Nothing matters after the game is won.
  2. Otherwise, give partial credit for occupying tiles that can be used for more winning moves. For example, the center tile can be used in 4 win-positions, the corners in 3, and the sides in 2. Therefore, center-tile is better if you have the choice and the opponent can't win-in-one.

You current evaluation function is broken, because it only implements #1, and only if the board is full. To see a great improvement, check for either sides' victory before checking for available moves in minimax(), and if any side has won, return with the value of getScore().

tucuxi
  • 17,561
  • 2
  • 43
  • 74
  • I once forgot to add a similar "end of the world" check to a chess AI. However, the king was still worth a lot. The result was a chess where the AI thought that "getting your king captured is ok, as long as you can nab theirs too" :-P – tucuxi Jun 28 '15 at 17:42
  • I know it's been a while, but your answer has helped me understand more about the minimax algorithm and how important the evaluation part actually is, so thank you :D – Valdrinium Mar 29 '17 at 16:01