From e546ff2dcd8ab57aee2abb11b3cbf5748ed7bd3e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 18 Jun 2023 12:35:32 -0700 Subject: [PATCH] Use ActionSet instead of a List --- Simulator/ActionSet.cs | 69 +++++++++++ Simulator/Simulator.cs | 2 - Solver/Crafty/SimulationNode.cs | 4 +- Solver/Crafty/Simulator.cs | 203 +++++++++++++++++--------------- Solver/Crafty/Solver.cs | 13 +- 5 files changed, 184 insertions(+), 107 deletions(-) create mode 100644 Simulator/ActionSet.cs diff --git a/Simulator/ActionSet.cs b/Simulator/ActionSet.cs new file mode 100644 index 0000000..5675f8c --- /dev/null +++ b/Simulator/ActionSet.cs @@ -0,0 +1,69 @@ +using Craftimizer.Simulator.Actions; +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics.X86; + +namespace Craftimizer.Simulator; + +public record ActionSet +{ + public ulong Bits { get; set; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasFlag(ActionType action) => (Bits & (1ul << ((int)action + 1))) != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetFlag(ActionType action) => Bits |= 1ul << ((int)action + 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ClearFlag(ActionType action) => Bits &= ~(1ul << ((int)action + 1)); + + public IEnumerable Actions => GetActions(); + + private IEnumerable GetActions() + { + foreach (var action in Enum.GetValues()) + if (HasFlag(action)) + yield return action; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NthBitSet(ulong value, int n) + { + if (Bmi2.X64.IsSupported) + return BitOperations.TrailingZeroCount(Bmi2.X64.ParallelBitDeposit(1ul << n, value)); + + ulong mask = 0x0000FFFFFFFFu; + var size = 32; + var _base = 0; + + if (n++ >= BitOperations.PopCount(value)) + return 64; + + while (size > 0) + { + var count = BitOperations.PopCount(value & mask); + if (n > count) + { + _base += size; + size >>= 1; + mask |= mask << size; + } + else + { + size >>= 1; + mask >>= size; + } + } + + return _base; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ActionType ActionAt(int index) => (ActionType)(NthBitSet(Bits, index) - 1); + + public int ActionCount => BitOperations.PopCount(Bits); + + public bool IsEmpty => Bits == 0; +} diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index 84699ff..df9e29b 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -29,8 +29,6 @@ public class Simulator } public virtual bool IsComplete => CompletionState != CompletionState.Incomplete; - public IEnumerable AvailableActions => ActionUtils.AvailableActions(this); - #pragma warning disable CS8618 // Emplace sets all the fields already public Simulator(SimulationState state) #pragma warning restore CS8618 diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs index 5dbd42d..eb0d77c 100644 --- a/Solver/Crafty/SimulationNode.cs +++ b/Solver/Crafty/SimulationNode.cs @@ -7,10 +7,10 @@ public readonly record struct SimulationNode { public SimulationState State { get; init; } public ActionType? Action { get; init; } - public List AvailableActions { get; init; } + public ActionSet AvailableActions { get; init; } public CompletionState SimulationCompletionState { get; init; } public CompletionState CompletionState => - AvailableActions.Count == 0 && SimulationCompletionState == CompletionState.Incomplete ? + AvailableActions.IsEmpty && SimulationCompletionState == CompletionState.Incomplete ? CompletionState.NoMoreActions : SimulationCompletionState; diff --git a/Solver/Crafty/Simulator.cs b/Solver/Crafty/Simulator.cs index 9bb437e..bbf70a8 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Crafty/Simulator.cs @@ -49,109 +49,116 @@ public class Simulator : Sim ActionType.BasicTouch, }; + // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L146 + private bool CanUseAction(ActionType action, bool strict) + { + var baseAction = action.WithUnsafe(); + + if (!baseAction.CanUse) + return false; + + if (CalculateSuccessRate(baseAction.SuccessRate) != 1) + return false; + + // don't allow quality moves at max quality + if (Quality >= Input.Recipe.MaxQuality && baseAction.IncreasesQuality) + return false; + + if (action == ActionType.Observe && + IsPreviousAction(ActionType.Observe)) + return false; + + if (action == ActionType.FinalAppraisal) + return false; + + if (strict) + { + // always used Trained Eye if it's available + if (action == ActionType.TrainedEye) + return true; + + // only allow Focused moves after Observe + if (IsPreviousAction(ActionType.Observe) && + action != ActionType.FocusedSynthesis && + action != ActionType.FocusedTouch) + return false; + + // don't allow quality moves under Muscle Memory for difficult crafts + if (Input.Recipe.ClassJobLevel == 90 && + HasEffect(EffectType.MuscleMemory) && + baseAction.IncreasesQuality) + return false; + + // don't allow pure quality moves under Veneration + if (HasEffect(EffectType.Veneration) && + !baseAction.IncreasesProgress && + baseAction.IncreasesQuality) + return false; + + if (baseAction.IncreasesProgress) + { + var progress_increase = CalculateProgressGain(baseAction.Efficiency); + var would_finish = Progress + progress_increase >= Input.Recipe.MaxProgress; + + if (would_finish) + { + // don't allow finishing the craft if there is significant quality remaining + if (Quality < (Input.Recipe.MaxQuality / 5)) + return false; + } + else + { + // don't allow pure progress moves under Innovation, if it wouldn't finish the craft + if (HasEffect(EffectType.Innovation) && + !baseAction.IncreasesQuality && + baseAction.IncreasesProgress) + return false; + } + } + + if (action == ActionType.ByregotsBlessing && + GetEffectStrength(EffectType.InnerQuiet) <= 1) + return false; + + if ((action == ActionType.WasteNot || action == ActionType.WasteNot2) && + (HasEffect(EffectType.WasteNot) || HasEffect(EffectType.WasteNot2))) + return false; + + if (action == ActionType.Observe && + CP < 5) + return false; + + if (action == ActionType.MastersMend && + Input.Recipe.MaxDurability - Durability < 25) + return false; + + if (action == ActionType.Manipulation && + HasEffect(EffectType.Manipulation)) + return false; + + if (action == ActionType.GreatStrides && + HasEffect(EffectType.GreatStrides)) + return false; + + if ((action == ActionType.Veneration || action == ActionType.Innovation) && + (GetEffectDuration(EffectType.Veneration) > 1 || GetEffectDuration(EffectType.Innovation) > 1)) + return false; + } + + return true; + } + // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L137 - public List AvailableActionsHeuristic(bool strict) + public ActionSet AvailableActionsHeuristic(bool strict) { if (IsComplete) return new(); ActionUtils.SetSimulation(this); - return AcceptedActions.Where(action => - { - var baseAction = action.WithUnsafe(); - - if (!baseAction.CanUse) - return false; - - if (CalculateSuccessRate(baseAction.SuccessRate) != 1) - return false; - - // don't allow quality moves at max quality - if (Quality >= Input.Recipe.MaxQuality && baseAction.IncreasesQuality) - return false; - - if (action == ActionType.Observe && - IsPreviousAction(ActionType.Observe)) - return false; - - if (action == ActionType.FinalAppraisal) - return false; - - if (strict) - { - // always used Trained Eye if it's available - if (action == ActionType.TrainedEye) - return true; - - // only allow Focused moves after Observe - if (IsPreviousAction(ActionType.Observe) && - action != ActionType.FocusedSynthesis && - action != ActionType.FocusedTouch) - return false; - - // don't allow quality moves under Muscle Memory for difficult crafts - if (Input.Recipe.ClassJobLevel == 90 && - HasEffect(EffectType.MuscleMemory) && - baseAction.IncreasesQuality) - return false; - - // don't allow pure quality moves under Veneration - if (HasEffect(EffectType.Veneration) && - !baseAction.IncreasesProgress && - baseAction.IncreasesQuality) - return false; - - if (baseAction.IncreasesProgress) - { - var progress_increase = CalculateProgressGain(baseAction.Efficiency); - var would_finish = Progress + progress_increase >= Input.Recipe.MaxProgress; - - if (would_finish) - { - // don't allow finishing the craft if there is significant quality remaining - if (Quality < (Input.Recipe.MaxQuality / 5)) - return false; - } - else - { - // don't allow pure progress moves under Innovation, if it wouldn't finish the craft - if (HasEffect(EffectType.Innovation) && - !baseAction.IncreasesQuality && - baseAction.IncreasesProgress) - return false; - } - } - - if (action == ActionType.ByregotsBlessing && - GetEffectStrength(EffectType.InnerQuiet) <= 1) - return false; - - if ((action == ActionType.WasteNot || action == ActionType.WasteNot2) && - (HasEffect(EffectType.WasteNot) || HasEffect(EffectType.WasteNot2))) - return false; - - if (action == ActionType.Observe && - CP < 5) - return false; - - if (action == ActionType.MastersMend && - Input.Recipe.MaxDurability - Durability < 25) - return false; - - if (action == ActionType.Manipulation && - HasEffect(EffectType.Manipulation)) - return false; - - if (action == ActionType.GreatStrides && - HasEffect(EffectType.GreatStrides)) - return false; - - if ((action == ActionType.Veneration || action == ActionType.Innovation) && - (GetEffectDuration(EffectType.Veneration) > 1 || GetEffectDuration(EffectType.Innovation) > 1)) - return false; - } - - return true; - }).ToList(); + var ret = new ActionSet(); + foreach(var action in AcceptedActions) + if (CanUseAction(action, strict)) + ret.SetFlag(action); + return ret; } } diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index 6b6f8b4..98fc8a1 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -56,8 +56,9 @@ public class Solver if (node.IsComplete) return (currentIndex, node.CompletionState); - if (!node.AvailableActions.Remove(action)) + if (!node.AvailableActions.HasFlag(action)) return (currentIndex, CompletionState.InvalidAction); + node.AvailableActions.ClearFlag(action); currentIndex = Tree.Insert(currentIndex, Execute(node.State, action, strict)); } @@ -104,7 +105,7 @@ public class Solver { var selectedNode = Tree.Get(selectedIndex); - var expandable = selectedNode.State.AvailableActions.Count != 0; + var expandable = !selectedNode.State.AvailableActions.IsEmpty; var likelyTerminal = selectedNode.Children.Count == 0; if (expandable || likelyTerminal) { break; @@ -123,8 +124,9 @@ public class Solver if (initialNode.IsComplete) return (initialIndex, initialNode.CompletionState, initialNode.CalculateScore() ?? 0); - var randomAction = initialNode.AvailableActions.ElementAt(0); - initialNode.AvailableActions.RemoveAt(0); + var randomIdx = random.Next(initialNode.AvailableActions.ActionCount); + var randomAction = initialNode.AvailableActions.ActionAt(randomIdx); + initialNode.AvailableActions.ClearFlag(randomAction); var expandedState = Execute(initialNode.State, randomAction, true); var expandedIndex = Tree.Insert(initialIndex, expandedState); @@ -135,7 +137,8 @@ public class Solver { if (currentState.IsComplete) break; - randomAction = currentState.AvailableActions.ElementAt(0); + randomIdx = random.Next(currentState.AvailableActions.ActionCount); + randomAction = currentState.AvailableActions.ActionAt(randomIdx); currentState = Execute(currentState.State, randomAction, true); }