From ecabc2451728ce7eba9b12b4cba898e84b9c20ae Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 00:01:55 -0800 Subject: [PATCH 01/12] Implement ActionPool (backend only) --- Solver/ActionPool.cs | 110 +++++++++++++++++++++++++++++++++++++++ Solver/ActionSet.cs | 98 +++++++--------------------------- Solver/MCTS.cs | 12 ++--- Solver/MCTSConfig.cs | 4 ++ Solver/Simulator.cs | 8 +-- Solver/Solver.cs | 6 +-- Solver/SolverConfig.cs | 2 + Test/Solver/ActionSet.cs | 98 +++++++++++++++++++--------------- 8 files changed, 204 insertions(+), 134 deletions(-) create mode 100644 Solver/ActionPool.cs diff --git a/Solver/ActionPool.cs b/Solver/ActionPool.cs new file mode 100644 index 0000000..9ab1880 --- /dev/null +++ b/Solver/ActionPool.cs @@ -0,0 +1,110 @@ +using Craftimizer.Simulator.Actions; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Craftimizer.Solver; + +[StructLayout(LayoutKind.Auto)] +public readonly struct ActionPool +{ + public static ActionPool Default { get; } = new(new[] + { + ActionType.StandardTouchCombo, + ActionType.AdvancedTouchCombo, + ActionType.FocusedTouchCombo, + ActionType.FocusedSynthesisCombo, + ActionType.TrainedFinesse, + ActionType.PrudentSynthesis, + ActionType.Groundwork, + ActionType.AdvancedTouch, + ActionType.CarefulSynthesis, + ActionType.TrainedEye, + ActionType.DelicateSynthesis, + ActionType.PreparatoryTouch, + ActionType.Reflect, + ActionType.PrudentTouch, + ActionType.Manipulation, + ActionType.MuscleMemory, + ActionType.ByregotsBlessing, + ActionType.WasteNot2, + ActionType.BasicSynthesis, + ActionType.Innovation, + ActionType.GreatStrides, + ActionType.StandardTouch, + ActionType.Veneration, + ActionType.WasteNot, + ActionType.MastersMend, + ActionType.BasicTouch, + }); + + public const int MaskSize = 32; + public const int EnumSize = 37; + + private unsafe struct EnumBuffer + { + public fixed byte Data[MaskSize]; + + public ref ActionType this[int index] => ref Unsafe.As(ref Data[index]); + + public Span AsSpan() => new(Unsafe.AsPointer(ref this[0]), MaskSize); + } + + private unsafe struct LUTBuffer + { + public fixed byte Data[EnumSize]; + + public ref byte this[ActionType index] => ref Data[(byte)index]; + +#pragma warning disable MA0099 + public Span AsSpan() => new(Unsafe.AsPointer(ref this[0]), EnumSize); +#pragma warning restore MA0099 + } + + // List of accepted actions (max 32) + private readonly EnumBuffer acceptedActions; + // Lookup table for accepted actions (ActionType as idx -> idx in acceptedActions) + private readonly LUTBuffer acceptedActionsLUT; + private readonly byte size; + + internal ReadOnlySpan AcceptedActions => acceptedActions.AsSpan().Slice(0, size); + + public ActionPool(ReadOnlySpan actions) + { + if (actions.Length > MaskSize) + throw new ArgumentOutOfRangeException(nameof(actions), actions.Length, $"ActionPool only supports up to {MaskSize} actions"); + + size = (byte)actions.Length; + + acceptedActions.AsSpan().Fill((ActionType)0xFF); + acceptedActionsLUT.AsSpan().Fill(0xFF); + + actions.CopyTo(acceptedActions.AsSpan()); + + for (var i = 0; i < size; i++) + acceptedActionsLUT[acceptedActions[i]] = (byte)i; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal byte FromAction(ActionType action) + { + var ret = acceptedActionsLUT[action]; + if (ret == 0xFF) + throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in this pool."); + return ret; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ActionType ToAction(byte index) + { + if (index < 0 || index >= size) + throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for this pool."); + return acceptedActions[index]; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); +} diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 9c7710c..3a8d92c 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -7,75 +7,13 @@ namespace Craftimizer.Solver; public struct ActionSet { - private uint bits; - - internal static ReadOnlySpan AcceptedActions => new[] - { - ActionType.StandardTouchCombo, - ActionType.AdvancedTouchCombo, - ActionType.FocusedTouchCombo, - ActionType.FocusedSynthesisCombo, - ActionType.TrainedFinesse, - ActionType.PrudentSynthesis, - ActionType.Groundwork, - ActionType.AdvancedTouch, - ActionType.CarefulSynthesis, - ActionType.TrainedEye, - ActionType.DelicateSynthesis, - ActionType.PreparatoryTouch, - ActionType.Reflect, - ActionType.PrudentTouch, - ActionType.Manipulation, - ActionType.MuscleMemory, - ActionType.ByregotsBlessing, - ActionType.WasteNot2, - ActionType.BasicSynthesis, - ActionType.Innovation, - ActionType.GreatStrides, - ActionType.StandardTouch, - ActionType.Veneration, - ActionType.WasteNot, - ActionType.MastersMend, - ActionType.BasicTouch, - }; - - public static readonly int[] AcceptedActionsLUT; - - static ActionSet() - { - AcceptedActionsLUT = new int[Enum.GetValues().Length]; - for (var i = 0; i < AcceptedActionsLUT.Length; i++) - AcceptedActionsLUT[i] = -1; - for (var i = 0; i < AcceptedActions.Length; i++) - AcceptedActionsLUT[(byte)AcceptedActions[i]] = i; - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FromAction(ActionType action) - { - var ret = AcceptedActionsLUT[(byte)action]; - if (ret == -1) - throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in {nameof(ActionSet)}."); - return ret; - } - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActionType ToAction(int index) - { - if (index < 0 || index >= AcceptedActions.Length) - throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for {nameof(ActionSet)}."); - return AcceptedActions[index]; - } - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); + internal uint bits; // Return true if action was newly added and not there before. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool AddAction(ActionType action) + public bool AddAction(in ActionPool pool, ActionType action) { - var mask = ToMask(action); + var mask = pool.ToMask(action); var old = bits; bits |= mask; return (old & mask) == 0; @@ -83,9 +21,9 @@ public struct ActionSet // Return true if action was newly removed and not already gone. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool RemoveAction(ActionType action) + public bool RemoveAction(in ActionPool pool, ActionType action) { - var mask = ToMask(action); + var mask = pool.ToMask(action); var old = bits; bits &= ~mask; return (old & mask) != 0; @@ -93,10 +31,10 @@ public struct ActionSet [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool HasAction(ActionType action) => (bits & ToMask(action)) != 0; + public readonly bool HasAction(in ActionPool pool, ActionType action) => (bits & pool.ToMask(action)) != 0; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index) - 1); + public readonly ActionType ElementAt(in ActionPool pool, int index) => pool.ToAction((byte)(Intrinsics.NthBitSet(bits, index) - 1)); [Pure] public readonly int Count => BitOperations.PopCount(bits); @@ -105,38 +43,38 @@ public struct ActionSet public readonly bool IsEmpty => bits == 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType SelectRandom(Random random) + public readonly ActionType SelectRandom(in ActionPool pool, Random random) { #if IS_DETERMINISTIC - return First(); + return First(in pool); #else - return ElementAt(random.Next(Count)); + return ElementAt(in pool, random.Next(Count)); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ActionType PopRandom(Random random) + public ActionType PopRandom(in ActionPool pool, Random random) { #if IS_DETERMINISTIC - return PopFirst(); + return PopFirst(in pool); #else - var action = ElementAt(random.Next(Count)); - RemoveAction(action); + var action = ElementAt(in pool, random.Next(Count)); + RemoveAction(in pool, action); return action; #endif } #if IS_DETERMINISTIC [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ActionType PopFirst() + private ActionType PopFirst(in pool) { - var action = First(); - RemoveAction(action); + var action = First(in pool); + RemoveAction(in pool, action); return action; } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly ActionType First() => ElementAt(0); + private readonly ActionType First(in pool) => ElementAt(in pool, 0); #endif } diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs index 56106b1..712aad8 100644 --- a/Solver/MCTS.cs +++ b/Solver/MCTS.cs @@ -23,7 +23,7 @@ public sealed class MCTS public MCTS(in MCTSConfig config, in SimulationState state) { this.config = config; - var sim = new Simulator(config.MaxStepCount) { State = state }; + var sim = new Simulator(config.ActionPool, config.MaxStepCount) { State = state }; rootNode = new(new( state, null, @@ -52,9 +52,9 @@ public sealed class MCTS if (state.IsComplete) return startNode; - if (!state.AvailableActions.HasAction(action)) + if (!state.AvailableActions.HasAction(in simulator.Pool, action)) return startNode; - state.AvailableActions.RemoveAction(action); + state.AvailableActions.RemoveAction(in simulator.Pool, action); startNode = startNode.Add(Execute(simulator, state.State, action, strict)); } @@ -184,7 +184,7 @@ public sealed class MCTS if (initialState.IsComplete) return (initialNode, initialState.CalculateScore(config) ?? 0); - var poppedAction = initialState.AvailableActions.PopRandom(random); + var poppedAction = initialState.AvailableActions.PopRandom(in simulator.Pool, random); var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true)); // playout to a terminal state @@ -198,7 +198,7 @@ public sealed class MCTS while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && actionCount < actions.Length) { - var nextAction = currentActions.SelectRandom(random); + var nextAction = currentActions.SelectRandom(in simulator.Pool, random); actions[actionCount++] = nextAction; (_, currentState) = simulator.Execute(currentState, nextAction); currentCompletionState = simulator.CompletionState; @@ -283,7 +283,7 @@ public sealed class MCTS [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Search(int iterations, ref int progress, CancellationToken token) { - Simulator simulator = new(config.MaxStepCount); + Simulator simulator = new(config.ActionPool, config.MaxStepCount); var random = rootNode.State.State.Input.Random; var staleCounter = 0; var i = 0; diff --git a/Solver/MCTSConfig.cs b/Solver/MCTSConfig.cs index 1bf4e3d..0451981 100644 --- a/Solver/MCTSConfig.cs +++ b/Solver/MCTSConfig.cs @@ -21,6 +21,8 @@ public readonly record struct MCTSConfig public float ScoreCP { get; init; } public float ScoreSteps { get; init; } + public ActionPool ActionPool { get; init; } + public MCTSConfig(in SolverConfig config) { MaxStepCount = config.MaxStepCount; @@ -36,5 +38,7 @@ public readonly record struct MCTSConfig ScoreDurability = config.ScoreDurability; ScoreCP = config.ScoreCP; ScoreSteps = config.ScoreSteps; + + ActionPool = config.ActionPool; } } diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index ea0404f..e31565b 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -7,6 +7,7 @@ namespace Craftimizer.Solver; internal sealed class Simulator : SimulatorNoRandom { + public readonly ActionPool Pool; private readonly int maxStepCount; public override CompletionState CompletionState @@ -20,8 +21,9 @@ internal sealed class Simulator : SimulatorNoRandom } } - public Simulator(int maxStepCount) + public Simulator(in ActionPool pool, int maxStepCount) { + Pool = pool; this.maxStepCount = maxStepCount; } @@ -133,9 +135,9 @@ internal sealed class Simulator : SimulatorNoRandom return new(); var ret = new ActionSet(); - foreach (var action in ActionSet.AcceptedActions) + foreach (var action in Pool.AcceptedActions) if (CanUseAction(action, strict)) - ret.AddAction(action); + ret.AddAction(in Pool, action); return ret; } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index cc43434..e60be04 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -145,7 +145,7 @@ public sealed class Solver : IDisposable var bestSims = new List<(float Score, SolverSolution Result)>(); var state = State; - var sim = new Simulator(Config.MaxStepCount); + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount); var activeStates = new List() { new(Array.Empty(), state) }; @@ -272,7 +272,7 @@ public sealed class Solver : IDisposable var actions = new List(); var state = State; - var sim = new Simulator(Config.MaxStepCount) { State = state }; + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount) { State = state }; while (true) { Token.ThrowIfCancellationRequested(); @@ -338,7 +338,7 @@ public sealed class Solver : IDisposable var actions = new List(); var state = State; - var sim = new Simulator(Config.MaxStepCount) { State = state }; + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount) { State = state }; while (true) { Token.ThrowIfCancellationRequested(); diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index 391ea79..1d03b1d 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -31,6 +31,7 @@ public readonly record struct SolverConfig public float ScoreCP { get; init; } public float ScoreSteps { get; init; } + public ActionPool ActionPool { get; init; } public SolverAlgorithm Algorithm { get; init; } public SolverConfig() @@ -54,6 +55,7 @@ public readonly record struct SolverConfig ScoreCP = .05f; ScoreSteps = .05f; + ActionPool = ActionPool.Default; Algorithm = SolverAlgorithm.StepwiseFurcated; } diff --git a/Test/Solver/ActionSet.cs b/Test/Solver/ActionSet.cs index 7d96f6a..46581a7 100644 --- a/Test/Solver/ActionSet.cs +++ b/Test/Solver/ActionSet.cs @@ -1,20 +1,34 @@ +using System.Runtime.CompilerServices; + namespace Craftimizer.Test.Solver; [TestClass] public class ActionSetTests { + private readonly ActionPool pool = ActionPool.Default; + + [TestMethod] + public void TestActionPoolSize() + { + Assert.AreEqual(ActionPool.EnumSize, Enum.GetValues().Length); + Assert.AreEqual(ActionPool.MaskSize, Unsafe.SizeOf() * 8); + } + [TestMethod] public void TestAcceptedActions() { - var actions = ActionSet.AcceptedActions; - var lut = ActionSet.AcceptedActionsLUT; - - Assert.IsTrue(actions.Length <= 32); foreach (var i in Enum.GetValues()) { - var idx = lut[(byte)i]; - if (idx != -1) - Assert.AreEqual(i, actions[idx]); + byte idx; + try + { + idx = pool.FromAction(i); + } + catch (ArgumentOutOfRangeException) + { + continue; + } + Assert.AreEqual(i, pool.ToAction(idx)); } } @@ -25,14 +39,14 @@ public class ActionSetTests Assert.IsTrue(set.IsEmpty); Assert.AreEqual(0, set.Count); - set.AddAction(ActionType.BasicSynthesis); - set.AddAction(ActionType.WasteNot2); + set.AddAction(in pool, ActionType.BasicSynthesis); + set.AddAction(in pool, ActionType.WasteNot2); Assert.AreEqual(2, set.Count); Assert.IsFalse(set.IsEmpty); - set.RemoveAction(ActionType.BasicSynthesis); - set.RemoveAction(ActionType.WasteNot2); + set.RemoveAction(in pool, ActionType.BasicSynthesis); + set.RemoveAction(in pool, ActionType.WasteNot2); Assert.IsTrue(set.IsEmpty); Assert.AreEqual(0, set.Count); @@ -43,17 +57,17 @@ public class ActionSetTests { var set = new ActionSet(); - Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); - Assert.IsFalse(set.AddAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.AddAction(in pool, ActionType.BasicSynthesis)); + Assert.IsFalse(set.AddAction(in pool, ActionType.BasicSynthesis)); - Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); - Assert.IsFalse(set.RemoveAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.RemoveAction(in pool, ActionType.BasicSynthesis)); + Assert.IsFalse(set.RemoveAction(in pool, ActionType.BasicSynthesis)); - Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); - Assert.IsTrue(set.AddAction(ActionType.WasteNot2)); + Assert.IsTrue(set.AddAction(in pool, ActionType.BasicSynthesis)); + Assert.IsTrue(set.AddAction(in pool, ActionType.WasteNot2)); - Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); - Assert.IsTrue(set.RemoveAction(ActionType.WasteNot2)); + Assert.IsTrue(set.RemoveAction(in pool, ActionType.BasicSynthesis)); + Assert.IsTrue(set.RemoveAction(in pool, ActionType.WasteNot2)); } [TestMethod] @@ -61,18 +75,18 @@ public class ActionSetTests { var set = new ActionSet(); - set.AddAction(ActionType.BasicSynthesis); + set.AddAction(in pool, ActionType.BasicSynthesis); - Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); - Assert.IsFalse(set.HasAction(ActionType.WasteNot2)); + Assert.IsTrue(set.HasAction(in pool, ActionType.BasicSynthesis)); + Assert.IsFalse(set.HasAction(in pool, ActionType.WasteNot2)); - set.AddAction(ActionType.WasteNot2); - Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); - Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); + set.AddAction(in pool, ActionType.WasteNot2); + Assert.IsTrue(set.HasAction(in pool, ActionType.BasicSynthesis)); + Assert.IsTrue(set.HasAction(in pool, ActionType.WasteNot2)); - set.RemoveAction(ActionType.BasicSynthesis); - Assert.IsFalse(set.HasAction(ActionType.BasicSynthesis)); - Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); + set.RemoveAction(in pool, ActionType.BasicSynthesis); + Assert.IsFalse(set.HasAction(in pool, ActionType.BasicSynthesis)); + Assert.IsTrue(set.HasAction(in pool, ActionType.WasteNot2)); } [TestMethod] @@ -80,25 +94,25 @@ public class ActionSetTests { var set = new ActionSet(); - set.AddAction(ActionType.BasicSynthesis); - set.AddAction(ActionType.ByregotsBlessing); - set.AddAction(ActionType.DelicateSynthesis); - set.AddAction(ActionType.Reflect); + set.AddAction(in pool, ActionType.BasicSynthesis); + set.AddAction(in pool, ActionType.ByregotsBlessing); + set.AddAction(in pool, ActionType.DelicateSynthesis); + set.AddAction(in pool, ActionType.Reflect); Assert.AreEqual(4, set.Count); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); - Assert.AreEqual(ActionType.Reflect, set.ElementAt(1)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(2)); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(3)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 0)); + Assert.AreEqual(ActionType.Reflect, set.ElementAt(in pool, 1)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 2)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 3)); - set.RemoveAction(ActionType.Reflect); + set.RemoveAction(in pool, ActionType.Reflect); Assert.AreEqual(3, set.Count); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(2)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 1)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 2)); } [TestMethod] @@ -118,13 +132,13 @@ public class ActionSetTests var set = new ActionSet(); foreach(var action in actions) - set.AddAction(action); + set.AddAction(in pool, action); var counts = new Dictionary(); var rng = new Random(0); for (var i = 0; i < 100; i++) { - var action = set.SelectRandom(rng); + var action = set.SelectRandom(in pool, rng); CollectionAssert.Contains(actions, action); From 2c15b23f4866c7a933ccfbefed860c3ecb8b6838 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 01:10:42 -0800 Subject: [PATCH 02/12] Fix ActionPool bugs --- Solver/ActionPool.cs | 42 +++++++++++++++++++++++++----------------- Solver/Simulator.cs | 4 +++- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Solver/ActionPool.cs b/Solver/ActionPool.cs index 9ab1880..c213ee5 100644 --- a/Solver/ActionPool.cs +++ b/Solver/ActionPool.cs @@ -43,22 +43,36 @@ public readonly struct ActionPool private unsafe struct EnumBuffer { - public fixed byte Data[MaskSize]; + private fixed byte data[MaskSize]; - public ref ActionType this[int index] => ref Unsafe.As(ref Data[index]); + public ActionType this[int index] => (ActionType)data[index]; - public Span AsSpan() => new(Unsafe.AsPointer(ref this[0]), MaskSize); + public EnumBuffer(ReadOnlySpan actions) + { + fixed (byte* dataPtr = data) + actions.CopyTo(new Span(dataPtr, MaskSize)); + } + + public readonly ActionType[] ToArray(int size) + { + fixed (byte* dataPtr = data) + return new Span(dataPtr, size).ToArray(); + } } private unsafe struct LUTBuffer { - public fixed byte Data[EnumSize]; + private fixed byte data[EnumSize]; - public ref byte this[ActionType index] => ref Data[(byte)index]; + public byte this[ActionType index] => data[(byte)index]; -#pragma warning disable MA0099 - public Span AsSpan() => new(Unsafe.AsPointer(ref this[0]), EnumSize); -#pragma warning restore MA0099 + public LUTBuffer(ReadOnlySpan actions) + { + for (var i = 0; i < EnumSize; i++) + data[i] = 0xFF; + for (var i = 0; i < actions.Length; i++) + data[(byte)actions[i]] = (byte)i; + } } // List of accepted actions (max 32) @@ -67,22 +81,16 @@ public readonly struct ActionPool private readonly LUTBuffer acceptedActionsLUT; private readonly byte size; - internal ReadOnlySpan AcceptedActions => acceptedActions.AsSpan().Slice(0, size); + internal ActionType[] AcceptedActions => acceptedActions.ToArray(size); public ActionPool(ReadOnlySpan actions) { if (actions.Length > MaskSize) throw new ArgumentOutOfRangeException(nameof(actions), actions.Length, $"ActionPool only supports up to {MaskSize} actions"); + acceptedActions = new(actions); + acceptedActionsLUT = new(actions); size = (byte)actions.Length; - - acceptedActions.AsSpan().Fill((ActionType)0xFF); - acceptedActionsLUT.AsSpan().Fill(0xFF); - - actions.CopyTo(acceptedActions.AsSpan()); - - for (var i = 0; i < size; i++) - acceptedActionsLUT[acceptedActions[i]] = (byte)i; } [Pure] diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index e31565b..45fc9a4 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -8,6 +8,7 @@ namespace Craftimizer.Solver; internal sealed class Simulator : SimulatorNoRandom { public readonly ActionPool Pool; + private readonly ActionType[] poolActions; private readonly int maxStepCount; public override CompletionState CompletionState @@ -24,6 +25,7 @@ internal sealed class Simulator : SimulatorNoRandom public Simulator(in ActionPool pool, int maxStepCount) { Pool = pool; + poolActions = Pool.AcceptedActions; this.maxStepCount = maxStepCount; } @@ -135,7 +137,7 @@ internal sealed class Simulator : SimulatorNoRandom return new(); var ret = new ActionSet(); - foreach (var action in Pool.AcceptedActions) + foreach (var action in poolActions) if (CanUseAction(action, strict)) ret.AddAction(in Pool, action); return ret; From 85922b225c08f292b28ea5f082b4b0bb8fa899fc Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 02:14:46 -0800 Subject: [PATCH 03/12] Use mask for ActionPool --- Solver/ActionPool.cs | 89 +++++++++++++++------------------------- Solver/ActionSet.cs | 2 +- Solver/Intrinsics.cs | 47 +++++++++++++++++++++ Test/Solver/ActionSet.cs | 49 ++++++++++++++++++---- 4 files changed, 123 insertions(+), 64 deletions(-) diff --git a/Solver/ActionPool.cs b/Solver/ActionPool.cs index c213ee5..c7682a2 100644 --- a/Solver/ActionPool.cs +++ b/Solver/ActionPool.cs @@ -1,5 +1,6 @@ using Craftimizer.Simulator.Actions; using System.Diagnostics.Contracts; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -39,77 +40,55 @@ public readonly struct ActionPool }); public const int MaskSize = 32; - public const int EnumSize = 37; - private unsafe struct EnumBuffer - { - private fixed byte data[MaskSize]; + // Bitmask of accepted actions + private readonly ulong acceptedActions; - public ActionType this[int index] => (ActionType)data[index]; - - public EnumBuffer(ReadOnlySpan actions) - { - fixed (byte* dataPtr = data) - actions.CopyTo(new Span(dataPtr, MaskSize)); - } - - public readonly ActionType[] ToArray(int size) - { - fixed (byte* dataPtr = data) - return new Span(dataPtr, size).ToArray(); - } - } - - private unsafe struct LUTBuffer - { - private fixed byte data[EnumSize]; - - public byte this[ActionType index] => data[(byte)index]; - - public LUTBuffer(ReadOnlySpan actions) - { - for (var i = 0; i < EnumSize; i++) - data[i] = 0xFF; - for (var i = 0; i < actions.Length; i++) - data[(byte)actions[i]] = (byte)i; - } - } - - // List of accepted actions (max 32) - private readonly EnumBuffer acceptedActions; - // Lookup table for accepted actions (ActionType as idx -> idx in acceptedActions) - private readonly LUTBuffer acceptedActionsLUT; - private readonly byte size; - - internal ActionType[] AcceptedActions => acceptedActions.ToArray(size); + internal ActionType[] AcceptedActions => GetActions(); + internal int Count => BitOperations.PopCount(acceptedActions); public ActionPool(ReadOnlySpan actions) { - if (actions.Length > MaskSize) - throw new ArgumentOutOfRangeException(nameof(actions), actions.Length, $"ActionPool only supports up to {MaskSize} actions"); + acceptedActions = 0; + foreach (var action in actions) + acceptedActions |= 1ul << ((byte)action); - acceptedActions = new(actions); - acceptedActionsLUT = new(actions); - size = (byte)actions.Length; + if (Count > MaskSize) + throw new ArgumentOutOfRangeException(nameof(actions), actions.Length, $"ActionPool only supports up to {MaskSize} actions"); } - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal byte FromAction(ActionType action) + private ActionType[] GetActions() { - var ret = acceptedActionsLUT[action]; - if (ret == 0xFF) - throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in this pool."); + var ret = new ActionType[Count]; + var i = 0; + foreach (var v in (byte[])Enum.GetValuesAsUnderlyingType()) + { + if ((acceptedActions & (1ul << v)) != 0) + ret[i++] = (ActionType)v; + } return ret; } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ActionType ToAction(byte index) + internal int FromAction(ActionType action) { - if (index < 0 || index >= size) + if ((acceptedActions & (1ul << (byte)action)) == 0) + throw new ArgumentOutOfRangeException(nameof(action), action, "Action is not accepted by this pool."); + + // Get number of 1s before action + return BitOperations.PopCount(acceptedActions & ((1ul << (byte)action) - 1)); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ActionType ToAction(int index) + { + if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for this pool."); - return acceptedActions[index]; + + // Get index of (index+1)th 1 in set + return (ActionType)Intrinsics.NthBitSet(acceptedActions, index); } [Pure] diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 3a8d92c..989c828 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -34,7 +34,7 @@ public struct ActionSet public readonly bool HasAction(in ActionPool pool, ActionType action) => (bits & pool.ToMask(action)) != 0; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType ElementAt(in ActionPool pool, int index) => pool.ToAction((byte)(Intrinsics.NthBitSet(bits, index) - 1)); + public readonly ActionType ElementAt(in ActionPool pool, int index) => pool.ToAction(Intrinsics.NthBitSet(bits, index) - 1); [Pure] public readonly int Count => BitOperations.PopCount(bits); diff --git a/Solver/Intrinsics.cs b/Solver/Intrinsics.cs index df946a7..d901f7f 100644 --- a/Solver/Intrinsics.cs +++ b/Solver/Intrinsics.cs @@ -92,11 +92,46 @@ internal static class Intrinsics return _base; } + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NthBitSetScalar(ulong value, int n) + { + var mask = 0x00000000FFFFFFFFul; + 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; + } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int NthBitSetBMI2(uint value, int n) => BitOperations.TrailingZeroCount(Bmi2.ParallelBitDeposit(1u << n, value)); + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NthBitSetBMI2(ulong value, int n) => + BitOperations.TrailingZeroCount(Bmi2.X64.ParallelBitDeposit(1ul << n, value)); + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int NthBitSet(uint value, int n) @@ -109,6 +144,18 @@ internal static class Intrinsics NthBitSetScalar(value, n); } + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int NthBitSet(ulong value, int n) + { + if (n >= BitOperations.PopCount(value)) + return 64; + + return Bmi2.X64.IsSupported ? + NthBitSetBMI2(value, n) : + NthBitSetScalar(value, n); + } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector ReciprocalSqrt(Vector data) diff --git a/Test/Solver/ActionSet.cs b/Test/Solver/ActionSet.cs index 46581a7..f2d6e77 100644 --- a/Test/Solver/ActionSet.cs +++ b/Test/Solver/ActionSet.cs @@ -10,16 +10,49 @@ public class ActionSetTests [TestMethod] public void TestActionPoolSize() { - Assert.AreEqual(ActionPool.EnumSize, Enum.GetValues().Length); Assert.AreEqual(ActionPool.MaskSize, Unsafe.SizeOf() * 8); } + [TestMethod] + public void TestActionPoolConstructor() + { + CollectionAssert.AreEquivalent(new ActionType[] + { + ActionType.StandardTouchCombo, + ActionType.AdvancedTouchCombo, + ActionType.FocusedTouchCombo, + ActionType.FocusedSynthesisCombo, + ActionType.TrainedFinesse, + ActionType.PrudentSynthesis, + ActionType.Groundwork, + ActionType.AdvancedTouch, + ActionType.CarefulSynthesis, + ActionType.TrainedEye, + ActionType.DelicateSynthesis, + ActionType.PreparatoryTouch, + ActionType.Reflect, + ActionType.PrudentTouch, + ActionType.Manipulation, + ActionType.MuscleMemory, + ActionType.ByregotsBlessing, + ActionType.WasteNot2, + ActionType.BasicSynthesis, + ActionType.Innovation, + ActionType.GreatStrides, + ActionType.StandardTouch, + ActionType.Veneration, + ActionType.WasteNot, + ActionType.MastersMend, + ActionType.BasicTouch, + }, pool.AcceptedActions); + } + [TestMethod] public void TestAcceptedActions() { foreach (var i in Enum.GetValues()) { - byte idx; + int idx; try { idx = pool.FromAction(i); @@ -101,18 +134,18 @@ public class ActionSetTests Assert.AreEqual(4, set.Count); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 0)); - Assert.AreEqual(ActionType.Reflect, set.ElementAt(in pool, 1)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 2)); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 3)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 1)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 2)); + Assert.AreEqual(ActionType.Reflect, set.ElementAt(in pool, 3)); set.RemoveAction(in pool, ActionType.Reflect); Assert.AreEqual(3, set.Count); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 0)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 0)); Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 1)); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 2)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 2)); } [TestMethod] From c8d3161eb1dadb1f95b419eccaaa245e33df7d84 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 02:47:59 -0800 Subject: [PATCH 04/12] Get rid of ActionPool, use ulong for ActionSet --- Solver/ActionPool.cs | 97 ---------------------------- Solver/ActionSet.cs | 48 ++++++++------ Solver/MCTS.cs | 8 +-- Solver/MCTSConfig.cs | 3 +- Solver/Simulator.cs | 12 ++-- Solver/SolverConfig.cs | 38 ++++++++++- Test/Solver/ActionSet.cs | 132 +++++++++++---------------------------- 7 files changed, 112 insertions(+), 226 deletions(-) delete mode 100644 Solver/ActionPool.cs diff --git a/Solver/ActionPool.cs b/Solver/ActionPool.cs deleted file mode 100644 index c7682a2..0000000 --- a/Solver/ActionPool.cs +++ /dev/null @@ -1,97 +0,0 @@ -using Craftimizer.Simulator.Actions; -using System.Diagnostics.Contracts; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Craftimizer.Solver; - -[StructLayout(LayoutKind.Auto)] -public readonly struct ActionPool -{ - public static ActionPool Default { get; } = new(new[] - { - ActionType.StandardTouchCombo, - ActionType.AdvancedTouchCombo, - ActionType.FocusedTouchCombo, - ActionType.FocusedSynthesisCombo, - ActionType.TrainedFinesse, - ActionType.PrudentSynthesis, - ActionType.Groundwork, - ActionType.AdvancedTouch, - ActionType.CarefulSynthesis, - ActionType.TrainedEye, - ActionType.DelicateSynthesis, - ActionType.PreparatoryTouch, - ActionType.Reflect, - ActionType.PrudentTouch, - ActionType.Manipulation, - ActionType.MuscleMemory, - ActionType.ByregotsBlessing, - ActionType.WasteNot2, - ActionType.BasicSynthesis, - ActionType.Innovation, - ActionType.GreatStrides, - ActionType.StandardTouch, - ActionType.Veneration, - ActionType.WasteNot, - ActionType.MastersMend, - ActionType.BasicTouch, - }); - - public const int MaskSize = 32; - - // Bitmask of accepted actions - private readonly ulong acceptedActions; - - internal ActionType[] AcceptedActions => GetActions(); - internal int Count => BitOperations.PopCount(acceptedActions); - - public ActionPool(ReadOnlySpan actions) - { - acceptedActions = 0; - foreach (var action in actions) - acceptedActions |= 1ul << ((byte)action); - - if (Count > MaskSize) - throw new ArgumentOutOfRangeException(nameof(actions), actions.Length, $"ActionPool only supports up to {MaskSize} actions"); - } - - private ActionType[] GetActions() - { - var ret = new ActionType[Count]; - var i = 0; - foreach (var v in (byte[])Enum.GetValuesAsUnderlyingType()) - { - if ((acceptedActions & (1ul << v)) != 0) - ret[i++] = (ActionType)v; - } - return ret; - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal int FromAction(ActionType action) - { - if ((acceptedActions & (1ul << (byte)action)) == 0) - throw new ArgumentOutOfRangeException(nameof(action), action, "Action is not accepted by this pool."); - - // Get number of 1s before action - return BitOperations.PopCount(acceptedActions & ((1ul << (byte)action) - 1)); - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal ActionType ToAction(int index) - { - if (index < 0 || index >= Count) - throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for this pool."); - - // Get index of (index+1)th 1 in set - return (ActionType)Intrinsics.NthBitSet(acceptedActions, index); - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); -} diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 989c828..860975d 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -7,13 +7,25 @@ namespace Craftimizer.Solver; public struct ActionSet { - internal uint bits; + internal ulong bits; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int FromAction(ActionType action) => (byte)action; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ActionType ToAction(int index) => (ActionType)index; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static ulong ToMask(ActionType action) => 1ul << FromAction(action); // Return true if action was newly added and not there before. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool AddAction(in ActionPool pool, ActionType action) + public bool AddAction(ActionType action) { - var mask = pool.ToMask(action); + var mask = ToMask(action); var old = bits; bits |= mask; return (old & mask) == 0; @@ -21,9 +33,9 @@ public struct ActionSet // Return true if action was newly removed and not already gone. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool RemoveAction(in ActionPool pool, ActionType action) + public bool RemoveAction(ActionType action) { - var mask = pool.ToMask(action); + var mask = ToMask(action); var old = bits; bits &= ~mask; return (old & mask) != 0; @@ -31,10 +43,10 @@ public struct ActionSet [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool HasAction(in ActionPool pool, ActionType action) => (bits & pool.ToMask(action)) != 0; + public readonly bool HasAction(ActionType action) => (bits & ToMask(action)) != 0; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType ElementAt(in ActionPool pool, int index) => pool.ToAction(Intrinsics.NthBitSet(bits, index) - 1); + public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index)); [Pure] public readonly int Count => BitOperations.PopCount(bits); @@ -43,38 +55,38 @@ public struct ActionSet public readonly bool IsEmpty => bits == 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType SelectRandom(in ActionPool pool, Random random) + public readonly ActionType SelectRandom(Random random) { #if IS_DETERMINISTIC - return First(in pool); + return First(); #else - return ElementAt(in pool, random.Next(Count)); + return ElementAt(random.Next(Count)); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ActionType PopRandom(in ActionPool pool, Random random) + public ActionType PopRandom(Random random) { #if IS_DETERMINISTIC - return PopFirst(in pool); + return PopFirst(); #else - var action = ElementAt(in pool, random.Next(Count)); - RemoveAction(in pool, action); + var action = ElementAt(random.Next(Count)); + RemoveAction(action); return action; #endif } #if IS_DETERMINISTIC [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ActionType PopFirst(in pool) + private ActionType PopFirst() { - var action = First(in pool); - RemoveAction(in pool, action); + var action = First(); + RemoveAction(action); return action; } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private readonly ActionType First(in pool) => ElementAt(in pool, 0); + private readonly ActionType First() => ElementAt(0); #endif } diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs index 712aad8..afcd89e 100644 --- a/Solver/MCTS.cs +++ b/Solver/MCTS.cs @@ -52,9 +52,9 @@ public sealed class MCTS if (state.IsComplete) return startNode; - if (!state.AvailableActions.HasAction(in simulator.Pool, action)) + if (!state.AvailableActions.HasAction(action)) return startNode; - state.AvailableActions.RemoveAction(in simulator.Pool, action); + state.AvailableActions.RemoveAction(action); startNode = startNode.Add(Execute(simulator, state.State, action, strict)); } @@ -184,7 +184,7 @@ public sealed class MCTS if (initialState.IsComplete) return (initialNode, initialState.CalculateScore(config) ?? 0); - var poppedAction = initialState.AvailableActions.PopRandom(in simulator.Pool, random); + var poppedAction = initialState.AvailableActions.PopRandom(random); var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true)); // playout to a terminal state @@ -198,7 +198,7 @@ public sealed class MCTS while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && actionCount < actions.Length) { - var nextAction = currentActions.SelectRandom(in simulator.Pool, random); + var nextAction = currentActions.SelectRandom(random); actions[actionCount++] = nextAction; (_, currentState) = simulator.Execute(currentState, nextAction); currentCompletionState = simulator.CompletionState; diff --git a/Solver/MCTSConfig.cs b/Solver/MCTSConfig.cs index 0451981..8f7f091 100644 --- a/Solver/MCTSConfig.cs +++ b/Solver/MCTSConfig.cs @@ -1,3 +1,4 @@ +using Craftimizer.Simulator.Actions; using System.Runtime.InteropServices; namespace Craftimizer.Solver; @@ -21,7 +22,7 @@ public readonly record struct MCTSConfig public float ScoreCP { get; init; } public float ScoreSteps { get; init; } - public ActionPool ActionPool { get; init; } + public ActionType[] ActionPool { get; init; } public MCTSConfig(in SolverConfig config) { diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index 45fc9a4..eb2176e 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -7,8 +7,7 @@ namespace Craftimizer.Solver; internal sealed class Simulator : SimulatorNoRandom { - public readonly ActionPool Pool; - private readonly ActionType[] poolActions; + private readonly ActionType[] actionPool; private readonly int maxStepCount; public override CompletionState CompletionState @@ -22,10 +21,9 @@ internal sealed class Simulator : SimulatorNoRandom } } - public Simulator(in ActionPool pool, int maxStepCount) + public Simulator(ActionType[] actionPool, int maxStepCount) { - Pool = pool; - poolActions = Pool.AcceptedActions; + this.actionPool = actionPool; this.maxStepCount = maxStepCount; } @@ -137,9 +135,9 @@ internal sealed class Simulator : SimulatorNoRandom return new(); var ret = new ActionSet(); - foreach (var action in poolActions) + foreach (var action in actionPool) if (CanUseAction(action, strict)) - ret.AddAction(in Pool, action); + ret.AddAction(action); return ret; } diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index 1d03b1d..f6c9830 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -1,3 +1,4 @@ +using Craftimizer.Simulator.Actions; using System.Runtime.InteropServices; namespace Craftimizer.Solver; @@ -31,7 +32,7 @@ public readonly record struct SolverConfig public float ScoreCP { get; init; } public float ScoreSteps { get; init; } - public ActionPool ActionPool { get; init; } + public ActionType[] ActionPool { get; init; } public SolverAlgorithm Algorithm { get; init; } public SolverConfig() @@ -55,10 +56,43 @@ public readonly record struct SolverConfig ScoreCP = .05f; ScoreSteps = .05f; - ActionPool = ActionPool.Default; + ActionPool = DefaultActionPool; Algorithm = SolverAlgorithm.StepwiseFurcated; } + public static ActionType[] OptimizeActionPool(IEnumerable actions) => + actions.Order().ToArray(); + + public static readonly ActionType[] DefaultActionPool = OptimizeActionPool(new[] + { + ActionType.StandardTouchCombo, + ActionType.AdvancedTouchCombo, + ActionType.FocusedTouchCombo, + ActionType.FocusedSynthesisCombo, + ActionType.TrainedFinesse, + ActionType.PrudentSynthesis, + ActionType.Groundwork, + ActionType.AdvancedTouch, + ActionType.CarefulSynthesis, + ActionType.TrainedEye, + ActionType.DelicateSynthesis, + ActionType.PreparatoryTouch, + ActionType.Reflect, + ActionType.PrudentTouch, + ActionType.Manipulation, + ActionType.MuscleMemory, + ActionType.ByregotsBlessing, + ActionType.WasteNot2, + ActionType.BasicSynthesis, + ActionType.Innovation, + ActionType.GreatStrides, + ActionType.StandardTouch, + ActionType.Veneration, + ActionType.WasteNot, + ActionType.MastersMend, + ActionType.BasicTouch, + }); + public static readonly SolverConfig SimulatorDefault = new SolverConfig() with { diff --git a/Test/Solver/ActionSet.cs b/Test/Solver/ActionSet.cs index f2d6e77..ec170fa 100644 --- a/Test/Solver/ActionSet.cs +++ b/Test/Solver/ActionSet.cs @@ -1,70 +1,8 @@ -using System.Runtime.CompilerServices; - namespace Craftimizer.Test.Solver; [TestClass] public class ActionSetTests { - private readonly ActionPool pool = ActionPool.Default; - - [TestMethod] - public void TestActionPoolSize() - { - Assert.AreEqual(ActionPool.MaskSize, Unsafe.SizeOf() * 8); - } - - [TestMethod] - public void TestActionPoolConstructor() - { - CollectionAssert.AreEquivalent(new ActionType[] - { - ActionType.StandardTouchCombo, - ActionType.AdvancedTouchCombo, - ActionType.FocusedTouchCombo, - ActionType.FocusedSynthesisCombo, - ActionType.TrainedFinesse, - ActionType.PrudentSynthesis, - ActionType.Groundwork, - ActionType.AdvancedTouch, - ActionType.CarefulSynthesis, - ActionType.TrainedEye, - ActionType.DelicateSynthesis, - ActionType.PreparatoryTouch, - ActionType.Reflect, - ActionType.PrudentTouch, - ActionType.Manipulation, - ActionType.MuscleMemory, - ActionType.ByregotsBlessing, - ActionType.WasteNot2, - ActionType.BasicSynthesis, - ActionType.Innovation, - ActionType.GreatStrides, - ActionType.StandardTouch, - ActionType.Veneration, - ActionType.WasteNot, - ActionType.MastersMend, - ActionType.BasicTouch, - }, pool.AcceptedActions); - } - - [TestMethod] - public void TestAcceptedActions() - { - foreach (var i in Enum.GetValues()) - { - int idx; - try - { - idx = pool.FromAction(i); - } - catch (ArgumentOutOfRangeException) - { - continue; - } - Assert.AreEqual(i, pool.ToAction(idx)); - } - } - [TestMethod] public void TestSize() { @@ -72,14 +10,14 @@ public class ActionSetTests Assert.IsTrue(set.IsEmpty); Assert.AreEqual(0, set.Count); - set.AddAction(in pool, ActionType.BasicSynthesis); - set.AddAction(in pool, ActionType.WasteNot2); + set.AddAction(ActionType.BasicSynthesis); + set.AddAction(ActionType.WasteNot2); Assert.AreEqual(2, set.Count); Assert.IsFalse(set.IsEmpty); - set.RemoveAction(in pool, ActionType.BasicSynthesis); - set.RemoveAction(in pool, ActionType.WasteNot2); + set.RemoveAction(ActionType.BasicSynthesis); + set.RemoveAction(ActionType.WasteNot2); Assert.IsTrue(set.IsEmpty); Assert.AreEqual(0, set.Count); @@ -90,17 +28,17 @@ public class ActionSetTests { var set = new ActionSet(); - Assert.IsTrue(set.AddAction(in pool, ActionType.BasicSynthesis)); - Assert.IsFalse(set.AddAction(in pool, ActionType.BasicSynthesis)); + Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); + Assert.IsFalse(set.AddAction(ActionType.BasicSynthesis)); - Assert.IsTrue(set.RemoveAction(in pool, ActionType.BasicSynthesis)); - Assert.IsFalse(set.RemoveAction(in pool, ActionType.BasicSynthesis)); + Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); + Assert.IsFalse(set.RemoveAction(ActionType.BasicSynthesis)); - Assert.IsTrue(set.AddAction(in pool, ActionType.BasicSynthesis)); - Assert.IsTrue(set.AddAction(in pool, ActionType.WasteNot2)); + Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.AddAction(ActionType.WasteNot2)); - Assert.IsTrue(set.RemoveAction(in pool, ActionType.BasicSynthesis)); - Assert.IsTrue(set.RemoveAction(in pool, ActionType.WasteNot2)); + Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.RemoveAction(ActionType.WasteNot2)); } [TestMethod] @@ -108,18 +46,18 @@ public class ActionSetTests { var set = new ActionSet(); - set.AddAction(in pool, ActionType.BasicSynthesis); + set.AddAction(ActionType.BasicSynthesis); - Assert.IsTrue(set.HasAction(in pool, ActionType.BasicSynthesis)); - Assert.IsFalse(set.HasAction(in pool, ActionType.WasteNot2)); + Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); + Assert.IsFalse(set.HasAction(ActionType.WasteNot2)); - set.AddAction(in pool, ActionType.WasteNot2); - Assert.IsTrue(set.HasAction(in pool, ActionType.BasicSynthesis)); - Assert.IsTrue(set.HasAction(in pool, ActionType.WasteNot2)); + set.AddAction(ActionType.WasteNot2); + Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); - set.RemoveAction(in pool, ActionType.BasicSynthesis); - Assert.IsFalse(set.HasAction(in pool, ActionType.BasicSynthesis)); - Assert.IsTrue(set.HasAction(in pool, ActionType.WasteNot2)); + set.RemoveAction(ActionType.BasicSynthesis); + Assert.IsFalse(set.HasAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); } [TestMethod] @@ -127,25 +65,25 @@ public class ActionSetTests { var set = new ActionSet(); - set.AddAction(in pool, ActionType.BasicSynthesis); - set.AddAction(in pool, ActionType.ByregotsBlessing); - set.AddAction(in pool, ActionType.DelicateSynthesis); - set.AddAction(in pool, ActionType.Reflect); + set.AddAction(ActionType.BasicSynthesis); + set.AddAction(ActionType.ByregotsBlessing); + set.AddAction(ActionType.DelicateSynthesis); + set.AddAction(ActionType.Reflect); Assert.AreEqual(4, set.Count); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 0)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 1)); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 2)); - Assert.AreEqual(ActionType.Reflect, set.ElementAt(in pool, 3)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); + Assert.AreEqual(ActionType.Reflect, set.ElementAt(3)); - set.RemoveAction(in pool, ActionType.Reflect); + set.RemoveAction(ActionType.Reflect); Assert.AreEqual(3, set.Count); - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(in pool, 0)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(in pool, 1)); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(in pool, 2)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); } [TestMethod] @@ -165,13 +103,13 @@ public class ActionSetTests var set = new ActionSet(); foreach(var action in actions) - set.AddAction(in pool, action); + set.AddAction(action); var counts = new Dictionary(); var rng = new Random(0); for (var i = 0; i < 100; i++) { - var action = set.SelectRandom(in pool, rng); + var action = set.SelectRandom(rng); CollectionAssert.Contains(actions, action); From a75a9b1be57e34a43d62c12de549ab416a058e90 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 03:32:12 -0800 Subject: [PATCH 05/12] Minor microoptimization --- Solver/ActionSet.cs | 8 ++++---- Solver/Simulator.cs | 12 +++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 860975d..796657e 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -7,19 +7,19 @@ namespace Craftimizer.Solver; public struct ActionSet { - internal ulong bits; + private ulong bits; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int FromAction(ActionType action) => (byte)action; + private static int FromAction(ActionType action) => (byte)action; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ActionType ToAction(int index) => (ActionType)index; + private static ActionType ToAction(int index) => (ActionType)index; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static ulong ToMask(ActionType action) => 1ul << FromAction(action); + private static ulong ToMask(ActionType action) => 1ul << FromAction(action); // Return true if action was newly added and not there before. [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index eb2176e..8ddea8f 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -7,7 +7,7 @@ namespace Craftimizer.Solver; internal sealed class Simulator : SimulatorNoRandom { - private readonly ActionType[] actionPool; + private readonly (BaseAction Data, ActionType Action)[] actionPoolObjects; private readonly int maxStepCount; public override CompletionState CompletionState @@ -23,7 +23,7 @@ internal sealed class Simulator : SimulatorNoRandom public Simulator(ActionType[] actionPool, int maxStepCount) { - this.actionPool = actionPool; + actionPoolObjects = actionPool.Select(x => (x.Base(), x)).ToArray(); this.maxStepCount = maxStepCount; } @@ -32,11 +32,9 @@ internal sealed class Simulator : SimulatorNoRandom [MethodImpl(MethodImplOptions.AggressiveInlining)] // It's just a bunch of if statements, I would assume this is actually quite simple to follow #pragma warning disable MA0051 // Method is too long - private bool CanUseAction(ActionType action, bool strict) + private bool CanUseAction(ActionType action, BaseAction baseAction, bool strict) #pragma warning restore MA0051 // Method is too long { - var baseAction = action.Base(); - if (CalculateSuccessRate(baseAction.SuccessRate(this)) != 1) return false; @@ -135,8 +133,8 @@ internal sealed class Simulator : SimulatorNoRandom return new(); var ret = new ActionSet(); - foreach (var action in actionPool) - if (CanUseAction(action, strict)) + foreach (var (data, action) in actionPoolObjects) + if (CanUseAction(action, data, strict)) ret.AddAction(action); return ret; } From aadde107523f3ab5739cdd3630b3a7cfd83397d1 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 3 Mar 2024 13:19:57 -0800 Subject: [PATCH 06/12] Somewhat fix large macro names --- Craftimizer/ImGuiUtils.cs | 5 +---- Craftimizer/Windows/RecipeNote.cs | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 7838d75..4102c7d 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -620,10 +620,7 @@ internal static class ImGuiUtils var lineSize = font.CalcWordWrapPositionA(1, textBuf, currentWrapWidth) ?? textBuf.Length; var lineBuf = textBuf[..lineSize]; ImGui.Text(lineBuf.ToString()); - var remainingBuf = textBuf[lineSize..]; - - while (!remainingBuf.IsEmpty && char.IsWhiteSpace(remainingBuf[0])) - remainingBuf = remainingBuf[1..]; + var remainingBuf = textBuf[lineSize..].TrimStart(); if (!remainingBuf.IsEmpty) { diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 1861829..6ad24b6 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -843,15 +843,14 @@ public sealed unsafe class RecipeNote : Window, IDisposable if (state.MacroName is { } macroName) { + using var _ = ImRaii2.TextWrapPos(panelWidth); if (state.MacroUrl is { } macroUrl) { ImGuiUtils.AlignCentered(ImGui.CalcTextSize(macroName).X, panelWidth); ImGuiUtils.Hyperlink(macroName, macroUrl, false); } else - { ImGuiUtils.TextCentered(macroName, panelWidth); - } } using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); From 6f094c58ae043532a337f86922883fdcc4d0547c Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 3 Mar 2024 18:58:57 -0800 Subject: [PATCH 07/12] Split CanUse into CouldUse & IsPossible --- Simulator/Actions/BaseAction.cs | 15 +++++++++++++-- Simulator/Actions/BaseComboAction.cs | 4 ++-- Simulator/Actions/BaseComboActionImpl.cs | 6 ++++-- Simulator/Actions/ByregotsBlessing.cs | 2 +- Simulator/Actions/CarefulObservation.cs | 7 +++++-- Simulator/Actions/HeartAndSoul.cs | 7 +++++-- Simulator/Actions/IntensiveSynthesis.cs | 4 ++-- Simulator/Actions/Manipulation.cs | 4 +++- Simulator/Actions/MuscleMemory.cs | 4 +++- Simulator/Actions/PreciseTouch.cs | 4 ++-- Simulator/Actions/PrudentSynthesis.cs | 4 ++-- Simulator/Actions/PrudentTouch.cs | 4 ++-- Simulator/Actions/Reflect.cs | 4 +++- Simulator/Actions/TrainedEye.cs | 7 ++++--- Simulator/Actions/TrainedFinesse.cs | 4 ++-- Simulator/Actions/TricksOfTheTrade.cs | 4 ++-- Simulator/Simulator.cs | 11 ++++++++++- 17 files changed, 65 insertions(+), 30 deletions(-) diff --git a/Simulator/Actions/BaseAction.cs b/Simulator/Actions/BaseAction.cs index 07c4cee..e650a00 100644 --- a/Simulator/Actions/BaseAction.cs +++ b/Simulator/Actions/BaseAction.cs @@ -25,8 +25,19 @@ public abstract class BaseAction public virtual int Efficiency(Simulator s) => 0; public virtual float SuccessRate(Simulator s) => 1f; - public virtual bool CanUse(Simulator s) => - s.Input.Stats.Level >= Level && s.CP >= CPCost(s); + // Return true if it can be in the action pool now or in the future + // e.g. if Heart and Soul is already used, it is impossible to use it again + // or if it's a first step action and IsFirstStep is false + public virtual bool IsPossible(Simulator s) => + s.Input.Stats.Level >= Level; + + // Return true if it can be used now + // This already assumes that IsPossible returns true *at some point before* + public virtual bool CouldUse(Simulator s) => + s.CP >= CPCost(s); + + public bool CanUse(Simulator s) => + IsPossible(s) && CouldUse(s); public virtual void Use(Simulator s) { diff --git a/Simulator/Actions/BaseComboAction.cs b/Simulator/Actions/BaseComboAction.cs index 16876c5..5151f4b 100644 --- a/Simulator/Actions/BaseComboAction.cs +++ b/Simulator/Actions/BaseComboAction.cs @@ -7,8 +7,8 @@ public abstract class BaseComboAction : BaseAction public sealed override ActionCategory Category => ActionCategory.Combo; - protected bool BaseCanUse(Simulator s) => - base.CanUse(s); + protected bool BaseCouldUse(Simulator s) => + base.CouldUse(s); private static bool VerifyDurability2(int durabilityA, int durability, in Effects effects) { diff --git a/Simulator/Actions/BaseComboActionImpl.cs b/Simulator/Actions/BaseComboActionImpl.cs index 623fd53..6f87f35 100644 --- a/Simulator/Actions/BaseComboActionImpl.cs +++ b/Simulator/Actions/BaseComboActionImpl.cs @@ -13,8 +13,10 @@ internal abstract class BaseComboAction : BaseComboAction where A : BaseAc public override int CPCost(Simulator s) => ActionA.CPCost(s) + ActionB.CPCost(s); - public override bool CanUse(Simulator s) => - BaseCanUse(s) && VerifyDurability2(s, ActionA.DurabilityCost); + public override bool IsPossible(Simulator s) => ActionA.IsPossible(s) && ActionB.IsPossible(s); + + public override bool CouldUse(Simulator s) => + BaseCouldUse(s) && VerifyDurability2(s, ActionA.DurabilityCost); public override void Use(Simulator s) { diff --git a/Simulator/Actions/ByregotsBlessing.cs b/Simulator/Actions/ByregotsBlessing.cs index 257db8e..7e29798 100644 --- a/Simulator/Actions/ByregotsBlessing.cs +++ b/Simulator/Actions/ByregotsBlessing.cs @@ -11,7 +11,7 @@ internal sealed class ByregotsBlessing : BaseAction public override int CPCost(Simulator s) => 24; public override int Efficiency(Simulator s) => 100 + (20 * s.GetEffectStrength(EffectType.InnerQuiet)); - public override bool CanUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CanUse(s); + public override bool CouldUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/CarefulObservation.cs b/Simulator/Actions/CarefulObservation.cs index ebee287..5ae64d8 100644 --- a/Simulator/Actions/CarefulObservation.cs +++ b/Simulator/Actions/CarefulObservation.cs @@ -12,10 +12,13 @@ internal sealed class CarefulObservation : BaseAction public override int CPCost(Simulator s) => 0; - public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3; + public override bool IsPossible(Simulator s) => + base.IsPossible(s) && s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3; + + public override bool CouldUse(Simulator s) => s.ActionStates.CarefulObservationCount < 3; public override void UseSuccess(Simulator s) => s.StepCondition(); public override string GetTooltip(Simulator s, bool addUsability) => - $"{base.GetTooltip(s, addUsability)}Specialist Only"; + $"{base.GetTooltip(s, addUsability)}Specialist Only\n"; } diff --git a/Simulator/Actions/HeartAndSoul.cs b/Simulator/Actions/HeartAndSoul.cs index 16e7b1a..06e183b 100644 --- a/Simulator/Actions/HeartAndSoul.cs +++ b/Simulator/Actions/HeartAndSoul.cs @@ -13,8 +13,11 @@ internal sealed class HeartAndSoul : BaseBuffAction public override int CPCost(Simulator s) => 0; - public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; + public override bool IsPossible(Simulator s) => + base.IsPossible(s) && s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; + + public override bool CouldUse(Simulator s) => !s.ActionStates.UsedHeartAndSoul; public override string GetTooltip(Simulator s, bool addUsability) => - $"{GetBaseTooltip(s, addUsability)}Specialist Only"; + $"{GetBaseTooltip(s, addUsability)}Specialist Only\n"; } diff --git a/Simulator/Actions/IntensiveSynthesis.cs b/Simulator/Actions/IntensiveSynthesis.cs index 54db7b0..e54f3d2 100644 --- a/Simulator/Actions/IntensiveSynthesis.cs +++ b/Simulator/Actions/IntensiveSynthesis.cs @@ -11,9 +11,9 @@ internal sealed class IntensiveSynthesis : BaseAction public override int CPCost(Simulator s) => 6; public override int Efficiency(Simulator s) => 400; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) - && base.CanUse(s); + && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/Manipulation.cs b/Simulator/Actions/Manipulation.cs index 804ee65..56dfa30 100644 --- a/Simulator/Actions/Manipulation.cs +++ b/Simulator/Actions/Manipulation.cs @@ -10,7 +10,9 @@ internal sealed class Manipulation : BaseBuffAction public override byte Duration => 8; public override int CPCost(Simulator s) => 96; - public override bool CanUse(Simulator s) => s.Input.Stats.CanUseManipulation && base.CanUse(s); + + public override bool IsPossible(Simulator s) => + s.Input.Stats.CanUseManipulation && base.IsPossible(s); public override void Use(Simulator s) { diff --git a/Simulator/Actions/MuscleMemory.cs b/Simulator/Actions/MuscleMemory.cs index f6eb09d..bde7941 100644 --- a/Simulator/Actions/MuscleMemory.cs +++ b/Simulator/Actions/MuscleMemory.cs @@ -11,7 +11,9 @@ internal sealed class MuscleMemory : BaseAction public override int CPCost(Simulator s) => 6; public override int Efficiency(Simulator s) => 300; - public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s); + public override bool IsPossible(Simulator s) => s.IsFirstStep && base.IsPossible(s); + + public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/PreciseTouch.cs b/Simulator/Actions/PreciseTouch.cs index 1be9c2d..1b4766e 100644 --- a/Simulator/Actions/PreciseTouch.cs +++ b/Simulator/Actions/PreciseTouch.cs @@ -11,9 +11,9 @@ internal sealed class PreciseTouch : BaseAction public override int CPCost(Simulator s) => 18; public override int Efficiency(Simulator s) => 150; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) - && base.CanUse(s); + && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/PrudentSynthesis.cs b/Simulator/Actions/PrudentSynthesis.cs index 1ac3a22..a441fe9 100644 --- a/Simulator/Actions/PrudentSynthesis.cs +++ b/Simulator/Actions/PrudentSynthesis.cs @@ -12,7 +12,7 @@ internal sealed class PrudentSynthesis : BaseAction public override int CPCost(Simulator s) => 18; public override int Efficiency(Simulator s) => 180; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) - && base.CanUse(s); + && base.CouldUse(s); } diff --git a/Simulator/Actions/PrudentTouch.cs b/Simulator/Actions/PrudentTouch.cs index f01f814..2298f10 100644 --- a/Simulator/Actions/PrudentTouch.cs +++ b/Simulator/Actions/PrudentTouch.cs @@ -12,7 +12,7 @@ internal sealed class PrudentTouch : BaseAction public override int CPCost(Simulator s) => 25; public override int Efficiency(Simulator s) => 100; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) - && base.CanUse(s); + && base.CouldUse(s); } diff --git a/Simulator/Actions/Reflect.cs b/Simulator/Actions/Reflect.cs index 9a7dbdc..28fc811 100644 --- a/Simulator/Actions/Reflect.cs +++ b/Simulator/Actions/Reflect.cs @@ -11,7 +11,9 @@ internal sealed class Reflect : BaseAction public override int CPCost(Simulator s) => 6; public override int Efficiency(Simulator s) => 100; - public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s); + public override bool IsPossible(Simulator s) => s.IsFirstStep && base.IsPossible(s); + + public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/TrainedEye.cs b/Simulator/Actions/TrainedEye.cs index 494fb0c..94c4990 100644 --- a/Simulator/Actions/TrainedEye.cs +++ b/Simulator/Actions/TrainedEye.cs @@ -10,11 +10,12 @@ internal sealed class TrainedEye : BaseAction public override int CPCost(Simulator s) => 250; - public override bool CanUse(Simulator s) => - s.IsFirstStep && + public override bool IsPossible(Simulator s) => s.IsFirstStep && !s.Input.Recipe.IsExpert && s.Input.Stats.Level >= (s.Input.Recipe.ClassJobLevel + 10) && - base.CanUse(s); + base.IsPossible(s); + + public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); public override void UseSuccess(Simulator s) => s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality); diff --git a/Simulator/Actions/TrainedFinesse.cs b/Simulator/Actions/TrainedFinesse.cs index 28026c2..724d0aa 100644 --- a/Simulator/Actions/TrainedFinesse.cs +++ b/Simulator/Actions/TrainedFinesse.cs @@ -12,7 +12,7 @@ internal sealed class TrainedFinesse : BaseAction public override int CPCost(Simulator s) => 32; public override int Efficiency(Simulator s) => 100; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => s.GetEffectStrength(EffectType.InnerQuiet) == 10 - && base.CanUse(s); + && base.CouldUse(s); } diff --git a/Simulator/Actions/TricksOfTheTrade.cs b/Simulator/Actions/TricksOfTheTrade.cs index ae9f5d2..22a375f 100644 --- a/Simulator/Actions/TricksOfTheTrade.cs +++ b/Simulator/Actions/TricksOfTheTrade.cs @@ -10,9 +10,9 @@ internal sealed class TricksOfTheTrade : BaseAction public override int CPCost(Simulator s) => 0; - public override bool CanUse(Simulator s) => + public override bool CouldUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) - && base.CanUse(s); + && base.CouldUse(s); public override void UseSuccess(Simulator s) { diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index 75364f5..b984d9b 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -35,7 +35,12 @@ public class Simulator } public bool IsComplete => CompletionState != CompletionState.Incomplete; - public IEnumerable AvailableActions => ActionUtils.AvailableActions(this); + public SimulationState ExecuteUnchecked(in SimulationState state, ActionType action) + { + this.state = state; + ExecuteUnchecked(action); + return this.state; + } public (ActionResponse Response, SimulationState NewState) Execute(in SimulationState state, ActionType action) { @@ -43,6 +48,10 @@ public class Simulator return (Execute(action), this.state); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ExecuteUnchecked(ActionType action) => + action.Base().Use(this); + private ActionResponse Execute(ActionType action) { if (IsComplete) From 923f29cf3cea4fbfa6d08002da92f2f2209f1882 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 3 Mar 2024 19:00:39 -0800 Subject: [PATCH 08/12] Increase arena node size 24 -> 32 --- Solver/ArenaBuffer.cs | 23 +++++++++++++---------- Solver/NodeScoresBuffer.cs | 23 +++++++---------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Solver/ArenaBuffer.cs b/Solver/ArenaBuffer.cs index 6405a85..dda988a 100644 --- a/Solver/ArenaBuffer.cs +++ b/Solver/ArenaBuffer.cs @@ -4,31 +4,34 @@ using System.Runtime.CompilerServices; namespace Craftimizer.Solver; -// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs -public struct ArenaBuffer where T : struct +public struct ArenaBuffer { // Technically 25, but it's very unlikely to actually get to there. // The benchmark reaches 20 at most, but here we have a little leeway just in case. - private const int MaxSize = 24; + internal const int MaxSize = 32; - private static readonly int BatchSize = Vector.Count; - private static readonly int BatchSizeBits = int.Log2(BatchSize); - private static readonly int BatchSizeMask = BatchSize - 1; + internal static readonly int BatchSize = Vector.Count; + internal static readonly int BatchSizeBits = int.Log2(BatchSize); + internal static readonly int BatchSizeMask = BatchSize - 1; - private static readonly int BatchCount = MaxSize / BatchSize; + internal static readonly int BatchCount = MaxSize / BatchSize; +} +// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs +public struct ArenaBuffer where T : struct +{ public ArenaNode[][] Data; public int Count { get; private set; } public void Add(ArenaNode node) { - Data ??= new ArenaNode[BatchCount][]; + Data ??= new ArenaNode[ArenaBuffer.BatchCount][]; var idx = Count++; var (arrayIdx, subIdx) = GetArrayIndex(idx); - Data[arrayIdx] ??= new ArenaNode[BatchSize]; + Data[arrayIdx] ??= new ArenaNode[ArenaBuffer.BatchSize]; node.ChildIdx = (arrayIdx, subIdx); Data[arrayIdx][subIdx] = node; @@ -37,5 +40,5 @@ public struct ArenaBuffer where T : struct [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int arrayIdx, int subIdx) GetArrayIndex(int idx) => - (idx >> BatchSizeBits, idx & BatchSizeMask); + (idx >> ArenaBuffer.BatchSizeBits, idx & ArenaBuffer.BatchSizeMask); } diff --git a/Solver/NodeScoresBuffer.cs b/Solver/NodeScoresBuffer.cs index 5d45e59..1178267 100644 --- a/Solver/NodeScoresBuffer.cs +++ b/Solver/NodeScoresBuffer.cs @@ -1,12 +1,13 @@ using System.Diagnostics.Contracts; -using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Craftimizer.Solver; // Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs public struct NodeScoresBuffer { + [StructLayout(LayoutKind.Auto)] public readonly struct ScoresBatch { public readonly Memory ScoreSum; @@ -15,28 +16,18 @@ public struct NodeScoresBuffer public ScoresBatch() { - ScoreSum = new float[BatchSize]; - MaxScore = new float[BatchSize]; - Visits = new int[BatchSize]; + ScoreSum = new float[ArenaBuffer.BatchSize]; + MaxScore = new float[ArenaBuffer.BatchSize]; + Visits = new int[ArenaBuffer.BatchSize]; } } - // Technically 25, but it's very unlikely to actually get to there. - // The benchmark reaches 20 at most, but here we have a little leeway just in case. - private const int MaxSize = 24; - - private static readonly int BatchSize = Vector.Count; - private static readonly int BatchSizeBits = int.Log2(BatchSize); - private static readonly int BatchSizeMask = BatchSize - 1; - - private static readonly int BatchCount = MaxSize / BatchSize; - public ScoresBatch[] Data; public int Count { get; private set; } public void Add() { - Data ??= new ScoresBatch[BatchCount]; + Data ??= new ScoresBatch[ArenaBuffer.BatchCount]; var idx = Count++; @@ -59,5 +50,5 @@ public struct NodeScoresBuffer [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int arrayIdx, int subIdx) GetArrayIndex(int idx) => - (idx >> BatchSizeBits, idx & BatchSizeMask); + (idx >> ArenaBuffer.BatchSizeBits, idx & ArenaBuffer.BatchSizeMask); } From 2f0e30e4c27f23327cc933ff2799a35f97f7797e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 3 Mar 2024 19:02:44 -0800 Subject: [PATCH 09/12] Solver optimizations & allow action pools --- Solver/ArenaNode.cs | 6 +++--- Solver/MCTS.cs | 36 +++++++----------------------------- Solver/Simulator.cs | 19 ++++++++++++------- Solver/Solver.cs | 4 ++-- 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/Solver/ArenaNode.cs b/Solver/ArenaNode.cs index 5d65e9f..ebafacf 100644 --- a/Solver/ArenaNode.cs +++ b/Solver/ArenaNode.cs @@ -12,7 +12,7 @@ public sealed class ArenaNode where T : struct public NodeScoresBuffer? ParentScores => Parent?.ChildScores; - public ArenaNode(T state, ArenaNode? parent = null) + public ArenaNode(in T state, ArenaNode? parent = null) { State = state; Children = new(); @@ -24,9 +24,9 @@ public sealed class ArenaNode where T : struct Children.Data?[at.arrayIdx]?[at.subIdx]; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArenaNode Add(T state) + public ArenaNode Add(in T state) { - var node = new ArenaNode(state, this); + var node = new ArenaNode(in state, this); ChildScores.Add(); Children.Add(node); return node; diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs index afcd89e..20454a4 100644 --- a/Solver/MCTS.cs +++ b/Solver/MCTS.cs @@ -3,7 +3,6 @@ using Craftimizer.Simulator; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; -using System.Text; using Node = Craftimizer.Solver.ArenaNode; namespace Craftimizer.Solver; @@ -23,7 +22,7 @@ public sealed class MCTS public MCTS(in MCTSConfig config, in SimulationState state) { this.config = config; - var sim = new Simulator(config.ActionPool, config.MaxStepCount) { State = state }; + var sim = new Simulator(config.ActionPool, config.MaxStepCount, state); rootNode = new(new( state, null, @@ -35,7 +34,7 @@ public sealed class MCTS private static SimulationNode Execute(Simulator simulator, in SimulationState state, ActionType action, bool strict) { - (_, var newState) = simulator.Execute(state, action); + var newState = simulator.ExecuteUnchecked(state, action); return new( newState, action, @@ -192,7 +191,6 @@ public sealed class MCTS var currentCompletionState = expandedNode.State.SimulationCompletionState; var currentActions = expandedNode.State.AvailableActions; - byte actionCount = 0; Span actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)]; while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && @@ -200,7 +198,7 @@ public sealed class MCTS { var nextAction = currentActions.SelectRandom(random); actions[actionCount++] = nextAction; - (_, currentState) = simulator.Execute(currentState, nextAction); + currentState = simulator.ExecuteUnchecked(currentState, nextAction); currentCompletionState = simulator.CompletionState; if (currentCompletionState != CompletionState.Incomplete) break; @@ -235,26 +233,6 @@ public sealed class MCTS } } - private void ShowAllNodes() - { - static void ShowNodes(StringBuilder b, Node node, Stack path) - { - path.Push(node); - b.AppendLine($"{new string(' ', path.Count)}{node.State.Action}"); - { - for (var i = 0; i < node.Children.Count; ++i) - { - var n = node.ChildAt((i >> 3, i & 7))!; - ShowNodes(b, n, path); - } - path.Pop(); - } - } - var b = new StringBuilder(); - ShowNodes(b, rootNode, new()); - Console.WriteLine(b.ToString()); - } - private bool AllNodesComplete() { static bool NodesIncomplete(Node node, Stack path) @@ -280,17 +258,14 @@ public sealed class MCTS return !NodesIncomplete(rootNode, new()); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Search(int iterations, ref int progress, CancellationToken token) { - Simulator simulator = new(config.ActionPool, config.MaxStepCount); + var simulator = new Simulator(config.ActionPool, config.MaxStepCount, rootNode.State.State); var random = rootNode.State.State.Input.Random; var staleCounter = 0; var i = 0; for (; i < iterations || MaxScore == 0; i++) { - token.ThrowIfCancellationRequested(); - var selectedNode = Select(); var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); if (MaxScore == 0) @@ -315,7 +290,10 @@ public sealed class MCTS Backpropagate(endNode, score); if ((i & (ProgressUpdateFrequency - 1)) == ProgressUpdateFrequency - 1) + { + token.ThrowIfCancellationRequested(); Interlocked.Add(ref progress, ProgressUpdateFrequency); + } } Interlocked.Add(ref progress, i & (ProgressUpdateFrequency - 1)); } diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index 8ddea8f..843e86c 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -21,9 +21,15 @@ internal sealed class Simulator : SimulatorNoRandom } } - public Simulator(ActionType[] actionPool, int maxStepCount) + public Simulator(ActionType[] actionPool, int maxStepCount, SimulationState? filteringState = null) { - actionPoolObjects = actionPool.Select(x => (x.Base(), x)).ToArray(); + var pool = actionPool.Select(x => (x.Base(), x)); + if (filteringState is { } state) + { + State = state; + pool = pool.Where(x => x.Item1.IsPossible(this)); + } + actionPoolObjects = pool.OrderBy(x => x.x).ToArray(); this.maxStepCount = maxStepCount; } @@ -32,7 +38,7 @@ internal sealed class Simulator : SimulatorNoRandom [MethodImpl(MethodImplOptions.AggressiveInlining)] // It's just a bunch of if statements, I would assume this is actually quite simple to follow #pragma warning disable MA0051 // Method is too long - private bool CanUseAction(ActionType action, BaseAction baseAction, bool strict) + private bool CouldUseAction(ActionType action, BaseAction baseAction, bool strict) #pragma warning restore MA0051 // Method is too long { if (CalculateSuccessRate(baseAction.SuccessRate(this)) != 1) @@ -46,7 +52,7 @@ internal sealed class Simulator : SimulatorNoRandom { // always use Trained Eye if it's available if (action == ActionType.TrainedEye) - return baseAction.CanUse(this); + return baseAction.CouldUse(this); // don't allow quality moves under Muscle Memory for difficult crafts if (Input.Recipe.ClassJobLevel == 90 && @@ -123,7 +129,7 @@ internal sealed class Simulator : SimulatorNoRandom return false; } - return baseAction.CanUse(this); + return baseAction.CouldUse(this); } // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L137 @@ -134,9 +140,8 @@ internal sealed class Simulator : SimulatorNoRandom var ret = new ActionSet(); foreach (var (data, action) in actionPoolObjects) - if (CanUseAction(action, data, strict)) + if (CouldUseAction(action, data, strict)) ret.AddAction(action); return ret; } - } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index e60be04..6cc53a5 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -272,7 +272,7 @@ public sealed class Solver : IDisposable var actions = new List(); var state = State; - var sim = new Simulator(Config.ActionPool, Config.MaxStepCount) { State = state }; + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount, state); while (true) { Token.ThrowIfCancellationRequested(); @@ -338,7 +338,7 @@ public sealed class Solver : IDisposable var actions = new List(); var state = State; - var sim = new Simulator(Config.ActionPool, Config.MaxStepCount) { State = state }; + var sim = new Simulator(Config.ActionPool, Config.MaxStepCount, state); while (true) { Token.ThrowIfCancellationRequested(); From 84d7d5737fd23ad5d6292b25471b585844b839c9 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 3 Mar 2024 19:03:07 -0800 Subject: [PATCH 10/12] Minor sim refactors --- Simulator/ActionCategory.cs | 1 - Simulator/Actions/ActionType.cs | 6 ----- Simulator/Condition.cs | 40 +++++++++++++++++++++++---------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/Simulator/ActionCategory.cs b/Simulator/ActionCategory.cs index 56ce764..727772e 100644 --- a/Simulator/ActionCategory.cs +++ b/Simulator/ActionCategory.cs @@ -22,7 +22,6 @@ public static class ActionCategoryUtils { SortedActions = new( Enum.GetValues() - .Where(a => a.Category() != ActionCategory.Combo) .GroupBy(a => a.Category()) .ToDictionary(g => g.Key, g => g.OrderBy(a => a.Level()).ToArray())); } diff --git a/Simulator/Actions/ActionType.cs b/Simulator/Actions/ActionType.cs index 1c6e589..d7dd285 100644 --- a/Simulator/Actions/ActionType.cs +++ b/Simulator/Actions/ActionType.cs @@ -63,12 +63,6 @@ public static class ActionUtils [MethodImpl(MethodImplOptions.AggressiveInlining)] public static BaseAction Base(this ActionType me) => Actions[(int)me]; - public static IEnumerable AvailableActions(Simulator simulation) => - simulation.IsComplete - ? Enumerable.Empty() - : Enum.GetValues() - .Where(a => a.Base().CanUse(simulation)); - public static int Level(this ActionType me) => me.Base().Level; diff --git a/Simulator/Condition.cs b/Simulator/Condition.cs index 6ba2103..0f9e6d7 100644 --- a/Simulator/Condition.cs +++ b/Simulator/Condition.cs @@ -1,22 +1,38 @@ namespace Craftimizer.Simulator; -public enum Condition : ushort +public enum Condition : byte { - Poor = 0x0008, - Normal = 0x0001, - Good = 0x0002, - Excellent = 0x0004, + Normal, + Good, + Excellent, + Poor, - Centered = 0x0010, - Sturdy = 0x0020, - Pliant = 0x0040, - Malleable = 0x0080, - Primed = 0x0100, - GoodOmen = 0x0200, + Centered, + Sturdy, + Pliant, + Malleable, + Primed, + GoodOmen, } public static class ConditionUtils { + [Flags] + private enum ConditionMask : ushort + { + Normal = 1 << 0, // 0x0001 + Good = 1 << 1, // 0x0002 + Excellent = 1 << 2, // 0x0004 + Poor = 1 << 3, // 0x0008 + + Centered = 1 << 4, // 0x0010 + Sturdy = 1 << 5, // 0x0020 + Pliant = 1 << 6, // 0x0040 + Malleable = 1 << 7, // 0x0080 + Primed = 1 << 8, // 0x0100 + GoodOmen = 1 << 9, // 0x0200 + } + public static Condition[] GetPossibleConditions(ushort conditionsFlag) => - Enum.GetValues().Where(c => ((Condition)conditionsFlag).HasFlag(c)).ToArray(); + Enum.GetValues().Where(c => ((ConditionMask)conditionsFlag).HasFlag((ConditionMask)(1 << (ushort)c))).ToArray(); } From 0eae63f7bccb11a057f3224b18db167b7b97373e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 3 Mar 2024 19:04:06 -0800 Subject: [PATCH 11/12] Add action pool selector to settings --- Craftimizer/Windows/Settings.cs | 133 ++++++++++++++++++++++++++++++++ Solver/SolverConfig.cs | 13 ++++ 2 files changed, 146 insertions(+) diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index e1d2d92..8391188 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -1,3 +1,5 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Craftimizer.Solver; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -8,8 +10,10 @@ using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Text; namespace Craftimizer.Plugin.Windows; @@ -558,6 +562,21 @@ public sealed class Settings : Window, IDisposable ); } + using (var panel = ImRaii2.GroupPanel("Action Pool", -1, out var poolWidth)) + { + poolWidth -= ImGui.GetStyle().ItemSpacing.X * 2; + + ImGui.Text("Select the actions you want the solver to choose from."); + + var pool = config.ActionPool; + DrawActionPool(ref pool, poolWidth, out var isPoolDirty); + if (isPoolDirty) + { + config = config with { ActionPool = pool }; + isDirty = true; + } + } + using (var panel = ImRaii2.GroupPanel("Advanced", -1, out _)) { DrawOption( @@ -676,6 +695,120 @@ public sealed class Settings : Window, IDisposable configRef = config; } + private static void DrawActionPool(ref ActionType[] actionPool, float poolWidth, out bool isDirty) + { + isDirty = false; + + var recipeData = Service.Plugin.GetDefaultStats().Recipe; + HashSet pool = new(actionPool); + + var imageSize = ImGui.GetFrameHeight() * 2; + var spacing = ImGui.GetStyle().ItemSpacing.Y; + + using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero); + using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero); + using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero); + using var _alpha = ImRaii.PushStyle(ImGuiStyleVar.DisabledAlpha, ImGui.GetStyle().DisabledAlpha * .5f); + foreach (var category in Enum.GetValues()) + { + if (category == ActionCategory.Combo) + continue; + + var actions = category.GetActions(); + using var panel = ImRaii2.GroupPanel(category.GetDisplayName(), poolWidth, out var availSpace); + var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing)); + var itemCount = actions.Count; + var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow); + for (var i = 0; i < iterCount; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + if (i < itemCount) + { + var actionBase = actions[i].Base(); + var isEnabled = pool.Contains(actions[i]); + var isInefficient = SolverConfig.InefficientActions.Contains(actions[i]); + var isRisky = SolverConfig.RiskyActions.Contains(actions[i]); + var iconTint = Vector4.One; + if (!isEnabled) + iconTint = new(1, 1, 1, ImGui.GetStyle().DisabledAlpha); + else if (isInefficient) + iconTint = new(1, 1f, .5f, 1); + else if (isRisky) + iconTint = new(1, .5f, .5f, 1); + if (ImGui.ImageButton(actions[i].GetIcon(recipeData.ClassJob).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, iconTint)) + { + isDirty = true; + if (isEnabled) + pool.Remove(actions[i]); + else + pool.Add(actions[i]); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + var s = new StringBuilder(); + s.AppendLine(actions[i].GetName(recipeData.ClassJob)); + if (isInefficient) + s.AppendLine( + "Not recommended. This action may be randomly used in a " + + "detrimental way to the rest of the craft. Always use " + + "your best judgement if enabling this action."); + if (isRisky) + s.AppendLine( + "Useless; the solver currently doesn't take any risks in " + + "its crafts. It only takes steps that have a 100% chance of " + + "succeeding. If you want have a moment where you want to take " + + "risks in your craft (like in expert recipes), don't rely " + + "on the solver during that time."); + ImGuiUtils.TooltipWrapped(s.ToString()); + } + } + else + ImGui.Dummy(new(imageSize)); + } + } + + if (isDirty) + { + bool InPool(BaseComboAction action) + { + if (action.ActionTypeA.Base() is BaseComboAction { } aCombo) + { + if (!InPool(aCombo)) + return false; + } + else + { + if (!pool.Contains(action.ActionTypeA)) + return false; + } + if (action.ActionTypeB.Base() is BaseComboAction { } bCombo) + { + if (!InPool(bCombo)) + return false; + } + else + { + if (!pool.Contains(action.ActionTypeB)) + return false; + } + return true; + } + + foreach(var combo in ActionCategory.Combo.GetActions()) + { + if (combo.Base() is BaseComboAction { } comboAction) + { + if (!InPool(comboAction)) + pool.Remove(combo); + else + pool.Add(combo); + } + } + actionPool = pool.ToArray(); + } + } + private void DrawTabSimulator() { using var tab = TabItem("Simulator"); diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index f6c9830..4522c50 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -93,6 +93,19 @@ public readonly record struct SolverConfig ActionType.BasicTouch, }); + public static readonly IReadOnlySet InefficientActions = new HashSet(new[] + { + ActionType.CarefulObservation, + ActionType.HeartAndSoul, + ActionType.FinalAppraisal + }); + + public static readonly IReadOnlySet RiskyActions = new HashSet(new[] + { + ActionType.RapidSynthesis, + ActionType.HastyTouch, + }); + public static readonly SolverConfig SimulatorDefault = new SolverConfig() with { From ea963f9e1b0a1eba1606dedbf64f0bd2d5971e79 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 14 Mar 2024 20:10:45 -0700 Subject: [PATCH 12/12] Fix touch combo action cp cost --- Simulator/Actions/AdvancedTouchCombo.cs | 2 ++ Simulator/Actions/StandardTouchCombo.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Simulator/Actions/AdvancedTouchCombo.cs b/Simulator/Actions/AdvancedTouchCombo.cs index 6a147a0..66e6123 100644 --- a/Simulator/Actions/AdvancedTouchCombo.cs +++ b/Simulator/Actions/AdvancedTouchCombo.cs @@ -4,4 +4,6 @@ internal sealed class AdvancedTouchCombo : BaseComboAction ActionType.StandardTouchCombo; public override ActionType ActionTypeB => ActionType.AdvancedTouch; + + public override int CPCost(Simulator s) => 18 * 3; } diff --git a/Simulator/Actions/StandardTouchCombo.cs b/Simulator/Actions/StandardTouchCombo.cs index 5fcea98..d932c85 100644 --- a/Simulator/Actions/StandardTouchCombo.cs +++ b/Simulator/Actions/StandardTouchCombo.cs @@ -4,4 +4,6 @@ internal sealed class StandardTouchCombo : BaseComboAction ActionType.BasicTouch; public override ActionType ActionTypeB => ActionType.StandardTouch; + + public override int CPCost(Simulator s) => 18 * 2; }