Introduce simulation engine facade
This commit is contained in:
@@ -1,706 +1,46 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
public sealed class SimulationEngine
|
public sealed class SimulationEngine
|
||||||
{
|
{
|
||||||
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
|
return m_Core.MoveRobot(level, destination);
|
||||||
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)
|
public LevelState InteractProp(LevelState level)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level))
|
return m_Core.InteractProp(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)
|
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level))
|
return m_Core.InteractLeak(level, carrier, useRemedy);
|
||||||
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)
|
public LevelState ApplyHeatShield(LevelState level)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level) || level.Robot.HeatShields <= 0)
|
return m_Core.ApplyHeatShield(level);
|
||||||
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)
|
public LevelState ActivateReactor(LevelState level)
|
||||||
{
|
{
|
||||||
var reactorIndex = level.Reactors.ToList().FindIndex(reactor => reactor.ControlPosition == level.Robot.Position);
|
return m_Core.ActivateReactor(level);
|
||||||
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)
|
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)
|
public LevelState AdvanceTurn(LevelState level)
|
||||||
{
|
{
|
||||||
return ResolveTurn(level);
|
return m_Core.AdvanceTurn(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||||
{
|
{
|
||||||
var forecasts = new List<Forecast>();
|
return m_Core.Forecast(level);
|
||||||
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 readonly SimulationCoreSystem m_Core = new();
|
||||||
}
|
|
||||||
|
|
||||||
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<ECarrierType>())
|
|
||||||
next = PropagateCarrier(next, carrier);
|
|
||||||
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UndergroundCell[] ClearTransient(IReadOnlyList<UndergroundCell> 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<GridPosition, JunctionFlow> junctions)
|
|
||||||
{
|
|
||||||
var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>();
|
|
||||||
var best = new Dictionary<GridPosition, float>();
|
|
||||||
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<GridPosition, JunctionFlow> 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<ReactorBinding, bool> 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<Forecast>()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddForecasts(List<Forecast> 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<GridPosition> 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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> Forecast(LevelState level)
|
||||||
|
{
|
||||||
|
var forecasts = new List<Forecast>();
|
||||||
|
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<ECarrierType>())
|
||||||
|
next = PropagateCarrier(next, carrier);
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UndergroundCell[] ClearTransient(IReadOnlyList<UndergroundCell> 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<GridPosition, JunctionFlow> junctions)
|
||||||
|
{
|
||||||
|
var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>();
|
||||||
|
var best = new Dictionary<GridPosition, float>();
|
||||||
|
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<GridPosition, JunctionFlow> 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<ReactorBinding, bool> 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<Forecast>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddForecasts(List<Forecast> 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<GridPosition> 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();
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user