fix(game): charge a ship upgrade against production only once
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:
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user