fix(game): charge a ship upgrade against production only once
Tests · Go / test (push) Successful in 2m7s
Tests · Go / test (pull_request) Successful in 2m10s
Tests · Integration / integration (pull_request) Successful in 1m41s

TurnPlanetProductions started its production budget from
PlanetProductionCapacity, which already subtracts the reserved upgrade
cost, and then subtracted each applied upgrade's cost again in the apply
loop — charging every applied upgrade twice. That both starved the
planet's build/research budget and could skip upgrades that were in fact
affordable.

The budget now starts from the planet's full production potential and the
apply loop deducts each upgrade once; PlanetProductionCapacity stays the
report's net-of-upgrades "free L".

Test: TestUpgradeDoesNotDoubleChargeProduction; the TestProduceShips MAT
expectation is updated to the once-charged value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-31 08:24:46 +02:00
parent b4abf90ec5
commit 53b3cafbc4
2 changed files with 41 additions and 7 deletions
+7 -2
View File
@@ -181,8 +181,13 @@ func (c *Cache) TurnPlanetProductions() {
ri := c.RaceIndex(*p.Owner) ri := c.RaceIndex(*p.Owner)
r := &c.g.Race[ri] r := &c.g.Race[ri]
// upgrade groups and return to in_orbit state // Upgrade groups (most expensive first) and return them to the
productionAvailable := c.PlanetProductionCapacity(pn) // in-orbit state, paying for each upgrade once out of the planet's
// full production potential; whatever remains feeds this turn's
// production below. Starting from PlanetProductionCapacity here would
// have charged every applied upgrade twice, since that helper already
// nets out the reserved upgrade cost for the report.
productionAvailable := p.ProductionCapacity()
for sg := range c.shipGroupsInUpgrade(p.Number) { for sg := range c.shipGroupsInUpgrade(p.Number) {
cost := c.upgradeCostNow(sg) cost := c.upgradeCostNow(sg)
if productionAvailable >= cost { if productionAvailable >= cost {
+34 -5
View File
@@ -208,11 +208,40 @@ func TestProduceShips(t *testing.T) {
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State()) assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
assert.Equal(t, 1.5, c.ShipGroup(0).TechLevel(game.TechDrive).F()) assert.Equal(t, 1.5, c.ShipGroup(0).TechLevel(game.TechDrive).F())
// Upgrade cost is now recomputed from the group's current ship count // The pending upgrade is now charged once (not twice) against the planet's
// (calc raw float) rather than read from the Fixed12-rounded stored value, // production potential, so MAT production keeps the budget it previously
// which shifts Material at the 12th decimal — far below the 3-decimal // lost to the double charge (the pre-fix value here was ~4346.68).
// report precision, so an InDelta check is the robust expectation here. assert.InDelta(t, 7173.3432, c.MustPlanet(R0_Planet_0_num).Material.F(), 0.001)
assert.InDelta(t, 4346.6766, c.MustPlanet(R0_Planet_0_num).Material.F(), 0.001) }
// TestUpgradeDoesNotDoubleChargeProduction guards that a pending upgrade is
// paid for once out of the planet's production potential, leaving the rest for
// the turn's production. The pre-fix code subtracted the upgrade cost twice
// (PlanetProductionCapacity already nets it out for the report, and the apply
// loop netted it again), which both starved production and could skip
// affordable upgrades.
func TestUpgradeDoesNotDoubleChargeProduction(t *testing.T) {
c, _ := newCache()
p := c.MustPlanet(R0_Planet_0_num)
p.Population = 1000
p.Industry = 1000 // ProductionCapacity = 1000*0.75 + 1000*0.25 = 1000
p.Resources = 1 // material produced == leftover production budget
p.Colonists = 0
p.Material = 0
assert.NoError(t, c.PlanetProduce(Race_0_idx, int(R0_Planet_0_num), game.ProductionMaterial, ""))
// One Cruiser with a pending drive upgrade 1.1 -> 2.0:
// block cost = (1 - 1.1/2.0) * 10 * 15 = 67.5 for the single ship.
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
c.ShipGroup(0).StateUpgrade = &game.InUpgrade{
UpgradeTech: []game.UpgradePreference{{Tech: game.TechDrive, Level: 2.0}},
}
c.TurnPlanetProductions()
assert.InDelta(t, 2.0, c.ShipGroup(0).TechLevel(game.TechDrive).F(), 0.0001)
// 1000 - 67.5 = 932.5; the pre-fix double charge would have left 865.
assert.InDelta(t, 932.5, c.MustPlanet(R0_Planet_0_num).Material.F(), 0.01)
} }
func TestProduceShip(t *testing.T) { func TestProduceShip(t *testing.T) {