Add MrGameEng.Pathfinding and MrGameEng.Collisions modules
CI / build-test (push) Failing after 1m5s
CI / build-test (push) Failing after 1m5s
Pathfinding (depends on Core only): GridPathfinder with A* (octile/
Manhattan heuristic), Dijkstra and BFS over a game-implemented
IPathGrid; 4/8 connectivity, diagonals never cut corners. FlowField +
FlowFieldBuilder (multi-source Dijkstra) give crowds O(1) steering per
agent per frame. All buffers are grid-sized once and invalidated by a
generation stamp - repeated queries allocate nothing and clear nothing.
Collisions (Graphics exception: Transform2D, RectF): Collider component
(circle/AABB, offset, two-way layer masks), CollisionWorld - a uniform
spatial hash on flat arrays rebuilt from scratch each tick (O(n) for
movers, zero alloc after warm-up, deterministic pair order), pair
collection, QueryAabb and closest-hit Raycast. scene.UseCollisions()
registers CollisionSystem after movement systems.
30 new tests (string-map mazes, cost weighting, corner cutting, flow
descent; pair/mask/query/raycast). Sample gains a PathfindingScene
('path' console command): click to set the goal, 250 agents follow the
flow field, the A* path is highlighted, colliding agents flash red.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b3415120c3
commit
a395e58458
@@ -0,0 +1,185 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Collisions.Tests;
|
||||
|
||||
public class CollisionWorldTests
|
||||
{
|
||||
private sealed class CollisionScene : Scene
|
||||
{
|
||||
public CollisionWorld World = null!;
|
||||
|
||||
protected override void OnLoad() => World = this.UseCollisions(cellSize: 32f);
|
||||
}
|
||||
|
||||
private static (EngineContext Context, CollisionScene Scene) CreateScene()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var scene = new CollisionScene();
|
||||
context.Scenes.Switch(scene);
|
||||
context.Scenes.Update(context.Clock); // применяет переключение и первый тик
|
||||
return (context, scene);
|
||||
}
|
||||
|
||||
private static Entity Spawn(Scene scene, Vector2 position, in Collider collider) =>
|
||||
scene.Store.CreateEntity(Transform2D.At(position), collider);
|
||||
|
||||
private static void Tick(EngineContext context)
|
||||
{
|
||||
context.Clock.Advance(0.016f);
|
||||
context.Scenes.Update(context.Clock);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverlappingCircles_ProduceOnePair()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
var a = Spawn(scene, new Vector2(0f, 0f), Collider.Circle(10f));
|
||||
var b = Spawn(scene, new Vector2(15f, 0f), Collider.Circle(10f));
|
||||
Spawn(scene, new Vector2(100f, 100f), Collider.Circle(10f)); // далёкий — без пар
|
||||
|
||||
Tick(context);
|
||||
|
||||
var pair = Assert.Single(scene.World.Pairs.ToArray());
|
||||
Assert.True((pair.A == a && pair.B == b) || (pair.A == b && pair.B == a));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeparatedCircles_NoPairs()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
Spawn(scene, new Vector2(0f, 0f), Collider.Circle(5f));
|
||||
Spawn(scene, new Vector2(11f, 0f), Collider.Circle(5f));
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.Equal(0, scene.World.Pairs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CircleAndBox_Overlap_DetectedBothWays()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
Spawn(scene, new Vector2(0f, 0f), Collider.Box(20f, 20f));
|
||||
Spawn(scene, new Vector2(14f, 0f), Collider.Circle(5f)); // касается правой грани
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.Equal(1, scene.World.Pairs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CornerCircle_DoesNotTouchBox()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
Spawn(scene, new Vector2(0f, 0f), Collider.Box(20f, 20f));
|
||||
// Угол бокса (10,10); круг r=5 в (16,16): расстояние до угла ~8.49 > 5.
|
||||
Spawn(scene, new Vector2(16f, 16f), Collider.Circle(5f));
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.Equal(0, scene.World.Pairs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LayerMasks_FilterPairs()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
var ghost = Collider.Circle(10f);
|
||||
ghost.Layer = 0b10;
|
||||
ghost.CollidesWith = 0b10; // призраки сталкиваются только с призраками
|
||||
|
||||
var wall = Collider.Circle(10f);
|
||||
wall.Layer = 0b01;
|
||||
wall.CollidesWith = 0b01;
|
||||
|
||||
Spawn(scene, new Vector2(0f, 0f), ghost);
|
||||
Spawn(scene, new Vector2(5f, 0f), wall);
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.Equal(0, scene.World.Pairs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovingApart_PairDisappearsNextTick()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
Spawn(scene, new Vector2(0f, 0f), Collider.Circle(10f));
|
||||
var mover = Spawn(scene, new Vector2(5f, 0f), Collider.Circle(10f));
|
||||
|
||||
Tick(context);
|
||||
Assert.Equal(1, scene.World.Pairs.Length);
|
||||
|
||||
mover.GetComponent<Transform2D>().Position = new Vector2(100f, 0f);
|
||||
Tick(context);
|
||||
Assert.Equal(0, scene.World.Pairs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeCluster_AllTouchingPairsFound_OnceEach()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
// Цепочка из 10 кругов: касаются только соседи → ровно 9 пар.
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
Spawn(scene, new Vector2(i * 18f, 0f), Collider.Circle(10f));
|
||||
}
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.Equal(9, scene.World.Pairs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QueryAabb_ReturnsOnlyEntitiesInArea()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
var inside = Spawn(scene, new Vector2(10f, 10f), Collider.Circle(5f));
|
||||
Spawn(scene, new Vector2(200f, 200f), Collider.Circle(5f));
|
||||
|
||||
Tick(context);
|
||||
|
||||
Span<Entity> results = new Entity[8];
|
||||
var found = scene.World.QueryAabb(new RectF(0f, 0f, 50f, 50f), results);
|
||||
|
||||
Assert.Equal(1, found);
|
||||
Assert.Equal(inside, results[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Raycast_HitsClosestCollider_AndRespectsMask()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
var near = Collider.Circle(5f);
|
||||
near.Layer = 0b01;
|
||||
var far = Collider.Circle(5f);
|
||||
far.Layer = 0b10;
|
||||
|
||||
var nearEntity = Spawn(scene, new Vector2(30f, 0f), near);
|
||||
var farEntity = Spawn(scene, new Vector2(60f, 0f), far);
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.True(scene.World.Raycast(new Vector2(0f, 0f), new Vector2(100f, 0f), out var hit));
|
||||
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.Equal(farEntity, hit.Entity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Raycast_Miss_ReturnsFalse()
|
||||
{
|
||||
var (context, scene) = CreateScene();
|
||||
Spawn(scene, new Vector2(0f, 50f), Collider.Box(10f, 10f));
|
||||
|
||||
Tick(context);
|
||||
|
||||
Assert.False(scene.World.Raycast(new Vector2(0f, 0f), new Vector2(100f, 0f), out _));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<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.Collisions\MrGameEng.Collisions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Pathfinding.Tests;
|
||||
|
||||
public class FlowFieldTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_DistancesGrowFromGoal_DirectionsDescend()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".###.",
|
||||
".....");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
builder.Build([new Point(0, 0)], field);
|
||||
|
||||
Assert.Equal(0f, field.DistanceAt(0, 0));
|
||||
Assert.True(field.DistanceAt(4, 2) > field.DistanceAt(1, 0));
|
||||
|
||||
// Из любой достижимой не-целевой клетки направление ведёт к клетке с меньшей дистанцией.
|
||||
for (var y = 0; y < grid.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < grid.Width; x++)
|
||||
{
|
||||
if (!grid.IsPassable(x, y) || !field.IsReachable(x, y) || field.DistanceAt(x, y) == 0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var direction = field.DirectionAt(x, y);
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_UnreachablePocket_IsFlagged()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
"..#..",
|
||||
"..#..",
|
||||
"..#..");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
builder.Build([new Point(0, 1)], field);
|
||||
|
||||
Assert.False(field.IsReachable(4, 1));
|
||||
Assert.Equal(Vector2.Zero, field.DirectionAt(4, 1));
|
||||
Assert.True(field.IsReachable(1, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_MultipleGoals_EachCellFlowsToNearest()
|
||||
{
|
||||
var grid = new TestGrid("..........");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
builder.Build([new Point(0, 0), new Point(9, 0)], field);
|
||||
|
||||
Assert.True(field.DirectionAt(2, 0).X < 0f); // ближе к левой цели
|
||||
Assert.True(field.DirectionAt(7, 0).X > 0f); // ближе к правой
|
||||
Assert.Equal(0f, field.DistanceAt(9, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_Rebuild_OverwritesPreviousField()
|
||||
{
|
||||
var grid = new TestGrid(".....");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
builder.Build([new Point(0, 0)], field);
|
||||
Assert.True(field.DirectionAt(4, 0).X < 0f);
|
||||
|
||||
builder.Build([new Point(4, 0)], field);
|
||||
Assert.True(field.DirectionAt(0, 0).X > 0f);
|
||||
Assert.Equal(0f, field.DistanceAt(4, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_NoValidGoals_EverythingUnreachable()
|
||||
{
|
||||
var grid = new TestGrid(".#.");
|
||||
var builder = new FlowFieldBuilder(grid);
|
||||
var field = new FlowField();
|
||||
|
||||
builder.Build([new Point(1, 0)], field); // цель — стена
|
||||
|
||||
Assert.False(field.IsReachable(0, 0));
|
||||
Assert.False(field.IsReachable(2, 0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Pathfinding.Tests;
|
||||
|
||||
public class GridPathfinderTests
|
||||
{
|
||||
private static List<Point> Path(IPathGrid grid, Point start, Point goal,
|
||||
PathAlgorithm algorithm = PathAlgorithm.AStar,
|
||||
GridConnectivity connectivity = GridConnectivity.Eight)
|
||||
{
|
||||
var pathfinder = new GridPathfinder(grid, connectivity);
|
||||
var path = new List<Point>();
|
||||
Assert.True(pathfinder.FindPath(start, goal, path, algorithm));
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void AssertValidPath(IPathGrid grid, List<Point> path, Point start, Point goal)
|
||||
{
|
||||
Assert.Equal(start, path[0]);
|
||||
Assert.Equal(goal, path[^1]);
|
||||
for (var i = 0; i < path.Count; i++)
|
||||
{
|
||||
Assert.True(grid.IsPassable(path[i].X, path[i].Y), $"impassable cell {path[i]}");
|
||||
if (i > 0)
|
||||
{
|
||||
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]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PathAlgorithm.AStar)]
|
||||
[InlineData(PathAlgorithm.Dijkstra)]
|
||||
[InlineData(PathAlgorithm.BreadthFirst)]
|
||||
public void FindPath_OpenField_StraightLine(PathAlgorithm algorithm)
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".....",
|
||||
".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 1), new Point(4, 1), algorithm);
|
||||
|
||||
AssertValidPath(grid, path, new Point(0, 1), new Point(4, 1));
|
||||
Assert.Equal(5, path.Count); // прямая, без лишних шагов
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PathAlgorithm.AStar)]
|
||||
[InlineData(PathAlgorithm.Dijkstra)]
|
||||
[InlineData(PathAlgorithm.BreadthFirst)]
|
||||
public void FindPath_WallsForceDetour(PathAlgorithm algorithm)
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
"####.",
|
||||
".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 0), new Point(0, 2), algorithm);
|
||||
|
||||
AssertValidPath(grid, path, new Point(0, 0), new Point(0, 2));
|
||||
Assert.Contains(new Point(4, 1), path); // единственный проход
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_NoRoute_ReturnsFalse()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".#.",
|
||||
".#.",
|
||||
".#.");
|
||||
var pathfinder = new GridPathfinder(grid);
|
||||
var path = new List<Point>();
|
||||
|
||||
Assert.False(pathfinder.FindPath(new Point(0, 0), new Point(2, 0), path));
|
||||
Assert.Empty(path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_StartEqualsGoal_SinglePoint()
|
||||
{
|
||||
var grid = new TestGrid("...");
|
||||
|
||||
var path = Path(grid, new Point(1, 0), new Point(1, 0));
|
||||
|
||||
Assert.Equal([new Point(1, 0)], path);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_DiagonalNeverCutsCorners()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".#",
|
||||
"#.");
|
||||
var pathfinder = new GridPathfinder(grid, GridConnectivity.Eight);
|
||||
var path = new List<Point>();
|
||||
|
||||
// Диагональ (0,0)->(1,1) зажата стенами — пути нет.
|
||||
Assert.False(pathfinder.FindPath(new Point(0, 0), new Point(1, 1), path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_FourConnectivity_NoDiagonalSteps()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
"...",
|
||||
"...",
|
||||
"...");
|
||||
|
||||
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++)
|
||||
{
|
||||
var dx = Math.Abs(path[i].X - path[i - 1].X);
|
||||
var dy = Math.Abs(path[i].Y - path[i - 1].Y);
|
||||
Assert.Equal(1, dx + dy);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PathAlgorithm.AStar)]
|
||||
[InlineData(PathAlgorithm.Dijkstra)]
|
||||
public void FindPath_CostAware_AvoidsExpensiveTerrain(PathAlgorithm algorithm)
|
||||
{
|
||||
// Прямой путь через болото (цена 9) дороже обхода по краю.
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".999.",
|
||||
".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 1), new Point(4, 1), algorithm);
|
||||
|
||||
Assert.DoesNotContain(path, p => grid.Cost(p.X, p.Y) > 1f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_BreadthFirst_IgnoresCosts()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".999.",
|
||||
".....");
|
||||
|
||||
var path = Path(grid, new Point(0, 1), new Point(4, 1), PathAlgorithm.BreadthFirst);
|
||||
|
||||
Assert.Equal(5, path.Count); // идёт напрямик через дорогие клетки
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_ReusedInstance_GivesCleanResults()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
".....",
|
||||
".###.",
|
||||
".....");
|
||||
var pathfinder = new GridPathfinder(grid);
|
||||
var path = new List<Point>();
|
||||
|
||||
Assert.True(pathfinder.FindPath(new Point(0, 0), new Point(4, 2), path));
|
||||
var first = path.ToArray();
|
||||
Assert.True(pathfinder.FindPath(new Point(0, 0), new Point(4, 2), path));
|
||||
|
||||
Assert.Equal(first, path); // generation-сброс не оставляет мусора между запросами
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindPath_AStarMatchesDijkstraCost()
|
||||
{
|
||||
var grid = new TestGrid(
|
||||
"..3..",
|
||||
".#3#.",
|
||||
"..3..",
|
||||
".###.",
|
||||
".....");
|
||||
var start = new Point(0, 0);
|
||||
var goal = new Point(4, 4);
|
||||
|
||||
var aStar = Path(grid, start, goal, PathAlgorithm.AStar);
|
||||
var dijkstra = Path(grid, start, goal, PathAlgorithm.Dijkstra);
|
||||
|
||||
Assert.Equal(PathCost(grid, dijkstra), PathCost(grid, aStar), 3);
|
||||
}
|
||||
|
||||
private static float PathCost(IPathGrid grid, List<Point> path)
|
||||
{
|
||||
var total = 0f;
|
||||
for (var i = 1; i < path.Count; i++)
|
||||
{
|
||||
var diagonal = path[i].X != path[i - 1].X && path[i].Y != path[i - 1].Y;
|
||||
total += (diagonal ? 1.4142135f : 1f) * grid.Cost(path[i].X, path[i].Y);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<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.Pathfinding\MrGameEng.Pathfinding.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
using MrGameEng.Pathfinding;
|
||||
|
||||
namespace MrGameEng.Pathfinding.Tests;
|
||||
|
||||
/// <summary>Грид из строк: '#' — стена, '.' — клетка с ценой 1, '2'..'9' — клетка с этой ценой.</summary>
|
||||
public sealed class TestGrid : IPathGrid
|
||||
{
|
||||
private readonly string[] _rows;
|
||||
|
||||
public TestGrid(params string[] rows) => _rows = rows;
|
||||
|
||||
public int Width => _rows[0].Length;
|
||||
|
||||
public int Height => _rows.Length;
|
||||
|
||||
public bool IsPassable(int x, int y) => _rows[y][x] != '#';
|
||||
|
||||
public float Cost(int x, int y)
|
||||
{
|
||||
var cell = _rows[y][x];
|
||||
return cell is >= '2' and <= '9' ? cell - '0' : 1f;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user