diff --git a/Simulator/ActionSet.cs b/Simulator/ActionSet.cs deleted file mode 100644 index c4b12de..0000000 --- a/Simulator/ActionSet.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Craftimizer.Simulator.Actions; -using System.Diagnostics.CodeAnalysis; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics.X86; - -namespace Craftimizer.Simulator; - -public record ActionSet -{ - public ulong Bits { get; set; } - public List SavedActions { get; set; } = new(); - - private bool HasFlagA(ActionType action) => (Bits & (1ul << ((int)action + 1))) != 0; - private bool HasFlagB(ActionType action) => SavedActions.Contains(action); - public bool HasFlag(ActionType action) - { - var a = HasFlagA(action); - var b = HasFlagB(action); - if (a != b) - throw new Exception($"Action {action} has different flags: {a} vs {b}"); - return a; - } - - private void SetFlagA(ActionType action) => Bits |= 1ul << ((int)action + 1); - private void SetFlagB(ActionType action) - { - if (!SavedActions.Contains(action)) - SavedActions.Add(action); - } - public void SetFlag(ActionType action) - { - SetFlagA(action); - SetFlagB(action); - } - - private void ClearFlagA(ActionType action) => Bits &= ~(1ul << ((int)action + 1)); - private void ClearFlagB(ActionType action) => SavedActions.RemoveAll(a => a == action); - public void ClearFlag(ActionType action) - { - ClearFlagA(action); - ClearFlagB(action); - } - - 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; - } - - private ActionType ActionAtA(int index) => Actions.ElementAt(index);//(ActionType)(NthBitSet(Bits, index) - 1); - private ActionType ActionAtB(int index) => SavedActions.ElementAt(index); - public ActionType ActionAt(int index) - { - return ActionAtB(index); - var a = ActionAtA(index); - var a2 = (ActionType)(NthBitSet(Bits, index) - 1); - var b = ActionAtB(index); - if (a != a2) - throw new Exception($"A2: Action {index} has different flags: {a} vs {a2}"); - if (a != b) - throw new Exception($"Action {index} has different flags: {a} vs {b}"); - return a; - } - - private int ActionCountA => BitOperations.PopCount(Bits); - private int ActionCountB => SavedActions.Count; - public int ActionCount { get - { - return ActionCountB; - var a = ActionCountA; - var b = ActionCountB; - if (a != b) - throw new Exception($"Action count has different flags: {a} vs {b}"); - return a; - } } - - private bool IsEmptyA => Bits == 0; - private bool IsEmptyB => SavedActions.Count == 0; - public bool IsEmpty - { - get - { - return IsEmptyB; - var a = IsEmptyA; - var b = IsEmptyB; - if (a != b) - throw new Exception($"IsEmpty has different flags: {a} vs {b}"); - return a; - } - } -} diff --git a/Solver/Crafty/ActionSet.cs b/Solver/Crafty/ActionSet.cs new file mode 100644 index 0000000..5e11f54 --- /dev/null +++ b/Solver/Crafty/ActionSet.cs @@ -0,0 +1,59 @@ +using Craftimizer.Simulator.Actions; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics.X86; + +namespace Craftimizer.Solver.Crafty; + +public sealed class ActionSet +{ + private uint Bits { get; set; } = 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NthBitSet(uint value, int n) + { + if (Bmi2.IsSupported) + return BitOperations.TrailingZeroCount(Bmi2.ParallelBitDeposit(1u << n, value)); + + var mask = 0x0000FFFFu; + var size = 16; + var _base = 0; + + if (n++ >= BitOperations.PopCount(value)) + return 32; + + 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)] + private static int FromAction(ActionType action) => Array.IndexOf(Simulator.AcceptedActions, action); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ActionType ToAction(int index) => Simulator.AcceptedActions[index]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasAction(ActionType action) => (Bits & (1u << (FromAction(action) + 1))) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddAction(ActionType action) => Bits |= 1u << (FromAction(action) + 1); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RemoveAction(ActionType action) => Bits &= ~(1u << (FromAction(action) + 1)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ActionType ElementAt(int index) => ToAction(NthBitSet(Bits, index) - 1); + + public int Count => BitOperations.PopCount(Bits); +} diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs index eb0d77c..f7031e2 100644 --- a/Solver/Crafty/SimulationNode.cs +++ b/Solver/Crafty/SimulationNode.cs @@ -10,7 +10,7 @@ public readonly record struct SimulationNode public ActionSet AvailableActions { get; init; } public CompletionState SimulationCompletionState { get; init; } public CompletionState CompletionState => - AvailableActions.IsEmpty && SimulationCompletionState == CompletionState.Incomplete ? + AvailableActions.Count == 0 && SimulationCompletionState == CompletionState.Incomplete ? CompletionState.NoMoreActions : SimulationCompletionState; diff --git a/Solver/Crafty/Simulator.cs b/Solver/Crafty/Simulator.cs index 5443e37..1e7193e 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Crafty/Simulator.cs @@ -20,7 +20,7 @@ public class Simulator : Sim public override bool RollSuccessRaw(float successRate) => successRate == 1; public override void StepCondition() { } - private static readonly ActionType[] AcceptedActions = new[] + public static readonly ActionType[] AcceptedActions = new[] { ActionType.TrainedFinesse, ActionType.PrudentSynthesis, @@ -49,6 +49,105 @@ 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 ActionSet AvailableActionsHeuristic(bool strict) { @@ -56,106 +155,10 @@ public class Simulator : Sim return new(); ActionUtils.SetSimulation(this); - var a = 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; - }); var ret = new ActionSet(); - foreach (var ac in a) - ret.SetFlag(ac); + foreach (var action in AcceptedActions) + if (CanUseAction(action, strict)) + ret.AddAction(action); return ret; } } diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index ad5ba37..fb98b25 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -12,7 +12,7 @@ public class Solver //public Random Random => Simulator.Input.Random; - public const int Iterations = 1000; + public const int Iterations = 30000; public const float ScoreStorageThreshold = 1f; public const float MaxScoreWeightingConstant = 0.1f; public const float ExplorationConstant = 4f; @@ -57,9 +57,9 @@ public class Solver if (node.IsComplete) return (currentIndex, node.CompletionState); - if (!node.AvailableActions.HasFlag(action)) + if (!node.AvailableActions.HasAction(action)) return (currentIndex, CompletionState.InvalidAction); - node.AvailableActions.ClearFlag(action); + node.AvailableActions.RemoveAction(action); currentIndex = Tree.Insert(currentIndex, Execute(node.State, action, strict)); } @@ -81,7 +81,7 @@ public class Solver return exploitation + exploration; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RustMaxBy(List source, Func into) { @@ -106,10 +106,9 @@ public class Solver { var selectedNode = Tree.Get(selectedIndex); - var expandable = !selectedNode.State.AvailableActions.IsEmpty; + var expandable = selectedNode.State.AvailableActions.Count != 0; var likelyTerminal = selectedNode.Children.Count == 0; - if (expandable || likelyTerminal) - { + if (expandable || likelyTerminal) { break; } @@ -126,9 +125,8 @@ public class Solver if (initialNode.IsComplete) return (initialIndex, initialNode.CompletionState, initialNode.CalculateScore() ?? 0); - var randomIdx = 0;// random.Next(initialNode.AvailableActions.ActionCount); - var randomAction = initialNode.AvailableActions.ActionAt(randomIdx); - initialNode.AvailableActions.ClearFlag(randomAction); + var randomAction = initialNode.AvailableActions.ElementAt(0); + initialNode.AvailableActions.RemoveAction(randomAction); var expandedState = Execute(initialNode.State, randomAction, true); var expandedIndex = Tree.Insert(initialIndex, expandedState); @@ -139,8 +137,7 @@ public class Solver { if (currentState.IsComplete) break; - randomIdx = 0;// random.Next(currentState.AvailableActions.ActionCount); - randomAction = currentState.AvailableActions.ActionAt(randomIdx); + randomAction = currentState.AvailableActions.ElementAt(0); currentState = Execute(currentState.State, randomAction, true); } @@ -190,8 +187,7 @@ public class Solver { var actions = new List(); var node = Tree.Get(0); - while (node.Children.Count != 0) - { + while (node.Children.Count != 0) { var next_index = RustMaxBy(node.Children, n => Tree.Get(n).State.Scores.MaxScore); node = Tree.Get(next_index); if (node.State.Action != null) @@ -213,8 +209,7 @@ public class Solver public static (List Actions, SimulationState State) SearchStepwise(SimulationInput input, List actions, Action? actionCallback) { var (state, result) = Simulate(input, actions); - if (result != CompletionState.Incomplete) - { + if (result != CompletionState.Incomplete) { return (actions, state); } @@ -224,8 +219,7 @@ public class Solver solver.Search(0); var (solution_actions, solution_node) = solver.Solution(); - if (solution_node.Scores.MaxScore >= 1.0) - { + if (solution_node.Scores.MaxScore >= 1.0) { actions.AddRange(solution_actions); return (actions, solution_node.State); }