package game import ( "fmt" "iter" "maps" "math" "slices" "github.com/google/uuid" e "github.com/iliadenisov/galaxy/internal/error" "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"` } 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) } 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) 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 } /* TODO: multi-command processing: Game data should NOT be changed when error is returned */ // Корабль может нести только один тип груза одновременно. // Возможные типы груза - это колонисты, сырье и промышленность. // Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется. 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) 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) 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 }