Chessistics/chessistics-engine/Rules/TransferResolver.cs
Samuel Bouchet 2b3d27d295 Fix transfer direction bug, validate GDD levels via red-green testing
- Fix cargo bouncing in relay chains: piece-to-piece transfers now prefer
  receivers farther from production (forward flow) instead of closer (backward)
- Level 2 stock corrected: 4R+1B → 6R+1B to match required solution
- Level 3 simplified to single cargo type (6R+1K) — dual-cargo on 6x6
  requires engine support for cargo-type filtering (Phase 2)
- Add PLAN.md with prototype roadmap (phases 2-6)
- 57 tests passing
2026-04-10 15:25:25 +02:00

147 lines
5.7 KiB
C#

using Chessistics.Engine.Events;
using Chessistics.Engine.Model;
namespace Chessistics.Engine.Rules;
public static class TransferResolver
{
public static List<IWorldEvent> ResolveTransfers(BoardState state)
{
var events = new List<IWorldEvent>();
var participated = new HashSet<int>(); // piece IDs that already gave or received
var productionGave = new HashSet<Coords>(); // productions that already gave
// Phase A: Productions give to adjacent pieces
ResolveProductionTransfers(state, events, participated, productionGave);
// Phase B: Pieces give to demands or other pieces
ResolvePieceTransfers(state, events, participated);
return events;
}
private static void ResolveProductionTransfers(
BoardState state, List<IWorldEvent> events,
HashSet<int> participated, HashSet<Coords> productionGave)
{
// Sort productions deterministically (by position)
var productions = state.Productions.Values
.Where(p => state.ProductionBuffers[p.Position] != null)
.OrderBy(p => p.Position.Col).ThenBy(p => p.Position.Row)
.ToList();
foreach (var prod in productions)
{
var cargoType = state.ProductionBuffers[prod.Position]!.Value;
// Find adjacent pieces without cargo, sorted by receiver priority
var receivers = GetAdjacentPiecesWithoutCargo(state, prod.Position, participated);
if (receivers.Count == 0) continue;
var receiver = receivers[0];
receiver.Cargo = cargoType;
state.ProductionBuffers[prod.Position] = null;
participated.Add(receiver.Id);
productionGave.Add(prod.Position);
events.Add(new CargoTransferredEvent(
prod.Position, receiver.CurrentCell, cargoType,
GivingPieceId: null, ReceivingPieceId: receiver.Id));
}
}
private static void ResolvePieceTransfers(
BoardState state, List<IWorldEvent> events, HashSet<int> participated)
{
// Get all pieces with cargo that haven't participated, sorted by giver priority
var givers = state.Pieces
.Where(p => p.Cargo != null && !participated.Contains(p.Id))
.OrderByDescending(p => p.SocialStatus)
.ThenBy(p => MinDistanceToProduction(p.CurrentCell, state))
.ThenBy(p => p.PlacementOrder)
.ToList();
foreach (var giver in givers)
{
if (participated.Contains(giver.Id)) continue;
var cargoType = giver.Cargo!.Value;
// Priority 1: deliver to adjacent demand
var adjacentDemand = GetAdjacentCompatibleDemand(state, giver.CurrentCell, cargoType);
if (adjacentDemand != null)
{
giver.Cargo = null;
adjacentDemand.ReceivedCount++;
participated.Add(giver.Id);
events.Add(new CargoTransferredEvent(
giver.CurrentCell, adjacentDemand.Position, cargoType,
GivingPieceId: giver.Id, ReceivingPieceId: null));
events.Add(new DemandProgressEvent(
adjacentDemand.Position, adjacentDemand.Name,
adjacentDemand.ReceivedCount, adjacentDemand.Required));
continue;
}
// Priority 2: transfer to adjacent piece without cargo
// Prefer receivers farther from production (push cargo forward in chain)
var receivers = GetAdjacentPiecesWithoutCargo(state, giver.CurrentCell, participated,
forwardDirection: true);
if (receivers.Count == 0) continue;
var receiver = receivers[0];
receiver.Cargo = cargoType;
giver.Cargo = null;
participated.Add(giver.Id);
participated.Add(receiver.Id);
events.Add(new CargoTransferredEvent(
giver.CurrentCell, receiver.CurrentCell, cargoType,
GivingPieceId: giver.Id, ReceivingPieceId: receiver.Id));
}
}
private static List<PieceState> GetAdjacentPiecesWithoutCargo(
BoardState state, Coords position, HashSet<int> participated,
bool forwardDirection = false)
{
var adjacent = position.GetAdjacent4(state.Width, state.Height);
var query = state.Pieces
.Where(p => p.Cargo == null
&& !participated.Contains(p.Id)
&& adjacent.Contains(p.CurrentCell))
.OrderByDescending(p => p.SocialStatus);
// For piece-to-piece transfers, prefer receivers farther from production
// (pushes cargo forward through relay chains instead of backward).
// For production pickups, prefer receivers closer to production.
var sorted = forwardDirection
? query.ThenByDescending(p => MinDistanceToProduction(p.CurrentCell, state))
: query.ThenBy(p => MinDistanceToProduction(p.CurrentCell, state));
return sorted.ThenBy(p => p.PlacementOrder).ToList();
}
private static DemandState? GetAdjacentCompatibleDemand(
BoardState state, Coords position, CargoType cargoType)
{
var adjacent = position.GetAdjacent4(state.Width, state.Height);
return state.Demands.Values
.Where(d => !d.IsSatisfied
&& d.Cargo == cargoType
&& adjacent.Contains(d.Position))
.FirstOrDefault();
}
private static int MinDistanceToProduction(Coords cell, BoardState state)
{
if (state.Productions.Count == 0) return int.MaxValue;
return state.Productions.Keys.Min(p => cell.ManhattanDistance(p));
}
}