245 lines
9.6 KiB
C#
245 lines
9.6 KiB
C#
|
|
using Chessistics.Engine.Events;
|
||
|
|
using Chessistics.Engine.Model;
|
||
|
|
using Chessistics.Tests.Helpers;
|
||
|
|
using Xunit;
|
||
|
|
|
||
|
|
namespace Chessistics.Tests.Simulation;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// End-to-end solvability tests: each test places pieces, runs the simulation,
|
||
|
|
/// and asserts VictoryEvent is produced — proving the level is winnable.
|
||
|
|
/// </summary>
|
||
|
|
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<CargoTransferredEvent>().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<DemandProgressEvent>().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<CargoTransferredEvent>().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<VictoryEvent>().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 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);
|
||
|
|
}
|
||
|
|
}
|