308 lines
8.5 KiB
Go
308 lines
8.5 KiB
Go
package game
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"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]),
|
|
// )
|
|
p := 0.
|
|
// 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)
|
|
}
|