Skip to content

Commit bc12aad

Browse files
committed
first attempt
1 parent ecbbfaa commit bc12aad

12 files changed

Lines changed: 305 additions & 55 deletions

Mythril.Headless/Simulation/ReachabilitySimulator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public void Run()
3939
var flowState = flowSim.Solve(finalState, seed);
4040
Console.WriteLine("Flow Analysis Complete.");
4141

42+
var routed = new RoutedSimulator(items, quests, questDetails, questUnlocks, questToCadenceUnlocks, cadences, locations, refinements, statAugments, stats);
43+
routed.Run();
44+
4245
GenerateIntegratedReport(finalState, flowState, flowSim);
4346
}
4447

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Linq;
5+
using Mythril.Data;
6+
7+
namespace Mythril.Headless.Simulation;
8+
9+
public class RoutedSimulator(
10+
Items items, Quests quests, QuestDetails questDetails, QuestUnlocks questUnlocks,
11+
QuestToCadenceUnlocks questToCadenceUnlocks, Cadences cadences, Locations locations,
12+
ItemRefinements refinements, StatAugments statAugments, Stats stats)
13+
{
14+
private readonly HashSet<string> _farmingStack = [];
15+
private const string END_QUEST = "Defeat the Mythril Construct";
16+
17+
public void Run()
18+
{
19+
Console.WriteLine("Starting Path-Routed Simulation...");
20+
var state = new SimulationState(stats);
21+
bool progressed = true; int steps = 0; const int MAX_STEPS = 5000;
22+
while (progressed && steps < MAX_STEPS)
23+
{
24+
steps++;
25+
progressed = AttemptStep(state);
26+
if (state.CompletedQuests.Contains(END_QUEST)) { Console.WriteLine($"[SUCCESS] End Game reached!"); break; }
27+
if (state.CurrentTime > 3600 * 24 * 365) break;
28+
}
29+
Console.WriteLine($"Routed Completion Time: {(state.CurrentTime / 60.0):F1} minutes");
30+
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 != null && match.Detail != 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+
foreach (var req in requirements) {
93+
if (req.Item == null) continue;
94+
if (GetFromInventory(state, req.Item.Name) >= req.Quantity) continue;
95+
var source = GetBestSource(state, req.Item);
96+
if (source.Quest == null && source.Ability == null) return false;
97+
}
98+
return true;
99+
}
100+
101+
private List<(Quest Quest, QuestDetail Detail)> GetAvailableQuests(SimulationState state)
102+
{
103+
var available = new List<(Quest, QuestDetail)>();
104+
foreach (var loc in locations.All) {
105+
if (!string.IsNullOrEmpty(loc.RequiredQuest) && !state.CompletedQuests.Contains(loc.RequiredQuest)) continue;
106+
if (loc.Quests == null) continue;
107+
foreach (var q in loc.Quests) {
108+
if (questUnlocks[q]?.Any(req => !state.CompletedQuests.Contains(req.Name)) ?? false) continue;
109+
var det = questDetails[q];
110+
if (det.RequiredStats?.All(rs => state.CurrentStats.GetValueOrDefault(rs.Key, 0) >= rs.Value) ?? true) available.Add((q, det));
111+
}
112+
}
113+
return available;
114+
}
115+
116+
private void ExecuteQuest(SimulationState state, Quest q, QuestDetail detail)
117+
{
118+
if (detail.Requirements != null) {
119+
foreach (var req in detail.Requirements) FarmResource(state, req.Item, req.Quantity);
120+
foreach (var req in detail.Requirements) SubtractFromInventory(state, req.Item.Name, req.Quantity);
121+
}
122+
state.CurrentTime += detail.DurationSeconds * Math.Pow(0.75, (state.CurrentStats.GetValueOrDefault(detail.PrimaryStat ?? "Vitality", 10) - 10) / 10.0);
123+
state.CompletedQuests.Add(q.Name);
124+
if (detail.Rewards != null) foreach (var rew in detail.Rewards) state.Inventory[rew.Item.Name] = GetFromInventory(state, rew.Item.Name) + rew.Quantity;
125+
var cads = questToCadenceUnlocks[q];
126+
if (cads != null) foreach (var cad in cads) state.UnlockedCadences.Add(cad.Name);
127+
UpdateStats(state);
128+
}
129+
130+
private bool FarmResource(SimulationState state, Item item, long quantityNeeded)
131+
{
132+
if (_farmingStack.Contains(item.Name)) return false;
133+
_farmingStack.Add(item.Name);
134+
try {
135+
long current = GetFromInventory(state, item.Name);
136+
if (current >= quantityNeeded) return true;
137+
var source = GetBestSource(state, item);
138+
var sq = source.Quest; var sd = source.Detail;
139+
if (sq != null && sd != null) {
140+
var rewards = ((QuestDetail)sd).Rewards;
141+
if (rewards != null) {
142+
var reward = rewards.First(r => r.Item == item);
143+
int runs = (int)Math.Min(1000, Math.Ceiling((double)(quantityNeeded - current) / reward.Quantity));
144+
for (int i = 0; i < runs; i++) ExecuteQuest(state, (Quest)sq, (QuestDetail)sd);
145+
return GetFromInventory(state, item.Name) >= quantityNeeded;
146+
}
147+
} else if (source.Ability != null && source.Recipes != null) {
148+
var recipe = source.Recipes.First(x => x.Value.OutputItem == item);
149+
int runs = (int)Math.Min(1000, Math.Ceiling((double)(quantityNeeded - current) / recipe.Value.OutputQuantity));
150+
for (int i = 0; i < runs; i++) ExecuteRefinement(state, (CadenceAbility)source.Ability, (string)source.PrimaryStat!, recipe.Key, recipe.Value);
151+
return GetFromInventory(state, item.Name) >= quantityNeeded;
152+
}
153+
return false;
154+
} finally { _farmingStack.Remove(item.Name); }
155+
}
156+
157+
private void ExecuteRefinement(SimulationState state, CadenceAbility ability, string primaryStat, Item input, Recipe recipe)
158+
{
159+
FarmResource(state, input, recipe.InputQuantity);
160+
SubtractFromInventory(state, input.Name, recipe.InputQuantity);
161+
state.CurrentTime += 15.0 * Math.Pow(0.75, (state.CurrentStats.GetValueOrDefault(primaryStat, 10) - 10) / 10.0);
162+
state.Inventory[recipe.OutputItem.Name] = GetFromInventory(state, recipe.OutputItem.Name) + recipe.OutputQuantity;
163+
UpdateStats(state);
164+
}
165+
166+
private ActivitySource GetBestSource(SimulationState state, Item item)
167+
{
168+
var recurring = locations.All.SelectMany(l => l.Quests ?? Array.Empty<Quest>()).Where(q => {
169+
var loc = locations.All.First(x => x.Quests != null && x.Quests.Contains(q));
170+
if (!string.IsNullOrEmpty(loc.RequiredQuest) && !state.CompletedQuests.Contains(loc.RequiredQuest)) return false;
171+
if (questUnlocks[q]?.Any(req => !state.CompletedQuests.Contains(req.Name)) ?? false) return false;
172+
var d = questDetails[q]; return d.Type == QuestType.Recurring && (d.Rewards?.Any(r => r.Item == item) ?? false);
173+
}).OrderByDescending(q => (double)(questDetails[q].Rewards?.First(r => r.Item == item).Quantity ?? 0) / questDetails[q].DurationSeconds).FirstOrDefault();
174+
if (recurring != null && recurring.Name != null) return new ActivitySource { Quest = recurring, Detail = questDetails[recurring] };
175+
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();
176+
if (refMatch.Key != null && refMatch.Key.Name != null) return new ActivitySource { Ability = refMatch.Key, PrimaryStat = refMatch.Value.PrimaryStat, Recipes = refMatch.Value.Recipes };
177+
return new ActivitySource();
178+
}
179+
180+
private void UpdateStats(SimulationState state)
181+
{
182+
if (state.CurrentStats.GetValueOrDefault("Strength", 0) >= 60) state.UnlockedCadences.Add("Geologist");
183+
if (state.CurrentStats.GetValueOrDefault("Speed", 0) >= 60) state.UnlockedCadences.Add("Tide-Caller");
184+
if (state.CurrentStats.GetValueOrDefault("Vitality", 0) >= 60) state.UnlockedCadences.Add("The Sentinel");
185+
if (state.CurrentStats.GetValueOrDefault("Magic", 0) >= 100) state.UnlockedCadences.Add("Scholar");
186+
if (state.CurrentStats.GetValueOrDefault("Strength", 0) >= 100 && state.CurrentStats.GetValueOrDefault("Speed", 0) >= 100) state.UnlockedCadences.Add("Slayer");
187+
188+
foreach (var stat in stats.All) {
189+
string jAbil = stat.Name switch { "Strength" => "J-Str", "Magic" => "J-Magic", "Vitality" => "J-Vit", "Speed" => "J-Speed", _ => "J-" + stat.Name };
190+
if (state.UnlockedAbilities.Any(ua => ua.EndsWith($":{jAbil}"))) {
191+
var best = items.All.Where(i => i.ItemType == ItemType.Spell && GetFromInventory(state, i.Name) > 0)
192+
.Select(i => (Item: i, Augment: statAugments[i].FirstOrDefault(a => a.Stat.Name == stat.Name)))
193+
.Where(x => x.Augment != null && x.Augment.Stat != null && x.Augment.Stat.Name != null).OrderByDescending(x => x.Augment.ModifierAtFull).FirstOrDefault();
194+
if (best.Item != null) {
195+
int val = 10 + (int)(state.MagicCapacity * (best.Augment.ModifierAtFull / 100.0));
196+
state.CurrentStats[stat.Name] = Math.Max(state.CurrentStats.GetValueOrDefault(stat.Name, 25), Math.Min(255, val));
197+
}
198+
}
199+
}
200+
}
201+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Mythril.Data;
4+
5+
namespace Mythril.Headless.Simulation;
6+
7+
public class SimulationState(Stats stats)
8+
{
9+
public double CurrentTime { get; set; } = 0;
10+
public Dictionary<string, long> Inventory { get; } = [];
11+
public HashSet<string> CompletedQuests { get; } = [];
12+
public HashSet<string> UnlockedCadences { get; } = ["Recruit"];
13+
public HashSet<string> UnlockedAbilities { get; } = [];
14+
public Dictionary<string, int> CurrentStats { get; } = InitializeStats(stats);
15+
public int MagicCapacity { get; set; } = 30;
16+
17+
private static Dictionary<string, int> InitializeStats(Stats stats)
18+
{
19+
var dict = new Dictionary<string, int>();
20+
foreach (var s in stats.All) dict[s.Name] = 25;
21+
return dict;
22+
}
23+
}
24+
25+
public class ActivitySource
26+
{
27+
public Quest? Quest { get; set; }
28+
public QuestDetail? Detail { get; set; }
29+
public CadenceAbility? Ability { get; set; }
30+
public string? PrimaryStat { get; set; }
31+
public Dictionary<Item, Recipe>? Recipes { get; set; }
32+
}

scripts/check_health.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def parse_coverage():
159159
if "obj" in filename or filename.endswith(".g.cs"):
160160
continue
161161

162-
ignored_files = ["Models.cs", "Cadences.cs", "Program.cs", "ReachabilitySimulator.cs", "FlowSimulator.cs", "LatticeSimulator.cs"]
162+
ignored_files = ["Models.cs", "Cadences.cs", "Program.cs", "ReachabilitySimulator.cs", "FlowSimulator.cs", "LatticeSimulator.cs", "RoutedSimulator.cs"]
163163
if any(ignored in filename for ignored in ignored_files):
164164
continue
165165

scripts/data/health_summary.json

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
{
2-
"is_healthy": true,
3-
"failure_count": 0,
2+
"is_healthy": false,
3+
"failure_count": 3,
44
"metrics": {
55
"monoliths": 0,
6-
"coverage": 74.34,
6+
"coverage": 0.0,
77
"mutation_score": 0.0,
88
"missing_tests": 0,
99
"key_violations": 0,
1010
"testid_violations": 0,
11-
"stale_docs": 1,
11+
"stale_docs": 0,
1212
"reachability_passed": {
13-
"passed": true,
14-
"time": "0.6m",
15-
"sustainable": 21,
16-
"unsustainable": 13
13+
"passed": false,
14+
"time": "N/A"
1715
},
1816
"pending_feedback": 0,
19-
"test_passed": true
17+
"test_passed": false
2018
},
21-
"failures": []
19+
"failures": [
20+
{
21+
"category": "tests",
22+
"message": "dotnet test failed",
23+
"metadata": {}
24+
},
25+
{
26+
"category": "coverage",
27+
"message": "Coverage report not found",
28+
"metadata": {}
29+
},
30+
{
31+
"category": "reachability",
32+
"message": "Simulation failed: One or more quests are mathematically unreachable.",
33+
"metadata": {}
34+
}
35+
]
2236
}

scripts/data/shield_coverage.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": 1,
33
"label": "coverage",
4-
"message": "74.3%",
5-
"color": "green"
4+
"message": "0.0%",
5+
"color": "red"
66
}

scripts/data/shield_docs.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": 1,
33
"label": "docs",
4-
"message": "stale",
5-
"color": "orange"
4+
"message": "up-to-date",
5+
"color": "brightgreen"
66
}

scripts/data/shield_game_time.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": 1,
33
"label": "optimal completion",
4-
"message": "0.6m",
4+
"message": "N/A",
55
"color": "blue"
66
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": 1,
33
"label": "reachability",
4-
"message": "passed",
5-
"color": "brightgreen"
4+
"message": "failed",
5+
"color": "red"
66
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"schemaVersion": 1,
33
"label": "economy",
4-
"message": "62% sustainable",
5-
"color": "orange"
4+
"message": "N/A",
5+
"color": "inactive"
66
}

0 commit comments

Comments
 (0)