package controller import ( "cmp" "iter" "maps" "math" "math/rand/v2" "slices" e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" "github.com/iliadenisov/galaxy/internal/util" ) func (c *Cache) PlanetRouteSet(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.OwnedBy(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.F(), p1.Y.F(), p2.X.F(), p2.Y.F()) 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 *Cache) PlanetRouteRemove(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.OwnedBy(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) } } func (c *Cache) SendRoutedGroups() { 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 *game.Float 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(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).F() if toLoad > sgCapacity { toLoad = sgCapacity } else if maxShips := uint(math.Ceil(toLoad / (sgCapacity / float64(ships)))); maxShips < ships { newGroupIdx := c.unsafeBreakGroup(c.RaceIndex(sg.OwnerID), sgi, maxShips) sgi = newGroupIdx sg = c.ShipGroup(newGroupIdx) } // decrease planet resource *res = (*res).Add(-toLoad) // load group sg.Load = sg.Load.Add(toLoad) sg.CargoType = &ct c.LaunchShips(sgi, 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 !p.OwnedBy(sg.OwnerID) || // 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)) } p.UnpackColonists() p.UnpackCapital() } } func (c *Cache) RemoveUnreachableRoutes() { for i := range c.g.Map.Planet { p1 := &c.g.Map.Planet[i] if !p1.Owned() { continue } ri := c.RaceIndex(*p1.Owner) for rt, destination := range p1.Route { p2 := c.MustPlanet(destination) rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F()) if rangeToDestination > c.g.Race[ri].FlightDistance() { delete(p1.Route, rt) } } } } func (c *Cache) doUnload(groups iter.Seq[int]) { for sgi := range groups { c.unsafeUnloadCargo(sgi, c.ShipGroup(sgi).Load.F()) } } func (c *Cache) unloadRoutedColonists(pn uint, groups iter.Seq[int]) iter.Seq[int] { p := c.MustPlanet(pn) gr := slices.Collect(groups) if !p.Owned() { return c.selectColUnloadGroup(gr) } return func(yield func(int) bool) { for _, sgi := range gr { sg := c.ShipGroup(sgi) if !p.OwnedBy(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.F() } 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 (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 } } } } } 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] }