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:
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.AI;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.AI.Tests;
|
||||
|
||||
public class BlackboardTests
|
||||
{
|
||||
[Fact]
|
||||
public void SetThenTryGet_RoundTripsTypedValues()
|
||||
{
|
||||
var board = new Blackboard();
|
||||
board.Set("target", new Vector2(3f, 4f));
|
||||
|
||||
Assert.True(board.TryGet<Vector2>("target", out var value));
|
||||
Assert.Equal(new Vector2(3f, 4f), value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_ReturnsFalse_OnMissingKeyOrTypeMismatch()
|
||||
{
|
||||
var board = new Blackboard();
|
||||
board.Set("count", 5);
|
||||
|
||||
Assert.False(board.TryGet<int>("missing", out _));
|
||||
Assert.False(board.TryGet<string>("count", out _)); // wrong type
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrDefault_FallsBackWhenAbsent()
|
||||
{
|
||||
var board = new Blackboard();
|
||||
|
||||
Assert.Equal(42, board.GetOrDefault("hp", 42));
|
||||
board.Set("hp", 7);
|
||||
Assert.Equal(7, board.GetOrDefault("hp", 42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_OverwritesExistingValue()
|
||||
{
|
||||
var board = new Blackboard();
|
||||
board.Set("k", 1);
|
||||
board.Set("k", 2);
|
||||
|
||||
Assert.Equal(2, board.GetOrDefault("k", 0));
|
||||
Assert.Equal(1, board.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveAndClear_DropKeys()
|
||||
{
|
||||
var board = new Blackboard();
|
||||
board.Set("a", 1);
|
||||
board.Set("b", 2);
|
||||
|
||||
Assert.True(board.Remove("a"));
|
||||
Assert.False(board.Remove("a"));
|
||||
Assert.True(board.Has("b"));
|
||||
|
||||
board.Clear();
|
||||
Assert.Equal(0, board.Count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using MrGameEng.AI;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.AI.Tests;
|
||||
|
||||
public class ResponseCurveTests
|
||||
{
|
||||
[Fact]
|
||||
public void Identity_ReturnsInputUnchanged()
|
||||
{
|
||||
var curve = ResponseCurve.Identity;
|
||||
|
||||
Assert.Equal(0f, curve.Evaluate(0f));
|
||||
Assert.Equal(0.5f, curve.Evaluate(0.5f), 5);
|
||||
Assert.Equal(1f, curve.Evaluate(1f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_HasZeroSlope_SoIdentityMustBeUsedExplicitly()
|
||||
{
|
||||
// Guards the gotcha that default(ResponseCurve) is NOT the identity curve.
|
||||
var zero = default(ResponseCurve);
|
||||
|
||||
Assert.Equal(0f, zero.Evaluate(0.5f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ClampsInputAndOutputToUnitRange()
|
||||
{
|
||||
var curve = ResponseCurve.Linear();
|
||||
|
||||
Assert.Equal(0f, curve.Evaluate(-2f));
|
||||
Assert.Equal(1f, curve.Evaluate(5f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Linear_NegativeSlope_InvertsTheInput()
|
||||
{
|
||||
var curve = ResponseCurve.Linear(slope: -1f, yShift: 1f);
|
||||
|
||||
Assert.Equal(1f, curve.Evaluate(0f), 5);
|
||||
Assert.Equal(0.75f, curve.Evaluate(0.25f), 5);
|
||||
Assert.Equal(0f, curve.Evaluate(1f), 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Polynomial_Quadratic_EasesInFromZero()
|
||||
{
|
||||
var curve = ResponseCurve.Polynomial(exponent: 2f);
|
||||
|
||||
Assert.Equal(0.25f, curve.Evaluate(0.5f), 5);
|
||||
Assert.True(curve.Evaluate(0.25f) < 0.25f); // below the linear line: slow start
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logistic_IsMonotonicAndCentredOnMidpoint()
|
||||
{
|
||||
var curve = ResponseCurve.Logistic(steepness: 12f, midpoint: 0.5f);
|
||||
|
||||
Assert.Equal(0.5f, curve.Evaluate(0.5f), 2);
|
||||
Assert.True(curve.Evaluate(0.2f) < 0.5f);
|
||||
Assert.True(curve.Evaluate(0.8f) > 0.5f);
|
||||
Assert.True(curve.Evaluate(0.6f) > curve.Evaluate(0.4f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmoothStep_IsFlatAtTheEnds()
|
||||
{
|
||||
var curve = ResponseCurve.SmoothStep();
|
||||
|
||||
Assert.Equal(0f, curve.Evaluate(0f), 5);
|
||||
Assert.Equal(1f, curve.Evaluate(1f), 5);
|
||||
Assert.Equal(0.5f, curve.Evaluate(0.5f), 5);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user