diff --git a/internal/controller/ballte_test.go b/internal/controller/ballte_test.go new file mode 100644 index 0000000..3c11cd4 --- /dev/null +++ b/internal/controller/ballte_test.go @@ -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) +} diff --git a/internal/game/battle/battle.go b/internal/controller/battle.go similarity index 92% rename from internal/game/battle/battle.go rename to internal/controller/battle.go index 9d94d8a..9ab81a4 100644 --- a/internal/game/battle/battle.go +++ b/internal/controller/battle.go @@ -1,4 +1,4 @@ -package battle +package controller import ( "maps" @@ -7,7 +7,6 @@ import ( "slices" "github.com/google/uuid" - "github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/model/game" ) @@ -27,7 +26,7 @@ type BattleAction struct { 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) for groupIndex := range c.ShipGroupsIndex() { state := c.ShipGroup(groupIndex).State() @@ -47,11 +46,11 @@ func CollectPlanetGroups(c *controller.Cache) map[uint]map[int]bool { 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 }) } -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 if attIdx == defIdx || c.ShipGroupOwnerRaceIndex(attIdx) == c.ShipGroupOwnerRaceIndex(defIdx) { 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) defer func() { clear(cacheProbability) }() @@ -138,7 +137,7 @@ func ProduceBattles(c *controller.Cache) []*Battle { return result } -func SingleBattle(c *controller.Cache, b *Battle) { +func SingleBattle(c *Cache, b *Battle) { for len(b.attacker) > 0 { attackers := slices.Collect(maps.Keys(b.attacker)) attIdx := attackers[rand.IntN(len(attackers))] diff --git a/internal/game/turn/battle.go b/internal/controller/battle_transform.go similarity index 84% rename from internal/game/turn/battle.go rename to internal/controller/battle_transform.go index 7b2c178..353dc29 100644 --- a/internal/game/turn/battle.go +++ b/internal/controller/battle_transform.go @@ -1,16 +1,16 @@ -package turn +package controller import ( - "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game/battle" + // "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 { +func TransformBattle(c *Cache, b *Battle) *game.BattleReport { r := &game.BattleReport{ ID: b.ID, Planet: b.Planet, - PlanetName: c.Planet(b.Planet).Name, + PlanetName: c.MustPlanet(b.Planet).Name, Races: make(map[int]string), Ships: make(map[int]string), Protocol: make([]game.BattleActionReport, len(b.Protocol)), diff --git a/internal/controller/cache.go b/internal/controller/cache.go index d438595..66f9812 100644 --- a/internal/controller/cache.go +++ b/internal/controller/cache.go @@ -2,7 +2,6 @@ package controller import ( "fmt" - "iter" "slices" "github.com/google/uuid" @@ -30,6 +29,7 @@ func NewCache(g *game.Game) *Cache { func (c *Cache) Relation(r1, r2 int) game.Relation { if c.cacheRelation == nil { + c.cacheRelation = make(map[int]map[int]game.Relation) for r1 := range c.g.Race { for r2 := range c.g.Race { if r1 == r2 { @@ -39,10 +39,11 @@ func (c *Cache) Relation(r1, r2 int) game.Relation { 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 + c.updateRelationCache(r1, r2, c.g.Race[r1].Relations[rel].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 } } @@ -57,18 +58,17 @@ func (c *Cache) Relation(r1, r2 int) game.Relation { } } -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] - } +func (c *Cache) updateRelationCache(r1, r2 int, rel game.Relation) { + if r1 == r2 { + return } - if v, ok := c.planetByPlanetNumber[planetNumber]; ok { - return v - } else { - panic(fmt.Sprintf("Planet: not found by number=%d", planetNumber)) + if c.cacheRelation == nil { + c.cacheRelation = make(map[int]map[int]game.Relation) } + 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 { @@ -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) { c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...) delete(c.raceIndexByShipGroupIndex, i) diff --git a/internal/controller/controller_helper.go b/internal/controller/controller_helper.go new file mode 100644 index 0000000..d2db3f3 --- /dev/null +++ b/internal/controller/controller_helper.go @@ -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 +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go new file mode 100644 index 0000000..5190647 --- /dev/null +++ b/internal/controller/controller_test.go @@ -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 +} diff --git a/internal/game/turn/turn.go b/internal/controller/generate_turn.go similarity index 79% rename from internal/game/turn/turn.go rename to internal/controller/generate_turn.go index a660568..b806351 100644 --- a/internal/game/turn/turn.go +++ b/internal/controller/generate_turn.go @@ -1,13 +1,13 @@ -package turn +package controller import ( - "github.com/iliadenisov/galaxy/internal/controller" + // "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/game/battle" "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 g.Age += 1 @@ -15,7 +15,7 @@ func MakeTurn(c *controller.Controller, r controller.Repo, g *game.Game) error { game.JoinEqualGroups(g) // 02. Враждующие корабли вступают в схватку. - battles := battle.ProduceBattles(c.Cache) + battles := ProduceBattles(c.Cache) // Internal control: after battles there are can't be groups with no ships left for i := range g.ShipGroups { diff --git a/internal/controller/planet.go b/internal/controller/planet.go new file mode 100644 index 0000000..9547fe0 --- /dev/null +++ b/internal/controller/planet.go @@ -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)) + } +} diff --git a/internal/controller/race.go b/internal/controller/race.go new file mode 100644 index 0000000..fdc87d1 --- /dev/null +++ b/internal/controller/race.go @@ -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 +} diff --git a/internal/controller/ship_class.go b/internal/controller/ship_class.go new file mode 100644 index 0000000..ed759f0 --- /dev/null +++ b/internal/controller/ship_class.go @@ -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 +} diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go new file mode 100644 index 0000000..2e98ae8 --- /dev/null +++ b/internal/controller/ship_group.go @@ -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) + } + } +} diff --git a/internal/game/battle/battle_test.go b/internal/game/battle/battle_test.go deleted file mode 100644 index a7e0943..0000000 --- a/internal/game/battle/battle_test.go +++ /dev/null @@ -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) -} diff --git a/internal/game/cmd_turn.go b/internal/game/cmd_turn.go index 6b02f33..c4567b6 100644 --- a/internal/game/cmd_turn.go +++ b/internal/game/cmd_turn.go @@ -2,13 +2,12 @@ package game import ( "github.com/iliadenisov/galaxy/internal/controller" - "github.com/iliadenisov/galaxy/internal/game/turn" "github.com/iliadenisov/galaxy/internal/model/game" ) 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(c, r, g) }) + c.ExecuteGame(func(r controller.Repo, g *game.Game) { controller.MakeTurn(c, r, g) }) }) return } diff --git a/internal/game/controller_test.go b/internal/game/controller_test.go index 589eba6..e12d542 100644 --- a/internal/game/controller_test.go +++ b/internal/game/controller_test.go @@ -46,7 +46,7 @@ func g(t *testing.T, f func(p func(*controller.Param), g func() *mg.Game)) { g, err := game.LoadState(p) if err != nil { assert.FailNow(t, "g: LoadState", err) - return nil // mg.Game{} + return nil } return g } diff --git a/internal/model/game/battle.go b/internal/model/game/battle.go index 4314ad0..89183ab 100644 --- a/internal/model/game/battle.go +++ b/internal/model/game/battle.go @@ -2,40 +2,10 @@ package game import ( "encoding/json" - "fmt" - "maps" - "math/rand/v2" - "slices" "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 { ID uuid.UUID `json:"id"` Planet uint `json:"planet"` @@ -53,251 +23,6 @@ type BattleActionReport struct { 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) { return json.Marshal(&b) } diff --git a/internal/model/game/battle_test.go b/internal/model/game/battle_test.go deleted file mode 100644 index 9b37508..0000000 --- a/internal/model/game/battle_test.go +++ /dev/null @@ -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) -}