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", rangeToDestination) } 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.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 } } } } // Невозможно лишь выгрузить колонистов на чужой планете. func (c *Cache) TurnUnloadEnroutedGroups() { for pi := range c.g.Map.Planet { p := &c.g.Map.Planet[pi] colGroups := c.listUnloadEligibleShipGroupIds(p.Number, game.RouteColonist) if p.Owner == uuid.Nil { c.selectColUnloadGroup(colGroups) } else { for sgi := range colGroups { sg := c.ShipGroup(sgi) if sg.OwnerID != p.Owner { continue } c.unloadCargoUnsafe(sgi, sg.Load) } } for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} { for sgi := range c.listUnloadEligibleShipGroupIds(p.Number, rt) { c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load) } } } } func (c *Cache) selectColUnloadGroup(seq iter.Seq[int]) { groupByRace := make(map[int][]int) loadByRace := make(map[int]float64) for i := range seq { sg := c.ShipGroup(i) ri := c.RaceIndex(sg.OwnerID) groupByRace[ri] = append(groupByRace[ri], i) loadByRace[ri] += sg.Load } if len(loadByRace) < 2 { for _, gr := range groupByRace { for _, sgi := range gr { c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load) } } return } // select winner to unload raceIdx := slices.Collect(maps.Keys(loadByRace)) slices.SortFunc(raceIdx, func(ri1, ri2 int) int { return cmp.Compare(loadByRace[ri2], loadByRace[ri1]) }) if loadByRace[raceIdx[0]] == loadByRace[raceIdx[1]] { // no single winner with highest load raceIdx = slices.DeleteFunc(raceIdx, func(v int) bool { return loadByRace[v] < loadByRace[raceIdx[0]] }) rand.Shuffle(len(raceIdx), func(i, j int) { raceIdx[i], raceIdx[j] = raceIdx[j], raceIdx[i] }) // now raceIdx[0] has a random race index } for _, sgi := range groupByRace[raceIdx[0]] { c.unloadCargoUnsafe(sgi, c.ShipGroup(sgi).Load) } } func (c *Cache) listUnloadEligibleShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] { return func(yield func(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 sg.FleetID != nil || sg.State() != game.StateInOrbit || sg.CargoType == nil { continue } if !yield(i) { return } } } } } }