bool ValidateNotation() { ValidatedMove = null; ValidatedNotation = null; MoveNotation notated; if (!MoveNotation.TryParse(txtNotation.Text, out notated)) { lblError.Text = "Unrecognized notation"; lblError.Visible = true; return(false); } var ai = new TakAI_V4(_game.Size); var moves = new List <IMove>(); Helper.EnumerateMoves(moves, _game, ai.NormalPositions); ValidatedMove = notated.MatchLegalMove(moves); if (ValidatedMove == null) { lblError.Text = "Illegal move"; lblError.Visible = true; return(false); } ValidatedNotation = notated; lblError.Visible = false; return(true); }
List <IMove> moveOptions(GameState game) { //built-in function that is used to list available moves (maxDepth is irrelivant) var ai = new TakAI_V4(game.Size, maxDepth: 0); var evaluator = ai.Evaluator; var legalMoves = new List <IMove>(); Helper.EnumerateMoves(legalMoves, game, ai.RandomPositions); return(legalMoves); }
protected override void OnLoad(EventArgs e) { base.OnLoad(e); _game = GameState.NewGame(5); _ai = new TakAI_V4(_game.Size); _evaluator = _ai.Evaluator; _boardView = new BoardView(); _boardView.Dock = DockStyle.Fill; _boardView.Game = _game; this.Controls.Add(_boardView); //_boardView.MouseOverSpotChanged += boardView_MouseOverSpotChanged; }
static void ReplayMoves(GameState game) { string movesText = @"NOT IN USE"; var ai = new TakAI_V4(game.Size); var lines = movesText.Split('\n'); foreach (var line in lines) { var notation = line.Substring(line.IndexOf(':') + 1).Trim(); var moves = new List <IMove>(); Helper.EnumerateMoves(moves, game, ai.NormalPositions); var move = moves.First(x => x.Notate() == notation); move.MakeMove(game); game.Ply++; } }
public string AB(int depth, float time) { List <IMove> moves = moveOptions(root.state); List <float> evaluations = new List <float>(); var ai = new TakAI_V4(root.state.Size, maxDepth: 0); var evaluator = ai.Evaluator; bool gameOver; int eval; Console.WriteLine(moves.Count); foreach (IMove m in moves) { GameState tempState = root.state.DeepCopy(); ABNode n = new ABNode(!root.player, tempState); TakEngine.Notation.MoveNotation notated; TakEngine.Notation.MoveNotation.TryParse(m.Notate(), out notated); var match = notated.MatchLegalMove(moves); match.MakeMove(n.state); tempState.Ply++; evaluator.Evaluate(root.state, out eval, out gameOver); if (gameOver) { return(m.Notate()); } evaluations.Add(ABSearch((new ABNode(!root.player, tempState)), 0, depth, time)); } //get argMax of list int i = 0; int arg = 0; float max = 0; foreach (float f in evaluations) { if (f > max) { max = f; arg = i; } i++; } return(moves[arg].Notate()); }
//uses above functions to perform an n-play MTCS public string MCTS(int n) { for (int i = 0; i < n; i++) { MCNode chosen = Selection(); Expand(chosen); bool result = simulate(chosen); backProp(chosen, result); } float maxWins = 0F; int winIndex = 0; float maxPlays = 0; var ai = new TakAI_V4(root.state.Size, maxDepth: 0); var evaluator = ai.Evaluator; bool gameOver; int eval; //chooses highest win rate (or winning move if available) for (int i = 0; i < root.children.Count; i++) { evaluator.Evaluate(root.children[i].state, out eval, out gameOver); if (gameOver) { return(root.legalMoves[i].Notate()); } if (root.children[i].winRate() > maxWins) { if (root.children[i].plays > maxPlays) { maxPlays = root.children[i].plays; } maxWins = root.children[i].wins; winIndex = i; } } return(root.legalMoves[winIndex].Notate()); }
//performs MCTS selection step (do not call if an available move is a win state) MCNode Selection() { var ai = new TakAI_V4(root.state.Size, maxDepth: 0); var evaluator = ai.Evaluator; //random selection for testing purposes Random r = new Random(); int index = r.Next(root.children.Count); MCNode iterator = root.children[index]; List <MCNode> iteratorChildren = iterator.children; //looks for unexpanded node while (iterator.children.Count > 0) { bool gameOver; int eval; //irrelivant, only exists to call evaluate function to look for gameover states //random int iteratorIndex = r.Next(iteratorChildren.Count); //avoid repeating pre-evaluated win states evaluator.Evaluate(iteratorChildren[iteratorIndex].state, out eval, out gameOver); if (!gameOver) { iterator = iteratorChildren[iteratorIndex]; iteratorChildren = iterator.children; } else { //avoids using the same value twice iteratorChildren.RemoveAt(iteratorIndex); } } return(iterator); }
//enumerates all available moves at a given node void Expand(MCNode leaf) { var ai = new TakAI_V4(root.state.Size, maxDepth: 4); var evaluator = ai.Evaluator; List <IMove> moves = moveOptions(leaf.state); TakEngine.Notation.MoveNotation notated; foreach (var m in moves) { GameState tempState = leaf.state.DeepCopy(); TakEngine.Notation.MoveNotation.TryParse(m.Notate(), out notated); var match = notated.MatchLegalMove(moves); match.MakeMove(tempState); tempState.Ply++; leaf.children.Add(new MCNode(!leaf.player, tempState, leaf)); leaf.legalMoves.Add(m); evaluator.Evaluate(leaf.children[leaf.children.Count - 1].state, out eval, out gameOver); if (gameOver) { backProp(leaf.children[leaf.children.Count - 1], !leaf.player); } } }
static void GameLoop() { int MCTSWins = 0; int ABWins = 0; int gameIterator = 0; Random r = new Random(); var fullauto = new bool[] { false, false }; fullauto[1] = true; var game = GameState.NewGame(5); MCTree tree = new MCTree(game); var ai = new TakAI_V4(game.Size, maxDepth: 3); var evaluator = ai.Evaluator; bool gameOver; int eval; var recentMoves = new Stack <IMove>(); var recentStates = new Stack <GameState>(); while (true) { // print board PrintBoard(game, previous: recentStates.Count > 0 ? recentStates.Peek() : null); evaluator.Evaluate(game, out eval, out gameOver); if (gameOver) { Console.Write("Game over, "); if (eval == 0) { Console.WriteLine("Tie"); } else if (eval > 0) { MCTSWins++; } else { ABWins++; } for (int i = 0; i < game.Size; i++) { gameIterator++; } //records results of game and resets board if (gameIterator < gameNum) { game = GameState.NewGame(game.Size); } } Console.Write("[T{0}, {1}]: ", game.Ply, (game.Ply & 1) == 0 ? 'X' : 'O'); string cmd; if (fullauto[game.Ply & 1] && !gameOver) { ABTree ABTree = new ABTree(game); cmd = ABTree.AB(1, 15F); /* * cmd = "ai"; * Console.WriteLine(cmd);*/ } else { //cmd = Console.ReadLine().Trim(); var legalMoves = new List <IMove>(); Helper.EnumerateMoves(legalMoves, game, ai.RandomPositions); int index = r.Next(legalMoves.Count); evaluator.Evaluate(game, out eval, out gameOver); if (!gameOver) { //check if current state exists in MCT. If so, move root node to maintain exploration records if (!tree.changeRoot(game)) { tree = new MCTree(game); } tree.evaluate(tree.root); cmd = tree.MCTS(15); /*ABTree tree = new ABTree(game); * cmd = tree.AB(1);*/ //Console.WriteLine("\nMCTree:" + cmd); } else { cmd = ""; } } if (string.IsNullOrEmpty(cmd) || cmd == "q") { break; } else if (cmd == "help") { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("==Global commands"); Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine("ai AI will choose a move for this player"); Console.WriteLine("ai on This player will become completely controlled by the AI"); Console.WriteLine("ai N Set AI difficulty to N [2-9], default is 3."); Console.WriteLine("ai off Disable AI control for all players"); Console.WriteLine("undo Undo last move, including the AI's response (if any)"); Console.WriteLine("list List all legal moves in the current board position"); Console.WriteLine(" Warning: level N+1 is roughly 30 to 50 times slower than N!"); Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("==Move notation"); Console.ForegroundColor = ConsoleColor.Gray; Console.WriteLine(" Use Portable Tak Notation (PTN)"); Console.WriteLine(" https://www.reddit.com/r/Tak/comments/3o2omm/tak_game_notation/"); Console.Write("<Press any key to continue>"); Console.ReadKey(); Console.WriteLine(); } else if (cmd == "undo") { while (recentMoves.Count > 0) { var undoing = recentMoves.Pop(); undoing.TakeBackMove(game); game.Ply--; recentStates.Pop(); if (!fullauto[game.Ply & 1]) { break; } } } else if (cmd == "ai off") { fullauto[0] = fullauto[1] = false; } else if (cmd.StartsWith("ai ") && cmd.Length > 3 && Char.IsDigit(cmd[3])) { int diff = 0; if (!int.TryParse(cmd.Substring(3), out diff)) { diff = 0; } if (diff < 2 || diff > 9) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Invalid difficulty level. Legal values are 2 thru 9."); Console.ForegroundColor = ConsoleColor.Gray; } else { if (diff == ai.MaxDepth) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("AI difficulty is already set to " + diff.ToString()); } else { Console.ForegroundColor = ConsoleColor.Cyan; ai.MaxDepth = diff; Console.WriteLine("AI difficulty set to " + diff.ToString()); Console.ForegroundColor = ConsoleColor.Gray; } } } else { if (gameOver) { Console.WriteLine("Invalid command"); } else if (cmd == "ai on" || cmd == "ai") { if (cmd == "ai on") { fullauto[game.Ply & 1] = true; } var move = ai.FindGoodMove(game); var restoreColor = Console.ForegroundColor; Console.ForegroundColor = ConsoleColor.DarkGray; Console.WriteLine("ai move => {0}", move.Notate()); Console.ForegroundColor = restoreColor; recentStates.Push(game.DeepCopy()); recentMoves.Push(move); move.MakeMove(game); game.Ply++; } else if (cmd == "list") { var legalMoves = new List <IMove>(); Helper.EnumerateMoves(legalMoves, game, ai.RandomPositions); foreach (var move in legalMoves) { Console.WriteLine(move.Notate()); } } else { TakEngine.Notation.MoveNotation notated; if (!TakEngine.Notation.MoveNotation.TryParse(cmd, out notated)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("UNRECOGNIZED MOVE NOTATION. Type 'help' for more information."); Console.ForegroundColor = ConsoleColor.Gray; continue; } var legalMoves = new List <IMove>(); Helper.EnumerateMoves(legalMoves, game, ai.RandomPositions); var match = notated.MatchLegalMove(legalMoves); if (match == null) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("ILLEGAL MOVE. Type 'help' for more information."); Console.ForegroundColor = ConsoleColor.Gray; } else { recentStates.Push(game.DeepCopy()); recentMoves.Push(match); match.MakeMove(game); game.Ply++; } } } } File.AppendAllText("Records/records.csv", MCTSWins.ToString() + "," + ABWins.ToString() + Environment.NewLine); Console.WriteLine("Exiting"); }
static void Analysis() { while (true) { GetPath: Console.Write("PTN file? "); var path = Console.ReadLine(); if (string.IsNullOrEmpty(path)) { break; } if (!System.IO.File.Exists(path)) { Console.WriteLine(); Console.WriteLine("File does not exist"); goto GetPath; } TakEngine.Notation.GameRecord gameRecord; int size; try { var database = TakEngine.Notation.TakPGN.LoadFromFile(path); if (database.Games.Count != 1) { Console.WriteLine("Database does not contain exactly 1 game"); goto GetPath; } gameRecord = database.Games[0]; size = gameRecord.BoardSize; } catch (Exception ex) { Console.WriteLine(ex.Message); goto GetPath; } var ai = new TakAI_V4(size); while (true) { Console.Write("Level [{0}]? ", ai.MaxDepth); var response = Console.ReadLine(); if (string.IsNullOrEmpty(response)) { break; } int level; if (int.TryParse(response.Trim(), out level) && level >= 2 && level < 10) { ai.MaxDepth = level; break; } Console.WriteLine("Invalid response"); } var game = GameState.NewGame(size); var legalMoves = new List <IMove>(); var evaluator = ai.Evaluator; foreach (var notated in gameRecord.MoveNotations) { int turn = (game.Ply >> 1) + 1; int player = (game.Ply & 1) + 1; string header = string.Format("#{0}, {1}.{2}: {3}", game.Ply + 1, turn, player, notated.Text); Console.Write("{0,-30}", header); legalMoves.Clear(); Helper.EnumerateMoves(legalMoves, game, ai.NormalPositions); var move = notated.MatchLegalMove(legalMoves); if (move == null) { Console.WriteLine("Illegal move?"); break; } move.MakeMove(game); game.Ply++; int unused; bool gameOver; evaluator.Evaluate(game, out unused, out gameOver); if (gameOver) { break; } ai.MaxDepth--; ai.FindGoodMove(game); var myeval = ai.LastEvaluation * -1; ai.MaxDepth++; move.TakeBackMove(game); game.Ply--; var aimove = ai.FindGoodMove(game); { int eval = myeval; if (0 != (game.Ply & 1)) { eval *= -1; } Console.WriteLine("Eval: {0}", eval); } if (Math.Abs(myeval - ai.LastEvaluation) > 100) { if (ai.LastEvaluation > 1000) { Console.WriteLine(); Console.WriteLine("Missed opportunity, AI sees certain victory:"); Console.WriteLine(" " + aimove.Notate()); Console.WriteLine(); } if (myeval < -1000) { Console.WriteLine(); Console.WriteLine("Blunder, defeat is now certain!"); Console.WriteLine(" AI suggests {0}, with expected outcome of {1}", aimove.Notate(), ai.LastEvaluation); } } else if (Math.Abs(myeval - ai.LastEvaluation) >= 20) { Console.WriteLine(); Console.WriteLine("Poor move? AI suggests {0}, with expected outcome of {1}", aimove.Notate(), ai.LastEvaluation); } move.MakeMove(game); game.Ply++; } } }
public static void FindQuickWin(string outpath) { var ai = new TakAI_V4[] { new TakAI_V4(BoardSize, maxDepth: 6, evaluator: new SimpleEvaluator(BoardSize)), new TakAI_V4(BoardSize, maxDepth: 5) }; var movelog = new List <string>(); int fastest = int.MaxValue; if (System.IO.File.Exists(outpath)) { using (var reader = System.IO.File.OpenText(outpath)) { var line = reader.ReadLine(); fastest = int.Parse(line); } } while (true) { var guid = System.Guid.NewGuid(); PrintTimeStampedMessage("Started new game"); var game = GameState.NewGame(BoardSize); movelog.Clear(); var fixedmoves = new string[] { "a1", "e1" }; foreach (var fixedmove in fixedmoves) { PrintTimeStampedMessage(string.Concat(game.Ply, ": ", fixedmove)); GameState.Play(game, fixedmove); movelog.Add(fixedmove); } bool gameOver; int eval; do { IMove move; int player = game.Ply & 1; move = ai[player].FindGoodMove(game); var notation = move.Notate(); PrintTimeStampedMessage(string.Concat(game.Ply, ": ", notation)); movelog.Add(notation); move.MakeMove(game); game.Ply++; ai[0].Evaluator.Evaluate(game, out eval, out gameOver); } while (!gameOver); string result; if (eval == 0) { result = "Tie"; } else { if (eval > 0) { result = "First player wins (W: " + ai[0].Evaluator.Name + ")"; } else { result = "Second player wins (B: " + ai[1].Evaluator.Name + ")"; } if (eval == Math.Abs(Evaluation.FlatWinEval)) { result += " via flats"; } else { result += " via road"; } } PrintTimeStampedMessage(result); if (eval > 0 && movelog.Count < fastest) { fastest = movelog.Count; using (var writer = System.IO.File.CreateText(outpath)) { writer.WriteLine(fastest); writer.WriteLine("' Game ID {0}", guid); writer.WriteLine("' Moves {0}", movelog.Count); writer.WriteLine("' Result {0}", result); for (int i = 0; i < movelog.Count; i++) { writer.WriteLine("{0}\t{1}", i + 1, movelog[i]); } } } } }
public static void RunTest(string appendPath) { int[] totalScore = new int[] { 0, 0 }; var aitype1 = new TakAI_V4(BoardSize); aitype1.MaxDepth = 6; var aitype2 = new TakAI_V4(BoardSize); aitype2.MaxDepth = 6; var evaluator = new PositionalEvaluatorV3(BoardSize); var movelog = new List <string>(); var durationlog = new List <TimeSpan>(); for (int gameCount = 0; ; gameCount++) { var guid = System.Guid.NewGuid(); PrintTimeStampedMessage("Started new game"); var game = GameState.NewGame(BoardSize); movelog.Clear(); durationlog.Clear(); bool gameOver; int eval; var starttime = DateTime.Now; ITakAI ai1, ai2; if (0 == (gameCount & 1)) { ai1 = aitype1; ai2 = aitype2; } else { ai1 = aitype2; ai2 = aitype1; } do { var movestart = DateTime.Now; IMove move; if (0 == (game.Ply & 1)) { move = ai1.FindGoodMove(game); } else { move = ai2.FindGoodMove(game); } var duration = DateTime.Now.Subtract(movestart); var notation = move.Notate(); PrintTimeStampedMessage(string.Concat(game.Ply, ": ", notation)); movelog.Add(notation); durationlog.Add(duration); move.MakeMove(game); game.Ply++; evaluator.Evaluate(game, out eval, out gameOver); } while (!gameOver); string result; if (eval == 0) { result = "Tie"; } else { if (eval > 0) { totalScore[gameCount & 1] += 1; result = "First player wins (W: " + ai1.Evaluator.Name + ")"; } else { totalScore[(gameCount + 1) & 1] += 1; result = "Second player wins (W: " + ai2.Evaluator.Name + ")"; } if (eval == Math.Abs(Evaluation.FlatWinEval)) { result += " via flats"; } else { result += " via road"; } } PrintTimeStampedMessage(result); PrintTimeStampedMessage(string.Format("{0}={1}, {2}={3}", aitype1.Evaluator.Name, totalScore[0], aitype2.Evaluator.Name, totalScore[1])); using (var writer = System.IO.File.AppendText(appendPath)) { writer.WriteLine("' Game ID {0}", guid); writer.WriteLine("' AI1 difficulty {0}", ai1.MaxDepth); writer.WriteLine("' AI2 difficulty {0}", ai2.MaxDepth); writer.WriteLine("' Started {0}", starttime.ToString(DateFormat)); writer.WriteLine("' Duration {0}", DateTime.Now.Subtract(starttime)); writer.WriteLine("' Moves {0}", movelog.Count); writer.WriteLine("' Result {0}", result); for (int i = 0; i < movelog.Count; i++) { writer.WriteLine("{0}\t{1}\t{2}", i + 1, movelog[i], durationlog[i]); } } } }
public float evaluate(MCNode node) { float positionValue = 0; int flatCount = 0; int highestCap = 0; int maxCap = 0, maxCapEnemy = 0; int losingState = 0; //Set coefficient values here for ease of parameter tuning float flatCo = 1F; float capCo = 5F; float lossCo = 1000F; //check if position is one move away from an enemy win state List <IMove> moves = moveOptions(node.state); var ai = new TakAI_V4(root.state.Size, maxDepth: 0); var evaluator = ai.Evaluator; bool gameOver; int eval; TakEngine.Notation.MoveNotation notated; GameState state = node.state.DeepCopy(); foreach (var m in moves) { TakEngine.Notation.MoveNotation.TryParse(m.Notate(), out notated); var match = notated.MatchLegalMove(moves); match.MakeMove(state); state.Ply++; evaluator.Evaluate(state, out eval, out gameOver); if (gameOver) { losingState = 1; } m.TakeBackMove(state); } //count the number of flat stones for (int i = 0; i < node.state.Size; i++) { for (int j = 0; j < node.state.Size; j++) { if (node.state.Board[i, j].Count > 0) { var piece = node.state.Board[i, j][node.state.Board[i, j].Count - 1]; if (Piece.GetStone(piece) == Piece.Stone_Flat) { if ((Piece.GetPlayerID(piece) == 1 && !node.player) || (Piece.GetPlayerID(piece) == 0 && node.player)) { flatCount++; } else { //Account for enemy flat stones, making the heuristic a differential rather than a simple count flatCount--; } } //check for highest capstone if (Piece.GetStone(piece) == Piece.Stone_Cap) { if (Piece.GetPlayerID(piece) == 1 && !node.player) { maxCapEnemy = node.state.Board[i, j].Count; } else if (Piece.GetPlayerID(piece) == 0 && node.player) { maxCap = node.state.Board[i, j].Count; } } } } } //check which (if either) player has the highest capstone if (maxCap > maxCapEnemy) { highestCap = 1; } else if (maxCap < maxCapEnemy) { highestCap = -1; } positionValue = flatCount * flatCo + highestCap * capCo + losingState * lossCo; return(positionValue); }
bool simulate(MCNode start) { int eval = 0; bool gameOver; var ai = new TakAI_V4(start.state.Size, maxDepth: 3); var evaluator = ai.Evaluator; List <IMove> moves = moveOptions(start.state); Expand(start); //check for end of game evaluator.Evaluate(start.state, out eval, out gameOver); if (gameOver) { return(!start.player); } List <float> evaluations = new List <float>(); //random play for (int i = 0; i < moves.Count; i++) { MCNode m = start.children[i]; float f = evaluate(m); //Console.WriteLine(f); if (f >= 0F) { evaluations.Add(f); } } if (evaluations.Count == 0) { return(false); } float sum = evaluations.Sum(); Random r = new Random(); double selectValue = r.NextDouble(); float selection = 0F; int moveIndex = 0; while (selection < selectValue) { //Console.WriteLine(evaluations.Count.ToString() + "," + moveIndex.ToString()); selection += evaluations[moveIndex] / sum; moveIndex++; } MCNode tempNode = new MCNode(!start.player, start.state.DeepCopy()); TakEngine.Notation.MoveNotation notated; //Console.WriteLine(moveIndex.ToString() + "," + evaluations.Count.ToString() + "," + moves.Count.ToString()); TakEngine.Notation.MoveNotation.TryParse(moves[moveIndex - 1].Notate(), out notated); var match = notated.MatchLegalMove(moves); match.MakeMove(tempNode.state); tempNode.state.Ply++; return(simulate(tempNode)); }
public float ABSearch(ABNode node, int ply, int depth, float time, float alpha = float.MinValue, float beta = float.MinValue) { if (ply > depth) { return(evaluate(node)); } List <IMove> moves = moveOptions(root.state); List <float> evaluations = new List <float>(); var ai = new TakAI_V4(root.state.Size, maxDepth: 0); var evaluator = ai.Evaluator; bool gameOver; int eval; int i = 0; time /= moves.Count; while (i < moves.Count) { IMove m = moves[i]; ABNode n = new ABNode(!node.player, node.state); TakEngine.Notation.MoveNotation notated; TakEngine.Notation.MoveNotation.TryParse(m.Notate(), out notated); var match = notated.MatchLegalMove(moves); /* match.MakeMove(n.state); * node.state.Ply++; * * evaluator.Evaluate(n.state, out eval, out gameOver); * if (gameOver) * { * if (node.player) * return 1000F; * else * return -1000F; * }*/ var task = Task.Run(() => { return(ABSearch(n, ply + 1, depth, (float)(time))); }); bool completedOnTime = task.Wait(TimeSpan.FromMilliseconds(time)); //float e = ABSearch(n, ply + 1, depth); float e = 0; if (completedOnTime) { e = task.Result; evaluations.Add(e); //alpha-beta pruning if (node.player) //max { if (e > alpha) { alpha = e; } if (e > beta) { return(e); } } if (!node.player) //min { if (e < beta) { beta = e; } if (e <= alpha) { return(e); } } } else { evaluations.Add(0); } i++; } if (node.player) { return(evaluations.Max()); } else { return(evaluations.Min()); } }