From 004529cdd383170c94f8aa551026da92ae2db249 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 13 Jan 2026 22:16:23 +0200 Subject: [PATCH] feat: support controller's cache --- internal/controller/cache.go | 194 +++++++++++++++++++++++++++ internal/controller/controller.go | 2 + internal/controller/generate_test.go | 2 +- internal/game/battle/battle.go | 194 +++++++++++++++++++++++++++ internal/game/battle/battle_test.go | 67 +++++++++ internal/game/cmd_turn.go | 2 +- internal/game/turn/battle.go | 51 +++++++ internal/game/turn/turn.go | 59 +++----- internal/model/game/battle.go | 32 ++--- internal/model/game/battle_test.go | 66 +-------- 10 files changed, 543 insertions(+), 126 deletions(-) create mode 100644 internal/controller/cache.go create mode 100644 internal/game/battle/battle.go create mode 100644 internal/game/battle/battle_test.go create mode 100644 internal/game/turn/battle.go diff --git a/internal/controller/cache.go b/internal/controller/cache.go new file mode 100644 index 0000000..d438595 --- /dev/null +++ b/internal/controller/cache.go @@ -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 +} diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 5a31de1..b33f2a0 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -33,6 +33,7 @@ type Repo interface { type Controller struct { param Param Repo Repo + Cache *Cache } type Param struct { @@ -74,6 +75,7 @@ func (c *Controller) ExecuteGame(consumer func(Repo, *game.Game)) error { if err != nil { return err } + c.Cache = NewCache(g) consumer(c.Repo, g) return c.Repo.Release() } diff --git a/internal/controller/generate_test.go b/internal/controller/generate_test.go index d8e2b9b..cc37be6 100644 --- a/internal/controller/generate_test.go +++ b/internal/controller/generate_test.go @@ -30,7 +30,7 @@ func TestNewGame(t *testing.T) { assert.NoError(t, err) 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() assert.NoError(t, err) diff --git a/internal/game/battle/battle.go b/internal/game/battle/battle.go new file mode 100644 index 0000000..9d94d8a --- /dev/null +++ b/internal/game/battle/battle.go @@ -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.) +} diff --git a/internal/game/battle/battle_test.go b/internal/game/battle/battle_test.go new file mode 100644 index 0000000..a7e0943 --- /dev/null +++ b/internal/game/battle/battle_test.go @@ -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) +} diff --git a/internal/game/cmd_turn.go b/internal/game/cmd_turn.go index 5dce4a1..6b02f33 100644 --- a/internal/game/cmd_turn.go +++ b/internal/game/cmd_turn.go @@ -8,7 +8,7 @@ import ( func MakeTurn(configure func(*controller.Param), race string, number int, name string) (err error) { 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 } diff --git a/internal/game/turn/battle.go b/internal/game/turn/battle.go new file mode 100644 index 0000000..7b2c178 --- /dev/null +++ b/internal/game/turn/battle.go @@ -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 +} diff --git a/internal/game/turn/turn.go b/internal/game/turn/turn.go index 36aaddf..a660568 100644 --- a/internal/game/turn/turn.go +++ b/internal/game/turn/turn.go @@ -1,14 +1,13 @@ package turn import ( - "fmt" - "github.com/iliadenisov/galaxy/internal/controller" e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/game/battle" "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 g.Age += 1 @@ -16,7 +15,7 @@ func MakeTurn(r controller.Repo, g *game.Game) error { game.JoinEqualGroups(g) // 02. Враждующие корабли вступают в схватку. - battles := game.ProduceBattles(g) + battles := battle.ProduceBattles(c.Cache) // Internal control: after battles there are can't be groups with no ships left 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 { 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 { return err } } } + // Remove killed ship groups + c.Cache.DeleteKilledShipGroups() + + // TODO: Store game state + + // TODO: Store individual reports + 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 -} diff --git a/internal/model/game/battle.go b/internal/model/game/battle.go index fcb9ff4..4314ad0 100644 --- a/internal/model/game/battle.go +++ b/internal/model/game/battle.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "maps" - "math" "math/rand/v2" "slices" @@ -143,13 +142,14 @@ func FilterBattleOpponents( 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 := 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 @@ -288,15 +288,15 @@ func RaceIndex(g *Game, ID uuid.UUID) int { 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 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 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) diff --git a/internal/model/game/battle_test.go b/internal/model/game/battle_test.go index f92d788..9b37508 100644 --- a/internal/model/game/battle_test.go +++ b/internal/model/game/battle_test.go @@ -1,34 +1,14 @@ package game_test import ( - "slices" "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", @@ -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) { g := newGame() @@ -158,24 +113,7 @@ func TestFilterBattleOpponents(t *testing.T) { assert.True(t, game.FilterBattleOpponents(g, 2, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability)) 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.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) - } -}