/// <summary> /// Returns a SabberStoneAction for the current state. /// </summary> /// <param name="state">The current game state.</param> /// <returns>SabberStoneAction</returns> public SabberStoneAction Act(SabberStoneState state) { // Check to make sure the player to act in the game-state matches our player. if (state.CurrentPlayer() != Player.Id) { return(null); } SabberStoneAction selectedAction; // When we have to act, check which policy we are going to use switch (Selection) { case SelectionType.UCB: selectedAction = SelectUCB(state); break; case SelectionType.EGreedy: selectedAction = SelectEGreedy(state); break; case SelectionType.Random: selectedAction = RandomPlayoutBot.CreateRandomAction(state); break; default: throw new InvalidEnumArgumentException($"SelectionType `{Selection}' is not supported."); } // Remember the action that was selected. ActionsTaken.Add(selectedAction); return(selectedAction); }
public double EvaluateStateTransition(SabberStoneState before, SabberStoneState after) { // These min and max values have been empirically determined and should be checked and re-set when it matters that they are correct. if (after.Game.CurrentOpponent.Hero.Health <= 0) { return(50); } if (after.Game.CurrentPlayer.Hero.Health <= 0) { return(-50); } double enemyPoints = calculateScoreHero(before.Game.CurrentOpponent, after.Game.CurrentOpponent); double myPoints = calculateScoreHero(before.Game.CurrentPlayer, after.Game.CurrentPlayer); double scoreEnemyMinions = calculateScoreMinions(before.Game.CurrentOpponent.BoardZone, after.Game.CurrentOpponent.BoardZone); double scoreMyMinions = calculateScoreMinions(before.Game.CurrentPlayer.BoardZone, after.Game.CurrentPlayer.BoardZone); int usedMana = before.Game.CurrentPlayer.RemainingMana - after.Game.CurrentPlayer.RemainingMana; double scoreManaUsed = usedMana * weights[MANA_REDUCED]; return(enemyPoints - myPoints + scoreEnemyMinions - scoreMyMinions - scoreManaUsed); }
/// <summary> /// Completes an incomplete move caused by Hierarchical Expansion. /// </summary> /// <param name="state">The game state to create an action for.</param> /// <param name="action">The currently created action.</param> private void CompleteHEMove(SabberStoneState state, SabberStoneAction action) { // Copy state so that we can process the tasks and get an updated options list. var copyState = (SabberStoneState)state.Copy(); // Process the currently selected tasks foreach (var task in action.Tasks) { try { copyState.Game.Process(task.Task); } catch (Exception e) { Console.WriteLine($"ERROR: {e.GetType()} thrown while trying to process a task."); break; } } // Ask the Searcher to determine the best tasks to complete the action var completingAction = Searcher.DetermineBestTasks(copyState); // Add the tasks to the provided action foreach (var task in completingAction.Tasks) { action.AddTask(task); } // If the move is not complete yet (for example, when the game is over), add EndTurn if (!action.IsComplete()) { action.AddTask((SabberStonePlayerTask)EndTurnTask.Any(Player)); } }
/// <summary> /// Creates a SabberStoneAction by randomly selecting one of the available PlayerTasks until the End_Turn task is selected. /// </summary> /// <param name="state">The game state for which an action should be created. Note: </param> /// <returns>SabberStoneAction</returns> public SabberStoneAction CreateRandomAction(SabberStoneState state) { // Clone game so that we can process the selected tasks and get an updated options list. var clonedGame = state.Game.Clone(); var playerID = clonedGame.CurrentPlayer.Id; // Create an action to store the selected tasks. var action = new SabberStoneAction(); // Keep adding random tasks until the player's turn is over, or the game has ended while (clonedGame.CurrentPlayer.Id == playerID && clonedGame.State != State.COMPLETE) { // Check if an duplicate positions need to be filtered out var availableOptions = clonedGame.CurrentPlayer.Options(); if (FilterDuplicatePositionTasks) { availableOptions = availableOptions.Where(i => i.ZonePosition <= 0).ToList(); } // Select a random available task var selectedTask = availableOptions.RandomElementOrDefault(); // Add the task to the action. action.AddTask((SabberStonePlayerTask)selectedTask); // Process the task on the cloned game state. clonedGame.Process(selectedTask); } // Check if the action is complete, if not, add EndTurn if (!action.IsComplete()) { action.AddTask((SabberStonePlayerTask)EndTurnTask.Any(clonedGame.CurrentPlayer)); } return(action); }
/// <summary> /// Plays out a player's turn. /// Note: this method asks the playout bot of the current player to Act and processes the returned action. /// </summary> /// <param name="game">The current game state.</param> private void PlayPlayerTurn(SabberStoneState game) { // Select the correct playoutBot to use. IPlayoutBot turnBot; if (Bots.ContainsKey(game.CurrentPlayer())) { turnBot = Bots[game.CurrentPlayer()]; } else { // Fall back to the default playout bot if the current player does not have a playout bot specified. turnBot = Bots[DEFAULT_PLAYOUT_BOT_ID]; turnBot.SetController(game.Game.CurrentPlayer); } // Ask the bot to act. var action = turnBot.Act(game); // Process each task. foreach (var item in action.Tasks) { game.Game.Process(item.Task); } }
public SabberStoneAction Act(SabberStoneState state) { var timer = System.Diagnostics.Stopwatch.StartNew(); var gameState = (SabberStoneState)state.Copy(); if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine(Name()); } if (_debug) { Console.WriteLine($"Starting a Heuristic search in turn {(gameState.Game.Turn + 1) / 2}"); } var solution = new SabberStoneAction(); while (gameState.CurrentPlayer() == PlayerID() && gameState.Game.State != State.COMPLETE) { var poGame = new POGame(gameState.Game, _debug); var task = GetMove(poGame); solution.AddTask((SabberStonePlayerTask)task); gameState.Game.Process(task); } var time = timer.ElapsedMilliseconds; if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine($"Heuristic returned with solution: {solution}"); } if (_debug) { Console.WriteLine($"My total calculation time was: {time} ms."); } // Check if the solution is a complete action. if (!solution.IsComplete()) { // Otherwise add an End-Turn task before returning. if (_debug) { Console.WriteLine("Solution was an incomplete action; adding End-Turn task."); } solution.Tasks.Add((SabberStonePlayerTask)EndTurnTask.Any(Player)); } if (_debug) { Console.WriteLine(); } return(solution); }
/// <inheritdoc /> public SabberStoneAction Act(SabberStoneState state) { var timer = System.Diagnostics.Stopwatch.StartNew(); var gameState = (SabberStoneState)state.Copy(); if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine(Name()); } if (_debug) { Console.WriteLine($"Starting a NMCTS search in turn {(gameState.Game.Turn + 1) / 2}"); } // Setup and start the ensemble-search EnsembleSolutions = new List <SabberStoneAction>(); var search = (NMCTS <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction>)Builder.Build(); var context = SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> .GameSearchSetup(GameLogic, EnsembleSolutions, gameState, null, search); Ensemble.EnsembleSearch(context, Searcher.Search, EnsembleSize); IterationsSpent = EnsembleSolutions.Sum(i => i.BudgetUsed); // Determine the best tasks to play based on the ensemble search, or just take the one in case of a single search. var solution = EnsembleSize > 1 ? Searcher.VoteForSolution(EnsembleSolutions, state) : EnsembleSolutions.First(); if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine($"NMCTS returned with solution: {solution}"); } if (_debug) { Console.WriteLine($"My total calculation time was: {timer.ElapsedMilliseconds} ms."); } // Check if the solution is a complete action. if (!solution.IsComplete()) { // Otherwise add an End-Turn task before returning. if (_debug) { Console.WriteLine("Solution was an incomplete action; adding End-Turn task."); } solution.Tasks.Add((SabberStonePlayerTask)EndTurnTask.Any(Player)); } if (_debug) { Console.WriteLine(); } return(solution); }
/// <summary> /// Finalise these statistics using the completed game's state and write the information to a file. /// </summary> /// <param name="state">The state of the completed game.</param> public void Finalise(SabberStoneState state) { Player1HP = state.Player1.Hero.Health; Player2HP = state.Player2.Hero.Health; FinalTurn = state.Game.Turn; Status = state.Game.State; WriteToFile(); }
/// <summary> /// Selects an action using the e-greedy algorithm. /// </summary> /// <param name="state">The game state.</param> /// <returns><see cref="SabberStoneAction"/>.</returns> private SabberStoneAction SelectEGreedy(SabberStoneState state) { // Determine whether or not to be greedy (chance is 1-e to use best action) if (Util.RNG.NextDouble() < EGreedyThreshold) { // Explore a random action return(RandomPlayoutBot.CreateRandomAction(state)); } var action = new SabberStoneAction(); var stateClone = state.Game.Clone(); // Repeatedly exploit the highest (average) reward task that is available in this state do { SabberStonePlayerTask selectedTask; // Get the stats of the tasks currently available in this state var availableTasks = stateClone.Game.CurrentPlayer.Options().Where(i => i.ZonePosition <= 0).Select(i => (SabberStonePlayerTask)i).ToList(); var availableTaskHashes = availableTasks.Select(i => i.GetHashCode()).ToList(); var availableStatistics = MASTTable.Where(i => availableTaskHashes.Contains(i.Key)).ToList(); // Find the task with the highest average value var bestTask = availableStatistics.OrderByDescending(i => i.Value.AverageValue()).FirstOrDefault(); // If no best task was found, randomly choose an available task if (bestTask.IsDefault()) { var randomTask = availableTasks.RandomElementOrDefault(); // If we also can't randomly find a task, stop if (randomTask == null) { break; } selectedTask = randomTask; } else { // Find all available tasks that have an average value similar to the best var bestValue = bestTask.Value.AverageValue(); var compTasks = availableStatistics.Where(i => Math.Abs(i.Value.AverageValue() - bestValue) < AVThesis.Constants.DOUBLE_EQUALITY_TOLERANCE).ToList(); // Select one of the tasks selectedTask = compTasks.RandomElementOrDefault().Value.Task; } // Add the task to the action we are building action.AddTask(selectedTask); // Process the task stateClone.Process(selectedTask.Task); // Continue until we have created a complete action, or the game has completed } while (!action.IsComplete() && stateClone.Game.State != State.COMPLETE); // Return the action we've created return(action); }
/// <summary> /// Plays out a player's turn. /// Note: this method continuously asks the bot to Act and stops when 'null' is returned. /// </summary> /// <param name="game">The current game state.</param> /// <param name="bot">The bot that should play the turn.</param> private void PlayPlayerTurn(SabberStoneState game, ISabberStoneBot bot) { var currentPlayerName = game.Game.CurrentPlayer.Name; if (_printToConsole) { Console.WriteLine($"- <{currentPlayerName}> ---------------------------"); } var timer = Stopwatch.StartNew(); // Ask the bot to act. var action = bot.Act(game); timer.Stop(); // In the case where an incomplete action was returned, add end-turn if (!action.IsComplete()) { Console.WriteLine("WARNING: Incomplete action received. Adding EndTurn."); action.AddTask((SabberStonePlayerTask)EndTurnTask.Any(game.Game.CurrentPlayer)); } // Process the tasks in the action var executedTasks = new List <SabberStonePlayerTask>(); foreach (var item in action.Tasks) { if (_printToConsole) { Console.WriteLine(item.Task.FullPrint()); } try { // Process the task game.Game.Process(item.Task); executedTasks.Add(item); } catch (Exception e) { Console.WriteLine($"ERROR: Exception thrown while processing a task for {game.Game.CurrentPlayer.Name} in turn {game.Game.Turn}."); WriteExceptionToFile(e); // If the game is still running and the current player is still active, pass the turn if (game.Game.CurrentPlayer.Id == bot.PlayerID()) { game.Game.Process(EndTurnTask.Any(game.Game.CurrentPlayer)); } // Do not continue with any other tasks in this action break; } } // Store the action in the match-statistics MatchStatistics.ProcessAction(currentPlayerName, executedTasks, timer.Elapsed, bot.BudgetSpent(), bot.MaxDepth()); if (_printToConsole) { Console.WriteLine($"*Action computation time: {timer.Elapsed:g}"); } }
public static void RunQuickMatch() { var game = new SabberStoneState(new SabberStoneCore.Model.Game(new GameConfig { StartPlayer = 1, Player1Name = Constants.SABBERSTONE_GAMECONFIG_PLAYER1_NAME, Player1HeroClass = CardClass.HUNTER, Player1Deck = Decks.GetRandomTournamentDeck(), Player2Name = Constants.SABBERSTONE_GAMECONFIG_PLAYER2_NAME, Player2HeroClass = CardClass.HUNTER, Player2Deck = Decks.GetRandomTournamentDeck(), FillDecks = false, Shuffle = true, SkipMulligan = false, History = false })); // Create two bots to play var bot1 = BotFactory.CreateSabberStoneBot(BotSetupType.RandomBot, game.Player1); var bot2 = BotFactory.CreateSabberStoneBot(BotSetupType.RandomBot, game.Player2); game.Game.StartGame(); game.Game.Process(MulliganStrategySabberStone.DefaultMulligan(game.Game.Player1)); game.Game.Process(MulliganStrategySabberStone.DefaultMulligan(game.Game.Player2)); game.Game.MainReady(); while (game.Game.State != State.COMPLETE) { Console.WriteLine(""); Console.WriteLine($"TURN {(game.Game.Turn + 1) / 2} - {game.Game.CurrentPlayer.Name}"); Console.WriteLine($"Hero[P1] {game.Player1.Hero} HP: {game.Player1.Hero.Health} / Hero[P2] {game.Player2.Hero} HP: {game.Player2.Hero.Health}"); Console.WriteLine($"- {game.Game.CurrentPlayer.Name} Action ----------------------------"); // Ask the bot to act. var action = game.Game.CurrentPlayer.Id == game.Player1.Id ? bot1.Act(game) : bot2.Act(game); // Check if the action is valid if (action == null || !action.IsComplete()) { continue; } // Process the tasks in the action foreach (var item in action.Tasks) { // Process the task Console.WriteLine(item.Task.FullPrint()); game.Game.Process(item.Task); } } Console.WriteLine($"Game: {game.Game.State}, Player1: {game.Player1.PlayState} / Player2: {game.Player2.PlayState}"); }
/// <summary> /// Returns a SabberStoneAction for the current state. /// Note: If this player has no available options, null is returned. /// </summary> /// <param name="state">The current game state.</param> /// <returns>SabberStoneAction or null in the case of no available options.</returns> public SabberStoneAction Act(SabberStoneState state) { // Check to make sure the player to act in the game-state matches our player. if (state.CurrentPlayer() != Player.Id) { return(null); } // Check if there are any options, otherwise return a randomly created action. return(Player.Options().IsNullOrEmpty() ? SabberStoneAction.CreateNullMove(Player) : CreateRandomAction(state)); }
/// <summary> /// Selects and action using the UCB1 algorithm. /// </summary> /// <param name="state">The game state.</param> /// <returns><see cref="SabberStoneAction"/>.</returns> private SabberStoneAction SelectUCB(SabberStoneState state) { var action = new SabberStoneAction(); var stateClone = state.Game.Clone(); // Repeatedly exploit the highest UCB-value task that is available in this state do { SabberStonePlayerTask selectedTask; // Get the stats of the tasks currently available in this state var availableTasks = stateClone.Game.CurrentPlayer.Options().Where(i => i.ZonePosition <= 0).Select(i => (SabberStonePlayerTask)i).ToList(); var availableTaskHashes = availableTasks.Select(i => i.GetHashCode()).ToList(); var availableStatistics = MASTTable.Where(i => availableTaskHashes.Contains(i.Key)).ToList(); var totalVisits = availableStatistics.Sum(i => i.Value.Visits); // Find the task with the highest UCB value var bestTask = availableStatistics.OrderByDescending(i => i.Value.UCB(totalVisits, UCBConstantC)).FirstOrDefault(); // If no best task was found, randomly choose an available task if (bestTask.IsDefault()) { var randomTask = availableTasks.RandomElementOrDefault(); // If we also can't randomly find a task, stop if (randomTask == null) { break; } selectedTask = randomTask; } else { // Find all available tasks that have an UCB value similar to the best var bestValue = bestTask.Value.UCB(totalVisits, UCBConstantC); var compTasks = availableStatistics.Where(i => Math.Abs(i.Value.UCB(totalVisits, UCBConstantC) - bestValue) < AVThesis.Constants.DOUBLE_EQUALITY_TOLERANCE).ToList(); // Select one of the tasks selectedTask = compTasks.RandomElementOrDefault().Value.Task; } // Add the task to the action we are building action.AddTask(selectedTask); // Process the task stateClone.Process(selectedTask.Task); // Continue until we have created a complete action, or the game has completed } while (!action.IsComplete() && stateClone.Game.State != State.COMPLETE); // Return the action we've created return(action); }
/// <inheritdoc /> public SabberStoneAction Sample(SabberStoneState state, OddmentTable <SabberStonePlayerTask> sideInformation) { var copyState = (SabberStoneState)state.Copy(); var availableTasks = GetAvailablePlayerTasks(copyState); var action = new SabberStoneAction(); var tries = 0; // Keep sampling tasks while we have not passed the turn yet and there are more tasks available than only EndTurn or HeroPower, of if we haven't generated a suitable task in 100 tries while (!action.IsComplete() && availableTasks.Any(i => i.Task.PlayerTaskType != PlayerTaskType.END_TURN && i.Task.PlayerTaskType != PlayerTaskType.HERO_POWER) && tries < 100) { // Sample a task from the OddmentTable var task = sideInformation.Next(); // Check if the task is available in the current state if (!availableTasks.Contains(task, PlayerTaskComparer.Comparer)) { tries++; continue; } tries = 0; action.AddTask(task); copyState.Game.Process(task.Task); availableTasks = GetAvailablePlayerTasks(copyState); } if (action.IsComplete()) { return(action); } // If hero power is available, add it if (availableTasks.Any(i => i.Task.PlayerTaskType == PlayerTaskType.HERO_POWER)) { action.AddTask(availableTasks.First(i => i.Task.PlayerTaskType == PlayerTaskType.HERO_POWER)); } // If the action is not complete yet, add EndTurn action.AddTask((SabberStonePlayerTask)EndTurnTask.Any(state.Game.CurrentPlayer)); return(action); }
/// <inheritdoc /> public SabberStoneAction Act(SabberStoneState state) { var timer = Stopwatch.StartNew(); var stateCopy = (SabberStoneState)state.Copy(); if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine(Name()); } if (_debug) { Console.WriteLine($"Starting an LSI search in turn {(stateCopy.Game.Turn + 1) / 2}"); } // Create a new LSI search var search = new LSI <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, TreeSearchNode <SabberStoneState, SabberStoneAction>, OddmentTable <SabberStonePlayerTask> >( SideInformationStrategy, SamplingStrategy, Playout, Evaluation, GameLogic, BudgetEstimationStrategy ); // Reset the solutions collection EnsembleSolutions = new List <SabberStoneAction>(); // Create a SearchContext that just holds the current state as Source and the Search. var context = SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> .Context(EnsembleSolutions, stateCopy, null, null, search, null); // The Playout strategy will call the Goal strategy from the context, so we set it here context.Goal = Goal; // Execute the search Ensemble.EnsembleSearch(context, Searcher.Search, EnsembleSize); SamplesSpent = EnsembleSolutions.Sum(i => i.BudgetUsed); // Determine a solution var solution = Searcher.VoteForSolution(EnsembleSolutions, state); timer.Stop(); if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine($"LSI returned with solution: {solution}"); } if (_debug) { Console.WriteLine($"My total calculation time was: {timer.ElapsedMilliseconds}ms"); } // Check if the solution is a complete action. if (!solution.IsComplete()) { // Otherwise add an End-Turn task before returning. solution.Tasks.Add((SabberStonePlayerTask)EndTurnTask.Any(Player)); } // If we are estimating the budget by using the previous search's results, save these now if (BudgetEstimation == BudgetEstimationType.PreviousSearchAverage && BudgetEstimationStrategy is PreviousSearchAverageBudgetEstimationStrategy estimationStrategy) { estimationStrategy.PreviousSearchTime = timer.ElapsedMilliseconds; estimationStrategy.PreviousSearchIterations = SamplesSpent; } if (_debug) { Console.WriteLine(); } return(solution); }
/// <inheritdoc /> public SabberStoneAction Sample(SabberStoneState state) { return(SabberStoneAction.CreateNullMove(state.CurrentPlayer() == state.Player1.Id ? state.Player1 : state.Player2)); }
/// <summary> /// Expands a specific state and returns all available SabberStonePlayerTasks. /// Note: it is assumed that the expansion is performed Hierarchically and therefore only contains single SabberStonePlayerTask SabberStoneActions. /// </summary> /// <param name="state">The game state.</param> /// <returns>Collection of SabberStonePlayerTasks that are available in the provided state.</returns> private List <SabberStonePlayerTask> GetAvailablePlayerTasks(SabberStoneState state) { return(GameLogic.Expand(null, state).Select(expandedAction => expandedAction.Tasks.First()).ToList()); }
/// <inheritdoc /> /// <summary> /// Requests the bot to return a SabberStoneAction based on the current SabberStoneState. /// </summary> /// <param name="state">The current game state.</param> /// <returns>SabberStoneAction that was voted as the best option by the ensemble.</returns> public SabberStoneAction Act(SabberStoneState state) { var timer = System.Diagnostics.Stopwatch.StartNew(); var gameState = (SabberStoneState)state.Copy(); if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine(Name()); } if (_debug) { Console.WriteLine($"Starting a MCTS search in turn {(gameState.Game.Turn + 1) / 2}"); } // Check if the task statistics in the searcher should be reset if (!RetainTaskStatistics) { Searcher.ResetTaskStatistics(); } // Setup and start the ensemble-search EnsembleSolutions = new List <SabberStoneAction>(); var search = (MCTS <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction>)Builder.Build(); var context = SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> .GameSearchSetup(GameLogic, EnsembleSolutions, gameState, null, search); Ensemble.EnsembleSearch(context, Searcher.Search, EnsembleSize); IterationsSpent = EnsembleSolutions.Sum(i => i.BudgetUsed); // Determine the best tasks to play based on the ensemble search, or just take the one in case of a single search. var solution = EnsembleSize > 1 ? Searcher.VoteForSolution(EnsembleSolutions, state) : EnsembleSolutions.First(); var time = timer.ElapsedMilliseconds; if (_debug) { Console.WriteLine(); } if (_debug) { Console.WriteLine($"MCTS returned with solution: {solution}"); } if (_debug) { Console.WriteLine($"My total calculation time was: {time} ms."); } if (_debug) { Console.WriteLine(); } // Check if MoveCompletion should be used. if (!solution.IsComplete()) { CompleteHEMove(state, solution); } return(solution); }
/// <summary> /// Runs a game of this match with the specified index. /// </summary> /// <param name="gameIndex">The index of the game that should be run.</param> public void RunGame(int gameIndex) { Console.WriteLine($"{DateTime.Now:T} -> Starting Game {gameIndex+1} of {NumberOfGames}"); try { var timer = Stopwatch.StartNew(); // Alternate which player starts. var config = GetTournamentConfiguration(); config.StartPlayer = gameIndex % 2 + 1; // Create a new game with the cloned configuration. var game = new SabberStoneState(new SabberStoneCore.Model.Game(config)); // Set up the bots with their Controller from the created game. Bots[0].SetController(game.Player1); Bots[1].SetController(game.Player2); // Get the game ready. game.Game.StartGame(); MatchStatistics.NewGameStarted(gameIndex + 1, game.Game.FirstPlayer.Name); // Default mulligan for each player. game.Game.Process(MulliganStrategySabberStone.DefaultMulligan(game.Game.Player1)); game.Game.Process(MulliganStrategySabberStone.DefaultMulligan(game.Game.Player2)); game.Game.MainReady(); // Play out the game. while (game.Game.State != State.COMPLETE) { if (_printToConsole) { Console.WriteLine(""); } if (_printToConsole) { Console.WriteLine($"*TURN {(game.Game.Turn + 1) / 2} - {game.Game.CurrentPlayer.Name}"); } if (_printToConsole) { Console.WriteLine($"*Hero[P1] {game.Player1.Hero} HP: {game.Player1.Hero.Health} / Hero[P2] {game.Player2.Hero} HP: {game.Player2.Hero.Health}"); } // Play out the current player's turn until they pass. if (game.Game.CurrentPlayer.Id == Bots[0].PlayerID()) { PlayPlayerTurn(game, Bots[0]); } else if (game.Game.CurrentPlayer.Id == Bots[1].PlayerID()) { PlayPlayerTurn(game, Bots[1]); } } if (_printToConsole) { Console.WriteLine($"*Game: {game.Game.State}, Player1: {game.Player1.PlayState} / Player2: {game.Player2.PlayState}"); Console.WriteLine($"*Game lasted {timer.Elapsed:g}"); } // Create game data. MatchStatistics.EndCurrentGame(game); } catch (Exception e) { Console.WriteLine($"ERROR: Exception thrown during game {gameIndex+1}"); WriteExceptionToFile(e); } }
/// <summary> /// Process the end of the current game. /// </summary> /// <param name="state">The end state of the game.</param> public void EndCurrentGame(SabberStoneState state) { CurrentGame.Finalise(state); WriteGameResultToFile(); Games.Add(CurrentGame); }
/// <summary> /// Plays a game to its end state. This end state should be determined by the goal strategy in the search's context. /// </summary> /// <param name="context">The context of the search.</param> /// <param name="position">The position from which to play out the game.</param> /// <returns>The end position.</returns> public SabberStoneState Playout(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState position) { // Play out the game as long as the Goal strategy dictates. while (!context.Goal.Done(context, position)) { PlayPlayerTurn(position); } // Tell the playout bot(s) that the playout has completed. foreach (var playoutBot in Bots) { playoutBot.Value.PlayoutCompleted(context, position); } return(position); }
/// <summary> /// Returns the value of the argument state with respect to the argument node. /// </summary> /// <param name="context">The context of the search.</param> /// <param name="node">The node that provides the context to evaluate the state.</param> /// <param name="state">The state that should be evaluated.</param> /// <returns>Double representing the value of the state with respect to the node.</returns> public double Evaluate(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, TreeSearchNode <SabberStoneState, SabberStoneAction> node, SabberStoneState state) { // Check if we can and want to use the HeuristicBot's evaluation if (UseHeuristicBotEvaluation) { // This scoring function is actually used to score the effect of tasks, but we are using it here to score the effect of the transition from our Source state to the state from which we are currently evaluating. // TODO using the HeuristicBot's evaluation function could be improved var heuristicEvaluation = HeuristicAgent.EvaluateStateTransition(context.Source, state); // Colour the evaluation depending on who the active player is in the state var isRootPlayer = state.CurrentPlayer() == context.Source.CurrentPlayer(); heuristicEvaluation = isRootPlayer ? heuristicEvaluation : heuristicEvaluation * -1; // Normalise the value between -1 and 1. The min and max values have been empirically set and equal the min and max possible evaluations that are returned by the HeuristicBot's function. var norm = 2 * Util.Normalise(heuristicEvaluation, -50, 50) - 1; // Note: this is a transformation from [0,1] to [-1,1] return(norm); } var rootPlayerId = context.Source.CurrentPlayer(); var rootPlayer = state.Player1.Id == rootPlayerId ? state.Player1 : state.Player2; var opponent = rootPlayer.Opponent; // Check for a win/loss if (state.PlayerWon != State.DRAW) { return(state.PlayerWon == rootPlayerId ? 1 : -1); } // Gather stats that we need // TODO gather stats from cards in hand // Opponent HP var oH = opponent.Hero.Health; // Opponent's Taunt Minions HP var opponentMinions = opponent.BoardZone.GetAll(); var oMtH = opponentMinions.Where(i => i.HasTaunt).Sum(j => j.Health); // Opponent's Unknown HP in Hand var oUHh = 0; // Opponent's Unknown Direct Damage in Hand var oDdh = 0; // Opponent's Minion Power var oMP = opponentMinions.Where(i => i.CanAttack).Sum(j => j.AttackDamage); // Opponent's Unknown Minion Power from Hand var oUMPh = 0; // Opponent's Weapon Damage var oWD = opponent.Hero.Weapon?.AttackDamage ?? 0; // Opponent's Fatigue Damage var oFD = opponent.DeckZone.IsEmpty ? opponent.Hero.Fatigue + 1 : 0; // Root Player HP var rH = rootPlayer.Hero.Health; // Root Player's Taunt Minions HP var rootPlayerMinions = rootPlayer.BoardZone.GetAll(); var rMtH = rootPlayerMinions.Where(i => i.HasTaunt).Sum(j => j.Health); // Root Player's HP in Hand var rHh = 0; // Root Player's Direct Damage in Hand var rDdh = 0; // Root Player's Minion Power var rMP = rootPlayerMinions.Where(i => i.CanAttack).Sum(j => j.AttackDamage); // Root Player's Minion Power from Hand var rMPh = 0; // Root Player's Weapon Damage var rWD = rootPlayer.Hero.Weapon?.AttackDamage ?? 0; // Root Player's Fatigue Damage var rFD = rootPlayer.DeckZone.IsEmpty ? rootPlayer.Hero.Fatigue + 1 : 0; // Calculate the approximate turns before the opponent dies var opponentHealth = oH + oMtH + oUHh - oFD - rDdh; var rootPlayerDamage = rMP + rMPh + rWD; var oTD = rootPlayerDamage > 0 ? opponentHealth / (rootPlayerDamage * 1.0) : int.MaxValue; // Calculate the approximate turns before the root player dies var rootPlayerHealth = rH + rMtH + rHh - rFD - oDdh; var opponentDamage = oMP + oUMPh + oWD; var rTD = opponentDamage > 0 ? rootPlayerHealth / (opponentDamage * 1.0) : int.MaxValue; // Check some situations var canKillOpponentThisTurn = (int)Math.Ceiling(oTD) == 1; var canBeKilledByOpponentThisTurn = (int)Math.Ceiling(rTD) == 1; var notARaceSituation = oTD >= 4 && rTD >= 4; // If the root player can kill the opponent, evaluation is 1 if (canKillOpponentThisTurn) { return(1); } // If opponent can't be killed, but they can kill root player next turn, evaluation is -1 if (canBeKilledByOpponentThisTurn) { return(-1); } // If this is not a racing situation (yet), return a cautious number if (notARaceSituation) { // Two aspects here (to keep it simple) // -> root player's HP vs opponent's HP // -> root player's #creatures vs opponent's #creatures // Having more HP ánd more creatures is quite good if (rH > oH && rootPlayerMinions.Length > opponentMinions.Length) { return(0.75); } if (rH > oH && rootPlayerMinions.Length == opponentMinions.Length) { return(0.25); } if (rH > oH && rootPlayerMinions.Length < opponentMinions.Length) { return(0.1); } if (rH == oH && rootPlayerMinions.Length > opponentMinions.Length) { return(0.33); } if (rH == oH && rootPlayerMinions.Length == opponentMinions.Length) { return(0); } if (rH == oH && rootPlayerMinions.Length < opponentMinions.Length) { return(-0.33); } if (rH < oH && rootPlayerMinions.Length > opponentMinions.Length) { return(-0.1); } if (rH < oH && rootPlayerMinions.Length == opponentMinions.Length) { return(-0.25); } // Having less HP ánd less creatures is quite bad if (rH < oH && rootPlayerMinions.Length < opponentMinions.Length) { return(-0.75); } } // If none of the above applies, look at the difference between when the opponent dies and when the root player dies var difference = oTD - rTD; // If the difference is between -1 and 1, it is too close to tell if (difference >= -1 && difference <= 1) { return(0); } // If the difference is negative, it means the root player would die later than the opponent, so the root player would be slightly ahead if (difference < -1) { return(0.5); } // If the difference is positive, it means the opponent would die later than the root player, so the root player would be losing slightly if (difference > 1) { return(-0.5); } throw new ArgumentOutOfRangeException($"Evaluation values do not fall into the expected range: oTD={oTD:F3} | rTD={rTD:F3}"); }
/// <summary> /// Determines if a Position represents a completed search. /// </summary> /// <param name="context">The context of the search.</param> /// <param name="position">The Position.</param> /// <returns>Whether or not the search is done.</returns> public bool Done(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState position) { // Determine the turn in which the search started. var sourceTurn = context.Source.Game.Turn; // The goal will be reached if the amount of turns since the source turn is greater than or equal to the cutoff, or the game has been completed. return(position.Game.Turn - sourceTurn >= CutoffThreshold || position.Game.State == SabberStoneCore.Enums.State.COMPLETE); }
/// <inheritdoc /> public void PlayoutCompleted(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState endState) { }
/// <inheritdoc /> public void PlayoutCompleted(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState endState) { // Evaluate the state. var value = Evaluation.Evaluate(context, null, endState); // Colour the value based on whether or not this playout bot is for the root player or not value = context.Source.CurrentPlayer() == PlayerID() ? value : value * -1; // Add data for all the actions that have been taken. foreach (var action in ActionsTaken) { AddData(action, value); } // Clear the taken actions. ActionsTaken = new List <SabberStoneAction>(); }