Convert Simulation into data-oriented SimualtionState

This commit is contained in:
Asriel Camora
2023-06-16 00:25:38 -07:00
parent 5555966b5b
commit 15d416ef2a
12 changed files with 97 additions and 67 deletions
+11 -7
View File
@@ -11,7 +11,7 @@ namespace Craftimizer.Plugin;
public class SimulatorWindow : Window public class SimulatorWindow : Window
{ {
public Simulation Simulation { get; } public SimulationState Simulation { get; }
private bool showOnlyGuaranteedActions = true; private bool showOnlyGuaranteedActions = true;
@@ -23,7 +23,11 @@ public class SimulatorWindow : Window
MaximumSize = new Vector2(float.MaxValue, float.MaxValue) MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
}; };
Simulation = new(new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90 }, LuminaSheets.RecipeSheet.GetRow(35499)!); Simulation = new(new()
{
Stats = new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90 },
Recipe = LuminaSheets.RecipeSheet.GetRow(35499)!
});
} }
public override void Draw() public override void Draw()
@@ -62,19 +66,19 @@ public class SimulatorWindow : Window
ImGui.Text($"Step {Simulation.StepCount + 1}"); ImGui.Text($"Step {Simulation.StepCount + 1}");
ImGui.Text(Simulation.Condition.Name()); ImGui.Text(Simulation.Condition.Name());
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip(Simulation.Condition.Description(Simulation.Stats.HasRelic)); ImGui.SetTooltip(Simulation.Condition.Description(Simulation.Input.Stats.HasRelic));
ImGui.Text($"{Simulation.HQPercent}%% HQ"); ImGui.Text($"{Simulation.HQPercent}%% HQ");
ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, 1f, .2f, 1f)); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, 1f, .2f, 1f));
ImGui.ProgressBar(Math.Min((float)Simulation.Progress / Simulation.MaxProgress, 1f), new Vector2(200, 20), $"{Simulation.Progress} / {Simulation.MaxProgress}"); ImGui.ProgressBar(Math.Min((float)Simulation.Progress / Simulation.Input.MaxProgress, 1f), new Vector2(200, 20), $"{Simulation.Progress} / {Simulation.Input.MaxProgress}");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, .2f, 1f, 1f)); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, .2f, 1f, 1f));
ImGui.ProgressBar(Math.Min((float)Simulation.Quality / Simulation.MaxQuality, 1f), new Vector2(200, 20), $"{Simulation.Quality} / {Simulation.MaxQuality}"); ImGui.ProgressBar(Math.Min((float)Simulation.Quality / Simulation.Input.MaxQuality, 1f), new Vector2(200, 20), $"{Simulation.Quality} / {Simulation.Input.MaxQuality}");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, 1f, .2f, 1f)); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, 1f, .2f, 1f));
ImGui.ProgressBar(Math.Clamp((float)Simulation.Durability / Simulation.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{Simulation.Durability} / {Simulation.MaxDurability}"); ImGui.ProgressBar(Math.Clamp((float)Simulation.Durability / Simulation.Input.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{Simulation.Durability} / {Simulation.Input.MaxDurability}");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, .2f, 1f, 1f)); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, .2f, 1f, 1f));
ImGui.ProgressBar(Math.Clamp((float)Simulation.CP / Simulation.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{Simulation.CP} / {Simulation.Stats.CP}"); ImGui.ProgressBar(Math.Clamp((float)Simulation.CP / Simulation.Input.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{Simulation.CP} / {Simulation.Input.Stats.CP}");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGui.Text($"Effects:"); ImGui.Text($"Effects:");
+1 -1
View File
@@ -61,7 +61,7 @@ internal static class ActionUtils
private static BaseAction Action(this ActionType me) => Actions[(int)me]; private static BaseAction Action(this ActionType me) => Actions[(int)me];
public static BaseAction With(this ActionType me, Simulation simulation) public static BaseAction With(this ActionType me, SimulationState simulation)
{ {
BaseAction.TLSSimulation.Value = simulation; BaseAction.TLSSimulation.Value = simulation;
return Action(me); return Action(me);
+3 -3
View File
@@ -6,8 +6,8 @@ namespace Craftimizer.Simulator.Actions;
internal abstract class BaseAction internal abstract class BaseAction
{ {
internal static readonly ThreadLocal<Simulation> TLSSimulation = new(false); internal static readonly ThreadLocal<SimulationState?> TLSSimulation = new(false);
protected static Simulation Simulation => TLSSimulation.Value ?? throw new NullReferenceException(); protected static SimulationState Simulation => TLSSimulation.Value ?? throw new NullReferenceException();
public BaseAction() { } public BaseAction() { }
@@ -28,7 +28,7 @@ internal abstract class BaseAction
public virtual bool IsGuaranteedAction => SuccessRate == 1f; public virtual bool IsGuaranteedAction => SuccessRate == 1f;
public virtual bool CanUse => public virtual bool CanUse =>
Simulation.Stats.Level >= Level && Simulation.CP >= CPCost; Simulation.Input.Stats.Level >= Level && Simulation.CP >= CPCost;
public virtual void Use() public virtual void Use()
{ {
@@ -8,6 +8,6 @@ internal class BasicSynthesis : BaseAction
public override int CPCost => 0; public override int CPCost => 0;
// Basic Synthesis Mastery Trait // Basic Synthesis Mastery Trait
public override float Efficiency => Simulation.Stats.Level >= 31 ? 1.20f : 1.00f; public override float Efficiency => Simulation.Input.Stats.Level >= 31 ? 1.20f : 1.00f;
public override bool IncreasesProgress => true; public override bool IncreasesProgress => true;
} }
@@ -10,7 +10,7 @@ internal class CarefulObservation : BaseAction
public override int DurabilityCost => 0; public override int DurabilityCost => 0;
public override bool IncreasesStepCount => false; public override bool IncreasesStepCount => false;
public override bool CanUse => Simulation.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.CarefulObservation) < 3; public override bool CanUse => Simulation.Input.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.CarefulObservation) < 3;
public override void UseSuccess() => public override void UseSuccess() =>
Simulation.StepCondition(); Simulation.StepCondition();
@@ -8,6 +8,6 @@ internal class CarefulSynthesis : BaseAction
public override int CPCost => 7; public override int CPCost => 7;
// Careful Synthesis Mastery Trait // Careful Synthesis Mastery Trait
public override float Efficiency => Simulation.Stats.Level >= 82 ? 1.80f : 1.50f; public override float Efficiency => Simulation.Input.Stats.Level >= 82 ? 1.80f : 1.50f;
public override bool IncreasesProgress => true; public override bool IncreasesProgress => true;
} }
+1 -1
View File
@@ -12,7 +12,7 @@ internal class Groundwork : BaseAction
{ {
get get
{ {
var ret = Simulation.Stats.Level >= 86 ? 3.60f : 3.00f; var ret = Simulation.Input.Stats.Level >= 86 ? 3.60f : 3.00f;
// TODO: does not account for waste not // TODO: does not account for waste not
return Simulation.Durability < DurabilityCost ? ret / 2 : ret; return Simulation.Durability < DurabilityCost ? ret / 2 : ret;
} }
@@ -11,5 +11,5 @@ internal class HeartAndSoul : BaseBuffAction
public override Effect Effect => new() { Type = EffectType.HeartAndSoul }; public override Effect Effect => new() { Type = EffectType.HeartAndSoul };
public override bool CanUse => Simulation.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.HeartAndSoul) == 0; public override bool CanUse => Simulation.Input.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.HeartAndSoul) == 0;
} }
@@ -8,7 +8,7 @@ internal class RapidSynthesis : BaseAction
public override int CPCost => 0; public override int CPCost => 0;
// Rapid Synthesis Mastery Trait // Rapid Synthesis Mastery Trait
public override float Efficiency => Simulation.Stats.Level >= 63 ? 5.00f : 2.50f; public override float Efficiency => Simulation.Input.Stats.Level >= 63 ? 5.00f : 2.50f;
public override bool IncreasesProgress => true; public override bool IncreasesProgress => true;
public override float SuccessRate => 0.50f; public override float SuccessRate => 0.50f;
} }
+1 -1
View File
@@ -12,5 +12,5 @@ internal class TrainedEye : BaseAction
public override bool CanUse => Simulation.IsFirstStep && base.CanUse; public override bool CanUse => Simulation.IsFirstStep && base.CanUse;
public override void UseSuccess() => public override void UseSuccess() =>
Simulation.IncreaseQualityRaw(Simulation.MaxQuality - Simulation.Quality); Simulation.IncreaseQualityRaw(Simulation.Input.MaxQuality - Simulation.Quality);
} }
+19
View File
@@ -0,0 +1,19 @@
using Lumina.Excel.GeneratedSheets;
using System;
namespace Craftimizer.Simulator;
public readonly record struct SimulationInput
{
public CharacterStats Stats { get; init; }
public Recipe Recipe { get; init; }
public Random Random { get; init; }
public RecipeLevelTable RecipeTable => Recipe.RecipeLevelTable.Value!;
public int RLvl => (int)RecipeTable.RowId;
public Condition[] AvailableConditions => ConditionUtils.GetPossibleConditions(RecipeTable.ConditionsFlag);
public int MaxDurability => RecipeTable.Durability * Recipe.DurabilityFactor / 100;
public int MaxQuality => (int)RecipeTable.Quality * Recipe.QualityFactor / 100;
public int MaxProgress => RecipeTable.Difficulty * Recipe.DifficultyFactor / 100;
}
@@ -1,23 +1,13 @@
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
using Dalamud.Logging;
using Lumina.Excel.GeneratedSheets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Craftimizer.Simulator; namespace Craftimizer.Simulator;
public class Simulation public record struct SimulationState
{ {
public CharacterStats Stats { get; } public readonly SimulationInput Input { get; }
public Recipe Recipe { get; }
public RecipeLevelTable RecipeTable => Recipe.RecipeLevelTable.Value!;
public int RLvl => (int)RecipeTable.RowId;
public readonly Condition[] AvailableConditions;
public int MaxDurability => RecipeTable.Durability * Recipe.DurabilityFactor / 100;
public int MaxQuality => (int)RecipeTable.Quality * Recipe.QualityFactor / 100;
public int MaxProgress => RecipeTable.Difficulty * Recipe.DifficultyFactor / 100;
public bool IsComplete { get; private set; } public bool IsComplete { get; private set; }
public int StepCount { get; private set; } public int StepCount { get; private set; }
@@ -26,8 +16,8 @@ public class Simulation
public int Durability { get; private set; } public int Durability { get; private set; }
public int CP { get; private set; } public int CP { get; private set; }
public Condition Condition { get; private set; } public Condition Condition { get; private set; }
public List<Effect> ActiveEffects { get; } = new(); public List<Effect> ActiveEffects { get; }
public List<ActionType> ActionHistory { get; } = new(); public List<ActionType> ActionHistory { get; }
// https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2 // https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2
private static readonly int[] HQPercentTable = { private static readonly int[] HQPercentTable = {
@@ -36,27 +26,33 @@ public class Simulation
17, 18, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 26, 28, 31, 34, 38, 42, 47, 52, 58, 64, 68, 71, 17, 18, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 26, 28, 31, 34, 38, 42, 47, 52, 58, 64, 68, 71,
74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100 74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100
}; };
public int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / MaxQuality * 100, 0, 100)]; public int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.MaxQuality * 100, 0, 100)];
public bool IsFirstStep => StepCount == 0; public bool IsFirstStep => StepCount == 0;
private Random Random { get; } = new(); public SimulationState(SimulationInput input)
public Simulation(CharacterStats stats, Recipe recipe)
{ {
Stats = stats; Input = input;
Recipe = recipe;
IsComplete = false; IsComplete = false;
StepCount = 0; StepCount = 0;
Progress = 0; Progress = 0;
Quality = 0; Quality = 0;
Durability = MaxDurability; Durability = Input.MaxDurability;
CP = Stats.CP; CP = Input.Stats.CP;
Condition = Condition.Normal; Condition = Condition.Normal;
AvailableConditions = ConditionUtils.GetPossibleConditions(RecipeTable.ConditionsFlag); ActiveEffects = new();
ActionHistory = new();
} }
public ActionResponse Execute(ActionType action) public (ActionResponse Response, SimulationState NewState) Execute(ActionType action)
{
var newState = this;
var response = newState.ExecuteSelf(action);
return (response, newState);
}
public ActionResponse ExecuteSelf(ActionType action)
{ {
if (IsComplete) if (IsComplete)
return ActionResponse.SimulationComplete; return ActionResponse.SimulationComplete;
@@ -64,7 +60,7 @@ public class Simulation
var baseAction = action.With(this); var baseAction = action.With(this);
if (!baseAction.CanUse) if (!baseAction.CanUse)
{ {
if (baseAction.Level > Stats.Level) if (baseAction.Level > Input.Stats.Level)
return ActionResponse.ActionNotUnlocked; return ActionResponse.ActionNotUnlocked;
if (baseAction.CPCost > CP) if (baseAction.CPCost > CP)
return ActionResponse.NotEnoughCP; return ActionResponse.NotEnoughCP;
@@ -85,7 +81,7 @@ public class Simulation
} }
} }
if (Progress >= MaxProgress) if (Progress >= Input.MaxProgress)
{ {
IsComplete = true; IsComplete = true;
return ActionResponse.ProgressComplete; return ActionResponse.ProgressComplete;
@@ -112,7 +108,8 @@ public class Simulation
duration++; duration++;
var currentEffect = GetEffect(effect); var currentEffect = GetEffect(effect);
if (currentEffect != null) { if (currentEffect != null)
{
currentEffect.Duration = duration; currentEffect.Duration = duration;
currentEffect.Strength = strength; currentEffect.Strength = strength;
} }
@@ -148,7 +145,7 @@ public class Simulation
ActionHistory.Count(a => a == action); ActionHistory.Count(a => a == action);
public bool RollSuccessRaw(float successRate) => public bool RollSuccessRaw(float successRate) =>
successRate >= Random.NextSingle(); successRate >= Input.Random.NextSingle();
public bool RollSuccess(float successRate) => public bool RollSuccess(float successRate) =>
RollSuccessRaw(CalculateSuccessRate(successRate)); RollSuccessRaw(CalculateSuccessRate(successRate));
@@ -159,10 +156,10 @@ public class Simulation
StepCondition(); StepCondition();
} }
private float GetConditionChance(Condition condition) => private static float GetConditionChance(SimulationInput input, Condition condition) =>
condition switch condition switch
{ {
Condition.Good => Recipe.IsExpert ? 0.12f : (Stats.Level >= 63 ? 0.15f : 0.18f), Condition.Good => input.Recipe.IsExpert ? 0.12f : (input.Stats.Level >= 63 ? 0.15f : 0.18f),
Condition.Excellent => 0.04f, Condition.Excellent => 0.04f,
Condition.Centered => 0.15f, Condition.Centered => 0.15f,
Condition.Sturdy => 0.15f, Condition.Sturdy => 0.15f,
@@ -173,16 +170,26 @@ public class Simulation
_ => 0.00f _ => 0.00f
}; };
private Condition GetNextRandomCondition()
{
var conditionChance = Input.Random.NextSingle();
foreach (var condition in Input.AvailableConditions)
if ((conditionChance -= GetConditionChance(Input, condition)) < 0)
return condition;
return Condition.Normal;
}
public void StepCondition() public void StepCondition()
{ {
var conditionChance = Random.NextSingle(); Condition = Condition switch
{
Condition = Condition switch {
Condition.Poor => Condition.Normal, Condition.Poor => Condition.Normal,
Condition.Good => Condition.Normal, Condition.Good => Condition.Normal,
Condition.Excellent => Condition.Poor, Condition.Excellent => Condition.Poor,
Condition.GoodOmen => Condition.Good, Condition.GoodOmen => Condition.Good,
_ => AvailableConditions.FirstOrDefault(c => (conditionChance -= GetConditionChance(c)) < 0, Condition.Normal) _ => GetNextRandomCondition()
}; };
} }
@@ -190,16 +197,16 @@ public class Simulation
{ {
Durability += amount; Durability += amount;
if (Durability > MaxDurability) if (Durability > Input.MaxDurability)
Durability = MaxDurability; Durability = Input.MaxDurability;
} }
public void RestoreCP(int amount) public void RestoreCP(int amount)
{ {
CP += amount; CP += amount;
if (CP > Stats.CP) if (CP > Input.Stats.CP)
CP = Stats.CP; CP = Input.Stats.CP;
} }
public float CalculateSuccessRate(float successRate) public float CalculateSuccessRate(float successRate)
@@ -246,9 +253,9 @@ public class Simulation
}; };
// https://github.com/NotRanged/NotRanged.github.io/blob/0f4aee074f969fb05aad34feaba605057c08ffd1/app/js/ffxivcraftmodel.js#L88 // https://github.com/NotRanged/NotRanged.github.io/blob/0f4aee074f969fb05aad34feaba605057c08ffd1/app/js/ffxivcraftmodel.js#L88
var baseIncrease = (Stats.Craftsmanship * 10f / RecipeTable.ProgressDivider) + 2; var baseIncrease = (Input.Stats.Craftsmanship * 10f / Input.RecipeTable.ProgressDivider) + 2;
if (Stats.CLvl <= RLvl) if (Input.Stats.CLvl <= Input.RLvl)
baseIncrease *= RecipeTable.ProgressModifier / 100f; baseIncrease *= Input.RecipeTable.ProgressModifier / 100f;
baseIncrease = MathF.Floor(baseIncrease); baseIncrease = MathF.Floor(baseIncrease);
var progressGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier); var progressGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier);
@@ -272,14 +279,14 @@ public class Simulation
var conditionModifier = Condition switch var conditionModifier = Condition switch
{ {
Condition.Poor => 0.50f, Condition.Poor => 0.50f,
Condition.Good => Stats.HasRelic ? 1.75f : 1.50f, Condition.Good => Input.Stats.HasRelic ? 1.75f : 1.50f,
Condition.Excellent => 4.00f, Condition.Excellent => 4.00f,
_ => 1.00f, _ => 1.00f,
}; };
var baseIncrease = (Stats.Control * 10f / RecipeTable.QualityDivider) + 35; var baseIncrease = (Input.Stats.Control * 10f / Input.RecipeTable.QualityDivider) + 35;
if (Stats.CLvl <= RLvl) if (Input.Stats.CLvl <= Input.RLvl)
baseIncrease *= RecipeTable.QualityModifier / 100f; baseIncrease *= Input.RecipeTable.QualityModifier / 100f;
baseIncrease = MathF.Floor(baseIncrease); baseIncrease = MathF.Floor(baseIncrease);
var qualityGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier); var qualityGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier);
@@ -296,9 +303,9 @@ public class Simulation
{ {
Progress += progressGain; Progress += progressGain;
if (HasEffect(EffectType.FinalAppraisal) && Progress >= MaxProgress) if (HasEffect(EffectType.FinalAppraisal) && Progress >= Input.MaxProgress)
{ {
Progress = MaxProgress - 1; Progress = Input.MaxProgress - 1;
RemoveEffect(EffectType.FinalAppraisal); RemoveEffect(EffectType.FinalAppraisal);
} }
} }
@@ -307,7 +314,7 @@ public class Simulation
{ {
Quality += qualityGain; Quality += qualityGain;
if (Stats.Level >= 11) if (Input.Stats.Level >= 11)
StrengthenEffect(EffectType.InnerQuiet); StrengthenEffect(EffectType.InnerQuiet);
} }