/// <summary> /// Creates the currently available options for a player after a specific action is executed. /// </summary> /// <param name="rootState">The game state before any tasks in the action are processed.</param> /// <param name="action">The action containing a list of tasks to process.</param> /// <param name="playerId">The unique identifier of the player that will play the action.</param> /// <param name="ignorePositioning">[Optional] Whether or not to treat minion play tasks with different positions as the same and ignore the extras. Default is true.</param> /// <returns>Collection of available tasks, potentially having minion play tasks with different positions filtered out.</returns> private static IEnumerable <SabberStonePlayerTask> CreateCurrentOptions(SabberStoneState rootState, SabberStoneAction action, int playerId, bool ignorePositioning = true) { // Clone the root state before tampering with it var clonedState = (SabberStoneState)rootState.Copy(); // Apply the tasks in the current action to the root state foreach (var task in action.Tasks) { clonedState.Game.Process(task.Task); } // If it's no longer our player's turn, or if the game has ended return an empty list // Note: this will happen if the last task processed was an end-turn task or if that task ended the game if (clonedState.Game.CurrentPlayer.Id != playerId || clonedState.Game.State == State.COMPLETE) { return(new List <SabberStonePlayerTask>()); } // Query the game for the currently available actions var currentOptions = clonedState.Game.CurrentPlayer.Options(); if (ignorePositioning) { // Filter out all tasks that have position set to anything higher than 0 // Note: a position of -1 means that the task doesn't care about positioning, so those are left in currentOptions = currentOptions.Where(i => i.ZonePosition <= 0).ToList(); } // Return the options return(currentOptions.Select(i => (SabberStonePlayerTask)i)); }
/// <summary> /// Creates a SabberStoneAction from a collection of possible solutions by voting for separate tasks. /// </summary> /// <param name="solutions">The available solutions.</param> /// <param name="state">The game state.</param> /// <returns>SabberStoneAction.</returns> public SabberStoneAction VoteForSolution(List <SabberStoneAction> solutions, SabberStoneState state) { // Clone game so that we can process the selected tasks and get an updated options list. var clonedGame = state.Game.Clone(); var action = new SabberStoneAction(); // Have all solutions vote on tasks var taskVotes = new Dictionary <int, int>(); foreach (var solution in solutions) { foreach (var task in solution.Tasks) { var taskHash = task.GetHashCode(); if (!taskVotes.ContainsKey(taskHash)) { taskVotes.Add(taskHash, 0); } taskVotes[taskHash]++; } } // Keep selecting tasks until the action is complete or the game has ended while (!action.IsComplete() && clonedGame.State != State.COMPLETE) { // Make a dictionary of available tasks, indexed by their hashcode, but ignore the END-TURN task for now var availableTasks = clonedGame.CurrentPlayer.Options().Where(i => i.PlayerTaskType != PlayerTaskType.END_TURN).Select(i => (SabberStonePlayerTask)i).ToList(); // Pick the one with most votes var votedOnTasks = availableTasks.Where(i => taskVotes.ContainsKey(i.GetHashCode())).ToList(); var mostVoted = votedOnTasks.OrderByDescending(i => taskVotes[i.GetHashCode()]).FirstOrDefault(); // If this is null, it means none of the tasks that are available appear in any of the solutions if (mostVoted == null) { // End the turn action.AddTask((SabberStonePlayerTask)EndTurnTask.Any(clonedGame.CurrentPlayer)); break; } // Find any tasks tied for most votes var mostVotes = taskVotes[mostVoted.GetHashCode()]; var ties = votedOnTasks.Where(i => taskVotes[i.GetHashCode()] == mostVotes); // Add one of the tasks with the most votes to the action //TODO Ties during voting can be handled differently than random, but handling ties based on visit count would require extra information from the separate searches' solutions. var chosenTask = ties.RandomElementOrDefault(); action.AddTask(chosenTask); // Process the task so we have an updated options list next iteration clonedGame.Process(chosenTask.Task); } return(action); }
/// <summary> /// Recursively expands an action by creating new actions and adding possible tasks to them. /// </summary> /// <param name="rootState">The game state at the root of the recursive call.</param> /// <param name="action">The action.</param> /// <param name="playerId">The unique identifier of the player that will play the action.</param> /// <param name="completeActions">A reference to a collection of completely expanded actions.</param> private static void ExpandAction(SabberStoneState rootState, SabberStoneAction action, int playerId, ref List <SabberStoneAction> completeActions) { // If the latest option added was the end-turn task, return if (action.IsComplete()) { completeActions.Add(action); return; } // Go through all available options foreach (var option in CreateCurrentOptions(rootState, action, playerId)) { // Create a new action that is a copy of the current action var nextLevelAction = new SabberStoneAction(action.Tasks); // Add that task as the latest task nextLevelAction.AddTask(option); // Recursively call this method to find all combinations ExpandAction(rootState, nextLevelAction, playerId, ref completeActions); } }
/// <summary> /// Determines the best tasks for the game state based on the provided statistics and creates a <see cref="SabberStoneAction"/> from them. /// </summary> /// <param name="state">The game state to create the best action for.</param> /// <returns><see cref="SabberStoneAction"/> created from the best individual tasks available in the provided state.</returns> public SabberStoneAction DetermineBestTasks(SabberStoneState state) { // Clone game so that we can process the selected tasks and get an updated options list. var clonedGame = state.Game.Clone(); // We have to determine which tasks are the best to execute in this state, based on the provided values of the MCTS search. // So we'll check the statistics table for the highest value among tasks that are currently available in the state. // This continues until the end-turn task is selected. var action = new SabberStoneAction(); while (!action.IsComplete() && clonedGame.State != State.COMPLETE) { // Get the available options in this state and find which tasks we have statistics on, but ignore the END-TURN task for now var availableTasks = clonedGame.CurrentPlayer.Options().Where(i => i.PlayerTaskType != PlayerTaskType.END_TURN).Select(i => ((SabberStonePlayerTask)i).GetHashCode()); var stats = TaskStatistics.Where(i => availableTasks.Contains(i.Key)).ToList(); var bestTask = stats.OrderByDescending(i => i.Value.AverageValue()).FirstOrDefault(); // If we can't find any task, stop. if (bestTask.IsDefault()) { // End the turn action.AddTask((SabberStonePlayerTask)EndTurnTask.Any(clonedGame.CurrentPlayer)); break; } // Handle the possibility of tasks with tied average value. var bestValue = bestTask.Value.AverageValue(); var tiedTasks = stats.Where(i => Math.Abs(i.Value.AverageValue() - bestValue) < Constants.DOUBLE_EQUALITY_TOLERANCE); var orderedTies = tiedTasks.OrderByDescending(i => i.Value.Visits); bestTask = orderedTies.First(); // If we found a task, add it to the Action and process it to progress the game. var task = bestTask.Value.Task; action.AddTask(task); clonedGame.Process(task.Task); } // Return the created action consisting of the best action available at each point. return(action); }
/// <summary> /// Calculate the scores of a SabberStoneState. /// </summary> /// <param name="position">The SabberStoneState.</param> /// <returns>Array of Double containing the scores per player, indexed by player ID.</returns> public double[] Scores(SabberStoneState position) => new[] { position.PlayerWon == position.Player1.Id ? PLAYER_WIN_SCORE : PLAYER_LOSS_SCORE, position.PlayerWon == position.Player2.Id ? PLAYER_WIN_SCORE : PLAYER_LOSS_SCORE };
/// <summary> /// Expand the search from a SabberStoneState. /// </summary> /// <param name="context">The context of the search.</param> /// <param name="position">The state to expand from.</param> /// <returns>An enumeration of possible actions from the argument state.</returns> public IPositionGenerator <SabberStoneAction> Expand(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState position) { var availableActionSequences = new List <SabberStoneAction>(); var activePlayer = position.Game.CurrentPlayer; var activePlayerId = activePlayer.Id; // When expanding on a position, we'll have a number of tasks to choose from. // The problem is that each task isn't exclusive with other tasks and/or may lead to more available tasks when processed. var availableOptions = activePlayer.Options(); // Filter out duplicate actions with different zone positions. // This significantly reduces complexity at a minor loss of game theoretic optimality. availableOptions = availableOptions.Where(i => i.ZonePosition <= 0).ToList(); // If we are expanding hierarchically we can just return the individual tasks. if (HierarchicalExpansion) { // Order the dimensions var orderedTasks = OrderTasks(availableOptions); return(new SabberStoneMoveGenerator(orderedTasks)); } // If we are not expanding hierarchically, we'll need to generate all action sequences at once. var topLevelActions = new List <SabberStoneAction>(); foreach (var task in availableOptions) { topLevelActions.Add(CreateActionFromSingleTask(task)); } // Recursively expand the top-level actions. foreach (var action in topLevelActions) { ExpandAction(position, action, activePlayerId, ref availableActionSequences); } // Return a move generator based on the list of available action sequences. return(new SabberStoneMoveGenerator(availableActionSequences)); }
/// <summary> /// Determines if a SabberStoneState represents a completed game. /// </summary> /// <param name="context">The context of the search.</param> /// <param name="position">The game state.</param> /// <returns>Whether or not the game is completed.</returns> public bool Done(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState position) { return(Goal.Done(context, position)); }
/// <summary> /// Applies a SabberStoneAction to a SabberStoneState which results in a new SabberStoneState. /// </summary> /// <param name="context">The context of the search.</param> /// <param name="position">The state to which the action should be applied.</param> /// <param name="action">The action to apply.</param> /// <returns>SabberStoneState that is the result of applying the action.</returns> public SabberStoneState Apply(SearchContext <List <SabberStoneAction>, SabberStoneState, SabberStoneAction, object, SabberStoneAction> context, SabberStoneState position, SabberStoneAction action) { // In case of hierarchical expansion, we'll arrive here with incomplete actions, i.e. actions that might not have end-turn tasks. // We'll still have to process these actions to move the state into a new state, ready for further expansion. // Check if the action is complete, or if we are applying Hierarchical Expansion (in that case the action will be incomplete). if (action.IsComplete() || HierarchicalExpansion) { // Process each task. foreach (var item in action.Tasks) { position.Game.Process(item.Task); } } else { // In the case of an incomplete action, just pass the turn. position.Game.Process(EndTurnTask.Any(position.Game.CurrentPlayer)); } // Return the position. return(position); }