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.
184 lines
6.6 KiB
C#
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;
|
|
}
|
|
}
|