Files
galaxy-game/pkg/calc/solve_test.go
T
Ilia Denisov 9ae7b88b89
Tests · UI / test (push) Successful in 2m14s
Tests · Go / test (push) Successful in 2m25s
feat(ui): Phase 30 ship-class calculator with goal-seek and reach circles
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>
2026-05-21 20:04:07 +02:00

67 lines
2.3 KiB
Go

package calc_test
import (
"math"
"testing"
"galaxy/calc"
)
func TestWeaponsForAttack(t *testing.T) {
got, ok := calc.WeaponsForAttack(calc.EffectiveAttack(12, 1.5), 1.5)
if !ok || math.Abs(got-12) > 1e-9 {
t.Errorf("WeaponsForAttack round-trip = %v (ok=%v), want 12", got, ok)
}
if _, ok := calc.WeaponsForAttack(10, 0); ok {
t.Error("WeaponsForAttack with zero tech should be infeasible")
}
}
func TestDriveForSpeed(t *testing.T) {
const drive, driveTech, restMass = 10.0, 1.2, 35.0
speed := calc.Speed(calc.DriveEffective(drive, driveTech), drive+restMass)
got, ok := calc.DriveForSpeed(speed, driveTech, restMass)
if !ok || math.Abs(got-drive) > 1e-9 {
t.Errorf("DriveForSpeed round-trip = %v (ok=%v), want %v", got, ok, drive)
}
// Speed can never reach the stripped-hull ceiling 20*driveTech.
if _, ok := calc.DriveForSpeed(20*driveTech, driveTech, restMass); ok {
t.Error("DriveForSpeed at the speed ceiling should be infeasible")
}
}
func TestShieldsForDefence(t *testing.T) {
const shields, shieldsTech, restMass = 5.75, 1.0, 40.0
defence := calc.EffectiveDefence(shields, shieldsTech, shields+restMass)
got, ok := calc.ShieldsForDefence(defence, shieldsTech, restMass)
if !ok || math.Abs(got-shields) > 1e-6 {
t.Errorf("ShieldsForDefence round-trip = %v (ok=%v), want %v", got, ok, shields)
}
if _, ok := calc.ShieldsForDefence(0, shieldsTech, restMass); ok {
t.Error("ShieldsForDefence at a zero target should be infeasible")
}
}
func TestCargoForEmptyMass(t *testing.T) {
const restMass, cargo = 30.0, 12.0
got, ok := calc.CargoForEmptyMass(restMass+cargo, restMass)
if !ok || math.Abs(got-cargo) > 1e-9 {
t.Errorf("CargoForEmptyMass round-trip = %v (ok=%v), want %v", got, ok, cargo)
}
if _, ok := calc.CargoForEmptyMass(restMass-1, restMass); ok {
t.Error("CargoForEmptyMass below the fixed block mass should be infeasible")
}
}
func TestLoadForFullMass(t *testing.T) {
const emptyMass, cargoTech, load = 45.0, 1.0, 20.0
full := calc.FullMass(emptyMass, calc.CarryingMass(load, cargoTech))
got, ok := calc.LoadForFullMass(full, emptyMass, cargoTech)
if !ok || math.Abs(got-load) > 1e-9 {
t.Errorf("LoadForFullMass round-trip = %v (ok=%v), want %v", got, ok, load)
}
if _, ok := calc.LoadForFullMass(emptyMass-1, emptyMass, cargoTech); ok {
t.Error("LoadForFullMass below empty mass should be infeasible")
}
}