package controller import ( "fmt" "iter" "math" "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" ) var fleetStateNil = game.ShipGroupState("-") type FleetState struct { State game.ShipGroupState Destination uint InSpace func() (game.InSpace, bool) AtPlanet func() (uint, bool) } func (fs *FleetState) inSpace() bool { _, ok := fs.InSpace() return ok } func (fs FleetState) AtSamePlanet(other FleetState) bool { pn1, ok := fs.AtPlanet() if !ok { return false } pn2, ok := other.AtPlanet() if !ok { return false } return pn1 == pn2 } func (c *Cache) FleetState(fleetID uuid.UUID) FleetState { fi := c.MustFleetIndex(fleetID) ri := c.RaceIndex(c.g.Fleets[fi].OwnerID) fs := &FleetState{ State: fleetStateNil, InSpace: func() (game.InSpace, bool) { return game.InSpace{}, false }, AtPlanet: func() (uint, bool) { return 0, false }, } for sgi := range c.FleetGroupIdx(ri, fi) { sg := c.ShipGroup(sgi) if fs.State == fleetStateNil { fs.State = sg.State() fs.Destination = sg.Destination if pn, ok := sg.AtPlanet(); ok { fs.AtPlanet = func() (uint, bool) { return pn, ok } } else if sg.StateInSpace != nil { fs.InSpace = func() (game.InSpace, bool) { return *sg.StateInSpace, true } } continue } if fs.State != sg.State() { panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different states", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) } if fs.Destination != sg.Destination { panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different destination", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) } if planet, ok := sg.AtPlanet(); ok { if onPlanet, ok := fs.AtPlanet(); ok && onPlanet != planet { panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q are on different planets: %d <> %d", c.g.Race[ri].Name, c.g.Fleets[fi].Name, onPlanet, planet)) } } if (!fs.inSpace() && sg.StateInSpace != nil) || (fs.inSpace() && sg.StateInSpace == nil) { panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q on_planet and in_space at the same time", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) } if is, ok := fs.InSpace(); ok && sg.StateInSpace != nil && !is.Equal(*sg.StateInSpace) { panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different is_space states", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) } } if fs.State == fleetStateNil { panic(fmt.Sprintf("FleetState: race's %q fleet %q has no ships", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) } return *fs } func (c *Cache) FleetSpeedAndMass(fi int) (float64, float64) { c.validateFleetIndex(fi) speed := math.MaxFloat64 mass := 0. for sgi := range c.ShipGroupsIndex() { if c.ShipGroup(sgi).FleetID == nil || *c.ShipGroup(sgi).FleetID != c.g.Fleets[fi].ID { continue } sg := c.ShipGroup(sgi) st := c.ShipGroupShipClass(sgi) typeSpeed := sg.Speed(st) if typeSpeed < speed { speed = typeSpeed } mass += sg.FullMass(st) } return speed, mass } func (c *Cache) ShipGroupJoinFleet(ri int, fleetName string, groupID uuid.UUID) (err error) { c.validateRaceIndex(ri) name, ok := util.ValidateTypeName(fleetName) if !ok { return e.NewEntityTypeNameValidationError("%q", name) } sgi, ok := c.raceShipGroupIndex(ri, groupID) if !ok { return e.NewEntityNotExistsError("group %s", groupID) } if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit { return e.NewShipsBusyError("state: %s", state) } var oldFleetID *uuid.UUID if c.ShipGroup(sgi).FleetID != nil { fID := *c.ShipGroup(sgi).FleetID oldFleetID = &fID } fi, ok := c.fleetIndex(ri, name) if !ok { fi, err = c.createFleet(ri, name) if err != nil { return err } } else { fleetState := c.FleetState(c.g.Fleets[fi].ID) if onPlanet, ok := fleetState.AtPlanet(); (ok && onPlanet != c.ShipGroup(sgi).Destination) || fleetState.State != game.StateInOrbit { return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName) } } c.internalShipGroupJoinFleet(sgi, &c.g.Fleets[fi].ID) if oldFleetID != nil { keepOldFleet := false for sg := range c.listShipGroups(ri) { if sg.FleetID != nil && *sg.FleetID == *oldFleetID { keepOldFleet = true break } } if !keepOldFleet { oldFleetIndex, ok := c.FleetIndex(*oldFleetID) if !ok { return e.NewGameStateError("old fleet index not found by ID=%v", *oldFleetID) } if err := c.deleteFleet(ri, c.g.Fleets[oldFleetIndex].Name); err != nil { return err } } } return nil } func (c *Cache) fleetMerge(ri int, fleetSourceName, fleetTargetName string) (err error) { fiSource, ok := c.fleetIndex(ri, fleetSourceName) if !ok { return e.NewEntityNotExistsError("source fleet %s", fleetSourceName) } fiTarget, ok := c.fleetIndex(ri, fleetTargetName) if !ok { return e.NewEntityNotExistsError("target fleet %s", fleetTargetName) } stateSrc := c.FleetState(c.g.Fleets[fiSource].ID) stateDst := c.FleetState(c.g.Fleets[fiTarget].ID) if !stateSrc.AtSamePlanet(stateDst) { return e.NewShipsNotOnSamePlanetError() } for sg := range c.listShipGroups(ri) { if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fiSource].ID { sg.FleetID = &c.g.Fleets[fiTarget].ID } } return c.deleteFleet(ri, fleetSourceName) } func (c *Cache) createFleet(ri int, name string) (int, error) { c.validateRaceIndex(ri) n, ok := util.ValidateTypeName(name) if !ok { return 0, e.NewEntityTypeNameValidationError("%q", n) } if _, ok := c.fleetIndex(ri, n); ok { return 0, e.NewEntityDuplicateIdentifierError("fleet %q", n) } fleets := slices.Clone(c.g.Fleets) fleets = append(fleets, game.Fleet{ ID: uuid.New(), OwnerID: c.g.Race[ri].ID, Name: n, }) c.g.Fleets = fleets i := len(c.g.Fleets) - 1 if c.cacheFleetIndexByID != nil { c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i } return i, nil } func (c *Cache) deleteFleet(ri int, name string) error { fi, ok := c.fleetIndex(ri, name) if !ok { return e.NewEntityNotExistsError("fleet %s", name) } for sg := range c.listShipGroups(ri) { if sg.FleetID != nil && *(sg.FleetID) == c.g.Fleets[fi].ID { return e.NewEntityInUseError("fleet %s: race %s, group #%d", name, c.g.Race[ri].Name, sg.Number) } } c.unsafeDeleteFleet(fi) return nil } func (c *Cache) unsafeDeleteFleet(fi int) { c.validateFleetIndex(fi) c.g.Fleets = append(c.g.Fleets[:fi], c.g.Fleets[fi+1:]...) c.invalidateFleetCache() } // Internal funcs func (c *Cache) FleetIndex(ID uuid.UUID) (int, bool) { if len(c.cacheFleetIndexByID) == 0 { c.cacheFleetIndex() } if v, ok := c.cacheFleetIndexByID[ID]; ok { return v, true } else { return -1, false } } func (c *Cache) cacheFleetIndex() { if c.cacheFleetIndexByID != nil { clear(c.cacheFleetIndexByID) } else { c.cacheFleetIndexByID = make(map[uuid.UUID]int) } for i := range c.g.Fleets { c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i } } func (c *Cache) MustFleetIndex(ID uuid.UUID) int { if v, ok := c.FleetIndex(ID); ok { return v } else { panic(fmt.Sprintf("fleet not found by ID=%v", ID)) } } func (c *Cache) FleetGroupIdx(ri, fi int) iter.Seq[int] { c.validateRaceIndex(ri) c.validateFleetIndex(fi) return func(yield func(int) bool) { for sgi := range c.listShipGroupIdx(ri) { sg := c.ShipGroup(sgi) if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fi].ID { if !yield(sgi) { break } } } } } func (c *Cache) fleetGroupIds(ri, fi int) iter.Seq[int] { c.validateRaceIndex(ri) c.validateFleetIndex(fi) return func(yield func(int) bool) { for i := range c.ShipGroupsIndex() { sg := c.ShipGroup(i) if c.g.Race[ri].ID != sg.OwnerID { continue } if sg.FleetID == nil || c.MustFleetIndex(*sg.FleetID) != fi { continue } if !yield(i) { return } } } } func (c *Cache) listFleets(ri int) iter.Seq[*game.Fleet] { c.validateRaceIndex(ri) return func(yield func(*game.Fleet) bool) { for i := range c.g.Fleets { if c.g.Fleets[i].OwnerID == c.g.Race[ri].ID { if !yield(&c.g.Fleets[i]) { return } } } } } func (c *Cache) fleetIndex(ri int, name string) (int, bool) { c.validateRaceIndex(ri) if i := slices.IndexFunc(c.g.Fleets, func(f game.Fleet) bool { return f.OwnerID == c.g.Race[ri].ID && f.Name == name }); i < 0 { return -1, false } else { return i, true } } func (c *Cache) validateFleetIndex(i int) { if i >= len(c.g.Fleets) { panic(fmt.Sprintf("fleet index out of range: %d >= %d", i, len(c.g.Fleets))) } }