// should only be called by MinMaxWorker, since it initializes a bunch of stuff private SolvedMove MinMaxMove(AITask aiTask, Move move, SolvedMove lastSolvedMove) { if (lastSolvedMove != null) { lastSolvedMove.startSolvingMove = move; aiTask.onMoveComplete(lastSolvedMove); } Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); boardHash = HashBoard.ApplyMove(aiTask.board, move, boardHash); ulong currentBoardHash = boardHash; lock (stateLock) { if (alreadySolved.Contains(move)) { // if the hash has board has already been analyzed then skip boardHash = HashBoard.ApplyMove(aiTask.board, move, boardHash); return(null); } if (min > bestScore) { bestScore = min; } } aiTask.board.Move(move); var startedFromMin = min; var detectWinnerMoves = aiTask.board.GetMoves(); // check for winner var winner = aiTask.board.detectWinner(detectWinnerMoves); if (winner != Winner.NONE) { float score = 0; if ((winner == Winner.WINNER_WHITE || winner == Winner.WINNER_BLACK)) { // if a checkmate is found then no deeper moves matter since we are going to play that move score = float.MaxValue - aiTask.board.VirtualLevel; } else if (winner == Winner.DRAW) { score = 0; } lock (stateLock) { alreadySolved.Add(move); min = Math.Max(score, min); } stopWatch.Stop(); return(new SolvedMove() { startFromMin = min, min = min, max = max, durationMS = stopWatch.ElapsedMilliseconds, move = new EvaluatedMove() { move = move, score = score }, taskId = currentTask.taskId, }); } if (aiTask.tiedPositions.Contains(currentBoardHash)) { float score = 0; lock (stateLock) { alreadySolved.Add(move); min = Math.Max(score, min); } return(new SolvedMove() { startFromMin = min, min = min, max = max, durationMS = stopWatch.ElapsedMilliseconds, taskId = currentTask.taskId, move = new EvaluatedMove() { move = move, score = score, }, }); } aiTask.board.VirtualLevel++; var moveScore = minmax.MinMax(aiTask.board, aiTask.depth, aiTask.tiedPositions, false, min, max); aiTask.board.VirtualLevel--; aiTask.board.UndoMove(move); boardHash = HashBoard.ApplyMove(aiTask.board, move, boardHash); lock (stateLock) { if (alreadySolved.Contains(move)) { //Console.WriteLine($"collision! refound found move! {MoveHelper.ReadableMove(move)}"); // if the hash has board has already been analyzed before this thread managed to do it, then skip return(null); } // optimize for player if (moveScore > bestScore) { bestScore = moveScore; } min = Math.Max(moveScore, min); alreadySolved.Add(move); } // outside of stateLock because otherwise it will trigger will trigger the lock in moveSolved Thread t = Thread.CurrentThread; stopWatch.Stop(); return(new SolvedMove() { startFromMin = startedFromMin, min = min, max = max, durationMS = stopWatch.ElapsedMilliseconds, move = new EvaluatedMove() { move = move, score = moveScore }, taskId = currentTask.taskId }); }
// start by solving your own threads problems // afterwards start solving moves that belong to other that have not been started yet // if there are only started moves left then start working a move that is already beeing worked on (they might have a worse starting minimum score) internal void MinMaxWorker(AITask aiTask) { currentTask = aiTask; Board board = aiTask.board; List <Move> moves = aiTask.moves[aiTask.taskId]; int depth = aiTask.depth; Action <SolvedMove> onMoveComplete = aiTask.onMoveComplete; lock (stateLock) { max = float.MaxValue; min = float.MinValue; alreadySolved.Clear(); begunMoves.Clear(); } bestScore = float.MinValue; boardHash = HashBoard.hash(board); SolvedMove lastSolvedMove = null; // ------------ start working on this threads moves ------------------ foreach (var move in moves) { // check that the other threads have not begun the next move yet if (!alreadySolved.Contains(move) && !begunMoves.Contains(move)) { lastSolvedMove = MinMaxMove(aiTask, move, lastSolvedMove); } } // ------------ start searching for other threads unfinished work -------------- IEnumerable <Move> availableMoves = new List <Move>(); Random random = new Random(); do { availableMoves = aiTask.moves.SelectMany((threadsMoves) => { return(threadsMoves.Where(move => !alreadySolved.Contains(move) && !begunMoves.Contains(move))); }).ToList(); // use random to avoid conflicts var nextMove = availableMoves.RandomElementUsing(random); if (MoveHelper.isValidMove(nextMove)) { lastSolvedMove = MinMaxMove(aiTask, nextMove, lastSolvedMove); } } while (availableMoves.Count() > 0); // ------------ start searching for other threads begun but unfinished work -------------- do { availableMoves = aiTask.moves.SelectMany((threadsMoves) => { return(threadsMoves.Where(move => !alreadySolved.Contains(move))); }).ToList(); // use random to avoid conflicts var nextMove = availableMoves.RandomElementUsing(random); if (MoveHelper.isValidMove(nextMove)) { lastSolvedMove = MinMaxMove(aiTask, nextMove, lastSolvedMove); } } while (availableMoves.Count() > 0); if (lastSolvedMove != null) { aiTask.onMoveComplete(lastSolvedMove); } }
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()); }