Merge pull request 'fix(game): release a banished race's assets during turn generation' (#80) from feature/game-banish-wipe into development
Deploy · Dev / deploy (push) Successful in 45s
Tests · Integration / integration (push) Successful in 1m39s
Tests · Go / test (push) Successful in 3m6s

This commit was merged in pull request #80.
This commit is contained in:
2026-05-31 07:17:16 +00:00
3 changed files with 67 additions and 5 deletions
+7 -3
View File
@@ -102,9 +102,13 @@ remove-and-banish flow.
non-empty and must match an existing race in the engine's roster.
- Successful response: `204 No Content` with an empty body.
- Error responses follow the same `400` / `500` envelope shape as the
other admin endpoints. The engine-side mechanics of `banish` (what
exactly happens to the race's planets, fleets, and pending orders) are
owned by the engine maintainers.
other admin endpoints. `banish` only flags the race extinct, so it can
no longer submit or have orders applied; its assets are released at the
start of the next turn generation (`TurnWipeExtinctRaces`), the same way
an idle/quit timeout is handled but without the wait — ship groups and
fleets are removed, its planets become uninhabited (the working industry
and the capital stockpile are cleared, raw material is retained), and
votes cast for it are reset.
### `GET /healthz`
+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)
}