public override GameState <S, M> ReadJson(JsonReader reader, Type objectType, GameState <S, M> existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { return(null); } JObject obj = JObject.Load(reader); serializer.Converters.Add(new GameStageConverter()); serializer.Converters.Add(new FutureMoveConverter <M>()); GameStage stage = obj["_gameStateStage"] .ToObject <GameStage>(serializer); S state = obj["_gameStateState"].ToObject <S>(serializer); FutureMove <M> futureMove = obj["_gameStateFutureMove"].Type == JTokenType.Null ? null : obj["_gameStateFutureMove"].ToObject <FutureMove <M> >(serializer); return(new GameState <S, M> { State = state, FutureMove = futureMove, Stage = stage }); }
// Make an actual move private void MakeAMove(FutureMove move) { // Who is making this move? PColor whoPlayed = board.Turn; // Perform move in game board, get column where move was performed int row = board.DoMove(move.shape, move.column); // If the column had space for the move... if (row >= 0) { // Get possible winner and solution Winner winner = board.CheckWinner(solution); // Update UI board with performed move, and possibly the // winner/solution view.UpdateBoard( new Move( row, move.column, new Piece(whoPlayed, move.shape)), winner, solution); // If the game is over... if (winner != Winner.None) { // Invoke MatchOver event OnMatchOver(winner); } } else // If we get here, column didn't have space for the move { throw new InvalidOperationException( $"Column {move.column + 1} is full."); } }
public override void WriteJson(JsonWriter writer, FutureMove <T> value, JsonSerializer serializer) { JObject o = (JObject)JToken.FromObject(new { }); o.Add("_futureMoveMove", JToken.FromObject(value.Move, serializer)); o.Add("_futureMoveTime", JToken.FromObject(value.DateTime.ToUniversalTime(), DateTimeJsonSerializer)); o.WriteTo(writer); }
// Displays the move performed by the specified thinker private void MovePerformed( PColor thinkerColor, string thinkerName, FutureMove move, int thinkingTime) { Console.WriteLine(thinkerColor.FormatName(thinkerName) + $" placed a {move.shape} piece at column {move.column}" + $" after {thinkingTime}ms"); }
private static void Main(string[] args) { // ////////////////////////////////////////////////////////////// // // Create an instance of our Ultron thinker via ThinkerPrototype. // // If we created directly with new, it would not be properly // // configured. // // ////////////////////////////////////////////////////////////// // // Create a configuration for a default ColorShapeLinks match MatchConfig mc = new MatchConfig(); // Get the fully qualified name of our basic Ultron thinker string ultronFullName = typeof(UltronThinker).FullName; // Create a prototype for our thinker ThinkerPrototype tp = new ThinkerPrototype(ultronFullName, "", mc); // Create an instance of our basic Ultron thinker IThinker ultronThinker = tp.Create(); // //////////////////////////////////////////////////////// // // Create a board so we can test how our thinker will play. // // //////////////////////////////////////////////////////// // // A cancellation token, will be ignored CancellationToken ct = new CancellationToken(); // Create a ColorShapeLinks board with default size Board board = new Board(); // Show initial board Console.WriteLine("\n=== Initial board ===\n"); ShowBoard(board); // Make some moves manually board.DoMove(PShape.Round, 0); // White plays round piece in col 0 board.DoMove(PShape.Square, 4); // Red plays square piece in col 4 board.DoMove(PShape.Square, 5); // White plays round piece in col 5 // Show board after our three manual moves Console.WriteLine("\n=== Board after three manual moves ===\n"); ShowBoard(board); // What move would Ultron make at this moment? FutureMove ultronMove = ultronThinker.Think(board, ct); // Show move Console.WriteLine($"-> Ultron will play {ultronMove}"); // Make the move selected by Ultron board.DoMove(ultronMove.shape, ultronMove.column); // Show board after Ultron made its move Console.WriteLine("\n=== Board after Ultron made move ===\n"); ShowBoard(board); }
// Callback method to perform a human move private void HumanMove(FutureMove move) { // Show message indicating what move the human chose view.SubmitMessage(string.Format( "{0} placed a {1} piece at column {2}", CurrPlrNameColor, move.shape.ToString().ToLower(), move.column)); // Make the move MakeAMove(move); // In the next human turn, show the human a message for him to play showHumanTurnMessage = true; }
/// <summary> /// Check enemy colums for a winning move according to shapes. /// </summary> /// <param name="board"></param> /// <param name="col"></param> /// <returns>A move that blocks enemy win.</returns> private FutureMove?CheckEnemyColsShape(Board board, int col) { FutureMove? move; List <bool> threeInLine = new List <bool>(); Piece piece; PShape enemyShape = color == PColor.White ? PShape.Square : PShape.Round; for (int i = 0; i < board.rows; i++) { if (board[i, col] == null) { return(null); } piece = (Piece)board[i, col]; if (piece.shape == enemyShape) { threeInLine.Add(true); } else { threeInLine.RemoveRange(0, threeInLine.Count); } if (threeInLine.Count == 3) { if (board[i + 1, col].HasValue || i == board.rows) { piece = (Piece)board[i + 1, col]; if (piece.shape == shape) { threeInLine.RemoveRange(0, threeInLine.Count); } } else { if (!board.IsColumnFull(col)) { return(move = new FutureMove(col, shape)); } } } } return(move = null); }
public override FutureMove <T> ReadJson(JsonReader reader, Type objectType, FutureMove <T> existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) { return(null); } JObject obj = JObject.Load(reader); T move = obj["_futureMoveMove"].ToObject <T>(serializer); DateTime dt = obj["_futureMoveTime"].ToObject <DateTime>(DateTimeJsonSerializer).ToLocalTime(); return(new FutureMove <T> { Move = move, DateTime = dt }); }
/// @copydoc IThinker.Think /// <seealso cref="IThinker.Think"/> public FutureMove Think(Board board, CancellationToken ct) { // The move to perform FutureMove move; // Find next free column where to play do { // Get next column lastCol++; if (lastCol >= board.cols) { lastCol = 0; } // Is this task to be cancelled? if (ct.IsCancellationRequested) { return(FutureMove.NoMove); } }while (board.IsColumnFull(lastCol)); // Try to use a round piece first if (board.PieceCount(board.Turn, PShape.Round) > 0) { move = new FutureMove(lastCol, PShape.Round); } // If there's no round pieces left, let's try a square pieces else if (board.PieceCount(board.Turn, PShape.Square) > 0) { move = new FutureMove(lastCol, PShape.Square); } // Otherwise return a "no move" (this should never happen) else { move = FutureMove.NoMove; } // Return move return(move); }
private List <FutureMove> getFutureMoves(Node node, String path, String pathIds) { List <FutureMove> rows = new List <FutureMove>(); StringBuilder currentPath = new StringBuilder(); StringBuilder currentPathIds = new StringBuilder(); if (path != null) { currentPath.AppendFormat("{0}", path); } else { currentPath.AppendFormat("{0}", Common.ConvertToBoardPosition(node.Square)); } if (pathIds != null) { currentPathIds.AppendFormat("{0}", pathIds); } else { currentPathIds.AppendFormat("{0}", node.Square); } foreach (var n in node.ReachableSquares) { string temp = String.Format("{0}-{1}", currentPath, Common.ConvertToBoardPosition(n.Square)); string tempPathIds = String.Format("{0}-{1}", currentPathIds, n.Square); if (n.ReachableSquares.Any()) { rows.AddRange(getFutureMoves(n, temp, tempPathIds)); } FutureMove fm = new FutureMove(); fm.Depth = n.RecurseDepth; fm.Path = temp.ToString(); fm.PathIds = tempPathIds; fm.Content = n.Content; rows.Add(fm); } return(rows); }
public FutureMove Think(Board board, CancellationToken ct) { int bestScore = int.MinValue; FutureMove finalMoveToReturn = FutureMove.NoMove; for (int i = 0; i < board.cols; i++) { //check if the column is full if (board.IsColumnFull(i)) { /* * if (i == board.cols) * { * i = 0; * //return FutureMove.NoMove; * } */ continue; } //score for this iteration int iterationScore; int roundPieceMoveScore = 0; int squarePieceMoveScore = 0; //best shape to be returned PShape bestShape; if (board.PieceCount(board.Turn, PShape.Round) != 0) { //try a move with a round piece and get score board.DoMove(PShape.Round, i); roundPieceMoveScore += -NegaMax(board, ct, _actualIteration + 1, board.Turn); board.UndoMove(); } if (board.PieceCount(board.Turn, PShape.Round) != 0) { //try a move with a square piece and get score board.DoMove(PShape.Square, i); squarePieceMoveScore = -NegaMax(board, ct, _actualIteration + 1, board.Turn); board.UndoMove(); } if (board.PieceCount(board.Turn, PShape.Round) == 0 && board.PieceCount(board.Turn, PShape.Square) == 0) { _cancelationRequested = true; } //check what piece is better if (squarePieceMoveScore > roundPieceMoveScore) { iterationScore = squarePieceMoveScore; if (board.PieceCount(board.Turn, PShape.Square) != 0) { bestShape = PShape.Square; } else { bestShape = PShape.Round; } } else { iterationScore = roundPieceMoveScore; if (board.PieceCount(board.Turn, PShape.Round) != 0) { bestShape = PShape.Round; } else { bestShape = PShape.Square; } } //Get the move with the best score if (iterationScore > bestScore) { bestScore = iterationScore; finalMoveToReturn = new FutureMove(i, bestShape); } } //check for think cancelation request if (_cancelationRequested == true) { finalMoveToReturn = FutureMove.NoMove; } //return the move return(finalMoveToReturn); }
private int NegaMax(Board board, CancellationToken ct, int iteration, PColor turn) { //score variable int bestScore = int.MinValue; //future move to be returned FutureMove finalMoveToReturn = FutureMove.NoMove; //check for cancelation request if (ct.IsCancellationRequested == true) { _cancelationRequested = true; } else { //check if we cant search more if (iteration == _iterationsLimit || board.CheckWinner() != Winner.None) { return(bestScore); } //if we can for (int i = 0; i < board.cols; i++) { //check if column is full if (board.IsColumnFull(i)) { /* * if (i == board.cols) * { * i = 0; * //return 0; * } */ // i = 0; continue; } int iterationScore; //score for each move int roundPieceMoveScore = 0; int squarePieceMoveScore = 0; //best shape to be returned PShape bestShape; //check if AI still have pieces if (board.PieceCount(board.Turn, PShape.Round) != 0) { //try a move board.DoMove(PShape.Round, i); roundPieceMoveScore = -NegaMax(board, ct, iteration + 1, turn) + GetBoardScore(board); board.UndoMove(); } //check if AI still have pieces if (board.PieceCount(board.Turn, PShape.Square) != 0) { //try a move board.DoMove(PShape.Square, i); squarePieceMoveScore = -NegaMax(board, ct, iteration + 1, turn) + GetBoardScore(board); board.UndoMove(); } if (board.PieceCount(board.Turn, PShape.Round) == 0 && board.PieceCount(board.Turn, PShape.Square) == 0) { _cancelationRequested = true; } //check witch move is better if (squarePieceMoveScore > roundPieceMoveScore) { iterationScore = squarePieceMoveScore; bestShape = PShape.Square; } else { iterationScore = roundPieceMoveScore; bestShape = PShape.Round; } //Set maxScore to largest value if (iterationScore > bestScore) { bestScore = iterationScore; finalMoveToReturn = new FutureMove(i, bestShape); } } } //return best value return(bestScore); }
// Update is called once per frame private void Update() { // Don't run update if the game's over if (matchOver) { return; } // Is the current player human? If so, let's see if we're supposed // to show him a message or if we've done so already if (matchData.CurrentThinker is HumanThinker) { if (showHumanTurnMessage) { view.SubmitMessage( $"Attention {CurrPlrNameColor}, it's your turn"); showHumanTurnMessage = false; } ; } // If the current player is an AI, let's see if we have to start // the AI thinking task, if the task is running, if it's completed, // etc. else { // If the AI task is null, we need to start an AI thinking task if (aiTask == null) { // Submit a message informing the user the AI is thinking view.SubmitMessage( $"{CurrPlrNameColor} is thinking, please wait..."); // Keep note of task start time (both system time and game // time) stopwatch.Restart(); taskStartGameTime = Time.time; // Create a new task cancellation token, so task can be // interrupted ts = new CancellationTokenSource(); // Get this AI's thinker thinker = matchData.CurrentThinker; // Start task in a separate thread using a copy of the // board aiTask = Task.Run( () => thinker.Think(board.Copy(), ts.Token)); } else // This else will run if the task is not null { // Did the task throw an exception? if (aiTask.IsFaulted) { // If so, notify user view.SubmitMessage(string.Format( "{0} exception: {1}", CurrPlrNameColor, aiTask.Exception.InnerException.Message)); // Send a cancellation token to the task in the hope it // might actually terminate ts.Cancel(); // Set task to null aiTask = null; // The AI player that throwed the exception will lose // the game, sorry OnMatchOver(board.Turn.Other().ToWinner()); } // Is the AI thinking task completed in time? else if (aiTask.IsCompleted && cancellationStopwatch == null) { // Register task duration, if we haven't done so yet if (float.IsNaN(lastTaskDuration)) { lastTaskDuration = (float)( stopwatch.ElapsedTicks / (double)(TimeSpan.TicksPerSecond)); } // Did we pass the minimum time between AI moves? if (Time.time > taskStartGameTime + matchConfig.MinMoveTimeSeconds) { // Get the move chosen by the thinker FutureMove move = aiTask.Result; // Was the thinker able to chose a move? if (move.IsNoMove) { // Thinker was not able to chose a move, // submit a message informing user of this view.SubmitMessage(string.Format( "{0} unable to perform move", CurrPlrNameColor)); // The AI player unable to move will lose // the game, sorry OnMatchOver(board.Turn.Other().ToWinner()); } else { try { // If so, submit a message informing of // the move chosen and the system time it // took the AI to think view.SubmitMessage(string.Format( "{0} placed a {1} piece at column {2} after {3}", CurrPlrNameColor, aiTask.Result.shape.ToString().ToLower(), aiTask.Result.column, $"thinking for {lastTaskDuration:f4}s")); // Player was able to make a move decision, // let's perform the actual move MakeAMove(aiTask.Result); } catch (Exception e) { // The act of making an actual move caused // an exception, which means the thinker // chose an invalid move, as such, // notify user of this view.SubmitMessage(string.Format( "{0} exception: {1}", CurrPlrNameColor, e.Message)); // The AI player that caused the exception // will lose the game, sorry OnMatchOver(board.Turn.Other().ToWinner()); } } // Set the task to null, so it can be // started again aiTask = null; // Reset the last task duration lastTaskDuration = float.NaN; } } // Is the task overdue? else if (stopwatch.Elapsed > aiTimeLimit) { // If so, check the status of the thinking cancellation // process if (cancellationStopwatch is null) { // The thinking cancellation process has not yet // been started, so let's start it // Inform user that the time limit for // the current thinker has been exceeded view.SubmitMessage( $"Time limit exceeded for {CurrPlrNameColor}!"); // Notify task it should cancel its thinking ts.Cancel(); // Start cancellation stopwatch cancellationStopwatch = Stopwatch.StartNew(); } else if (aiTask.IsCompleted) { // The thinking task is completed after the // cancelation request, terminate match normally // Set task to null aiTask = null; // Set cancellation stopwatch to null cancellationStopwatch = null; // The AI player that was overdue loses the game OnMatchOver(board.Turn.Other().ToWinner()); } else if (cancellationStopwatch.ElapsedMilliseconds > UncooperativeThinkerException.HardThinkingLimitMs) { UnityEngine.Debug.LogWarning($"{cancellationStopwatch.ElapsedMilliseconds}ms have passed :("); // If the hard thinking process time limit has been // reached, throw an exception to terminate the app throw new UncooperativeThinkerException(thinker); } } } } }
private static void Main(string[] args) { // ////////////////////////////////////////////////////////////// // // Create an instance of our Oizys thinker via ThinkerPrototype. // // If we created directly with new, it would not be properly // // configured. // // ////////////////////////////////////////////////////////////// // // Create a configuration for a default ColorShapeLinks match MatchConfig mc = new MatchConfig(); // Get the fully qualified name of our basic Oizys thinker string oizysFullName = typeof(OizysThinker).FullName; // Create a prototype for our thinker ThinkerPrototype tp = new ThinkerPrototype(oizysFullName, "", mc); // Create an instance of our basic Oizys thinker IThinker oizysThinker = tp.Create(); // //////////////////////////////////////////////////////// // // Create a board so we can test how our thinker will play. // // //////////////////////////////////////////////////////// // // A cancellation token, will be ignored CancellationToken ct = new CancellationToken(); // Create a ColorShapeLinks board with default size Board board = new Board(); // Show initial board Console.WriteLine("\n=== Initial board ===\n"); ShowBoard(board); // Make some moves manually board.DoMove(PShape.Round, 3); // White plays (Round,col 3) board.DoMove(PShape.Square, 3); // Red plays (Square,col 3) board.DoMove(PShape.Round, 2); // White plays (Round,col 2) // board.DoMove(PShape.Square, 3); // Red plays (Square,col 3) // board.DoMove(PShape.Round, 1); // White plays (Round,col 1) // board.DoMove(PShape.Square, 3); // Red plays (Square,col 3) // board.DoMove(PShape.Round, 3); // White plays (Round,col 3) // Show board after our three manual moves Console.WriteLine("\n=== Board after three manual moves ===\n"); ShowBoard(board); // Starts timer DateTime startTime = DateTime.Now; // What move would Oizys make at this moment? FutureMove oizysMove = oizysThinker.Think(board, ct); // Show move and time Console.WriteLine(string.Format( "-> Oizys will play {0} after {1} ms.", oizysMove, (DateTime.Now - startTime).TotalMilliseconds)); // Make the move selected by Oizys board.DoMove(oizysMove.shape, oizysMove.column); // Show board after Oizys made its move Console.WriteLine("\n=== Board after Oizys made move ===\n"); ShowBoard(board); }
/// @copydoc ColorShapeLinks.Common.AI.IThinker.Think /// <remarks> /// This method asks the human to play. /// </remarks> /// <seealso cref="ColorShapeLinks.Common.AI.IThinker.Think"/> public override FutureMove Think(Board board, CancellationToken ct) { // By default, no move is performed in case of timeout FutureMove move = FutureMove.NoMove; // No thinking notification has taken place DateTime lastNotificationTime = DateTime.MinValue; // Set thinking notification interval between frames to 20ms TimeSpan notificationInterval = TimeSpan.FromMilliseconds(20); // Calculate the time limit for the human to play DateTime timeLimit = DateTime.Now + TimeSpan.FromMilliseconds(TimeLimitMillis); // Show some info on what keys are used for input OnThinkingInfo("T to toggle piece, < > to change selected column"); // If a certain type of piece is not available, make the other // type the selected one if (board.PieceCount(board.Turn, PShape.Round) == 0) { selectedShape = PShape.Square; } if (board.PieceCount(board.Turn, PShape.Square) == 0) { selectedShape = PShape.Round; } // If the current column is full, iterate column selection until // one is not while (board.IsColumnFull(selectedCol)) { selectedCol++; if (selectedCol >= Cols) { selectedCol = 0; } } // Wait for human input at most until the cancellation token is // activated while (true) { // Was a key pressed? if (Console.KeyAvailable) { // Retrieve key ConsoleKey key = Console.ReadKey().Key; // Check if it was a column increment key if (key == ConsoleKey.RightArrow || key == ConsoleKey.D || key == ConsoleKey.NumPad6 || key == ConsoleKey.D6) { // Increment column... do { // ...making sure a full column is not selectable, // and wraping around selectedCol++; if (selectedCol >= Cols) { selectedCol = 0; } } while (board.IsColumnFull(selectedCol)); } // Check if it was a column decrement key else if (key == ConsoleKey.LeftArrow || key == ConsoleKey.A || key == ConsoleKey.NumPad4 || key == ConsoleKey.D4) { // Decrement column... do { // ...making sure a full column is not selectable, // and wraping around selectedCol--; if (selectedCol < 0) { selectedCol = Cols - 1; } } while (board.IsColumnFull(selectedCol)); } // Check if it was a piece toggle key else if (key == ConsoleKey.T) { // Toggle piece if the other type of piece is available if (selectedShape == PShape.Round && board.PieceCount(board.Turn, PShape.Square) > 0) { selectedShape = PShape.Square; } else if (selectedShape == PShape.Square && board.PieceCount(board.Turn, PShape.Round) > 0) { selectedShape = PShape.Round; } } // Check if it was a piece drop key else if (key == ConsoleKey.Enter) { // Drop piece and get out of the input loop move = new FutureMove(selectedCol, selectedShape); break; } } // If cancellation token is activated, terminate input loop if (ct.IsCancellationRequested) { break; } // Is it time for another thinking notification? if (DateTime.Now > lastNotificationTime + notificationInterval) { // Show dialog Console.CursorLeft = 0; Console.Write(String.Format( "Col [{0,4}] | Shape [{1,7}] | Time [{2,14}]", selectedCol, selectedShape, timeLimit - DateTime.Now)); Console.CursorLeft = 0; // Update last notification time lastNotificationTime = DateTime.Now; } } // Return chosen move return(move); }
/// Current thinker makes its move private Winner Play() { // Get a reference to the current thinker IThinker thinker = matchData.CurrentThinker; // Determine the color of the current thinker PColor color = board.Turn; // Match result so far Winner winner = Winner.None; // Thinking start time DateTime startTime = DateTime.Now; // Real think time in milliseconds int thinkTimeMillis; // Apparent thinking time left int timeLeftMillis; // Task to execute the thinker in a separate thread Task <FutureMove> thinkTask; // Notify listeners that next turn is about to start NextTurn?.Invoke(color, thinker.ToString()); // Ask thinker to think about its next move thinkTask = Task.Run( () => thinker.Think(board.Copy(), ts.Token)); // The thinking process might throw an exception, so we wrap // task waiting in a try/catch block try { // Wait for thinker to think... until the allowed time limit if (thinkTask.Wait(timeLimitMillis)) { // Thinker successfully made a move within the time limit // Get the move selected by the thinker FutureMove move = thinkTask.Result; // Was the thinker able to chose a move? if (move.IsNoMove) { // Thinker was not able to chose a move // Raise an invalid play event and set the other // thinker as the winner of the match winner = OnInvalidPlay( color, thinker, "Thinker unable to perform move"); } else { // Thinker was able to chose a move // Perform move in game board, get column where move // was performed int row = board.DoMove(move.shape, move.column); // If the column had space for the move... if (row >= 0) { // Obtain thinking end time thinkTimeMillis = (int)(DateTime.Now - startTime) .TotalMilliseconds; // How much time left for the minimum apparent move // time? timeLeftMillis = minMoveTimeMillis - thinkTimeMillis; // Was the minimum apparent move time reached if (timeLeftMillis > 0) { // If not, wait until it is reached Thread.Sleep(timeLeftMillis); } // Notify listeners of the move performed MovePerformed?.Invoke( color, thinker.ToString(), move, thinkTimeMillis); // Get possible winner and solution winner = board.CheckWinner(solution); } else { // If we get here, column didn't have space for the // move, which means that thinker made an invalid // move and should lose the game // Raise an invalid play event and set the other // thinker as the winner of the match winner = OnInvalidPlay( color, thinker, "Tried to place piece in column " + $"{move.column}, which is full"); } } } else // Did the time limit expired? { // Notify thinker to voluntarily stop thinking ts.Cancel(); // Raise an invalid play event and set the other thinker // as the winner of the match winner = OnInvalidPlay( color, thinker, "Time limit expired"); } } catch (Exception e) { // Is this an inner exception? if (e.InnerException != null) { // If so, use it for error message purposes e = e.InnerException; } // Raise an invalid play event and set the other thinker as // the winner of the match winner = OnInvalidPlay( color, thinker, $"Thinker exception: '{e.Message}'"); } // Notify listeners that the board was updated BoardUpdate?.Invoke(board); // Return winner return(winner); }
public FutureMove Negamax(Board board, PColor turn, int depth, CancellationToken ct) { FutureMove bestMove = default; PColor proxTurn = turn == PColor.White ? PColor.Red : PColor.White; if (ct.IsCancellationRequested) { return(FutureMove.NoMove); } else { if (myDepth == depth) { return(bestMove); } myDepth++; for (int i = 0; i < board.cols; i++) { for (int j = 0; j < board.rows; j++) { Vector2Int pos = new Vector2Int(i, j); if (board[i, j] == null) { int roundPieces = board.PieceCount(board.Turn, PShape.Round); int squarePieces = board.PieceCount(board.Turn, PShape.Square); if (shape == PShape.Round) { if (roundPieces == 0) { shape = PShape.Square; } else if (squarePieces == 0) { shape = PShape.Round; } } FutureMove move = default; board.DoMove(shape, i); if (board.CheckWinner() == Winner.None) { move = Negamax(board, proxTurn, depth, ct); } board.UndoMove(); } } } /*if (board.Turn == PColor.Red) * bestMove = new FutureMove(board.cols - 1, PShape.Round); * else * bestMove = new FutureMove(0, PShape.Round);*/ } return(bestMove); }