Expand rule event coverage
This commit is contained in:
@@ -121,14 +121,107 @@ public sealed class LevelValidator
|
||||
{
|
||||
foreach (var ruleEvent in level.RuleEvents)
|
||||
{
|
||||
foreach (var predicate in ruleEvent.Predicates)
|
||||
ValidateRulePredicate(level, predicate, errors);
|
||||
|
||||
foreach (var effect in ruleEvent.Effects)
|
||||
{
|
||||
if (!level.InBounds(effect.Position) && effect.Kind != ERuleEffectKind.EmitWarning && effect.Kind != ERuleEffectKind.MarkTerminalLoss && effect.Kind != ERuleEffectKind.AddInventory)
|
||||
errors.Add(new("Rule effect target is out of bounds.", effect.Position));
|
||||
}
|
||||
ValidateRuleEffect(level, effect, errors);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRulePredicate(LevelState level, RulePredicate predicate, List<ValidationIssue> errors)
|
||||
{
|
||||
switch (predicate.Kind)
|
||||
{
|
||||
case ERulePredicateKind.PropStateAt:
|
||||
if (!level.InBounds(predicate.Position) || level.GetProp(predicate.Position).Type == EPropType.None)
|
||||
errors.Add(new("Rule prop predicate must target a prop.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.ConsumerStateAt:
|
||||
if (!IsProp(level, predicate.Position, EPropType.Consumer))
|
||||
errors.Add(new("Rule consumer predicate must target a consumer prop.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.NetworkBandAt:
|
||||
if (!level.InBounds(predicate.Position) || !level.GetUnderground(predicate.Position, predicate.Carrier).IsPresent)
|
||||
errors.Add(new("Rule network predicate must target an underground cell.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.SurfaceBandAt:
|
||||
case ERulePredicateKind.RobotAt:
|
||||
if (!level.IsFloor(predicate.Position))
|
||||
errors.Add(new("Rule floor predicate must target a floor cell.", predicate.Position));
|
||||
break;
|
||||
case ERulePredicateKind.ReactorReadyIs:
|
||||
case ERulePredicateKind.ReactorWonIs:
|
||||
ValidateOptionalReactorId(level, predicate.ReactorId, predicate.Position, errors);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRuleEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
|
||||
{
|
||||
if (RequiresNonNegativeAmount(effect.Kind) && effect.Amount < 0)
|
||||
errors.Add(new("Rule effect amount must be non-negative.", effect.Position));
|
||||
|
||||
switch (effect.Kind)
|
||||
{
|
||||
case ERuleEffectKind.StartLeak:
|
||||
ValidateRuleLeakEffect(level, effect, errors);
|
||||
break;
|
||||
case ERuleEffectKind.WorsenLeak:
|
||||
case ERuleEffectKind.RepairNetworkCell:
|
||||
case ERuleEffectKind.DisableNetworkCell:
|
||||
if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
|
||||
errors.Add(new("Rule network effect must target an underground cell.", effect.Position));
|
||||
break;
|
||||
case ERuleEffectKind.SetPropEnabled:
|
||||
if (!level.InBounds(effect.Position) || level.GetProp(effect.Position).Type == EPropType.None)
|
||||
errors.Add(new("Rule prop effect must target a prop.", effect.Position));
|
||||
break;
|
||||
case ERuleEffectKind.AddSurfaceHazard:
|
||||
case ERuleEffectKind.RemoveSurfaceHazard:
|
||||
case ERuleEffectKind.AddHeat:
|
||||
case ERuleEffectKind.RemoveHeat:
|
||||
if (!level.IsFloor(effect.Position))
|
||||
errors.Add(new("Rule surface effect must target a floor cell.", effect.Position));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RequiresNonNegativeAmount(ERuleEffectKind kind)
|
||||
{
|
||||
return kind is ERuleEffectKind.AddSurfaceHazard
|
||||
or ERuleEffectKind.RemoveSurfaceHazard
|
||||
or ERuleEffectKind.AddHeat
|
||||
or ERuleEffectKind.RemoveHeat
|
||||
or ERuleEffectKind.AddInventory
|
||||
or ERuleEffectKind.RemoveInventory;
|
||||
}
|
||||
|
||||
private static void ValidateRuleLeakEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
|
||||
{
|
||||
var accessPosition = effect.AccessPosition ?? effect.Position;
|
||||
if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
|
||||
errors.Add(new("Rule leak effect must target an underground cell.", effect.Position));
|
||||
|
||||
if (!level.IsFloor(accessPosition))
|
||||
{
|
||||
errors.Add(new("Rule leak effect must have valid floor access.", accessPosition));
|
||||
return;
|
||||
}
|
||||
|
||||
if (effect.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && effect.Position != accessPosition)
|
||||
errors.Add(new("Rule fuel and coolant leak effects must use their underground coordinate as access.", accessPosition));
|
||||
|
||||
if (effect.Carrier == ECarrierType.Electricity && effect.Position.ManhattanDistance(accessPosition) != 1)
|
||||
errors.Add(new("Rule electricity leak effect access must be an adjacent floor face.", accessPosition));
|
||||
}
|
||||
|
||||
private static void ValidateOptionalReactorId(LevelState level, int reactorId, GridPosition position, List<ValidationIssue> errors)
|
||||
{
|
||||
if (reactorId > 0 && level.Reactors.All(reactor => reactor.ReactorId != reactorId))
|
||||
errors.Add(new("Rule reactor predicate must reference an existing reactor.", position));
|
||||
}
|
||||
|
||||
private static void ValidateWarnings(LevelState level, List<ValidationIssue> warnings)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECellTerrain
|
||||
{
|
||||
@@ -108,10 +108,15 @@ public enum ERulePredicateKind
|
||||
{
|
||||
TurnAtLeast,
|
||||
LevelStateIs,
|
||||
ReactorReadyIs,
|
||||
ReactorLostIs,
|
||||
ReactorWonIs,
|
||||
PropStateAt,
|
||||
ConsumerStateAt,
|
||||
NetworkBandAt,
|
||||
SurfaceBandAt,
|
||||
RobotAt,
|
||||
RobotInventoryAtLeast,
|
||||
AllSeeingEyeUnlocked
|
||||
}
|
||||
|
||||
@@ -123,12 +128,21 @@ public enum ERuleEffectKind
|
||||
DisableNetworkCell,
|
||||
SetPropEnabled,
|
||||
AddSurfaceHazard,
|
||||
RemoveSurfaceHazard,
|
||||
AddHeat,
|
||||
RemoveHeat,
|
||||
AddInventory,
|
||||
RemoveInventory,
|
||||
MarkTerminalLoss,
|
||||
EmitWarning
|
||||
}
|
||||
|
||||
public enum ENetworkValueKind
|
||||
{
|
||||
Amount,
|
||||
Intensity
|
||||
}
|
||||
|
||||
public enum EBand
|
||||
{
|
||||
Safe,
|
||||
@@ -279,10 +293,10 @@ public sealed record RobotState
|
||||
public RobotState Add(ERemedyType remedy, int amount)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => this with { FuelNeutralizers = FuelNeutralizers + amount },
|
||||
ERemedyType.CoolantNeutralizer => this with { CoolantNeutralizers = CoolantNeutralizers + amount },
|
||||
ERemedyType.ElectricityNeutralizer => this with { ElectricityNeutralizers = ElectricityNeutralizers + amount },
|
||||
ERemedyType.HeatShield => this with { HeatShields = HeatShields + amount },
|
||||
ERemedyType.FuelNeutralizer => this with { FuelNeutralizers = ClampInventory(FuelNeutralizers + amount) },
|
||||
ERemedyType.CoolantNeutralizer => this with { CoolantNeutralizers = ClampInventory(CoolantNeutralizers + amount) },
|
||||
ERemedyType.ElectricityNeutralizer => this with { ElectricityNeutralizers = ClampInventory(ElectricityNeutralizers + amount) },
|
||||
ERemedyType.HeatShield => this with { HeatShields = ClampInventory(HeatShields + amount) },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
@@ -291,18 +305,27 @@ public sealed record RobotState
|
||||
{
|
||||
return Count(remedy) <= 0 ? this : Add(remedy, -1);
|
||||
}
|
||||
|
||||
private static int ClampInventory(int value)
|
||||
{
|
||||
return Math.Clamp(value, 0, Balancing.Current.InventoryCapacityPerRemedy);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record RulePredicate
|
||||
{
|
||||
public ERulePredicateKind Kind { get; init; }
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public int ReactorId { get; init; }
|
||||
public int Turn { get; init; }
|
||||
public ELevelState LevelState { get; init; }
|
||||
public EPropSwitchState PropSwitchState { get; init; }
|
||||
public EConsumerServiceState ConsumerServiceState { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ENetworkValueKind NetworkValue { get; init; }
|
||||
public ERemedyType Remedy { get; init; }
|
||||
public EBand Band { get; init; }
|
||||
public int InventoryCount { get; init; }
|
||||
public bool BoolValue { get; init; }
|
||||
}
|
||||
|
||||
@@ -310,6 +333,7 @@ public sealed record RuleEffect
|
||||
{
|
||||
public ERuleEffectKind Kind { get; init; }
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public GridPosition? AccessPosition { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ERemedyType Remedy { get; init; }
|
||||
public float Amount { get; init; }
|
||||
|
||||
@@ -447,6 +447,24 @@ public sealed class SimulationEngine
|
||||
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;
|
||||
@@ -472,10 +490,15 @@ public sealed class SimulationEngine
|
||||
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
|
||||
};
|
||||
@@ -490,8 +513,11 @@ public sealed class SimulationEngine
|
||||
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
|
||||
@@ -503,7 +529,7 @@ public sealed class SimulationEngine
|
||||
var leak = new LeakState {
|
||||
Carrier = effect.Carrier,
|
||||
UndergroundPosition = effect.Position,
|
||||
AccessPosition = 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] };
|
||||
}
|
||||
@@ -625,6 +651,17 @@ public sealed class SimulationEngine
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user