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.
317 lines
13 KiB
C#
317 lines
13 KiB
C#
using Chessistics.Engine.Events;
|
|
using Chessistics.Engine.Model;
|
|
using Chessistics.Engine.Rules;
|
|
using Chessistics.Tests.Helpers;
|
|
using Xunit;
|
|
|
|
namespace Chessistics.Tests.Rules;
|
|
|
|
public class TransferResolverTests
|
|
{
|
|
[Fact]
|
|
public void Production_GivesToAdjacentEmptyPiece()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.BuildState();
|
|
|
|
// Place a piece at (1,0) — adjacent to production at (0,0)
|
|
var piece = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0);
|
|
piece.CurrentCell = new Coords(1, 0);
|
|
board.Pieces.Add(piece);
|
|
|
|
// Fill production buffer
|
|
board.ProductionBuffers[new Coords(0, 0)] = CargoType.Wood;
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
Assert.Contains(events, e => e is CargoTransferredEvent ct
|
|
&& ct.From == new Coords(0, 0)
|
|
&& ct.To == new Coords(1, 0)
|
|
&& ct.Type == CargoType.Wood);
|
|
|
|
Assert.Equal(CargoType.Wood, piece.Cargo);
|
|
Assert.Null(board.ProductionBuffers[new Coords(0, 0)]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Production_DoesNotGiveToPieceWithCargo()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.BuildState();
|
|
|
|
var piece = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0);
|
|
piece.CurrentCell = new Coords(1, 0);
|
|
piece.Cargo = CargoType.Wood; // already carrying
|
|
board.Pieces.Add(piece);
|
|
board.ProductionBuffers[new Coords(0, 0)] = CargoType.Wood;
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
// No transfer from production — piece already has cargo
|
|
Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.From == new Coords(0, 0));
|
|
Assert.Equal(CargoType.Wood, board.ProductionBuffers[new Coords(0, 0)]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Piece_TransfersToAdjacentEmptyPiece()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.BuildState();
|
|
|
|
var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0);
|
|
giver.CurrentCell = new Coords(1, 0);
|
|
giver.Cargo = CargoType.Wood;
|
|
|
|
var receiver = new PieceState(2, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 1);
|
|
receiver.CurrentCell = new Coords(2, 0);
|
|
|
|
board.Pieces.AddRange([giver, receiver]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
Assert.Contains(events, e => e is CargoTransferredEvent ct
|
|
&& ct.GivingPieceId == 1 && ct.ReceivingPieceId == 2);
|
|
Assert.Null(giver.Cargo);
|
|
Assert.Equal(CargoType.Wood, receiver.Cargo);
|
|
}
|
|
|
|
[Fact]
|
|
public void Piece_DeliversToDemand()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 3, 99)
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.BuildState();
|
|
|
|
var piece = new PieceState(1, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 0);
|
|
piece.CurrentCell = new Coords(2, 0); // adjacent to demand at (3,0)
|
|
piece.Cargo = CargoType.Wood;
|
|
board.Pieces.Add(piece);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
Assert.Contains(events, e => e is CargoTransferredEvent ct
|
|
&& ct.To == new Coords(3, 0) && ct.GivingPieceId == 1);
|
|
Assert.Contains(events, e => e is DemandProgressEvent dp
|
|
&& dp.Current == 1 && dp.Required == 3);
|
|
Assert.Null(piece.Cargo);
|
|
}
|
|
|
|
[Fact]
|
|
public void Piece_DoesNotDeliverWrongType()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 3, 99) // wants Wood
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.BuildState();
|
|
|
|
var piece = new PieceState(1, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 0);
|
|
piece.CurrentCell = new Coords(2, 0);
|
|
piece.Cargo = CargoType.Stone; // carrying Stone, demand wants Wood
|
|
board.Pieces.Add(piece);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.To == new Coords(3, 0));
|
|
Assert.Equal(CargoType.Stone, piece.Cargo); // still holding
|
|
}
|
|
|
|
[Fact]
|
|
public void HigherStatus_ReceivesFirst()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.WithStock(PieceKind.Knight, 3)
|
|
.BuildState();
|
|
|
|
// Giver piece at (1,0) with cargo
|
|
var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0);
|
|
giver.CurrentCell = new Coords(1, 0);
|
|
giver.Cargo = CargoType.Wood;
|
|
|
|
// Two receivers: Rook (status 5) and Knight (status 3) both adjacent
|
|
var rookReceiver = new PieceState(2, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 1);
|
|
rookReceiver.CurrentCell = new Coords(1, 1); // adjacent to (1,0)
|
|
|
|
var knightReceiver = new PieceState(3, PieceKind.Knight, new Coords(2, 0), new Coords(0, 1), 2);
|
|
knightReceiver.CurrentCell = new Coords(2, 0); // adjacent to (1,0)
|
|
|
|
board.Pieces.AddRange([giver, rookReceiver, knightReceiver]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
// Rook (status 5) should receive before Knight (status 3)
|
|
var transfer = events.OfType<CargoTransferredEvent>().First(e => e.GivingPieceId == 1);
|
|
Assert.Equal(2, transfer.ReceivingPieceId); // rook receives
|
|
}
|
|
|
|
[Fact]
|
|
public void HigherStatus_GivesFirst()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 3)
|
|
.WithStock(PieceKind.Knight, 3)
|
|
.BuildState();
|
|
|
|
// Two givers: Rook (5) at (1,0) and Knight (3) at (1,1), both with cargo
|
|
var rook = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0);
|
|
rook.CurrentCell = new Coords(1, 0);
|
|
rook.Cargo = CargoType.Wood;
|
|
|
|
var knight = new PieceState(2, PieceKind.Knight, new Coords(1, 1), new Coords(3, 2), 1);
|
|
knight.CurrentCell = new Coords(1, 1);
|
|
knight.Cargo = CargoType.Wood;
|
|
|
|
// One receiver adjacent to both: at (2,0) — adjacent to rook at (1,0) but not to knight at (1,1)
|
|
// Let's make receiver at (1,2) — adjacent to knight (1,1) only
|
|
// Actually: receiver at (2,1) — adjacent to nothing. Let me think...
|
|
// Receiver needs to be adjacent to both. (1,0) and (1,1) share neighbor (2,0)? No, (2,0) is adjacent to (1,0) only.
|
|
// Shared neighbor: none directly... Let's change: both givers adjacent to same receiver
|
|
// Put receiver at (0, 0) — but that's production. Put receiver at (0, 1):
|
|
// (0,1) is adjacent to (1,1) [knight] and (0,0) [production], not to (1,0) [rook]
|
|
// Let's use a simpler setup:
|
|
|
|
// Rook with cargo at (2,0), Knight with cargo at (2,2), receiver at (2,1) — adjacent to both
|
|
rook.CurrentCell = new Coords(2, 0);
|
|
knight.CurrentCell = new Coords(2, 2);
|
|
|
|
var receiver = new PieceState(3, PieceKind.Rook, new Coords(2, 1), new Coords(3, 1), 2);
|
|
receiver.CurrentCell = new Coords(2, 1);
|
|
|
|
board.Pieces.AddRange([rook, knight, receiver]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
// Rook (status 5) should give first to the receiver
|
|
var transfer = events.OfType<CargoTransferredEvent>().First();
|
|
Assert.Equal(1, transfer.GivingPieceId); // rook gives first
|
|
}
|
|
|
|
[Fact]
|
|
public void TieBreaker_PlacementOrder()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Knight, 5)
|
|
.BuildState();
|
|
|
|
// Two knights (same status 3) with cargo, both adjacent to same empty receiver
|
|
var knight1 = new PieceState(1, PieceKind.Knight, new Coords(2, 0), new Coords(0, 1), 0); // earlier
|
|
knight1.CurrentCell = new Coords(2, 0);
|
|
knight1.Cargo = CargoType.Wood;
|
|
|
|
var knight2 = new PieceState(2, PieceKind.Knight, new Coords(2, 2), new Coords(0, 3), 1); // later
|
|
knight2.CurrentCell = new Coords(2, 2);
|
|
knight2.Cargo = CargoType.Wood;
|
|
|
|
var receiver = new PieceState(3, PieceKind.Knight, new Coords(2, 1), new Coords(0, 2), 2);
|
|
receiver.CurrentCell = new Coords(2, 1); // adjacent to both (2,0) and (2,2)
|
|
|
|
board.Pieces.AddRange([knight1, knight2, receiver]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
var transfer = events.OfType<CargoTransferredEvent>().First();
|
|
|
|
// knight1 is closer to production at (0,0): dist = 2, knight2: dist = 4
|
|
// So knight1 gives first due to proximity (tiebreaker before placement order)
|
|
Assert.Equal(1, transfer.GivingPieceId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cargo_MovesOneHopPerTurn()
|
|
{
|
|
var board = new BoardBuilder(5, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 5)
|
|
.BuildState();
|
|
|
|
// Chain of 3 pieces: A(0,0→1,0), B(1,0→2,0), C(2,0→3,0)
|
|
// All currently at their start cells with A having cargo
|
|
var a = new PieceState(1, PieceKind.Rook, new Coords(0, 0), new Coords(1, 0), 0);
|
|
a.CurrentCell = new Coords(1, 0); // at end cell, adjacent to B
|
|
a.Cargo = CargoType.Wood;
|
|
|
|
var b = new PieceState(2, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 1);
|
|
b.CurrentCell = new Coords(2, 0); // at end cell, adjacent to C
|
|
|
|
var c = new PieceState(3, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 2);
|
|
c.CurrentCell = new Coords(3, 0); // at end cell
|
|
|
|
board.Pieces.AddRange([a, b, c]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
// A gives to B (adjacent: (1,0)→(2,0))
|
|
Assert.Contains(events, e => e is CargoTransferredEvent ct && ct.GivingPieceId == 1 && ct.ReceivingPieceId == 2);
|
|
|
|
// B should NOT give to C in the same turn (B just received, participated)
|
|
Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.GivingPieceId == 2 && ct.ReceivingPieceId == 3);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoCrossTransfer_NonAdjacent()
|
|
{
|
|
var board = new BoardBuilder(5, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
|
|
.WithStock(PieceKind.Rook, 5)
|
|
.BuildState();
|
|
|
|
var giver = new PieceState(1, PieceKind.Rook, new Coords(0, 0), new Coords(1, 0), 0);
|
|
giver.CurrentCell = new Coords(0, 0);
|
|
giver.Cargo = CargoType.Wood;
|
|
|
|
var farPiece = new PieceState(2, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 1);
|
|
farPiece.CurrentCell = new Coords(2, 0); // 2 cells away, not adjacent
|
|
|
|
board.Pieces.AddRange([giver, farPiece]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
// No piece-to-piece transfer — they're not adjacent
|
|
Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.GivingPieceId == 1 && ct.ReceivingPieceId == 2);
|
|
}
|
|
|
|
[Fact]
|
|
public void DemandPriority_OverPieceReceiver()
|
|
{
|
|
var board = new BoardBuilder(4, 4)
|
|
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
|
.WithDemand(2, 0, "D", CargoType.Wood, 3, 99)
|
|
.WithStock(PieceKind.Rook, 5)
|
|
.BuildState();
|
|
|
|
// Piece with cargo at (1,0) adjacent to both demand at (2,0) and empty piece at (1,1)
|
|
var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0);
|
|
giver.CurrentCell = new Coords(1, 0);
|
|
giver.Cargo = CargoType.Wood;
|
|
|
|
var receiver = new PieceState(2, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 1);
|
|
receiver.CurrentCell = new Coords(1, 1); // adjacent to giver
|
|
|
|
board.Pieces.AddRange([giver, receiver]);
|
|
|
|
var events = TransferResolver.ResolveTransfers(board);
|
|
|
|
// Should deliver to demand, not to piece
|
|
Assert.Contains(events, e => e is CargoTransferredEvent ct && ct.To == new Coords(2, 0));
|
|
Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.ReceivingPieceId == 2);
|
|
}
|
|
}
|