Compare commits
10 Commits
3d406179bf
...
672f055a80
| Author | SHA1 | Date | |
|---|---|---|---|
| 672f055a80 | |||
| adf1475fc0 | |||
| 06d37aac10 | |||
| 99482c7011 | |||
| fbb7c0490c | |||
| dfe0cb3b6a | |||
| 884cc4503f | |||
| 0651603fd2 | |||
| 69ed79ce86 | |||
| e1ac56d201 |
@@ -1,3 +1,3 @@
|
||||
# Linux-specific instructions
|
||||
|
||||
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
|
||||
- After every iteration, run `dotnet jb cleanupcode --verbosity:ERROR ./ReactorMaintenance.slnx`.
|
||||
@@ -1,4 +1,4 @@
|
||||
# Windows-specific instructions
|
||||
|
||||
- After the implementation is finished, run `python D:\Code\crlf.py $file1 $file2 ...` for changed files you recognize, in order to normalize all line endings of all touched files to CRLF.
|
||||
- After every iteration, run `jb cleanupcode '$file1' '$file2' ...` for every C# file you touched.
|
||||
- After every iteration, run `jb cleanupcode --verbosity:ERROR ReactorMaintenance.slnx`.
|
||||
65
TASKS.md
65
TASKS.md
@@ -2,8 +2,13 @@
|
||||
|
||||
## Current State
|
||||
- Approved design iteration targets the simulation model, rule removal, action economy, reactor requirements, and editor layer workflow.
|
||||
- Work is proceeding on branch `design-iteration-structural-editor` in methodical commits: docs/tasks, simulation rework, editor rework, cleanup.
|
||||
- Work is proceeding on branch `design-iteration-structural-editor` in methodical commits.
|
||||
- Completed commits:
|
||||
- `787f1e5` Document approved design iteration.
|
||||
- `3d40617` Restore complete design system documentation.
|
||||
- `e1ac56d` Rework simulation rules.
|
||||
- Design documentation must preserve every existing system-level rule unless a change explicitly supersedes it. Superseded sections must document the replacement behavior with equal detail.
|
||||
- The next implementation iteration is the Win2D editor overhaul.
|
||||
|
||||
## Completed Work
|
||||
- Created the approved implementation plan for:
|
||||
@@ -15,11 +20,63 @@
|
||||
- quick/lengthy action economy,
|
||||
- all-seeing-eye viewing without persistent unlocking,
|
||||
- layer-aware editor visualization and tools.
|
||||
- Repaired the design documentation after the hazard-interaction regression:
|
||||
- restored the complete surface hazard interaction matrix,
|
||||
- documented that leaked coolant plus leaked fuel directly holds unless mediated by heat/electricity,
|
||||
- expanded structural integrity, consumer, reactor, all-seeing-eye, and action-economy details.
|
||||
- Reworked simulation state and systems:
|
||||
- removed data-driven rule predicates/effects/events from runtime state, validation, forecasts, serialization, and tests,
|
||||
- replaced explicit reactor consumer bindings with unbound reactor controls plus required fuel/coolant/electricity consumer counts,
|
||||
- made consumer props carrier-agnostic with per-carrier service state derived from networks beneath the cell,
|
||||
- moved doors from explicit edge state to door props on floor cells with orientation inferred from opposing wall cells,
|
||||
- added underground structural integrity, high-pressure degradation, automatic leak creation, structural forecasts, and repair-to-max behavior,
|
||||
- removed action budgets and made movement quick while mutating interactions resolve one simulation step,
|
||||
- removed persistent all-seeing-eye unlocking from simulation state,
|
||||
- bumped serialized level schema to version 3.
|
||||
- Updated simulation tests for the replacement systems:
|
||||
- consumer derivation and disabled consumer service state,
|
||||
- count-based reactor readiness and reactor-under-network positive-flow requirement,
|
||||
- quick movement versus lengthy door interaction,
|
||||
- inferred door blocking,
|
||||
- structural degradation, automatic leaks, repair integrity reset,
|
||||
- schema version 3 round-tripping and old-schema rejection.
|
||||
- Kept the Win2D project compiling after the simulation model changes with narrow compatibility edits. Full editor workflow/rendering work remains outstanding.
|
||||
- Verified after the simulation rework:
|
||||
- `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passed with 23 tests,
|
||||
- `dotnet build ReactorMaintenance.slnx` passed with 0 warnings.
|
||||
- Reworked the Win2D editor workflow:
|
||||
- added the Surface/Electricity/Fuel/Coolant layer combobox,
|
||||
- filtered tools by active layer and fixed exclusive tool selection,
|
||||
- rendered underground networks as carrier-colored centerline networks with source dots and layer opacity rules,
|
||||
- removed Rule Events, Reactor Binding, and pending workflow panels from the editor UI,
|
||||
- replaced two-click electricity leak authoring with electric-layer leak access cycling,
|
||||
- made Shift+left drag pan in all tools and Cursor drag move the robot or props.
|
||||
- Added editor-helper tests for electricity leak access cycling and cursor drag movement behavior.
|
||||
- Verified after the editor overhaul:
|
||||
- `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passed with 26 tests,
|
||||
- `dotnet build ReactorMaintenance.slnx` passed with 0 warnings.
|
||||
|
||||
## Current Work
|
||||
- Repair design documentation gaps before simulation implementation.
|
||||
- Rework simulation state, validation, serialization, forecasts, and tests.
|
||||
- Rework Win2D editor layer selection, rendering, tool filtering, drag behavior, and removed panels.
|
||||
- Editor overhaul implementation is complete; commit is pending.
|
||||
|
||||
## Editor Overhaul Requirements
|
||||
- Add a layer combobox with Surface, Electricity, Fuel, and Coolant.
|
||||
- When Surface is active, draw the surface layer at full opacity and all underground layers at 25% opacity.
|
||||
- When an underground layer is active, draw the surface layer at 50% opacity, other underground layers at 25% opacity, and the active underground layer at full opacity.
|
||||
- Render coolant blue, fuel red, and electricity yellow.
|
||||
- Render networks as thick lines connecting adjacent cell centers; render sources as large centered dots.
|
||||
- Make tools layer-aware:
|
||||
- Cursor is always available.
|
||||
- Heat, Floor, Walls, Props, Consumers, Hazards, and Doors are only available for Surface.
|
||||
- Network painting and Sources are only available on their respective underground layers.
|
||||
- Selecting a tool must deselect all other tools. The current two-way binding can leave multiple tools selected.
|
||||
- Shift+LMB should pan the view in all tools, including Cursor mode.
|
||||
- Cursor LMB drag should move any prop or robot from one cell to another.
|
||||
- Remove Rule Events UI.
|
||||
- Remove Reactor Binding UI.
|
||||
- Remove editor workflow and pending actions.
|
||||
- Door cells are redesigned as single prop cells.
|
||||
- Electricity leak neighbour should be toggled by using the electric leak tool on an existing electric leak cell.
|
||||
|
||||
## Future Work
|
||||
- Add authored sample levels once the new schema stabilizes.
|
||||
|
||||
@@ -455,6 +455,8 @@ The editor includes layer selection for Surface, Electricity, Fuel, and Coolant:
|
||||
- Networks render as thick lines connecting adjacent cell centers; sources render as large centered dots.
|
||||
- Tools are layer-aware. Cursor is always available. Surface terrain, props, consumers, hazards, doors, and heat tools are available only on Surface. Network painting and sources are available only on their matching underground layer.
|
||||
|
||||
Editor tool badges and drag previews use stable semantic image keys when assets are available. Assets may be added under `Images/Badges` or `Images/Elements` with filenames such as `tool-door.png`, `prop-reactor.png`, `carrier-fuel-source.png`, `leak-electricity.png`, or `robot.png`; missing assets fall back to compact procedural badges and text labels.
|
||||
|
||||
The serialized level schema stores level metadata, dimensions, terrain, underground layers including structural integrity, props and prop state, required reactor consumer counts, leaks, robot state, inventory, forecasts, and dynamic state when saving active play.
|
||||
|
||||
The loader accepts only schema-valid level data and returns clear errors for malformed data.
|
||||
|
||||
@@ -60,6 +60,16 @@ public abstract class Balancing
|
||||
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)
|
||||
{
|
||||
return new() {
|
||||
@@ -110,7 +120,6 @@ public abstract class Balancing
|
||||
public abstract int DefaultLevelWidth { get; }
|
||||
public abstract int DefaultLevelHeight { get; }
|
||||
public abstract int MinimumLevelSize { get; }
|
||||
public abstract int ActionsPerTurn { get; }
|
||||
public abstract int ForecastHorizon { get; }
|
||||
public abstract float MinValue { get; }
|
||||
public abstract float MaxValue { get; }
|
||||
@@ -139,6 +148,9 @@ public abstract class Balancing
|
||||
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
|
||||
public abstract float ConsumerRequiredAmount { 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 LeakAmountScale { get; }
|
||||
public abstract float LeakIntensityScale { get; }
|
||||
|
||||
@@ -5,7 +5,6 @@ public class NormalBalancing : Balancing
|
||||
public override int DefaultLevelWidth => 16;
|
||||
public override int DefaultLevelHeight => 12;
|
||||
public override int MinimumLevelSize => 4;
|
||||
public override int ActionsPerTurn => 3;
|
||||
public override int ForecastHorizon => 6;
|
||||
public override float MinValue => 0;
|
||||
public override float MaxValue => 10;
|
||||
@@ -48,6 +47,9 @@ public class NormalBalancing : Balancing
|
||||
|
||||
public override float ConsumerRequiredAmount => 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 LeakAmountScale => 0.15f;
|
||||
public override float LeakIntensityScale => 0.1f;
|
||||
|
||||
@@ -2,6 +2,132 @@
|
||||
|
||||
public static class LevelEditor
|
||||
{
|
||||
public sealed record MoveOccupantResult(bool Success, LevelState Level, string Reason)
|
||||
{
|
||||
public static MoveOccupantResult Succeeded(LevelState level)
|
||||
{
|
||||
return new(true, level, string.Empty);
|
||||
}
|
||||
|
||||
public static MoveOccupantResult Failed(LevelState level, string reason)
|
||||
{
|
||||
return new(false, level, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public static LevelState MoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||
{
|
||||
return TryMoveOccupant(level, source, destination).Level;
|
||||
}
|
||||
|
||||
public static MoveOccupantResult TryMoveOccupant(LevelState level, GridPosition source, GridPosition destination)
|
||||
{
|
||||
if (!level.InBounds(source))
|
||||
return MoveOccupantResult.Failed(level, "Drag start is outside the level.");
|
||||
|
||||
if (!level.InBounds(destination))
|
||||
return MoveOccupantResult.Failed(level, "Drop target is outside the level.");
|
||||
|
||||
if (source == destination)
|
||||
return MoveOccupantResult.Failed(level, "Drop target is the same cell.");
|
||||
|
||||
var prop = level.GetProp(source);
|
||||
if (prop.Type != EPropType.None)
|
||||
return TryMoveProp(level, source, destination, prop);
|
||||
|
||||
var leak = level.Leaks.FirstOrDefault(leak => !leak.Repaired && (leak.AccessPosition == source || leak.UndergroundPosition == source));
|
||||
if (leak is not null)
|
||||
return TryMoveLeak(level, leak, destination);
|
||||
|
||||
return level.Robot.Position == source
|
||||
? TryMoveRobot(level, destination)
|
||||
: MoveOccupantResult.Failed(level, "No movable robot, prop, source, or leak starts here.");
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Robot destination must be a floor cell.");
|
||||
|
||||
return MoveOccupantResult.Succeeded(level with { Robot = level.Robot with { Position = destination } });
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveProp(LevelState level, GridPosition source, GridPosition destination, PropState prop)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Prop destination must be a floor cell.");
|
||||
|
||||
if (level.GetProp(destination).Type != EPropType.None)
|
||||
return MoveOccupantResult.Failed(level, "Prop destination is already occupied.");
|
||||
|
||||
var next = level.SetProp(source, new()).SetProp(destination, prop);
|
||||
if (prop.Type != EPropType.ReactorControl)
|
||||
return MoveOccupantResult.Succeeded(next);
|
||||
|
||||
return MoveOccupantResult.Succeeded(next with {
|
||||
Reactors = next.Reactors
|
||||
.Select(reactor => reactor.ControlPosition == source ? reactor with { ControlPosition = destination } : reactor)
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
private static MoveOccupantResult TryMoveLeak(LevelState level, LeakState leak, GridPosition destination)
|
||||
{
|
||||
if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Fuel and coolant leaks must move to a floor cell.");
|
||||
|
||||
var next = ClearLeak(level, leak)
|
||||
.SetUnderground(leak.UndergroundPosition, leak.Carrier, new());
|
||||
return MoveOccupantResult.Succeeded(SetLeak(next, destination, destination, leak.Carrier));
|
||||
}
|
||||
|
||||
if (leak.Carrier == ECarrierType.Electricity)
|
||||
{
|
||||
if (!level.IsFloor(destination))
|
||||
return MoveOccupantResult.Failed(level, "Electric leak destination must be an adjacent floor access cell.");
|
||||
|
||||
var undergroundPosition = leak.UndergroundPosition;
|
||||
if (undergroundPosition.ManhattanDistance(destination) != 1)
|
||||
return MoveOccupantResult.Failed(level, "Electric leak destination must stay adjacent to its underground wall cell.");
|
||||
|
||||
return MoveOccupantResult.Succeeded(SetLeak(ClearLeak(level, leak), undergroundPosition, destination, leak.Carrier));
|
||||
}
|
||||
|
||||
return MoveOccupantResult.Failed(level, "Unsupported leak carrier.");
|
||||
}
|
||||
|
||||
private static LevelState ClearLeak(LevelState level, LeakState leak)
|
||||
{
|
||||
return level with {
|
||||
Leaks = level.Leaks.Where(candidate => candidate != leak).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public static LevelState CycleElectricityLeakAccess(LevelState level, GridPosition undergroundPosition)
|
||||
{
|
||||
if (!level.InBounds(undergroundPosition))
|
||||
return level;
|
||||
|
||||
if (!level.GetUnderground(undergroundPosition, ECarrierType.Electricity).IsPresent)
|
||||
return level;
|
||||
|
||||
var accessPositions = undergroundPosition.Neighbors().Where(level.IsFloor).ToArray();
|
||||
if (accessPositions.Length == 0)
|
||||
return level;
|
||||
|
||||
var existingLeak = level.Leaks.FirstOrDefault(leak => leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition == undergroundPosition);
|
||||
var nextAccessPosition = accessPositions[0];
|
||||
if (existingLeak is not null)
|
||||
{
|
||||
var index = Array.IndexOf(accessPositions, existingLeak.AccessPosition);
|
||||
nextAccessPosition = accessPositions[(index + 1) % accessPositions.Length];
|
||||
}
|
||||
|
||||
return SetLeak(level, undergroundPosition, nextAccessPosition, ECarrierType.Electricity);
|
||||
}
|
||||
|
||||
public static LevelState Apply(LevelState level, GridPosition position, EditorToolCommand command)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
@@ -13,9 +139,9 @@ public static class LevelEditor
|
||||
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
||||
EEditorTool.Underground => SetUnderground(level, position, 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.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }),
|
||||
EEditorTool.Door => ToggleOrSetDoor(level, position),
|
||||
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
|
||||
EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }),
|
||||
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||
@@ -27,19 +153,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)
|
||||
{
|
||||
if (!level.InBounds(undergroundPosition) || !level.IsFloor(accessPosition))
|
||||
@@ -51,7 +164,10 @@ public static class LevelEditor
|
||||
if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1)
|
||||
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 {
|
||||
Leaks = [
|
||||
.. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier),
|
||||
@@ -64,32 +180,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)
|
||||
{
|
||||
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)
|
||||
@@ -113,6 +209,21 @@ public static class LevelEditor
|
||||
return level.IsFloor(position) ? level.SetProp(position, prop) : level;
|
||||
}
|
||||
|
||||
private static LevelState ToggleOrSetDoor(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
return level;
|
||||
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Door)
|
||||
{
|
||||
var nextState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open;
|
||||
return level.SetProp(position, prop with { DoorState = nextState });
|
||||
}
|
||||
|
||||
return level.SetProp(position, new() { Type = EPropType.Door, DoorState = EDoorState.Closed });
|
||||
}
|
||||
|
||||
private static LevelState SetReactorControl(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
@@ -125,10 +236,7 @@ public static class LevelEditor
|
||||
.. level.Reactors,
|
||||
new() {
|
||||
ReactorId = id,
|
||||
ControlPosition = position,
|
||||
FuelConsumerPosition = position,
|
||||
CoolantConsumerPosition = position,
|
||||
ElectricityConsumerPosition = position
|
||||
ControlPosition = position
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -141,28 +249,4 @@ public static class LevelEditor
|
||||
|
||||
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;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
@@ -33,7 +33,7 @@ public static class LevelSerializer
|
||||
return level;
|
||||
}
|
||||
|
||||
private const int c_CurrentVersion = 2;
|
||||
private const int c_CurrentVersion = 3;
|
||||
|
||||
private static readonly JsonSerializerOptions s_Options = new() {
|
||||
WriteIndented = true,
|
||||
|
||||
@@ -42,7 +42,7 @@ public static class LevelStateExtensions
|
||||
|
||||
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)
|
||||
@@ -103,14 +103,34 @@ public static class LevelStateExtensions
|
||||
private static LevelState ClearFloorOnlyState(this LevelState level, GridPosition position)
|
||||
{
|
||||
return level.SetSurface(position, new())
|
||||
.SetProp(position, new())
|
||||
.SetUnderground(position, ECarrierType.Fuel, new())
|
||||
.SetUnderground(position, ECarrierType.Coolant, new())
|
||||
.SetUnderground(position, ECarrierType.Electricity, new());
|
||||
.SetProp(position, 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);
|
||||
ValidateReactors(level, errors, warnings);
|
||||
ValidateJunctions(level, errors);
|
||||
ValidateRuleEvents(level, errors);
|
||||
ValidateWarnings(level, warnings);
|
||||
|
||||
return new() { Errors = errors, Warnings = warnings };
|
||||
@@ -60,10 +59,21 @@ public sealed class LevelValidator
|
||||
|
||||
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)
|
||||
errors.Add(new("Door edge must connect two adjacent floor cells.", door.A));
|
||||
if (level.GetProp(position).Type != EPropType.Door)
|
||||
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)
|
||||
{
|
||||
if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
|
||||
errors.Add(new("Reactor binding 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);
|
||||
errors.Add(new("Reactor control position must point to a reactor control prop.", reactor.ControlPosition));
|
||||
|
||||
if (!reactor.Ready)
|
||||
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List<ValidationIssue> errors)
|
||||
{
|
||||
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));
|
||||
if (level.RequiredFuelConsumers < 0 || level.RequiredCoolantConsumers < 0 || level.RequiredElectricityConsumers < 0)
|
||||
errors.Add(new("Required consumer counts cannot be negative."));
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
@@ -243,12 +141,31 @@ public sealed class LevelValidator
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier))
|
||||
warnings.Add(new("Enabled consumer is initially starved.", position));
|
||||
if (prop.Type != EPropType.Consumer || prop.SwitchState != EPropSwitchState.Enabled)
|
||||
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)
|
||||
{
|
||||
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,
|
||||
ConsumerStarved,
|
||||
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 int Turn { get; init; }
|
||||
public int ActionsRemaining { get; init; } = Balancing.Current.ActionsPerTurn;
|
||||
public ELevelState LevelState { get; init; } = ELevelState.Stable;
|
||||
public string Status { get; init; } = "STABLE";
|
||||
public bool AllSeeingEyeUnlocked { get; init; }
|
||||
public bool TerminalLoss { get; init; }
|
||||
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 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 IReadOnlyList<DoorState> Doors { get; init; } = Array.Empty<DoorState>();
|
||||
public IReadOnlyList<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
|
||||
public IReadOnlyList<ReactorBinding> Reactors { get; init; } = Array.Empty<ReactorBinding>();
|
||||
public IReadOnlyList<RuleEventState> RuleEvents { get; init; } = Array.Empty<RuleEventState>();
|
||||
public IReadOnlyList<ReactorState> Reactors { get; init; } = Array.Empty<ReactorState>();
|
||||
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 GlobalState Global { get; init; } = new();
|
||||
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||
|
||||
@@ -2,14 +2,28 @@
|
||||
|
||||
public sealed record PropState
|
||||
{
|
||||
public EConsumerServiceState ServiceStateFor(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => FuelServiceState,
|
||||
ECarrierType.Coolant => CoolantServiceState,
|
||||
ECarrierType.Electricity => ElectricityServiceState,
|
||||
_ => EConsumerServiceState.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
public EPropType Type { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled;
|
||||
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 ERemedyType RemedyType { get; init; }
|
||||
public bool Depleted { get; init; }
|
||||
public int ReactorId { get; init; }
|
||||
public EDoorState DoorState { get; init; } = EDoorState.Closed;
|
||||
|
||||
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
|
||||
}
|
||||
@@ -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 float Amount { get; init; }
|
||||
public float Intensity { get; init; }
|
||||
public int StructuralIntegrity { get; init; } = Balancing.Current.MaxStructuralIntegrity;
|
||||
|
||||
public bool IsPresent => State != EUndergroundState.Absent;
|
||||
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)
|
||||
{
|
||||
return PlayerActionSystem.MoveRobot(level, destination, SpendAction);
|
||||
return PlayerActionSystem.MoveRobot(level, destination);
|
||||
}
|
||||
|
||||
public LevelState InteractProp(LevelState level)
|
||||
{
|
||||
return PlayerActionSystem.InteractProp(level, SpendAction);
|
||||
return PlayerActionSystem.InteractProp(level, ResolveStep);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return PlayerActionSystem.ApplyHeatShield(level, SpendAction);
|
||||
return PlayerActionSystem.ApplyHeatShield(level, ResolveStep);
|
||||
}
|
||||
|
||||
private LevelState ResolveStep(LevelState level)
|
||||
{
|
||||
return ResolveStep(level, true);
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
@@ -29,45 +34,37 @@ public sealed class SimulationEngine
|
||||
|
||||
public LevelState EndTurn(LevelState level)
|
||||
{
|
||||
return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } });
|
||||
return ResolveStep(level);
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return ResolveTurn(level);
|
||||
return ResolveStep(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)
|
||||
{
|
||||
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)
|
||||
private LevelState ResolveStep(LevelState level, bool refreshForecasts)
|
||||
{
|
||||
var report = m_Validator.Validate(level);
|
||||
if (!report.IsValid)
|
||||
return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } };
|
||||
|
||||
var next = RuleEventSystem.Apply(level, ERuleEventPhase.StartOfSimulation);
|
||||
var next = level;
|
||||
next = NetworkPropagationSystem.Propagate(next);
|
||||
next = ConsumerSystem.Resolve(next);
|
||||
next = StructuralIntegritySystem.Resolve(next);
|
||||
next = LeakSystem.Inject(next);
|
||||
next = SurfaceInteractionSystem.Resolve(next);
|
||||
next = RobotSafetySystem.Resolve(next);
|
||||
next = ReactorSystem.DeriveState(next);
|
||||
next = RuleEventSystem.Apply(next, ERuleEventPhase.EndOfTurn);
|
||||
next = SurfaceInteractionSystem.AdvanceDurations(next);
|
||||
next = next with {
|
||||
Global = next.Global with {
|
||||
Turn = next.Global.Turn + 1,
|
||||
ActionsRemaining = Balancing.Current.ActionsPerTurn
|
||||
Turn = next.Global.Turn + 1
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,15 +14,59 @@ internal static class ConsumerSystem
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var underground = level.GetUnderground(position, prop.Carrier);
|
||||
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
|
||||
props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved };
|
||||
var fuel = ServiceStateFor(level, position, ECarrierType.Fuel);
|
||||
var coolant = ServiceStateFor(level, position, ECarrierType.Coolant);
|
||||
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 };
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved)
|
||||
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved"));
|
||||
if (prop.Type == EPropType.Consumer)
|
||||
{
|
||||
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);
|
||||
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"));
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
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 spendAction(level with {
|
||||
return level with {
|
||||
Robot = level.Robot with {
|
||||
Position = destination,
|
||||
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))
|
||||
return Refuse(level, "NO ACTIONS");
|
||||
if (!CanAct(level))
|
||||
return Refuse(level, "NO CONTROL");
|
||||
|
||||
var position = level.Robot.Position;
|
||||
var prop = level.GetProp(position);
|
||||
@@ -28,20 +28,20 @@ internal static class PlayerActionSystem
|
||||
var next = prop.Type switch {
|
||||
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
|
||||
EPropType.Junction => CycleJunctionMode(level, position, prop),
|
||||
EPropType.Door => ToggleDoor(level, position),
|
||||
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } },
|
||||
EPropType.Door => ToggleDoor(level, position, prop),
|
||||
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { Status = "ALL-SEEING-EYE AVAILABLE" } },
|
||||
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
|
||||
EPropType.ReactorControl => ReactorSystem.Activate(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))
|
||||
return Refuse(level, "NO ACTIONS");
|
||||
if (!CanAct(level))
|
||||
return Refuse(level, "NO CONTROL");
|
||||
|
||||
var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position);
|
||||
if (leakIndex < 0)
|
||||
@@ -49,15 +49,15 @@ internal static class PlayerActionSystem
|
||||
|
||||
var leak = level.Leaks[leakIndex];
|
||||
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 spendAction(level with {
|
||||
return resolveLengthyAction(level with {
|
||||
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 });
|
||||
}
|
||||
|
||||
private static LevelState ToggleDoor(LevelState level, GridPosition position)
|
||||
private static LevelState ToggleDoor(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var doors = level.Doors.ToArray();
|
||||
var index = Array.FindIndex(doors, door => door.A == position || door.B == position);
|
||||
if (index < 0)
|
||||
return level;
|
||||
|
||||
doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open };
|
||||
return level with { Doors = doors };
|
||||
var doorState = prop.DoorState == EDoorState.Open ? EDoorState.Closed : EDoorState.Open;
|
||||
return level.SetProp(position, prop with { DoorState = doorState });
|
||||
}
|
||||
|
||||
private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop)
|
||||
@@ -91,7 +86,12 @@ internal static class PlayerActionSystem
|
||||
{
|
||||
var leaks = level.Leaks.ToArray();
|
||||
leaks[leakIndex] = leak with { Repaired = true };
|
||||
return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks };
|
||||
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)
|
||||
@@ -121,9 +121,9 @@ internal static class PlayerActionSystem
|
||||
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)
|
||||
|
||||
@@ -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" } };
|
||||
|
||||
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 :
|
||||
hasCaution ? ELevelState.Caution : ELevelState.Stable;
|
||||
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;
|
||||
}
|
||||
|
||||
public static bool MatchesWon(LevelState level, RulePredicate predicate)
|
||||
{
|
||||
var won = predicate.ReactorId > 0
|
||||
? level.Reactors.Any(reactor => reactor.ReactorId == predicate.ReactorId && reactor.Activated)
|
||||
: level.Global.LevelState == ELevelState.Won || level.Reactors.Any(reactor => reactor.Activated);
|
||||
return won == predicate.BoolValue;
|
||||
}
|
||||
|
||||
private 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)
|
||||
return ReactorFeedsPresentAndProducing(level, reactor.ControlPosition)
|
||||
&& ProducingConsumerCount(level, ECarrierType.Fuel) >= level.RequiredFuelConsumers
|
||||
&& ProducingConsumerCount(level, ECarrierType.Coolant) >= level.RequiredCoolantConsumers
|
||||
&& ProducingConsumerCount(level, ECarrierType.Electricity) >= level.RequiredElectricityConsumers
|
||||
&& 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;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool MatchesId(ReactorBinding reactor, int reactorId)
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
64
src/ReactorMaintenance.Win2D/EditorImageRegistry.cs
Normal file
64
src/ReactorMaintenance.Win2D/EditorImageRegistry.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
|
||||
namespace ReactorMaintenance.Win2D;
|
||||
|
||||
public sealed class EditorImageRegistry
|
||||
{
|
||||
public async Task LoadAsync(CanvasControl sender)
|
||||
{
|
||||
m_Images.Clear();
|
||||
await LoadFolderAsync(sender, "Images", "Props");
|
||||
await LoadFolderAsync(sender, "Images", "Pipes");
|
||||
await LoadFolderAsync(sender, "Images", "Badges");
|
||||
await LoadFolderAsync(sender, "Images", "Elements");
|
||||
AddAlias("tool-floor", "floor");
|
||||
AddAlias("tool-wall", "wall");
|
||||
AddAlias("tool-heat", "heat");
|
||||
AddAlias("tool-robot", "robot");
|
||||
AddAlias("robot", "robot");
|
||||
AddAlias("prop-reactor", "reactor");
|
||||
AddAlias("prop-consumer", "generator");
|
||||
AddAlias("prop-flow", "generator");
|
||||
AddAlias("carrier-fuel-source", "generator");
|
||||
AddAlias("carrier-coolant-source", "cooling-pump");
|
||||
AddAlias("carrier-electricity-source", "generator");
|
||||
AddAlias("prop-junction", "pressure-regulator");
|
||||
AddAlias("prop-door", "wall");
|
||||
AddAlias("prop-eye-terminal", "diagnostic-terminal");
|
||||
AddAlias("prop-remedy", "repair");
|
||||
AddAlias("leak-fuel", "leak");
|
||||
AddAlias("leak-coolant", "leak");
|
||||
AddAlias("leak-electricity", "leak");
|
||||
AddAlias("hazard-heat", "heat");
|
||||
AddAlias("hazard-fuel", "fire");
|
||||
AddAlias("hazard-coolant", "leak");
|
||||
AddAlias("hazard-electricity", "heat");
|
||||
}
|
||||
|
||||
public CanvasBitmap? Get(string key)
|
||||
{
|
||||
return m_Images.GetValueOrDefault(key);
|
||||
}
|
||||
|
||||
private async Task LoadFolderAsync(CanvasControl sender, params string[] pathParts)
|
||||
{
|
||||
var folder = Path.Combine([AppContext.BaseDirectory, .. pathParts]);
|
||||
if (!Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
foreach (var path in Directory.EnumerateFiles(folder, "*.png"))
|
||||
{
|
||||
var key = Path.GetFileNameWithoutExtension(path);
|
||||
m_Images[key] = await CanvasBitmap.LoadAsync(sender, path);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAlias(string alias, string key)
|
||||
{
|
||||
if (!m_Images.ContainsKey(alias) && m_Images.TryGetValue(key, out var image))
|
||||
m_Images[alias] = image;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, CanvasBitmap> m_Images = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
|
||||
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
|
||||
<AppBarSeparator />
|
||||
<AppBarButton Icon="Play" Label="End Turn" Click="EndTurn_Click" />
|
||||
<AppBarButton x:Name="PlayPauseButton" Icon="Play" Label="Play" Click="PlayPause_Click" />
|
||||
<AppBarButton Icon="Forward" Label="End Turn" Click="EndTurn_Click" />
|
||||
<AppBarButton Label="Interact" Click="Interact_Click" />
|
||||
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
|
||||
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
|
||||
@@ -30,6 +31,10 @@
|
||||
|
||||
<ScrollViewer Grid.Column="0" Background="#1C2126">
|
||||
<StackPanel Padding="12" Spacing="10">
|
||||
<TextBlock Text="Layer" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ComboBox x:Name="LayerPicker"
|
||||
SelectionChanged="LayerPicker_SelectionChanged"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<TextBlock Text="Tools" FontSize="18" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ToolPicker">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -51,10 +56,9 @@
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
|
||||
Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Text="Door and wall electricity leaks use two clicks: choose the source cell, then the adjacent floor face."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Text="Shift+left drag pans. Cursor drag moves the robot or a prop."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -90,42 +94,193 @@
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Global Systems" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="GlobalText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Inventory" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="InventoryGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid MinWidth="55" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="18" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Selected Cell" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="CellText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Required" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="RequiredGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="84" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8"
|
||||
FontSize="18" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Editor Workflow" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="WorkflowText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Text="Reactor Binding" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="ReactorBindingText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<Grid ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock x:Name="HoveredCellText" Text="Hovered Cell:" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="SelectedCellText" Text="Selected Cell:" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="#F4F1E8" />
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Content="Fuel" Click="BindFuel_Click" HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="1" Content="Coolant" Click="BindCoolant_Click"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="2" Content="Electric" Click="BindElectricity_Click"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<TextBlock Grid.Column="0" Grid.Row="0" Text="Terrain" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="0" Grid.Row="1" x:Name="TerrainText" Foreground="#F4F1E8" FontSize="16"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Text="Prop" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="1" x:Name="PropText" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="Rule Events" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<TextBlock x:Name="RuleEventText" Foreground="#CCD4DA" TextWrapping="Wrap" />
|
||||
<Grid ColumnSpacing="8" RowSpacing="8">
|
||||
<TextBlock Text="Consumers:" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ConsumersGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="96" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Services:" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ServicesGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="96" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Leaks:" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="LeaksGrid">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0,4,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#F4F1E8" FontSize="16"
|
||||
Margin="0,0,10,0" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Value}" Foreground="#F4F1E8"
|
||||
FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Surface" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="SurfaceGrid">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Width="64" Margin="0,0,4,0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Label}" Foreground="#9EA7AE" FontSize="11"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock Grid.Row="1" Text="{Binding Value}" Foreground="#F4F1E8" FontSize="16" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Network" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<Grid ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Content="Warn Next Turn" Click="AddWarningRule_Click" HorizontalAlignment="Stretch" />
|
||||
<Button Grid.Column="1" Content="Leak Next Turn" Click="AddLeakRule_Click"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<TextBlock Text="Carrier" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="1" Text="State" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="2" Text="Amt" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="3" Text="Int" Foreground="#9EA7AE" FontSize="11" />
|
||||
<TextBlock Grid.Column="4" Text="HP" Foreground="#9EA7AE" FontSize="11" />
|
||||
</Grid>
|
||||
<Button Content="Remove Last Rule" Click="RemoveLastRule_Click" HorizontalAlignment="Stretch" />
|
||||
<ItemsControl x:Name="NetworkGrid">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0,0,8" ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="60" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
<ColumnDefinition Width="34" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Carrier}" Foreground="#F4F1E8" FontSize="12"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding State}" Foreground="#9EA7AE"
|
||||
FontSize="12" TextWrapping="Wrap" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding Amount}" Foreground="#F4F1E8"
|
||||
FontSize="12" />
|
||||
<TextBlock Grid.Column="3" Text="{Binding Intensity}" Foreground="#F4F1E8"
|
||||
FontSize="12" />
|
||||
<TextBlock Grid.Column="4" Text="{Binding Integrity}" Foreground="#F4F1E8"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Forecasts" FontSize="16" FontWeight="SemiBold" Foreground="#F4F1E8" />
|
||||
<ItemsControl x:Name="ForecastList">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,18 +3,71 @@
|
||||
public sealed class LevelEditorTests
|
||||
{
|
||||
[Fact]
|
||||
public void DoorToolRequiresExplicitAdjacentEdgeSelection()
|
||||
public void DoorToolPlacesSingleFloorDoorProp()
|
||||
{
|
||||
var level = LevelState.Create("Door editor", 6, 6);
|
||||
|
||||
var withDoorProp = 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));
|
||||
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
|
||||
Assert.Equal(EPropType.Door, withDoorProp.GetProp(new(2, 2)).Type);
|
||||
Assert.Empty(withDoorProp.Doors);
|
||||
Assert.Single(withDoorEdge.Doors);
|
||||
Assert.Equal(withDoorEdge.Doors, rejected.Doors);
|
||||
Assert.Equal(EPropType.Door, next.GetProp(new(2, 2)).Type);
|
||||
Assert.Equal(EDoorState.Closed, next.GetProp(new(2, 2)).DoorState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoorToolTogglesExistingDoorState()
|
||||
{
|
||||
var level = LevelState.Create("Door editor", 6, 6);
|
||||
level = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
|
||||
var opened = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
var closed = LevelEditor.Apply(opened, new(2, 2), new() { Tool = EEditorTool.Door });
|
||||
|
||||
Assert.Equal(EDoorState.Open, opened.GetProp(new(2, 2)).DoorState);
|
||||
Assert.Equal(EDoorState.Closed, closed.GetProp(new(2, 2)).DoorState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WallToolPreservesUndergroundNetworks()
|
||||
{
|
||||
var level = LevelState.Create("Wall editor", 6, 6);
|
||||
var position = new GridPosition(2, 2);
|
||||
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Fuel });
|
||||
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Coolant });
|
||||
level = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Electricity });
|
||||
|
||||
var next = LevelEditor.Apply(level, position, new() { Tool = EEditorTool.Wall });
|
||||
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Coolant).State);
|
||||
Assert.Equal(EUndergroundState.Intact, next.GetUnderground(position, ECarrierType.Electricity).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UndergroundToolCanPaintAdjacentCellsRepeatedly()
|
||||
{
|
||||
var level = LevelState.Create("Network editor", 6, 6);
|
||||
var command = new EditorToolCommand { Tool = EEditorTool.Underground, Carrier = ECarrierType.Fuel };
|
||||
|
||||
level = LevelEditor.Apply(level, new(1, 1), command);
|
||||
level = LevelEditor.Apply(level, new(2, 1), command);
|
||||
level = LevelEditor.Apply(level, new(3, 1), command);
|
||||
|
||||
Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(1, 1), ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(2, 1), ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Intact, level.GetUnderground(new(3, 1), ECarrierType.Fuel).State);
|
||||
}
|
||||
|
||||
[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]
|
||||
@@ -33,44 +86,116 @@ public sealed class LevelEditorTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReactorBindingUpdatesOnlyMatchingCarrierConsumer()
|
||||
public void ElectricityLeakAccessCyclesAcrossAdjacentFloorFaces()
|
||||
{
|
||||
var level = LevelState.Create("Binding editor", 8, 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 level = LevelState.Create("Electricity leak editor", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = level.SetTerrain(new(2, 1), ECellTerrain.Wall);
|
||||
level = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.Underground, Carrier = ECarrierType.Electricity });
|
||||
|
||||
var bound = LevelEditor.BindReactorConsumer(level, 1, ECarrierType.Fuel, new(2, 1));
|
||||
var rejected = LevelEditor.BindReactorConsumer(bound, 1, ECarrierType.Fuel, new(3, 1));
|
||||
var first = LevelEditor.CycleElectricityLeakAccess(level, new(2, 2));
|
||||
var second = LevelEditor.CycleElectricityLeakAccess(first, new(2, 2));
|
||||
|
||||
Assert.Equal(new(2, 1), bound.Reactors[0].FuelConsumerPosition);
|
||||
Assert.Equal(bound.Reactors[0].FuelConsumerPosition, rejected.Reactors[0].FuelConsumerPosition);
|
||||
Assert.Single(first.Leaks);
|
||||
Assert.Equal(new(3, 2), first.Leaks[0].AccessPosition);
|
||||
Assert.Single(second.Leaks);
|
||||
Assert.Equal(new(2, 3), second.Leaks[0].AccessPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuleEventEditorAssignsStableIdsAndCanRemoveEvents()
|
||||
public void ReactorControlToolCreatesUnboundReactorState()
|
||||
{
|
||||
var level = LevelState.Create("Rule editor", 6, 6);
|
||||
var level = LevelState.Create("Reactor 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");
|
||||
var next = LevelEditor.Apply(level, new(2, 2), new() { Tool = EEditorTool.ReactorControl });
|
||||
|
||||
Assert.Single(withRule.RuleEvents);
|
||||
Assert.Equal("rule-1", withRule.RuleEvents[0].Id);
|
||||
Assert.Empty(removed.RuleEvents);
|
||||
Assert.Single(next.Reactors);
|
||||
Assert.Equal(new(2, 2), next.Reactors[0].ControlPosition);
|
||||
Assert.Equal(1, next.Reactors[0].ReactorId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesRobotToFloorDestination()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6) with { Robot = new() { Position = new(1, 1) } };
|
||||
|
||||
var next = LevelEditor.MoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.Equal(new(3, 3), next.Robot.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesPropAndUpdatesReactorControlPosition()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = LevelEditor.Apply(level, new(1, 1), new() { Tool = EEditorTool.ReactorControl });
|
||||
|
||||
var next = LevelEditor.MoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.Equal(EPropType.None, next.GetProp(new(1, 1)).Type);
|
||||
Assert.Equal(EPropType.ReactorControl, next.GetProp(new(3, 3)).Type);
|
||||
Assert.Equal(new(3, 3), next.Reactors[0].ControlPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesSourcesAsProps()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = LevelEditor.Apply(level, new(1, 1), new() { Tool = EEditorTool.Flow, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var result = LevelEditor.TryMoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.True(result.Success, result.Reason);
|
||||
Assert.Equal(EPropType.None, result.Level.GetProp(new(1, 1)).Type);
|
||||
Assert.Equal(EPropType.Flow, result.Level.GetProp(new(3, 3)).Type);
|
||||
Assert.Equal(ECarrierType.Fuel, result.Level.GetProp(new(3, 3)).Carrier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesFuelLeakToFloorDestination()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = LevelEditor.SetLeak(level, new(1, 1), new(1, 1), ECarrierType.Fuel);
|
||||
|
||||
var result = LevelEditor.TryMoveOccupant(level, new(1, 1), new(3, 3));
|
||||
|
||||
Assert.True(result.Success, result.Reason);
|
||||
Assert.Single(result.Level.Leaks);
|
||||
Assert.Equal(new(3, 3), result.Level.Leaks[0].UndergroundPosition);
|
||||
Assert.Equal(new(3, 3), result.Level.Leaks[0].AccessPosition);
|
||||
Assert.Equal(EUndergroundState.Leaking, result.Level.GetUnderground(new(3, 3), ECarrierType.Fuel).State);
|
||||
Assert.Equal(EUndergroundState.Absent, result.Level.GetUnderground(new(1, 1), ECarrierType.Fuel).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantMovesElectricityLeakAccessFace()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = LevelEditor.SetLeak(level, new(2, 2), new(2, 3), ECarrierType.Electricity);
|
||||
|
||||
var result = LevelEditor.TryMoveOccupant(level, new(2, 3), new(3, 2));
|
||||
|
||||
Assert.True(result.Success, result.Reason);
|
||||
Assert.Single(result.Level.Leaks);
|
||||
Assert.Equal(new(2, 2), result.Level.Leaks[0].UndergroundPosition);
|
||||
Assert.Equal(new(3, 2), result.Level.Leaks[0].AccessPosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveOccupantReportsInvalidStartAndDestinationReasons()
|
||||
{
|
||||
var level = LevelState.Create("Move editor", 6, 6);
|
||||
level = level.SetTerrain(new(3, 3), ECellTerrain.Wall);
|
||||
level = level.SetProp(new(1, 1), new() { Type = EPropType.Consumer });
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Consumer });
|
||||
|
||||
var invalidStart = LevelEditor.TryMoveOccupant(level, new(4, 4), new(5, 5));
|
||||
var invalidDestination = LevelEditor.TryMoveOccupant(level, new(1, 1), new(2, 2));
|
||||
|
||||
Assert.False(invalidStart.Success);
|
||||
Assert.Contains("No movable", invalidStart.Reason);
|
||||
Assert.False(invalidDestination.Success);
|
||||
Assert.Contains("occupied", invalidDestination.Reason);
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,31 @@
|
||||
public sealed class SimulationEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor()
|
||||
public void NetworkPropagationSuppliesConsumerServicesAndReadiesReactor()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
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, next.GetProp(new(3, 3)).ServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, consumer.FuelServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, consumer.CoolantServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, consumer.ElectricityServiceState);
|
||||
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
||||
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]
|
||||
public void ReactorActivatesOnlyAtReadyControl()
|
||||
{
|
||||
@@ -30,19 +42,110 @@ public sealed class SimulationEngineTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LeakingUndergroundCellInjectsMatchingSurfaceHazard()
|
||||
public void DisabledConsumerReportsDisabledOnlyForNetworksBeneathIt()
|
||||
{
|
||||
var level = LevelState.Create("Leak", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with {
|
||||
Leaks = [new() { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
var level = LevelState.Create("Disabled", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[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]
|
||||
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
||||
{
|
||||
@@ -60,19 +163,6 @@ public sealed class SimulationEngineTests
|
||||
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]
|
||||
public void HeatShieldPreventsRobotHeatLoss()
|
||||
{
|
||||
@@ -122,217 +212,19 @@ public sealed class SimulationEngineTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidatorRejectsJunctionWithoutTwoOrThreeOutflows()
|
||||
{
|
||||
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()
|
||||
public void ValidatorRejectsInvalidDoorGeometryAndWallHazards()
|
||||
{
|
||||
var level = LevelState.Create("Invalid", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
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(2, 2))] = new() { Heat = 1 };
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Door });
|
||||
level = level.SetTerrain(new(4, 4), ECellTerrain.Wall);
|
||||
level = level with { Surface = level.Surface.ToArray() };
|
||||
level.Surface[level.Index(new(4, 4))] = new() { Heat = 1 };
|
||||
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
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("Reactor binding", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -343,36 +235,27 @@ public sealed class SimulationEngineTests
|
||||
var json = LevelSerializer.Serialize(level);
|
||||
var loaded = LevelSerializer.Deserialize(json);
|
||||
|
||||
Assert.Contains("\"Version\": 2", json);
|
||||
Assert.Contains("\"Version\": 3", json);
|
||||
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]
|
||||
public void LevelSerializationRoundTripsRuleEventsDoorsAndElectricityLeakFaces()
|
||||
public void LevelSerializationRoundTripsPropDoorsAndElectricityLeakFaces()
|
||||
{
|
||||
var level = BuildReadyLevel();
|
||||
level = level.SetTerrain(new(6, 4), ECellTerrain.Wall);
|
||||
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) }],
|
||||
RuleEvents = [
|
||||
new() {
|
||||
Id = "authored",
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
Predicates = [new() { Kind = ERulePredicateKind.TurnAtLeast, Turn = 2 }],
|
||||
Effects = [new() { Kind = ERuleEffectKind.EmitWarning, Message = "serialized" }]
|
||||
}
|
||||
]
|
||||
Leaks = [new() { Carrier = ECarrierType.Electricity, UndergroundPosition = new(6, 4), AccessPosition = new(6, 3) }]
|
||||
};
|
||||
level = DoorLevel(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.RuleEvents);
|
||||
Assert.Equal(new(6, 3), loaded.Leaks[0].AccessPosition);
|
||||
Assert.Equal("authored", loaded.RuleEvents[0].Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -380,43 +263,45 @@ public sealed class SimulationEngineTests
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Version": 1,
|
||||
"Version": 2,
|
||||
"Level": {}
|
||||
}
|
||||
""";
|
||||
|
||||
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()
|
||||
{
|
||||
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.Electricity, new(2, 4), new(3, 4));
|
||||
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.Coolant });
|
||||
level = AddLine(level, ECarrierType.Electricity, new(2, 3), new(3, 3));
|
||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
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(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetUnderground(new(2, 4), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(new(2, 3), ECarrierType.Electricity, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer });
|
||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||
return level with {
|
||||
Robot = new() { Position = new(5, 3) },
|
||||
Reactors = [
|
||||
new() {
|
||||
ReactorId = 1,
|
||||
ControlPosition = new(5, 3),
|
||||
FuelConsumerPosition = new(3, 2),
|
||||
CoolantConsumerPosition = new(3, 3),
|
||||
ElectricityConsumerPosition = new(3, 4)
|
||||
}
|
||||
]
|
||||
Reactors = [new() { ReactorId = 1, ControlPosition = new(5, 3) }]
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user