using Chessistics.Engine.Commands; 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 MissionCompleteEvent is produced — proving the level is winnable. /// public class SolvabilityTests { [Fact] public void SingleRook_ShortRelay_MissionComplete() { var level = new BoardBuilder(3, 1) .WithProduction(0, 0, "Scierie", CargoType.Wood) .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)); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is MissionCompleteEvent); } [Fact] public void ThreePieceChain_SharedRelayPoints_MissionComplete() { var level = new BoardBuilder(5, 2) .WithProduction(0, 0, "Scierie", CargoType.Wood) .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)); var allEvents = sim.StepN(30); Assert.Contains(allEvents, e => e is MissionCompleteEvent); Assert.True( allEvents.OfType().Count() >= 4, "Expected at least 4 cargo transfers across the 3-piece chain"); } [Fact] public void TwoDemands_SingleSource_BothSatisfied() { var level = new BoardBuilder(4, 3) .WithProduction(0, 0, "Scierie", CargoType.Wood) .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)); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is MissionCompleteEvent); 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_MissionComplete() { var level = new BoardBuilder(4, 2) .WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 1, "Carriere", CargoType.Stone) .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)); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is MissionCompleteEvent); 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_MissionComplete() { var level = new BoardBuilder(4, 3) .WithProduction(0, 0, "Scierie", CargoType.Wood) .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)); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is MissionCompleteEvent); } [Fact] public void Knight_JumpsWall_MissionComplete() { var level = new BoardBuilder(5, 3) .WithProduction(0, 0, "Scierie", CargoType.Wood) .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)); var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is MissionCompleteEvent); Assert.Contains(allEvents, e => e is PieceMovedEvent m && m.To == new Coords(3, 0)); } [Fact] public void NoCollision_WithSharedRelayPoints() { var level = new BoardBuilder(5, 2) .WithProduction(0, 0, "P", CargoType.Wood) .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)); var allEvents = sim.StepN(20); Assert.DoesNotContain(allEvents, e => e is PieceReturnedToStockEvent); } [Fact] public void CargoFilter_AutoAssigned_PreventsContamination() { var level = new BoardBuilder(4, 1) .WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(3, 0, "Carriere", CargoType.Stone) .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)); var snapshot = sim.Snapshot; Assert.Equal(CargoType.Wood, snapshot.Pieces[0].CargoFilter); var allEvents = sim.StepN(20); var transfers = allEvents.OfType().ToList(); Assert.All(transfers, t => Assert.Equal(CargoType.Wood, t.Type)); Assert.Contains(allEvents, e => e is MissionCompleteEvent); } [Fact] public void CargoFilter_PropagatesThroughChain() { var level = new BoardBuilder(5, 2) .WithProduction(0, 0, "Scierie", CargoType.Wood) .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)); 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 StepFromPaused_Works() { var level = new BoardBuilder(3, 1) .WithProduction(0, 0, "P", CargoType.Wood) .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)); // Step directly from Paused var allEvents = sim.StepN(20); Assert.Contains(allEvents, e => e is TurnStartedEvent); Assert.Contains(allEvents, e => e is MissionCompleteEvent); } /// /// Full transformer chain: Production(Wood) → Piece → Forge(Wood→Tools) → Piece → Demand(Tools) /// [Fact] public void TransformerChain_WoodToTools_MissionComplete() { var campaign = new CampaignDef { Name = "Solvability: Transformer", InitialWidth = 5, InitialHeight = 1, Missions = [ new MissionDef { Id = 1, Name = "Forge", TerrainPatch = new TerrainPatch { NewWidth = 5, NewHeight = 1, Cells = [ new PatchCell { Col = 0, Row = 0, Type = CellType.Production, Production = new ProductionDef(new Coords(0, 0), "Scierie", CargoType.Wood, 4) }, new PatchCell { Col = 2, Row = 0, Type = CellType.Transformer, Transformer = new TransformerDef(new Coords(2, 0), "Forge", CargoType.Wood, 2, CargoType.Tools, 1) }, new PatchCell { Col = 4, Row = 0, Type = CellType.Demand, Demand = new DemandDef(new Coords(4, 0), "Atelier", CargoType.Tools, 2) } ] }, UnlockedPieces = [PieceKind.Rook], UnlockedLevels = [new PieceUpgrade(PieceKind.Rook, 1)], Stock = [new PieceStock(PieceKind.Rook, 3)] } ] }; var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); // Rook 1: delivers wood to forge input sim.Place(PieceKind.Rook, (0, 0), (1, 0)); // Rook 2: picks up tools from forge output, delivers to demand sim.Place(PieceKind.Rook, (3, 0), (4, 0)); var allEvents = sim.StepN(50); Assert.Contains(allEvents, e => e is CargoConvertedEvent); Assert.Contains(allEvents, e => e is MissionCompleteEvent); } /// /// Two-stage transformation: Wood → Forge → Tools → Comptoir → Gold /// [Fact] public void DoubleTransformerChain_WoodToToolsToGold_MissionComplete() { var campaign = new CampaignDef { Name = "Solvability: Double Transformer", InitialWidth = 7, InitialHeight = 1, Missions = [ new MissionDef { Id = 1, Name = "Double Chain", TerrainPatch = new TerrainPatch { NewWidth = 7, NewHeight = 1, Cells = [ new PatchCell { Col = 0, Row = 0, Type = CellType.Production, Production = new ProductionDef(new Coords(0, 0), "Scierie", CargoType.Wood, 4) }, new PatchCell { Col = 2, Row = 0, Type = CellType.Transformer, Transformer = new TransformerDef(new Coords(2, 0), "Forge", CargoType.Wood, 2, CargoType.Tools, 1) }, new PatchCell { Col = 4, Row = 0, Type = CellType.Transformer, Transformer = new TransformerDef(new Coords(4, 0), "Comptoir", CargoType.Tools, 2, CargoType.Gold, 1) }, new PatchCell { Col = 6, Row = 0, Type = CellType.Demand, Demand = new DemandDef(new Coords(6, 0), "Tresor", CargoType.Gold, 1) } ] }, UnlockedPieces = [PieceKind.Rook], UnlockedLevels = [new PieceUpgrade(PieceKind.Rook, 1)], Stock = [new PieceStock(PieceKind.Rook, 4)] } ] }; var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); // Chain: Scierie → Rook1 → Forge → Rook2 → Comptoir → Rook3 → Tresor sim.Place(PieceKind.Rook, (0, 0), (1, 0)); // wood delivery sim.Place(PieceKind.Rook, (3, 0), (4, 0)); // tools delivery (picks from forge, delivers to comptoir) sim.Place(PieceKind.Rook, (5, 0), (6, 0)); // gold delivery var allEvents = sim.StepN(80); // Should see both transformations var conversions = allEvents.OfType().ToList(); Assert.Contains(conversions, c => c.OutputCargo == CargoType.Tools); Assert.Contains(conversions, c => c.OutputCargo == CargoType.Gold); Assert.Contains(allEvents, e => e is MissionCompleteEvent); } }