feat: enroute groups
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 ***/
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user