Agent Tests for API, MAUI, and Slop Features
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
using Model.Economy;
|
||||
using Model.Entity;
|
||||
using Model.Feedback;
|
||||
using Model.Glossary;
|
||||
using Model.MemoryTester;
|
||||
using Model.Notes;
|
||||
using Model.Website;
|
||||
@@ -240,6 +241,30 @@ public interface IKeyService
|
||||
public void Unsubscribe(Action? action);
|
||||
}
|
||||
|
||||
public interface IGlossaryService
|
||||
{
|
||||
public GlossaryTermModel? GetTerm(string id);
|
||||
public List<GlossaryTermModel> SearchTerms(string query);
|
||||
public List<GlossaryTermModel> GetTermsByCategory(string category);
|
||||
public List<GlossaryTermModel> GetAllTerms();
|
||||
public List<string> GetCategories();
|
||||
public string LinkifyText(string text);
|
||||
public void Subscribe(Action action);
|
||||
public void Unsubscribe(Action action);
|
||||
}
|
||||
|
||||
public interface IGlossaryDialogService
|
||||
{
|
||||
public void Subscribe(Action action);
|
||||
public void Unsubscribe(Action action);
|
||||
public void AddDialog(string termId);
|
||||
public void CloseDialog();
|
||||
public void BackDialog();
|
||||
public string? GetTermId();
|
||||
public bool HasDialog();
|
||||
public bool HasHistory();
|
||||
}
|
||||
|
||||
public interface IMemoryTesterService
|
||||
{
|
||||
public delegate void MemoryAction(MemoryTesterEvent memoryEvent);
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
using Model.Entity.Data;
|
||||
using Model.TechTree;
|
||||
using Model.Types;
|
||||
|
||||
namespace Services.Immortal;
|
||||
|
||||
public class TechTreeService
|
||||
{
|
||||
private TechTreeGraphModel? _graph;
|
||||
|
||||
public TechTreeGraphModel BuildGraph(string? factionFilter = null)
|
||||
{
|
||||
_graph = new TechTreeGraphModel();
|
||||
var entities = EntityData.Get();
|
||||
var factions = new HashSet<string>();
|
||||
|
||||
var factionEntityIds = new HashSet<string>();
|
||||
|
||||
foreach (var kvp in entities)
|
||||
{
|
||||
var entity = kvp.Value;
|
||||
var entityFaction = entity.Faction()?.Faction;
|
||||
|
||||
if (factionFilter != null)
|
||||
{
|
||||
if (entityFaction == null) continue;
|
||||
if (!entityFaction.Equals(factionFilter, StringComparison.OrdinalIgnoreCase)
|
||||
&& !entityFaction.Equals(DataType.FACTION_Neutral, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
factionEntityIds.Add(kvp.Key);
|
||||
}
|
||||
|
||||
if (entityFaction != null)
|
||||
factions.Add(entityFaction);
|
||||
|
||||
var node = new TechTreeNodeModel
|
||||
{
|
||||
Id = kvp.Key,
|
||||
Name = entity.Info()?.Name ?? kvp.Key,
|
||||
EntityType = entity.EntityType,
|
||||
Faction = entityFaction ?? "",
|
||||
Descriptive = entity.Descriptive
|
||||
};
|
||||
|
||||
_graph.Nodes.Add(node);
|
||||
}
|
||||
|
||||
foreach (var kvp in entities)
|
||||
{
|
||||
var entity = kvp.Value;
|
||||
var entityId = kvp.Key;
|
||||
|
||||
// ProducedBy -> the building that produces this entity
|
||||
var production = entity.Production();
|
||||
if (production?.ProducedBy != null && entities.ContainsKey(production.ProducedBy))
|
||||
if (factionFilter == null ||
|
||||
(factionEntityIds.Contains(entityId) && factionEntityIds.Contains(production.ProducedBy)))
|
||||
_graph.Edges.Add(new TechTreeEdgeModel
|
||||
{
|
||||
SourceId = production.ProducedBy,
|
||||
TargetId = entityId,
|
||||
EdgeType = TechTreeEdgeType.Produces
|
||||
});
|
||||
|
||||
// Requirements
|
||||
foreach (var req in entity.Requirements())
|
||||
{
|
||||
if (!entities.ContainsKey(req.Id)) continue;
|
||||
|
||||
if (factionFilter != null &&
|
||||
(!factionEntityIds.Contains(entityId) || !factionEntityIds.Contains(req.Id)))
|
||||
continue;
|
||||
|
||||
string edgeType;
|
||||
if (req.Requirement == RequirementType.Production_Building)
|
||||
edgeType = TechTreeEdgeType.RequiresProduction;
|
||||
else if (req.Requirement == RequirementType.Research_Building)
|
||||
edgeType = TechTreeEdgeType.RequiresResearch;
|
||||
else if (req.Requirement == RequirementType.Research_Upgrade)
|
||||
edgeType = TechTreeEdgeType.RequiresResearch;
|
||||
else if (req.Requirement == RequirementType.Morph)
|
||||
edgeType = TechTreeEdgeType.Morph;
|
||||
else
|
||||
edgeType = TechTreeEdgeType.RequiresProduction;
|
||||
|
||||
_graph.Edges.Add(new TechTreeEdgeModel
|
||||
{
|
||||
SourceId = req.Id,
|
||||
TargetId = entityId,
|
||||
EdgeType = edgeType
|
||||
});
|
||||
}
|
||||
|
||||
// Upgrades
|
||||
foreach (var upgrade in entity.IdUpgrades())
|
||||
{
|
||||
if (!entities.ContainsKey(upgrade.Id)) continue;
|
||||
|
||||
if (factionFilter != null &&
|
||||
(!factionEntityIds.Contains(entityId) || !factionEntityIds.Contains(upgrade.Id)))
|
||||
continue;
|
||||
|
||||
_graph.Edges.Add(new TechTreeEdgeModel
|
||||
{
|
||||
SourceId = upgrade.Id,
|
||||
TargetId = entityId,
|
||||
EdgeType = TechTreeEdgeType.Upgrades
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Build reverse index: what does each entity unlock?
|
||||
foreach (var edge in _graph.Edges)
|
||||
{
|
||||
if (!_graph.Unlocks.ContainsKey(edge.SourceId))
|
||||
_graph.Unlocks[edge.SourceId] = new List<string>();
|
||||
|
||||
if (!_graph.Unlocks[edge.SourceId].Contains(edge.TargetId))
|
||||
_graph.Unlocks[edge.SourceId].Add(edge.TargetId);
|
||||
}
|
||||
|
||||
// Compute layers (BFS from root nodes with no incoming edges)
|
||||
ComputeLayers();
|
||||
|
||||
return _graph;
|
||||
}
|
||||
|
||||
public List<TechTreeNodeModel> GetUpgradePath(string entityId)
|
||||
{
|
||||
var graph = GetGraph();
|
||||
var path = new List<TechTreeNodeModel>();
|
||||
var visited = new HashSet<string>();
|
||||
var queue = new Queue<string>();
|
||||
queue.Enqueue(entityId);
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
if (!visited.Add(current)) continue;
|
||||
|
||||
var node = graph.Nodes.FirstOrDefault(n => n.Id == current);
|
||||
if (node != null) path.Add(node);
|
||||
|
||||
var upgradeEdges = graph.Edges
|
||||
.Where(e => e.SourceId == current && e.EdgeType == TechTreeEdgeType.Upgrades);
|
||||
foreach (var edge in upgradeEdges)
|
||||
queue.Enqueue(edge.TargetId);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public List<TechTreeNodeModel> GetPrerequisites(string entityId)
|
||||
{
|
||||
var graph = GetGraph();
|
||||
var prereqs = new List<TechTreeNodeModel>();
|
||||
|
||||
var incomingEdges = graph.Edges
|
||||
.Where(e => e.TargetId == entityId && e.EdgeType != TechTreeEdgeType.UpgradedBy);
|
||||
|
||||
foreach (var edge in incomingEdges)
|
||||
{
|
||||
var node = graph.Nodes.FirstOrDefault(n => n.Id == edge.SourceId);
|
||||
if (node != null) prereqs.Add(node);
|
||||
}
|
||||
|
||||
return prereqs;
|
||||
}
|
||||
|
||||
public List<TechTreeNodeModel> GetUnlocks(string entityId)
|
||||
{
|
||||
var graph = GetGraph();
|
||||
var unlocks = new List<TechTreeNodeModel>();
|
||||
|
||||
if (!graph.Unlocks.TryGetValue(entityId, out var unlockIds))
|
||||
return unlocks;
|
||||
|
||||
foreach (var id in unlockIds)
|
||||
{
|
||||
var node = graph.Nodes.FirstOrDefault(n => n.Id == id);
|
||||
if (node != null) unlocks.Add(node);
|
||||
}
|
||||
|
||||
return unlocks;
|
||||
}
|
||||
|
||||
public List<string> GetFactions()
|
||||
{
|
||||
return GetGraph().Nodes
|
||||
.Where(n => !string.IsNullOrEmpty(n.Faction))
|
||||
.Select(n => n.Faction)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public TechTreeGraphModel GetFilteredGraph(string? faction, string? searchText, string? highlightEntity)
|
||||
{
|
||||
var graph = GetGraph();
|
||||
|
||||
if (faction == null && string.IsNullOrEmpty(searchText) && highlightEntity == null)
|
||||
return graph;
|
||||
|
||||
// Build from scratch with faction filter
|
||||
return BuildGraph(faction);
|
||||
}
|
||||
|
||||
private TechTreeGraphModel GetGraph()
|
||||
{
|
||||
_graph ??= BuildGraph();
|
||||
return _graph;
|
||||
}
|
||||
|
||||
private void ComputeLayers()
|
||||
{
|
||||
var inDegree = new Dictionary<string, int>();
|
||||
var adjacency = new Dictionary<string, List<string>>();
|
||||
|
||||
foreach (var node in _graph!.Nodes)
|
||||
{
|
||||
inDegree[node.Id] = 0;
|
||||
adjacency[node.Id] = new List<string>();
|
||||
}
|
||||
|
||||
foreach (var edge in _graph.Edges)
|
||||
{
|
||||
if (adjacency.ContainsKey(edge.SourceId)) adjacency[edge.SourceId].Add(edge.TargetId);
|
||||
|
||||
if (inDegree.ContainsKey(edge.TargetId)) inDegree[edge.TargetId]++;
|
||||
}
|
||||
|
||||
var queue = new Queue<string>();
|
||||
foreach (var kvp in inDegree)
|
||||
if (kvp.Value == 0)
|
||||
queue.Enqueue(kvp.Key);
|
||||
|
||||
var layers = new Dictionary<string, int>();
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var current = queue.Dequeue();
|
||||
var currentLayer = layers.TryGetValue(current, out var l) ? l : 0;
|
||||
|
||||
foreach (var next in adjacency.GetValueOrDefault(current, new List<string>()))
|
||||
{
|
||||
var nextLayer = currentLayer + 1;
|
||||
if (!layers.ContainsKey(next) || layers[next] < nextLayer) layers[next] = nextLayer;
|
||||
|
||||
inDegree[next]--;
|
||||
if (inDegree[next] == 0) queue.Enqueue(next);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var node in _graph.Nodes) node.Layer = layers.TryGetValue(node.Id, out var layer) ? layer : 0;
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,13 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazor-Analytics" Version="3.11.0"/>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.3.0-preview.1"/>
|
||||
<PackageReference Include="Microsoft.JSInterop" Version="8.0.14"/>
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Model\Model.csproj" />
|
||||
<ProjectReference Include="..\Model\Model.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Blazor.Analytics;
|
||||
|
||||
namespace Services.Website;
|
||||
namespace Services.Website;
|
||||
|
||||
public class DataCollectionKeys
|
||||
{
|
||||
@@ -12,15 +10,12 @@ public class DataCollectionKeys
|
||||
|
||||
public class DataCollectionService : IDataCollectionService, IDisposable
|
||||
{
|
||||
private readonly IAnalytics _globalTracking;
|
||||
private readonly IStorageService _storageService;
|
||||
|
||||
private bool _isEnabled;
|
||||
|
||||
public DataCollectionService(IAnalytics globalTracking,
|
||||
IStorageService storageService)
|
||||
public DataCollectionService(IStorageService storageService)
|
||||
{
|
||||
_globalTracking = globalTracking;
|
||||
_storageService = storageService;
|
||||
|
||||
_storageService.Subscribe(Refresh);
|
||||
@@ -30,7 +25,7 @@ public class DataCollectionService : IDataCollectionService, IDisposable
|
||||
|
||||
public void SendEvent<T>(string eventName, T eventData)
|
||||
{
|
||||
if (_isEnabled) _globalTracking.TrackEvent(eventName, eventData);
|
||||
// No-op
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace Services.Website;
|
||||
|
||||
public class GlossaryDialogService : IGlossaryDialogService
|
||||
{
|
||||
private readonly List<string> history = new();
|
||||
private string? termId;
|
||||
|
||||
public void Subscribe(Action action)
|
||||
{
|
||||
OnChange += action;
|
||||
}
|
||||
|
||||
public void Unsubscribe(Action action)
|
||||
{
|
||||
OnChange += action;
|
||||
}
|
||||
|
||||
public void AddDialog(string id)
|
||||
{
|
||||
termId = id;
|
||||
history.Add(id);
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
public void CloseDialog()
|
||||
{
|
||||
termId = null;
|
||||
history.Clear();
|
||||
NotifyDataChanged();
|
||||
}
|
||||
|
||||
public void BackDialog()
|
||||
{
|
||||
if (history.Count > 1)
|
||||
{
|
||||
history.RemoveAt(history.Count - 1);
|
||||
termId = history.Count > 0 ? history.Last() : null;
|
||||
NotifyDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasDialog()
|
||||
{
|
||||
return termId != null;
|
||||
}
|
||||
|
||||
public bool HasHistory()
|
||||
{
|
||||
return history.Count > 1;
|
||||
}
|
||||
|
||||
public string? GetTermId()
|
||||
{
|
||||
return termId;
|
||||
}
|
||||
|
||||
private event Action OnChange = null!;
|
||||
|
||||
private void NotifyDataChanged()
|
||||
{
|
||||
OnChange?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Model.Glossary;
|
||||
|
||||
namespace Services.Website;
|
||||
|
||||
public class GlossaryService : IGlossaryService
|
||||
{
|
||||
private List<string>? _sortedTerms;
|
||||
private Dictionary<string, GlossaryTermModel>? _terms;
|
||||
|
||||
public GlossaryTermModel? GetTerm(string id)
|
||||
{
|
||||
var terms = GetAllTermsDict();
|
||||
return terms.TryGetValue(id, out var term) ? term : null;
|
||||
}
|
||||
|
||||
public List<GlossaryTermModel> SearchTerms(string query)
|
||||
{
|
||||
var q = query.ToLowerInvariant();
|
||||
return GetAllTerms().Where(t =>
|
||||
t.Term.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
t.ShortDefinition.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||
t.Category.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
public List<GlossaryTermModel> GetTermsByCategory(string category)
|
||||
{
|
||||
return GetAllTerms().Where(t =>
|
||||
t.Category.Equals(category, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
public List<GlossaryTermModel> GetAllTerms()
|
||||
{
|
||||
return GetAllTermsDict().Values.ToList();
|
||||
}
|
||||
|
||||
public List<string> GetCategories()
|
||||
{
|
||||
return GetAllTerms().Select(t => t.Category).Distinct().ToList();
|
||||
}
|
||||
|
||||
public string LinkifyText(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return text;
|
||||
|
||||
var sorted = GetSortedTerms();
|
||||
|
||||
foreach (var term in sorted)
|
||||
{
|
||||
var pattern = $@"\b{Regex.Escape(term)}\b";
|
||||
text = Regex.Replace(text, pattern, match =>
|
||||
{
|
||||
var termData = GetTermByName(term);
|
||||
if (termData == null) return match.Value;
|
||||
return
|
||||
$"<span class=\"glossary-link\" data-glossary-id=\"{termData.Id}\">{match.Value}</span>";
|
||||
});
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
public void Subscribe(Action action)
|
||||
{
|
||||
OnChange += action;
|
||||
}
|
||||
|
||||
public void Unsubscribe(Action action)
|
||||
{
|
||||
OnChange += action;
|
||||
}
|
||||
|
||||
private event Action OnChange = null!;
|
||||
|
||||
private void NotifyDataChanged()
|
||||
{
|
||||
OnChange?.Invoke();
|
||||
}
|
||||
|
||||
private Dictionary<string, GlossaryTermModel> GetAllTermsDict()
|
||||
{
|
||||
_terms ??= GlossaryData.GetTerms();
|
||||
return _terms;
|
||||
}
|
||||
|
||||
private List<string> GetSortedTerms()
|
||||
{
|
||||
if (_sortedTerms != null) return _sortedTerms;
|
||||
|
||||
_sortedTerms = GetAllTerms()
|
||||
.Select(t => t.Term)
|
||||
.OrderByDescending(t => t.Length)
|
||||
.ToList();
|
||||
|
||||
return _sortedTerms;
|
||||
}
|
||||
|
||||
private GlossaryTermModel? GetTermByName(string name)
|
||||
{
|
||||
return GetAllTerms().FirstOrDefault(t =>
|
||||
t.Term.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Model.Entity.Data;
|
||||
using Model.Glossary;
|
||||
using Model.Website;
|
||||
using Model.Website.Data;
|
||||
|
||||
@@ -43,6 +44,7 @@ public class SearchService : ISearchService
|
||||
Searches.Add("Pages", new List<SearchPointModel>());
|
||||
Searches.Add("Notes", new List<SearchPointModel>());
|
||||
Searches.Add("Entities", new List<SearchPointModel>());
|
||||
Searches.Add("Glossary", new List<SearchPointModel>());
|
||||
|
||||
foreach (var webPage in WebsiteData.GetPages())
|
||||
{
|
||||
@@ -92,6 +94,20 @@ public class SearchService : ISearchService
|
||||
Searches["Entities"].Add(SearchPoints.Last());
|
||||
}
|
||||
|
||||
if (WebsiteData.allowSlopData)
|
||||
foreach (var term in GlossaryData.GetTerms().Values)
|
||||
{
|
||||
SearchPoints.Add(new SearchPointModel
|
||||
{
|
||||
Title = term.Term,
|
||||
PointType = "Glossary",
|
||||
Summary = term.ShortDefinition,
|
||||
Href = $"glossary/{term.Id}"
|
||||
});
|
||||
|
||||
Searches["Glossary"].Add(SearchPoints.Last());
|
||||
}
|
||||
|
||||
isLoaded = true;
|
||||
|
||||
NotifyDataChanged();
|
||||
|
||||
Reference in New Issue
Block a user