diff --git a/game/internal/controller/bombing.go b/game/internal/controller/bombing.go index 85376ca..fbdaf3f 100644 --- a/game/internal/controller/bombing.go +++ b/game/internal/controller/bombing.go @@ -1,6 +1,10 @@ package controller import ( + "cmp" + "maps" + "slices" + "galaxy/game/internal/model/game" "github.com/google/uuid" @@ -13,8 +17,14 @@ func (c *Cache) ProduceBombings() []*game.Bombing { if !p.Owned() { continue } - for ri, groups := range enemies { - br := c.bombingReport(p, ri, groups) + // The planet is hit by all attacking races at once, accounted from the + // 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) if br.Wiped { break @@ -22,7 +32,11 @@ func (c *Cache) ProduceBombings() []*game.Bombing { } 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.Ind(0) } else { // Если на планете остались также и колонисты, то они превращаются в население, // а накопленная промышленность возмещает потери производства. @@ -33,13 +47,16 @@ func (c *Cache) ProduceBombings() []*game.Bombing { return report } -func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) *game.Bombing { - attackPower := 0. +func (c *Cache) bombingPower(groups []int) float64 { + var power float64 for _, i := range groups { - sg := c.ShipGroup(i) - st := c.ShipGroupShipClass(i) - attackPower += sg.BombingPower(st) + power += c.ShipGroup(i).BombingPower(c.ShipGroupShipClass(i)) } + return power +} + +func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) *game.Bombing { + attackPower := c.bombingPower(groups) r := &game.Bombing{ ID: uuid.New(), PlanetOwnedID: *p.Owner, diff --git a/game/internal/controller/bombing_test.go b/game/internal/controller/bombing_test.go index a0f9e86..a5ef274 100644 --- a/game/internal/controller/bombing_test.go +++ b/game/internal/controller/bombing_test.go @@ -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") +}