8f982278d2
* add multimodule * re-package modules
232 lines
6.4 KiB
Go
232 lines
6.4 KiB
Go
package controller
|
|
|
|
import (
|
|
"iter"
|
|
"maps"
|
|
"math"
|
|
"math/rand/v2"
|
|
"slices"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/iliadenisov/galaxy/server/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.F(),
|
|
c.ShipGroup(attIdx).TechLevel(game.TechWeapons).F(),
|
|
c.ShipGroupShipClass(defIdx).Shields.F(),
|
|
c.ShipGroup(defIdx).TechLevel(game.TechShields).F(),
|
|
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)
|
|
|
|
// Multiple battles on single planet shoul be produced as single battle:
|
|
// A <--> B
|
|
// C <--> D
|
|
// where: [A] and [B] are mutual enemies, as well [C] and [D]
|
|
for pl, observerGroups := range planetGroups {
|
|
battleGroups := FilterBattleGroups(c, observerGroups)
|
|
b := &Battle{
|
|
Planet: pl,
|
|
ObserverGroups: observerGroups,
|
|
InitialNumbers: make(map[int]uint),
|
|
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 {
|
|
if _, ok := b.attacker[attIdx][defIdx]; !ok {
|
|
b.attacker[attIdx] = make(map[int]float64)
|
|
}
|
|
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) {
|
|
roundShooters := make(map[int]bool)
|
|
for len(b.attacker) > 0 {
|
|
// список участников раунда
|
|
clear(roundShooters)
|
|
for sgi := range b.attacker {
|
|
roundShooters[sgi] = true
|
|
}
|
|
|
|
for len(roundShooters) > 0 {
|
|
// attacke group id among round participants
|
|
attIdx := randomValue(maps.Keys(roundShooters))
|
|
delete(roundShooters, attIdx)
|
|
|
|
for range b.shipAmmo[attIdx] {
|
|
// defender group id among all attacker's opponents
|
|
defIdx := randomValue(maps.Keys(b.attacker[attIdx]))
|
|
|
|
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.ShipGroupDestroyItem(defIdx)
|
|
}
|
|
if c.ShipGroup(defIdx).Number == 0 {
|
|
// Eliminated group cant attack anyone
|
|
delete(b.attacker, defIdx)
|
|
delete(roundShooters, defIdx)
|
|
|
|
for attIdx := range b.attacker {
|
|
// Other attackers can't attack eliminated group anymore
|
|
delete(b.attacker[attIdx], defIdx)
|
|
|
|
if len(b.attacker[attIdx]) == 0 {
|
|
// Remove attacker if he lost all opponents
|
|
delete(b.attacker, attIdx)
|
|
delete(roundShooters, attIdx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// When attacker has no more targets to shoot - break its ammo cycle
|
|
if len(b.attacker[attIdx]) == 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.)
|
|
}
|
|
|
|
func randomValue(v iter.Seq[int]) int {
|
|
ids := slices.Collect(v)
|
|
return ids[rand.IntN(len(ids))]
|
|
}
|