From 449c3273bfe3e5f6864555cdd935587331782c19 Mon Sep 17 00:00:00 2001 From: IliaDenisov Date: Fri, 6 Feb 2026 17:21:42 +0300 Subject: [PATCH] test: battle multiple non-crossing enemies --- internal/controller/battle.go | 25 ++++--- internal/controller/battle_test.go | 69 +++++++++++++++++++ internal/controller/battle_transform.go | 11 +-- internal/controller/controller_export_test.go | 27 ++++++++ internal/controller/generate_game.go | 7 +- internal/controller/generate_turn.go | 2 +- internal/controller/ship_group.go | 1 - internal/model/game/game.go | 9 +++ internal/model/report/battle.go | 19 ++--- 9 files changed, 129 insertions(+), 41 deletions(-) diff --git a/internal/controller/battle.go b/internal/controller/battle.go index f599a4f..4c6f273 100644 --- a/internal/controller/battle.go +++ b/internal/controller/battle.go @@ -13,8 +13,8 @@ import ( type Battle struct { ID uuid.UUID Planet uint - observerGroups map[int]bool // True = In_Battle, False = Out_Battle - initialNumbers map[int]uint // Initial number of ships in the group + ObserverGroups map[int]bool // True = In_Battle, False = Out_Battle + InitialNumbers map[int]uint // Initial number of ships in the group Protocol []BattleAction shipAmmo map[int]uint @@ -95,21 +95,21 @@ func ProduceBattles(c *Cache) []*Battle { result := make([]*Battle, 0) - // TODO: check this behavior: - // Multiple battles on single planet shoul be produced as single battle too: + // Multiple battles on single planet shoul be produced as single battle: // A <--> B // C <--> D - // where: A in peace with [C, D], B in peace with [C, D], and so on. + // where: [A] and [B] are mutual enemies, as well [C] and [D] for pl, observerGroups := range planetGroups { battleGroups := FilterBattleGroups(c, observerGroups) b := &Battle{ Planet: pl, - observerGroups: observerGroups, + ObserverGroups: observerGroups, + InitialNumbers: make(map[int]uint), attacker: make(map[int]map[int]float64), shipAmmo: make(map[int]uint), } for sgi := range observerGroups { - b.initialNumbers[sgi] = c.ShipGroup(sgi).Number + b.InitialNumbers[sgi] = c.ShipGroup(sgi).Number } for i := range battleGroups { @@ -125,10 +125,13 @@ func ProduceBattles(c *Cache) []*Battle { }) if len(opponents) > 0 { b.shipAmmo[attIdx] = c.ShipGroupShipClass(attIdx).Armament - b.observerGroups[attIdx] = true + b.ObserverGroups[attIdx] = true for _, defIdx := range opponents { + if _, ok := b.attacker[attIdx][defIdx]; !ok { + b.attacker[attIdx] = make(map[int]float64) + } b.attacker[attIdx][defIdx] = cacheProbability[attIdx][defIdx] - b.observerGroups[defIdx] = true + b.ObserverGroups[defIdx] = true } } } @@ -178,13 +181,13 @@ func SingleBattle(c *Cache, b *Battle) { if c.ShipGroup(defIdx).Number == 0 { delete(b.attacker, defIdx) // Eliminated group cant attack anyone for attIdx := range b.attacker { - delete(b.attacker[attIdx], defIdx) // Attackers can't attack eliminated group anymore + delete(b.attacker[attIdx], defIdx) // Other attackers can't attack eliminated group anymore if len(b.attacker[attIdx]) == 0 { delete(b.attacker, attIdx) // Remove attacker if he lost all opponents } } } - if len(b.attacker) == 0 { + if len(b.attacker[attIdx]) == 0 { break } } diff --git a/internal/controller/battle_test.go b/internal/controller/battle_test.go index b716920..9134a29 100644 --- a/internal/controller/battle_test.go +++ b/internal/controller/battle_test.go @@ -1,6 +1,8 @@ package controller_test import ( + "maps" + "slices" "testing" "github.com/iliadenisov/galaxy/internal/controller" @@ -124,3 +126,70 @@ func TestFilterBattleOpponents(t *testing.T) { assert.True(t, controller.FilterBattleOpponents(c, 1, 3, cacheProbability)) assert.NotContains(t, cacheProbability[1], 3) } + +func TestProduceBattles(t *testing.T) { + c, g := newCache() + + race_C_name, race_D_name := "Race_C", "Race_D" + race_C_idx, _ := c.AddRace(race_C_name) + race_D_idx, _ := c.AddRace(race_D_name) + + assert.NoError(t, g.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) + assert.NoError(t, g.UpdateRelation(Race_1.Name, Race_0.Name, game.RelationWar)) + + assert.NoError(t, g.UpdateRelation(race_C_name, race_D_name, game.RelationWar)) + assert.NoError(t, g.UpdateRelation(race_D_name, race_C_name, game.RelationWar)) + + rel, err := g.Relation(Race_0.Name, race_C_name) + assert.NoError(t, err) + assert.Equal(t, game.RelationPeace, rel) + + rel, err = g.Relation(Race_1.Name, race_C_name) + assert.NoError(t, err) + assert.Equal(t, game.RelationPeace, rel) + + rel, err = g.Relation(Race_0.Name, race_D_name) + assert.NoError(t, err) + assert.Equal(t, game.RelationPeace, rel) + + rel, err = g.Relation(Race_1.Name, race_D_name) + assert.NoError(t, err) + assert.Equal(t, game.RelationPeace, rel) + + // Race_0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) + + // Race_1 + c.CreateShipsUnsafe_T(Race_1_idx, c.MustShipClass(Race_1_idx, Race_1_Gunship).ID, R0_Planet_0_num, 11) + + // Race_C + assert.NoError(t, c.CreateShipType(race_C_idx, Cruiser.Name, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F())) + c.CreateShipsUnsafe_T(race_C_idx, c.MustShipClass(race_C_idx, Cruiser.Name).ID, R0_Planet_0_num, 12) + + // Race_D + assert.NoError(t, c.CreateShipType(race_D_idx, Cruiser.Name, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F())) + c.CreateShipsUnsafe_T(race_D_idx, c.MustShipClass(race_D_idx, Cruiser.Name).ID, R0_Planet_0_num, 13) + + battle := controller.ProduceBattles(c) + + assert.Len(t, battle, 1) + b := battle[0] + assert.Equal(t, R0_Planet_0_num, b.Planet) + assert.Len(t, b.ObserverGroups, 4) + assert.Len(t, b.InitialNumbers, 4) + assert.ElementsMatch(t, slices.Collect(maps.Keys(b.ObserverGroups)), slices.Collect(maps.Keys(b.InitialNumbers))) + assert.Equal(t, 10, int(b.InitialNumbers[0])) + assert.Equal(t, 11, int(b.InitialNumbers[1])) + assert.Equal(t, 12, int(b.InitialNumbers[2])) + assert.Equal(t, 13, int(b.InitialNumbers[3])) + if c.ShipGroup(0).Number == 0 { + assert.Greater(t, c.ShipGroup(1).Number, uint(0)) + } else { + assert.Zero(t, c.ShipGroup(1).Number) + } + if c.ShipGroup(2).Number == 0 { + assert.Greater(t, c.ShipGroup(3).Number, uint(0)) + } else { + assert.Zero(t, c.ShipGroup(3).Number) + } +} diff --git a/internal/controller/battle_transform.go b/internal/controller/battle_transform.go index 5026644..0043c45 100644 --- a/internal/controller/battle_transform.go +++ b/internal/controller/battle_transform.go @@ -23,20 +23,13 @@ func TransformBattle(c *Cache, b *Battle) *report.BattleReport { sg := c.ShipGroup(groupId) itemNumber := len(r.Ships) bg := &report.BattleReportGroup{ - // OwnerID: sg.OwnerID, - // ClassArmament: shipClass.Armament, - // ClassMass: report.F(shipClass.EmptyMass()), Race: c.g.Race[c.RaceIndex(sg.OwnerID)].Name, InBattle: inBattle, - Number: b.initialNumbers[groupId], + Number: b.InitialNumbers[groupId], NumberLeft: sg.Number, ClassName: shipClass.Name, LoadType: sg.CargoString(), LoadQuantity: report.F(sg.Load.F()), - // DriveTech: report.F(sg.TechLevel(game.TechDrive).F()), - // WeaponsTech: report.F(sg.TechLevel(game.TechWeapons).F()), - // ShieldsTech: report.F(sg.TechLevel(game.TechShields).F()), - // CargoTech: report.F(sg.TechLevel(game.TechCargo).F()), } for t, v := range sg.Tech { bg.Tech[t.String()] = report.F(v.F()) @@ -77,7 +70,7 @@ func TransformBattle(c *Cache, b *Battle) *report.BattleReport { } } - for sgi, inBattle := range b.observerGroups { + for sgi, inBattle := range b.ObserverGroups { if !inBattle { addShipGroup(sgi, false) } diff --git a/internal/controller/controller_export_test.go b/internal/controller/controller_export_test.go index ee27124..4ffc8a2 100644 --- a/internal/controller/controller_export_test.go +++ b/internal/controller/controller_export_test.go @@ -8,6 +8,29 @@ import ( "github.com/iliadenisov/galaxy/internal/model/game" ) +func (c *Cache) AddRace(n string) (int, uuid.UUID) { + id := uuid.New() + r := &game.Race{ + ID: id, + VoteFor: id, + Name: n, + Tech: game.NewTechSet(), + Relations: make([]game.RaceRelation, len(c.g.Race)), + } + c.g.Race = append(c.g.Race, *r) + for i := range c.g.Race { + if c.g.Race[i].ID != id { + c.g.Race[i].Relations = append(c.g.Race[i].Relations, game.RaceRelation{RaceID: id, Relation: game.RelationPeace}) + continue + } + for j := range c.g.Race[i].Relations { + c.g.Race[i].Relations[j].RaceID = c.g.Race[j].ID + c.g.Race[i].Relations[j].Relation = game.RelationPeace + } + } + return len(c.g.Race) - 1, id +} + func (c *Cache) Race(i int) game.Race { c.validateRaceIndex(i) return c.g.Race[i] @@ -102,3 +125,7 @@ func (c *Cache) VotesByRace() map[int]float64 { func VotingWinners(calc []*VoteGroup, gameVotes float64) []int { return votingWinners(calc, gameVotes) } + +func (c *Cache) CreateShipsUnsafe_T(ri int, classID uuid.UUID, planet uint, quantity uint) { + c.createShipsUnsafe(ri, classID, planet, quantity) +} diff --git a/internal/controller/generate_game.go b/internal/controller/generate_game.go index 84a7d0e..e933c61 100644 --- a/internal/controller/generate_game.go +++ b/internal/controller/generate_game.go @@ -61,12 +61,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { ID: raceID, Name: races[i], VoteFor: raceID, - Tech: map[game.Tech]game.Float{ - game.TechDrive: 1, - game.TechWeapons: 1, - game.TechShields: 1, - game.TechCargo: 1, - }, + Tech: game.NewTechSet(), } gameMap.Planet = append(gameMap.Planet, NewPlanet( planetCount, diff --git a/internal/controller/generate_turn.go b/internal/controller/generate_turn.go index 24b3124..0109847 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -85,7 +85,7 @@ func MakeTurn(c *Controller, r Repo) error { b := battles[i] observers := make(map[uuid.UUID]bool) - for sgi := range b.observerGroups { + for sgi := range b.ObserverGroups { observers[c.Cache.ShipGroup(sgi).OwnerID] = true } diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go index 54f426d..0c69835 100644 --- a/internal/controller/ship_group.go +++ b/internal/controller/ship_group.go @@ -43,7 +43,6 @@ func (c *Cache) createShipsUnsafe(ri int, classID uuid.UUID, planet uint, quanti game.TechCargo: game.F(c.g.Race[ri].TechLevel(game.TechCargo)), }, }) - } // ShipGroup is a proxy func, nothing to cache diff --git a/internal/model/game/game.go b/internal/model/game/game.go index b4af9a1..ff4bae4 100644 --- a/internal/model/game/game.go +++ b/internal/model/game/game.go @@ -39,6 +39,15 @@ func (ts TechSet) Set(t Tech, v float64) TechSet { return m } +func NewTechSet() TechSet { + return TechSet{ + TechDrive: 1., + TechWeapons: 1., + TechShields: 1., + TechCargo: 1., + } +} + // TODO: turn's incremental Version type Game struct { ID uuid.UUID `json:"id"` diff --git a/internal/model/report/battle.go b/internal/model/report/battle.go index f354753..d7a92f8 100644 --- a/internal/model/report/battle.go +++ b/internal/model/report/battle.go @@ -16,21 +16,14 @@ type BattleReport struct { } type BattleReportGroup struct { - // OwnerID uuid.UUID `json:"-"` // make report: visible ship class - InBattle bool `json:"inBattle"` - Number uint `json:"num"` - NumberLeft uint `json:"numLeft"` - // ClassArmament uint `json:"-"` // make report: visible ship class - // ClassMass Float `json:"-"` // make report: visible ship class + InBattle bool `json:"inBattle"` + Number uint `json:"num"` + NumberLeft uint `json:"numLeft"` LoadQuantity Float `json:"loadQuantity"` Tech map[string]Float `json:"tech"` - // DriveTech Float `json:"drive"` - // WeaponsTech Float `json:"weapons"` - // ShieldsTech Float `json:"shields"` - // CargoTech Float `json:"cargo"` - Race string `json:"race"` - ClassName string `json:"className"` - LoadType string `json:"loadType"` + Race string `json:"race"` + ClassName string `json:"className"` + LoadType string `json:"loadType"` } type BattleActionReport struct {