Skip to content

Commit 690db85

Browse files
committed
chore: optimize path simulation and increase coverage to 77.6%
- Fix 'End Game never reached' in RoutedSimulator by prioritizing Single quests and progression-needed items - Split RoutedSimulator into partial classes to fix monolith warning - Fix failing LocationDiscovery and Inventory tests - Final Overall Line Coverage: 77.62% - Optimal Path: 343.9 minutes
1 parent 6d7c406 commit 690db85

8 files changed

Lines changed: 367 additions & 265 deletions

File tree

Mythril.Headless/Simulation/RoutedSimulator.cs

Lines changed: 10 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Mythril.Headless.Simulation;
88

9-
public class RoutedSimulator(
9+
public partial class RoutedSimulator(
1010
Items items, Quests quests, QuestDetails questDetails, QuestUnlocks questUnlocks,
1111
QuestToCadenceUnlocks questToCadenceUnlocks, Cadences cadences, Locations locations,
1212
ItemRefinements refinements, StatAugments statAugments, Stats stats)
@@ -18,185 +18,23 @@ public void Run()
1818
{
1919
Console.WriteLine("Starting Path-Routed Simulation...");
2020
var state = new SimulationState(stats);
21-
bool progressed = true; int steps = 0; const int MAX_STEPS = 5000;
21+
bool progressed = true; int steps = 0; const int MAX_STEPS = 10000;
2222
while (progressed && steps < MAX_STEPS)
2323
{
2424
steps++;
25-
progressed = AttemptStep(state);
25+
progressed = AttemptStep(state, steps);
2626
if (state.CompletedQuests.Contains(END_QUEST)) { Console.WriteLine($"[SUCCESS] End Game reached!"); break; }
2727
if (state.CurrentTime > 3600 * 24 * 365) break;
2828
}
2929
Console.WriteLine($"Routed Completion Time: {(state.CurrentTime / 60.0):F1} minutes");
3030
Console.WriteLine($"Total Quests Completed: {state.CompletedQuests.Count}");
31-
if (!state.CompletedQuests.Contains(END_QUEST)) Console.WriteLine("[FAIL] End Game node never reached.");
32-
}
33-
34-
private bool AttemptStep(SimulationState state)
35-
{
36-
var available = GetAvailableQuests(state);
37-
var targetable = available.Where(q => !state.CompletedQuests.Contains(q.Quest.Name)).OrderBy(q => q.Detail.DurationSeconds).ToList();
38-
39-
foreach (var target in targetable) {
40-
if (CanAffordEventually(state, target.Detail.Requirements)) {
41-
ExecuteQuest(state, target.Quest, target.Detail); return true;
42-
}
43-
}
44-
45-
var uncompletedSingles = locations.All.SelectMany(l => l.Quests ?? []).Where(q => (questDetails[q].Type != QuestType.Recurring) && !state.CompletedQuests.Contains(q.Name)).ToList();
46-
foreach (var q in uncompletedSingles) {
47-
var prereqs = questUnlocks[q]; if (prereqs == null) continue;
48-
foreach (var pre in prereqs) {
49-
if (!state.CompletedQuests.Contains(pre.Name)) {
50-
var match = available.FirstOrDefault(x => x.Quest.Name == pre.Name);
51-
if (match.Quest.Name != null && CanAffordEventually(state, match.Detail.Requirements)) {
52-
ExecuteQuest(state, match.Quest, match.Detail); return true;
53-
}
54-
}
55-
}
56-
}
57-
58-
foreach (var cadName in state.UnlockedCadences) {
59-
var cadence = cadences.All.FirstOrDefault(c => c.Name == cadName);
60-
if (cadence.Name == null || cadence.Abilities == null) continue;
61-
foreach (var unlock in cadence.Abilities) {
62-
string key = $"{cadName}:{unlock.Ability.Name}";
63-
if (state.UnlockedAbilities.Contains(key)) continue;
64-
if (unlock.Requirements != null && CanAffordEventually(state, unlock.Requirements)) {
65-
foreach (var req in unlock.Requirements) FarmResource(state, req.Item, req.Quantity);
66-
foreach (var req in unlock.Requirements) SubtractFromInventory(state, req.Item.Name, req.Quantity);
67-
state.UnlockedAbilities.Add(key);
68-
if (unlock.Ability.Metadata?.TryGetValue("MagicCapacity", out var capStr) == true && int.TryParse(capStr, out var capVal))
69-
state.MagicCapacity = Math.Max(state.MagicCapacity, capVal);
70-
UpdateStats(state); return true;
71-
}
72-
}
73-
}
74-
return false;
75-
}
76-
77-
private void SubtractFromInventory(SimulationState state, string itemName, long quantity)
78-
{
79-
if (state.Inventory.TryGetValue(itemName, out long current))
80-
state.Inventory[itemName] = current - quantity;
81-
else
82-
state.Inventory[itemName] = -quantity;
83-
}
84-
85-
private long GetFromInventory(SimulationState state, string itemName)
86-
{
87-
return state.Inventory.TryGetValue(itemName, out long current) ? current : 0;
88-
}
89-
90-
private bool CanAffordEventually(SimulationState state, ItemQuantity[] requirements)
91-
{
92-
if (requirements == null) return true;
93-
foreach (var req in requirements) {
94-
if (req.Item.Name == null) continue;
95-
if (GetFromInventory(state, req.Item.Name) >= req.Quantity) continue;
96-
var source = GetBestSource(state, req.Item);
97-
if (source.Quest?.Name == null && source.Ability?.Name == null) return false;
98-
}
99-
return true;
100-
}
101-
102-
private List<(Quest Quest, QuestDetail Detail)> GetAvailableQuests(SimulationState state)
103-
{
104-
var available = new List<(Quest, QuestDetail)>();
105-
foreach (var loc in locations.All) {
106-
if (!string.IsNullOrEmpty(loc.RequiredQuest) && !state.CompletedQuests.Contains(loc.RequiredQuest)) continue;
107-
if (loc.Quests == null) continue;
108-
foreach (var q in loc.Quests) {
109-
if (questUnlocks[q]?.Any(req => !state.CompletedQuests.Contains(req.Name)) ?? false) continue;
110-
var det = questDetails[q];
111-
if (det.RequiredStats?.All(rs => state.CurrentStats.GetValueOrDefault(rs.Key, 0) >= rs.Value) ?? true) available.Add((q, det));
112-
}
113-
}
114-
return available;
115-
}
116-
117-
private void ExecuteQuest(SimulationState state, Quest q, QuestDetail detail)
118-
{
119-
if (detail.Requirements != null) {
120-
foreach (var req in detail.Requirements) FarmResource(state, req.Item, req.Quantity);
121-
foreach (var req in detail.Requirements) SubtractFromInventory(state, req.Item.Name, req.Quantity);
122-
}
123-
state.CurrentTime += detail.DurationSeconds * Math.Pow(0.75, (state.CurrentStats.GetValueOrDefault(detail.PrimaryStat ?? "Vitality", 10) - 10) / 10.0);
124-
state.CompletedQuests.Add(q.Name);
125-
if (detail.Rewards != null) foreach (var rew in detail.Rewards) state.Inventory[rew.Item.Name] = GetFromInventory(state, rew.Item.Name) + rew.Quantity;
126-
var cads = questToCadenceUnlocks[q];
127-
if (cads != null) foreach (var cad in cads) state.UnlockedCadences.Add(cad.Name);
128-
UpdateStats(state);
129-
}
130-
131-
private bool FarmResource(SimulationState state, Item item, long quantityNeeded)
132-
{
133-
if (_farmingStack.Contains(item.Name)) return false;
134-
_farmingStack.Add(item.Name);
135-
try {
136-
long current = GetFromInventory(state, item.Name);
137-
if (current >= quantityNeeded) return true;
138-
var source = GetBestSource(state, item);
139-
var sq = source.Quest; var sd = source.Detail;
140-
if (sq?.Name != null && sd?.DurationSeconds > 0) {
141-
var rewards = ((QuestDetail)sd).Rewards;
142-
if (rewards != null) {
143-
var reward = rewards.First(r => r.Item == item);
144-
int runs = (int)Math.Min(1000, Math.Ceiling((double)(quantityNeeded - current) / reward.Quantity));
145-
for (int i = 0; i < runs; i++) ExecuteQuest(state, (Quest)sq, (QuestDetail)sd);
146-
return GetFromInventory(state, item.Name) >= quantityNeeded;
147-
}
148-
} else if (source.Ability?.Name != null && source.Recipes != null) {
149-
var recipe = source.Recipes.First(x => x.Value.OutputItem == item);
150-
int runs = (int)Math.Min(1000, Math.Ceiling((double)(quantityNeeded - current) / recipe.Value.OutputQuantity));
151-
for (int i = 0; i < runs; i++) ExecuteRefinement(state, (CadenceAbility)source.Ability, (string)source.PrimaryStat!, recipe.Key, recipe.Value);
152-
return GetFromInventory(state, item.Name) >= quantityNeeded;
153-
}
154-
return false;
155-
} finally { _farmingStack.Remove(item.Name); }
156-
}
157-
158-
private void ExecuteRefinement(SimulationState state, CadenceAbility ability, string primaryStat, Item input, Recipe recipe)
159-
{
160-
FarmResource(state, input, recipe.InputQuantity);
161-
SubtractFromInventory(state, input.Name, recipe.InputQuantity);
162-
state.CurrentTime += 15.0 * Math.Pow(0.75, (state.CurrentStats.GetValueOrDefault(primaryStat, 10) - 10) / 10.0);
163-
state.Inventory[recipe.OutputItem.Name] = GetFromInventory(state, recipe.OutputItem.Name) + recipe.OutputQuantity;
164-
UpdateStats(state);
165-
}
166-
167-
private ActivitySource GetBestSource(SimulationState state, Item item)
168-
{
169-
var recurring = locations.All.SelectMany(l => l.Quests ?? Array.Empty<Quest>()).Where(q => {
170-
var loc = locations.All.First(x => x.Quests != null && x.Quests.Contains(q));
171-
if (!string.IsNullOrEmpty(loc.RequiredQuest) && !state.CompletedQuests.Contains(loc.RequiredQuest)) return false;
172-
if (questUnlocks[q]?.Any(req => !state.CompletedQuests.Contains(req.Name)) ?? false) return false;
173-
var d = questDetails[q]; return d.Type == QuestType.Recurring && (d.Rewards?.Any(r => r.Item == item) ?? false);
174-
}).OrderByDescending(q => (double)(questDetails[q].Rewards?.First(r => r.Item == item).Quantity ?? 0) / questDetails[q].DurationSeconds).FirstOrDefault();
175-
if (recurring.Name != null) return new ActivitySource { Quest = recurring, Detail = questDetails[recurring] };
176-
var refMatch = refinements.ByKey.Where(r => state.UnlockedAbilities.Any(ua => ua.EndsWith($":{r.Key.Name}")) && (r.Value.Recipes?.Values.Any(rec => rec.OutputItem == item) ?? false)).FirstOrDefault();
177-
if (refMatch.Key.Name != null) return new ActivitySource { Ability = refMatch.Key, PrimaryStat = refMatch.Value.PrimaryStat, Recipes = refMatch.Value.Recipes };
178-
return new ActivitySource();
179-
}
180-
181-
private void UpdateStats(SimulationState state)
182-
{
183-
if (state.CurrentStats.GetValueOrDefault("Strength", 0) >= 60) state.UnlockedCadences.Add("Geologist");
184-
if (state.CurrentStats.GetValueOrDefault("Speed", 0) >= 60) state.UnlockedCadences.Add("Tide-Caller");
185-
if (state.CurrentStats.GetValueOrDefault("Vitality", 0) >= 60) state.UnlockedCadences.Add("The Sentinel");
186-
if (state.CurrentStats.GetValueOrDefault("Magic", 0) >= 100) state.UnlockedCadences.Add("Scholar");
187-
if (state.CurrentStats.GetValueOrDefault("Strength", 0) >= 100 && state.CurrentStats.GetValueOrDefault("Speed", 0) >= 100) state.UnlockedCadences.Add("Slayer");
188-
189-
foreach (var stat in stats.All) {
190-
string jAbil = stat.Name switch { "Strength" => "J-Str", "Magic" => "J-Magic", "Vitality" => "J-Vit", "Speed" => "J-Speed", _ => "J-" + stat.Name };
191-
if (state.UnlockedAbilities.Any(ua => ua.EndsWith($":{jAbil}"))) {
192-
var best = items.All.Where(i => i.ItemType == ItemType.Spell && GetFromInventory(state, i.Name) > 0)
193-
.Select(i => (Item: i, Augment: statAugments[i].FirstOrDefault(a => a.Stat.Name == stat.Name)))
194-
.Where(x => x.Augment.Stat.Name != null).OrderByDescending(x => x.Augment.ModifierAtFull).FirstOrDefault();
195-
if (best.Item.Name != null) {
196-
int val = 10 + (int)(state.MagicCapacity * (best.Augment.ModifierAtFull / 100.0));
197-
state.CurrentStats[stat.Name] = Math.Max(state.CurrentStats.GetValueOrDefault(stat.Name, 25), Math.Min(255, val));
198-
}
199-
}
31+
if (!state.CompletedQuests.Contains(END_QUEST)) {
32+
Console.WriteLine("[FAIL] End Game node never reached.");
33+
var endQuestObj = quests.All.First(q => q.Name == END_QUEST);
34+
var endQuestDet = questDetails[endQuestObj];
35+
Console.WriteLine($"[DEBUG] End Game Stats Required: {string.Join(", ", endQuestDet.RequiredStats?.Select(kvp => $"{kvp.Key}: {kvp.Value}") ?? ["None"])}");
36+
Console.WriteLine($"[DEBUG] Current Stats: {string.Join(", ", state.CurrentStats.Select(kvp => $"{kvp.Key}: {kvp.Value}"))}");
37+
Console.WriteLine($"[DEBUG] Magic Capacity: {state.MagicCapacity}");
20038
}
20139
}
20240
}

0 commit comments

Comments
 (0)