refactor: battle at controller

This commit is contained in:
Ilia Denisov
2026-01-14 14:40:04 +02:00
parent 004529cdd3
commit 1bfc9242af
16 changed files with 583 additions and 546 deletions
-275
View File
@@ -2,40 +2,10 @@ 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"`
@@ -53,251 +23,6 @@ type BattleActionReport struct {
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)
}
-119
View File
@@ -1,119 +0,0 @@
package game_test
import (
"testing"
"github.com/iliadenisov/galaxy/internal/game/battle"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
var (
ship = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Ship",
Drive: 10,
Armament: 1,
Weapons: 10,
Shields: 10,
Cargo: 0,
},
}
)
func TestCollectPlanetGroups(t *testing.T) {
g := newGame()
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) // 0
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 1
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 2
g.ShipGroups[2].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} // 2 -> In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 3
g.ShipGroups[3].Destination = R1_Planet_1_num // 3 -> Planet_1
assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 4
g.ShipGroups[4].Destination = R0_Planet_0_num // 4 -> Planet_0
cacheShipGroupRaceID := make(map[int]int)
cacheShipClass := make(map[int]*game.ShipType)
planetGroups := game.CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass)
for pl := range planetGroups {
switch pl {
case R0_Planet_0_num:
assert.Equal(t, 3, len(planetGroups[pl]))
assert.Contains(t, planetGroups[pl], 0)
assert.Contains(t, planetGroups[pl], 1)
assert.Contains(t, planetGroups[pl], 4)
default:
assert.Fail(t, "planet #%d should not contain groups for battle", pl)
}
}
assert.Len(t, cacheShipGroupRaceID, 4)
assert.Contains(t, cacheShipGroupRaceID, 0)
assert.Contains(t, cacheShipGroupRaceID, 1)
assert.Contains(t, cacheShipGroupRaceID, 3)
assert.Contains(t, cacheShipGroupRaceID, 4)
assert.Equal(t, Race_0_idx, cacheShipGroupRaceID[0])
assert.Equal(t, Race_0_idx, cacheShipGroupRaceID[1])
assert.Equal(t, Race_0_idx, cacheShipGroupRaceID[3])
assert.Equal(t, Race_1_idx, cacheShipGroupRaceID[4])
assert.Len(t, cacheShipClass, 4) // all registered ship classes for all In_Orbit ship groups
cacheRelation := game.CacheRelations(g, cacheShipGroupRaceID)
assert.Len(t, cacheRelation, 2)
assert.Len(t, cacheRelation[Race_0_idx], 1)
assert.Len(t, cacheRelation[Race_1_idx], 1)
assert.Equal(t, game.RelationWar, cacheRelation[Race_0_idx][Race_1_idx])
assert.Equal(t, game.RelationPeace, cacheRelation[Race_1_idx][Race_0_idx])
assert.Empty(t, cacheRelation[Race_0_idx][Race_0_idx])
assert.Empty(t, cacheRelation[Race_1_idx][Race_1_idx])
}
func TestFilterBattleOpponents(t *testing.T) {
g := newGame()
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) // 0
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 1
assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 2
undefeatedShip := ship
undefeatedShip.Shields = 100
assert.NoError(t, g.CreateShipType(Race_1.Name, undefeatedShip.Name, undefeatedShip.Drive, undefeatedShip.Weapons, undefeatedShip.Shields, undefeatedShip.Cargo, int(undefeatedShip.Armament)))
assert.NoError(t, g.CreateShips(Race_1_idx, undefeatedShip.Name, R1_Planet_1_num, 1)) // 3
cacheShipGroupRaceID := make(map[int]int)
cacheShipClass := make(map[int]*game.ShipType)
cacheProbability := make(map[int]map[int]float64)
cacheRelation := make(map[int]map[int]game.Relation)
game.CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass)
cacheRelation[Race_0_idx] = make(map[int]game.Relation)
cacheRelation[Race_1_idx] = make(map[int]game.Relation)
cacheRelation[Race_0_idx][Race_1_idx] = game.RelationPeace
cacheRelation[Race_1_idx][Race_0_idx] = game.RelationWar
assert.False(t, game.FilterBattleOpponents(g, 0, 2, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.Contains(t, cacheProbability, 0)
assert.Contains(t, cacheProbability[0], 2)
assert.InDelta(t, 0.396222, cacheProbability[0][2], 0.0000001)
assert.False(t, game.FilterBattleOpponents(g, 2, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.Contains(t, cacheProbability, 2)
assert.Contains(t, cacheProbability[2], 0)
assert.InDelta(t, 0.495, cacheProbability[2][0], 0.0001)
// Test: same owner
assert.True(t, game.FilterBattleOpponents(g, 0, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.True(t, game.FilterBattleOpponents(g, 0, 1, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.True(t, game.FilterBattleOpponents(g, 1, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
// Test: reace reations
cacheRelation[Race_1_idx][Race_0_idx] = game.RelationPeace
assert.True(t, game.FilterBattleOpponents(g, 0, 2, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.True(t, game.FilterBattleOpponents(g, 2, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
cacheRelation[Race_1_idx][Race_0_idx] = game.RelationWar
assert.LessOrEqual(t, battle.DestructionProbability(Cruiser.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass()), 0.)
assert.True(t, game.FilterBattleOpponents(g, 1, 3, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.NotContains(t, cacheProbability[1], 3)
}