Add MrGameEng.AI utility-AI module; format codebase with CSharpier New MrGameEng.AI module (ResponseCurve, Consideration, UtilityAction, UtilityAi selector, Blackboard) plus CSharpier formatting applied across the whole engine. Documents the CSharpier convention in CLAUDE.md. @
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,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.AI\MrGameEng.AI.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ public class AssetHandlesGeneratorTests
|
||||
values.TryGetValue(key, out value!);
|
||||
}
|
||||
|
||||
private sealed class FakeOptionsProvider(Dictionary<string, string> values) : AnalyzerConfigOptionsProvider
|
||||
private sealed class FakeOptionsProvider(Dictionary<string, string> values)
|
||||
: AnalyzerConfigOptionsProvider
|
||||
{
|
||||
public override AnalyzerConfigOptions GlobalOptions { get; } = new FakeOptions(values);
|
||||
|
||||
@@ -36,11 +37,18 @@ public class AssetHandlesGeneratorTests
|
||||
{
|
||||
var driver = CSharpGeneratorDriver.Create(
|
||||
[new AssetHandlesGenerator().AsSourceGenerator()],
|
||||
additionalTexts: Array.ConvertAll(files, f => (AdditionalText)new FakeAdditionalText(f)),
|
||||
optionsProvider: new FakeOptionsProvider(options ?? new Dictionary<string, string>
|
||||
{
|
||||
["build_property.RootNamespace"] = "MyGame",
|
||||
}));
|
||||
additionalTexts: Array.ConvertAll(
|
||||
files,
|
||||
f => (AdditionalText)new FakeAdditionalText(f)
|
||||
),
|
||||
optionsProvider: new FakeOptionsProvider(
|
||||
options
|
||||
?? new Dictionary<string, string>
|
||||
{
|
||||
["build_property.RootNamespace"] = "MyGame",
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
var compilation = CSharpCompilation.Create("test");
|
||||
var result = driver.RunGenerators(compilation).GetRunResult();
|
||||
@@ -50,8 +58,7 @@ public class AssetHandlesGeneratorTests
|
||||
[Fact]
|
||||
public void GeneratesTypedHandles_ForKnownExtensions()
|
||||
{
|
||||
var source = RunGenerator(
|
||||
[
|
||||
var source = RunGenerator([
|
||||
@"D:\game\Assets\Textures\player.png",
|
||||
@"D:\game\Assets\Sounds\jump.wav",
|
||||
@"D:\game\Assets\Fonts\main.ttf",
|
||||
@@ -63,10 +70,12 @@ public class AssetHandlesGeneratorTests
|
||||
Assert.Contains("public static class Textures", source);
|
||||
Assert.Contains(
|
||||
"AssetRef<global::Microsoft.Xna.Framework.Graphics.Texture2D> Player = new(\"Textures/player.png\")",
|
||||
source);
|
||||
source
|
||||
);
|
||||
Assert.Contains(
|
||||
"AssetRef<global::Microsoft.Xna.Framework.Audio.SoundEffect> Jump = new(\"Sounds/jump.wav\")",
|
||||
source);
|
||||
source
|
||||
);
|
||||
Assert.Contains("AssetRef<global::FontStashSharp.FontSystem> Main", source);
|
||||
Assert.Contains("AssetRef<global::MrGameEng.Core.MusicTrack> Theme", source);
|
||||
}
|
||||
@@ -74,8 +83,7 @@ public class AssetHandlesGeneratorTests
|
||||
[Fact]
|
||||
public void IgnoresUnknownExtensions_AndFilesOutsideAssets()
|
||||
{
|
||||
var source = RunGenerator(
|
||||
[
|
||||
var source = RunGenerator([
|
||||
@"D:\game\Assets\readme.md",
|
||||
@"D:\game\Other\image.png",
|
||||
@"D:\game\Assets\valid.png",
|
||||
@@ -89,8 +97,7 @@ public class AssetHandlesGeneratorTests
|
||||
[Fact]
|
||||
public void AtlasFiles_GetTextureAtlasHandles_AndPagesAreExcluded()
|
||||
{
|
||||
var source = RunGenerator(
|
||||
[
|
||||
var source = RunGenerator([
|
||||
@"D:\game\Assets\Atlases\Things.Pawn.atlas",
|
||||
@"D:\game\Assets\Atlases\Things.Pawn.atlas.0.png",
|
||||
@"D:\game\Assets\Atlases\Things.Pawn.atlas.1.png",
|
||||
@@ -98,7 +105,8 @@ public class AssetHandlesGeneratorTests
|
||||
|
||||
Assert.Contains(
|
||||
"AssetRef<global::MrGameEng.Atlases.TextureAtlas> ThingsPawn = new(\"Atlases/Things.Pawn.atlas\")",
|
||||
source);
|
||||
source
|
||||
);
|
||||
Assert.DoesNotContain("Texture2D> ThingsPawn", source);
|
||||
}
|
||||
|
||||
@@ -125,15 +133,33 @@ public class AssetHandlesGeneratorTests
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
"generated",
|
||||
[CSharpSyntaxTree.ParseText(source, cancellationToken: TestContext.Current.CancellationToken),
|
||||
CSharpSyntaxTree.ParseText(stubs, cancellationToken: TestContext.Current.CancellationToken)],
|
||||
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(System.Runtime.Loader.AssemblyLoadContext.Default
|
||||
.LoadFromAssemblyName(new System.Reflection.AssemblyName("System.Runtime")).Location)],
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
[
|
||||
CSharpSyntaxTree.ParseText(
|
||||
source,
|
||||
cancellationToken: TestContext.Current.CancellationToken
|
||||
),
|
||||
CSharpSyntaxTree.ParseText(
|
||||
stubs,
|
||||
cancellationToken: TestContext.Current.CancellationToken
|
||||
),
|
||||
],
|
||||
[
|
||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(
|
||||
System
|
||||
.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyName(
|
||||
new System.Reflection.AssemblyName("System.Runtime")
|
||||
)
|
||||
.Location
|
||||
),
|
||||
],
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
);
|
||||
|
||||
var errors = compilation.GetDiagnostics(TestContext.Current.CancellationToken)
|
||||
.Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
|
||||
var errors = compilation
|
||||
.GetDiagnostics(TestContext.Current.CancellationToken)
|
||||
.Where(d => d.Severity == DiagnosticSeverity.Error)
|
||||
.ToList();
|
||||
Assert.Empty(errors);
|
||||
}
|
||||
|
||||
@@ -162,7 +188,10 @@ public class AssetHandlesGeneratorTests
|
||||
[InlineData(@"D:\game\Assets\icon.png", @"D:\game\", "icon.png")]
|
||||
[InlineData(@"D:\game\Other\icon.png", @"D:\game", null)]
|
||||
public void ToAssetPath_WithProjectDir_RootsAtProjectAssetsFolder(
|
||||
string fullPath, string projectDir, string? expected)
|
||||
string fullPath,
|
||||
string projectDir,
|
||||
string? expected
|
||||
)
|
||||
{
|
||||
Assert.Equal(expected, AssetHandlesGenerator.ToAssetPath(fullPath, projectDir));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -16,5 +15,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,7 +14,15 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
public void Dispose() => Directory.Delete(_root, recursive: true);
|
||||
|
||||
/// <summary>Writes a PNG filled with one RGBA color.</summary>
|
||||
private void WritePng(string relativePath, int width, int height, byte r, byte g, byte b, byte a = 255)
|
||||
private void WritePng(
|
||||
string relativePath,
|
||||
int width,
|
||||
int height,
|
||||
byte r,
|
||||
byte g,
|
||||
byte b,
|
||||
byte a = 255
|
||||
)
|
||||
{
|
||||
var fullPath = Path.Combine(SourceDir, relativePath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
|
||||
@@ -28,25 +36,37 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
}
|
||||
|
||||
using var stream = File.Create(fullPath);
|
||||
new ImageWriter().WritePng(data, width, height, StbImageWriteSharp.ColorComponents.RedGreenBlueAlpha, stream);
|
||||
new ImageWriter().WritePng(
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
StbImageWriteSharp.ColorComponents.RedGreenBlueAlpha,
|
||||
stream
|
||||
);
|
||||
}
|
||||
|
||||
private AtlasBuildOptions Options(int groupDepth = 1, bool force = false) => new()
|
||||
{
|
||||
SourceDirectory = SourceDir,
|
||||
OutputDirectory = OutputDir,
|
||||
GroupDepth = groupDepth,
|
||||
MaxPageSize = 128,
|
||||
Padding = 2,
|
||||
Force = force,
|
||||
};
|
||||
private AtlasBuildOptions Options(int groupDepth = 1, bool force = false) =>
|
||||
new()
|
||||
{
|
||||
SourceDirectory = SourceDir,
|
||||
OutputDirectory = OutputDir,
|
||||
GroupDepth = groupDepth,
|
||||
MaxPageSize = 128,
|
||||
Padding = 2,
|
||||
Force = force,
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData("Terrain/Surfaces/Marsh.png", 1, "Terrain", "Terrain/Surfaces/Marsh")]
|
||||
[InlineData("Terrain/Surfaces/Marsh.png", 2, "Terrain.Surfaces", "Terrain/Surfaces/Marsh")]
|
||||
[InlineData("Terrain/Surfaces/Marsh.png", 0, "Root", "Terrain/Surfaces/Marsh")]
|
||||
[InlineData("loose.png", 3, "Root", "loose")]
|
||||
public void ClassifyPath_GroupsByDepth(string path, int depth, string expectedAtlas, string expectedKey)
|
||||
public void ClassifyPath_GroupsByDepth(
|
||||
string path,
|
||||
int depth,
|
||||
string expectedAtlas,
|
||||
string expectedKey
|
||||
)
|
||||
{
|
||||
var (atlas, key) = AtlasBuilder.ClassifyPath(path, depth, "Root");
|
||||
|
||||
@@ -67,13 +87,18 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
Assert.Equal(2, group.RegionCount);
|
||||
Assert.False(group.Skipped);
|
||||
|
||||
var metadata = AtlasMetadata.FromJson(File.ReadAllText(Path.Combine(OutputDir, "Terrain.atlas")));
|
||||
var metadata = AtlasMetadata.FromJson(
|
||||
File.ReadAllText(Path.Combine(OutputDir, "Terrain.atlas"))
|
||||
);
|
||||
Assert.Equal(["Terrain/Grass", "Terrain/Water"], metadata.Regions.Select(x => x.Key));
|
||||
|
||||
var grass = metadata.Regions.Single(x => x.Key == "Terrain/Grass");
|
||||
var page = metadata.Pages[grass.Page];
|
||||
using var stream = File.OpenRead(Path.Combine(OutputDir, page.File));
|
||||
var pixels = ImageResult.FromStream(stream, StbImageSharp.ColorComponents.RedGreenBlueAlpha);
|
||||
var pixels = ImageResult.FromStream(
|
||||
stream,
|
||||
StbImageSharp.ColorComponents.RedGreenBlueAlpha
|
||||
);
|
||||
|
||||
// Центральный пиксель региона должен быть цветом исходной картинки.
|
||||
var center = ((grass.Y + 8) * pixels.Width + grass.X + 8) * 4;
|
||||
@@ -101,7 +126,9 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
AtlasBuilder.Build(Options());
|
||||
|
||||
File.SetLastWriteTimeUtc(
|
||||
Path.Combine(SourceDir, "UI/button.png"), DateTime.UtcNow.AddMinutes(1));
|
||||
Path.Combine(SourceDir, "UI/button.png"),
|
||||
DateTime.UtcNow.AddMinutes(1)
|
||||
);
|
||||
var result = AtlasBuilder.Build(Options());
|
||||
|
||||
Assert.False(Assert.Single(result.Groups).Skipped);
|
||||
@@ -192,8 +219,27 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
Name = "Things.Pawn",
|
||||
PageSize = 2048,
|
||||
Padding = 2,
|
||||
Pages = [new AtlasPage { File = "Things.Pawn.atlas.0.png", Width = 256, Height = 128 }],
|
||||
Regions = [new AtlasRegion { Key = "Things/Pawn/Fox", Page = 0, X = 2, Y = 4, Width = 64, Height = 32 }],
|
||||
Pages =
|
||||
[
|
||||
new AtlasPage
|
||||
{
|
||||
File = "Things.Pawn.atlas.0.png",
|
||||
Width = 256,
|
||||
Height = 128,
|
||||
},
|
||||
],
|
||||
Regions =
|
||||
[
|
||||
new AtlasRegion
|
||||
{
|
||||
Key = "Things/Pawn/Fox",
|
||||
Page = 0,
|
||||
X = 2,
|
||||
Y = 4,
|
||||
Width = 64,
|
||||
Height = 32,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var parsed = AtlasMetadata.FromJson(metadata.ToJson());
|
||||
@@ -203,8 +249,10 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
var page = Assert.Single(parsed.Pages);
|
||||
Assert.Equal(("Things.Pawn.atlas.0.png", 256, 128), (page.File, page.Width, page.Height));
|
||||
var region = Assert.Single(parsed.Regions);
|
||||
Assert.Equal(("Things/Pawn/Fox", 0, 2, 4, 64, 32),
|
||||
(region.Key, region.Page, region.X, region.Y, region.Width, region.Height));
|
||||
Assert.Equal(
|
||||
("Things/Pawn/Fox", 0, 2, 4, 64, 32),
|
||||
(region.Key, region.Page, region.X, region.Y, region.Width, region.Height)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -213,10 +261,26 @@ public sealed class AtlasBuilderTests : IDisposable
|
||||
var metadata = new AtlasMetadata
|
||||
{
|
||||
Name = "Test",
|
||||
Pages = [new AtlasPage { File = "Test.atlas.0.png", Width = 64, Height = 64 }],
|
||||
Pages =
|
||||
[
|
||||
new AtlasPage
|
||||
{
|
||||
File = "Test.atlas.0.png",
|
||||
Width = 64,
|
||||
Height = 64,
|
||||
},
|
||||
],
|
||||
Regions =
|
||||
[
|
||||
new AtlasRegion { Key = "a/b", Page = 0, X = 2, Y = 2, Width = 10, Height = 12 },
|
||||
new AtlasRegion
|
||||
{
|
||||
Key = "a/b",
|
||||
Page = 0,
|
||||
X = 2,
|
||||
Y = 2,
|
||||
Width = 10,
|
||||
Height = 12,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Atlases\MrGameEng.Atlases.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -46,8 +46,10 @@ public class ShelfPackerTests
|
||||
var a = list[i];
|
||||
var b = list[j];
|
||||
var separated =
|
||||
a.X + a.Width + 2 <= b.X || b.X + b.Width + 2 <= a.X ||
|
||||
a.Y + a.Height + 2 <= b.Y || b.Y + b.Height + 2 <= a.Y;
|
||||
a.X + a.Width + 2 <= b.X
|
||||
|| b.X + b.Width + 2 <= a.X
|
||||
|| a.Y + a.Height + 2 <= b.Y
|
||||
|| b.Y + b.Height + 2 <= a.Y;
|
||||
Assert.True(separated, $"{a.Key} overlaps {b.Key} (padding included)");
|
||||
}
|
||||
}
|
||||
@@ -61,7 +63,10 @@ public class ShelfPackerTests
|
||||
var result = ShelfPacker.Pack(Squares(9, 100), maxPageSize: 256, padding: 2);
|
||||
|
||||
Assert.True(result.PageSizes.Count >= 3);
|
||||
Assert.Equal(Enumerable.Range(0, result.PageSizes.Count), result.Placements.Select(p => p.Page).Distinct().Order());
|
||||
Assert.Equal(
|
||||
Enumerable.Range(0, result.PageSizes.Count),
|
||||
result.Placements.Select(p => p.Page).Distinct().Order()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -80,7 +85,9 @@ public class ShelfPackerTests
|
||||
[Fact]
|
||||
public void Pack_IsDeterministic_RegardlessOfInputOrder()
|
||||
{
|
||||
var items = Squares(30, 20).Concat(Squares(10, 50).Select(i => i with { Key = "b" + i.Key })).ToList();
|
||||
var items = Squares(30, 20)
|
||||
.Concat(Squares(10, 50).Select(i => i with { Key = "b" + i.Key }))
|
||||
.ToList();
|
||||
var shuffled = items.AsEnumerable().Reverse().ToList();
|
||||
|
||||
var a = ShelfPacker.Pack(items, 128, 2);
|
||||
@@ -88,7 +95,8 @@ public class ShelfPackerTests
|
||||
|
||||
Assert.Equal(
|
||||
a.Placements.OrderBy(p => p.Key, StringComparer.Ordinal),
|
||||
b.Placements.OrderBy(p => p.Key, StringComparer.Ordinal));
|
||||
b.Placements.OrderBy(p => p.Key, StringComparer.Ordinal)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -115,11 +123,14 @@ public class ShelfPackerTests
|
||||
// 40² с padding на лимит 100: POT-округление дало бы 128 > лимита.
|
||||
var result = ShelfPacker.Pack(Squares(10, 40), maxPageSize: 100, padding: 2);
|
||||
|
||||
Assert.All(result.PageSizes, size =>
|
||||
{
|
||||
Assert.True(size.Width <= 100, $"page width {size.Width} > 100");
|
||||
Assert.True(size.Height <= 100, $"page height {size.Height} > 100");
|
||||
});
|
||||
Assert.All(
|
||||
result.PageSizes,
|
||||
size =>
|
||||
{
|
||||
Assert.True(size.Width <= 100, $"page width {size.Width} > 100");
|
||||
Assert.True(size.Height <= 100, $"page height {size.Height} > 100");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -168,7 +168,9 @@ public class CollisionWorldTests
|
||||
Assert.Equal(nearEntity, hit.Entity);
|
||||
Assert.Equal(25f, hit.Point.X, 1);
|
||||
|
||||
Assert.True(scene.World.Raycast(new Vector2(0f, 0f), new Vector2(100f, 0f), out hit, mask: 0b10));
|
||||
Assert.True(
|
||||
scene.World.Raycast(new Vector2(0f, 0f), new Vector2(100f, 0f), out hit, mask: 0b10)
|
||||
);
|
||||
Assert.Equal(farEntity, hit.Entity);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Collisions\MrGameEng.Collisions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -94,7 +94,7 @@ public class DevConsoleTests
|
||||
Assert.Equal("a 1", console.HistoryPrevious());
|
||||
Assert.Equal("a 1", console.HistoryPrevious()); // упёрлись в начало
|
||||
Assert.Equal("a 2", console.HistoryNext());
|
||||
Assert.Equal("", console.HistoryNext()); // за последним — пустая строка
|
||||
Assert.Equal("", console.HistoryNext()); // за последним — пустая строка
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.DevConsole\MrGameEng.DevConsole.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -26,7 +26,12 @@ public class CameraMathTests
|
||||
public void ScreenToWorld_RoundTripsWithWorldToScreen()
|
||||
{
|
||||
var camera = new Camera(new Vector2(123f, -45f), zoom: 1.5f, rotation: 0.3f);
|
||||
var state = CameraMath.Compute(camera, 1280, 720, new ViewportMapping(new Vector2(0f, 60f), 1.5f));
|
||||
var state = CameraMath.Compute(
|
||||
camera,
|
||||
1280,
|
||||
720,
|
||||
new ViewportMapping(new Vector2(0f, 60f), 1.5f)
|
||||
);
|
||||
|
||||
var screen = new Vector2(200f, 500f);
|
||||
var world = state.ScreenToWorld(screen);
|
||||
@@ -49,8 +54,18 @@ public class CameraMathTests
|
||||
[Fact]
|
||||
public void Rotation_ExpandsCullRectToCoverRotatedView()
|
||||
{
|
||||
var straight = CameraMath.Compute(new Camera(Vector2.Zero), 800, 600, ViewportMapping.Identity);
|
||||
var rotated = CameraMath.Compute(new Camera(Vector2.Zero, rotation: MathF.PI / 4f), 800, 600, ViewportMapping.Identity);
|
||||
var straight = CameraMath.Compute(
|
||||
new Camera(Vector2.Zero),
|
||||
800,
|
||||
600,
|
||||
ViewportMapping.Identity
|
||||
);
|
||||
var rotated = CameraMath.Compute(
|
||||
new Camera(Vector2.Zero, rotation: MathF.PI / 4f),
|
||||
800,
|
||||
600,
|
||||
ViewportMapping.Identity
|
||||
);
|
||||
|
||||
Assert.True(rotated.CullRect.Width > straight.CullRect.Width);
|
||||
Assert.True(rotated.CullRect.Height > straight.CullRect.Height);
|
||||
|
||||
@@ -22,7 +22,12 @@ public class CullingTests
|
||||
{
|
||||
var transform = Transform2D.At(new Vector2(50f, 50f));
|
||||
|
||||
var (center, _) = CullingMath.SpriteBoundingCircle(transform, 64f, 32f, new Vector2(32f, 16f));
|
||||
var (center, _) = CullingMath.SpriteBoundingCircle(
|
||||
transform,
|
||||
64f,
|
||||
32f,
|
||||
new Vector2(32f, 16f)
|
||||
);
|
||||
|
||||
Assert.Equal(new Vector2(50f, 50f), center);
|
||||
}
|
||||
@@ -41,7 +46,11 @@ public class CullingTests
|
||||
public void BoundingCircle_RegionOverload_MatchesSizeOverload_ForUniformScale()
|
||||
{
|
||||
var region = new Texture2DRegion(null!, new Rectangle(0, 0, 48, 24));
|
||||
var transform = new Transform2D(new Vector2(10f, 20f), rotation: 0.6f, scale: new Vector2(1.5f, 1.5f));
|
||||
var transform = new Transform2D(
|
||||
new Vector2(10f, 20f),
|
||||
rotation: 0.6f,
|
||||
scale: new Vector2(1.5f, 1.5f)
|
||||
);
|
||||
var origin = new Vector2(5f, 7f);
|
||||
|
||||
var (centerA, radiusA) = CullingMath.SpriteBoundingCircle(transform, 48f, 24f, origin);
|
||||
@@ -59,16 +68,20 @@ public class CullingTests
|
||||
var transform = new Transform2D(Vector2.Zero, scale: new Vector2(1f, 3f));
|
||||
|
||||
var (_, exact) = CullingMath.SpriteBoundingCircle(transform, 100f, 10f, Vector2.Zero);
|
||||
var (_, conservative) = CullingMath.SpriteBoundingCircle(in transform, region, Vector2.Zero);
|
||||
var (_, conservative) = CullingMath.SpriteBoundingCircle(
|
||||
in transform,
|
||||
region,
|
||||
Vector2.Zero
|
||||
);
|
||||
|
||||
Assert.True(conservative >= exact);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(50f, 50f, true)] // inside
|
||||
[InlineData(-4f, 50f, true)] // touching from the left (radius 5)
|
||||
[InlineData(-20f, 50f, false)] // far left
|
||||
[InlineData(50f, 130f, false)] // far below
|
||||
[InlineData(50f, 50f, true)] // inside
|
||||
[InlineData(-4f, 50f, true)] // touching from the left (radius 5)
|
||||
[InlineData(-20f, 50f, false)] // far left
|
||||
[InlineData(50f, 130f, false)] // far below
|
||||
public void CircleIntersectsRect_DetectsOverlap(float x, float y, bool expected)
|
||||
{
|
||||
var rect = new RectF(0f, 0f, 100f, 100f);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -31,7 +31,9 @@ public class SortKeyTests
|
||||
[InlineData(-0.5f, 0.5f)]
|
||||
public void DepthBits_PreserveFloatOrder(float smaller, float larger)
|
||||
{
|
||||
Assert.True(SpriteSortKey.DepthToSortableBits(smaller) < SpriteSortKey.DepthToSortableBits(larger));
|
||||
Assert.True(
|
||||
SpriteSortKey.DepthToSortableBits(smaller) < SpriteSortKey.DepthToSortableBits(larger)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -42,7 +42,8 @@ public class SpriteAnimationTests
|
||||
this.UseSpriteAnimation();
|
||||
Animated = Store.CreateEntity(
|
||||
new Sprite { Color = Color.White },
|
||||
new SpriteAnimator(Clip));
|
||||
new SpriteAnimator(Clip)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,9 +122,9 @@ public class SpriteBatcherTests
|
||||
Assert.Equal(3, accepted);
|
||||
Assert.Equal(1, batcher.LastChunkCulled);
|
||||
var order = batcher.Sort();
|
||||
Assert.Equal(1, batcher[order[0]].Layer); // сначала чанк 0...
|
||||
Assert.Equal(1, batcher[order[0]].Layer); // сначала чанк 0...
|
||||
Assert.Equal(2, batcher[order[1]].Layer);
|
||||
Assert.Equal(10, batcher[order[2]].Layer); // ...затем чанк 1 — стабильно
|
||||
Assert.Equal(10, batcher[order[2]].Layer); // ...затем чанк 1 — стабильно
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -7,11 +7,28 @@ namespace MrGameEng.Input.Tests;
|
||||
|
||||
public class InputManagerTests
|
||||
{
|
||||
private static MouseState Mouse(int x = 0, int y = 0, int wheel = 0, ButtonState left = ButtonState.Released) =>
|
||||
new(x, y, wheel, left, ButtonState.Released, ButtonState.Released, ButtonState.Released, ButtonState.Released);
|
||||
private static MouseState Mouse(
|
||||
int x = 0,
|
||||
int y = 0,
|
||||
int wheel = 0,
|
||||
ButtonState left = ButtonState.Released
|
||||
) =>
|
||||
new(
|
||||
x,
|
||||
y,
|
||||
wheel,
|
||||
left,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released,
|
||||
ButtonState.Released
|
||||
);
|
||||
|
||||
private static void Frame(InputManager input, KeyboardState keyboard = default, MouseState mouse = default) =>
|
||||
input.Apply(keyboard, mouse, GamePadState.Default);
|
||||
private static void Frame(
|
||||
InputManager input,
|
||||
KeyboardState keyboard = default,
|
||||
MouseState mouse = default
|
||||
) => input.Apply(keyboard, mouse, GamePadState.Default);
|
||||
|
||||
[Fact]
|
||||
public void KeyPressed_OnlyOnTheFrameItGoesDown()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Input\MrGameEng.Input.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -45,7 +45,8 @@ public sealed class DefDatabaseTests : IDisposable
|
||||
{ "defName": "Wolf", "label": "волк", "speed": 9 },
|
||||
{ "defName": "Bear", "speed": 6 }
|
||||
]}
|
||||
""");
|
||||
"""
|
||||
);
|
||||
|
||||
var database = LoadAnimals(mod);
|
||||
|
||||
@@ -65,15 +66,16 @@ public sealed class DefDatabaseTests : IDisposable
|
||||
{ "defName": "Hare", "parent": "BaseAnimal", "speed": 12 },
|
||||
{ "defName": "Snail", "parent": "BaseAnimal", "legs": 0, "tags": ["slow", "slimy"] }
|
||||
]}
|
||||
""");
|
||||
"""
|
||||
);
|
||||
|
||||
var database = LoadAnimals(mod);
|
||||
|
||||
var hare = database.Get<AnimalDef>("Hare");
|
||||
Assert.Equal(12f, hare.Speed); // своё поле победило
|
||||
Assert.Equal(["wild"], hare.Tags); // унаследовано
|
||||
Assert.Equal(12f, hare.Speed); // своё поле победило
|
||||
Assert.Equal(["wild"], hare.Tags); // унаследовано
|
||||
var snail = database.Get<AnimalDef>("Snail");
|
||||
Assert.Equal(5f, snail.Speed); // унаследовано
|
||||
Assert.Equal(5f, snail.Speed); // унаследовано
|
||||
Assert.Equal(0, snail.Legs);
|
||||
Assert.Equal(["slow", "slimy"], snail.Tags); // массив заменён целиком
|
||||
Assert.False(database.TryGet<AnimalDef>("BaseAnimal", out _)); // абстрактный не эмитится
|
||||
@@ -83,9 +85,11 @@ public sealed class DefDatabaseTests : IDisposable
|
||||
public void Load_LaterMod_ReplacesSameDefName()
|
||||
{
|
||||
var core = WriteDefsMod(
|
||||
"""{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 9, "tags": ["wild"] } ] }""");
|
||||
"""{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 9, "tags": ["wild"] } ] }"""
|
||||
);
|
||||
var patch = WriteDefsMod(
|
||||
"""{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 20 } ] }""");
|
||||
"""{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 20 } ] }"""
|
||||
);
|
||||
|
||||
var database = LoadAnimals(core, patch);
|
||||
|
||||
@@ -103,7 +107,8 @@ public sealed class DefDatabaseTests : IDisposable
|
||||
{ "defName": "A", "parent": "B" },
|
||||
{ "defName": "B", "parent": "A" }
|
||||
]}
|
||||
""");
|
||||
"""
|
||||
);
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => LoadAnimals(mod));
|
||||
Assert.Contains("Cyclic", exception.Message);
|
||||
@@ -112,7 +117,9 @@ public sealed class DefDatabaseTests : IDisposable
|
||||
[Fact]
|
||||
public void Load_UnknownParent_Throws()
|
||||
{
|
||||
var mod = WriteDefsMod("""{ "type": "Animal", "defs": [ { "defName": "X", "parent": "Ghost" } ] }""");
|
||||
var mod = WriteDefsMod(
|
||||
"""{ "type": "Animal", "defs": [ { "defName": "X", "parent": "Ghost" } ] }"""
|
||||
);
|
||||
|
||||
var exception = Assert.Throws<InvalidDataException>(() => LoadAnimals(mod));
|
||||
Assert.Contains("Ghost", exception.Message);
|
||||
|
||||
@@ -22,7 +22,11 @@ public sealed class LanguageManagerTests : IDisposable
|
||||
[Fact]
|
||||
public void Get_UsesCurrentLanguage_FallsBackToDefault_ThenKey()
|
||||
{
|
||||
var mod = WriteLanguageMod("en", "ui.json", """{ "hud.title": "Title", "hud.only-en": "English only" }""");
|
||||
var mod = WriteLanguageMod(
|
||||
"en",
|
||||
"ui.json",
|
||||
"""{ "hud.title": "Title", "hud.only-en": "English only" }"""
|
||||
);
|
||||
var ruDir = Path.Combine(mod.RootPath, "Languages", "ru");
|
||||
Directory.CreateDirectory(ruDir);
|
||||
File.WriteAllText(Path.Combine(ruDir, "ui.json"), """{ "hud.title": "Заголовок" }""");
|
||||
@@ -39,7 +43,11 @@ public sealed class LanguageManagerTests : IDisposable
|
||||
[Fact]
|
||||
public void Load_LaterMod_OverridesKey()
|
||||
{
|
||||
var core = WriteLanguageMod("en", "ui.json", """{ "hud.title": "Core", "hud.other": "Other" }""");
|
||||
var core = WriteLanguageMod(
|
||||
"en",
|
||||
"ui.json",
|
||||
"""{ "hud.title": "Core", "hud.other": "Other" }"""
|
||||
);
|
||||
var patch = WriteLanguageMod("en", "ui.json", """{ "hud.title": "Patched" }""");
|
||||
|
||||
var languages = new LanguageManager();
|
||||
|
||||
@@ -18,7 +18,8 @@ public sealed class ModLoaderTests : IDisposable
|
||||
var deps = string.Join(", ", dependencies.Select(d => $"\"{d}\""));
|
||||
File.WriteAllText(
|
||||
Path.Combine(aboutDir, "About.json"),
|
||||
$$"""{ "id": "{{id}}", "name": "{{id}} mod", "version": "1.0", "dependencies": [{{deps}}] }""");
|
||||
$$"""{ "id": "{{id}}", "name": "{{id}} mod", "version": "1.0", "dependencies": [{{deps}}] }"""
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Mods\MrGameEng.Mods.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,10 +8,7 @@ public class FlowFieldTests
|
||||
[Fact]
|
||||
public void Build_DistancesGrowFromGoal_DirectionsDescend()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".###.",
|
||||
".....");
|
||||
var grid = new TestGrid(".....", ".###.", ".....");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
@@ -25,7 +22,11 @@ public class FlowFieldTests
|
||||
{
|
||||
for (var x = 0; x < grid.Width; x++)
|
||||
{
|
||||
if (!grid.IsPassable(x, y) || !field.IsReachable(x, y) || field.DistanceAt(x, y) == 0f)
|
||||
if (
|
||||
!grid.IsPassable(x, y)
|
||||
|| !field.IsReachable(x, y)
|
||||
|| field.DistanceAt(x, y) == 0f
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -34,8 +35,10 @@ public class FlowFieldTests
|
||||
Assert.NotEqual(Vector2.Zero, direction);
|
||||
var nx = x + Math.Sign(MathF.Round(direction.X * 10f));
|
||||
var ny = y + Math.Sign(MathF.Round(direction.Y * 10f));
|
||||
Assert.True(field.DistanceAt(nx, ny) < field.DistanceAt(x, y),
|
||||
$"direction at ({x},{y}) does not descend");
|
||||
Assert.True(
|
||||
field.DistanceAt(nx, ny) < field.DistanceAt(x, y),
|
||||
$"direction at ({x},{y}) does not descend"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,10 +46,7 @@ public class FlowFieldTests
|
||||
[Fact]
|
||||
public void Build_UnreachablePocket_IsFlagged()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
"..#..",
|
||||
"..#..",
|
||||
"..#..");
|
||||
var grid = new TestGrid("..#..", "..#..", "..#..");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ namespace MrGameEng.Pathfinding.Tests;
|
||||
|
||||
public class GridPathfinderTests
|
||||
{
|
||||
private static List<Point> Path(IPathGrid grid, Point start, Point goal,
|
||||
private static List<Point> Path(
|
||||
IPathGrid grid,
|
||||
Point start,
|
||||
Point goal,
|
||||
PathAlgorithm algorithm = PathAlgorithm.AStar,
|
||||
GridConnectivity connectivity = GridConnectivity.Eight)
|
||||
GridConnectivity connectivity = GridConnectivity.Eight
|
||||
)
|
||||
{
|
||||
var pathfinder = new GridPathfinder(grid, connectivity);
|
||||
var path = new List<Point>();
|
||||
@@ -26,7 +30,10 @@ public class GridPathfinderTests
|
||||
{
|
||||
var dx = Math.Abs(path[i].X - path[i - 1].X);
|
||||
var dy = Math.Abs(path[i].Y - path[i - 1].Y);
|
||||
Assert.True(dx <= 1 && dy <= 1 && dx + dy > 0, $"non-adjacent step {path[i - 1]} -> {path[i]}");
|
||||
Assert.True(
|
||||
dx <= 1 && dy <= 1 && dx + dy > 0,
|
||||
$"non-adjacent step {path[i - 1]} -> {path[i]}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,10 +44,7 @@ public class GridPathfinderTests
|
||||
[InlineData(PathAlgorithm.BreadthFirst)]
|
||||
public void FindPath_OpenField_StraightLine(PathAlgorithm algorithm)
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".....",
|
||||
".....");
|
||||
var grid = new TestGrid(".....", ".....", ".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 1), new Point(4, 1), algorithm);
|
||||
|
||||
@@ -54,10 +58,7 @@ public class GridPathfinderTests
|
||||
[InlineData(PathAlgorithm.BreadthFirst)]
|
||||
public void FindPath_WallsForceDetour(PathAlgorithm algorithm)
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
"####.",
|
||||
".....");
|
||||
var grid = new TestGrid(".....", "####.", ".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 0), new Point(0, 2), algorithm);
|
||||
|
||||
@@ -68,10 +69,7 @@ public class GridPathfinderTests
|
||||
[Fact]
|
||||
public void FindPath_NoRoute_ReturnsFalse()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".#.",
|
||||
".#.",
|
||||
".#.");
|
||||
var grid = new TestGrid(".#.", ".#.", ".#.");
|
||||
var pathfinder = new GridPathfinder(grid);
|
||||
var path = new List<Point>();
|
||||
|
||||
@@ -92,9 +90,7 @@ public class GridPathfinderTests
|
||||
[Fact]
|
||||
public void FindPath_DiagonalNeverCutsCorners()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".#",
|
||||
"#.");
|
||||
var grid = new TestGrid(".#", "#.");
|
||||
var pathfinder = new GridPathfinder(grid, GridConnectivity.Eight);
|
||||
var path = new List<Point>();
|
||||
|
||||
@@ -105,12 +101,14 @@ public class GridPathfinderTests
|
||||
[Fact]
|
||||
public void FindPath_FourConnectivity_NoDiagonalSteps()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
"...",
|
||||
"...",
|
||||
"...");
|
||||
var grid = new TestGrid("...", "...", "...");
|
||||
|
||||
var path = Path(grid, new Point(0, 0), new Point(2, 2), connectivity: GridConnectivity.Four);
|
||||
var path = Path(
|
||||
grid,
|
||||
new Point(0, 0),
|
||||
new Point(2, 2),
|
||||
connectivity: GridConnectivity.Four
|
||||
);
|
||||
|
||||
Assert.Equal(5, path.Count); // манхэттен: 4 шага
|
||||
for (var i = 1; i < path.Count; i++)
|
||||
@@ -127,10 +125,7 @@ public class GridPathfinderTests
|
||||
public void FindPath_CostAware_AvoidsExpensiveTerrain(PathAlgorithm algorithm)
|
||||
{
|
||||
// Прямой путь через болото (цена 9) дороже обхода по краю.
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".999.",
|
||||
".....");
|
||||
var grid = new TestGrid(".....", ".999.", ".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 1), new Point(4, 1), algorithm);
|
||||
|
||||
@@ -140,10 +135,7 @@ public class GridPathfinderTests
|
||||
[Fact]
|
||||
public void FindPath_BreadthFirst_IgnoresCosts()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".999.",
|
||||
".....");
|
||||
var grid = new TestGrid(".....", ".999.", ".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 1), new Point(4, 1), PathAlgorithm.BreadthFirst);
|
||||
|
||||
@@ -153,10 +145,7 @@ public class GridPathfinderTests
|
||||
[Fact]
|
||||
public void FindPath_ReusedInstance_GivesCleanResults()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".###.",
|
||||
".....");
|
||||
var grid = new TestGrid(".....", ".###.", ".....");
|
||||
var pathfinder = new GridPathfinder(grid);
|
||||
var path = new List<Point>();
|
||||
|
||||
@@ -170,12 +159,7 @@ public class GridPathfinderTests
|
||||
[Fact]
|
||||
public void FindPath_AStarMatchesDijkstraCost()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
"..3..",
|
||||
".#3#.",
|
||||
"..3..",
|
||||
".###.",
|
||||
".....");
|
||||
var grid = new TestGrid("..3..", ".#3#.", "..3..", ".###.", ".....");
|
||||
var start = new Point(0, 0);
|
||||
var goal = new Point(4, 4);
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ public class GridResizeTests
|
||||
var pathfinder = new GridPathfinder(grid);
|
||||
grid.Width = 8;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => pathfinder.FindPath(new Point(0, 0), new Point(1, 1), []));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
pathfinder.FindPath(new Point(0, 0), new Point(1, 1), [])
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -38,7 +39,8 @@ public class GridResizeTests
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
grid.Height = 8;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => builder.Build([new Point(0, 0)], new FlowField()));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
builder.Build([new Point(0, 0)], new FlowField())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Pathfinding\MrGameEng.Pathfinding.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -15,5 +14,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Tilemaps\MrGameEng.Tilemaps.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -105,8 +105,17 @@ public class TilemapMathTests
|
||||
{
|
||||
var cull = new RectF(35f, 18f, 40f, 30f); // правый край 75, нижний 48
|
||||
|
||||
var visible = TilemapMath.VisibleCells(in cull, Vector2.Zero, 16f, 10, 10,
|
||||
out var x0, out var y0, out var x1, out var y1);
|
||||
var visible = TilemapMath.VisibleCells(
|
||||
in cull,
|
||||
Vector2.Zero,
|
||||
16f,
|
||||
10,
|
||||
10,
|
||||
out var x0,
|
||||
out var y0,
|
||||
out var x1,
|
||||
out var y1
|
||||
);
|
||||
|
||||
Assert.True(visible);
|
||||
Assert.Equal((2, 1, 4, 3), (x0, y0, x1, y1));
|
||||
@@ -117,8 +126,17 @@ public class TilemapMathTests
|
||||
{
|
||||
var cull = new RectF(0f, 0f, 64f, 64f);
|
||||
|
||||
var visible = TilemapMath.VisibleCells(in cull, new Vector2(-32f, -32f), 16f, 100, 100,
|
||||
out var x0, out var y0, out var x1, out var y1);
|
||||
var visible = TilemapMath.VisibleCells(
|
||||
in cull,
|
||||
new Vector2(-32f, -32f),
|
||||
16f,
|
||||
100,
|
||||
100,
|
||||
out var x0,
|
||||
out var y0,
|
||||
out var x1,
|
||||
out var y1
|
||||
);
|
||||
|
||||
Assert.True(visible);
|
||||
Assert.Equal((2, 2, 6, 6), (x0, y0, x1, y1));
|
||||
@@ -129,23 +147,41 @@ public class TilemapMathTests
|
||||
{
|
||||
var cull = new RectF(-1000f, -1000f, 5000f, 5000f);
|
||||
|
||||
var visible = TilemapMath.VisibleCells(in cull, Vector2.Zero, 16f, 8, 6,
|
||||
out var x0, out var y0, out var x1, out var y1);
|
||||
var visible = TilemapMath.VisibleCells(
|
||||
in cull,
|
||||
Vector2.Zero,
|
||||
16f,
|
||||
8,
|
||||
6,
|
||||
out var x0,
|
||||
out var y0,
|
||||
out var x1,
|
||||
out var y1
|
||||
);
|
||||
|
||||
Assert.True(visible);
|
||||
Assert.Equal((0, 0, 7, 5), (x0, y0, x1, y1));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(200f, 0f)] // справа от карты
|
||||
[InlineData(-200f, 0f)] // слева
|
||||
[InlineData(0f, 200f)] // ниже
|
||||
[InlineData(200f, 0f)] // справа от карты
|
||||
[InlineData(-200f, 0f)] // слева
|
||||
[InlineData(0f, 200f)] // ниже
|
||||
public void CameraOutsideMap_ReturnsFalse(float offsetX, float offsetY)
|
||||
{
|
||||
var cull = new RectF(offsetX, offsetY, 100f, 100f);
|
||||
|
||||
var visible = TilemapMath.VisibleCells(in cull, new Vector2(-150f, -150f), 16f, 8, 8,
|
||||
out _, out _, out _, out _);
|
||||
var visible = TilemapMath.VisibleCells(
|
||||
in cull,
|
||||
new Vector2(-150f, -150f),
|
||||
16f,
|
||||
8,
|
||||
8,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _
|
||||
);
|
||||
|
||||
Assert.False(visible);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user