feat(starter/csharp): complete C# starter kit with docs, tests, and Docker verification
- 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>
This commit is contained in:
parent
6dd69f596d
commit
ca5b20b7b7
7 changed files with 408 additions and 4 deletions
|
|
@ -1 +1 @@
|
|||
467b7b67ea1a5b2deab9646070fd3096cb487f9f
|
||||
c7dfbffcc7eb9824c5f54fccb5ea0e0a2daa4fbf
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# Build stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
|
@ -6,7 +7,17 @@ RUN dotnet restore
|
|||
COPY Program.cs Grid.cs .
|
||||
RUN dotnet publish -c Release -o /out
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/runtime:9.0-alpine
|
||||
# Test stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS test
|
||||
WORKDIR /app/tests/GridTests
|
||||
COPY tests/GridTests/GridTests.csproj .
|
||||
RUN dotnet restore
|
||||
COPY tests/GridTests/GridTests.cs .
|
||||
COPY tests/GridTests/Grid.cs .
|
||||
RUN dotnet test
|
||||
|
||||
# Runtime stage - use ASP.NET Core runtime for web apps
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /out .
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ var app = builder.Build();
|
|||
|
||||
app.MapGet("/health", () => Results.Ok("OK"));
|
||||
|
||||
// --- Constants ---
|
||||
string[] Directions = ["N", "E", "S", "W"];
|
||||
|
||||
app.MapPost("/turn", (HttpContext ctx) =>
|
||||
{
|
||||
var signature = ctx.Request.Headers["X-ACB-Signature"].FirstOrDefault() ?? "";
|
||||
|
|
@ -73,8 +76,6 @@ app.Run();
|
|||
// --- Strategy ---
|
||||
// Replace this with your own logic!
|
||||
|
||||
string[] Directions = ["N", "E", "S", "W"];
|
||||
|
||||
List<Move> ComputeMoves(GameState state)
|
||||
{
|
||||
var rows = state.Config.Rows;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,27 @@ Dockerfile # Container build
|
|||
- `Grid.Neighbors(p, rows, cols)` — 8-directional neighbors with wrap
|
||||
- `Grid.Bfs(start, goal, passable, rows, cols)` — BFS pathfinding, returns path or `null`
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests for the Grid helper methods are provided in the `tests/GridTests/` directory.
|
||||
|
||||
```bash
|
||||
# Run tests locally
|
||||
cd tests/GridTests
|
||||
dotnet restore
|
||||
dotnet test
|
||||
|
||||
# Run tests with Docker
|
||||
docker build --target test -t my-bot-tests .
|
||||
docker run --rm my-bot-tests
|
||||
```
|
||||
|
||||
The test suite covers:
|
||||
- Toroidal distance calculations (Manhattan and Chebyshev)
|
||||
- Wrap-around behavior at grid boundaries
|
||||
- 8-directional neighbor enumeration
|
||||
- BFS pathfinding with obstacles
|
||||
|
||||
## Customization
|
||||
|
||||
Edit `ComputeMoves()` in `Program.cs` to implement your strategy. The `GameState` record provides:
|
||||
|
|
|
|||
79
starters/csharp/tests/GridTests/Grid.cs
Normal file
79
starters/csharp/tests/GridTests/Grid.cs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Grid utility functions for AI Code Battle.
|
||||
//
|
||||
// Provides toroidal distance calculations, neighbor enumeration,
|
||||
// and BFS pathfinding on a wrapping grid.
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
static class Grid
|
||||
{
|
||||
private static readonly (int dr, int dc)[] Offsets =
|
||||
{
|
||||
(-1, -1), (-1, 0), (-1, 1),
|
||||
(0, -1), (0, 1),
|
||||
(1, -1), (1, 0), (1, 1),
|
||||
};
|
||||
|
||||
/// Manhattan distance with wrap-around on a toroidal grid.
|
||||
public static int ToroidalManhattan(int r1, int c1, int r2, int c2, int rows, int cols)
|
||||
{
|
||||
int dr = Math.Min(Math.Abs(r1 - r2), rows - Math.Abs(r1 - r2));
|
||||
int dc = Math.Min(Math.Abs(c1 - c2), cols - Math.Abs(c1 - c2));
|
||||
return dr + dc;
|
||||
}
|
||||
|
||||
/// Chebyshev distance with wrap-around on a toroidal grid.
|
||||
public static int ToroidalChebyshev(int r1, int c1, int r2, int c2, int rows, int cols)
|
||||
{
|
||||
int dr = Math.Min(Math.Abs(r1 - r2), rows - Math.Abs(r1 - r2));
|
||||
int dc = Math.Min(Math.Abs(c1 - c2), cols - Math.Abs(c1 - c2));
|
||||
return Math.Max(dr, dc);
|
||||
}
|
||||
|
||||
/// 8-directional neighbors with wrap-around.
|
||||
public static Position[] Neighbors(Position p, int rows, int cols)
|
||||
{
|
||||
var result = new Position[8];
|
||||
for (int i = 0; i < Offsets.Length; i++)
|
||||
{
|
||||
result[i] = new Position
|
||||
{
|
||||
Row = (p.Row + Offsets[i].dr + rows) % rows,
|
||||
Col = (p.Col + Offsets[i].dc + cols) % cols,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// BFS pathfinding on a toroidal grid.
|
||||
/// Returns path (excluding start) or null if unreachable.
|
||||
public static List<Position>? Bfs(Position start, Position goal,
|
||||
Func<Position, bool> passable, int rows, int cols)
|
||||
{
|
||||
if (start.Row == goal.Row && start.Col == goal.Col)
|
||||
return [];
|
||||
|
||||
var visited = new HashSet<(int, int)> { (start.Row, start.Col) };
|
||||
var queue = new Queue<(Position pos, List<Position> path)>();
|
||||
queue.Enqueue((start, []));
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var (cur, path) = queue.Dequeue();
|
||||
foreach (var nb in Neighbors(cur, rows, cols))
|
||||
{
|
||||
var newPath = new List<Position>(path) { nb };
|
||||
if (nb.Row == goal.Row && nb.Col == goal.Col)
|
||||
return newPath;
|
||||
|
||||
var key = (nb.Row, nb.Col);
|
||||
if (!visited.Contains(key) && passable(nb))
|
||||
{
|
||||
visited.Add(key);
|
||||
queue.Enqueue((nb, newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
268
starters/csharp/tests/GridTests/GridTests.cs
Normal file
268
starters/csharp/tests/GridTests/GridTests.cs
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
24
starters/csharp/tests/GridTests/GridTests.csproj
Normal file
24
starters/csharp/tests/GridTests/GridTests.csproj
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Loading…
Add table
Reference in a new issue