public Minimax(GameEngine game, int depth) { this.game = game; this.definedDepth = depth; this.current = new State(game.currentState.Grid, GameEngine.PLAYER); this.deck = new Deck(); }
public MCTS(GameEngine gameEngine) { this.gameEngine = gameEngine; this.deck = new Deck(); this.currentState = new State(BoardHelper.CloneGrid(this.gameEngine.currentState.Grid), GameEngine.PLAYER); this.random = new Random(); }
public GameEngine() { deck = new Deck(); int[][] grid = initializeGrid(); currentState = new State(grid, COMPUTER); UpdatePeekCard(); }
public Node(Move move, Node parent, State state, Deck deck) { this.state = state; this.generatingMove = move; this.parent = parent; this.results = 0; this.visits = 0; this.children = new List<Node>(); this.untriedMoves = state.GetMoves(deck); }
// Returns all available moves in the state internal List<Move> GetMoves(Deck deck) { if (player == GameEngine.PLAYER) { return GetAllPlayerMoves(); } else { return GetAllComputerMoves(deck); } }
// Returns a list of all available computer moves given a deck public List<Move> GetAllComputerMoves(Deck deck) { IEnumerable<int> cardsLeft = deck.GetAllCards().Distinct(); // get all distinct cards left List<Move> moves = new List<Move>(); foreach (int card in cardsLeft) { switch (((PlayerMove)generatingMove).Direction) { case DIRECTION.LEFT: return AddComputerMovesAfterLeft(moves, card); case DIRECTION.RIGHT: return AddComputerMovesAfterRight(moves, card); case DIRECTION.UP: return AddComputerMovesAfterUp(moves, card); case DIRECTION.DOWN: return AddComputerMovesAfterDown(moves, card); default: throw new Exception(); } } return moves; }
// To take advantage of the fact that we can count cards private void UpdateDeckMemory(int card, bool firstMove) { if (firstMove) { for (int i = 0; i < GameEngine.COLUMNS; i++) { for (int j = 0; j < GameEngine.ROWS; j++) { if (current.Grid[i][j] != 0) { deck.Remove(current.Grid[i][j]); } } } } else { deck.Remove(card); // remove the observed card from our deck memory if (deck.IsEmpty()) { deck = new Deck(); } } }
// Recursive part of iterative deepening private Tuple<Move, bool> RecursiveIterativeDeepening(State state, int depth, double alpha, double beta, int timeLimit, Stopwatch timer, Deck deck) { Move bestMove; if (depth == 0 || state.IsGameOver()) { if (state.Player == GameEngine.PLAYER) { bestMove = new PlayerMove(); // default constructor creates dummy action bestMove.Score = AI.Evaluate(state); return new Tuple<Move, Boolean>(bestMove, true); } else if (state.Player == GameEngine.COMPUTER) { bestMove = new ComputerMove(); // default constructor creates dummy action bestMove.Score = AI.Evaluate(state); return new Tuple<Move, Boolean>(bestMove, true); } else { throw new Exception(); } } if (state.Player == GameEngine.PLAYER){ bestMove = new PlayerMove(); double highestScore = Double.MinValue, currentScore = Double.MinValue; List<Move> moves = state.GetAllMoves(); foreach (Move move in moves) { State resultingState = state.ApplyMove(move); currentScore = RecursiveIterativeDeepening(resultingState, depth - 1, alpha, beta, timeLimit, timer, deck).Item1.Score; if (currentScore > highestScore) { highestScore = currentScore; bestMove = (PlayerMove)move; } alpha = Math.Max(alpha, highestScore); if (beta <= alpha) break; if (timer.ElapsedMilliseconds > timeLimit) { bestMove.Score = highestScore; return new Tuple<Move, Boolean>(bestMove, false); // recursion not completed, return false } } bestMove.Score = highestScore; return new Tuple<Move, Boolean>(bestMove, true); } else { bestMove = new ComputerMove(); double lowestScore = Double.MaxValue, currentScore = Double.MaxValue; List<Move> moves = null; if (depth == definedDepth - 1) { int nextCard = game.nextCard; moves = state.GetAllComputerMoves(nextCard); } else { if (deck.IsEmpty()) deck = new Deck(); moves = state.GetAllComputerMoves(deck); } foreach (Move move in moves) { deck.Remove(((ComputerMove)move).Card); State resultingState = state.ApplyMove(move); currentScore = RecursiveIterativeDeepening(resultingState, depth - 1, alpha, beta, timeLimit, timer, deck).Item1.Score; if (currentScore < lowestScore) { lowestScore = currentScore; bestMove = (ComputerMove)move; } beta = Math.Min(beta, lowestScore); if (beta <= alpha) break; deck.Add(((ComputerMove)move).Card); if (timer.ElapsedMilliseconds > timeLimit) { bestMove.Score = lowestScore; return new Tuple<Move, Boolean>(bestMove, false); // recursion not completed, return false } } bestMove.Score = lowestScore; return new Tuple<Move, Boolean>(bestMove, true); } }
// Runs an entire game using a parallelized version of iterative deepening minimax private Move ParallelIterativeDeepening(State state, int timeLimit, Deck deck) { Move bestMove = new PlayerMove(); List<Move> moves = state.GetMoves(deck); ConcurrentBag<Tuple<double, Move>> scores = new ConcurrentBag<Tuple<double, Move>>(); if (moves.Count == 0) { // game over return bestMove; } // create the resulting states before starting the threads List<State> resultingStates = new List<State>(); foreach (Move move in moves) { State resultingState = state.ApplyMove(move); resultingStates.Add(resultingState); } Parallel.ForEach(resultingStates, resultingState => { double score = IterativeDeepening(resultingState, timeLimit, deck.Clone()).Score; scores.Add(new Tuple<double, Move>(score, resultingState.GeneratingMove)); }); // find the best score double highestScore = Double.MinValue; foreach (Tuple<double, Move> score in scores) { PlayerMove move = (PlayerMove)score.Item2; if (score.Item1 > highestScore) { highestScore = score.Item1; bestMove = score.Item2; } } return bestMove; }
// Starts the time limited Monte Carlo Tree Search and returns the best child node // resulting from the search public Node TimeLimitedMCTS(State rootState, int timeLimit, Deck deck) { Stopwatch timer = new Stopwatch(); Node bestNode = null; while (bestNode == null && !rootState.IsGameOver()) { timer.Start(); Node rootNode = TimeLimited(rootState, timeLimit, timer, deck); bestNode = FindBestChild(rootNode.Children); timeLimit += 10; timer.Reset(); } return bestNode; }
// MIN part of Minimax Move Min(State state, int depth, double alpha, double beta) { ComputerMove bestMove = new ComputerMove(); double lowestScore = Double.MaxValue, currentScore = Double.MaxValue; List<Move> moves = null; if (depth == definedDepth - 1) { int nextCard = game.nextCard; moves = state.GetAllComputerMoves(nextCard); } else { if (deck.IsEmpty()) deck = new Deck(); moves = state.GetAllComputerMoves(deck); } foreach (Move move in moves) { deck.Remove(((ComputerMove)move).Card); State resultingState = state.ApplyMove(move); currentScore = MinimaxAlgorithm(resultingState, depth - 1, alpha, beta, deck).Score; if (currentScore < lowestScore) { lowestScore = currentScore; bestMove = (ComputerMove)move; } beta = Math.Min(beta, lowestScore); if (beta <= alpha) break; deck.Add(((ComputerMove)move).Card); } bestMove.Score = lowestScore; return bestMove; }
// Iterative deepening minimax private Move IterativeDeepening(State state, int timeLimit, Deck deck) { int depth = 1; Stopwatch timer = new Stopwatch(); Move bestMove = null; // start the search timer.Start(); while (timer.ElapsedMilliseconds < timeLimit) { Tuple<Move, Boolean> result = RecursiveIterativeDeepening(state, depth, Double.MinValue, Double.MaxValue, timeLimit, timer, deck); if (result.Item2) bestMove = result.Item1; // only update bestMove if full recursion depth++; } return bestMove; }
// adds a child node to the list of children // after exploring a move - removes the move from untried public Node AddChild(Move move, State state, Deck deck) { Node child = new Node(move, this, state, deck); this.untriedMoves.Remove(move); this.children.Add(child); return child; }
// Time limited MCTS private Node TimeLimited(State rootState, int timeLimit, Stopwatch timer, Deck deck) { Node rootNode = new Node(null, null, rootState, deck); while (true) { if (timer.ElapsedMilliseconds > timeLimit) { if (FindBestChild(rootNode.Children) == null && !rootNode.state.IsGameOver()) { timeLimit += 10; timer.Restart(); } else { return rootNode; } } Node node = rootNode; State state = rootState.Clone(); Deck clonedDeck = deck.Clone(); // 1: Select while (node.UntriedMoves.Count == 0 && node.Children.Count != 0) { node = node.SelectChild(); state = state.ApplyMove(node.GeneratingMove); if (node.GeneratingMove is ComputerMove) { clonedDeck.Remove(((ComputerMove)node.GeneratingMove).Card); if (clonedDeck.IsEmpty()) clonedDeck = new Deck(); } } // 2: Expand if (node.UntriedMoves.Count != 0) { Move randomMove = node.UntriedMoves[random.Next(0, node.UntriedMoves.Count)]; if (randomMove is ComputerMove) { if (clonedDeck.IsEmpty()) clonedDeck = new Deck(); clonedDeck.Remove(((ComputerMove)randomMove).Card); state = state.ApplyMove(randomMove); node = node.AddChild(randomMove, state, clonedDeck); } else { state = state.ApplyMove(randomMove); node = node.AddChild(randomMove, state, clonedDeck); } } // 3: Simulation while (state.GetMoves(clonedDeck).Count != 0) { Move move = state.GetRandomMove(clonedDeck); if (move is ComputerMove) { if (clonedDeck.IsEmpty()) clonedDeck = new Deck(); clonedDeck.Remove(((ComputerMove)move).Card); state = state.ApplyMove(move); } else { state = state.ApplyMove(move); } } // 4: Backpropagation while (node != null) { node.Update(state.GetResult()); node = node.Parent; } } }
// Parallel time limited MCTS private DIRECTION ParallelTimeLimitedMCTS(State currentState, int timeLimit, Deck deck) { ConcurrentBag<Node> allChildren = new ConcurrentBag<Node>(); int numOfChildren = currentState.GetMoves(deck).Count; Stopwatch timer = new Stopwatch(); timer.Start(); Parallel.For(0, NUM_THREADS, i => { Node resultRoot = TimeLimited(currentState, timeLimit, timer, deck); foreach (Node child in resultRoot.Children) { allChildren.Add(child); } }); timer.Stop(); List<int> totalVisits = new List<int>(4) { 0, 0, 0, 0 }; List<double> totalResults = new List<double>(4) { 0, 0, 0, 0 }; foreach (Node child in allChildren) { int direction = (int)((PlayerMove)child.GeneratingMove).Direction; totalVisits[direction] += child.Visits; totalResults[direction] += child.Results; } double best = Double.MinValue; int bestDirection = -1; for (int k = 0; k < 4; k++) { double avg = totalResults[k] / totalVisits[k]; if (avg > best) { best = avg; bestDirection = k; } } if (bestDirection == -1) return (DIRECTION)(-1); return (DIRECTION)bestDirection; }
// Returns a random move internal Move GetRandomMove(Deck deck) { List<Move> moves = GetMoves(deck); int index = random.Next(0, moves.Count); return moves[index]; }
public Deck(Deck deck) { this.cards = new List<int>(deck.cards); }
// Classic Minimax using alpha-beta pruning private Move MinimaxAlgorithm(State state, int depth, double alpha, double beta, Deck deck) { Move bestMove; if (depth == 0 || state.IsGameOver()) { if (state.Player == GameEngine.PLAYER) { bestMove = new PlayerMove(); // default constructor creates dummy action bestMove.Score = AI.Evaluate(state); return bestMove; } else if (state.Player == GameEngine.COMPUTER) { bestMove = new ComputerMove(); // default constructor creates dummy action bestMove.Score = AI.Evaluate(state); return bestMove; } else { throw new Exception(); } } if (state.Player == GameEngine.PLAYER) return Max(state, depth, alpha, beta); else return Min(state, depth, alpha, beta); }