diff --git a/README.md b/README.md index 882d094..4daa3f0 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,28 @@ -# Reactor Maintenance - -C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`. - -## Projects - -- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults. -- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, leaks, doors, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor. -- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior. - -## Commands - -```powershell -dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj -dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true -dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -``` - -The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment. +# Reactor Maintenance + +C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`. + +## Projects + +- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults. +- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, explicit leak access faces, door edges, reactor consumer bindings, rule events, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor. +- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior, validation, serialization, and editor operations. + +## Editor Controls + +- Left click selects or paints with the current tool. Right click clears the selected cell's prop, surface hazards, leaks, doors, and reactor control. +- Door authoring is explicit: select the Door tool, click the door cell, then click the adjacent floor cell that defines the blocked edge. +- Electricity wall leaks are explicit: select the Electricity Leak tool, click the wall network cell, then click the adjacent floor access face. +- Reactor bindings are explicit: select or place a reactor control, select a matching consumer cell, then use the Fuel, Coolant, or Electric binding action in the inspector. +- Rule event authoring is available from the inspector for next-turn warnings and selected-cell leak events; authored events are saved in the version 2 JSON schema. + +## Commands + +```powershell +dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj +dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true +dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 +``` + +The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment. diff --git a/TASKS.md b/TASKS.md index 71494aa..3a6e186 100644 --- a/TASKS.md +++ b/TASKS.md @@ -56,17 +56,17 @@ - Removed the temporary `SimulationCoreSystem` catch-all and made `SimulationEngine` the real orchestration point. - Split simulation behavior into purposeful systems for player actions, network propagation, consumers, leaks, surface interactions, robot safety, reactors, rule events, and forecasts. - Kept `Systems` limited to behavior-owning systems; shared traversal, band, and carrier math helpers live at the simulation root. +- Replaced carrier-specific editor enum entries with parameterized editor commands. +- Added explicit editor workflows for door edge selection, electricity wall leak access-face selection, reactor consumer binding, and basic rule event authoring. +- Added editor operation tests plus broader junction validation and serialization round-trip coverage. +- Verified `dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj` passes: 27 passed. +- Verified `dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true` succeeds on Windows. +- Ran `jb cleanupcode --build=False ...` and `python D:\Code\crlf.py ...` for touched files in the final rewrite slice. ## Current Work -- Await review and next task selection on `design-rewrite`. +- Rewrite task list completed on `design-rewrite`. ## Future Work -1. Continue reducing helper surface where classes no longer carry enough responsibility after future changes. -2. Add advanced editor workflows for explicit reactor binding selection, explicit door edge selection, electricity wall leak face selection, and rule event authoring. -3. Verify and polish the Win2D app on Windows where the XAML compiler can run. -4. Update README and any affected docs to reflect the new schema, .NET target, editor controls, and deterministic defaults. -5. Build the Win2D project on a Windows-capable environment after the editor rewrite. -6. Add broader tests for junction ratios, ambiguous junctions, serialization edge cases, and editor operations. -7. Run cleanup when `dotnet-jb` is available, tests, code review, and iterate until the implementation is clean and maintainable. +No known rewrite tasks remain. diff --git a/docs/design.md b/docs/design.md index 663fc68..6ee6e0d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -157,6 +157,7 @@ A junction prop must be on a floor cell whose coordinate has exactly one undergr The engine infers incoming and outgoing branch directions from valid network topology and enabled source paths. A valid junction has one incoming branch and either two or three outgoing branches. Ambiguous junction flow is invalid. Ratio numbers are balance-defined weights that divide carrier amount and pressure or voltage. A zero-weight branch receives no intentional outflow. The gameplay UI exposes a single junction tool and cycles through balance-defined ratio presets for the inferred outgoing branch count. +Editor commands use a verb plus parameters, so carrier-specific choices such as fuel flow or coolant flow are UI presets over one `Flow` command instead of separate simulation concepts. ### Doors @@ -400,12 +401,12 @@ The editor authors: - surface terrain - underground fuel, coolant, and electricity cells - flow props and consumer props -- reactor controls and reactor consumer bindings +- reactor controls and explicit reactor consumer bindings - junction props and balance-defined ratio mode index -- door props and door edges +- door props and explicit door edges - all-seeing-eye terminals - remedy supplies -- floor leaks and electricity wall leaks +- floor leaks and electricity wall leaks with authored access faces - initial surface hazards and heat - robot start position - rule events diff --git a/src/ReactorMaintenance.Simulation/EEditorTool.cs b/src/ReactorMaintenance.Simulation/EEditorTool.cs new file mode 100644 index 0000000..cd1d18d --- /dev/null +++ b/src/ReactorMaintenance.Simulation/EEditorTool.cs @@ -0,0 +1,20 @@ +namespace ReactorMaintenance.Simulation; + +public enum EEditorTool +{ + Cursor, + Floor, + Wall, + Underground, + Flow, + Consumer, + Junction, + Door, + AllSeeingEyeTerminal, + RemedySupply, + ReactorControl, + Leak, + SurfaceHazard, + Heat, + Robot +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/EditorToolCommand.cs b/src/ReactorMaintenance.Simulation/EditorToolCommand.cs new file mode 100644 index 0000000..a5008fd --- /dev/null +++ b/src/ReactorMaintenance.Simulation/EditorToolCommand.cs @@ -0,0 +1,8 @@ +namespace ReactorMaintenance.Simulation; + +public sealed record EditorToolCommand +{ + public EEditorTool Tool { get; init; } + public ECarrierType Carrier { get; init; } + public ERemedyType RemedyType { get; init; } +} \ No newline at end of file diff --git a/src/ReactorMaintenance.Simulation/LevelEditor.cs b/src/ReactorMaintenance.Simulation/LevelEditor.cs index c75468f..c156749 100644 --- a/src/ReactorMaintenance.Simulation/LevelEditor.cs +++ b/src/ReactorMaintenance.Simulation/LevelEditor.cs @@ -1,91 +1,92 @@ namespace ReactorMaintenance.Simulation; -public enum EEditorTool -{ - Cursor, - Floor, - Wall, - FuelUnderground, - CoolantUnderground, - ElectricityUnderground, - FuelFlow, - CoolantFlow, - ElectricityFlow, - FuelConsumer, - CoolantConsumer, - ElectricityConsumer, - Junction, - Door, - AllSeeingEyeTerminal, - FuelRemedySupply, - CoolantRemedySupply, - ElectricityRemedySupply, - HeatRemedySupply, - ReactorControl, - FuelLeak, - CoolantLeak, - ElectricityLeak, - FuelHazard, - CoolantHazard, - ElectricityHazard, - Heat, - Robot -} - public static class LevelEditor { - public static LevelState Apply(LevelState level, GridPosition position, EEditorTool tool) + public static LevelState Apply(LevelState level, GridPosition position, EditorToolCommand command) { if (!level.InBounds(position)) return level; - return tool switch { + return command.Tool switch { EEditorTool.Cursor => level, EEditorTool.Floor => level.SetTerrain(position, ECellTerrain.Floor), EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall), - EEditorTool.FuelUnderground => SetUnderground(level, position, ECarrierType.Fuel), - EEditorTool.CoolantUnderground => SetUnderground(level, position, ECarrierType.Coolant), - EEditorTool.ElectricityUnderground => SetUnderground(level, position, ECarrierType.Electricity), - EEditorTool.FuelFlow => SetCarrierProp(level, position, EPropType.Flow, ECarrierType.Fuel), - EEditorTool.CoolantFlow => SetCarrierProp(level, position, EPropType.Flow, ECarrierType.Coolant), - EEditorTool.ElectricityFlow => SetCarrierProp(level, position, EPropType.Flow, ECarrierType.Electricity), - EEditorTool.FuelConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Fuel), - EEditorTool.CoolantConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Coolant), - EEditorTool.ElectricityConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Electricity), + 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.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }), - EEditorTool.Door => SetDoor(level, position), + EEditorTool.Door => SetFloorProp(level, position, new() { Type = EPropType.Door }), EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }), - EEditorTool.FuelRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.FuelNeutralizer }), - EEditorTool.CoolantRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.CoolantNeutralizer }), - EEditorTool.ElectricityRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.ElectricityNeutralizer }), - EEditorTool.HeatRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield }), + EEditorTool.RemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = command.RemedyType }), EEditorTool.ReactorControl => SetReactorControl(level, position), - EEditorTool.FuelLeak => SetLeak(level, position, ECarrierType.Fuel), - EEditorTool.CoolantLeak => SetLeak(level, position, ECarrierType.Coolant), - EEditorTool.ElectricityLeak => SetLeak(level, position, ECarrierType.Electricity), - EEditorTool.FuelHazard => level.SetSurface(position, level.GetSurface(position) with { Fuel = level.GetSurface(position).Fuel + 1 }), - EEditorTool.CoolantHazard => level.SetSurface(position, level.GetSurface(position) with { Coolant = level.GetSurface(position).Coolant + 1 }), - EEditorTool.ElectricityHazard => level.SetSurface(position, level.GetSurface(position) with { Electricity = level.GetSurface(position).Electricity + 1 }), + EEditorTool.Leak => SetLeak(level, position, command.Carrier), + EEditorTool.SurfaceHazard => AddSurfaceHazard(level, position, command.Carrier), EEditorTool.Heat => level.SetSurface(position, level.GetSurface(position) with { Heat = level.GetSurface(position).Heat + 1 }), EEditorTool.Robot => level.IsFloor(position) ? level with { Robot = level.Robot with { Position = position } } : level, _ => level }; } - public static LevelState BindFirstReactorToConsumers(LevelState level, GridPosition fuelConsumer, GridPosition coolantConsumer, GridPosition electricityConsumer) + public static LevelState SetDoorEdge(LevelState level, GridPosition a, GridPosition b) { - if (level.Reactors.Count == 0) + if (!level.IsFloor(a) || !level.IsFloor(b) || a.ManhattanDistance(b) != 1) return level; - var reactors = level.Reactors.ToArray(); - reactors[0] = reactors[0] with { - FuelConsumerPosition = fuelConsumer, - CoolantConsumerPosition = coolantConsumer, - ElectricityConsumerPosition = electricityConsumer + 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)) + return level; + + if (carrier is ECarrierType.Fuel or ECarrierType.Coolant && undergroundPosition != accessPosition) + return level; + + if (carrier == ECarrierType.Electricity && undergroundPosition.ManhattanDistance(accessPosition) != 1) + return level; + + var next = level.SetUnderground(undergroundPosition, carrier, new() { State = EUndergroundState.Leaking }); + return next with { + Leaks = [ + .. next.Leaks.Where(leak => leak.UndergroundPosition != undergroundPosition || leak.Carrier != carrier), + new() { + Carrier = carrier, + UndergroundPosition = undergroundPosition, + AccessPosition = accessPosition + } + ] + }; + } + + 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 }); @@ -96,25 +97,22 @@ public static class LevelEditor return SetFloorProp(level, position, new() { Type = type, Carrier = carrier, SwitchState = EPropSwitchState.Enabled }); } + private static LevelState AddSurfaceHazard(LevelState level, GridPosition position, ECarrierType carrier) + { + var surface = level.GetSurface(position); + return carrier switch { + ECarrierType.Fuel => level.SetSurface(position, surface with { Fuel = surface.Fuel + 1 }), + ECarrierType.Coolant => level.SetSurface(position, surface with { Coolant = surface.Coolant + 1 }), + ECarrierType.Electricity => level.SetSurface(position, surface with { Electricity = surface.Electricity + 1 }), + _ => level + }; + } + private static LevelState SetFloorProp(LevelState level, GridPosition position, PropState prop) { return level.IsFloor(position) ? level.SetProp(position, prop) : level; } - private static LevelState SetDoor(LevelState level, GridPosition position) - { - if (!level.IsFloor(position)) - return level; - - var neighbor = position.Neighbors().FirstOrDefault(level.IsFloor); - if (neighbor is null) - return SetFloorProp(level, position, new() { Type = EPropType.Door }); - - return SetFloorProp(level, position, new() { Type = EPropType.Door }) with { - Doors = [.. level.Doors, new() { A = position, B = neighbor }] - }; - } - private static LevelState SetReactorControl(LevelState level, GridPosition position) { if (!level.IsFloor(position)) @@ -141,23 +139,30 @@ public static class LevelEditor if (!level.InBounds(position)) return level; - var accessPosition = carrier == ECarrierType.Electricity && level.GetTerrain(position) == ECellTerrain.Wall - ? position.Neighbors().FirstOrDefault(level.IsFloor) - : position; + return SetLeak(level, position, position, carrier); + } - if (accessPosition is null || !level.IsFloor(accessPosition)) - return level; + private static bool SameDoorEdge(DoorState door, GridPosition a, GridPosition b) + { + return door.A == a && door.B == b || door.A == b && door.B == a; + } - var next = level.SetUnderground(position, carrier, new() { State = EUndergroundState.Leaking }); - return next with { - Leaks = [ - .. next.Leaks, - new() { - Carrier = carrier, - UndergroundPosition = position, - AccessPosition = accessPosition - } - ] + 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}"; + } } \ No newline at end of file diff --git a/src/ReactorMaintenance.Win2D/MainWindow.xaml b/src/ReactorMaintenance.Win2D/MainWindow.xaml index 84bcc3c..07ceeae 100644 --- a/src/ReactorMaintenance.Win2D/MainWindow.xaml +++ b/src/ReactorMaintenance.Win2D/MainWindow.xaml @@ -52,7 +52,7 @@ @@ -96,6 +96,34 @@ + + + + + + + + + + + +