Bundles in-flight work on the campaign/missions system (CampaignDef, MissionDef, TerrainPatch, TransformerDef, MissionChecker, CampaignLoader, FlavorBanner, transformer rules), plan files, and matching tests. Baseline commit so the upcoming automation testing harness lands on a clean tree.
318 lines
12 KiB
C#
318 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;
|
|
|
|
/// <summary>
|
|
/// End-to-end solvability tests: each test places pieces, runs the simulation,
|
|
/// and asserts MissionCompleteEvent is produced — proving the level is winnable.
|
|
/// </summary>
|
|
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<CargoTransferredEvent>().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<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_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<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_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<CargoTransferredEvent>().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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Full transformer chain: Production(Wood) → Piece → Forge(Wood→Tools) → Piece → Demand(Tools)
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Two-stage transformation: Wood → Forge → Tools → Comptoir → Gold
|
|
/// </summary>
|
|
[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<CargoConvertedEvent>().ToList();
|
|
Assert.Contains(conversions, c => c.OutputCargo == CargoType.Tools);
|
|
Assert.Contains(conversions, c => c.OutputCargo == CargoType.Gold);
|
|
Assert.Contains(allEvents, e => e is MissionCompleteEvent);
|
|
}
|
|
}
|