package game import ( "fmt" "math" "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/number" ) type CargoType string const ( CargoColonist CargoType = "COL" // Колонисты CargoMaterial CargoType = "MAT" // Сырьё CargoCapital CargoType = "CAP" // Промышленность ) var ( CargoTypeSet map[string]CargoType = map[string]CargoType{ CargoColonist.String(): CargoColonist, CargoMaterial.String(): CargoMaterial, CargoCapital.String(): CargoCapital, } ) func (ct CargoType) Ref() *CargoType { return &ct } func (ct CargoType) String() string { return string(ct) } type ShipGroupState string const ( StateInOrbit ShipGroupState = "In_Orbit" StateLaunched ShipGroupState = "Launched" StateInSpace ShipGroupState = "In_Space" StateUpgrade ShipGroupState = "Upgrade" StateTransfer ShipGroupState = "Transfer_Status" ) type InSpace struct { Origin uint `json:"origin"` X float64 `json:"x"` Y float64 `json:"y"` // zero is for Launched status // TODO: calculate range dynamically Range float64 `json:"range"` } func (is InSpace) Equal(other InSpace) bool { return is.Origin == other.Origin && is.X == other.X && is.Y == other.Y } func (is InSpace) Launched() bool { return is.Range == 0 } type InUpgrade struct { UpgradeTech []UpgradePreference `json:"preference"` } func (iu InUpgrade) Cost() float64 { var sum float64 for i := range iu.UpgradeTech { sum += iu.UpgradeTech[i].Cost } return sum } func (iu InUpgrade) TechCost(t Tech) float64 { for i := range iu.UpgradeTech { if iu.UpgradeTech[i].Tech == t { return iu.UpgradeTech[i].Cost } } return 0. } type UpgradePreference struct { Tech Tech `json:"tech"` Level float64 `json:"level"` Cost float64 `json:"cost"` } type Tech string const ( TechAll Tech = "ALL" TechDrive Tech = "DRIVE" TechWeapons Tech = "WEAPONS" TechShields Tech = "SHIELDS" TechCargo Tech = "CARGO" ) func (t Tech) String() string { return string(t) } type ShipGroup struct { Index uint `json:"index"` // FIXME: use UUID for Group Index (ordered) OwnerID uuid.UUID `json:"ownerId"` // Race link TypeID uuid.UUID `json:"typeId"` // ShipType link FleetID *uuid.UUID `json:"fleetId,omitempty"` // Fleet link Number uint `json:"number"` // Number (quantity) ships of specific ShipType CargoType *CargoType `json:"loadType,omitempty"` Load float64 `json:"load"` // Cargo loaded - "Масса груза" Tech TechSet `json:"tech"` // TODO: TEST: Destination, Origin, Range Destination uint `json:"destination"` StateInSpace *InSpace `json:"stateInSpace,omitempty"` StateUpgrade *InUpgrade `json:"stateUpgrade,omitempty"` } func (sg ShipGroup) TechLevel(t Tech) float64 { return sg.Tech.Value(t) } // TODO: refactor to separate method with *ShipGroup as parameter func (sg *ShipGroup) SetTechLevel(t Tech, v float64) { sg.Tech = sg.Tech.Set(t, v) } func (sg ShipGroup) State() ShipGroupState { switch { case sg.StateInSpace == nil && sg.StateUpgrade == nil: return StateInOrbit case sg.StateInSpace != nil && sg.StateUpgrade == nil: if sg.StateInSpace.Range > 0 { return StateInSpace } return StateLaunched case sg.StateUpgrade != nil && sg.StateInSpace == nil: return StateUpgrade default: panic(fmt.Sprintf("ambigous group state: in_space=%#v upgrage=%#v", sg.StateInSpace, sg.StateUpgrade)) } } func (sg ShipGroup) OnPlanet() (uint, bool) { switch sg.State() { case StateInOrbit: return sg.Destination, true case StateLaunched: return sg.StateInSpace.Origin, true default: return 0, false } } func (sg ShipGroup) Equal(other ShipGroup) bool { return sg.OwnerID == other.OwnerID && sg.TypeID == other.TypeID && sg.FleetID == other.FleetID && sg.TechLevel(TechDrive) == other.TechLevel(TechDrive) && sg.TechLevel(TechWeapons) == other.TechLevel(TechWeapons) && sg.TechLevel(TechShields) == other.TechLevel(TechShields) && sg.TechLevel(TechCargo) == other.TechLevel(TechCargo) && sg.CargoType == other.CargoType && sg.Load/float64(sg.Number) == other.Load/float64(other.Number) && sg.State() == other.State() } // Грузоподъёмность func (sg ShipGroup) CargoCapacity(st *ShipType) float64 { return sg.TechLevel(TechCargo) * (st.Cargo + (st.Cargo*st.Cargo)/20) * float64(sg.Number) } // Масса перевозимого груза - // общее количество единиц груза, деленное на технологический уровень Грузоперевозок func (sg ShipGroup) CarryingMass() float64 { return sg.Load / sg.TechLevel(TechCargo) } // Масса группы без учёта груза func (sg ShipGroup) EmptyMass(st *ShipType) float64 { return st.EmptyMass() * float64(sg.Number) } // Полная масса - // массу корабля самого по себе плюс масса перевозимого груза func (sg ShipGroup) FullMass(st *ShipType) float64 { return sg.EmptyMass(st) + sg.CarryingMass() } // Эффективность двигателя - // равна мощности Двигателей, умноженной на технологический уровень блока Двигателей func (sg ShipGroup) DriveEffective(st *ShipType) float64 { return st.Drive * sg.TechLevel(TechDrive) } // Корабли перемещаются за один ход на количество световых лет, равное // эффективности двигателя, умноженной на 20 и деленной на "Полную массу" корабля func (sg ShipGroup) Speed(st *ShipType) float64 { return sg.DriveEffective(st) * 20 / sg.FullMass(st) } func (sg ShipGroup) UpgradeDriveCost(st *ShipType, drive float64) float64 { return (1 - sg.TechLevel(TechDrive)/drive) * 10 * st.Drive } // TODO: test on other values func (sg ShipGroup) UpgradeWeaponsCost(st *ShipType, weapons float64) float64 { return (1 - sg.TechLevel(TechWeapons)/weapons) * 10 * st.WeaponsBlockMass() } func (sg ShipGroup) UpgradeShieldsCost(st *ShipType, shields float64) float64 { return (1 - sg.TechLevel(TechShields)/shields) * 10 * st.Shields } func (sg ShipGroup) UpgradeCargoCost(st *ShipType, cargo float64) float64 { return (1 - sg.TechLevel(TechCargo)/cargo) * 10 * st.Cargo } // Мощность бомбардировки // TODO: maybe rounding must be done only for display? func (sg ShipGroup) BombingPower(st *ShipType) float64 { // return math.Sqrt(sg.Type.Weapons * sg.Weapons) result := (math.Sqrt(st.Weapons*sg.TechLevel(TechWeapons))/10. + 1.) * st.Weapons * sg.TechLevel(TechWeapons) * float64(st.Armament) * float64(sg.Number) return number.Fixed3(result) } // JoinEqualGroups iterates over all races and joins their respective equal ship groups. // Used in turn production. // func JoinEqualGroups(g *Game) { // // for i := range g.Race { // // g.joinEqualGroupsInternal(i) // // } // } // func (g *Game) JoinEqualGroups(raceName string) error { // ri, err := g.raceIndex(raceName) // if err != nil { // return err // } // g.joinEqualGroupsInternal(ri) // return nil // } // func (g *Game) joinEqualGroupsInternal(ri int) { // shipGroups := slices.Collect(maps.Values(maps.Collect(g.listIndexShipGroups(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 // } // g.ShipGroups = slices.DeleteFunc(g.ShipGroups, func(v ShipGroup) bool { return v.OwnerID == g.Race[ri].ID }) // g.ShipGroups = append(g.ShipGroups, shipGroups...) // } // func (g *Game) BreakGroup(raceName string, groupIndex, quantity uint) error { // ri, err := g.raceIndex(raceName) // if err != nil { // return err // } // return g.breakGroupInternal(ri, groupIndex, quantity) // } // func (g *Game) DisassembleGroup(raceName string, groupIndex, quantity uint) error { // ri, err := g.raceIndex(raceName) // if err != nil { // return err // } // return g.disassembleGroupInternal(ri, groupIndex, quantity) // } // func (g *Game) disassembleGroupInternal(ri int, groupIndex, quantity uint) error { // sgi := -1 // var maxIndex uint // for i, sg := range g.listIndexShipGroups(ri) { // if sgi < 0 && sg.Index == groupIndex { // sgi = i // } // if sg.Index > maxIndex { // maxIndex = sg.Index // } // } // if sgi < 0 { // return e.NewEntityNotExistsError("group #%d", groupIndex) // } // if g.ShipGroups[sgi].State() != StateInOrbit { // return e.NewShipsBusyError() // } // if g.ShipGroups[sgi].Number < quantity { // return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) // } // pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == g.ShipGroups[sgi].Destination }) // if pl < 0 { // return e.NewGameStateError("planet #%d", g.ShipGroups[sgi].Destination) // } // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == g.ShipGroups[sgi].TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", g.ShipGroups[sgi].TypeID) // } // if quantity > 0 && quantity < g.ShipGroups[sgi].Number { // // make new group for disassembly // nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) // if err != nil { // return err // } // sgi = nsgi // } // if g.ShipGroups[sgi].CargoType != nil { // ct := *g.ShipGroups[sgi].CargoType // load := g.ShipGroups[sgi].Load // switch ct { // case CargoColonist: // if g.Map.Planet[pl].Owner == g.Race[ri].ID { // g.Map.Planet[pl] = UnloadColonists(g.Map.Planet[pl], load) // } // case CargoMaterial: // g.Map.Planet[pl].Material += load // case CargoCapital: // g.Map.Planet[pl].Capital += load // } // } // g.Map.Planet[pl].Material += g.ShipGroups[sgi].EmptyMass(&g.Race[ri].ShipTypes[sti]) // g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) // return nil // } // func (g *Game) LoadCargo(raceName string, groupIndex uint, cargoType string, ships uint, quantity float64) error { // ri, err := g.raceIndex(raceName) // if err != nil { // return err // } // ct, ok := CargoTypeSet[cargoType] // if !ok { // return e.NewCargoTypeInvalidError(cargoType) // } // return g.loadCargoInternal(ri, groupIndex, ct, ships, quantity) // } // func (g *Game) UnloadCargo(raceName string, groupIndex uint, ships uint, quantity float64) error { // ri, err := g.raceIndex(raceName) // if err != nil { // return err // } // return g.unloadCargoInternal(ri, groupIndex, ships, quantity) // } // // Промышленность и Сырье могут быть выгружены на любой планете. // // Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты. // func (g *Game) unloadCargoInternal(ri int, groupIndex uint, ships uint, quantity float64) error { // if ships == 0 && quantity > 0 { // return e.NewCargoQuantityWithoutGroupBreakError() // } // sgi := -1 // for i, sg := range g.listIndexShipGroups(ri) { // if sgi < 0 && sg.Index == groupIndex { // sgi = i // } // } // if sgi < 0 { // return e.NewEntityNotExistsError("group #%d", groupIndex) // } // if g.ShipGroups[sgi].State() != StateInOrbit { // return e.NewShipsBusyError() // } // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == g.ShipGroups[sgi].TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", g.ShipGroups[sgi].TypeID) // } // if g.Race[ri].ShipTypes[sti].Cargo < 1 { // return e.NewNoCargoBayError("ship_type %q", g.Race[ri].ShipTypes[sti].Name) // } // if g.ShipGroups[sgi].CargoType == nil || g.ShipGroups[sgi].Load == 0 { // return e.NewCargoUnloadEmptyError() // } // ct := *g.ShipGroups[sgi].CargoType // pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == g.ShipGroups[sgi].Destination }) // if pl < 0 { // return e.NewGameStateError("planet #%d", g.ShipGroups[sgi].Destination) // } // if ct == CargoColonist { // if g.Map.Planet[pl].Owner != uuid.Nil && g.Map.Planet[pl].Owner != g.Race[ri].ID { // return e.NewEntityNotOwnedError("planet #%d unload %v", g.Map.Planet[pl].Number, ct) // } // if g.Map.Planet[pl].Owner == uuid.Nil { // g.Map.Planet[pl].Owner = g.Race[ri].ID // } // } // var availableOnPlanet *float64 // switch ct { // case CargoMaterial: // availableOnPlanet = &g.Map.Planet[pl].Material // case CargoCapital: // availableOnPlanet = &g.Map.Planet[pl].Capital // case CargoColonist: // availableOnPlanet = &g.Map.Planet[pl].Colonists // default: // return e.NewGameStateError("CargoType not accepted: %v", ct) // } // if ships > 0 && ships < g.ShipGroups[sgi].Number { // nsgi, err := g.breakGroupSafe(ri, groupIndex, ships) // if err != nil { // return err // } // sgi = nsgi // } // toBeUnloaded := quantity // if quantity == 0 { // toBeUnloaded = g.ShipGroups[sgi].Load // } // if toBeUnloaded > g.ShipGroups[sgi].Load { // return e.NewCargoUnoadNotEnoughError("load: %.03f", g.ShipGroups[sgi].Load) // } // *availableOnPlanet += toBeUnloaded // g.ShipGroups[sgi].Load -= toBeUnloaded // if g.ShipGroups[sgi].Load == 0 { // g.ShipGroups[sgi].CargoType = nil // } // return nil // } // // Корабль может нести только один тип груза одновременно. // // Возможные типы груза - это колонисты, сырье и промышленность. // // Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется. // func (g *Game) loadCargoInternal(ri int, groupIndex uint, ct CargoType, ships uint, quantity float64) error { // if ships == 0 && quantity > 0 { // return e.NewCargoQuantityWithoutGroupBreakError() // } // sgi := -1 // for i, sg := range g.listIndexShipGroups(ri) { // if sgi < 0 && sg.Index == groupIndex { // sgi = i // } // } // if sgi < 0 { // return e.NewEntityNotExistsError("group #%d", groupIndex) // } // if g.ShipGroups[sgi].State() != StateInOrbit { // return e.NewShipsBusyError() // } // pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == g.ShipGroups[sgi].Destination }) // if pl < 0 { // return e.NewGameStateError("planet #%d", g.ShipGroups[sgi].Destination) // } // if g.Map.Planet[pl].Owner != uuid.Nil && g.Map.Planet[pl].Owner != g.Race[ri].ID { // return e.NewEntityNotOwnedError("planet #%d", g.Map.Planet[pl].Number) // } // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == g.ShipGroups[sgi].TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", g.ShipGroups[sgi].TypeID) // } // if g.Race[ri].ShipTypes[sti].Cargo < 1 { // return e.NewNoCargoBayError("ship_type %q", g.Race[ri].ShipTypes[sti].Name) // } // if g.ShipGroups[sgi].CargoType != nil && *g.ShipGroups[sgi].CargoType != ct { // return e.NewCargoLoadNotEqualError("cargo: %v", *g.ShipGroups[sgi].CargoType) // } // if ships > 0 && ships < g.ShipGroups[sgi].Number { // nsgi, err := g.breakGroupSafe(ri, groupIndex, ships) // if err != nil { // return err // } // sgi = nsgi // } // capacity := g.ShipGroups[sgi].CargoCapacity(&g.Race[ri].ShipTypes[sti]) // freeShipGroupCargoLoad := capacity - g.ShipGroups[sgi].Load // if freeShipGroupCargoLoad == 0 { // return e.NewCargoLoadNoSpaceLeftError() // } // var availableOnPlanet *float64 // switch ct { // case CargoMaterial: // availableOnPlanet = &g.Map.Planet[pl].Material // case CargoCapital: // availableOnPlanet = &g.Map.Planet[pl].Capital // case CargoColonist: // availableOnPlanet = &g.Map.Planet[pl].Colonists // default: // return e.NewGameStateError("CargoType not accepted: %v", ct) // } // if quantity > *availableOnPlanet || *availableOnPlanet == 0 { // return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", g.Map.Planet[pl].Number, ct, *availableOnPlanet) // } // toBeLoaded := quantity // if quantity == 0 { // toBeLoaded = *availableOnPlanet // } // if toBeLoaded > freeShipGroupCargoLoad { // toBeLoaded = freeShipGroupCargoLoad // } // *availableOnPlanet = *availableOnPlanet - toBeLoaded // g.ShipGroups[sgi].Load += toBeLoaded // if g.ShipGroups[sgi].Load > 0 { // g.ShipGroups[sgi].CargoType = &ct // } // return nil // } // func (g *Game) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { // ri, err := g.raceIndex(raceName) // if err != nil { // return err // } // riAccept, err := g.raceIndex(raceAcceptor) // if err != nil { // return err // } // return g.giveawayGroupInternal(ri, riAccept, groupIndex, quantity) // } // func (g *Game) giveawayGroupInternal(ri, riAccept int, groupIndex, quantity uint) (err error) { // if ri == riAccept { // return e.NewSameRaceError(g.Race[riAccept].Name) // } // sgi := -1 // for i, sg := range g.listIndexShipGroups(ri) { // if sgi < 0 && sg.Index == groupIndex { // sgi = i // } // } // if sgi < 0 { // return e.NewEntityNotExistsError("group #%d", groupIndex) // } // if g.ShipGroups[sgi].Number < quantity { // return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) // } // var sti int // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == g.ShipGroups[sgi].TypeID }); sti < 0 { // // hard to test, need manual game data invalidation // return e.NewGameStateError("not found: ShipType ID=%v", g.ShipGroups[sgi].TypeID) // } // var stAcc int // if stAcc = slices.IndexFunc(g.Race[riAccept].ShipTypes, func(st ShipType) bool { return st.Name == g.Race[ri].ShipTypes[sti].Name }); stAcc >= 0 && // !g.Race[ri].ShipTypes[sti].Equal(g.Race[riAccept].ShipTypes[stAcc]) { // return e.NewGiveawayGroupShipsTypeNotEqualError("race %w, ship type %w", g.Race[riAccept].Name, g.Race[riAccept].ShipTypes[stAcc].Name) // } // if stAcc < 0 { // stAcc, err = g.createShipTypeInternal(riAccept, // g.Race[ri].ShipTypes[sti].Name, // g.Race[ri].ShipTypes[sti].Drive, // g.Race[ri].ShipTypes[sti].Weapons, // g.Race[ri].ShipTypes[sti].Shields, // g.Race[ri].ShipTypes[sti].Cargo, // int(g.Race[ri].ShipTypes[sti].Armament)) // if err != nil { // return err // } // } // var maxIndex uint // for sg := range g.listShipGroups(riAccept) { // if sg.Index > maxIndex { // maxIndex = sg.Index // } // } // g.ShipGroups = append(g.ShipGroups, ShipGroup{ // Index: maxIndex + 1, // OwnerID: g.Race[riAccept].ID, // TypeID: g.Race[riAccept].ShipTypes[stAcc].ID, // Number: uint(quantity), // CargoType: g.ShipGroups[sgi].CargoType, // Load: g.ShipGroups[sgi].Load, // Tech: maps.Clone(g.ShipGroups[sgi].Tech), // Destination: g.ShipGroups[sgi].Destination, // StateInSpace: g.ShipGroups[sgi].StateInSpace, // StateUpgrade: g.ShipGroups[sgi].StateUpgrade, // }) // if quantity == 0 || quantity == g.ShipGroups[sgi].Number { // g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) // } else { // g.ShipGroups[sgi].Number -= quantity // } // return nil // } // func (g *Game) breakGroupInternal(ri int, groupIndex, quantity uint) error { // sgi := -1 // var maxIndex uint // for i, sg := range g.listIndexShipGroups(ri) { // if sgi < 0 && sg.Index == groupIndex { // sgi = i // } // if sg.Index > maxIndex { // maxIndex = sg.Index // } // } // if sgi < 0 { // return e.NewEntityNotExistsError("group #%d", groupIndex) // } // if g.ShipGroups[sgi].State() != StateInOrbit { // return e.NewShipsBusyError() // } // if g.ShipGroups[sgi].Number < quantity { // return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) // } // if quantity == 0 || quantity == g.ShipGroups[sgi].Number { // g.ShipGroups[sgi].FleetID = nil // } else { // if _, err := g.breakGroupSafe(ri, groupIndex, quantity); err != nil { // return err // } // } // return nil // } // func (g *Game) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int, error) { // sgi := -1 // var maxIndex uint // for i, sg := range g.listIndexShipGroups(ri) { // if sgi < 0 && sg.Index == groupIndex { // sgi = i // } // if sg.Index > maxIndex { // maxIndex = sg.Index // } // } // if sgi < 0 { // return -1, e.NewEntityNotExistsError("group #%d", groupIndex) // } // if g.ShipGroups[sgi].Number < newGroupShips { // return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", g.ShipGroups[sgi].Index, g.ShipGroups[sgi].Number, newGroupShips) // } // newGroup := g.ShipGroups[sgi] // if g.ShipGroups[sgi].CargoType != nil { // newGroup.Load = g.ShipGroups[sgi].Load / float64(g.ShipGroups[sgi].Number) * float64(newGroupShips) // g.ShipGroups[sgi].Load -= newGroup.Load // } // newGroup.Number = newGroupShips // g.ShipGroups[sgi].Number -= newGroup.Number // newGroup.Index = maxIndex + 1 // newGroup.FleetID = nil // g.ShipGroups = append(g.ShipGroups, newGroup) // return len(g.ShipGroups) - 1, nil // } // func (g *Game) createShips(ri int, shipTypeName string, planetNumber int, quantity int) error { // st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == shipTypeName }) // if st < 0 { // return e.NewEntityNotExistsError("ship type %w", shipTypeName) // } // pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(planetNumber) }) // if pl < 0 { // return e.NewEntityNotExistsError("planet #%d", planetNumber) // } // if g.Map.Planet[pl].Owner != g.Race[ri].ID { // return e.NewEntityNotOwnedError("planet #%d", planetNumber) // } // var maxIndex uint // for _, sg := range g.listIndexShipGroups(ri) { // if sg.Index > maxIndex { // maxIndex = sg.Index // } // } // g.ShipGroups = append(g.ShipGroups, ShipGroup{ // Index: maxIndex + 1, // OwnerID: g.Race[ri].ID, // TypeID: g.Race[ri].ShipTypes[st].ID, // Destination: g.Map.Planet[pl].Number, // Number: uint(quantity), // Tech: map[Tech]float64{ // TechDrive: g.Race[ri].TechLevel(TechDrive), // TechWeapons: g.Race[ri].TechLevel(TechWeapons), // TechShields: g.Race[ri].TechLevel(TechShields), // TechCargo: g.Race[ri].TechLevel(TechCargo), // }, // }) // return nil // } // func (g Game) listShipGroups(ri int) iter.Seq[ShipGroup] { // return func(yield func(ShipGroup) bool) { // for _, sg := range g.listIndexShipGroups(ri) { // if !yield(sg) { // return // } // } // } // } // func (g Game) listIndexShipGroups(ri int) iter.Seq2[int, ShipGroup] { // return func(yield func(int, ShipGroup) bool) { // for i := range g.ShipGroups { // if g.ShipGroups[i].OwnerID == g.Race[ri].ID { // if !yield(i, g.ShipGroups[i]) { // return // } // } // } // } // } // func MustShipGroup(g *Game, ri int, index uint) ShipGroup { // for sg := range g.listShipGroups(ri) { // if sg.Index == index { // return sg // } // } // panic(fmt.Sprintf("race i=%d have no group i=%d", ri, index)) // } // func maxUint(a, b uint) uint { // if b > a { // return b // } // return a // }