feat: enroute groups

This commit is contained in:
Ilia Denisov
2026-01-16 22:20:27 +02:00
parent 16aba8435d
commit 741a5f726b
8 changed files with 299 additions and 9 deletions
@@ -66,3 +66,7 @@ func (c *Cache) PutMaterial(pn uint, v float64) {
func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) {
c.raceTechLevel(ri, t, v)
}
func (c *Cache) ListRouteEligibleGroupIds(pn uint) iter.Seq[int] {
return c.listRouteEligibleGroupIds(pn)
}
+11
View File
@@ -2,10 +2,13 @@ package controller_test
import (
"fmt"
"slices"
"testing"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/controller"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
var (
@@ -67,6 +70,14 @@ var (
}
)
// [ ] Delete this fake test
func TestSlicesDelete(t *testing.T) {
sl := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
assert.Len(t, sl, 10)
sl = slices.DeleteFunc(sl, func(v int) bool { return v%2 == 0 })
assert.Len(t, sl, 5)
}
func assertNoError(err error) {
if err != nil {
panic(fmt.Sprintf("init assertion failed: %v", err))
+2 -8
View File
@@ -1,8 +1,6 @@
package controller
import (
// "github.com/iliadenisov/galaxy/internal/controller"
e "github.com/iliadenisov/galaxy/internal/error"
// "github.com/iliadenisov/galaxy/internal/game/battle"
"github.com/iliadenisov/galaxy/internal/model/game"
)
@@ -17,12 +15,8 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error {
// 02. Враждующие корабли вступают в схватку.
battles := ProduceBattles(c.Cache)
// Internal control: after battles there are can't be groups with no ships left
for i := range g.ShipGroups {
if g.ShipGroups[i].Number == 0 {
return e.NewGameStateError("")
}
}
// 03. Товары загружаются на корабли, находящиеся в начале грузовых маршрутов, и корабли входят в гиперпространство.
c.Cache.EnrouteGroups()
/*** Last steps ***/
+106
View File
@@ -1,6 +1,11 @@
package controller
import (
"cmp"
"iter"
"math"
"slices"
e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/iliadenisov/galaxy/internal/util"
@@ -82,3 +87,104 @@ func (c *Cache) RemovePlanetRoute(rt game.RouteType, origin uint) {
delete(c.g.Map.Planet[pi].Route, rt)
}
}
// TODO: NOT IN THIS FUNC: remove routes if planet became uninhabited
func (c *Cache) EnrouteGroups() {
for pi := range c.g.Map.Planet {
if len(c.g.Map.Planet[pi].Route) == 0 {
continue
}
groups := slices.Collect(c.listRouteEligibleGroupIds(c.g.Map.Planet[pi].Number))
if len(groups) == 0 {
continue
}
sortGroups := func(g []int) {
// sort groups by largest CargoCapacity
slices.SortFunc(g, func(l, r int) int {
return cmp.Or(cmp.Compare(c.ShipGroup(r).CargoCapacity(c.ShipGroupShipClass(r)),
c.ShipGroup(l).CargoCapacity(c.ShipGroupShipClass(l))),
cmp.Compare(l, r))
})
}
reorderGroups := func(g []int) []int {
g = slices.DeleteFunc(g, func(i int) bool { return c.ShipGroup(i).State() != game.StateInOrbit })
sortGroups(g)
return g
}
sortGroups(groups)
p := c.MustPlanet(c.g.Map.Planet[pi].Number)
// COL -> CAP -> MAT -> EMPTY
for _, rt := range []game.RouteType{game.RouteColonist, game.RouteCapital, game.RouteMaterial, game.RouteEmpty} {
dest, ok := c.g.Map.Planet[pi].Route[rt]
if !ok {
continue
}
var res *float64
var ct game.CargoType
switch rt {
case game.RouteColonist:
res = &p.Colonists
ct = game.CargoColonist
case game.RouteCapital:
res = &p.Capital
ct = game.CargoCapital
case game.RouteMaterial:
res = &p.Material
ct = game.CargoMaterial
default:
for _, sgi := range groups {
c.LaunchShips(c.ShipGroup(sgi), dest)
}
groups = reorderGroups(groups)
continue
}
for res != nil && *res > 0 && len(groups) > 0 {
sgi := groups[0]
sg := c.ShipGroup(sgi)
st := c.ShipGroupShipClass(sgi)
ships := sg.Number
sgCapacity := sg.CargoCapacity(st)
toLoad := *res
if toLoad > sgCapacity {
toLoad = sgCapacity
} else if maxShips := uint(math.Ceil(toLoad / (sgCapacity / float64(ships)))); maxShips < ships {
newGroupIdx := c.breakGroupUnsafe(c.RaceIndex(sg.OwnerID), sgi, maxShips)
sg = c.ShipGroup(newGroupIdx)
}
// decrease planet resource
*res = *res - toLoad
// load group
sg.Load += toLoad
sg.CargoType = &ct
c.LaunchShips(sg, dest)
groups = reorderGroups(groups)
}
}
}
}
func (c *Cache) listRouteEligibleGroupIds(pn uint) iter.Seq[int] {
return func(yield func(int) bool) {
p := c.MustPlanet(pn)
for i := range c.ShipGroupsIndex() {
sg := c.ShipGroup(i)
st := c.ShipGroupShipClass(i)
if sg.OwnerID != p.Owner || // Planet must be owned by ships owner
sg.FleetID != nil || // Ships must not be part of a Fleet
sg.State() != game.StateInOrbit || // Ships must be only In_Orbit state
st.CargoBlockMass() == 0 || // Ship Class must have Cargo bays
sg.Load != 0 || // Ships must not be loaded for enrouting
sg.Destination != p.Number {
continue
}
if !yield(i) {
return
}
}
}
}
+171
View File
@@ -1,6 +1,7 @@
package controller_test
import (
"slices"
"testing"
e "github.com/iliadenisov/galaxy/internal/error"
@@ -89,3 +90,173 @@ func TestRemoveRoute(t *testing.T) {
g.RemoveRoute(Race_0.Name, "COL", 1),
e.GenericErrorText(e.ErrInputEntityNotOwned))
}
func TestListRouteEligibleGroupIds(t *testing.T) {
c, g := newCache()
// 1: idx = 0 / Ready to load
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
// 2: idx = 1 / Has no cargo bay
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
// 3: idx = 2 / In_Space
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
c.ShipGroup(2).StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
// 4: idx = 3 / loaded with COL
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
c.ShipGroup(3).Load = 1.234
// Foreign group -> idx 1
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
assert.NoError(t, g.GiveawayGroup(Race_0.Name, Race_1.Name, 5, 0))
// 5: idx = 4 / Part of the Fleet
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", 5, 0))
planet_0_groups := slices.Collect(c.ListRouteEligibleGroupIds(0))
assert.Len(t, planet_0_groups, 1)
for _, i := range planet_0_groups {
sg := c.ShipGroup(i)
st := c.ShipGroupShipClass(i)
assert.Equal(t, Race_0_ID, sg.OwnerID)
assert.Greater(t, sg.CargoCapacity(st), 0.)
assert.Equal(t, game.StateInOrbit, sg.State())
assert.Equal(t, 0., sg.Load)
assert.Nil(t, sg.FleetID)
}
}
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
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) // 21.0 per Ship
assert.Equal(t, 105., c.ShipGroup(0).CargoCapacity(c.ShipGroupShipClass(0)))
c.EnrouteGroups()
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
assert.Equal(t, uint(1), c.ShipGroup(0).Number)
assert.Equal(t, 0., c.ShipGroup(0).Load)
assert.Equal(t, game.StateLaunched, c.ShipGroup(1).State())
assert.Equal(t, uint(4), c.ShipGroup(1).Number)
assert.Equal(t, 65., c.ShipGroup(1).Load)
assert.Equal(t, 0., c.MustPlanet(R0_Planet_0_num).Colonists)
}
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
// 0: idx = 1
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 4)) // 21.0 per Ship
assert.Equal(t, 84., c.ShipGroup(0).CargoCapacity(c.ShipGroupShipClass(0)))
// 1: idx = 2
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) // 21.0 per Ship
assert.Equal(t, 105., c.ShipGroup(1).CargoCapacity(c.ShipGroupShipClass(1)))
c.EnrouteGroups()
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
assert.Equal(t, game.StateLaunched, c.ShipGroup(1).State())
assert.Equal(t, 100., c.ShipGroup(1).Load)
assert.Equal(t, 0., c.MustPlanet(R0_Planet_0_num).Colonists)
}
func TestEnrouteGroups_LaunchOrder(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, "CAP", 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, "EMP", R0_Planet_0_num, R1_Planet_1_num))
c.MustPlanet(R0_Planet_0_num).Colonists = 150
c.MustPlanet(R0_Planet_0_num).Capital = 100
c.MustPlanet(R0_Planet_0_num).Material = 20
// 0: idx = 1 (105 COL) ->
// 3: idx = 4 ( 45 COL)
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
assert.Equal(t, 105., c.ShipGroup(0).CargoCapacity(c.ShipGroupShipClass(0)))
// 1: idx = 2 (In_Orbit) ->
// 4: idx = 5 (20 MAT)
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
assert.Equal(t, 105., c.ShipGroup(1).CargoCapacity(c.ShipGroupShipClass(1)))
// 2: idx = 3 (100 CAP)
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
assert.Equal(t, 105., c.ShipGroup(2).CargoCapacity(c.ShipGroupShipClass(2)))
c.EnrouteGroups()
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 5)
// full load of COL
sgi := 0
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
assert.Equal(t, 105., c.ShipGroup(sgi).Load)
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
assert.Equal(t, game.CargoColonist, *c.ShipGroup(sgi).CargoType)
assert.Equal(t, uint(5), c.ShipGroup(sgi).Number)
// rest of COL
sgi = 3
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
assert.Equal(t, 45., c.ShipGroup(sgi).Load)
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
assert.Equal(t, game.CargoColonist, *c.ShipGroup(sgi).CargoType)
assert.Equal(t, uint(3), c.ShipGroup(sgi).Number)
// full load of CAP
sgi = 2
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
assert.Equal(t, 100., c.ShipGroup(sgi).Load)
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
assert.Equal(t, game.CargoCapital, *c.ShipGroup(sgi).CargoType)
assert.Equal(t, uint(5), c.ShipGroup(sgi).Number)
// partial load of MAT
sgi = 4
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
assert.Equal(t, 20., c.ShipGroup(sgi).Load)
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
assert.Equal(t, game.CargoMaterial, *c.ShipGroup(sgi).CargoType)
assert.Equal(t, uint(1), c.ShipGroup(sgi).Number)
// empty / on_planet
sgi = 1
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
assert.Equal(t, R1_Planet_1_num, c.ShipGroup(sgi).Destination)
assert.Equal(t, 0., c.ShipGroup(sgi).Load)
assert.Nil(t, c.ShipGroup(sgi).CargoType)
assert.Equal(t, uint(1), c.ShipGroup(sgi).Number)
}
+5 -1
View File
@@ -504,6 +504,10 @@ func (c *Cache) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int
if c.ShipGroup(sgi).Number < newGroupShips {
return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", c.ShipGroup(sgi).Index, c.ShipGroup(sgi).Number, newGroupShips)
}
return c.breakGroupUnsafe(ri, sgi, newGroupShips), nil
}
func (c *Cache) breakGroupUnsafe(ri, sgi int, newGroupShips uint) int {
newGroup := *c.ShipGroup(sgi)
if c.ShipGroup(sgi).CargoType != nil {
newGroup.Load = c.ShipGroup(sgi).Load / float64(c.ShipGroup(sgi).Number) * float64(newGroupShips)
@@ -511,7 +515,7 @@ func (c *Cache) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int
newGroup.Number = newGroupShips
c.ShipGroupShipsNumber(sgi, c.ShipGroup(sgi).Number-newGroup.Number)
newGroup.FleetID = nil
return c.appendShipGroup(ri, &newGroup), nil
return c.appendShipGroup(ri, &newGroup)
}
// Internal funcs