package controller import ( "fmt" "iter" "maps" "slices" "github.com/google/uuid" e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" ) func (c *Cache) CreateShips(ri int, shipTypeName string, planetNumber uint, quantity int) error { class, _, ok := c.ShipClass(ri, shipTypeName) if !ok { return e.NewEntityNotExistsError("ship class %w", shipTypeName) } p, ok := c.Planet(planetNumber) if !ok { return e.NewEntityNotExistsError("planet #%d", planetNumber) } if p.Owner != c.g.Race[ri].ID { return e.NewEntityNotOwnedError("planet #%d", planetNumber) } // FIXME: move maxindex to appendShipGroup nextIndex := c.ShipGroupMaxIndex(ri) + 1 c.appendShipGroup(ri, class, &game.ShipGroup{ Index: nextIndex, OwnerID: c.g.Race[ri].ID, TypeID: class.ID, Destination: p.Number, Number: uint(quantity), Tech: map[game.Tech]float64{ game.TechDrive: c.g.Race[ri].TechLevel(game.TechDrive), game.TechWeapons: c.g.Race[ri].TechLevel(game.TechWeapons), game.TechShields: c.g.Race[ri].TechLevel(game.TechShields), game.TechCargo: c.g.Race[ri].TechLevel(game.TechCargo), }, }) return nil } // ShipGroup is a proxy func, nothing to cache func (c *Cache) ShipGroup(groupIndex int) *game.ShipGroup { c.validateShipGroupIndex(groupIndex) return &c.g.ShipGroups[groupIndex] } func (c *Cache) ShipGroupFleet(groupIndex int, fID *uuid.UUID) { c.validateShipGroupIndex(groupIndex) c.g.ShipGroups[groupIndex].FleetID = fID } func (c *Cache) ShipGroupShipsNumber(groupIndex int, number uint) { c.validateShipGroupIndex(groupIndex) if c.g.ShipGroups[groupIndex].Number > 0 { c.g.ShipGroups[groupIndex].Load = c.g.ShipGroups[groupIndex].Load / float64(c.g.ShipGroups[groupIndex].Number) * float64(number) } c.g.ShipGroups[groupIndex].Number = number } func (c *Cache) ShipGroupsIndex() iter.Seq[int] { return func(yield func(int) bool) { for i := range c.g.ShipGroups { if !yield(i) { return } } } } func (c *Cache) ShipGroupMaxIndex(ri int) uint { var max uint = 0 for i := range c.g.ShipGroups { if r := c.ShipGroupOwnerRaceIndex(i); r == ri && c.ShipGroup(i).Index > max { max = c.ShipGroup(i).Index } } return max } func (c *Cache) ShipGroupOwnerRaceIndex(groupIndex int) int { c.validateShipGroupIndex(groupIndex) if len(c.raceIndexByShipGroupIndex) == 0 { c.cacheShipsAndGroups() } if v, ok := c.raceIndexByShipGroupIndex[groupIndex]; ok { return v } else { panic(fmt.Sprintf("ShipGroupRace: group not found by index=%v", groupIndex)) } } func (c *Cache) ShipGroupOwnerRace(groupIndex int) *game.Race { return &c.g.Race[c.ShipGroupOwnerRaceIndex(groupIndex)] } func (c *Cache) ShipGroupNumber(i int, n uint) { c.validateShipGroupIndex(i) c.g.ShipGroups[i].Number = n } func (c *Cache) DeleteShipGroup(i int) { c.validateShipGroupIndex(i) c.unsafeDeleteShipGroup(i) } func (c *Cache) DeleteKilledShipGroups() { for i := len(c.g.ShipGroups) - 1; i >= 0; i-- { if c.g.ShipGroups[i].Number == 0 { c.unsafeDeleteShipGroup(i) } } } func (c *Controller) JoinEqualGroups(raceName string) error { ri, err := c.Cache.raceIndex(raceName) if err != nil { return err } c.Cache.JoinEqualGroups(ri) return nil } func (c *Cache) CmdJoinEqualGroups() { for i := range c.g.Race { c.JoinEqualGroups(i) } } func (c *Cache) JoinEqualGroups(ri int) { c.validateRaceIndex(ri) shipGroups := slices.Collect(c.listShipGroups(ri)) origin := len(shipGroups) if origin < 2 { return } for i := 0; i < len(shipGroups)-1; i++ { for j := len(shipGroups) - 1; j > i; j-- { if shipGroups[i].Equal(*shipGroups[j]) { shipGroups[i].Index = maxUint(shipGroups[i].Index, shipGroups[j].Index) shipGroups[i].Number += shipGroups[j].Number shipGroups = append(shipGroups[:j], shipGroups[j+1:]...) } } } if len(shipGroups) == origin { return } toDelete := make([]int, 0) for i := range c.ShipGroupsIndex() { if c.ShipGroup(i).OwnerID == c.g.Race[ri].ID { toDelete = append(toDelete, i) } } // c.g.ShipGroups = slices.DeleteFunc(c.g.ShipGroups, func(v game.ShipGroup) bool { return v.OwnerID == c.g.Race[ri].ID }) for _, idx := range toDelete { c.unsafeDeleteShipGroup(idx) } // c.g.ShipGroups = append(c.g.ShipGroups, shipGroups...) for i := range shipGroups { c.g.ShipGroups = append(c.g.ShipGroups, *shipGroups[i]) } c.invalidateShipGroupCache() } func (c *Controller) BreakGroup(raceName string, groupIndex, quantity uint) error { ri, err := c.Cache.raceIndex(raceName) if err != nil { return err } return c.Cache.BreakGroup(ri, groupIndex, quantity) } func (c *Controller) DisassembleGroup(raceName string, groupIndex, quantity uint) error { ri, err := c.Cache.raceIndex(raceName) if err != nil { return err } return c.Cache.DisassembleGroup(ri, groupIndex, quantity) } func (c *Cache) DisassembleGroup(ri int, groupIndex, quantity uint) error { sgi, ok := c.raceShipGroupIndex(ri, groupIndex) if !ok { return e.NewEntityNotExistsError("group #%d", groupIndex) } if c.ShipGroup(sgi).State() != game.StateInOrbit { return e.NewShipsBusyError() } if c.ShipGroup(sgi).Number < quantity { return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) } p, ok := c.Planet(c.ShipGroup(sgi).Destination) if !ok { return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) } // pl := slices.IndexFunc(g.Map.Planet, func(p game.Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) // if pl < 0 { // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) // } st := c.ShipGroupShipClass(sgi) // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) // } if quantity > 0 && quantity < c.ShipGroup(sgi).Number { // make new group for disassembly nsgi, err := c.breakGroupSafe(ri, groupIndex, quantity) if err != nil { return err } sgi = nsgi } if c.ShipGroup(sgi).CargoType != nil { ct := *c.ShipGroup(sgi).CargoType load := c.ShipGroup(sgi).Load switch ct { case game.CargoColonist: if p.Owner == c.g.Race[ri].ID { pn := UnloadColonists(*p, load) p = &pn } case game.CargoMaterial: p.Material += load case game.CargoCapital: p.Capital += load } } p.Material += c.ShipGroup(sgi).EmptyMass(st) // g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) c.unsafeDeleteShipGroup(sgi) return nil } func (c *Controller) LoadCargo(raceName string, groupIndex uint, cargoType string, ships uint, quantity float64) error { ri, err := c.Cache.raceIndex(raceName) if err != nil { return err } ct, ok := game.CargoTypeSet[cargoType] if !ok { return e.NewCargoTypeInvalidError(cargoType) } return c.Cache.LoadCargo(ri, groupIndex, ct, ships, quantity) } // Корабль может нести только один тип груза одновременно. // Возможные типы груза - это колонисты, сырье и промышленность. // Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется. func (c *Cache) LoadCargo(ri int, groupIndex uint, ct game.CargoType, ships uint, quantity float64) error { if ships == 0 && quantity > 0 { return e.NewCargoQuantityWithoutGroupBreakError() } sgi, ok := c.raceShipGroupIndex(ri, groupIndex) if !ok { return e.NewEntityNotExistsError("group #%d", groupIndex) } if c.ShipGroup(sgi).State() != game.StateInOrbit { return e.NewShipsBusyError() } p, ok := c.Planet(c.ShipGroup(sgi).Destination) if !ok { return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) } // pl := slices.IndexFunc(g.Map.Planet, func(p game.Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) // if pl < 0 { // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) // } if p.Owner != uuid.Nil && p.Owner != c.g.Race[ri].ID { return e.NewEntityNotOwnedError("planet #%d", p.Number) } st := c.ShipGroupShipClass(sgi) // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) // } if st.Cargo < 1 { return e.NewNoCargoBayError("ship_type %q", st.Name) } if c.ShipGroup(sgi).CargoType != nil && *c.ShipGroup(sgi).CargoType != ct { return e.NewCargoLoadNotEqualError("cargo: %v", *c.ShipGroup(sgi).CargoType) } if ships > 0 && ships < c.ShipGroup(sgi).Number { nsgi, err := c.breakGroupSafe(ri, groupIndex, ships) if err != nil { return err } sgi = nsgi } capacity := c.ShipGroup(sgi).CargoCapacity(st) freeShipGroupCargoLoad := capacity - c.ShipGroup(sgi).Load if freeShipGroupCargoLoad == 0 { return e.NewCargoLoadNoSpaceLeftError() } var availableOnPlanet *float64 switch ct { case game.CargoMaterial: availableOnPlanet = &p.Material case game.CargoCapital: availableOnPlanet = &p.Capital case game.CargoColonist: availableOnPlanet = &p.Colonists default: return e.NewGameStateError("CargoType not accepted: %v", ct) } if quantity > *availableOnPlanet || *availableOnPlanet == 0 { return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", p.Number, ct, *availableOnPlanet) } toBeLoaded := quantity if quantity == 0 { toBeLoaded = *availableOnPlanet } if toBeLoaded > freeShipGroupCargoLoad { toBeLoaded = freeShipGroupCargoLoad } *availableOnPlanet = *availableOnPlanet - toBeLoaded c.ShipGroup(sgi).Load += toBeLoaded if c.ShipGroup(sgi).Load > 0 { c.ShipGroup(sgi).CargoType = &ct } return nil } func (c *Controller) UnloadCargo(raceName string, groupIndex uint, ships uint, quantity float64) error { ri, err := c.Cache.raceIndex(raceName) if err != nil { return err } return c.Cache.UnloadCargo(ri, groupIndex, ships, quantity) } // Промышленность и Сырье могут быть выгружены на любой планете. // Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты. func (c *Cache) UnloadCargo(ri int, groupIndex uint, ships uint, quantity float64) error { c.validateRaceIndex(ri) if ships == 0 && quantity > 0 { return e.NewCargoQuantityWithoutGroupBreakError() } sgi, ok := c.raceShipGroupIndex(ri, groupIndex) if !ok { return e.NewEntityNotExistsError("group #%d", groupIndex) } if c.ShipGroup(sgi).State() != game.StateInOrbit { return e.NewShipsBusyError() } st := c.ShipGroupShipClass(sgi) // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) // } if st.Cargo < 1 { return e.NewNoCargoBayError("ship_type %q", st.Name) } if c.ShipGroup(sgi).CargoType == nil || c.ShipGroup(sgi).Load == 0 { return e.NewCargoUnloadEmptyError() } ct := *c.ShipGroup(sgi).CargoType p, ok := c.Planet(c.ShipGroup(sgi).Destination) if !ok { return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) } // pl := slices.IndexFunc(g.Map.Planet, func(p game.Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) // if pl < 0 { // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) // } if ct == game.CargoColonist { if p.Owner != uuid.Nil && p.Owner != c.g.Race[ri].ID { return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct) } if p.Owner == uuid.Nil { p.Owner = c.g.Race[ri].ID } } var availableOnPlanet *float64 switch ct { case game.CargoMaterial: availableOnPlanet = &p.Material case game.CargoCapital: availableOnPlanet = &p.Capital case game.CargoColonist: availableOnPlanet = &p.Colonists default: return e.NewGameStateError("CargoType not accepted: %v", ct) } if ships > 0 && ships < c.ShipGroup(sgi).Number { nsgi, err := c.breakGroupSafe(ri, groupIndex, ships) if err != nil { return err } sgi = nsgi } toBeUnloaded := quantity if quantity == 0 { toBeUnloaded = c.ShipGroup(sgi).Load } if toBeUnloaded > c.ShipGroup(sgi).Load { return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load) } *availableOnPlanet += toBeUnloaded c.ShipGroup(sgi).Load -= toBeUnloaded if c.ShipGroup(sgi).Load == 0 { c.ShipGroup(sgi).CargoType = nil } return nil } func (c *Controller) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { ri, err := c.Cache.raceIndex(raceName) if err != nil { return err } riAccept, err := c.Cache.raceIndex(raceAcceptor) if err != nil { return err } return c.Cache.GiveawayGroup(ri, riAccept, groupIndex, quantity) } func (c *Cache) GiveawayGroup(ri, riAccept int, groupIndex, quantity uint) (err error) { if ri == riAccept { return e.NewSameRaceError(c.g.Race[riAccept].Name) } sgi, ok := c.raceShipGroupIndex(ri, groupIndex) if !ok { return e.NewEntityNotExistsError("group #%d", groupIndex) } if c.ShipGroup(sgi).Number < quantity { return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) } st := c.ShipGroupShipClass(sgi) // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) // } var stAcc int if stAcc = slices.IndexFunc(c.g.Race[riAccept].ShipTypes, func(v game.ShipType) bool { return v.Name == st.Name }); stAcc >= 0 && !st.Equal(c.g.Race[riAccept].ShipTypes[stAcc]) { return e.NewGiveawayGroupShipsTypeNotEqualError("race %w, ship type %w", c.g.Race[riAccept].Name, c.g.Race[riAccept].ShipTypes[stAcc].Name) } if stAcc < 0 { err = c.CreateShipType(riAccept, st.Name, st.Drive, int(st.Armament), st.Weapons, st.Shields, st.Cargo) stAcc = len(c.g.Race[ri].ShipTypes) - 1 if err != nil { return err } } // maxIndex := c.ShipGroupMaxIndex(riAccept) // var maxIndex uint // for sg := range g.listShipGroups(riAccept) { // if sg.Index > maxIndex { // maxIndex = sg.Index // } // } c.appendShipGroup(ri, st, &game.ShipGroup{ // Index: maxIndex + 1, OwnerID: c.g.Race[riAccept].ID, TypeID: c.g.Race[riAccept].ShipTypes[stAcc].ID, Number: uint(quantity), CargoType: c.ShipGroup(sgi).CargoType, Load: c.ShipGroup(sgi).Load, Tech: maps.Clone(c.ShipGroup(sgi).Tech), Destination: c.ShipGroup(sgi).Destination, StateInSpace: c.ShipGroup(sgi).StateInSpace, StateUpgrade: c.ShipGroup(sgi).StateUpgrade, }) // g.ShipGroups = append(g.ShipGroups, game.ShipGroup{ // Index: maxIndex + 1, // OwnerID: g.Race[riAccept].ID, // TypeID: g.Race[riAccept].ShipTypes[stAcc].ID, // Number: uint(quantity), // CargoType: c.ShipGroup(sgi).CargoType, // Load: c.ShipGroup(sgi).Load, // Tech: maps.Clone(c.ShipGroup(sgi).Tech), // Destination: c.ShipGroup(sgi).Destination, // StateInSpace: c.ShipGroup(sgi).StateInSpace, // StateUpgrade: c.ShipGroup(sgi).StateUpgrade, // }) if quantity == 0 || quantity == c.ShipGroup(sgi).Number { c.unsafeDeleteShipGroup(sgi) // g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) } else { c.ShipGroup(sgi).Number -= quantity } return nil } func (c *Cache) BreakGroup(ri int, groupIndex, quantity uint) error { c.validateRaceIndex(ri) sgi := -1 for i := range c.ShipGroupsIndex() { if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).Index == groupIndex { sgi = i break } } if sgi < 0 { return e.NewEntityNotExistsError("group #%d", groupIndex) } if c.ShipGroup(sgi).State() != game.StateInOrbit { return e.NewShipsBusyError() } if c.ShipGroup(sgi).Number < quantity { return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) } if quantity == 0 || quantity == c.ShipGroup(sgi).Number { c.ShipGroupFleet(sgi, nil) } else { if _, err := c.breakGroupSafe(ri, groupIndex, quantity); err != nil { return err } } return nil } func (c *Cache) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int, error) { c.validateRaceIndex(ri) sgi, ok := c.raceShipGroupIndex(ri, groupIndex) if !ok { return -1, e.NewEntityNotExistsError("group #%d", groupIndex) } // maxIndex := c.ShipGroupMaxIndex(ri) if c.ShipGroup(sgi).Number < newGroupShips { return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", c.ShipGroup(sgi).Index, c.ShipGroup(sgi).Number, newGroupShips) } newGroup := *c.ShipGroup(sgi) if c.ShipGroup(sgi).CargoType != nil { newGroup.Load = c.ShipGroup(sgi).Load / float64(c.ShipGroup(sgi).Number) * float64(newGroupShips) // c.ShipGroup(sgi).Load -= newGroup.Load } newGroup.Number = newGroupShips c.ShipGroupShipsNumber(sgi, c.ShipGroup(sgi).Number-newGroup.Number) // c.ShipGroup(sgi).Number -= newGroup.Number // newGroup.Index = maxIndex + 1 newGroup.FleetID = nil st := c.ShipGroupShipClass(sgi) // c.g.ShipGroups = append(c.g.ShipGroups, newGroup) return c.appendShipGroup(ri, st, &newGroup), nil } // Internal funcs func (c *Cache) appendShipGroup(ri int, class *game.ShipType, sg *game.ShipGroup) int { c.validateRaceIndex(ri) sg.Index = c.ShipGroupMaxIndex(ri) + 1 c.g.ShipGroups = append(c.g.ShipGroups, *sg) i := len(c.g.ShipGroups) - 1 c.cacheShipGroup(i, ri, class) return i } func (c *Cache) raceShipGroupIndex(ri int, index uint) (int, bool) { c.validateRaceIndex(ri) for i := range c.ShipGroupsIndex() { if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).Index == index { return i, true } } return -1, false } func (c *Cache) listShipGroups(ri int) iter.Seq[*game.ShipGroup] { c.validateRaceIndex(ri) return func(yield func(*game.ShipGroup) bool) { for i := range c.g.ShipGroups { if ri == c.ShipGroupOwnerRaceIndex(i) { if !yield(&c.g.ShipGroups[i]) { return } } } } } func (c *Cache) shipGroupsInUpgrade(planetNumber uint) iter.Seq[*game.ShipGroup] { return func(yield func(*game.ShipGroup) bool) { for sg := range c.g.ShipGroups { if c.g.ShipGroups[sg].Destination == planetNumber && c.g.ShipGroups[sg].State() == game.StateUpgrade { if !yield(&c.g.ShipGroups[sg]) { break } } } } } func (c *Cache) unsafeDeleteShipGroup(i int) { c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...) delete(c.raceIndexByShipGroupIndex, i) delete(c.shipClassByShipGroupIndex, i) } func (c *Cache) validateShipGroupIndex(i int) { if i >= len(c.g.ShipGroups) { panic(fmt.Sprintf("group index out of range: %d >= %d", i, len(c.g.ShipGroups))) } }