feat: moge groups in hyperspace

This commit is contained in:
Ilia Denisov
2026-01-18 22:38:11 +02:00
parent 741a5f726b
commit bd9db26ef4
12 changed files with 273 additions and 20 deletions
@@ -70,3 +70,7 @@ func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) {
func (c *Cache) ListRouteEligibleGroupIds(pn uint) iter.Seq[int] {
return c.listRouteEligibleGroupIds(pn)
}
func (c *Cache) ListMoveableGroupIds() iter.Seq[int] {
return c.listMoveableGroupIds()
}
+3 -3
View File
@@ -94,9 +94,9 @@ func newGame() *game.Game {
Width: 1000,
Height: 1000,
Planet: []game.Planet{
controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 0, 0, 100, 100, 100, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 1, 1, 100, 100, 100, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 3, 3, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(3, "Planet_3", uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
},
},
+19
View File
@@ -250,6 +250,25 @@ func (c *Cache) FleetGroups(ri, fi int) iter.Seq[*game.ShipGroup] {
}
}
func (c *Cache) fleetGroupIds(ri, fi int) iter.Seq[int] {
c.validateRaceIndex(ri)
c.validateFleetIndex(fi)
return func(yield func(int) bool) {
for i := range c.ShipGroupsIndex() {
sg := c.ShipGroup(i)
if c.g.Race[ri].ID != sg.OwnerID {
continue
}
if sg.FleetID == nil || c.MustFleetIndex(*sg.FleetID) != fi {
continue
}
if !yield(i) {
return
}
}
}
}
func (c *Cache) listFleets(ri int) iter.Seq[*game.Fleet] {
c.validateRaceIndex(ri)
return func(yield func(*game.Fleet) bool) {
+10 -1
View File
@@ -15,9 +15,18 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error {
// 02. Враждующие корабли вступают в схватку.
battles := ProduceBattles(c.Cache)
// 03. Товары загружаются на корабли, находящиеся в начале грузовых маршрутов, и корабли входят в гиперпространство.
// 03. Товары загружаются на корабли, находящиеся в начале грузовых маршрутов, и корабли входят в гиперпространство (но ещё не полетели)
c.Cache.EnrouteGroups()
// 04. Корабли пролетают сквозь гиперпространство.
c.Cache.MoveShipGroups()
// 05. Корабли, где это возможно, объединяются в группы.
c.Cache.CmdJoinEqualGroups()
// 06. Враждующие корабли снова вступают в схватку (это происходит после выхода из гиперпространства).
battles = append(battles, ProduceBattles(c.Cache)...)
/*** Last steps ***/
// Store battles
-6
View File
@@ -137,9 +137,6 @@ func TestEnrouteGroups_SplitGroup(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
// assert.NoError(t, g.SetRoute(Race_0.Name, "MAT", R0_Planet_0_num, R0_Planet_2_num))
// assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", R0_Planet_0_num, R0_Planet_2_num))
// assert.NoError(t, g.SetRoute(Race_0.Name, "EMP", R0_Planet_2_num, R1_Planet_1_num))
c.MustPlanet(R0_Planet_0_num).Colonists = 65
@@ -162,9 +159,6 @@ func TestEnrouteGroups_GroupSorting(t *testing.T) {
c, g := newCache()
assert.NoError(t, g.SetRoute(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
// assert.NoError(t, g.SetRoute(Race_0.Name, "MAT", R0_Planet_0_num, R0_Planet_2_num))
// assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", R0_Planet_0_num, R0_Planet_2_num))
// assert.NoError(t, g.SetRoute(Race_0.Name, "EMP", R0_Planet_2_num, R1_Planet_1_num))
c.MustPlanet(R0_Planet_0_num).Colonists = 100
+62
View File
@@ -0,0 +1,62 @@
package controller
import (
"fmt"
"iter"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/iliadenisov/galaxy/internal/util"
)
func (c *Cache) MoveShipGroups() {
moved := make(map[int]bool)
for i := range c.listMoveableGroupIds() {
if v, ok := moved[i]; ok && v {
continue
}
sg := c.ShipGroup(i)
if sg.FleetID != nil {
fi := c.MustFleetIndex(*sg.FleetID)
delta := c.FleetSpeed(c.g.Fleets[fi])
for fgi := range c.fleetGroupIds(c.RaceIndex(sg.OwnerID), c.MustFleetIndex(*sg.FleetID)) {
c.moveShipGroup(fgi, delta)
moved[fgi] = true
}
continue
}
c.moveShipGroup(i, sg.Speed(c.ShipGroupShipClass(i)))
moved[i] = true
}
}
func (c *Cache) moveShipGroup(i int, delta float64) {
sg := c.ShipGroup(i)
originX, originY, ok := sg.Coord()
if !ok {
panic(fmt.Sprintf("ship group state invalid: %v", sg.State()))
}
destPlanet := c.MustPlanet(sg.Destination)
arrived := false
sg.StateInSpace.X, sg.StateInSpace.Y, arrived =
util.NextTravelCoord(c.g.Map.Width, c.g.Map.Height, originX, originY, destPlanet.X, destPlanet.Y, delta)
if arrived {
sg.StateInSpace = nil
}
}
func (c *Cache) listMoveableGroupIds() iter.Seq[int] {
return func(yield func(int) bool) {
for i := range c.ShipGroupsIndex() {
sg := c.ShipGroup(i)
state := sg.State()
if !(state == game.StateInOrbit || state == game.StateLaunched || state == game.StateInSpace) {
continue
}
if !yield(i) {
return
}
}
}
}
@@ -0,0 +1,49 @@
package controller_test
import (
"slices"
"testing"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
func TestListMoveableGroupIds(t *testing.T) {
c, g := newCache()
// 1: idx = 0 / [v] Non-Fleet group
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
// 2: idx = 1 / [v] In-Fleet group
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
// 3: idx = 2 / [v] In-Fleet group
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 2, 0))
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "Fleet", 3, 0))
// 4: idx = 3 / [v] In_Space
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
c.ShipGroup(3).StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
// 5: idx = 4 / [x] In_Upgrage
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
c.ShipGroup(4).StateUpgrade = &game.InUpgrade{
UpgradeTech: []game.UpgradePreference{},
}
// 6: idx = 5 / [v] Just launched group
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
assert.NoError(t, g.SendGroup(Race_0.Name, 6, R0_Planet_2_num, 0))
movableGroups := slices.Collect(c.ListMoveableGroupIds())
assert.Len(t, movableGroups, 5)
for _, i := range movableGroups {
sg := c.ShipGroup(i)
assert.NotEqual(t, game.StateUpgrade, sg.State())
assert.NotEqual(t, game.StateTransfer, sg.State()) // TODO: Transfer state movable or not?
}
}
+11 -3
View File
@@ -72,10 +72,16 @@ func (c *Cache) LaunchShips(sg *game.ShipGroup, destination uint) *game.ShipGrou
for i := range c.ShipGroupsIndex() {
if c.ShipGroup(i).OwnerID == sg.OwnerID && c.ShipGroup(i).Index == sg.Index {
state := c.ShipGroup(i).State()
if state != game.StateInOrbit && state != game.StateLaunched {
var p *game.Planet
switch state {
case game.StateInOrbit:
p = c.MustPlanet(sg.Destination)
case game.StateLaunched:
p = c.MustPlanet(sg.StateInSpace.Origin)
default:
panic("state invalid")
}
c.g.ShipGroups[i] = LaunchShips(*sg, destination)
c.g.ShipGroups[i] = LaunchShips(*sg, destination, p.X, p.Y)
return &c.g.ShipGroups[i]
}
}
@@ -96,9 +102,11 @@ func (c *Cache) UnsendShips(sg *game.ShipGroup) *game.ShipGroup {
panic("ship group not found")
}
func LaunchShips(sg game.ShipGroup, destination uint) game.ShipGroup {
func LaunchShips(sg game.ShipGroup, destination uint, originX, originY float64) game.ShipGroup {
sg.StateInSpace = &game.InSpace{
Origin: sg.Destination,
X: originX,
Y: originY,
}
sg.Destination = destination
return sg
+10 -4
View File
@@ -44,26 +44,32 @@ func TestSendGroup(t *testing.T) {
g.SendGroup(Race_0.Name, 1, 3, 0),
e.GenericErrorText(e.ErrSendUnreachableDestination))
assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 3)) // send 3 of 10
assert.NoError(t, g.SendGroup(Race_0.Name, 1, R0_Planet_2_num, 3)) // send 3 of 10
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
assert.Equal(t, uint(7), c.ShipGroup(0).Number)
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
assert.Equal(t, uint(3), c.ShipGroup(3).Number)
assert.Equal(t, game.StateLaunched, c.ShipGroup(3).State())
assert.NotNil(t, c.ShipGroup(3).StateInSpace)
assert.Equal(t, c.MustPlanet(R0_Planet_0_num).X, c.ShipGroup(3).StateInSpace.X)
assert.Equal(t, c.MustPlanet(R0_Planet_0_num).Y, c.ShipGroup(3).StateInSpace.Y)
assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 2)) // un-send 2 of 3
assert.NoError(t, g.SendGroup(Race_0.Name, 4, R0_Planet_0_num, 2)) // un-send 2 of 3
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
assert.Equal(t, uint(9), c.MustShipGroup(Race_0_idx, 1).Number)
assert.Equal(t, game.StateInOrbit, c.MustShipGroup(Race_0_idx, 1).State())
assert.Equal(t, uint(1), c.MustShipGroup(Race_0_idx, 4).Number)
assert.Equal(t, game.StateLaunched, c.MustShipGroup(Race_0_idx, 4).State())
assert.NotNil(t, c.MustShipGroup(Race_0_idx, 4).StateInSpace)
assert.Equal(t, c.MustPlanet(R0_Planet_0_num).X, c.MustShipGroup(Race_0_idx, 4).StateInSpace.X)
assert.Equal(t, c.MustPlanet(R0_Planet_0_num).Y, c.MustShipGroup(Race_0_idx, 4).StateInSpace.Y)
assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 0)) // un-send the rest 1
assert.NoError(t, g.SendGroup(Race_0.Name, 4, R0_Planet_0_num, 0)) // un-send the rest 1
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3)
assert.Equal(t, uint(10), c.MustShipGroup(Race_0_idx, 1).Number)
assert.Equal(t, game.StateInOrbit, c.MustShipGroup(Race_0_idx, 1).State())
assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 0))
assert.NoError(t, g.SendGroup(Race_0.Name, 1, R0_Planet_2_num, 0))
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3)
assert.Equal(t, uint(10), c.MustShipGroup(Race_0_idx, 1).Number)
assert.Equal(t, game.StateLaunched, c.MustShipGroup(Race_0_idx, 1).State())