public static List<Node> GenerateSuccessors(Board board, bool isBlack) { //Validate moves based on current state to generate new states var nodes = new List<Node>(); //Generate # of valid moves var ValidMoves = GetNumberOfMoves(board); //Foreach valid move, generate a new node (evaluated in minimax) for (var i = 0; i < ValidMoves.Length; i++) { if (ValidMoves[i]) { var tempBoard = new Board(board.State); //Make the move tempBoard.AddToBoard(i, isBlack); //Create a Node var node = new Node(); node.Board = tempBoard; nodes.Add(node); } } return nodes; }
public int Search(Node node, int depth, bool maxPlayer) { if (depth == 0 || node.IsTerminalNode()) { return Utility.Score(node); } if (maxPlayer) { int bestValue = Int32.MinValue; node.Children = Utility.GenerateSuccessors(node.Board, false); for (var i = 0; i < node.Children.Count; i++) { var currentValue = Search(node.Children[i], depth - 1, false); bestValue = Math.Max(bestValue, currentValue); } return bestValue; } else { int bestValue = Int32.MaxValue; node.Children = Utility.GenerateSuccessors(node.Board, true); for (var i = 0; i < node.Children.Count; i++) { var currentValue = Search(node.Children[i], depth - 1, true); bestValue = Math.Max(bestValue, currentValue); } return bestValue; } }
//This method will search all the columns and return the best move for the AI to make. //Will run AlphaBeta or regular Minimax depending on user selection. public static int AiHelperMove(Board board, bool alphabeta, int depth) { var rows = board.State.GetLength(1) - 1; int bestValue = Int32.MinValue; int bestColumn = -1; if (alphabeta) { MiniMaxAb miniMaxAb = new MiniMaxAb(); for (var i = 0; i <= rows; i++) { var node = new Node(); var tempBoard = new Board(board.State); if (tempBoard.ValidateMove(i)) { tempBoard.AddToBoard(i, true); node.Board = tempBoard; var currentValue = miniMaxAb.Search(node, depth, Int32.MinValue, Int32.MaxValue, false); if (currentValue >= bestValue) { bestValue = currentValue; bestColumn = i; } } } } else { MiniMax miniMax = new MiniMax(); for (var i = 0; i <= rows; i++) { var node = new Node(); var tempBoard = new Board(board.State); if (tempBoard.ValidateMove(i)) { tempBoard.AddToBoard(i, true); node.Board = tempBoard; var currentValue = miniMax.Search(node, depth, false); if (currentValue >= bestValue) { bestValue = currentValue; bestColumn = i; } } } } return bestColumn; }
//Search with alpha beta pruning public int Search(Node node, int depth, int alpha, int beta, bool maxPlayer) { if (depth == 0 || node.IsTerminalNode()) { return Utility.Score(node); } if (maxPlayer) { int bestValue = Int32.MinValue; node.Children = Utility.GenerateSuccessors(node.Board, false); for (var i = 0; i < node.Children.Count; i++) { bestValue = Math.Max(bestValue, Search(node.Children[i], depth - 1, alpha, beta, false)); alpha = Math.Max(bestValue, bestValue); if (beta <= alpha) { break; } } return alpha; } else { int bestValue = Int32.MaxValue; node.Children = Utility.GenerateSuccessors(node.Board, true); for (var i = 0; i < node.Children.Count; i++) { bestValue = Math.Min(bestValue, Search(node.Children[i], depth - 1, alpha, beta, true)); beta = Math.Min(beta, bestValue); if (beta <= alpha) { break; } } return beta; } }
//Same as above, except it returns a Tuple with both Black and White scores. public static Tuple<int, int> FinalScore(Node node) { var row = node.Board.State.GetLength(0); var col = node.Board.State.GetLength(1); var BlackScore = 0; var WhiteScore = 0; node.Board.ResetVisited(); //Console.WriteLine("\n"); //Console.WriteLine("Scoring The following Board"); //node.Board.PrintState(); for (var i = 0; i < row; i++) { for (var j = 0; j < col; j++) { //Get first piece found if (node.Board.State[i, j] != null) { var piece = node.Board.State[i, j]; //Get list of Adjacent pieces var adjacentPieces = GetAdjacentPieces(piece, node.Board); //Compare piece to each adj piece, if its diag/or not, and add up score foreach (var tuple in adjacentPieces) { //If piece has been visited or is another color, skip it. if (tuple.Item2 != null && !tuple.Item2.Visited) { if (tuple.Item1) //if true, its a diagonal score { if (piece.IsBlack && tuple.Item2.IsBlack) { BlackScore++; piece.Visited = true; } else if (!piece.IsBlack && !tuple.Item2.IsBlack) { WhiteScore++; piece.Visited = true; } } else //Its a normal position worth 2 points { if (piece.IsBlack && tuple.Item2.IsBlack) { BlackScore += 2; piece.Visited = true; } else if (!piece.IsBlack && !tuple.Item2.IsBlack) { WhiteScore += 2; piece.Visited = true; } } } } } } } return Tuple.Create(WhiteScore, BlackScore); }
static void Main(string[] args) { //Following is setup for AI and board Console.WriteLine("Welcome to Simacogo! You are Player 1, Color white."); Console.WriteLine("At any time, type 'exit' to leave the game!"); Console.WriteLine("----------------------------------------------------------"); Console.WriteLine("\n"); Console.WriteLine("Enter # of plies for AI to search to? (numeric only)"); var depth = Int32.Parse(Console.ReadLine()); //AI lookahead value Console.WriteLine("\n"); Console.WriteLine("Enable Alpha Beta Pruning? (True or False)"); var alphabeta = Boolean.Parse(Console.ReadLine()); //AI lookahead value Console.WriteLine("\n"); Console.WriteLine("Enter board width? (numeric only)"); var width = Int32.Parse(Console.ReadLine()); //Board width Console.WriteLine("Enter board height? (numeric only)"); var height = Int32.Parse(Console.ReadLine()); //Board height Console.WriteLine("\n"); Board board = new Board(width, height); bool humanTurn = true; bool runProgram = true; while (!board.IsFull() && runProgram) { if (humanTurn) { Console.WriteLine("--------------------Begin Human Turn-----------------------------------"); Console.WriteLine("Please type the column you want to drop a piece into and press enter. (1-9)"); string enteredValue = Console.ReadLine(); if (enteredValue.Equals("exit")) { runProgram = false; break; } int colValue = Int32.MinValue; Int32.TryParse(enteredValue, out colValue); if (colValue > 0 && colValue < 10 && board.ValidateMove(colValue-1)) { //Get column from User && Create new piece and enter it in the board at deepest possible position in column. board.AddToBoard(colValue-1, false); board.PrintState(); var node = new Node(); node.Board = board; var score = Utility.FinalScore(node); Console.WriteLine("\n"); Console.WriteLine("Current Score: \n"); Console.WriteLine("Human: " + score.Item1); Console.WriteLine("AI " + score.Item2); Console.WriteLine("--------------------End Human Turn----------------------"); Console.WriteLine("\n"); humanTurn = !humanTurn; } else { Console.WriteLine("Invalid selection, please try again."); } } else { Console.WriteLine("--------------------Begin AI Turn--------------------"); //Let AI player pick position //Update Board //AI Runs either Miniax or MinimaxAB depending on user selection. var bestMove = Utility.AiHelperMove(board, alphabeta, depth); board.AddToBoard(bestMove, true); board.PrintState(); var node = new Node(); node.Board = board; var score = Utility.FinalScore(node); Console.WriteLine("\n"); Console.WriteLine("Current Score: \n"); Console.WriteLine("Human: " + score.Item1); Console.WriteLine("AI " + score.Item2); Console.WriteLine("\n"); Console.WriteLine("--------------------End AI Turn----------------------"); humanTurn = !humanTurn; } } Console.WriteLine("\n"); Console.WriteLine("Gameboard is Full!"); Console.WriteLine("\n"); var finalNode = new Node(); var tempBoard = new Board(board.State); finalNode.Board = tempBoard; var finalScore = Utility.FinalScore(finalNode); Console.WriteLine("Final Board"); Console.WriteLine("\n"); board.PrintState(); Console.WriteLine("\n"); Console.WriteLine("Final Score Is: "); Console.WriteLine("Human: " + finalScore.Item1); Console.WriteLine("AI: " + finalScore.Item2); Console.WriteLine("Thank you for playing Simacogo! Application will close in 5 seconds."); System.Threading.Thread.Sleep(5000); }