fix(game): bomb in descending power order, collapse industry on wipe #79
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user