protected override AnalysisResult DoAnalyze(Game clonedGame) { // If game is over then stop if (clonedGame.IsOver) { return(null); } // Get all the placed tiles to determine all the correct playable tiles IReadOnlyList <Tile> placedTiles = clonedGame.History; // If it is a new game, select the center most if (placedTiles.Count == 0) { return(new AnalysisResult( clonedGame.Board[clonedGame.Board.Width / 2, clonedGame.Board.Height / 2])); } // Get hold of who player this analysis should be done for Player forPlayer = clonedGame.Manager.CurrentPlayer; // Generate nodes for all possible moves var possiblePositions = CandidateSearcher.Search(clonedGame).ToList(); // make a list of pairs of positions - value var evaluations = new List <(IPositional positional, double value)>(); // keep a record of max value var max = double.MinValue; // evaluate each possible position foreach (IPositional position in possiblePositions) { // play the board to get the next game state clonedGame.Play(position); // Evaluate the position using the algorithm // Note: because the possible position represents the maximizing node, // the next node should be done in minimizing phase var value = EvaluateMinimax(clonedGame, position, forPlayer, Level, isMaximizing: false); // If the value has reached max value, no need to evaluate further if (value == double.MaxValue) { return(new AnalysisResult(position)); } // Update the evaluations and max value accordingly if (value > max) { evaluations.Clear(); evaluations.Add((position, value)); max = value; } else if (value == max) { evaluations.Add((position, value)); } // Evaluation is done, undo the game state clonedGame.Undo(); } // Take out all the best choices var choices = (from evaluation in evaluations select evaluation.positional) .ToList(); // Randomly pick one result from the choices var choice = Random.Next(choices.Count); return(new AnalysisResult(choices[choice], choices)); }
protected IEnumerable <AINode> Search(Game game, int level) { // Get all the placed tiles to determine all the correct playable tiles IReadOnlyList <Tile> placedTiles = game.History; // If it is a new game, select the center most if (placedTiles.Count == 0) { return(new List <AINode>() { new AINode( game.Board[game.Board.Width / 2, game.Board.Height / 2], 0) }); } // Get current player to determine which side to search for Player player = game.Manager.CurrentPlayer; var playerCount = game.Manager.Players.Length; var maxPoint = double.MinValue; var candidateNodes = new List <AINode>(); // Get all the playable tiles IEnumerable <IPositional> playableTiles = CandidateSearcher.Search(game); // Populate corresponding NTrees with each playable tile found. foreach (Tile tile in playableTiles) { // Play the new cloned board game.Play(tile.X, tile.Y); // Evaluate this tile var point = TileEvaluator.Evaluate(game, tile, player.Piece); point = Math.Abs(point); var aiNode = new AINode(tile, point); if (level < Level) { if (aiNode.Point > maxPoint) { maxPoint = aiNode.Point; candidateNodes.Clear(); candidateNodes.Add(aiNode); } else if (aiNode.Point == maxPoint) { candidateNodes.Add(aiNode); } else { continue; } } else { candidateNodes.Add(aiNode); } game.Undo(); } if (level == Level && placedTiles.Count <= 2) { return(candidateNodes); } if (level == 0) { return(candidateNodes); } foreach (AINode node in candidateNodes) { if (node.Point >= 1000.0) { return(candidateNodes); } var childNodes = Search(game, level - 1).ToList(); childNodes.Sort((x, y) => - 1 * x.CompareTo(y)); AINode maxNode = childNodes.First(); childNodes.RemoveAll((x) => x.Point < maxNode.Point); // If the current node's board's game is over, stop evaluating because // there is a chance this node will reach the end of game, so there is // no longer any need to continue evaluating //if (b.IsOver) //{ // break; //} node.Point -= maxNode.Point; // Minus the current node's point by the max point so if the children // node's point is high, this node is less likely to be selected. node.Point -= 0.01 * childNodes.Count; } candidateNodes.Sort((x, y) => - 1 * x.CompareTo(y)); AINode maxNode2 = candidateNodes.First(); candidateNodes.RemoveAll((x) => x.Point < maxNode2.Point); return(candidateNodes); }
private double EvaluateMinimax( Game game, IPositional positional, Player forPlayer, int depth, double alpha = double.MinValue, double beta = double.MaxValue, bool isMaximizing = true) { // If is leaf node, evaluate it if (depth == 0) { // The leaf node is effectively created by the previous player so it // makes sense to evaluate it for the previous player return(EvaluateGame(game, positional, forPlayer, game.Manager.PreviousPlayer)); } // If game is over, but not leaf node, evaluate for the current player instead if (game.IsOver) { return(EvaluateGame(game, positional, forPlayer, game.Manager.CurrentPlayer)); } // Get all the playable tiles IEnumerable <IPositional> playableTiles = CandidateSearcher.Search(game); double value; if (isMaximizing) { var maxValue = double.MinValue; // Populate deeper nodes with each playable tile found foreach (Tile childTile in playableTiles) { // play the board to get the next game state game.Play(childTile); // Prepare to evaluate the next state by getting the next position and // recursively evaluate it var childValue = EvaluateMinimax(game, childTile, forPlayer, depth - 1, alpha, beta, false); // Evaluation is done, undo the game state game.Undo(); // Negative inifity signifies a lose to our team. Marking this node as // negative infinity so that the previous minimizing phase will not // pick this node if (childValue == double.MinValue) { maxValue = childValue; break; } // Perform algorithmic updates maxValue = Math.Max(maxValue, childValue); alpha = Math.Max(alpha, childValue); if (beta <= alpha) { break; } } value = maxValue; } else { var minValue = double.MaxValue; // Populate deeper nodes with each playable tile found foreach (Tile childTile in playableTiles) { // play the board to get the next game state game.Play(childTile); // Prepare to evaluate the next state by getting the next position and // recursively evaluate it var childValue = EvaluateMinimax(game, childTile, forPlayer, depth - 1, alpha, beta, true); // Evaluation is done, undo the game state game.Undo(); // Positive inifity signifies a win to our team. Marking this node as // positive infinity so that the previous maximizing phase will pick // this node if (childValue == double.MaxValue) { minValue = childValue; break; } // Perform algorithmic updates minValue = Math.Min(minValue, childValue); beta = Math.Min(beta, childValue); if (beta <= alpha) { break; } } value = minValue; } // Merging current state's value to child's value. Because the current // state is effectively created by the previous player so it makes sense // to evaluate it for the previous player value += EvaluateGame(game, positional, forPlayer, game.Manager.PreviousPlayer); return(value); }
protected List <NTree <AINode> > Search(Game game, NTree <AINode> currentNode, int level) { // Get all the placed tiles to determine all the correct playable tiles IReadOnlyList <Tile> placedTiles = game.History; // If it is a new game, select the center most if (placedTiles.Count == 0) { return(new List <NTree <AINode> >() { new NTree <AINode>( new AINode( game.Board[game.Board.Width / 2, game.Board.Height / 2], 0)) }); } // Get all the playable tiles IEnumerable <IPositional> playableTiles = CandidateSearcher.Search(game); // Get current player to determine which side to search for Player player = game.Manager.CurrentPlayer; var playerCount = game.Manager.Players.Length; // Populate corresponding NTrees with each playable tile found. var nTrees = new List <NTree <AINode> >(); foreach (Tile tile in playableTiles) { // Play the board game.Play(tile.X, tile.Y); // Evalue this tile var point = TileEvaluator.Evaluate(game, tile, player.Piece); point = Math.Abs(point); var aiNode = new AINode(tile, point); // Add to the list of NTrees var nTree = new NTree <AINode>(currentNode, aiNode); nTrees.Add(nTree); // If the current node's board's game is over, stop evaluating because // there is a chance this node will reach the end of game, so there is // no longer any need to continue evaluating if (game.IsOver) { game.Undo(); break; } // If the recursion of searching didn't reach the bottom (where level == // 0) then, keep evaluating current node by evaluating its children // nodes, where the children's side is the next player's side. if (level > 0 && level <= Level) { // Evaluate children nodes by recursion nTree.Nodes = Search(game, nTree, level - 1); if (nTree.Nodes.Count > 0) { // Get max point of the children nodes Take the first node because // it is sorted by descending NTree <AINode> firstNode = nTree.Nodes.First(); var maxPoint = firstNode.Value.Point; // Minus the current node's point by the max point so if the // children node's point is high, this node is less likely to be // selected. use j as a factor for the point as it goes shallower // back to its parent right before the original player's turn again NTree <AINode> traverseNode = nTree; for (var j = 1; j < playerCount && !(traverseNode is null); j++) { traverseNode.Value.Point -= j * maxPoint; traverseNode = traverseNode.ParentNode; } // Remove all chilren nodes with lower points nTree.Nodes.RemoveAll(n => n.Value.Point < maxPoint); // Call GC to free up memory //GC.Collect(); //GC.WaitForPendingFinalizers(); // The more the chilren nodes are left, the less likely the node is // to be selected. use j as a factor for the point as it goes // shallower back to its parent right before the original player's // turn again use 0.01 as a small factor to penalize same nodes left traverseNode = nTree; for (var j = 1; j < playerCount && !(traverseNode is null); j++) { traverseNode.Value.Point -= j * nTree.Nodes.Count * 0.01; traverseNode = traverseNode.ParentNode; } } } // Evaludation is done, undo the step game.Undo(); } // Sort the NTrees list by descending where the first item has the largest point nTrees.Sort((x, y) => - 1 * x.Value.CompareTo(y.Value)); return(nTrees); }