test: battle multiple non-crossing enemies

This commit is contained in:
IliaDenisov
2026-02-06 17:21:42 +03:00
parent ef10842dac
commit 449c3273bf
9 changed files with 129 additions and 41 deletions
+14 -11
View File
@@ -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
}
}
+69
View File
@@ -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)
}
}
+2 -9
View File
@@ -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)
}
@@ -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)
}
+1 -6
View File
@@ -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,
+1 -1
View File
@@ -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
}
-1
View File
@@ -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
+9
View File
@@ -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"`
+6 -13
View File
@@ -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 {