Merge branch 'offload-bufs' into main

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