refactor: battle at controller
This commit is contained in:
@@ -0,0 +1,132 @@
|
|||||||
|
package controller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
|
"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 := controller.DestructionProbability(ship.Weapons, 1, ship.Shields, 1, ship.EmptyMass())
|
||||||
|
assert.Equal(t, .5, probability)
|
||||||
|
|
||||||
|
undefeatedShip := ship
|
||||||
|
undefeatedShip.Shields = 55
|
||||||
|
probability = controller.DestructionProbability(ship.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass())
|
||||||
|
assert.LessOrEqual(t, probability, 0.)
|
||||||
|
|
||||||
|
disruptiveShip := ship
|
||||||
|
disruptiveShip.Weapons = 40
|
||||||
|
probability = controller.DestructionProbability(disruptiveShip.Weapons, 1, ship.Shields, 1, ship.EmptyMass())
|
||||||
|
assert.GreaterOrEqual(t, probability, 1.)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEffectiveDefence(t *testing.T) {
|
||||||
|
assert.Equal(t, 10., controller.EffectiveDefence(ship.Shields, 1, ship.EmptyMass()))
|
||||||
|
|
||||||
|
attackerEffectiveDefence := controller.EffectiveDefence(attacker.Shields, 1, attacker.EmptyMass())
|
||||||
|
defenderEffectiveDefence := controller.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) {
|
||||||
|
c, g := newCache()
|
||||||
|
|
||||||
|
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) // 0
|
||||||
|
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 1
|
||||||
|
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 2
|
||||||
|
g.ShipGroups[2].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} // 2 -> In_Space
|
||||||
|
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 3
|
||||||
|
g.ShipGroups[3].Destination = R1_Planet_1_num // 3 -> Planet_1
|
||||||
|
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 4
|
||||||
|
g.ShipGroups[4].Destination = R0_Planet_0_num // 4 -> Planet_0
|
||||||
|
|
||||||
|
planetGroups := controller.CollectPlanetGroups(c)
|
||||||
|
|
||||||
|
for pl := range planetGroups {
|
||||||
|
switch pl {
|
||||||
|
case R0_Planet_0_num:
|
||||||
|
assert.Equal(t, 3, len(planetGroups[pl]))
|
||||||
|
assert.Contains(t, planetGroups[pl], 0)
|
||||||
|
assert.Contains(t, planetGroups[pl], 1)
|
||||||
|
assert.Contains(t, planetGroups[pl], 4)
|
||||||
|
default:
|
||||||
|
assert.Fail(t, "planet #%d should not contain groups for battle", pl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterBattleOpponents(t *testing.T) {
|
||||||
|
c, _ := newCache()
|
||||||
|
|
||||||
|
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) // 0
|
||||||
|
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 1
|
||||||
|
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 2
|
||||||
|
undefeatedShip := ship
|
||||||
|
undefeatedShip.Shields = 100
|
||||||
|
assert.NoError(t, c.CreateShipType(Race_1.Name, undefeatedShip.Name, undefeatedShip.Drive, int(undefeatedShip.Armament), undefeatedShip.Weapons, undefeatedShip.Shields, undefeatedShip.Cargo))
|
||||||
|
assert.NoError(t, c.CreateShips(Race_1_idx, undefeatedShip.Name, R1_Planet_1_num, 1)) // 3
|
||||||
|
|
||||||
|
cacheProbability := make(map[int]map[int]float64)
|
||||||
|
|
||||||
|
assert.False(t, controller.FilterBattleOpponents(c, 0, 2, cacheProbability))
|
||||||
|
assert.Contains(t, cacheProbability, 0)
|
||||||
|
assert.Contains(t, cacheProbability[0], 2)
|
||||||
|
assert.InDelta(t, 0.396222, cacheProbability[0][2], 0.0000001)
|
||||||
|
assert.False(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability))
|
||||||
|
assert.Contains(t, cacheProbability, 2)
|
||||||
|
assert.Contains(t, cacheProbability[2], 0)
|
||||||
|
assert.InDelta(t, 0.495, cacheProbability[2][0], 0.0001)
|
||||||
|
|
||||||
|
// Test: same owner
|
||||||
|
assert.True(t, controller.FilterBattleOpponents(c, 0, 0, cacheProbability))
|
||||||
|
assert.True(t, controller.FilterBattleOpponents(c, 0, 1, cacheProbability))
|
||||||
|
assert.True(t, controller.FilterBattleOpponents(c, 1, 0, cacheProbability))
|
||||||
|
|
||||||
|
// Test: reace reations
|
||||||
|
assert.NoError(t, c.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationPeace))
|
||||||
|
assert.True(t, controller.FilterBattleOpponents(c, 0, 2, cacheProbability))
|
||||||
|
assert.True(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability))
|
||||||
|
assert.NoError(t, c.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar))
|
||||||
|
|
||||||
|
assert.LessOrEqual(t, controller.DestructionProbability(Cruiser.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass()), 0.)
|
||||||
|
assert.True(t, controller.FilterBattleOpponents(c, 1, 3, cacheProbability))
|
||||||
|
assert.NotContains(t, cacheProbability[1], 3)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package battle
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"maps"
|
||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/iliadenisov/galaxy/internal/controller"
|
|
||||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ type BattleAction struct {
|
|||||||
Destroyed bool
|
Destroyed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func CollectPlanetGroups(c *controller.Cache) map[uint]map[int]bool {
|
func CollectPlanetGroups(c *Cache) map[uint]map[int]bool {
|
||||||
planetGroup := make(map[uint]map[int]bool)
|
planetGroup := make(map[uint]map[int]bool)
|
||||||
for groupIndex := range c.ShipGroupsIndex() {
|
for groupIndex := range c.ShipGroupsIndex() {
|
||||||
state := c.ShipGroup(groupIndex).State()
|
state := c.ShipGroup(groupIndex).State()
|
||||||
@@ -47,11 +46,11 @@ func CollectPlanetGroups(c *controller.Cache) map[uint]map[int]bool {
|
|||||||
return planetGroup
|
return planetGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilterBattleGroups(c *controller.Cache, groups map[int]bool) []int {
|
func FilterBattleGroups(c *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 })
|
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 {
|
func FilterBattleOpponents(c *Cache, attIdx, defIdx int, cacheProbability map[int]map[int]float64) bool {
|
||||||
// Same Race's groups can't attack themselves
|
// Same Race's groups can't attack themselves
|
||||||
if attIdx == defIdx || c.ShipGroupOwnerRaceIndex(attIdx) == c.ShipGroupOwnerRaceIndex(defIdx) {
|
if attIdx == defIdx || c.ShipGroupOwnerRaceIndex(attIdx) == c.ShipGroupOwnerRaceIndex(defIdx) {
|
||||||
return true
|
return true
|
||||||
@@ -84,7 +83,7 @@ func FilterBattleOpponents(c *controller.Cache, attIdx, defIdx int, cacheProbabi
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProduceBattles(c *controller.Cache) []*Battle {
|
func ProduceBattles(c *Cache) []*Battle {
|
||||||
cacheProbability := make(map[int]map[int]float64)
|
cacheProbability := make(map[int]map[int]float64)
|
||||||
defer func() { clear(cacheProbability) }()
|
defer func() { clear(cacheProbability) }()
|
||||||
|
|
||||||
@@ -138,7 +137,7 @@ func ProduceBattles(c *controller.Cache) []*Battle {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func SingleBattle(c *controller.Cache, b *Battle) {
|
func SingleBattle(c *Cache, b *Battle) {
|
||||||
for len(b.attacker) > 0 {
|
for len(b.attacker) > 0 {
|
||||||
attackers := slices.Collect(maps.Keys(b.attacker))
|
attackers := slices.Collect(maps.Keys(b.attacker))
|
||||||
attIdx := attackers[rand.IntN(len(attackers))]
|
attIdx := attackers[rand.IntN(len(attackers))]
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
package turn
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/iliadenisov/galaxy/internal/controller"
|
// "github.com/iliadenisov/galaxy/internal/controller"
|
||||||
"github.com/iliadenisov/galaxy/internal/game/battle"
|
// "github.com/iliadenisov/galaxy/internal/game/battle"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TransformBattle(c *controller.Cache, b *battle.Battle) *game.BattleReport {
|
func TransformBattle(c *Cache, b *Battle) *game.BattleReport {
|
||||||
r := &game.BattleReport{
|
r := &game.BattleReport{
|
||||||
ID: b.ID,
|
ID: b.ID,
|
||||||
Planet: b.Planet,
|
Planet: b.Planet,
|
||||||
PlanetName: c.Planet(b.Planet).Name,
|
PlanetName: c.MustPlanet(b.Planet).Name,
|
||||||
Races: make(map[int]string),
|
Races: make(map[int]string),
|
||||||
Ships: make(map[int]string),
|
Ships: make(map[int]string),
|
||||||
Protocol: make([]game.BattleActionReport, len(b.Protocol)),
|
Protocol: make([]game.BattleActionReport, len(b.Protocol)),
|
||||||
@@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"iter"
|
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -30,6 +29,7 @@ func NewCache(g *game.Game) *Cache {
|
|||||||
|
|
||||||
func (c *Cache) Relation(r1, r2 int) game.Relation {
|
func (c *Cache) Relation(r1, r2 int) game.Relation {
|
||||||
if c.cacheRelation == nil {
|
if c.cacheRelation == nil {
|
||||||
|
c.cacheRelation = make(map[int]map[int]game.Relation)
|
||||||
for r1 := range c.g.Race {
|
for r1 := range c.g.Race {
|
||||||
for r2 := range c.g.Race {
|
for r2 := range c.g.Race {
|
||||||
if r1 == r2 {
|
if r1 == r2 {
|
||||||
@@ -39,10 +39,11 @@ func (c *Cache) Relation(r1, r2 int) game.Relation {
|
|||||||
if rel < 0 {
|
if rel < 0 {
|
||||||
panic(fmt.Sprintf("Relation: opponent not found idx=%d", r2))
|
panic(fmt.Sprintf("Relation: opponent not found idx=%d", r2))
|
||||||
}
|
}
|
||||||
if _, ok := c.cacheRelation[r1]; !ok {
|
c.updateRelationCache(r1, r2, c.g.Race[r1].Relations[rel].Relation)
|
||||||
c.cacheRelation[r1] = make(map[int]game.Relation)
|
// 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
|
// }
|
||||||
|
// c.cacheRelation[r1][r2] = c.g.Race[r1].Relations[rel].Relation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,18 +58,17 @@ func (c *Cache) Relation(r1, r2 int) game.Relation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Planet(planetNumber uint) *game.Planet {
|
func (c *Cache) updateRelationCache(r1, r2 int, rel game.Relation) {
|
||||||
if c.planetByPlanetNumber == nil {
|
if r1 == r2 {
|
||||||
c.planetByPlanetNumber = make(map[uint]*game.Planet)
|
return
|
||||||
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 {
|
if c.cacheRelation == nil {
|
||||||
return v
|
c.cacheRelation = make(map[int]map[int]game.Relation)
|
||||||
} else {
|
|
||||||
panic(fmt.Sprintf("Planet: not found by number=%d", planetNumber))
|
|
||||||
}
|
}
|
||||||
|
if _, ok := c.cacheRelation[r1]; !ok {
|
||||||
|
c.cacheRelation[r1] = make(map[int]game.Relation)
|
||||||
|
}
|
||||||
|
c.cacheRelation[r1][r2] = rel
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) ShipGroupShipClass(groupIndex int) *game.ShipType {
|
func (c *Cache) ShipGroupShipClass(groupIndex int) *game.ShipType {
|
||||||
@@ -97,56 +97,6 @@ func (c *Cache) RaceIndex(ID uuid.UUID) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func (c *Cache) unsafeDeleteShipGroup(i int) {
|
||||||
c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...)
|
c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...)
|
||||||
delete(c.raceIndexByShipGroupIndex, i)
|
delete(c.raceIndexByShipGroupIndex, i)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// validateTypeName always return v without leading and trailing spaces
|
||||||
|
func validateTypeName(v string) (string, bool) {
|
||||||
|
s := strings.TrimSpace(v)
|
||||||
|
if len(s) > 0 {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
// TODO: special symbols AND include error check in all user-input test
|
||||||
|
return s, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package controller_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Race_0 = game.Race{
|
||||||
|
ID: Race_0_ID,
|
||||||
|
Vote: Race_0_ID,
|
||||||
|
Name: "Race_0",
|
||||||
|
Tech: map[game.Tech]float64{
|
||||||
|
game.TechDrive: 1.1,
|
||||||
|
game.TechWeapons: 1.2,
|
||||||
|
game.TechShields: 1.3,
|
||||||
|
game.TechCargo: 1.4,
|
||||||
|
},
|
||||||
|
Relations: []game.RaceRelation{{RaceID: Race_1_ID, Relation: game.RelationWar}},
|
||||||
|
}
|
||||||
|
Race_1 = game.Race{
|
||||||
|
ID: Race_1_ID,
|
||||||
|
Vote: Race_1_ID,
|
||||||
|
Name: "Race_1",
|
||||||
|
Tech: map[game.Tech]float64{
|
||||||
|
game.TechDrive: 2.1,
|
||||||
|
game.TechWeapons: 2.2,
|
||||||
|
game.TechShields: 2.3,
|
||||||
|
game.TechCargo: 2.4,
|
||||||
|
},
|
||||||
|
Relations: []game.RaceRelation{{RaceID: Race_0_ID, Relation: game.RelationPeace}},
|
||||||
|
}
|
||||||
|
|
||||||
|
Race_0_ID = uuid.New()
|
||||||
|
Race_0_idx = 0
|
||||||
|
Race_0_Gunship = "R0_Gunship"
|
||||||
|
Race_0_Freighter = "R0_Freighter"
|
||||||
|
R0_Planet_0_num uint = 0
|
||||||
|
R0_Planet_2_num uint = 2
|
||||||
|
Race_0_Gunship_idx = 0
|
||||||
|
Race_0_Freighter_idx = 1
|
||||||
|
Race_0_Cruiser_idx = 2
|
||||||
|
|
||||||
|
Race_1_ID = uuid.New()
|
||||||
|
Race_1_idx = 1
|
||||||
|
Race_1_Gunship = "R1_Gunship"
|
||||||
|
Race_1_Freighter = "R1_Freighter"
|
||||||
|
R1_Planet_1_num uint = 1
|
||||||
|
Race_1_Gunship_idx = 0
|
||||||
|
Race_1_Freighter_idx = 1
|
||||||
|
Race_1_Cruiser_idx = 2
|
||||||
|
|
||||||
|
ShipType_Cruiser = "Cruiser"
|
||||||
|
|
||||||
|
Cruiser = game.ShipType{
|
||||||
|
ShipTypeReport: game.ShipTypeReport{
|
||||||
|
Name: "Cruiser",
|
||||||
|
Drive: 15,
|
||||||
|
Armament: 1,
|
||||||
|
Weapons: 15,
|
||||||
|
Shields: 15,
|
||||||
|
Cargo: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertNoError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("init assertion failed: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGame() *game.Game {
|
||||||
|
g := &game.Game{
|
||||||
|
Race: []game.Race{
|
||||||
|
Race_0,
|
||||||
|
Race_1,
|
||||||
|
},
|
||||||
|
Map: game.Map{
|
||||||
|
Width: 1000,
|
||||||
|
Height: 1000,
|
||||||
|
Planet: []game.Planet{
|
||||||
|
controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 0, 0, 100, 100, 100, 0, game.ProductionNone.AsType(uuid.Nil)),
|
||||||
|
controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
|
||||||
|
controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
|
||||||
|
controller.NewPlanet(3, "Planet_3", uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assertNoError(g.CreateShipType(Race_0.Name, Race_0_Gunship, 60, 30, 100, 0, 3))
|
||||||
|
assertNoError(g.CreateShipType(Race_0.Name, Race_0_Freighter, 8, 0, 2, 10, 0))
|
||||||
|
assertNoError(g.CreateShipType(Race_0.Name, ShipType_Cruiser, Cruiser.Drive, Cruiser.Weapons, Cruiser.Shields, Cruiser.Cargo, int(Cruiser.Armament)))
|
||||||
|
|
||||||
|
assertNoError(g.CreateShipType(Race_1.Name, Race_1_Gunship, 60, 30, 100, 0, 3))
|
||||||
|
assertNoError(g.CreateShipType(Race_1.Name, Race_1_Freighter, 8, 0, 2, 10, 0))
|
||||||
|
assertNoError(g.CreateShipType(Race_1.Name, ShipType_Cruiser, 15, 15, 15, 0, 2)) // same name - different type (why.)
|
||||||
|
return g
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCache() (*controller.Cache, *game.Game) {
|
||||||
|
g := newGame()
|
||||||
|
c := controller.NewCache(g)
|
||||||
|
return c, g
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package turn
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"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/game/battle"
|
||||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MakeTurn(c *controller.Controller, r controller.Repo, g *game.Game) error {
|
func MakeTurn(c *Controller, r Repo, g *game.Game) error {
|
||||||
// Next turn
|
// Next turn
|
||||||
g.Age += 1
|
g.Age += 1
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ func MakeTurn(c *controller.Controller, r controller.Repo, g *game.Game) error {
|
|||||||
game.JoinEqualGroups(g)
|
game.JoinEqualGroups(g)
|
||||||
|
|
||||||
// 02. Враждующие корабли вступают в схватку.
|
// 02. Враждующие корабли вступают в схватку.
|
||||||
battles := battle.ProduceBattles(c.Cache)
|
battles := 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 {
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Cache) Planet(planetNumber uint) (*game.Planet, bool) {
|
||||||
|
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, true
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) MustPlanet(planetNumber uint) *game.Planet {
|
||||||
|
if v, ok := c.Planet(planetNumber); ok {
|
||||||
|
return v
|
||||||
|
} else {
|
||||||
|
panic(fmt.Sprintf("Planet: not found by number=%d", planetNumber))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
e "github.com/iliadenisov/galaxy/internal/error"
|
||||||
|
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
// func (c *Cache) CmdRelation(hostRace, opponentRace string) (game.RaceRelation, error) {
|
||||||
|
// ri, err := c.raceIndex(hostRace)
|
||||||
|
// if err != nil {
|
||||||
|
// return game.RaceRelation{}, err
|
||||||
|
// }
|
||||||
|
// other, err := c.raceIndex(opponentRace)
|
||||||
|
// if err != nil {
|
||||||
|
// return game.RaceRelation{}, err
|
||||||
|
// }
|
||||||
|
// if ri == other {
|
||||||
|
// return game.RaceRelation{
|
||||||
|
// RaceID: c.g.Race[ri].ID,
|
||||||
|
// Relation: game.RelationPeace,
|
||||||
|
// }, nil
|
||||||
|
// }
|
||||||
|
// rel := slices.IndexFunc(c.g.Race[ri].Relations, func(r game.RaceRelation) bool { return r.RaceID == c.g.Race[other].ID })
|
||||||
|
// if rel < 0 {
|
||||||
|
// return game.RaceRelation{}, e.NewGameStateError("Relation: opponent not found")
|
||||||
|
// }
|
||||||
|
// return c.g.Race[ri].Relations[rel], nil
|
||||||
|
|
||||||
|
// // return g.relationInternal(ri, other)
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (c *Cache) UpdateRelation(race, opponent string, rel game.Relation) error {
|
||||||
|
r1, err := c.raceIndex(race)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var r2 int
|
||||||
|
if race == opponent {
|
||||||
|
r2 = r1
|
||||||
|
} else if r2, err = c.raceIndex(opponent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r2 == r1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for o := range c.g.Race[r1].Relations {
|
||||||
|
if c.g.Race[r1].Relations[o].RaceID == c.g.Race[r2].ID {
|
||||||
|
c.g.Race[r1].Relations[o].Relation = rel
|
||||||
|
if c.cacheRelation != nil {
|
||||||
|
c.updateRelationCache(r1, r2, rel)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// switch {
|
||||||
|
// case r1 == r2:
|
||||||
|
// c.g.Race[r1].Relations[o].Relation = rel
|
||||||
|
// case c.g.Race[r1].Relations[o].RaceID == c.g.Race[r2].ID:
|
||||||
|
// c.g.Race[r1].Relations[o].Relation = rel
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return e.NewGameStateError("UpdateRelation: opponent not found")
|
||||||
|
// if r1 != r2 {
|
||||||
|
// return e.NewGameStateError("UpdateRelation: opponent not found")
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// return g.updateRelationInternal(ri, other, rel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) raceIndex(name string) (int, error) {
|
||||||
|
i := slices.IndexFunc(c.g.Race, func(r game.Race) bool { return r.Name == name })
|
||||||
|
if i < 0 {
|
||||||
|
return i, e.NewRaceUnknownError(name)
|
||||||
|
}
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
e "github.com/iliadenisov/galaxy/internal/error"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Cache) ShipClass(ri int, name string) (*game.ShipType, int, bool) {
|
||||||
|
i := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == name })
|
||||||
|
if i < 0 {
|
||||||
|
return nil, -1, false
|
||||||
|
}
|
||||||
|
return &c.g.Race[ri].ShipTypes[i], i, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) CreateShipType(raceName, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error {
|
||||||
|
ri, err := c.raceIndex(raceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = c.createShipTypeInternal(ri, typeName, drive, ammo, weapons, shileds, cargo)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) createShipTypeInternal(ri int, name string, drive float64, ammo int, weapons, shileds, cargo float64) (int, error) {
|
||||||
|
if err := checkShipTypeValues(drive, ammo, weapons, shileds, cargo); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
n, ok := validateTypeName(name)
|
||||||
|
if !ok {
|
||||||
|
return -1, e.NewEntityTypeNameValidationError("%q", n)
|
||||||
|
}
|
||||||
|
if st := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == name }); st >= 0 {
|
||||||
|
return -1, e.NewEntityTypeNameDuplicateError("ship type %w", c.g.Race[ri].ShipTypes[st].Name)
|
||||||
|
}
|
||||||
|
c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes, game.ShipType{
|
||||||
|
ID: uuid.New(),
|
||||||
|
ShipTypeReport: game.ShipTypeReport{
|
||||||
|
Name: n,
|
||||||
|
Drive: drive,
|
||||||
|
Weapons: weapons,
|
||||||
|
Shields: shileds,
|
||||||
|
Cargo: cargo,
|
||||||
|
Armament: uint(ammo),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return len(c.g.Race[ri].ShipTypes) - 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkShipTypeValues(d float64, a int, w, s, c float64) error {
|
||||||
|
if !checkShipTypeValueDWSC(d) {
|
||||||
|
return e.NewDriveValueError(d)
|
||||||
|
}
|
||||||
|
if !checkShipTypeValueDWSC(w) {
|
||||||
|
return e.NewWeaponsValueError(w)
|
||||||
|
}
|
||||||
|
if !checkShipTypeValueDWSC(s) {
|
||||||
|
return e.NewShieldsValueError(s)
|
||||||
|
}
|
||||||
|
if !checkShipTypeValueDWSC(c) {
|
||||||
|
return e.NewCargoValueError(s)
|
||||||
|
}
|
||||||
|
if a < 0 {
|
||||||
|
return e.NewShipTypeArmamentValueError(a)
|
||||||
|
}
|
||||||
|
if (w == 0 && a > 0) || (a == 0 && w > 0) {
|
||||||
|
return e.NewShipTypeArmamentAndWeaponsValueError("A=%d W=%.0f", a, w)
|
||||||
|
}
|
||||||
|
if d == 0 && w == 0 && s == 0 && c == 0 && a == 0 {
|
||||||
|
return e.NewShipTypeShipTypeZeroValuesError()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkShipTypeValueDWSC(v float64) bool {
|
||||||
|
return v == 0 || v >= 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
|
||||||
|
e "github.com/iliadenisov/galaxy/internal/error"
|
||||||
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Cache) CreateShips(ri int, shipTypeName string, planetNumber uint, quantity int) error {
|
||||||
|
class, _, ok := c.ShipClass(ri, shipTypeName)
|
||||||
|
if !ok {
|
||||||
|
return e.NewEntityNotExistsError("ship class %w", shipTypeName)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
p, ok := c.Planet(planetNumber)
|
||||||
|
if !ok {
|
||||||
|
return e.NewEntityNotExistsError("planet #%d", planetNumber)
|
||||||
|
}
|
||||||
|
if p.Owner != c.g.Race[ri].ID {
|
||||||
|
return e.NewEntityNotOwnedError("planet #%d", planetNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIndex := c.ShipGroupMaxIndex(ri) + 1
|
||||||
|
c.g.ShipGroups = append(c.g.ShipGroups, game.ShipGroup{
|
||||||
|
Index: nextIndex,
|
||||||
|
OwnerID: c.g.Race[ri].ID,
|
||||||
|
TypeID: class.ID,
|
||||||
|
Destination: p.Number,
|
||||||
|
Number: uint(quantity),
|
||||||
|
Tech: map[game.Tech]float64{
|
||||||
|
game.TechDrive: c.g.Race[ri].TechLevel(game.TechDrive),
|
||||||
|
game.TechWeapons: c.g.Race[ri].TechLevel(game.TechWeapons),
|
||||||
|
game.TechShields: c.g.Race[ri].TechLevel(game.TechShields),
|
||||||
|
game.TechCargo: c.g.Race[ri].TechLevel(game.TechCargo),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if c.raceIndexByShipGroupIndex != nil {
|
||||||
|
c.raceIndexByShipGroupIndex[len(c.g.ShipGroups)-1] = ri
|
||||||
|
}
|
||||||
|
if c.shipClassByShipGroupIndex != nil {
|
||||||
|
c.shipClassByShipGroupIndex[len(c.g.ShipGroups)-1] = class
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) ShipGroupMaxIndex(ri int) uint {
|
||||||
|
var max uint = 0
|
||||||
|
for i := range c.g.ShipGroups {
|
||||||
|
if r := c.ShipGroupOwnerRaceIndex(i); r == ri && c.ShipGroup(i).Index > max {
|
||||||
|
max = c.ShipGroup(i).Index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,12 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/iliadenisov/galaxy/internal/controller"
|
"github.com/iliadenisov/galaxy/internal/controller"
|
||||||
"github.com/iliadenisov/galaxy/internal/game/turn"
|
|
||||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
"github.com/iliadenisov/galaxy/internal/model/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
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(c, r, g) })
|
c.ExecuteGame(func(r controller.Repo, g *game.Game) { controller.MakeTurn(c, r, g) })
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func g(t *testing.T, f func(p func(*controller.Param), g func() *mg.Game)) {
|
|||||||
g, err := game.LoadState(p)
|
g, err := game.LoadState(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
assert.FailNow(t, "g: LoadState", err)
|
assert.FailNow(t, "g: LoadState", err)
|
||||||
return nil // mg.Game{}
|
return nil
|
||||||
}
|
}
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,40 +2,10 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"math/rand/v2"
|
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Battle struct {
|
|
||||||
ID uuid.UUID
|
|
||||||
Planet uint
|
|
||||||
// True = In_Battle, False = Out_Battle
|
|
||||||
observerGroups map[int]bool
|
|
||||||
Protocol []BattleAction
|
|
||||||
|
|
||||||
shipAmmo map[int]uint
|
|
||||||
shipName map[int]string
|
|
||||||
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 (b Battle) ShipClassName(groupIndex int) string {
|
|
||||||
if v, ok := b.shipName[groupIndex]; ok {
|
|
||||||
return v
|
|
||||||
} else {
|
|
||||||
panic(fmt.Sprintf("Battle.ShipClassName: no name stored for groupIndex=%d", groupIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BattleReport struct {
|
type BattleReport struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Planet uint `json:"planet"`
|
Planet uint `json:"planet"`
|
||||||
@@ -53,251 +23,6 @@ type BattleActionReport struct {
|
|||||||
Destroyed bool `json:"d"`
|
Destroyed bool `json:"d"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShipClassBattle struct {
|
|
||||||
ClassName string `json:"class"`
|
|
||||||
Tech TechSet `json:"tech"`
|
|
||||||
Number uint `json:"number"`
|
|
||||||
CargoType *CargoType `json:"loadType,omitempty"`
|
|
||||||
Quantity float64 `json:"quantity"`
|
|
||||||
Left uint `json:"left"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func CollectPlanetGroups(g *Game, cacheShipGroupRaceID map[int]int, cacheShipClass map[int]*ShipType) map[uint]map[int]bool {
|
|
||||||
planetGroup := make(map[uint]map[int]bool)
|
|
||||||
for groupIndex := range g.ShipGroups {
|
|
||||||
state := g.ShipGroups[groupIndex].State()
|
|
||||||
if state == StateInOrbit || state == StateUpgrade {
|
|
||||||
planetNumber := g.ShipGroups[groupIndex].Destination
|
|
||||||
if _, ok := planetGroup[planetNumber]; !ok {
|
|
||||||
planetGroup[planetNumber] = make(map[int]bool)
|
|
||||||
}
|
|
||||||
planetGroup[planetNumber][groupIndex] = false
|
|
||||||
|
|
||||||
if _, ok := cacheShipGroupRaceID[groupIndex]; !ok {
|
|
||||||
cacheShipGroupRaceID[groupIndex] = RaceIndex(g, g.ShipGroups[groupIndex].OwnerID)
|
|
||||||
}
|
|
||||||
ri := cacheShipGroupRaceID[groupIndex]
|
|
||||||
|
|
||||||
if _, ok := cacheShipClass[groupIndex]; !ok {
|
|
||||||
sti, ok := ShipClassIndex(g, ri, g.ShipGroups[groupIndex].TypeID)
|
|
||||||
if !ok {
|
|
||||||
panic(fmt.Sprintf("CollectPlanetGroups: ship class not found for race=%q group=%v", g.Race[ri].Name, g.ShipGroups[groupIndex].Index))
|
|
||||||
}
|
|
||||||
cacheShipClass[groupIndex] = &g.Race[ri].ShipTypes[sti]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for pl := range planetGroup {
|
|
||||||
if len(planetGroup[pl]) < 2 {
|
|
||||||
delete(planetGroup, pl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return planetGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
func FilterBattleGroups(g *Game, groups map[int]bool) []int {
|
|
||||||
return slices.DeleteFunc(slices.Collect(maps.Keys(groups)), func(groupIndex int) bool { return g.ShipGroups[groupIndex].State() != StateInOrbit })
|
|
||||||
}
|
|
||||||
|
|
||||||
func CacheRelations(g *Game, cacheShipGroupRaceID map[int]int) map[int]map[int]Relation {
|
|
||||||
cache := make(map[int]map[int]Relation)
|
|
||||||
ri := make(map[int]bool)
|
|
||||||
for _, raceIdx := range cacheShipGroupRaceID {
|
|
||||||
ri[raceIdx] = true
|
|
||||||
}
|
|
||||||
for r1 := range ri {
|
|
||||||
for r2 := range ri {
|
|
||||||
if r1 == r2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
rel, err := g.relationInternal(r1, r2)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if _, ok := cache[r1]; !ok {
|
|
||||||
cache[r1] = make(map[int]Relation)
|
|
||||||
}
|
|
||||||
cache[r1][r2] = rel.Relation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cache
|
|
||||||
}
|
|
||||||
|
|
||||||
func FilterBattleOpponents(
|
|
||||||
g *Game,
|
|
||||||
attIdx, defIdx int,
|
|
||||||
cacheShipGroupRaceID map[int]int,
|
|
||||||
cacheRelation map[int]map[int]Relation,
|
|
||||||
cacheShipClass map[int]*ShipType,
|
|
||||||
cacheProbability map[int]map[int]float64,
|
|
||||||
) bool {
|
|
||||||
// Same Race's groups can't attack themselves
|
|
||||||
if attIdx == defIdx || g.ShipGroups[attIdx].OwnerID == g.ShipGroups[defIdx].OwnerID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any opponent has War relation to another, both will stay in battle
|
|
||||||
if cacheRelation[cacheShipGroupRaceID[attIdx]][cacheShipGroupRaceID[defIdx]] == RelationPeace &&
|
|
||||||
cacheRelation[cacheShipGroupRaceID[defIdx]][cacheShipGroupRaceID[attIdx]] == RelationPeace {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// p := DestructionProbability(
|
|
||||||
// cacheShipClass[attIdx].Weapons,
|
|
||||||
// g.ShipGroups[attIdx].TechLevel(TechWeapons),
|
|
||||||
// cacheShipClass[defIdx].Shields,
|
|
||||||
// g.ShipGroups[defIdx].TechLevel(TechShields),
|
|
||||||
// g.ShipGroups[defIdx].FullMass(cacheShipClass[defIdx]),
|
|
||||||
// )
|
|
||||||
p := 0.
|
|
||||||
// Exclude opponent's group which cannot be probably destroyed
|
|
||||||
if p <= 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := cacheProbability[attIdx]; !ok {
|
|
||||||
cacheProbability[attIdx] = make(map[int]float64)
|
|
||||||
}
|
|
||||||
cacheProbability[attIdx][defIdx] = p
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProduceBattles(g *Game) []*Battle {
|
|
||||||
cacheShipGroupRaceID := make(map[int]int)
|
|
||||||
cacheShipClass := make(map[int]*ShipType)
|
|
||||||
cacheProbability := make(map[int]map[int]float64)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
clear(cacheShipGroupRaceID)
|
|
||||||
clear(cacheShipClass)
|
|
||||||
clear(cacheProbability)
|
|
||||||
}()
|
|
||||||
|
|
||||||
planetGroups := CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass)
|
|
||||||
if len(planetGroups) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheRelation := CacheRelations(g, cacheShipGroupRaceID)
|
|
||||||
defer func() {
|
|
||||||
clear(cacheRelation)
|
|
||||||
}()
|
|
||||||
|
|
||||||
result := make([]*Battle, 0)
|
|
||||||
|
|
||||||
for pl, observerGroups := range planetGroups {
|
|
||||||
battleGroups := FilterBattleGroups(g, observerGroups)
|
|
||||||
b := &Battle{
|
|
||||||
Planet: pl,
|
|
||||||
observerGroups: observerGroups,
|
|
||||||
attacker: make(map[int]map[int]float64),
|
|
||||||
shipAmmo: make(map[int]uint),
|
|
||||||
shipName: make(map[int]string),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range battleGroups {
|
|
||||||
attIdx := battleGroups[i]
|
|
||||||
|
|
||||||
// Ships with no Ammo will never attack somebody
|
|
||||||
if cacheShipClass[attIdx].Armament == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
opponents := slices.DeleteFunc(slices.Clone(battleGroups), func(defIdx int) bool {
|
|
||||||
return FilterBattleOpponents(g, attIdx, defIdx, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability)
|
|
||||||
})
|
|
||||||
if len(opponents) > 0 {
|
|
||||||
b.shipAmmo[attIdx] = cacheShipClass[attIdx].Armament
|
|
||||||
b.shipName[attIdx] = cacheShipClass[attIdx].Name
|
|
||||||
b.observerGroups[attIdx] = true
|
|
||||||
for _, defIdx := range opponents {
|
|
||||||
b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx]
|
|
||||||
b.shipName[defIdx] = cacheShipClass[defIdx].Name
|
|
||||||
b.observerGroups[defIdx] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(b.attacker) > 0 {
|
|
||||||
SingleBattle(g, b)
|
|
||||||
b.ID = uuid.New()
|
|
||||||
result = append(result, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(b.attacker)
|
|
||||||
clear(b.shipAmmo)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func SingleBattle(g *Game, b *Battle) {
|
|
||||||
for len(b.attacker) > 0 {
|
|
||||||
attackers := slices.Collect(maps.Keys(b.attacker))
|
|
||||||
attIdx := attackers[rand.IntN(len(attackers))]
|
|
||||||
|
|
||||||
for range b.shipAmmo[attIdx] {
|
|
||||||
defenders := slices.Collect(maps.Keys(b.attacker[attIdx]))
|
|
||||||
defIdx := defenders[rand.IntN(len(defenders))]
|
|
||||||
destroyed := false
|
|
||||||
|
|
||||||
probability := b.attacker[attIdx][defIdx]
|
|
||||||
switch {
|
|
||||||
case probability >= 1:
|
|
||||||
destroyed = true
|
|
||||||
case probability > 0:
|
|
||||||
destroyed = rand.Float64() >= probability
|
|
||||||
default:
|
|
||||||
panic("SingleBattle: probability unexpected: value <= 0")
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Protocol = append(b.Protocol, BattleAction{
|
|
||||||
Attacker: attIdx,
|
|
||||||
Defenter: defIdx,
|
|
||||||
Destroyed: destroyed,
|
|
||||||
})
|
|
||||||
|
|
||||||
if destroyed {
|
|
||||||
g.ShipGroups[defIdx].Number--
|
|
||||||
}
|
|
||||||
if g.ShipGroups[defIdx].Number == 0 {
|
|
||||||
delete(b.attacker, defIdx) // Eliminated group cant attack anyone
|
|
||||||
for attIdx := range b.attacker {
|
|
||||||
delete(b.attacker[attIdx], defIdx) // Attackers can't attack eliminated group anymore
|
|
||||||
if len(b.attacker[attIdx]) == 0 {
|
|
||||||
delete(b.attacker, attIdx) // Remove attacker if he lost all opponents
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// FIXME: удалять ShipGroups после генерирования пользовательского отчёта
|
|
||||||
// g.ShipGroups = append(g.ShipGroups[:defIdx], g.ShipGroups[defIdx+1:]...)
|
|
||||||
}
|
|
||||||
if len(b.attacker) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func RaceIndex(g *Game, ID uuid.UUID) int {
|
|
||||||
i := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == ID })
|
|
||||||
if i < 0 {
|
|
||||||
panic(fmt.Sprintf("RaceIndex: race not found by ID=%v", ID))
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 {
|
|
||||||
// effAttack := attWeapons * attWeaponsTech
|
|
||||||
// effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass)
|
|
||||||
// return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 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) {
|
func (b BattleReport) MarshalBinary() (data []byte, err error) {
|
||||||
return json.Marshal(&b)
|
return json.Marshal(&b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
package game_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/iliadenisov/galaxy/internal/game/battle"
|
|
||||||
"github.com/iliadenisov/galaxy/internal/model/game"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ship = game.ShipType{
|
|
||||||
ShipTypeReport: game.ShipTypeReport{
|
|
||||||
Name: "Ship",
|
|
||||||
Drive: 10,
|
|
||||||
Armament: 1,
|
|
||||||
Weapons: 10,
|
|
||||||
Shields: 10,
|
|
||||||
Cargo: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCollectPlanetGroups(t *testing.T) {
|
|
||||||
g := newGame()
|
|
||||||
|
|
||||||
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) // 0
|
|
||||||
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 1
|
|
||||||
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 2
|
|
||||||
g.ShipGroups[2].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} // 2 -> In_Space
|
|
||||||
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 3
|
|
||||||
g.ShipGroups[3].Destination = R1_Planet_1_num // 3 -> Planet_1
|
|
||||||
assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 4
|
|
||||||
g.ShipGroups[4].Destination = R0_Planet_0_num // 4 -> Planet_0
|
|
||||||
|
|
||||||
cacheShipGroupRaceID := make(map[int]int)
|
|
||||||
cacheShipClass := make(map[int]*game.ShipType)
|
|
||||||
planetGroups := game.CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass)
|
|
||||||
|
|
||||||
for pl := range planetGroups {
|
|
||||||
switch pl {
|
|
||||||
case R0_Planet_0_num:
|
|
||||||
assert.Equal(t, 3, len(planetGroups[pl]))
|
|
||||||
assert.Contains(t, planetGroups[pl], 0)
|
|
||||||
assert.Contains(t, planetGroups[pl], 1)
|
|
||||||
assert.Contains(t, planetGroups[pl], 4)
|
|
||||||
default:
|
|
||||||
assert.Fail(t, "planet #%d should not contain groups for battle", pl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.Len(t, cacheShipGroupRaceID, 4)
|
|
||||||
assert.Contains(t, cacheShipGroupRaceID, 0)
|
|
||||||
assert.Contains(t, cacheShipGroupRaceID, 1)
|
|
||||||
assert.Contains(t, cacheShipGroupRaceID, 3)
|
|
||||||
assert.Contains(t, cacheShipGroupRaceID, 4)
|
|
||||||
assert.Equal(t, Race_0_idx, cacheShipGroupRaceID[0])
|
|
||||||
assert.Equal(t, Race_0_idx, cacheShipGroupRaceID[1])
|
|
||||||
assert.Equal(t, Race_0_idx, cacheShipGroupRaceID[3])
|
|
||||||
assert.Equal(t, Race_1_idx, cacheShipGroupRaceID[4])
|
|
||||||
|
|
||||||
assert.Len(t, cacheShipClass, 4) // all registered ship classes for all In_Orbit ship groups
|
|
||||||
|
|
||||||
cacheRelation := game.CacheRelations(g, cacheShipGroupRaceID)
|
|
||||||
assert.Len(t, cacheRelation, 2)
|
|
||||||
assert.Len(t, cacheRelation[Race_0_idx], 1)
|
|
||||||
assert.Len(t, cacheRelation[Race_1_idx], 1)
|
|
||||||
assert.Equal(t, game.RelationWar, cacheRelation[Race_0_idx][Race_1_idx])
|
|
||||||
assert.Equal(t, game.RelationPeace, cacheRelation[Race_1_idx][Race_0_idx])
|
|
||||||
assert.Empty(t, cacheRelation[Race_0_idx][Race_0_idx])
|
|
||||||
assert.Empty(t, cacheRelation[Race_1_idx][Race_1_idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterBattleOpponents(t *testing.T) {
|
|
||||||
g := newGame()
|
|
||||||
|
|
||||||
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) // 0
|
|
||||||
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 1
|
|
||||||
assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 2
|
|
||||||
undefeatedShip := ship
|
|
||||||
undefeatedShip.Shields = 100
|
|
||||||
assert.NoError(t, g.CreateShipType(Race_1.Name, undefeatedShip.Name, undefeatedShip.Drive, undefeatedShip.Weapons, undefeatedShip.Shields, undefeatedShip.Cargo, int(undefeatedShip.Armament)))
|
|
||||||
assert.NoError(t, g.CreateShips(Race_1_idx, undefeatedShip.Name, R1_Planet_1_num, 1)) // 3
|
|
||||||
|
|
||||||
cacheShipGroupRaceID := make(map[int]int)
|
|
||||||
cacheShipClass := make(map[int]*game.ShipType)
|
|
||||||
cacheProbability := make(map[int]map[int]float64)
|
|
||||||
cacheRelation := make(map[int]map[int]game.Relation)
|
|
||||||
|
|
||||||
game.CollectPlanetGroups(g, cacheShipGroupRaceID, cacheShipClass)
|
|
||||||
|
|
||||||
cacheRelation[Race_0_idx] = make(map[int]game.Relation)
|
|
||||||
cacheRelation[Race_1_idx] = make(map[int]game.Relation)
|
|
||||||
cacheRelation[Race_0_idx][Race_1_idx] = game.RelationPeace
|
|
||||||
cacheRelation[Race_1_idx][Race_0_idx] = game.RelationWar
|
|
||||||
|
|
||||||
assert.False(t, game.FilterBattleOpponents(g, 0, 2, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
|
|
||||||
assert.Contains(t, cacheProbability, 0)
|
|
||||||
assert.Contains(t, cacheProbability[0], 2)
|
|
||||||
assert.InDelta(t, 0.396222, cacheProbability[0][2], 0.0000001)
|
|
||||||
assert.False(t, game.FilterBattleOpponents(g, 2, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
|
|
||||||
assert.Contains(t, cacheProbability, 2)
|
|
||||||
assert.Contains(t, cacheProbability[2], 0)
|
|
||||||
assert.InDelta(t, 0.495, cacheProbability[2][0], 0.0001)
|
|
||||||
|
|
||||||
// Test: same owner
|
|
||||||
assert.True(t, game.FilterBattleOpponents(g, 0, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
|
|
||||||
assert.True(t, game.FilterBattleOpponents(g, 0, 1, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
|
|
||||||
assert.True(t, game.FilterBattleOpponents(g, 1, 0, cacheShipGroupRaceID, cacheRelation, cacheShipClass, cacheProbability))
|
|
||||||
|
|
||||||
// Test: reace reations
|
|
||||||
cacheRelation[Race_1_idx][Race_0_idx] = game.RelationPeace
|
|
||||||
assert.True(t, game.FilterBattleOpponents(g, 0, 2, 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
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user