Skip to main content
added 75 characters in body
Source Link
coderodde
  • 32.1k
  • 15
  • 78
  • 205

(The entire project lives here.)

(The entire project lives here.)

Source Link
coderodde
  • 32.1k
  • 15
  • 78
  • 205

Comparing game tree search AI algorithms in Java

I have a program that benchmarks three game tree search algorithms:

  1. Minimax,
  2. Alpha-beta pruning,
  3. Alpha-beta pruning with move ordering.

So here is my code:

net.coderodde.zerosum.ai.impl.MinimaxGameEngine

package net.coderodde.zerosum.ai.impl;

import net.coderodde.zerosum.ai.EvaluatorFunction;
import net.coderodde.zerosum.ai.AbstractGameEngine;
import net.coderodde.zerosum.ai.AbstractState;

/**
 * This class implements the 
 * <a href="https://en.wikipedia.org/wiki/Minimax">Minimax</a> algorithm for 
 * zero-sum two-player games.
 * 
 * @param <S> the game state type.
 * @param <P> the player color type.
 * @author Rodion "rodde" Efremov
 * @version 1.6 (May 26, 2019)
 */
public final class MinimaxGameEngine<S extends AbstractState<S, P>,
                                     P extends Enum<P>> 
        extends AbstractGameEngine<S, P> {

    /**
     * Constructs this minimax game engine.
     * @param evaluatorFunction the evaluator function.
     * @param depth the search depth.
     */
    public MinimaxGameEngine(EvaluatorFunction<S> evaluatorFunction,
                             int depth) {
        super(evaluatorFunction, depth, Integer.MAX_VALUE);
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public S makePly(S state, 
                     P minimizingPlayer,
                     P maximizingPlayer,
                     P initialPlayer) {
        state.setDepth(depth);

        // Do the game tree search:
        return makePlyImplTopmost(state,
                                  minimizingPlayer,
                                  maximizingPlayer,
                                  initialPlayer);
    }

    private S makePlyImplTopmost(S state, 
                                 P minimizingPlayer,
                                 P maximizingPlayer,
                                 P currentPlayer) {
        S bestState = null;

        if (currentPlayer == maximizingPlayer) {
            double tentativeValue = Double.NEGATIVE_INFINITY;

            for (S childState : state.children()) {
                double value = makePlyImpl(childState,
                                           depth - 1,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue < value) {
                    tentativeValue = value;
                    bestState = childState;
                }
            }
        } else {
            // Here, 'initialPlayer == minimizingPlayer'.
            double tentativeValue = Double.POSITIVE_INFINITY;

            for (S childState : state.children()) {
                double value = makePlyImpl(childState,
                                           depth - 1,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue > value) {
                    tentativeValue = value;
                    bestState = childState;
                }
            }
        }

        return bestState;
    }

    /**
     * Performs a single step down the game tree branch.
     * 
     * @param state            the starting state.
     * @param depth            the maximum depth of the game tree.
     * @param minimizingPlayer the minimizing player.
     * @param maximizingPlayer the maximizing player.
     * @param currentPlayer    the current player.
     * 
     * @return the value of the best ply.
     */
    private double makePlyImpl(S state,
                               int depth,
                               P minimizingPlayer,
                               P maximizingPlayer,
                               P currentPlayer) {
        if (state.getDepth() == 0 
                || state.checkVictory() != null
                || state.isTerminal()) {
            return evaluatorFunction.evaluate(state);
        }

        if (currentPlayer == maximizingPlayer) {
            double tentativeValue = Double.NEGATIVE_INFINITY;

            for (S child : state.children()) {
                double value = makePlyImpl(child,
                                           depth - 1,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue < value) {
                    tentativeValue = value;
                }
            }

            return tentativeValue;
        } else {
            // Here, 'initialPlayer == minimizingPlayer'.
            double tentativeValue = Double.POSITIVE_INFINITY;

            for (S child : state.children()) {
                double value = makePlyImpl(child,
                                           depth - 1,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue > value) {
                    tentativeValue = value;
                } 
            }

            return tentativeValue;
        }
    }
}

net.coderodde.zerosum.ai.impl.AlphaBetaPruningGameEngine

package net.coderodde.zerosum.ai.impl;

import net.coderodde.zerosum.ai.EvaluatorFunction;
import net.coderodde.zerosum.ai.AbstractGameEngine;
import net.coderodde.zerosum.ai.AbstractState;

/**
 * This class implements the 
 * <a href="https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning">
 * Alpha-beta pruning</a> algorithm for zero-sum two-player games.
 * 
 * @param <S> the game state type.
 * @param <P> the player color type.
 * @author Rodion "rodde" Efremov
 * @version 1.6 (May 26, 2019)
 * @version 1.61 (Sep 12, 2019)
 * @since 1.6 (May 26, 2019)
 */
public final class AlphaBetaPruningGameEngine<S extends AbstractState<S, P>, 
                                              P extends Enum<P>> 
extends AbstractGameEngine<S, P> {

    /**
     * Constructs this minimax game engine.
     * @param evaluatorFunction the evaluator function.
     * @param depth the search depth.
     */
    public AlphaBetaPruningGameEngine(EvaluatorFunction<S> evaluatorFunction,
                                      int depth) {
        super(evaluatorFunction, depth, Integer.MAX_VALUE);
    }

    /**
     * {@inheritDoc}
     */
    public S makePly(S state,
                     P minimizingPlayer,
                     P maximizingPlayer,
                     P initialPlayer) {
        state.setDepth(depth);

        // Do the game tree search with Alpha-beta pruning:
        return makePlyImplTopmost(state,
                                  depth,
                                  -Double.NEGATIVE_INFINITY,
                                   Double.POSITIVE_INFINITY,
                                  minimizingPlayer,
                                  maximizingPlayer,
                                  initialPlayer);
    }

    /**
     * Pefrorms the topmost search of a game tree.
     * 
     * @param state            the state to start the search from.
     * @param depth            the depth of the tree to search.
     * @param alpha            the alpha cut-off value.
     * @param beta             the beta cut-off value.
     * @param minimizingPlayer the minimizing player color.
     * @param maximizingPlayer the maximizing player color.
     * @param currentPlayer    the current player color.
     * @return 
     */
    private S makePlyImplTopmost(S state,
                                 int depth,
                                 double alpha,
                                 double beta,
                                 P minimizingPlayer,
                                 P maximizingPlayer,
                                 P currentPlayer) {
        S bestState = null;

        if (currentPlayer == maximizingPlayer) {
            double tentativeValue = Double.NEGATIVE_INFINITY;

            for (S childState : state.children()) {
                double value = makePlyImpl(childState,
                                           depth - 1,
                                           alpha,
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue < value) {
                    tentativeValue = value;
                    bestState = childState;
                }

                alpha = Math.max(alpha, tentativeValue);

                if (alpha >= beta) {
                    return bestState;
                }
            }
        } else {
            // Here, 'initialPlayer == minimizingPlayer'.
            double tentativeValue = Double.POSITIVE_INFINITY;

            for (S childState : state.children()) {
                double value = makePlyImpl(childState,
                                           depth - 1,
                                           alpha,
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue > value) {
                    tentativeValue = value;
                    bestState = childState;
                }

                beta = Math.min(beta, tentativeValue);

                if (alpha >= beta) {
                    return bestState;
                }
            }
        }

        return bestState;
    }

    /**
     * Performs a single step down the game tree.
     * 
     * @param state            the starting state.
     * @param depth            the maximum depth of the game tree.
     * @param alpha            the alpha cut-off.
     * @param beta             the beta cut-off.
     * @param minimizingPlayer the minimizing player.
     * @param maximizingPlayer the maximizing player.
     * @param currentPlayer    the current player.
     * 
     * @return the value of the best ply.
     */
    private double makePlyImpl(S state,
                               int depth,
                               double alpha,
                               double beta,
                               P minimizingPlayer,
                               P maximizingPlayer,
                               P currentPlayer) {
        if (state.getDepth() == 0 
                || state.checkVictory() != null
                || state.isTerminal()) {
            return evaluatorFunction.evaluate(state);
        }

        if (currentPlayer == maximizingPlayer) {
            double tentativeValue = Double.NEGATIVE_INFINITY;

            for (S child : state.children()) {
                double value = makePlyImpl(child,
                                           depth - 1,
                                           alpha, 
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue < value) {
                    tentativeValue = value;
                }

                alpha = Math.max(alpha, tentativeValue);

                if (alpha >= beta) {
                    break;
                }
            }

            return tentativeValue;
        } else {
            // Here, 'initialPlayer == minimizingPlayer'.
            double tentativeValue = Double.POSITIVE_INFINITY;

            for (S child : state.children()) {
                double value = makePlyImpl(child,
                                           depth - 1,
                                           alpha,
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue > value) {
                    tentativeValue = value;
                }

                beta = Math.min(beta, tentativeValue);

                if (alpha >= beta) {
                    break;
                }
            }

            return tentativeValue;
        }
    }
}

net.coderodde.zerosum.ai.impl.SortingAlphaBetaPruningGameEngine

package net.coderodde.zerosum.ai.impl;

import java.util.List;
import net.coderodde.zerosum.ai.EvaluatorFunction;
import net.coderodde.zerosum.ai.AbstractGameEngine;
import net.coderodde.zerosum.ai.AbstractState;
import net.coderodde.zerosum.ai.demo.DemoPlayerColor;

/**
 * This class implements the 
 * <a href="https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning">
 * Alpha-beta pruning</a> algorithm for zero-sum two-player games.
 * 
 * @param <S> the game state type.
 * @param <P> the player color type.
 * @author Rodion "rodde" Efremov
 * @version 1.6 (May 26, 2019)
 * @version 1.61 (Sep 12, 2019)
 * @since 1.6 (May 26, 2019)
 */
public final class SortingAlphaBetaPruningGameEngine
        <S extends AbstractState<S, P>, 
         P extends Enum<P>>
           extends AbstractGameEngine<S, P> {

    /**
     * Constructs this minimax game engine.
     * @param evaluatorFunction the evaluator function.
     * @param depth the search depth.
     */
    public SortingAlphaBetaPruningGameEngine(EvaluatorFunction<S> evaluatorFunction,
                                      int depth) {
        super(evaluatorFunction, depth, Integer.MAX_VALUE);
    }

    /**
     * {@inheritDoc}
     */
    public S makePly(S state,
                     P minimizingPlayer,
                     P maximizingPlayer,
                     P initialPlayer) {
        state.setDepth(depth);

        // Do the game tree search with Alpha-beta pruning:
        return makePlyImplTopmost(state,
                                  depth,
                                  -Double.NEGATIVE_INFINITY,
                                   Double.POSITIVE_INFINITY,
                                  minimizingPlayer,
                                  maximizingPlayer,
                                  initialPlayer);
    }

    /**
     * Pefrorms the topmost search of a game tree.
     * 
     * @param state            the state to start the search from.
     * @param depth            the depth of the tree to search.
     * @param alpha            the alpha cut-off value.
     * @param beta             the beta cut-off value.
     * @param minimizingPlayer the minimizing player color.
     * @param maximizingPlayer the maximizing player color.
     * @param currentPlayer    the current player color.
     * @return 
     */
    private S makePlyImplTopmost(S state,
                                 int depth,
                                 double alpha,
                                 double beta,
                                 P minimizingPlayer,
                                 P maximizingPlayer,
                                 P currentPlayer) {
        S bestState = null;
        List<S> children = state.children();

        if (currentPlayer == maximizingPlayer) {
            children.sort((a, b) -> {
                double valueOfA = super.evaluatorFunction.evaluate(a);
                double valueOfB = super.evaluatorFunction.evaluate(b);
                return Double.compare(valueOfA, valueOfB);
            });

            double tentativeValue = Double.NEGATIVE_INFINITY;

            for (S childState : children) {
                double value = makePlyImpl(childState,
                                           depth - 1,
                                           alpha,
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue < value) {
                    tentativeValue = value;
                    bestState = childState;
                }

                alpha = Math.max(alpha, tentativeValue);

                if (alpha >= beta) {
                    return bestState;
                }
            }
        } else {
            // Here, 'initialPlayer == minimizingPlayer'.
            children.sort((a, b) -> {
                double valueOfA = super.evaluatorFunction.evaluate(a);
                double valueOfB = super.evaluatorFunction.evaluate(b);
                return Double.compare(valueOfB, valueOfA);
            });

            double tentativeValue = Double.POSITIVE_INFINITY;

            for (S childState : children) {
                double value = makePlyImpl(childState,
                                           depth - 1,
                                           alpha,
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue > value) {
                    tentativeValue = value;
                    bestState = childState;
                }

                beta = Math.min(beta, tentativeValue);

                if (alpha >= beta) {
                    return bestState;
                }
            }
        }

        return bestState;
    }

    /**
     * Performs a single step down the game tree.
     * 
     * @param state            the starting state.
     * @param depth            the maximum depth of the game tree.
     * @param alpha            the alpha cut-off.
     * @param beta             the beta cut-off.
     * @param minimizingPlayer the minimizing player.
     * @param maximizingPlayer the maximizing player.
     * @param currentPlayer    the current player.
     * 
     * @return the value of the best ply.
     */
    private double makePlyImpl(S state,
                               int depth,
                               double alpha,
                               double beta,
                               P minimizingPlayer,
                               P maximizingPlayer,
                               P currentPlayer) {
        if (state.getDepth() == 0 
                || state.checkVictory() != null
                || state.isTerminal()) {
            return evaluatorFunction.evaluate(state);
        }

        List<S> children = state.children();

        if (currentPlayer == maximizingPlayer) {
            children.sort((a, b) -> {
                double valueOfA = super.evaluatorFunction.evaluate(a);
                double valueOfB = super.evaluatorFunction.evaluate(b);
                return Double.compare(valueOfA, valueOfB);
            });

            double tentativeValue = Double.NEGATIVE_INFINITY;

            for (S child : children) {
                double value = makePlyImpl(child,
                                           depth - 1,
                                           alpha, 
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue < value) {
                    tentativeValue = value;
                }

                alpha = Math.max(alpha, tentativeValue);

                if (alpha >= beta) {
                    break;
                }
            }

            return tentativeValue;
        } else {
            // Here, 'initialPlayer == minimizingPlayer'.
            children.sort((a, b) -> {
                double valueOfA = super.evaluatorFunction.evaluate(a);
                double valueOfB = super.evaluatorFunction.evaluate(b);
                return Double.compare(valueOfB, valueOfA);
            });

            double tentativeValue = Double.POSITIVE_INFINITY;

            for (S child : children) {
                double value = makePlyImpl(child,
                                           depth - 1,
                                           alpha,
                                           beta,
                                           minimizingPlayer,
                                           maximizingPlayer,
                                           minimizingPlayer);

                if (tentativeValue > value) {
                    tentativeValue = value;
                }

                beta = Math.min(beta, tentativeValue);

                if (alpha >= beta) {
                    break;
                }
            }

            return tentativeValue;
        }
    }
}

net.coderodde.zerosum.ai.impl.AbstractGameEngine

package net.coderodde.zerosum.ai;

/**
 * This abstract class defines the API for game-playing AI algorithms such as 
 * Minimax, Alpha-beta pruning, and so on.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (May 26, 2019)
 * @param <S> the board state type.
 * @param <P> the player color type.
 */
public abstract class AbstractGameEngine<
    S extends AbstractState<S, P>,
    P extends Enum<P>
> {

    /**
     * The minimum depth of the game tree to traverse.
     */
    private static final int MINIMUM_DEPTH = 1;

    /**
     * The depth, after reaching which, the search spawns isolated tasks for a 
     * thread pool to process.
     */
    private static final int MINIMUM_PARALLEL_DEPTH = 1;

    /**
     * The state evaluator function.
     */
    protected EvaluatorFunction<S> evaluatorFunction;

    /**
     * The maximum depth of the game tree to construct.
     */
    protected int depth;

    /**
     * The depth after which to switch to parallel computation.
     */
    protected int parallelDepth;

    /**
     * Constructs this game engine with given parameters. Note that if 
     * {@code parallelDepth > depth}, the entire computation will be run in this
     * thread without spawning 
     * @param evaluatorFunction
     * @param depth
     * @param parallelDepth 
     */
    public AbstractGameEngine(EvaluatorFunction<S> evaluatorFunction,
                      int depth,
                      int parallelDepth) {
        setEvaluatorFunction(evaluatorFunction);
        setDepth(depth);
        setParallelDepth(parallelDepth);
    }

    public EvaluatorFunction<S> getEvaluatorFunction() {
        return evaluatorFunction;
    }

    public int getDepth() {
        return depth;
    }

    public int getParallelDepth() {
        return parallelDepth;
    }

    public void setEvaluatorFunction(EvaluatorFunction<S> evaluatorFunction) {
        this.evaluatorFunction = evaluatorFunction;
    }

    public void setDepth(int depth) {
        this.depth = checkDepth(depth);
    }

    public void setParallelDepth(int parallelDepth) {
        this.parallelDepth = checkParallelDepth(parallelDepth);
    }

    /**
     * Computes and makes a single move. 
     * @param state the source game state.
     * @param minimizingPlayer the player that seeks to minimize the score.
     * @param maximizingPlayer the player that seeks to maximize the score.
     * @param initialPlayer the initial player. Must be either 
     * {@code minimizingPlayer} or {@code maximizingPlayer}. The ply is computed
     * for this specific player.
     * @return the next game state.
     */
    public abstract S makePly(S state, 
                              P minimizingPlayer,
                              P maximizingPlayer,
                              P initialPlayer);

    /**
     * Validates the depth candidate.
     * @param depthCandidate the depth candidate to validate.
     * @return the depth candidate if valid.
     */
    private int checkDepth(int depthCandidate) {
        if (depthCandidate < MINIMUM_DEPTH) {
            throw new IllegalArgumentException(
                    "The requested depth (" + depthCandidate + ") is too " + 
                    "small. Must be at least " + MINIMUM_DEPTH + ".");
        }

        return depthCandidate;
    }

    /**
     * Validates the parallel depth candidate.
     * @param parallelDepthCandidate the parallel depth candidate to validate.
     * @return the parallel depth candidate.
     */
    private int checkParallelDepth(int parallelDepthCandidate) {
        if (parallelDepthCandidate < MINIMUM_PARALLEL_DEPTH) {
            throw new IllegalArgumentException(
                    "The requested parallel depth (" + parallelDepthCandidate +
                    ") is too small. Must be at least " +
                    MINIMUM_PARALLEL_DEPTH + ".");
        }

        return parallelDepthCandidate;
    }
}

net.coderodde.zerosum.ai.impl.AbstractState

package net.coderodde.zerosum.ai;

import java.util.List;

/**
 * This interface defines the API for search states.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (May 26, 2019)
 * @param <S> the actual state type.
 */
public abstract class AbstractState<S extends AbstractState<S, P>,
                                    P extends Enum<P>> {

    /**
     * The depth of this state.
     */
    private int depth;

    /**
     * Returns the next ply.
     * 
     * @return the collection of next states.
     */
    public abstract List<S> children();

    /**
     * Returns {@code true} if this state is a terminal state.
     * 
     * @return a boolean indicating whether this state is terminal.
     */
    public abstract boolean isTerminal();

    /**
     * Checks whether this state represents a victory of a player.
     * 
     * @return the winning player or {@code null} if there is no such.
     */
    public abstract P checkVictory();

    public int getDepth() {
        return depth;
    }

    public void setDepth(int depth) {
        this.depth = depth;
    }
}

net.coderodde.zerosum.ai.impl.EvaluatorFunction

package net.coderodde.zerosum.ai;

/**
 * This interface defines the API for evaluation functions.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (May 26, 2019)
 * @param <S> the state type.
 */
public interface EvaluatorFunction<S> {

    /**
     * Evaluates the given state and returns the result.
     * @param state the state to evaluate.
     * @return the evaluation score.
     */
    public double evaluate(S state);
}

Critique request

I would like to hear comments about general code design, efficiency and readability/maintainability of my code. Yet, please tell me anything that comes to mind.