/// <summary> /// Recreates the current gamestate in SpaceDataHandlers /// </summary> /// <param name="player"> Determines who's view: Player's or AI's.</param> /// <returns>A reenactment of the current gameboard.</returns> public GameStateReader CurrentGameState(bool player) { var res = new GameStateReader(); var gameBoard = GameManager.instance.gameBoard; for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { var foo = gameBoard[i, j]; if (foo.Occupied) { bool friendly = player ? foo.Friendly : !foo.Friendly; res.gameBoard[i, j] = new SpaceDataHandler(foo.OccupiedPiece.unitType, friendly); } else { res.gameBoard[i, j] = new SpaceDataHandler(); } } } res.miniMax = this; res.zobrist = zobrist; res.zobristHash = zobrist.GetZobristHash(res.gameBoard, true); return(res); }
/// <summary> /// Quickly evaluates a move for ordering. /// </summary> /// <param name="gameState"> The move to evaluate. </param> /// <param name="move"> Move being made. </param> /// <param name="depth"> The depth of the search. </param> /// <returns> An evaluation. </returns> private float QuickEval(GameStateReader gameState, Vector2Int[] move, int ply) { var oldPos = move[0]; var newPos = move[1]; var enemy = gameState.gameBoard[newPos.x, newPos.y].unitType; if (enemy != UnitType.None) { var res = GamePiece.CaptureScore(gameState.gameBoard[oldPos.x, oldPos.y].unitType, enemy); switch (res) { case float value when value > 0: return(res + 1100000); case 0: return(res + 1000000); default: return(res); } } //A unit was captured... else if (killerMoves[ply, 0] == move) { return(900000); } else if (killerMoves[ply, 1] == move) { return(800000); } // Killer moves return(historyMoves[oldPos.x, oldPos.y, newPos.x, newPos.y]); // Sorts units by history. }
/// <summary> /// Handles Move Ordering. /// </summary> /// <param name="list"> Moves to order. </param> /// <param name="depth"> Depth of search. </param> /// <returns> Sorted List. </returns> public List <Vector2Int[]> OrderMoves(List <Vector2Int[]> moves, GameStateReader gameState, int depth) { var pvMove = transpositionTable.ContainsKey(gameState.zobristHash) ? transpositionTable[gameState.zobristHash].pvMove : null; var ply = this.depth - depth; var score = new Dictionary <Vector2Int[], float>(moves.ToDictionary(x => x, x => x == pvMove ? 2000000 : QuickEval(gameState, x, ply))); return(score.OrderBy(x => x.Value).Reverse().ToDictionary(x => x.Key, x => x.Value).Keys.ToList()); }
/// <summary> /// Use iterative deepening for a depth first search. /// </summary> /// <param name="currentGameState"> Current gamestate to search.</param> /// <returns> The best move. </returns> private Vector2Int[] IterativeDeepening(GameStateReader currentGameState) { var timer = new System.Diagnostics.Stopwatch(); timer.Start(); depth = 1; var bestMove = RootNegaMax(currentGameState, depth); bool OutOfTime() => timer.ElapsedMilliseconds >= timeLimit; for (depth = 2; depth <= maxDepth && !OutOfTime(); depth++) { bestMove = RootNegaMax(currentGameState, depth); } timer.Stop(); ClearTable(); return(bestMove); }
/// <summary> /// Evaluates the current boardstate, /// </summary> /// <param name="gameState">The boardstate to evaluate.</param> /// <returns> inf or -inf if the game would be over.</returns> private float EvaluateBoardState(GameStateReader gameState) { var friendlyUnits = new Dictionary <Vector2Int, UnitType>(); var enemyUnits = new Dictionary <Vector2Int, UnitType>(); for (int i = 0; i < 8; i++) { for (int j = 0; j < 8; j++) { var foo = gameState.gameBoard[i, j]; if (foo.Friendly) { friendlyUnits.Add(new Vector2Int(i, j), foo.unitType); } else if (foo.Enemy) { enemyUnits.Add(new Vector2Int(i, j), foo.unitType); } } } if (!friendlyUnits.ContainsValue(UnitType.General)) { return(float.NegativeInfinity); } //If this move would result in opponent winning, return neg infinity. if (!enemyUnits.ContainsValue(UnitType.General)) { return(float.PositiveInfinity); } //If this move would result in victory, give it highest priority. float res = 0.0f; foreach (var foo in friendlyUnits) { res += GamePiece.PieceValue(foo.Key, foo.Value); } foreach (var foo in enemyUnits) { res -= GamePiece.PieceValue(foo.Key, foo.Value); } return(res); }
/// <summary> /// Quiescence Search to avoid Horizon Effect. /// </summary> /// <param name="gameState"> Gamestate to evaluate. </param> /// <param name="depth"> Depth to search. </param> /// <param name="maximizingPlayer"> Player to move. </param> /// <param name="alpha"> Alpha value. </param> /// <param name="beta"> Beta value. </param> /// <returns> Quiesce Eval. </returns> private float Quiesce(GameStateReader gameState, int depth, bool maximizingPlayer, float alpha, float beta) { float stand_pat = EvaluateBoardState(gameState) * (maximizingPlayer ? -1 : 1); if (depth == 0 || gameState.Quiet() || gameState.GameOver) { return(stand_pat); } if (stand_pat >= beta) { return(beta); } if (alpha < stand_pat) { alpha = stand_pat; } var captureMoves = gameState.CaptureMoves(maximizingPlayer); captureMoves = OrderMoves(captureMoves, gameState, depth); foreach (var child in captureMoves) { gameState.MakeMove(child); var score = -Quiesce(gameState, depth - 1, !maximizingPlayer, -beta, -alpha); gameState.UndoMove(); if (score >= beta) { return(beta); } if (score > alpha) { alpha = score; } } return(alpha); }
/// <summary> /// Using NegaMax with Alpha-Beta Pruning and Transposition Table to determine the best possible move. /// </summary> /// <param name="gameState"> Current GameState. </param> /// <param name="depth"> How many layers of branches to foresee. </param> /// <param name="alpha"> Maximum to prune out. </param> /// <param name="beta"> Minimum to prune out. </param> /// <param name="maximizingPlayer"> Current Player. True = caller. </param> /// <returns> The evaluated score. </returns> private float NegaMax(GameStateReader gameState, int depth, float alpha = float.NegativeInfinity, float beta = float.PositiveInfinity, bool maximizingPlayer = true /*, bool allowNullMove = true*/) { #region Transposition Table retrieval var origAlpha = alpha; long zobristHash = gameState.zobristHash; if (transpositionTable.ContainsKey(zobristHash)) { var ttEntry = transpositionTable[zobristHash]; var value = ttEntry.value; if (!maximizingPlayer) { value *= -1; } if (ttEntry.depth >= depth) { switch (ttEntry.flag) { case TranspositionTableEntry.Flag.EXACT: return(value); case TranspositionTableEntry.Flag.UPPERBOUND: alpha = Mathf.Max(alpha, value); break; case TranspositionTableEntry.Flag.LOWERBOUND: beta = Mathf.Min(beta, value); break; } if (alpha >= beta) { return(value); } } } #endregion #region End Node if (gameState.GameOver) { return(EvaluateBoardState(gameState) * (maximizingPlayer ? 1 : -1)); } if (depth == 0) { if (gameState.Quiet()) { return(EvaluateBoardState(gameState) * (maximizingPlayer ? 1 : -1)); } else { return(Quiesce(gameState, quiesceDepth, !maximizingPlayer, alpha, beta)); } } #endregion #region Search Tree var bestEval = float.NegativeInfinity; var bestMove = new Vector2Int[2]; var orderedMoves = gameState.PossibleMovesQuick(maximizingPlayer); orderedMoves = OrderMoves(orderedMoves, gameState, depth); foreach (var child in orderedMoves) { gameState.MakeMove(child); var eval = -NegaMax(gameState, depth - 1, -beta, -alpha, !maximizingPlayer); gameState.UndoMove(); if (eval > bestEval) { bestEval = eval; bestMove = child; alpha = Mathf.Max(alpha, bestEval); if (alpha >= beta) { if (gameState.IsntCapture(child[1])) { int ply = this.depth - depth; killerMoves[ply, 1] = killerMoves[ply, 0]; killerMoves[ply, 0] = child; historyMoves[child[0].x, child[0].y, child[1].x, child[1].y] += depth * depth; } break; } } } #endregion #region Add to Transposition Table. var newEntry = new TranspositionTableEntry { depth = depth, value = bestEval * (maximizingPlayer ? 1 : -1), pvMove = bestMove }; switch (bestEval) { case float value when value <= origAlpha: newEntry.flag = TranspositionTableEntry.Flag.UPPERBOUND; break; case float value when value >= beta: newEntry.flag = TranspositionTableEntry.Flag.LOWERBOUND; break; default: newEntry.flag = TranspositionTableEntry.Flag.EXACT; break; } transpositionTable[zobristHash] = newEntry; #endregion #region Return result return(bestEval); #endregion }
/// <summary> /// Using (Root) NegaMax with Alpha-Beta Pruning and Transposition Table to determine the best possible move. /// </summary> /// <param name="gameState"> Current GameState. </param> /// <param name="depth"> How many layers of branches to foresee. </param> /// <param name="alpha"> Maximum to prune out. </param> /// <param name="beta"> Minimum to prune out. </param> /// <param name="maximizingPlayer"> Current Player. True = caller. </param> /// <returns> The best move. </returns> private Vector2Int[] RootNegaMax(GameStateReader gameState, int depth, float alpha = float.NegativeInfinity, float beta = float.PositiveInfinity, bool maximizingPlayer = true /*, bool allowNullMove = true*/) { #region Inititalize Variables var origAlpha = alpha; long zobristHash = gameState.zobristHash; var bestEval = float.NegativeInfinity; var orderedMoves = gameState.PossibleMovesQuick(maximizingPlayer); orderedMoves = OrderMoves(orderedMoves, gameState, depth); var bestMove = orderedMoves[0]; #endregion #region Search Tree foreach (var child in orderedMoves) { gameState.MakeMove(child); var eval = -NegaMax(gameState, depth - 1, -beta, -alpha, !maximizingPlayer); gameState.UndoMove(); if (eval > bestEval) { bestEval = eval; bestMove = child; alpha = Mathf.Max(alpha, bestEval); if (alpha >= beta) { if (gameState.IsntCapture(child[1])) { int ply = this.depth - depth; killerMoves[ply, 1] = killerMoves[ply, 0]; killerMoves[ply, 0] = child; historyMoves[child[0].x, child[0].y, child[1].x, child[1].y] += depth * depth; } break; } } } #endregion #region Add to Transposition Table. var newEntry = new TranspositionTableEntry { depth = depth, value = bestEval, pvMove = bestMove }; switch (bestEval) { case float value when value <= origAlpha: newEntry.flag = TranspositionTableEntry.Flag.UPPERBOUND; break; case float value when value >= beta: newEntry.flag = TranspositionTableEntry.Flag.LOWERBOUND; break; default: newEntry.flag = TranspositionTableEntry.Flag.EXACT; break; } transpositionTable[zobristHash] = newEntry; #endregion #region Return result return(bestMove); #endregion }