From ef6e0d58a72b3d2fbb6316a9a9c132bfa139252e Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Tue, 28 Apr 2026 03:38:14 +0300 Subject: [PATCH] systems-power: System data + addPower/removePower with tests systems-power --- systems.go | 77 +++++++++++++++++++++ systems_test.go | 173 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 systems.go create mode 100644 systems_test.go diff --git a/systems.go b/systems.go new file mode 100644 index 0000000000000000000000000000000000000000..16ddb6d22171ee36bab8868ef92b87a52edd0d56 --- /dev/null +++ b/systems.go @@ -0,0 +1,77 @@ +package main + +// ReactorCap is the maximum total power that can be distributed across all +// ship systems at any time. +const ReactorCap = 8 + +// System holds the power state of a single ship system. +// Instances are indexed in a slice by int(RoomRole) so that callers can do +// a direct O(1) lookup when the role is known; addPower / removePower still +// do a linear scan by Role field for safety. +type System struct { + Role RoomRole + PowerLevel int + MaxLevel int +} + +// NewStartingSystems returns a 5-element slice indexed by int(RoomRole). +// All systems start at PowerLevel 0. +func NewStartingSystems() []System { + return []System{ + int(RolePilot): {Role: RolePilot, PowerLevel: 0, MaxLevel: 1}, + int(RoleWeapons): {Role: RoleWeapons, PowerLevel: 0, MaxLevel: 4}, + int(RoleShields): {Role: RoleShields, PowerLevel: 0, MaxLevel: 4}, + int(RoleMedBay): {Role: RoleMedBay, PowerLevel: 0, MaxLevel: 2}, + int(RoleEngines): {Role: RoleEngines, PowerLevel: 0, MaxLevel: 4}, + } +} + +// reactorUsed returns the sum of PowerLevel across all systems. +func reactorUsed(sys []System) int { + total := 0 + for _, s := range sys { + total += s.PowerLevel + } + return total +} + +// addPower attempts to raise the PowerLevel of the system with the given role +// by 1. It returns false (no-op) if: +// - the system is already at its MaxLevel (IV4), or +// - the reactor would exceed reactorCap (IV3). +// +// Respects IV2: this is the sole mutator of System.PowerLevel for increments. +func addPower(sys []System, role RoomRole, reactorCap int) bool { + for i := range sys { + if sys[i].Role != role { + continue + } + if sys[i].PowerLevel >= sys[i].MaxLevel { + return false + } + if reactorUsed(sys) >= reactorCap { + return false + } + sys[i].PowerLevel++ + return true + } + return false +} + +// removePower attempts to lower the PowerLevel of the system with the given +// role by 1. It returns false (no-op) if the system is already at 0 (IV4). +// +// Respects IV2: this is the sole mutator of System.PowerLevel for decrements. +func removePower(sys []System, role RoomRole) bool { + for i := range sys { + if sys[i].Role != role { + continue + } + if sys[i].PowerLevel <= 0 { + return false + } + sys[i].PowerLevel-- + return true + } + return false +} diff --git a/systems_test.go b/systems_test.go new file mode 100644 index 0000000000000000000000000000000000000000..153c720629d743e882d824540c39fc321771f2ad --- /dev/null +++ b/systems_test.go @@ -0,0 +1,173 @@ +package main + +import "testing" + +// TestNewStartingSystems_layout verifies the 5-entry slice is indexed by +// int(RoomRole) and that each entry carries the correct role and cap. +func TestNewStartingSystems_layout(t *testing.T) { + sys := NewStartingSystems() + if len(sys) != 5 { + t.Fatalf("expected 5 systems, got %d", len(sys)) + } + + type want struct { + role RoomRole + maxLevel int + } + cases := []want{ + {RolePilot, 1}, + {RoleWeapons, 4}, + {RoleShields, 4}, + {RoleMedBay, 2}, + {RoleEngines, 4}, + } + + for _, w := range cases { + idx := int(w.role) + s := sys[idx] + if s.Role != w.role { + t.Errorf("sys[%d].Role = %v, want %v", idx, s.Role, w.role) + } + if s.MaxLevel != w.maxLevel { + t.Errorf("sys[%d].MaxLevel = %d, want %d", idx, s.MaxLevel, w.maxLevel) + } + if s.PowerLevel != 0 { + t.Errorf("sys[%d].PowerLevel = %d, want 0", idx, s.PowerLevel) + } + } +} + +// TestReactorUsed_zero verifies that fresh systems report zero reactor usage. +func TestReactorUsed_zero(t *testing.T) { + sys := NewStartingSystems() + if got := reactorUsed(sys); got != 0 { + t.Fatalf("reactorUsed on fresh systems = %d, want 0", got) + } +} + +// TestReactorUsed_sums verifies that reactorUsed returns the sum of all PowerLevels. +func TestReactorUsed_sums(t *testing.T) { + sys := NewStartingSystems() + // Directly set some levels for this test only (bypassing addPower to isolate the sum logic). + sys[int(RolePilot)].PowerLevel = 1 + sys[int(RoleWeapons)].PowerLevel = 3 + sys[int(RoleEngines)].PowerLevel = 2 + want := 1 + 3 + 2 + if got := reactorUsed(sys); got != want { + t.Fatalf("reactorUsed = %d, want %d", got, want) + } +} + +// TestAddPower_happy verifies a normal add when below cap and reactor has headroom. +func TestAddPower_happy(t *testing.T) { + sys := NewStartingSystems() + ok := addPower(sys, RoleWeapons, ReactorCap) + if !ok { + t.Fatal("addPower returned false, expected true") + } + if sys[int(RoleWeapons)].PowerLevel != 1 { + t.Fatalf("PowerLevel = %d, want 1", sys[int(RoleWeapons)].PowerLevel) + } +} + +// TestAddPower_systemAtCap verifies that adding beyond MaxLevel is rejected. +func TestAddPower_systemAtCap(t *testing.T) { + sys := NewStartingSystems() + // Pilot cap is 1; add once to hit the cap. + if !addPower(sys, RolePilot, ReactorCap) { + t.Fatal("first addPower on Pilot should succeed") + } + if sys[int(RolePilot)].PowerLevel != 1 { + t.Fatalf("expected level 1 after first add, got %d", sys[int(RolePilot)].PowerLevel) + } + // Now at cap — must be rejected. + ok := addPower(sys, RolePilot, ReactorCap) + if ok { + t.Fatal("addPower at cap returned true, expected false") + } + if sys[int(RolePilot)].PowerLevel != 1 { + t.Fatalf("PowerLevel changed after rejected add: got %d", sys[int(RolePilot)].PowerLevel) + } +} + +// TestAddPower_reactorFull verifies that adding power when reactor is exhausted is rejected. +func TestAddPower_reactorFull(t *testing.T) { + sys := NewStartingSystems() + // Fill reactor to cap using Weapons (cap 4) and Engines (cap 4). + for i := 0; i < 4; i++ { + if !addPower(sys, RoleWeapons, ReactorCap) { + t.Fatalf("addPower Weapons step %d failed unexpectedly", i) + } + } + for i := 0; i < 4; i++ { + if !addPower(sys, RoleEngines, ReactorCap) { + t.Fatalf("addPower Engines step %d failed unexpectedly", i) + } + } + if reactorUsed(sys) != ReactorCap { + t.Fatalf("expected reactor full (%d), got %d", ReactorCap, reactorUsed(sys)) + } + // Shields is below cap but reactor is full — must be rejected. + ok := addPower(sys, RoleShields, ReactorCap) + if ok { + t.Fatal("addPower with full reactor returned true, expected false") + } + if sys[int(RoleShields)].PowerLevel != 0 { + t.Fatalf("PowerLevel changed on rejected add: got %d", sys[int(RoleShields)].PowerLevel) + } +} + +// TestAddPower_invariantsHold verifies IV3 and IV4 across all outcomes of addPower. +func TestAddPower_invariantsHold(t *testing.T) { + checkInvariants := func(sys []System) { + t.Helper() + used := reactorUsed(sys) + if used > ReactorCap { + t.Errorf("IV3 violated: reactorUsed %d > ReactorCap %d", used, ReactorCap) + } + for i, s := range sys { + if s.PowerLevel < 0 || s.PowerLevel > s.MaxLevel { + t.Errorf("IV4 violated: sys[%d].PowerLevel %d not in [0, %d]", i, s.PowerLevel, s.MaxLevel) + } + } + } + + roles := []RoomRole{RolePilot, RoleWeapons, RoleShields, RoleMedBay, RoleEngines} + sys := NewStartingSystems() + + // Repeatedly try to add power to every system in a round-robin until the + // reactor is full, then one more round of attempts (all should fail). + for round := 0; round < 10; round++ { + for _, r := range roles { + addPower(sys, r, ReactorCap) //nolint:errcheck — we only care about invariants + checkInvariants(sys) + } + } +} + +// TestRemovePower_happy verifies that removing from a powered system decrements level. +func TestRemovePower_happy(t *testing.T) { + sys := NewStartingSystems() + if !addPower(sys, RoleShields, ReactorCap) { + t.Fatal("addPower Shields failed") + } + ok := removePower(sys, RoleShields) + if !ok { + t.Fatal("removePower returned false, expected true") + } + if sys[int(RoleShields)].PowerLevel != 0 { + t.Fatalf("PowerLevel = %d, want 0", sys[int(RoleShields)].PowerLevel) + } +} + +// TestRemovePower_zeroLevel verifies that removing from an unpowered system is rejected. +func TestRemovePower_zeroLevel(t *testing.T) { + sys := NewStartingSystems() + ok := removePower(sys, RoleEngines) + if ok { + t.Fatal("removePower at level 0 returned true, expected false") + } + if sys[int(RoleEngines)].PowerLevel != 0 { + t.Fatalf("PowerLevel changed after rejected remove: got %d", sys[int(RoleEngines)].PowerLevel) + } +}