wip: battle mechanism

This commit is contained in:
Ilia Denisov
2026-01-11 20:27:43 +02:00
parent b3de13b6e1
commit ac60bb3020
4 changed files with 322 additions and 94 deletions
+208 -88
View File
@@ -2,15 +2,20 @@ package game
import (
"fmt"
"maps"
"math"
"math/rand/v2"
"slices"
"github.com/google/uuid"
)
type Battle struct {
Planet uint
Groups []int // ShipGroup indexes
BattleReport BattleReport
shipAmmo map[int]uint
attacker map[int]map[int]float64 // a group able to attack and destroy an opponent with some probability
}
type BattleReport struct {
@@ -23,100 +28,215 @@ type BattleAction struct {
Destroyed bool
}
type BattleOpponent struct {
RaceIndex int
ShipGroupIndex int
ShipType ShipType
}
func CollectPlanetGroups(g *Game, cacheShipGroupRaceID map[int]int, cacheShipClass map[int]*ShipType) map[uint][]int {
planetGroup := make(map[uint][]int)
for groupIndex := range g.ShipGroups {
if g.ShipGroups[groupIndex].State() == StateInOrbit {
planetNumber := g.ShipGroups[groupIndex].Destination
planetGroup[planetNumber] = append(planetGroup[planetNumber], groupIndex)
func ProduceBattles(g *Game) error {
if _, ok := cacheShipGroupRaceID[groupIndex]; !ok {
cacheShipGroupRaceID[groupIndex] = RaceIndex(g, g.ShipGroups[groupIndex].OwnerID)
}
ri := cacheShipGroupRaceID[groupIndex]
battleOnPlanet := Battle{}
_ = battleOnPlanet
return nil
}
func SingleBattle(g *Game, b Battle) {
attacker := SelectAttackShip(g, b.Groups)
for shots := range attacker.ShipType.Armament {
// groupsCopy := slices.Clone(b.Groups)
// groupsWithoutAttacker := append(groupsCopy[:attackerIdx], groupsCopy[attackerIdx+1:]...)
_ = SelectDefendShip(g, slices.Clone(b.Groups), attacker)
_ = shots
}
}
func SelectAttackShip(g *Game, battleGroups []int) BattleOpponent {
sgi := rand.IntN(len(battleGroups))
if sgi > len(g.ShipGroups)-1 {
panic("SelectAttackShip: battleGroups is bigger than game's ship groups")
}
sg := g.ShipGroups[sgi]
ri := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == sg.OwnerID })
if ri < 0 {
panic(fmt.Sprintf("SelectAttackShip: ship group #%v owner race not found by ID=%v", sg.Index, sg.OwnerID))
}
st, ok := ShipClass(g, ri, sg.TypeID)
if !ok {
panic(fmt.Sprintf("SelectAttackShip: ship class not found for race=%q group=%v", g.Race[ri].Name, sg.Index))
}
if st.Weapons == 0 || st.Armament == 0 {
panic(fmt.Sprintf("SelectAttackShip: ship_class=%q of race=%q has no weapons for attack", st.Name, g.Race[ri].Name))
}
return BattleOpponent{
RaceIndex: ri,
ShipGroupIndex: sgi,
ShipType: st,
}
}
func SelectDefendShip(g *Game, battleGroups []int, attacker BattleOpponent) BattleOpponent {
enemyGroups := FilterAttackingPretendent(g, attacker.RaceIndex, battleGroups)
sgi := rand.IntN(len(enemyGroups))
if sgi > len(g.ShipGroups)-1 {
panic("SelectDefendShip: battleGroups is bigger than game's ship groups")
}
sg := g.ShipGroups[sgi]
ri := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == sg.OwnerID })
if ri < 0 {
panic(fmt.Sprintf("SelectDefendShip: ship group #%v owner race not found by ID=%v", sg.Index, sg.OwnerID))
}
st, ok := ShipClass(g, ri, sg.TypeID)
if !ok {
panic(fmt.Sprintf("SelectDefendShip: ship class not found for race=%q group=%v", g.Race[ri].Name, sg.Index))
}
return BattleOpponent{
RaceIndex: ri,
ShipGroupIndex: sgi,
ShipType: st,
}
}
// attackerIdx - attacker race index
func FilterAttackingPretendent(g *Game, attackerIdx int, battleGroups []int) []int {
result := make([]int, 0)
for sgi := range battleGroups {
sg := g.ShipGroups[sgi]
enemyIdx := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == sg.OwnerID })
if enemyIdx < 0 {
panic(fmt.Sprintf("FilterAttackingPretendent: ship group #%v owner race not found by ID=%v", sg.Index, sg.OwnerID))
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]
}
}
rel, err := g.relationInternal(attackerIdx, enemyIdx)
if err != nil {
panic(err)
}
// attacker race will be in peace with itself, so attacker ships will be filtered out as well
if rel.Relation == RelationPeace {
continue
}
result = append(result, sgi)
}
for pl := range planetGroup {
if len(planetGroup[pl]) < 2 {
delete(planetGroup, pl)
}
}
return planetGroup
}
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)
}()
planetGroup := CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass)
if len(planetGroup) == 0 {
return nil
}
cacheRelation := CacheRelations(g, cacheShipGroupRaceID)
defer func() {
clear(cacheRelation)
}()
result := make([]*Battle, 0)
for pl, groups := range planetGroup {
b := &Battle{
Planet: pl,
attacker: make(map[int]map[int]float64),
shipAmmo: make(map[int]uint),
}
for i := range groups {
attIdx := groups[i]
// Ships with no Ammo will never attack somebody
if cacheShipClass[attIdx].Armament == 0 {
continue
}
// TODO: remove slices.Clone?
opponents := slices.DeleteFunc(slices.Clone(groups), func(defIdx int) bool {
return FilterBattleOpponents(g, attIdx, defIdx, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability)
})
if len(opponents) > 0 {
b.shipAmmo[attIdx] = cacheShipClass[attIdx].Armament
for _, defIdx := range opponents {
b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx]
}
}
}
if len(b.attacker) > 0 {
SingleBattle(g, b)
result = append(result, b)
}
clear(b.attacker)
clear(b.shipAmmo)
}
return result
}
func DestroyProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 {
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("probabilities cache returned unexpected value")
}
b.BattleReport.BattleAction = append(b.BattleReport.BattleAction, 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
}
}
}
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