Remove obsolete project files for MrGameEng.AI, MrGameEng.Assets, MrGameEng.Atlases, and MrGameEng.Collisions modules. Introduce new AssetManager and related classes for asset loading and management, including support for texture atlases and mod definitions. Enhance mod loading capabilities with DefDatabase and LanguageManager for JSON-based definitions and localization. Implement a shelf packing algorithm for efficient texture atlas creation.

This commit is contained in:
Leonid Pershin
2026-06-12 07:47:04 +03:00
parent 1f87fb0b74
commit c30e2ce764
70 changed files with 0 additions and 233 deletions
@@ -0,0 +1,168 @@
using MrGameEng.AI;
using Xunit;
namespace MrGameEng.AI.Tests;
public class UtilityAiTests
{
// A minimal agent context: everything the considerations read.
private record struct Ctx(float Energy, float Hunger);
private static UtilityAi<Ctx> BuildBrain()
{
// Rest gets attractive as energy drops; wander as energy is high.
var rest = new UtilityAction<Ctx>(
"rest",
new Consideration<Ctx>(
"tired",
c => c.Energy,
0f,
1f,
ResponseCurve.Linear(slope: -1f, yShift: 1f)
)
);
var wander = new UtilityAction<Ctx>(
"wander",
new Consideration<Ctx>("rested", c => c.Energy)
);
return new UtilityAi<Ctx>(rest, wander);
}
[Fact]
public void Select_PicksTheHighestScoringAction()
{
var brain = BuildBrain();
Assert.Equal("rest", brain.Select(new Ctx(Energy: 0.1f, Hunger: 0f))!.Name);
Assert.Equal("wander", brain.Select(new Ctx(Energy: 0.9f, Hunger: 0f))!.Name);
}
[Fact]
public void Select_IsDeterministicAcrossRepeatedCalls()
{
var brain = BuildBrain();
var ctx = new Ctx(Energy: 0.3f, Hunger: 0.5f);
var first = brain.Select(ctx)!.Name;
for (var i = 0; i < 100; i++)
{
Assert.Equal(first, brain.Select(ctx)!.Name);
}
}
[Fact]
public void Select_TieResolvesToEarliestAction()
{
// Two actions that always score equally; the first declared must win.
var a = new UtilityAction<Ctx>("a", new Consideration<Ctx>("k", _ => 0.5f));
var b = new UtilityAction<Ctx>("b", new Consideration<Ctx>("k", _ => 0.5f));
var brain = new UtilityAi<Ctx>(a, b);
Assert.Equal("a", brain.Select(default)!.Name);
}
[Fact]
public void Select_ReturnsNull_WhenNothingBeatsThreshold()
{
var brain = BuildBrain();
Assert.Null(brain.Select(new Ctx(Energy: 0.5f, Hunger: 0f), threshold: 0.99f));
}
[Fact]
public void Select_PopulatesLastScoresAlignedWithActions()
{
var brain = BuildBrain();
brain.Select(new Ctx(Energy: 0.2f, Hunger: 0f));
Assert.Equal(2, brain.LastScores.Length);
Assert.True(brain.LastScores[0] > brain.LastScores[1]); // rest scores above wander
}
[Fact]
public void VetoConsideration_ZeroesTheAction()
{
var action = new UtilityAction<Ctx>(
"eat",
new Consideration<Ctx>("has-food", _ => 0f), // veto: no food
new Consideration<Ctx>("hungry", _ => 1f)
);
Assert.Equal(0f, action.Score(default));
}
[Fact]
public void Weight_ScalesTheActionScore()
{
var low = new UtilityAction<Ctx>("a", 0.5f, new Consideration<Ctx>("k", _ => 0.4f));
var high = new UtilityAction<Ctx>("a", 2f, new Consideration<Ctx>("k", _ => 0.4f));
Assert.True(high.Score(default) > low.Score(default));
}
[Fact]
public void SelectWeighted_IsReproducibleForTheSameSeed()
{
var brain = BuildBrain();
var ctx = new Ctx(Energy: 0.5f, Hunger: 0f);
var first = Run(new Random(1234));
var second = Run(new Random(1234));
Assert.Equal(first, second);
List<string> Run(Random random)
{
var picks = new List<string>();
for (var i = 0; i < 50; i++)
{
picks.Add(brain.SelectWeighted(ctx, random)!.Name);
}
return picks;
}
}
[Fact]
public void SelectWeighted_FavoursTheHigherScoreOverManyRolls()
{
var brain = BuildBrain();
var ctx = new Ctx(Energy: 0.1f, Hunger: 0f); // rest should dominate
var random = new Random(7);
var rest = 0;
for (var i = 0; i < 1000; i++)
{
if (brain.SelectWeighted(ctx, random)!.Name == "rest")
{
rest++;
}
}
Assert.True(rest > 800, $"expected rest to dominate, got {rest}/1000");
}
[Fact]
public void Constructor_Throws_WhenNoActions()
{
Assert.Throws<ArgumentException>(() => new UtilityAi<Ctx>());
}
[Fact]
public void Consideration_Throws_WhenRangeIsDegenerate()
{
Assert.Throws<ArgumentException>(() =>
new Consideration<Ctx>("bad", c => c.Energy, min: 1f, max: 1f)
);
}
[Fact]
public void Consideration_NormalizesRawValuesAgainstItsRange()
{
var c = new Consideration<Ctx>("hunger", x => x.Hunger, min: 0f, max: 200f);
Assert.Equal(0f, c.Score(new Ctx(0f, 0f)), 5);
Assert.Equal(0.5f, c.Score(new Ctx(0f, 100f)), 5);
Assert.Equal(1f, c.Score(new Ctx(0f, 9999f)), 5); // clamps above range
}
}