// Runs an entire game using parallelized iterative deepening minimax public State RunParallelIterativeDeepening(bool print, int timeLimit) { bool gameOver = false; // update deck memory initially with observed cards on board this.UpdateDeckMemory(0, true); current = new State(game.currentState.Grid, GameEngine.PLAYER); int nextCard = game.nextCard; while (!gameOver) { if (print) { Program.CleanConsole(); Console.Write(BoardHelper.ToString(current.Grid)); } PlayerMove action = ((PlayerMove)ParallelIterativeDeepening(current, timeLimit, deck.Clone())); if (action.Direction != (DIRECTION)(-1)) { gameOver = game.SendUserAction(action); current = new State(game.currentState.Grid, GameEngine.PLAYER); if (nextCard > 0) this.UpdateDeckMemory(nextCard, false); nextCard = game.nextCard; } else { gameOver = true; Program.CleanConsole(); Console.Write(BoardHelper.ToString(current.Grid)); } } return current; }
// Runs an entire game using MCTS limited by time public State RunTimeLimitedMCTS(bool print, int timeLimit) { // update deck memory initially with observed cards on board this.UpdateDeckMemory(0, true); int nextCard = this.gameEngine.nextCard; while (true) { currentState = new State(BoardHelper.CloneGrid(this.gameEngine.currentState.Grid), GameEngine.PLAYER); if (print) { Program.CleanConsole(); Console.WriteLine(BoardHelper.ToString(currentState.Grid)); } Node result = TimeLimitedMCTS(currentState, timeLimit, deck.Clone()); if (result == null) { // game over return currentState; } gameEngine.SendUserAction((PlayerMove)result.GeneratingMove); // update deck memory only if the new card is not a bonus card (we know what value the new card has by keeping track of nextcard if (nextCard > 0) this.UpdateDeckMemory(nextCard, false); nextCard = this.gameEngine.nextCard; } }
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 the number of trapped cards in the game state // Use this heuristic as a penalty, i.e. subtract it public static double TrappedPenalty(State state) { double trapped = 0; for (int i = 0; i < GameEngine.COLUMNS; i++) { for (int j = 0; j < GameEngine.ROWS; j++) { if (state.Grid[i][j] != 0) { // check neighbours in vertical direction int neighbourRowAbove = j + 1; int neighbourRowBelow = j - 1; if (neighbourRowAbove < GameEngine.ROWS && j == 0) // trapped between wall below and higher card above { if (state.Grid[i][j] < 3 && state.Grid[i][neighbourRowAbove] >= 3) { trapped++; } else if (state.Grid[i][j] >= 3 && state.Grid[i][neighbourRowAbove] >= 3 && state.Grid[i][neighbourRowAbove] > state.Grid[i][j]) { trapped++; } } else if (neighbourRowBelow >= 0 && j == 3) // trapped between wall above and higher card below { if (state.Grid[i][j] < 3 && state.Grid[i][neighbourRowBelow] >= 3) { trapped++; } else if (state.Grid[i][j] >= 3 && state.Grid[i][neighbourRowBelow] >= 3 && state.Grid[i][neighbourRowBelow] > state.Grid[i][j]) { trapped++; } } else if (neighbourRowAbove < GameEngine.ROWS && neighbourRowBelow >= 0) // trapped between two higher cards { if (state.Grid[i][j] < 3 && state.Grid[i][neighbourRowBelow] >= 3 && state.Grid[i][neighbourRowAbove] >= 3) { trapped++; } else if (state.Grid[i][j] >= 3 && state.Grid[i][neighbourRowBelow] >= 3 && state.Grid[i][neighbourRowAbove] >= 3 && state.Grid[i][neighbourRowBelow] > state.Grid[i][j] && state.Grid[i][neighbourRowAbove] > state.Grid[i][j]) { trapped++; } } // check neighbours in horizontal direction int neighbourColumnToRight = i + 1; int neighbourColumnToLeft = i - 1; if (neighbourColumnToRight < GameEngine.COLUMNS && i == 0) // trapped between wall to the left and higher card to the right { if (state.Grid[i][j] < 3 && state.Grid[neighbourColumnToRight][j] >= 3) { trapped++; } else if (state.Grid[i][j] >= 3 && state.Grid[neighbourColumnToRight][j] >= 3 && state.Grid[neighbourColumnToRight][j] > state.Grid[i][j]) { trapped++; } } else if (neighbourColumnToLeft >= 0 && i == 3) // trapped between wall to the right and higher card to the left { if (state.Grid[i][j] < 3 && state.Grid[neighbourColumnToLeft][j] >= 3) { trapped++; } else if (state.Grid[i][j] >= 3 && state.Grid[neighbourColumnToLeft][j] >= 3 && state.Grid[neighbourColumnToLeft][j] > state.Grid[i][j]) { trapped++; } } else if (neighbourColumnToRight < GameEngine.COLUMNS && neighbourColumnToLeft >= 0) // trapped between two higher cards { if (state.Grid[i][j] < 3 && state.Grid[neighbourColumnToRight][j] >= 3 && state.Grid[neighbourColumnToLeft][j] >= 3) { trapped++; } else if(state.Grid[i][j] >= 3 && state.Grid[neighbourColumnToRight][j] >= 3 && state.Grid[neighbourColumnToLeft][j] >= 3 && state.Grid[neighbourColumnToRight][j] > state.Grid[i][j] && state.Grid[neighbourColumnToLeft][j] > state.Grid[i][j]) { trapped++; } } } } } return trapped; }
// 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; }
// 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); }
// MAX part of Minimax Move Max(State state, int depth, double alpha, double beta) { PlayerMove 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 = MinimaxAlgorithm(resultingState, depth - 1, alpha, beta, deck).Score; if (currentScore > highestScore) { highestScore = currentScore; bestMove = (PlayerMove)move; } alpha = Math.Max(alpha, highestScore); if (beta <= alpha) break; } bestMove.Score = highestScore; return bestMove; }
// Runs an entire game using classic Minimax to decide on moves internal State Run(bool print) { bool gameOver = false; // update deck memory initially with observed cards on board this.UpdateDeckMemory(0, true); current = new State(game.currentState.Grid, GameEngine.PLAYER); int nextCard = game.nextCard; while (!gameOver) { if (print) { Program.CleanConsole(); Console.Write(BoardHelper.ToString(current.Grid)); } PlayerMove action = ((PlayerMove)MinimaxAlgorithm(current, definedDepth, double.MinValue, double.MaxValue, deck.Clone())); if (action.Direction != (DIRECTION)(-1)) { gameOver = game.SendUserAction(action); current = new State(game.currentState.Grid, GameEngine.PLAYER); if(nextCard > 0) this.UpdateDeckMemory(nextCard, false); nextCard = game.nextCard; } else { gameOver = true; Program.CleanConsole(); Console.Write(BoardHelper.ToString(current.Grid)); } } return current; }
// Evaluation function used by Minimax and Expectimax public static double Evaluate(State state) { if (state.IsGameOver()) return -1000; double smoothness = Smoothness(state); double monotonicity = Monotonicity(state); double emptycells = EmptyCells(state); double highestvalue = HighestCard(state); double trappedpenalty = TrappedPenalty(state); return smoothness_weight * smoothness + monotonicity_weight * monotonicity + emptycells_weight * emptycells + highestvalue_weight * highestvalue - trappedpenalty_weight * trappedpenalty; }
// 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; }
// Arranges the tiles in a "snake" // As there are 8 different ways the tiles can be arranged in a "snake" on the grid, this method // finds the one that fits the best, allowing the AI to adjust to a different "snake" pattern public static double WeightSnake(State state) { double[][] snake1 = new double[][] { new double[]{20,9,4,.1}, new double[]{19,10,3,0.2}, new double[]{18,11,2,0.3}, new double[]{17,12,1,0.4} }; double[][] snake2 = new double[][] { new double[]{20,19,18,17}, new double[]{9,10,11,12}, new double[]{4,3,2,1}, new double[]{0.1,0.2,0.3,0.4} }; double[][] snake3 = new double[][]{ new double[]{17,12,1,0.4}, new double[]{18,11,2,0.3}, new double[]{19,10,3,0.2}, new double[]{20,9,4,0.1} }; double[][] snake4 = new double[][] { new double[]{17,18,19,20}, new double[]{12,11,10,9}, new double[]{1,2,3,4}, new double[]{0.4,0.3,0.2,0.1} }; double[][] snake5 = new double[][] { new double[]{0.1,0.2,0.3,0.4}, new double[]{4,3,2,1}, new double[]{9,10,11,12}, new double[]{20,19,18,17} }; double[][] snake6 = new double[][] { new double[]{0.1,4,9,20}, new double[]{0.2,3,10,19}, new double[]{0.3,2,11,18}, new double[]{0.4,1,12,17} }; double[][] snake7 = new double[][] { new double[]{0.4,0.3,0.2,0.1}, new double[]{1,2,3,4}, new double[]{12,11,10,9}, new double[]{17,18,19,20} }; double[][] snake8 = new double[][] { new double[]{0.4,1,12,17}, new double[]{0.3,2,11,18}, new double[]{0.2,3,10,19}, new double[]{0.1,4,9,20} }; List<double[][]> weightMatrices = new List<double[][]>(); weightMatrices.Add(snake1); weightMatrices.Add(snake2); weightMatrices.Add(snake3); weightMatrices.Add(snake4); weightMatrices.Add(snake5); weightMatrices.Add(snake6); weightMatrices.Add(snake7); weightMatrices.Add(snake8); return MaxProductMatrix(state.Grid, weightMatrices); }
// Returns number of empty cells on board public static double EmptyCells(State state) { double numEptyCells = 0; for (int i = 0; i < GameEngine.COLUMNS; i++) { for (int j = 0; j < GameEngine.ROWS; j++) { if (state.Grid[i][j] == 0) { numEptyCells++; } } } return numEptyCells; }
// Returns the highest card on board public static double HighestCard(State state) { return BoardHelper.GetHighestCard(state.Grid); }
// 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; }
// Counts number of merges on board public static double Mergeability(State state) { double numMerges = 0; for (int i = 0; i < GameEngine.COLUMNS; i++) { for (int j = 0; j < GameEngine.ROWS; j++) { // check merges horizontally if (i + 1 < state.Grid.Length && state.Grid[i][j] != 0 && state.Grid[i + 1][j] != 0) { if (state.Grid[i][j] == 1 && state.Grid[i + 1][j] == 2 || state.Grid[i][j] == 2 && state.Grid[i + 1][j] == 1 || state.Grid[i][j] == state.Grid[i + 1][j]) { if (i == 0) numMerges++; else if (state.Grid[i - 1][j] != 0) numMerges++; } } // check merges vertically if (j + 1 < state.Grid.Length && state.Grid[i][j] != 0 && state.Grid[i][j + 1] != 0) { if (state.Grid[i][j] == 1 && state.Grid[i][j + 1] == 2 || state.Grid[i][j] == 2 && state.Grid[i][j + 1] == 1 || state.Grid[i][j] == state.Grid[i][j]) { if (j == 0) numMerges++; else if (state.Grid[i][j - 1] != 0) numMerges++; } } } } return numMerges; }
// 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; }
// returns a score ranking a state according to how the tiles are increasing/decreasing in all directions // increasing/decreasing in the same directions (for example generally increasing in up and right direction) will return higher score // than a state increasing in one row and decreasing in another row public static double Monotonicity(State state) { double left = 0; double right = 0; double up = 0; double down = 0; // up/down direction for (int i = 0; i < state.Grid.Length; i++) { int current = 0; int next = current + 1; while (next < state.Grid.Length) { // skip empty cells while (next < state.Grid.Length && state.Grid[i][next] == 0) next++; // check boundaries if (next >= state.Grid.Length) next--; // only count instances where both cells are occupied if (state.Grid[i][current] != 0 && state.Grid[i][next] != 0) { double currentValue = 0; if (state.Grid[i][current] == 1 || state.Grid[i][current] == 2) currentValue = 1; else currentValue = Math.Log(state.Grid[i][current] / 3) / Math.Log(2) + 2; double nextValue = 0; if(state.Grid[i][next] == 1 || state.Grid[i][next] == 2) nextValue = 1; else nextValue = Math.Log(state.Grid[i][next] / 3) / Math.Log(2) + 2; if (currentValue > nextValue) // increasing in down direction down += nextValue - currentValue; else if (nextValue > currentValue) // increasing in up direction up += currentValue - nextValue; } current = next; next++; } } // left/right direction for (int j = 0; j < state.Grid.Length; j++) { int current = 0; int next = current + 1; while (next < state.Grid.Length) { // skip empty cells while (next < state.Grid.Length && state.Grid[next][j] == 0) next++; // check boundaries if (next >= state.Grid.Length) next--; // only consider instances where both cells are occupied if (state.Grid[current][j] != 0 && state.Grid[next][j] != 0) { double currentValue = 0; if (state.Grid[current][j] == 1 || state.Grid[current][j] == 2) currentValue = 1; else currentValue = Math.Log(state.Grid[current][j] / 3) / Math.Log(2) + 2; double nextValue = 0; if (state.Grid[next][j] == 1 || state.Grid[next][j] == 2) nextValue = 1; else nextValue = Math.Log(state.Grid[next][j] / 3) / Math.Log(2) + 2; if (currentValue > nextValue) // increasing in left direction left += nextValue - currentValue; else if (nextValue > currentValue) // increasing in right direction right += currentValue - nextValue; } current = next; next++; } } return Math.Max(up, down) + Math.Max(left, right); }
// 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; }
// Arranges tiles in a corner and prefers higher cards along edges public static double Corner(State state) { double[][] corner1 = new double[][] { new double[]{20,12,4,0.4}, new double[]{19,11,3,0.3}, new double[]{18,10,2,0.2}, new double[]{17,9,1,0.1} }; double[][] corner2 = new double[][] { new double[]{0.4,4,12,20}, new double[]{0.3,3,11,19}, new double[]{0.2,2,10,18}, new double[]{0.1,1,9,17} }; double[][] corner3 = new double[][] { new double[]{17,9,1,0.1}, new double[]{18,10,2,0.2}, new double[]{19,11,3,0.3}, new double[]{20,12,4,0.4} }; double[][] corner4 = new double[][] { new double[]{0.1,1,9,17}, new double[]{0.2,2,10,18}, new double[]{0.3,3,11,19}, new double[]{0.4,4,12,20} }; double[][] corner5 = new double[][] { new double[]{20,19,18,17}, new double[]{12,11,10,9}, new double[]{4,3,2,1}, new double[]{0.4,0.3,0.2,0.1} }; double[][] corner6 = new double[][] { new double[]{17,18,19,20}, new double[]{9,10,11,12}, new double[]{1,2,3,4}, new double[]{0.1,0.2,0.3,0.4} }; double[][] corner7 = new double[][] { new double[]{0.4,0.3,0.2,0.1}, new double[]{4,3,2,1}, new double[]{12,11,10,9}, new double[]{20,19,18,17} }; double[][] corner8 = new double[][] { new double[]{0.1,0.2,0.3,0.4}, new double[]{1,2,3,4}, new double[]{9,10,11,12}, new double[]{17,18,19,20} }; List<double[][]> weightMatrices = new List<double[][]>(); weightMatrices.Add(corner1); weightMatrices.Add(corner2); weightMatrices.Add(corner3); weightMatrices.Add(corner4); weightMatrices.Add(corner5); weightMatrices.Add(corner6); weightMatrices.Add(corner7); weightMatrices.Add(corner8); return MaxProductMatrix(state.Grid, weightMatrices); }
// 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); } }
// Returns the point score of the given game state public static double Points(State state) { return state.CalculateFinalScore(); }
// Applies the move and returns the result state internal State ApplyMove(Move move) { if (move is PlayerMove) { State result = new State(BoardHelper.CloneGrid(this.grid), GameEngine.COMPUTER); if (((PlayerMove)move).Direction == DIRECTION.LEFT) { return result.ApplyLeft(); } else if (((PlayerMove)move).Direction == DIRECTION.RIGHT) { return result.ApplyRight(); } else if (((PlayerMove)move).Direction == DIRECTION.UP) { return result.ApplyUp(); } else if (((PlayerMove)move).Direction == DIRECTION.DOWN) { return result.ApplyDown(); } return result; } else if (move is ComputerMove) { State result = new State(BoardHelper.CloneGrid(this.grid), GameEngine.PLAYER); result.grid[((ComputerMove)move).Position.Item1][((ComputerMove)move).Position.Item2] = ((ComputerMove)move).Card; result.generatingMove = (ComputerMove)move; return result; } else { throw new Exception(); } }
// Ranks the state according to how smooth it is // Smoothness prefers cards of similar values next to each other public static double Smoothness(State state) { double smoothness = 0; for (int i = 0; i < state.Grid.Length; i++) { for (int j = 0; j < state.Grid.Length; j++) { if (state.Grid[i][j] != 0) { double currentValue = 0; if (state.Grid[i][j] == 1 || state.Grid[i][j] == 2) currentValue = 1; else currentValue = Math.Log(state.Grid[i][j] / 3) / Math.Log(2) + 2; // we only check right and up for each tile Cell nearestTileRight = FindNearestCard(new Cell(i, j), DIRECTION.RIGHT, state.Grid); Cell nearestTileUp = FindNearestCard(new Cell(i, j), DIRECTION.UP, state.Grid); // check that we found a tile (do not take empty cells into account) if (nearestTileRight.IsValid() && state.Grid[nearestTileRight.x][nearestTileRight.y] != 0) { double neighbourValue = 0; if (state.Grid[nearestTileRight.x][nearestTileRight.y] == 1 || state.Grid[nearestTileRight.x][nearestTileRight.y] == 2) neighbourValue = 1; else neighbourValue = Math.Log(state.Grid[nearestTileRight.x][nearestTileRight.y] / 3) / Math.Log(2) + 2; smoothness += Math.Abs(currentValue - neighbourValue); } if (nearestTileUp.IsValid() && state.Grid[nearestTileUp.x][nearestTileUp.y] != 0) { double neighbourValue = 0; if (state.Grid[nearestTileUp.x][nearestTileUp.y] == 1 || state.Grid[nearestTileUp.x][nearestTileUp.y] == 2) neighbourValue = 1; else neighbourValue = Math.Log(state.Grid[nearestTileUp.x][nearestTileUp.y] / 3) / Math.Log(2) + 2; smoothness += Math.Abs(currentValue - neighbourValue); } } } } return -smoothness; }