Files
galaxy-game/game/internal/controller/bombing_test.go
T
Ilia Denisov a01f39e4a7
Tests · Go / test (push) Successful in 1m57s
Tests · Integration / integration (pull_request) Successful in 1m46s
Tests · Go / test (pull_request) Successful in 2m2s
fix(game): bomb in descending power order, collapse industry on wipe
Per the rules ("Бомбардировка планет"), a planet is bombed from the
strongest attacking power downwards, and a planet bombed to extinction
keeps its material and capital stockpiles but loses its working industry.

ProduceBombings now sorts attacking races by total bombing power
(descending) instead of iterating the attacker map in random order, and
on a wipe zeroes the planet's industry (Free already keeps capital and
material). bombingPower is extracted as a shared helper.

The rules already describe both, so no documentation change. Tests:
bombing order by power, and industry collapse with capital/material kept
on a wipe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 08:47:45 +02:00

200 lines
7.4 KiB
Go

package controller_test
import (
"testing"
"galaxy/game/internal/controller"
"galaxy/game/internal/model/game"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestBombPlanet(t *testing.T) {
id := uuid.New()
p := controller.NewPlanet(0, "Planet_0", &id, 1, 1, 1000, 300, 200, 10, game.ResearchDrive.AsType(uuid.Nil))
(&p).Colonists = 100.
assert.Equal(t, 0., p.Material.F())
controller.BombPlanet(&p, 55.)
assert.Equal(t, 245., p.Population.F())
assert.Equal(t, 45., p.Colonists.F())
assert.Equal(t, 145., p.Industry.F())
assert.Equal(t, 55., p.Material.F())
controller.BombPlanet(&p, 56.)
assert.Equal(t, 189., p.Population.F())
assert.Equal(t, 0., p.Colonists.F())
assert.Equal(t, 89., p.Industry.F())
assert.Equal(t, 111., p.Material.F())
controller.BombPlanet(&p, 200.)
assert.Equal(t, 0., p.Population.F())
assert.Equal(t, 0., p.Colonists.F())
assert.Equal(t, 0., p.Industry.F())
assert.Equal(t, 200., p.Material.F())
}
func TestCollectBombingGroups(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
// 1: idx = 0 / Ready to bomb: Race_1/Planet_1
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) // bombs
c.ShipGroup(0).Destination = R1_Planet_1_num
// 2: idx = 1 / Ready to bomb: Race_0/Planet_2
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 3)) // bombs
c.ShipGroup(1).Destination = R0_Planet_2_num
// 3: idx = 2 / In_Space
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
c.ShipGroup(2).StateInSpace = &InSpace
// 4: idx = 3 / Has no Ammo
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1))
c.ShipGroup(3).Destination = R1_Planet_1_num
// 5: idx = 4 / On it's own planet
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
// 6: idx = 5 / On uninhabited planet
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2))
c.ShipGroup(5).Destination = Uninhabited_Planet_3_num
bg := c.CollectBombingGroups()
assert.Len(t, bg, 2)
assert.Contains(t, bg, R1_Planet_1_num)
assert.Contains(t, bg, R0_Planet_2_num)
assert.Len(t, bg[R1_Planet_1_num], 1)
assert.Contains(t, bg[R1_Planet_1_num], Race_0_idx)
assert.Len(t, bg[R0_Planet_2_num], 1)
assert.Contains(t, bg[R0_Planet_2_num], Race_1_idx)
assert.Len(t, bg[R1_Planet_1_num][Race_0_idx], 1)
assert.Equal(t, 0, bg[R1_Planet_1_num][Race_0_idx][0])
assert.Len(t, bg[R0_Planet_2_num][Race_1_idx], 1)
assert.Equal(t, 1, bg[R0_Planet_2_num][Race_1_idx][0])
// remove bombings from Race_1
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationPeace.String()))
bg = c.CollectBombingGroups()
assert.Len(t, bg, 1)
assert.Contains(t, bg, R1_Planet_1_num)
assert.Len(t, bg[R1_Planet_1_num], 1)
assert.Contains(t, bg[R1_Planet_1_num], Race_0_idx)
assert.Len(t, bg[R1_Planet_1_num][Race_0_idx], 1)
assert.Equal(t, 0, bg[R1_Planet_1_num][Race_0_idx][0])
}
func TestProduceBombings(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
// 1: idx = 0 / Bombs on: Race_1/Planet_1
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3))
c.ShipGroup(0).Destination = R1_Planet_1_num
// 2: idx = 1 / Bombs on: Race_1/Planet_1
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 7))
c.ShipGroup(1).Destination = R1_Planet_1_num
// 3: idx = 2 / Bombs on: Race_0/Planet_2
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1))
c.ShipGroup(2).Destination = R0_Planet_2_num
c.MustPlanet(R0_Planet_2_num).Population = 500
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", R0_Planet_2_num, R0_Planet_0_num))
assert.NotEmpty(t, c.MustPlanet(R0_Planet_2_num).Route)
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "EMP", R1_Planet_1_num, R0_Planet_2_num))
assert.NotEmpty(t, c.MustPlanet(R1_Planet_1_num).Route)
reports := c.ProduceBombings()
assert.Len(t, reports, 2)
for _, b := range reports {
assert.NotEqual(t, uuid.Nil, b.ID)
switch pn := b.Number; pn {
case R1_Planet_1_num:
assert.Equal(t, Race_1.Name, b.Owner)
assert.Equal(t, Race_0.Name, b.Attacker)
assert.InDelta(t, 697.857, b.AttackPower.F(), 0.0003)
assert.True(t, b.Wiped)
assert.False(t, c.MustPlanet(pn).Owned())
assert.Empty(t, c.MustPlanet(pn).Route)
assert.Equal(t, 0., c.MustPlanet(pn).Population.F())
case R0_Planet_2_num:
assert.Equal(t, Race_0.Name, b.Owner)
assert.Equal(t, Race_1.Name, b.Attacker)
assert.InDelta(t, 358.856, b.AttackPower.F(), 0.0001)
assert.False(t, b.Wiped)
assert.True(t, c.MustPlanet(pn).OwnedBy(Race_0_ID))
assert.NotEmpty(t, c.MustPlanet(pn).Route)
assert.InDelta(t, 500.-358.85596, c.MustPlanet(pn).Population.F(), 0.000001)
}
}
}
// 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")
}