Chessistics/chessistics-tests/Simulation/FullLevelTests.cs
Samuel Bouchet a7280b1a5a Overhaul turn mechanics, collision destruction, and visual animations
- New turn order: produce -> transfer -> move -> collision resolution
- Collisions now destroy weaker pieces (status > level > mutual destruction)
  instead of halting the simulation. SimPhase.Collision removed.
- Add piece Level property (all level 1 in proto, prepared for future)
- Production fires every turn (interval concept removed), buffer = Amount
  (default 1, future 2-4), leftovers overwritten each turn
- Transfer tiebreaker: status > level > clockwise direction (alternating
  even/odd turns in y-up coords), replaces distance-to-production
- Demands always accept matching cargo even when already satisfied
- TurnNumber added to all turn events for animation grouping
- Simultaneous animations: produce flash, cargo slide, parallel piece moves
- Camera centering fix + middle-click pan
- GDD updated with new rules + lore section added

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:44:12 +02:00

135 lines
5.2 KiB
C#

using Chessistics.Engine.Events;
using Chessistics.Engine.Model;
using Chessistics.Tests.Helpers;
using Xunit;
namespace Chessistics.Tests.Simulation;
public class FullLevelTests
{
[Fact]
public void Level1_PremierConvoi_Victory()
{
// GDD Level 1: 4x4, Scierie(0,0) → Depot(3,0), 3 Rooks
// Solution: single rook relay at (1,0)↔(2,0)
var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30)
.WithStock(PieceKind.Rook, 3)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Start();
var allEvents = sim.StepN(30);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Level2_DeuxClients_Victory()
{
// GDD Level 2: 6x6, Scierie(0,0), Depot Royal(5,0), Caserne(5,4)
// Stock: 6 Rooks + 1 Bishop (fixed from GDD's 4R+1B — insufficient)
//
// Solution requires two routes from single source:
// Route 1 → (5,0): A(1,0↔2,0), B(2,0↔4,0)
// Route 2 → (5,4): C(0,1↔0,2), D(0,2↔2,2), E(2,2↔3,2),
// Bishop(3,2↔4,3), G(4,3↔5,3)
// Total needed: 6 Rooks + 1 Bishop
var level = new BoardBuilder(6, 6)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(5, 0, "Depot Royal", CargoType.Wood, 2, 50)
.WithDemand(5, 4, "Caserne", CargoType.Wood, 2, 50)
.WithStock(PieceKind.Rook, 6)
.WithStock(PieceKind.Bishop, 1)
.Build();
var sim = SimHelper.FromLevel(level);
// Route 1: bottom row → demand (5,0)
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Place(PieceKind.Rook, (2, 0), (4, 0));
// Route 2: up then right → demand (5,4)
sim.Place(PieceKind.Rook, (0, 1), (0, 2));
sim.Place(PieceKind.Rook, (0, 2), (2, 2));
// 5th rook — stock exhausted at 4!
var events5 = sim.Place(PieceKind.Rook, (2, 2), (3, 2));
Assert.DoesNotContain(events5, e => e is PlacementRejectedEvent);
sim.Place(PieceKind.Bishop, (3, 2), (4, 3));
// 6th rook needed but only 4 in stock
var events6 = sim.Place(PieceKind.Rook, (4, 3), (5, 3));
Assert.DoesNotContain(events6, e => e is PlacementRejectedEvent);
sim.Start();
var allEvents = sim.StepN(60);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Level3_LeCol_Victory()
{
// GDD Level 3: 6x6, L-shaped wall, 2 cargo types, knights jump obstacle
// Stock: 8 Rooks + 2 Knights (fixed from GDD's 4R+1B+2K)
//
// CargoFilter (Phase 2) prevents cross-route contamination:
// pieces auto-inherit their production's cargo type via relay chain.
//
// Route Wood (0,0→5,5): R1(0,1↔1,1), K1(1,1↔3,2),
// R2(3,2↔4,2), R3(4,2↔5,2), R4(5,2↔5,3), R5(5,3↔5,4)
// Route Stone (5,0→0,5): S1(4,0↔3,0), S2(3,0↔2,0),
// K2(2,0↔1,2), S3(1,2↔1,3), S4(1,3↔1,4), S5(1,4↔0,4)
// Total: 10 Rooks + 2 Knights
var level = new BoardBuilder(6, 6)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithProduction(5, 0, "Carriere", CargoType.Stone)
.WithDemand(5, 5, "Depot Royal", CargoType.Wood, 2, 60)
.WithDemand(0, 5, "Forge", CargoType.Stone, 2, 60)
.WithWall(2, 2).WithWall(2, 3).WithWall(2, 4).WithWall(3, 4).WithWall(4, 4)
.WithStock(PieceKind.Rook, 10)
.WithStock(PieceKind.Knight, 2)
.Build();
var sim = SimHelper.FromLevel(level);
// Route Wood: prod(0,0) → demand(5,5)
sim.Place(PieceKind.Rook, (0, 1), (1, 1));
sim.Place(PieceKind.Knight, (1, 1), (3, 2));
sim.Place(PieceKind.Rook, (3, 2), (4, 2));
sim.Place(PieceKind.Rook, (4, 2), (5, 2));
sim.Place(PieceKind.Rook, (5, 2), (5, 3));
sim.Place(PieceKind.Rook, (5, 3), (5, 4));
// Route Stone: prod(5,0) → demand(0,5)
sim.Place(PieceKind.Rook, (4, 0), (3, 0));
sim.Place(PieceKind.Rook, (3, 0), (2, 0));
sim.Place(PieceKind.Knight, (2, 0), (1, 2));
sim.Place(PieceKind.Rook, (1, 2), (1, 3));
sim.Place(PieceKind.Rook, (1, 3), (1, 4));
sim.Place(PieceKind.Rook, (1, 4), (0, 4));
sim.Start();
var allEvents = sim.StepN(80);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Level1_InsufficientPieces_NoVictory()
{
var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood)
.WithDemand(3, 3, "Depot Royal", CargoType.Wood, 3, 5)
.WithStock(PieceKind.Rook, 1)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 1), (2, 1));
sim.Start();
var allEvents = sim.StepN(8);
Assert.DoesNotContain(allEvents, e => e is VictoryEvent);
Assert.Contains(allEvents, e => e is DeadlineExpiredEvent);
}
}