fix(game): release a banished race's assets during turn generation
Tests · Go / test (push) Successful in 1m57s
Tests · Integration / integration (pull_request) Successful in 1m49s
Tests · Go / test (pull_request) Successful in 2m2s

TurnWipeExtinctRaces iterated only non-extinct races, so an
administratively banished race (flagged extinct, TTL untouched) was never
wiped: its planets stayed owned and its ships lingered, while the race
itself could no longer act. The loop now covers every race and wipes when
either an active race's TTL has run out (idle / quit) or an extinct race
still holds assets (banish). The asset check makes repeated passes
idempotent.

wipeRace already matched the rules for exclusion (ships removed, planets
uninhabited, industry and capital cleared, material retained), so the
behaviour is just documented in game/README.md.

Tests: banish releases planets and ships on the next turn (and is
idempotent); idle-timeout wipe still fires under the new iterator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-31 09:03:01 +02:00
parent 200236369f
commit 3b1c52cd02
3 changed files with 67 additions and 5 deletions
+23 -2
View File
@@ -117,13 +117,34 @@ func (c *Cache) raceTechLevel(ri int, t game.Tech, v float64) {
}
func (c *Cache) TurnWipeExtinctRaces() {
for i := range c.listRaceActingIdx() {
if (c.g.Race[i].Extinct && c.g.Race[i].TTL > 0) || (!c.g.Race[i].Extinct && c.g.Race[i].TTL == 0) {
for i := range c.listRaceIdx() {
r := &c.g.Race[i]
// Idle timeout or voluntary quit: a still-active race whose TTL ran
// out. Administrative banish: a race already flagged extinct that
// still holds assets to release. Once a race is wiped it owns nothing,
// so the asset check keeps this idempotent across later turns.
if (!r.Extinct && r.TTL == 0) || (r.Extinct && c.raceHasAssets(i)) {
c.wipeRace(i)
}
}
}
// raceHasAssets reports whether the race still owns a planet or a ship group.
func (c *Cache) raceHasAssets(ri int) bool {
id := c.g.Race[ri].ID
for i := range c.g.Map.Planet {
if c.g.Map.Planet[i].OwnedBy(id) {
return true
}
}
for i := range c.g.ShipGroups {
if c.g.ShipGroups[i].OwnerID == id {
return true
}
}
return false
}
func (c *Cache) wipeRace(ri int) {
c.validateRaceIndex(ri)
r := &c.g.Race[ri]
+37
View File
@@ -1,6 +1,7 @@
package controller_test
import (
"slices"
"testing"
e "galaxy/error"
@@ -90,3 +91,39 @@ func TestRaceID(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, Race_0_ID, id)
}
// TestBanishReleasesAssets checks that an administratively banished race only
// gets flagged extinct, and its planets and ships are released during turn
// generation; a second pass is a no-op.
func TestBanishReleasesAssets(t *testing.T) {
c, g := newCache()
c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R1_Planet_1_num, 3)
assert.True(t, c.MustPlanet(R1_Planet_1_num).OwnedBy(Race_1_ID))
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 1)
assert.NoError(t, g.RaceBanish(Race_1.Name))
assert.True(t, c.Race(Race_1_idx).Extinct)
assert.True(t, c.MustPlanet(R1_Planet_1_num).OwnedBy(Race_1_ID), "still owned until the turn runs")
c.TurnWipeExtinctRaces()
assert.False(t, c.MustPlanet(R1_Planet_1_num).Owned())
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 0)
// Idempotent: re-running over an already-wiped (asset-less) race is a no-op.
c.TurnWipeExtinctRaces()
assert.False(t, c.MustPlanet(R1_Planet_1_num).Owned())
}
// TestIdleRaceWipedOnTimeout guards that a still-active race whose TTL ran out
// (idle timeout or quit) is still wiped after the iterator change.
func TestIdleRaceWipedOnTimeout(t *testing.T) {
c, _ := newCache()
c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R1_Planet_1_num, 1)
c.Race(Race_1_idx).TTL = 0
assert.False(t, c.Race(Race_1_idx).Extinct)
c.TurnWipeExtinctRaces()
assert.True(t, c.Race(Race_1_idx).Extinct)
assert.False(t, c.MustPlanet(R1_Planet_1_num).Owned())
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 0)
}