From e4d9e3a52e4446d0fe12b52e844522f8d93359cf Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 7 Jul 2023 15:45:42 +0200 Subject: [PATCH 01/10] Offload node score buffers --- Solver/Crafty/ArenaBuffer.cs | 18 ++-- Solver/Crafty/ArenaNode.cs | 12 ++- Solver/Crafty/ISolver.cs | 6 +- Solver/Crafty/Intrinsics.cs | 3 +- Solver/Crafty/NodeScoresBuffer.cs | 90 +++++++++++++++++++ .../Crafty/{NodeScores.cs => RootScores.cs} | 2 +- Solver/Crafty/SimulationNode.cs | 1 - Solver/Crafty/SolverConcurrent.cs | 57 ++++++------ Solver/Crafty/SolverSingle.cs | 44 ++++----- Solver/Crafty/SolverUtils.cs | 52 +++++------ 10 files changed, 188 insertions(+), 97 deletions(-) create mode 100644 Solver/Crafty/NodeScoresBuffer.cs rename Solver/Crafty/{NodeScores.cs => RootScores.cs} (94%) diff --git a/Solver/Crafty/ArenaBuffer.cs b/Solver/Crafty/ArenaBuffer.cs index ccd7709..3ec0f9c 100644 --- a/Solver/Crafty/ArenaBuffer.cs +++ b/Solver/Crafty/ArenaBuffer.cs @@ -5,7 +5,7 @@ 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. @@ -17,39 +17,41 @@ public struct ArenaBuffer private static int BatchCount = MaxSize / BatchSize; - public T[][] Data; + public ArenaNode[][] Data; private int index; // Unused in single threaded workload private int count; public readonly int Count => count; - public void AddConcurrent(T node) + public void AddConcurrent(ArenaNode node) { if (Data == null) - Interlocked.CompareExchange(ref Data, new T[BatchCount][], null); + Interlocked.CompareExchange(ref Data, new ArenaNode[BatchCount][], null); var idx = Interlocked.Increment(ref index) - 1; var (arrayIdx, subIdx) = GetArrayIndex(idx); if (Data[arrayIdx] == null) - Interlocked.CompareExchange(ref Data[arrayIdx], new T[BatchSize], null); + Interlocked.CompareExchange(ref Data[arrayIdx], new ArenaNode[BatchSize], null); + node.ChildIdx = (arrayIdx, subIdx); Data[arrayIdx][subIdx] = node; Interlocked.Increment(ref count); } - public void Add(T node) + public void Add(ArenaNode node) { - Data ??= new T[BatchCount][]; + Data ??= new ArenaNode[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..90d5b6c 100644 --- a/Solver/Crafty/ArenaNode.cs +++ b/Solver/Crafty/ArenaNode.cs @@ -5,21 +5,30 @@ 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; } + public ArenaNode ChildAt((int arrayIdx, int subIdx) at) => + Children.Data[at.arrayIdx][at.subIdx]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArenaNode AddConcurrent(T state) { var node = new ArenaNode(state, this); Children.AddConcurrent(node); + ChildScores.AddConcurrent(); return node; } @@ -28,6 +37,7 @@ public sealed class ArenaNode where T : struct { var node = new ArenaNode(state, this); Children.Add(node); + ChildScores.Add(); return node; } } diff --git a/Solver/Crafty/ISolver.cs b/Solver/Crafty/ISolver.cs index 050b3f7..7c760f2 100644 --- a/Solver/Crafty/ISolver.cs +++ b/Solver/Crafty/ISolver.cs @@ -4,9 +4,7 @@ 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, RootScores rootScores, Node rootNode, Random random, Simulator simulator); - abstract static bool SearchIter(ref SolverConfig config, Node rootNode, Random random, Simulator simulator); - - abstract static void Search(ref SolverConfig config, Node rootNode, CancellationToken token); + abstract static void Search(ref SolverConfig config, RootScores rootScores, Node rootNode, CancellationToken token); } diff --git a/Solver/Crafty/Intrinsics.cs b/Solver/Crafty/Intrinsics.cs index a1baef0..33e95f0 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) : diff --git a/Solver/Crafty/NodeScoresBuffer.cs b/Solver/Crafty/NodeScoresBuffer.cs new file mode 100644 index 0000000..1e93ee9 --- /dev/null +++ b/Solver/Crafty/NodeScoresBuffer.cs @@ -0,0 +1,90 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +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 int BatchSize = Vector.Count; + private static int BatchSizeBits = int.Log2(BatchSize); + private static int BatchSizeMask = BatchSize - 1; + + private static int BatchCount = MaxSize / BatchSize; + + public ScoresBatch[] Data; + private int index; + private int count; + + public readonly int Count => count; + + public void AddConcurrent() + { + if (Data == null) + Interlocked.CompareExchange(ref Data, new ScoresBatch[BatchCount], null); + + var idx = Interlocked.Increment(ref index) - 1; + + var (arrayIdx, _) = GetArrayIndex(idx); + + if (Data[arrayIdx] == null) + Interlocked.CompareExchange(ref Data[arrayIdx], new ScoresBatch(), null); + + Interlocked.Increment(ref count); + } + + public void Add() + { + Data ??= new ScoresBatch[BatchCount]; + + var idx = count++; + + var (arrayIdx, _) = GetArrayIndex(idx); + + Data[arrayIdx] ??= new(); + } + + public readonly void VisitConcurrent((int arrayIdx, int subIdx) at, float score) + { + Intrinsics.CASAdd(ref Data[at.arrayIdx].ScoreSum.Span[at.subIdx], score); + Intrinsics.CASMax(ref Data[at.arrayIdx].MaxScore.Span[at.subIdx], score); + Interlocked.Increment(ref Data[at.arrayIdx].Visits.Span[at.subIdx]); + } + + 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 94% rename from Solver/Crafty/NodeScores.cs rename to Solver/Crafty/RootScores.cs index d0a2044..fa7350d 100644 --- a/Solver/Crafty/NodeScores.cs +++ b/Solver/Crafty/RootScores.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; namespace Craftimizer.Solver.Crafty; [StructLayout(LayoutKind.Auto)] -public struct NodeScores +public sealed class RootScores { public float ScoreSum; public float MaxScore; diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs index e66ad23..ad2aef2 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); diff --git a/Solver/Crafty/SolverConcurrent.cs b/Solver/Crafty/SolverConcurrent.cs index a258d9c..91080a0 100644 --- a/Solver/Crafty/SolverConcurrent.cs +++ b/Solver/Crafty/SolverConcurrent.cs @@ -7,29 +7,18 @@ 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 Node.ChildBuffer children) => + public static (int arrayIdx, int subIdx)? EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer children) => parentVisits == 0 ? null : SolverUtils.EvalBestChild(ref config, parentVisits, ref children); [Pure] - public static Node Select(ref SolverConfig config, Node rootNode) + public static Node Select(ref SolverConfig config, int rootNodeVisits, Node rootNode) { var node = rootNode; + var nodeVisits = rootNodeVisits; while (true) { var expandable = !node.State.AvailableActions.IsEmpty; @@ -39,11 +28,21 @@ public sealed class SolverConcurrent : ISolver // 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; + var at = EvalBestChild(ref config, nodeVisits, ref node.ChildScores); + if (at.HasValue) + { + nodeVisits = node.ChildScores.GetVisits(at.Value); + node = node.ChildAt(at.Value); + } + else + { + node = rootNode; + nodeVisits = rootNodeVisits; + } } } - public static (Node ExpandedNode, float Score)? ExpandAndRollout(ref SolverConfig config, Node rootNode, Random random, Simulator simulator, Node initialNode) + public static (Node ExpandedNode, float Score)? ExpandAndRollout(ref SolverConfig config, float maxScore, Node rootNode, Random random, Simulator simulator, Node initialNode) { ref var initialState = ref initialNode.State; // expand once @@ -55,43 +54,45 @@ public sealed class SolverConcurrent : ISolver return null; var expandedNode = initialNode.AddConcurrent(SolverUtils.Execute(simulator, initialState.State, poppedAction.Value, true)); - return SolverUtils.Rollout(ref config, rootNode, expandedNode, random, simulator); + return SolverUtils.Rollout(ref config, maxScore, rootNode, expandedNode, random, simulator); } - public static void Backpropagate(Node rootNode, Node startNode, float score) + public static void Backpropagate(RootScores rootScores, Node rootNode, Node startNode, float score) { while (true) { - startNode.State.Scores.VisitConcurrent(score); - if (startNode == rootNode) + { + rootScores.VisitConcurrent(score); break; + } + startNode.ParentScores!.Value.VisitConcurrent(startNode.ChildIdx, score); startNode = startNode.Parent!; } } - public static bool SearchIter(ref SolverConfig config, Node rootNode, Random random, Simulator simulator) + public static bool SearchIter(ref SolverConfig config, RootScores rootScores, Node rootNode, Random random, Simulator simulator) { - var selectedNode = Select(ref config, rootNode); - var rolledOut = ExpandAndRollout(ref config, rootNode, random, simulator, selectedNode); + var selectedNode = Select(ref config, rootScores.Visits, rootNode); + var rolledOut = ExpandAndRollout(ref config, rootScores.MaxScore, rootNode, random, simulator, selectedNode); if (!rolledOut.HasValue) return false; var (endNode, score) = rolledOut.Value; - Backpropagate(rootNode, endNode, score); + Backpropagate(rootScores, 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 SearchThread(SolverConfig config, RootScores rootScores, Node rootNode, CancellationToken token) => + SolverUtils.Search(ref config, config.Iterations / config.ThreadCount, rootScores, rootNode, token); - public static void Search(ref SolverConfig config, Node rootNode, CancellationToken token) + public static void Search(ref SolverConfig config, RootScores rootScores, 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); + tasks[i] = Task.Run(() => SearchThread(configP, rootScores, rootNode, token), token); Task.WaitAll(tasks, CancellationToken.None); } } diff --git a/Solver/Crafty/SolverSingle.cs b/Solver/Crafty/SolverSingle.cs index a22fa56..b53fe08 100644 --- a/Solver/Crafty/SolverSingle.cs +++ b/Solver/Crafty/SolverSingle.cs @@ -7,25 +7,13 @@ 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 Node.ChildBuffer children) => + public static (int arrayIdx, int subIdx) EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer children) => SolverUtils.EvalBestChild(ref config, parentVisits, ref children); [Pure] - public static Node Select(ref SolverConfig config, Node node) + public static Node Select(ref SolverConfig config, int nodeVisits, Node node) { while (true) { @@ -35,11 +23,13 @@ public sealed class SolverSingle : ISolver return node; // select the node with the highest score - node = EvalBestChild(ref config, node.State.Scores.Visits, ref node.Children); + var at = EvalBestChild(ref config, nodeVisits, ref node.ChildScores); + nodeVisits = node.ChildScores.GetVisits(at); + node = node.ChildAt(at); } } - public static (Node ExpandedNode, float Score) ExpandAndRollout(ref SolverConfig config, Node rootNode, Random random, Simulator simulator, Node initialNode) + public static (Node ExpandedNode, float Score) ExpandAndRollout(ref SolverConfig config, float maxScore, Node rootNode, Random random, Simulator simulator, Node initialNode) { ref var initialState = ref initialNode.State; // expand once @@ -49,31 +39,33 @@ public sealed class SolverSingle : ISolver 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); + return SolverUtils.Rollout(ref config, maxScore, rootNode, expandedNode, random, simulator); } - public static void Backpropagate(Node rootNode, Node startNode, float score) + public static void Backpropagate(RootScores rootScores, Node rootNode, Node startNode, float score) { while (true) { - startNode.State.Scores.Visit(score); - if (startNode == rootNode) + { + rootScores.Visit(score); break; + } + startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score); startNode = startNode.Parent!; } } - public static bool SearchIter(ref SolverConfig config, Node rootNode, Random random, Simulator simulator) + public static bool SearchIter(ref SolverConfig config, RootScores rootScores, Node rootNode, Random random, Simulator simulator) { - var selectedNode = Select(ref config, rootNode); - var (endNode, score) = ExpandAndRollout(ref config, rootNode, random, simulator, selectedNode); + var selectedNode = Select(ref config, rootScores.Visits, rootNode); + var (endNode, score) = ExpandAndRollout(ref config, rootScores.MaxScore, rootNode, random, simulator, selectedNode); - Backpropagate(rootNode, endNode, score); + Backpropagate(rootScores, rootNode, endNode, score); return true; } - public static void Search(ref SolverConfig config, Node rootNode, CancellationToken token) => - SolverUtils.Search(ref config, config.Iterations, rootNode, token); + public static void Search(ref SolverConfig config, RootScores rootScores, Node rootNode, CancellationToken token) => + SolverUtils.Search(ref config, config.Iterations, rootScores, rootNode, token); } diff --git a/Solver/Crafty/SolverUtils.cs b/Solver/Crafty/SolverUtils.cs index a08d525..10cd3d3 100644 --- a/Solver/Crafty/SolverUtils.cs +++ b/Solver/Crafty/SolverUtils.cs @@ -39,35 +39,32 @@ public static class SolverUtils [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node ChildMaxScore(ref Node.ChildBuffer children) + public static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores) { - var length = children.Count; + var length = scores.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; + ref var chunk = ref scores.Data[i]; + var m = new Vector(chunk.MaxScore.Span); - var idx = Intrinsics.HMaxIndex(new Vector(scores), iterCount); + var idx = Intrinsics.HMaxIndex(m, iterCount); - if (scores[idx] >= maxScore) + if (m[idx] >= maxScore) { max = (i, idx); - maxScore = scores[idx]; + maxScore = m[idx]; } length -= iterCount; } - return children.Data[max.Item1][max.Item2]; + return max; } [Pure] @@ -76,7 +73,7 @@ public static class SolverUtils var actions = new List(); while (node.Children.Count != 0) { - node = ChildMaxScore(ref node.Children); + node = node.ChildAt(ChildMaxScore(ref node.ChildScores)); if (node.State.Action != null) actions.Add(node.State.Action.Value); @@ -87,9 +84,9 @@ public static class SolverUtils [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static Node EvalBestChild(ref SolverConfig config, int parentVisits, ref Node.ChildBuffer children) where S : ISolver + public static (int arrayIdx, int subIdx) EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer scores) where S : ISolver { - var length = children.Count; + var length = scores.Count; var vecLength = Vector.Count; var C = MathF.Sqrt(config.ExplorationConstant * MathF.Log(parentVisits)); @@ -107,13 +104,14 @@ public static class SolverUtils { var iterCount = Math.Min(vecLength, length); - S.LoadChildData(scoreSums, visits, maxScores, ref children.Data[i], iterCount); + 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); - 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; @@ -129,11 +127,11 @@ public static class SolverUtils length -= iterCount; } - return children.Data[max.Item1][max.Item2]; + return max; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (Node ExpandedNode, float Score) Rollout(ref SolverConfig config, Node rootNode, Node expandedNode, Random random, Simulator simulator) + public static (Node ExpandedNode, float Score) Rollout(ref SolverConfig config, float maxScore, Node rootNode, Node expandedNode, Random random, Simulator simulator) { // playout to a terminal state var currentState = expandedNode.State.State; @@ -157,7 +155,7 @@ public static class SolverUtils var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config.MaxStepCount) ?? 0; if (currentCompletionState == CompletionState.ProgressComplete) { - if (score >= config.ScoreStorageThreshold && score >= rootNode.State.Scores.MaxScore) + if (score >= config.ScoreStorageThreshold && score >= maxScore) { (var terminalNode, _) = ExecuteActions(simulator, expandedNode, actions[..actionCount], true); return (terminalNode, score); @@ -167,7 +165,7 @@ public static class SolverUtils } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Search(ref SolverConfig config, int iterations, Node rootNode, CancellationToken token) where S : ISolver + public static void Search(ref SolverConfig config, int iterations, RootScores rootScores, Node rootNode, CancellationToken token) where S : ISolver { Simulator simulator = new(rootNode.State.State, config.MaxStepCount); var random = rootNode.State.State.Input.Random; @@ -176,7 +174,7 @@ public static class SolverUtils if (token.IsCancellationRequested) break; - if (!S.SearchIter(ref config, rootNode, random, simulator)) + if (!S.SearchIter(ref config, rootScores, rootNode, random, simulator)) { // Retry, count this iteration as moot i--; @@ -211,15 +209,16 @@ public static class SolverUtils var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); var rootNode = CreateRootNode(config, state, true); + RootScores rootScores = new(); while (!sim.IsComplete) { if (token.IsCancellationRequested) break; - S.Search(ref config, rootNode, token); + S.Search(ref config, rootScores, rootNode, token); var (solution_actions, solution_node) = Solution(rootNode); - if (solution_node.Scores.MaxScore >= 1.0) + if (rootScores.MaxScore >= 1.0) { actions.AddRange(solution_actions); return (actions, solution_node.State); @@ -243,7 +242,8 @@ public static class SolverUtils 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); + RootScores rootScores = new(); + S.Search(ref config, rootScores, rootNode, token); var (solution_actions, solution_node) = Solution(rootNode); return (solution_actions, solution_node.State); } From 7d8fc9ff8fd736d2a5a73e3c816e5c53e9b8acdd Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 7 Jul 2023 16:35:22 +0200 Subject: [PATCH 02/10] Remove extra stackallocs --- Solver/Crafty/SolverUtils.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Solver/Crafty/SolverUtils.cs b/Solver/Crafty/SolverUtils.cs index 10cd3d3..fbf92a0 100644 --- a/Solver/Crafty/SolverUtils.cs +++ b/Solver/Crafty/SolverUtils.cs @@ -94,10 +94,6 @@ public static class SolverUtils 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) From 3ab50d389e60eab64b0f240761cfc6dd4aa6fc0f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 7 Jul 2023 18:13:27 +0200 Subject: [PATCH 03/10] Fix concurrency, add forked simulations --- Benchmark/Program.cs | 8 ++-- Solver/Crafty/ArenaNode.cs | 8 ++-- Solver/Crafty/SolverConcurrent.cs | 5 +++ Solver/Crafty/SolverConfig.cs | 4 +- Solver/Crafty/SolverSingle.cs | 2 +- Solver/Crafty/SolverUtils.cs | 71 ++++++++++++++++++++++++++++--- 6 files changed, 84 insertions(+), 14 deletions(-) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index ef2ee2d..69f0147 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -46,14 +46,16 @@ internal static class Program var config = new SolverConfig() { - Iterations = 1_000_000, + Iterations = 100_000, ThreadCount = 8, }; Debugger.Break(); var s = Stopwatch.StartNew(); - if (true) - _ = SolverUtils.SearchStepwise(config, input, a => Console.WriteLine(a)); + if (true) { + (_, var state) = SolverUtils.SearchStepwise(config, input, a => Console.WriteLine(a)); + Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); + } else { (var actions, _) = SolverUtils.SearchOneshot(config, input); diff --git a/Solver/Crafty/ArenaNode.cs b/Solver/Crafty/ArenaNode.cs index 90d5b6c..89fba01 100644 --- a/Solver/Crafty/ArenaNode.cs +++ b/Solver/Crafty/ArenaNode.cs @@ -20,15 +20,15 @@ public sealed class ArenaNode where T : struct Parent = parent; } - public ArenaNode ChildAt((int arrayIdx, int subIdx) at) => - Children.Data[at.arrayIdx][at.subIdx]; + public ArenaNode? ChildAt((int arrayIdx, int subIdx) at) => + Children.Data?[at.arrayIdx]?[at.subIdx]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArenaNode AddConcurrent(T state) { var node = new ArenaNode(state, this); - Children.AddConcurrent(node); ChildScores.AddConcurrent(); + Children.AddConcurrent(node); return node; } @@ -36,8 +36,8 @@ public sealed class ArenaNode where T : struct public ArenaNode Add(T state) { var node = new ArenaNode(state, this); - Children.Add(node); ChildScores.Add(); + Children.Add(node); return node; } } diff --git a/Solver/Crafty/SolverConcurrent.cs b/Solver/Crafty/SolverConcurrent.cs index 91080a0..3bb6032 100644 --- a/Solver/Crafty/SolverConcurrent.cs +++ b/Solver/Crafty/SolverConcurrent.cs @@ -33,6 +33,11 @@ public sealed class SolverConcurrent : ISolver { nodeVisits = node.ChildScores.GetVisits(at.Value); node = node.ChildAt(at.Value); + if (node == null) + { + node = rootNode; + nodeVisits = rootNodeVisits; + } } else { diff --git a/Solver/Crafty/SolverConfig.cs b/Solver/Crafty/SolverConfig.cs index 46c7515..4ffd308 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/Crafty/SolverConfig.cs @@ -10,6 +10,7 @@ public readonly record struct SolverConfig public float MaxScoreWeightingConstant { get; init; } public float ExplorationConstant { get; init; } public int MaxStepCount { get; init; } + public int MaxRolloutStepCount { get; init; } public int ThreadCount { get; init; } public SolverConfig() @@ -17,8 +18,9 @@ public readonly record struct SolverConfig Iterations = 300000; ScoreStorageThreshold = 1f; MaxScoreWeightingConstant = 0.1f; - ExplorationConstant = 4f; + ExplorationConstant = 2; MaxStepCount = 25; + MaxRolloutStepCount = 99; ThreadCount = Environment.ProcessorCount; } } diff --git a/Solver/Crafty/SolverSingle.cs b/Solver/Crafty/SolverSingle.cs index b53fe08..0ecc287 100644 --- a/Solver/Crafty/SolverSingle.cs +++ b/Solver/Crafty/SolverSingle.cs @@ -25,7 +25,7 @@ public sealed class SolverSingle : ISolver // select the node with the highest score var at = EvalBestChild(ref config, nodeVisits, ref node.ChildScores); nodeVisits = node.ChildScores.GetVisits(at); - node = node.ChildAt(at); + node = node.ChildAt(at)!; } } diff --git a/Solver/Crafty/SolverUtils.cs b/Solver/Crafty/SolverUtils.cs index fbf92a0..959a8c8 100644 --- a/Solver/Crafty/SolverUtils.cs +++ b/Solver/Crafty/SolverUtils.cs @@ -1,9 +1,9 @@ -using Craftimizer.Simulator.Actions; using Craftimizer.Simulator; -using Node = Craftimizer.Solver.Crafty.ArenaNode; +using Craftimizer.Simulator.Actions; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; +using Node = Craftimizer.Solver.Crafty.ArenaNode; namespace Craftimizer.Solver.Crafty; public static class SolverUtils @@ -73,7 +73,7 @@ public static class SolverUtils var actions = new List(); while (node.Children.Count != 0) { - node = node.ChildAt(ChildMaxScore(ref node.ChildScores)); + node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!; if (node.State.Action != null) actions.Add(node.State.Action.Value); @@ -82,6 +82,23 @@ public static class SolverUtils return (actions, node.State); } + // 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)] public static (int arrayIdx, int subIdx) EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer scores) where S : ISolver @@ -135,8 +152,8 @@ public static class SolverUtils var currentActions = expandedNode.State.AvailableActions; byte actionCount = 0; - Span actions = stackalloc ActionType[config.MaxStepCount - currentState.ActionCount]; - while (true) + Span actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)]; + while (actionCount < actions.Length) { if (SimulationNode.GetCompletionState(currentCompletionState, currentActions) != CompletionState.Incomplete) break; @@ -197,6 +214,50 @@ public static class SolverUtils )); } + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationInput input, Action? actionCallback, CancellationToken token = default) where S : ISolver => + SearchStepwiseForked(config, forkCount, new SimulationState(input), actionCallback, token); + + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationState state, Action? actionCallback, CancellationToken token = default) where S : ISolver + { + var actions = new List(); + var sim = new Simulator(state, config.MaxStepCount); + while (!sim.IsComplete) + { + if (token.IsCancellationRequested) + break; + + var tasks = new Task<(float score, List actions, SimulationState state)>[forkCount]; + for (var i = 0; i < forkCount; ++i) + tasks[i] = Task.Run(() => + { + var rootNode = CreateRootNode(config, state, true); + RootScores rootScores = new(); + + S.Search(ref config, rootScores, rootNode, token); + var (solution_actions, solution_node) = Solution(rootNode); + + return (rootScores.MaxScore, solution_actions, solution_node.State); + }, token); + Task.WaitAll(tasks, CancellationToken.None); + + var (score, solution_actions, solution_state) = tasks.Select(t => t.Result).MaxBy(r => r.score); + + if (score >= 1.0) + { + actions.AddRange(solution_actions); + return (actions, solution_state); + } + + var chosen_action = solution_actions[0]; + (_, state) = sim.Execute(state, chosen_action); + actions.Add(chosen_action); + + actionCallback?.Invoke(chosen_action); + } + + return (actions, state); + } + 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); From 636501ab8659d50d8360e3c2b2f3c61515d0fb4c Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 7 Jul 2023 20:17:35 +0200 Subject: [PATCH 04/10] Remove all concurrency code Muddled the code too much, and only gave a marginal performance improvement in the grand scheme of things. Other ways to parallelize MCTS will be nicer to implement and could yield better results. --- Benchmark/Program.cs | 12 +- Solver/Crafty/ActionSet.cs | 69 +------ Solver/Crafty/ArenaBuffer.cs | 33 +--- Solver/Crafty/ArenaNode.cs | 9 - Solver/Crafty/ISolver.cs | 10 - Solver/Crafty/Intrinsics.cs | 24 --- Solver/Crafty/NodeScoresBuffer.cs | 40 +--- Solver/Crafty/RootScores.cs | 7 - Solver/Crafty/{SolverUtils.cs => Solver.cs} | 206 ++++++++++++-------- Solver/Crafty/SolverConcurrent.cs | 103 ---------- Solver/Crafty/SolverSingle.cs | 71 ------- 11 files changed, 153 insertions(+), 431 deletions(-) delete mode 100644 Solver/Crafty/ISolver.cs rename Solver/Crafty/{SolverUtils.cs => Solver.cs} (61%) delete mode 100644 Solver/Crafty/SolverConcurrent.cs delete mode 100644 Solver/Crafty/SolverSingle.cs diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 69f0147..6ebdbdc 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -46,22 +46,22 @@ internal static class Program var config = new SolverConfig() { - Iterations = 100_000, + Iterations = 30_000, ThreadCount = 8, }; Debugger.Break(); var s = Stopwatch.StartNew(); if (true) { - (_, var state) = SolverUtils.SearchStepwise(config, input, a => Console.WriteLine(a)); + (_, var state) = Solver.Crafty.Solver.SearchStepwise(config, input, a => Console.WriteLine(a)); Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); } else { - (var actions, _) = SolverUtils.SearchOneshot(config, input); - foreach (var action in actions) - Console.Write($">{action.IntName()}"); - Console.WriteLine(); + //(var actions, _) = SolverUtils.SearchOneshot(config, input); + //foreach (var action in actions) + // Console.Write($">{action.IntName()}"); + //Console.WriteLine(); } s.Stop(); Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}"); diff --git a/Solver/Crafty/ActionSet.cs b/Solver/Crafty/ActionSet.cs index 8d0a49c..cd1d067 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 = true; + 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 3ec0f9c..c3775c2 100644 --- a/Solver/Crafty/ArenaBuffer.cs +++ b/Solver/Crafty/ArenaBuffer.cs @@ -11,41 +11,20 @@ public struct ArenaBuffer where T : struct // 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 ArenaNode[][] Data; - private int index; // Unused in single threaded workload - private int count; - - public readonly int Count => count; - - public void AddConcurrent(ArenaNode node) - { - if (Data == null) - Interlocked.CompareExchange(ref Data, new ArenaNode[BatchCount][], null); - - var idx = Interlocked.Increment(ref index) - 1; - - var (arrayIdx, subIdx) = GetArrayIndex(idx); - - if (Data[arrayIdx] == null) - Interlocked.CompareExchange(ref Data[arrayIdx], new ArenaNode[BatchSize], null); - - node.ChildIdx = (arrayIdx, subIdx); - Data[arrayIdx][subIdx] = node; - - Interlocked.Increment(ref count); - } + public int Count { get; private set; } public void Add(ArenaNode node) { Data ??= new ArenaNode[BatchCount][]; - var idx = count++; + var idx = Count++; var (arrayIdx, subIdx) = GetArrayIndex(idx); diff --git a/Solver/Crafty/ArenaNode.cs b/Solver/Crafty/ArenaNode.cs index 89fba01..5ac88ea 100644 --- a/Solver/Crafty/ArenaNode.cs +++ b/Solver/Crafty/ArenaNode.cs @@ -23,15 +23,6 @@ public sealed class ArenaNode where T : struct public ArenaNode? ChildAt((int arrayIdx, int subIdx) at) => Children.Data?[at.arrayIdx]?[at.subIdx]; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ArenaNode AddConcurrent(T state) - { - var node = new ArenaNode(state, this); - ChildScores.AddConcurrent(); - Children.AddConcurrent(node); - return node; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArenaNode Add(T state) { diff --git a/Solver/Crafty/ISolver.cs b/Solver/Crafty/ISolver.cs deleted file mode 100644 index 7c760f2..0000000 --- a/Solver/Crafty/ISolver.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Node = Craftimizer.Solver.Crafty.ArenaNode; - -namespace Craftimizer.Solver.Crafty; - -public interface ISolver -{ - abstract static bool SearchIter(ref SolverConfig config, RootScores rootScores, Node rootNode, Random random, Simulator simulator); - - abstract static void Search(ref SolverConfig config, RootScores rootScores, Node rootNode, CancellationToken token); -} diff --git a/Solver/Crafty/Intrinsics.cs b/Solver/Crafty/Intrinsics.cs index 33e95f0..1380ae9 100644 --- a/Solver/Crafty/Intrinsics.cs +++ b/Solver/Crafty/Intrinsics.cs @@ -124,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 index 1e93ee9..fa394bb 100644 --- a/Solver/Crafty/NodeScoresBuffer.cs +++ b/Solver/Crafty/NodeScoresBuffer.cs @@ -1,9 +1,6 @@ -using System; -using System.ComponentModel; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace Craftimizer.Solver.Crafty; @@ -28,51 +25,26 @@ public struct NodeScoresBuffer // 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 ScoresBatch[] Data; - private int index; - private int count; - - public readonly int Count => count; - - public void AddConcurrent() - { - if (Data == null) - Interlocked.CompareExchange(ref Data, new ScoresBatch[BatchCount], null); - - var idx = Interlocked.Increment(ref index) - 1; - - var (arrayIdx, _) = GetArrayIndex(idx); - - if (Data[arrayIdx] == null) - Interlocked.CompareExchange(ref Data[arrayIdx], new ScoresBatch(), null); - - Interlocked.Increment(ref count); - } + public int Count { get; private set; } public void Add() { Data ??= new ScoresBatch[BatchCount]; - var idx = count++; + var idx = Count++; var (arrayIdx, _) = GetArrayIndex(idx); Data[arrayIdx] ??= new(); } - public readonly void VisitConcurrent((int arrayIdx, int subIdx) at, float score) - { - Intrinsics.CASAdd(ref Data[at.arrayIdx].ScoreSum.Span[at.subIdx], score); - Intrinsics.CASMax(ref Data[at.arrayIdx].MaxScore.Span[at.subIdx], score); - Interlocked.Increment(ref Data[at.arrayIdx].Visits.Span[at.subIdx]); - } - public readonly void Visit((int arrayIdx, int subIdx) at, float score) { Data[at.arrayIdx].ScoreSum.Span[at.subIdx] += score; diff --git a/Solver/Crafty/RootScores.cs b/Solver/Crafty/RootScores.cs index fa7350d..2ac7c2f 100644 --- a/Solver/Crafty/RootScores.cs +++ b/Solver/Crafty/RootScores.cs @@ -9,13 +9,6 @@ public sealed class RootScores 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/SolverUtils.cs b/Solver/Crafty/Solver.cs similarity index 61% rename from Solver/Crafty/SolverUtils.cs rename to Solver/Crafty/Solver.cs index 959a8c8..a6f4016 100644 --- a/Solver/Crafty/SolverUtils.cs +++ b/Solver/Crafty/Solver.cs @@ -1,14 +1,35 @@ -using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; +using Craftimizer.Simulator; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; using Node = Craftimizer.Solver.Crafty.ArenaNode; namespace Craftimizer.Solver.Crafty; -public static class SolverUtils + +// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs +public sealed class Solver { - public static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict) + private SolverConfig config; + private Node rootNode; + private RootScores rootScores; + + public float MaxScore => rootScores.MaxScore; + + public Solver(SolverConfig config, SimulationState state, bool strict) + { + this.config = config; + var sim = new Simulator(state, config.MaxStepCount); + rootNode = new(new( + state, + null, + sim.CompletionState, + sim.AvailableActionsHeuristic(strict) + )); + rootScores = new(); + } + + private static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict) { (_, var newState) = simulator.Execute(state, action); return new( @@ -19,27 +40,50 @@ public static class SolverUtils ); } - public static (Node EndNode, CompletionState State) ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan actions, bool strict = false) + 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, state.CompletionState); + return startNode; if (!state.AvailableActions.HasAction(action)) - return (startNode, CompletionState.InvalidAction); + return startNode; state.AvailableActions.RemoveAction(action); startNode = startNode.Add(Execute(simulator, state.State, action, strict)); } - return (startNode, startNode.State.CompletionState); + 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)] - public static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores) + private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores) { var length = scores.Count; var vecLength = Vector.Count; @@ -67,21 +111,6 @@ public static class SolverUtils return max; } - [Pure] - public static (List Actions, SimulationNode Node) Solution(Node node) - { - var actions = new List(); - while (node.Children.Count != 0) - { - node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!; - - if (node.State.Action != null) - actions.Add(node.State.Action.Value); - } - - return (actions, node.State); - } - // Calculates the best child node to explore next // Exploitation: ((1 - w) * (s / v)) + (w * m) // Exploration: sqrt(c * ln(V) / v) @@ -98,10 +127,9 @@ public static class SolverUtils // 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)] - public static (int arrayIdx, int subIdx) EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer scores) where S : ISolver + private (int arrayIdx, int subIdx) EvalBestChild(int parentVisits, ref NodeScoresBuffer scores) { var length = scores.Count; var vecLength = Vector.Count; @@ -143,9 +171,36 @@ public static class SolverUtils return max; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (Node ExpandedNode, float Score) Rollout(ref SolverConfig config, float maxScore, Node rootNode, Node expandedNode, Random random, Simulator simulator) + [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.MaxStepCount) ?? 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; @@ -168,56 +223,51 @@ public static class SolverUtils var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config.MaxStepCount) ?? 0; if (currentCompletionState == CompletionState.ProgressComplete) { - if (score >= config.ScoreStorageThreshold && score >= maxScore) + if (score >= config.ScoreStorageThreshold && score >= MaxScore) { - (var terminalNode, _) = ExecuteActions(simulator, expandedNode, actions[..actionCount], true); + 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!; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Search(ref SolverConfig config, int iterations, RootScores rootScores, Node rootNode, CancellationToken token) where S : ISolver + private void Search(CancellationToken token) { Simulator simulator = new(rootNode.State.State, config.MaxStepCount); var random = rootNode.State.State.Input.Random; - for (var i = 0; i < iterations; i++) + for (var i = 0; i < config.Iterations; i++) { if (token.IsCancellationRequested) break; - if (!S.SearchIter(ref config, rootScores, rootNode, random, simulator)) - { - // Retry, count this iteration as moot - i--; - continue; - } + var selectedNode = Select(); + var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); + + Backpropagate(endNode, score); } } - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Node CreateRootNode(SolverConfig config, SimulationInput input, bool strict) => - CreateRootNode(config, new SimulationState(input), strict); + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationInput input, Action? actionCallback, CancellationToken token = default) => + SearchStepwiseForked(config, forkCount, new SimulationState(input), actionCallback, token); - [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) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationInput input, Action? actionCallback, CancellationToken token = default) where S : ISolver => - SearchStepwiseForked(config, forkCount, new SimulationState(input), actionCallback, token); - - public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationState state, Action? actionCallback, CancellationToken token = default) where S : ISolver + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationState state, Action? actionCallback, CancellationToken token = default) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); @@ -230,13 +280,11 @@ public static class SolverUtils for (var i = 0; i < forkCount; ++i) tasks[i] = Task.Run(() => { - var rootNode = CreateRootNode(config, state, true); - RootScores rootScores = new(); + var solver = new Solver(config, state, true); + solver.Search(token); + var (solution_actions, solution_node) = solver.Solution(); - S.Search(ref config, rootScores, rootNode, token); - var (solution_actions, solution_node) = Solution(rootNode); - - return (rootScores.MaxScore, solution_actions, solution_node.State); + return (solver.MaxScore, solution_actions, solution_node.State); }, token); Task.WaitAll(tasks, CancellationToken.None); @@ -258,24 +306,23 @@ public static class SolverUtils return (actions, state); } - 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, SimulationInput input, Action? actionCallback, CancellationToken token = default) => + 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 + public static (List Actions, SimulationState State) SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token = default) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); - var rootNode = CreateRootNode(config, state, true); - RootScores rootScores = new(); + var solver = new Solver(config, state, true); while (!sim.IsComplete) { if (token.IsCancellationRequested) break; - S.Search(ref config, rootScores, rootNode, token); - var (solution_actions, solution_node) = Solution(rootNode); + solver.Search(token); + var (solution_actions, solution_node) = solver.Solution(); - if (rootScores.MaxScore >= 1.0) + if (solver.MaxScore >= 1.0) { actions.AddRange(solution_actions); return (actions, solution_node.State); @@ -287,21 +334,20 @@ public static class SolverUtils actionCallback?.Invoke(chosen_action); - rootNode = CreateRootNode(config, state, true); + solver = new Solver(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, 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) where S : ISolver + public static (List Actions, SimulationState State) SearchOneshot(SolverConfig config, SimulationState state, CancellationToken token = default) { - var rootNode = CreateRootNode(config, state, false); - RootScores rootScores = new(); - S.Search(ref config, rootScores, rootNode, token); - var (solution_actions, solution_node) = Solution(rootNode); + var solver = new Solver(config, state, false); + solver.Search(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 3bb6032..0000000 --- a/Solver/Crafty/SolverConcurrent.cs +++ /dev/null @@ -1,103 +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 -{ - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (int arrayIdx, int subIdx)? EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer children) => - parentVisits == 0 ? - null : - SolverUtils.EvalBestChild(ref config, parentVisits, ref children); - - [Pure] - public static Node Select(ref SolverConfig config, int rootNodeVisits, Node rootNode) - { - var node = rootNode; - var nodeVisits = rootNodeVisits; - 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 - var at = EvalBestChild(ref config, nodeVisits, ref node.ChildScores); - if (at.HasValue) - { - nodeVisits = node.ChildScores.GetVisits(at.Value); - node = node.ChildAt(at.Value); - if (node == null) - { - node = rootNode; - nodeVisits = rootNodeVisits; - } - } - else - { - node = rootNode; - nodeVisits = rootNodeVisits; - } - } - } - - public static (Node ExpandedNode, float Score)? ExpandAndRollout(ref SolverConfig config, float maxScore, 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, maxScore, rootNode, expandedNode, random, simulator); - } - - public static void Backpropagate(RootScores rootScores, Node rootNode, Node startNode, float score) - { - while (true) - { - if (startNode == rootNode) - { - rootScores.VisitConcurrent(score); - break; - } - startNode.ParentScores!.Value.VisitConcurrent(startNode.ChildIdx, score); - - startNode = startNode.Parent!; - } - } - - public static bool SearchIter(ref SolverConfig config, RootScores rootScores, Node rootNode, Random random, Simulator simulator) - { - var selectedNode = Select(ref config, rootScores.Visits, rootNode); - var rolledOut = ExpandAndRollout(ref config, rootScores.MaxScore, rootNode, random, simulator, selectedNode); - if (!rolledOut.HasValue) - return false; - - var (endNode, score) = rolledOut.Value; - Backpropagate(rootScores, rootNode, endNode, score); - return true; - } - - public static void SearchThread(SolverConfig config, RootScores rootScores, Node rootNode, CancellationToken token) => - SolverUtils.Search(ref config, config.Iterations / config.ThreadCount, rootScores, rootNode, token); - - public static void Search(ref SolverConfig config, RootScores rootScores, 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, rootScores, rootNode, token), token); - Task.WaitAll(tasks, CancellationToken.None); - } -} diff --git a/Solver/Crafty/SolverSingle.cs b/Solver/Crafty/SolverSingle.cs deleted file mode 100644 index 0ecc287..0000000 --- a/Solver/Crafty/SolverSingle.cs +++ /dev/null @@ -1,71 +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 -{ - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (int arrayIdx, int subIdx) EvalBestChild(ref SolverConfig config, int parentVisits, ref NodeScoresBuffer children) => - SolverUtils.EvalBestChild(ref config, parentVisits, ref children); - - [Pure] - public static Node Select(ref SolverConfig config, int nodeVisits, 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 - var at = EvalBestChild(ref config, nodeVisits, ref node.ChildScores); - nodeVisits = node.ChildScores.GetVisits(at); - node = node.ChildAt(at)!; - } - } - - public static (Node ExpandedNode, float Score) ExpandAndRollout(ref SolverConfig config, float maxScore, 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, maxScore, rootNode, expandedNode, random, simulator); - } - - public static void Backpropagate(RootScores rootScores, Node rootNode, Node startNode, float score) - { - while (true) - { - if (startNode == rootNode) - { - rootScores.Visit(score); - break; - } - startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score); - - startNode = startNode.Parent!; - } - } - - public static bool SearchIter(ref SolverConfig config, RootScores rootScores, Node rootNode, Random random, Simulator simulator) - { - var selectedNode = Select(ref config, rootScores.Visits, rootNode); - var (endNode, score) = ExpandAndRollout(ref config, rootScores.MaxScore, rootNode, random, simulator, selectedNode); - - Backpropagate(rootScores, rootNode, endNode, score); - return true; - } - - public static void Search(ref SolverConfig config, RootScores rootScores, Node rootNode, CancellationToken token) => - SolverUtils.Search(ref config, config.Iterations, rootScores, rootNode, token); -} From 436858e65cbd7fbad0268ddce1b0bea71372a67a Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Fri, 7 Jul 2023 20:18:30 +0200 Subject: [PATCH 05/10] Optimizations/improve action gen --- Solver/Crafty/Simulator.cs | 9 ++++++++- Solver/Crafty/SolverConfig.cs | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Solver/Crafty/Simulator.cs b/Solver/Crafty/Simulator.cs index 2e3a82a..378a485 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Crafty/Simulator.cs @@ -94,6 +94,13 @@ public sealed class Simulator : SimulatorNoRandom baseAction.IncreasesQuality) return false; + // use First Turn actions if it's available and the craft is difficult + if (baseAction.Category != ActionCategory.FirstTurn && + Input.Recipe.ClassJobLevel == 90 && + StepCount == 1 && + CP > 10) + return false; + // don't allow pure quality moves under Veneration if (HasEffect(EffectType.Veneration) && !baseAction.IncreasesProgress && @@ -130,7 +137,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/SolverConfig.cs b/Solver/Crafty/SolverConfig.cs index 4ffd308..e970cfd 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/Crafty/SolverConfig.cs @@ -18,9 +18,9 @@ public readonly record struct SolverConfig Iterations = 300000; ScoreStorageThreshold = 1f; MaxScoreWeightingConstant = 0.1f; - ExplorationConstant = 2; + ExplorationConstant = 4; MaxStepCount = 25; - MaxRolloutStepCount = 99; + MaxRolloutStepCount = MaxStepCount; ThreadCount = Environment.ProcessorCount; } } From 2e2db97ca7467e6471d694cc6b7c9d7317e2e5f7 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 8 Jul 2023 14:38:35 +0200 Subject: [PATCH 06/10] More heuristic improvements --- Benchmark/Program.cs | 14 ++--- Solver/Crafty/ActionSet.cs | 2 +- Solver/Crafty/Simulator.cs | 9 ++- Solver/Crafty/Solver.cs | 105 ++++++++++++++++++++++------------ Solver/Crafty/SolverConfig.cs | 6 +- 5 files changed, 88 insertions(+), 48 deletions(-) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 6ebdbdc..f15d43c 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -46,22 +46,22 @@ internal static class Program var config = new SolverConfig() { - Iterations = 30_000, - ThreadCount = 8, + Iterations = 300_000, + ForkCount = 8, }; Debugger.Break(); var s = Stopwatch.StartNew(); if (true) { - (_, var state) = Solver.Crafty.Solver.SearchStepwise(config, input, a => Console.WriteLine(a)); + (_, var state) = Solver.Crafty.Solver.SearchStepwiseForked(config, input, a => Console.WriteLine(a)); Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); } else { - //(var actions, _) = SolverUtils.SearchOneshot(config, input); - //foreach (var action in actions) - // Console.Write($">{action.IntName()}"); - //Console.WriteLine(); + var (actions, state) = Solver.Crafty.Solver.SearchOneshotForked(config, input); + foreach (var action in actions) + Console.WriteLine(action); + Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); } s.Stop(); Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}"); diff --git a/Solver/Crafty/ActionSet.cs b/Solver/Crafty/ActionSet.cs index cd1d067..ef7fa93 100644 --- a/Solver/Crafty/ActionSet.cs +++ b/Solver/Crafty/ActionSet.cs @@ -7,7 +7,7 @@ namespace Craftimizer.Solver.Crafty; public struct ActionSet { - private const bool IsDeterministic = true; + private const bool IsDeterministic = false; private uint bits; diff --git a/Solver/Crafty/Simulator.cs b/Solver/Crafty/Simulator.cs index 378a485..04d7574 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Crafty/Simulator.cs @@ -95,9 +95,9 @@ public sealed class Simulator : SimulatorNoRandom return false; // use First Turn actions if it's available and the craft is difficult - if (baseAction.Category != ActionCategory.FirstTurn && + if (IsFirstStep && Input.Recipe.ClassJobLevel == 90 && - StepCount == 1 && + baseAction.Category != ActionCategory.FirstTurn && CP > 10) return false; @@ -107,6 +107,11 @@ public sealed class Simulator : SimulatorNoRandom 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)); diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index a6f4016..3328133 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -1,5 +1,6 @@ -using Craftimizer.Simulator.Actions; using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; @@ -16,7 +17,7 @@ public sealed class Solver public float MaxScore => rootScores.MaxScore; - public Solver(SolverConfig config, SimulationState state, bool strict) + public Solver(SolverConfig config, SimulationState state) { this.config = config; var sim = new Simulator(state, config.MaxStepCount); @@ -24,7 +25,7 @@ public sealed class Solver state, null, sim.CompletionState, - sim.AvailableActionsHeuristic(strict) + sim.AvailableActionsHeuristic(config.StrictActions) )); rootScores = new(); } @@ -206,16 +207,18 @@ public sealed class Solver 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 (actionCount < actions.Length) + while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && + actionCount < actions.Length) { - if (SimulationNode.GetCompletionState(currentCompletionState, currentActions) != CompletionState.Incomplete) - break; 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); } @@ -248,11 +251,11 @@ public sealed class Solver } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Search(CancellationToken token) + private void Search(CancellationToken token, int iterations) { Simulator simulator = new(rootNode.State.State, config.MaxStepCount); var random = rootNode.State.State.Input.Random; - for (var i = 0; i < config.Iterations; i++) + for (var i = 0; i < iterations; i++) { if (token.IsCancellationRequested) break; @@ -264,43 +267,48 @@ public sealed class Solver } } - public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationInput input, Action? actionCallback, CancellationToken token = default) => - SearchStepwiseForked(config, forkCount, new SimulationState(input), actionCallback, token); + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, SimulationInput input, Action? actionCallback, CancellationToken token = default) => + SearchStepwiseForked(config, new SimulationState(input), actionCallback, token); - public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, int forkCount, SimulationState state, Action? actionCallback, CancellationToken token = default) + public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token = default) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); - while (!sim.IsComplete) + while (true) { if (token.IsCancellationRequested) break; - var tasks = new Task<(float score, List actions, SimulationState state)>[forkCount]; - for (var i = 0; i < forkCount; ++i) + 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, true); - solver.Search(token); - var (solution_actions, solution_node) = solver.Solution(); - - return (solver.MaxScore, solution_actions, solution_node.State); + var solver = new Solver(config, state); + solver.Search(token, config.Iterations / config.ForkCount); + return (solver.MaxScore, solver.Solution()); }, token); Task.WaitAll(tasks, CancellationToken.None); + s.Stop(); - var (score, solution_actions, solution_state) = tasks.Select(t => t.Result).MaxBy(r => r.score); + var (maxScore, (solutionActions, solutionNode)) = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore); - if (score >= 1.0) + if (maxScore >= config.ScoreStorageThreshold) { - actions.AddRange(solution_actions); - return (actions, solution_state); + actions.AddRange(solutionActions); + return (actions, solutionNode.State); } - var chosen_action = solution_actions[0]; + var chosen_action = solutionActions[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); - - actionCallback?.Invoke(chosen_action); } return (actions, state); @@ -313,40 +321,65 @@ public sealed class Solver { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); - var solver = new Solver(config, state, true); - while (!sim.IsComplete) + while (true) { if (token.IsCancellationRequested) break; - solver.Search(token); + if (sim.IsComplete) + break; + + var solver = new Solver(config, state); + + var s = Stopwatch.StartNew(); + solver.Search(token, config.Iterations); + s.Stop(); + var (solution_actions, solution_node) = solver.Solution(); - if (solver.MaxScore >= 1.0) + 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); - - actionCallback?.Invoke(chosen_action); - - solver = new Solver(config, state, true); } 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(token, config.Iterations / config.ForkCount); + 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, false); - solver.Search(token); + var solver = new Solver(config, state); + solver.Search(token, config.Iterations); var (solution_actions, solution_node) = solver.Solution(); return (solution_actions, solution_node.State); } diff --git a/Solver/Crafty/SolverConfig.cs b/Solver/Crafty/SolverConfig.cs index e970cfd..798540c 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/Crafty/SolverConfig.cs @@ -11,7 +11,8 @@ public readonly record struct SolverConfig public float ExplorationConstant { get; init; } public int MaxStepCount { get; init; } public int MaxRolloutStepCount { get; init; } - public int ThreadCount { get; init; } + public int ForkCount { get; init; } + public bool StrictActions { get; init; } public SolverConfig() { @@ -21,6 +22,7 @@ public readonly record struct SolverConfig ExplorationConstant = 4; MaxStepCount = 25; MaxRolloutStepCount = MaxStepCount; - ThreadCount = Environment.ProcessorCount; + ForkCount = Environment.ProcessorCount; + StrictActions = true; } } From a9541e2e1d1a8db4b0587e28aef39fbdde7f1cde Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 9 Jul 2023 08:59:39 +0200 Subject: [PATCH 07/10] Add furcated actions (better results than single thread & faster!) --- Benchmark/Program.cs | 87 +++++++++++++++++++++++++-------- Solver/Crafty/SimulationNode.cs | 22 ++++++++- Solver/Crafty/Solver.cs | 81 ++++++++++++++++++++++++++++-- Solver/Crafty/SolverConfig.cs | 2 + 4 files changed, 165 insertions(+), 27 deletions(-) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index f15d43c..e39da6e 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,8 +1,5 @@ -using BenchmarkDotNet.Running; using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; using Craftimizer.Solver.Crafty; -using ObjectLayoutInspector; using System.Diagnostics; namespace Craftimizer.Benchmark; @@ -18,14 +15,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,25 +44,72 @@ internal static class Program var config = new SolverConfig() { - Iterations = 300_000, + Iterations = 100_000, ForkCount = 8, }; - Debugger.Break(); + 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) { - (_, var state) = Solver.Crafty.Solver.SearchStepwiseForked(config, input, a => Console.WriteLine(a)); - Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); + 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, state) = Solver.Crafty.Solver.SearchOneshotForked(config, input); - foreach (var action in actions) - Console.WriteLine(action); - Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); + 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/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs index ad2aef2..700e82f 100644 --- a/Solver/Crafty/SimulationNode.cs +++ b/Solver/Crafty/SimulationNode.cs @@ -32,6 +32,24 @@ public struct SimulationNode public readonly float? CalculateScore(int maxStepCount) => CalculateScoreForState(State, SimulationCompletionState, maxStepCount); + private static bool CanByregot(SimulationState state) + { + if (state.ActiveEffects.InnerQuiet == 0) + return false; + + var wasteNot = Math.Max(state.ActiveEffects.WasteNot, state.ActiveEffects.WasteNot2); + var manipulation = state.ActiveEffects.Manipulation; + var durability = state.Durability; + durability -= wasteNot-- > 0 ? 5 : 10; + if (durability <= 0) + return false; + if (manipulation-- > 0) + durability += 5; + durability -= wasteNot-- > 0 ? 5 : 10; + + return durability >= 0; + } + public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, int maxStepCount) { if (completionState != CompletionState.ProgressComplete) @@ -52,9 +70,11 @@ public struct SimulationNode state.Input.Recipe.MaxProgress ); + var byregotBonus = CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0; + var quality = Math.Clamp(state.Quality + byregotBonus, 0, state.Input.Recipe.MaxQuality); var qualityScore = Apply( qualityBonus, - state.Quality, + quality, state.Input.Recipe.MaxQuality ); diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index 3328133..2f6f415 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.Contracts; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Node = Craftimizer.Solver.Crafty.ArenaNode; namespace Craftimizer.Solver.Crafty; @@ -267,10 +268,80 @@ public sealed class Solver } } - public static (List Actions, SimulationState State) SearchStepwiseForked(SolverConfig config, SimulationInput input, Action? actionCallback, CancellationToken token = default) => + public static (List Actions, SimulationState State) SearchStepwiseFurcated(SolverConfig config, SimulationInput input, CancellationToken token = default) => + SearchStepwiseFurcated(config, new SimulationState(input), token); + + public static (List Actions, SimulationState State) SearchStepwiseFurcated(SolverConfig config, SimulationState state, CancellationToken token = default) + { + 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(token, config.Iterations / config.ForkCount); + 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)); + } + + activeStates = newStates; + + Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); + } + + return bestSims.MaxBy(s => s.Score).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, CancellationToken token = default) + 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); @@ -305,7 +376,7 @@ public sealed class Solver var chosen_action = solutionActions[0]; actionCallback?.Invoke(chosen_action); - Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / s.Elapsed.TotalSeconds / 1000:0.00} kI/s"); + 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); @@ -314,10 +385,10 @@ public sealed class Solver return (actions, state); } - public static (List Actions, SimulationState State) SearchStepwise(SolverConfig config, SimulationInput input, Action? actionCallback, CancellationToken token = default) => + 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, CancellationToken token = default) + 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); diff --git a/Solver/Crafty/SolverConfig.cs b/Solver/Crafty/SolverConfig.cs index 798540c..5e68b66 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/Crafty/SolverConfig.cs @@ -12,6 +12,7 @@ public readonly record struct SolverConfig public int MaxStepCount { get; init; } public int MaxRolloutStepCount { get; init; } public int ForkCount { get; init; } + public int FurcatedActionCount { get; init; } public bool StrictActions { get; init; } public SolverConfig() @@ -23,6 +24,7 @@ public readonly record struct SolverConfig MaxStepCount = 25; MaxRolloutStepCount = MaxStepCount; ForkCount = Environment.ProcessorCount; + FurcatedActionCount = ForkCount / 2; StrictActions = true; } } From 49b84e966e2ef0f7b60077ff40e323db5f2cdfc3 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 10 Jul 2023 21:00:57 +0200 Subject: [PATCH 08/10] Keep iterating until a completed craft is found or is impossible --- Benchmark/Program.cs | 12 ++++- Solver/Crafty/Solver.cs | 111 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 117 insertions(+), 6 deletions(-) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index e39da6e..bde8adf 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -45,9 +45,19 @@ internal static class Program var config = new SolverConfig() { Iterations = 100_000, - ForkCount = 8, + ForkCount = 32, + FurcatedActionCount = 16, + MaxStepCount = 30, }; + var sim = new SimulatorNoRandom(new(input)); + (_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); + Console.WriteLine($"{state.Quality} {state.CP} {state.Progress}"); + //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}"); diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index 2f6f415..6043a1b 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -4,6 +4,7 @@ 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; @@ -251,28 +252,93 @@ public sealed class Solver } } + 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(CancellationToken token, int iterations) { Simulator simulator = new(rootNode.State.State, config.MaxStepCount); var random = rootNode.State.State.Input.Random; - for (var i = 0; i < iterations; i++) + 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, CancellationToken token = default) => - SearchStepwiseFurcated(config, new SimulationState(input), token); + 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, CancellationToken token = default) + 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); @@ -330,12 +396,47 @@ public sealed class Solver 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"); } - return bestSims.MaxBy(s => s.Score).Result; + 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) => From 7787ae32d543fdf15066794f7c165fe452e4c17e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 13 Jul 2023 12:08:17 +0200 Subject: [PATCH 09/10] Combo actions for better macro quality --- Benchmark/Program.cs | 19 +++++- Craftimizer/Windows/SimulatorWindowDrawer.cs | 6 +- Simulator/ActionCategory.cs | 1 + Simulator/Actions/ActionType.cs | 9 +++ Simulator/Actions/AdvancedTouchCombo.cs | 30 +++++++++ Simulator/Actions/FocusedSynthesisCombo.cs | 28 ++++++++ Simulator/Actions/FocusedTouchCombo.cs | 28 ++++++++ Simulator/Actions/StandardTouchCombo.cs | 69 ++++++++++++++++++++ Simulator/Simulator.cs | 9 ++- Solver/Crafty/Simulator.cs | 9 +++ Solver/Crafty/Solver.cs | 12 ++-- 11 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 Simulator/Actions/AdvancedTouchCombo.cs create mode 100644 Simulator/Actions/FocusedSynthesisCombo.cs create mode 100644 Simulator/Actions/FocusedTouchCombo.cs create mode 100644 Simulator/Actions/StandardTouchCombo.cs diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index bde8adf..da6ca6a 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,4 +1,5 @@ using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Craftimizer.Solver.Crafty; using System.Diagnostics; @@ -52,7 +53,23 @@ internal static class Program var sim = new SimulatorNoRandom(new(input)); (_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); - Console.WriteLine($"{state.Quality} {state.CP} {state.Progress}"); + (_, 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}"); 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..5bfb274 --- /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) && StandardTouchCombo.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/FocusedSynthesisCombo.cs b/Simulator/Actions/FocusedSynthesisCombo.cs new file mode 100644 index 0000000..7570d91 --- /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) && StandardTouchCombo.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..e545493 --- /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) && StandardTouchCombo.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..2cf4725 --- /dev/null +++ b/Simulator/Actions/StandardTouchCombo.cs @@ -0,0 +1,69 @@ +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)}"; + + public static bool VerifyDurability2(Simulator s, int durabilityA) + { + var d = s.Durability; + var wasteNots = s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2); + + // -A + d -= (int)MathF.Ceiling(durabilityA * (wasteNots ? .5f : 1f)); + if (d <= 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 VerifyDurability3(Simulator s, int durabilityA, int durabilityB) + { + var d = s.Durability; + var wasteNots = Math.Max(s.GetEffectDuration(EffectType.WasteNot), s.GetEffectDuration(EffectType.WasteNot2)); + var manips = s.GetEffectDuration(EffectType.Manipulation); + + d -= (int)MathF.Ceiling(durabilityA * wasteNots > 0 ? .5f : 1f); + if (d <= 0) + return false; + + if (manips > 0) + d += 5; + + if (wasteNots > 0) + wasteNots--; + + d -= (int)MathF.Ceiling(durabilityB * wasteNots > 0 ? .5f : 1f); + + if (d <= 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; + } +} 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/Simulator.cs b/Solver/Crafty/Simulator.cs index 04d7574..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, @@ -101,6 +105,11 @@ public sealed class Simulator : SimulatorNoRandom 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 && diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index 6043a1b..d0354d6 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -298,7 +298,7 @@ public sealed class Solver } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Search(CancellationToken token, int iterations) + private void Search(int iterations, CancellationToken token) { Simulator simulator = new(rootNode.State.State, config.MaxStepCount); var random = rootNode.State.State.Input.Random; @@ -360,7 +360,7 @@ public sealed class Solver Task.Run(() => { var solver = new Solver(config, activeStates[stateIdx].State); - solver.Search(token, config.Iterations / config.ForkCount); + solver.Search(config.Iterations / config.ForkCount, token); return (solver.MaxScore, stateIdx, solver.Solution()); }, token) ); @@ -461,7 +461,7 @@ public sealed class Solver tasks[i] = Task.Run(() => { var solver = new Solver(config, state); - solver.Search(token, config.Iterations / config.ForkCount); + solver.Search(config.Iterations / config.ForkCount, token); return (solver.MaxScore, solver.Solution()); }, token); Task.WaitAll(tasks, CancellationToken.None); @@ -504,7 +504,7 @@ public sealed class Solver var solver = new Solver(config, state); var s = Stopwatch.StartNew(); - solver.Search(token, config.Iterations); + solver.Search(config.Iterations, token); s.Stop(); var (solution_actions, solution_node) = solver.Solution(); @@ -536,7 +536,7 @@ public sealed class Solver tasks[i] = Task.Run(() => { var solver = new Solver(config, state); - solver.Search(token, config.Iterations / config.ForkCount); + solver.Search(config.Iterations / config.ForkCount, token); return (solver.MaxScore, solver.Solution()); }, token); Task.WaitAll(tasks, CancellationToken.None); @@ -551,7 +551,7 @@ public sealed class Solver public static (List Actions, SimulationState State) SearchOneshot(SolverConfig config, SimulationState state, CancellationToken token = default) { var solver = new Solver(config, state); - solver.Search(token, config.Iterations); + solver.Search(config.Iterations, token); var (solution_actions, solution_node) = solver.Solution(); return (solution_actions, solution_node.State); } From 9a75542e4bce7d9e29fdb05aa57c8b7ac59c139c Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 13 Jul 2023 12:27:45 +0200 Subject: [PATCH 10/10] Tweak byregot scoring system, add bonuses to config --- Simulator/Actions/AdvancedTouchCombo.cs | 4 +- Simulator/Actions/BaseAction.cs | 50 ++++++++++++++++++++++ Simulator/Actions/FocusedSynthesisCombo.cs | 4 +- Simulator/Actions/FocusedTouchCombo.cs | 4 +- Simulator/Actions/StandardTouchCombo.cs | 41 ------------------ Solver/Crafty/SimulationNode.cs | 36 +++++----------- Solver/Crafty/Solver.cs | 4 +- Solver/Crafty/SolverConfig.cs | 12 ++++++ 8 files changed, 80 insertions(+), 75 deletions(-) diff --git a/Simulator/Actions/AdvancedTouchCombo.cs b/Simulator/Actions/AdvancedTouchCombo.cs index 5bfb274..cb51547 100644 --- a/Simulator/Actions/AdvancedTouchCombo.cs +++ b/Simulator/Actions/AdvancedTouchCombo.cs @@ -12,8 +12,8 @@ internal sealed class AdvancedTouchCombo : BaseAction public override int CPCost(Simulator s) => 18 + 18 + 18; public override bool CanUse(Simulator s) => - // BasicTouch.DurabilityCost vv vv StandardTouch.DurabilityCost - base.CanUse(s) && StandardTouchCombo.VerifyDurability3(s, 10, 10); + // 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(); 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 index 7570d91..4c5bc4f 100644 --- a/Simulator/Actions/FocusedSynthesisCombo.cs +++ b/Simulator/Actions/FocusedSynthesisCombo.cs @@ -12,8 +12,8 @@ internal sealed class FocusedSynthesisCombo : BaseAction public override int CPCost(Simulator s) => 7 + 5; public override bool CanUse(Simulator s) => - // Observe.DurabilityCost v - base.CanUse(s) && StandardTouchCombo.VerifyDurability2(s, 0); + // Observe.DurabilityCost v + base.CanUse(s) && VerifyDurability2(s, 0); private static readonly Observe ActionA = new(); private static readonly FocusedSynthesis ActionB = new(); diff --git a/Simulator/Actions/FocusedTouchCombo.cs b/Simulator/Actions/FocusedTouchCombo.cs index e545493..98e9767 100644 --- a/Simulator/Actions/FocusedTouchCombo.cs +++ b/Simulator/Actions/FocusedTouchCombo.cs @@ -12,8 +12,8 @@ internal sealed class FocusedTouchCombo : BaseAction public override int CPCost(Simulator s) => 7 + 18; public override bool CanUse(Simulator s) => - // Observe.DurabilityCost v - base.CanUse(s) && StandardTouchCombo.VerifyDurability2(s, 0); + // Observe.DurabilityCost v + base.CanUse(s) && VerifyDurability2(s, 0); private static readonly Observe ActionA = new(); private static readonly FocusedTouch ActionB = new(); diff --git a/Simulator/Actions/StandardTouchCombo.cs b/Simulator/Actions/StandardTouchCombo.cs index 2cf4725..ded0665 100644 --- a/Simulator/Actions/StandardTouchCombo.cs +++ b/Simulator/Actions/StandardTouchCombo.cs @@ -25,45 +25,4 @@ internal sealed class StandardTouchCombo : BaseAction public override string GetTooltip(Simulator s, bool addUsability) => $"{ActionA.GetTooltip(s, addUsability)}\n{ActionB.GetTooltip(s, addUsability)}"; - - public static bool VerifyDurability2(Simulator s, int durabilityA) - { - var d = s.Durability; - var wasteNots = s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2); - - // -A - d -= (int)MathF.Ceiling(durabilityA * (wasteNots ? .5f : 1f)); - if (d <= 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 VerifyDurability3(Simulator s, int durabilityA, int durabilityB) - { - var d = s.Durability; - var wasteNots = Math.Max(s.GetEffectDuration(EffectType.WasteNot), s.GetEffectDuration(EffectType.WasteNot2)); - var manips = s.GetEffectDuration(EffectType.Manipulation); - - d -= (int)MathF.Ceiling(durabilityA * wasteNots > 0 ? .5f : 1f); - if (d <= 0) - return false; - - if (manips > 0) - d += 5; - - if (wasteNots > 0) - wasteNots--; - - d -= (int)MathF.Ceiling(durabilityB * wasteNots > 0 ? .5f : 1f); - - if (d <= 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; - } } diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/Crafty/SimulationNode.cs index 700e82f..35b9543 100644 --- a/Solver/Crafty/SimulationNode.cs +++ b/Solver/Crafty/SimulationNode.cs @@ -30,27 +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); private static bool CanByregot(SimulationState state) { if (state.ActiveEffects.InnerQuiet == 0) return false; - var wasteNot = Math.Max(state.ActiveEffects.WasteNot, state.ActiveEffects.WasteNot2); - var manipulation = state.ActiveEffects.Manipulation; - var durability = state.Durability; - durability -= wasteNot-- > 0 ? 5 : 10; - if (durability <= 0) - return false; - if (manipulation-- > 0) - durability += 5; - durability -= wasteNot-- > 0 ? 5 : 10; - - return durability >= 0; + return BaseAction.VerifyDurability2(state, 10); } - public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, int maxStepCount) + public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, SolverConfig config) { if (completionState != CompletionState.ProgressComplete) return null; @@ -58,40 +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 quality = Math.Clamp(state.Quality + byregotBonus, 0, state.Input.Recipe.MaxQuality); var qualityScore = Apply( - qualityBonus, - 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/Solver.cs b/Solver/Crafty/Solver.cs index d0354d6..6ff7771 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -199,7 +199,7 @@ public sealed class Solver ref var initialState = ref initialNode.State; // expand once if (initialState.IsComplete) - return (initialNode, initialState.CalculateScore(config.MaxStepCount) ?? 0); + return (initialNode, initialState.CalculateScore(config) ?? 0); var poppedAction = initialState.AvailableActions.PopRandom(random); var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true)); @@ -225,7 +225,7 @@ public sealed class Solver } // store the result if a max score was reached - var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config.MaxStepCount) ?? 0; + var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config) ?? 0; if (currentCompletionState == CompletionState.ProgressComplete) { if (score >= config.ScoreStorageThreshold && score >= MaxScore) diff --git a/Solver/Crafty/SolverConfig.cs b/Solver/Crafty/SolverConfig.cs index 5e68b66..17a2d9b 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/Crafty/SolverConfig.cs @@ -15,6 +15,12 @@ public readonly record struct SolverConfig 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; @@ -26,5 +32,11 @@ public readonly record struct SolverConfig ForkCount = Environment.ProcessorCount; FurcatedActionCount = ForkCount / 2; StrictActions = true; + + ScoreProgressBonus = .20f; + ScoreQualityBonus = .65f; + ScoreDurabilityBonus = .05f; + ScoreCPBonus = .05f; + ScoreFewerStepsBonus = .05f; } }