From 7ffaa140a88f5db4e9e5fc48f2c074bf9c85aeb3 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 10 May 2026 18:08:03 +0200 Subject: [PATCH] Introduce simulation engine facade --- .../SimulationEngine.cs | 680 +---------------- .../Systems/SimulationCoreSystem.cs | 707 ++++++++++++++++++ 2 files changed, 717 insertions(+), 670 deletions(-) create mode 100644 src/ReactorMaintenance.Simulation/Systems/SimulationCoreSystem.cs diff --git a/src/ReactorMaintenance.Simulation/SimulationEngine.cs b/src/ReactorMaintenance.Simulation/SimulationEngine.cs index 2b03c6e..501938f 100644 --- a/src/ReactorMaintenance.Simulation/SimulationEngine.cs +++ b/src/ReactorMaintenance.Simulation/SimulationEngine.cs @@ -1,706 +1,46 @@ -namespace ReactorMaintenance.Simulation; +namespace ReactorMaintenance.Simulation; public sealed class SimulationEngine { public LevelState MoveRobot(LevelState level, GridPosition destination) { - if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1) - return Refuse(level, "MOVE BLOCKED"); - - return SpendAction(level with { - Robot = level.Robot with { - Position = destination, - HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1) - } - }); + return m_Core.MoveRobot(level, destination); } public LevelState InteractProp(LevelState level) { - if (!CanSpendAction(level)) - return Refuse(level, "NO ACTIONS"); - - var position = level.Robot.Position; - var prop = level.GetProp(position); - if (prop.Type == EPropType.None) - return Refuse(level, "NO PROP"); - - var next = prop.Type switch { - EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop), - EPropType.Junction => CycleJunctionMode(level, position, prop), - EPropType.Door => ToggleDoor(level, position), - EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } }, - EPropType.RemedySupply => PickUpRemedy(level, position, prop), - EPropType.ReactorControl => ActivateReactor(level), - _ => level - }; - - return SpendAction(next); + return m_Core.InteractProp(level); } public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy) { - if (!CanSpendAction(level)) - return Refuse(level, "NO ACTIONS"); - - var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position); - if (leakIndex < 0) - return Refuse(level, "NO REACHABLE LEAK"); - - var leak = level.Leaks[leakIndex]; - var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak); - return SpendAction(next); + return m_Core.InteractLeak(level, carrier, useRemedy); } public LevelState ApplyHeatShield(LevelState level) { - if (!CanSpendAction(level) || level.Robot.HeatShields <= 0) - return Refuse(level, "NO HEAT SHIELD"); - - return SpendAction(level with { - Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps } - }); + return m_Core.ApplyHeatShield(level); } public LevelState ActivateReactor(LevelState level) { - var reactorIndex = level.Reactors.ToList().FindIndex(reactor => reactor.ControlPosition == level.Robot.Position); - if (reactorIndex < 0) - return Refuse(level, "NO REACTOR CONTROL"); - - var reactor = level.Reactors[reactorIndex]; - if (!reactor.Ready) - return Refuse(level, "REACTOR NOT READY"); - - var reactors = level.Reactors.ToArray(); - reactors[reactorIndex] = reactor with { Activated = true }; - return level with { - Reactors = reactors, - Global = level.Global with { LevelState = ELevelState.Won, Status = "REACTOR ONLINE" } - }; + return m_Core.ActivateReactor(level); } public LevelState EndTurn(LevelState level) { - return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } }); + return m_Core.EndTurn(level); } public LevelState AdvanceTurn(LevelState level) { - return ResolveTurn(level); + return m_Core.AdvanceTurn(level); } public IReadOnlyList Forecast(LevelState level) { - var forecasts = new List(); - var simulated = CopyForForecast(level); - - for (var turn = 0; turn <= Balancing.Current.ForecastHorizon; turn++) - { - AddForecasts(forecasts, simulated, turn); - if (simulated.Global.LevelState is ELevelState.Lost or ELevelState.Ready or ELevelState.Won) - break; - - if (turn < Balancing.Current.ForecastHorizon) - simulated = ResolveTurn(simulated, false); - } - - return forecasts.DistinctBy(forecast => (forecast.Kind, forecast.Position, forecast.Message)).OrderBy(forecast => forecast.Turns).ThenBy(forecast => forecast.Message).ToArray(); + return m_Core.Forecast(level); } - private LevelState ResolveTurn(LevelState level, bool refreshForecasts = true) - { - var report = m_Validator.Validate(level); - if (!report.IsValid) - return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } }; - - var next = ApplyRuleEvents(level, ERuleEventPhase.StartOfSimulation); - next = PropagateNetworks(next); - next = ResolveConsumers(next); - next = InjectLeaks(next); - next = ResolveSurfaceInteractions(next); - next = ResolveRobotSafety(next); - next = DeriveReactorAndLevelState(next); - next = ApplyRuleEvents(next, ERuleEventPhase.EndOfTurn); - next = AdvanceDurations(next); - next = next with { - Global = next.Global with { - Turn = next.Global.Turn + 1, - ActionsRemaining = Balancing.Current.ActionsPerTurn - } - }; - - return refreshForecasts ? next with { Forecasts = Forecast(next) } : next; - } - - private LevelState PropagateNetworks(LevelState level) - { - var fuel = ClearTransient(level.Fuel); - var coolant = ClearTransient(level.Coolant); - var electricity = ClearTransient(level.Electricity); - var next = level.WithRuntimeArrays(fuel, coolant, electricity, level.Surface.ToArray(), level.Props.ToArray()); - - foreach (var carrier in Enum.GetValues()) - next = PropagateCarrier(next, carrier); - - return next; - } - - private static UndergroundCell[] ClearTransient(IReadOnlyList layer) - { - return layer.Select(cell => cell with { Amount = 0, Intensity = 0 }).ToArray(); - } - - private LevelState PropagateCarrier(LevelState level, ECarrierType carrier) - { - var layer = level.Layer(carrier).ToArray(); - var sources = AllPositions(level).Where(position => level.GetProp(position) is { Type: EPropType.Flow, SwitchState: EPropSwitchState.Enabled, Carrier: var sourceCarrier } && sourceCarrier == carrier && level.GetUnderground(position, carrier).CarriesFlow).ToArray(); - var junctions = JunctionFlowAnalyzer.Analyze(level).Where(junction => junction.IsValid && junction.Carrier == carrier).ToDictionary(junction => junction.Position); - - foreach (var source in sources) - ApplySourceFlow(level, layer, source, carrier, junctions); - - return carrier switch { - ECarrierType.Fuel => level with { Fuel = layer }, - ECarrierType.Coolant => level with { Coolant = layer }, - ECarrierType.Electricity => level with { Electricity = layer }, - _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") - }; - } - - private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier, IReadOnlyDictionary junctions) - { - var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>(); - var best = new Dictionary(); - open.Enqueue((source, 0, 1, 1)); - best[source] = 1; - - while (open.Count > 0) - { - var current = open.Dequeue(); - var amount = Balancing.Current.ClampValue((Balancing.Current.SourceAmount * current.AmountFactor) - (current.Distance * Balancing.Current.DistanceAmountFalloff)); - var intensity = Balancing.Current.ClampValue((Balancing.Current.SourceIntensity * current.IntensityFactor) - (current.Distance * Balancing.Current.DistanceIntensityFalloff)); - var index = level.Index(current.Position); - layer[index] = layer[index] with { - Amount = Math.Max(layer[index].Amount, amount), - Intensity = Math.Max(layer[index].Intensity, intensity) - }; - - foreach (var next in current.Position.Neighbors().Where(level.InBounds)) - { - if (!level.GetUnderground(next, carrier).CarriesFlow) - continue; - - var weights = BranchWeights(current.Position, next, junctions); - var amountFactor = current.AmountFactor * weights.Amount; - var intensityFactor = current.IntensityFactor * weights.Intensity; - if (amountFactor <= 0 || intensityFactor <= 0) - continue; - - if (best.TryGetValue(next, out var oldBest) && oldBest >= amountFactor) - continue; - - best[next] = amountFactor; - open.Enqueue((next, current.Distance + 1, amountFactor, intensityFactor)); - } - } - } - - private static (float Amount, float Intensity) BranchWeights(GridPosition from, GridPosition to, IReadOnlyDictionary junctions) - { - if (!junctions.TryGetValue(from, out var junction)) - return (1, 1); - - var weight = junction.WeightFor(to); - return (weight, weight); - } - - private LevelState ResolveConsumers(LevelState level) - { - var props = level.Props.ToArray(); - foreach (var position in AllPositions(level)) - { - var index = level.Index(position); - var prop = props[index]; - if (prop.Type != EPropType.Consumer) - continue; - - if (prop.SwitchState == EPropSwitchState.Disabled) - { - props[index] = prop with { ServiceState = EConsumerServiceState.Disabled }; - continue; - } - - var underground = level.GetUnderground(position, prop.Carrier); - var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity; - props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved }; - } - - return level with { Props = props }; - } - - private LevelState InjectLeaks(LevelState level) - { - var surface = level.Surface.ToArray(); - foreach (var leak in level.Leaks.Where(leak => !leak.Repaired)) - { - var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier); - if (underground.State != EUndergroundState.Leaking) - continue; - - var accessIndex = level.Index(leak.AccessPosition); - if (surface[accessIndex].Blocks(leak.Carrier)) - continue; - - var amount = Balancing.Current.LeakBaseAmount + underground.Amount * Balancing.Current.LeakAmountScale + underground.Intensity * Balancing.Current.LeakIntensityScale; - surface[accessIndex] = AddSurfaceCarrier(surface[accessIndex], leak.Carrier, amount); - } - - return level with { Surface = surface.Select(cell => cell.Clamp()).ToArray() }; - } - - private LevelState ResolveSurfaceInteractions(LevelState level) - { - var deltas = Enumerable.Range(0, level.Width * level.Height).Select(_ => new SurfaceDelta()).ToArray(); - foreach (var position in AllPositions(level).Where(level.IsFloor)) - ApplySameCellInteractions(level, position, deltas); - - foreach (var position in AllPositions(level).Where(level.IsFloor)) - { - foreach (var neighbor in position.Neighbors().Where(level.IsFloor)) - { - if (level.Index(position) >= level.Index(neighbor) || level.IsClosedDoorEdge(position, neighbor)) - continue; - - ApplyAdjacentInteractions(level, position, neighbor, deltas); - } - } - - var surface = level.Surface.ToArray(); - for (var i = 0; i < surface.Length; i++) - surface[i] = deltas[i].Apply(surface[i]).Clamp(); - - return level with { Surface = surface }; - } - - private static void ApplySameCellInteractions(LevelState level, GridPosition position, SurfaceDelta[] deltas) - { - var surface = level.GetSurface(position); - ApplyPair(level, position, position, ECarrierType.Fuel, BandFuel(surface.Fuel), ECarrierType.Electricity, BandElectricity(surface.Electricity), deltas); - ApplyPair(level, position, position, ECarrierType.Fuel, BandFuel(surface.Fuel), null, BandHeat(surface.Heat), deltas); - ApplyPair(level, position, position, ECarrierType.Coolant, BandCoolant(surface.Coolant), ECarrierType.Electricity, BandElectricity(surface.Electricity), deltas); - ApplyPair(level, position, position, ECarrierType.Coolant, BandCoolant(surface.Coolant), null, BandHeat(surface.Heat), deltas); - } - - private static void ApplyAdjacentInteractions(LevelState level, GridPosition a, GridPosition b, SurfaceDelta[] deltas) - { - var surfaceA = level.GetSurface(a); - var surfaceB = level.GetSurface(b); - FlowBetween(level, a, b, surfaceA.Fuel, surfaceB.Fuel, Balancing.Current.FlowInteraction(ESurfaceQuantity.Fuel), deltas); - FlowBetween(level, a, b, surfaceA.Coolant, surfaceB.Coolant, Balancing.Current.FlowInteraction(ESurfaceQuantity.Coolant), deltas); - FlowBetween(level, a, b, surfaceA.Electricity, surfaceB.Electricity, Balancing.Current.FlowInteraction(ESurfaceQuantity.Electricity), deltas); - FlowBetween(level, a, b, surfaceA.Heat, surfaceB.Heat, Balancing.Current.FlowInteraction(ESurfaceQuantity.Heat), deltas); - } - - private static void ApplyPair(LevelState level, GridPosition a, GridPosition b, ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand, SurfaceDelta[] deltas) - { - ApplyEffect(level, a, Balancing.Current.SameCellInteraction(rowCarrier, rowBand, colCarrier, colBand), deltas); - } - - private static void ApplyEffect(LevelState level, GridPosition position, SurfaceInteractionEffect effect, SurfaceDelta[] deltas) - { - var index = level.Index(position); - switch (effect.Verb) - { - case ESurfaceInteractionVerb.Warm: - deltas[index].Heat += effect.Amount; - break; - case ESurfaceInteractionVerb.Quench: - deltas[index].Heat -= effect.Amount; - break; - case ESurfaceInteractionVerb.Short: - deltas[index].Heat += effect.Amount; - deltas[index].Electricity -= effect.SecondaryAmount; - break; - case ESurfaceInteractionVerb.Ignite: - deltas[index].Heat += effect.Amount; - deltas[index].Fuel -= effect.SecondaryAmount; - break; - } - } - - private static void FlowBetween(LevelState level, GridPosition a, GridPosition b, float valueA, float valueB, SurfaceInteractionEffect effect, SurfaceDelta[] deltas) - { - var difference = valueA - valueB; - if (Math.Abs(difference) < 0.01f) - return; - - var amount = difference * effect.Amount; - var indexA = level.Index(a); - var indexB = level.Index(b); - - switch (effect.Quantity) - { - case ESurfaceQuantity.Fuel: - deltas[indexA].Fuel -= amount; - deltas[indexB].Fuel += amount; - break; - case ESurfaceQuantity.Coolant: - deltas[indexA].Coolant -= amount; - deltas[indexB].Coolant += amount; - break; - case ESurfaceQuantity.Electricity: - deltas[indexA].Electricity -= amount; - deltas[indexB].Electricity += amount; - break; - case ESurfaceQuantity.Heat: - deltas[indexA].Heat -= amount; - deltas[indexB].Heat += amount; - break; - } - } - - private LevelState ResolveRobotSafety(LevelState level) - { - var surface = level.GetSurface(level.Robot.Position); - var unsafeElement = surface.Fuel >= Balancing.Current.RobotFuelSafetyThreshold || surface.Coolant >= Balancing.Current.RobotCoolantSafetyThreshold || surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold; - var unsafeHeat = surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold && level.Robot.HeatImmunitySteps <= 0; - return unsafeElement || unsafeHeat - ? level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "ROBOT LOST" } } - : level; - } - - private LevelState DeriveReactorAndLevelState(LevelState level) - { - if (level.Global.LevelState is ELevelState.Lost or ELevelState.Won) - return level; - - var reactors = level.Reactors.Select(reactor => reactor with { Ready = IsReactorReady(level, reactor) }).ToArray(); - if (reactors.Any(reactor => reactor.Ready)) - return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Ready, Status = "REACTOR READY" } }; - - var maxHeat = level.Surface.Where((_, index) => level.Terrain[index] == ECellTerrain.Floor).Select(surface => surface.Heat).DefaultIfEmpty(0).Max(); - if (maxHeat >= Balancing.Current.TerminalHeat) - return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } }; - - var hasCritical = level.Surface.Any(surface => BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical); - var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired); - var state = hasCritical ? ELevelState.Critical : - hasCaution ? ELevelState.Caution : ELevelState.Stable; - return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } }; - } - - private static bool IsReactorReady(LevelState level, ReactorBinding reactor) - { - return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel) - && HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant) - && HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity) - && level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat; - } - - private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier) - { - return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier; - } - - private static bool ReactorMatches(LevelState level, RulePredicate predicate, Func selector) - { - return level.Reactors.Any(reactor => MatchesReactorId(reactor, predicate.ReactorId) && selector(reactor)) == predicate.BoolValue; - } - - private static bool MatchesReactorId(ReactorBinding reactor, int reactorId) - { - return reactorId <= 0 || reactor.ReactorId == reactorId; - } - - private static bool ReactorWonMatches(LevelState level, RulePredicate predicate) - { - var won = predicate.ReactorId > 0 - ? level.Reactors.Any(reactor => reactor.ReactorId == predicate.ReactorId && reactor.Activated) - : level.Global.LevelState == ELevelState.Won || level.Reactors.Any(reactor => reactor.Activated); - return won == predicate.BoolValue; - } - - private LevelState ApplyRuleEvents(LevelState level, ERuleEventPhase phase) - { - var next = level; - var events = level.RuleEvents.Select((ruleEvent, index) => (Event: ruleEvent, Index: index)).Where(item => item.Event.Enabled && item.Event.Phase == phase && (item.Event.Repeat || !item.Event.Triggered)).OrderBy(item => item.Event.Priority).ToArray(); - var ruleEvents = next.RuleEvents.ToArray(); - - foreach (var item in events) - { - if (!item.Event.Predicates.All(predicate => PredicateMatches(next, predicate))) - continue; - - foreach (var effect in item.Event.Effects) - next = ApplyRuleEffect(next, effect); - - ruleEvents[item.Index] = item.Event with { Triggered = true }; - } - - return next with { RuleEvents = ruleEvents }; - } - - private static bool PredicateMatches(LevelState level, RulePredicate predicate) - { - return predicate.Kind switch { - ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn, - ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState, - ERulePredicateKind.ReactorReadyIs => ReactorMatches(level, predicate, reactor => reactor.Ready), - ERulePredicateKind.ReactorLostIs => (level.Global.LevelState == ELevelState.Lost) == predicate.BoolValue, - ERulePredicateKind.ReactorWonIs => ReactorWonMatches(level, predicate), - ERulePredicateKind.PropStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).SwitchState == predicate.PropSwitchState, - ERulePredicateKind.ConsumerStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).ServiceState == predicate.ConsumerServiceState, - ERulePredicateKind.NetworkBandAt => level.InBounds(predicate.Position) && NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band, - ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SurfaceBand(level.GetSurface(predicate.Position), predicate.Carrier) >= predicate.Band, - ERulePredicateKind.RobotAt => level.Robot.Position == predicate.Position, - ERulePredicateKind.RobotInventoryAtLeast => level.Robot.Count(predicate.Remedy) >= predicate.InventoryCount, - ERulePredicateKind.AllSeeingEyeUnlocked => level.Global.AllSeeingEyeUnlocked == predicate.BoolValue, - _ => false - }; - } - - private static LevelState ApplyRuleEffect(LevelState level, RuleEffect effect) - { - return effect.Kind switch { - ERuleEffectKind.StartLeak => StartLeak(level, effect), - ERuleEffectKind.WorsenLeak => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }), - ERuleEffectKind.RepairNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Intact }), - ERuleEffectKind.DisableNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, new()), - ERuleEffectKind.SetPropEnabled => level.SetProp(effect.Position, level.GetProp(effect.Position) with { SwitchState = effect.PropSwitchState }), - ERuleEffectKind.AddSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)), - ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, -effect.Amount)), - ERuleEffectKind.AddHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat + effect.Amount }), - ERuleEffectKind.RemoveHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat - effect.Amount }), - ERuleEffectKind.AddInventory => level with { Robot = level.Robot.Add(effect.Remedy, (int)effect.Amount) }, - ERuleEffectKind.RemoveInventory => level with { Robot = level.Robot.Add(effect.Remedy, -(int)effect.Amount) }, - ERuleEffectKind.MarkTerminalLoss => level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = string.IsNullOrWhiteSpace(effect.Message) ? "TERMINAL FAILURE" : effect.Message } }, - ERuleEffectKind.EmitWarning => level with { Global = level.Global with { Warnings = [.. level.Global.Warnings, effect.Message] } }, - _ => level - }; - } - - private static LevelState StartLeak(LevelState level, RuleEffect effect) - { - var leak = new LeakState { - Carrier = effect.Carrier, - UndergroundPosition = effect.Position, - AccessPosition = effect.AccessPosition ?? effect.Position - }; - return level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }) with { Leaks = [.. level.Leaks, leak] }; - } - - private LevelState AdvanceDurations(LevelState level) - { - var surface = level.Surface.Select(cell => cell with { - FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1), - CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1), - ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1) - }) - .ToArray(); - - return level with { Surface = surface }; - } - - private static LevelState ToggleProp(LevelState level, GridPosition position, PropState prop) - { - var switchState = prop.SwitchState == EPropSwitchState.Enabled ? EPropSwitchState.Disabled : EPropSwitchState.Enabled; - return level.SetProp(position, prop with { SwitchState = switchState }); - } - - private static LevelState ToggleDoor(LevelState level, GridPosition position) - { - var doors = level.Doors.ToArray(); - var index = Array.FindIndex(doors, door => door.A == position || door.B == position); - if (index < 0) - return level; - - doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open }; - return level with { Doors = doors }; - } - - private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop) - { - if (prop.Depleted || level.Robot.Count(prop.RemedyType) >= Balancing.Current.InventoryCapacityPerRemedy) - return level; - - return level.SetProp(position, prop with { Depleted = true }) with { Robot = level.Robot.Add(prop.RemedyType, 1) }; - } - - private static LevelState RepairLeak(LevelState level, int leakIndex, LeakState leak) - { - var leaks = level.Leaks.ToArray(); - leaks[leakIndex] = leak with { Repaired = true }; - return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks }; - } - - private static LevelState ApplyElementRemedy(LevelState level, LeakState leak) - { - var remedy = leak.Carrier switch { - ECarrierType.Fuel => ERemedyType.FuelNeutralizer, - ECarrierType.Coolant => ERemedyType.CoolantNeutralizer, - ECarrierType.Electricity => ERemedyType.ElectricityNeutralizer, - _ => throw new ArgumentOutOfRangeException(nameof(leak), leak.Carrier, "Unsupported leak carrier.") - }; - - if (level.Robot.Count(remedy) <= 0) - return Refuse(level, "NO REMEDY"); - - var surface = RemoveSurfaceCarrier(level.GetSurface(leak.AccessPosition), leak.Carrier); - return level.SetSurface(leak.AccessPosition, surface) with { Robot = level.Robot.Spend(remedy) }; - } - - private static SurfaceState AddSurfaceCarrier(SurfaceState surface, ECarrierType carrier, float amount) - { - return carrier switch { - ECarrierType.Fuel => surface with { Fuel = surface.Fuel + amount }, - ECarrierType.Coolant => surface with { Coolant = surface.Coolant + amount }, - ECarrierType.Electricity => surface with { Electricity = surface.Electricity + amount }, - _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") - }; - } - - private static SurfaceState RemoveSurfaceCarrier(SurfaceState surface, ECarrierType carrier) - { - return carrier switch { - ECarrierType.Fuel => surface with { Fuel = 0, FuelBlockTurns = Balancing.Current.RemedyBlockTurns }, - ECarrierType.Coolant => surface with { Coolant = 0, CoolantBlockTurns = Balancing.Current.RemedyBlockTurns }, - ECarrierType.Electricity => surface with { Electricity = 0, ElectricityBlockTurns = Balancing.Current.RemedyBlockTurns }, - _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") - }; - } - - private LevelState SpendAction(LevelState level) - { - var actions = Math.Max(0, level.Global.ActionsRemaining - 1); - var next = level with { Global = level.Global with { ActionsRemaining = actions } }; - return actions == 0 ? ResolveTurn(next) : next; - } - - private static bool CanSpendAction(LevelState level) - { - return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0; - } - - private static LevelState Refuse(LevelState level, string message) - { - return level with { Global = level.Global with { Status = message } }; - } - - private static LevelState CycleJunctionMode(LevelState level, GridPosition position, PropState prop) - { - var flow = JunctionFlowAnalyzer.Analyze(level).FirstOrDefault(junction => junction.Position == position); - var outflowCount = flow?.OutgoingBranches.Count ?? 2; - var ratios = Balancing.Current.JunctionRatios(outflowCount); - if (ratios.Count == 0) - return level; - - return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count }); - } - - private static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier) - { - return carrier switch { - ECarrierType.Fuel => BandFuel(surface.Fuel), - ECarrierType.Coolant => BandCoolant(surface.Coolant), - ECarrierType.Electricity => BandElectricity(surface.Electricity), - _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") - }; - } - - private static EBand NetworkBand(UndergroundCell underground, ECarrierType carrier, ENetworkValueKind valueKind) - { - var value = valueKind == ENetworkValueKind.Amount ? underground.Amount : underground.Intensity; - return carrier switch { - ECarrierType.Fuel => BandFuel(value), - ECarrierType.Coolant => BandCoolant(value), - ECarrierType.Electricity => BandElectricity(value), - _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") - }; - } - - private static EBand BandFuel(float value) - { - return Balancing.Current.Band(value, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical); - } - - private static EBand BandCoolant(float value) - { - return Balancing.Current.Band(value, Balancing.Current.CoolantCaution, Balancing.Current.CoolantCritical); - } - - private static EBand BandElectricity(float value) - { - return Balancing.Current.Band(value, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical); - } - - private static EBand BandHeat(float value) - { - return Balancing.Current.Band(value, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical); - } - - private static LevelState CopyForForecast(LevelState level) - { - return level with { - Terrain = level.Terrain.ToArray(), - Fuel = level.Fuel.ToArray(), - Coolant = level.Coolant.ToArray(), - Electricity = level.Electricity.ToArray(), - Surface = level.Surface.ToArray(), - Props = level.Props.ToArray(), - Forecasts = Array.Empty() - }; - } - - private static void AddForecasts(List forecasts, LevelState level, int turn) - { - if (level.Global.LevelState == ELevelState.Lost) - forecasts.Add(new(EForecastKind.TerminalLoss, level.Robot.Position, turn, level.Global.Status)); - - if (level.Global.LevelState == ELevelState.Ready) - forecasts.Add(new(EForecastKind.ReactorReady, null, turn, "REACTOR READY")); - - foreach (var position in AllPositions(level)) - { - var prop = level.GetProp(position); - if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved) - forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved")); - - var surface = level.GetSurface(position); - if (BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical) - forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard")); - } - - foreach (var ruleEvent in level.RuleEvents.Where(ruleEvent => ruleEvent.Enabled && !string.IsNullOrWhiteSpace(ruleEvent.ForecastText) && ruleEvent.Predicates.All(predicate => PredicateMatches(level, predicate)))) - forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText)); - } - - private static IEnumerable AllPositions(LevelState level) - { - for (var y = 0; y < level.Height; y++) - { - for (var x = 0; x < level.Width; x++) - yield return new(x, y); - } - } - - private sealed class SurfaceDelta - { - public SurfaceState Apply(SurfaceState surface) - { - return surface with { - Fuel = surface.Fuel + Fuel, - Coolant = surface.Coolant + Coolant, - Electricity = surface.Electricity + Electricity, - Heat = surface.Heat + Heat - }; - } - - public float Fuel { get; set; } - public float Coolant { get; set; } - public float Electricity { get; set; } - public float Heat { get; set; } - } - - private readonly LevelValidator m_Validator = new(); + private readonly SimulationCoreSystem m_Core = new(); } diff --git a/src/ReactorMaintenance.Simulation/Systems/SimulationCoreSystem.cs b/src/ReactorMaintenance.Simulation/Systems/SimulationCoreSystem.cs new file mode 100644 index 0000000..7ea8559 --- /dev/null +++ b/src/ReactorMaintenance.Simulation/Systems/SimulationCoreSystem.cs @@ -0,0 +1,707 @@ +namespace ReactorMaintenance.Simulation; + +internal sealed class SimulationCoreSystem +{ + public LevelState MoveRobot(LevelState level, GridPosition destination) + { + if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1) + return Refuse(level, "MOVE BLOCKED"); + + return SpendAction(level with { + Robot = level.Robot with { + Position = destination, + HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1) + } + }); + } + + public LevelState InteractProp(LevelState level) + { + if (!CanSpendAction(level)) + return Refuse(level, "NO ACTIONS"); + + var position = level.Robot.Position; + var prop = level.GetProp(position); + if (prop.Type == EPropType.None) + return Refuse(level, "NO PROP"); + + var next = prop.Type switch { + EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop), + EPropType.Junction => CycleJunctionMode(level, position, prop), + EPropType.Door => ToggleDoor(level, position), + EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } }, + EPropType.RemedySupply => PickUpRemedy(level, position, prop), + EPropType.ReactorControl => ActivateReactor(level), + _ => level + }; + + return SpendAction(next); + } + + public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy) + { + if (!CanSpendAction(level)) + return Refuse(level, "NO ACTIONS"); + + var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position); + if (leakIndex < 0) + return Refuse(level, "NO REACHABLE LEAK"); + + var leak = level.Leaks[leakIndex]; + var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak); + return SpendAction(next); + } + + public LevelState ApplyHeatShield(LevelState level) + { + if (!CanSpendAction(level) || level.Robot.HeatShields <= 0) + return Refuse(level, "NO HEAT SHIELD"); + + return SpendAction(level with { + Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps } + }); + } + + public LevelState ActivateReactor(LevelState level) + { + var reactorIndex = level.Reactors.ToList().FindIndex(reactor => reactor.ControlPosition == level.Robot.Position); + if (reactorIndex < 0) + return Refuse(level, "NO REACTOR CONTROL"); + + var reactor = level.Reactors[reactorIndex]; + if (!reactor.Ready) + return Refuse(level, "REACTOR NOT READY"); + + var reactors = level.Reactors.ToArray(); + reactors[reactorIndex] = reactor with { Activated = true }; + return level with { + Reactors = reactors, + Global = level.Global with { LevelState = ELevelState.Won, Status = "REACTOR ONLINE" } + }; + } + + public LevelState EndTurn(LevelState level) + { + return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } }); + } + + public LevelState AdvanceTurn(LevelState level) + { + return ResolveTurn(level); + } + + public IReadOnlyList Forecast(LevelState level) + { + var forecasts = new List(); + var simulated = CopyForForecast(level); + + for (var turn = 0; turn <= Balancing.Current.ForecastHorizon; turn++) + { + AddForecasts(forecasts, simulated, turn); + if (simulated.Global.LevelState is ELevelState.Lost or ELevelState.Ready or ELevelState.Won) + break; + + if (turn < Balancing.Current.ForecastHorizon) + simulated = ResolveTurn(simulated, false); + } + + return forecasts.DistinctBy(forecast => (forecast.Kind, forecast.Position, forecast.Message)).OrderBy(forecast => forecast.Turns).ThenBy(forecast => forecast.Message).ToArray(); + } + + private LevelState ResolveTurn(LevelState level, bool refreshForecasts = true) + { + var report = m_Validator.Validate(level); + if (!report.IsValid) + return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } }; + + var next = ApplyRuleEvents(level, ERuleEventPhase.StartOfSimulation); + next = PropagateNetworks(next); + next = ResolveConsumers(next); + next = InjectLeaks(next); + next = ResolveSurfaceInteractions(next); + next = ResolveRobotSafety(next); + next = DeriveReactorAndLevelState(next); + next = ApplyRuleEvents(next, ERuleEventPhase.EndOfTurn); + next = AdvanceDurations(next); + next = next with { + Global = next.Global with { + Turn = next.Global.Turn + 1, + ActionsRemaining = Balancing.Current.ActionsPerTurn + } + }; + + return refreshForecasts ? next with { Forecasts = Forecast(next) } : next; + } + + private LevelState PropagateNetworks(LevelState level) + { + var fuel = ClearTransient(level.Fuel); + var coolant = ClearTransient(level.Coolant); + var electricity = ClearTransient(level.Electricity); + var next = level.WithRuntimeArrays(fuel, coolant, electricity, level.Surface.ToArray(), level.Props.ToArray()); + + foreach (var carrier in Enum.GetValues()) + next = PropagateCarrier(next, carrier); + + return next; + } + + private static UndergroundCell[] ClearTransient(IReadOnlyList layer) + { + return layer.Select(cell => cell with { Amount = 0, Intensity = 0 }).ToArray(); + } + + private LevelState PropagateCarrier(LevelState level, ECarrierType carrier) + { + var layer = level.Layer(carrier).ToArray(); + var sources = AllPositions(level).Where(position => level.GetProp(position) is { Type: EPropType.Flow, SwitchState: EPropSwitchState.Enabled, Carrier: var sourceCarrier } && sourceCarrier == carrier && level.GetUnderground(position, carrier).CarriesFlow).ToArray(); + var junctions = JunctionFlowAnalyzer.Analyze(level).Where(junction => junction.IsValid && junction.Carrier == carrier).ToDictionary(junction => junction.Position); + + foreach (var source in sources) + ApplySourceFlow(level, layer, source, carrier, junctions); + + return carrier switch { + ECarrierType.Fuel => level with { Fuel = layer }, + ECarrierType.Coolant => level with { Coolant = layer }, + ECarrierType.Electricity => level with { Electricity = layer }, + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + } + + private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier, IReadOnlyDictionary junctions) + { + var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>(); + var best = new Dictionary(); + open.Enqueue((source, 0, 1, 1)); + best[source] = 1; + + while (open.Count > 0) + { + var current = open.Dequeue(); + var amount = Balancing.Current.ClampValue((Balancing.Current.SourceAmount * current.AmountFactor) - (current.Distance * Balancing.Current.DistanceAmountFalloff)); + var intensity = Balancing.Current.ClampValue((Balancing.Current.SourceIntensity * current.IntensityFactor) - (current.Distance * Balancing.Current.DistanceIntensityFalloff)); + var index = level.Index(current.Position); + layer[index] = layer[index] with { + Amount = Math.Max(layer[index].Amount, amount), + Intensity = Math.Max(layer[index].Intensity, intensity) + }; + + foreach (var next in current.Position.Neighbors().Where(level.InBounds)) + { + if (!level.GetUnderground(next, carrier).CarriesFlow) + continue; + + var weights = BranchWeights(current.Position, next, junctions); + var amountFactor = current.AmountFactor * weights.Amount; + var intensityFactor = current.IntensityFactor * weights.Intensity; + if (amountFactor <= 0 || intensityFactor <= 0) + continue; + + if (best.TryGetValue(next, out var oldBest) && oldBest >= amountFactor) + continue; + + best[next] = amountFactor; + open.Enqueue((next, current.Distance + 1, amountFactor, intensityFactor)); + } + } + } + + private static (float Amount, float Intensity) BranchWeights(GridPosition from, GridPosition to, IReadOnlyDictionary junctions) + { + if (!junctions.TryGetValue(from, out var junction)) + return (1, 1); + + var weight = junction.WeightFor(to); + return (weight, weight); + } + + private LevelState ResolveConsumers(LevelState level) + { + var props = level.Props.ToArray(); + foreach (var position in AllPositions(level)) + { + var index = level.Index(position); + var prop = props[index]; + if (prop.Type != EPropType.Consumer) + continue; + + if (prop.SwitchState == EPropSwitchState.Disabled) + { + props[index] = prop with { ServiceState = EConsumerServiceState.Disabled }; + continue; + } + + var underground = level.GetUnderground(position, prop.Carrier); + var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity; + props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved }; + } + + return level with { Props = props }; + } + + private LevelState InjectLeaks(LevelState level) + { + var surface = level.Surface.ToArray(); + foreach (var leak in level.Leaks.Where(leak => !leak.Repaired)) + { + var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier); + if (underground.State != EUndergroundState.Leaking) + continue; + + var accessIndex = level.Index(leak.AccessPosition); + if (surface[accessIndex].Blocks(leak.Carrier)) + continue; + + var amount = Balancing.Current.LeakBaseAmount + underground.Amount * Balancing.Current.LeakAmountScale + underground.Intensity * Balancing.Current.LeakIntensityScale; + surface[accessIndex] = AddSurfaceCarrier(surface[accessIndex], leak.Carrier, amount); + } + + return level with { Surface = surface.Select(cell => cell.Clamp()).ToArray() }; + } + + private LevelState ResolveSurfaceInteractions(LevelState level) + { + var deltas = Enumerable.Range(0, level.Width * level.Height).Select(_ => new SurfaceDelta()).ToArray(); + foreach (var position in AllPositions(level).Where(level.IsFloor)) + ApplySameCellInteractions(level, position, deltas); + + foreach (var position in AllPositions(level).Where(level.IsFloor)) + { + foreach (var neighbor in position.Neighbors().Where(level.IsFloor)) + { + if (level.Index(position) >= level.Index(neighbor) || level.IsClosedDoorEdge(position, neighbor)) + continue; + + ApplyAdjacentInteractions(level, position, neighbor, deltas); + } + } + + var surface = level.Surface.ToArray(); + for (var i = 0; i < surface.Length; i++) + surface[i] = deltas[i].Apply(surface[i]).Clamp(); + + return level with { Surface = surface }; + } + + private static void ApplySameCellInteractions(LevelState level, GridPosition position, SurfaceDelta[] deltas) + { + var surface = level.GetSurface(position); + ApplyPair(level, position, position, ECarrierType.Fuel, BandFuel(surface.Fuel), ECarrierType.Electricity, BandElectricity(surface.Electricity), deltas); + ApplyPair(level, position, position, ECarrierType.Fuel, BandFuel(surface.Fuel), null, BandHeat(surface.Heat), deltas); + ApplyPair(level, position, position, ECarrierType.Coolant, BandCoolant(surface.Coolant), ECarrierType.Electricity, BandElectricity(surface.Electricity), deltas); + ApplyPair(level, position, position, ECarrierType.Coolant, BandCoolant(surface.Coolant), null, BandHeat(surface.Heat), deltas); + } + + private static void ApplyAdjacentInteractions(LevelState level, GridPosition a, GridPosition b, SurfaceDelta[] deltas) + { + var surfaceA = level.GetSurface(a); + var surfaceB = level.GetSurface(b); + FlowBetween(level, a, b, surfaceA.Fuel, surfaceB.Fuel, Balancing.Current.FlowInteraction(ESurfaceQuantity.Fuel), deltas); + FlowBetween(level, a, b, surfaceA.Coolant, surfaceB.Coolant, Balancing.Current.FlowInteraction(ESurfaceQuantity.Coolant), deltas); + FlowBetween(level, a, b, surfaceA.Electricity, surfaceB.Electricity, Balancing.Current.FlowInteraction(ESurfaceQuantity.Electricity), deltas); + FlowBetween(level, a, b, surfaceA.Heat, surfaceB.Heat, Balancing.Current.FlowInteraction(ESurfaceQuantity.Heat), deltas); + } + + private static void ApplyPair(LevelState level, GridPosition a, GridPosition b, ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand, SurfaceDelta[] deltas) + { + ApplyEffect(level, a, Balancing.Current.SameCellInteraction(rowCarrier, rowBand, colCarrier, colBand), deltas); + } + + private static void ApplyEffect(LevelState level, GridPosition position, SurfaceInteractionEffect effect, SurfaceDelta[] deltas) + { + var index = level.Index(position); + switch (effect.Verb) + { + case ESurfaceInteractionVerb.Warm: + deltas[index].Heat += effect.Amount; + break; + case ESurfaceInteractionVerb.Quench: + deltas[index].Heat -= effect.Amount; + break; + case ESurfaceInteractionVerb.Short: + deltas[index].Heat += effect.Amount; + deltas[index].Electricity -= effect.SecondaryAmount; + break; + case ESurfaceInteractionVerb.Ignite: + deltas[index].Heat += effect.Amount; + deltas[index].Fuel -= effect.SecondaryAmount; + break; + } + } + + private static void FlowBetween(LevelState level, GridPosition a, GridPosition b, float valueA, float valueB, SurfaceInteractionEffect effect, SurfaceDelta[] deltas) + { + var difference = valueA - valueB; + if (Math.Abs(difference) < 0.01f) + return; + + var amount = difference * effect.Amount; + var indexA = level.Index(a); + var indexB = level.Index(b); + + switch (effect.Quantity) + { + case ESurfaceQuantity.Fuel: + deltas[indexA].Fuel -= amount; + deltas[indexB].Fuel += amount; + break; + case ESurfaceQuantity.Coolant: + deltas[indexA].Coolant -= amount; + deltas[indexB].Coolant += amount; + break; + case ESurfaceQuantity.Electricity: + deltas[indexA].Electricity -= amount; + deltas[indexB].Electricity += amount; + break; + case ESurfaceQuantity.Heat: + deltas[indexA].Heat -= amount; + deltas[indexB].Heat += amount; + break; + } + } + + private LevelState ResolveRobotSafety(LevelState level) + { + var surface = level.GetSurface(level.Robot.Position); + var unsafeElement = surface.Fuel >= Balancing.Current.RobotFuelSafetyThreshold || surface.Coolant >= Balancing.Current.RobotCoolantSafetyThreshold || surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold; + var unsafeHeat = surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold && level.Robot.HeatImmunitySteps <= 0; + return unsafeElement || unsafeHeat + ? level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "ROBOT LOST" } } + : level; + } + + private LevelState DeriveReactorAndLevelState(LevelState level) + { + if (level.Global.LevelState is ELevelState.Lost or ELevelState.Won) + return level; + + var reactors = level.Reactors.Select(reactor => reactor with { Ready = IsReactorReady(level, reactor) }).ToArray(); + if (reactors.Any(reactor => reactor.Ready)) + return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Ready, Status = "REACTOR READY" } }; + + var maxHeat = level.Surface.Where((_, index) => level.Terrain[index] == ECellTerrain.Floor).Select(surface => surface.Heat).DefaultIfEmpty(0).Max(); + if (maxHeat >= Balancing.Current.TerminalHeat) + return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } }; + + var hasCritical = level.Surface.Any(surface => BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical); + var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired); + var state = hasCritical ? ELevelState.Critical : + hasCaution ? ELevelState.Caution : ELevelState.Stable; + return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } }; + } + + private static bool IsReactorReady(LevelState level, ReactorBinding reactor) + { + return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel) + && HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant) + && HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity) + && level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat; + } + + private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier) + { + return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier; + } + + private static bool ReactorMatches(LevelState level, RulePredicate predicate, Func selector) + { + return level.Reactors.Any(reactor => MatchesReactorId(reactor, predicate.ReactorId) && selector(reactor)) == predicate.BoolValue; + } + + private static bool MatchesReactorId(ReactorBinding reactor, int reactorId) + { + return reactorId <= 0 || reactor.ReactorId == reactorId; + } + + private static bool ReactorWonMatches(LevelState level, RulePredicate predicate) + { + var won = predicate.ReactorId > 0 + ? level.Reactors.Any(reactor => reactor.ReactorId == predicate.ReactorId && reactor.Activated) + : level.Global.LevelState == ELevelState.Won || level.Reactors.Any(reactor => reactor.Activated); + return won == predicate.BoolValue; + } + + private LevelState ApplyRuleEvents(LevelState level, ERuleEventPhase phase) + { + var next = level; + var events = level.RuleEvents.Select((ruleEvent, index) => (Event: ruleEvent, Index: index)).Where(item => item.Event.Enabled && item.Event.Phase == phase && (item.Event.Repeat || !item.Event.Triggered)).OrderBy(item => item.Event.Priority).ToArray(); + var ruleEvents = next.RuleEvents.ToArray(); + + foreach (var item in events) + { + if (!item.Event.Predicates.All(predicate => PredicateMatches(next, predicate))) + continue; + + foreach (var effect in item.Event.Effects) + next = ApplyRuleEffect(next, effect); + + ruleEvents[item.Index] = item.Event with { Triggered = true }; + } + + return next with { RuleEvents = ruleEvents }; + } + + private static bool PredicateMatches(LevelState level, RulePredicate predicate) + { + return predicate.Kind switch { + ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn, + ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState, + ERulePredicateKind.ReactorReadyIs => ReactorMatches(level, predicate, reactor => reactor.Ready), + ERulePredicateKind.ReactorLostIs => (level.Global.LevelState == ELevelState.Lost) == predicate.BoolValue, + ERulePredicateKind.ReactorWonIs => ReactorWonMatches(level, predicate), + ERulePredicateKind.PropStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).SwitchState == predicate.PropSwitchState, + ERulePredicateKind.ConsumerStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).ServiceState == predicate.ConsumerServiceState, + ERulePredicateKind.NetworkBandAt => level.InBounds(predicate.Position) && NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band, + ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SurfaceBand(level.GetSurface(predicate.Position), predicate.Carrier) >= predicate.Band, + ERulePredicateKind.RobotAt => level.Robot.Position == predicate.Position, + ERulePredicateKind.RobotInventoryAtLeast => level.Robot.Count(predicate.Remedy) >= predicate.InventoryCount, + ERulePredicateKind.AllSeeingEyeUnlocked => level.Global.AllSeeingEyeUnlocked == predicate.BoolValue, + _ => false + }; + } + + private static LevelState ApplyRuleEffect(LevelState level, RuleEffect effect) + { + return effect.Kind switch { + ERuleEffectKind.StartLeak => StartLeak(level, effect), + ERuleEffectKind.WorsenLeak => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }), + ERuleEffectKind.RepairNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Intact }), + ERuleEffectKind.DisableNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, new()), + ERuleEffectKind.SetPropEnabled => level.SetProp(effect.Position, level.GetProp(effect.Position) with { SwitchState = effect.PropSwitchState }), + ERuleEffectKind.AddSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)), + ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, -effect.Amount)), + ERuleEffectKind.AddHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat + effect.Amount }), + ERuleEffectKind.RemoveHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat - effect.Amount }), + ERuleEffectKind.AddInventory => level with { Robot = level.Robot.Add(effect.Remedy, (int)effect.Amount) }, + ERuleEffectKind.RemoveInventory => level with { Robot = level.Robot.Add(effect.Remedy, -(int)effect.Amount) }, + ERuleEffectKind.MarkTerminalLoss => level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = string.IsNullOrWhiteSpace(effect.Message) ? "TERMINAL FAILURE" : effect.Message } }, + ERuleEffectKind.EmitWarning => level with { Global = level.Global with { Warnings = [.. level.Global.Warnings, effect.Message] } }, + _ => level + }; + } + + private static LevelState StartLeak(LevelState level, RuleEffect effect) + { + var leak = new LeakState { + Carrier = effect.Carrier, + UndergroundPosition = effect.Position, + AccessPosition = effect.AccessPosition ?? effect.Position + }; + return level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }) with { Leaks = [.. level.Leaks, leak] }; + } + + private LevelState AdvanceDurations(LevelState level) + { + var surface = level.Surface.Select(cell => cell with { + FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1), + CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1), + ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1) + }) + .ToArray(); + + return level with { Surface = surface }; + } + + private static LevelState ToggleProp(LevelState level, GridPosition position, PropState prop) + { + var switchState = prop.SwitchState == EPropSwitchState.Enabled ? EPropSwitchState.Disabled : EPropSwitchState.Enabled; + return level.SetProp(position, prop with { SwitchState = switchState }); + } + + private static LevelState ToggleDoor(LevelState level, GridPosition position) + { + var doors = level.Doors.ToArray(); + var index = Array.FindIndex(doors, door => door.A == position || door.B == position); + if (index < 0) + return level; + + doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open }; + return level with { Doors = doors }; + } + + private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop) + { + if (prop.Depleted || level.Robot.Count(prop.RemedyType) >= Balancing.Current.InventoryCapacityPerRemedy) + return level; + + return level.SetProp(position, prop with { Depleted = true }) with { Robot = level.Robot.Add(prop.RemedyType, 1) }; + } + + private static LevelState RepairLeak(LevelState level, int leakIndex, LeakState leak) + { + var leaks = level.Leaks.ToArray(); + leaks[leakIndex] = leak with { Repaired = true }; + return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks }; + } + + private static LevelState ApplyElementRemedy(LevelState level, LeakState leak) + { + var remedy = leak.Carrier switch { + ECarrierType.Fuel => ERemedyType.FuelNeutralizer, + ECarrierType.Coolant => ERemedyType.CoolantNeutralizer, + ECarrierType.Electricity => ERemedyType.ElectricityNeutralizer, + _ => throw new ArgumentOutOfRangeException(nameof(leak), leak.Carrier, "Unsupported leak carrier.") + }; + + if (level.Robot.Count(remedy) <= 0) + return Refuse(level, "NO REMEDY"); + + var surface = RemoveSurfaceCarrier(level.GetSurface(leak.AccessPosition), leak.Carrier); + return level.SetSurface(leak.AccessPosition, surface) with { Robot = level.Robot.Spend(remedy) }; + } + + private static SurfaceState AddSurfaceCarrier(SurfaceState surface, ECarrierType carrier, float amount) + { + return carrier switch { + ECarrierType.Fuel => surface with { Fuel = surface.Fuel + amount }, + ECarrierType.Coolant => surface with { Coolant = surface.Coolant + amount }, + ECarrierType.Electricity => surface with { Electricity = surface.Electricity + amount }, + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + } + + private static SurfaceState RemoveSurfaceCarrier(SurfaceState surface, ECarrierType carrier) + { + return carrier switch { + ECarrierType.Fuel => surface with { Fuel = 0, FuelBlockTurns = Balancing.Current.RemedyBlockTurns }, + ECarrierType.Coolant => surface with { Coolant = 0, CoolantBlockTurns = Balancing.Current.RemedyBlockTurns }, + ECarrierType.Electricity => surface with { Electricity = 0, ElectricityBlockTurns = Balancing.Current.RemedyBlockTurns }, + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + } + + private LevelState SpendAction(LevelState level) + { + var actions = Math.Max(0, level.Global.ActionsRemaining - 1); + var next = level with { Global = level.Global with { ActionsRemaining = actions } }; + return actions == 0 ? ResolveTurn(next) : next; + } + + private static bool CanSpendAction(LevelState level) + { + return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0; + } + + private static LevelState Refuse(LevelState level, string message) + { + return level with { Global = level.Global with { Status = message } }; + } + + private static LevelState CycleJunctionMode(LevelState level, GridPosition position, PropState prop) + { + var flow = JunctionFlowAnalyzer.Analyze(level).FirstOrDefault(junction => junction.Position == position); + var outflowCount = flow?.OutgoingBranches.Count ?? 2; + var ratios = Balancing.Current.JunctionRatios(outflowCount); + if (ratios.Count == 0) + return level; + + return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count }); + } + + private static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier) + { + return carrier switch { + ECarrierType.Fuel => BandFuel(surface.Fuel), + ECarrierType.Coolant => BandCoolant(surface.Coolant), + ECarrierType.Electricity => BandElectricity(surface.Electricity), + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + } + + private static EBand NetworkBand(UndergroundCell underground, ECarrierType carrier, ENetworkValueKind valueKind) + { + var value = valueKind == ENetworkValueKind.Amount ? underground.Amount : underground.Intensity; + return carrier switch { + ECarrierType.Fuel => BandFuel(value), + ECarrierType.Coolant => BandCoolant(value), + ECarrierType.Electricity => BandElectricity(value), + _ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.") + }; + } + + private static EBand BandFuel(float value) + { + return Balancing.Current.Band(value, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical); + } + + private static EBand BandCoolant(float value) + { + return Balancing.Current.Band(value, Balancing.Current.CoolantCaution, Balancing.Current.CoolantCritical); + } + + private static EBand BandElectricity(float value) + { + return Balancing.Current.Band(value, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical); + } + + private static EBand BandHeat(float value) + { + return Balancing.Current.Band(value, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical); + } + + private static LevelState CopyForForecast(LevelState level) + { + return level with { + Terrain = level.Terrain.ToArray(), + Fuel = level.Fuel.ToArray(), + Coolant = level.Coolant.ToArray(), + Electricity = level.Electricity.ToArray(), + Surface = level.Surface.ToArray(), + Props = level.Props.ToArray(), + Forecasts = Array.Empty() + }; + } + + private static void AddForecasts(List forecasts, LevelState level, int turn) + { + if (level.Global.LevelState == ELevelState.Lost) + forecasts.Add(new(EForecastKind.TerminalLoss, level.Robot.Position, turn, level.Global.Status)); + + if (level.Global.LevelState == ELevelState.Ready) + forecasts.Add(new(EForecastKind.ReactorReady, null, turn, "REACTOR READY")); + + foreach (var position in AllPositions(level)) + { + var prop = level.GetProp(position); + if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved) + forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved")); + + var surface = level.GetSurface(position); + if (BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical) + forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard")); + } + + foreach (var ruleEvent in level.RuleEvents.Where(ruleEvent => ruleEvent.Enabled && !string.IsNullOrWhiteSpace(ruleEvent.ForecastText) && ruleEvent.Predicates.All(predicate => PredicateMatches(level, predicate)))) + forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText)); + } + + private static IEnumerable AllPositions(LevelState level) + { + for (var y = 0; y < level.Height; y++) + { + for (var x = 0; x < level.Width; x++) + yield return new(x, y); + } + } + + private sealed class SurfaceDelta + { + public SurfaceState Apply(SurfaceState surface) + { + return surface with { + Fuel = surface.Fuel + Fuel, + Coolant = surface.Coolant + Coolant, + Electricity = surface.Electricity + Electricity, + Heat = surface.Heat + Heat + }; + } + + public float Fuel { get; set; } + public float Coolant { get; set; } + public float Electricity { get; set; } + public float Heat { get; set; } + } + + private readonly LevelValidator m_Validator = new(); +} +