From ecabc2451728ce7eba9b12b4cba898e84b9c20ae Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 00:01:55 -0800 Subject: [PATCH] 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);