package controller import ( "maps" "math" "math/rand/v2" "slices" "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/model/game" ) type Battle struct { ID uuid.UUID Planet uint observerGroups map[int]bool // True = In_Battle, False = Out_Battle initialNumbers map[int]uint // Initial number of ships in the group Protocol []BattleAction shipAmmo map[int]uint attacker map[int]map[int]float64 // a group able to attack and destroy an opponent with some probability > 0 } type BattleAction struct { Attacker int Defender int Destroyed bool } func CollectPlanetGroups(c *Cache) map[uint]map[int]bool { planetGroup := make(map[uint]map[int]bool) for groupIndex := range c.ShipGroupsIndex() { state := c.ShipGroup(groupIndex).State() if state == game.StateInOrbit || state == game.StateUpgrade { planetNumber := c.ShipGroup(groupIndex).Destination if _, ok := planetGroup[planetNumber]; !ok { planetGroup[planetNumber] = make(map[int]bool) } planetGroup[planetNumber][groupIndex] = false } } for pl := range planetGroup { if len(planetGroup[pl]) < 2 { delete(planetGroup, pl) } } return planetGroup } func FilterBattleGroups(c *Cache, groups map[int]bool) []int { return slices.DeleteFunc(slices.Collect(maps.Keys(groups)), func(groupIndex int) bool { return c.ShipGroup(groupIndex).State() != game.StateInOrbit }) } func FilterBattleOpponents(c *Cache, attIdx, defIdx int, cacheProbability map[int]map[int]float64) bool { // Same Race's groups can't attack themselves if attIdx == defIdx || c.ShipGroupOwnerRaceIndex(attIdx) == c.ShipGroupOwnerRaceIndex(defIdx) { return true } // If any opponent has War relation to another, both will stay in battle if c.Relation(c.ShipGroupOwnerRaceIndex(attIdx), c.ShipGroupOwnerRaceIndex(defIdx)) == game.RelationPeace && c.Relation(c.ShipGroupOwnerRaceIndex(defIdx), c.ShipGroupOwnerRaceIndex(attIdx)) == game.RelationPeace { return true } p := DestructionProbability( c.ShipGroupShipClass(attIdx).Weapons, c.ShipGroup(attIdx).TechLevel(game.TechWeapons), c.ShipGroupShipClass(defIdx).Shields, c.ShipGroup(defIdx).TechLevel(game.TechShields), c.ShipGroup(defIdx).FullMass(c.ShipGroupShipClass(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(c *Cache) []*Battle { cacheProbability := make(map[int]map[int]float64) defer func() { clear(cacheProbability) }() planetGroups := CollectPlanetGroups(c) if len(planetGroups) == 0 { return nil } result := make([]*Battle, 0) // TODO: check this behavior: // Multiple battles on single planet shoul be produced as single battle too: // A <--> B // C <--> D // where: A in peace with [C, D], B in peace with [C, D], and so on. for pl, observerGroups := range planetGroups { battleGroups := FilterBattleGroups(c, observerGroups) b := &Battle{ Planet: pl, observerGroups: observerGroups, attacker: make(map[int]map[int]float64), shipAmmo: make(map[int]uint), } for sgi := range observerGroups { b.initialNumbers[sgi] = c.ShipGroup(sgi).Number } for i := range battleGroups { attIdx := battleGroups[i] // Ships with no Ammo will never attack somebody if c.ShipGroupShipClass(attIdx).Armament == 0 { continue } opponents := slices.DeleteFunc(slices.Clone(battleGroups), func(defIdx int) bool { return FilterBattleOpponents(c, attIdx, defIdx, cacheProbability) }) if len(opponents) > 0 { b.shipAmmo[attIdx] = c.ShipGroupShipClass(attIdx).Armament b.observerGroups[attIdx] = true for _, defIdx := range opponents { b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx] b.observerGroups[defIdx] = true } } } if len(b.attacker) > 0 { SingleBattle(c, b) b.ID = uuid.New() result = append(result, b) } clear(b.attacker) clear(b.shipAmmo) } return result } func SingleBattle(c *Cache, 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, Defender: defIdx, Destroyed: destroyed, }) if destroyed { c.ShipGroupNumber(defIdx, c.ShipGroup(defIdx).Number-1) } if c.ShipGroup(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 } } } if len(b.attacker) == 0 { break } } } } 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.) }