Rework simulation rules
This commit is contained in:
@@ -60,6 +60,16 @@ public abstract class Balancing
|
|||||||
return new() { Verb = ESurfaceInteractionVerb.Flow, Quantity = quantity, Amount = FlowTransferRatio };
|
return new() { Verb = ESurfaceInteractionVerb.Flow, Quantity = quantity, Amount = FlowTransferRatio };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float StructuralPressureThreshold(ECarrierType carrier)
|
||||||
|
{
|
||||||
|
return carrier switch {
|
||||||
|
ECarrierType.Fuel => FuelCritical,
|
||||||
|
ECarrierType.Coolant => CoolantCritical,
|
||||||
|
ECarrierType.Electricity => ElectricityCritical,
|
||||||
|
_ => MaxValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private SurfaceInteractionEffect Warm(EBand rowBand, EBand colBand)
|
private SurfaceInteractionEffect Warm(EBand rowBand, EBand colBand)
|
||||||
{
|
{
|
||||||
return new() {
|
return new() {
|
||||||
@@ -110,7 +120,6 @@ public abstract class Balancing
|
|||||||
public abstract int DefaultLevelWidth { get; }
|
public abstract int DefaultLevelWidth { get; }
|
||||||
public abstract int DefaultLevelHeight { get; }
|
public abstract int DefaultLevelHeight { get; }
|
||||||
public abstract int MinimumLevelSize { get; }
|
public abstract int MinimumLevelSize { get; }
|
||||||
public abstract int ActionsPerTurn { get; }
|
|
||||||
public abstract int ForecastHorizon { get; }
|
public abstract int ForecastHorizon { get; }
|
||||||
public abstract float MinValue { get; }
|
public abstract float MinValue { get; }
|
||||||
public abstract float MaxValue { get; }
|
public abstract float MaxValue { get; }
|
||||||
@@ -139,6 +148,9 @@ public abstract class Balancing
|
|||||||
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
|
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
|
||||||
public abstract float ConsumerRequiredAmount { get; }
|
public abstract float ConsumerRequiredAmount { get; }
|
||||||
public abstract float ConsumerRequiredIntensity { get; }
|
public abstract float ConsumerRequiredIntensity { get; }
|
||||||
|
public abstract int MaxStructuralIntegrity { get; }
|
||||||
|
public abstract int StructuralIntegrityLeakThreshold { get; }
|
||||||
|
public abstract float StructuralIntegrityDamageScale { get; }
|
||||||
public abstract float LeakBaseAmount { get; }
|
public abstract float LeakBaseAmount { get; }
|
||||||
public abstract float LeakAmountScale { get; }
|
public abstract float LeakAmountScale { get; }
|
||||||
public abstract float LeakIntensityScale { get; }
|
public abstract float LeakIntensityScale { get; }
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ public class NormalBalancing : Balancing
|
|||||||
public override int DefaultLevelWidth => 16;
|
public override int DefaultLevelWidth => 16;
|
||||||
public override int DefaultLevelHeight => 12;
|
public override int DefaultLevelHeight => 12;
|
||||||
public override int MinimumLevelSize => 4;
|
public override int MinimumLevelSize => 4;
|
||||||
public override int ActionsPerTurn => 3;
|
|
||||||
public override int ForecastHorizon => 6;
|
public override int ForecastHorizon => 6;
|
||||||
public override float MinValue => 0;
|
public override float MinValue => 0;
|
||||||
public override float MaxValue => 10;
|
public override float MaxValue => 10;
|
||||||
@@ -48,6 +47,9 @@ public class NormalBalancing : Balancing
|
|||||||
|
|
||||||
public override float ConsumerRequiredAmount => 2.5f;
|
public override float ConsumerRequiredAmount => 2.5f;
|
||||||
public override float ConsumerRequiredIntensity => 2.5f;
|
public override float ConsumerRequiredIntensity => 2.5f;
|
||||||
|
public override int MaxStructuralIntegrity => 10;
|
||||||
|
public override int StructuralIntegrityLeakThreshold => 2;
|
||||||
|
public override float StructuralIntegrityDamageScale => 0.35f;
|
||||||
public override float LeakBaseAmount => 0.5f;
|
public override float LeakBaseAmount => 0.5f;
|
||||||
public override float LeakAmountScale => 0.15f;
|
public override float LeakAmountScale => 0.15f;
|
||||||
public override float LeakIntensityScale => 0.1f;
|
public override float LeakIntensityScale => 0.1f;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public static class LevelEditor
|
|||||||
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
||||||
EEditorTool.Underground => SetUnderground(level, position, command.Carrier),
|
EEditorTool.Underground => SetUnderground(level, position, command.Carrier),
|
||||||
EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
EEditorTool.Flow => SetCarrierProp(level, position, EPropType.Flow, command.Carrier),
|
||||||
EEditorTool.Consumer => SetCarrierProp(level, position, EPropType.Consumer, command.Carrier),
|
EEditorTool.Consumer => SetFloorProp(level, position, new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Enabled }),
|
||||||
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
|
||||||
EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
|
EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
|
||||||
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||||
@@ -27,19 +27,6 @@ public static class LevelEditor
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState SetDoorEdge(LevelState level, GridPosition a, GridPosition b)
|
|
||||||
{
|
|
||||||
if (!level.IsFloor(a) || !level.IsFloor(b) || a.ManhattanDistance(b) != 1)
|
|
||||||
return level;
|
|
||||||
|
|
||||||
return level.SetProp(a, new() { Type = EPropType.Door }) with {
|
|
||||||
Doors = [
|
|
||||||
.. level.Doors.Where(door => !SameDoorEdge(door, a, b)),
|
|
||||||
new() { A = a, B = b, State = EDoorState.Closed }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LevelState SetLeak(LevelState level, GridPosition undergroundPosition, GridPosition accessPosition, ECarrierType carrier)
|
public static LevelState SetLeak(LevelState level, GridPosition undergroundPosition, GridPosition accessPosition, ECarrierType carrier)
|
||||||
{
|
{
|
||||||
if (!level.InBounds(undergroundPosition) || !level.IsFloor(accessPosition))
|
if (!level.InBounds(undergroundPosition) || !level.IsFloor(accessPosition))
|
||||||
@@ -51,7 +38,10 @@ public static class LevelEditor
|
|||||||
if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1)
|
if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1)
|
||||||
return level;
|
return level;
|
||||||
|
|
||||||
var next = level.SetUnderground(undergroundPosition, carrier, new() { State = EUndergroundState.Leaking });
|
var next = level.SetUnderground(undergroundPosition, carrier, new() {
|
||||||
|
State = EUndergroundState.Leaking,
|
||||||
|
StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold
|
||||||
|
});
|
||||||
return next with {
|
return next with {
|
||||||
Leaks = [
|
Leaks = [
|
||||||
.. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier),
|
.. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier),
|
||||||
@@ -64,32 +54,12 @@ public static class LevelEditor
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState BindReactorConsumer(LevelState level, int reactorId, ECarrierType carrier, GridPosition consumerPosition)
|
|
||||||
{
|
|
||||||
if (!level.InBounds(consumerPosition) || level.GetProp(consumerPosition) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
|
|
||||||
return level;
|
|
||||||
|
|
||||||
var reactors = level.Reactors.Select(reactor => reactor.ReactorId == reactorId ? BindConsumer(reactor, carrier, consumerPosition) : reactor).ToArray();
|
|
||||||
return level with { Reactors = reactors };
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LevelState AddRuleEvent(LevelState level, RuleEventState ruleEvent)
|
|
||||||
{
|
|
||||||
var id = string.IsNullOrWhiteSpace(ruleEvent.Id) ? NextRuleEventId(level) : ruleEvent.Id;
|
|
||||||
var authoredEvent = ruleEvent with { Id = id };
|
|
||||||
return level with {
|
|
||||||
RuleEvents = [.. level.RuleEvents.Where(existing => existing.Id != id), authoredEvent]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static LevelState RemoveRuleEvent(LevelState level, string id)
|
|
||||||
{
|
|
||||||
return level with { RuleEvents = level.RuleEvents.Where(ruleEvent => ruleEvent.Id != id).ToArray() };
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier)
|
private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier)
|
||||||
{
|
{
|
||||||
return level.SetUnderground(position, carrier, new() { State = EUndergroundState.Intact });
|
return level.SetUnderground(position, carrier, new() {
|
||||||
|
State = EUndergroundState.Intact,
|
||||||
|
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState SetCarrierProp(LevelState level, GridPosition position, EPropType type, ECarrierType carrier)
|
private static LevelState SetCarrierProp(LevelState level, GridPosition position, EPropType type, ECarrierType carrier)
|
||||||
@@ -125,10 +95,7 @@ public static class LevelEditor
|
|||||||
.. level.Reactors,
|
.. level.Reactors,
|
||||||
new() {
|
new() {
|
||||||
ReactorId = id,
|
ReactorId = id,
|
||||||
ControlPosition = position,
|
ControlPosition = position
|
||||||
FuelConsumerPosition = position,
|
|
||||||
CoolantConsumerPosition = position,
|
|
||||||
ElectricityConsumerPosition = position
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
@@ -141,28 +108,4 @@ public static class LevelEditor
|
|||||||
|
|
||||||
return SetLeak(level, position, position, carrier);
|
return SetLeak(level, position, position, carrier);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool SameDoorEdge(DoorState door, GridPosition a, GridPosition b)
|
|
||||||
{
|
|
||||||
return (door.A == a && door.B == b) || (door.A == b && door.B == a);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ReactorBinding BindConsumer(ReactorBinding reactor, ECarrierType carrier, GridPosition consumerPosition)
|
|
||||||
{
|
|
||||||
return carrier switch {
|
|
||||||
ECarrierType.Fuel => reactor with { FuelConsumerPosition = consumerPosition },
|
|
||||||
ECarrierType.Coolant => reactor with { CoolantConsumerPosition = consumerPosition },
|
|
||||||
ECarrierType.Electricity => reactor with { ElectricityConsumerPosition = consumerPosition },
|
|
||||||
_ => reactor
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NextRuleEventId(LevelState level)
|
|
||||||
{
|
|
||||||
var next = level.RuleEvents.Count + 1;
|
|
||||||
while (level.RuleEvents.Any(ruleEvent => ruleEvent.Id == $"rule-{next}"))
|
|
||||||
next++;
|
|
||||||
|
|
||||||
return $"rule-{next}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ReactorMaintenance.Simulation;
|
namespace ReactorMaintenance.Simulation;
|
||||||
@@ -33,7 +33,7 @@ public static class LevelSerializer
|
|||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int c_CurrentVersion = 2;
|
private const int c_CurrentVersion = 3;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions s_Options = new() {
|
private static readonly JsonSerializerOptions s_Options = new() {
|
||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public static class LevelStateExtensions
|
|||||||
|
|
||||||
public static bool IsClosedDoorEdge(this LevelState level, GridPosition a, GridPosition b)
|
public static bool IsClosedDoorEdge(this LevelState level, GridPosition a, GridPosition b)
|
||||||
{
|
{
|
||||||
return level.Doors.Any(door => door.State == EDoorState.Closed && SameEdge(door.A, door.B, a, b));
|
return DoorBlocksEdge(level, a, b) || DoorBlocksEdge(level, b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState SetTerrain(this LevelState level, GridPosition position, ECellTerrain terrain)
|
public static LevelState SetTerrain(this LevelState level, GridPosition position, ECellTerrain terrain)
|
||||||
@@ -109,8 +109,31 @@ public static class LevelStateExtensions
|
|||||||
.SetUnderground(position, ECarrierType.Electricity, new());
|
.SetUnderground(position, ECarrierType.Electricity, new());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool SameEdge(GridPosition edgeA, GridPosition edgeB, GridPosition a, GridPosition b)
|
private static bool DoorBlocksEdge(LevelState level, GridPosition doorPosition, GridPosition neighbor)
|
||||||
{
|
{
|
||||||
return (edgeA == a && edgeB == b) || (edgeA == b && edgeB == a);
|
if (!level.InBounds(doorPosition) || !level.InBounds(neighbor))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var prop = level.GetProp(doorPosition);
|
||||||
|
if (prop is not { Type: EPropType.Door, DoorState: EDoorState.Closed } || doorPosition.ManhattanDistance(neighbor) != 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var north = new GridPosition(doorPosition.X, doorPosition.Y - 1);
|
||||||
|
var south = new GridPosition(doorPosition.X, doorPosition.Y + 1);
|
||||||
|
var west = new GridPosition(doorPosition.X - 1, doorPosition.Y);
|
||||||
|
var east = new GridPosition(doorPosition.X + 1, doorPosition.Y);
|
||||||
|
|
||||||
|
if (IsWall(level, north) && IsWall(level, south))
|
||||||
|
return neighbor.Y == doorPosition.Y;
|
||||||
|
|
||||||
|
if (IsWall(level, west) && IsWall(level, east))
|
||||||
|
return neighbor.X == doorPosition.X;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWall(LevelState level, GridPosition position)
|
||||||
|
{
|
||||||
|
return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,6 @@ public sealed class LevelValidator
|
|||||||
ValidateLeaks(level, errors);
|
ValidateLeaks(level, errors);
|
||||||
ValidateReactors(level, errors, warnings);
|
ValidateReactors(level, errors, warnings);
|
||||||
ValidateJunctions(level, errors);
|
ValidateJunctions(level, errors);
|
||||||
ValidateRuleEvents(level, errors);
|
|
||||||
ValidateWarnings(level, warnings);
|
ValidateWarnings(level, warnings);
|
||||||
|
|
||||||
return new() { Errors = errors, Warnings = warnings };
|
return new() { Errors = errors, Warnings = warnings };
|
||||||
@@ -60,10 +59,21 @@ public sealed class LevelValidator
|
|||||||
|
|
||||||
private static void ValidateDoors(LevelState level, List<ValidationIssue> errors)
|
private static void ValidateDoors(LevelState level, List<ValidationIssue> errors)
|
||||||
{
|
{
|
||||||
foreach (var door in level.Doors)
|
foreach (var position in LevelTraversal.AllPositions(level))
|
||||||
{
|
{
|
||||||
if (!level.IsFloor(door.A) || !level.IsFloor(door.B) || door.A.ManhattanDistance(door.B) != 1)
|
if (level.GetProp(position).Type != EPropType.Door)
|
||||||
errors.Add(new("Door edge must connect two adjacent floor cells.", door.A));
|
continue;
|
||||||
|
|
||||||
|
if (!level.IsFloor(position))
|
||||||
|
{
|
||||||
|
errors.Add(new("Door prop must be placed on a floor cell.", position));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var northSouthWalls = IsWall(level, new(position.X, position.Y - 1)) && IsWall(level, new(position.X, position.Y + 1));
|
||||||
|
var westEastWalls = IsWall(level, new(position.X - 1, position.Y)) && IsWall(level, new(position.X + 1, position.Y));
|
||||||
|
if (northSouthWalls == westEastWalls)
|
||||||
|
errors.Add(new("Door must be surrounded by one opposing pair of wall cells.", position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,21 +104,14 @@ public sealed class LevelValidator
|
|||||||
foreach (var reactor in level.Reactors)
|
foreach (var reactor in level.Reactors)
|
||||||
{
|
{
|
||||||
if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
|
if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
|
||||||
errors.Add(new("Reactor binding control position must point to a reactor control prop.", reactor.ControlPosition));
|
errors.Add(new("Reactor control position must point to a reactor control prop.", reactor.ControlPosition));
|
||||||
|
|
||||||
ValidateConsumerBinding(level, reactor.FuelConsumerPosition, ECarrierType.Fuel, errors);
|
|
||||||
ValidateConsumerBinding(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant, errors);
|
|
||||||
ValidateConsumerBinding(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity, errors);
|
|
||||||
|
|
||||||
if (!reactor.Ready)
|
if (!reactor.Ready)
|
||||||
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
|
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List<ValidationIssue> errors)
|
if (level.RequiredFuelConsumers < 0 || level.RequiredCoolantConsumers < 0 || level.RequiredElectricityConsumers < 0)
|
||||||
{
|
errors.Add(new("Required consumer counts cannot be negative."));
|
||||||
if (!level.InBounds(position) || level.GetProp(position) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
|
|
||||||
errors.Add(new($"Missing or invalid {carrier} consumer binding.", position));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
|
private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
|
||||||
@@ -117,111 +120,6 @@ public sealed class LevelValidator
|
|||||||
errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position)));
|
errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateRuleEvents(LevelState level, List<ValidationIssue> errors)
|
|
||||||
{
|
|
||||||
foreach (var ruleEvent in level.RuleEvents)
|
|
||||||
{
|
|
||||||
foreach (var predicate in ruleEvent.Predicates)
|
|
||||||
ValidateRulePredicate(level, predicate, errors);
|
|
||||||
|
|
||||||
foreach (var effect in ruleEvent.Effects)
|
|
||||||
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)
|
private static void ValidateWarnings(LevelState level, List<ValidationIssue> warnings)
|
||||||
{
|
{
|
||||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||||
@@ -243,12 +141,31 @@ public sealed class LevelValidator
|
|||||||
{
|
{
|
||||||
var position = new GridPosition(x, y);
|
var position = new GridPosition(x, y);
|
||||||
var prop = level.GetProp(position);
|
var prop = level.GetProp(position);
|
||||||
if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier))
|
if (prop.Type != EPropType.Consumer || prop.SwitchState != EPropSwitchState.Enabled)
|
||||||
warnings.Add(new("Enabled consumer is initially starved.", position));
|
continue;
|
||||||
|
|
||||||
|
var hasPresentNetwork = false;
|
||||||
|
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||||
|
{
|
||||||
|
if (!level.GetUnderground(position, carrier).IsPresent)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
hasPresentNetwork = true;
|
||||||
|
if (!HasSourcePath(level, position, carrier))
|
||||||
|
warnings.Add(new($"Enabled consumer has no {carrier} source path.", position));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPresentNetwork)
|
||||||
|
warnings.Add(new("Enabled consumer has no underground network beneath it.", position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsWall(LevelState level, GridPosition position)
|
||||||
|
{
|
||||||
|
return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier)
|
private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier)
|
||||||
{
|
{
|
||||||
if (!level.GetUnderground(start, carrier).CarriesFlow)
|
if (!level.GetUnderground(start, carrier).CarriesFlow)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
public sealed record DoorState
|
|
||||||
{
|
|
||||||
public GridPosition A { get; init; } = new(0, 0);
|
|
||||||
public GridPosition B { get; init; } = new(0, 0);
|
|
||||||
public EDoorState State { get; init; } = EDoorState.Closed;
|
|
||||||
}
|
|
||||||
@@ -6,5 +6,5 @@ public enum EForecastKind
|
|||||||
ReactorReady,
|
ReactorReady,
|
||||||
ConsumerStarved,
|
ConsumerStarved,
|
||||||
HazardGrowth,
|
HazardGrowth,
|
||||||
RuleEvent
|
StructuralIntegrity
|
||||||
}
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
public enum ERuleEffectKind
|
|
||||||
{
|
|
||||||
StartLeak,
|
|
||||||
WorsenLeak,
|
|
||||||
RepairNetworkCell,
|
|
||||||
DisableNetworkCell,
|
|
||||||
SetPropEnabled,
|
|
||||||
AddSurfaceHazard,
|
|
||||||
RemoveSurfaceHazard,
|
|
||||||
AddHeat,
|
|
||||||
RemoveHeat,
|
|
||||||
AddInventory,
|
|
||||||
RemoveInventory,
|
|
||||||
MarkTerminalLoss,
|
|
||||||
EmitWarning
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
public enum ERuleEventPhase
|
|
||||||
{
|
|
||||||
StartOfSimulation,
|
|
||||||
EndOfTurn
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
public enum ERulePredicateKind
|
|
||||||
{
|
|
||||||
TurnAtLeast,
|
|
||||||
LevelStateIs,
|
|
||||||
ReactorReadyIs,
|
|
||||||
ReactorLostIs,
|
|
||||||
ReactorWonIs,
|
|
||||||
PropStateAt,
|
|
||||||
ConsumerStateAt,
|
|
||||||
NetworkBandAt,
|
|
||||||
SurfaceBandAt,
|
|
||||||
RobotAt,
|
|
||||||
RobotInventoryAtLeast,
|
|
||||||
AllSeeingEyeUnlocked
|
|
||||||
}
|
|
||||||
@@ -3,10 +3,8 @@
|
|||||||
public sealed record GlobalState
|
public sealed record GlobalState
|
||||||
{
|
{
|
||||||
public int Turn { get; init; }
|
public int Turn { get; init; }
|
||||||
public int ActionsRemaining { get; init; } = Balancing.Current.ActionsPerTurn;
|
|
||||||
public ELevelState LevelState { get; init; } = ELevelState.Stable;
|
public ELevelState LevelState { get; init; } = ELevelState.Stable;
|
||||||
public string Status { get; init; } = "STABLE";
|
public string Status { get; init; } = "STABLE";
|
||||||
public bool AllSeeingEyeUnlocked { get; init; }
|
|
||||||
public bool TerminalLoss { get; init; }
|
public bool TerminalLoss { get; init; }
|
||||||
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
|
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
@@ -16,10 +16,11 @@ public sealed record LevelState
|
|||||||
public UndergroundCell[] Electricity { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
public UndergroundCell[] Electricity { get; init; } = LevelStateFactory.CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||||
public SurfaceState[] Surface { get; init; } = LevelStateFactory.CreateSurface(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
public SurfaceState[] Surface { get; init; } = LevelStateFactory.CreateSurface(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||||
public PropState[] Props { get; init; } = LevelStateFactory.CreateProps(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
public PropState[] Props { get; init; } = LevelStateFactory.CreateProps(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||||
public IReadOnlyList<DoorState> Doors { get; init; } = Array.Empty<DoorState>();
|
|
||||||
public IReadOnlyList<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
|
public IReadOnlyList<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
|
||||||
public IReadOnlyList<ReactorBinding> Reactors { get; init; } = Array.Empty<ReactorBinding>();
|
public IReadOnlyList<ReactorState> Reactors { get; init; } = Array.Empty<ReactorState>();
|
||||||
public IReadOnlyList<RuleEventState> RuleEvents { get; init; } = Array.Empty<RuleEventState>();
|
public int RequiredFuelConsumers { get; init; } = 1;
|
||||||
|
public int RequiredCoolantConsumers { get; init; } = 1;
|
||||||
|
public int RequiredElectricityConsumers { get; init; } = 1;
|
||||||
public RobotState Robot { get; init; } = new();
|
public RobotState Robot { get; init; } = new();
|
||||||
public GlobalState Global { get; init; } = new();
|
public GlobalState Global { get; init; } = new();
|
||||||
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||||
|
|||||||
@@ -6,10 +6,24 @@ public sealed record PropState
|
|||||||
public ECarrierType Carrier { get; init; }
|
public ECarrierType Carrier { get; init; }
|
||||||
public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled;
|
public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled;
|
||||||
public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown;
|
public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||||
|
public EConsumerServiceState FuelServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||||
|
public EConsumerServiceState CoolantServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||||
|
public EConsumerServiceState ElectricityServiceState { get; init; } = EConsumerServiceState.Unknown;
|
||||||
public int JunctionMode { get; init; }
|
public int JunctionMode { get; init; }
|
||||||
public ERemedyType RemedyType { get; init; }
|
public ERemedyType RemedyType { get; init; }
|
||||||
public bool Depleted { get; init; }
|
public bool Depleted { get; init; }
|
||||||
public int ReactorId { get; init; }
|
public int ReactorId { get; init; }
|
||||||
|
public EDoorState DoorState { get; init; } = EDoorState.Closed;
|
||||||
|
|
||||||
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
|
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
|
||||||
|
|
||||||
|
public EConsumerServiceState ServiceStateFor(ECarrierType carrier)
|
||||||
|
{
|
||||||
|
return carrier switch {
|
||||||
|
ECarrierType.Fuel => FuelServiceState,
|
||||||
|
ECarrierType.Coolant => CoolantServiceState,
|
||||||
|
ECarrierType.Electricity => ElectricityServiceState,
|
||||||
|
_ => EConsumerServiceState.Unknown
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
public sealed record ReactorBinding
|
|
||||||
{
|
|
||||||
public int ReactorId { get; init; }
|
|
||||||
public GridPosition ControlPosition { get; init; } = new(0, 0);
|
|
||||||
public GridPosition FuelConsumerPosition { get; init; } = new(0, 0);
|
|
||||||
public GridPosition CoolantConsumerPosition { get; init; } = new(0, 0);
|
|
||||||
public GridPosition ElectricityConsumerPosition { get; init; } = new(0, 0);
|
|
||||||
public bool Ready { get; init; }
|
|
||||||
public bool Activated { get; init; }
|
|
||||||
}
|
|
||||||
9
src/ReactorMaintenance.Simulation/Models/ReactorState.cs
Normal file
9
src/ReactorMaintenance.Simulation/Models/ReactorState.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
public sealed record ReactorState
|
||||||
|
{
|
||||||
|
public int ReactorId { get; init; }
|
||||||
|
public GridPosition ControlPosition { get; init; } = new(0, 0);
|
||||||
|
public bool Ready { get; init; }
|
||||||
|
public bool Activated { get; init; }
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
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; }
|
|
||||||
public EPropSwitchState PropSwitchState { get; init; }
|
|
||||||
public string Message { get; init; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
public sealed record RuleEventState
|
|
||||||
{
|
|
||||||
public string Id { get; init; } = string.Empty;
|
|
||||||
public bool Enabled { get; init; } = true;
|
|
||||||
public bool Repeat { get; init; }
|
|
||||||
public bool Triggered { get; init; }
|
|
||||||
public int Priority { get; init; }
|
|
||||||
public ERuleEventPhase Phase { get; init; }
|
|
||||||
public IReadOnlyList<RulePredicate> Predicates { get; init; } = Array.Empty<RulePredicate>();
|
|
||||||
public IReadOnlyList<RuleEffect> Effects { get; init; } = Array.Empty<RuleEffect>();
|
|
||||||
public string ForecastText { get; init; } = string.Empty;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,7 @@ public sealed record UndergroundCell
|
|||||||
public EUndergroundState State { get; init; }
|
public EUndergroundState State { get; init; }
|
||||||
public float Amount { get; init; }
|
public float Amount { get; init; }
|
||||||
public float Intensity { get; init; }
|
public float Intensity { get; init; }
|
||||||
|
public int StructuralIntegrity { get; init; } = Balancing.Current.MaxStructuralIntegrity;
|
||||||
|
|
||||||
public bool IsPresent => State != EUndergroundState.Absent;
|
public bool IsPresent => State != EUndergroundState.Absent;
|
||||||
public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking;
|
public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking;
|
||||||
|
|||||||
@@ -4,22 +4,27 @@ public sealed class SimulationEngine
|
|||||||
{
|
{
|
||||||
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||||
{
|
{
|
||||||
return PlayerActionSystem.MoveRobot(level, destination, SpendAction);
|
return PlayerActionSystem.MoveRobot(level, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LevelState InteractProp(LevelState level)
|
public LevelState InteractProp(LevelState level)
|
||||||
{
|
{
|
||||||
return PlayerActionSystem.InteractProp(level, SpendAction);
|
return PlayerActionSystem.InteractProp(level, ResolveStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||||
{
|
{
|
||||||
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, SpendAction);
|
return PlayerActionSystem.InteractLeak(level, carrier, useRemedy, ResolveStep);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LevelState ApplyHeatShield(LevelState level)
|
public LevelState ApplyHeatShield(LevelState level)
|
||||||
{
|
{
|
||||||
return PlayerActionSystem.ApplyHeatShield(level, SpendAction);
|
return PlayerActionSystem.ApplyHeatShield(level, ResolveStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LevelState ResolveStep(LevelState level)
|
||||||
|
{
|
||||||
|
return ResolveStep(level, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LevelState ActivateReactor(LevelState level)
|
public LevelState ActivateReactor(LevelState level)
|
||||||
@@ -29,45 +34,37 @@ public sealed class SimulationEngine
|
|||||||
|
|
||||||
public LevelState EndTurn(LevelState level)
|
public LevelState EndTurn(LevelState level)
|
||||||
{
|
{
|
||||||
return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } });
|
return ResolveStep(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
public LevelState AdvanceTurn(LevelState level)
|
public LevelState AdvanceTurn(LevelState level)
|
||||||
{
|
{
|
||||||
return ResolveTurn(level);
|
return ResolveStep(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||||
{
|
{
|
||||||
return ForecastSystem.Forecast(level, simulated => ResolveTurn(simulated, false));
|
return ForecastSystem.Forecast(level, simulated => ResolveStep(simulated, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private LevelState SpendAction(LevelState level)
|
private LevelState ResolveStep(LevelState level, bool refreshForecasts)
|
||||||
{
|
|
||||||
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 LevelState ResolveTurn(LevelState level, bool refreshForecasts = true)
|
|
||||||
{
|
{
|
||||||
var report = m_Validator.Validate(level);
|
var report = m_Validator.Validate(level);
|
||||||
if (!report.IsValid)
|
if (!report.IsValid)
|
||||||
return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } };
|
return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } };
|
||||||
|
|
||||||
var next = RuleEventSystem.Apply(level, ERuleEventPhase.StartOfSimulation);
|
var next = level;
|
||||||
next = NetworkPropagationSystem.Propagate(next);
|
next = NetworkPropagationSystem.Propagate(next);
|
||||||
next = ConsumerSystem.Resolve(next);
|
next = ConsumerSystem.Resolve(next);
|
||||||
|
next = StructuralIntegritySystem.Resolve(next);
|
||||||
next = LeakSystem.Inject(next);
|
next = LeakSystem.Inject(next);
|
||||||
next = SurfaceInteractionSystem.Resolve(next);
|
next = SurfaceInteractionSystem.Resolve(next);
|
||||||
next = RobotSafetySystem.Resolve(next);
|
next = RobotSafetySystem.Resolve(next);
|
||||||
next = ReactorSystem.DeriveState(next);
|
next = ReactorSystem.DeriveState(next);
|
||||||
next = RuleEventSystem.Apply(next, ERuleEventPhase.EndOfTurn);
|
|
||||||
next = SurfaceInteractionSystem.AdvanceDurations(next);
|
next = SurfaceInteractionSystem.AdvanceDurations(next);
|
||||||
next = next with {
|
next = next with {
|
||||||
Global = next.Global with {
|
Global = next.Global with {
|
||||||
Turn = next.Global.Turn + 1,
|
Turn = next.Global.Turn + 1
|
||||||
ActionsRemaining = Balancing.Current.ActionsPerTurn
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,59 @@ internal static class ConsumerSystem
|
|||||||
|
|
||||||
if (prop.SwitchState == EPropSwitchState.Disabled)
|
if (prop.SwitchState == EPropSwitchState.Disabled)
|
||||||
{
|
{
|
||||||
props[index] = prop with { ServiceState = EConsumerServiceState.Disabled };
|
var disabledFuel = DisabledServiceStateFor(level, position, ECarrierType.Fuel);
|
||||||
|
var disabledCoolant = DisabledServiceStateFor(level, position, ECarrierType.Coolant);
|
||||||
|
var disabledElectricity = DisabledServiceStateFor(level, position, ECarrierType.Electricity);
|
||||||
|
props[index] = prop with {
|
||||||
|
ServiceState = Aggregate(disabledFuel, disabledCoolant, disabledElectricity),
|
||||||
|
FuelServiceState = disabledFuel,
|
||||||
|
CoolantServiceState = disabledCoolant,
|
||||||
|
ElectricityServiceState = disabledElectricity
|
||||||
|
};
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var underground = level.GetUnderground(position, prop.Carrier);
|
var fuel = ServiceStateFor(level, position, ECarrierType.Fuel);
|
||||||
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
|
var coolant = ServiceStateFor(level, position, ECarrierType.Coolant);
|
||||||
props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved };
|
var electricity = ServiceStateFor(level, position, ECarrierType.Electricity);
|
||||||
|
props[index] = prop with {
|
||||||
|
ServiceState = Aggregate(fuel, coolant, electricity),
|
||||||
|
FuelServiceState = fuel,
|
||||||
|
CoolantServiceState = coolant,
|
||||||
|
ElectricityServiceState = electricity
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return level with { Props = props };
|
return level with { Props = props };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static EConsumerServiceState ServiceStateFor(LevelState level, GridPosition position, ECarrierType carrier)
|
||||||
|
{
|
||||||
|
var underground = level.GetUnderground(position, carrier);
|
||||||
|
if (!underground.IsPresent)
|
||||||
|
return EConsumerServiceState.Unknown;
|
||||||
|
|
||||||
|
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
|
||||||
|
return supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EConsumerServiceState DisabledServiceStateFor(LevelState level, GridPosition position, ECarrierType carrier)
|
||||||
|
{
|
||||||
|
return level.GetUnderground(position, carrier).IsPresent ? EConsumerServiceState.Disabled : EConsumerServiceState.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EConsumerServiceState Aggregate(params EConsumerServiceState[] states)
|
||||||
|
{
|
||||||
|
var participating = states.Where(state => state != EConsumerServiceState.Unknown).ToArray();
|
||||||
|
if (participating.Length == 0)
|
||||||
|
return EConsumerServiceState.Unknown;
|
||||||
|
|
||||||
|
if (participating.Any(state => state == EConsumerServiceState.Starved))
|
||||||
|
return EConsumerServiceState.Starved;
|
||||||
|
|
||||||
|
if (participating.Any(state => state == EConsumerServiceState.Disabled))
|
||||||
|
return EConsumerServiceState.Disabled;
|
||||||
|
|
||||||
|
return EConsumerServiceState.Producing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -44,15 +44,25 @@ internal static class ForecastSystem
|
|||||||
foreach (var position in LevelTraversal.AllPositions(level))
|
foreach (var position in LevelTraversal.AllPositions(level))
|
||||||
{
|
{
|
||||||
var prop = level.GetProp(position);
|
var prop = level.GetProp(position);
|
||||||
if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved)
|
if (prop.Type == EPropType.Consumer)
|
||||||
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved"));
|
{
|
||||||
|
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||||
|
{
|
||||||
|
if (prop.ServiceStateFor(carrier) == EConsumerServiceState.Starved)
|
||||||
|
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{carrier} consumer starved"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||||
|
{
|
||||||
|
var underground = level.GetUnderground(position, carrier);
|
||||||
|
if (underground.IsPresent && underground.StructuralIntegrity <= Balancing.Current.StructuralIntegrityLeakThreshold)
|
||||||
|
forecasts.Add(new(EForecastKind.StructuralIntegrity, position, turn, $"{carrier} structural integrity failing"));
|
||||||
|
}
|
||||||
|
|
||||||
var surface = level.GetSurface(position);
|
var surface = level.GetSurface(position);
|
||||||
if (SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical)
|
if (SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical)
|
||||||
forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard"));
|
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 => RuleEventSystem.PredicateMatches(level, predicate))))
|
|
||||||
forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,23 +2,23 @@
|
|||||||
|
|
||||||
internal static class PlayerActionSystem
|
internal static class PlayerActionSystem
|
||||||
{
|
{
|
||||||
public static LevelState MoveRobot(LevelState level, GridPosition destination, Func<LevelState, LevelState> spendAction)
|
public static LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
|
if (!CanAct(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
|
||||||
return Refuse(level, "MOVE BLOCKED");
|
return Refuse(level, "MOVE BLOCKED");
|
||||||
|
|
||||||
return spendAction(level with {
|
return level with {
|
||||||
Robot = level.Robot with {
|
Robot = level.Robot with {
|
||||||
Position = destination,
|
Position = destination,
|
||||||
HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1)
|
HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1)
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState InteractProp(LevelState level, Func<LevelState, LevelState> spendAction)
|
public static LevelState InteractProp(LevelState level, Func<LevelState, LevelState> resolveLengthyAction)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level))
|
if (!CanAct(level))
|
||||||
return Refuse(level, "NO ACTIONS");
|
return Refuse(level, "NO CONTROL");
|
||||||
|
|
||||||
var position = level.Robot.Position;
|
var position = level.Robot.Position;
|
||||||
var prop = level.GetProp(position);
|
var prop = level.GetProp(position);
|
||||||
@@ -28,20 +28,20 @@ internal static class PlayerActionSystem
|
|||||||
var next = prop.Type switch {
|
var next = prop.Type switch {
|
||||||
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
|
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
|
||||||
EPropType.Junction => CycleJunctionMode(level, position, prop),
|
EPropType.Junction => CycleJunctionMode(level, position, prop),
|
||||||
EPropType.Door => ToggleDoor(level, position),
|
EPropType.Door => ToggleDoor(level, position, prop),
|
||||||
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } },
|
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { Status = "ALL-SEEING-EYE AVAILABLE" } },
|
||||||
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
|
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
|
||||||
EPropType.ReactorControl => ReactorSystem.Activate(level),
|
EPropType.ReactorControl => ReactorSystem.Activate(level),
|
||||||
_ => level
|
_ => level
|
||||||
};
|
};
|
||||||
|
|
||||||
return spendAction(next);
|
return prop.Type == EPropType.AllSeeingEyeTerminal || prop.Type == EPropType.ReactorControl ? next : resolveLengthyAction(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func<LevelState, LevelState> spendAction)
|
public static LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy, Func<LevelState, LevelState> resolveLengthyAction)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level))
|
if (!CanAct(level))
|
||||||
return Refuse(level, "NO ACTIONS");
|
return Refuse(level, "NO CONTROL");
|
||||||
|
|
||||||
var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position);
|
var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position);
|
||||||
if (leakIndex < 0)
|
if (leakIndex < 0)
|
||||||
@@ -49,15 +49,15 @@ internal static class PlayerActionSystem
|
|||||||
|
|
||||||
var leak = level.Leaks[leakIndex];
|
var leak = level.Leaks[leakIndex];
|
||||||
var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak);
|
var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak);
|
||||||
return spendAction(next);
|
return resolveLengthyAction(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LevelState ApplyHeatShield(LevelState level, Func<LevelState, LevelState> spendAction)
|
public static LevelState ApplyHeatShield(LevelState level, Func<LevelState, LevelState> resolveLengthyAction)
|
||||||
{
|
{
|
||||||
if (!CanSpendAction(level) || level.Robot.HeatShields <= 0)
|
if (!CanAct(level) || level.Robot.HeatShields <= 0)
|
||||||
return Refuse(level, "NO HEAT SHIELD");
|
return Refuse(level, "NO HEAT SHIELD");
|
||||||
|
|
||||||
return spendAction(level with {
|
return resolveLengthyAction(level with {
|
||||||
Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps }
|
Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -68,15 +68,10 @@ internal static class PlayerActionSystem
|
|||||||
return level.SetProp(position, prop with { SwitchState = switchState });
|
return level.SetProp(position, prop with { SwitchState = switchState });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState ToggleDoor(LevelState level, GridPosition position)
|
private static LevelState ToggleDoor(LevelState level, GridPosition position, PropState prop)
|
||||||
{
|
{
|
||||||
var doors = level.Doors.ToArray();
|
var doorState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open;
|
||||||
var index = Array.FindIndex(doors, door => door.A == position || door.B == position);
|
return level.SetProp(position, prop with { DoorState = doorState });
|
||||||
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)
|
private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop)
|
||||||
@@ -91,7 +86,12 @@ internal static class PlayerActionSystem
|
|||||||
{
|
{
|
||||||
var leaks = level.Leaks.ToArray();
|
var leaks = level.Leaks.ToArray();
|
||||||
leaks[leakIndex] = leak with { Repaired = true };
|
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 };
|
return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with {
|
||||||
|
State = EUndergroundState.Intact,
|
||||||
|
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity
|
||||||
|
}) with {
|
||||||
|
Leaks = leaks
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState ApplyElementRemedy(LevelState level, LeakState leak)
|
private static LevelState ApplyElementRemedy(LevelState level, LeakState leak)
|
||||||
@@ -121,9 +121,9 @@ internal static class PlayerActionSystem
|
|||||||
return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count });
|
return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CanSpendAction(LevelState level)
|
private static bool CanAct(LevelState level)
|
||||||
{
|
{
|
||||||
return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0;
|
return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState Refuse(LevelState level, string message)
|
private static LevelState Refuse(LevelState level, string message)
|
||||||
|
|||||||
@@ -34,41 +34,54 @@ internal static class ReactorSystem
|
|||||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } };
|
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } };
|
||||||
|
|
||||||
var hasCritical = level.Surface.Any(surface => SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(surface.Heat) == EBand.Critical);
|
var hasCritical = level.Surface.Any(surface => SimulationBands.Fuel(surface.Fuel) == EBand.Critical || SimulationBands.Coolant(surface.Coolant) == EBand.Critical || SimulationBands.Electricity(surface.Electricity) == EBand.Critical || SimulationBands.Heat(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 hasCaution = hasCritical
|
||||||
|
|| !HasRequiredConsumers(level)
|
||||||
|
|| level.Props.Any(prop => prop.Type == EPropType.Consumer && HasConsumerTrouble(prop))
|
||||||
|
|| level.Leaks.Any(leak => !leak.Repaired);
|
||||||
var state = hasCritical ? ELevelState.Critical :
|
var state = hasCritical ? ELevelState.Critical :
|
||||||
hasCaution ? ELevelState.Caution : ELevelState.Stable;
|
hasCaution ? ELevelState.Caution : ELevelState.Stable;
|
||||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
|
return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool MatchesReady(LevelState level, RulePredicate predicate)
|
private static bool IsReady(LevelState level, ReactorState reactor)
|
||||||
{
|
{
|
||||||
return level.Reactors.Any(reactor => MatchesId(reactor, predicate.ReactorId) && reactor.Ready) == predicate.BoolValue;
|
return ReactorFeedsPresentAndProducing(level, reactor.ControlPosition)
|
||||||
}
|
&& ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers
|
||||||
|
&& ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers
|
||||||
public static bool MatchesWon(LevelState level, RulePredicate predicate)
|
&& ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers
|
||||||
{
|
|
||||||
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 static bool IsReady(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;
|
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier)
|
private static bool ReactorFeedsPresentAndProducing(LevelState level, GridPosition position)
|
||||||
{
|
{
|
||||||
return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier;
|
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||||
|
{
|
||||||
|
var underground = level.GetUnderground(position, carrier);
|
||||||
|
if (underground.IsPresent && (underground.Amount <= 0 || underground.Intensity <= 0))
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool MatchesId(ReactorBinding reactor, int reactorId)
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ProducingConsumerCount(LevelState level, ECarrierType carrier)
|
||||||
{
|
{
|
||||||
return reactorId <= 0 || reactor.ReactorId == reactorId;
|
return LevelTraversal.AllPositions(level)
|
||||||
|
.Count(position => level.GetProp(position) is { Type: EPropType.Consumer } prop && prop.ServiceStateFor(carrier) == EConsumerServiceState.Producing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasRequiredConsumers(LevelState level)
|
||||||
|
{
|
||||||
|
return ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers
|
||||||
|
&& ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers
|
||||||
|
&& ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasConsumerTrouble(PropState prop)
|
||||||
|
{
|
||||||
|
return prop.FuelServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled
|
||||||
|
|| prop.CoolantServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled
|
||||||
|
|| prop.ElectricityServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState Refuse(LevelState level, string message)
|
private static LevelState Refuse(LevelState level, string message)
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
namespace ReactorMaintenance.Simulation;
|
|
||||||
|
|
||||||
internal static class RuleEventSystem
|
|
||||||
{
|
|
||||||
public static LevelState Apply(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 = ApplyEffect(next, effect);
|
|
||||||
|
|
||||||
ruleEvents[item.Index] = item.Event with { Triggered = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
return next with { RuleEvents = ruleEvents };
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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 => ReactorSystem.MatchesReady(level, predicate),
|
|
||||||
ERulePredicateKind.ReactorLostIs => level.Global.LevelState == ELevelState.Lost == predicate.BoolValue,
|
|
||||||
ERulePredicateKind.ReactorWonIs => ReactorSystem.MatchesWon(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) && SimulationBands.NetworkBand(level.GetUnderground(predicate.Position, predicate.Carrier), predicate.Carrier, predicate.NetworkValue) >= predicate.Band,
|
|
||||||
ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SimulationBands.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 ApplyEffect(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, SurfaceCarrierMath.AddCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)),
|
|
||||||
ERuleEffectKind.RemoveSurfaceHazard => level.SetSurface(effect.Position, SurfaceCarrierMath.AddCarrier(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] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
namespace ReactorMaintenance.Simulation;
|
||||||
|
|
||||||
|
internal static class StructuralIntegritySystem
|
||||||
|
{
|
||||||
|
public static LevelState Resolve(LevelState level)
|
||||||
|
{
|
||||||
|
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||||
|
level = ResolveCarrier(level, carrier);
|
||||||
|
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LevelState ResolveCarrier(LevelState level, ECarrierType carrier)
|
||||||
|
{
|
||||||
|
var layer = level.Layer(carrier).ToArray();
|
||||||
|
var leaks = level.Leaks.ToList();
|
||||||
|
|
||||||
|
foreach (var position in LevelTraversal.AllPositions(level))
|
||||||
|
{
|
||||||
|
var index = level.Index(position);
|
||||||
|
var cell = layer[index];
|
||||||
|
if (!cell.IsPresent)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var integrity = DegradeIntegrity(cell, carrier);
|
||||||
|
var state = cell.State;
|
||||||
|
if (state != EUndergroundState.Leaking && integrity <= Balancing.Current.StructuralIntegrityLeakThreshold && cell.Intensity > 0)
|
||||||
|
{
|
||||||
|
state = EUndergroundState.Leaking;
|
||||||
|
leaks = UpsertLeak(leaks, level, position, carrier);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer[index] = cell with { State = state, StructuralIntegrity = integrity };
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = 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.")
|
||||||
|
};
|
||||||
|
|
||||||
|
return next with { Leaks = leaks.ToArray() };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DegradeIntegrity(UndergroundCell cell, ECarrierType carrier)
|
||||||
|
{
|
||||||
|
if (cell.StructuralIntegrity >= Balancing.Current.MaxStructuralIntegrity && cell.Intensity <= Balancing.Current.StructuralPressureThreshold(carrier))
|
||||||
|
return Balancing.Current.MaxStructuralIntegrity;
|
||||||
|
|
||||||
|
var overPressure = Math.Max(0, cell.Intensity - Balancing.Current.StructuralPressureThreshold(carrier));
|
||||||
|
if (overPressure <= 0 || cell.StructuralIntegrity >= Balancing.Current.MaxStructuralIntegrity)
|
||||||
|
return Math.Clamp(cell.StructuralIntegrity, 0, Balancing.Current.MaxStructuralIntegrity);
|
||||||
|
|
||||||
|
var damage = Math.Max(1, (int)Math.Ceiling(overPressure * Balancing.Current.StructuralIntegrityDamageScale));
|
||||||
|
return Math.Clamp(cell.StructuralIntegrity - damage, 0, Balancing.Current.MaxStructuralIntegrity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<LeakState> UpsertLeak(List<LeakState> leaks, LevelState level, GridPosition position, ECarrierType carrier)
|
||||||
|
{
|
||||||
|
var accessPosition = carrier == ECarrierType.Electricity
|
||||||
|
? position.Neighbors().FirstOrDefault(level.IsFloor)
|
||||||
|
: position;
|
||||||
|
if (accessPosition is null || !level.IsFloor(accessPosition))
|
||||||
|
return leaks;
|
||||||
|
|
||||||
|
var index = leaks.FindIndex(leak => leak.UndergroundPosition == position && leak.Carrier == carrier);
|
||||||
|
var leakState = new LeakState {
|
||||||
|
Carrier = carrier,
|
||||||
|
UndergroundPosition = position,
|
||||||
|
AccessPosition = accessPosition,
|
||||||
|
Repaired = false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (index >= 0)
|
||||||
|
leaks[index] = leakState;
|
||||||
|
else
|
||||||
|
leaks.Add(leakState);
|
||||||
|
|
||||||
|
return leaks;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -280,7 +280,6 @@ public sealed partial class MainWindow
|
|||||||
ClearPendingEditorOperation();
|
ClearPendingEditorOperation();
|
||||||
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
|
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
|
||||||
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(),
|
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(),
|
||||||
Doors = m_Level.Doors.Where(door => door.A != position && door.B != position).ToArray(),
|
|
||||||
Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray()
|
Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray()
|
||||||
};
|
};
|
||||||
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
||||||
@@ -384,6 +383,11 @@ public sealed partial class MainWindow
|
|||||||
return m_Level.InBounds(position) ? m_Level.GetTerrain(position) : ECellTerrain.Wall;
|
return m_Level.InBounds(position) ? m_Level.GetTerrain(position) : ECellTerrain.Wall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsWall(GridPosition position)
|
||||||
|
{
|
||||||
|
return m_Level.InBounds(position) && m_Level.GetTerrain(position) == ECellTerrain.Wall;
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
|
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||||
{
|
{
|
||||||
foreach (var position in AllPositions())
|
foreach (var position in AllPositions())
|
||||||
@@ -431,11 +435,19 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
|
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||||
{
|
{
|
||||||
foreach (var door in m_Level.Doors)
|
foreach (var position in AllPositions())
|
||||||
{
|
{
|
||||||
var centerA = Center(layout.CellRect(door.A));
|
var prop = m_Level.GetProp(position);
|
||||||
var centerB = Center(layout.CellRect(door.B));
|
if (prop.Type != EPropType.Door)
|
||||||
drawing.DrawLine((float)centerA.X, (float)centerA.Y, (float)centerB.X, (float)centerB.Y, door.State == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, 5);
|
continue;
|
||||||
|
|
||||||
|
var rect = layout.CellRect(position);
|
||||||
|
var center = Center(rect);
|
||||||
|
var color = prop.DoorState == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed;
|
||||||
|
if (IsWall(new(position.X, position.Y - 1)) && IsWall(new(position.X, position.Y + 1)))
|
||||||
|
drawing.DrawLine((float)rect.Left, (float)center.Y, (float)rect.Right, (float)center.Y, color, 5);
|
||||||
|
else
|
||||||
|
drawing.DrawLine((float)center.X, (float)rect.Top, (float)center.X, (float)rect.Bottom, color, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,11 +566,11 @@ public sealed partial class MainWindow
|
|||||||
LevelNameText.Text = m_Level.Name;
|
LevelNameText.Text = m_Level.Name;
|
||||||
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
|
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
|
||||||
StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}";
|
StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}";
|
||||||
GlobalText.Text = $"Actions: {m_Level.Global.ActionsRemaining}/{Balancing.Current.ActionsPerTurn}\n"
|
var doorCount = m_Level.Props.Count(prop => prop.Type == EPropType.Door);
|
||||||
+ $"All-seeing-eye: {(m_Level.Global.AllSeeingEyeUnlocked ? "unlocked" : "locked")}\n"
|
GlobalText.Text = $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n"
|
||||||
+ $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n"
|
|
||||||
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
|
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
|
||||||
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {m_Level.Doors.Count}";
|
+ $"Required consumers F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}\n"
|
||||||
|
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {doorCount}";
|
||||||
|
|
||||||
CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected.";
|
CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected.";
|
||||||
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
|
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
|
||||||
@@ -588,20 +600,9 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private void ApplyDoorTool(GridPosition position)
|
private void ApplyDoorTool(GridPosition position)
|
||||||
{
|
{
|
||||||
if (!m_Level.IsFloor(position))
|
ClearPendingEditorOperation();
|
||||||
return;
|
|
||||||
|
|
||||||
if (m_PendingDoorCell is not { } pending)
|
|
||||||
{
|
|
||||||
m_PendingDoorCell = position;
|
|
||||||
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
|
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
|
||||||
RefreshForecasts();
|
RefreshForecasts();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_Level = LevelEditor.SetDoorEdge(m_Level, pending, position);
|
|
||||||
m_PendingDoorCell = null;
|
|
||||||
RefreshForecasts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyElectricityLeakTool(GridPosition position)
|
private void ApplyElectricityLeakTool(GridPosition position)
|
||||||
@@ -641,42 +642,17 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private void AddWarningRule_Click(object sender, RoutedEventArgs e)
|
private void AddWarningRule_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var turn = m_Level.Global.Turn + 1;
|
StatusText.Text = "Rule events were removed from level authoring.";
|
||||||
m_Level = LevelEditor.AddRuleEvent(m_Level, new() {
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
ForecastText = $"Warning on turn {turn}",
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = turn }],
|
|
||||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = $"Authored warning on turn {turn}" }]
|
|
||||||
});
|
|
||||||
RefreshForecasts();
|
|
||||||
RefreshInspector();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddLeakRule_Click(object sender, RoutedEventArgs e)
|
private void AddLeakRule_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (m_SelectedCell is not { } position || !TryGetAuthoredLeakCarrier(position, out var carrier))
|
StatusText.Text = "Rule events were removed from level authoring.";
|
||||||
return;
|
|
||||||
|
|
||||||
var turn = m_Level.Global.Turn + 1;
|
|
||||||
m_Level = LevelEditor.AddRuleEvent(m_Level, new() {
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
ForecastText = $"{carrier} leak starts on turn {turn}",
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = turn }],
|
|
||||||
Effects = [new() { Kind = ERuleEffectKind.StartLeak, Position = position, AccessPosition = position, Carrier = carrier }]
|
|
||||||
});
|
|
||||||
RefreshForecasts();
|
|
||||||
RefreshInspector();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveLastRule_Click(object sender, RoutedEventArgs e)
|
private void RemoveLastRule_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var ruleEvent = m_Level.RuleEvents.LastOrDefault();
|
StatusText.Text = "Rule events were removed from level authoring.";
|
||||||
if (ruleEvent is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_Level = LevelEditor.RemoveRuleEvent(m_Level, ruleEvent.Id);
|
|
||||||
RefreshForecasts();
|
|
||||||
RefreshInspector();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BindSelectedConsumer(ECarrierType carrier)
|
private void BindSelectedConsumer(ECarrierType carrier)
|
||||||
@@ -684,9 +660,7 @@ public sealed partial class MainWindow
|
|||||||
if (m_SelectedCell is not { } position || m_SelectedReactorId is not { } reactorId)
|
if (m_SelectedCell is not { } position || m_SelectedReactorId is not { } reactorId)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_Level = LevelEditor.BindReactorConsumer(m_Level, reactorId, carrier, position);
|
StatusText.Text = "Reactors now use required consumer counts instead of bindings.";
|
||||||
RefreshForecasts();
|
|
||||||
RefreshInspector();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CellInspectionText(GridPosition position)
|
private string CellInspectionText(GridPosition position)
|
||||||
@@ -698,7 +672,7 @@ public sealed partial class MainWindow
|
|||||||
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
||||||
return $"Position: {position.X},{position.Y}\n"
|
return $"Position: {position.X},{position.Y}\n"
|
||||||
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
|
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
|
||||||
+ $"Prop: {prop.Type} {prop.Carrier} {prop.SwitchState} {prop.ServiceState}\n"
|
+ $"Prop: {prop.Type} {prop.SwitchState} {prop.ServiceState}\n"
|
||||||
+ $"Fuel: {UndergroundText(fuel)}\n"
|
+ $"Fuel: {UndergroundText(fuel)}\n"
|
||||||
+ $"Coolant: {UndergroundText(coolant)}\n"
|
+ $"Coolant: {UndergroundText(coolant)}\n"
|
||||||
+ $"Electricity: {UndergroundText(electricity)}\n"
|
+ $"Electricity: {UndergroundText(electricity)}\n"
|
||||||
@@ -708,9 +682,6 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private string WorkflowInspectionText()
|
private string WorkflowInspectionText()
|
||||||
{
|
{
|
||||||
if (m_PendingDoorCell is { } door)
|
|
||||||
return $"Door edge: select an adjacent floor for {door.X},{door.Y}.";
|
|
||||||
|
|
||||||
if (m_PendingElectricityLeakCell is { } leak)
|
if (m_PendingElectricityLeakCell is { } leak)
|
||||||
return $"Electricity leak face: select adjacent floor access for {leak.X},{leak.Y}.";
|
return $"Electricity leak face: select adjacent floor access for {leak.X},{leak.Y}.";
|
||||||
|
|
||||||
@@ -724,18 +695,14 @@ public sealed partial class MainWindow
|
|||||||
return "Select or place a reactor control.";
|
return "Select or place a reactor control.";
|
||||||
|
|
||||||
return $"Reactor {reactor.ReactorId}\n"
|
return $"Reactor {reactor.ReactorId}\n"
|
||||||
+ $"Fuel: {PositionText(reactor.FuelConsumerPosition)}\n"
|
+ $"Control: {PositionText(reactor.ControlPosition)}\n"
|
||||||
+ $"Coolant: {PositionText(reactor.CoolantConsumerPosition)}\n"
|
+ $"Ready: {reactor.Ready}\n"
|
||||||
+ $"Electric: {PositionText(reactor.ElectricityConsumerPosition)}";
|
+ $"Required F/C/E: {m_Level.RequiredFuelConsumers}/{m_Level.RequiredCoolantConsumers}/{m_Level.RequiredElectricityConsumers}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string RuleEventInspectionText()
|
private string RuleEventInspectionText()
|
||||||
{
|
{
|
||||||
if (m_Level.RuleEvents.Count == 0)
|
return "Rule events were removed from level authoring.";
|
||||||
return "No authored rule events.";
|
|
||||||
|
|
||||||
var last = m_Level.RuleEvents[^1];
|
|
||||||
return $"{m_Level.RuleEvents.Count} authored. Last: {last.Id} ({last.Phase}).";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string PositionText(GridPosition position)
|
private static string PositionText(GridPosition position)
|
||||||
@@ -745,7 +712,7 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private static string UndergroundText(UndergroundCell cell)
|
private static string UndergroundText(UndergroundCell cell)
|
||||||
{
|
{
|
||||||
return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)}";
|
return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)} integrity {cell.StructuralIntegrity}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string Format(float value)
|
private static string Format(float value)
|
||||||
@@ -762,26 +729,25 @@ public sealed partial class MainWindow
|
|||||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||||
level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
||||||
level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
||||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer });
|
||||||
level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer });
|
||||||
level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer });
|
||||||
level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||||
level = level.SetProp(new(11, 4), new() { Type = EPropType.AllSeeingEyeTerminal });
|
level = level.SetProp(new(11, 4), new() { Type = EPropType.AllSeeingEyeTerminal });
|
||||||
level = level.SetProp(new(11, 6), new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield });
|
level = level.SetProp(new(11, 6), new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield });
|
||||||
level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with {
|
level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with {
|
||||||
Leaks = [new() { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
|
Leaks = [new() { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
|
||||||
Doors = [new() { A = new(8, 5), B = new(9, 5), State = EDoorState.Closed }],
|
|
||||||
Robot = new() { Position = new(10, 5) },
|
Robot = new() { Position = new(10, 5) },
|
||||||
Reactors = [
|
Reactors = [
|
||||||
new() {
|
new() {
|
||||||
ReactorId = 1,
|
ReactorId = 1,
|
||||||
ControlPosition = new(10, 5),
|
ControlPosition = new(10, 5)
|
||||||
FuelConsumerPosition = new(5, 3),
|
|
||||||
CoolantConsumerPosition = new(5, 5),
|
|
||||||
ElectricityConsumerPosition = new(5, 7)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
level = level.SetTerrain(new(8, 4), ECellTerrain.Wall);
|
||||||
|
level = level.SetTerrain(new(8, 6), ECellTerrain.Wall);
|
||||||
|
level = level.SetProp(new(8, 5), new() { Type = EPropType.Door });
|
||||||
|
|
||||||
return level with { Forecasts = new SimulationEngine().Forecast(level) };
|
return level with { Forecasts = new SimulationEngine().Forecast(level) };
|
||||||
}
|
}
|
||||||
@@ -860,7 +826,7 @@ public sealed partial class MainWindow
|
|||||||
{
|
{
|
||||||
return prop.Type switch {
|
return prop.Type switch {
|
||||||
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
|
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
|
||||||
EPropType.Consumer => $"{CarrierShort(prop.Carrier)} CON",
|
EPropType.Consumer => "CON",
|
||||||
EPropType.Junction => $"J {prop.JunctionMode}",
|
EPropType.Junction => $"J {prop.JunctionMode}",
|
||||||
EPropType.Door => "DOOR",
|
EPropType.Door => "DOOR",
|
||||||
EPropType.AllSeeingEyeTerminal => "EYE",
|
EPropType.AllSeeingEyeTerminal => "EYE",
|
||||||
@@ -975,7 +941,6 @@ public sealed partial class MainWindow
|
|||||||
|
|
||||||
private void ClearPendingEditorOperation()
|
private void ClearPendingEditorOperation()
|
||||||
{
|
{
|
||||||
m_PendingDoorCell = null;
|
|
||||||
m_PendingElectricityLeakCell = null;
|
m_PendingElectricityLeakCell = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,7 +970,6 @@ public sealed partial class MainWindow
|
|||||||
private LevelState m_Level;
|
private LevelState m_Level;
|
||||||
private double m_PanX;
|
private double m_PanX;
|
||||||
private double m_PanY;
|
private double m_PanY;
|
||||||
private GridPosition? m_PendingDoorCell;
|
|
||||||
private GridPosition? m_PendingElectricityLeakCell;
|
private GridPosition? m_PendingElectricityLeakCell;
|
||||||
private CanvasBitmap? m_RobotSprite;
|
private CanvasBitmap? m_RobotSprite;
|
||||||
private GridPosition? m_SelectedCell;
|
private GridPosition? m_SelectedCell;
|
||||||
|
|||||||
@@ -3,18 +3,27 @@
|
|||||||
public sealed class LevelEditorTests
|
public sealed class LevelEditorTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DoorToolRequiresExplicitAdjacentEdgeSelection()
|
public void DoorToolPlacesSingleFloorDoorProp()
|
||||||
{
|
{
|
||||||
var level = LevelState.Create("Door editor", 6, 6);
|
var level = LevelState.Create("Door editor", 6, 6);
|
||||||
|
|
||||||
var withDoorProp = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||||
var withDoorEdge = LevelEditor.SetDoorEdge(withDoorProp, new(2, 2), new(3, 2));
|
|
||||||
var rejected = LevelEditor.SetDoorEdge(withDoorEdge, new(2, 2), new(4, 2));
|
|
||||||
|
|
||||||
Assert.Equal(EPropType.Door, withDoorProp.GetProp(new(2, 2)).Type);
|
Assert.Equal(EPropType.Door, next.GetProp(new(2, 2)).Type);
|
||||||
Assert.Empty(withDoorProp.Doors);
|
Assert.Equal(EDoorState.Closed, next.GetProp(new(2, 2)).DoorState);
|
||||||
Assert.Single(withDoorEdge.Doors);
|
}
|
||||||
Assert.Equal(withDoorEdge.Doors, rejected.Doors);
|
|
||||||
|
[Fact]
|
||||||
|
public void ConsumerToolPlacesCarrierAgnosticConsumer()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Consumer editor", 6, 6);
|
||||||
|
|
||||||
|
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Consumer, Carrier = ECarrierType.Fuel });
|
||||||
|
|
||||||
|
Assert.Equal(EPropType.Consumer, next.GetProp(new(2, 2)).Type);
|
||||||
|
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).FuelServiceState);
|
||||||
|
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).CoolantServiceState);
|
||||||
|
Assert.Equal(EConsumerServiceState.Unknown, next.GetProp(new(2, 2)).ElectricityServiceState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -33,44 +42,14 @@ public sealed class LevelEditorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReactorBindingUpdatesOnlyMatchingCarrierConsumer()
|
public void ReactorControlToolCreatesUnboundReactorState()
|
||||||
{
|
{
|
||||||
var level = LevelState.Create("Binding editor", 8, 6);
|
var level = LevelState.Create("Reactor editor", 6, 6);
|
||||||
level = level.SetProp(new(1, 1), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
|
||||||
level = level.SetProp(new(2, 1), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
|
||||||
level = level.SetProp(new(3, 1), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant }) with {
|
|
||||||
Reactors = [
|
|
||||||
new() {
|
|
||||||
ReactorId = 1,
|
|
||||||
ControlPosition = new(1, 1),
|
|
||||||
FuelConsumerPosition = new(1, 1),
|
|
||||||
CoolantConsumerPosition = new(1, 1),
|
|
||||||
ElectricityConsumerPosition = new(1, 1)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
var bound = LevelEditor.BindReactorConsumer(level, 1, ECarrierType.Fuel, new(2, 1));
|
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.ReactorControl });
|
||||||
var rejected = LevelEditor.BindReactorConsumer(bound, 1, ECarrierType.Fuel, new(3, 1));
|
|
||||||
|
|
||||||
Assert.Equal(new(2, 1), bound.Reactors[0].FuelConsumerPosition);
|
Assert.Single(next.Reactors);
|
||||||
Assert.Equal(bound.Reactors[0].FuelConsumerPosition, rejected.Reactors[0].FuelConsumerPosition);
|
Assert.Equal(new(2, 2), next.Reactors[0].ControlPosition);
|
||||||
}
|
Assert.Equal(1, next.Reactors[0].ReactorId);
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RuleEventEditorAssignsStableIdsAndCanRemoveEvents()
|
|
||||||
{
|
|
||||||
var level = LevelState.Create("Rule editor", 6, 6);
|
|
||||||
|
|
||||||
var withRule = LevelEditor.AddRuleEvent(level, new() {
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 1 }],
|
|
||||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "authored" }]
|
|
||||||
});
|
|
||||||
var removed = LevelEditor.RemoveRuleEvent(withRule, "rule-1");
|
|
||||||
|
|
||||||
Assert.Single(withRule.RuleEvents);
|
|
||||||
Assert.Equal("rule-1", withRule.RuleEvents[0].Id);
|
|
||||||
Assert.Empty(removed.RuleEvents);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,19 +3,31 @@
|
|||||||
public sealed class SimulationEngineTests
|
public sealed class SimulationEngineTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor()
|
public void NetworkPropagationSuppliesConsumerServicesAndReadiesReactor()
|
||||||
{
|
{
|
||||||
var level = BuildReadyLevel();
|
var level = BuildReadyLevel();
|
||||||
|
|
||||||
var next = m_Engine.AdvanceTurn(level);
|
var next = m_Engine.AdvanceTurn(level);
|
||||||
|
var consumer = next.GetProp(new(3, 3));
|
||||||
|
|
||||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 2)).ServiceState);
|
Assert.Equal(EConsumerServiceState.Producing, consumer.FuelServiceState);
|
||||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
|
Assert.Equal(EConsumerServiceState.Producing, consumer.CoolantServiceState);
|
||||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState);
|
Assert.Equal(EConsumerServiceState.Producing, consumer.ElectricityServiceState);
|
||||||
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
||||||
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
|
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReactorNeedsPositiveFlowOnlyForNetworksBeneathControl()
|
||||||
|
{
|
||||||
|
var level = BuildReadyLevel();
|
||||||
|
level = level.SetUnderground(new(5, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||||
|
|
||||||
|
var next = m_Engine.AdvanceTurn(level);
|
||||||
|
|
||||||
|
Assert.NotEqual(ELevelState.Ready, next.Global.LevelState);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ReactorActivatesOnlyAtReadyControl()
|
public void ReactorActivatesOnlyAtReadyControl()
|
||||||
{
|
{
|
||||||
@@ -30,19 +42,110 @@ public sealed class SimulationEngineTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LeakingUndergroundCellInjectsMatchingSurfaceHazard()
|
public void DisabledConsumerReportsDisabledOnlyForNetworksBeneathIt()
|
||||||
{
|
{
|
||||||
var level = LevelState.Create("Leak", 6, 6);
|
var level = LevelState.Create("Disabled", 6, 6);
|
||||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with {
|
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||||
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer, SwitchState = EPropSwitchState.Disabled });
|
||||||
|
|
||||||
|
var next = m_Engine.AdvanceTurn(level);
|
||||||
|
var consumer = next.GetProp(new(2, 2));
|
||||||
|
|
||||||
|
Assert.Equal(EConsumerServiceState.Disabled, consumer.FuelServiceState);
|
||||||
|
Assert.Equal(EConsumerServiceState.Unknown, consumer.CoolantServiceState);
|
||||||
|
Assert.Equal(EConsumerServiceState.Unknown, consumer.ElectricityServiceState);
|
||||||
|
Assert.Equal(EConsumerServiceState.Disabled, consumer.ServiceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MovementIsQuickAndDoesNotResolveSimulationStep()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Quick", 6, 6) with {
|
||||||
|
Robot = new() { Position = new(1, 1) }
|
||||||
};
|
};
|
||||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
|
||||||
|
var next = m_Engine.MoveRobot(level, new(2, 1));
|
||||||
|
|
||||||
|
Assert.Equal(new(2, 1), next.Robot.Position);
|
||||||
|
Assert.Equal(0, next.Global.Turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoorInteractionIsLengthyAndResolvesSimulationStep()
|
||||||
|
{
|
||||||
|
var level = DoorLevel();
|
||||||
|
level = level with { Robot = new() { Position = new(3, 2) } };
|
||||||
|
|
||||||
|
var next = m_Engine.InteractProp(level);
|
||||||
|
|
||||||
|
Assert.Equal(EDoorState.Open, next.GetProp(new(3, 2)).DoorState);
|
||||||
|
Assert.Equal(1, next.Global.Turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClosedInferredDoorBlocksAdjacentHeatFlow()
|
||||||
|
{
|
||||||
|
var level = DoorLevel();
|
||||||
|
level = level.SetSurface(new(3, 2), new() { Heat = 8 });
|
||||||
|
|
||||||
var next = m_Engine.AdvanceTurn(level);
|
var next = m_Engine.AdvanceTurn(level);
|
||||||
|
|
||||||
|
Assert.Equal(0, next.GetSurface(new(4, 2)).Heat);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StructuralIntegrityCreatesLeakWhenWeakCellHasPositivePressure()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Integrity leak", 6, 6);
|
||||||
|
level = AddLine(level, ECarrierType.Fuel, new(1, 2), new(2, 2));
|
||||||
|
level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||||
|
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, level.GetUnderground(new(2, 2), ECarrierType.Fuel) with {
|
||||||
|
StructuralIntegrity = Balancing.Current.StructuralIntegrityLeakThreshold
|
||||||
|
});
|
||||||
|
|
||||||
|
var next = m_Engine.AdvanceTurn(level);
|
||||||
|
|
||||||
|
Assert.Equal(EUndergroundState.Leaking, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State);
|
||||||
|
Assert.Contains(next.Leaks, leak => leak.Carrier == ECarrierType.Fuel && leak.UndergroundPosition == new GridPosition(2, 2));
|
||||||
Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
|
Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HighPressureWorsensNonMaxStructuralIntegrity()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Integrity damage", 6, 6);
|
||||||
|
level = AddLine(level, ECarrierType.Fuel, new(1, 2), new(2, 2));
|
||||||
|
level = level.SetProp(new(1, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||||
|
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, level.GetUnderground(new(2, 2), ECarrierType.Fuel) with {
|
||||||
|
StructuralIntegrity = Balancing.Current.MaxStructuralIntegrity - 1
|
||||||
|
});
|
||||||
|
|
||||||
|
var next = m_Engine.AdvanceTurn(level);
|
||||||
|
|
||||||
|
Assert.True(next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity < Balancing.Current.MaxStructuralIntegrity - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RepairingLeakRestoresStructuralIntegrity()
|
||||||
|
{
|
||||||
|
var level = LevelState.Create("Repair", 6, 6);
|
||||||
|
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() {
|
||||||
|
State = EUndergroundState.Leaking,
|
||||||
|
Amount = 5,
|
||||||
|
Intensity = 5,
|
||||||
|
StructuralIntegrity = 0
|
||||||
|
}) with {
|
||||||
|
Robot = new() { Position = new(2, 2) },
|
||||||
|
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||||
|
};
|
||||||
|
|
||||||
|
var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, false);
|
||||||
|
|
||||||
|
Assert.True(next.Leaks[0].Repaired);
|
||||||
|
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(new(2, 2), ECarrierType.Fuel).State);
|
||||||
|
Assert.Equal(Balancing.Current.MaxStructuralIntegrity, next.GetUnderground(new(2, 2), ECarrierType.Fuel).StructuralIntegrity);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
||||||
{
|
{
|
||||||
@@ -60,19 +163,6 @@ public sealed class SimulationEngineTests
|
|||||||
Assert.Equal(0, next.Robot.FuelNeutralizers);
|
Assert.Equal(0, next.Robot.FuelNeutralizers);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ClosedDoorBlocksAdjacentHeatFlow()
|
|
||||||
{
|
|
||||||
var level = LevelState.Create("Door", 6, 6);
|
|
||||||
level = level.SetSurface(new(2, 2), new() { Heat = 8 }) with {
|
|
||||||
Doors = [new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }]
|
|
||||||
};
|
|
||||||
|
|
||||||
var next = m_Engine.AdvanceTurn(level);
|
|
||||||
|
|
||||||
Assert.Equal(0, next.GetSurface(new(3, 2)).Heat);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HeatShieldPreventsRobotHeatLoss()
|
public void HeatShieldPreventsRobotHeatLoss()
|
||||||
{
|
{
|
||||||
@@ -122,217 +212,19 @@ public sealed class SimulationEngineTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ValidatorRejectsJunctionWithoutTwoOrThreeOutflows()
|
public void ValidatorRejectsInvalidDoorGeometryAndWallHazards()
|
||||||
{
|
|
||||||
var level = BuildJunctionLevel(2);
|
|
||||||
level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new());
|
|
||||||
|
|
||||||
var report = new LevelValidator().Validate(level);
|
|
||||||
|
|
||||||
Assert.False(report.IsValid);
|
|
||||||
Assert.Contains(report.Errors, error => error.Message.Contains("one incoming branch and two or three outgoing branches", StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NonJunctionCellsAcceptBestFlowFromMultipleSourcePaths()
|
|
||||||
{
|
|
||||||
var level = LevelState.Create("Best path", 7, 7);
|
|
||||||
level = AddHorizontalUnderground(level, ECarrierType.Fuel, 1, 5, 3);
|
|
||||||
level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
|
||||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
|
||||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
|
||||||
|
|
||||||
var report = new LevelValidator().Validate(level);
|
|
||||||
var next = m_Engine.AdvanceTurn(level);
|
|
||||||
|
|
||||||
Assert.True(report.IsValid);
|
|
||||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RobotLosesOnUnsafeElementHazard()
|
|
||||||
{
|
|
||||||
var level = LevelState.Create("Unsafe", 6, 6);
|
|
||||||
level = level.SetSurface(new(2, 2), new() { Electricity = Balancing.Current.MaxValue }) with {
|
|
||||||
Robot = new() { Position = new(2, 2) }
|
|
||||||
};
|
|
||||||
|
|
||||||
var next = m_Engine.AdvanceTurn(level);
|
|
||||||
|
|
||||||
Assert.Equal(ELevelState.Lost, next.Global.LevelState);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void RuleEventCanCreateTerminalLossForecast()
|
|
||||||
{
|
|
||||||
var level = LevelState.Create("Rule", 6, 6) with {
|
|
||||||
RuleEvents = [
|
|
||||||
new() {
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
ForecastText = "containment failure",
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
|
||||||
Effects = [new() { Kind = ERuleEffectKind.MarkTerminalLoss, Message = "CONTAINMENT FAILURE" }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
var forecasts = m_Engine.Forecast(level);
|
|
||||||
|
|
||||||
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.RuleEvent && forecast.Message == "containment failure");
|
|
||||||
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() {
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.NetworkBandAt, Position = new(3, 2), Carrier = ECarrierType.Fuel, NetworkValue = ENetworkValueKind.Amount, Band = EBand.Critical }],
|
|
||||||
Effects = [new() { 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() {
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.ReactorReadyIs, ReactorId = 1, BoolValue = true }],
|
|
||||||
Effects = [new() { 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() {
|
|
||||||
Phase = ERuleEventPhase.StartOfSimulation,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.RobotInventoryAtLeast, Remedy = ERemedyType.FuelNeutralizer, InventoryCount = 1 }],
|
|
||||||
Effects = [new() { 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() { A = new(2, 2), B = new(1, 2), State = EDoorState.Closed },
|
|
||||||
new() { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed },
|
|
||||||
new() { A = new(2, 2), B = new(2, 1), State = EDoorState.Closed },
|
|
||||||
new() { A = new(2, 2), B = new(2, 3), State = EDoorState.Closed }
|
|
||||||
],
|
|
||||||
RuleEvents = [
|
|
||||||
new() {
|
|
||||||
Phase = ERuleEventPhase.StartOfSimulation,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
|
||||||
Effects = [
|
|
||||||
new() { Kind = ERuleEffectKind.RemoveSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 2 },
|
|
||||||
new() { Kind = ERuleEffectKind.RemoveHeat, Position = new(2, 2), Amount = 3 },
|
|
||||||
new() { 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() {
|
|
||||||
Phase = ERuleEventPhase.StartOfSimulation,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
|
||||||
Effects = [new() { 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() {
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.PropStateAt, Position = new(1, 1) }],
|
|
||||||
Effects = [
|
|
||||||
new() { Kind = ERuleEffectKind.AddSurfaceHazard, Position = new(2, 2), Carrier = ECarrierType.Fuel, Amount = 1 },
|
|
||||||
new() { 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()
|
|
||||||
{
|
{
|
||||||
var level = LevelState.Create("Invalid", 6, 6);
|
var level = LevelState.Create("Invalid", 6, 6);
|
||||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
level = level.SetProp(new(2, 2), new() { Type = EPropType.Door });
|
||||||
level = level with {
|
level = level.SetTerrain(new(4, 4), ECellTerrain.Wall);
|
||||||
Surface = level.Surface.ToArray(),
|
level = level with { Surface = level.Surface.ToArray() };
|
||||||
Reactors = [new() { ControlPosition = new(3, 3), FuelConsumerPosition = new(1, 1), CoolantConsumerPosition = new(1, 1), ElectricityConsumerPosition = new(1, 1) }]
|
level.Surface[level.Index(new(4, 4))] = new() { Heat = 1 };
|
||||||
};
|
|
||||||
level.Surface[level.Index(new(2, 2))] = new() { Heat = 1 };
|
|
||||||
|
|
||||||
var report = new LevelValidator().Validate(level);
|
var report = new LevelValidator().Validate(level);
|
||||||
|
|
||||||
Assert.False(report.IsValid);
|
Assert.False(report.IsValid);
|
||||||
|
Assert.Contains(report.Errors, error => error.Message.Contains("Door must be surrounded", StringComparison.Ordinal));
|
||||||
Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
|
Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
|
||||||
Assert.Contains(report.Errors, error => error.Message.Contains("Reactor binding", StringComparison.Ordinal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -343,36 +235,27 @@ public sealed class SimulationEngineTests
|
|||||||
var json = LevelSerializer.Serialize(level);
|
var json = LevelSerializer.Serialize(level);
|
||||||
var loaded = LevelSerializer.Deserialize(json);
|
var loaded = LevelSerializer.Deserialize(json);
|
||||||
|
|
||||||
Assert.Contains("\"Version\": 2", json);
|
Assert.Contains("\"Version\": 3", json);
|
||||||
Assert.Equal(level.Name, loaded.Name);
|
Assert.Equal(level.Name, loaded.Name);
|
||||||
Assert.Equal(EPropType.Flow, loaded.GetProp(new(2, 2)).Type);
|
Assert.Equal(EPropType.Consumer, loaded.GetProp(new(3, 3)).Type);
|
||||||
|
Assert.Equal(level.RequiredFuelConsumers, loaded.RequiredFuelConsumers);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LevelSerializationRoundTripsRuleEventsDoorsAndElectricityLeakFaces()
|
public void LevelSerializationRoundTripsPropDoorsAndElectricityLeakFaces()
|
||||||
{
|
{
|
||||||
var level = BuildReadyLevel();
|
var level = BuildReadyLevel();
|
||||||
level = level.SetTerrain(new(6, 4), ECellTerrain.Wall);
|
level = level.SetTerrain(new(6, 4), ECellTerrain.Wall);
|
||||||
level = level.SetUnderground(new(6, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Leaking }) with {
|
level = level.SetUnderground(new(6, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Leaking }) with {
|
||||||
Doors = [new() { A = new(5, 3), B = new(5, 4), State = EDoorState.Closed }],
|
Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }]
|
||||||
Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }],
|
|
||||||
RuleEvents = [
|
|
||||||
new() {
|
|
||||||
Id = "authored",
|
|
||||||
Phase = ERuleEventPhase.EndOfTurn,
|
|
||||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 2 }],
|
|
||||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "serialized" }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
level = DoorLevel(level);
|
||||||
|
|
||||||
var loaded = LevelSerializer.Deserialize(LevelSerializer.Serialize(level));
|
var loaded = LevelSerializer.Deserialize(LevelSerializer.Serialize(level));
|
||||||
|
|
||||||
Assert.Single(loaded.Doors);
|
Assert.Equal(EPropType.Door, loaded.GetProp(new(3, 2)).Type);
|
||||||
Assert.Single(loaded.Leaks);
|
Assert.Single(loaded.Leaks);
|
||||||
Assert.Single(loaded.RuleEvents);
|
|
||||||
Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition);
|
Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition);
|
||||||
Assert.Equal("authored", loaded.RuleEvents[0].Id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -380,43 +263,45 @@ public sealed class SimulationEngineTests
|
|||||||
{
|
{
|
||||||
var json = """
|
var json = """
|
||||||
{
|
{
|
||||||
"Version": 1,
|
"Version": 2,
|
||||||
"Level": {}
|
"Level": {}
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
||||||
|
|
||||||
Assert.Contains("Unsupported level file version 1", exception.Message);
|
Assert.Contains("Unsupported level file version 2", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState BuildReadyLevel()
|
private static LevelState BuildReadyLevel()
|
||||||
{
|
{
|
||||||
var level = LevelState.Create("Ready", 8, 7);
|
var level = LevelState.Create("Ready", 8, 7);
|
||||||
level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
|
level = AddLine(level, ECarrierType.Fuel, new(2, 3), new(3, 3));
|
||||||
level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
|
level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
|
||||||
level = AddLine(level, ECarrierType.Electricity, new(2, 4), new(3, 4));
|
level = AddLine(level, ECarrierType.Electricity, new(2, 3), new(3, 3));
|
||||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
||||||
|
level = level.SetUnderground(new(2, 2), ECarrierType.Coolant, new() { State = EUndergroundState.Intact });
|
||||||
|
level = level.SetUnderground(new(2, 3), ECarrierType.Coolant, new() { State = EUndergroundState.Intact });
|
||||||
level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
||||||
level = level.SetProp(new(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
level = level.SetUnderground(new(2, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
|
||||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
level = level.SetUnderground(new(2, 3), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
|
||||||
level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer });
|
||||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||||
return level with {
|
return level with {
|
||||||
Robot = new() { Position = new(5, 3) },
|
Robot = new() { Position = new(5, 3) },
|
||||||
Reactors = [
|
Reactors = [new() { ReactorId = 1, ControlPosition = new(5, 3) }]
|
||||||
new() {
|
|
||||||
ReactorId = 1,
|
|
||||||
ControlPosition = new(5, 3),
|
|
||||||
FuelConsumerPosition = new(3, 2),
|
|
||||||
CoolantConsumerPosition = new(3, 3),
|
|
||||||
ElectricityConsumerPosition = new(3, 4)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static LevelState DoorLevel(LevelState? seed = null)
|
||||||
|
{
|
||||||
|
var level = seed ?? LevelState.Create("Door", 6, 6);
|
||||||
|
level = level.SetTerrain(new(3, 1), ECellTerrain.Wall);
|
||||||
|
level = level.SetTerrain(new(3, 3), ECellTerrain.Wall);
|
||||||
|
return level.SetProp(new(3, 2), new() { Type = EPropType.Door, DoorState = EDoorState.Closed });
|
||||||
|
}
|
||||||
|
|
||||||
private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
|
private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
|
||||||
{
|
{
|
||||||
level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
|
level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
|
||||||
@@ -435,13 +320,5 @@ public sealed class SimulationEngineTests
|
|||||||
return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode });
|
return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LevelState AddHorizontalUnderground(LevelState level, ECarrierType carrier, int startX, int endX, int y)
|
|
||||||
{
|
|
||||||
for (var x = startX; x <= endX; x++)
|
|
||||||
level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });
|
|
||||||
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly SimulationEngine m_Engine = new();
|
private readonly SimulationEngine m_Engine = new();
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user