using Chessistics.Engine.Events; using Chessistics.Engine.Model; using Chessistics.Tests.Helpers; using Xunit; namespace Chessistics.Tests.Simulation; /// /// End-to-end solvability tests: each test places pieces, runs the simulation, /// and asserts VictoryEvent is produced — proving the level is winnable. /// public class SolvabilityTests { [Fact] public void SingleRook_ShortRelay_Victory() { // 3x1: Prod(0,0) — Rook(1,0↔2,0) — Demand(2,0) // Rook at (1,0) picks up from prod, at (2,0) is ON demand (not adjacent). // Delivery happens when rook returns to (1,0), adjacent to demand at (2,0). // Wait — (1,0) is adjacent to (2,0) ✓ so delivery from (1,0). var level = new BoardBuilder(3, 1) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithDemand(2, 0, "Depot", CargoType.Wood, 2, 30) .WithStock(PieceKind.Rook, 1) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Start(); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is VictoryEvent); } [Fact] public void ThreePieceChain_SharedRelayPoints_Victory() { // 5x2: three rooks form a chain with shared relay points. // Prod(0,0) — A(1,0↔2,0) — B(2,0↔3,0) — C(3,0↔4,0) — Demand(4,0) // Pieces share cells (2,0) and (3,0) but never collide: // Odd turns: A@(2,0) B@(3,0) C@(4,0) // Even turns: A@(1,0) B@(2,0) C@(3,0) var level = new BoardBuilder(5, 2) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40) .WithStock(PieceKind.Rook, 3) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Place(PieceKind.Rook, (2, 0), (3, 0)); sim.Place(PieceKind.Rook, (3, 0), (4, 0)); sim.Start(); var allEvents = sim.StepN(30); Assert.Contains(allEvents, e => e is VictoryEvent); // Verify cargo actually traversed the chain (not just a shortcut) Assert.True( allEvents.OfType().Count() >= 4, "Expected at least 4 cargo transfers across the 3-piece chain"); } [Fact] public void TwoDemands_SingleSource_BothSatisfied() { // 4x3: one production feeds two demands via two rooks. // Prod(0,0) at origin. // D1(2,0) along row 0, D2(0,2) along col 0. // Rook A(1,0↔2,0): picks up at (1,0), delivers to D1 from (1,0). // Rook B(0,1↔0,2): picks up at (0,1), delivers to D2 from (0,1). // Both rooks compete for the same buffer; A gets priority (placed first). var level = new BoardBuilder(4, 3) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithDemand(2, 0, "Depot Royal", CargoType.Wood, 2, 30) .WithDemand(0, 2, "Caserne", CargoType.Wood, 2, 30) .WithStock(PieceKind.Rook, 2) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Place(PieceKind.Rook, (0, 1), (0, 2)); sim.Start(); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is VictoryEvent); // Both demands must have received progress events var demandProgress = allEvents.OfType().ToList(); Assert.Contains(demandProgress, dp => dp.DemandCell == new Coords(2, 0) && dp.Current == dp.Required); Assert.Contains(demandProgress, dp => dp.DemandCell == new Coords(0, 2) && dp.Current == dp.Required); } [Fact] public void TwoCargoTypes_ParallelRoutes_Victory() { // 4x2: two independent production→demand chains, one Wood, one Stone. // Row 0: Prod_Wood(0,0) — Rook A(1,0↔2,0) — Demand_Wood(3,0) // Row 1: Prod_Stone(0,1) — Rook B(1,1↔2,1) — Demand_Stone(3,1) // Proves two cargo types flow independently to their matching demands. var level = new BoardBuilder(4, 2) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithProduction(0, 1, "Carriere", CargoType.Stone, 1) .WithDemand(3, 0, "Depot Royal", CargoType.Wood, 2, 30) .WithDemand(3, 1, "Forge", CargoType.Stone, 2, 30) .WithStock(PieceKind.Rook, 2) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Place(PieceKind.Rook, (1, 1), (2, 1)); sim.Start(); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is VictoryEvent); // Verify no wrong-type delivery (Wood to Stone demand or vice-versa) var transfers = allEvents.OfType().ToList(); foreach (var t in transfers.Where(t => t.To == new Coords(3, 0))) Assert.Equal(CargoType.Wood, t.Type); foreach (var t in transfers.Where(t => t.To == new Coords(3, 1))) Assert.Equal(CargoType.Stone, t.Type); } [Fact] public void Bishop_DiagonalRelay_Victory() { // 4x3: bishop provides the diagonal link in a two-piece chain. // Prod(0,0), Demand(2,1). // Rook(0,1↔0,0): at (0,1) picks up from prod. // Bishop(1,1↔2,2): at (1,1) receives from rook, at (2,2) delivers to demand (2,1). // Even turns: Rook@(0,1), Bishop@(1,1) — adjacent, transfer. // Odd turns: Rook@(0,0), Bishop@(2,2) — bishop delivers. var level = new BoardBuilder(4, 3) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithDemand(2, 1, "Depot", CargoType.Wood, 2, 30) .WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Bishop, 1) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (0, 1), (0, 0)); sim.Place(PieceKind.Bishop, (1, 1), (2, 2)); sim.Start(); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is VictoryEvent); } [Fact] public void Knight_JumpsWall_Victory() { // 5x3: a wall blocks the direct path, knight jumps over it. // Prod(0,0), Demand(4,0). // Walls: full column 2 — (2,0), (2,1), (2,2). // Rook(1,0↔1,1): at (1,0) picks up from prod. // Knight(1,1↔3,0): L-shape (+2,-1) jumps over wall, at (3,0) delivers to demand (4,0). // Even turns: Rook@(1,0), Knight@(1,1) — adjacent, transfer. // Odd turns: Knight@(3,0), adjacent to demand — delivers. var level = new BoardBuilder(5, 3) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithDemand(4, 0, "Depot", CargoType.Wood, 2, 30) .WithWall(2, 0).WithWall(2, 1).WithWall(2, 2) .WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Knight, 1) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (1, 1)); sim.Place(PieceKind.Knight, (1, 1), (3, 0)); sim.Start(); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is VictoryEvent); // Verify the knight actually moved across the wall Assert.Contains(allEvents, e => e is PieceMovedEvent m && m.To == new Coords(3, 0)); } [Fact] public void Victory_ReportsCorrectMetrics() { // 3x1: single rook relay, verify PiecesUsed, TurnsTaken, CellsOccupied. var level = new BoardBuilder(3, 1) .WithProduction(0, 0, "P", CargoType.Wood, 1) .WithDemand(2, 0, "D", CargoType.Wood, 2, 30) .WithStock(PieceKind.Rook, 1) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Start(); var allEvents = sim.StepN(20); var victory = allEvents.OfType().FirstOrDefault(); Assert.NotNull(victory); Assert.Equal(1, victory.Metrics.PiecesUsed); Assert.True(victory.Metrics.TurnsTaken > 0); Assert.Equal(2, victory.Metrics.CellsOccupied); // cells (1,0) and (2,0) } [Fact] public void NoCollision_WithSharedRelayPoints() { // Two rooks sharing a relay point never collide. // A(1,0↔2,0), B(2,0↔3,0) — share cell (2,0) but occupy it on alternate turns. var level = new BoardBuilder(5, 2) .WithProduction(0, 0, "P", CargoType.Wood, 1) .WithDemand(4, 0, "D", CargoType.Wood, 1, 40) .WithStock(PieceKind.Rook, 2) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Place(PieceKind.Rook, (2, 0), (3, 0)); sim.Start(); var allEvents = sim.StepN(20); Assert.DoesNotContain(allEvents, e => e is CollisionDetectedEvent); } [Fact] public void CargoFilter_AutoAssigned_PreventsContamination() { // 4x1: two productions side by side, two routes with adjacent pieces. // Prod_Wood(0,0), Prod_Stone(3,0) // Rook A(1,0↔2,0) — adjacent to both prods on alternating turns. // Without CargoFilter, A would pick up both types randomly. // With CargoFilter, A's start (1,0) is adjacent to prod_Wood(0,0), // so A is filtered to Wood and ignores Stone. var level = new BoardBuilder(4, 1) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithProduction(3, 0, "Carriere", CargoType.Stone, 1) .WithDemand(2, 0, "D_Wood", CargoType.Wood, 2, 20) .WithStock(PieceKind.Rook, 1) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); // Verify CargoFilter was auto-assigned var snapshot = sim.Snapshot; Assert.Equal(CargoType.Wood, snapshot.Pieces[0].CargoFilter); sim.Start(); var allEvents = sim.StepN(20); // Piece should only carry Wood — never Stone var transfers = allEvents.OfType().ToList(); Assert.All(transfers, t => Assert.Equal(CargoType.Wood, t.Type)); Assert.Contains(allEvents, e => e is VictoryEvent); } [Fact] public void CargoFilter_PropagatesThroughChain() { // 5x2: chain of 3 rooks, first adjacent to Wood production. // All should inherit Wood filter via relay chain propagation. var level = new BoardBuilder(5, 2) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1) .WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40) .WithStock(PieceKind.Rook, 3) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); // adj to prod → Wood sim.Place(PieceKind.Rook, (2, 0), (3, 0)); // shares (2,0) → inherits Wood sim.Place(PieceKind.Rook, (3, 0), (4, 0)); // shares (3,0) → inherits Wood var snapshot = sim.Snapshot; Assert.Equal(CargoType.Wood, snapshot.Pieces[0].CargoFilter); Assert.Equal(CargoType.Wood, snapshot.Pieces[1].CargoFilter); Assert.Equal(CargoType.Wood, snapshot.Pieces[2].CargoFilter); } [Fact] public void StepFromEdit_AutoStartsSimulation() { // Stepping from Edit phase should auto-start without needing Start command. var level = new BoardBuilder(3, 1) .WithProduction(0, 0, "P", CargoType.Wood, 1) .WithDemand(2, 0, "D", CargoType.Wood, 1, 30) .WithStock(PieceKind.Rook, 1) .Build(); var sim = SimHelper.FromLevel(level); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); // No Start() — step directly from Edit var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is TurnStartedEvent); Assert.Contains(allEvents, e => e is VictoryEvent); } }