feat: support controller's cache

This commit is contained in:
Ilia Denisov
2026-01-13 22:16:23 +02:00
parent 45c725a3ee
commit 004529cdd3
10 changed files with 543 additions and 126 deletions
+194
View File
@@ -0,0 +1,194 @@
package controller
import (
"fmt"
"iter"
"slices"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/model/game"
)
type Cache struct {
g *game.Game
cacheRaceIndexByID map[uuid.UUID]int
raceIndexByShipGroupIndex map[int]int
shipClassByShipGroupIndex map[int]*game.ShipType
planetByPlanetNumber map[uint]*game.Planet
cacheRelation map[int]map[int]game.Relation
}
func NewCache(g *game.Game) *Cache {
if g == nil {
panic("NewCache: nil Game passed")
}
c := &Cache{
g: g,
}
return c
}
func (c *Cache) Relation(r1, r2 int) game.Relation {
if c.cacheRelation == nil {
for r1 := range c.g.Race {
for r2 := range c.g.Race {
if r1 == r2 {
continue
}
rel := slices.IndexFunc(c.g.Race[r1].Relations, func(r game.RaceRelation) bool { return r.RaceID == c.g.Race[r2].ID })
if rel < 0 {
panic(fmt.Sprintf("Relation: opponent not found idx=%d", r2))
}
if _, ok := c.cacheRelation[r1]; !ok {
c.cacheRelation[r1] = make(map[int]game.Relation)
}
c.cacheRelation[r1][r2] = c.g.Race[r1].Relations[rel].Relation
}
}
}
if _, ok := c.cacheRelation[r1]; !ok {
panic(fmt.Sprintf("Relation: no left race idx=%d", r1))
}
if v, ok := c.cacheRelation[r1][r2]; !ok {
panic(fmt.Sprintf("Relation: no right race idx=%d", r2))
} else {
return v
}
}
func (c *Cache) Planet(planetNumber uint) *game.Planet {
if c.planetByPlanetNumber == nil {
c.planetByPlanetNumber = make(map[uint]*game.Planet)
for p := range c.g.Map.Planet {
c.planetByPlanetNumber[c.g.Map.Planet[p].Number] = &c.g.Map.Planet[p]
}
}
if v, ok := c.planetByPlanetNumber[planetNumber]; ok {
return v
} else {
panic(fmt.Sprintf("Planet: not found by number=%d", planetNumber))
}
}
func (c *Cache) ShipGroupShipClass(groupIndex int) *game.ShipType {
if c.shipClassByShipGroupIndex == nil {
c.fillShipsAndGroups()
}
c.validateShipGroupIndex(groupIndex)
if v, ok := c.shipClassByShipGroupIndex[groupIndex]; ok {
return v
} else {
panic(fmt.Sprintf("ShipClassByShipGroupIndex: group not found by index=%v", groupIndex))
}
}
func (c *Cache) RaceIndex(ID uuid.UUID) int {
if c.cacheRaceIndexByID == nil {
c.cacheRaceIndexByID = make(map[uuid.UUID]int)
for i := range c.g.Race {
c.cacheRaceIndexByID[c.g.Race[i].ID] = i
}
}
if v, ok := c.cacheRaceIndexByID[ID]; ok {
return v
} else {
panic(fmt.Sprintf("RaceIndex: race not found by ID=%v", ID))
}
}
// ShipGroup is a proxy func, nothing to cache
func (c *Cache) ShipGroup(groupIndex int) *game.ShipGroup {
c.validateShipGroupIndex(groupIndex)
return &c.g.ShipGroups[groupIndex]
}
func (c *Cache) ShipGroupsIndex() iter.Seq[int] {
return func(yield func(int) bool) {
for i := range c.g.ShipGroups {
if !yield(i) {
return
}
}
}
}
func (c *Cache) ShipGroupOwnerRaceIndex(groupIndex int) int {
if c.raceIndexByShipGroupIndex == nil {
c.fillShipsAndGroups()
}
c.validateShipGroupIndex(groupIndex)
if v, ok := c.raceIndexByShipGroupIndex[groupIndex]; ok {
return v
} else {
panic(fmt.Sprintf("ShipGroupRace: group not found by index=%v", groupIndex))
}
}
func (c *Cache) ShipGroupOwnerRace(groupIndex int) *game.Race {
return &c.g.Race[c.ShipGroupOwnerRaceIndex(groupIndex)]
}
func (c *Cache) ShipGroupNumber(i int, n uint) {
c.validateShipGroupIndex(i)
c.g.ShipGroups[i].Number = n
}
func (c *Cache) DeleteShipGroup(i int) {
c.validateShipGroupIndex(i)
c.unsafeDeleteShipGroup(i)
}
func (c *Cache) DeleteKilledShipGroups() {
for i := len(c.g.ShipGroups) - 1; i >= 0; i-- {
if c.g.ShipGroups[i].Number == 0 {
c.unsafeDeleteShipGroup(i)
}
}
}
func (c *Cache) unsafeDeleteShipGroup(i int) {
c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...)
delete(c.raceIndexByShipGroupIndex, i)
delete(c.shipClassByShipGroupIndex, i)
}
// Internal
func (c *Cache) validateShipGroupIndex(i int) {
if i >= len(c.g.ShipGroups) {
panic(fmt.Sprintf("group index out of groups len: %d >= %d", i, len(c.g.ShipGroups)))
}
}
func (c *Cache) fillShipsAndGroups() {
if c.raceIndexByShipGroupIndex != nil {
clear(c.raceIndexByShipGroupIndex)
} else {
c.raceIndexByShipGroupIndex = make(map[int]int)
}
if c.shipClassByShipGroupIndex != nil {
clear(c.shipClassByShipGroupIndex)
} else {
c.shipClassByShipGroupIndex = make(map[int]*game.ShipType)
}
for groupIndex := range c.g.ShipGroups {
ri := c.RaceIndex(c.g.ShipGroups[groupIndex].OwnerID)
c.raceIndexByShipGroupIndex[groupIndex] = ri
sti, ok := ShipClassIndex(c.g, ri, c.g.ShipGroups[groupIndex].TypeID)
if !ok {
panic(fmt.Sprintf("CollectPlanetGroups: ship class not found for race=%q group=%v", c.g.Race[ri].Name, c.g.ShipGroups[groupIndex].Index))
}
c.shipClassByShipGroupIndex[groupIndex] = &c.g.Race[ri].ShipTypes[sti]
}
}
// Helpers
func ShipClassIndex(g *game.Game, ri int, classID uuid.UUID) (int, bool) {
if len(g.Race) < ri+1 {
panic(fmt.Sprintf("ShipClass: game race index %d invalid: len=%d", ri, len(g.Race)))
}
sti := slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == classID })
return sti, sti >= 0
}
+2
View File
@@ -33,6 +33,7 @@ type Repo interface {
type Controller struct { type Controller struct {
param Param param Param
Repo Repo Repo Repo
Cache *Cache
} }
type Param struct { type Param struct {
@@ -74,6 +75,7 @@ func (c *Controller) ExecuteGame(consumer func(Repo, *game.Game)) error {
if err != nil { if err != nil {
return err return err
} }
c.Cache = NewCache(g)
consumer(c.Repo, g) consumer(c.Repo, g)
return c.Repo.Release() return c.Repo.Release()
} }
+1 -1
View File
@@ -30,7 +30,7 @@ func TestNewGame(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.FileExists(t, filepath.Join(root, "state.json")) assert.FileExists(t, filepath.Join(root, "state.json"))
assert.FileExists(t, filepath.Join(root, "000/state.json")) assert.FileExists(t, filepath.Join(root, "0000/state.json"))
g, err := r.LoadState() g, err := r.LoadState()
assert.NoError(t, err) assert.NoError(t, err)
+194
View File
@@ -0,0 +1,194 @@
package battle
import (
"maps"
"math"
"math/rand/v2"
"slices"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/internal/controller"
"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
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
Defenter int
Destroyed bool
}
func CollectPlanetGroups(c *controller.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 *controller.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 *controller.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 *controller.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)
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 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 *controller.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,
Defenter: 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.)
}
+67
View File
@@ -0,0 +1,67 @@
package battle_test
import (
"testing"
"github.com/iliadenisov/galaxy/internal/game/battle"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
var (
attacker = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Attacker",
Drive: 8,
Armament: 1,
Weapons: 8,
Shields: 8,
Cargo: 0,
},
}
defender = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Defender",
Drive: 1,
Armament: 1,
Weapons: 1,
Shields: 1,
Cargo: 0,
},
}
ship = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Ship",
Drive: 10,
Armament: 1,
Weapons: 10,
Shields: 10,
Cargo: 0,
},
}
)
func TestDestructionProbability(t *testing.T) {
probability := battle.DestructionProbability(ship.Weapons, 1, ship.Shields, 1, ship.EmptyMass())
assert.Equal(t, .5, probability)
undefeatedShip := ship
undefeatedShip.Shields = 55
probability = battle.DestructionProbability(ship.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass())
assert.LessOrEqual(t, probability, 0.)
disruptiveShip := ship
disruptiveShip.Weapons = 40
probability = battle.DestructionProbability(disruptiveShip.Weapons, 1, ship.Shields, 1, ship.EmptyMass())
assert.GreaterOrEqual(t, probability, 1.)
}
func TestEffectiveDefence(t *testing.T) {
assert.Equal(t, 10., battle.EffectiveDefence(ship.Shields, 1, ship.EmptyMass()))
attackerEffectiveDefence := battle.EffectiveDefence(attacker.Shields, 1, attacker.EmptyMass())
defenderEffectiveDefence := battle.EffectiveDefence(defender.Shields, 1, defender.EmptyMass())
// attacker's effective shields must be 'just' 4 times greater than defender's
assert.InDelta(t, defenderEffectiveDefence*4, attackerEffectiveDefence, 0)
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func MakeTurn(configure func(*controller.Param), race string, number int, name string) (err error) { func MakeTurn(configure func(*controller.Param), race string, number int, name string) (err error) {
control(configure, func(c *controller.Controller) { control(configure, func(c *controller.Controller) {
c.ExecuteGame(func(r controller.Repo, g *game.Game) { turn.MakeTurn(r, g) }) c.ExecuteGame(func(r controller.Repo, g *game.Game) { turn.MakeTurn(c, r, g) })
}) })
return return
} }
+51
View File
@@ -0,0 +1,51 @@
package turn
import (
"github.com/iliadenisov/galaxy/internal/controller"
"github.com/iliadenisov/galaxy/internal/game/battle"
"github.com/iliadenisov/galaxy/internal/model/game"
)
func TransformBattle(c *controller.Cache, b *battle.Battle) *game.BattleReport {
r := &game.BattleReport{
ID: b.ID,
Planet: b.Planet,
PlanetName: c.Planet(b.Planet).Name,
Races: make(map[int]string),
Ships: make(map[int]string),
Protocol: make([]game.BattleActionReport, len(b.Protocol)),
}
cacheShipClass := make(map[string]int)
cacheRaceName := make(map[string]int)
cacher := func(shipClass string, cache map[string]int) int {
if v, ok := cache[shipClass]; ok {
return v
} else {
itemNumber := len(r.Ships)
r.Ships[itemNumber] = shipClass
cache[shipClass] = itemNumber
return itemNumber
}
}
for i := range b.Protocol {
r.Protocol[i] = game.BattleActionReport{
Attacker: cacher(c.ShipGroupOwnerRace(b.Protocol[i].Attacker).Name, cacheRaceName),
AttackerShipClass: cacher(c.ShipGroupShipClass(b.Protocol[i].Attacker).Name, cacheShipClass),
Defender: cacher(c.ShipGroupOwnerRace(b.Protocol[i].Defenter).Name, cacheRaceName),
DefenderShipClass: cacher(c.ShipGroupShipClass(b.Protocol[i].Defenter).Name, cacheShipClass),
Destroyed: b.Protocol[i].Destroyed,
}
}
for name, index := range cacheRaceName {
r.Races[index] = name
}
for name, index := range cacheShipClass {
r.Ships[index] = name
}
return r
}
+15 -44
View File
@@ -1,14 +1,13 @@
package turn package turn
import ( import (
"fmt"
"github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/controller"
e "github.com/iliadenisov/galaxy/internal/error" e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/game/battle"
"github.com/iliadenisov/galaxy/internal/model/game" "github.com/iliadenisov/galaxy/internal/model/game"
) )
func MakeTurn(r controller.Repo, g *game.Game) error { func MakeTurn(c *controller.Controller, r controller.Repo, g *game.Game) error {
// Next turn // Next turn
g.Age += 1 g.Age += 1
@@ -16,7 +15,7 @@ func MakeTurn(r controller.Repo, g *game.Game) error {
game.JoinEqualGroups(g) game.JoinEqualGroups(g)
// 02. Враждующие корабли вступают в схватку. // 02. Враждующие корабли вступают в схватку.
battles := game.ProduceBattles(g) battles := battle.ProduceBattles(c.Cache)
// Internal control: after battles there are can't be groups with no ships left // Internal control: after battles there are can't be groups with no ships left
for i := range g.ShipGroups { for i := range g.ShipGroups {
@@ -25,52 +24,24 @@ func MakeTurn(r controller.Repo, g *game.Game) error {
} }
} }
// Last step: storing battles /*** Last steps ***/
// Store battles
if len(battles) > 0 { if len(battles) > 0 {
for i := range battles { for i := range battles {
br := TransformBattle(g, battles[i]) // TODO: add In_Battle / Out_Battle participants?
br := TransformBattle(c.Cache, battles[i])
if err := r.SaveBattle(g.Age, br); err != nil { if err := r.SaveBattle(g.Age, br); err != nil {
return err return err
} }
} }
} }
// Remove killed ship groups
c.Cache.DeleteKilledShipGroups()
// TODO: Store game state
// TODO: Store individual reports
return nil return nil
} }
func TransformBattle(g *game.Game, b *game.Battle) *game.BattleReport {
p, ok := game.PlanetByNum(g, b.Planet)
if !ok {
panic(fmt.Sprintf("TransformBattle: no planet with number #%d", b.Planet))
}
r := &game.BattleReport{
ID: b.ID,
Planet: b.Planet,
PlanetName: p.Name,
Races: make(map[int]string),
Ships: make(map[int]string),
Protocol: make([]game.BattleActionReport, len(b.Protocol)),
}
cacheShipClass := make(map[string]int)
shipClass := func(shipClass string) int {
if v, ok := cacheShipClass[shipClass]; ok {
return v
} else {
l := len(r.Ships)
r.Ships[l] = shipClass
cacheShipClass[shipClass] = l
return l
}
}
for i := range b.Protocol {
r.Protocol[i] = game.BattleActionReport{
AttackerShipClass: shipClass(b.ShipClassName(b.Protocol[i].Attacker)),
DefenderShipClass: shipClass(b.ShipClassName(b.Protocol[i].Defenter)),
Destroyed: b.Protocol[i].Destroyed,
}
}
return r
}
+16 -16
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps" "maps"
"math"
"math/rand/v2" "math/rand/v2"
"slices" "slices"
@@ -143,13 +142,14 @@ func FilterBattleOpponents(
return true return true
} }
p := DestructionProbability( // p := DestructionProbability(
cacheShipClass[attIdx].Weapons, // cacheShipClass[attIdx].Weapons,
g.ShipGroups[attIdx].TechLevel(TechWeapons), // g.ShipGroups[attIdx].TechLevel(TechWeapons),
cacheShipClass[defIdx].Shields, // cacheShipClass[defIdx].Shields,
g.ShipGroups[defIdx].TechLevel(TechShields), // g.ShipGroups[defIdx].TechLevel(TechShields),
g.ShipGroups[defIdx].FullMass(cacheShipClass[defIdx]), // g.ShipGroups[defIdx].FullMass(cacheShipClass[defIdx]),
) // )
p := 0.
// Exclude opponent's group which cannot be probably destroyed // Exclude opponent's group which cannot be probably destroyed
if p <= 0 { if p <= 0 {
return true return true
@@ -288,15 +288,15 @@ func RaceIndex(g *Game, ID uuid.UUID) int {
return i return i
} }
func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 { // func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 {
effAttack := attWeapons * attWeaponsTech // effAttack := attWeapons * attWeaponsTech
effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass) // effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass)
return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2 // return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2
} // }
func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 { // func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 {
return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.) // return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.)
} // }
func (b BattleReport) MarshalBinary() (data []byte, err error) { func (b BattleReport) MarshalBinary() (data []byte, err error) {
return json.Marshal(&b) return json.Marshal(&b)
+2 -64
View File
@@ -1,34 +1,14 @@
package game_test package game_test
import ( import (
"slices"
"testing" "testing"
"github.com/iliadenisov/galaxy/internal/game/battle"
"github.com/iliadenisov/galaxy/internal/model/game" "github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var ( var (
attacker = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Attacker",
Drive: 8,
Armament: 1,
Weapons: 8,
Shields: 8,
Cargo: 0,
},
}
defender = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Defender",
Drive: 1,
Armament: 1,
Weapons: 1,
Shields: 1,
Cargo: 0,
},
}
ship = game.ShipType{ ship = game.ShipType{
ShipTypeReport: game.ShipTypeReport{ ShipTypeReport: game.ShipTypeReport{
Name: "Ship", Name: "Ship",
@@ -41,31 +21,6 @@ var (
} }
) )
func TestDestructionProbability(t *testing.T) {
probability := game.DestructionProbability(ship.Weapons, 1, ship.Shields, 1, ship.EmptyMass())
assert.Equal(t, .5, probability)
undefeatedShip := ship
undefeatedShip.Shields = 55
probability = game.DestructionProbability(ship.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass())
assert.LessOrEqual(t, probability, 0.)
disruptiveShip := ship
disruptiveShip.Weapons = 40
probability = game.DestructionProbability(disruptiveShip.Weapons, 1, ship.Shields, 1, ship.EmptyMass())
assert.GreaterOrEqual(t, probability, 1.)
}
func TestEffectiveDefence(t *testing.T) {
assert.Equal(t, 10., game.EffectiveDefence(ship.Shields, 1, ship.EmptyMass()))
attackerEffectiveDefence := game.EffectiveDefence(attacker.Shields, 1, attacker.EmptyMass())
defenderEffectiveDefence := game.EffectiveDefence(defender.Shields, 1, defender.EmptyMass())
// attacker's effective shields must be 'just' 4 times greater than defender's
assert.InDelta(t, defenderEffectiveDefence*4, attackerEffectiveDefence, 0)
}
func TestCollectPlanetGroups(t *testing.T) { func TestCollectPlanetGroups(t *testing.T) {
g := newGame() g := newGame()
@@ -158,24 +113,7 @@ func TestFilterBattleOpponents(t *testing.T) {
assert.True(t, game.FilterBattleOpponents(g, 2, 0, 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 cacheRelation[Race_1_idx][Race_0_idx] = game.RelationWar
assert.LessOrEqual(t, game.DestructionProbability(Cruiser.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass()), 0.) 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.True(t, game.FilterBattleOpponents(g, 1, 3, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
assert.NotContains(t, cacheProbability[1], 3) assert.NotContains(t, cacheProbability[1], 3)
} }
func TestSlicesDeleteFunc(t *testing.T) {
type Container struct {
S []int
}
c := Container{S: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
r1 := slices.DeleteFunc(c.S, func(e int) bool { return e%2 != 0 })
assert.Len(t, r1, 5)
for i := range r1 {
assert.Equal(t, i*2, r1[i], "elem #%d", i)
assert.Equal(t, i*2, c.S[i], "elem #%d", i)
}
assert.Len(t, c.S, 10)
for i := len(r1); i < len(c.S); i++ {
assert.Equal(t, 0, c.S[i], "elem #%d", i)
}
}