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