using Chessistics.Engine.Commands; using Chessistics.Engine.Events; using Chessistics.Engine.Model; using Chessistics.Tests.Helpers; using Xunit; namespace Chessistics.Tests.Simulation; public class CampaignTests { private static CampaignDef CreateTwOMissionCampaign() { return new CampaignDef { Name = "Test Campaign", InitialWidth = 4, InitialHeight = 4, Missions = [ new MissionDef { Id = 1, Name = "Mission 1", Description = "First mission", TerrainPatch = new TerrainPatch { NewWidth = 4, NewHeight = 4, Cells = [ new PatchCell { Col = 0, Row = 0, Type = CellType.Production, Production = new ProductionDef(new Coords(0, 0), "Scierie", CargoType.Wood) }, new PatchCell { Col = 3, Row = 0, Type = CellType.Demand, Demand = new DemandDef(new Coords(3, 0), "Depot", CargoType.Wood, 2) } ] }, UnlockedPieces = [PieceKind.Pawn, PieceKind.Rook], UnlockedLevels = [new PieceUpgrade(PieceKind.Pawn, 1), new PieceUpgrade(PieceKind.Rook, 1)], Stock = [new PieceStock(PieceKind.Rook, 3)] }, new MissionDef { Id = 2, Name = "Mission 2", Description = "Second mission — terrain expands east", TerrainPatch = new TerrainPatch { NewWidth = 6, NewHeight = 4, Cells = [ new PatchCell { Col = 4, Row = 0, Type = CellType.Empty }, new PatchCell { Col = 4, Row = 1, Type = CellType.Wall }, new PatchCell { Col = 5, Row = 0, Type = CellType.Production, Production = new ProductionDef(new Coords(5, 0), "Carriere", CargoType.Stone) }, new PatchCell { Col = 5, Row = 3, Type = CellType.Demand, Demand = new DemandDef(new Coords(5, 3), "Chantier", CargoType.Stone, 3) } ] }, UnlockedPieces = [], UnlockedLevels = [], Stock = [new PieceStock(PieceKind.Rook, 2)] } ] }; } [Fact] public void LoadCampaign_InitializesBoard() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); var events = sim.Sim.ProcessCommand(new LoadCampaignCommand()); Assert.Contains(events, e => e is CampaignLoadedEvent); Assert.Contains(events, e => e is MissionStartedEvent ms && ms.MissionIndex == 0); Assert.Contains(events, e => e is PieceUnlockedEvent pu && pu.Kind == PieceKind.Rook); var snap = sim.Snapshot; Assert.Equal(4, snap.Width); Assert.Equal(4, snap.Height); Assert.Equal(3, snap.RemainingStock[PieceKind.Rook]); Assert.Equal(SimPhase.Paused, snap.Phase); Assert.NotNull(snap.Campaign); Assert.Equal(0, snap.Campaign.CurrentMissionIndex); } [Fact] public void Campaign_CompleteMission1_AutoAdvances() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); // Place rook to relay wood sim.Place(PieceKind.Rook, (1, 0), (2, 0)); // Run — mission 1 completes and auto-advances to mission 2 var allEvents = sim.StepN(30); Assert.Contains(allEvents, e => e is MissionCompleteEvent mc && mc.MissionIndex == 0); Assert.Contains(allEvents, e => e is MissionStartedEvent ms && ms.MissionIndex == 1); Assert.Contains(allEvents, e => e is TerrainExpandedEvent te && te.NewWidth == 6); var snap = sim.Snapshot; Assert.Equal(6, snap.Width); Assert.Equal(4, snap.Height); // Phase stays Paused (from StepSimulationCommand), NOT MissionComplete Assert.Equal(SimPhase.Paused, snap.Phase); Assert.Equal(1, snap.Campaign!.CurrentMissionIndex); // Original rook is still in place Assert.Single(snap.Pieces); // Additional stock from mission 2 added Assert.Equal(2 + 2, snap.RemainingStock[PieceKind.Rook]); // 2 leftover from M1 + 2 new } [Fact] public void Campaign_PiecesRemainAfterAutoAdvance() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); // Run until auto-advance sim.StepN(30); // Piece is STILL on the board after auto-advancing Assert.Single(sim.Snapshot.Pieces); Assert.Equal(new Coords(1, 0), sim.Snapshot.Pieces[0].StartCell); } [Fact] public void Campaign_PlacePieceDuringRunning() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); sim.Resume(); // Paused → Running var events = sim.Place(PieceKind.Rook, (1, 0), (2, 0)); Assert.IsType(events[0]); } [Fact] public void Campaign_RemovePieceDuringRunning() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); sim.Resume(); // Running var events = sim.Remove(1); Assert.IsType(events[0]); Assert.Equal(3, sim.Snapshot.RemainingStock[PieceKind.Rook]); } [Fact] public void Campaign_CollisionReturnsPieceToStock() { // Two rooks heading to same cell → collision var campaign = new CampaignDef { Name = "Collision Test", InitialWidth = 4, InitialHeight = 4, Missions = [ new MissionDef { Id = 1, Name = "M1", TerrainPatch = new TerrainPatch { NewWidth = 4, NewHeight = 4, Cells = [ new PatchCell { Col = 0, Row = 0, Type = CellType.Production, Production = new ProductionDef(new Coords(0, 0), "P", CargoType.Wood) }, new PatchCell { Col = 3, Row = 0, Type = CellType.Demand, Demand = new DemandDef(new Coords(3, 0), "D", CargoType.Wood, 5) } ] }, UnlockedPieces = [PieceKind.Rook, PieceKind.Queen], UnlockedLevels = [new PieceUpgrade(PieceKind.Rook, 1), new PieceUpgrade(PieceKind.Queen, 1)], Stock = [new PieceStock(PieceKind.Rook, 2), new PieceStock(PieceKind.Queen, 1)] } ] }; var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); // Rook and Queen both pass through (1,0): collision! // Rook: (0,0) ↔ (2,0), Queen: (2,0) ↔ (0,0) — they swap cells each turn, meeting at... // Actually let's make them collide: same end cell sim.Place(PieceKind.Rook, (0, 1), (1, 1)); sim.Place(PieceKind.Queen, (2, 1), (1, 1)); // same end cell → collision after first step var stockBefore = sim.Snapshot.RemainingStock[PieceKind.Rook]; var events = sim.Step(); // Queen wins (status 7 vs 5), rook returns to stock Assert.Contains(events, e => e is PieceReturnedToStockEvent ret && ret.Kind == PieceKind.Rook); // Rook returned to stock var stockAfter = sim.Snapshot.RemainingStock[PieceKind.Rook]; Assert.Equal(stockBefore + 1, stockAfter); // Auto-pause on collision Assert.Equal(SimPhase.Paused, sim.Snapshot.Phase); } [Fact] public void Campaign_ManualAdvanceWithoutComplete_Rejected() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); // AdvanceMissionCommand requires MissionComplete phase var events = sim.AdvanceMission(); Assert.IsType(events[0]); } [Fact] public void Campaign_LastMission_SetsMissionCompletePhase() { // Single-mission campaign — completing it should set MissionComplete phase var campaign = new CampaignDef { Name = "Short", InitialWidth = 3, InitialHeight = 1, Missions = [ new MissionDef { Id = 1, Name = "Only", TerrainPatch = new TerrainPatch { NewWidth = 3, NewHeight = 1, Cells = [ new PatchCell { Col = 0, Row = 0, Type = CellType.Production, Production = new ProductionDef(new Coords(0, 0), "P", CargoType.Wood) }, new PatchCell { Col = 2, Row = 0, Type = CellType.Demand, Demand = new DemandDef(new Coords(2, 0), "D", CargoType.Wood, 1) } ] }, UnlockedPieces = [PieceKind.Rook], UnlockedLevels = [new PieceUpgrade(PieceKind.Rook, 1)], Stock = [new PieceStock(PieceKind.Rook, 1)] } ] }; var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); var allEvents = sim.StepN(10); // Last mission → MissionComplete phase (no auto-advance) Assert.Contains(allEvents, e => e is MissionCompleteEvent); Assert.DoesNotContain(allEvents, e => e is MissionStartedEvent); Assert.Equal(SimPhase.MissionComplete, sim.Snapshot.Phase); } [Fact] public void Campaign_UnlockedPiecesEnforced() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); // Bishop not unlocked — placement rejected // First, add bishop to stock manually for this test // Actually, the stock doesn't have bishops. Let's check that even if stock existed, unlock blocks it. // The campaign only unlocks Pawn and Rook. No bishop stock either. // Let's just verify the unlock set is correct. var snap = sim.Snapshot; Assert.Contains(PieceKind.Rook, snap.Campaign!.AvailablePieceKinds); Assert.Contains(PieceKind.Pawn, snap.Campaign!.AvailablePieceKinds); Assert.DoesNotContain(PieceKind.Bishop, snap.Campaign!.AvailablePieceKinds); } [Fact] public void MovePiece_UpdatesPosition() { var campaign = CreateTwOMissionCampaign(); var sim = SimHelper.FromCampaign(campaign); sim.Sim.ProcessCommand(new LoadCampaignCommand()); sim.Place(PieceKind.Rook, (1, 0), (2, 0)); var events = sim.Sim.ProcessCommand(new MovePieceCommand(1, new Coords(1, 1), new Coords(2, 1))); Assert.Contains(events, e => e is PieceMovedByPlayerEvent mv && mv.OldStart == new Coords(1, 0) && mv.NewStart == new Coords(1, 1)); var snap = sim.Snapshot; Assert.Equal(new Coords(1, 1), snap.Pieces[0].StartCell); Assert.Equal(new Coords(2, 1), snap.Pieces[0].EndCell); Assert.Equal(new Coords(1, 1), snap.Pieces[0].CurrentCell); } }