package game import ( "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 ( // CargoNone CargoType = "-" CargoColonist CargoType = "COL" // Колонисты CargoMaterial CargoType = "MAT" // Сырьё CargoCapital CargoType = "CAP" // Промышленность ) func (ct CargoType) Ref() *CargoType { return &ct } 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 State string `json:"state"` // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade CargoType *CargoType `json:"loadType,omitempty"` Load float64 `json:"load"` // Cargo loaded - "Масса груза" Drive float64 `json:"drive"` Weapons float64 `json:"weapons"` Shields float64 `json:"shields"` Cargo float64 `json:"cargo"` // TODO: TEST: Destination, Origin, Range Destination uint `json:"destination"` Origin *uint `json:"origin,omitempty"` Range *float64 `json:"range,omitempty"` } func (sg ShipGroup) Equal(other ShipGroup) bool { return sg.OwnerID == other.OwnerID && sg.TypeID == other.TypeID && sg.FleetID == other.FleetID && sg.Drive == other.Drive && sg.Weapons == other.Weapons && sg.Shields == other.Shields && sg.Cargo == other.Cargo && sg.CargoType == other.CargoType && sg.Load == other.Load && sg.State == other.State } // Грузоподъёмность func (sg ShipGroup) CargoCapacity(st *ShipType) float64 { return sg.Cargo * (st.Cargo + (st.Cargo*st.Cargo)/20) * float64(sg.Number) } // Масса перевозимого груза - // общее количество единиц груза, деленное на технологический уровень Грузоперевозок func (sg ShipGroup) CarryingMass() float64 { return sg.Load / sg.Cargo } // Полная масса - // массу корабля самого по себе плюс масса перевозимого груза. func (sg ShipGroup) FullMass(st *ShipType) float64 { return st.EmptyMass() + sg.CarryingMass() } // Эффективность двигателя - // равна мощности Двигателей, умноженной на технологический уровень блока Двигателей func (sg ShipGroup) DriveEffective(st *ShipType) float64 { return st.Drive * sg.Drive } // Корабли перемещаются за один ход на количество световых лет, равное // эффективности двигателя, умноженной на 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.Drive/drive) * 10 * st.Drive } // TODO: test on other values func (sg ShipGroup) UpgradeWeaponsCost(st *ShipType, weapons float64) float64 { return (1 - sg.Weapons/weapons) * 10 * st.WeaponsMass() } func (sg ShipGroup) UpgradeShieldsCost(st *ShipType, shields float64) float64 { return (1 - sg.Shields/shields) * 10 * st.Shields } func (sg ShipGroup) UpgradeCargoCost(st *ShipType, cargo float64) float64 { return (1 - sg.Cargo/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.Weapons)/10. + 1.) * st.Weapons * sg.Weapons * 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) 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.NewInputSameRaceError(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), State: g.ShipGroups[sgi].State, CargoType: g.ShipGroups[sgi].CargoType, Load: g.ShipGroups[sgi].Load, Drive: g.ShipGroups[sgi].Drive, Weapons: g.ShipGroups[sgi].Weapons, Shields: g.ShipGroups[sgi].Shields, Cargo: g.ShipGroups[sgi].Cargo, Destination: g.ShipGroups[sgi].Destination, Origin: g.ShipGroups[sgi].Origin, Range: g.ShipGroups[sgi].Range, }) 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 != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil { 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 { newGroup := g.ShipGroups[sgi] newGroup.Number = quantity g.ShipGroups[sgi].Number -= quantity newGroup.Index = maxIndex + 1 newGroup.FleetID = nil g.ShipGroups = append(g.ShipGroups, newGroup) } 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) 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), State: "In_Orbit", Drive: g.Race[ri].Drive, Weapons: g.Race[ri].Weapons, Shields: g.Race[ri].Shields, Cargo: g.Race[ri].Cargo, }) 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 maxUint(a, b uint) uint { if b > a { return b } return a }