package game import ( "encoding/json" "fmt" "maps" "math" "math/rand/v2" "slices" "github.com/google/uuid" ) type Battle struct { ID uuid.UUID Planet uint // True = In_Battle, False = Out_Battle observerGroups map[int]bool Protocol []BattleAction shipAmmo map[int]uint shipName map[int]string attacker map[int]map[int]float64 // a group able to attack and destroy an opponent with some probability > 0 } type BattleAction struct { Attacker int Defenter int Destroyed bool } func (b Battle) ShipClassName(groupIndex int) string { if v, ok := b.shipName[groupIndex]; ok { return v } else { panic(fmt.Sprintf("Battle.ShipClassName: no name stored for groupIndex=%d", groupIndex)) } } type BattleReport struct { ID uuid.UUID `json:"id"` Planet uint `json:"planet"` PlanetName string `json:"planet_name"` Races map[int]string `json:"races"` Ships map[int]string `json:"ships"` Protocol []BattleActionReport `json:"protocol"` } type BattleActionReport struct { Attacker int `json:"r1"` AttackerShipClass int `json:"s1"` Defender int `json:"r2"` DefenderShipClass int `json:"s2"` Destroyed bool `json:"d"` } type ShipClassBattle struct { ClassName string `json:"class"` Tech TechSet `json:"tech"` Number uint `json:"number"` CargoType *CargoType `json:"loadType,omitempty"` Quantity float64 `json:"quantity"` Left uint `json:"left"` } func CollectPlanetGroups(g *Game, cacheShipGroupRaceID map[int]int, cacheShipClass map[int]*ShipType) map[uint]map[int]bool { planetGroup := make(map[uint]map[int]bool) for groupIndex := range g.ShipGroups { state := g.ShipGroups[groupIndex].State() if state == StateInOrbit || state == StateUpgrade { planetNumber := g.ShipGroups[groupIndex].Destination if _, ok := planetGroup[planetNumber]; !ok { planetGroup[planetNumber] = make(map[int]bool) } planetGroup[planetNumber][groupIndex] = false if _, ok := cacheShipGroupRaceID[groupIndex]; !ok { cacheShipGroupRaceID[groupIndex] = RaceIndex(g, g.ShipGroups[groupIndex].OwnerID) } ri := cacheShipGroupRaceID[groupIndex] if _, ok := cacheShipClass[groupIndex]; !ok { sti, ok := ShipClassIndex(g, ri, g.ShipGroups[groupIndex].TypeID) if !ok { panic(fmt.Sprintf("CollectPlanetGroups: ship class not found for race=%q group=%v", g.Race[ri].Name, g.ShipGroups[groupIndex].Index)) } cacheShipClass[groupIndex] = &g.Race[ri].ShipTypes[sti] } } } for pl := range planetGroup { if len(planetGroup[pl]) < 2 { delete(planetGroup, pl) } } return planetGroup } func FilterBattleGroups(g *Game, groups map[int]bool) []int { return slices.DeleteFunc(slices.Collect(maps.Keys(groups)), func(groupIndex int) bool { return g.ShipGroups[groupIndex].State() != StateInOrbit }) } func CacheRelations(g *Game, cacheShipGroupRaceID map[int]int) map[int]map[int]Relation { cache := make(map[int]map[int]Relation) ri := make(map[int]bool) for _, raceIdx := range cacheShipGroupRaceID { ri[raceIdx] = true } for r1 := range ri { for r2 := range ri { if r1 == r2 { continue } rel, err := g.relationInternal(r1, r2) if err != nil { panic(err) } if _, ok := cache[r1]; !ok { cache[r1] = make(map[int]Relation) } cache[r1][r2] = rel.Relation } } return cache } func FilterBattleOpponents( g *Game, attIdx, defIdx int, cacheShipGroupRaceID map[int]int, cacheRelation map[int]map[int]Relation, cacheShipClass map[int]*ShipType, cacheProbability map[int]map[int]float64, ) bool { // Same Race's groups can't attack themselves if attIdx == defIdx || g.ShipGroups[attIdx].OwnerID == g.ShipGroups[defIdx].OwnerID { return true } // If any opponent has War relation to another, both will stay in battle if cacheRelation[cacheShipGroupRaceID[attIdx]][cacheShipGroupRaceID[defIdx]] == RelationPeace && cacheRelation[cacheShipGroupRaceID[defIdx]][cacheShipGroupRaceID[attIdx]] == RelationPeace { return true } p := DestructionProbability( cacheShipClass[attIdx].Weapons, g.ShipGroups[attIdx].TechLevel(TechWeapons), cacheShipClass[defIdx].Shields, g.ShipGroups[defIdx].TechLevel(TechShields), g.ShipGroups[defIdx].FullMass(cacheShipClass[defIdx]), ) // Exclude opponent's group which cannot be probably destroyed if p <= 0 { return true } if _, ok := cacheProbability[attIdx]; !ok { cacheProbability[attIdx] = make(map[int]float64) } cacheProbability[attIdx][defIdx] = p return false } func ProduceBattles(g *Game) []*Battle { cacheShipGroupRaceID := make(map[int]int) cacheShipClass := make(map[int]*ShipType) cacheProbability := make(map[int]map[int]float64) defer func() { clear(cacheShipGroupRaceID) clear(cacheShipClass) clear(cacheProbability) }() planetGroups := CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass) if len(planetGroups) == 0 { return nil } cacheRelation := CacheRelations(g, cacheShipGroupRaceID) defer func() { clear(cacheRelation) }() result := make([]*Battle, 0) for pl, observerGroups := range planetGroups { battleGroups := FilterBattleGroups(g, observerGroups) b := &Battle{ Planet: pl, observerGroups: observerGroups, attacker: make(map[int]map[int]float64), shipAmmo: make(map[int]uint), shipName: make(map[int]string), } for i := range battleGroups { attIdx := battleGroups[i] // Ships with no Ammo will never attack somebody if cacheShipClass[attIdx].Armament == 0 { continue } opponents := slices.DeleteFunc(slices.Clone(battleGroups), func(defIdx int) bool { return FilterBattleOpponents(g, attIdx, defIdx, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability) }) if len(opponents) > 0 { b.shipAmmo[attIdx] = cacheShipClass[attIdx].Armament b.shipName[attIdx] = cacheShipClass[attIdx].Name b.observerGroups[attIdx] = true for _, defIdx := range opponents { b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx] b.shipName[defIdx] = cacheShipClass[defIdx].Name b.observerGroups[defIdx] = true } } } if len(b.attacker) > 0 { SingleBattle(g, b) b.ID = uuid.New() result = append(result, b) } clear(b.attacker) clear(b.shipAmmo) } return result } func SingleBattle(g *Game, b *Battle) { for len(b.attacker) > 0 { attackers := slices.Collect(maps.Keys(b.attacker)) attIdx := attackers[rand.IntN(len(attackers))] for range b.shipAmmo[attIdx] { defenders := slices.Collect(maps.Keys(b.attacker[attIdx])) defIdx := defenders[rand.IntN(len(defenders))] destroyed := false probability := b.attacker[attIdx][defIdx] switch { case probability >= 1: destroyed = true case probability > 0: destroyed = rand.Float64() >= probability default: panic("SingleBattle: probability unexpected: value <= 0") } b.Protocol = append(b.Protocol, BattleAction{ Attacker: attIdx, Defenter: defIdx, Destroyed: destroyed, }) if destroyed { g.ShipGroups[defIdx].Number-- } if g.ShipGroups[defIdx].Number == 0 { delete(b.attacker, defIdx) // Eliminated group cant attack anyone for attIdx := range b.attacker { delete(b.attacker[attIdx], defIdx) // Attackers can't attack eliminated group anymore if len(b.attacker[attIdx]) == 0 { delete(b.attacker, attIdx) // Remove attacker if he lost all opponents } } // FIXME: удалять ShipGroups после генерирования пользовательского отчёта // g.ShipGroups = append(g.ShipGroups[:defIdx], g.ShipGroups[defIdx+1:]...) } if len(b.attacker) == 0 { break } } } } func RaceIndex(g *Game, ID uuid.UUID) int { i := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == ID }) if i < 0 { panic(fmt.Sprintf("RaceIndex: race not found by ID=%v", ID)) } return i } func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 { effAttack := attWeapons * attWeaponsTech effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass) return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2 } func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 { return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.) } func (b BattleReport) MarshalBinary() (data []byte, err error) { return json.Marshal(&b) } func (b *BattleReport) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, b) }