diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index ef2ee2d..da6ca6a 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,8 +1,6 @@ -using BenchmarkDotNet.Running; using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Craftimizer.Solver.Crafty; -using ObjectLayoutInspector; using System.Diagnostics; namespace Craftimizer.Benchmark; @@ -18,14 +16,15 @@ internal static class Program //return; var input = new SimulationInput( - new CharacterStats { - Craftsmanship = 4041, - Control = 3905, - CP = 609, + new CharacterStats + { + Craftsmanship = 4078, + Control = 3897, + CP = 704, Level = 90, CanUseManipulation = true, - HasSplendorousBuff = true, - IsSpecialist = true, + HasSplendorousBuff = false, + IsSpecialist = false, CLvl = 560, }, new RecipeInfo() @@ -46,23 +45,98 @@ internal static class Program var config = new SolverConfig() { - Iterations = 1_000_000, - ThreadCount = 8, + Iterations = 100_000, + ForkCount = 32, + FurcatedActionCount = 16, + MaxStepCount = 30, }; - Debugger.Break(); + var sim = new SimulatorNoRandom(new(input)); + (_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); + (_, state) = sim.Execute(state, ActionType.PrudentTouch); + (_, state) = sim.Execute(state, ActionType.Manipulation); + (_, state) = sim.Execute(state, ActionType.Veneration); + (_, state) = sim.Execute(state, ActionType.WasteNot); + (_, state) = sim.Execute(state, ActionType.Groundwork); + (_, state) = sim.Execute(state, ActionType.Groundwork); + (_, state) = sim.Execute(state, ActionType.Groundwork); + (_, state) = sim.Execute(state, ActionType.Innovation); + (_, state) = sim.Execute(state, ActionType.PrudentTouch); + (_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); + (_, state) = sim.Execute(state, ActionType.Manipulation); + (_, state) = sim.Execute(state, ActionType.Innovation); + (_, state) = sim.Execute(state, ActionType.PrudentTouch); + (_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); + (_, state) = sim.Execute(state, ActionType.GreatStrides); + + Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); + //return; + var (_, s) = Solver.Crafty.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a)); + Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); + return; + + for (var i = 0; i < 7; ++i) + { + Console.WriteLine($"{i + 1}"); + var c = config with { FurcatedActionCount = i + 1 }; + Benchmark(() => Solver.Crafty.Solver.SearchStepwiseFurcated(c, input).State); + } + } + + private static void Benchmark(Func search) + { var s = Stopwatch.StartNew(); - if (true) - _ = SolverUtils.SearchStepwise(config, input, a => Console.WriteLine(a)); + List q = new(); + for (var i = 0; i < 60; ++i) + { + var state = search(); + //Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); + + q.Add(state.Quality); + } + + s.Stop(); + Console.WriteLine($"{s.Elapsed.TotalMilliseconds/60:0.00}ms/cycle"); + Console.WriteLine(string.Join(',', q)); + q.Sort(); + Console.WriteLine($"Min: {Quartile(q, 0)}, Max: {Quartile(q, 4)}, Avg: {Quartile(q, 2)}, Q1: {Quartile(q, 1)}, Q3: {Quartile(q, 3)}"); + } + + // https://stackoverflow.com/a/31536435 + private static float Quartile(List input, int quartile) + { + float dblPercentage = quartile switch + { + 0 => 0, // Smallest value in the data set + 1 => 25, // First quartile (25th percentile) + 2 => 50, // Second quartile (50th percentile) + 3 => 75, // Third quartile (75th percentile) + 4 => 100, // Largest value in the data set + _ => 0, + }; + if (dblPercentage >= 100) return input[^1]; + + var position = (input.Count + 1) * dblPercentage / 100f; + var n = (dblPercentage / 100f * (input.Count - 1)) + 1; + + float leftNumber, rightNumber; + if (position >= 1) + { + leftNumber = input[(int)MathF.Floor(n) - 1]; + rightNumber = input[(int)MathF.Floor(n)]; + } else { - (var actions, _) = SolverUtils.SearchOneshot(config, input); - foreach (var action in actions) - Console.Write($">{action.IntName()}"); - Console.WriteLine(); + leftNumber = input[0]; // first data + rightNumber = input[1]; // first data + } + + if (leftNumber == rightNumber) + return leftNumber; + else + { + var part = n - MathF.Floor(n); + return leftNumber + (part * (rightNumber - leftNumber)); } - s.Stop(); - Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}"); - Debugger.Break(); } } diff --git a/Craftimizer/Windows/SimulatorWindowDrawer.cs b/Craftimizer/Windows/SimulatorWindowDrawer.cs index 689f91b..965b198 100644 --- a/Craftimizer/Windows/SimulatorWindowDrawer.cs +++ b/Craftimizer/Windows/SimulatorWindowDrawer.cs @@ -38,7 +38,11 @@ public sealed partial class SimulatorWindow : Window, IDisposable static SimulatorWindow() { - SortedActions = Enum.GetValues().GroupBy(a => a.Category()).Select(g => (g.Key, g.OrderBy(a => a.Level()).ToArray())).ToArray(); + SortedActions = Enum.GetValues() + .Where(a => a.Category() != ActionCategory.Combo) + .GroupBy(a => a.Category()) + .Select(g => (g.Key, g.OrderBy(a => a.Level()).ToArray())) + .ToArray(); } public override void Draw() diff --git a/Simulator/ActionCategory.cs b/Simulator/ActionCategory.cs index 3bb2632..24d671b 100644 --- a/Simulator/ActionCategory.cs +++ b/Simulator/ActionCategory.cs @@ -7,6 +7,7 @@ public enum ActionCategory Quality, Durability, Buffs, + Combo, Other } diff --git a/Simulator/Actions/ActionType.cs b/Simulator/Actions/ActionType.cs index c89699a..bb852f9 100644 --- a/Simulator/Actions/ActionType.cs +++ b/Simulator/Actions/ActionType.cs @@ -38,6 +38,11 @@ public enum ActionType : byte Veneration, WasteNot, WasteNot2, + + StandardTouchCombo, + AdvancedTouchCombo, + FocusedSynthesisCombo, + FocusedTouchCombo, } public static class ActionUtils @@ -106,6 +111,10 @@ public static class ActionUtils ActionType.Veneration => "Veneration", ActionType.WasteNot => "Waste Not", ActionType.WasteNot2 => "Waste Not II", + ActionType.StandardTouchCombo => "Standard Touch Combo", + ActionType.AdvancedTouchCombo => "Advanced Touch Combo", + ActionType.FocusedSynthesisCombo => "Focused Synthesis Combo", + ActionType.FocusedTouchCombo => "Focused Touch Combo", _ => me.ToString(), }; } diff --git a/Simulator/Actions/AdvancedTouchCombo.cs b/Simulator/Actions/AdvancedTouchCombo.cs new file mode 100644 index 0000000..cb51547 --- /dev/null +++ b/Simulator/Actions/AdvancedTouchCombo.cs @@ -0,0 +1,30 @@ +namespace Craftimizer.Simulator.Actions; + +// Basic Touch -> Standard Touch -> Advanced Touch +internal sealed class AdvancedTouchCombo : BaseAction +{ + public override ActionCategory Category => ActionCategory.Combo; + public override int Level => 84; + public override uint ActionId => 100411; + + public override bool IncreasesQuality => true; + + public override int CPCost(Simulator s) => 18 + 18 + 18; + + public override bool CanUse(Simulator s) => + // BasicTouch.DurabilityCost vv vv StandardTouch.DurabilityCost + base.CanUse(s) && VerifyDurability3(s, 10, 10); + + private static readonly BasicTouch ActionA = new(); + private static readonly StandardTouch ActionB = new(); + private static readonly AdvancedTouch ActionC = new(); + public override void Use(Simulator s) + { + s.ExecuteForced(ActionType.BasicTouch, ActionA); + s.ExecuteForced(ActionType.StandardTouch, ActionB); + ActionC.Use(s); + } + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{ActionA.GetTooltip(s, addUsability)}\n{ActionB.GetTooltip(s, addUsability)}\n{ActionC.GetTooltip(s, addUsability)}"; +} diff --git a/Simulator/Actions/BaseAction.cs b/Simulator/Actions/BaseAction.cs index 4fc63a3..1470989 100644 --- a/Simulator/Actions/BaseAction.cs +++ b/Simulator/Actions/BaseAction.cs @@ -80,4 +80,54 @@ public abstract class BaseAction builder.AppendLine($"{s.CalculateSuccessRate(SuccessRate(s)) * 100:##}%% Success Rate"); return builder.ToString(); } + + private static bool VerifyDurability2(int durabilityA, int durability, Effects effects) + { + var wasteNots = effects.HasEffect(EffectType.WasteNot) || effects.HasEffect(EffectType.WasteNot2); + // -A + durability -= (int)MathF.Ceiling(durabilityA * (wasteNots ? .5f : 1f)); + if (durability <= 0) + return false; + + // If we can do the first action and still have durability left to survive to the next + // step (even before the Manipulation modifier), we can certainly do the next action. + return true; + } + + public static bool VerifyDurability2(SimulationState s, int durabilityA) => + VerifyDurability2(durabilityA, s.Durability, s.ActiveEffects); + + public static bool VerifyDurability2(Simulator s, int durabilityA) => + VerifyDurability2(durabilityA, s.Durability, s.ActiveEffects); + + public static bool VerifyDurability3(int durabilityA, int durabilityB, int durability, Effects effects) + { + var wasteNots = Math.Max(effects.GetDuration(EffectType.WasteNot), effects.GetDuration(EffectType.WasteNot2)); + var manips = effects.HasEffect(EffectType.Manipulation); + + durability -= (int)MathF.Ceiling(durabilityA * wasteNots > 0 ? .5f : 1f); + if (durability <= 0) + return false; + + if (manips) + durability += 5; + + if (wasteNots > 0) + wasteNots--; + + durability -= (int)MathF.Ceiling(durabilityB * wasteNots > 0 ? .5f : 1f); + + if (durability <= 0) + return false; + + // If we can do the second action and still have durability left to survive to the next + // step (even before the Manipulation modifier), we can certainly do the next action. + return true; + } + + public static bool VerifyDurability3(Simulator s, int durabilityA, int durabilityB) => + VerifyDurability3(durabilityA, durabilityB, s.Durability, s.ActiveEffects); + + public static bool VerifyDurability3(SimulationState s, int durabilityA, int durabilityB) => + VerifyDurability3(durabilityA, durabilityB, s.Durability, s.ActiveEffects); } diff --git a/Simulator/Actions/FocusedSynthesisCombo.cs b/Simulator/Actions/FocusedSynthesisCombo.cs new file mode 100644 index 0000000..4c5bc4f --- /dev/null +++ b/Simulator/Actions/FocusedSynthesisCombo.cs @@ -0,0 +1,28 @@ +namespace Craftimizer.Simulator.Actions; + +// Observe -> Focused Synthesis +internal sealed class FocusedSynthesisCombo : BaseAction +{ + public override ActionCategory Category => ActionCategory.Combo; + public override int Level => 67; + public override uint ActionId => 100235; + + public override bool IncreasesProgress => true; + + public override int CPCost(Simulator s) => 7 + 5; + + public override bool CanUse(Simulator s) => + // Observe.DurabilityCost v + base.CanUse(s) && VerifyDurability2(s, 0); + + private static readonly Observe ActionA = new(); + private static readonly FocusedSynthesis ActionB = new(); + public override void Use(Simulator s) + { + s.ExecuteForced(ActionType.Observe, ActionA); + ActionB.Use(s); + } + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{ActionA.GetTooltip(s, addUsability)}\n{ActionB.GetTooltip(s, addUsability)}"; +} diff --git a/Simulator/Actions/FocusedTouchCombo.cs b/Simulator/Actions/FocusedTouchCombo.cs new file mode 100644 index 0000000..98e9767 --- /dev/null +++ b/Simulator/Actions/FocusedTouchCombo.cs @@ -0,0 +1,28 @@ +namespace Craftimizer.Simulator.Actions; + +// Observe -> Focused Touch +internal sealed class FocusedTouchCombo : BaseAction +{ + public override ActionCategory Category => ActionCategory.Combo; + public override int Level => 68; + public override uint ActionId => 100243; + + public override bool IncreasesQuality => true; + + public override int CPCost(Simulator s) => 7 + 18; + + public override bool CanUse(Simulator s) => + // Observe.DurabilityCost v + base.CanUse(s) && VerifyDurability2(s, 0); + + private static readonly Observe ActionA = new(); + private static readonly FocusedTouch ActionB = new(); + public override void Use(Simulator s) + { + s.ExecuteForced(ActionType.Observe, ActionA); + ActionB.Use(s); + } + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{ActionA.GetTooltip(s, addUsability)}\n{ActionB.GetTooltip(s, addUsability)}"; +} diff --git a/Simulator/Actions/StandardTouchCombo.cs b/Simulator/Actions/StandardTouchCombo.cs new file mode 100644 index 0000000..ded0665 --- /dev/null +++ b/Simulator/Actions/StandardTouchCombo.cs @@ -0,0 +1,28 @@ +namespace Craftimizer.Simulator.Actions; + +// Basic Touch -> Standard Touch +internal sealed class StandardTouchCombo : BaseAction +{ + public override ActionCategory Category => ActionCategory.Combo; + public override int Level => 18; + public override uint ActionId => 100004; + + public override bool IncreasesQuality => true; + + public override int CPCost(Simulator s) => 18 + 18; + + public override bool CanUse(Simulator s) => + // BasicTouch.DurabilityCost vv + base.CanUse(s) && VerifyDurability2(s, 10); + + private static readonly BasicTouch ActionA = new(); + private static readonly StandardTouch ActionB = new(); + public override void Use(Simulator s) + { + s.ExecuteForced(ActionType.BasicTouch, ActionA); + ActionB.Use(s); + } + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{ActionA.GetTooltip(s, addUsability)}\n{ActionB.GetTooltip(s, addUsability)}"; +} diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index ffc2f8d..f132105 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -57,13 +57,18 @@ public class Simulator return ActionResponse.CannotUseAction; } + ExecuteForced(action, baseAction); + + return ActionResponse.UsedAction; + } + + public void ExecuteForced(ActionType action, BaseAction baseAction) + { baseAction.Use(this); ActionStates.MutateState(action); ActionCount++; ActiveEffects.DecrementDuration(); - - return ActionResponse.UsedAction; } public int GetEffectStrength(EffectType effect) => diff --git a/Solver/Crafty/ActionSet.cs b/Solver/Crafty/ActionSet.cs index 8d0a49c..ef7fa93 100644 --- a/Solver/Crafty/ActionSet.cs +++ b/Solver/Crafty/ActionSet.cs @@ -7,6 +7,8 @@ namespace Craftimizer.Solver.Crafty; public struct ActionSet { + private const bool IsDeterministic = false; + private uint bits; [Pure] @@ -19,24 +21,6 @@ public struct ActionSet [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint ToMask(ActionType action) => 1u << FromAction(action) + 1; - // Return true if action was newly added and not there before. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool AddActionConcurrent(ActionType action) - { - var mask = ToMask(action); - var old = Interlocked.Or(ref bits, mask); - return (old & mask) == 0; - } - - // Return true if action was newly removed and not already gone. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool RemoveActionConcurrent(ActionType action) - { - var mask = ToMask(action); - var old = Interlocked.And(ref bits, ~mask); - return (old & mask) != 0; - } - // Return true if action was newly added and not there before. [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool AddAction(ActionType action) @@ -71,52 +55,17 @@ public struct ActionSet public readonly bool IsEmpty => bits == 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType SelectRandom(Random random) => ElementAt(random.Next(Count)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ActionType? PopRandomConcurrent(Random random) - { - uint snapshot; - uint newValue; - ActionType action; - do - { - snapshot = bits; - if (snapshot == 0) - return null; - - var count = BitOperations.PopCount(snapshot); - var index = random.Next(count); - - action = ToAction(Intrinsics.NthBitSet(snapshot, index) - 1); - newValue = snapshot & ~ToMask(action); - } - while (Interlocked.CompareExchange(ref bits, newValue, snapshot) != snapshot); - return action; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ActionType? PopFirstConcurrent() - { - uint snapshot; - uint newValue; - ActionType action; - do - { - snapshot = bits; - if (snapshot == 0) - return null; - - action = ToAction(Intrinsics.NthBitSet(snapshot, 0) - 1); - newValue = snapshot & ~ToMask(action); - } - while (Interlocked.CompareExchange(ref bits, newValue, snapshot) != snapshot); - return action; - } + public readonly ActionType SelectRandom(Random random) => + IsDeterministic ? + First() : + ElementAt(random.Next(Count)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public ActionType PopRandom(Random random) { + if (IsDeterministic) + return PopFirst(); + var action = ElementAt(random.Next(Count)); RemoveAction(action); return action; diff --git a/Solver/Crafty/ArenaBuffer.cs b/Solver/Crafty/ArenaBuffer.cs index ccd7709..c3775c2 100644 --- a/Solver/Crafty/ArenaBuffer.cs +++ b/Solver/Crafty/ArenaBuffer.cs @@ -5,51 +5,32 @@ using System.Runtime.CompilerServices; namespace Craftimizer.Solver.Crafty; // Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs -public struct ArenaBuffer +public struct ArenaBuffer where T : struct { // 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 int BatchSize = Vector.Count; - private static int BatchSizeBits = int.Log2(BatchSize); - private static int BatchSizeMask = BatchSize - 1; + private static readonly int BatchSize = Vector.Count; + private static readonly int BatchSizeBits = int.Log2(BatchSize); + private static readonly int BatchSizeMask = BatchSize - 1; - private static int BatchCount = MaxSize / BatchSize; + private static readonly int BatchCount = MaxSize / BatchSize; - public T[][] Data; - private int index; // Unused in single threaded workload - private int count; + public ArenaNode[][] Data; + public int Count { get; private set; } - public readonly int Count => count; - - public void AddConcurrent(T node) + public void Add(ArenaNode node) { - if (Data == null) - Interlocked.CompareExchange(ref Data, new T[BatchCount][], null); + Data ??= new ArenaNode[BatchCount][]; - var idx = Interlocked.Increment(ref index) - 1; + var idx = Count++; var (arrayIdx, subIdx) = GetArrayIndex(idx); - if (Data[arrayIdx] == null) - Interlocked.CompareExchange(ref Data[arrayIdx], new T[BatchSize], null); - - Data[arrayIdx][subIdx] = node; - - Interlocked.Increment(ref count); - } - - public void Add(T node) - { - Data ??= new T[BatchCount][]; - - var idx = count++; - - var (arrayIdx, subIdx) = GetArrayIndex(idx); - - Data[arrayIdx] ??= new T[BatchSize]; + Data[arrayIdx] ??= new ArenaNode[BatchSize]; + node.ChildIdx = (arrayIdx, subIdx); Data[arrayIdx][subIdx] = node; } diff --git a/Solver/Crafty/ArenaNode.cs b/Solver/Crafty/ArenaNode.cs index 560ebaf..5ac88ea 100644 --- a/Solver/Crafty/ArenaNode.cs +++ b/Solver/Crafty/ArenaNode.cs @@ -5,28 +5,29 @@ namespace Craftimizer.Solver.Crafty; public sealed class ArenaNode where T : struct { public T State; - public ArenaBuffer> Children; + public ArenaBuffer Children; + public NodeScoresBuffer ChildScores; + public (int arrayIdx, int subIdx) ChildIdx; public readonly ArenaNode? Parent; + public NodeScoresBuffer? ParentScores => Parent?.ChildScores; + public ArenaNode(T state, ArenaNode? parent = null) { State = state; Children = new(); + ChildScores = new(); Parent = parent; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArenaNode AddConcurrent(T state) - { - var node = new ArenaNode(state, this); - Children.AddConcurrent(node); - return node; - } + public ArenaNode? ChildAt((int arrayIdx, int subIdx) at) => + Children.Data?[at.arrayIdx]?[at.subIdx]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArenaNode Add(T state) { var node = new ArenaNode(state, this); + ChildScores.Add(); Children.Add(node); return node; } diff --git a/Solver/Crafty/ISolver.cs b/Solver/Crafty/ISolver.cs deleted file mode 100644 index 050b3f7..0000000 --- a/Solver/Crafty/ISolver.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Node = Craftimizer.Solver.Crafty.ArenaNode; - -namespace Craftimizer.Solver.Crafty; - -public interface ISolver -{ - abstract static void LoadChildData(Span scoreSums, Span visits, Span maxScores, ref Node[] chunk, int iterCount); - - abstract static bool SearchIter(ref SolverConfig config, Node rootNode, Random random, Simulator simulator); - - abstract static void Search(ref SolverConfig config, Node rootNode, CancellationToken token); -} diff --git a/Solver/Crafty/Intrinsics.cs b/Solver/Crafty/Intrinsics.cs index a1baef0..1380ae9 100644 --- a/Solver/Crafty/Intrinsics.cs +++ b/Solver/Crafty/Intrinsics.cs @@ -101,9 +101,8 @@ internal static class Intrinsics [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int NthBitSet(uint value, int n) { - // TODO: debug if (n >= BitOperations.PopCount(value)) - throw new ArgumentException(null, nameof(value)); + return 32; return Bmi2.IsSupported ? NthBitSetBMI2(value, n) : @@ -125,28 +124,4 @@ internal static class Intrinsics result[i] = MathF.ReciprocalSqrtEstimate(data[i]); return new(result); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CASMax(ref float location, float newValue) - { - float snapshot; - do - { - snapshot = location; - if (snapshot >= newValue) return; - } while (Interlocked.CompareExchange(ref location, newValue, snapshot) != snapshot); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CASAdd(ref float location, float value) - { - float snapshot; - float newValue; - do - { - snapshot = location; - newValue = snapshot + value; - } - while (Interlocked.CompareExchange(ref location, newValue, snapshot) != snapshot); - } } diff --git a/Solver/Crafty/NodeScoresBuffer.cs b/Solver/Crafty/NodeScoresBuffer.cs new file mode 100644 index 0000000..fa394bb --- /dev/null +++ b/Solver/Crafty/NodeScoresBuffer.cs @@ -0,0 +1,62 @@ +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Craftimizer.Solver.Crafty; + +// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs +public struct NodeScoresBuffer +{ + public sealed class ScoresBatch + { + public Memory ScoreSum; + public Memory MaxScore; + public Memory Visits; + + public ScoresBatch() + { + ScoreSum = new float[BatchSize]; + MaxScore = new float[BatchSize]; + Visits = new int[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]; + + var idx = Count++; + + var (arrayIdx, _) = GetArrayIndex(idx); + + Data[arrayIdx] ??= new(); + } + + public readonly void Visit((int arrayIdx, int subIdx) at, float score) + { + Data[at.arrayIdx].ScoreSum.Span[at.subIdx] += score; + Data[at.arrayIdx].MaxScore.Span[at.subIdx] = Math.Max(Data[at.arrayIdx].MaxScore.Span[at.subIdx], score); + Data[at.arrayIdx].Visits.Span[at.subIdx]++; + } + + public readonly int GetVisits((int arrayIdx, int subIdx) at) => + Data[at.arrayIdx].Visits.Span[at.subIdx]; + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int arrayIdx, int subIdx) GetArrayIndex(int idx) => + (idx >> BatchSizeBits, idx & BatchSizeMask); +} diff --git a/Solver/Crafty/NodeScores.cs b/Solver/Crafty/RootScores.cs similarity index 59% rename from Solver/Crafty/NodeScores.cs rename to Solver/Crafty/RootScores.cs index d0a2044..2ac7c2f 100644 --- a/Solver/Crafty/NodeScores.cs +++ b/Solver/Crafty/RootScores.cs @@ -3,19 +3,12 @@ using System.Runtime.InteropServices; namespace Craftimizer.Solver.Crafty; [StructLayout(LayoutKind.Auto)] -public struct NodeScores +public sealed class RootScores { public float ScoreSum; public float MaxScore; public int Visits; - public void VisitConcurrent(float score) - { - Intrinsics.CASAdd(ref ScoreSum, score); - Intrinsics.CASMax(ref MaxScore, score); - Interlocked.Increment(ref Visits); - } - public void Visit(float score) { ScoreSum += score; diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs index e66ad23..35b9543 100644 --- a/Solver/Crafty/SimulationNode.cs +++ b/Solver/Crafty/SimulationNode.cs @@ -12,7 +12,6 @@ public struct SimulationNode public readonly CompletionState SimulationCompletionState; public ActionSet AvailableActions; - public NodeScores Scores; public readonly CompletionState CompletionState => GetCompletionState(SimulationCompletionState, AvailableActions); @@ -31,9 +30,18 @@ public struct SimulationNode CompletionState.NoMoreActions : simCompletionState; - public readonly float? CalculateScore(int maxStepCount) => CalculateScoreForState(State, SimulationCompletionState, maxStepCount); + public readonly float? CalculateScore(SolverConfig config) => + CalculateScoreForState(State, SimulationCompletionState, config); - public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, int maxStepCount) + private static bool CanByregot(SimulationState state) + { + if (state.ActiveEffects.InnerQuiet == 0) + return false; + + return BaseAction.VerifyDurability2(state, 10); + } + + public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, SolverConfig config) { if (completionState != CompletionState.ProgressComplete) return null; @@ -41,38 +49,33 @@ public struct SimulationNode static float Apply(float bonus, float value, float target) => bonus * Math.Min(1f, value / target); - var progressBonus = 0.20f; - var qualityBonus = 0.65f; - var durabilityBonus = 0.05f; - var cpBonus = 0.05f; - var fewerStepsBonus = 0.05f; - var progressScore = Apply( - progressBonus, + config.ScoreProgressBonus, state.Progress, state.Input.Recipe.MaxProgress ); + var byregotBonus = CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0; var qualityScore = Apply( - qualityBonus, - state.Quality, + config.ScoreQualityBonus, + state.Quality + byregotBonus, state.Input.Recipe.MaxQuality ); var durabilityScore = Apply( - durabilityBonus, + config.ScoreDurabilityBonus, state.Durability, state.Input.Recipe.MaxDurability ); var cpScore = Apply( - cpBonus, + config.ScoreCPBonus, state.CP, state.Input.Stats.CP ); var fewerStepsScore = - fewerStepsBonus * (1f - ((float)(state.ActionCount + 1) / maxStepCount)); + config.ScoreFewerStepsBonus * (1f - ((float)(state.ActionCount + 1) / config.MaxStepCount)); return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore; } diff --git a/Solver/Crafty/Simulator.cs b/Solver/Crafty/Simulator.cs index 2e3a82a..4aceeb5 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Crafty/Simulator.cs @@ -19,6 +19,10 @@ public sealed class Simulator : SimulatorNoRandom public static readonly ActionType[] AcceptedActions = new[] { + ActionType.StandardTouchCombo, + ActionType.AdvancedTouchCombo, + ActionType.FocusedTouchCombo, + ActionType.FocusedSynthesisCombo, ActionType.TrainedFinesse, ActionType.PrudentSynthesis, ActionType.Groundwork, @@ -94,12 +98,29 @@ public sealed class Simulator : SimulatorNoRandom baseAction.IncreasesQuality) return false; + // use First Turn actions if it's available and the craft is difficult + if (IsFirstStep && + Input.Recipe.ClassJobLevel == 90 && + baseAction.Category != ActionCategory.FirstTurn && + CP > 10) + return false; + + // don't allow combo actions if the combo is already in progress + if (ActionStates.TouchComboIdx != 0 && + (action == ActionType.StandardTouchCombo || action == ActionType.AdvancedTouchCombo)) + return false; + // don't allow pure quality moves under Veneration if (HasEffect(EffectType.Veneration) && !baseAction.IncreasesProgress && baseAction.IncreasesQuality) return false; + // don't allow pure quality moves when it won't be able to finish the craft + if (baseAction.IncreasesQuality && + CalculateDurabilityCost(baseAction.DurabilityCost) > Durability) + return false; + if (baseAction.IncreasesProgress) { var progressIncrease = CalculateProgressGain(baseAction.Efficiency(this)); @@ -130,7 +151,7 @@ public sealed class Simulator : SimulatorNoRandom return false; if (action == ActionType.Observe && - CP < 5) + CP < 12) return false; if (action == ActionType.MastersMend && diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs new file mode 100644 index 0000000..6ff7771 --- /dev/null +++ b/Solver/Crafty/Solver.cs @@ -0,0 +1,558 @@ +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using System.Diagnostics; +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Node = Craftimizer.Solver.Crafty.ArenaNode; + +namespace Craftimizer.Solver.Crafty; + +// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs +public sealed class Solver +{ + private SolverConfig config; + private Node rootNode; + private RootScores rootScores; + + public float MaxScore => rootScores.MaxScore; + + public Solver(SolverConfig config, SimulationState state) + { + this.config = config; + var sim = new Simulator(state, config.MaxStepCount); + rootNode = new(new( + state, + null, + sim.CompletionState, + sim.AvailableActionsHeuristic(config.StrictActions) + )); + rootScores = new(); + } + + private static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict) + { + (_, var newState) = simulator.Execute(state, action); + return new( + newState, + action, + simulator.CompletionState, + simulator.AvailableActionsHeuristic(strict) + ); + } + + private static Node ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan actions, bool strict) + { + foreach (var action in actions) + { + var state = startNode.State; + if (state.IsComplete) + return startNode; + + if (!state.AvailableActions.HasAction(action)) + return startNode; + state.AvailableActions.RemoveAction(action); + + startNode = startNode.Add(Execute(simulator, state.State, action, strict)); + } + + return startNode; + } + + [Pure] + private (List Actions, SimulationNode Node) Solution() + { + var actions = new List(); + var node = rootNode; + + while (node.Children.Count != 0) + { + node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!; + + if (node.State.Action != null) + actions.Add(node.State.Action.Value); + } + + var at = node.ChildIdx; + ref var sum = ref node.ParentScores!.Value.Data[at.arrayIdx].ScoreSum.Span[at.subIdx]; + ref var max = ref node.ParentScores!.Value.Data[at.arrayIdx].MaxScore.Span[at.subIdx]; + ref var visits = ref node.ParentScores!.Value.Data[at.arrayIdx].Visits.Span[at.subIdx]; + //Console.WriteLine($"{sum} {max} {visits}"); + + return (actions, node.State); + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores) + { + var length = scores.Count; + var vecLength = Vector.Count; + + var max = (0, 0); + var maxScore = 0f; + for (var i = 0; length > 0; ++i) + { + var iterCount = Math.Min(vecLength, length); + + ref var chunk = ref scores.Data[i]; + var m = new Vector(chunk.MaxScore.Span); + + var idx = Intrinsics.HMaxIndex(m, iterCount); + + if (m[idx] >= maxScore) + { + max = (i, idx); + maxScore = m[idx]; + } + + length -= iterCount; + } + + return max; + } + + // Calculates the best child node to explore next + // Exploitation: ((1 - w) * (s / v)) + (w * m) + // Exploration: sqrt(c * ln(V) / v) + // w = maxScoreWeightingConstant + // s = score sum + // m = max score + // v = visits + // V = parentVisits + // c = explorationConstant + + // Somewhat based off of https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation + // Here, w_i = (1-w)*score sum + // n_i = visits + // max score is tacked onto it + // N_i = parent visits + // c = exploration constant (but crafty places it inside the sqrt..?) + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private (int arrayIdx, int subIdx) EvalBestChild(int parentVisits, ref NodeScoresBuffer scores) + { + var length = scores.Count; + var vecLength = Vector.Count; + + var C = MathF.Sqrt(config.ExplorationConstant * MathF.Log(parentVisits)); + var w = config.MaxScoreWeightingConstant; + var W = 1f - w; + var CVector = new Vector(C); + + var max = (0, 0); + var maxScore = 0f; + for (var i = 0; length > 0; ++i) + { + var iterCount = Math.Min(vecLength, length); + + ref var chunk = ref scores.Data[i]; + var s = new Vector(chunk.ScoreSum.Span); + var vInt = new Vector(chunk.Visits.Span); + var m = new Vector(chunk.MaxScore.Span); + + vInt = Vector.Max(vInt, Vector.One); + var v = Vector.ConvertToSingle(vInt); + + var exploitation = (W * (s / v)) + (w * m); + var exploration = CVector * Intrinsics.ReciprocalSqrt(v); + var evalScores = exploitation + exploration; + + var idx = Intrinsics.HMaxIndex(evalScores, iterCount); + + if (evalScores[idx] >= maxScore) + { + max = (i, idx); + maxScore = evalScores[idx]; + } + + length -= iterCount; + } + + return max; + } + + [Pure] + public Node Select() + { + var node = rootNode; + var nodeVisits = rootScores.Visits; + + while (true) + { + var expandable = !node.State.AvailableActions.IsEmpty; + var likelyTerminal = node.Children.Count == 0; + if (expandable || likelyTerminal) + return node; + + // select the node with the highest score + var at = EvalBestChild(nodeVisits, ref node.ChildScores); + nodeVisits = node.ChildScores.GetVisits(at); + node = node.ChildAt(at)!; + } + } + + public (Node ExpandedNode, float Score) ExpandAndRollout(Random random, Simulator simulator, Node initialNode) + { + ref var initialState = ref initialNode.State; + // expand once + if (initialState.IsComplete) + return (initialNode, initialState.CalculateScore(config) ?? 0); + + var poppedAction = initialState.AvailableActions.PopRandom(random); + var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true)); + + // playout to a terminal state + var currentState = expandedNode.State.State; + 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 && + actionCount < actions.Length) + { + var nextAction = currentActions.SelectRandom(random); + actions[actionCount++] = nextAction; + (_, currentState) = simulator.Execute(currentState, nextAction); + currentCompletionState = simulator.CompletionState; + if (currentCompletionState != CompletionState.Incomplete) + break; + currentActions = simulator.AvailableActionsHeuristic(true); + } + + // store the result if a max score was reached + var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config) ?? 0; + if (currentCompletionState == CompletionState.ProgressComplete) + { + if (score >= config.ScoreStorageThreshold && score >= MaxScore) + { + var terminalNode = ExecuteActions(simulator, expandedNode, actions[..actionCount], true); + return (terminalNode, score); + } + } + return (expandedNode, score); + } + + public void Backpropagate(Node startNode, float score) + { + while (true) + { + if (startNode == rootNode) + { + rootScores.Visit(score); + break; + } + startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score); + + startNode = startNode.Parent!; + } + } + + 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) + { + path.Push(node); + if (node.Children.Count == 0) + { + if (!node.State.AvailableActions.IsEmpty) + return true; + } + else + { + for(var i = 0; i < node.Children.Count; ++i) + { + var n = node.ChildAt((i >> 3, i & 7))!; + if (NodesIncomplete(n, path)) + return true; + } + path.Pop(); + } + return false; + } + return !NodesIncomplete(rootNode, new()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Search(int iterations, CancellationToken token) + { + Simulator simulator = new(rootNode.State.State, config.MaxStepCount); + var random = rootNode.State.State.Input.Random; + var n = 0; + for (var i = 0; i < iterations || MaxScore == 0; i++) + { + if (token.IsCancellationRequested) + break; + + var selectedNode = Select(); + var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); + if (MaxScore == 0) + { + if (endNode == selectedNode) + { + if (n++ > 5000) + { + n = 0; + if (AllNodesComplete()) + { + //Console.WriteLine("All nodes solved for. Can't find a valid solution."); + //ShowAllNodes(); + return; + } + } + } + else + n = 0; + } + + Backpropagate(endNode, score); + } + } + + public static (List Actions, SimulationState State) SearchStepwiseFurcated(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => + SearchStepwiseFurcated(config, new SimulationState(input), actionCallback, token); + + public static (List Actions, SimulationState State) SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + { + var definiteActionCount = 0; + var bestSims = new List<(float Score, (List Actions, SimulationState State) Result)>(); + + var sim = new Simulator(state, config.MaxStepCount); + + var activeStates = new List<(List Actions, SimulationState State)>() { (new(), state) }; + + while (activeStates.Count != 0) + { + if (token.IsCancellationRequested) + break; + + var s = Stopwatch.StartNew(); + var tasks = new List Actions, SimulationNode Node) Solution)>>(config.ForkCount); + for (var i = 0; i < config.ForkCount; i++) + { + var stateIdx = (int)((float)i / config.ForkCount * activeStates.Count); + var st = activeStates[stateIdx]; + tasks.Add( + Task.Run(() => + { + var solver = new Solver(config, activeStates[stateIdx].State); + solver.Search(config.Iterations / config.ForkCount, token); + return (solver.MaxScore, stateIdx, solver.Solution()); + }, token) + ); + } + Task.WaitAll(tasks.ToArray(), CancellationToken.None); + s.Stop(); + + var bestActions = tasks.Select(t => t.Result).OrderByDescending(r => r.MaxScore).Take(config.FurcatedActionCount).ToArray(); + + var bestAction = bestActions[0]; + if (bestAction.MaxScore >= config.ScoreStorageThreshold) + { + var (maxScore, furcatedActionIdx, (solutionActions, solutionNode)) = bestAction; + var (activeActions, activeState) = activeStates[furcatedActionIdx]; + + activeActions.AddRange(solutionActions); + return (activeActions, solutionNode.State); + } + + var newStates = new List<(List Actions, SimulationState State)>(config.FurcatedActionCount); + for (var i = 0; i < bestActions.Length; ++i) + { + var (maxScore, furcatedActionIdx, (solutionActions, solutionNode)) = bestActions[i]; + var (activeActions, activeState) = activeStates[furcatedActionIdx]; + + var chosenAction = solutionActions[0]; + + var newActions = new List(activeActions) { chosenAction }; + var newState = sim.Execute(activeState, chosenAction).NewState; + if (sim.IsComplete) + bestSims.Add((maxScore, (newActions, newState))); + else + newStates.Add((newActions, newState)); + } + + if (bestSims.Count == 0 && newStates.Count != 0) + { + var definiteCount = definiteActionCount; + var equalCount = int.MaxValue; + var refActions = newStates[0].Actions; + for(var i = 1; i < newStates.Count; ++i) + { + var cmpActions = newStates[i].Actions; + var possibleCount = Math.Min(Math.Min(refActions.Count, cmpActions.Count), equalCount); + var completelyEqual = true; + for (var j = definiteCount; j < possibleCount; ++j) + { + if (refActions[j] != cmpActions[j]) + { + equalCount = j; + completelyEqual = false; + break; + } + } + if (completelyEqual) + equalCount = possibleCount; + } + if (definiteCount != equalCount) + { + for (var i = definiteCount; i < equalCount; ++i) + actionCallback?.Invoke(refActions[i]); + + definiteActionCount = equalCount; + } + } + + activeStates = newStates; + + Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); + } + + var result = bestSims.MaxBy(s => s.Score).Result; + for (var i = definiteActionCount; i < result.Actions.Count; ++i) + actionCallback?.Invoke(result.Actions[i]); + + return result; + } + + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => + SearchStepwiseForked(config, new SimulationState(input), actionCallback, token); + + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + { + var actions = new List(); + var sim = new Simulator(state, config.MaxStepCount); + while (true) + { + if (token.IsCancellationRequested) + break; + + if (sim.IsComplete) + break; + + + var s = Stopwatch.StartNew(); + var tasks = new Task<(float MaxScore, (List Actions, SimulationNode Node) Solution)>[config.ForkCount]; + for (var i = 0; i < config.ForkCount; ++i) + tasks[i] = Task.Run(() => + { + var solver = new Solver(config, state); + solver.Search(config.Iterations / config.ForkCount, token); + return (solver.MaxScore, solver.Solution()); + }, token); + Task.WaitAll(tasks, CancellationToken.None); + s.Stop(); + + var (maxScore, (solutionActions, solutionNode)) = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore); + + if (maxScore >= config.ScoreStorageThreshold) + { + actions.AddRange(solutionActions); + return (actions, solutionNode.State); + } + + var chosen_action = solutionActions[0]; + actionCallback?.Invoke(chosen_action); + Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); + + (_, state) = sim.Execute(state, chosen_action); + actions.Add(chosen_action); + } + + return (actions, state); + } + + public static (List Actions, SimulationState State) SearchStepwise(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => + SearchStepwise(config, new SimulationState(input), actionCallback, token); + + public static (List Actions, SimulationState State) SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + { + var actions = new List(); + var sim = new Simulator(state, config.MaxStepCount); + while (true) + { + if (token.IsCancellationRequested) + break; + + if (sim.IsComplete) + break; + + var solver = new Solver(config, state); + + var s = Stopwatch.StartNew(); + solver.Search(config.Iterations, token); + s.Stop(); + + var (solution_actions, solution_node) = solver.Solution(); + + if (solver.MaxScore >= config.ScoreStorageThreshold) + { + actions.AddRange(solution_actions); + return (actions, solution_node.State); + } + + var chosen_action = solution_actions[0]; + actionCallback?.Invoke(chosen_action); + Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / s.Elapsed.TotalSeconds / 1000:0.00} kI/s"); + + (_, state) = sim.Execute(state, chosen_action); + actions.Add(chosen_action); + } + + return (actions, state); + } + + public static (List Actions, SimulationState State) SearchOneshotForked(SolverConfig config, SimulationInput input, CancellationToken token = default) => + SearchOneshotForked(config, new SimulationState(input), token); + + public static (List Actions, SimulationState State) SearchOneshotForked(SolverConfig config, SimulationState state, CancellationToken token = default) + { + var tasks = new Task<(float MaxScore, (List Actions, SimulationNode Node) Solution)>[config.ForkCount]; + for (var i = 0; i < config.ForkCount; ++i) + tasks[i] = Task.Run(() => + { + var solver = new Solver(config, state); + solver.Search(config.Iterations / config.ForkCount, token); + return (solver.MaxScore, solver.Solution()); + }, token); + Task.WaitAll(tasks, CancellationToken.None); + + var (solutionActions, solutionNode) = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution; + return (solutionActions, solutionNode.State); + } + + public static (List Actions, SimulationState State) SearchOneshot(SolverConfig config, SimulationInput input, CancellationToken token = default) => + SearchOneshot(config, new SimulationState(input), token); + + public static (List Actions, SimulationState State) SearchOneshot(SolverConfig config, SimulationState state, CancellationToken token = default) + { + var solver = new Solver(config, state); + solver.Search(config.Iterations, token); + var (solution_actions, solution_node) = solver.Solution(); + return (solution_actions, solution_node.State); + } +} diff --git a/Solver/Crafty/SolverConcurrent.cs b/Solver/Crafty/SolverConcurrent.cs deleted file mode 100644 index e49e74a..0000000 --- a/Solver/Crafty/SolverConcurrent.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Runtime.CompilerServices; -using Node = Craftimizer.Solver.Crafty.ArenaNode; - -namespace Craftimizer.Solver.Crafty; - -// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs -public sealed class SolverConcurrent : ISolver -{ - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static void LoadChildData(Span scoreSums, Span visits, Span maxScores, ref Node[] chunk, int iterCount) - { - for (var j = 0; j < iterCount; ++j) - { - var node = chunk[j]?.State.Scores ?? new(); - scoreSums[j] = node.ScoreSum; - visits[j] = node.Visits; - maxScores[j] = node.MaxScore; - } - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node? EvalBestChild(ref SolverConfig config, int parentVisits, ref ArenaBuffer children) => - parentVisits == 0 ? - null : - SolverUtils.EvalBestChild(ref config, parentVisits, ref children); - - [Pure] - public static Node Select(ref SolverConfig config, Node rootNode) - { - var node = rootNode; - while (true) - { - var expandable = !node.State.AvailableActions.IsEmpty; - var likelyTerminal = node.Children.Count == 0; - if (expandable || likelyTerminal) - return node; - - // select the node with the highest score - // if null (current node is invalid & not backpropagated just yet), try again from root - node = EvalBestChild(ref config, node.State.Scores.Visits, ref node.Children) ?? rootNode; - } - } - - public static (Node ExpandedNode, float Score)? ExpandAndRollout(ref SolverConfig config, Node rootNode, Random random, Simulator simulator, Node initialNode) - { - ref var initialState = ref initialNode.State; - // expand once - if (initialState.IsComplete) - return (initialNode, initialState.CalculateScore(config.MaxStepCount) ?? 0); - - var poppedAction = initialState.AvailableActions.PopRandomConcurrent(random); - if (!poppedAction.HasValue) - return null; - var expandedNode = initialNode.AddConcurrent(SolverUtils.Execute(simulator, initialState.State, poppedAction.Value, true)); - - return SolverUtils.Rollout(ref config, rootNode, expandedNode, random, simulator); - } - - public static void Backpropagate(Node rootNode, Node startNode, float score) - { - while (true) - { - startNode.State.Scores.VisitConcurrent(score); - - if (startNode == rootNode) - break; - - startNode = startNode.Parent!; - } - } - - public static bool SearchIter(ref SolverConfig config, Node rootNode, Random random, Simulator simulator) - { - var selectedNode = Select(ref config, rootNode); - var rolledOut = ExpandAndRollout(ref config, rootNode, random, simulator, selectedNode); - if (!rolledOut.HasValue) - return false; - - var (endNode, score) = rolledOut.Value; - Backpropagate(rootNode, endNode, score); - return true; - } - - public static void SearchThread(SolverConfig config, Node rootNode, CancellationToken token) => - SolverUtils.Search(ref config, config.Iterations / config.ThreadCount, rootNode, token); - - public static void Search(ref SolverConfig config, Node rootNode, CancellationToken token) - { - var configP = config; - var tasks = new Task[config.ThreadCount]; - for (var i = 0; i < config.ThreadCount; ++i) - tasks[i] = Task.Run(() => SearchThread(configP, rootNode, token), token); - Task.WaitAll(tasks, CancellationToken.None); - } -} diff --git a/Solver/Crafty/SolverConfig.cs b/Solver/Crafty/SolverConfig.cs index 46c7515..17a2d9b 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/Crafty/SolverConfig.cs @@ -10,15 +10,33 @@ public readonly record struct SolverConfig public float MaxScoreWeightingConstant { get; init; } public float ExplorationConstant { get; init; } public int MaxStepCount { get; init; } - public int ThreadCount { get; init; } + public int MaxRolloutStepCount { get; init; } + public int ForkCount { get; init; } + public int FurcatedActionCount { get; init; } + public bool StrictActions { get; init; } + + public float ScoreProgressBonus { get; init; } + public float ScoreQualityBonus { get; init; } + public float ScoreDurabilityBonus { get; init; } + public float ScoreCPBonus { get; init; } + public float ScoreFewerStepsBonus { get; init; } public SolverConfig() { Iterations = 300000; ScoreStorageThreshold = 1f; MaxScoreWeightingConstant = 0.1f; - ExplorationConstant = 4f; + ExplorationConstant = 4; MaxStepCount = 25; - ThreadCount = Environment.ProcessorCount; + MaxRolloutStepCount = MaxStepCount; + ForkCount = Environment.ProcessorCount; + FurcatedActionCount = ForkCount / 2; + StrictActions = true; + + ScoreProgressBonus = .20f; + ScoreQualityBonus = .65f; + ScoreDurabilityBonus = .05f; + ScoreCPBonus = .05f; + ScoreFewerStepsBonus = .05f; } } diff --git a/Solver/Crafty/SolverSingle.cs b/Solver/Crafty/SolverSingle.cs deleted file mode 100644 index 38f3c37..0000000 --- a/Solver/Crafty/SolverSingle.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Runtime.CompilerServices; -using Node = Craftimizer.Solver.Crafty.ArenaNode; - -namespace Craftimizer.Solver.Crafty; - -// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs -public sealed class SolverSingle : ISolver -{ - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static void LoadChildData(Span scoreSums, Span visits, Span maxScores, ref Node[] chunk, int iterCount) - { - for (var j = 0; j < iterCount; ++j) - { - ref var node = ref chunk[j].State.Scores; - scoreSums[j] = node.ScoreSum; - visits[j] = node.Visits; - maxScores[j] = node.MaxScore; - } - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node EvalBestChild(ref SolverConfig config, int parentVisits, ref ArenaBuffer children) => - SolverUtils.EvalBestChild(ref config, parentVisits, ref children); - - [Pure] - public static Node Select(ref SolverConfig config, Node node) - { - while (true) - { - var expandable = !node.State.AvailableActions.IsEmpty; - var likelyTerminal = node.Children.Count == 0; - if (expandable || likelyTerminal) - return node; - - // select the node with the highest score - node = EvalBestChild(ref config, node.State.Scores.Visits, ref node.Children); - } - } - - public static (Node ExpandedNode, float Score) ExpandAndRollout(ref SolverConfig config, Node rootNode, Random random, Simulator simulator, Node initialNode) - { - ref var initialState = ref initialNode.State; - // expand once - if (initialState.IsComplete) - return (initialNode, initialState.CalculateScore(config.MaxStepCount) ?? 0); - - var poppedAction = initialState.AvailableActions.PopRandom(random); - var expandedNode = initialNode.Add(SolverUtils.Execute(simulator, initialState.State, poppedAction, true)); - - return SolverUtils.Rollout(ref config, rootNode, expandedNode, random, simulator); - } - - public static void Backpropagate(Node rootNode, Node startNode, float score) - { - while (true) - { - startNode.State.Scores.Visit(score); - - if (startNode == rootNode) - break; - - startNode = startNode.Parent!; - } - } - - public static bool SearchIter(ref SolverConfig config, Node rootNode, Random random, Simulator simulator) - { - var selectedNode = Select(ref config, rootNode); - var (endNode, score) = ExpandAndRollout(ref config, rootNode, random, simulator, selectedNode); - - Backpropagate(rootNode, endNode, score); - return true; - } - - public static void Search(ref SolverConfig config, Node rootNode, CancellationToken token) => - SolverUtils.Search(ref config, config.Iterations, rootNode, token); -} diff --git a/Solver/Crafty/SolverUtils.cs b/Solver/Crafty/SolverUtils.cs deleted file mode 100644 index 526f559..0000000 --- a/Solver/Crafty/SolverUtils.cs +++ /dev/null @@ -1,250 +0,0 @@ -using Craftimizer.Simulator.Actions; -using Craftimizer.Simulator; -using Node = Craftimizer.Solver.Crafty.ArenaNode; -using System.Diagnostics.Contracts; -using System.Numerics; -using System.Runtime.CompilerServices; - -namespace Craftimizer.Solver.Crafty; -public static class SolverUtils -{ - public static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict) - { - (_, var newState) = simulator.Execute(state, action); - return new( - newState, - action, - simulator.CompletionState, - simulator.AvailableActionsHeuristic(strict) - ); - } - - public static (Node EndNode, CompletionState State) ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan actions, bool strict = false) - { - foreach (var action in actions) - { - var state = startNode.State; - if (state.IsComplete) - return (startNode, state.CompletionState); - - if (!state.AvailableActions.HasAction(action)) - return (startNode, CompletionState.InvalidAction); - state.AvailableActions.RemoveAction(action); - - startNode = startNode.Add(Execute(simulator, state.State, action, strict)); - } - - return (startNode, startNode.State.CompletionState); - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node ChildMaxScore(ref ArenaBuffer children) - { - var length = children.Count; - var vecLength = Vector.Count; - - Span scores = stackalloc float[vecLength]; - - var max = (0, 0); - var maxScore = 0f; - for (var i = 0; length > 0; ++i) - { - var iterCount = Math.Min(vecLength, length); - - ref var chunk = ref children.Data[i]; - for (var j = 0; j < iterCount; ++j) - scores[j] = chunk[j].State.Scores.MaxScore; - - var idx = Intrinsics.HMaxIndex(new Vector(scores), iterCount); - - if (scores[idx] >= maxScore) - { - max = (i, idx); - maxScore = scores[idx]; - } - - length -= iterCount; - } - - return children.Data[max.Item1][max.Item2]; - } - - [Pure] - public static (List Actions, SimulationNode Node) Solution(Node node) - { - var actions = new List(); - while (node.Children.Count != 0) - { - node = ChildMaxScore(ref node.Children); - - if (node.State.Action != null) - actions.Add(node.State.Action.Value); - } - - return (actions, node.State); - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static Node EvalBestChild(ref SolverConfig config, int parentVisits, ref ArenaBuffer children) where S : ISolver - { - var length = children.Count; - var vecLength = Vector.Count; - - var C = MathF.Sqrt(config.ExplorationConstant * MathF.Log(parentVisits)); - var w = config.MaxScoreWeightingConstant; - var W = 1f - w; - var CVector = new Vector(C); - - Span scoreSums = stackalloc float[vecLength]; - Span visits = stackalloc int[vecLength]; - Span maxScores = stackalloc float[vecLength]; - - var max = (0, 0); - var maxScore = 0f; - for (var i = 0; length > 0; ++i) - { - var iterCount = Math.Min(vecLength, length); - - S.LoadChildData(scoreSums, visits, maxScores, ref children.Data[i], iterCount); - - var s = new Vector(scoreSums); - var m = new Vector(maxScores); - var vInt = new Vector(visits); - vInt = Vector.Max(vInt, Vector.One); - var v = Vector.ConvertToSingle(vInt); - var exploitation = (W * (s / v)) + (w * m); - var exploration = CVector * Intrinsics.ReciprocalSqrt(v); - var evalScores = exploitation + exploration; - - var idx = Intrinsics.HMaxIndex(evalScores, iterCount); - - if (evalScores[idx] >= maxScore) - { - max = (i, idx); - maxScore = evalScores[idx]; - } - - length -= iterCount; - } - - return children.Data[max.Item1][max.Item2]; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (Node ExpandedNode, float Score) Rollout(ref SolverConfig config, Node rootNode, Node expandedNode, Random random, Simulator simulator) - { - // playout to a terminal state - var currentState = expandedNode.State.State; - var currentCompletionState = expandedNode.State.SimulationCompletionState; - var currentActions = expandedNode.State.AvailableActions; - - byte actionCount = 0; - Span actions = stackalloc ActionType[config.MaxStepCount - currentState.ActionCount]; - while (true) - { - if (SimulationNode.GetCompletionState(currentCompletionState, currentActions) != CompletionState.Incomplete) - break; - var nextAction = currentActions.SelectRandom(random); - actions[actionCount++] = nextAction; - (_, currentState) = simulator.Execute(currentState, nextAction); - currentCompletionState = simulator.CompletionState; - currentActions = simulator.AvailableActionsHeuristic(true); - } - - // store the result if a max score was reached - var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config.MaxStepCount) ?? 0; - if (currentCompletionState == CompletionState.ProgressComplete) - { - if (score >= config.ScoreStorageThreshold && score >= rootNode.State.Scores.MaxScore) - { - (var terminalNode, _) = ExecuteActions(simulator, expandedNode, actions[..actionCount], true); - return (terminalNode, score); - } - } - return (expandedNode, score); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Search(ref SolverConfig config, int iterations, Node rootNode, CancellationToken token) where S : ISolver - { - Simulator simulator = new(rootNode.State.State, config.MaxStepCount); - var random = rootNode.State.State.Input.Random; - for (var i = 0; i < iterations; i++) - { - if (token.IsCancellationRequested) - break; - - if (!S.SearchIter(ref config, rootNode, random, simulator)) - { - // Retry, count this iteration as moot - i--; - continue; - } - } - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node CreateRootNode(SolverConfig config, SimulationInput input, bool strict) => - CreateRootNode(config, new SimulationState(input), strict); - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node CreateRootNode(SolverConfig config, SimulationState state, bool strict) - { - var sim = new Simulator(state, config.MaxStepCount); - return new(new( - state, - null, - sim.CompletionState, - sim.AvailableActionsHeuristic(strict) - )); - } - - public static (List Actions, SimulationState State) SearchStepwise(SolverConfig config, SimulationInput input, Action? actionCallback, CancellationToken token = default) where S : ISolver => - SearchStepwise(config, new SimulationState(input), actionCallback, token); - - public static (List Actions, SimulationState State) SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token = default) where S : ISolver - { - var actions = new List(); - var sim = new Simulator(state, config.MaxStepCount); - var rootNode = CreateRootNode(config, state, true); - while (!sim.IsComplete) - { - if (token.IsCancellationRequested) - break; - - S.Search(ref config, rootNode, token); - var (solution_actions, solution_node) = Solution(rootNode); - - if (solution_node.Scores.MaxScore >= 1.0) - { - actions.AddRange(solution_actions); - return (actions, solution_node.State); - } - - var chosen_action = solution_actions[0]; - (_, state) = sim.Execute(state, chosen_action); - actions.Add(chosen_action); - - actionCallback?.Invoke(chosen_action); - - rootNode = CreateRootNode(config, state, true); - } - - return (actions, state); - } - - public static (List Actions, SimulationState State) SearchOneshot(SolverConfig config, SimulationInput input, CancellationToken token = default) where S : ISolver => - SearchOneshot(config, new SimulationState(input), token); - - public static (List Actions, SimulationState State) SearchOneshot(SolverConfig config, SimulationState state, CancellationToken token = default) where S : ISolver - { - var rootNode = CreateRootNode(config, state, false); - S.Search(ref config, rootNode, token); - var (solution_actions, solution_node) = Solution(rootNode); - return (solution_actions, solution_node.State); - } -}