fix(game): bomb in descending power order, collapse industry on wipe #79

Merged
developer merged 1 commits from feature/game-bombing-order into development 2026-05-31 06:59:44 +00:00
2 changed files with 80 additions and 7 deletions
+24 -7
View File
@@ -1,6 +1,10 @@
package controller package controller
import ( import (
"cmp"
"maps"
"slices"
"galaxy/game/internal/model/game" "galaxy/game/internal/model/game"
"github.com/google/uuid" "github.com/google/uuid"
@@ -13,8 +17,14 @@ func (c *Cache) ProduceBombings() []*game.Bombing {
if !p.Owned() { if !p.Owned() {
continue continue
} }
for ri, groups := range enemies { // The planet is hit by all attacking races at once, accounted from the
br := c.bombingReport(p, ri, groups) // strongest bombing power downwards, until no population remains.
attackers := slices.Collect(maps.Keys(enemies))
slices.SortFunc(attackers, func(a, b int) int {
return cmp.Compare(c.bombingPower(enemies[b]), c.bombingPower(enemies[a]))
})
for _, ri := range attackers {
br := c.bombingReport(p, ri, enemies[ri])
report = append(report, br) report = append(report, br)
if br.Wiped { if br.Wiped {
break break
@@ -22,7 +32,11 @@ func (c *Cache) ProduceBombings() []*game.Bombing {
} }
if p.Population == 0 { if p.Population == 0 {
// Wiped out: the planet turns uninhabited and its industry
// collapses, but the material and capital stockpiles survive for
// whoever colonises it next (rules "Бомбардировка планет").
p.Free() p.Free()
p.Ind(0)
} else { } else {
// Если на планете остались также и колонисты, то они превращаются в население, // Если на планете остались также и колонисты, то они превращаются в население,
// а накопленная промышленность возмещает потери производства. // а накопленная промышленность возмещает потери производства.
@@ -33,13 +47,16 @@ func (c *Cache) ProduceBombings() []*game.Bombing {
return report return report
} }
func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) *game.Bombing { func (c *Cache) bombingPower(groups []int) float64 {
attackPower := 0. var power float64
for _, i := range groups { for _, i := range groups {
sg := c.ShipGroup(i) power += c.ShipGroup(i).BombingPower(c.ShipGroupShipClass(i))
st := c.ShipGroupShipClass(i)
attackPower += sg.BombingPower(st)
} }
return power
}
func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) *game.Bombing {
attackPower := c.bombingPower(groups)
r := &game.Bombing{ r := &game.Bombing{
ID: uuid.New(), ID: uuid.New(),
PlanetOwnedID: *p.Owner, PlanetOwnedID: *p.Owner,
+56
View File
@@ -141,3 +141,59 @@ func TestProduceBombings(t *testing.T) {
} }
} }
} }
// TestBombingOrderByPower checks that attacking races are accounted from the
// strongest bombing power downwards (the report order), not in the random map
// iteration order the engine used before.
func TestBombingOrderByPower(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
weakIdx, _ := c.AddRace("Weakling")
assert.NoError(t, c.UpdateRelation(weakIdx, Race_0_idx, game.RelationWar))
assert.NoError(t, c.ShipClassCreate(weakIdx, "Pebble", 1, 1, 1, 1, 0))
// Planet_0 (Race_0) survives both attacks.
c.MustPlanet(R0_Planet_0_num).Population = 1000
// Strong: one Race_1 gunship (~358.9 power); weak: one Pebble (~1.1 power).
c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R0_Planet_0_num, 1)
c.CreateShipsUnsafe_T(weakIdx, c.MustShipClass(weakIdx, "Pebble").ID, R0_Planet_0_num, 1)
reports := c.ProduceBombings()
assert.Len(t, reports, 2)
assert.Equal(t, Race_1.Name, reports[0].Attacker, "strongest attacker comes first")
assert.Equal(t, "Weakling", reports[1].Attacker)
assert.Greater(t, reports[0].AttackPower.F(), reports[1].AttackPower.F())
}
// TestBombingWipeZeroesIndustry checks that a planet bombed to extinction loses
// its industry but keeps its material and capital stockpiles for the next
// colonist (rules "Бомбардировка планет").
func TestBombingWipeZeroesIndustry(t *testing.T) {
c, _ := newCache()
bomberIdx, _ := c.AddRace("Bomber")
assert.NoError(t, c.UpdateRelation(bomberIdx, Race_0_idx, game.RelationWar))
// Bombing power ~106.5 (W=60, A=1, weapons tech 1.0): wipes pop 50 while
// only partly converting industry, so the leftover industry is observable.
assert.NoError(t, c.ShipClassCreate(bomberIdx, "Reaper", 1, 1, 60, 1, 0))
p := c.MustPlanet(R0_Planet_0_num)
p.Population = 50
p.Industry = 200
p.Capital = 30
p.Material = 20
p.Colonists = 0
c.CreateShipsUnsafe_T(bomberIdx, c.MustShipClass(bomberIdx, "Reaper").ID, R0_Planet_0_num, 1)
reports := c.ProduceBombings()
assert.Len(t, reports, 1)
assert.True(t, reports[0].Wiped)
pl := c.MustPlanet(R0_Planet_0_num)
assert.False(t, pl.Owned())
assert.Equal(t, 0., pl.Population.F())
assert.Equal(t, 0., pl.Industry.F(), "industry collapses on wipe")
assert.Equal(t, 30., pl.Capital.F(), "capital stockpile survives")
assert.InDelta(t, 126.476, pl.Material.F(), 0.01, "material keeps the converted industry")
}