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:
jedarden 2026-05-04 01:26:25 -04:00
parent 6dd69f596d
commit ca5b20b7b7
7 changed files with 408 additions and 4 deletions

View file

@ -1 +1 @@
467b7b67ea1a5b2deab9646070fd3096cb487f9f
c7dfbffcc7eb9824c5f54fccb5ea0e0a2daa4fbf

View file

@ -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 .

View file

@ -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;

View file

@ -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:

View 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;
}
}

View 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);
}
}

View 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>