- README.md with comprehensive setup/compile/test instructions - Unit tests for Grid helper methods (toroidal distance, neighbors, BFS) - Dockerfile with multi-stage build (builder, test, runtime stages) - Verified: docker build --target test passes, HTTP endpoints work - C# starter already indexed in web/src/pages/docs.ts Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
268 lines
7.8 KiB
C#
268 lines
7.8 KiB
C#
using Xunit;
|
|
|
|
// Minimal types needed for Grid tests
|
|
record Position
|
|
{
|
|
public int Row { get; init; }
|
|
public int Col { get; init; }
|
|
}
|
|
|
|
public class GridTests
|
|
{
|
|
[Fact]
|
|
public void ToroidalManhattan_SamePosition_ReturnsZero()
|
|
{
|
|
int result = Grid.ToroidalManhattan(5, 5, 5, 5, 10, 10);
|
|
Assert.Equal(0, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalManhattan_AdacentNoWrap_ReturnsOne()
|
|
{
|
|
int result = Grid.ToroidalManhattan(5, 5, 5, 6, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalManhattan_WrapAroundRows_ShortestPath()
|
|
{
|
|
// On a 10x10 grid, distance from row 0 to row 9 should be 1 (wrap) not 9
|
|
int result = Grid.ToroidalManhattan(0, 5, 9, 5, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalManhattan_WrapAroundCols_ShortestPath()
|
|
{
|
|
// On a 10x10 grid, distance from col 0 to col 9 should be 1 (wrap) not 9
|
|
int result = Grid.ToroidalManhattan(5, 0, 5, 9, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalManhattan_DiagonalNoWrap_ReturnsTwo()
|
|
{
|
|
int result = Grid.ToroidalManhattan(5, 5, 6, 6, 10, 10);
|
|
Assert.Equal(2, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalManhattan_OppositeCorners_WrapPath()
|
|
{
|
|
// On a 10x10 grid, (0,0) to (9,9): best is wrap both ways = 1 + 1 = 2
|
|
int result = Grid.ToroidalManhattan(0, 0, 9, 9, 10, 10);
|
|
Assert.Equal(2, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalChebyshev_SamePosition_ReturnsZero()
|
|
{
|
|
int result = Grid.ToroidalChebyshev(5, 5, 5, 5, 10, 10);
|
|
Assert.Equal(0, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalChebyshev_AdacentNoWrap_ReturnsOne()
|
|
{
|
|
int result = Grid.ToroidalChebyshev(5, 5, 5, 6, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalChebyshev_DiagonalNoWrap_ReturnsOne()
|
|
{
|
|
int result = Grid.ToroidalChebyshev(5, 5, 6, 6, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalChebyshev_WrapAroundRows_ShortestPath()
|
|
{
|
|
int result = Grid.ToroidalChebyshev(0, 5, 9, 5, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalChebyshev_WrapAroundCols_ShortestPath()
|
|
{
|
|
int result = Grid.ToroidalChebyshev(5, 0, 5, 9, 10, 10);
|
|
Assert.Equal(1, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToroidalChebyshev_KnightMoveNoWrap_ReturnsTwo()
|
|
{
|
|
int result = Grid.ToroidalChebyshev(5, 5, 7, 6, 10, 10);
|
|
Assert.Equal(2, result);
|
|
}
|
|
|
|
[Fact]
|
|
public void Neighbors_ReturnsEightPositions()
|
|
{
|
|
var pos = new Position { Row = 5, Col = 5 };
|
|
var neighbors = Grid.Neighbors(pos, 10, 10);
|
|
Assert.Equal(8, neighbors.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void Neighbors_CornerPosition_WrapsCorrectly()
|
|
{
|
|
var pos = new Position { Row = 0, Col = 0 };
|
|
var neighbors = Grid.Neighbors(pos, 10, 10);
|
|
|
|
// Should include wrap-around positions
|
|
Assert.Contains(neighbors, n => n.Row == 9 && n.Col == 9); // NW wrap
|
|
Assert.Contains(neighbors, n => n.Row == 9 && n.Col == 0); // N wrap
|
|
Assert.Contains(neighbors, n => n.Row == 9 && n.Col == 1); // NE wrap
|
|
Assert.Contains(neighbors, n => n.Row == 0 && n.Col == 9); // W wrap
|
|
Assert.Contains(neighbors, n => n.Row == 0 && n.Col == 1); // E
|
|
Assert.Contains(neighbors, n => n.Row == 1 && n.Col == 9); // SW wrap
|
|
Assert.Contains(neighbors, n => n.Row == 1 && n.Col == 0); // S
|
|
Assert.Contains(neighbors, n => n.Row == 1 && n.Col == 1); // SE
|
|
}
|
|
|
|
[Fact]
|
|
public void Neighbors_EdgePosition_WrapsCorrectly()
|
|
{
|
|
var pos = new Position { Row = 0, Col = 5 };
|
|
var neighbors = Grid.Neighbors(pos, 10, 10);
|
|
|
|
// Should wrap on row axis only
|
|
Assert.Contains(neighbors, n => n.Row == 9);
|
|
Assert.Contains(neighbors, n => n.Row == 1);
|
|
}
|
|
|
|
[Fact]
|
|
public void Neighbors_AllNeighborsAreDistinct()
|
|
{
|
|
var pos = new Position { Row = 5, Col = 5 };
|
|
var neighbors = Grid.Neighbors(pos, 10, 10);
|
|
var distinct = neighbors.DistinctBy(n => (n.Row, n.Col)).ToArray();
|
|
Assert.Equal(8, distinct.Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_StartEqualsGoal_ReturnsEmptyPath()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 5 };
|
|
bool alwaysPassable(Position p) => true;
|
|
|
|
var path = Grid.Bfs(start, goal, alwaysPassable, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
Assert.Empty(path);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_AdacentPosition_ReturnsSingleStep()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 6 };
|
|
bool alwaysPassable(Position p) => true;
|
|
|
|
var path = Grid.Bfs(start, goal, alwaysPassable, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
Assert.Single(path);
|
|
Assert.Equal(5, path[0].Row);
|
|
Assert.Equal(6, path[0].Col);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_UnobstructedPath_ReturnsShortestPath()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 8 };
|
|
bool alwaysPassable(Position p) => true;
|
|
|
|
var path = Grid.Bfs(start, goal, alwaysPassable, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
Assert.Equal(3, path.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_WrappedPath_FindsShortestRoute()
|
|
{
|
|
var start = new Position { Row = 0, Col = 0 };
|
|
var goal = new Position { Row = 9, Col = 9 };
|
|
bool alwaysPassable(Position p) => true;
|
|
|
|
var path = Grid.Bfs(start, goal, alwaysPassable, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
// With 8-directional movement, NW from (0,0) wraps to (9,9) in 1 step
|
|
Assert.Single(path);
|
|
Assert.Equal(9, path[0].Row);
|
|
Assert.Equal(9, path[0].Col);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_AllBlocked_ReturnsNull()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 8 };
|
|
bool neverPassable(Position p) => false;
|
|
|
|
var path = Grid.Bfs(start, goal, neverPassable, 10, 10);
|
|
|
|
Assert.Null(path);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_GoalBlocked_ReturnsNull()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 8 };
|
|
bool goalBlocked(Position p) => p.Row == 5 && p.Col == 8;
|
|
|
|
var path = Grid.Bfs(start, goal, goalBlocked, 10, 10);
|
|
|
|
Assert.Null(path);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_NavigatesAroundObstacle()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 7 };
|
|
|
|
// Block direct path
|
|
bool hasWall(Position p) => !(p.Row == 5 && p.Col == 6);
|
|
|
|
var path = Grid.Bfs(start, goal, hasWall, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
Assert.NotEmpty(path);
|
|
Assert.Equal(5, path[^1].Row);
|
|
Assert.Equal(7, path[^1].Col);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_PathDoesNotIncludeStart()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 5, Col = 7 };
|
|
bool alwaysPassable(Position p) => true;
|
|
|
|
var path = Grid.Bfs(start, goal, alwaysPassable, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
Assert.DoesNotContain(path, p => p.Row == 5 && p.Col == 5);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bfs_PathEndsAtGoal()
|
|
{
|
|
var start = new Position { Row = 5, Col = 5 };
|
|
var goal = new Position { Row = 7, Col = 8 };
|
|
bool alwaysPassable(Position p) => true;
|
|
|
|
var path = Grid.Bfs(start, goal, alwaysPassable, 10, 10);
|
|
|
|
Assert.NotNull(path);
|
|
Assert.Equal(7, path[^1].Row);
|
|
Assert.Equal(8, path[^1].Col);
|
|
}
|
|
}
|