331 lines
8.7 KiB
Go
331 lines
8.7 KiB
Go
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 sg := range c.FleetGroups(ri, fi) {
|
|
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) JoinShipGroupToFleet(ri int, fleetName string, groupIndex, quantity uint) (err error) {
|
|
c.validateRaceIndex(ri)
|
|
name, ok := util.ValidateTypeName(fleetName)
|
|
if !ok {
|
|
return e.NewEntityTypeNameValidationError("%q", name)
|
|
}
|
|
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
|
|
if !ok {
|
|
return e.NewEntityNotExistsError("group #%d", groupIndex)
|
|
}
|
|
|
|
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
|
return e.NewShipsBusyError("state: %s", state)
|
|
}
|
|
|
|
if c.ShipGroup(sgi).Number < quantity {
|
|
return e.NewJoinFleetGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
if quantity > 0 && quantity < c.ShipGroup(sgi).Number {
|
|
nsgi, err := c.breakGroupSafe(ri, groupIndex, quantity)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sgi = nsgi
|
|
}
|
|
|
|
c.ShipGroupJoinFleet(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.deleteFleetSafe(ri, c.g.Fleets[oldFleetIndex].Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Cache) JoinFleets(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.deleteFleetSafe(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.NewEntityTypeNameDuplicateError("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) deleteFleetSafe(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) FleetGroups(ri, fi int) iter.Seq[*game.ShipGroup] {
|
|
c.validateRaceIndex(ri)
|
|
c.validateFleetIndex(fi)
|
|
return func(yield func(*game.ShipGroup) bool) {
|
|
for sg := range c.listShipGroups(ri) {
|
|
if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fi].ID {
|
|
if !yield(sg) {
|
|
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)))
|
|
}
|
|
}
|