9ae7b88b89
Fuse the standalone ship-class designer (Phases 17/18) into a sidebar calculator: live mass/speed/attack/defence/bombing results, a planet build-rate readout, single-target goal-seek, a modernization-cost mode, and auto reach circles on the map for the selected planet. pkg/calc becomes the single source for the new math (no mirroring): extract BombingPower from the engine model and the per-turn ship-production loop from controller.ProduceShip into pkg/calc (engine now delegates), and add inverse goal-seek solvers in pkg/calc/solve.go. Thin-bridge the combat, planet-build, and solver functions through ui/core/calc + ui/wasm and rebuild core.wasm. Remove the standalone designer view/route; the ship-classes table and the view/bottom menus open the calculator via a shared request store. Docs: rewrite ui/PLAN.md Phase 30, adjust Phase 34 (realistic forecast + CAP/COL ownership), add ui/docs/calculator-ux.md, extend calc-bridge.md, fix navigation.md; remove ui/CALCULATOR.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
113 lines
3.3 KiB
Go
113 lines
3.3 KiB
Go
package calc_test
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
|
|
"galaxy/calc"
|
|
)
|
|
|
|
func TestShipBuildCost(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
shipMass float64
|
|
material float64
|
|
resources float64
|
|
want float64
|
|
}{
|
|
{
|
|
name: "material exceeds mass: no farming needed",
|
|
shipMass: 5,
|
|
material: 10,
|
|
resources: 0.5,
|
|
want: 50, // ShipProductionCost(5) = 50; matFarm = 0.
|
|
},
|
|
{
|
|
name: "material equal to mass: no farming needed",
|
|
shipMass: 5,
|
|
material: 5,
|
|
resources: 0.5,
|
|
want: 50,
|
|
},
|
|
{
|
|
name: "material short of mass: farming term added",
|
|
shipMass: 10,
|
|
material: 3,
|
|
resources: 0.5,
|
|
want: 114, // 100 + (7 / 0.5).
|
|
},
|
|
{
|
|
name: "no material at all: full mass farmed",
|
|
shipMass: 4,
|
|
material: 0,
|
|
resources: 0.5,
|
|
want: 48, // 40 + (4 / 0.5).
|
|
},
|
|
{
|
|
name: "zero resources collapses farming term to zero",
|
|
shipMass: 10,
|
|
material: 3,
|
|
resources: 0,
|
|
want: 100, // 100 + 0; resources == 0 is a pathological guard.
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := calc.ShipBuildCost(tc.shipMass, tc.material, tc.resources)
|
|
if math.Abs(got-tc.want) > 1e-9 {
|
|
t.Errorf("ShipBuildCost(%v, %v, %v) = %v, want %v",
|
|
tc.shipMass, tc.material, tc.resources, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProduceShipsInTurn(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
productionAvailable, material, resources, shipMass float64
|
|
wantShips uint
|
|
wantMaterialLeft, wantProductionUsed, wantProgress float64
|
|
}{
|
|
{
|
|
name: "ample material: ten ships, no farming",
|
|
productionAvailable: 100, material: 100, resources: 10, shipMass: 1,
|
|
wantShips: 10, wantMaterialLeft: 90, wantProductionUsed: 0, wantProgress: 0,
|
|
},
|
|
{
|
|
name: "no material: partial progress on a farmed ship",
|
|
productionAvailable: 114, material: 0, resources: 0.5, shipMass: 10,
|
|
// ShipBuildCost(10,0,0.5) = 100 + 10/0.5 = 120; 114/120 = 0.95.
|
|
wantShips: 0, wantMaterialLeft: 0, wantProductionUsed: 114, wantProgress: 0.95,
|
|
},
|
|
{
|
|
name: "no production available leaves the stockpile",
|
|
productionAvailable: 0, material: 50, resources: 10, shipMass: 5,
|
|
wantShips: 0, wantMaterialLeft: 50, wantProductionUsed: 0, wantProgress: 0,
|
|
},
|
|
{
|
|
name: "zero ship mass is guarded against an endless loop",
|
|
productionAvailable: 100, material: 50, resources: 10, shipMass: 0,
|
|
wantShips: 0, wantMaterialLeft: 50, wantProductionUsed: 0, wantProgress: 0,
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ships, materialLeft, productionUsed, progress := calc.ProduceShipsInTurn(
|
|
tc.productionAvailable, tc.material, tc.resources, tc.shipMass)
|
|
if ships != tc.wantShips {
|
|
t.Errorf("ships = %d, want %d", ships, tc.wantShips)
|
|
}
|
|
if math.Abs(materialLeft-tc.wantMaterialLeft) > 1e-9 {
|
|
t.Errorf("materialLeft = %v, want %v", materialLeft, tc.wantMaterialLeft)
|
|
}
|
|
if math.Abs(productionUsed-tc.wantProductionUsed) > 1e-9 {
|
|
t.Errorf("productionUsed = %v, want %v", productionUsed, tc.wantProductionUsed)
|
|
}
|
|
if math.Abs(progress-tc.wantProgress) > 1e-9 {
|
|
t.Errorf("progress = %v, want %v", progress, tc.wantProgress)
|
|
}
|
|
})
|
|
}
|
|
}
|