296 lines
8.0 KiB
Go
296 lines
8.0 KiB
Go
package controller
|
|
|
|
import (
|
|
"cmp"
|
|
"iter"
|
|
"maps"
|
|
"math"
|
|
"math/rand/v2"
|
|
"slices"
|
|
|
|
"github.com/google/uuid"
|
|
e "github.com/iliadenisov/galaxy/internal/error"
|
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
|
"github.com/iliadenisov/galaxy/internal/util"
|
|
)
|
|
|
|
func (c *Controller) SetRoute(raceName, loadType string, origin, destination uint) error {
|
|
ri, err := c.Cache.raceIndex(raceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rt, ok := game.RouteTypeSet[loadType]
|
|
if !ok {
|
|
return e.NewCargoTypeInvalidError(loadType)
|
|
}
|
|
return c.Cache.SetRoute(ri, rt, origin, destination)
|
|
}
|
|
|
|
func (c *Cache) SetRoute(ri int, rt game.RouteType, origin, destination uint) error {
|
|
c.validateRaceIndex(ri)
|
|
p1, ok := c.Planet(origin)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("origin planet #%d", origin)
|
|
}
|
|
if p1.Owner != c.g.Race[ri].ID {
|
|
return e.NewEntityNotOwnedError("planet #%d", origin)
|
|
}
|
|
p2, ok := c.Planet(destination)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("destination planet #%d", destination)
|
|
}
|
|
rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X, p1.Y, p2.X, p2.Y)
|
|
if rangeToDestination > c.g.Race[ri].FlightDistance() {
|
|
return e.NewSendUnreachableDestinationError("range=%.03f max=%.03f", rangeToDestination, c.g.Race[ri].FlightDistance())
|
|
}
|
|
|
|
c.SetPlanetRoute(rt, origin, destination)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) RemoveRoute(raceName, loadType string, origin uint) error {
|
|
ri, err := c.Cache.raceIndex(raceName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rt, ok := game.RouteTypeSet[loadType]
|
|
if !ok {
|
|
return e.NewCargoTypeInvalidError(loadType)
|
|
}
|
|
return c.Cache.RemoveRoute(ri, rt, origin)
|
|
}
|
|
|
|
func (c *Cache) RemoveRoute(ri int, rt game.RouteType, origin uint) error {
|
|
c.validateRaceIndex(ri)
|
|
p1, ok := c.Planet(origin)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("origin planet #%d", origin)
|
|
}
|
|
if p1.Owner != c.g.Race[ri].ID {
|
|
return e.NewEntityNotOwnedError("planet #%d", origin)
|
|
}
|
|
|
|
c.RemovePlanetRoute(rt, origin)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) SetPlanetRoute(rt game.RouteType, origin, destination uint) {
|
|
pi := c.MustPlanetIndex(origin)
|
|
if c.g.Map.Planet[pi].Route == nil {
|
|
c.g.Map.Planet[pi].Route = make(map[game.RouteType]uint)
|
|
}
|
|
c.g.Map.Planet[pi].Route[rt] = destination
|
|
}
|
|
|
|
func (c *Cache) RemovePlanetRoute(rt game.RouteType, origin uint) {
|
|
pi := c.MustPlanetIndex(origin)
|
|
if c.g.Map.Planet[pi].Route != nil {
|
|
delete(c.g.Map.Planet[pi].Route, rt)
|
|
}
|
|
}
|
|
|
|
// TODO: NOT IN THIS FUNC: remove routes if planet became uninhabited (bombing, quit game, etc)
|
|
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.listRoutedSendGroupIds(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) listRoutedSendGroupIds(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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Невозможно лишь выгрузить колонистов на чужой планете.
|
|
func (c *Cache) TurnUnloadEnroutedGroups() {
|
|
for i := range c.g.Map.Planet {
|
|
p := &c.g.Map.Planet[i]
|
|
c.doUnload(c.unloadRoutedColonists(p.Number, c.listRoutedUnloadShipGroupIds(p.Number, game.RouteColonist)))
|
|
for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} {
|
|
c.doUnload(c.listRoutedUnloadShipGroupIds(p.Number, rt))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) doUnload(groups iter.Seq[int]) {
|
|
for sgi := range groups {
|
|
c.unsafeUnloadCargo(sgi, c.ShipGroup(sgi).Load)
|
|
}
|
|
}
|
|
|
|
func (c *Cache) unloadRoutedColonists(pn uint, groups iter.Seq[int]) iter.Seq[int] {
|
|
p := c.MustPlanet(pn)
|
|
gr := slices.Collect(groups)
|
|
if p.Owner == uuid.Nil {
|
|
return c.selectColUnloadGroup(gr)
|
|
}
|
|
return func(yield func(int) bool) {
|
|
for _, sgi := range gr {
|
|
sg := c.ShipGroup(sgi)
|
|
if p.Owner != sg.OwnerID {
|
|
continue
|
|
}
|
|
if !yield(sgi) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func (c *Cache) selectColUnloadGroup(groups []int) (result iter.Seq[int]) {
|
|
groupByRace := make(map[int][]int)
|
|
loadByRace := make(map[int]float64)
|
|
for _, i := range groups {
|
|
sg := c.ShipGroup(i)
|
|
ri := c.RaceIndex(sg.OwnerID)
|
|
groupByRace[ri] = append(groupByRace[ri], i)
|
|
loadByRace[ri] += sg.Load
|
|
}
|
|
if len(loadByRace) < 2 {
|
|
// only one race has to unload cargo
|
|
result = slices.Values(groups)
|
|
return
|
|
}
|
|
|
|
// select winner to unload cargo
|
|
id := MaxOrRandomLoadId(loadByRace)
|
|
result = slices.Values(groupByRace[id])
|
|
|
|
return
|
|
}
|
|
|
|
func MaxOrRandomLoadId(IDtoLoad map[int]float64) int {
|
|
if len(IDtoLoad) < 2 {
|
|
panic("IDtoLoad must contain at least 2 keys")
|
|
}
|
|
IDs := slices.Collect(maps.Keys(IDtoLoad))
|
|
slices.SortFunc(IDs, func(id1, id2 int) int { return cmp.Compare(IDtoLoad[id2], IDtoLoad[id1]) })
|
|
|
|
// no single winner with highest load
|
|
if IDtoLoad[IDs[0]] == IDtoLoad[IDs[1]] {
|
|
// remove IDs which load less than maximum
|
|
IDs = slices.DeleteFunc(IDs, func(v int) bool { return IDtoLoad[v] < IDtoLoad[IDs[0]] })
|
|
// IDs[0] will have random index
|
|
rand.Shuffle(len(IDs), func(i, j int) { IDs[i], IDs[j] = IDs[j], IDs[i] })
|
|
}
|
|
return IDs[0]
|
|
}
|
|
|
|
func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] {
|
|
return func(yield func(int) bool) {
|
|
yielded := make(map[int]bool)
|
|
for i := range c.g.Map.Planet {
|
|
for rt, dest := range c.g.Map.Planet[i].Route {
|
|
if dest != pn || rt != routeType {
|
|
continue
|
|
}
|
|
for i := range c.ShipGroupsIndex() {
|
|
sg := c.ShipGroup(i)
|
|
if _, ok := yielded[i]; ok || sg.FleetID != nil || sg.CargoType == nil || sg.Load == 0. || sg.State() != game.StateInOrbit || sg.Destination != dest {
|
|
continue
|
|
}
|
|
if v, ok := game.RouteToCargo[rt]; !ok || v != *sg.CargoType {
|
|
continue
|
|
}
|
|
if !yield(i) {
|
|
return
|
|
}
|
|
yielded[i] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|