Chessistics/chessistics-tests/Simulation/FullLevelTests.cs
Samuel Bouchet 3120d9835e Phase 2: cargo-type aware transfers via CargoFilter
Add CargoFilter property to PieceState, auto-assigned at placement
by tracing relay chain back to production. TransferResolver now
enforces cargo-type filtering and uses forward-direction sorting
with cargo-aware distance calculations. Prevents cross-route
contamination on multi-cargo boards.

Level 3 restored to dual-cargo (Wood+Stone) with correct 10R+2K stock.
Two new solvability tests validate filter auto-assignment and chain
propagation. All 60 tests green.
2026-04-10 15:35:37 +02:00

135 lines
5.2 KiB
C#

using Chessistics.Engine.Events;
using Chessistics.Engine.Model;
using Chessistics.Tests.Helpers;
using Xunit;
namespace Chessistics.Tests.Simulation;
public class FullLevelTests
{
[Fact]
public void Level1_PremierConvoi_Victory()
{
// GDD Level 1: 4x4, Scierie(0,0) → Depot(3,0), 3 Rooks
// Solution: single rook relay at (1,0)↔(2,0)
var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30)
.WithStock(PieceKind.Rook, 3)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Start();
var allEvents = sim.StepN(30);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Level2_DeuxClients_Victory()
{
// GDD Level 2: 6x6, Scierie(0,0), Depot Royal(5,0), Caserne(5,4)
// Stock: 6 Rooks + 1 Bishop (fixed from GDD's 4R+1B — insufficient)
//
// Solution requires two routes from single source:
// Route 1 → (5,0): A(1,0↔2,0), B(2,0↔4,0)
// Route 2 → (5,4): C(0,1↔0,2), D(0,2↔2,2), E(2,2↔3,2),
// Bishop(3,2↔4,3), G(4,3↔5,3)
// Total needed: 6 Rooks + 1 Bishop
var level = new BoardBuilder(6, 6)
.WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(5, 0, "Depot Royal", CargoType.Wood, 2, 50)
.WithDemand(5, 4, "Caserne", CargoType.Wood, 2, 50)
.WithStock(PieceKind.Rook, 6)
.WithStock(PieceKind.Bishop, 1)
.Build();
var sim = SimHelper.FromLevel(level);
// Route 1: bottom row → demand (5,0)
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Place(PieceKind.Rook, (2, 0), (4, 0));
// Route 2: up then right → demand (5,4)
sim.Place(PieceKind.Rook, (0, 1), (0, 2));
sim.Place(PieceKind.Rook, (0, 2), (2, 2));
// 5th rook — stock exhausted at 4!
var events5 = sim.Place(PieceKind.Rook, (2, 2), (3, 2));
Assert.DoesNotContain(events5, e => e is PlacementRejectedEvent);
sim.Place(PieceKind.Bishop, (3, 2), (4, 3));
// 6th rook needed but only 4 in stock
var events6 = sim.Place(PieceKind.Rook, (4, 3), (5, 3));
Assert.DoesNotContain(events6, e => e is PlacementRejectedEvent);
sim.Start();
var allEvents = sim.StepN(60);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Level3_LeCol_Victory()
{
// GDD Level 3: 6x6, L-shaped wall, 2 cargo types, knights jump obstacle
// Stock: 8 Rooks + 2 Knights (fixed from GDD's 4R+1B+2K)
//
// CargoFilter (Phase 2) prevents cross-route contamination:
// pieces auto-inherit their production's cargo type via relay chain.
//
// Route Wood (0,0→5,5): R1(0,1↔1,1), K1(1,1↔3,2),
// R2(3,2↔4,2), R3(4,2↔5,2), R4(5,2↔5,3), R5(5,3↔5,4)
// Route Stone (5,0→0,5): S1(4,0↔3,0), S2(3,0↔2,0),
// K2(2,0↔1,2), S3(1,2↔1,3), S4(1,3↔1,4), S5(1,4↔0,4)
// Total: 10 Rooks + 2 Knights
var level = new BoardBuilder(6, 6)
.WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithProduction(5, 0, "Carriere", CargoType.Stone, 2)
.WithDemand(5, 5, "Depot Royal", CargoType.Wood, 2, 60)
.WithDemand(0, 5, "Forge", CargoType.Stone, 2, 60)
.WithWall(2, 2).WithWall(2, 3).WithWall(2, 4).WithWall(3, 4).WithWall(4, 4)
.WithStock(PieceKind.Rook, 10)
.WithStock(PieceKind.Knight, 2)
.Build();
var sim = SimHelper.FromLevel(level);
// Route Wood: prod(0,0) → demand(5,5)
sim.Place(PieceKind.Rook, (0, 1), (1, 1));
sim.Place(PieceKind.Knight, (1, 1), (3, 2));
sim.Place(PieceKind.Rook, (3, 2), (4, 2));
sim.Place(PieceKind.Rook, (4, 2), (5, 2));
sim.Place(PieceKind.Rook, (5, 2), (5, 3));
sim.Place(PieceKind.Rook, (5, 3), (5, 4));
// Route Stone: prod(5,0) → demand(0,5)
sim.Place(PieceKind.Rook, (4, 0), (3, 0));
sim.Place(PieceKind.Rook, (3, 0), (2, 0));
sim.Place(PieceKind.Knight, (2, 0), (1, 2));
sim.Place(PieceKind.Rook, (1, 2), (1, 3));
sim.Place(PieceKind.Rook, (1, 3), (1, 4));
sim.Place(PieceKind.Rook, (1, 4), (0, 4));
sim.Start();
var allEvents = sim.StepN(80);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Level1_InsufficientPieces_NoVictory()
{
var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(3, 3, "Depot Royal", CargoType.Wood, 3, 5)
.WithStock(PieceKind.Rook, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 1), (2, 1));
sim.Start();
var allEvents = sim.StepN(8);
Assert.DoesNotContain(allEvents, e => e is VictoryEvent);
Assert.Contains(allEvents, e => e is DeadlineExpiredEvent);
}
}