diff --git a/Craftimizer.sln b/Craftimizer.sln
index b20584e..891d739 100644
--- a/Craftimizer.sln
+++ b/Craftimizer.sln
@@ -21,37 +21,70 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Test\Cr
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
+ Deterministic|Any CPU = Deterministic|Any CPU
Deterministic|x64 = Deterministic|x64
+ Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Deterministic|Any CPU.ActiveCfg = Debug|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Deterministic|Any CPU.Build.0 = Debug|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Deterministic|x64.ActiveCfg = Release|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64
+ {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
+ {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.Build.0 = Debug|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|x64
+ {057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64
+ {057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|Any CPU.Build.0 = Deterministic|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|x64.ActiveCfg = Deterministic|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|x64.Build.0 = Deterministic|x64
+ {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.ActiveCfg = Release|x64
+ {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.Build.0 = Release|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|x64
+ {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.Build.0 = Debug|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|x64
+ {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64
+ {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|Any CPU.Build.0 = Deterministic|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|x64.ActiveCfg = Deterministic|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|x64.Build.0 = Deterministic|x64
+ {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.ActiveCfg = Release|x64
+ {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.Build.0 = Release|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|x64
+ {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.Build.0 = Debug|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|x64
+ {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64
+ {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|Any CPU.Build.0 = Deterministic|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|x64.ActiveCfg = Deterministic|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|x64.Build.0 = Deterministic|x64
+ {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.ActiveCfg = Release|x64
+ {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.Build.0 = Release|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|x64
+ {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.Build.0 = Debug|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64
+ {C3AEA981-9DA8-405C-995B-86528493891B}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64
+ {C3AEA981-9DA8-405C-995B-86528493891B}.Deterministic|Any CPU.Build.0 = Deterministic|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Deterministic|x64.ActiveCfg = Deterministic|x64
+ {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.ActiveCfg = Release|x64
+ {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.Build.0 = Release|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64
EndGlobalSection
diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj
index 8a52408..a79793d 100644
--- a/Solver/Craftimizer.Solver.csproj
+++ b/Solver/Craftimizer.Solver.csproj
@@ -15,6 +15,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Solver/RaphaelUtils.cs b/Solver/RaphaelUtils.cs
new file mode 100644
index 0000000..52dccb2
--- /dev/null
+++ b/Solver/RaphaelUtils.cs
@@ -0,0 +1,104 @@
+using Craftimizer.Simulator.Actions;
+using Action = Raphael.Action;
+
+namespace Craftimizer.Solver;
+
+internal static unsafe class RaphaelUtils
+{
+ public static ActionType[] ConvertRawActions(IReadOnlyList actions)
+ {
+ var result = new ActionType[actions.Count];
+ for (var i = 0; i < actions.Count; i++)
+ result[i] = ConvertRawAction(actions[i]);
+ return result;
+ }
+
+ public static Action[] ConvertToRawActions(IReadOnlyList actions)
+ {
+ var result = new List(actions.Count);
+ foreach(var action in actions)
+ {
+ if (ConvertToRawAction(action) is { } a)
+ result.Add(a);
+ }
+ return [.. result];
+ }
+
+ public static ActionType ConvertRawAction(Action action)
+ {
+ return action switch
+ {
+ Action.BasicSynthesis => ActionType.BasicSynthesis,
+ Action.BasicTouch => ActionType.BasicTouch,
+ Action.MasterMend => ActionType.MastersMend,
+ Action.Observe => ActionType.Observe,
+ Action.TricksOfTheTrade => ActionType.TricksOfTheTrade,
+ Action.WasteNot => ActionType.WasteNot,
+ Action.Veneration => ActionType.Veneration,
+ Action.StandardTouch => ActionType.StandardTouch,
+ Action.GreatStrides => ActionType.GreatStrides,
+ Action.Innovation => ActionType.Innovation,
+ Action.WasteNot2 => ActionType.WasteNot2,
+ Action.ByregotsBlessing => ActionType.ByregotsBlessing,
+ Action.PreciseTouch => ActionType.PreciseTouch,
+ Action.MuscleMemory => ActionType.MuscleMemory,
+ Action.CarefulSynthesis => ActionType.CarefulSynthesis,
+ Action.Manipulation => ActionType.Manipulation,
+ Action.PrudentTouch => ActionType.PrudentTouch,
+ Action.AdvancedTouch => ActionType.AdvancedTouch,
+ Action.Reflect => ActionType.Reflect,
+ Action.PreparatoryTouch => ActionType.PreparatoryTouch,
+ Action.Groundwork => ActionType.Groundwork,
+ Action.DelicateSynthesis => ActionType.DelicateSynthesis,
+ Action.IntensiveSynthesis => ActionType.IntensiveSynthesis,
+ Action.TrainedEye => ActionType.TrainedEye,
+ Action.HeartAndSoul => ActionType.HeartAndSoul,
+ Action.PrudentSynthesis => ActionType.PrudentSynthesis,
+ Action.TrainedFinesse => ActionType.TrainedFinesse,
+ Action.RefinedTouch => ActionType.RefinedTouch,
+ Action.QuickInnovation => ActionType.QuickInnovation,
+ Action.ImmaculateMend => ActionType.ImmaculateMend,
+ Action.TrainedPerfection => ActionType.TrainedPerfection,
+ _ => throw new ArgumentOutOfRangeException(nameof(action), action, $"Invalid action value {action}"),
+ };
+ }
+
+ public static Action? ConvertToRawAction(ActionType action)
+ {
+ return action switch
+ {
+ ActionType.BasicSynthesis => Action.BasicSynthesis,
+ ActionType.BasicTouch => Action.BasicTouch,
+ ActionType.MastersMend => Action.MasterMend,
+ ActionType.Observe => Action.Observe,
+ ActionType.TricksOfTheTrade => Action.TricksOfTheTrade,
+ ActionType.WasteNot => Action.WasteNot,
+ ActionType.Veneration => Action.Veneration,
+ ActionType.StandardTouch => Action.StandardTouch,
+ ActionType.GreatStrides => Action.GreatStrides,
+ ActionType.Innovation => Action.Innovation,
+ ActionType.WasteNot2 => Action.WasteNot2,
+ ActionType.ByregotsBlessing => Action.ByregotsBlessing,
+ ActionType.PreciseTouch => Action.PreciseTouch,
+ ActionType.MuscleMemory => Action.MuscleMemory,
+ ActionType.CarefulSynthesis => Action.CarefulSynthesis,
+ ActionType.Manipulation => Action.Manipulation,
+ ActionType.PrudentTouch => Action.PrudentTouch,
+ ActionType.AdvancedTouch => Action.AdvancedTouch,
+ ActionType.Reflect => Action.Reflect,
+ ActionType.PreparatoryTouch => Action.PreparatoryTouch,
+ ActionType.Groundwork => Action.Groundwork,
+ ActionType.DelicateSynthesis => Action.DelicateSynthesis,
+ ActionType.IntensiveSynthesis => Action.IntensiveSynthesis,
+ ActionType.TrainedEye => Action.TrainedEye,
+ ActionType.HeartAndSoul => Action.HeartAndSoul,
+ ActionType.PrudentSynthesis => Action.PrudentSynthesis,
+ ActionType.TrainedFinesse => Action.TrainedFinesse,
+ ActionType.RefinedTouch => Action.RefinedTouch,
+ ActionType.QuickInnovation => Action.QuickInnovation,
+ ActionType.ImmaculateMend => Action.ImmaculateMend,
+ ActionType.TrainedPerfection => Action.TrainedPerfection,
+ _ => null,
+ };
+ }
+}
diff --git a/Solver/Solver.cs b/Solver/Solver.cs
index a848bf3..b06c68a 100644
--- a/Solver/Solver.cs
+++ b/Solver/Solver.cs
@@ -39,6 +39,8 @@ public sealed class Solver : IDisposable
}
}
+ public bool IsIndeterminate => progress == 0 && progressStage == 0;
+
public delegate void LogDelegate(string text);
public delegate void NewActionDelegate(ActionType action);
public delegate void SolutionDelegate(SolverSolution solution);
@@ -46,9 +48,16 @@ public sealed class Solver : IDisposable
// Print to console or plugin log.
public event LogDelegate? OnLog;
+ // Display as notification.
+ public event LogDelegate? OnWarn;
+
// Always called when a new step is generated.
public event NewActionDelegate? OnNewAction;
+ // Called when the solver can provide a "probable" solution.
+ // OnNewAction actions precede these proposed solutions. Purely visual.
+ public event SolutionDelegate? OnSuggestSolution;
+
public Solver(in SolverConfig config, in SimulationState state)
{
Config = config;
@@ -61,6 +70,7 @@ public sealed class Solver : IDisposable
SolverAlgorithm.Stepwise => (SearchStepwise, true),
SolverAlgorithm.StepwiseForked => (SearchStepwiseForked, true),
SolverAlgorithm.StepwiseGenetic => (SearchStepwiseGenetic, true),
+ SolverAlgorithm.Raphael => (SearchRaphael, true),
_ => throw new ArgumentOutOfRangeException(nameof(config), config, $"Invalid algorithm: {config.Algorithm}")
});
@@ -136,6 +146,82 @@ public sealed class Solver : IDisposable
Interlocked.Increment(ref progressStage);
}
+ private async Task SearchRaphael()
+ {
+ if (State.ActionCount > 0)
+ {
+ OnWarn?.Invoke("Optimal solver not support existing actions; falling back to Stepwise Genetic.");
+ return await SearchStepwiseGenetic().ConfigureAwait(false);
+ }
+
+ maxProgress = 2000;
+
+ Raphael.SolverConfig config = new()
+ {
+ Adversarial = Config.Adversarial,
+ BackloadProgress = Config.BackloadProgress,
+ UnsoundBranchPruning = Config.UnsoundBranchPruning,
+ };
+
+ ActionType[]? solution = null;
+
+ var s = new SimulatorNoRandom() { State = State };
+ var pool = RaphaelUtils.ConvertToRawActions(Config.ActionPool.Where(a => a.Base().IsPossible(s)).ToArray());
+ var input = new Raphael.SolverInput()
+ {
+ CP = checked((short)State.Input.Stats.CP),
+ Durability = checked((sbyte)State.Input.Recipe.MaxDurability),
+ Progress = checked((ushort)State.Input.Recipe.MaxProgress),
+ Quality = checked((ushort)(State.Input.Recipe.MaxQuality - State.Input.StartingQuality)),
+ BaseProgressGain = checked((ushort)State.Input.BaseProgressGain),
+ BaseQualityGain = checked((ushort)State.Input.BaseQualityGain),
+ JobLevel = checked((byte)State.Input.Stats.Level),
+ };
+
+ using Raphael.Solver solver = new(in config, input, pool);
+
+ solver.OnFinish += s => solution = RaphaelUtils.ConvertRawActions(s);
+ solver.OnSuggestSolution += s =>
+ {
+ var steps = RaphaelUtils.ConvertRawActions(s);
+ var sim = new SimulatorNoRandom();
+ var (resp, outState, failedIdx) = sim.ExecuteMultiple(State, steps);
+ if (resp != ActionResponse.SimulationComplete)
+ {
+ if (failedIdx != -1)
+ OnLog?.Invoke($"Invalid state; simulation failed to execute solution: {string.Join(',', s)}");
+ }
+
+ OnSuggestSolution?.Invoke(new(steps, in outState));
+ };
+ solver.OnProgress += p =>
+ {
+ var prog = checked((int)p);
+ var stage = prog / maxProgress;
+ while (stage != progressStage)
+ ResetProgress();
+ progress = prog % maxProgress;
+ };
+
+ await using var registration = Token.Register(solver.Cancel).ConfigureAwait(true);
+ await Task.Run(solver.Solve, Token).ConfigureAwait(true);
+
+ if (solution == null)
+ return new([], State);
+
+ foreach (var action in solution)
+ InvokeNewAction(action);
+
+ var sim = new SimulatorNoRandom();
+ var (resp, outState, failedIdx) = sim.ExecuteMultiple(State, solution);
+ if (resp != ActionResponse.SimulationComplete)
+ {
+ if (failedIdx != -1)
+ throw new Exception($"Invalid state; simulation failed to execute solution: {string.Join(',', solution)}");
+ }
+ return new(solution, outState);
+ }
+
private async Task SearchStepwiseGenetic()
{
var iterCount = Config.Iterations / Config.ForkCount;
diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs
index f125ca8..82a4d1e 100644
--- a/Solver/SolverConfig.cs
+++ b/Solver/SolverConfig.cs
@@ -11,6 +11,7 @@ public enum SolverAlgorithm
Stepwise,
StepwiseForked,
StepwiseGenetic,
+ Raphael,
}
[StructLayout(LayoutKind.Auto)]
@@ -33,6 +34,10 @@ public readonly record struct SolverConfig
public float ScoreCP { get; init; }
public float ScoreSteps { get; init; }
+ public bool Adversarial { get; init; }
+ public bool BackloadProgress { get; init; }
+ public bool UnsoundBranchPruning { get; init; }
+
public ActionType[] ActionPool { get; init; }
public SolverAlgorithm Algorithm { get; init; }