DemandDef gains optional ConsumptionPerTurn and SustainTurns. When ConsumptionPerTurn > 0 the demand maintains a buffer filled by deliveries and drained each turn. Shortage fires the first turn the buffer can't cover consumption; it clears when the buffer refills. SustainedTurns counts consecutive non-shortage turns, and IsSatisfied flips to true once it meets SustainTurns — so the victory condition becomes "no shortage for N consecutive turns" as soon as a mission opts in. Classic demands (ConsumptionPerTurn = 0) behave exactly as before. TurnExecutor runs the consumption sub-phase after transfers. Two new events (DemandShortageStarted / DemandShortageCleared) let the presentation surface the state later. BoardSnapshot + CampaignLoader carry the new fields; no existing mission opts in yet, so campaign_01.json is unaffected.
183 lines
6.5 KiB
C#
183 lines
6.5 KiB
C#
using Chessistics.Engine.Events;
|
|
using Chessistics.Engine.Model;
|
|
using Chessistics.Engine.Rules;
|
|
|
|
namespace Chessistics.Engine.Simulation;
|
|
|
|
public static class TurnExecutor
|
|
{
|
|
public static void ExecuteTurn(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
state.TurnNumber++;
|
|
changeList.Add(new TurnStartedEvent(state.TurnNumber));
|
|
|
|
// Sub-phase 1: PRODUCTION
|
|
ExecuteProduction(state, changeList);
|
|
|
|
// Sub-phase 2: TRANSFORMATION (convert accumulated input → output)
|
|
ExecuteTransformation(state, changeList);
|
|
|
|
// Sub-phase 3: TRANSFERS
|
|
var transferEvents = TransferResolver.ResolveTransfers(state);
|
|
changeList.AddRange(transferEvents);
|
|
|
|
// Sub-phase 4: MOVEMENT
|
|
ExecuteMovement(state, changeList);
|
|
|
|
// Sub-phase 4b: RECURRING DEMAND CONSUMPTION
|
|
ExecuteRecurringConsumption(state, changeList);
|
|
|
|
// Sub-phase 5: COLLISION RESOLUTION
|
|
var collisions = CollisionResolver.ResolveCollisions(state.Pieces);
|
|
foreach (var (survivor, destroyed, cell) in collisions)
|
|
{
|
|
foreach (var victim in destroyed)
|
|
{
|
|
state.Pieces.Remove(victim);
|
|
victim.Cargo = null;
|
|
|
|
// Return piece to stock instead of destroying permanently
|
|
state.RemainingStock[victim.Kind] = state.RemainingStock.GetValueOrDefault(victim.Kind) + 1;
|
|
changeList.Add(new PieceReturnedToStockEvent(
|
|
state.TurnNumber, victim.Id, victim.Kind, survivor?.Id, cell));
|
|
}
|
|
}
|
|
|
|
// Auto-pause on collision
|
|
if (collisions.Count > 0)
|
|
{
|
|
state.Phase = SimPhase.Paused;
|
|
changeList.Add(new SimulationPausedEvent());
|
|
}
|
|
|
|
// Check mission completion
|
|
if (MissionChecker.AllCurrentDemandsMet(state) && state.Demands.Count > 0)
|
|
{
|
|
var campaign = state.Campaign;
|
|
var missionIndex = campaign?.CurrentMissionIndex ?? 0;
|
|
campaign?.CompletedMissions.Add(missionIndex);
|
|
changeList.Add(new MissionCompleteEvent(state.TurnNumber, missionIndex));
|
|
|
|
// Auto-advance to next mission if available (campaign mode)
|
|
if (campaign != null && !campaign.IsLastMission)
|
|
{
|
|
AdvanceToNextMission(state, campaign, changeList);
|
|
// Phase stays Running — simulation continues
|
|
}
|
|
else
|
|
{
|
|
// Last mission or legacy mode — pause
|
|
state.Phase = SimPhase.MissionComplete;
|
|
}
|
|
}
|
|
|
|
changeList.Add(new TurnEndedEvent(state.TurnNumber));
|
|
}
|
|
|
|
private static void AdvanceToNextMission(BoardState state, CampaignState campaign, List<IWorldEvent> changeList)
|
|
{
|
|
campaign.CurrentMissionIndex++;
|
|
var mission = campaign.CurrentMission;
|
|
|
|
var oldWidth = state.Width;
|
|
var oldHeight = state.Height;
|
|
state.ApplyTerrainPatch(mission.TerrainPatch, campaign.CurrentMissionIndex);
|
|
|
|
if (state.Width != oldWidth || state.Height != oldHeight)
|
|
changeList.Add(new TerrainExpandedEvent(state.Width, state.Height, mission.TerrainPatch.Cells));
|
|
|
|
state.AddStock(mission.Stock);
|
|
|
|
foreach (var kind in mission.UnlockedPieces)
|
|
{
|
|
campaign.AvailablePieceKinds.Add(kind);
|
|
changeList.Add(new PieceUnlockedEvent(kind, 1));
|
|
}
|
|
foreach (var upgrade in mission.UnlockedLevels)
|
|
{
|
|
campaign.AvailableLevels.Add(upgrade);
|
|
changeList.Add(new PieceUnlockedEvent(upgrade.Kind, upgrade.Level));
|
|
}
|
|
|
|
changeList.Add(new MissionStartedEvent(campaign.CurrentMissionIndex, state.Width, state.Height));
|
|
}
|
|
|
|
private static void ExecuteMovement(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
var moves = state.Pieces.Select(p => (piece: p, from: p.CurrentCell, to: p.TargetCell)).ToList();
|
|
|
|
foreach (var (piece, from, to) in moves)
|
|
{
|
|
piece.CurrentCell = to;
|
|
state.OccupiedCells.Add(to);
|
|
changeList.Add(new PieceMovedEvent(state.TurnNumber, piece.Id, from, to));
|
|
}
|
|
}
|
|
|
|
private static void ExecuteTransformation(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
foreach (var (pos, transformer) in state.Transformers)
|
|
{
|
|
var inputBuffer = state.TransformerInputBuffers[pos];
|
|
if (inputBuffer >= transformer.InputRequired)
|
|
{
|
|
state.TransformerInputBuffers[pos] = inputBuffer - transformer.InputRequired;
|
|
state.TransformerOutputBuffers[pos] += transformer.OutputAmount;
|
|
changeList.Add(new CargoConvertedEvent(
|
|
state.TurnNumber, pos, transformer.InputCargo, transformer.OutputCargo, transformer.OutputAmount));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ExecuteRecurringConsumption(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
foreach (var demand in state.Demands.Values)
|
|
{
|
|
if (!demand.IsRecurring) continue;
|
|
|
|
// Consume from buffer
|
|
var consumed = Math.Min(demand.Buffer, demand.Definition.ConsumptionPerTurn);
|
|
demand.Buffer -= consumed;
|
|
|
|
var short_ = demand.Buffer <= 0;
|
|
if (short_)
|
|
{
|
|
if (!demand.InShortage)
|
|
{
|
|
demand.InShortage = true;
|
|
changeList.Add(new DemandShortageStartedEvent(
|
|
state.TurnNumber, demand.Position, demand.Name));
|
|
}
|
|
demand.SustainedTurns = 0;
|
|
}
|
|
else
|
|
{
|
|
if (demand.InShortage)
|
|
{
|
|
demand.InShortage = false;
|
|
changeList.Add(new DemandShortageClearedEvent(
|
|
state.TurnNumber, demand.Position, demand.Name));
|
|
}
|
|
demand.SustainedTurns++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ExecuteProduction(BoardState state, List<IWorldEvent> changeList)
|
|
{
|
|
foreach (var (pos, prod) in state.Productions)
|
|
{
|
|
state.ProductionBuffers[pos] = prod.Amount;
|
|
changeList.Add(new CargoProducedEvent(state.TurnNumber, pos, prod.Cargo));
|
|
}
|
|
}
|
|
|
|
private static Metrics ComputeMetrics(BoardState state)
|
|
{
|
|
return new Metrics(
|
|
PiecesUsed: state.Pieces.Count + state.DestroyedPieces.Count,
|
|
TurnsTaken: state.TurnNumber,
|
|
CellsOccupied: state.OccupiedCells.Count
|
|
);
|
|
}
|
|
}
|