public void spawnWorkers(int?number = null) { if (number == null) { number = Environment.ProcessorCount; } for (int i = 0; i < number; i++) { var worker = new AIWorker(); Thread thread = new Thread(() => { EvalBoard.initThreadStaticVariables(); worker.WaitForTask(); }); thread.Start(); lock (stateLock) { workers.Add(worker); } } }
protected float MinMaxInteral(Board board, int depth, HashSet <ulong> tiedPositions, bool maximizing = true, float min = float.MinValue, float max = float.MaxValue) { var optimizeForColor = maximizing ? 1 : 0; var minimizeForColor = optimizeForColor ^ 1; float bestMove = maximizing ? float.MinValue : float.MaxValue; if (tiedPositions.Contains(boardHash)) { // if the position is tied due to repetition then return 0; return(0); } var moveList = layeredLists[board.VirtualLevel]; moveList.Clear(); //var moves = Board.GetMoves(board, moveList); var moves = board.GetMoves(moveList); if (board.VirtualLevel >= depth) { // if we have reached max depth assign a score. var winner = board.detectWinner(moves); if ((winner == Winner.WINNER_WHITE || winner == Winner.WINNER_BLACK)) { if (maximizing) { // if a checkmate is found then no deeper moves matter since we are going to play that move return(float.MinValue + board.VirtualLevel); } else { return(float.MaxValue - board.VirtualLevel); } } else if (winner == Winner.DRAW) { return(0); } float score = EvalBoard.evalBoard(board, moves); int isWhite = ((board.IsWhiteTurn ^ depth) & 1); if (isWhite != 1) { // if black started the query then optimize for black score *= -1; } return(score); } // because detectWinner requires checking for valid moves, which is slow only do it for end nodes // for all other cases reimplement reimplement the logic locally if (board.hasInsufficientMaterialOrTimeLimit()) { return(0); } // hasValidMove is used to track if the player has a valid move they can play, // if not this is used to declare a winner bool foundValidMove = false; foreach (MoveAndEvaluatedPosition moveAndEvluated in moves .Select(GetMoveAndEvaluatedPositionFromMove) .OrderByDescending(OrderMoveAndEvaluatedPositionFromMove)) { var move = moveAndEvluated.move; //foreach (var move in moves) { byte myTurn = board.IsWhiteTurn; //var calculatedBeforeHash = HashBoard.hash(board); //var beforeHash = boardHash; boardHash = HashBoard.ApplyMove(board, move, boardHash); board.Move(move); //var expectedHash = HashBoard.hash(board); //if (expectedHash != boardHash) { // Console.WriteLine("ARG"); // Console.WriteLine(calculatedBeforeHash); // Console.WriteLine(beforeHash); //} board.VirtualLevel++; var attacked = board.Attacked(board.GetKingPosition(myTurn), myTurn); if (attacked) { // if the king is under attack after making the move then it is not a valid move, in which case ignore the move board.VirtualLevel--; board.UndoMove(move); boardHash = HashBoard.ApplyMove(board, move, boardHash); continue; } foundValidMove = true; EvaluatedPosition existingScore = moveAndEvluated.evaluatedPosition; bool useCache = false; float moveScore = 0; int movesFromMaxDepth = depth - board.VirtualLevel; if (moveAndEvluated.hasEvaluatedMove) { // subtract the difference between the 2 depths to give previous best moves a disadvantage // because a previous move that was explored at less depth has less knowlegde than the current best if (existingScore.distanceToEndSearch - depth + existingScore.depth >= movesFromMaxDepth) { useCache = true; moveScore = existingScore.score; } } if (!useCache) { moveScore = MinMax(board, depth, tiedPositions, !maximizing, min, max); moveScores[boardHash] = new EvaluatedPosition() { distanceToEndSearch = (byte)movesFromMaxDepth, score = moveScore, depth = (byte)depth, //fen = board.simplifiedFEN, }; } board.VirtualLevel--; board.UndoMove(move); // undo previous hash boardHash = HashBoard.ApplyMove(board, move, boardHash); if (maximizing) { // optimize for player if (moveScore > bestMove) { bestMove = moveScore; } min = Math.Max(moveScore, min); if (min > max) { return(bestMove); } } else { if (moveScore < bestMove) { bestMove = moveScore; } max = Math.Min(moveScore, max); if (min > max) { return(bestMove); } } } if (!foundValidMove) { if (maximizing) { return(float.MinValue + board.VirtualLevel); // if a checkmate is found then no deeper moves matter since we are going to play that move } else { return(float.MaxValue - board.VirtualLevel); } } return(bestMove); }
public async Task <List <EvaluatedMove> > analyzeBoard(Board board, int depth, Stack <Move> history = null, Action <AIProgress> onProgress = null) { EvalBoard.initThreadStaticVariables(); HashSet <ulong> tiedBoards; if (history != null) { tiedBoards = this.findTiedBoard(board, history); } else { tiedBoards = new HashSet <ulong>(); } var minmax = new MinMaxAI(); // get shallow minMax to figure out a initial ordering, because at low depth the thread overhead is going to cost more than it gains var moves = minmax.MinMaxList(board, 2); solvedMoves = moves.Select(move => new SolvedMove() { move = move, }).ToList(); var combinedMoves = moves.ToList(); var oldMoves = new List <EvaluatedMove>(); totalFound = 0; totalMoves = (depth - 2) * moves.Count; for (int currentDepth = 3; currentDepth <= depth; currentDepth++) { moves = await delegateToWorkers(board, combinedMoves, currentDepth, tiedBoards, onProgress); combinedMoves.Clear(); for (int i = 0; i < moves.Count; i++) { // mix the last 2 move orders to try and find the best more as early as possible EvaluatedMove nextMove; if (oldMoves.Count > i) { nextMove = oldMoves[i]; if (combinedMoves.Find(existingMove => existingMove.move.Equals(nextMove.move)) == null) { combinedMoves.Add(nextMove); } } nextMove = moves[i]; if (combinedMoves.Find(existingMove => existingMove.move.Equals(nextMove.move)) == null) { combinedMoves.Add(nextMove); } } oldMoves = moves.ToList(); } return(moves); }
private async Task <List <EvaluatedMove> > delegateToWorkers(Board board, List <EvaluatedMove> moves, int depth, HashSet <ulong> tiedBoards, Action <AIProgress> onProgress = null) { EvalBoard.initThreadStaticVariables(); int workerWorkId = random.Next(); lock (stateLock) { workId = workerWorkId; } solvedMoves.Clear(); //var minmax = new MinMaxAI(); // get shallow minMax to figure out a initial ordering, which will be used to share out //var moves = minmax.MinMaxList(board, 2); //var moves = Board.GetMoves(board) // .Where(move => Board.IsLegalMove(board, move)) // .Select(move => new BestMove() { move = move, score = 10 }) // .ToList(); // create a list of moves foreach worker List <List <Move> > workerMoves = workers.Select((worker) => new List <Move>()).ToList(); // the idea here is that every worker gets the complete list of moves to try but they start at different points // the goal is to find the best move as soon as possible as possible and then share it to all other threads // therefor the moves are sorted first and then given out so the first worker // worker 1: 1,4,7,10, // worker 2: 2,5,8,11, // worker 3: 3,6,9,12, for (int i = 0; i < moves.Count; i++) { var move = moves[i]; workerMoves[i % workerMoves.Count].Add(move.move); } CancellationTokenSource cancelationSource = new CancellationTokenSource(); var cancelationtoken = cancelationSource.Token; int moveCount = moves.Count; object alreadyCompletedLock = new object(); try { var task = new Task(() => { for (int i = 0; i < workers.Count; i++) { var worker = workers[i]; var aiTask = new AITask() { taskId = i, board = board.CreateCopy(), moves = workerMoves, depth = depth, tiedPositions = tiedBoards, onMoveComplete = (solvedMove) => { int count = 0; bool isNew = false; lock (stateLock) { if (workId != workerWorkId) { // if the worker returned to late then refuse the task return; } var exists = solvedMoves.Count(move => move.move.move.Equals(solvedMove.move.move)); if (exists == 0) { isNew = true; solvedMoves.Add(solvedMove); foreach (var otherWorker in workers) { if (worker != otherWorker) { otherWorker.moveSolved(solvedMove); } } count = solvedMoves.Count; if (solvedMoves.Count >= moveCount) { cancelationSource.Cancel(); } } } if (onProgress != null && isNew) { totalFound++; onProgress(new AIProgress() { foundScore = solvedMove.move.score, total = totalMoves, progress = totalFound, move = solvedMove, depth = depth }); } } }; worker.tasks.Enqueue(aiTask); } // max wait for 3 minutes Task.Delay(1000 * 60 * 3).Wait(); cancelationSource.Cancel(); }); task.Start(); //while (!alreadyComplete) { // Task.Delay(10).GetAwaiter().GetResult(); //} //lock(alreadyCompleteLock) { // wasComplated = alreadyComplete; //} //if(!wasComplated) { task.Wait(cancelationtoken); //} } catch (OperationCanceledException) { // intentional cancel } return(solvedMoves.Select(move => move.move).OrderBy(move => move.score).Reverse().ToList()); }