diff --git a/Simulator/ActionSet.cs b/Simulator/ActionSet.cs index 5675f8c..c4b12de 100644 --- a/Simulator/ActionSet.cs +++ b/Simulator/ActionSet.cs @@ -1,5 +1,5 @@ using Craftimizer.Simulator.Actions; -using System; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.Intrinsics.X86; @@ -9,15 +9,38 @@ namespace Craftimizer.Simulator; public record ActionSet { public ulong Bits { get; set; } + public List SavedActions { get; set; } = new(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasFlag(ActionType action) => (Bits & (1ul << ((int)action + 1))) != 0; + 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; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetFlag(ActionType action) => Bits |= 1ul << ((int)action + 1); + 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); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ClearFlag(ActionType action) => Bits &= ~(1ul << ((int)action + 1)); + 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(); @@ -60,10 +83,45 @@ public record ActionSet return _base; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ActionType ActionAt(int index) => (ActionType)(NthBitSet(Bits, index) - 1); + 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; + } - public int ActionCount => BitOperations.PopCount(Bits); + 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; + } } - public bool IsEmpty => Bits == 0; + 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/Simulator/Simulator.cs b/Simulator/Simulator.cs index df9e29b..84699ff 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -29,6 +29,8 @@ 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/Simulator.cs b/Solver/Crafty/Simulator.cs index bbf70a8..5443e37 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Crafty/Simulator.cs @@ -49,105 +49,6 @@ 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) { @@ -155,10 +56,106 @@ 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 action in AcceptedActions) - if (CanUseAction(action, strict)) - ret.SetFlag(action); + foreach (var ac in a) + ret.SetFlag(ac); return ret; } } diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index 98fc8a1..ad5ba37 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -1,5 +1,6 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; +using System.Runtime.CompilerServices; namespace Craftimizer.Solver.Crafty; @@ -80,7 +81,7 @@ public class Solver return exploitation + exploration; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int RustMaxBy(List source, Func into) { @@ -107,7 +108,8 @@ public class Solver var expandable = !selectedNode.State.AvailableActions.IsEmpty; var likelyTerminal = selectedNode.Children.Count == 0; - if (expandable || likelyTerminal) { + if (expandable || likelyTerminal) + { break; } @@ -124,7 +126,7 @@ public class Solver if (initialNode.IsComplete) return (initialIndex, initialNode.CompletionState, initialNode.CalculateScore() ?? 0); - var randomIdx = random.Next(initialNode.AvailableActions.ActionCount); + var randomIdx = 0;// random.Next(initialNode.AvailableActions.ActionCount); var randomAction = initialNode.AvailableActions.ActionAt(randomIdx); initialNode.AvailableActions.ClearFlag(randomAction); var expandedState = Execute(initialNode.State, randomAction, true); @@ -137,7 +139,7 @@ public class Solver { if (currentState.IsComplete) break; - randomIdx = random.Next(currentState.AvailableActions.ActionCount); + randomIdx = 0;// random.Next(currentState.AvailableActions.ActionCount); randomAction = currentState.AvailableActions.ActionAt(randomIdx); currentState = Execute(currentState.State, randomAction, true); } @@ -188,7 +190,8 @@ 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) @@ -210,7 +213,8 @@ 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); } @@ -220,7 +224,8 @@ 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); }