309 lines
12 KiB
C#
309 lines
12 KiB
C#
|
|
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<PiecePlacedEvent>(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<PieceRemovedEvent>(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<CommandRejectedEvent>(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);
|
||
|
|
}
|
||
|
|
}
|