fix(game): release a banished race's assets during turn generation #80
+7
-3
@@ -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`
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user