Chessistics/chessistics-tests/Simulation/GameSimTests.cs
Samuel Bouchet e6eaae43ab Initial commit: Chessistics prototype v0.3
Black box sim engine (commands in, events out) with 3 piece types
(Rook, Bishop, Knight), cargo transfer system with social status
priority, collision detection, and victory/defeat conditions.

57 tests covering rules, simulation, loading, and solvability.
Godot 4 presentation layer scaffolding.
2026-04-10 14:58:03 +02:00

204 lines
6.3 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 GameSimTests
{
private SimHelper CreateLevel1Sim()
{
var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30)
.WithStock(PieceKind.Rook, 3)
.Build();
return SimHelper.FromLevel(level);
}
[Fact]
public void PlacePiece_Succeeds()
{
var sim = CreateLevel1Sim();
var events = sim.Place(PieceKind.Rook, (0, 0), (1, 0));
Assert.Single(events);
Assert.IsType<PiecePlacedEvent>(events[0]);
var placed = (PiecePlacedEvent)events[0];
Assert.Equal(PieceKind.Rook, placed.Kind);
}
[Fact]
public void PlacePiece_StockExhausted_Rejected()
{
var sim = CreateLevel1Sim();
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Place(PieceKind.Rook, (0, 1), (1, 1));
sim.Place(PieceKind.Rook, (0, 2), (1, 2));
// 4th rook — only 3 in stock
var events = sim.Place(PieceKind.Rook, (0, 3), (1, 3));
Assert.IsType<PlacementRejectedEvent>(events[0]);
}
[Fact]
public void PlaceDuringRunning_Rejected()
{
var sim = CreateLevel1Sim();
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Start();
var events = sim.Place(PieceKind.Rook, (0, 1), (1, 1));
Assert.IsType<CommandRejectedEvent>(events[0]);
}
[Fact]
public void StartWithNoPieces_Rejected()
{
var sim = CreateLevel1Sim();
var events = sim.Start();
Assert.IsType<CommandRejectedEvent>(events[0]);
}
[Fact]
public void RemoveDuringRunning_Rejected()
{
var sim = CreateLevel1Sim();
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Start();
var events = sim.Remove(1);
Assert.IsType<CommandRejectedEvent>(events[0]);
}
[Fact]
public void StopDuringEdit_Rejected()
{
var sim = CreateLevel1Sim();
var events = sim.Stop();
Assert.IsType<CommandRejectedEvent>(events[0]);
}
[Fact]
public void SinglePiece_Oscillates()
{
var sim = CreateLevel1Sim();
sim.Place(PieceKind.Rook, (0, 0), (2, 0));
sim.Start();
// Step 1: piece moves from (0,0) to (2,0)
var events1 = sim.Step();
Assert.Contains(events1, e => e is PieceMovedEvent m && m.From == new Coords(0, 0) && m.To == new Coords(2, 0));
// Step 2: piece moves back from (2,0) to (0,0)
var events2 = sim.Step();
Assert.Contains(events2, e => e is PieceMovedEvent m && m.From == new Coords(2, 0) && m.To == new Coords(0, 0));
// Step 3: piece moves forward again
var events3 = sim.Step();
Assert.Contains(events3, e => e is PieceMovedEvent m && m.From == new Coords(0, 0) && m.To == new Coords(2, 0));
}
[Fact]
public void ChainedPieces_TransferCargo()
{
var sim = CreateLevel1Sim();
// Piece A: (0,0) → (1,0), Piece B: (2,0) → (3,0)
// Adjacent at (1,0)↔(2,0) when A is at end and B is at start
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Place(PieceKind.Rook, (2, 0), (3, 0));
sim.Start();
// Run until we see a cargo transfer between pieces
var allEvents = sim.StepN(20);
// Should have production events and cargo transfers
Assert.Contains(allEvents, e => e is CargoProducedEvent);
Assert.Contains(allEvents, e => e is CargoTransferredEvent);
}
[Fact]
public void Production_GeneratesOnInterval()
{
var sim = CreateLevel1Sim();
// Place rook adjacent to production so it picks up cargo, freeing the buffer
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Start();
var allEvents = sim.StepN(6);
var prodEvents = allEvents.OfType<CargoProducedEvent>().ToList();
// With interval 2, produces on turns 2, 4, 6 (buffer freed each time by adjacent piece)
Assert.True(prodEvents.Count >= 2, $"Expected at least 2 productions, got {prodEvents.Count}");
}
[Fact]
public void Victory_WhenAllDemandsMet()
{
// Tiny level: prod adjacent to demand, just need one piece to relay
var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "P", CargoType.Wood, 1)
.WithDemand(2, 0, "D", CargoType.Wood, 1, 30)
.WithStock(PieceKind.Rook, 2)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Start();
// Run enough turns for production → piece → demand
var allEvents = sim.StepN(10);
Assert.Contains(allEvents, e => e is VictoryEvent);
}
[Fact]
public void Defeat_WhenDeadlineExpires()
{
// Demand with very tight deadline, piece placed far from demand
var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 3, "D", CargoType.Wood, 10, 3) // need 10 in 3 turns — impossible
.WithStock(PieceKind.Rook, 3)
.Build();
var sim = SimHelper.FromLevel(level);
sim.Place(PieceKind.Rook, (1, 0), (2, 0));
sim.Start();
var allEvents = sim.StepN(5);
Assert.Contains(allEvents, e => e is DeadlineExpiredEvent);
}
[Fact]
public void StopResetsState()
{
var sim = CreateLevel1Sim();
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Start();
sim.StepN(5);
sim.Stop();
var snap = sim.Snapshot;
Assert.Equal(SimPhase.Edit, snap.Phase);
Assert.Equal(0, snap.TurnNumber);
// Pieces should be back at start cells
Assert.All(snap.Pieces, p => Assert.Equal(p.StartCell, p.CurrentCell));
}
[Fact]
public void ResetClearsEverything()
{
var sim = CreateLevel1Sim();
sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Reset();
var snap = sim.Snapshot;
Assert.Equal(SimPhase.Edit, snap.Phase);
Assert.Empty(snap.Pieces);
Assert.Equal(3, snap.RemainingStock[PieceKind.Rook]);
}
}