Expand rule event coverage
This commit is contained in:
10
TASKS.md
10
TASKS.md
@@ -40,6 +40,12 @@
|
||||
- Added tests for T-junction ratio splits, zero-weight branches, ambiguous junction validation, and best-path flow into non-junction cells.
|
||||
- Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes: 15 passed.
|
||||
- Ran `jb cleanupcode --build=False ...` and `python D:\Code\crlf.py ...` for touched C# files after the junction slice.
|
||||
- Expanded rule predicates with reactor readiness/loss/win, network value bands, and robot inventory checks.
|
||||
- Expanded rule effects with removal of surface hazards, heat, and inventory, plus authored access positions for leak-start effects.
|
||||
- Hardened rule event validation for predicate targets, effect targets, leak access, and non-negative amount effects.
|
||||
- Added tests for network-band rules, reactor-ready rules, inventory rules, removal effects, electricity leak access, and invalid rule targets.
|
||||
- Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes after the rule slice: 21 passed.
|
||||
- Ran `jb cleanupcode --build=False ...` and `python D:\Code\crlf.py ...` for touched C# files after the rule slice.
|
||||
|
||||
## Current Work
|
||||
|
||||
@@ -47,10 +53,10 @@
|
||||
|
||||
## Future Work
|
||||
|
||||
1. Expand simulation fidelity where the first slice is intentionally simplified: complete pair table coverage, richer rule predicates/effects, and stronger forecast proof cases.
|
||||
1. Expand simulation fidelity where the first slice is intentionally simplified: complete pair table coverage and stronger forecast proof cases.
|
||||
2. Add advanced editor workflows for explicit reactor binding selection, explicit door edge selection, electricity wall leak face selection, and rule event authoring.
|
||||
3. Verify and polish the Win2D app on Windows where the XAML compiler can run.
|
||||
4. Update README and any affected docs to reflect the new schema, .NET target, editor controls, and deterministic defaults.
|
||||
5. Build the Win2D project on a Windows-capable environment after the editor rewrite.
|
||||
6. Add broader tests for junction ratios, ambiguous junctions, all rule event families, serialization edge cases, and editor operations.
|
||||
6. Add broader tests for junction ratios, ambiguous junctions, serialization edge cases, and editor operations.
|
||||
7. Run cleanup when `dotnet-jb` is available, tests, code review, and iterate until the implementation is clean and maintainable.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -170,6 +170,141 @@ public sealed class SimulationEngineTests
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.TerminalLoss);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventCanTriggerFromNetworkBand()
|
||||
{
|
||||
var level = LevelState.Create("Network rule", 6, 6);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel }) with {
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.NetworkBandAt, Position = new(3, 2), Carrier = ECarrierType.Fuel, NetworkValue = ENetworkValueKind.Amount, Band = EBand.Critical }],
|
||||
Effects = [new RuleEffect { Kind = ERuleEffectKind.EmitWarning, Message = "fuel pressure high" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains("fuel pressure high", next.Global.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventCanTriggerFromReactorReadiness()
|
||||
{
|
||||
var level = BuildReadyLevel() with {
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.ReactorReadyIs, ReactorId = 1, BoolValue = true }],
|
||||
Effects = [new RuleEffect { Kind = ERuleEffectKind.EmitWarning, Message = "reactor ready rule" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains("reactor ready rule", next.Global.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventCanTriggerFromRobotInventory()
|
||||
{
|
||||
var level = LevelState.Create("Inventory rule", 6, 6) with {
|
||||
Robot = new() { Position = new(1, 1), FuelNeutralizers = 1 },
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Phase = ERuleEventPhase.StartOfSimulation,
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.RobotInventoryAtLeast, Remedy = ERemedyType.FuelNeutralizer, InventoryCount = 1 }],
|
||||
Effects = [new RuleEffect { Kind = ERuleEffectKind.EmitWarning, Message = "fuel kit detected" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains("fuel kit detected", next.Global.Warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventCanRemoveHazardsHeatAndInventory()
|
||||
{
|
||||
var level = LevelState.Create("Remove rule", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Fuel = 5, Heat = 5 }) with {
|
||||
Robot = new() { Position = new(1, 1), FuelNeutralizers = 2 },
|
||||
Doors = [
|
||||
new DoorState { A = new(2, 2), B = new(1, 2), State = EDoorState.Closed },
|
||||
new DoorState { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed },
|
||||
new DoorState { A = new(2, 2), B = new(2, 1), State = EDoorState.Closed },
|
||||
new DoorState { A = new(2, 2), B = new(2, 3), State = EDoorState.Closed }
|
||||
],
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Phase = ERuleEventPhase.StartOfSimulation,
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
||||
Effects = [
|
||||
new RuleEffect { Kind = ERuleEffectKind.RemoveSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 2 },
|
||||
new RuleEffect { Kind = ERuleEffectKind.RemoveHeat, Position = new(2, 2), Amount = 3 },
|
||||
new RuleEffect { Kind = ERuleEffectKind.RemoveInventory, Remedy = ERemedyType.FuelNeutralizer, Amount = 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(3, next.GetSurface(new(2, 2)).Fuel);
|
||||
Assert.Equal(2, next.GetSurface(new(2, 2)).Heat);
|
||||
Assert.Equal(1, next.Robot.FuelNeutralizers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventStartLeakUsesAuthoredElectricityAccessFace()
|
||||
{
|
||||
var level = LevelState.Create("Electricity leak rule", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Electricity, new() { State = EUndergroundState.Intact }) with {
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Phase = ERuleEventPhase.StartOfSimulation,
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
||||
Effects = [new RuleEffect { Kind = ERuleEffectKind.StartLeak, Position = new(2, 2), AccessPosition = new(2, 3), Carrier = ECarrierType.Electricity }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Single(next.Leaks);
|
||||
Assert.Equal(new(2, 3), next.Leaks[0].AccessPosition);
|
||||
Assert.True(next.GetSurface(new(2, 3)).Electricity > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsInvalidRuleTargets()
|
||||
{
|
||||
var level = LevelState.Create("Invalid rules", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall) with {
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.PropStateAt, Position = new(1, 1) }],
|
||||
Effects = [
|
||||
new RuleEffect { Kind = ERuleEffectKind.AddSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 1 },
|
||||
new RuleEffect { Kind = ERuleEffectKind.RepairNetworkCell, Position = new(3, 3), Carrier = ECarrierType.Coolant }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Rule prop predicate", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Rule surface effect", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Rule network effect", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsWallHazardsAndInvalidReactorBinding()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user