Chessistics/chessistics-engine/Commands/WorldCommands.cs
Samuel Bouchet 2d1aea0a7a Snapshot campaign system progress before automation harness
Bundles in-flight work on the campaign/missions system (CampaignDef,
MissionDef, TerrainPatch, TransformerDef, MissionChecker, CampaignLoader,
FlavorBanner, transformer rules), plan files, and matching tests. Baseline
commit so the upcoming automation testing harness lands on a clean tree.
2026-04-16 21:22:49 +02:00

184 lines
6.6 KiB
C#

using Chessistics.Engine.Events;
using Chessistics.Engine.Model;
using Chessistics.Engine.Rules;
using Chessistics.Engine.Simulation;
namespace Chessistics.Engine.Commands;
/// <summary>
/// Place a piece on the board. Works in any phase (Running or Paused).
/// The placement takes effect between turns.
/// </summary>
public class PlacePieceCommand : WorldCommand
{
public PieceKind Kind { get; }
public Coords Start { get; }
public Coords End { get; }
public int Level { get; }
public PlacePieceCommand(PieceKind kind, Coords start, Coords end, int level = 1)
{
Kind = kind;
Start = start;
End = end;
Level = level;
}
public override void AssertApplicationConditions(BoardState state)
{
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."));
// Check piece kind is unlocked (campaign mode)
if (state.Campaign != null && !state.Campaign.IsPieceAvailable(Kind))
throw new CommandRejectedException(
new PlacementRejectedEvent(Kind, Start, End, $"Piece type {Kind} is not unlocked yet."));
// Check piece level is unlocked (campaign mode)
if (state.Campaign != null && !state.Campaign.IsLevelAvailable(Kind, Level))
throw new CommandRejectedException(
new PlacementRejectedEvent(Kind, Start, End, $"Level {Level} for {Kind} is not unlocked yet."));
}
protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
{
var piece = new PieceState(
state.NextPieceId++, Kind, Start, End, state.Pieces.Count, Level);
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));
}
private static CargoType? InferCargoFilter(BoardState state, PieceState piece)
{
foreach (var (prodPos, prod) in state.Productions)
{
if (piece.StartCell.IsAdjacent4(prodPos) || piece.EndCell.IsAdjacent4(prodPos))
return prod.Cargo;
}
// Transformer output acts like a production
foreach (var (tPos, transformer) in state.Transformers)
{
if (piece.StartCell.IsAdjacent4(tPos) || piece.EndCell.IsAdjacent4(tPos))
return transformer.OutputCargo;
}
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;
}
}
/// <summary>
/// Remove a piece from the board. Works in any phase.
/// </summary>
public class RemovePieceCommand : WorldCommand
{
public int PieceId { get; }
public RemovePieceCommand(int pieceId)
{
PieceId = pieceId;
}
public override void AssertApplicationConditions(BoardState state)
{
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 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 && state.Phase != SimPhase.MissionComplete)
throw new CommandRejectedException(
new CommandRejectedEvent(nameof(ResumeSimulationCommand), "Can only resume from Paused or MissionComplete 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.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)
{
var wasRunning = state.Phase == SimPhase.Running;
TurnExecutor.ExecuteTurn(state, changeList);
// After a manual step (was Paused), remain Paused.
// After an auto-play step (was Running), stay Running unless
// TurnExecutor changed it (collision → Paused, last mission → MissionComplete).
if (!wasRunning && state.Phase == SimPhase.Running)
state.Phase = SimPhase.Paused;
}
}