fix(game): fight before departure and reorder the turn sequence
Tests · Go / test (push) Successful in 1m58s
Tests · Integration / integration (pull_request) Successful in 1m50s
Tests · Go / test (pull_request) Successful in 2m5s

Per the documented turn order (game/rules.txt "Последовательность
действий"), no ship should dodge the pre-departure battle by slipping
into hyperspace. MakeTurn now runs merge -> battle -> load+launch routed
groups -> fly -> merge -> battle, so:

- ships ordered to depart (Launched) and ships being upgraded now take
  part in the pre-departure battle at their planet (CollectPlanetGroups /
  FilterBattleGroups); only survivors then enter hyperspace;
- routed transports are loaded and launched AFTER that battle, so they
  fight empty and cannot escape it.

A just-launched group has no stored hyperspace position, so moveShipGroup
starts its first leg from the origin planet; the previous code read the
nil launch coordinate and would panic.

Because upgrading groups can now lose ships in the battle, the pending
upgrade cost is recomputed from the group's current ship count instead of
the value stored when the order was validated.

Rules: reordered "Последовательность действий" and rewrote the combat
note that ordered/routed ships skip the battle.

Tests: launched-group move from origin, launched/upgrade groups taking
part in battle, upgrade cost tracking ship losses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-31 00:25:46 +02:00
parent 5e86ca9999
commit b4abf90ec5
11 changed files with 198 additions and 53 deletions
+53
View File
@@ -326,3 +326,56 @@ func TestSingleBattleOneSidedWipe(t *testing.T) {
}
assert.Equal(t, 5, kills, "exactly the five transports are destroyed")
}
// TestCollectPlanetGroupsIncludesLaunchedAndUpgrade checks that every group
// physically at a planet — in orbit, being upgraded, or ordered to depart but
// not yet flown (Launched) — is collected for, and kept in, the battle.
func TestCollectPlanetGroupsIncludesLaunchedAndUpgrade(t *testing.T) {
c, _ := newCache()
// group 0: in orbit at Planet_0
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
// group 1: ordered to depart Planet_0 (Launched), still physically there
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
c.ShipGroup(1).StateInSpace = &game.InSpace{Origin: R0_Planet_0_num}
c.ShipGroup(1).Destination = R0_Planet_2_num
assert.Equal(t, game.StateLaunched, c.ShipGroup(1).State())
// group 2: being upgraded at Planet_0
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
c.ShipGroup(2).StateUpgrade = &game.InUpgrade{UpgradeTech: []game.UpgradePreference{{Tech: game.TechDrive, Level: 2.0, Cost: 100}}}
assert.Equal(t, game.StateUpgrade, c.ShipGroup(2).State())
pg := controller.CollectPlanetGroups(c)
assert.Contains(t, pg, R0_Planet_0_num)
assert.Len(t, pg[R0_Planet_0_num], 3)
for _, idx := range []int{0, 1, 2} {
assert.Contains(t, pg[R0_Planet_0_num], idx)
}
battleGroups := controller.FilterBattleGroups(c, pg[R0_Planet_0_num])
assert.Len(t, battleGroups, 3)
}
// TestProduceBattlesLaunchedFightsAtOrigin checks that a group ordered to
// depart (Launched) still fights the pre-departure battle at its origin
// planet, rather than escaping into hyperspace before the fight.
func TestProduceBattlesLaunchedFightsAtOrigin(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()))
// Race_0: armed group in orbit at Planet_0.
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10))
// Race_1: armed group ordered to depart Planet_0 (Launched), still there.
c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R0_Planet_0_num, 10)
c.ShipGroup(1).StateInSpace = &game.InSpace{Origin: R0_Planet_0_num}
c.ShipGroup(1).Destination = R0_Planet_2_num
assert.Equal(t, game.StateLaunched, c.ShipGroup(1).State())
battles := controller.ProduceBattles(c)
assert.Len(t, battles, 1)
assert.True(t, battles[0].ObserverGroups[1], "launched group must be marked in-battle")
if c.ShipGroup(0).Number == 0 {
assert.Greater(t, c.ShipGroup(1).Number, uint(0))
} else {
assert.Zero(t, c.ShipGroup(1).Number)
}
}