Add CargoFilter property to PieceState, auto-assigned at placement by tracing relay chain back to production. TransferResolver now enforces cargo-type filtering and uses forward-direction sorting with cargo-aware distance calculations. Prevents cross-route contamination on multi-cargo boards. Level 3 restored to dual-cargo (Wood+Stone) with correct 10R+2K stock. Two new solvability tests validate filter auto-assignment and chain propagation. All 60 tests green.
240 lines
8.2 KiB
C#
240 lines
8.2 KiB
C#
using Chessistics.Engine.Events;
|
|
using Chessistics.Engine.Model;
|
|
using Chessistics.Engine.Rules;
|
|
using Chessistics.Engine.Simulation;
|
|
|
|
namespace Chessistics.Engine.Commands;
|
|
|
|
public class PlacePieceCommand : WorldCommand
|
|
{
|
|
public PieceKind Kind { get; }
|
|
public Coords Start { get; }
|
|
public Coords End { get; }
|
|
|
|
public PlacePieceCommand(PieceKind kind, Coords start, Coords end)
|
|
{
|
|
Kind = kind;
|
|
Start = start;
|
|
End = end;
|
|
}
|
|
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase != SimPhase.Edit)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(PlacePieceCommand), "Can only place pieces during Edit phase."));
|
|
|
|
if (!state.RemainingStock.TryGetValue(Kind, out var remaining) || remaining <= 0)
|
|
throw new CommandRejectedException(
|
|
new PlacementRejectedEvent(Kind, Start, End, "No pieces of this type remaining in stock."));
|
|
|
|
if (!state.IsOnBoard(Start) || !state.IsOnBoard(End))
|
|
throw new CommandRejectedException(
|
|
new PlacementRejectedEvent(Kind, Start, End, "Position is off the board."));
|
|
|
|
if (state.GetCell(Start) == CellType.Wall)
|
|
throw new CommandRejectedException(
|
|
new PlacementRejectedEvent(Kind, Start, End, "Cannot place on a wall."));
|
|
|
|
if (!MoveValidator.IsLegalPlacement(Kind, Start, End, state))
|
|
throw new CommandRejectedException(
|
|
new PlacementRejectedEvent(Kind, Start, End, "Illegal move for this piece type."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
var piece = new PieceState(
|
|
state.NextPieceId++, Kind, Start, End, state.Pieces.Count);
|
|
|
|
piece.CargoFilter = InferCargoFilter(state, piece);
|
|
|
|
state.Pieces.Add(piece);
|
|
state.RemainingStock[Kind] = state.RemainingStock[Kind] - 1;
|
|
state.OccupiedCells.Add(Start);
|
|
state.OccupiedCells.Add(End);
|
|
|
|
changeList.Add(new PiecePlacedEvent(piece.Id, Kind, Start, End));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Auto-assign cargo filter by tracing the relay chain back to a production.
|
|
/// Priority: direct adjacency to production, then shared relay with filtered piece.
|
|
/// </summary>
|
|
private static CargoType? InferCargoFilter(BoardState state, PieceState piece)
|
|
{
|
|
// Check if start or end cell is adjacent to a production
|
|
foreach (var (prodPos, prod) in state.Productions)
|
|
{
|
|
if (piece.StartCell.IsAdjacent4(prodPos) || piece.EndCell.IsAdjacent4(prodPos))
|
|
return prod.Cargo;
|
|
}
|
|
|
|
// Check if start or end shares a relay point with an existing piece that has a filter
|
|
foreach (var existing in state.Pieces)
|
|
{
|
|
if (existing.CargoFilter == null) continue;
|
|
|
|
bool sharesRelay = piece.StartCell == existing.StartCell
|
|
|| piece.StartCell == existing.EndCell
|
|
|| piece.EndCell == existing.StartCell
|
|
|| piece.EndCell == existing.EndCell;
|
|
|
|
if (sharesRelay)
|
|
return existing.CargoFilter;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public class RemovePieceCommand : WorldCommand
|
|
{
|
|
public int PieceId { get; }
|
|
|
|
public RemovePieceCommand(int pieceId)
|
|
{
|
|
PieceId = pieceId;
|
|
}
|
|
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase != SimPhase.Edit)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(RemovePieceCommand), "Can only remove pieces during Edit phase."));
|
|
|
|
if (state.GetPieceById(PieceId) == null)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(RemovePieceCommand), $"Piece {PieceId} not found."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
var piece = state.GetPieceById(PieceId)!;
|
|
state.Pieces.Remove(piece);
|
|
state.RemainingStock[piece.Kind] = state.RemainingStock.GetValueOrDefault(piece.Kind) + 1;
|
|
|
|
changeList.Add(new PieceRemovedEvent(PieceId));
|
|
}
|
|
}
|
|
|
|
public class StartSimulationCommand : WorldCommand
|
|
{
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase != SimPhase.Edit)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(StartSimulationCommand), "Can only start from Edit phase."));
|
|
|
|
if (state.Pieces.Count == 0)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(StartSimulationCommand), "Place at least one piece before starting."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
state.Phase = SimPhase.Running;
|
|
changeList.Add(new SimulationStartedEvent());
|
|
}
|
|
}
|
|
|
|
public class PauseSimulationCommand : WorldCommand
|
|
{
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase != SimPhase.Running)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(PauseSimulationCommand), "Can only pause while running."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
state.Phase = SimPhase.Paused;
|
|
changeList.Add(new SimulationPausedEvent());
|
|
}
|
|
}
|
|
|
|
public class ResumeSimulationCommand : WorldCommand
|
|
{
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase != SimPhase.Paused)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(ResumeSimulationCommand), "Can only resume from Paused phase."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
state.Phase = SimPhase.Running;
|
|
changeList.Add(new SimulationResumedEvent());
|
|
}
|
|
}
|
|
|
|
public class StepSimulationCommand : WorldCommand
|
|
{
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase == SimPhase.Edit && state.Pieces.Count == 0)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(StepSimulationCommand), "Place at least one piece before stepping."));
|
|
|
|
if (state.Phase != SimPhase.Edit && state.Phase != SimPhase.Running && state.Phase != SimPhase.Paused)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(StepSimulationCommand), "Cannot step in current phase."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
if (state.Phase == SimPhase.Edit)
|
|
state.Phase = SimPhase.Paused;
|
|
|
|
TurnExecutor.ExecuteTurn(state, changeList);
|
|
|
|
// After a step, remain in Paused unless victory/defeat/collision occurred
|
|
if (state.Phase == SimPhase.Running)
|
|
state.Phase = SimPhase.Paused;
|
|
}
|
|
}
|
|
|
|
public class StopSimulationCommand : WorldCommand
|
|
{
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
if (state.Phase == SimPhase.Edit)
|
|
throw new CommandRejectedException(
|
|
new CommandRejectedEvent(nameof(StopSimulationCommand), "Already in Edit phase."));
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
foreach (var piece in state.Pieces)
|
|
{
|
|
piece.CurrentCell = piece.StartCell;
|
|
piece.Cargo = null;
|
|
}
|
|
|
|
foreach (var pos in state.ProductionBuffers.Keys.ToList())
|
|
state.ProductionBuffers[pos] = null;
|
|
|
|
foreach (var demand in state.Demands.Values)
|
|
demand.ReceivedCount = 0;
|
|
|
|
state.TurnNumber = 0;
|
|
state.Phase = SimPhase.Edit;
|
|
|
|
changeList.Add(new SimulationStoppedEvent());
|
|
}
|
|
}
|
|
|
|
public class ResetLevelCommand : WorldCommand
|
|
{
|
|
public override void AssertApplicationConditions(BoardState state)
|
|
{
|
|
// Reset is always valid
|
|
}
|
|
|
|
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
state.ResetFromLevel();
|
|
changeList.Add(new LevelResetEvent());
|
|
}
|
|
}
|