@
CI / build-test (push) Failing after 1m8s

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:
Leonid Pershin
2026-06-12 07:19:10 +03:00
parent 4ae730cafa
commit fd6343bd09
96 changed files with 2066 additions and 589 deletions
@@ -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);
}
}
+168
View File
@@ -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);
+20 -7
View File
@@ -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>
+16 -9
View File
@@ -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();
+2 -1
View File
@@ -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>
+47 -11
View File
@@ -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);
}