Chessistics/chessistics-engine/Commands/WorldCommands.cs
Samuel Bouchet a7280b1a5a Overhaul turn mechanics, collision destruction, and visual animations
- New turn order: produce -> transfer -> move -> collision resolution
- Collisions now destroy weaker pieces (status > level > mutual destruction)
  instead of halting the simulation. SimPhase.Collision removed.
- Add piece Level property (all level 1 in proto, prepared for future)
- Production fires every turn (interval concept removed), buffer = Amount
  (default 1, future 2-4), leftovers overwritten each turn
- Transfer tiebreaker: status > level > clockwise direction (alternating
  even/odd turns in y-up coords), replaces distance-to-production
- Demands always accept matching cargo even when already satisfied
- TurnNumber added to all turn events for animation grouping
- Simultaneous animations: produce flash, cargo slide, parallel piece moves
- Camera centering fix + middle-click pan
- GDD updated with new rules + lore section added

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 21:44:12 +02:00

246 lines
8.4 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 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.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, 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));
}
/// <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 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)
{
// Restore destroyed pieces
state.Pieces.AddRange(state.DestroyedPieces);
state.DestroyedPieces.Clear();
foreach (var piece in state.Pieces)
{
piece.CurrentCell = piece.StartCell;
piece.Cargo = null;
}
foreach (var pos in state.ProductionBuffers.Keys.ToList())
state.ProductionBuffers[pos] = 0;
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());
}
}