Files
OpenFrontIO/tests
Evan 661d96ba28 Fix structure cost double-counting units under construction (#4320)
## Problem

The ghost/build-menu price of a structure can show the wrong (inflated)
cost. Concretely: a player who owns a **captured** city and then starts
building their **first** city sees the 3rd-city price (**500k**) for
that build instead of the 2nd-city price (**250k**).

## Root cause

Structure cost scales as `2^(units built) × base` (city: 125k / 250k /
500k …), counted via:

```ts
Math.min(player.unitsOwned(type), player.unitsConstructed(type))
```

The `Math.min` is deliberate — it caps the count at how many you've
actually **built**, so **captured** units (owned but not built) don't
inflate the price.

`unitsConstructed()` defeated that by double-counting in-progress
builds:

```ts
const built = this.numUnitsConstructed[type] ?? 0;   // already includes the building unit
let constructing = 0;
for (const unit of this._units) {
  if (unit.type() !== type) continue;
  if (!unit.isUnderConstruction()) continue;
  constructing++;                                     // counts the SAME unit again
}
return constructing + built;                          // doubled
```

`recordUnitConstructed()` is called in `buildUnit()` the moment the unit
is created — while it is still under construction — so
`numUnitsConstructed` already accounts for in-progress builds. The extra
loop counted them a second time.

With one captured city + one city under construction: `unitsOwned = 2`,
double-counted `unitsConstructed = 2`, so `Math.min(2, 2) = 2` → 500k.
Without the double-count it's `Math.min(2, 1) = 1` → 250k. 

The redundant loop is a leftover from #2378, which removed the separate
`UnitType.Construction` unit. Back then in-progress builds were a
distinct unit type **not** recorded in `numUnitsConstructed`, so the
loop was needed; afterward it became a pure double-count. This is a
long-standing latent bug — present identically on `v31` — not a recent
regression.

## Fix

`unitsConstructed()` now just returns `numUnitsConstructed[type]`, which
already includes under-construction builds.

## Tests

`tests/economy/ConstructionCost.test.ts` covers both:
- pure case (first city under construction) → still 250k
- captured city + first city under construction → was 500k, now 250k
(fails without the fix with `expected 2 to be 1`)

All related suites (economy, PlayerImpl, nation structure behavior,
upgrades, MIRV pricing, stats) — 144 tests — pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:19:04 -07:00
..
2026-05-22 13:19:22 +01:00
2026-03-17 15:55:47 -07:00