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) .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)] = 1; 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.Equal(0, board.ProductionBuffers[new Coords(0, 0)]); } [Fact] public void Production_DoesNotGiveToPieceWithCargo() { var board = new BoardBuilder(4, 4) .WithProduction(0, 0, "P", CargoType.Wood) .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)] = 1; 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(1, board.ProductionBuffers[new Coords(0, 0)]); } [Fact] public void Piece_TransfersToAdjacentEmptyPiece() { var board = new BoardBuilder(4, 4) .WithProduction(0, 0, "P", CargoType.Wood) .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) .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) .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) .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().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) .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().First(); Assert.Equal(1, transfer.GivingPieceId); // rook gives first } [Fact] public void TieBreaker_ClockwiseDirection_EvenTurn() { var board = new BoardBuilder(4, 4) .WithProduction(0, 0, "P", CargoType.Wood) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithStock(PieceKind.Knight, 5) .BuildState(); // Giver at (1,1) with cargo, two receivers: right(2,1) and up(1,2) // On even turn (TurnNumber=0), clockwise from right: right=0 < up=1 var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 0); giver.CurrentCell = new Coords(1, 1); giver.Cargo = CargoType.Wood; var receiverRight = new PieceState(2, PieceKind.Rook, new Coords(2, 1), new Coords(3, 1), 1); receiverRight.CurrentCell = new Coords(2, 1); // right of giver var receiverUp = new PieceState(3, PieceKind.Rook, new Coords(1, 2), new Coords(1, 3), 2); receiverUp.CurrentCell = new Coords(1, 2); // up of giver board.Pieces.AddRange([giver, receiverRight, receiverUp]); board.TurnNumber = 2; // even turn var events = TransferResolver.ResolveTransfers(board); var transfer = events.OfType().First(); // On even turn, right(0) has priority over up(1) Assert.Equal(2, transfer.ReceivingPieceId); // receiverRight } [Fact] public void TieBreaker_ClockwiseDirection_OddTurn() { var board = new BoardBuilder(4, 4) .WithProduction(0, 0, "P", CargoType.Wood) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithStock(PieceKind.Knight, 5) .BuildState(); // Same setup but on odd turn: left=0 < down=1 < right=2 < up=3 var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 0); giver.CurrentCell = new Coords(1, 1); giver.Cargo = CargoType.Wood; var receiverRight = new PieceState(2, PieceKind.Rook, new Coords(2, 1), new Coords(3, 1), 1); receiverRight.CurrentCell = new Coords(2, 1); // right of giver var receiverLeft = new PieceState(3, PieceKind.Rook, new Coords(0, 1), new Coords(0, 2), 2); receiverLeft.CurrentCell = new Coords(0, 1); // left of giver board.Pieces.AddRange([giver, receiverRight, receiverLeft]); board.TurnNumber = 1; // odd turn var events = TransferResolver.ResolveTransfers(board); var transfer = events.OfType().First(); // On odd turn, left(0) has priority over right(2) Assert.Equal(3, transfer.ReceivingPieceId); // receiverLeft } [Fact] public void Cargo_MovesOneHopPerTurn() { var board = new BoardBuilder(5, 4) .WithProduction(0, 0, "P", CargoType.Wood) .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) .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 Production_Amount3_FeedsMultiplePieces() { var board = new BoardBuilder(4, 4) .WithProduction(0, 0, "P", CargoType.Wood, amount: 3) .WithDemand(3, 0, "D", CargoType.Wood, 10, 99) .WithStock(PieceKind.Rook, 5) .BuildState(); // Three pieces adjacent to production at (0,0): (1,0), (0,1) var p1 = new PieceState(1, PieceKind.Rook, new Coords(1, 0), new Coords(2, 0), 0); p1.CurrentCell = new Coords(1, 0); var p2 = new PieceState(2, PieceKind.Rook, new Coords(0, 1), new Coords(0, 2), 1); p2.CurrentCell = new Coords(0, 1); board.Pieces.AddRange([p1, p2]); board.ProductionBuffers[new Coords(0, 0)] = 3; // amount=3 var events = TransferResolver.ResolveTransfers(board); // Both pieces should receive cargo (buffer had 3, 2 pieces adjacent) Assert.Equal(CargoType.Wood, p1.Cargo); Assert.Equal(CargoType.Wood, p2.Cargo); Assert.Equal(1, board.ProductionBuffers[new Coords(0, 0)]); // 3 - 2 = 1 remaining } [Fact] public void DemandPriority_OverPieceReceiver() { var board = new BoardBuilder(4, 4) .WithProduction(0, 0, "P", CargoType.Wood) .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); } }