feat: support controller's cache
This commit is contained in:
@@ -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.)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user