fs storage
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"maps"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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
|
||||
Protocol []BattleAction
|
||||
|
||||
shipAmmo map[int]uint
|
||||
attacker map[int]map[int]float64 // a group able to attack and destroy an opponent with some probability > 0
|
||||
}
|
||||
|
||||
type BattleAction struct {
|
||||
Attacker int
|
||||
Defender int
|
||||
Destroyed 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()
|
||||
if state == game.StateInOrbit || state == game.StateUpgrade {
|
||||
planetNumber := c.ShipGroup(groupIndex).Destination
|
||||
if _, ok := planetGroup[planetNumber]; !ok {
|
||||
planetGroup[planetNumber] = make(map[int]bool)
|
||||
}
|
||||
planetGroup[planetNumber][groupIndex] = false
|
||||
}
|
||||
}
|
||||
for pl := range planetGroup {
|
||||
if len(planetGroup[pl]) < 2 {
|
||||
delete(planetGroup, pl)
|
||||
}
|
||||
}
|
||||
return planetGroup
|
||||
}
|
||||
|
||||
func FilterBattleGroups(c *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 *Cache, attIdx, defIdx int, cacheProbability map[int]map[int]float64) bool {
|
||||
// Same Race's groups can't attack themselves
|
||||
if attIdx == defIdx || c.ShipGroupOwnerRaceIndex(attIdx) == c.ShipGroupOwnerRaceIndex(defIdx) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If any opponent has War relation to another, both will stay in battle
|
||||
if c.Relation(c.ShipGroupOwnerRaceIndex(attIdx), c.ShipGroupOwnerRaceIndex(defIdx)) == game.RelationPeace &&
|
||||
c.Relation(c.ShipGroupOwnerRaceIndex(defIdx), c.ShipGroupOwnerRaceIndex(attIdx)) == game.RelationPeace {
|
||||
return true
|
||||
}
|
||||
|
||||
p := DestructionProbability(
|
||||
c.ShipGroupShipClass(attIdx).Weapons.F(),
|
||||
c.ShipGroup(attIdx).TechLevel(game.TechWeapons).F(),
|
||||
c.ShipGroupShipClass(defIdx).Shields.F(),
|
||||
c.ShipGroup(defIdx).TechLevel(game.TechShields).F(),
|
||||
c.ShipGroup(defIdx).FullMass(c.ShipGroupShipClass(defIdx)),
|
||||
)
|
||||
// Exclude opponent's group which cannot be probably destroyed
|
||||
if p <= 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := cacheProbability[attIdx]; !ok {
|
||||
cacheProbability[attIdx] = make(map[int]float64)
|
||||
}
|
||||
cacheProbability[attIdx][defIdx] = p
|
||||
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func ProduceBattles(c *Cache) []*Battle {
|
||||
cacheProbability := make(map[int]map[int]float64)
|
||||
defer func() { clear(cacheProbability) }()
|
||||
|
||||
planetGroups := CollectPlanetGroups(c)
|
||||
if len(planetGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*Battle, 0)
|
||||
|
||||
// Multiple battles on single planet shoul be produced as single battle:
|
||||
// A <--> B
|
||||
// C <--> D
|
||||
// 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,
|
||||
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
|
||||
}
|
||||
|
||||
for i := range battleGroups {
|
||||
attIdx := battleGroups[i]
|
||||
|
||||
// Ships with no Ammo will never attack somebody
|
||||
if c.ShipGroupShipClass(attIdx).Armament == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
opponents := slices.DeleteFunc(slices.Clone(battleGroups), func(defIdx int) bool {
|
||||
return FilterBattleOpponents(c, attIdx, defIdx, cacheProbability)
|
||||
})
|
||||
if len(opponents) > 0 {
|
||||
b.shipAmmo[attIdx] = c.ShipGroupShipClass(attIdx).Armament
|
||||
b.ObserverGroups[attIdx] = true
|
||||
for _, defIdx := range opponents {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.attacker) > 0 {
|
||||
SingleBattle(c, b)
|
||||
b.ID = uuid.New()
|
||||
result = append(result, b)
|
||||
}
|
||||
|
||||
clear(b.attacker)
|
||||
clear(b.shipAmmo)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func SingleBattle(c *Cache, b *Battle) {
|
||||
roundShooters := make(map[int]bool)
|
||||
for len(b.attacker) > 0 {
|
||||
// список участников раунда
|
||||
clear(roundShooters)
|
||||
for sgi := range b.attacker {
|
||||
roundShooters[sgi] = true
|
||||
}
|
||||
|
||||
for len(roundShooters) > 0 {
|
||||
// attacke group id among round participants
|
||||
attIdx := randomValue(maps.Keys(roundShooters))
|
||||
delete(roundShooters, attIdx)
|
||||
|
||||
for range b.shipAmmo[attIdx] {
|
||||
// defender group id among all attacker's opponents
|
||||
defIdx := randomValue(maps.Keys(b.attacker[attIdx]))
|
||||
|
||||
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,
|
||||
Defender: defIdx,
|
||||
Destroyed: destroyed,
|
||||
})
|
||||
|
||||
if destroyed {
|
||||
c.ShipGroupDestroyItem(defIdx)
|
||||
}
|
||||
if c.ShipGroup(defIdx).Number == 0 {
|
||||
// Eliminated group cant attack anyone
|
||||
delete(b.attacker, defIdx)
|
||||
delete(roundShooters, defIdx)
|
||||
|
||||
for attIdx := range b.attacker {
|
||||
// Other attackers can't attack eliminated group anymore
|
||||
delete(b.attacker[attIdx], defIdx)
|
||||
|
||||
if len(b.attacker[attIdx]) == 0 {
|
||||
// Remove attacker if he lost all opponents
|
||||
delete(b.attacker, attIdx)
|
||||
delete(roundShooters, attIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When attacker has no more targets to shoot - break its ammo cycle
|
||||
if len(b.attacker[attIdx]) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DestructionProbability(attWeapons, attWeaponsTech, defShields, defShiledsTech, defFullMass float64) float64 {
|
||||
effAttack := attWeapons * attWeaponsTech
|
||||
effDefence := EffectiveDefence(defShields, defShiledsTech, defFullMass)
|
||||
return (math.Log10(effAttack/effDefence)/math.Log10(4) + 1) / 2
|
||||
}
|
||||
|
||||
func EffectiveDefence(defShields, defShiledsTech, defFullMass float64) float64 {
|
||||
return defShields * defShiledsTech / math.Pow(defFullMass, 1./3.) * math.Pow(30., 1./3.)
|
||||
}
|
||||
|
||||
func randomValue(v iter.Seq[int]) int {
|
||||
ids := slices.Collect(v)
|
||||
return ids[rand.IntN(len(ids))]
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
attacker = game.ShipType{
|
||||
Name: "Attacker",
|
||||
Drive: 8,
|
||||
Armament: 1,
|
||||
Weapons: 8,
|
||||
Shields: 8,
|
||||
Cargo: 0,
|
||||
}
|
||||
defender = game.ShipType{
|
||||
Name: "Defender",
|
||||
Drive: 1,
|
||||
Armament: 1,
|
||||
Weapons: 1,
|
||||
Shields: 1,
|
||||
Cargo: 0,
|
||||
}
|
||||
ship = game.ShipType{
|
||||
Name: "Ship",
|
||||
Drive: 10,
|
||||
Armament: 1,
|
||||
Weapons: 10,
|
||||
Shields: 10,
|
||||
Cargo: 0,
|
||||
}
|
||||
)
|
||||
|
||||
func TestDestructionProbability(t *testing.T) {
|
||||
probability := controller.DestructionProbability(ship.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
|
||||
assert.Equal(t, .5, probability)
|
||||
|
||||
undefeatedShip := ship
|
||||
undefeatedShip.Shields = 55
|
||||
probability = controller.DestructionProbability(ship.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass())
|
||||
assert.LessOrEqual(t, probability, 0.)
|
||||
|
||||
disruptiveShip := ship
|
||||
disruptiveShip.Weapons = 40
|
||||
probability = controller.DestructionProbability(disruptiveShip.Weapons.F(), 1, ship.Shields.F(), 1, ship.EmptyMass())
|
||||
assert.GreaterOrEqual(t, probability, 1.)
|
||||
}
|
||||
|
||||
func TestEffectiveDefence(t *testing.T) {
|
||||
assert.Equal(t, 10., controller.EffectiveDefence(ship.Shields.F(), 1, ship.EmptyMass()))
|
||||
|
||||
attackerEffectiveDefence := controller.EffectiveDefence(attacker.Shields.F(), 1, attacker.EmptyMass())
|
||||
defenderEffectiveDefence := controller.EffectiveDefence(defender.Shields.F(), 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, _ := newCache()
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) // 1 #0
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 2 #1
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 3 #2
|
||||
c.ShipGroup(2).StateInSpace = &InSpace // 3 #2 -> In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 4 #3
|
||||
c.ShipGroup(3).Destination = R1_Planet_1_num // 4 #3 -> Planet_1
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 5 #4
|
||||
c.ShipGroup(4).Destination = R0_Planet_0_num // 5 #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.ShipClassCreate(Race_1_idx, undefeatedShip.Name, undefeatedShip.Drive.F(), int(undefeatedShip.Armament), undefeatedShip.Weapons.F(), undefeatedShip.Shields.F(), undefeatedShip.Cargo.F()))
|
||||
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_idx, Race_1_idx, 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_idx, Race_1_idx, game.RelationWar))
|
||||
|
||||
assert.LessOrEqual(t, controller.DestructionProbability(Cruiser.Weapons.F(), 1, undefeatedShip.Shields.F(), 1, undefeatedShip.EmptyMass()), 0.)
|
||||
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.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
|
||||
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
|
||||
|
||||
assert.NoError(t, g.RaceRelation(race_C_name, race_D_name, game.RelationWar.String()))
|
||||
assert.NoError(t, g.RaceRelation(race_D_name, race_C_name, game.RelationWar.String()))
|
||||
|
||||
assert.Equal(t, game.RelationPeace, c.Relation(Race_0_idx, race_C_idx))
|
||||
assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, race_C_idx))
|
||||
assert.Equal(t, game.RelationPeace, c.Relation(Race_0_idx, race_D_idx))
|
||||
assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, race_D_idx))
|
||||
|
||||
// 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.ShipClassCreate(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.ShipClassCreate(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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"galaxy/model/report"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TransformBattle(c *Cache, b *Battle) *report.BattleReport {
|
||||
r := &report.BattleReport{
|
||||
ID: b.ID,
|
||||
Planet: b.Planet,
|
||||
PlanetName: c.MustPlanet(b.Planet).Name,
|
||||
Races: make(map[int]uuid.UUID),
|
||||
Ships: make(map[int]report.BattleReportGroup),
|
||||
Protocol: make([]report.BattleActionReport, len(b.Protocol)),
|
||||
}
|
||||
|
||||
cacheShipClass := make(map[uuid.UUID]int)
|
||||
cacheRaceName := make(map[uuid.UUID]int)
|
||||
|
||||
addShipGroup := func(groupId int, inBattle bool) int {
|
||||
shipClass := c.ShipGroupShipClass(groupId)
|
||||
sg := c.ShipGroup(groupId)
|
||||
itemNumber := len(r.Ships)
|
||||
bg := &report.BattleReportGroup{
|
||||
Race: c.g.Race[c.RaceIndex(sg.OwnerID)].Name,
|
||||
InBattle: inBattle,
|
||||
Number: b.InitialNumbers[groupId],
|
||||
NumberLeft: sg.Number,
|
||||
ClassName: shipClass.Name,
|
||||
LoadType: sg.CargoString(),
|
||||
LoadQuantity: report.F(sg.Load.F()),
|
||||
}
|
||||
for t, v := range sg.Tech {
|
||||
bg.Tech[t.String()] = report.F(v.F())
|
||||
}
|
||||
r.Ships[itemNumber] = *bg
|
||||
cacheShipClass[shipClass.ID] = itemNumber
|
||||
return itemNumber
|
||||
}
|
||||
|
||||
ship := func(groupId int) int {
|
||||
shipClass := c.ShipGroupShipClass(groupId)
|
||||
if v, ok := cacheShipClass[shipClass.ID]; ok {
|
||||
return v
|
||||
} else {
|
||||
return addShipGroup(groupId, true)
|
||||
}
|
||||
}
|
||||
|
||||
race := func(groupId int) int {
|
||||
race := c.ShipGroupOwnerRace(groupId)
|
||||
if v, ok := cacheRaceName[race.ID]; ok {
|
||||
return v
|
||||
} else {
|
||||
itemNumber := len(r.Races)
|
||||
r.Races[itemNumber] = race.ID
|
||||
cacheRaceName[race.ID] = itemNumber
|
||||
return itemNumber
|
||||
}
|
||||
}
|
||||
|
||||
for i := range b.Protocol {
|
||||
r.Protocol[i] = report.BattleActionReport{
|
||||
Attacker: race(b.Protocol[i].Attacker),
|
||||
AttackerShipClass: ship(b.Protocol[i].Attacker),
|
||||
Defender: race(b.Protocol[i].Defender),
|
||||
DefenderShipClass: ship(b.Protocol[i].Defender),
|
||||
Destroyed: b.Protocol[i].Destroyed,
|
||||
}
|
||||
}
|
||||
|
||||
for sgi, inBattle := range b.ObserverGroups {
|
||||
if !inBattle {
|
||||
addShipGroup(sgi, false)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) ProduceBombings() []*game.Bombing {
|
||||
report := make([]*game.Bombing, 0)
|
||||
for pn, enemies := range c.collectBombingGroups() {
|
||||
p := c.MustPlanet(pn)
|
||||
if !p.Owned() {
|
||||
continue
|
||||
}
|
||||
for ri, groups := range enemies {
|
||||
br := c.bombingReport(p, ri, groups)
|
||||
report = append(report, br)
|
||||
if br.Wiped {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p.Population == 0 {
|
||||
p.Free()
|
||||
} else {
|
||||
// Если на планете остались также и колонисты, то они превращаются в население,
|
||||
// а накопленная промышленность возмещает потери производства.
|
||||
p.UnpackColonists()
|
||||
p.UnpackCapital()
|
||||
}
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
func (c *Cache) bombingReport(p *game.Planet, ri int, groups []int) *game.Bombing {
|
||||
attackPower := 0.
|
||||
for _, i := range groups {
|
||||
sg := c.ShipGroup(i)
|
||||
st := c.ShipGroupShipClass(i)
|
||||
attackPower += sg.BombingPower(st)
|
||||
}
|
||||
r := &game.Bombing{
|
||||
ID: uuid.New(),
|
||||
PlanetOwnedID: *p.Owner,
|
||||
Planet: p.Name,
|
||||
Number: p.Number,
|
||||
Owner: c.g.Race[c.RaceIndex(*p.Owner)].Name,
|
||||
Attacker: c.g.Race[ri].Name,
|
||||
Production: c.PlanetProductionDisplayName(p.Number),
|
||||
Industry: p.Industry,
|
||||
Population: p.Population,
|
||||
Colonists: p.Colonists,
|
||||
Capital: p.Capital,
|
||||
Material: p.Material,
|
||||
AttackPower: game.F(attackPower),
|
||||
}
|
||||
bombPlanet(p, attackPower)
|
||||
r.Wiped = p.Population == 0
|
||||
return r
|
||||
}
|
||||
|
||||
func bombPlanet(p *game.Planet, power float64) {
|
||||
// Уничтожается население и колонисты в количестве равном [суммарной] мощности бомбардировки
|
||||
if power > p.Population.F() {
|
||||
p.Pop(0)
|
||||
} else {
|
||||
p.Pop(p.Population.F() - power)
|
||||
}
|
||||
if power > p.Colonists.F() {
|
||||
p.Col(0)
|
||||
} else {
|
||||
p.Col(p.Colonists.F() - power)
|
||||
}
|
||||
// Такое же количество промышленности превращается в сырье
|
||||
if power > p.Industry.F() {
|
||||
p.Mat(p.Material.F() + p.Industry.F())
|
||||
p.Ind(0)
|
||||
} else {
|
||||
p.Mat(p.Material.F() + power)
|
||||
p.Ind(p.Industry.F() - power)
|
||||
}
|
||||
}
|
||||
|
||||
// [planet_num] -> [enemy_race_id] -> []group_id
|
||||
func (c *Cache) collectBombingGroups() map[uint]map[int][]int {
|
||||
result := make(map[uint]map[int][]int)
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(i)
|
||||
if sg.State() != game.StateInOrbit {
|
||||
continue
|
||||
}
|
||||
st := c.ShipGroupShipClass(i)
|
||||
if st.WeaponsBlockMass() == 0 {
|
||||
continue
|
||||
}
|
||||
p := c.MustPlanet(sg.Destination)
|
||||
if p.OwnedBy(sg.OwnerID) || !p.Owned() {
|
||||
continue
|
||||
}
|
||||
r1 := c.RaceIndex(sg.OwnerID)
|
||||
r2 := c.RaceIndex(*p.Owner)
|
||||
if c.Relation(r1, r2) == game.RelationPeace {
|
||||
continue
|
||||
}
|
||||
// add result
|
||||
if _, ok := result[p.Number]; !ok {
|
||||
result[p.Number] = make(map[int][]int)
|
||||
}
|
||||
result[p.Number][r1] = append(result[p.Number][r1], i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBombPlanet(t *testing.T) {
|
||||
id := uuid.New()
|
||||
p := controller.NewPlanet(0, "Planet_0", &id, 1, 1, 1000, 300, 200, 10, game.ResearchDrive.AsType(uuid.Nil))
|
||||
(&p).Colonists = 100.
|
||||
assert.Equal(t, 0., p.Material.F())
|
||||
|
||||
controller.BombPlanet(&p, 55.)
|
||||
assert.Equal(t, 245., p.Population.F())
|
||||
assert.Equal(t, 45., p.Colonists.F())
|
||||
assert.Equal(t, 145., p.Industry.F())
|
||||
assert.Equal(t, 55., p.Material.F())
|
||||
|
||||
controller.BombPlanet(&p, 56.)
|
||||
assert.Equal(t, 189., p.Population.F())
|
||||
assert.Equal(t, 0., p.Colonists.F())
|
||||
assert.Equal(t, 89., p.Industry.F())
|
||||
assert.Equal(t, 111., p.Material.F())
|
||||
|
||||
controller.BombPlanet(&p, 200.)
|
||||
assert.Equal(t, 0., p.Population.F())
|
||||
assert.Equal(t, 0., p.Colonists.F())
|
||||
assert.Equal(t, 0., p.Industry.F())
|
||||
assert.Equal(t, 200., p.Material.F())
|
||||
}
|
||||
|
||||
func TestCollectBombingGroups(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
|
||||
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
|
||||
|
||||
// 1: idx = 0 / Ready to bomb: Race_1/Planet_1
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) // bombs
|
||||
c.ShipGroup(0).Destination = R1_Planet_1_num
|
||||
|
||||
// 2: idx = 1 / Ready to bomb: Race_0/Planet_2
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 3)) // bombs
|
||||
c.ShipGroup(1).Destination = R0_Planet_2_num
|
||||
|
||||
// 3: idx = 2 / In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
c.ShipGroup(2).StateInSpace = &InSpace
|
||||
|
||||
// 4: idx = 3 / Has no Ammo
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1))
|
||||
c.ShipGroup(3).Destination = R1_Planet_1_num
|
||||
|
||||
// 5: idx = 4 / On it's own planet
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
|
||||
// 6: idx = 5 / On uninhabited planet
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2))
|
||||
c.ShipGroup(5).Destination = Uninhabited_Planet_3_num
|
||||
|
||||
bg := c.CollectBombingGroups()
|
||||
|
||||
assert.Len(t, bg, 2)
|
||||
assert.Contains(t, bg, R1_Planet_1_num)
|
||||
assert.Contains(t, bg, R0_Planet_2_num)
|
||||
|
||||
assert.Len(t, bg[R1_Planet_1_num], 1)
|
||||
assert.Contains(t, bg[R1_Planet_1_num], Race_0_idx)
|
||||
|
||||
assert.Len(t, bg[R0_Planet_2_num], 1)
|
||||
assert.Contains(t, bg[R0_Planet_2_num], Race_1_idx)
|
||||
|
||||
assert.Len(t, bg[R1_Planet_1_num][Race_0_idx], 1)
|
||||
assert.Equal(t, 0, bg[R1_Planet_1_num][Race_0_idx][0])
|
||||
|
||||
assert.Len(t, bg[R0_Planet_2_num][Race_1_idx], 1)
|
||||
assert.Equal(t, 1, bg[R0_Planet_2_num][Race_1_idx][0])
|
||||
|
||||
// remove bombings from Race_1
|
||||
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationPeace.String()))
|
||||
bg = c.CollectBombingGroups()
|
||||
assert.Len(t, bg, 1)
|
||||
assert.Contains(t, bg, R1_Planet_1_num)
|
||||
assert.Len(t, bg[R1_Planet_1_num], 1)
|
||||
assert.Contains(t, bg[R1_Planet_1_num], Race_0_idx)
|
||||
assert.Len(t, bg[R1_Planet_1_num][Race_0_idx], 1)
|
||||
assert.Equal(t, 0, bg[R1_Planet_1_num][Race_0_idx][0])
|
||||
}
|
||||
|
||||
func TestProduceBombings(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, game.RelationWar.String()))
|
||||
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, game.RelationWar.String()))
|
||||
|
||||
// 1: idx = 0 / Bombs on: Race_1/Planet_1
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3))
|
||||
c.ShipGroup(0).Destination = R1_Planet_1_num
|
||||
// 2: idx = 1 / Bombs on: Race_1/Planet_1
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(1).Destination = R1_Planet_1_num
|
||||
|
||||
// 3: idx = 2 / Bombs on: Race_0/Planet_2
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1))
|
||||
c.ShipGroup(2).Destination = R0_Planet_2_num
|
||||
c.MustPlanet(R0_Planet_2_num).Population = 500
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", R0_Planet_2_num, R0_Planet_0_num))
|
||||
assert.NotEmpty(t, c.MustPlanet(R0_Planet_2_num).Route)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "EMP", R1_Planet_1_num, R0_Planet_2_num))
|
||||
assert.NotEmpty(t, c.MustPlanet(R1_Planet_1_num).Route)
|
||||
|
||||
reports := c.ProduceBombings()
|
||||
assert.Len(t, reports, 2)
|
||||
for _, b := range reports {
|
||||
assert.NotEqual(t, uuid.Nil, b.ID)
|
||||
switch pn := b.Number; pn {
|
||||
case R1_Planet_1_num:
|
||||
assert.Equal(t, Race_1.Name, b.Owner)
|
||||
assert.Equal(t, Race_0.Name, b.Attacker)
|
||||
assert.InDelta(t, 697.857, b.AttackPower.F(), 0.0003)
|
||||
assert.True(t, b.Wiped)
|
||||
assert.False(t, c.MustPlanet(pn).Owned())
|
||||
assert.Empty(t, c.MustPlanet(pn).Route)
|
||||
assert.Equal(t, 0., c.MustPlanet(pn).Population.F())
|
||||
case R0_Planet_2_num:
|
||||
assert.Equal(t, Race_0.Name, b.Owner)
|
||||
assert.Equal(t, Race_1.Name, b.Attacker)
|
||||
assert.InDelta(t, 358.856, b.AttackPower.F(), 0.0001)
|
||||
assert.False(t, b.Wiped)
|
||||
assert.True(t, c.MustPlanet(pn).OwnedBy(Race_0_ID))
|
||||
assert.NotEmpty(t, c.MustPlanet(pn).Route)
|
||||
assert.InDelta(t, 500.-358.85596, c.MustPlanet(pn).Population.F(), 0.000001)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
g *game.Game
|
||||
cacheRaceIndexByID map[uuid.UUID]int
|
||||
cacheFleetIndexByID map[uuid.UUID]int
|
||||
cacheRaceIndexByShipGroupIndex map[int]int
|
||||
cacheShipClassByShipGroupIndex map[int]*game.ShipType
|
||||
cachePlanetByPlanetNumber map[uint]*game.Planet
|
||||
cacheRelation map[int]map[int]game.Relation
|
||||
}
|
||||
|
||||
func NewCache(g *game.Game) *Cache {
|
||||
if g == nil {
|
||||
panic("NewCache: nil Game passed")
|
||||
}
|
||||
c := &Cache{
|
||||
g: g,
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Cache) StageCommand() {
|
||||
c.g.Stage++
|
||||
}
|
||||
|
||||
func (c Cache) Stage() uint {
|
||||
return c.g.Stage
|
||||
}
|
||||
|
||||
func (c *Cache) ShipGroupShipClass(groupIndex int) *game.ShipType {
|
||||
if len(c.cacheShipClassByShipGroupIndex) == 0 {
|
||||
c.cacheShipsAndGroups()
|
||||
}
|
||||
c.validateShipGroupIndex(groupIndex)
|
||||
if v, ok := c.cacheShipClassByShipGroupIndex[groupIndex]; ok {
|
||||
return v
|
||||
} else {
|
||||
panic(fmt.Sprintf("ShipClassByShipGroupIndex: group not found by index=%v", groupIndex))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) RaceIndex(ID uuid.UUID) int {
|
||||
if c.cacheRaceIndexByID == nil {
|
||||
c.cacheRaceIndexByID = make(map[uuid.UUID]int)
|
||||
for i := range c.listRaceIdx() {
|
||||
c.cacheRaceIndexByID[c.g.Race[i].ID] = i
|
||||
}
|
||||
}
|
||||
if v, ok := c.cacheRaceIndexByID[ID]; ok {
|
||||
return v
|
||||
} else {
|
||||
panic(fmt.Sprintf("RaceIndex: race not found by ID=%v", ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) cacheShipsAndGroups() {
|
||||
if c.cacheRaceIndexByShipGroupIndex != nil {
|
||||
clear(c.cacheRaceIndexByShipGroupIndex)
|
||||
} else {
|
||||
c.cacheRaceIndexByShipGroupIndex = make(map[int]int)
|
||||
}
|
||||
if c.cacheShipClassByShipGroupIndex != nil {
|
||||
clear(c.cacheShipClassByShipGroupIndex)
|
||||
} else {
|
||||
c.cacheShipClassByShipGroupIndex = make(map[int]*game.ShipType)
|
||||
}
|
||||
for sgi := range c.g.ShipGroups {
|
||||
ri := c.RaceIndex(c.g.ShipGroups[sgi].OwnerID)
|
||||
c.cacheRaceIndexByShipGroupIndex[sgi] = ri
|
||||
sci, ok := ShipClassIndex(c.g, ri, c.g.ShipGroups[sgi].TypeID)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("CollectPlanetGroups: ship class not found for race=%q group=%v", c.g.Race[ri].Name, c.g.ShipGroups[sgi].ID))
|
||||
}
|
||||
c.cacheShipClassByShipGroupIndex[sgi] = &c.g.Race[ri].ShipTypes[sci]
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) invalidateShipGroupCache() {
|
||||
clear(c.cacheRaceIndexByShipGroupIndex)
|
||||
clear(c.cacheShipClassByShipGroupIndex)
|
||||
}
|
||||
|
||||
func (c *Cache) invalidateFleetCache() {
|
||||
clear(c.cacheFleetIndexByID)
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func ShipClassIndex(g *game.Game, ri int, classID uuid.UUID) (int, bool) {
|
||||
if len(g.Race) < ri+1 {
|
||||
panic(fmt.Sprintf("ShipClass: game race index %d invalid: len=%d", ri, len(g.Race)))
|
||||
}
|
||||
sti := slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == classID })
|
||||
return sti, sti >= 0
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RaceID returns ID of race with given actor's name or error when race not found or extinct
|
||||
func (c Controller) RaceID(actor string) (uuid.UUID, error) {
|
||||
ri, err := c.Cache.validRace(actor)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
return c.Cache.g.Race[ri].ID, nil
|
||||
}
|
||||
|
||||
func (c Controller) RaceQuit(actor string) error {
|
||||
ri, err := c.Cache.validRace(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Cache.g.Race[ri].TTL = 3
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Controller) RaceVote(actor, acceptor string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rec, err := c.Cache.validRace(acceptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Cache.g.Race[ri].VoteFor = c.Cache.g.Race[rec].ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Controller) RaceRelation(actor, acceptor string, v string) error {
|
||||
rel, ok := game.ParseRelation(v)
|
||||
if !ok {
|
||||
return e.NewUnknownRelationError(v)
|
||||
}
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
other, err := c.Cache.validRace(acceptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.UpdateRelation(ri, other, rel)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipClassCreate(actor, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.ShipClassCreate(ri, typeName, drive, ammo, weapons, shileds, cargo)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipClassMerge(actor, name, targetName string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipClassMerge(ri, name, targetName)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipClassRemove(actor, typeName string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipClassRemove(ri, typeName)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupLoad(actor string, groupID uuid.UUID, cargoType string, quantity float64) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ct, ok := game.CargoTypeSet[strings.ToLower(cargoType)]
|
||||
if !ok {
|
||||
return e.NewCargoTypeInvalidError(cargoType)
|
||||
}
|
||||
return c.Cache.shipGroupLoad(ri, groupID, ct, quantity)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupUnload(actor string, groupID uuid.UUID, quantity float64) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipGroupUnload(ri, groupID, quantity)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupSend(actor string, groupID uuid.UUID, planetNumber uint) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipGroupSend(ri, groupID, planetNumber)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupUpgrade(actor string, groupID uuid.UUID, techInput string, limitLevel float64) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipGroupUpgrade(ri, groupID, techInput, limitLevel)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupMerge(actor string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Cache.shipGroupMerge(ri)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupBreak(actor string, groupID, newID uuid.UUID, quantity uint) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.ShipGroupBreak(ri, groupID, newID, quantity)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupDismantle(actor string, groupID uuid.UUID) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipGroupDismantle(ri, groupID)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupTransfer(actor, acceptor string, groupID uuid.UUID) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
riAccept, err := c.Cache.validRace(acceptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.shipGroupTransfer(ri, riAccept, groupID)
|
||||
}
|
||||
|
||||
func (c *Controller) ShipGroupJoinFleet(actor, fleetName string, groupID uuid.UUID) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.ShipGroupJoinFleet(ri, fleetName, groupID)
|
||||
}
|
||||
|
||||
func (c *Controller) FleetMerge(actor, fleetSourceName, fleetTargetName string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.fleetMerge(ri, fleetSourceName, fleetTargetName)
|
||||
}
|
||||
|
||||
func (c *Controller) FleetSend(actor, fleetName string, planetNumber uint) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fi, ok := c.Cache.fleetIndex(ri, fleetName)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("fleet %q", fleetName)
|
||||
}
|
||||
return c.Cache.FleetSend(ri, fi, planetNumber)
|
||||
}
|
||||
|
||||
func (c *Controller) ScienceCreate(actor, typeName string, drive, weapons, shields, cargo float64) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.ScienceCreate(ri, typeName, drive, weapons, shields, cargo)
|
||||
}
|
||||
|
||||
func (c *Controller) ScienceRemove(actor, typeName string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.ScienceRemove(ri, typeName)
|
||||
}
|
||||
|
||||
func (c *Controller) PlanetRename(actor string, planetNumber int, name string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Cache.PlanetRename(ri, planetNumber, name)
|
||||
}
|
||||
|
||||
func (c *Controller) PlanetProduce(actor string, planetNumber int, prodType, subject string) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var prod game.ProductionType
|
||||
switch game.ProductionType(strings.ToUpper(prodType)) {
|
||||
case game.ProductionMaterial:
|
||||
prod = game.ProductionMaterial
|
||||
case game.ProductionCapital:
|
||||
prod = game.ProductionCapital
|
||||
case game.ResearchDrive:
|
||||
prod = game.ResearchDrive
|
||||
case game.ResearchWeapons:
|
||||
prod = game.ResearchWeapons
|
||||
case game.ResearchShields:
|
||||
prod = game.ResearchShields
|
||||
case game.ResearchCargo:
|
||||
prod = game.ResearchCargo
|
||||
case game.ResearchScience:
|
||||
prod = game.ResearchScience
|
||||
case game.ProductionShip:
|
||||
prod = game.ProductionShip
|
||||
default:
|
||||
return e.NewProductionInvalidError(prodType)
|
||||
}
|
||||
return c.Cache.PlanetProduce(ri, planetNumber, prod, subject)
|
||||
}
|
||||
|
||||
func (c *Controller) PlanetRouteSet(actor, loadType string, origin, destination uint) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rt, ok := game.RouteTypeSet[strings.ToLower(loadType)]
|
||||
if !ok {
|
||||
return e.NewCargoTypeInvalidError(loadType)
|
||||
}
|
||||
return c.Cache.PlanetRouteSet(ri, rt, origin, destination)
|
||||
}
|
||||
|
||||
func (c *Controller) PlanetRouteRemove(actor, loadType string, origin uint) error {
|
||||
ri, err := c.Cache.validActor(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rt, ok := game.RouteTypeSet[strings.ToLower(loadType)]
|
||||
if !ok {
|
||||
return e.NewCargoTypeInvalidError(loadType)
|
||||
}
|
||||
return c.Cache.PlanetRouteRemove(ri, rt, origin)
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"galaxy/model/order"
|
||||
"galaxy/model/report"
|
||||
|
||||
"galaxy/game/internal/repo"
|
||||
)
|
||||
|
||||
type Configurer func(*Param)
|
||||
|
||||
type Repo interface {
|
||||
// Lock must be called before any repository operations
|
||||
Lock() error
|
||||
|
||||
// Release must be called after first and only repository operation
|
||||
Release() error
|
||||
|
||||
// SaveTurn stores just generated new turn
|
||||
SaveNewTurn(uint, *game.Game) error
|
||||
|
||||
// SaveState stores current game state updated between turns
|
||||
SaveLastState(*game.Game) error
|
||||
|
||||
// LoadState retrieves game current state with required lock acquisition
|
||||
LoadState() (*game.Game, error)
|
||||
|
||||
// LoadStateSafe retrieves game current state without preliminary locking
|
||||
LoadStateSafe() (*game.Game, error)
|
||||
|
||||
// SaveBattle stores a new battle protocol and battle meta data for turn t
|
||||
SaveBattle(uint, *report.BattleReport, *game.BattleMeta) error
|
||||
|
||||
// SaveBombing stores all prodused bombings for turn t
|
||||
SaveBombings(uint, []*game.Bombing) error
|
||||
|
||||
// SaveReport stores latest report for a race
|
||||
SaveReport(uint, *report.Report) error
|
||||
|
||||
// LoadReport loads report for specific turn and player id
|
||||
LoadReport(uint, uuid.UUID) (*report.Report, error)
|
||||
|
||||
// SaveOrder stores order for given turn
|
||||
SaveOrder(uint, uuid.UUID, *order.Order) error
|
||||
|
||||
// LoadOrder loads order for specific turn and player id
|
||||
LoadOrder(uint, uuid.UUID) (*order.Order, bool, error)
|
||||
}
|
||||
|
||||
type Ctrl interface {
|
||||
ValidateOrder(actor string, cmd ...order.DecodableCommand) error
|
||||
// remove below funcs if /command api will be deleted
|
||||
RaceID(actor string) (uuid.UUID, error)
|
||||
RaceQuit(actor string) error
|
||||
RaceVote(actor, acceptor string) error
|
||||
RaceRelation(actor, acceptor string, rel string) error
|
||||
ShipClassCreate(actor, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error
|
||||
ShipClassMerge(actor, name, targetName string) error
|
||||
ShipClassRemove(actor, typeName string) error
|
||||
ShipGroupLoad(actor string, groupID uuid.UUID, cargoType string, quantity float64) error
|
||||
ShipGroupUnload(actor string, groupID uuid.UUID, quantity float64) error
|
||||
ShipGroupSend(actor string, groupID uuid.UUID, planetNumber uint) error
|
||||
ShipGroupUpgrade(actor string, groupID uuid.UUID, techInput string, limitLevel float64) error
|
||||
ShipGroupBreak(actor string, groupID, newID uuid.UUID, quantity uint) error
|
||||
ShipGroupMerge(actor string) error
|
||||
ShipGroupDismantle(actor string, groupID uuid.UUID) error
|
||||
ShipGroupTransfer(actor, acceptor string, groupID uuid.UUID) error
|
||||
ShipGroupJoinFleet(actor, fleetName string, groupID uuid.UUID) error
|
||||
FleetMerge(actor, fleetSourceName, fleetTargetName string) error
|
||||
FleetSend(actor, fleetName string, planetNumber uint) error
|
||||
ScienceCreate(actor, typeName string, drive, weapons, shields, cargo float64) error
|
||||
ScienceRemove(actor, typeName string) error
|
||||
PlanetRename(actor string, planetNumber int, typeName string) error
|
||||
PlanetProduce(actor string, planetNumber int, prodType, subject string) error
|
||||
PlanetRouteSet(actor, loadType string, origin, destination uint) error
|
||||
PlanetRouteRemove(actor, loadType string, origin uint) error
|
||||
}
|
||||
|
||||
func GenerateGame(configure func(*Param), races []string) (s game.State, err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return game.State{}, err
|
||||
}
|
||||
if err = ec.Repo.Lock(); err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, ec.Repo.Release())
|
||||
if err == nil {
|
||||
s, err = GameState(configure)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = NewGame(ec.Repo, races)
|
||||
return
|
||||
}
|
||||
|
||||
func GenerateTurn(configure func(*Param)) (err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ec.executeLocked(func(c *Controller) error { return c.MakeTurn() })
|
||||
return
|
||||
}
|
||||
|
||||
func ExecuteCommand(configure func(*Param), consumer func(c Ctrl) error) (err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ec.executeCommand(func(c *Controller) error { return consumer(c) })
|
||||
}
|
||||
|
||||
func ValidateOrder(configure func(*Param), actor string, cmd ...order.DecodableCommand) (err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ec.validateOrder(actor, cmd...)
|
||||
}
|
||||
|
||||
func GameState(configure func(*Param)) (s game.State, err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return game.State{}, err
|
||||
}
|
||||
|
||||
g, err := ec.Repo.LoadStateSafe()
|
||||
if err != nil {
|
||||
return game.State{}, err
|
||||
}
|
||||
|
||||
result := &game.State{
|
||||
ID: g.ID,
|
||||
Turn: g.Turn,
|
||||
Stage: g.Stage,
|
||||
Players: make([]game.PlayerState, len(g.Race)),
|
||||
}
|
||||
|
||||
for i := range g.Race {
|
||||
r := &g.Race[i]
|
||||
result.Players[i].ID = r.ID
|
||||
result.Players[i].Name = r.Name
|
||||
result.Players[i].Extinct = r.Extinct
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
type RepoController struct {
|
||||
Repo Repo
|
||||
}
|
||||
|
||||
func NewRepoController(config Configurer) (*RepoController, error) {
|
||||
c := &Param{
|
||||
StoragePath: ".",
|
||||
}
|
||||
if config != nil {
|
||||
config(c)
|
||||
}
|
||||
r, err := repo.NewFileRepo(c.StoragePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RepoController{
|
||||
Repo: r,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ec *RepoController) NewGameController(g *game.Game) *Controller {
|
||||
return &Controller{
|
||||
RepoController: ec,
|
||||
Cache: NewCache(g),
|
||||
}
|
||||
}
|
||||
|
||||
func (ec *RepoController) validateOrder(actor string, cmd ...order.DecodableCommand) (err error) {
|
||||
return ec.executeSafe(func(t uint, c *Controller) error {
|
||||
id, err := c.RaceID(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.ValidateOrder(actor, cmd...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o := &order.Order{Commands: make([]order.DecodableCommand, len(cmd))}
|
||||
copy(o.Commands, cmd)
|
||||
return ec.Repo.SaveOrder(t, id, o)
|
||||
})
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeCommand(consumer func(*Controller) error) (err error) {
|
||||
return ec.executeLocked(func(c *Controller) error {
|
||||
err = consumer(c)
|
||||
if err == nil {
|
||||
c.Cache.StageCommand()
|
||||
err = c.saveState()
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeSafe(consumer func(uint, *Controller) error) (err error) {
|
||||
g, err := ec.Repo.LoadStateSafe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = consumer(g.Turn, ec.NewGameController(g))
|
||||
return
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeLocked(consumer func(*Controller) error) (err error) {
|
||||
if err := ec.Repo.Lock(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, ec.Repo.Release())
|
||||
}()
|
||||
|
||||
g, err := ec.Repo.LoadState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = consumer(ec.NewGameController(g))
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Controller) saveState() error {
|
||||
return c.Repo.SaveLastState(c.Cache.g)
|
||||
}
|
||||
|
||||
type Controller struct {
|
||||
*RepoController
|
||||
Cache *Cache
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
StoragePath string
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"iter"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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 q", shipTypeName)
|
||||
}
|
||||
|
||||
p, ok := c.Planet(planetNumber)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("planet #%d", planetNumber)
|
||||
}
|
||||
if !p.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d", planetNumber)
|
||||
}
|
||||
|
||||
c.unsafeCreateShips(ri, class.ID, p.Number, uint(quantity))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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.listRaceIdx() {
|
||||
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]
|
||||
}
|
||||
|
||||
func (c *Cache) RaceShipGroups(ri int) iter.Seq[*game.ShipGroup] {
|
||||
return c.listShipGroups(ri)
|
||||
}
|
||||
|
||||
func (c *Cache) RaceScience(ri int) []game.Science {
|
||||
return c.raceScience(ri)
|
||||
}
|
||||
|
||||
func (c *Cache) ListFleets(ri int) iter.Seq[*game.Fleet] {
|
||||
return c.listFleets(ri)
|
||||
}
|
||||
|
||||
func (c *Cache) MustFleetID(ri int, name string) uuid.UUID {
|
||||
for f := range c.listFleets(ri) {
|
||||
if f.Name == name {
|
||||
return f.ID
|
||||
}
|
||||
}
|
||||
panic("fleet not found")
|
||||
}
|
||||
|
||||
func (c *Cache) MustShipClass(ri int, name string) *game.ShipType {
|
||||
st, _, ok := c.ShipClass(ri, name)
|
||||
if !ok {
|
||||
panic("ship class not found")
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func (c *Cache) PutPopulation(pn uint, v float64) {
|
||||
c.putPopulation(pn, v)
|
||||
}
|
||||
|
||||
func (c *Cache) PutColonists(pn uint, v float64) {
|
||||
c.putColonists(pn, v)
|
||||
}
|
||||
|
||||
func (c *Cache) PutMaterial(pn uint, v float64) {
|
||||
c.putMaterial(pn, v)
|
||||
}
|
||||
|
||||
func (c *Cache) RaceTechLevel(ri int, t game.Tech, v float64) {
|
||||
c.raceTechLevel(ri, t, v)
|
||||
}
|
||||
|
||||
func (c *Cache) ListRoutedSendGroupIds(pn uint) iter.Seq[int] {
|
||||
return c.listRoutedSendGroupIds(pn)
|
||||
}
|
||||
|
||||
func (c *Cache) ListRoutedUnloadShipGroupIds(pn uint, rt game.RouteType) iter.Seq[int] {
|
||||
return c.listRoutedUnloadShipGroupIds(pn, rt)
|
||||
}
|
||||
|
||||
func (c *Cache) SelectColUnloadGroup(groups []int) (result iter.Seq[int]) {
|
||||
return c.selectColUnloadGroup(groups)
|
||||
}
|
||||
|
||||
func (c *Cache) ListMoveableGroupIds() iter.Seq[int] {
|
||||
return c.listMoveableGroupIds()
|
||||
}
|
||||
|
||||
func (c *Cache) CollectBombingGroups() map[uint]map[int][]int {
|
||||
return c.collectBombingGroups()
|
||||
}
|
||||
|
||||
func BombPlanet(p *game.Planet, power float64) {
|
||||
bombPlanet(p, power)
|
||||
}
|
||||
|
||||
func (c *Cache) ListProducingPlanets() iter.Seq[uint] {
|
||||
return c.listProducingPlanets()
|
||||
}
|
||||
|
||||
func (c *Cache) VotesByRace() map[int]float64 {
|
||||
return c.votesByRace()
|
||||
}
|
||||
|
||||
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) int {
|
||||
return c.unsafeCreateShips(ri, classID, planet, quantity)
|
||||
}
|
||||
|
||||
func (c *Cache) WipeRace(ri int) {
|
||||
c.wipeRace(ri)
|
||||
}
|
||||
|
||||
func (c *Cache) UnsafeDeleteShipGroup(sgi int) {
|
||||
c.unsafeDeleteShipGroup(sgi)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var (
|
||||
Race_0 = game.Race{
|
||||
ID: Race_0_ID,
|
||||
VoteFor: Race_0_ID,
|
||||
Name: "Race_0",
|
||||
TTL: 10,
|
||||
Tech: map[game.Tech]game.Float{
|
||||
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},
|
||||
{RaceID: Race_2_ID, Relation: game.RelationWar},
|
||||
},
|
||||
}
|
||||
Race_1 = game.Race{
|
||||
ID: Race_1_ID,
|
||||
VoteFor: Race_1_ID,
|
||||
Name: "Race_1",
|
||||
TTL: 10,
|
||||
Tech: map[game.Tech]game.Float{
|
||||
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},
|
||||
{RaceID: Race_2_ID, Relation: game.RelationPeace},
|
||||
},
|
||||
}
|
||||
Race_Extinct = game.Race{
|
||||
ID: Race_2_ID,
|
||||
VoteFor: Race_2_ID,
|
||||
Name: "Race_Extinct",
|
||||
Extinct: true,
|
||||
TTL: 0,
|
||||
Tech: map[game.Tech]game.Float{
|
||||
game.TechDrive: 3.1,
|
||||
game.TechWeapons: 3.2,
|
||||
game.TechShields: 3.3,
|
||||
game.TechCargo: 3.4,
|
||||
},
|
||||
Relations: []game.RaceRelation{
|
||||
{RaceID: Race_0_ID, Relation: game.RelationPeace},
|
||||
{RaceID: Race_1_ID, Relation: game.RelationWar},
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Race_2_ID = uuid.New()
|
||||
|
||||
Uninhabited_Planet_3_num uint = 3
|
||||
Uninhabited_Planet_4_num uint = 4
|
||||
|
||||
ShipType_Cruiser = "Cruiser"
|
||||
|
||||
Cruiser = game.ShipType{
|
||||
Name: "Cruiser",
|
||||
Drive: 15,
|
||||
Armament: 1,
|
||||
Weapons: 15,
|
||||
Shields: 15,
|
||||
Cargo: 0,
|
||||
}
|
||||
|
||||
BadEntityName = "_Bad_entitty_Name"
|
||||
UnknownRace = "UnknownRace"
|
||||
|
||||
InSpace = game.InSpace{Origin: 2, X: floatRef(1.23), Y: floatRef(1.23)}
|
||||
)
|
||||
|
||||
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,
|
||||
Race_Extinct,
|
||||
},
|
||||
Map: game.Map{
|
||||
Width: 1000,
|
||||
Height: 1000,
|
||||
Planet: []game.Planet{
|
||||
controller.NewPlanet(R0_Planet_0_num, "Planet_0", &Race_0.ID, 1, 1, 100, 100, 100, 0, game.ProductionCapital.AsType(uuid.Nil)),
|
||||
controller.NewPlanet(R1_Planet_1_num, "Planet_1", &Race_1.ID, 2, 2, 100, 0, 0, 0, game.ProductionCapital.AsType(uuid.Nil)),
|
||||
controller.NewPlanet(R0_Planet_2_num, "Planet_2", &Race_0.ID, 3, 3, 100, 0, 0, 0, game.ProductionCapital.AsType(uuid.Nil)),
|
||||
controller.NewPlanet(Uninhabited_Planet_3_num, "Planet_3", &uuid.Nil, 500, 500, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
|
||||
controller.NewPlanet(Uninhabited_Planet_4_num, "Planet_4", nil, 10, 10, 500, 0, 0, 10, game.ProductionNone.AsType(uuid.Nil)),
|
||||
},
|
||||
},
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func newCache() (*controller.Cache, *controller.Controller) {
|
||||
ctl := &controller.Controller{
|
||||
RepoController: nil,
|
||||
Cache: controller.NewCache(newGame()),
|
||||
}
|
||||
assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, Race_0_Gunship, 60, 3, 30, 100, 0))
|
||||
assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, Race_0_Freighter, 8, 0, 0, 2, 10))
|
||||
assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, ShipType_Cruiser, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F()))
|
||||
|
||||
assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, Race_1_Gunship, 60, 3, 30, 100, 0))
|
||||
assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, Race_1_Freighter, 8, 0, 0, 2, 10))
|
||||
assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.)
|
||||
|
||||
return ctl.Cache, ctl
|
||||
}
|
||||
|
||||
func floatRef(v float64) *game.Float {
|
||||
f := game.Float(v)
|
||||
return &f
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"math"
|
||||
"slices"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var fleetStateNil = game.ShipGroupState("-")
|
||||
|
||||
type FleetState struct {
|
||||
State game.ShipGroupState
|
||||
Destination uint
|
||||
InSpace func() (game.InSpace, bool)
|
||||
AtPlanet func() (uint, bool)
|
||||
}
|
||||
|
||||
func (fs *FleetState) inSpace() bool {
|
||||
_, ok := fs.InSpace()
|
||||
return ok
|
||||
}
|
||||
|
||||
func (fs FleetState) AtSamePlanet(other FleetState) bool {
|
||||
pn1, ok := fs.AtPlanet()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
pn2, ok := other.AtPlanet()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return pn1 == pn2
|
||||
}
|
||||
|
||||
func (c *Cache) FleetState(fleetID uuid.UUID) FleetState {
|
||||
fi := c.MustFleetIndex(fleetID)
|
||||
ri := c.RaceIndex(c.g.Fleets[fi].OwnerID)
|
||||
fs := &FleetState{
|
||||
State: fleetStateNil,
|
||||
InSpace: func() (game.InSpace, bool) { return game.InSpace{}, false },
|
||||
AtPlanet: func() (uint, bool) { return 0, false },
|
||||
}
|
||||
for sgi := range c.FleetGroupIdx(ri, fi) {
|
||||
sg := c.ShipGroup(sgi)
|
||||
if fs.State == fleetStateNil {
|
||||
fs.State = sg.State()
|
||||
fs.Destination = sg.Destination
|
||||
if pn, ok := sg.AtPlanet(); ok {
|
||||
fs.AtPlanet = func() (uint, bool) { return pn, ok }
|
||||
} else if sg.StateInSpace != nil {
|
||||
fs.InSpace = func() (game.InSpace, bool) { return *sg.StateInSpace, true }
|
||||
}
|
||||
continue
|
||||
}
|
||||
if fs.State != sg.State() {
|
||||
panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different states", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
|
||||
}
|
||||
if fs.Destination != sg.Destination {
|
||||
panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different destination", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
|
||||
}
|
||||
if planet, ok := sg.AtPlanet(); ok {
|
||||
if onPlanet, ok := fs.AtPlanet(); ok && onPlanet != planet {
|
||||
panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q are on different planets: %d <> %d", c.g.Race[ri].Name, c.g.Fleets[fi].Name, onPlanet, planet))
|
||||
}
|
||||
}
|
||||
if (!fs.inSpace() && sg.StateInSpace != nil) || (fs.inSpace() && sg.StateInSpace == nil) {
|
||||
panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q on_planet and in_space at the same time", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
|
||||
}
|
||||
if is, ok := fs.InSpace(); ok && sg.StateInSpace != nil && !is.Equal(*sg.StateInSpace) {
|
||||
panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different is_space states", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
|
||||
}
|
||||
}
|
||||
if fs.State == fleetStateNil {
|
||||
panic(fmt.Sprintf("FleetState: race's %q fleet %q has no ships", c.g.Race[ri].Name, c.g.Fleets[fi].Name))
|
||||
}
|
||||
return *fs
|
||||
}
|
||||
|
||||
func (c *Cache) FleetSpeedAndMass(fi int) (float64, float64) {
|
||||
c.validateFleetIndex(fi)
|
||||
speed := math.MaxFloat64
|
||||
mass := 0.
|
||||
for sgi := range c.ShipGroupsIndex() {
|
||||
if c.ShipGroup(sgi).FleetID == nil || *c.ShipGroup(sgi).FleetID != c.g.Fleets[fi].ID {
|
||||
continue
|
||||
}
|
||||
sg := c.ShipGroup(sgi)
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
typeSpeed := sg.Speed(st)
|
||||
if typeSpeed < speed {
|
||||
speed = typeSpeed
|
||||
}
|
||||
mass += sg.FullMass(st)
|
||||
}
|
||||
return speed, mass
|
||||
}
|
||||
|
||||
func (c *Cache) ShipGroupJoinFleet(ri int, fleetName string, groupID uuid.UUID) (err error) {
|
||||
c.validateRaceIndex(ri)
|
||||
name, ok := util.ValidateTypeName(fleetName)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", name)
|
||||
}
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
|
||||
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
||||
return e.NewShipsBusyError("state: %s", state)
|
||||
}
|
||||
|
||||
var oldFleetID *uuid.UUID
|
||||
if c.ShipGroup(sgi).FleetID != nil {
|
||||
fID := *c.ShipGroup(sgi).FleetID
|
||||
oldFleetID = &fID
|
||||
}
|
||||
|
||||
fi, ok := c.fleetIndex(ri, name)
|
||||
if !ok {
|
||||
fi, err = c.createFleet(ri, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
fleetState := c.FleetState(c.g.Fleets[fi].ID)
|
||||
if onPlanet, ok := fleetState.AtPlanet(); (ok && onPlanet != c.ShipGroup(sgi).Destination) || fleetState.State != game.StateInOrbit {
|
||||
return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName)
|
||||
}
|
||||
}
|
||||
|
||||
c.internalShipGroupJoinFleet(sgi, c.g.Fleets[fi].ID)
|
||||
|
||||
if oldFleetID != nil {
|
||||
keepOldFleet := false
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.FleetID != nil && *sg.FleetID == *oldFleetID {
|
||||
keepOldFleet = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !keepOldFleet {
|
||||
oldFleetIndex, ok := c.FleetIndex(*oldFleetID)
|
||||
if !ok {
|
||||
return e.NewGameStateError("old fleet index not found by ID=%v", *oldFleetID)
|
||||
}
|
||||
if err := c.deleteFleet(ri, c.g.Fleets[oldFleetIndex].Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) fleetMerge(ri int, fleetSourceName, fleetTargetName string) (err error) {
|
||||
fiSource, ok := c.fleetIndex(ri, fleetSourceName)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("source fleet %s", fleetSourceName)
|
||||
}
|
||||
fiTarget, ok := c.fleetIndex(ri, fleetTargetName)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("target fleet %s", fleetTargetName)
|
||||
}
|
||||
stateSrc := c.FleetState(c.g.Fleets[fiSource].ID)
|
||||
stateDst := c.FleetState(c.g.Fleets[fiTarget].ID)
|
||||
if !stateSrc.AtSamePlanet(stateDst) {
|
||||
return e.NewShipsNotOnSamePlanetError()
|
||||
}
|
||||
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fiSource].ID {
|
||||
sg.FleetID = &c.g.Fleets[fiTarget].ID
|
||||
}
|
||||
}
|
||||
return c.deleteFleet(ri, fleetSourceName)
|
||||
}
|
||||
|
||||
func (c *Cache) createFleet(ri int, name string) (int, error) {
|
||||
c.validateRaceIndex(ri)
|
||||
n, ok := util.ValidateTypeName(name)
|
||||
if !ok {
|
||||
return 0, e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if _, ok := c.fleetIndex(ri, n); ok {
|
||||
return 0, e.NewEntityDuplicateIdentifierError("fleet %q", n)
|
||||
}
|
||||
fleets := slices.Clone(c.g.Fleets)
|
||||
fleets = append(fleets, game.Fleet{
|
||||
ID: uuid.New(),
|
||||
OwnerID: c.g.Race[ri].ID,
|
||||
Name: n,
|
||||
})
|
||||
c.g.Fleets = fleets
|
||||
i := len(c.g.Fleets) - 1
|
||||
if c.cacheFleetIndexByID != nil {
|
||||
c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (c *Cache) deleteFleet(ri int, name string) error {
|
||||
fi, ok := c.fleetIndex(ri, name)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("fleet %s", name)
|
||||
}
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.FleetID != nil && *(sg.FleetID) == c.g.Fleets[fi].ID {
|
||||
return e.NewEntityInUseError("fleet %s: race %s, group #%d", name, c.g.Race[ri].Name, sg.Number)
|
||||
}
|
||||
}
|
||||
c.unsafeDeleteFleet(fi)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) unsafeDeleteFleet(fi int) {
|
||||
c.validateFleetIndex(fi)
|
||||
c.g.Fleets = append(c.g.Fleets[:fi], c.g.Fleets[fi+1:]...)
|
||||
c.invalidateFleetCache()
|
||||
}
|
||||
|
||||
// Internal funcs
|
||||
|
||||
func (c *Cache) FleetIndex(ID uuid.UUID) (int, bool) {
|
||||
if len(c.cacheFleetIndexByID) == 0 {
|
||||
c.cacheFleetIndex()
|
||||
}
|
||||
if v, ok := c.cacheFleetIndexByID[ID]; ok {
|
||||
return v, true
|
||||
} else {
|
||||
return -1, false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) cacheFleetIndex() {
|
||||
if c.cacheFleetIndexByID != nil {
|
||||
clear(c.cacheFleetIndexByID)
|
||||
} else {
|
||||
c.cacheFleetIndexByID = make(map[uuid.UUID]int)
|
||||
}
|
||||
for i := range c.g.Fleets {
|
||||
c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) MustFleetIndex(ID uuid.UUID) int {
|
||||
if v, ok := c.FleetIndex(ID); ok {
|
||||
return v
|
||||
} else {
|
||||
panic(fmt.Sprintf("fleet not found by ID=%v", ID))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) FleetGroupIdx(ri, fi int) iter.Seq[int] {
|
||||
c.validateRaceIndex(ri)
|
||||
c.validateFleetIndex(fi)
|
||||
return func(yield func(int) bool) {
|
||||
for sgi := range c.listShipGroupIdx(ri) {
|
||||
sg := c.ShipGroup(sgi)
|
||||
if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fi].ID {
|
||||
if !yield(sgi) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) fleetGroupIds(ri, fi int) iter.Seq[int] {
|
||||
c.validateRaceIndex(ri)
|
||||
c.validateFleetIndex(fi)
|
||||
return func(yield func(int) bool) {
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(i)
|
||||
if c.g.Race[ri].ID != sg.OwnerID {
|
||||
continue
|
||||
}
|
||||
if sg.FleetID == nil || c.MustFleetIndex(*sg.FleetID) != fi {
|
||||
continue
|
||||
}
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) listFleets(ri int) iter.Seq[*game.Fleet] {
|
||||
c.validateRaceIndex(ri)
|
||||
return func(yield func(*game.Fleet) bool) {
|
||||
for i := range c.g.Fleets {
|
||||
if c.g.Fleets[i].OwnerID == c.g.Race[ri].ID {
|
||||
if !yield(&c.g.Fleets[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) fleetIndex(ri int, name string) (int, bool) {
|
||||
c.validateRaceIndex(ri)
|
||||
if i := slices.IndexFunc(c.g.Fleets, func(f game.Fleet) bool { return f.OwnerID == c.g.Race[ri].ID && f.Name == name }); i < 0 {
|
||||
return -1, false
|
||||
} else {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) validateFleetIndex(i int) {
|
||||
if i >= len(c.g.Fleets) {
|
||||
panic(fmt.Sprintf("fleet index out of range: %d >= %d", i, len(c.g.Fleets)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
)
|
||||
|
||||
func (c *Cache) FleetSend(ri, fi int, planetNumber uint) error {
|
||||
c.validateRaceIndex(ri)
|
||||
c.validateFleetIndex(fi)
|
||||
fleetState := c.FleetState(c.g.Fleets[fi].ID)
|
||||
sourcePlanet, ok := fleetState.AtPlanet()
|
||||
if !ok || game.StateInOrbit != fleetState.State && game.StateLaunched != fleetState.State {
|
||||
return e.NewShipsBusyError("state: %s", fleetState.State)
|
||||
}
|
||||
|
||||
p1, ok := c.Planet(sourcePlanet)
|
||||
if !ok {
|
||||
return e.NewGameStateError("source planet #%d does not exists", sourcePlanet)
|
||||
}
|
||||
p2, ok := c.Planet(planetNumber)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("destination planet #%d", planetNumber)
|
||||
}
|
||||
rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
||||
if rangeToDestination > c.g.Race[ri].FlightDistance() {
|
||||
return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination)
|
||||
}
|
||||
|
||||
for sgi := range c.FleetGroupIdx(ri, fi) {
|
||||
st := c.MustShipType(ri, c.ShipGroup(sgi).TypeID)
|
||||
if st.DriveBlockMass() == 0 {
|
||||
return e.NewSendShipHasNoDrivesError("Class=%s", st.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if sourcePlanet == planetNumber {
|
||||
c.UnsendFleet(ri, fi)
|
||||
return nil
|
||||
}
|
||||
|
||||
c.LaunchFleet(ri, fi, planetNumber)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) LaunchFleet(ri, fi int, destination uint) {
|
||||
c.validateRaceIndex(ri)
|
||||
c.validateFleetIndex(fi)
|
||||
for sgi := range c.FleetGroupIdx(ri, fi) {
|
||||
c.LaunchShips(sgi, destination)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) UnsendFleet(ri, fi int) {
|
||||
c.validateRaceIndex(ri)
|
||||
c.validateFleetIndex(fi)
|
||||
for sgi := range c.FleetGroupIdx(ri, fi) {
|
||||
c.UnsendShips(sgi)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFleetSend(t *testing.T) {
|
||||
c, g := newCache()
|
||||
// group #1 - in_orbit Planet_0
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1))
|
||||
// group #2 - in_space (later)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3))
|
||||
// group #3 - in_orbit Planet_0, unmovable
|
||||
g.ShipClassCreate(Race_0.Name, "Fortress", 0, 50, 30, 100, 0)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1))
|
||||
// group #4 - in_orbit Planet_0
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2))
|
||||
|
||||
// ensure race has no Fleets
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 0)
|
||||
|
||||
fleetSending := "R0_Fleet_one"
|
||||
fleetInSpace := "R0_Fleet_inSpace"
|
||||
fleetUnmovable := "R0_Fleet_unmovable"
|
||||
fleetUnmovable2 := "R0_Fleet_unmovable2"
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetSending, c.ShipGroup(0).ID))
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 1)
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetSending, c.ShipGroup(2).ID))
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 1)
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetInSpace, c.ShipGroup(1).ID))
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 2)
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetUnmovable, c.ShipGroup(2).ID))
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 3)
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetUnmovable2, c.ShipGroup(3).ID))
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 4)
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetUnmovable, c.ShipGroup(3).ID))
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 3)
|
||||
|
||||
// group #2 - in_space
|
||||
c.ShipGroup(1).StateInSpace = &InSpace
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(UnknownRace, fleetSending, 2),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(Race_Extinct.Name, fleetSending, 2),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(Race_0.Name, "UnknownFleet", 2),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(Race_0.Name, fleetInSpace, 2),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(Race_0.Name, fleetSending, 200),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(Race_0.Name, fleetSending, 3),
|
||||
e.GenericErrorText(e.ErrSendUnreachableDestination))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetSend(Race_0.Name, fleetUnmovable, 2),
|
||||
e.GenericErrorText(e.ErrSendShipHasNoDrives))
|
||||
|
||||
assert.NoError(t, g.FleetSend(Race_0.Name, fleetSending, 2))
|
||||
fleetState := c.FleetState(c.MustFleetID(Race_0_idx, fleetSending))
|
||||
assert.Equal(t, game.StateLaunched, fleetState.State)
|
||||
for sgi := range c.FleetGroupIdx(Race_0_idx, c.MustFleetIndex(c.MustFleetID(Race_0_idx, fleetSending))) {
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
|
||||
}
|
||||
|
||||
assert.NoError(t, g.FleetSend(Race_0.Name, fleetSending, 0))
|
||||
fleetState = c.FleetState(c.MustFleetID(Race_0_idx, fleetSending))
|
||||
assert.Equal(t, game.StateInOrbit, fleetState.State)
|
||||
for sgi := range c.FleetGroupIdx(Race_0_idx, c.MustFleetIndex(c.MustFleetID(Race_0_idx, fleetSending))) {
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(sgi).State())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShipGroupJoinFleet(t *testing.T) {
|
||||
c, g := newCache()
|
||||
groupIndex := uuid.Nil
|
||||
|
||||
fleetOne := "R0_Fleet_one"
|
||||
fleetTwo := "R0_Fleet_two"
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(Race_0.Name, BadEntityName, groupIndex),
|
||||
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(Race_0.Name, "Unnamed", groupIndex),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
|
||||
// creating ShipGroup
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
|
||||
groupIndex = c.ShipGroup(0).ID
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(UnknownRace, fleetOne, groupIndex),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(Race_Extinct.Name, fleetOne, groupIndex),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
|
||||
// ensure race has no Fleets
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 0)
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetOne, groupIndex))
|
||||
fleets := slices.Collect(c.ListFleets(Race_0_idx))
|
||||
groups := slices.Collect(c.RaceShipGroups(Race_0_idx))
|
||||
assert.Len(t, groups, 1)
|
||||
gi := 0
|
||||
assert.Len(t, fleets, 1)
|
||||
assert.Equal(t, fleets[0].Name, fleetOne)
|
||||
fleetState := c.FleetState(fleets[0].ID)
|
||||
assert.Equal(t, game.StateInOrbit, fleetState.State)
|
||||
|
||||
assert.NotNil(t, groups[gi].FleetID)
|
||||
assert.Equal(t, fleets[0].ID, *groups[gi].FleetID)
|
||||
|
||||
// create another ShipGroup
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3))
|
||||
groupIndex = c.ShipGroup(1).ID
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetTwo, groupIndex))
|
||||
fleets = slices.Collect(c.ListFleets(Race_0_idx))
|
||||
groups = slices.Collect(c.RaceShipGroups(Race_0_idx))
|
||||
assert.Len(t, groups, 2)
|
||||
assert.Len(t, fleets, 2)
|
||||
assert.Equal(t, fleets[1].Name, fleetTwo)
|
||||
fleetState = c.FleetState(fleets[1].ID)
|
||||
assert.Equal(t, game.StateInOrbit, fleetState.State)
|
||||
|
||||
gi = 1
|
||||
assert.Len(t, groups, 2)
|
||||
assert.NotNil(t, groups[gi].FleetID)
|
||||
assert.Equal(t, fleets[1].ID, *groups[gi].FleetID)
|
||||
assert.Equal(t, uint(3), groups[gi].Number)
|
||||
|
||||
groupIndex = groups[gi].ID
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetOne, groupIndex))
|
||||
fleets = slices.Collect(c.ListFleets(Race_0_idx))
|
||||
assert.Len(t, fleets, 1)
|
||||
groups = slices.Collect(c.RaceShipGroups(Race_0_idx))
|
||||
assert.NotNil(t, groups[gi].FleetID)
|
||||
assert.Equal(t, fleets[0].ID, *groups[gi].FleetID)
|
||||
fleetState = c.FleetState(fleets[0].ID)
|
||||
assert.Equal(t, game.StateInOrbit, fleetState.State)
|
||||
|
||||
// group not In_Orbit
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7))
|
||||
gi = 2
|
||||
c.ShipGroup(gi).StateInSpace = &InSpace
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(Race_0.Name, fleetOne, c.ShipGroup(gi).ID),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
c.ShipGroup(gi).StateInSpace = nil
|
||||
|
||||
// existing fleet not on the same planet or in_orbit
|
||||
c.ShipGroup(0).StateInSpace = &InSpace
|
||||
c.ShipGroup(1).StateInSpace = c.ShipGroup(0).StateInSpace
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(Race_0.Name, fleetOne, c.ShipGroup(gi).ID),
|
||||
e.GenericErrorText(e.ErrShipsNotOnSamePlanet))
|
||||
}
|
||||
|
||||
func TestFleetMerge(t *testing.T) {
|
||||
c, g := newCache()
|
||||
// creating ShipGroup #1 at Planet_0
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1
|
||||
// creating ShipGroup #2 at Planet_2
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 2)) // group #2
|
||||
// creating ShipGroup #3 at Planet_0
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // group #3
|
||||
|
||||
// ensure race has no Fleets
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 0)
|
||||
|
||||
fleetOnPlanet2 := "R0_Fleet_On_Planet_2"
|
||||
fleetSourceOne := "R0_Fleet_one"
|
||||
fleetTargetTwo := "R0_Fleet_two"
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.FleetMerge(Race_0.Name, fleetSourceOne, fleetTargetTwo),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetMerge(UnknownRace, fleetSourceOne, fleetTargetTwo),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.FleetMerge(Race_Extinct.Name, fleetSourceOne, fleetTargetTwo),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(UnknownRace, fleetSourceOne, c.ShipGroup(0).ID),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupJoinFleet(Race_Extinct.Name, fleetSourceOne, c.ShipGroup(0).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetSourceOne, c.ShipGroup(0).ID))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.FleetMerge(Race_0.Name, fleetSourceOne, fleetTargetTwo),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetTargetTwo, c.ShipGroup(2).ID))
|
||||
assert.NoError(t, g.FleetMerge(Race_0.Name, fleetSourceOne, fleetTargetTwo))
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleetOnPlanet2, c.ShipGroup(1).ID))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.FleetMerge(Race_0.Name, fleetOnPlanet2, fleetTargetTwo),
|
||||
e.GenericErrorText(e.ErrShipsNotOnSamePlanet))
|
||||
}
|
||||
|
||||
func TestFleetSpeedAndMass(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Material = 100.
|
||||
c.MustPlanet(R0_Planet_0_num).Capital = 100.
|
||||
fleet := "Fleet"
|
||||
var speed, mass float64
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 1
|
||||
|
||||
s := c.ShipGroup(0).Speed(c.MustShipClass(Race_0_idx, Race_0_Gunship))
|
||||
m := c.ShipGroup(0).FullMass(c.MustShipClass(Race_0_idx, Race_0_Gunship))
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) // 2
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(1).ID, "MAT", 10.))
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) // 3
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(2).ID, "CAP", 10.))
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleet, c.ShipGroup(0).ID))
|
||||
fleetIndex := 0
|
||||
speed, mass = c.FleetSpeedAndMass(fleetIndex)
|
||||
assert.Equal(t, s, speed)
|
||||
assert.Equal(t, m, mass)
|
||||
|
||||
s = math.Min(s, c.ShipGroup(1).Speed(c.MustShipClass(Race_0_idx, Race_0_Freighter)))
|
||||
m += c.ShipGroup(1).FullMass(c.MustShipClass(Race_0_idx, Race_0_Freighter))
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleet, c.ShipGroup(1).ID))
|
||||
speed, mass = c.FleetSpeedAndMass(fleetIndex)
|
||||
assert.Equal(t, s, speed)
|
||||
assert.Equal(t, m, mass)
|
||||
|
||||
s = math.Min(s, c.ShipGroup(2).Speed(c.MustShipClass(Race_0_idx, Race_0_Freighter)))
|
||||
m += c.ShipGroup(2).FullMass(c.MustShipClass(Race_0_idx, Race_0_Freighter))
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleet, c.ShipGroup(2).ID))
|
||||
speed, mass = c.FleetSpeedAndMass(fleetIndex)
|
||||
assert.Equal(t, s, speed)
|
||||
assert.Equal(t, m, mass)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
|
||||
"galaxy/game/internal/generator"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func NewGame(r Repo, races []string) (uuid.UUID, error) {
|
||||
m, err := generator.Generate(func(ms *generator.MapSetting) {
|
||||
ms.Players = uint32(len(races))
|
||||
})
|
||||
if err != nil {
|
||||
return uuid.Nil, fmt.Errorf("generate map: %s", err)
|
||||
}
|
||||
return newGameOnMap(r, races, m)
|
||||
}
|
||||
|
||||
func newGameOnMap(r Repo, races []string, m generator.Map) (uuid.UUID, error) {
|
||||
g, err := buildGameOnMap(races, m)
|
||||
if err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
if err := r.SaveNewTurn(0, g); err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
return g.ID, nil
|
||||
}
|
||||
|
||||
func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) {
|
||||
if len(races) != len(m.HomePlanets) {
|
||||
return nil, fmt.Errorf("generate map: wrong number of home planets: %d, expected: %d ", len(m.HomePlanets), len(races))
|
||||
}
|
||||
gameID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate game uuid: %s", err)
|
||||
}
|
||||
g := &game.Game{
|
||||
ID: gameID,
|
||||
Turn: 0,
|
||||
Race: make([]game.Race, len(races)),
|
||||
}
|
||||
gameMap := &game.Map{
|
||||
Width: m.Width,
|
||||
Height: m.Height,
|
||||
Planet: make([]game.Planet, 0),
|
||||
}
|
||||
var planetCount uint = 0
|
||||
relations := make([]game.RaceRelation, len(races))
|
||||
for i := range races {
|
||||
raceID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generate race uuid: %s", err)
|
||||
}
|
||||
relations[i] = game.RaceRelation{RaceID: raceID, Relation: game.RelationWar}
|
||||
g.Race[i] = game.Race{
|
||||
ID: raceID,
|
||||
Name: races[i],
|
||||
VoteFor: raceID,
|
||||
TTL: 10,
|
||||
Tech: game.NewTechSet(),
|
||||
}
|
||||
gameMap.Planet = append(gameMap.Planet, NewPlanet(
|
||||
planetCount,
|
||||
m.HomePlanets[i].HW.RandomName(),
|
||||
&raceID,
|
||||
m.HomePlanets[i].HW.Position.X,
|
||||
m.HomePlanets[i].HW.Position.Y,
|
||||
m.HomePlanets[i].HW.Size,
|
||||
m.HomePlanets[i].HW.Size, // HW's pop & ind = size
|
||||
m.HomePlanets[i].HW.Size,
|
||||
m.HomePlanets[i].HW.Resources,
|
||||
game.ResearchDrive.AsType(uuid.Nil),
|
||||
))
|
||||
planetCount++
|
||||
for dw := range m.HomePlanets[i].DW {
|
||||
gameMap.Planet = append(gameMap.Planet, NewPlanet(
|
||||
planetCount,
|
||||
m.HomePlanets[i].DW[dw].RandomName(),
|
||||
&raceID,
|
||||
m.HomePlanets[i].DW[dw].Position.X,
|
||||
m.HomePlanets[i].DW[dw].Position.Y,
|
||||
m.HomePlanets[i].DW[dw].Size,
|
||||
m.HomePlanets[i].DW[dw].Size, // DW's pop & ind = size
|
||||
m.HomePlanets[i].DW[dw].Size,
|
||||
m.HomePlanets[i].DW[dw].Resources,
|
||||
game.ResearchDrive.AsType(uuid.Nil),
|
||||
))
|
||||
planetCount++
|
||||
}
|
||||
}
|
||||
for i := range g.Race {
|
||||
rel := slices.Clone(relations)
|
||||
selfIdx := slices.IndexFunc(rel, func(a game.RaceRelation) bool { return a.RaceID == g.Race[i].ID })
|
||||
g.Race[i].Relations = append(rel[:selfIdx], rel[selfIdx+1:]...)
|
||||
}
|
||||
|
||||
for i := range m.FreePlanets {
|
||||
gameMap.Planet = append(gameMap.Planet, NewPlanet(
|
||||
planetCount,
|
||||
m.FreePlanets[i].RandomName(),
|
||||
&uuid.Nil,
|
||||
m.FreePlanets[i].Position.X,
|
||||
m.FreePlanets[i].Position.Y,
|
||||
m.FreePlanets[i].Size,
|
||||
0,
|
||||
0,
|
||||
m.FreePlanets[i].Resources,
|
||||
game.ProductionNone.AsType(uuid.Nil),
|
||||
))
|
||||
planetCount++
|
||||
}
|
||||
|
||||
rand.Shuffle(len(gameMap.Planet), func(i, j int) {
|
||||
gameMap.Planet[i].Number, gameMap.Planet[j].Number = gameMap.Planet[j].Number, gameMap.Planet[i].Number
|
||||
})
|
||||
|
||||
for i := range gameMap.Planet {
|
||||
g.Votes = g.Votes.Add(gameMap.Planet[i].Votes())
|
||||
}
|
||||
|
||||
g.Map = *gameMap
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func NewPlanet(num uint, name string, owner *uuid.UUID, x, y, size, pop, ind, res float64, prod game.Production) game.Planet {
|
||||
if owner != nil && *owner == uuid.Nil {
|
||||
owner = nil
|
||||
}
|
||||
return game.Planet{
|
||||
Owner: owner,
|
||||
X: game.F(x),
|
||||
Y: game.F(y),
|
||||
Number: num,
|
||||
Size: game.F(size),
|
||||
Name: name,
|
||||
Resources: game.F(res),
|
||||
Population: game.F(pop),
|
||||
Industry: game.F(ind),
|
||||
Production: prod,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
"galaxy/game/internal/repo"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewGame(t *testing.T) {
|
||||
root, cleanup := util.CreateWorkDir(t)
|
||||
defer cleanup()
|
||||
|
||||
r, err := repo.NewFileRepo(root)
|
||||
assert.NoError(t, err)
|
||||
|
||||
players := 20
|
||||
races := make([]string, players)
|
||||
for i := range players {
|
||||
races[i] = fmt.Sprintf("race_%02d", i)
|
||||
}
|
||||
assert.NoError(t, r.Lock())
|
||||
gameID, err := controller.NewGame(r, races)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.FileExists(t, filepath.Join(root, "state.json"))
|
||||
assert.FileExists(t, filepath.Join(root, "0000/state.json"))
|
||||
|
||||
g, err := r.LoadState()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, gameID, g.ID)
|
||||
assert.Equal(t, uint(0), g.Turn)
|
||||
assert.Equal(t, players, len(g.Race))
|
||||
|
||||
for r := range g.Race {
|
||||
assert.NotEqual(t, uuid.Nil, g.Race[r].ID)
|
||||
assert.Equal(t, players-1, len(g.Race[r].Relations))
|
||||
assert.Equal(t, uint(10), g.Race[r].TTL)
|
||||
for i := range g.Race[r].Relations {
|
||||
assert.NotEqual(t, uuid.Nil, g.Race[r].Relations[i].RaceID)
|
||||
if g.Race[r].Relations[i].RaceID == g.Race[r].ID {
|
||||
assert.Fail(t, "race relation with itself")
|
||||
}
|
||||
assert.Equal(t, game.RelationWar, g.Race[r].Relations[i].Relation)
|
||||
}
|
||||
}
|
||||
|
||||
numShuffled := false
|
||||
for i := range g.Map.Planet {
|
||||
p := &g.Map.Planet[i]
|
||||
if strings.HasPrefix(p.Name, "HW") || strings.HasPrefix(p.Name, "DW") {
|
||||
assert.True(t, p.Owned())
|
||||
assert.NotNil(t, p.Owner)
|
||||
assert.NotEqual(t, uuid.Nil, *p.Owner)
|
||||
}
|
||||
numShuffled = numShuffled || p.Number != uint(i)
|
||||
}
|
||||
assert.True(t, numShuffled)
|
||||
|
||||
assert.NoError(t, r.Release())
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"galaxy/model/report"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Controller) MakeTurn() error {
|
||||
if err := c.applyOrders(c.Cache.g.Turn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Next turn
|
||||
c.Cache.g.Turn += 1
|
||||
c.Cache.g.Stage = 0
|
||||
|
||||
// 01. Вышедшие расы удаляются из списка участвующих рас перед началом просчета очередного хода
|
||||
c.Cache.TurnWipeExtinctRaces()
|
||||
|
||||
// 02. Товары загружаются на корабли, находящиеся в начале грузовых маршрутов, и корабли входят в гиперпространство (но ещё не полетели)
|
||||
c.Cache.SendRoutedGroups()
|
||||
|
||||
// 03. Корабли, где это возможно, объединяются в группы.
|
||||
c.Cache.TurnMergeEqualShipGroups()
|
||||
|
||||
// 04. Враждующие корабли вступают в схватку.
|
||||
battles := ProduceBattles(c.Cache)
|
||||
|
||||
// 05. Корабли пролетают сквозь гиперпространство.
|
||||
c.Cache.MoveShipGroups()
|
||||
|
||||
// 06. Корабли, где это возможно, объединяются в группы.
|
||||
c.Cache.TurnMergeEqualShipGroups()
|
||||
|
||||
// 07. Враждующие корабли снова вступают в схватку (это происходит после выхода из гиперпространства).
|
||||
battles = append(battles, ProduceBattles(c.Cache)...)
|
||||
|
||||
// 08. Корабли бомбят вражеские планеты.
|
||||
bombings := c.Cache.ProduceBombings()
|
||||
|
||||
// 09. На планетах строятся корабли.
|
||||
// 10. Корабли, где это возможно, объединяются в группы.
|
||||
// 11. На планетах производится промышленность, добывается сырье, разрабатываются новые технологии.
|
||||
// 12. Увеличивается население планет.
|
||||
c.Cache.TurnPlanetProductions()
|
||||
|
||||
// 13. Товары выгружаются в конце грузовых маршрутов.
|
||||
// 14. Выгруженные колонисты увеличивают население планеты (если население планеты ниже её размера).
|
||||
// 15. Накопленная и выгруженная промышленность увеличивает производственный уровень планеты (если производственный уровень планеты ниже уровня населения).
|
||||
c.Cache.TurnUnloadEnroutedGroups()
|
||||
|
||||
// 16. Происходит отмена маршрутов, выходящих за зону полета кораблей.
|
||||
c.Cache.RemoveUnreachableRoutes()
|
||||
|
||||
// 17. Происходит голосование.
|
||||
winners := c.Cache.TurnCalculateVotes()
|
||||
c.Cache.TurnAcceptWinners(winners)
|
||||
|
||||
/*** Last steps ***/
|
||||
|
||||
// Store bombings
|
||||
bombingReport := make([]*report.Bombing, len(bombings))
|
||||
if len(bombings) > 0 {
|
||||
if err := c.Repo.SaveBombings(c.Cache.g.Turn, bombings); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range bombings {
|
||||
bombingReport[i].Planet = bombings[i].Planet
|
||||
bombingReport[i].PlanetOwnedID = bombings[i].PlanetOwnedID
|
||||
bombingReport[i].Number = bombings[i].Number
|
||||
bombingReport[i].Owner = bombings[i].Owner
|
||||
bombingReport[i].Attacker = bombings[i].Attacker
|
||||
bombingReport[i].Production = bombings[i].Production
|
||||
bombingReport[i].Industry = report.F(bombings[i].Industry.F())
|
||||
bombingReport[i].Population = report.F(bombings[i].Population.F())
|
||||
bombingReport[i].Colonists = report.F(bombings[i].Colonists.F())
|
||||
bombingReport[i].Capital = report.F(bombings[i].Capital.F())
|
||||
bombingReport[i].Material = report.F(bombings[i].Material.F())
|
||||
bombingReport[i].AttackPower = report.F(bombings[i].AttackPower.F())
|
||||
bombingReport[i].Wiped = bombings[i].Wiped
|
||||
}
|
||||
}
|
||||
|
||||
// Store battles
|
||||
battleReport := make([]*report.BattleReport, len(battles))
|
||||
if len(battles) > 0 {
|
||||
battleMeta := make([]game.BattleMeta, len(battles))
|
||||
for i := range battles {
|
||||
b := battles[i]
|
||||
|
||||
observers := make(map[uuid.UUID]bool)
|
||||
for sgi := range b.ObserverGroups {
|
||||
observers[c.Cache.ShipGroup(sgi).OwnerID] = true
|
||||
}
|
||||
|
||||
battleMeta[i] = game.BattleMeta{
|
||||
Turn: c.Cache.g.Turn,
|
||||
Planet: b.Planet,
|
||||
BattleID: b.ID,
|
||||
ObserverIDs: slices.Collect(maps.Keys(observers)),
|
||||
}
|
||||
|
||||
report := TransformBattle(c.Cache, b)
|
||||
if err := c.Repo.SaveBattle(c.Cache.g.Turn, report, &battleMeta[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
battleReport[i] = report
|
||||
}
|
||||
}
|
||||
|
||||
// Remove killed ship groups
|
||||
c.Cache.DeleteKilledShipGroups()
|
||||
|
||||
// Store game state for the new turn and 'current' state as well
|
||||
if err := c.Repo.SaveNewTurn(c.Cache.g.Turn, c.Cache.g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rep := range c.Cache.Report(c.Cache.g.Turn, battleReport, bombingReport) {
|
||||
if err := c.Repo.SaveReport(c.Cache.g.Turn, rep); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i := range c.Cache.g.Race {
|
||||
if c.Cache.g.Race[i].Extinct {
|
||||
continue
|
||||
}
|
||||
c.Cache.g.Race[i].TTL -= 1
|
||||
}
|
||||
|
||||
// [ ] monitor memory consumption at this point?
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"galaxy/model/order"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Controller) ValidateOrder(actor string, commands ...order.DecodableCommand) (err error) {
|
||||
for i := range commands {
|
||||
if _, ok := commands[i].(order.CommandRaceQuit); ok && i != len(commands)-1 {
|
||||
err = e.NewQuitCommandFollowedByCommandError()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = errors.Join(err, c.applyCommand(actor, commands[i]))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Controller) applyCommand(actor string, cmd order.DecodableCommand) (err error) {
|
||||
var m *order.CommandMeta
|
||||
if v, ok := order.AsCommand[*order.CommandRaceQuit](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.RaceQuit(actor)
|
||||
} else if v, ok := order.AsCommand[*order.CommandRaceVote](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.RaceVote(actor, v.Acceptor)
|
||||
} else if v, ok := order.AsCommand[*order.CommandRaceRelation](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.RaceRelation(actor, v.Acceptor, v.Relation)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipClassCreate](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipClassCreate(actor, v.Name, v.Drive, int(v.Armament), v.Weapons, v.Shields, v.Cargo)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipClassMerge](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipClassMerge(actor, v.Name, v.Target)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipClassRemove](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipClassRemove(actor, v.Name)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupLoad](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupLoad(actor, uuid.MustParse(v.ID), v.Cargo, v.Quantity)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupUnload](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupUnload(actor, uuid.MustParse(v.ID), v.Quantity)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupSend](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupSend(actor, uuid.MustParse(v.ID), uint(v.Destination))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupUpgrade](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupUpgrade(actor, uuid.MustParse(v.ID), v.Tech, v.Level)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupMerge](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupMerge(actor)
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupBreak](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupBreak(actor, uuid.MustParse(v.ID), uuid.MustParse(v.NewID), uint(v.Quantity))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupDismantle](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupDismantle(actor, uuid.MustParse(v.ID))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupTransfer](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupTransfer(actor, v.Acceptor, uuid.MustParse(v.ID))
|
||||
} else if v, ok := order.AsCommand[*order.CommandShipGroupJoinFleet](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ShipGroupJoinFleet(actor, v.Name, uuid.MustParse(v.ID))
|
||||
} else if v, ok := order.AsCommand[*order.CommandFleetMerge](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.FleetMerge(actor, v.Name, v.Target)
|
||||
} else if v, ok := order.AsCommand[*order.CommandFleetSend](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.FleetSend(actor, v.Name, uint(v.Destination))
|
||||
} else if v, ok := order.AsCommand[*order.CommandScienceCreate](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ScienceCreate(actor, v.Name, v.Drive, v.Weapons, v.Shields, v.Cargo)
|
||||
} else if v, ok := order.AsCommand[*order.CommandScienceRemove](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.ScienceRemove(actor, v.Name)
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetRename](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetRename(actor, v.Number, v.Name)
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetProduce](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetProduce(actor, v.Number, v.Production, v.Subject)
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetRouteSet](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetRouteSet(actor, v.LoadType, uint(v.Origin), uint(v.Destination))
|
||||
} else if v, ok := order.AsCommand[*order.CommandPlanetRouteRemove](cmd); ok {
|
||||
m = &v.CommandMeta
|
||||
err = c.PlanetRouteRemove(actor, v.LoadType, uint(v.Origin))
|
||||
} else {
|
||||
return e.NewUnrecognizedCommandError(cmd.CommandType().String())
|
||||
}
|
||||
|
||||
if ge, ok := errors.AsType[*e.GenericError](err); ok {
|
||||
m.Result(ge.Code)
|
||||
} else if err != nil {
|
||||
panic(fmt.Errorf("error applying command has unknown origin: %w", err))
|
||||
} else {
|
||||
m.Result(0)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Controller) applyOrders(t uint) error {
|
||||
raceOrder := make(map[int][]order.DecodableCommand)
|
||||
commandRace := make(map[string]string)
|
||||
challenge := make(map[string]*order.CommandShipGroupUnload)
|
||||
cmdApplied := make(map[string]bool)
|
||||
|
||||
for ri := range c.Cache.listRaceActingIdx() {
|
||||
o, ok, err := c.Repo.LoadOrder(t, c.Cache.g.Race[ri].ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
raceOrder[ri] = o.Commands
|
||||
for i := range o.Commands {
|
||||
commandRace[o.Commands[i].CommandID()] = c.Cache.g.Race[ri].Name
|
||||
if v, ok := order.AsCommand[*order.CommandShipGroupUnload](o.Commands[i]); ok {
|
||||
if _, ok := challenge[v.ID]; ok {
|
||||
panic(fmt.Sprintf("unload command %s already cached", v.ID))
|
||||
}
|
||||
if ok, err := c.shouldChallenge(v); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
challenge[v.ID] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, cmdID := range c.challengeUnload(challenge) {
|
||||
if err := c.applyCommand(commandRace[cmdID], challenge[cmdID]); err == nil {
|
||||
cmdApplied[cmdID] = true
|
||||
}
|
||||
}
|
||||
|
||||
for ri := range raceOrder {
|
||||
for _, cmd := range raceOrder[ri] {
|
||||
if v, ok := cmdApplied[cmd.CommandID()]; ok && v {
|
||||
continue
|
||||
}
|
||||
// any command might fail due to challenged planets colonization
|
||||
_ = c.applyCommand(commandRace[cmd.CommandID()], cmd)
|
||||
}
|
||||
}
|
||||
|
||||
for ri := range c.Cache.listRaceActingIdx() {
|
||||
if err := c.Repo.SaveOrder(t, c.Cache.g.Race[ri].ID, &order.Order{Commands: raceOrder[ri]}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Controller) shouldChallenge(cmd *order.CommandShipGroupUnload) (resut bool, err error) {
|
||||
sgi, ok := c.Cache.shipGroupIndexByID(uuid.MustParse(cmd.ID))
|
||||
if !ok {
|
||||
err = e.NewGameStateError("challenge group unload: group not found: %v", cmd.ID)
|
||||
return
|
||||
}
|
||||
sg := c.Cache.ShipGroup(sgi)
|
||||
pn, ok := sg.AtPlanet()
|
||||
if !ok || sg.CargoType == nil {
|
||||
return false, nil
|
||||
}
|
||||
p := c.Cache.MustPlanet(pn)
|
||||
if p.Owned() || *sg.CargoType != game.CargoColonist {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Controller) challengeUnload(challenge map[string]*order.CommandShipGroupUnload) []string {
|
||||
if len(challenge) == 0 {
|
||||
return nil
|
||||
}
|
||||
planetRaceQuantity := make(map[uint]map[int]float64, 0)
|
||||
raceCommand := make(map[uint]map[int][]string)
|
||||
for cmdID, cmd := range challenge {
|
||||
sgi, ok := c.Cache.shipGroupIndexByID(uuid.MustParse(cmd.ID))
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("challenge group unload: group not found: %v", cmd.ID))
|
||||
}
|
||||
sg := c.Cache.ShipGroup(sgi)
|
||||
ri := c.Cache.ShipGroupOwnerRaceIndex(sgi)
|
||||
pn, ok := sg.AtPlanet()
|
||||
if _, ok := raceCommand[pn]; !ok {
|
||||
raceCommand[pn] = make(map[int][]string)
|
||||
}
|
||||
raceCommand[pn][ri] = append(raceCommand[pn][ri], cmdID)
|
||||
if _, ok := planetRaceQuantity[pn]; !ok {
|
||||
planetRaceQuantity[pn] = make(map[int]float64)
|
||||
}
|
||||
planetRaceQuantity[pn][ri] = planetRaceQuantity[pn][ri] + UnloadCargoRequest(float64(sg.Load), cmd.Quantity)
|
||||
}
|
||||
|
||||
result := make([]string, 0)
|
||||
for pn := range planetRaceQuantity {
|
||||
if len(planetRaceQuantity[pn]) < 2 {
|
||||
continue
|
||||
}
|
||||
winner := MaxOrRandomLoadId(planetRaceQuantity[pn], func(ri int) float64 { return float64(c.Cache.g.Race[ri].Votes) })
|
||||
result = append(result, raceCommand[pn][winner]...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) PlanetRename(ri int, number int, name string) error {
|
||||
n, ok := util.ValidateTypeName(name)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if number < 0 {
|
||||
return e.NewPlanetNumberError(number)
|
||||
}
|
||||
p, ok := c.Planet(uint(number))
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("planet #%d", number)
|
||||
}
|
||||
if !p.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d", number)
|
||||
}
|
||||
c.g.Map.Planet[c.MustPlanetIndex(p.Number)].Name = n
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) PlanetProduce(ri int, number int, prod game.ProductionType, subj string) error {
|
||||
c.validateRaceIndex(ri)
|
||||
if number < 0 {
|
||||
return e.NewPlanetNumberError(number)
|
||||
}
|
||||
p, ok := c.Planet(uint(number))
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("planet #%d", number)
|
||||
}
|
||||
if !p.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d", number)
|
||||
}
|
||||
var subjectID *uuid.UUID
|
||||
if prod == game.ResearchScience || prod == game.ProductionShip {
|
||||
if _, ok := util.ValidateTypeName(subj); !ok {
|
||||
return e.NewEntityTypeNameValidationError("%s=%q", prod, subj)
|
||||
}
|
||||
}
|
||||
|
||||
if prod == game.ResearchScience {
|
||||
i := slices.IndexFunc(c.g.Race[ri].Sciences, func(s game.Science) bool { return s.Name == subj })
|
||||
if i < 0 {
|
||||
return e.NewEntityNotExistsError("science %q", subj)
|
||||
}
|
||||
subjectID = &c.g.Race[ri].Sciences[i].ID
|
||||
}
|
||||
|
||||
if prod == game.ProductionShip {
|
||||
st, _, ok := c.ShipClass(ri, subj)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("ship type %q", subj)
|
||||
}
|
||||
if p.Production.Type == game.ProductionShip &&
|
||||
p.Production.SubjectID != nil &&
|
||||
*p.Production.SubjectID == st.ID {
|
||||
// Planet already produces this ship type, keeping progress intact
|
||||
return nil
|
||||
}
|
||||
subjectID = &st.ID
|
||||
}
|
||||
|
||||
if p.Production.Type == game.ProductionShip && (prod != game.ProductionShip || *subjectID != *p.Production.SubjectID) {
|
||||
p.ReleaseMaterial(c.MustShipType(ri, *p.Production.SubjectID).EmptyMass())
|
||||
} else if prod == game.ProductionShip {
|
||||
// new ship class to produce; otherwise we must have been returned from the func earlier
|
||||
p.Production.Progress = new(game.Float)
|
||||
p.Production.ProdUsed = new(game.Float)
|
||||
}
|
||||
|
||||
if prod != game.ProductionShip {
|
||||
p.Production.Progress = nil
|
||||
p.Production.ProdUsed = nil
|
||||
}
|
||||
|
||||
p.Production.Type = prod
|
||||
p.Production.SubjectID = subjectID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) PlanetProductionDisplayName(pn uint) string {
|
||||
p := c.MustPlanet(pn)
|
||||
if !p.Owned() {
|
||||
return "-"
|
||||
}
|
||||
ri := c.RaceIndex(*p.Owner)
|
||||
switch pt := p.Production.Type; pt {
|
||||
case game.ResearchDrive:
|
||||
return "Drive"
|
||||
case game.ResearchWeapons:
|
||||
return "Weapons"
|
||||
case game.ResearchShields:
|
||||
return "Shields"
|
||||
case game.ResearchCargo:
|
||||
return "Cargo"
|
||||
case game.ProductionMaterial:
|
||||
return "Material"
|
||||
case game.ProductionCapital:
|
||||
return "Capital"
|
||||
case game.ProductionShip:
|
||||
return c.MustShipType(ri, *p.Production.SubjectID).Name
|
||||
case game.ResearchScience:
|
||||
i := slices.IndexFunc(c.g.Race[ri].Sciences, func(sc game.Science) bool { return sc.ID == *p.Production.SubjectID })
|
||||
if i < 0 {
|
||||
panic("researching science not found")
|
||||
}
|
||||
return c.g.Race[ri].Sciences[i].Name
|
||||
default:
|
||||
return string(pt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Planet(planetNumber uint) (*game.Planet, bool) {
|
||||
if c.cachePlanetByPlanetNumber == nil {
|
||||
c.cachePlanetByPlanetNumber = make(map[uint]*game.Planet)
|
||||
for p := range c.g.Map.Planet {
|
||||
c.cachePlanetByPlanetNumber[c.g.Map.Planet[p].Number] = &c.g.Map.Planet[p]
|
||||
}
|
||||
}
|
||||
if v, ok := c.cachePlanetByPlanetNumber[planetNumber]; ok {
|
||||
return v, true
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) MustPlanet(pn uint) *game.Planet {
|
||||
if v, ok := c.Planet(pn); ok {
|
||||
return v
|
||||
} else {
|
||||
panic(fmt.Sprintf("planet not found by number=%d", pn))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) MustPlanetIndex(pn uint) int {
|
||||
if idx := slices.IndexFunc(c.g.Map.Planet, func(p game.Planet) bool { return p.Number == pn }); idx < 0 {
|
||||
panic(fmt.Sprintf("planet not found by number=%d", pn))
|
||||
} else {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
|
||||
// Свободный "Производственный Потенциал" (L)
|
||||
// промышленность * 0.75 + население * 0.25
|
||||
// за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
|
||||
func (c *Cache) PlanetProductionCapacity(planetNumber uint) float64 {
|
||||
p := c.MustPlanet(planetNumber)
|
||||
var busyResources float64
|
||||
for sg := range c.shipGroupsInUpgrade(p.Number) {
|
||||
busyResources += sg.StateUpgrade.Cost()
|
||||
}
|
||||
return p.ProductionCapacity() - busyResources
|
||||
}
|
||||
|
||||
func (c *Cache) TurnPlanetProductions() {
|
||||
for sgi := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(sgi)
|
||||
// cancel upgrade for groups on wiped planets
|
||||
if sg.State() == game.StateUpgrade && !c.MustPlanet(sg.Destination).Owned() {
|
||||
sg.StateUpgrade = nil
|
||||
}
|
||||
}
|
||||
|
||||
for pn := range c.listProducingPlanets() {
|
||||
p := c.MustPlanet(pn)
|
||||
ri := c.RaceIndex(*p.Owner)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
// upgrade groups and return to in_orbit state
|
||||
productionAvailable := c.PlanetProductionCapacity(pn)
|
||||
for sg := range c.shipGroupsInUpgrade(p.Number) {
|
||||
cost := sg.StateUpgrade.Cost()
|
||||
if productionAvailable >= cost {
|
||||
for i := range sg.StateUpgrade.UpgradeTech {
|
||||
sg.Tech = sg.Tech.Set(sg.StateUpgrade.UpgradeTech[i].Tech, util.Fixed3(sg.StateUpgrade.UpgradeTech[i].Level.F()))
|
||||
}
|
||||
productionAvailable -= cost
|
||||
}
|
||||
sg.StateUpgrade = nil
|
||||
}
|
||||
|
||||
switch pt := p.Production.Type; pt {
|
||||
case game.ProductionShip:
|
||||
st := c.MustShipType(ri, *p.Production.SubjectID)
|
||||
if ships := ProduceShip(p, productionAvailable, st.EmptyMass()); ships > 0 {
|
||||
c.unsafeCreateShips(ri, st.ID, p.Number, ships)
|
||||
}
|
||||
case game.ResearchScience:
|
||||
sc := c.mustScience(ri, *p.Production.SubjectID)
|
||||
ResearchTech(r, productionAvailable, sc.Drive.F(), sc.Weapons.F(), sc.Shields.F(), sc.Cargo.F())
|
||||
case game.ResearchDrive:
|
||||
ResearchTech(r, productionAvailable, 1., 0, 0, 0)
|
||||
case game.ResearchWeapons:
|
||||
ResearchTech(r, productionAvailable, 0, 1., 0, 0)
|
||||
case game.ResearchShields:
|
||||
ResearchTech(r, productionAvailable, 0, 0, 1., 0)
|
||||
case game.ResearchCargo:
|
||||
ResearchTech(r, productionAvailable, 0, 0, 0, 1.)
|
||||
case game.ProductionMaterial:
|
||||
p.ProduceMaterial(productionAvailable)
|
||||
case game.ProductionCapital:
|
||||
p.ProduceIndustry(productionAvailable)
|
||||
default:
|
||||
panic(fmt.Sprintf("unprocessed production type: '%v' for planet: #%d owner=%v", pt, pn, p.Owner))
|
||||
}
|
||||
|
||||
// last step: increase population / colonists
|
||||
p.ProducePopulation()
|
||||
}
|
||||
c.TurnMergeEqualShipGroups()
|
||||
}
|
||||
|
||||
// listProducingPlanets iterates over all inhabited planet numbers with defined production type.
|
||||
// Planets producing ships guaranteed to be iterated first for correct turn actions order.
|
||||
func (c *Cache) listProducingPlanets() iter.Seq[uint] {
|
||||
ordered := make([]int, 0)
|
||||
for i := range c.g.Map.Planet {
|
||||
if !c.g.Map.Planet[i].Owned() || c.g.Map.Planet[i].Production.Type == game.ProductionNone {
|
||||
continue
|
||||
}
|
||||
ordered = append(ordered, i)
|
||||
}
|
||||
slices.SortFunc(ordered, func(l, r int) int {
|
||||
if c.g.Map.Planet[l].Production.Type == game.ProductionShip && c.g.Map.Planet[r].Production.Type != game.ProductionShip {
|
||||
return -1
|
||||
}
|
||||
if c.g.Map.Planet[l].Production.Type != game.ProductionShip && c.g.Map.Planet[r].Production.Type == game.ProductionShip {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return func(yield func(uint) bool) {
|
||||
for _, i := range ordered {
|
||||
if !yield(c.g.Map.Planet[i].Number) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal funcs
|
||||
|
||||
func (c *Cache) putPopulation(pn uint, v float64) {
|
||||
c.MustPlanet(pn).Pop(v)
|
||||
}
|
||||
|
||||
func (c *Cache) putColonists(pn uint, v float64) {
|
||||
c.MustPlanet(pn).Col(v)
|
||||
}
|
||||
|
||||
func (c *Cache) putMaterial(pn uint, v float64) {
|
||||
c.MustPlanet(pn).Mat(v)
|
||||
}
|
||||
|
||||
func ProduceShip(p *game.Planet, productionAvailable, shipMass float64) uint {
|
||||
if productionAvailable <= 0 {
|
||||
return 0
|
||||
}
|
||||
ships := uint(0)
|
||||
pa := productionAvailable
|
||||
PRODcost := ShipProductionCost(shipMass)
|
||||
var MATneed, MATfarm, totalCost float64
|
||||
for {
|
||||
MATneed = shipMass - float64(p.Material)
|
||||
if MATneed < 0 {
|
||||
MATneed = 0
|
||||
}
|
||||
MATfarm = MATneed / float64(p.Resources)
|
||||
totalCost = PRODcost + MATfarm
|
||||
// fmt.Printf("PRODcost: %3.03f MATcost: %3.03f MAThave: %3.03f MATneed: %3.03f MATfarm: %3.03f total: %3.03f \n",
|
||||
// PRODcost, shipMass, float64(p.Material), MATneed, MATfarm, totalCost)
|
||||
if pa < totalCost {
|
||||
progress := pa / totalCost
|
||||
pval := game.F(progress)
|
||||
if p.Production.Progress != nil {
|
||||
pval += *p.Production.Progress
|
||||
}
|
||||
p.Production.Progress = &pval
|
||||
fval := game.F(pa)
|
||||
p.Production.ProdUsed = &fval
|
||||
// fmt.Println("pa", pa, "progress", progress, "MAT:", progress*shipMass)
|
||||
return ships
|
||||
} else {
|
||||
pa -= totalCost
|
||||
p.Mat(float64(p.Material) - shipMass + MATneed)
|
||||
ships += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ShipProductionCost(shipMass float64) float64 {
|
||||
return shipMass * 10.
|
||||
}
|
||||
func ShipMaterialCost(shipMass, planetResource float64) float64 {
|
||||
return shipMass / planetResource
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPlanetRename(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.Equal(t, "Planet_0", c.MustPlanet(R0_Planet_0_num).Name)
|
||||
assert.NoError(t, g.PlanetRename(Race_0.Name, int(R0_Planet_0_num), "Home_World"))
|
||||
assert.Equal(t, "Home_World", c.MustPlanet(R0_Planet_0_num).Name)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRename(UnknownRace, int(R0_Planet_0_num), "Home_World"),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRename(Race_Extinct.Name, int(R0_Planet_0_num), "Home_World"),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRename(Race_0.Name, -1, "Home_World"),
|
||||
e.GenericErrorText(e.ErrInputPlanetNumber))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRename(Race_0.Name, 500, "Home_World"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRename(Race_0.Name, int(R1_Planet_1_num), "Home_World"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
}
|
||||
|
||||
func TestPlanetProduce(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
scienceName := "Drive_Shields"
|
||||
assert.NoError(t, g.ScienceCreate(Race_0.Name, scienceName, 0.4, 0, 0.6, 0))
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 1)
|
||||
scID := c.RaceScience(Race_0_idx)[0].ID
|
||||
|
||||
assert.Equal(t, "-", c.PlanetProductionDisplayName(3))
|
||||
|
||||
pn := int(R0_Planet_0_num)
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "MAT", ""))
|
||||
assert.Equal(t, game.ProductionMaterial, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.Equal(t, "Material", c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "CAP", ""))
|
||||
assert.Equal(t, game.ProductionCapital, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.Equal(t, "Capital", c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "Weapons", "500"))
|
||||
assert.Equal(t, game.ResearchWeapons, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.Equal(t, "Weapons", c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "cargo", ""))
|
||||
assert.Equal(t, game.ResearchCargo, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.Equal(t, "Cargo", c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SHIELDS", scienceName))
|
||||
assert.Equal(t, game.ResearchShields, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.Equal(t, "Shields", c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "DrivE", ""))
|
||||
assert.Equal(t, game.ResearchDrive, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.Equal(t, "Drive", c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "Science", scienceName))
|
||||
assert.Equal(t, game.ResearchScience, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.Nil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Equal(t, scID, *c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Equal(t, scienceName, c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SHIP", Race_0_Gunship))
|
||||
assert.Equal(t, game.ProductionShip, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
st := c.MustShipClass(Race_0_idx, Race_0_Gunship)
|
||||
assert.Equal(t, st.ID, *c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
assert.Equal(t, st.Name, c.PlanetProductionDisplayName(R0_Planet_0_num))
|
||||
|
||||
pn = int(R0_Planet_2_num)
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(UnknownRace, pn, "DRIVE", ""),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_Extinct.Name, pn, "DRIVE", ""),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, pn, "Hyperdrive", ""),
|
||||
e.GenericErrorText(e.ErrInputProductionInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, -1, "DRIVE", ""),
|
||||
e.GenericErrorText(e.ErrInputPlanetNumber))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, 500, "DRIVE", ""),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, int(R1_Planet_1_num), "DRIVE", ""),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, pn, "Science", ""),
|
||||
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, pn, "SHIP", ""),
|
||||
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, pn, "Science", "Winning"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetProduce(Race_0.Name, pn, "SHIP", "Drone"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
}
|
||||
|
||||
func TestPlanetProductionCapacity(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
|
||||
assert.Equal(t, 100., c.PlanetProductionCapacity(R0_Planet_0_num))
|
||||
c.UpgradeShipGroup(0, game.TechDrive, 1.6)
|
||||
assert.Equal(t, 53.125, c.PlanetProductionCapacity(R0_Planet_0_num))
|
||||
}
|
||||
|
||||
func TestProduceShips(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
pn := int(R0_Planet_0_num)
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SHIP", Race_0_Gunship))
|
||||
assert.Equal(t, game.ProductionShip, c.MustPlanet(R0_Planet_0_num).Production.Type)
|
||||
assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
st := c.MustShipClass(Race_0_idx, Race_0_Gunship)
|
||||
assert.Equal(t, st.ID, *c.MustPlanet(R0_Planet_0_num).Production.SubjectID)
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Size = 1000.
|
||||
c.MustPlanet(R0_Planet_0_num).Population = 1000.
|
||||
c.MustPlanet(R0_Planet_0_num).Industry = 1000.
|
||||
c.MustPlanet(R0_Planet_0_num).Resources = 10.
|
||||
shipMass := st.EmptyMass()
|
||||
|
||||
c.TurnPlanetProductions()
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 0)
|
||||
assert.NotNil(t, c.MustPlanet(R0_Planet_0_num).Production.Progress)
|
||||
progress := *c.MustPlanet(R0_Planet_0_num).Production.Progress
|
||||
assert.InDelta(t, 0.45, progress.F(), 0.001)
|
||||
|
||||
assert.NoError(t, c.ShipClassCreate(Race_0_idx, "Drone", 1, 0, 0, 0, 0))
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, "Drone", uint(pn), 7))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
|
||||
assert.Equal(t, uint(7), c.ShipGroup(0).Number)
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SHIP", "Drone"))
|
||||
assert.InDelta(t, shipMass*progress.F(), c.MustPlanet(R0_Planet_0_num).Material.F(), 0.00001) // 99.(0099) material build
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Material = 0
|
||||
c.MustPlanet(R0_Planet_0_num).Colonists = 0
|
||||
|
||||
c.TurnPlanetProductions()
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
|
||||
assert.Equal(t, uint(106), c.ShipGroup(0).Number)
|
||||
progress = *c.MustPlanet(R0_Planet_0_num).Production.Progress
|
||||
assert.InDelta(t, 0.0099, progress.F(), 0.00001) // 1.(0099) drones with no CAP on planet
|
||||
|
||||
//
|
||||
// groups is upgrade state
|
||||
//
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, int(R0_Planet_0_num), "MAT", ""))
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, int(R0_Planet_2_num), "CAP", ""))
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, "Drone", R0_Planet_2_num, 5))
|
||||
c.MustPlanet(R0_Planet_2_num).Resources = 5
|
||||
c.MustPlanet(R0_Planet_2_num).Population = 100
|
||||
c.MustPlanet(R0_Planet_2_num).Industry = 100
|
||||
c.RaceTechLevel(Race_0_idx, game.TechDrive, 1.5)
|
||||
assert.NoError(t, g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(1).ID, "Drive", 0))
|
||||
assert.NoError(t, g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "Drive", 0))
|
||||
assert.Equal(t, game.StateUpgrade, c.ShipGroup(0).State())
|
||||
assert.Equal(t, game.StateUpgrade, c.ShipGroup(1).State())
|
||||
|
||||
c.MustPlanet(R0_Planet_2_num).Free() // wipe planet as battle result
|
||||
c.TurnPlanetProductions()
|
||||
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(1).State())
|
||||
assert.Equal(t, 1.1, c.ShipGroup(1).TechLevel(game.TechDrive).F())
|
||||
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
|
||||
assert.Equal(t, 1.5, c.ShipGroup(0).TechLevel(game.TechDrive).F())
|
||||
|
||||
assert.Equal(t, 4346.676567656759, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
}
|
||||
|
||||
func TestProduceShip(t *testing.T) {
|
||||
Drone := game.ShipType{
|
||||
Name: "Drone",
|
||||
Drive: 1,
|
||||
Armament: 0,
|
||||
Weapons: 0,
|
||||
Shields: 0,
|
||||
Cargo: 0,
|
||||
}
|
||||
BattleShip := game.ShipType{
|
||||
Name: "BattleShip",
|
||||
Drive: 25,
|
||||
Armament: 1,
|
||||
Weapons: 30,
|
||||
Shields: 35,
|
||||
Cargo: 0,
|
||||
}
|
||||
TestShipCargo1 := game.ShipType{
|
||||
Name: "Cargo1",
|
||||
Drive: 30.18,
|
||||
Armament: 0,
|
||||
Weapons: 0.,
|
||||
Shields: 0.,
|
||||
Cargo: 19.,
|
||||
}
|
||||
TestShipDROCOLZ := game.ShipType{
|
||||
Name: "DROCOLZ",
|
||||
Drive: 11.32,
|
||||
Armament: 0,
|
||||
Weapons: 0,
|
||||
Shields: 0,
|
||||
Cargo: 1,
|
||||
}
|
||||
TestShipElephant := game.ShipType{
|
||||
Name: "ElEphant",
|
||||
Drive: 80,
|
||||
Armament: 30,
|
||||
Weapons: 50,
|
||||
Shields: 100,
|
||||
Cargo: 0,
|
||||
}
|
||||
|
||||
id := uuid.New()
|
||||
var r uint
|
||||
hw := controller.NewPlanet(0, "Planet_0", &id, 1, 1, 1000, 1000, 1000, 10, game.ProductionShip.AsType(uuid.Nil))
|
||||
|
||||
//
|
||||
// documented data
|
||||
//
|
||||
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), Drone.EmptyMass())
|
||||
assert.Equal(t, uint(99), r)
|
||||
assert.InDelta(t, 0.0099, (*hw.Production.Progress).F(), 0.000001)
|
||||
assert.Equal(t, 0.009900990099, (*hw.Production.Progress).F()) // 0.0099 % = 99.0099 mass production per turn
|
||||
|
||||
(&hw).Production = game.ProductionShip.AsType(uuid.Nil)
|
||||
(&hw).Material = 100. // no material deficit
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), Drone.EmptyMass())
|
||||
assert.Equal(t, uint(100), r)
|
||||
assert.Equal(t, 0., (*hw.Production.Progress).F())
|
||||
assert.Equal(t, 0., hw.Material.F())
|
||||
|
||||
(&hw).Production = game.ProductionShip.AsType(uuid.Nil)
|
||||
(&hw).Material = 0.
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), BattleShip.EmptyMass())
|
||||
assert.Equal(t, uint(1), r)
|
||||
assert.InDelta(t, 0.1, (*hw.Production.Progress).F(), 0.001)
|
||||
|
||||
(&hw).Production = game.ProductionShip.AsType(uuid.Nil)
|
||||
(&hw).Material = 900. // no material deficit
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), BattleShip.EmptyMass())
|
||||
assert.Equal(t, uint(1), r)
|
||||
assert.Equal(t, util.Fixed12(1./9.), (*hw.Production.Progress).F())
|
||||
|
||||
//
|
||||
// real report data
|
||||
//
|
||||
|
||||
dw1 := controller.NewPlanet(0, "DW2", &id, 1, 1, 500, 500, 500, 10, game.ProductionShip.AsType(uuid.Nil))
|
||||
dw2 := controller.NewPlanet(0, "DW1", &id, 1, 1, 500, 500, 500, 10, game.ProductionShip.AsType(uuid.Nil))
|
||||
|
||||
(&dw1).Material = 0.0
|
||||
r = controller.ProduceShip(&dw1, dw1.ProductionCapacity(), TestShipDROCOLZ.EmptyMass())
|
||||
assert.Equal(t, uint(4), r)
|
||||
assert.Equal(t, 2.272, (*dw1.Production.ProdUsed).F())
|
||||
|
||||
(&dw2).Material = 0.0
|
||||
r = controller.ProduceShip(&dw2, dw2.ProductionCapacity(), TestShipCargo1.EmptyMass())
|
||||
assert.Equal(t, uint(1), r)
|
||||
assert.Equal(t, 3.282, (*dw2.Production.ProdUsed).F())
|
||||
|
||||
// production stopped and extra MAT released
|
||||
(&dw2).ReleaseMaterial(TestShipCargo1.EmptyMass())
|
||||
assert.Equal(t, 0.32495049505, (&dw2).Material.F()) // from report: 0.32
|
||||
|
||||
// building new ship with extra MAT
|
||||
r = controller.ProduceShip(&dw2, dw2.ProductionCapacity(), TestShipDROCOLZ.EmptyMass())
|
||||
assert.Equal(t, uint(4), r)
|
||||
assert.Equal(t, 2.304495049505, (*dw2.Production.ProdUsed).F()) // from report: 2.3
|
||||
|
||||
//
|
||||
// insufficient production capacity to produce single ship at one turn
|
||||
//
|
||||
|
||||
assert.Greater(t, TestShipElephant.EmptyMass(), 100.)
|
||||
|
||||
// one turn
|
||||
(&hw).Production = game.ProductionShip.AsType(uuid.Nil)
|
||||
(&hw).Material = 0.
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), TestShipElephant.EmptyMass())
|
||||
assert.Equal(t, uint(0), r)
|
||||
assert.Equal(t, 0., (&hw).Material.F())
|
||||
assert.InDelta(t, 0.1, (*hw.Production.Progress).F(), 0.01)
|
||||
(&hw).ReleaseMaterial(TestShipElephant.EmptyMass())
|
||||
assert.Equal(t, 99.009900990099, (&hw).Material.F())
|
||||
|
||||
// two turns
|
||||
(&hw).Production = game.ProductionShip.AsType(uuid.Nil)
|
||||
(&hw).Material = 0.
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), TestShipElephant.EmptyMass())
|
||||
assert.Equal(t, uint(0), r)
|
||||
assert.Equal(t, 0., (&hw).Material.F())
|
||||
assert.InDelta(t, 0.1, (*hw.Production.Progress).F(), 0.01)
|
||||
r = controller.ProduceShip(&hw, hw.ProductionCapacity(), TestShipElephant.EmptyMass())
|
||||
assert.Equal(t, uint(0), r)
|
||||
assert.Equal(t, 0., (&hw).Material.F())
|
||||
assert.InDelta(t, 0.2, (*hw.Production.Progress).F(), 0.01)
|
||||
|
||||
(&hw).ReleaseMaterial(TestShipElephant.EmptyMass())
|
||||
assert.Equal(t, 198.019801980198, (&hw).Material.F())
|
||||
}
|
||||
|
||||
func TestListProducingPlanets(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
c.MustPlanet(0).Production = game.ProductionNone.AsType(uuid.Nil)
|
||||
c.MustPlanet(1).Production = game.ProductionNone.AsType(uuid.Nil)
|
||||
c.MustPlanet(2).Production = game.ProductionNone.AsType(uuid.Nil)
|
||||
|
||||
planets := slices.Collect(c.ListProducingPlanets())
|
||||
assert.Len(t, planets, 0)
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, int(R0_Planet_0_num), "CAP", ""))
|
||||
planets = slices.Collect(c.ListProducingPlanets())
|
||||
assert.Len(t, planets, 1)
|
||||
assert.Equal(t, R0_Planet_0_num, c.MustPlanet(planets[0]).Number)
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, int(R0_Planet_2_num), "SHIP", Race_0_Gunship))
|
||||
planets = slices.Collect(c.ListProducingPlanets())
|
||||
assert.Len(t, planets, 2)
|
||||
assert.Equal(t, R0_Planet_2_num, c.MustPlanet(planets[0]).Number)
|
||||
assert.Equal(t, R0_Planet_0_num, c.MustPlanet(planets[1]).Number)
|
||||
}
|
||||
|
||||
func TestTurnPlanetProductions(t *testing.T) {
|
||||
c, g := newCache()
|
||||
assert.NoError(t, c.ShipClassCreate(Race_0_idx, "Drone", 1, 0, 0, 0, 0))
|
||||
assert.NoError(t, g.ScienceCreate(Race_0.Name, "Equality", 0.25, 0.25, 0.25, 0.25))
|
||||
c.MustPlanet(R0_Planet_0_num).Resources = 10.
|
||||
c.MustPlanet(R0_Planet_0_num).Size = 1000.
|
||||
c.MustPlanet(R0_Planet_0_num).Population = 1000.
|
||||
c.MustPlanet(R0_Planet_0_num).Industry = 1000.
|
||||
|
||||
pn := int(R0_Planet_0_num)
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "CAP", ""))
|
||||
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Capital.F())
|
||||
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Colonists.F())
|
||||
c.TurnPlanetProductions()
|
||||
assert.InDelta(t, 196., c.MustPlanet(R0_Planet_0_num).Capital.F(), 0.1)
|
||||
assert.Equal(t, 10.0, c.MustPlanet(R0_Planet_0_num).Colonists.F())
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "MAT", ""))
|
||||
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
c.TurnPlanetProductions()
|
||||
assert.Equal(t, 10000., c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.InDelta(t, 20.0, c.MustPlanet(R0_Planet_0_num).Colonists.F(), 0.000001)
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "DRIVE", ""))
|
||||
assert.Equal(t, 1.1, c.Race(Race_0_idx).TechLevel(game.TechDrive))
|
||||
c.TurnPlanetProductions()
|
||||
assert.Equal(t, 1.3, c.Race(Race_0_idx).TechLevel(game.TechDrive))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "WEAPONS", ""))
|
||||
assert.Equal(t, 1.2, c.Race(Race_0_idx).TechLevel(game.TechWeapons))
|
||||
c.TurnPlanetProductions()
|
||||
assert.Equal(t, 1.4, c.Race(Race_0_idx).TechLevel(game.TechWeapons))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SHIELDS", ""))
|
||||
assert.Equal(t, 1.3, c.Race(Race_0_idx).TechLevel(game.TechShields))
|
||||
c.TurnPlanetProductions()
|
||||
assert.Equal(t, 1.5, c.Race(Race_0_idx).TechLevel(game.TechShields))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "CARGO", ""))
|
||||
assert.Equal(t, 1.4, c.Race(Race_0_idx).TechLevel(game.TechCargo))
|
||||
c.TurnPlanetProductions()
|
||||
assert.Equal(t, 1.6, c.Race(Race_0_idx).TechLevel(game.TechCargo))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SCIENCE", "Equality"))
|
||||
c.TurnPlanetProductions()
|
||||
assert.Equal(t, 1.35, c.Race(Race_0_idx).TechLevel(game.TechDrive))
|
||||
assert.Equal(t, 1.45, c.Race(Race_0_idx).TechLevel(game.TechWeapons))
|
||||
assert.Equal(t, 1.55, c.Race(Race_0_idx).TechLevel(game.TechShields))
|
||||
assert.Equal(t, 1.65, c.Race(Race_0_idx).TechLevel(game.TechCargo))
|
||||
|
||||
assert.NoError(t, g.PlanetProduce(Race_0.Name, pn, "SHIP", "Drone"))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 0)
|
||||
c.TurnPlanetProductions()
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
|
||||
assert.Equal(t, uint(100), c.ShipGroup(0).Number)
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
)
|
||||
|
||||
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.listRaceActingIdx() {
|
||||
for r2 := range c.listRaceActingIdx() {
|
||||
if r1 == r2 {
|
||||
continue
|
||||
}
|
||||
rel := slices.IndexFunc(c.g.Race[r1].Relations, func(r game.RaceRelation) bool { return r.RaceID == c.g.Race[r2].ID })
|
||||
if rel < 0 {
|
||||
panic(fmt.Sprintf("Relation: opponent not found idx=%d", r2))
|
||||
}
|
||||
c.updateRelationCache(r1, r2, c.g.Race[r1].Relations[rel].Relation)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if _, ok := c.cacheRelation[r1]; !ok {
|
||||
panic(fmt.Sprintf("Relation: no left race idx=%d", r1))
|
||||
}
|
||||
if v, ok := c.cacheRelation[r1][r2]; !ok {
|
||||
panic(fmt.Sprintf("Relation: no right race idx=%d", r2))
|
||||
} else {
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) updateRelationCache(r1, r2 int, rel game.Relation) {
|
||||
if r1 == r2 {
|
||||
return
|
||||
}
|
||||
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) Voted(ri int) int {
|
||||
c.validateRaceIndex(ri)
|
||||
return c.RaceIndex(c.g.Race[ri].VoteFor)
|
||||
}
|
||||
|
||||
func (c *Cache) UpdateRelation(ri, other int, rel game.Relation) (err error) {
|
||||
defer func() {
|
||||
if err == nil && c.cacheRelation != nil {
|
||||
c.updateRelationCache(ri, other, rel)
|
||||
}
|
||||
}()
|
||||
for o := range c.g.Race[ri].Relations {
|
||||
switch {
|
||||
case ri == other:
|
||||
c.g.Race[ri].Relations[o].Relation = rel
|
||||
case c.g.Race[ri].Relations[o].RaceID == c.g.Race[other].ID:
|
||||
c.g.Race[ri].Relations[o].Relation = rel
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if ri != other {
|
||||
err = e.NewGameStateError("UpdateRelation: opponent not found")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache) validateRaceIndex(i int) {
|
||||
if i >= len(c.g.Race) {
|
||||
panic(fmt.Sprintf("race index out of range: %d >= %d", i, len(c.g.Race)))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) validActor(name string) (int, error) {
|
||||
i, err := c.validRace(name)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
c.g.Race[i].TTL = 10
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// validRace returns index of race with given name or error when race not found or extinct
|
||||
func (c *Cache) validRace(name string) (int, error) {
|
||||
i, err := c.raceIndex(name)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if c.g.Race[i].Extinct {
|
||||
return -1, e.NewRaceExinctError(name)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *Cache) raceTechLevel(ri int, t game.Tech, v float64) {
|
||||
c.validateRaceIndex(ri)
|
||||
c.g.Race[ri].Tech = c.g.Race[ri].Tech.Set(t, v)
|
||||
}
|
||||
|
||||
func (c *Cache) TurnWipeExtinctRaces() {
|
||||
for i := range c.listRaceActingIdx() {
|
||||
if c.g.Race[i].TTL == 0 {
|
||||
c.wipeRace(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) wipeRace(ri int) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
c.g.ShipGroups = slices.DeleteFunc(c.g.ShipGroups, func(v game.ShipGroup) bool { return v.OwnerID == r.ID })
|
||||
c.g.Fleets = slices.DeleteFunc(c.g.Fleets, func(v game.Fleet) bool { return v.OwnerID == r.ID })
|
||||
clear(r.ShipTypes)
|
||||
clear(r.Sciences)
|
||||
for i := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[i]
|
||||
if p.Owner != nil && *p.Owner != r.ID {
|
||||
continue
|
||||
}
|
||||
p.Wipe()
|
||||
}
|
||||
for i := range c.listRaceActingIdx() {
|
||||
if i == ri {
|
||||
continue
|
||||
}
|
||||
if c.g.Race[i].VoteFor == r.ID {
|
||||
c.g.Race[i].VoteFor = c.g.Race[i].ID
|
||||
}
|
||||
}
|
||||
r.Votes = 0
|
||||
r.VoteFor = r.ID
|
||||
r.Extinct = true
|
||||
r.TTL = 0
|
||||
c.invalidateFleetCache()
|
||||
c.invalidateShipGroupCache()
|
||||
}
|
||||
|
||||
func (c *Cache) listRaceActingIdx() iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
for i := range c.listRaceIdx() {
|
||||
if c.g.Race[i].Extinct {
|
||||
continue
|
||||
}
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) listRaceIdx() iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
for i := range c.g.Race {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRaceVote(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.Equal(t, c.Voted(Race_0_idx), Race_0_idx)
|
||||
assert.Equal(t, c.Voted(Race_1_idx), Race_1_idx)
|
||||
|
||||
assert.NoError(t, g.RaceVote(Race_0.Name, Race_1.Name))
|
||||
assert.Equal(t, Race_1_idx, c.Voted(Race_0_idx))
|
||||
assert.Equal(t, Race_1_idx, c.Voted(Race_1_idx))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.RaceVote(UnknownRace, Race_1.Name),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceVote(Race_0.Name, UnknownRace),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceVote(Race_0.Name, Race_Extinct.Name),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceVote(Race_Extinct.Name, Race_1.Name),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
}
|
||||
|
||||
func TestRaceRelation(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.RaceRelation(Race_0.Name, Race_1.Name, "war"))
|
||||
assert.NoError(t, g.RaceRelation(Race_1.Name, Race_0.Name, "PEACE"))
|
||||
|
||||
assert.Equal(t, game.RelationWar, c.Relation(Race_0_idx, Race_1_idx))
|
||||
assert.Equal(t, game.RelationPeace, c.Relation(Race_1_idx, Race_0_idx))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.RaceRelation(Race_0.Name, Race_1.Name, "Wojna"),
|
||||
e.GenericErrorText(e.ErrInputUnknownRelation))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceRelation(Race_0.Name, UnknownRace, "War"),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceRelation(UnknownRace, Race_0.Name, "Peace"),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceRelation(Race_0.Name, Race_Extinct.Name, "War"),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.RaceRelation(Race_Extinct.Name, Race_0.Name, "War"),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
}
|
||||
|
||||
func TestRaceQuit(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.RaceQuit(UnknownRace),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.RaceQuit(Race_Extinct.Name),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
|
||||
assert.NoError(t, g.RaceQuit(Race_0.Name))
|
||||
assert.Equal(t, 3, int(c.Race(Race_0_idx).TTL))
|
||||
}
|
||||
|
||||
func TestRaceID(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
c.Race(Race_0_idx).TTL = 9
|
||||
|
||||
_, err := g.RaceID(UnknownRace)
|
||||
assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
|
||||
_, err = g.RaceID(Race_Extinct.Name)
|
||||
assert.ErrorContains(t, err, e.GenericErrorText(e.ErrRaceExinct))
|
||||
|
||||
id, err := g.RaceID(Race_0.Name)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, Race_0_ID, id)
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
mr "galaxy/model/report"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) Report(t uint, battles []*mr.BattleReport, bombings []*mr.Bombing) iter.Seq[*mr.Report] {
|
||||
report := c.InitReport(t)
|
||||
return func(yield func(*mr.Report) bool) {
|
||||
for i := range c.listRaceActingIdx() {
|
||||
c.ReportRace(i, report, battles, bombings)
|
||||
if !yield(report) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) InitReport(t uint) *mr.Report {
|
||||
report := &mr.Report{
|
||||
Turn: t,
|
||||
Width: c.g.Map.Width,
|
||||
Height: c.g.Map.Height,
|
||||
PlanetCount: uint32(len(c.g.Map.Planet)),
|
||||
Player: make([]mr.Player, len(c.g.Race)),
|
||||
LocalScience: make([]mr.Science, 0, 10),
|
||||
OtherScience: make([]mr.OtherScience, 0, 10),
|
||||
LocalShipClass: make([]mr.ShipClass, 0, 20),
|
||||
OtherShipClass: make([]mr.OthersShipClass, 0, 50),
|
||||
Battle: make([]uuid.UUID, 0, 10),
|
||||
Bombing: make([]*mr.Bombing, 0, 10),
|
||||
IncomingGroup: make([]mr.IncomingGroup, 0, 10),
|
||||
OnPlanetGroupCache: make(map[uint][]int),
|
||||
InSpaceGroupRangeCache: make(map[int]map[uint]float64),
|
||||
}
|
||||
|
||||
sumVote, sumPop, sumInd := make(map[int]float64), make(map[int]float64), make(map[int]float64)
|
||||
planets := make(map[int]uint16)
|
||||
for pi := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[pi]
|
||||
if !p.Owned() {
|
||||
continue
|
||||
}
|
||||
ri := c.RaceIndex(*p.Owner)
|
||||
sumPop[ri] += p.Population.F()
|
||||
sumInd[ri] += p.Industry.F()
|
||||
planets[ri] = planets[ri] + 1
|
||||
}
|
||||
|
||||
for ri := range c.listRaceIdx() {
|
||||
r := &c.g.Race[ri]
|
||||
rr := &report.Player[ri]
|
||||
|
||||
rr.ID = r.ID
|
||||
rr.Name = r.Name
|
||||
rr.Extinct = r.Extinct
|
||||
rr.Drive = mr.F(r.TechLevel(game.TechDrive))
|
||||
rr.Weapons = mr.F(r.TechLevel(game.TechWeapons))
|
||||
rr.Shields = mr.F(r.TechLevel(game.TechShields))
|
||||
rr.Cargo = mr.F(r.TechLevel(game.TechCargo))
|
||||
rr.Planets = planets[ri]
|
||||
rr.Population = mr.F(sumPop[ri])
|
||||
rr.Industry = mr.F(sumInd[ri])
|
||||
|
||||
// give voices by race index
|
||||
if vi := slices.IndexFunc(c.g.Race, func(v game.Race) bool { return r.VoteFor == v.ID }); vi < 0 {
|
||||
panic(fmt.Sprintf("voting for unknown race, id=%v", r.VoteFor))
|
||||
} else {
|
||||
sumVote[vi] += r.Votes.F()
|
||||
dest := &report.Player[vi]
|
||||
dest.Votes = mr.F(sumVote[vi])
|
||||
}
|
||||
}
|
||||
slices.SortFunc(report.Player, func(a, b mr.Player) int { return cmp.Compare(a.Name, b.Name) })
|
||||
|
||||
for sgi := range c.g.ShipGroups {
|
||||
sg := &c.g.ShipGroups[sgi]
|
||||
if sg.State() == game.StateInSpace {
|
||||
// pre-calculate distances from in_space ship groups to every planet
|
||||
if _, ok := report.InSpaceGroupRangeCache[sgi]; !ok {
|
||||
report.InSpaceGroupRangeCache[sgi] = make(map[uint]float64)
|
||||
}
|
||||
for pi := range c.g.Map.Planet {
|
||||
p2 := &c.g.Map.Planet[pi]
|
||||
distance := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F())
|
||||
report.InSpaceGroupRangeCache[sgi][p2.Number] = distance
|
||||
}
|
||||
} else {
|
||||
// collect all orbiting ship groups by planet
|
||||
report.OnPlanetGroupCache[sg.Destination] = append(report.OnPlanetGroupCache[sg.Destination], sgi)
|
||||
}
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
||||
func (c *Cache) ReportRace(ri int, rep *mr.Report, battles []*mr.BattleReport, bombings []*mr.Bombing) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
rep.Race = r.Name
|
||||
rep.RaceID = r.ID
|
||||
|
||||
// votes based on population
|
||||
rep.Votes = mr.F(r.Votes.F())
|
||||
|
||||
// relations
|
||||
for i := range r.Relations {
|
||||
rii := slices.IndexFunc(rep.Player, func(v mr.Player) bool { return v.ID == r.Relations[i].RaceID })
|
||||
if rii < 0 {
|
||||
panic(fmt.Sprintf("opponent race for relation not found, id=%v", r.Relations[i].RaceID))
|
||||
}
|
||||
rep.Player[rii].Relation = r.Relations[i].Relation.String()
|
||||
}
|
||||
// self-relation is undefined
|
||||
if i := slices.IndexFunc(rep.Player, func(v mr.Player) bool { return v.ID == r.ID }); i < 0 {
|
||||
panic(fmt.Sprintf("race not found in report, id=%v", r.ID))
|
||||
} else {
|
||||
rep.Player[i].Relation = "-"
|
||||
}
|
||||
|
||||
// sciences
|
||||
c.ReportLocalScience(ri, rep)
|
||||
c.ReportOtherScience(ri, rep)
|
||||
|
||||
// ship classes
|
||||
c.ReportLocalShipClass(ri, rep)
|
||||
c.ReportOtherShipClass(ri, rep)
|
||||
|
||||
// battles
|
||||
c.ReportBattle(ri, rep, battles)
|
||||
|
||||
// bombings
|
||||
c.ReportBombing(ri, rep, bombings)
|
||||
|
||||
// incoming groups
|
||||
c.ReportIncomingGroup(ri, rep)
|
||||
|
||||
// player's planets
|
||||
c.ReportLocalPlanet(ri, rep)
|
||||
|
||||
// ships in production
|
||||
c.ReportShipProduction(ri, rep)
|
||||
|
||||
// cargo routes
|
||||
c.ReportRoute(ri, rep)
|
||||
|
||||
// others' planets
|
||||
c.ReportOtherPlanet(ri, rep)
|
||||
|
||||
// uninhabited planets
|
||||
c.ReportUninhabitedPlanet(ri, rep)
|
||||
|
||||
// unidentified planets
|
||||
c.ReportUnidentifiedPlanet(ri, rep)
|
||||
|
||||
// fleets
|
||||
c.ReportLocalFleet(ri, rep)
|
||||
|
||||
// player's groups
|
||||
c.ReportLocalGroup(ri, rep)
|
||||
|
||||
// others' groups
|
||||
c.ReportOtherGroup(ri, rep)
|
||||
|
||||
// unidentified groups
|
||||
c.ReportUnidentifiedGroup(ri, rep)
|
||||
}
|
||||
|
||||
func (c *Cache) ReportLocalScience(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.LocalScience)
|
||||
|
||||
for i := range r.Sciences {
|
||||
sliceIndexValidate(&rep.LocalScience, i)
|
||||
rep.LocalScience[i].Name = r.Sciences[i].Name
|
||||
rep.LocalScience[i].Drive = mr.F(r.Sciences[i].Drive.F())
|
||||
rep.LocalScience[i].Weapons = mr.F(r.Sciences[i].Weapons.F())
|
||||
rep.LocalScience[i].Shields = mr.F(r.Sciences[i].Shields.F())
|
||||
rep.LocalScience[i].Cargo = mr.F(r.Sciences[i].Cargo.F())
|
||||
}
|
||||
|
||||
slices.SortFunc(rep.LocalScience, func(a, b mr.Science) int { return cmp.Compare(a.Name, b.Name) })
|
||||
}
|
||||
|
||||
func (c *Cache) ReportOtherScience(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.OtherScience)
|
||||
|
||||
i := 0
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.State() != game.StateInOrbit {
|
||||
continue
|
||||
}
|
||||
p := c.MustPlanet(sg.Destination)
|
||||
if !p.Owned() || p.OwnedBy(r.ID) || p.Production.Type != game.ResearchScience {
|
||||
continue
|
||||
}
|
||||
ownerIdx := c.RaceIndex(*p.Owner)
|
||||
owner := &c.g.Race[ownerIdx]
|
||||
sc := c.mustScience(ownerIdx, *p.Production.SubjectID)
|
||||
|
||||
sliceIndexValidate(&rep.OtherScience, i)
|
||||
rep.OtherScience[i].Name = owner.Name
|
||||
rep.OtherScience[i].Drive = mr.F(sc.Drive.F())
|
||||
rep.OtherScience[i].Weapons = mr.F(sc.Weapons.F())
|
||||
rep.OtherScience[i].Shields = mr.F(sc.Shields.F())
|
||||
rep.OtherScience[i].Cargo = mr.F(sc.Cargo.F())
|
||||
i++
|
||||
}
|
||||
|
||||
slices.SortFunc(rep.OtherScience, func(a, b mr.OtherScience) int {
|
||||
return cmp.Or(cmp.Compare(a.Race, b.Race), cmp.Compare(a.Name, b.Name))
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) ReportLocalShipClass(ri int, report *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
|
||||
clear(report.LocalShipClass)
|
||||
|
||||
i := 0
|
||||
for st := range c.ListShipTypes(ri) {
|
||||
sliceIndexValidate(&report.LocalShipClass, i)
|
||||
report.LocalShipClass[i].Name = st.Name
|
||||
report.LocalShipClass[i].Drive = mr.F(st.Drive.F())
|
||||
report.LocalShipClass[i].Armament = st.Armament
|
||||
report.LocalShipClass[i].Weapons = mr.F(st.Weapons.F())
|
||||
report.LocalShipClass[i].Shields = mr.F(st.Shields.F())
|
||||
report.LocalShipClass[i].Cargo = mr.F(st.Cargo.F())
|
||||
report.LocalShipClass[i].Mass = mr.F(st.EmptyMass())
|
||||
i++
|
||||
}
|
||||
|
||||
slices.SortFunc(report.LocalShipClass, func(a, b mr.ShipClass) int { return cmp.Compare(a.Name, b.Name) })
|
||||
}
|
||||
|
||||
func (c *Cache) ReportOtherShipClass(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.OtherShipClass)
|
||||
|
||||
i := 0
|
||||
|
||||
used := make(map[uuid.UUID]map[string]bool)
|
||||
skip := func(ownerID uuid.UUID, className string) bool {
|
||||
if ownerID == r.ID {
|
||||
return true
|
||||
}
|
||||
if _, ok := used[ownerID]; ok {
|
||||
if _, ok := used[ownerID][className]; ok {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
used[ownerID] = make(map[string]bool)
|
||||
}
|
||||
used[ownerID][className] = true
|
||||
return false
|
||||
}
|
||||
|
||||
// add visible ship classes from battles
|
||||
// for bi := range battle {
|
||||
// for si := range battle[bi].Ships {
|
||||
// g := battle[bi].Ships[si]
|
||||
// if skip(g.OwnerID, g.ClassName) {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// sliceIndexValidate(&rep.OtherShipClass, i)
|
||||
// rep.OtherShipClass[i].Race = c.g.Race[c.RaceIndex(g.OwnerID)].Name
|
||||
// rep.OtherShipClass[i].Name = g.ClassName
|
||||
// rep.OtherShipClass[i].Drive = g.DriveTech
|
||||
// rep.OtherShipClass[i].Armament = g.ClassArmament
|
||||
// rep.OtherShipClass[i].Weapons = g.WeaponsTech
|
||||
// rep.OtherShipClass[i].Shields = g.ShieldsTech
|
||||
// rep.OtherShipClass[i].Cargo = g.CargoTech
|
||||
// rep.OtherShipClass[i].Mass = g.ClassMass
|
||||
// i++
|
||||
// }
|
||||
// }
|
||||
|
||||
// add visible ships from owned and observed planets
|
||||
for pn := range rep.OnPlanetGroupCache {
|
||||
p := c.MustPlanet(pn)
|
||||
if p.OwnedBy(r.ID) ||
|
||||
slices.IndexFunc(rep.OnPlanetGroupCache[pn], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
|
||||
for _, sgi := range rep.OnPlanetGroupCache[pn] {
|
||||
sg := c.ShipGroup(sgi)
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
if skip(sg.OwnerID, st.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.OtherShipClass, i)
|
||||
rep.OtherShipClass[i].Race = c.g.Race[c.RaceIndex(sg.OwnerID)].Name
|
||||
rep.OtherShipClass[i].Name = st.Name
|
||||
rep.OtherShipClass[i].Drive = mr.F(st.Drive.F())
|
||||
rep.OtherShipClass[i].Armament = st.Armament
|
||||
rep.OtherShipClass[i].Weapons = mr.F(st.Weapons.F())
|
||||
rep.OtherShipClass[i].Shields = mr.F(st.Shields.F())
|
||||
rep.OtherShipClass[i].Cargo = mr.F(st.Cargo.F())
|
||||
rep.OtherShipClass[i].Mass = mr.F(st.EmptyMass())
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(rep.OtherShipClass, func(a, b mr.OthersShipClass) int {
|
||||
return cmp.Or(cmp.Compare(a.Race, b.Race), cmp.Compare(a.Name, b.Name))
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) ReportBattle(ri int, rep *mr.Report, br []*mr.BattleReport) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.Battle)
|
||||
|
||||
i := 0
|
||||
for bi := range br {
|
||||
visible := false
|
||||
for k := range br[bi].Races {
|
||||
visible = visible || br[bi].Races[k] == r.ID
|
||||
}
|
||||
if !visible {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.Battle, i)
|
||||
rep.Battle[i] = br[bi].ID
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportBombing(ri int, rep *mr.Report, bombing []*mr.Bombing) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.Bombing)
|
||||
|
||||
i := 0
|
||||
for bi := range bombing {
|
||||
pn := bombing[bi].Number
|
||||
visible := bombing[bi].PlanetOwnedID == r.ID // planet may be bombed and wiped
|
||||
for _, sgi := range rep.OnPlanetGroupCache[pn] {
|
||||
sg := c.ShipGroup(sgi)
|
||||
visible = visible || (sg.OwnerID == r.ID && sg.Destination == pn)
|
||||
}
|
||||
if !visible {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.Bombing, i)
|
||||
rep.Bombing[i] = bombing[bi]
|
||||
i++
|
||||
}
|
||||
|
||||
slices.SortFunc(rep.Bombing, func(a, b *mr.Bombing) int {
|
||||
return cmp.Or(cmp.Compare(a.Number, b.Number), boolCompare(a.Wiped, b.Wiped))
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) ReportIncomingGroup(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.IncomingGroup)
|
||||
|
||||
i := 0
|
||||
for sgi := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(sgi)
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
if sg.OwnerID == r.ID || sg.State() != game.StateInSpace {
|
||||
continue
|
||||
}
|
||||
p1 := c.MustPlanet(sg.StateInSpace.Origin)
|
||||
p2 := c.MustPlanet(sg.Destination)
|
||||
if !p2.OwnedBy(r.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
distance := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
||||
var speed, mass float64
|
||||
if sg.FleetID != nil {
|
||||
speed, mass = c.FleetSpeedAndMass(c.MustFleetIndex(*sg.FleetID))
|
||||
} else {
|
||||
speed, mass = sg.Speed(st), sg.FullMass(st)
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.IncomingGroup, i)
|
||||
rep.IncomingGroup[i].Origin = sg.StateInSpace.Origin
|
||||
rep.IncomingGroup[i].Destination = sg.Destination
|
||||
rep.IncomingGroup[i].Distance = mr.F(distance)
|
||||
rep.IncomingGroup[i].Speed = mr.F(speed)
|
||||
rep.IncomingGroup[i].Mass = mr.F(mass)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportLocalPlanet(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.LocalPlanet)
|
||||
|
||||
i := 0
|
||||
for pi := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[pi]
|
||||
if !p.OwnedBy(r.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.LocalPlanet, i)
|
||||
c.localPlanet(&rep.LocalPlanet[i], p)
|
||||
// rep.LocalPlanet[i].UnidentifiedPlanet.Number = p.Number
|
||||
// rep.LocalPlanet[i].UnidentifiedPlanet.X = mr.F(p.X.F())
|
||||
// rep.LocalPlanet[i].UnidentifiedPlanet.Y = mr.F(p.Y.F())
|
||||
// rep.LocalPlanet[i].UninhabitedPlanet.Size = mr.F(p.Size.F())
|
||||
// rep.LocalPlanet[i].UninhabitedPlanet.Name = p.Name
|
||||
// rep.LocalPlanet[i].UninhabitedPlanet.Resources = mr.F(p.Resources.F())
|
||||
// rep.LocalPlanet[i].UninhabitedPlanet.Capital = mr.F(p.Capital.F())
|
||||
// rep.LocalPlanet[i].UninhabitedPlanet.Material = mr.F(p.Material.F())
|
||||
// rep.LocalPlanet[i].Industry = mr.F(p.Industry.F())
|
||||
// rep.LocalPlanet[i].Population = mr.F(p.Population.F())
|
||||
// rep.LocalPlanet[i].Colonists = mr.F(p.Colonists.F())
|
||||
// rep.LocalPlanet[i].Production = c.PlanetProductionDisplayName(p.Number)
|
||||
// rep.LocalPlanet[i].FreeIndustry = mr.F(p.ProductionCapacity())
|
||||
// for _, sgi := range rep.PlanetGroupsCache[p.Number] {
|
||||
// sg := c.ShipGroup(sgi)
|
||||
// if sg.StateUpgrade == nil {
|
||||
// break
|
||||
// }
|
||||
// // between-turn report: ships upgrading on the planet decreases free indistrial potential
|
||||
// rep.LocalPlanet[i].FreeIndustry -= mr.F(sg.StateUpgrade.Cost())
|
||||
// }
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportOtherPlanet(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.OtherPlanet)
|
||||
|
||||
i := 0
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.State() != game.StateInOrbit {
|
||||
continue
|
||||
}
|
||||
p := c.MustPlanet(sg.Destination)
|
||||
if !p.Owned() || p.OwnedBy(r.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.OtherPlanet, i)
|
||||
c.localPlanet(&rep.OtherPlanet[i].LocalPlanet, p)
|
||||
rep.OtherPlanet[i].Owner = c.g.Race[c.RaceIndex(*p.Owner)].Name
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportUninhabitedPlanet(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
|
||||
clear(rep.UninhabitedPlanet)
|
||||
|
||||
i := 0
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.State() != game.StateInOrbit {
|
||||
continue
|
||||
}
|
||||
p := c.MustPlanet(sg.Destination)
|
||||
if p.Owned() {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.UninhabitedPlanet, i)
|
||||
uninhabitedPlanet(&rep.UninhabitedPlanet[i], p)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportUnidentifiedPlanet(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.UnidentifiedPlanet)
|
||||
|
||||
i := 0
|
||||
for pi := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[pi]
|
||||
|
||||
// skip player's owned planets
|
||||
if p.OwnedBy(r.ID) {
|
||||
continue
|
||||
}
|
||||
|
||||
// skip planets where player's group are orbiting
|
||||
if slices.IndexFunc(rep.OnPlanetGroupCache[p.Number], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.UnidentifiedPlanet, i)
|
||||
unidentifiedPlanet(&rep.UnidentifiedPlanet[i], p)
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportShipProduction(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.ShipProduction)
|
||||
|
||||
i := 0
|
||||
for pi := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[pi]
|
||||
if !p.OwnedBy(r.ID) || p.Production.Type != game.ProductionShip {
|
||||
continue
|
||||
}
|
||||
st := c.MustShipType(ri, *p.Production.SubjectID)
|
||||
|
||||
sliceIndexValidate(&rep.ShipProduction, i)
|
||||
rep.ShipProduction[pi].Planet = p.Number
|
||||
rep.ShipProduction[pi].Class = st.Name
|
||||
rep.ShipProduction[pi].Cost = mr.F(ShipProductionCost(st.EmptyMass()))
|
||||
rep.ShipProduction[pi].Free = mr.F(c.PlanetProductionCapacity(p.Number))
|
||||
rep.ShipProduction[pi].ProdUsed = mr.F((*p.Production.ProdUsed).F())
|
||||
rep.ShipProduction[pi].Percent = mr.F((*p.Production.Progress).F())
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportRoute(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.Route)
|
||||
|
||||
i := 0
|
||||
for pi := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[pi]
|
||||
if !p.OwnedBy(r.ID) || len(p.Route) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.Route, i)
|
||||
rep.Route[i].Planet = p.Number
|
||||
// rep.Route[i].Route = make(map[uint]string)
|
||||
for rt, dest := range p.Route {
|
||||
rep.Route[i].Route[dest] = rt.String()
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportLocalFleet(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
|
||||
clear(rep.LocalFleet)
|
||||
|
||||
i := 0
|
||||
for fl := range c.listFleets(ri) {
|
||||
fi := c.MustFleetIndex(fl.ID)
|
||||
gid := slices.Collect(c.fleetGroupIds(ri, fi))
|
||||
if len(gid) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
speed, _ := c.FleetSpeedAndMass(fi)
|
||||
fleetState := c.FleetState(fl.ID)
|
||||
|
||||
sliceIndexValidate(&rep.LocalFleet, i)
|
||||
rep.LocalFleet[i].Name = fl.Name
|
||||
rep.LocalFleet[i].Groups = uint(len(gid))
|
||||
rep.LocalFleet[i].Speed = mr.F(speed)
|
||||
rep.LocalFleet[i].State = fleetState.State.String()
|
||||
rep.LocalFleet[i].Destination = fleetState.Destination
|
||||
if inSpace, ok := fleetState.InSpace(); ok {
|
||||
rep.LocalFleet[i].Origin = &inSpace.Origin
|
||||
p2 := c.MustPlanet(rep.LocalFleet[i].Destination)
|
||||
rangeToDestination := mr.F(util.ShortDistance(c.g.Map.Width, c.g.Map.Height, inSpace.X.F(), inSpace.Y.F(), p2.X.F(), p2.Y.F()))
|
||||
rep.LocalFleet[i].Range = &rangeToDestination
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportLocalGroup(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
|
||||
clear(rep.LocalGroup)
|
||||
|
||||
i := 0
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
sliceIndexValidate(&rep.LocalGroup, i)
|
||||
st := c.MustShipType(ri, sg.TypeID)
|
||||
c.otherGroup(&rep.LocalGroup[i].OtherGroup, sg, st)
|
||||
rep.LocalGroup[i].ID = sg.ID
|
||||
rep.LocalGroup[i].State = sg.State().String()
|
||||
if sg.FleetID != nil {
|
||||
rep.LocalGroup[i].Fleet = &c.g.Fleets[c.MustFleetIndex(*sg.FleetID)].Name
|
||||
}
|
||||
// rep.LocalGroup[i].Number = sg.Number
|
||||
// rep.LocalGroup[i].Class = st.Name
|
||||
// // rep.LocalGroup[i].Tech = make(map[string]mr.Float)
|
||||
// for t, v := range sg.Tech {
|
||||
// rep.LocalGroup[i].Tech[t.String()] = mr.F(v)
|
||||
// }
|
||||
// rep.LocalGroup[i].Cargo = sg.CargoString()
|
||||
// rep.LocalGroup[i].Load = mr.F(sg.Load.F())
|
||||
// rep.LocalGroup[i].Destination = sg.Destination
|
||||
// if sg.State() == game.StateInSpace {
|
||||
// rep.LocalGroup[i].Origin = &sg.StateInSpace.Origin
|
||||
// p2 := c.MustPlanet(rep.LocalGroup[i].Destination)
|
||||
// rangeToDestination := mr.F(util.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()))
|
||||
// rep.LocalGroup[i].Range = &rangeToDestination
|
||||
// }
|
||||
// rep.LocalGroup[i].Speed = mr.F(sg.Speed(st))
|
||||
// rep.LocalGroup[i].Mass = mr.F(st.EmptyMass())
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportOtherGroup(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
|
||||
clear(rep.OtherGroup)
|
||||
|
||||
used := make(map[int]bool)
|
||||
skip := func(sgi int) bool {
|
||||
if c.ShipGroup(sgi).OwnerID == r.ID {
|
||||
return true
|
||||
}
|
||||
if _, ok := used[sgi]; ok {
|
||||
return true
|
||||
}
|
||||
used[sgi] = true
|
||||
return false
|
||||
}
|
||||
|
||||
i := 0
|
||||
|
||||
// visible groups from owned and observed planets
|
||||
for pn := range rep.OnPlanetGroupCache {
|
||||
p := c.MustPlanet(pn)
|
||||
if p.OwnedBy(r.ID) ||
|
||||
slices.IndexFunc(rep.OnPlanetGroupCache[pn], func(sgi int) bool { return c.ShipGroup(sgi).OwnerID == r.ID }) >= 0 {
|
||||
for _, sgi := range rep.OnPlanetGroupCache[pn] {
|
||||
sg := c.ShipGroup(sgi)
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
if skip(sgi) {
|
||||
continue
|
||||
}
|
||||
|
||||
sliceIndexValidate(&rep.OtherGroup, i)
|
||||
c.otherGroup(&rep.OtherGroup[i], sg, st)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) ReportUnidentifiedGroup(ri int, rep *mr.Report) {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
flightDistance := r.FlightDistance()
|
||||
|
||||
clear(rep.UnidentifiedGroup)
|
||||
|
||||
i := 0
|
||||
for sgi := range rep.InSpaceGroupRangeCache {
|
||||
sg := c.ShipGroup(sgi)
|
||||
if sg.OwnerID == rep.RaceID {
|
||||
continue
|
||||
}
|
||||
if sg.StateInSpace == nil {
|
||||
panic(fmt.Sprintf("pre-calculated distance group not in space: i=%d", sgi))
|
||||
}
|
||||
for pi := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[pi]
|
||||
if !p.OwnedBy(r.ID) {
|
||||
continue
|
||||
}
|
||||
if v, ok := rep.InSpaceGroupRangeCache[sgi][p.Number]; !ok {
|
||||
panic(fmt.Sprintf("distance cache not pre-calculated: i=%d p=#%d", sgi, p.Number))
|
||||
} else if v <= flightDistance {
|
||||
sliceIndexValidate(&rep.UnidentifiedGroup, i)
|
||||
rep.UnidentifiedGroup[i].X = mr.F(sg.StateInSpace.X.F())
|
||||
rep.UnidentifiedGroup[i].Y = mr.F(sg.StateInSpace.Y.F())
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) otherGroup(v *mr.OtherGroup, sg *game.ShipGroup, st *game.ShipType) {
|
||||
v.Number = sg.Number
|
||||
v.Class = st.Name
|
||||
// rep.LocalGroup[i].Tech = make(map[string]mr.Float)
|
||||
for t, val := range sg.Tech {
|
||||
v.Tech[t.String()] = mr.F(val.F())
|
||||
}
|
||||
v.Cargo = sg.CargoString()
|
||||
v.Load = mr.F(sg.Load.F())
|
||||
v.Destination = sg.Destination
|
||||
if sg.State() == game.StateInSpace {
|
||||
v.Origin = &sg.StateInSpace.Origin
|
||||
p2 := c.MustPlanet(v.Destination)
|
||||
rangeToDestination := mr.F(util.ShortDistance(c.g.Map.Width, c.g.Map.Height, sg.StateInSpace.X.F(), sg.StateInSpace.Y.F(), p2.X.F(), p2.Y.F()))
|
||||
v.Range = &rangeToDestination
|
||||
}
|
||||
v.Speed = mr.F(sg.Speed(st))
|
||||
v.Mass = mr.F(st.EmptyMass())
|
||||
}
|
||||
|
||||
func (c *Cache) localPlanet(v *mr.LocalPlanet, p *game.Planet) {
|
||||
uninhabitedPlanet(&v.UninhabitedPlanet, p)
|
||||
v.Industry = mr.F(p.Industry.F())
|
||||
v.Population = mr.F(p.Population.F())
|
||||
v.Colonists = mr.F(p.Colonists.F())
|
||||
v.Production = c.PlanetProductionDisplayName(p.Number)
|
||||
// between-turn report: ships upgrading on the planet decreases free indistrial potential
|
||||
v.FreeIndustry = mr.F(c.PlanetProductionCapacity(p.Number))
|
||||
}
|
||||
|
||||
func uninhabitedPlanet(v *mr.UninhabitedPlanet, p *game.Planet) {
|
||||
unidentifiedPlanet(&v.UnidentifiedPlanet, p)
|
||||
v.Size = mr.F(p.Size.F())
|
||||
v.Name = p.Name
|
||||
v.Resources = mr.F(p.Resources.F())
|
||||
v.Capital = mr.F(p.Capital.F())
|
||||
v.Material = mr.F(p.Material.F())
|
||||
}
|
||||
|
||||
func unidentifiedPlanet(v *mr.UnidentifiedPlanet, p *game.Planet) {
|
||||
v.Number = p.Number
|
||||
v.X = mr.F(p.X.F())
|
||||
v.Y = mr.F(p.Y.F())
|
||||
}
|
||||
|
||||
func sliceIndexValidate[S ~[]E, E any](s *S, i int) {
|
||||
if cap(*s) < i+1 {
|
||||
*s = slices.Grow(*s, 10)
|
||||
}
|
||||
if len(*s) < i+1 {
|
||||
*s = (*s)[:i+1]
|
||||
}
|
||||
}
|
||||
|
||||
func boolCompare(a, b bool) int {
|
||||
if a == b {
|
||||
return 0
|
||||
}
|
||||
if a == false {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"galaxy/model/report"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReportRace(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
|
||||
c.TurnCalculateVotes()
|
||||
|
||||
rep := c.InitReport(2)
|
||||
assert.Equal(t, 2, int(rep.Turn))
|
||||
|
||||
c.ReportRace(Race_0_idx, rep, nil, nil)
|
||||
|
||||
assert.Equal(t, Race_0.Name, rep.Race)
|
||||
assert.Equal(t, Race_0.ID, rep.RaceID)
|
||||
assert.Equal(t, 0.1, float64(rep.Votes))
|
||||
|
||||
for i := range rep.Player {
|
||||
p := &rep.Player[i]
|
||||
switch p.ID {
|
||||
case Race_0_ID:
|
||||
assert.Equal(t, Race_0.Name, p.Name)
|
||||
assert.Equal(t, 1.1, float64(p.Drive))
|
||||
assert.Equal(t, 1.2, float64(p.Weapons))
|
||||
assert.Equal(t, 1.3, float64(p.Shields))
|
||||
assert.Equal(t, 1.4, float64(p.Cargo))
|
||||
assert.Equal(t, 100., float64(p.Population))
|
||||
assert.Equal(t, 100., float64(p.Industry))
|
||||
assert.Equal(t, 2, int(p.Planets))
|
||||
assert.Equal(t, 0.1, float64(p.Votes))
|
||||
assert.Equal(t, "-", p.Relation)
|
||||
case Race_1_ID:
|
||||
assert.Equal(t, Race_1.Name, p.Name)
|
||||
assert.Equal(t, 2.1, float64(p.Drive))
|
||||
assert.Equal(t, 2.2, float64(p.Weapons))
|
||||
assert.Equal(t, 2.3, float64(p.Shields))
|
||||
assert.Equal(t, 2.4, float64(p.Cargo))
|
||||
assert.Equal(t, 0., float64(p.Population))
|
||||
assert.Equal(t, 0., float64(p.Industry))
|
||||
assert.Equal(t, 1, int(p.Planets))
|
||||
assert.Equal(t, 0., float64(p.Votes))
|
||||
assert.Equal(t, "WAR", p.Relation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportLocalShipClass(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
|
||||
r := &report.Report{}
|
||||
assert.Len(t, r.LocalShipClass, 0)
|
||||
|
||||
c.ReportLocalShipClass(Race_0_idx, r)
|
||||
|
||||
assert.Len(t, r.LocalShipClass, 3)
|
||||
for i := range r.LocalShipClass {
|
||||
assert.NotEmpty(t, r.LocalShipClass[i].Name)
|
||||
switch n := r.LocalShipClass[i].Name; n {
|
||||
case Cruiser.Name:
|
||||
assert.Equal(t, report.F(Cruiser.Drive.F()), r.LocalShipClass[i].Drive)
|
||||
assert.Equal(t, Cruiser.Armament, r.LocalShipClass[i].Armament)
|
||||
assert.Equal(t, report.F(Cruiser.Weapons.F()), r.LocalShipClass[i].Weapons)
|
||||
assert.Equal(t, report.F(Cruiser.Shields.F()), r.LocalShipClass[i].Shields)
|
||||
assert.Equal(t, report.F(Cruiser.Cargo.F()), r.LocalShipClass[i].Cargo)
|
||||
case Race_0_Gunship:
|
||||
assert.Equal(t, report.F(60.), r.LocalShipClass[i].Drive)
|
||||
assert.Equal(t, uint(3), r.LocalShipClass[i].Armament)
|
||||
assert.Equal(t, report.F(30.), r.LocalShipClass[i].Weapons)
|
||||
assert.Equal(t, report.F(100.), r.LocalShipClass[i].Shields)
|
||||
assert.Equal(t, report.F(0.), r.LocalShipClass[i].Cargo)
|
||||
case Race_0_Freighter:
|
||||
assert.Equal(t, report.F(8.), r.LocalShipClass[i].Drive)
|
||||
assert.Equal(t, uint(0), r.LocalShipClass[i].Armament)
|
||||
assert.Equal(t, report.F(0.), r.LocalShipClass[i].Weapons)
|
||||
assert.Equal(t, report.F(2.), r.LocalShipClass[i].Shields)
|
||||
assert.Equal(t, report.F(10.), r.LocalShipClass[i].Cargo)
|
||||
default:
|
||||
assert.Failf(t, "unexpected ship class", "name=%s", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"iter"
|
||||
"maps"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
)
|
||||
|
||||
func (c *Cache) PlanetRouteSet(ri int, rt game.RouteType, origin, destination uint) error {
|
||||
c.validateRaceIndex(ri)
|
||||
p1, ok := c.Planet(origin)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("origin planet #%d", origin)
|
||||
}
|
||||
if !p1.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d", origin)
|
||||
}
|
||||
p2, ok := c.Planet(destination)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("destination planet #%d", destination)
|
||||
}
|
||||
rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
||||
if rangeToDestination > c.g.Race[ri].FlightDistance() {
|
||||
return e.NewSendUnreachableDestinationError("range=%.03f max=%.03f", rangeToDestination, c.g.Race[ri].FlightDistance())
|
||||
}
|
||||
|
||||
c.SetPlanetRoute(rt, origin, destination)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) PlanetRouteRemove(ri int, rt game.RouteType, origin uint) error {
|
||||
c.validateRaceIndex(ri)
|
||||
p1, ok := c.Planet(origin)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("origin planet #%d", origin)
|
||||
}
|
||||
if !p1.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d", origin)
|
||||
}
|
||||
|
||||
c.RemovePlanetRoute(rt, origin)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) SetPlanetRoute(rt game.RouteType, origin, destination uint) {
|
||||
pi := c.MustPlanetIndex(origin)
|
||||
if c.g.Map.Planet[pi].Route == nil {
|
||||
c.g.Map.Planet[pi].Route = make(map[game.RouteType]uint)
|
||||
}
|
||||
c.g.Map.Planet[pi].Route[rt] = destination
|
||||
}
|
||||
|
||||
func (c *Cache) RemovePlanetRoute(rt game.RouteType, origin uint) {
|
||||
pi := c.MustPlanetIndex(origin)
|
||||
if c.g.Map.Planet[pi].Route != nil {
|
||||
delete(c.g.Map.Planet[pi].Route, rt)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) SendRoutedGroups() {
|
||||
for pi := range c.g.Map.Planet {
|
||||
if len(c.g.Map.Planet[pi].Route) == 0 {
|
||||
continue
|
||||
}
|
||||
groups := slices.Collect(c.listRoutedSendGroupIds(c.g.Map.Planet[pi].Number))
|
||||
if len(groups) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sortGroups := func(g []int) {
|
||||
// sort groups by largest CargoCapacity
|
||||
slices.SortFunc(g, func(l, r int) int {
|
||||
return cmp.Or(
|
||||
cmp.Compare(c.ShipGroup(r).CargoCapacity(c.ShipGroupShipClass(r)), c.ShipGroup(l).CargoCapacity(c.ShipGroupShipClass(l))),
|
||||
cmp.Compare(l, r))
|
||||
})
|
||||
|
||||
}
|
||||
reorderGroups := func(g []int) []int {
|
||||
g = slices.DeleteFunc(g, func(i int) bool { return c.ShipGroup(i).State() != game.StateInOrbit })
|
||||
sortGroups(g)
|
||||
return g
|
||||
}
|
||||
|
||||
sortGroups(groups)
|
||||
|
||||
p := c.MustPlanet(c.g.Map.Planet[pi].Number)
|
||||
|
||||
// COL -> CAP -> MAT -> EMPTY
|
||||
for _, rt := range []game.RouteType{game.RouteColonist, game.RouteCapital, game.RouteMaterial, game.RouteEmpty} {
|
||||
dest, ok := c.g.Map.Planet[pi].Route[rt]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var res *game.Float
|
||||
var ct game.CargoType
|
||||
switch rt {
|
||||
case game.RouteColonist:
|
||||
res = &p.Colonists
|
||||
ct = game.CargoColonist
|
||||
case game.RouteCapital:
|
||||
res = &p.Capital
|
||||
ct = game.CargoCapital
|
||||
case game.RouteMaterial:
|
||||
res = &p.Material
|
||||
ct = game.CargoMaterial
|
||||
case game.RouteEmpty:
|
||||
// empty routes launched immediately so the're not required to be loaded
|
||||
for _, sgi := range groups {
|
||||
c.LaunchShips(sgi, dest)
|
||||
}
|
||||
groups = reorderGroups(groups)
|
||||
continue
|
||||
default:
|
||||
continue
|
||||
}
|
||||
for res != nil && *res > 0 && len(groups) > 0 {
|
||||
sgi := groups[0]
|
||||
sg := c.ShipGroup(sgi)
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
ships := sg.Number
|
||||
sgCapacity := sg.CargoCapacity(st)
|
||||
toLoad := (*res).F()
|
||||
if toLoad > sgCapacity {
|
||||
toLoad = sgCapacity
|
||||
} else if maxShips := uint(math.Ceil(toLoad / (sgCapacity / float64(ships)))); maxShips < ships {
|
||||
newGroupIdx := c.unsafeBreakGroup(c.RaceIndex(sg.OwnerID), sgi, maxShips)
|
||||
sgi = newGroupIdx
|
||||
sg = c.ShipGroup(newGroupIdx)
|
||||
}
|
||||
// decrease planet resource
|
||||
*res = (*res).Add(-toLoad)
|
||||
// load group
|
||||
sg.Load = sg.Load.Add(toLoad)
|
||||
sg.CargoType = &ct
|
||||
c.LaunchShips(sgi, dest)
|
||||
groups = reorderGroups(groups)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) listRoutedSendGroupIds(pn uint) iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
p := c.MustPlanet(pn)
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(i)
|
||||
st := c.ShipGroupShipClass(i)
|
||||
if !p.OwnedBy(sg.OwnerID) || // Planet must be owned by ships owner
|
||||
sg.FleetID != nil || // Ships must not be part of a Fleet
|
||||
sg.State() != game.StateInOrbit || // Ships must be only In_Orbit state
|
||||
st.CargoBlockMass() == 0 || // Ship Class must have Cargo bays
|
||||
sg.Load != 0 || // Ships must not be loaded for enrouting
|
||||
sg.Destination != p.Number {
|
||||
continue
|
||||
}
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Невозможно лишь выгрузить колонистов на чужой планете.
|
||||
func (c *Cache) TurnUnloadEnroutedGroups() {
|
||||
for i := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[i]
|
||||
c.doUnload(c.unloadRoutedColonists(p.Number, c.listRoutedUnloadShipGroupIds(p.Number, game.RouteColonist)))
|
||||
for _, rt := range []game.RouteType{game.RouteMaterial, game.RouteCapital} {
|
||||
c.doUnload(c.listRoutedUnloadShipGroupIds(p.Number, rt))
|
||||
}
|
||||
p.UnpackColonists()
|
||||
p.UnpackCapital()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) RemoveUnreachableRoutes() {
|
||||
for i := range c.g.Map.Planet {
|
||||
p1 := &c.g.Map.Planet[i]
|
||||
if !p1.Owned() {
|
||||
continue
|
||||
}
|
||||
ri := c.RaceIndex(*p1.Owner)
|
||||
for rt, destination := range p1.Route {
|
||||
p2 := c.MustPlanet(destination)
|
||||
rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
||||
if rangeToDestination > c.g.Race[ri].FlightDistance() {
|
||||
delete(p1.Route, rt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) doUnload(groups iter.Seq[int]) {
|
||||
for sgi := range groups {
|
||||
c.unsafeUnloadCargo(sgi, c.ShipGroup(sgi).Load.F())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) unloadRoutedColonists(pn uint, groups iter.Seq[int]) iter.Seq[int] {
|
||||
p := c.MustPlanet(pn)
|
||||
gr := slices.Collect(groups)
|
||||
if !p.Owned() {
|
||||
return c.selectColUnloadGroup(gr)
|
||||
}
|
||||
return func(yield func(int) bool) {
|
||||
for _, sgi := range gr {
|
||||
sg := c.ShipGroup(sgi)
|
||||
if !p.OwnedBy(sg.OwnerID) {
|
||||
continue
|
||||
}
|
||||
if !yield(sgi) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *Cache) selectColUnloadGroup(groups []int) (result iter.Seq[int]) {
|
||||
groupByRace := make(map[int][]int)
|
||||
loadByRace := make(map[int]float64)
|
||||
for _, i := range groups {
|
||||
sg := c.ShipGroup(i)
|
||||
ri := c.RaceIndex(sg.OwnerID)
|
||||
groupByRace[ri] = append(groupByRace[ri], i)
|
||||
loadByRace[ri] += sg.Load.F()
|
||||
}
|
||||
if len(loadByRace) < 2 {
|
||||
// only one race has to unload cargo
|
||||
result = slices.Values(groups)
|
||||
return
|
||||
}
|
||||
|
||||
// select winner to unload cargo
|
||||
id := MaxOrRandomLoadId(loadByRace, func(ri int) float64 { return float64(c.g.Race[ri].Votes) })
|
||||
result = slices.Values(groupByRace[id])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache) listRoutedUnloadShipGroupIds(pn uint, routeType game.RouteType) iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
yielded := make(map[int]bool)
|
||||
for i := range c.g.Map.Planet {
|
||||
for rt, dest := range c.g.Map.Planet[i].Route {
|
||||
if dest != pn || rt != routeType {
|
||||
continue
|
||||
}
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(i)
|
||||
if _, ok := yielded[i]; ok || sg.FleetID != nil || sg.CargoType == nil || sg.Load == 0. || sg.State() != game.StateInOrbit || sg.Destination != dest {
|
||||
continue
|
||||
}
|
||||
if v, ok := game.RouteToCargo[rt]; !ok || v != *sg.CargoType {
|
||||
continue
|
||||
}
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
yielded[i] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MaxOrRandomLoadId(raceLoad map[int]float64, pop func(int) float64) int {
|
||||
if len(raceLoad) < 2 {
|
||||
panic("loadByRace must contain at least 2 keys")
|
||||
}
|
||||
raceIndex := slices.Collect(maps.Keys(raceLoad))
|
||||
slices.SortFunc(raceIndex, func(ria, rib int) int {
|
||||
return cmp.Or(
|
||||
// maximum quantity of unloading colonists
|
||||
cmp.Compare(raceLoad[rib], raceLoad[ria]),
|
||||
|
||||
// maximum population of the race
|
||||
cmp.Compare(pop(rib), pop(ria)),
|
||||
|
||||
// Random winner
|
||||
cmp.Compare(rand.Float64(), rand.Float64()),
|
||||
|
||||
// in theoty, unreacheable option, but let's randomize again
|
||||
cmp.Compare(rand.Float64(), rand.Float64()),
|
||||
)
|
||||
})
|
||||
return raceIndex[0]
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPlanetRouteSet(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteMaterial)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteEmpty)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", 0, 2))
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteMaterial)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteEmpty)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "MAT", 0, 2))
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteMaterial)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteEmpty)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", 0, 2))
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteMaterial)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteEmpty)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "EMP", 0, 2))
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteMaterial)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteEmpty)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteSet(UnknownRace, "COL", 0, 2),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteSet(Race_Extinct.Name, "COL", 0, 2),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteSet(Race_0.Name, "IND", 0, 2),
|
||||
e.GenericErrorText(e.ErrInputCargoTypeInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteSet(Race_0.Name, "COL", 500, 2),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteSet(Race_0.Name, "COL", 1, 2),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteSet(Race_0.Name, "COL", 0, 3),
|
||||
e.GenericErrorText(e.ErrSendUnreachableDestination))
|
||||
}
|
||||
|
||||
func TestPlanetRouteRemove(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", 0, 2))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", 0, 2))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "EMP", 2, 0))
|
||||
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
assert.Contains(t, c.MustPlanet(2).Route, game.RouteEmpty)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteRemove(Race_0.Name, "COL", 0))
|
||||
assert.NotContains(t, c.MustPlanet(0).Route, game.RouteColonist)
|
||||
assert.Contains(t, c.MustPlanet(0).Route, game.RouteCapital)
|
||||
|
||||
assert.NoError(t, g.PlanetRouteRemove(Race_0.Name, "EMP", 2))
|
||||
assert.NotContains(t, c.MustPlanet(2).Route, game.RouteEmpty)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteRemove(UnknownRace, "COL", 0),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteRemove(Race_Extinct.Name, "COL", 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteRemove(Race_0.Name, "IND", 0),
|
||||
e.GenericErrorText(e.ErrInputCargoTypeInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteRemove(Race_0.Name, "COL", 500),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.PlanetRouteRemove(Race_0.Name, "COL", 1),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
}
|
||||
|
||||
func TestListRoutedSendGroupIds(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
// 1: idx = 0 / Ready to load
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
|
||||
// 2: idx = 1 / Has no cargo bay
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
|
||||
// 3: idx = 2 / In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(2).StateInSpace = &InSpace
|
||||
|
||||
// 4: idx = 3 / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(3).Load = 1.234
|
||||
|
||||
// Foreign group -> idx 1
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(4).ID))
|
||||
|
||||
// 5: idx = 4 / Part of the Fleet
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "Fleet", c.ShipGroup(5).ID))
|
||||
|
||||
planet_0_groups := slices.Collect(c.ListRoutedSendGroupIds(0))
|
||||
assert.Len(t, planet_0_groups, 1)
|
||||
for _, i := range planet_0_groups {
|
||||
sg := c.ShipGroup(i)
|
||||
st := c.ShipGroupShipClass(i)
|
||||
assert.Equal(t, Race_0_ID, sg.OwnerID)
|
||||
assert.Greater(t, sg.CargoCapacity(st), 0.)
|
||||
assert.Equal(t, game.StateInOrbit, sg.State())
|
||||
assert.Equal(t, 0., sg.Load.F())
|
||||
assert.Nil(t, sg.FleetID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrouteGroups_SplitGroup(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Colonists = 65
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) // 21.0 per Ship
|
||||
assert.Equal(t, 105., c.ShipGroup(0).CargoCapacity(c.ShipGroupShipClass(0)))
|
||||
|
||||
c.SendRoutedGroups()
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
|
||||
assert.Equal(t, uint(1), c.ShipGroup(0).Number)
|
||||
assert.Equal(t, 0., c.ShipGroup(0).Load.F())
|
||||
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(1).State())
|
||||
assert.Equal(t, uint(4), c.ShipGroup(1).Number)
|
||||
assert.Equal(t, 65., c.ShipGroup(1).Load.F())
|
||||
assert.Equal(t, 0., c.MustPlanet(R0_Planet_0_num).Colonists.F())
|
||||
}
|
||||
|
||||
func TestEnrouteGroups_GroupSorting(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Colonists = 100
|
||||
|
||||
// 0: idx = 1
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 4)) // 21.0 per Ship
|
||||
assert.Equal(t, 84., c.ShipGroup(0).CargoCapacity(c.ShipGroupShipClass(0)))
|
||||
|
||||
// 1: idx = 2
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) // 21.0 per Ship
|
||||
assert.Equal(t, 105., c.ShipGroup(1).CargoCapacity(c.ShipGroupShipClass(1)))
|
||||
|
||||
c.SendRoutedGroups()
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(1).State())
|
||||
|
||||
assert.Equal(t, 100., c.ShipGroup(1).Load.F())
|
||||
assert.Equal(t, 0., c.MustPlanet(R0_Planet_0_num).Colonists.F())
|
||||
}
|
||||
|
||||
func TestEnrouteGroups_LaunchOrder(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", R0_Planet_0_num, R0_Planet_2_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "MAT", R0_Planet_0_num, R0_Planet_2_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "EMP", R0_Planet_0_num, R1_Planet_1_num))
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Colonists = 150
|
||||
c.MustPlanet(R0_Planet_0_num).Capital = 100
|
||||
c.MustPlanet(R0_Planet_0_num).Material = 20
|
||||
|
||||
// 0: idx = 1 (105 COL) ->
|
||||
// 3: idx = 4 ( 45 COL)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
|
||||
assert.Equal(t, 105., c.ShipGroup(0).CargoCapacity(c.ShipGroupShipClass(0)))
|
||||
|
||||
// 1: idx = 2 (In_Orbit) ->
|
||||
// 4: idx = 5 (20 MAT)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
|
||||
assert.Equal(t, 105., c.ShipGroup(1).CargoCapacity(c.ShipGroupShipClass(1)))
|
||||
|
||||
// 2: idx = 3 (100 CAP)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
|
||||
assert.Equal(t, 105., c.ShipGroup(2).CargoCapacity(c.ShipGroupShipClass(2)))
|
||||
|
||||
c.SendRoutedGroups()
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 5)
|
||||
|
||||
// full load of COL
|
||||
sgi := 0
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
|
||||
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
|
||||
assert.Equal(t, 105., c.ShipGroup(sgi).Load.F())
|
||||
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, game.CargoColonist, *c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, uint(5), c.ShipGroup(sgi).Number)
|
||||
|
||||
// rest of COL
|
||||
sgi = 3
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
|
||||
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
|
||||
assert.Equal(t, 45., c.ShipGroup(sgi).Load.F())
|
||||
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, game.CargoColonist, *c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, uint(3), c.ShipGroup(sgi).Number)
|
||||
|
||||
// full load of CAP
|
||||
sgi = 2
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
|
||||
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
|
||||
assert.Equal(t, 100., c.ShipGroup(sgi).Load.F())
|
||||
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, game.CargoCapital, *c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, uint(5), c.ShipGroup(sgi).Number)
|
||||
|
||||
// partial load of MAT
|
||||
sgi = 4
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
|
||||
assert.Equal(t, R0_Planet_2_num, c.ShipGroup(sgi).Destination)
|
||||
assert.Equal(t, 20., c.ShipGroup(sgi).Load.F())
|
||||
assert.NotNil(t, c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, game.CargoMaterial, *c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, uint(1), c.ShipGroup(sgi).Number)
|
||||
|
||||
// empty / on_planet
|
||||
sgi = 1
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(sgi).State())
|
||||
assert.Equal(t, R1_Planet_1_num, c.ShipGroup(sgi).Destination)
|
||||
assert.Equal(t, 0., c.ShipGroup(sgi).Load.F())
|
||||
assert.Nil(t, c.ShipGroup(sgi).CargoType)
|
||||
assert.Equal(t, uint(1), c.ShipGroup(sgi).Number)
|
||||
}
|
||||
|
||||
func TestListRoutedUnloadShipGroupIds(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
// 1: idx = 0 / Empty cargo
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 0.
|
||||
|
||||
// 2: idx = 1 / Has no cargo bay
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
|
||||
// 3: idx = 2 / In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(2).StateInSpace = &InSpace
|
||||
|
||||
// 4: idx = 3 / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(3).Load = 1.234
|
||||
|
||||
// 5: idx = 4 / Part of the Fleet
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "Fleet", c.ShipGroup(4).ID))
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_0_num, R0_Planet_2_num))
|
||||
for _, rt := range game.RouteTypeSet {
|
||||
groups := slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_2_num, rt))
|
||||
assert.Len(t, groups, 0, "route: %v", rt)
|
||||
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, rt))
|
||||
assert.Len(t, groups, 0, "route: %v", rt)
|
||||
}
|
||||
|
||||
// double route from different planets - must not double group ids
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_2_num, R0_Planet_0_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
|
||||
|
||||
// 6: idx = 5 / loaded with CAP
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(5).CargoType = game.CargoCapital.Ref()
|
||||
c.ShipGroup(5).Load = 1.234
|
||||
|
||||
groups := slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteColonist))
|
||||
assert.Len(t, groups, 1)
|
||||
for _, sgi := range groups {
|
||||
assert.Greater(t, c.ShipGroup(sgi).Load, 0.)
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(sgi).State())
|
||||
assert.Equal(t, R0_Planet_0_num, c.ShipGroup(sgi).Destination)
|
||||
}
|
||||
|
||||
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteMaterial))
|
||||
assert.Len(t, groups, 0)
|
||||
|
||||
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteCapital))
|
||||
assert.Len(t, groups, 0)
|
||||
}
|
||||
|
||||
func TestMaxOrRandomLoadId(t *testing.T) {
|
||||
IDtoLoad := make(map[int]float64)
|
||||
|
||||
pop := func(ri int) float64 {
|
||||
switch ri {
|
||||
case 1:
|
||||
return 0
|
||||
case 3:
|
||||
return 0
|
||||
case 5:
|
||||
return 9.99
|
||||
case 7:
|
||||
return 10
|
||||
case 11:
|
||||
return 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad, pop) })
|
||||
IDtoLoad[1] = 100.
|
||||
assert.Panics(t, func() { controller.MaxOrRandomLoadId(IDtoLoad, pop) })
|
||||
|
||||
IDtoLoad[5] = 100.001
|
||||
assert.Equal(t, 5, controller.MaxOrRandomLoadId(IDtoLoad, pop))
|
||||
|
||||
IDtoLoad[3] = 100.
|
||||
assert.NotContains(t, []int{1, 3}, controller.MaxOrRandomLoadId(IDtoLoad, pop))
|
||||
|
||||
IDtoLoad[7] = 100.001
|
||||
assert.Equal(t, 7, controller.MaxOrRandomLoadId(IDtoLoad, pop))
|
||||
|
||||
IDtoLoad[11] = 100.001
|
||||
rndCount := make(map[int]int)
|
||||
for range 100 {
|
||||
id := controller.MaxOrRandomLoadId(IDtoLoad, pop)
|
||||
assert.NotContains(t, []int{1, 3, 5}, id)
|
||||
assert.Contains(t, []int{7, 11}, id)
|
||||
rndCount[id]++
|
||||
}
|
||||
assert.Greater(t, rndCount[7], 10)
|
||||
assert.Greater(t, rndCount[11], 10)
|
||||
}
|
||||
|
||||
func TestSelectColUnloadGroup(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_2_num, R0_Planet_0_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
|
||||
|
||||
// 1: idx = 0 / Loaded COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 7.
|
||||
|
||||
// 2: idx = 1 / Loaded COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5))
|
||||
c.ShipGroup(1).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(1).Load = 5.
|
||||
|
||||
groups := slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteColonist))
|
||||
assert.Len(t, groups, 2)
|
||||
unloadGroups := slices.Collect(c.SelectColUnloadGroup(groups))
|
||||
assert.ElementsMatch(t, groups, unloadGroups)
|
||||
|
||||
// 3: idx = 2 / Loaded COL - another race, winner
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 10))
|
||||
c.ShipGroup(2).Destination = R0_Planet_0_num
|
||||
c.ShipGroup(2).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(2).Load = 12.1
|
||||
|
||||
groups = slices.Collect(c.ListRoutedUnloadShipGroupIds(R0_Planet_0_num, game.RouteColonist))
|
||||
assert.Len(t, groups, 3)
|
||||
unloadGroups = slices.Collect(c.SelectColUnloadGroup(groups))
|
||||
assert.Equal(t, 2, unloadGroups[0])
|
||||
}
|
||||
|
||||
func TestTurnUnloadEnroutedGroups(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "MAT", R0_Planet_2_num, R0_Planet_0_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", R0_Planet_2_num, R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "COL", R1_Planet_1_num, Uninhabited_Planet_4_num))
|
||||
|
||||
// 1: idx = 0 / Loaded MAT
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
c.ShipGroup(0).CargoType = game.CargoMaterial.Ref()
|
||||
c.ShipGroup(0).Load = 222.
|
||||
|
||||
// 2: idx = 1 / Loaded CAP
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
c.ShipGroup(1).CargoType = game.CargoCapital.Ref()
|
||||
c.ShipGroup(1).Load = 11.
|
||||
|
||||
// 3: idx = 2 / Loaded COL - on empty planet
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 10))
|
||||
c.ShipGroup(2).Destination = Uninhabited_Planet_4_num
|
||||
c.ShipGroup(2).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(2).Load = 12.1
|
||||
|
||||
// 4: idx = 3 / Loaded COL - on inhabited planet
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 10))
|
||||
c.ShipGroup(3).Destination = R0_Planet_0_num
|
||||
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(3).Load = 17.3
|
||||
|
||||
c.TurnUnloadEnroutedGroups()
|
||||
|
||||
assert.Equal(t, 0., c.ShipGroup(0).Load.F())
|
||||
assert.Equal(t, 222., c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.Equal(t, 0., c.ShipGroup(1).Load.F())
|
||||
assert.Equal(t, 11., c.MustPlanet(R0_Planet_0_num).Capital.F())
|
||||
assert.Equal(t, 0., c.ShipGroup(2).Load.F())
|
||||
assert.Equal(t, 96.8, c.MustPlanet(Uninhabited_Planet_4_num).Population.F())
|
||||
assert.True(t, c.MustPlanet(Uninhabited_Planet_4_num).OwnedBy(Race_1_ID))
|
||||
assert.Equal(t, game.ProductionCapital, c.MustPlanet(Uninhabited_Planet_4_num).Production.Type)
|
||||
assert.Equal(t, 17.3, c.ShipGroup(3).Load.F())
|
||||
}
|
||||
|
||||
func TestRemoveUnreachableRoutes(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "MAT", R0_Planet_2_num, R0_Planet_0_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_0.Name, "CAP", R0_Planet_2_num, R0_Planet_0_num))
|
||||
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "COL", R1_Planet_1_num, R0_Planet_0_num))
|
||||
assert.NoError(t, g.PlanetRouteSet(Race_1.Name, "CAP", R1_Planet_1_num, Uninhabited_Planet_4_num))
|
||||
|
||||
assert.Error(t, g.PlanetRouteSet(Race_0.Name, "COL", R0_Planet_2_num, Uninhabited_Planet_3_num))
|
||||
c.MustPlanet(R0_Planet_2_num).Route[game.RouteColonist] = Uninhabited_Planet_3_num
|
||||
|
||||
c.RemoveUnreachableRoutes()
|
||||
|
||||
assert.NotContains(t, c.MustPlanet(R0_Planet_2_num).Route, game.RouteColonist)
|
||||
assert.Contains(t, c.MustPlanet(R0_Planet_2_num).Route, game.RouteMaterial)
|
||||
assert.Contains(t, c.MustPlanet(R0_Planet_2_num).Route, game.RouteCapital)
|
||||
assert.Contains(t, c.MustPlanet(R1_Planet_1_num).Route, game.RouteColonist)
|
||||
assert.Contains(t, c.MustPlanet(R1_Planet_1_num).Route, game.RouteCapital)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) ScienceCreate(ri int, name string, drive, weapons, shileds, cargo float64) error {
|
||||
c.validateRaceIndex(ri)
|
||||
n, ok := util.ValidateTypeName(name)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if sc := slices.IndexFunc(c.g.Race[ri].Sciences, func(s game.Science) bool { return s.Name == n }); sc >= 0 {
|
||||
return e.NewEntityDuplicateIdentifierError("science %q", c.g.Race[ri].Sciences[sc].Name)
|
||||
}
|
||||
|
||||
if drive < 0 {
|
||||
return e.NewDriveValueError(drive)
|
||||
}
|
||||
if weapons < 0 {
|
||||
return e.NewWeaponsValueError(weapons)
|
||||
}
|
||||
if shileds < 0 {
|
||||
return e.NewShieldsValueError(shileds)
|
||||
}
|
||||
if cargo < 0 {
|
||||
return e.NewCargoValueError(cargo)
|
||||
}
|
||||
sum := drive + weapons + shileds + cargo
|
||||
if sum != 1 {
|
||||
return e.NewScienceSumValuesError("D=%f W=%f S=%f C=%f sum=%f", drive, weapons, shileds, cargo, sum)
|
||||
}
|
||||
c.g.Race[ri].Sciences = append(c.g.Race[ri].Sciences, game.Science{
|
||||
ID: uuid.New(),
|
||||
Name: n,
|
||||
Drive: game.Float(drive),
|
||||
Weapons: game.Float(weapons),
|
||||
Shields: game.Float(shileds),
|
||||
Cargo: game.Float(cargo),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) ScienceRemove(ri int, name string) error {
|
||||
c.validateRaceIndex(ri)
|
||||
sc := slices.IndexFunc(c.g.Race[ri].Sciences, func(s game.Science) bool { return s.Name == name })
|
||||
if sc < 0 {
|
||||
return e.NewEntityNotExistsError("science %q", name)
|
||||
}
|
||||
if pl := slices.IndexFunc(c.g.Map.Planet, func(p game.Planet) bool {
|
||||
return p.Production.Type == game.ResearchScience &&
|
||||
p.Production.SubjectID != nil &&
|
||||
*p.Production.SubjectID == c.g.Race[ri].Sciences[sc].ID
|
||||
}); pl >= 0 {
|
||||
return e.NewDeleteSciencePlanetProductionError(c.g.Map.Planet[pl].Name)
|
||||
}
|
||||
c.g.Race[ri].Sciences = append(c.g.Race[ri].Sciences[:sc], c.g.Race[ri].Sciences[sc+1:]...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResearchTech(r *game.Race, freeProduction float64, drive, weapons, shields, cargo float64) {
|
||||
increment := freeProduction / 5000.
|
||||
if drive > 0 {
|
||||
r.Tech = r.Tech.Set(game.TechDrive, r.Tech.Value(game.TechDrive)+increment*drive)
|
||||
}
|
||||
if weapons > 0 {
|
||||
r.Tech = r.Tech.Set(game.TechWeapons, r.Tech.Value(game.TechWeapons)+increment*weapons)
|
||||
}
|
||||
if shields > 0 {
|
||||
r.Tech = r.Tech.Set(game.TechShields, r.Tech.Value(game.TechShields)+increment*shields)
|
||||
}
|
||||
if cargo > 0 {
|
||||
r.Tech = r.Tech.Set(game.TechCargo, r.Tech.Value(game.TechCargo)+increment*cargo)
|
||||
}
|
||||
}
|
||||
|
||||
// Internal func
|
||||
|
||||
func (c *Cache) raceScience(ri int) []game.Science {
|
||||
c.validateRaceIndex(ri)
|
||||
return c.g.Race[ri].Sciences
|
||||
}
|
||||
|
||||
func (c *Cache) mustScience(ri int, id uuid.UUID) *game.Science {
|
||||
c.validateRaceIndex(ri)
|
||||
r := &c.g.Race[ri]
|
||||
i := slices.IndexFunc(r.Sciences, func(s game.Science) bool { return s.ID == id })
|
||||
if i < 0 {
|
||||
panic(fmt.Sprintf("science not found for race=%q id=%v", r.Name, id))
|
||||
}
|
||||
return &c.g.Race[ri].Sciences[i]
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScienceCreate(t *testing.T) {
|
||||
c, g := newCache()
|
||||
first := "Drive_Shields"
|
||||
second := "Hyperdrive"
|
||||
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 0)
|
||||
assert.NoError(t, g.ScienceCreate(Race_0.Name, first, 0.4, 0, 0.6, 0))
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 1)
|
||||
sc := c.RaceScience(Race_0_idx)[0]
|
||||
assert.NoError(t, uuid.Validate(sc.ID.String()))
|
||||
assert.Equal(t, first, sc.Name)
|
||||
assert.Equal(t, 0.4, sc.Drive.F())
|
||||
assert.Equal(t, 0., sc.Weapons.F())
|
||||
assert.Equal(t, 0.6, sc.Shields.F())
|
||||
assert.Equal(t, 0., sc.Cargo.F())
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(UnknownRace, second, 0.4, 0, 0.6, 0),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_Extinct.Name, second, 0.4, 0, 0.6, 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, BadEntityName, 0.4, 0, 0.6, 0),
|
||||
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, first, 0.4, 0, 0.6, 0),
|
||||
e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, -0.1, 0, 1.1, 0),
|
||||
e.GenericErrorText(e.ErrInputDriveValue))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 1.5, -0.5, 0, 0),
|
||||
e.GenericErrorText(e.ErrInputWeaponsValue))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 1.3, 0, -0.3, 0),
|
||||
e.GenericErrorText(e.ErrInputShieldsValue))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 0, 1.07, 0, -0.07),
|
||||
e.GenericErrorText(e.ErrInputCargoValue))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 0.26, 0.25, 0.25, 0.25),
|
||||
e.GenericErrorText(e.ErrInputScienceSumValues))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 0.25, 0.26, 0.25, 0.25),
|
||||
e.GenericErrorText(e.ErrInputScienceSumValues))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 0.25, 0.25, 0.26, 0.25),
|
||||
e.GenericErrorText(e.ErrInputScienceSumValues))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceCreate(Race_0.Name, second, 0.25, 0.25, 0.25, 0.26),
|
||||
e.GenericErrorText(e.ErrInputScienceSumValues))
|
||||
|
||||
assert.NoError(t, g.ScienceCreate(Race_0.Name, second, 0.25, 0.25, 0.25, 0.25))
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 2)
|
||||
sc = c.RaceScience(Race_0_idx)[1]
|
||||
assert.NoError(t, uuid.Validate(sc.ID.String()))
|
||||
assert.Equal(t, second, sc.Name)
|
||||
assert.Equal(t, 0.25, sc.Drive.F())
|
||||
assert.Equal(t, 0.25, sc.Weapons.F())
|
||||
assert.Equal(t, 0.25, sc.Shields.F())
|
||||
assert.Equal(t, 0.25, sc.Cargo.F())
|
||||
}
|
||||
|
||||
func TestScienceRemove(t *testing.T) {
|
||||
c, g := newCache()
|
||||
first := "Drive_Shields"
|
||||
second := "Hyperdrive"
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 0)
|
||||
assert.NoError(t, g.ScienceCreate(Race_0.Name, first, 0.4, 0, 0.6, 0))
|
||||
assert.NoError(t, g.ScienceCreate(Race_0.Name, second, 0.25, 0.25, 0.25, 0.25))
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 2)
|
||||
|
||||
assert.NoError(t, g.ScienceRemove(Race_0.Name, first))
|
||||
assert.Len(t, c.RaceScience(Race_0_idx), 1)
|
||||
|
||||
g.PlanetProduce(Race_0.Name, int(R0_Planet_0_num), "SCIENCE", second)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceRemove(UnknownRace, second),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceRemove(Race_Extinct.Name, second),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceRemove(Race_0.Name, first),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ScienceRemove(Race_0.Name, second),
|
||||
e.GenericErrorText(e.ErrDeleteSciencePlanetProduction))
|
||||
}
|
||||
|
||||
func TestResearchTech(t *testing.T) {
|
||||
r := Race_0
|
||||
rr := &r
|
||||
assert.Equal(t, 1.1, rr.Tech.Value(game.TechDrive))
|
||||
assert.Equal(t, 1.2, rr.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 1.3, rr.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 1.4, rr.Tech.Value(game.TechCargo))
|
||||
|
||||
controller.ResearchTech(rr, 500, 1.0, 0.0, 0.0, 0.0)
|
||||
assert.InDelta(t, 1.2, rr.Tech.Value(game.TechDrive), 0.000001)
|
||||
assert.Equal(t, 1.2, rr.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 1.3, rr.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 1.4, rr.Tech.Value(game.TechCargo))
|
||||
|
||||
controller.ResearchTech(rr, 500, 0.0, 0.5, 0.0, 0.5)
|
||||
assert.InDelta(t, 1.2, rr.Tech.Value(game.TechDrive), 0.000001)
|
||||
assert.Equal(t, 1.25, rr.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 1.3, rr.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo))
|
||||
|
||||
controller.ResearchTech(rr, 500, 0.5, 0.0, 0.5, 0.0)
|
||||
assert.InDelta(t, 1.25, rr.Tech.Value(game.TechDrive), 0.000001)
|
||||
assert.Equal(t, 1.25, rr.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 1.35, rr.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo))
|
||||
|
||||
controller.ResearchTech(rr, 1000, 0.0, 1.0, 0.0, 0.0)
|
||||
assert.InDelta(t, 1.25, rr.Tech.Value(game.TechDrive), 0.000001)
|
||||
assert.Equal(t, 1.45, rr.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 1.35, rr.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 1.45, rr.Tech.Value(game.TechCargo))
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
"slices"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) ShipClassCreate(ri int, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error {
|
||||
c.validateRaceIndex(ri)
|
||||
if err := validateShipTypeValues(drive, ammo, weapons, shileds, cargo); err != nil {
|
||||
return err
|
||||
}
|
||||
n, ok := util.ValidateTypeName(typeName)
|
||||
if !ok {
|
||||
return e.NewEntityTypeNameValidationError("%q", n)
|
||||
}
|
||||
if st := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == typeName }); st >= 0 {
|
||||
return e.NewEntityDuplicateIdentifierError("ship class %q", c.g.Race[ri].ShipTypes[st].Name)
|
||||
}
|
||||
c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes, game.ShipType{
|
||||
ID: uuid.New(),
|
||||
Name: n,
|
||||
Drive: game.Float(drive),
|
||||
Armament: uint(ammo),
|
||||
Weapons: game.Float(weapons),
|
||||
Shields: game.Float(shileds),
|
||||
Cargo: game.Float(cargo),
|
||||
})
|
||||
c.invalidateShipGroupCache()
|
||||
c.invalidateFleetCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) shipClassMerge(ri int, sourceName, targetName string) error {
|
||||
c.validateRaceIndex(ri)
|
||||
sourceClass, sti, ok := c.ShipClass(ri, sourceName)
|
||||
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("source ship type %q", sourceName)
|
||||
}
|
||||
targetClass, _, ok := c.ShipClass(ri, targetName)
|
||||
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("target ship type %q", sourceName)
|
||||
}
|
||||
|
||||
if sourceClass.Name == targetClass.Name {
|
||||
return e.NewEntityTypeNameEqualityError("ship type %q", targetName)
|
||||
}
|
||||
|
||||
if !sourceClass.Equal(*targetClass) {
|
||||
return e.NewMergeShipTypeNotEqualError()
|
||||
}
|
||||
|
||||
// switch planet productions to the new type
|
||||
for pl := range c.g.Map.Planet {
|
||||
if c.g.Map.Planet[pl].OwnedBy(c.g.Race[ri].ID) &&
|
||||
c.g.Map.Planet[pl].Production.Type == game.ProductionShip &&
|
||||
c.g.Map.Planet[pl].Production.SubjectID != nil &&
|
||||
*c.g.Map.Planet[pl].Production.SubjectID == sourceClass.ID {
|
||||
|
||||
c.g.Map.Planet[pl].Production.SubjectID = &targetClass.ID
|
||||
}
|
||||
}
|
||||
|
||||
// switch ship groups to the new type
|
||||
for sg := range c.g.ShipGroups {
|
||||
if c.g.ShipGroups[sg].OwnerID == c.g.Race[ri].ID && c.g.ShipGroups[sg].TypeID == sourceClass.ID {
|
||||
c.g.ShipGroups[sg].TypeID = targetClass.ID
|
||||
}
|
||||
}
|
||||
|
||||
// remove the source type
|
||||
c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes[:sti], c.g.Race[ri].ShipTypes[sti+1:]...)
|
||||
|
||||
c.invalidateShipGroupCache()
|
||||
c.invalidateFleetCache()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) shipClassRemove(ri int, name string) error {
|
||||
c.validateRaceIndex(ri)
|
||||
st, i, ok := c.ShipClass(ri, name)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("ship type %q", name)
|
||||
}
|
||||
if pl := slices.IndexFunc(c.g.Map.Planet, func(p game.Planet) bool {
|
||||
return p.Production.Type == game.ProductionShip &&
|
||||
p.Production.SubjectID != nil &&
|
||||
st.ID == *p.Production.SubjectID
|
||||
}); pl >= 0 {
|
||||
return e.NewDeleteShipTypePlanetProductionError(c.g.Map.Planet[pl].Name)
|
||||
}
|
||||
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.TypeID == st.ID {
|
||||
return e.NewDeleteShipTypeExistingGroupError("group: %s", sg.ID)
|
||||
}
|
||||
}
|
||||
c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes[:i], c.g.Race[ri].ShipTypes[i+1:]...)
|
||||
|
||||
c.invalidateShipGroupCache()
|
||||
c.invalidateFleetCache()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShipTypes used for tests only
|
||||
func (c *Cache) ShipTypes(ri int) []*game.ShipType {
|
||||
c.validateRaceIndex(ri)
|
||||
result := make([]*game.ShipType, len(c.g.Race[ri].ShipTypes))
|
||||
for i := range c.g.Race[ri].ShipTypes {
|
||||
result[i] = &c.g.Race[ri].ShipTypes[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Cache) ListShipTypes(ri int) iter.Seq[*game.ShipType] {
|
||||
return func(yield func(*game.ShipType) bool) {
|
||||
for i := range c.g.Race[ri].ShipTypes {
|
||||
if !yield(&c.g.Race[ri].ShipTypes[i]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) ShipType(ri int, ID uuid.UUID) (*game.ShipType, bool) {
|
||||
c.validateRaceIndex(ri)
|
||||
for i := range c.g.Race[ri].ShipTypes {
|
||||
if c.g.Race[ri].ShipTypes[i].ID == ID {
|
||||
return &c.g.Race[ri].ShipTypes[i], true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Cache) MustShipType(ri int, ID uuid.UUID) *game.ShipType {
|
||||
if v, ok := c.ShipType(ri, ID); ok {
|
||||
return v
|
||||
}
|
||||
panic(fmt.Sprintf("ship class not found: race_idx=%d id=%v", ri, ID))
|
||||
}
|
||||
|
||||
func validateShipTypeValues(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,150 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShipClassCreate(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Random", 1, 3, 5, 4, 2))
|
||||
ships := slices.Collect(c.ListShipTypes(Race_0_idx))
|
||||
assert.Len(t, ships, 4)
|
||||
st := ships[3]
|
||||
assert.Equal(t, 1., float64(st.Drive))
|
||||
assert.Equal(t, 3, int(st.Armament))
|
||||
assert.Equal(t, 5., float64(st.Weapons))
|
||||
assert.Equal(t, 4., float64(st.Shields))
|
||||
assert.Equal(t, 2., float64(st.Cargo))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassCreate(Race_0.Name, Race_0_Gunship, 1, 0, 0, 0, 0),
|
||||
e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassCreate(UnknownRace, "Drone", 1, 0, 0, 0, 0),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassCreate(Race_Extinct.Name, "Drone", 1, 0, 0, 0, 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassCreate(Race_0.Name, BadEntityName, 1, 0, 0, 0, 0),
|
||||
e.GenericErrorText(e.ErrInputEntityTypeNameInvalid))
|
||||
}
|
||||
|
||||
func TestCreateShipTypeValidation(t *testing.T) {
|
||||
race := Race_0.Name
|
||||
typeName := "Drone"
|
||||
type tc struct {
|
||||
name string
|
||||
d, w, s, c float64
|
||||
a int
|
||||
err string
|
||||
}
|
||||
table := []tc{
|
||||
// correct values
|
||||
{typeName, 1, 0, 0, 0, 0, ""},
|
||||
{typeName, 1.1, 0, 0, 0, 0, ""},
|
||||
{typeName, 1, 1.2, 0, 0, 1, ""},
|
||||
{typeName, 1, 1.2, 2.5, 0, 1, ""},
|
||||
{typeName, 1, 0, 2.5, 7.7, 0, ""},
|
||||
// incorrect values...
|
||||
{"", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)},
|
||||
{" ", 1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)},
|
||||
{typeName, 0, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeZeroValues)},
|
||||
// drive
|
||||
{typeName, -1, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)},
|
||||
{typeName, 0.5, 0, 0, 0, 0, e.GenericErrorText(e.ErrInputDriveValue)},
|
||||
// weapons
|
||||
{typeName, 0, -1, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)},
|
||||
{typeName, 0, 0.5, 0, 0, 0, e.GenericErrorText(e.ErrInputWeaponsValue)},
|
||||
// shields
|
||||
{typeName, 0, 0, -1, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)},
|
||||
{typeName, 0, 0, 0.5, 0, 0, e.GenericErrorText(e.ErrInputShieldsValue)},
|
||||
// cargo
|
||||
{typeName, 0, 0, 0, -1, 0, e.GenericErrorText(e.ErrInputCargoValue)},
|
||||
{typeName, 0, 0, 0, 0.5, 0, e.GenericErrorText(e.ErrInputCargoValue)},
|
||||
// armament (and weapons)
|
||||
{typeName, 0, 0, 0, 0, -1, e.GenericErrorText(e.ErrInputShipTypeArmamentValue)},
|
||||
{typeName, 0, 1, 0, 0, 0, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)},
|
||||
{typeName, 0, 0, 0, 0, 1, e.GenericErrorText(e.ErrInputShipTypeWeaponsAndArmamentValue)},
|
||||
}
|
||||
for i, tc := range table {
|
||||
_, g := newCache()
|
||||
|
||||
if tc.err == "" {
|
||||
err := g.ShipClassCreate(race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c)
|
||||
assert.NoError(t, err)
|
||||
err = g.ShipClassCreate(race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c)
|
||||
assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
||||
} else {
|
||||
err := g.ShipClassCreate(race, tc.name, tc.d, tc.a, tc.w, tc.s, tc.c)
|
||||
assert.ErrorContains(t, err, tc.err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShipClassMerge(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.Len(t, c.ShipTypes(Race_0_idx), 3)
|
||||
|
||||
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Drone", 1, 0, 0, 0, 0))
|
||||
assert.Len(t, c.ShipTypes(Race_0_idx), 4)
|
||||
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Spy", 1, 0, 0, 0, 0))
|
||||
assert.Len(t, c.ShipTypes(Race_0_idx), 5)
|
||||
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Surfer", 15, 15, 15, 0, 1))
|
||||
assert.Len(t, c.ShipTypes(Race_0_idx), 6)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassMerge(Race_0.Name, "Sky", "Drone"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassMerge(Race_0.Name, "Spy", "Elephant"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassMerge(Race_Extinct.Name, "Spy", "Drone"),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassMerge(Race_0.Name, "Spy", "Spy"),
|
||||
e.GenericErrorText(e.ErrInputEntityTypeNameEquality))
|
||||
|
||||
assert.NoError(t, g.ShipClassMerge(Race_0.Name, "Spy", "Drone"))
|
||||
assert.Len(t, c.ShipTypes(Race_0_idx), 5)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassMerge(Race_0.Name, "Drone", "Surfer"),
|
||||
e.GenericErrorText(e.ErrMergeShipTypeNotEqual))
|
||||
}
|
||||
|
||||
func TestShipClassRemove(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
assert.NoError(t, g.ShipClassCreate(Race_0.Name, "Drone", 1, 0, 0, 0, 0))
|
||||
g.PlanetProduce(Race_0.Name, int(R0_Planet_0_num), "SHIP", "Drone")
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassRemove(UnknownRace, Race_0_Freighter),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassRemove(Race_Extinct.Name, Race_0_Freighter),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassRemove(Race_0.Name, "Elephant"),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassRemove(Race_0.Name, "Drone"),
|
||||
e.GenericErrorText(e.ErrDeleteShipTypePlanetProduction))
|
||||
|
||||
assert.NoError(t, g.ShipClassRemove(Race_0.Name, Race_0_Freighter))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipClassRemove(Race_0.Name, Race_0_Gunship),
|
||||
e.GenericErrorText(e.ErrDeleteShipTypeExistingGroup))
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// 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) internalShipGroupJoinFleet(groupIndex int, fID uuid.UUID) {
|
||||
c.validateShipGroupIndex(groupIndex)
|
||||
c.g.ShipGroups[groupIndex].FleetID = &fID
|
||||
c.invalidateFleetCache()
|
||||
}
|
||||
|
||||
func (c *Cache) ShipGroupShipsNumber(groupIndex int, number uint) {
|
||||
c.validateShipGroupIndex(groupIndex)
|
||||
if c.g.ShipGroups[groupIndex].Number > 0 {
|
||||
c.g.ShipGroups[groupIndex].Load = game.F(c.g.ShipGroups[groupIndex].Load.F() / float64(c.g.ShipGroups[groupIndex].Number) * float64(number))
|
||||
}
|
||||
c.g.ShipGroups[groupIndex].Number = number
|
||||
}
|
||||
|
||||
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 {
|
||||
c.validateShipGroupIndex(groupIndex)
|
||||
if len(c.cacheRaceIndexByShipGroupIndex) == 0 {
|
||||
c.cacheShipsAndGroups()
|
||||
}
|
||||
if v, ok := c.cacheRaceIndexByShipGroupIndex[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) ShipGroupDestroyItem(i int) {
|
||||
c.validateShipGroupIndex(i)
|
||||
sg := &c.g.ShipGroups[i]
|
||||
if sg.Number == 0 {
|
||||
panic("group has no ships")
|
||||
}
|
||||
sg.Load = game.F(sg.Load.F() / float64(sg.Number) * float64(sg.Number-1))
|
||||
sg.Number -= 1
|
||||
}
|
||||
|
||||
func (c *Cache) DeleteKilledShipGroups() {
|
||||
keepFleet := make(map[uuid.UUID]bool, len(c.g.Fleets))
|
||||
for sgi := len(c.g.ShipGroups) - 1; sgi >= 0; sgi-- {
|
||||
if c.g.ShipGroups[sgi].FleetID != nil {
|
||||
id := *c.g.ShipGroups[sgi].FleetID
|
||||
keepFleet[id] = keepFleet[id] || c.g.ShipGroups[sgi].Number > 0
|
||||
}
|
||||
if c.g.ShipGroups[sgi].Number == 0 {
|
||||
c.g.ShipGroups = append(c.g.ShipGroups[:sgi], c.g.ShipGroups[sgi+1:]...)
|
||||
}
|
||||
}
|
||||
c.invalidateShipGroupCache()
|
||||
for id, keep := range keepFleet {
|
||||
if keep {
|
||||
continue
|
||||
}
|
||||
c.unsafeDeleteFleet(c.MustFleetIndex(id))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) TurnMergeEqualShipGroups() {
|
||||
for i := range c.listRaceActingIdx() {
|
||||
c.transferPendingGroups(i)
|
||||
c.shipGroupMerge(i)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) transferPendingGroups(ri int) {
|
||||
c.validateRaceIndex(ri)
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
if sg.State() == game.StateTransfer {
|
||||
sg.StateTransfer = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shipGroupMerge merges several equal ship groups into one
|
||||
func (c *Cache) shipGroupMerge(ri int) {
|
||||
c.validateRaceIndex(ri)
|
||||
raceGroups := make([]game.ShipGroup, 0)
|
||||
for sg := range c.listShipGroups(ri) {
|
||||
raceGroups = append(raceGroups, *sg)
|
||||
}
|
||||
|
||||
origin := len(raceGroups)
|
||||
if origin < 2 {
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(raceGroups)-1; i++ {
|
||||
for j := len(raceGroups) - 1; j > i; j-- {
|
||||
if raceGroups[i].Equal(raceGroups[j]) {
|
||||
raceGroups[i].ID = raceGroups[j].ID // resulting group will have latest ID
|
||||
raceGroups[i].Number += raceGroups[j].Number
|
||||
raceGroups = append(raceGroups[:j], raceGroups[j+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(raceGroups) == origin {
|
||||
return
|
||||
}
|
||||
|
||||
toDelete := make([]int, 0)
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
if c.ShipGroup(i).OwnerID == c.g.Race[ri].ID {
|
||||
toDelete = append(toDelete, i)
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(toDelete)
|
||||
slices.Reverse(toDelete)
|
||||
for _, sgi := range toDelete {
|
||||
c.unsafeDeleteShipGroup(sgi)
|
||||
}
|
||||
|
||||
for i := range raceGroups {
|
||||
c.appendShipGroup(ri, &raceGroups[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupDismantle(ri int, groupIndex uuid.UUID) error {
|
||||
c.validateRaceIndex(ri)
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupIndex)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group #%d", groupIndex)
|
||||
}
|
||||
|
||||
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
||||
return e.NewShipsBusyError("state: %s", state)
|
||||
}
|
||||
|
||||
pl, ok := c.Planet(c.ShipGroup(sgi).Destination)
|
||||
if !ok {
|
||||
return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
|
||||
}
|
||||
p := *pl
|
||||
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
|
||||
if c.ShipGroup(sgi).CargoType != nil {
|
||||
ct := *c.ShipGroup(sgi).CargoType
|
||||
load := c.ShipGroup(sgi).Load.F()
|
||||
switch ct {
|
||||
case game.CargoColonist:
|
||||
if p.OwnedBy(c.g.Race[ri].ID) {
|
||||
p = game.UnloadColonists(p, load)
|
||||
}
|
||||
case game.CargoMaterial:
|
||||
p.Material = p.Material.Add(load)
|
||||
case game.CargoCapital:
|
||||
p.Capital = p.Capital.Add(load)
|
||||
}
|
||||
}
|
||||
|
||||
p.Material = p.Material.Add(c.ShipGroup(sgi).EmptyMass(st))
|
||||
|
||||
c.unsafeDeleteShipGroup(sgi)
|
||||
|
||||
c.g.Map.Planet[c.MustPlanetIndex(p.Number)] = p
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Корабль может нести только один тип груза одновременно.
|
||||
// Возможные типы груза - это колонисты, сырье и промышленность.
|
||||
// Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется.
|
||||
// Указанное количество груза равномерно распределяется между всеми кораблями группы.
|
||||
func (c *Cache) shipGroupLoad(ri int, groupID uuid.UUID, ct game.CargoType, quantity float64) error {
|
||||
c.validateRaceIndex(ri)
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
||||
return e.NewShipsBusyError("state: %s", state)
|
||||
}
|
||||
p, ok := c.Planet(c.ShipGroup(sgi).Destination)
|
||||
if !ok {
|
||||
return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination)
|
||||
}
|
||||
|
||||
if p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d", p.Number)
|
||||
}
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
|
||||
if st.Cargo < 1 {
|
||||
return e.NewNoCargoBayError("ship_type %q", st.Name)
|
||||
}
|
||||
if c.ShipGroup(sgi).CargoType != nil && *c.ShipGroup(sgi).CargoType != ct {
|
||||
return e.NewCargoLoadNotEqualError("cargo: %v", *c.ShipGroup(sgi).CargoType)
|
||||
}
|
||||
|
||||
capacity := c.ShipGroup(sgi).CargoCapacity(st)
|
||||
freeShipGroupCargoLoad := capacity - float64(c.ShipGroup(sgi).Load)
|
||||
if freeShipGroupCargoLoad == 0 {
|
||||
return e.NewCargoLoadNoSpaceLeftError()
|
||||
}
|
||||
var availableOnPlanet *game.Float
|
||||
switch ct {
|
||||
case game.CargoMaterial:
|
||||
availableOnPlanet = &p.Material
|
||||
case game.CargoCapital:
|
||||
availableOnPlanet = &p.Capital
|
||||
case game.CargoColonist:
|
||||
availableOnPlanet = &p.Colonists
|
||||
default:
|
||||
return e.NewGameStateError("CargoType not accepted: %v", ct)
|
||||
}
|
||||
if quantity > float64(*availableOnPlanet) || *availableOnPlanet == 0 {
|
||||
return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", p.Number, ct, *availableOnPlanet)
|
||||
}
|
||||
toBeLoaded := quantity
|
||||
if quantity == 0 {
|
||||
toBeLoaded = float64(*availableOnPlanet)
|
||||
}
|
||||
if toBeLoaded > freeShipGroupCargoLoad {
|
||||
toBeLoaded = freeShipGroupCargoLoad
|
||||
}
|
||||
*availableOnPlanet = (*availableOnPlanet).Add(-toBeLoaded)
|
||||
c.ShipGroup(sgi).Load = c.ShipGroup(sgi).Load.Add(toBeLoaded)
|
||||
if c.ShipGroup(sgi).Load > 0 {
|
||||
c.ShipGroup(sgi).CargoType = &ct
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Промышленность и Сырье могут быть выгружены на любой планете.
|
||||
// Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты.
|
||||
func (c *Cache) shipGroupUnload(ri int, groupID uuid.UUID, quantity float64) error {
|
||||
c.validateRaceIndex(ri)
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
||||
return e.NewShipsBusyError("state: %s", state)
|
||||
}
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
if st.Cargo < 1 {
|
||||
return e.NewNoCargoBayError("ship_type %q", st.Name)
|
||||
}
|
||||
if c.ShipGroup(sgi).CargoType == nil || c.ShipGroup(sgi).Load == 0 {
|
||||
return e.NewCargoUnloadEmptyError()
|
||||
}
|
||||
|
||||
ct := *c.ShipGroup(sgi).CargoType
|
||||
p := c.MustPlanet(c.ShipGroup(sgi).Destination)
|
||||
|
||||
if ct == game.CargoColonist && p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct)
|
||||
}
|
||||
|
||||
c.unsafeUnloadCargo(sgi, UnloadCargoRequest(float64(c.ShipGroup(sgi).Load), quantity))
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnloadCargoRequest(load, quantity float64) float64 {
|
||||
result := quantity
|
||||
if result == 0 || result > load {
|
||||
result = load
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupIndexByID(id uuid.UUID) (int, bool) {
|
||||
for sgi := range c.g.ShipGroups {
|
||||
if c.g.ShipGroups[sgi].ID == id {
|
||||
return sgi, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (c *Cache) unsafeUnloadCargo(sgi int, q float64) {
|
||||
if q <= 0 {
|
||||
return
|
||||
}
|
||||
if st := c.ShipGroup(sgi).State(); st != game.StateInOrbit {
|
||||
panic(fmt.Sprintf("invalid group state: %v", st))
|
||||
}
|
||||
c.validateShipGroupIndex(sgi)
|
||||
p := c.MustPlanet(c.ShipGroup(sgi).Destination)
|
||||
ct := *c.ShipGroup(sgi).CargoType
|
||||
|
||||
var availableOnPlanet *game.Float
|
||||
switch ct {
|
||||
case game.CargoColonist:
|
||||
availableOnPlanet = &p.Colonists
|
||||
if !p.Owned() {
|
||||
p.Own(c.ShipGroup(sgi).OwnerID)
|
||||
p.Production = game.ProductionCapital.AsType(uuid.Nil)
|
||||
}
|
||||
case game.CargoMaterial:
|
||||
availableOnPlanet = &p.Material
|
||||
case game.CargoCapital:
|
||||
availableOnPlanet = &p.Capital
|
||||
}
|
||||
*availableOnPlanet = (*availableOnPlanet).Add(q)
|
||||
|
||||
c.ShipGroup(sgi).Load = c.ShipGroup(sgi).Load.Add(-q)
|
||||
if c.ShipGroup(sgi).Load == 0 {
|
||||
c.ShipGroup(sgi).CargoType = nil
|
||||
}
|
||||
|
||||
p.UnpackColonists()
|
||||
p.UnpackCapital()
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupTransfer(ri, riAccept int, groupID uuid.UUID) (err error) {
|
||||
c.validateRaceIndex(ri)
|
||||
if ri == riAccept {
|
||||
return e.NewSameRaceError(c.g.Race[riAccept].Name)
|
||||
}
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
sg := c.ShipGroup(sgi)
|
||||
state := sg.State()
|
||||
if state == game.StateTransfer {
|
||||
return e.NewShipsBusyError("state: %s", state)
|
||||
}
|
||||
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
|
||||
var stAcc int
|
||||
var name = st.Name
|
||||
if stAcc = slices.IndexFunc(c.g.Race[riAccept].ShipTypes, func(v game.ShipType) bool { return v.Name == st.Name }); stAcc >= 0 &&
|
||||
!st.Equal(c.g.Race[riAccept].ShipTypes[stAcc]) {
|
||||
name = util.AppendRandomSuffix(name)
|
||||
}
|
||||
if stAcc < 0 || name != st.Name {
|
||||
err = c.ShipClassCreate(riAccept,
|
||||
name,
|
||||
st.Drive.F(),
|
||||
int(st.Armament),
|
||||
st.Weapons.F(),
|
||||
st.Shields.F(),
|
||||
st.Cargo.F())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stAcc = len(c.g.Race[riAccept].ShipTypes) - 1
|
||||
}
|
||||
|
||||
newGroup := *(sg)
|
||||
newGroup.ID = uuid.New()
|
||||
newGroup.TypeID = c.g.Race[riAccept].ShipTypes[stAcc].ID
|
||||
newGroup.Tech = maps.Clone(sg.Tech)
|
||||
if state == game.StateLaunched {
|
||||
newGroup.StateTransfer = true
|
||||
}
|
||||
|
||||
c.appendShipGroup(riAccept, &newGroup)
|
||||
c.unsafeDeleteShipGroup(sgi)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) ShipGroupBreak(ri int, groupID, newID uuid.UUID, quantity uint) (err error) {
|
||||
c.validateRaceIndex(ri)
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
for sgi := range c.g.ShipGroups {
|
||||
if c.g.ShipGroups[sgi].ID == newID {
|
||||
return e.NewEntityDuplicateIdentifierError("group %s", newID)
|
||||
}
|
||||
}
|
||||
|
||||
if state := c.ShipGroup(sgi).State(); state != game.StateInOrbit {
|
||||
return e.NewShipsBusyError()
|
||||
}
|
||||
|
||||
if c.ShipGroup(sgi).Number < quantity {
|
||||
return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity)
|
||||
}
|
||||
|
||||
if quantity > 0 && quantity < c.ShipGroup(sgi).Number {
|
||||
if sgi, err = c.breakGroup(ri, groupID, quantity); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.ShipGroup(sgi).FleetID = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) breakGroup(ri int, groupID uuid.UUID, newGroupShips uint) (int, error) {
|
||||
c.validateRaceIndex(ri)
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return -1, e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
if c.ShipGroup(sgi).Number < newGroupShips {
|
||||
return -1, e.NewBreakGroupIllegalNumberError("group=%s ships: %d -> %d", c.ShipGroup(sgi).ID, c.ShipGroup(sgi).Number, newGroupShips)
|
||||
}
|
||||
return c.unsafeBreakGroup(ri, sgi, newGroupShips), nil
|
||||
}
|
||||
|
||||
func (c *Cache) unsafeBreakGroup(ri, sgi int, newGroupShips uint) int {
|
||||
newGroup := *c.ShipGroup(sgi)
|
||||
if c.ShipGroup(sgi).CargoType != nil {
|
||||
newGroup.Load = game.F(float64(c.ShipGroup(sgi).Load) / float64(c.ShipGroup(sgi).Number) * float64(newGroupShips))
|
||||
}
|
||||
newGroup.Number = newGroupShips
|
||||
c.ShipGroupShipsNumber(sgi, c.ShipGroup(sgi).Number-newGroup.Number)
|
||||
newGroup.FleetID = nil
|
||||
return c.appendShipGroup(ri, &newGroup)
|
||||
}
|
||||
|
||||
// Internal funcs
|
||||
|
||||
func (c *Cache) raceShipGroupIndex(ri int, id uuid.UUID) (int, bool) {
|
||||
c.validateRaceIndex(ri)
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).ID == id {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func (c *Cache) listShipGroupIdx(ri int) iter.Seq[int] {
|
||||
c.validateRaceIndex(ri)
|
||||
return func(yield func(int) bool) {
|
||||
for i := range c.g.ShipGroups {
|
||||
if ri == c.ShipGroupOwnerRaceIndex(i) {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) listShipGroups(ri int) iter.Seq[*game.ShipGroup] {
|
||||
c.validateRaceIndex(ri)
|
||||
return func(yield func(*game.ShipGroup) bool) {
|
||||
for sgi := range c.listShipGroupIdx(ri) {
|
||||
if !yield(c.ShipGroup(sgi)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) shipGroupsInUpgrade(planetNumber uint) iter.Seq[*game.ShipGroup] {
|
||||
return func(yield func(*game.ShipGroup) bool) {
|
||||
result := make([]int, 0)
|
||||
for sg := range c.g.ShipGroups {
|
||||
// number checked for further sanity after battles
|
||||
if c.g.ShipGroups[sg].Number > 0 && c.g.ShipGroups[sg].Destination == planetNumber && c.g.ShipGroups[sg].State() == game.StateUpgrade {
|
||||
result = append(result, sg)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(result, func(a, b int) int {
|
||||
return cmp.Compare(c.g.ShipGroups[b].StateUpgrade.Cost(), c.g.ShipGroups[a].StateUpgrade.Cost())
|
||||
})
|
||||
for i := range result {
|
||||
if !yield(&c.g.ShipGroups[result[i]]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) unsafeDeleteShipGroup(sgi int) {
|
||||
c.validateShipGroupIndex(sgi)
|
||||
|
||||
sg := c.ShipGroup(sgi)
|
||||
if sg.FleetID != nil {
|
||||
fi := c.MustFleetIndex(*sg.FleetID)
|
||||
fleetGroups := slices.Collect(c.fleetGroupIds(c.RaceIndex(sg.OwnerID), fi))
|
||||
if len(fleetGroups) == 1 {
|
||||
// remove fleet when deleting last group in the fleet
|
||||
c.unsafeDeleteFleet(fi)
|
||||
}
|
||||
}
|
||||
|
||||
c.g.ShipGroups = append(c.g.ShipGroups[:sgi], c.g.ShipGroups[sgi+1:]...)
|
||||
c.invalidateShipGroupCache()
|
||||
}
|
||||
|
||||
func (c *Cache) validateShipGroupIndex(i int) {
|
||||
if i >= len(c.g.ShipGroups) {
|
||||
panic(fmt.Sprintf("group index out of range: %d >= %d", i, len(c.g.ShipGroups)))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) unsafeCreateShips(ri int, classID uuid.UUID, planet uint, quantity uint) int {
|
||||
st := c.MustShipType(ri, classID)
|
||||
level := func(t game.Tech) float64 {
|
||||
if t == game.TechDrive && st.DriveBlockMass() > 0 {
|
||||
return util.Fixed3(c.g.Race[ri].TechLevel(game.TechDrive))
|
||||
}
|
||||
if t == game.TechWeapons && st.WeaponsBlockMass() > 0 {
|
||||
return util.Fixed3(c.g.Race[ri].TechLevel(game.TechWeapons))
|
||||
}
|
||||
if t == game.TechShields && st.ShieldsBlockMass() > 0 {
|
||||
return util.Fixed3(c.g.Race[ri].TechLevel(game.TechShields))
|
||||
}
|
||||
if t == game.TechCargo && st.CargoBlockMass() > 0 {
|
||||
return util.Fixed3(c.g.Race[ri].TechLevel(game.TechCargo))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return c.appendShipGroup(ri, &game.ShipGroup{
|
||||
OwnerID: c.g.Race[ri].ID,
|
||||
TypeID: classID,
|
||||
Destination: planet,
|
||||
Number: uint(quantity),
|
||||
Tech: map[game.Tech]game.Float{
|
||||
game.TechDrive: game.F(level(game.TechDrive)),
|
||||
game.TechWeapons: game.F(level(game.TechWeapons)),
|
||||
game.TechShields: game.F(level(game.TechShields)),
|
||||
game.TechCargo: game.F(level(game.TechCargo)),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) appendShipGroup(ri int, sg *game.ShipGroup) int {
|
||||
c.validateRaceIndex(ri)
|
||||
sg.ID = uuid.New()
|
||||
sg.OwnerID = c.g.Race[ri].ID
|
||||
sg.FleetID = nil
|
||||
c.g.ShipGroups = append(c.g.ShipGroups, *sg)
|
||||
i := len(c.g.ShipGroups) - 1
|
||||
c.invalidateShipGroupCache()
|
||||
return i
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"iter"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
)
|
||||
|
||||
func (c *Cache) MoveShipGroups() {
|
||||
moved := make(map[int]bool)
|
||||
for i := range c.listMoveableGroupIds() {
|
||||
if v, ok := moved[i]; ok && v {
|
||||
continue
|
||||
}
|
||||
sg := c.ShipGroup(i)
|
||||
|
||||
if sg.FleetID != nil {
|
||||
fi := c.MustFleetIndex(*sg.FleetID)
|
||||
delta, _ := c.FleetSpeedAndMass(fi)
|
||||
for fgi := range c.fleetGroupIds(c.RaceIndex(sg.OwnerID), c.MustFleetIndex(*sg.FleetID)) {
|
||||
c.moveShipGroup(fgi, delta)
|
||||
moved[fgi] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
c.moveShipGroup(i, sg.Speed(c.ShipGroupShipClass(i)))
|
||||
moved[i] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) moveShipGroup(i int, delta float64) {
|
||||
sg := c.ShipGroup(i)
|
||||
originX, originY, ok := sg.Coord()
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("ship group state invalid: %v", sg.State()))
|
||||
}
|
||||
destPlanet := c.MustPlanet(sg.Destination)
|
||||
arrived := false
|
||||
var x, y float64
|
||||
x, y, arrived =
|
||||
util.NextTravelCoord(c.g.Map.Width, c.g.Map.Height, originX, originY, destPlanet.X.F(), destPlanet.Y.F(), delta)
|
||||
fx, fy := game.F(x), game.F(y)
|
||||
sg.StateInSpace.X = &fx
|
||||
sg.StateInSpace.Y = &fy
|
||||
if arrived {
|
||||
sg.StateInSpace = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) listMoveableGroupIds() iter.Seq[int] {
|
||||
return func(yield func(int) bool) {
|
||||
for i := range c.ShipGroupsIndex() {
|
||||
sg := c.ShipGroup(i)
|
||||
state := sg.State()
|
||||
if !(state == game.StateInOrbit || state == game.StateLaunched || state == game.StateInSpace) {
|
||||
continue
|
||||
}
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestListMoveableGroupIds(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
// 1: idx = 0 / [v] Non-Fleet group
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
|
||||
// 2: idx = 1 / [v] In-Fleet group
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
// 3: idx = 2 / [v] In-Fleet group
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "Fleet", c.ShipGroup(1).ID))
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "Fleet", c.ShipGroup(2).ID))
|
||||
|
||||
// 4: idx = 3 / [v] In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(3).StateInSpace = &InSpace
|
||||
|
||||
// 5: idx = 4 / [x] In_Upgrage
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(4).StateUpgrade = &game.InUpgrade{
|
||||
UpgradeTech: []game.UpgradePreference{},
|
||||
}
|
||||
|
||||
// 6: idx = 5 / [v] Just launched group
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
assert.NoError(t, g.ShipGroupSend(Race_0.Name, c.ShipGroup(5).ID, R0_Planet_2_num))
|
||||
|
||||
movableGroups := slices.Collect(c.ListMoveableGroupIds())
|
||||
assert.Len(t, movableGroups, 5)
|
||||
for _, i := range movableGroups {
|
||||
sg := c.ShipGroup(i)
|
||||
assert.NotEqual(t, game.StateUpgrade, sg.State())
|
||||
assert.NotEqual(t, game.StateTransfer, sg.State())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) shipGroupSend(ri int, groupID uuid.UUID, planetNumber uint) error {
|
||||
c.validateRaceIndex(ri)
|
||||
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
|
||||
pcount := 0
|
||||
for i := range c.g.Map.Planet {
|
||||
if c.g.Map.Planet[i].OwnedBy(c.g.Race[ri].ID) {
|
||||
pcount++
|
||||
break
|
||||
}
|
||||
}
|
||||
if pcount == 0 {
|
||||
return e.NewSendShipOwnerHasNoPlanetsError()
|
||||
}
|
||||
|
||||
if st.DriveBlockMass() == 0 {
|
||||
return e.NewSendShipHasNoDrivesError()
|
||||
}
|
||||
|
||||
sourcePlanet, ok := c.ShipGroup(sgi).AtPlanet()
|
||||
if !ok {
|
||||
return e.NewShipsBusyError("state: %s", c.ShipGroup(sgi).State())
|
||||
}
|
||||
|
||||
p1, ok := c.Planet(sourcePlanet)
|
||||
if !ok {
|
||||
return e.NewGameStateError("source planet #%d does not exists", sourcePlanet)
|
||||
}
|
||||
p2, ok := c.Planet(planetNumber)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("destination planet #%d", planetNumber)
|
||||
}
|
||||
rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X.F(), p1.Y.F(), p2.X.F(), p2.Y.F())
|
||||
if rangeToDestination > c.g.Race[ri].FlightDistance() {
|
||||
return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination)
|
||||
}
|
||||
|
||||
if p1.Number == p2.Number {
|
||||
c.UnsendShips(sgi)
|
||||
c.shipGroupMerge(ri)
|
||||
return nil
|
||||
}
|
||||
|
||||
c.LaunchShips(sgi, planetNumber)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) LaunchShips(sgi int, destination uint) *game.ShipGroup {
|
||||
sg := c.ShipGroup(sgi)
|
||||
var p *game.Planet
|
||||
switch sg.State() {
|
||||
case game.StateInOrbit:
|
||||
p = c.MustPlanet(sg.Destination)
|
||||
case game.StateLaunched:
|
||||
p = c.MustPlanet(sg.StateInSpace.Origin)
|
||||
default:
|
||||
panic("state invalid")
|
||||
}
|
||||
c.g.ShipGroups[sgi] = LaunchShips(*sg, destination, p.X.F(), p.Y.F())
|
||||
return &c.g.ShipGroups[sgi]
|
||||
}
|
||||
|
||||
func (c *Cache) UnsendShips(sgi int) *game.ShipGroup {
|
||||
sg := c.ShipGroup(sgi)
|
||||
if sg.State() != game.StateLaunched {
|
||||
panic("state invalid")
|
||||
}
|
||||
c.g.ShipGroups[sgi] = UnsendShips(*sg)
|
||||
return &c.g.ShipGroups[sgi]
|
||||
}
|
||||
|
||||
func LaunchShips(sg game.ShipGroup, destination uint, originX, originY float64) game.ShipGroup {
|
||||
sg.StateInSpace = &game.InSpace{
|
||||
Origin: sg.Destination,
|
||||
X: nil,
|
||||
Y: nil,
|
||||
}
|
||||
sg.Destination = destination
|
||||
return sg
|
||||
}
|
||||
|
||||
func UnsendShips(sg game.ShipGroup) game.ShipGroup {
|
||||
sg.Destination = sg.StateInSpace.Origin
|
||||
sg.StateInSpace = nil
|
||||
return sg
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShipGroupSend(t *testing.T) {
|
||||
c, g := newCache()
|
||||
// group #1 - in_orbit, free to upgrade
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 10))
|
||||
// group #2 - in_space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
|
||||
c.ShipGroup(1).StateInSpace = &InSpace
|
||||
// group #3 - in_orbit, unmovable
|
||||
g.ShipClassCreate(Race_0.Name, "Fortress", 0, 50, 30, 100, 0)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1))
|
||||
|
||||
shiplessRace := "Shipless"
|
||||
ri, _ := c.AddRace(shiplessRace)
|
||||
assert.NoError(t, c.ShipClassCreate(ri, "Drone", 1, 0, 0, 0, 0))
|
||||
sgi := c.CreateShipsUnsafe_T(ri, c.MustShipClass(ri, "Drone").ID, R0_Planet_0_num, 1)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(UnknownRace, c.ShipGroup(0).ID, 2),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(Race_Extinct.Name, c.ShipGroup(0).ID, 2),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(Race_0.Name, uuid.New(), 2),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(Race_0.Name, c.ShipGroup(0).ID, 222),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(Race_0.Name, c.ShipGroup(1).ID, 1),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(shiplessRace, c.ShipGroup(sgi).ID, 2),
|
||||
e.GenericErrorText(e.ErrSendShipOwnerHasNoPlanets))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(Race_0.Name, c.ShipGroup(2).ID, 2),
|
||||
e.GenericErrorText(e.ErrSendShipHasNoDrives))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupSend(Race_0.Name, c.ShipGroup(0).ID, 3),
|
||||
e.GenericErrorText(e.ErrSendUnreachableDestination))
|
||||
|
||||
assert.NoError(t, g.ShipGroupSend(Race_0.Name, c.ShipGroup(0).ID, R0_Planet_2_num)) // send #0
|
||||
assert.Equal(t, game.StateLaunched, c.ShipGroup(0).State())
|
||||
assert.NotNil(t, c.ShipGroup(0).StateInSpace)
|
||||
assert.Nil(t, c.ShipGroup(0).StateInSpace.X)
|
||||
assert.Nil(t, c.ShipGroup(0).StateInSpace.Y)
|
||||
|
||||
assert.NoError(t, g.ShipGroupSend(Race_0.Name, c.ShipGroup(0).ID, R0_Planet_0_num)) // un-send #0
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"galaxy/util"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateShips(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
|
||||
assert.ErrorContains(t,
|
||||
c.CreateShips(Race_0_idx, "Unknown_Ship_Type", R0_Planet_0_num, 2),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
c.CreateShips(Race_0_idx, Race_0_Gunship, R1_Planet_1_num, 2),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(1)), 1)
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(1)), 2)
|
||||
}
|
||||
|
||||
func TestUnsafeCreateShips(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
r := c.Race(Race_0_idx)
|
||||
r.Tech = r.Tech.Set(game.TechDrive, 1.001)
|
||||
r.Tech = r.Tech.Set(game.TechWeapons, 2.999)
|
||||
r.Tech = r.Tech.Set(game.TechShields, 3.1003)
|
||||
r.Tech = r.Tech.Set(game.TechCargo, 4.0005001)
|
||||
|
||||
sgi := c.CreateShipsUnsafe_T(Race_0_idx, c.MustShipClass(Race_0_idx, Race_0_Freighter).ID, R0_Planet_0_num, 1)
|
||||
sg := c.ShipGroup(sgi)
|
||||
assert.Equal(t, 1.001, sg.Tech.Value(game.TechDrive))
|
||||
assert.Equal(t, 0., sg.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 3.100, sg.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 4.001, sg.Tech.Value(game.TechCargo))
|
||||
|
||||
sgi = c.CreateShipsUnsafe_T(Race_0_idx, c.MustShipClass(Race_0_idx, Race_0_Gunship).ID, R0_Planet_0_num, 1)
|
||||
sg = c.ShipGroup(sgi)
|
||||
assert.Equal(t, 1.001, sg.Tech.Value(game.TechDrive))
|
||||
assert.Equal(t, 2.999, sg.Tech.Value(game.TechWeapons))
|
||||
assert.Equal(t, 3.100, sg.Tech.Value(game.TechShields))
|
||||
assert.Equal(t, 0., sg.Tech.Value(game.TechCargo))
|
||||
}
|
||||
|
||||
func TestShipGroupMerge(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // 1 -> 2
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1))
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) // (2)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) // (3)
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1))
|
||||
|
||||
c.RaceTechLevel(Race_0_idx, game.TechDrive, 1.5)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 9)) // 4 -> 6
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) // 5 -> 7
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 4)) // (6)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 4)) // (7)
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 7)
|
||||
|
||||
c.RaceTechLevel(Race_1_idx, game.TechShields, 2.0)
|
||||
assert.Equal(t, 2.0, c.Race(Race_1_idx).Tech[game.TechShields].F())
|
||||
assert.NoError(t, c.CreateShips(1, Race_1_Freighter, R1_Planet_1_num, 1))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 3)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupMerge(UnknownRace),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupMerge(Race_Extinct.Name),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
|
||||
assert.NoError(t, g.ShipGroupMerge(Race_0.Name))
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 3)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
|
||||
|
||||
shipTypeID := func(ri int, name string) uuid.UUID {
|
||||
class, _, ok := c.ShipClass(ri, name)
|
||||
if !ok {
|
||||
t.Fatalf("ship_class not found: %s", name)
|
||||
return uuid.Nil
|
||||
}
|
||||
return class.ID
|
||||
}
|
||||
|
||||
for sg := range c.RaceShipGroups(Race_0_idx) {
|
||||
switch {
|
||||
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Freighter) && sg.TechLevel(game.TechDrive) == 1.1:
|
||||
assert.Equal(t, uint(7), sg.Number)
|
||||
// assert.Equal(t, uint(1), sg.Index)
|
||||
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Freighter) && sg.TechLevel(game.TechDrive) == 1.5:
|
||||
assert.Equal(t, uint(11), sg.Number)
|
||||
// assert.Equal(t, uint(4), sg.Index)
|
||||
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Gunship) && sg.TechLevel(game.TechDrive) == 1.1:
|
||||
assert.Equal(t, uint(2), sg.Number)
|
||||
// assert.Equal(t, uint(2), sg.Index)
|
||||
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Gunship) && sg.TechLevel(game.TechDrive) == 1.5:
|
||||
assert.Equal(t, uint(13), sg.Number)
|
||||
// assert.Equal(t, uint(3), sg.Index)
|
||||
default:
|
||||
t.Error("not all ship groups covered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShipGroupBreak(t *testing.T) {
|
||||
c, g := newCache()
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 13)) // group #1 (0)
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7)) // group #2 (1) - In_Space
|
||||
c.ShipGroup(1).StateInSpace = &InSpace
|
||||
|
||||
fleet := "R0_Fleet"
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, fleet, c.ShipGroup(0).ID))
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(UnknownRace, c.ShipGroup(0).ID, uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_Extinct.Name, c.ShipGroup(0).ID, uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, uuid.New(), uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, c.ShipGroup(0).ID, c.ShipGroup(0).ID, 1),
|
||||
e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, c.ShipGroup(0).ID, uuid.New(), 17),
|
||||
e.GenericErrorText(e.ErrBeakGroupNumberNotEnough))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, c.ShipGroup(1).ID, uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 1)
|
||||
|
||||
// group #1 -> group #3 (5 new, 8 left)
|
||||
assert.NoError(t, c.ShipGroupBreak(Race_0_idx, c.ShipGroup(0).ID, uuid.New(), 5)) // group #3 (2)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3)
|
||||
assert.Equal(t, uint(8), c.ShipGroup(0).Number)
|
||||
assert.NotNil(t, c.ShipGroup(0).FleetID)
|
||||
assert.Equal(t, uint(5), c.ShipGroup(2).Number)
|
||||
// assert.Equal(t, uint(3), c.ShipGroup(2).Index)
|
||||
assert.Nil(t, c.ShipGroup(2).FleetID)
|
||||
assert.Nil(t, c.ShipGroup(2).CargoType)
|
||||
|
||||
// group #1 -> group #4 (2 new, 6 left)
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 32.8 // 8 ships
|
||||
assert.NoError(t, c.ShipGroupBreak(Race_0_idx, c.ShipGroup(0).ID, uuid.New(), 2)) // group #4 (3)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
|
||||
assert.Equal(t, uint(6), c.ShipGroup(0).Number)
|
||||
assert.NotNil(t, c.ShipGroup(0).FleetID)
|
||||
assert.Equal(t, uint(2), c.ShipGroup(3).Number)
|
||||
// assert.Equal(t, uint(4), c.ShipGroup(3).Index)
|
||||
assert.Nil(t, c.ShipGroup(3).FleetID)
|
||||
assert.NoError(t, c.ShipGroupJoinFleet(Race_0_idx, fleet, c.ShipGroup(3).ID))
|
||||
assert.NotNil(t, c.ShipGroup(3).FleetID)
|
||||
|
||||
assert.Equal(t, game.CargoColonist.Ref(), c.ShipGroup(0).CargoType)
|
||||
assert.Equal(t, 24.6, util.Fixed3(c.ShipGroup(0).Load.F()))
|
||||
assert.Equal(t, game.CargoColonist.Ref(), c.ShipGroup(3).CargoType)
|
||||
assert.Equal(t, 8.2, util.Fixed3(c.ShipGroup(3).Load.F()))
|
||||
|
||||
// group #1 -> MAX 6 off the fleet
|
||||
assert.NoError(t, g.ShipGroupBreak(Race_0.Name, c.ShipGroup(0).ID, uuid.New(), 6)) // group #1 (0)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
|
||||
assert.Equal(t, uint(6), c.ShipGroup(0).Number)
|
||||
assert.Nil(t, c.ShipGroup(0).FleetID)
|
||||
|
||||
// group #4 -> ALL off the fleet
|
||||
assert.NoError(t, g.ShipGroupBreak(Race_0.Name, c.ShipGroup(3).ID, uuid.New(), 0)) // group #1 (0)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
|
||||
assert.Equal(t, uint(2), c.ShipGroup(3).Number)
|
||||
assert.Nil(t, c.ShipGroup(3).FleetID)
|
||||
}
|
||||
|
||||
func TestShipGroupTransfer(t *testing.T) {
|
||||
c, g := newCache()
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 11)) // group #1 (0)
|
||||
assert.NoError(t, c.CreateShips(Race_1_idx, ShipType_Cruiser, R1_Planet_1_num, 23)) // group #2 (1)
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 17)) // group #3 (2) - In_Space
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "R0_Fleet", c.ShipGroup(2).ID))
|
||||
assert.NotNil(t, c.ShipGroup(2).FleetID)
|
||||
c.ShipGroup(2).StateInSpace = &InSpace
|
||||
c.ShipGroup(2).CargoType = game.CargoMaterial.Ref()
|
||||
c.ShipGroup(2).Load = 1.234
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 1)
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(UnknownRace, Race_1.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_0.Name, UnknownRace, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_0.Name, Race_Extinct.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_Extinct.Name, Race_1.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_0.Name, Race_0.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrInputSameRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_0.Name, Race_1.Name, uuid.New()),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
|
||||
orig := *c.ShipGroup(2)
|
||||
assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(2).ID)) // group #2 (3)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 2)
|
||||
|
||||
newSg := c.ShipGroup(2)
|
||||
assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Name, c.MustShipClass(Race_0_idx, Race_0_Gunship).Name)
|
||||
assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Drive, c.MustShipClass(Race_0_idx, Race_0_Gunship).Drive)
|
||||
assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Weapons, c.MustShipClass(Race_0_idx, Race_0_Gunship).Weapons)
|
||||
assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Shields, c.MustShipClass(Race_0_idx, Race_0_Gunship).Shields)
|
||||
assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Cargo, c.MustShipClass(Race_0_idx, Race_0_Gunship).Cargo)
|
||||
assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Armament, c.MustShipClass(Race_0_idx, Race_0_Gunship).Armament)
|
||||
assert.Equal(t, orig.State(), newSg.State())
|
||||
assert.Equal(t, orig.CargoType, newSg.CargoType)
|
||||
assert.Equal(t, orig.Load, newSg.Load)
|
||||
assert.Equal(t, orig.TechLevel(game.TechDrive), newSg.TechLevel(game.TechDrive))
|
||||
assert.Equal(t, orig.TechLevel(game.TechWeapons), newSg.TechLevel(game.TechWeapons))
|
||||
assert.Equal(t, orig.TechLevel(game.TechShields), newSg.TechLevel(game.TechShields))
|
||||
assert.Equal(t, orig.TechLevel(game.TechCargo), newSg.TechLevel(game.TechCargo))
|
||||
assert.Equal(t, orig.Destination, newSg.Destination)
|
||||
assert.Equal(t, orig.StateInSpace, newSg.StateInSpace)
|
||||
assert.Equal(t, orig.StateUpgrade, newSg.StateUpgrade)
|
||||
assert.Equal(t, orig.StateTransfer, newSg.StateTransfer)
|
||||
assert.Equal(t, newSg.TypeID, c.MustShipClass(Race_1_idx, Race_0_Gunship).ID)
|
||||
assert.Equal(t, orig.Number, newSg.Number)
|
||||
assert.Equal(t, Race_1_ID, newSg.OwnerID)
|
||||
assert.Nil(t, newSg.FleetID)
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
sg := c.ShipGroup(3)
|
||||
assert.Equal(t, game.StateInOrbit, sg.State())
|
||||
assert.NoError(t, g.ShipGroupSend(Race_0.Name, sg.ID, R0_Planet_2_num))
|
||||
assert.Equal(t, game.StateLaunched, sg.State())
|
||||
assert.Equal(t, sg.OwnerID, Race_0_ID)
|
||||
|
||||
assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, sg.ID))
|
||||
sg = c.ShipGroup(3)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 1)
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_1_idx)), 3)
|
||||
assert.Equal(t, game.StateTransfer, sg.State())
|
||||
assert.Equal(t, sg.OwnerID, Race_1_ID)
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_1.Name, Race_0.Name, sg.ID),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
|
||||
// transfer ship class with existing name
|
||||
originalName := c.MustShipClass(Race_0_idx, ShipType_Cruiser).Name
|
||||
assert.NoError(t, g.ShipGroupTransfer(Race_0.Name, Race_1.Name, c.ShipGroup(0).ID))
|
||||
var s *game.ShipType
|
||||
for st := range c.ListShipTypes(Race_1_idx) {
|
||||
if strings.HasPrefix(st.Name, originalName) && st.Name != originalName {
|
||||
s = st
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, s)
|
||||
assert.Greater(t, len(s.Name), len(originalName))
|
||||
}
|
||||
|
||||
func TestShipGroupLoad(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
// 1: idx = 0 / Ready to load
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
|
||||
// 2: idx = 1 / Has no cargo bay
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
|
||||
// 3: idx = 2 / In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(2).StateInSpace = &InSpace
|
||||
|
||||
// 4: idx = 3 / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(3).Load = 1.234
|
||||
|
||||
// 5: idx = 4 / on foreign planet
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(4).Destination = R1_Planet_1_num
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 5)
|
||||
|
||||
// tests
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(UnknownRace, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_Extinct.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, "GOLD", 0),
|
||||
e.GenericErrorText(e.ErrInputCargoTypeInvalid))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, uuid.New(), game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(2).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(4).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(1).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputNoCargoBay))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(3).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputCargoLoadNotEqual))
|
||||
|
||||
// initial planet is empty
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputCargoLoadNotEnough))
|
||||
// add cargo to planet
|
||||
c.PutMaterial(R0_Planet_0_num, 100)
|
||||
// not enough on the planet
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 101),
|
||||
e.GenericErrorText(e.ErrInputCargoLoadNotEnough))
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 5)
|
||||
|
||||
// load maximum
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0))
|
||||
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType)
|
||||
assert.Equal(t, 100.0, c.ShipGroup(0).Load.F())
|
||||
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 0))
|
||||
|
||||
// load limited
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 18))
|
||||
assert.Equal(t, 82.0, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType)
|
||||
assert.Equal(t, 18.0, c.ShipGroup(0).Load.F())
|
||||
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 0))
|
||||
|
||||
// add cargo to planet
|
||||
c.PutMaterial(R0_Planet_0_num, 100)
|
||||
// loading all available cargo
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0))
|
||||
assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.Equal(t, 100.0, c.ShipGroup(0).Load.F()) // free: 131.0
|
||||
assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType)
|
||||
|
||||
// add cargo to planet
|
||||
c.PutMaterial(R0_Planet_0_num, 200)
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 31))
|
||||
assert.Equal(t, 169.0, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.Equal(t, 131.0, c.ShipGroup(0).Load.F()) // free: 100.0
|
||||
assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType)
|
||||
|
||||
// load to maximum cargo space left
|
||||
assert.NoError(t, g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0))
|
||||
assert.Equal(t, 69.0, c.MustPlanet(R0_Planet_0_num).Material.F())
|
||||
assert.Equal(t, 231.0, c.ShipGroup(0).Load.F()) // free: 0.0
|
||||
assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType)
|
||||
|
||||
// ship group is full
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrInputCargoLoadNoSpaceLeft))
|
||||
}
|
||||
|
||||
func TestShipGroupUnload(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
// 1: idx = 0 / empty
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
|
||||
// 2: idx = 1 / Has no cargo bay
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1))
|
||||
|
||||
// 3: idx = 2 / In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(2).StateInSpace = &InSpace
|
||||
|
||||
// 4: idx = 3 / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(3).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(3).Load = 1.234
|
||||
|
||||
// 5: idx = 4 / on foreign planet / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(4).Destination = R1_Planet_1_num
|
||||
c.ShipGroup(4).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(4).Load = 1.234
|
||||
|
||||
// 6: idx = 5 / on foreign planet / loaded with MAT
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(5).Destination = R1_Planet_1_num
|
||||
c.ShipGroup(5).CargoType = game.CargoMaterial.Ref()
|
||||
c.ShipGroup(5).Load = 100.0
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 6)
|
||||
|
||||
// tests
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(UnknownRace, c.ShipGroup(0).ID, 0),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_Extinct.Name, c.ShipGroup(0).ID, 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, uuid.New(), 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, c.ShipGroup(2).ID, 0),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, c.ShipGroup(1).ID, 0),
|
||||
e.GenericErrorText(e.ErrInputNoCargoBay))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 0),
|
||||
e.GenericErrorText(e.ErrInputCargoUnloadEmpty))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, c.ShipGroup(4).ID, 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 6)
|
||||
|
||||
// unload MAT on foreign planet / limited
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(5).ID, 20.1))
|
||||
assert.Equal(t, 20.1, util.Fixed3(c.MustPlanet(R1_Planet_1_num).Material.F()))
|
||||
assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(5).CargoType)
|
||||
assert.Equal(t, 79.9, util.Fixed3(c.ShipGroup(5).Load.F()))
|
||||
|
||||
// unload MAT on foreign planet / ALL
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(5).ID, 0))
|
||||
assert.Equal(t, 100.0, util.Fixed3(c.MustPlanet(R1_Planet_1_num).Material.F()))
|
||||
assert.Equal(t, 0.0, util.Fixed3(c.ShipGroup(5).Load.F()))
|
||||
assert.Nil(t, c.ShipGroup(5).CargoType)
|
||||
|
||||
// unload ALL
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 100
|
||||
assert.NoError(t, g.ShipGroupUnload(Race_0.Name, c.ShipGroup(0).ID, 101))
|
||||
assert.Equal(t, 100.0, util.Fixed3(c.MustPlanet(R0_Planet_0_num).Colonists.F()))
|
||||
assert.Equal(t, 0.0, util.Fixed3(c.ShipGroup(0).Load.F()))
|
||||
assert.Nil(t, c.ShipGroup(0).CargoType)
|
||||
}
|
||||
|
||||
func TestShipGroupDismantle(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
// 1: idx = 0 / empty
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
|
||||
// 2: idx = 1 / In_Space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
|
||||
c.ShipGroup(1).StateInSpace = &InSpace
|
||||
|
||||
// 3: idx = 2 / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
c.ShipGroup(2).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(2).Load = 80.0
|
||||
|
||||
// 4: idx = 3 / on foreign planet / loaded with MAT
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(3).Destination = R1_Planet_1_num
|
||||
c.ShipGroup(3).CargoType = game.CargoMaterial.Ref()
|
||||
c.ShipGroup(3).Load = 100.0
|
||||
|
||||
// 5: idx = 4 / on foreign planet / loaded with COL
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
|
||||
c.ShipGroup(4).Destination = R1_Planet_1_num
|
||||
c.ShipGroup(4).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(4).Load = 2.345
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 5)
|
||||
|
||||
// tests
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupDismantle(UnknownRace, c.ShipGroup(0).ID),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupDismantle(Race_Extinct.Name, c.ShipGroup(0).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupDismantle(Race_0.Name, uuid.New()),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupDismantle(Race_0.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
|
||||
groupEmptyMass := c.ShipGroup(4).EmptyMass(c.MustShipClass(Race_0_idx, Race_0_Freighter))
|
||||
planetMAT := c.MustPlanet(R1_Planet_1_num).Material.F()
|
||||
planetCOL := c.MustPlanet(R1_Planet_1_num).Colonists.F()
|
||||
|
||||
assert.NoError(t, g.ShipGroupDismantle(Race_0.Name, c.ShipGroup(4).ID))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 4)
|
||||
assert.Equal(t, planetMAT+groupEmptyMass, c.MustPlanet(R1_Planet_1_num).Material.F())
|
||||
assert.Equal(t, planetCOL, c.MustPlanet(R1_Planet_1_num).Colonists.F())
|
||||
|
||||
groupEmptyMass = c.ShipGroup(3).EmptyMass(c.MustShipClass(Race_0_idx, Race_0_Freighter))
|
||||
groupLoadMAT := c.ShipGroup(3).Load.F()
|
||||
planetMAT = c.MustPlanet(R1_Planet_1_num).Material.F()
|
||||
assert.NoError(t, g.ShipGroupDismantle(Race_0.Name, c.ShipGroup(3).ID))
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3)
|
||||
assert.Equal(t, planetMAT+groupEmptyMass+groupLoadMAT, c.MustPlanet(R1_Planet_1_num).Material.F())
|
||||
}
|
||||
|
||||
func TestShipGroupDestroyItem(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
|
||||
c.ShipGroup(0).CargoType = game.CargoColonist.Ref()
|
||||
c.ShipGroup(0).Load = 100.0
|
||||
|
||||
for c.ShipGroup(0).Number > 0 {
|
||||
c.ShipGroupDestroyItem(0)
|
||||
assert.Equal(t, float64(c.ShipGroup(0).Number)*10, c.ShipGroup(0).Load.F())
|
||||
}
|
||||
}
|
||||
|
||||
func TestState(t *testing.T) {
|
||||
assert.Equal(t, "In_Orbit", fmt.Sprintf("%s", game.StateInOrbit))
|
||||
}
|
||||
|
||||
func TestUnsafeDeleteShipGroup(t *testing.T) {
|
||||
c, g := newCache()
|
||||
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 0
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 5)) // 1
|
||||
assert.NoError(t, g.ShipGroupJoinFleet(Race_0.Name, "Fleet", c.ShipGroup(0).ID))
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_2_num, 7)) // 2
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 3)
|
||||
|
||||
c.UnsafeDeleteShipGroup(1)
|
||||
|
||||
assert.Len(t, slices.Collect(c.RaceShipGroups(Race_0_idx)), 2)
|
||||
assert.Equal(t, uint(3), c.ShipGroup(0).Number)
|
||||
assert.Equal(t, uint(7), c.ShipGroup(1).Number)
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func (c *Cache) shipGroupUpgrade(ri int, groupID uuid.UUID, techInput string, limitLevel float64) error {
|
||||
c.validateRaceIndex(ri)
|
||||
sgi, ok := c.raceShipGroupIndex(ri, groupID)
|
||||
if !ok {
|
||||
return e.NewEntityNotExistsError("group %s", groupID)
|
||||
}
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
sg := c.ShipGroup(sgi)
|
||||
|
||||
if state := sg.State(); state != game.StateInOrbit {
|
||||
return e.NewShipsBusyError("state: %s", state)
|
||||
}
|
||||
|
||||
p := c.MustPlanet(sg.Destination)
|
||||
if p.Owned() && !p.OwnedBy(c.g.Race[ri].ID) {
|
||||
return e.NewEntityNotOwnedError("planet #%d for upgrade group %s", p.Number, groupID)
|
||||
}
|
||||
|
||||
upgradeValidTech := map[string]game.Tech{
|
||||
strings.ToLower(game.TechDrive.String()): game.TechDrive,
|
||||
strings.ToLower(game.TechWeapons.String()): game.TechWeapons,
|
||||
strings.ToLower(game.TechShields.String()): game.TechShields,
|
||||
strings.ToLower(game.TechCargo.String()): game.TechCargo,
|
||||
strings.ToLower(game.TechAll.String()): game.TechAll,
|
||||
}
|
||||
|
||||
techRequest, ok := upgradeValidTech[strings.ToLower(techInput)]
|
||||
if !ok {
|
||||
return e.NewTechUnknownError(techInput)
|
||||
}
|
||||
|
||||
var blockMasses map[game.Tech]float64 = map[game.Tech]float64{
|
||||
game.TechDrive: st.DriveBlockMass(),
|
||||
game.TechWeapons: st.WeaponsBlockMass(),
|
||||
game.TechShields: st.ShieldsBlockMass(),
|
||||
game.TechCargo: st.CargoBlockMass(),
|
||||
}
|
||||
|
||||
switch {
|
||||
case techRequest != game.TechAll && blockMasses[techRequest] == 0:
|
||||
return e.NewUpgradeShipTechNotUsedError()
|
||||
case techRequest == game.TechAll && limitLevel != 0:
|
||||
return e.NewUpgradeParameterNotAllowedError("tech=%s max_level=%f", techRequest.String(), limitLevel)
|
||||
}
|
||||
|
||||
targetLevel := make(map[game.Tech]float64)
|
||||
var sumLevels float64
|
||||
for _, tech := range []game.Tech{game.TechDrive, game.TechWeapons, game.TechShields, game.TechCargo} {
|
||||
if techRequest == game.TechAll || tech == techRequest {
|
||||
if c.g.Race[ri].TechLevel(tech) < limitLevel {
|
||||
return e.NewUpgradeTechLevelInsufficientError("%s=%.03f < %.03f", tech.String(), c.g.Race[ri].TechLevel(tech), limitLevel)
|
||||
}
|
||||
targetLevel[tech] = FutureUpgradeLevel(c.g.Race[ri].TechLevel(tech), sg.TechLevel(tech).F(), limitLevel)
|
||||
} else {
|
||||
targetLevel[tech] = CurrentUpgradingLevel(sg, tech)
|
||||
}
|
||||
sumLevels += targetLevel[tech]
|
||||
}
|
||||
|
||||
productionCapacity := c.PlanetProductionCapacity(p.Number)
|
||||
uc := GroupUpgradeCost(sg, *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo])
|
||||
costForShip := uc.UpgradeCost(1)
|
||||
if costForShip == 0 {
|
||||
return e.NewUpgradeShipsAlreadyUpToDateError("%#v", targetLevel)
|
||||
}
|
||||
|
||||
shipsToUpgrade := sg.Number
|
||||
maxUpgradableShips := uc.UpgradeMaxShips(productionCapacity)
|
||||
|
||||
/*
|
||||
1. считаем стоимость модернизации одного корабля
|
||||
2. считаем сколько кораблей можно модернизировать
|
||||
3. если не хватает даже на 1 корабль, ограничиваемся одним кораблём и пересчитываем коэффициент пропорционально массе блоков
|
||||
4. иначе, считаем истинное количество кораблей с учётом ограничения maxShips
|
||||
*/
|
||||
blockMassSum := st.EmptyMass()
|
||||
|
||||
coef := productionCapacity / costForShip
|
||||
if maxUpgradableShips == 0 {
|
||||
if limitLevel > 0 {
|
||||
return e.NewUpgradeInsufficientResourcesError("ship cost=%.03f L=%.03f", costForShip, productionCapacity)
|
||||
}
|
||||
sumLevels = sumLevels * coef
|
||||
for tech := range targetLevel {
|
||||
if blockMasses[tech] > 0 {
|
||||
proportional := sumLevels * (blockMasses[tech] / blockMassSum)
|
||||
targetLevel[tech] = proportional
|
||||
}
|
||||
}
|
||||
maxUpgradableShips = 1
|
||||
} else if maxUpgradableShips > shipsToUpgrade {
|
||||
maxUpgradableShips = shipsToUpgrade
|
||||
}
|
||||
|
||||
// sanity check
|
||||
uc = GroupUpgradeCost(sg, *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo])
|
||||
costForGroup := uc.UpgradeCost(maxUpgradableShips)
|
||||
if costForGroup > productionCapacity {
|
||||
e.NewGameStateError("cost recalculation: coef=%f cost(%d)=%f L=%f", coef, maxUpgradableShips, costForGroup, productionCapacity)
|
||||
}
|
||||
|
||||
// break group if needed
|
||||
if maxUpgradableShips < sg.Number {
|
||||
nsgi, err := c.breakGroup(ri, groupID, maxUpgradableShips)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sgi = nsgi
|
||||
}
|
||||
|
||||
// finally, fill group upgrade prefs
|
||||
for tech := range targetLevel {
|
||||
if targetLevel[tech] > 0 {
|
||||
c.UpgradeShipGroup(sgi, tech, targetLevel[tech])
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) UpgradeShipGroup(sgi int, tech game.Tech, v float64) {
|
||||
sg := *(c.ShipGroup(sgi))
|
||||
st := c.ShipGroupShipClass(sgi)
|
||||
c.g.ShipGroups[sgi] = UpgradeGroupPreference(sg, *st, tech, v)
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
type UpgradeCalc struct {
|
||||
Cost map[game.Tech]float64
|
||||
}
|
||||
|
||||
func (uc UpgradeCalc) UpgradeCost(ships uint) float64 {
|
||||
var sum float64
|
||||
for _, v := range uc.Cost {
|
||||
sum += v
|
||||
}
|
||||
return sum * float64(ships)
|
||||
}
|
||||
|
||||
func (uc UpgradeCalc) UpgradeMaxShips(resources float64) uint {
|
||||
return uint(math.Floor(resources / uc.UpgradeCost(1)))
|
||||
}
|
||||
|
||||
func BlockUpgradeCost(blockMass, currentBlockTech, targetBlockTech float64) float64 {
|
||||
if blockMass == 0 || targetBlockTech <= currentBlockTech {
|
||||
return 0
|
||||
}
|
||||
return (1 - currentBlockTech/targetBlockTech) * 10 * blockMass
|
||||
}
|
||||
|
||||
func GroupUpgradeCost(sg *game.ShipGroup, st game.ShipType, drive, weapons, shields, cargo float64) UpgradeCalc {
|
||||
uc := &UpgradeCalc{Cost: make(map[game.Tech]float64)}
|
||||
if drive > 0 {
|
||||
uc.Cost[game.TechDrive] = BlockUpgradeCost(st.DriveBlockMass(), sg.TechLevel(game.TechDrive).F(), drive)
|
||||
}
|
||||
if weapons > 0 {
|
||||
uc.Cost[game.TechWeapons] = BlockUpgradeCost(st.WeaponsBlockMass(), sg.TechLevel(game.TechWeapons).F(), weapons)
|
||||
}
|
||||
if shields > 0 {
|
||||
uc.Cost[game.TechShields] = BlockUpgradeCost(st.ShieldsBlockMass(), sg.TechLevel(game.TechShields).F(), shields)
|
||||
}
|
||||
if cargo > 0 {
|
||||
uc.Cost[game.TechCargo] = BlockUpgradeCost(st.CargoBlockMass(), sg.TechLevel(game.TechCargo).F(), cargo)
|
||||
}
|
||||
return *uc
|
||||
}
|
||||
|
||||
func CurrentUpgradingLevel(sg *game.ShipGroup, tech game.Tech) float64 {
|
||||
if sg.StateUpgrade == nil {
|
||||
return 0
|
||||
}
|
||||
ti := slices.IndexFunc(sg.StateUpgrade.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech })
|
||||
if ti >= 0 {
|
||||
return sg.StateUpgrade.UpgradeTech[ti].Level.F()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func FutureUpgradeLevel(raceLevel, groupLevel, limit float64) float64 {
|
||||
target := limit
|
||||
if target == 0 || target > raceLevel {
|
||||
target = raceLevel
|
||||
}
|
||||
if groupLevel == target {
|
||||
return 0
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func UpgradeGroupPreference(sg game.ShipGroup, st game.ShipType, tech game.Tech, v float64) game.ShipGroup {
|
||||
if v <= 0 || st.BlockMass(tech) == 0 || sg.TechLevel(tech).F() >= v {
|
||||
return sg
|
||||
}
|
||||
var su game.InUpgrade
|
||||
if sg.StateUpgrade != nil {
|
||||
su = *sg.StateUpgrade
|
||||
} else {
|
||||
su = game.InUpgrade{UpgradeTech: []game.UpgradePreference{}}
|
||||
}
|
||||
ti := slices.IndexFunc(su.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech })
|
||||
if ti < 0 {
|
||||
su.UpgradeTech = append(su.UpgradeTech, game.UpgradePreference{Tech: tech})
|
||||
ti = len(su.UpgradeTech) - 1
|
||||
}
|
||||
su.UpgradeTech[ti].Level = game.F(v)
|
||||
su.UpgradeTech[ti].Cost = game.F(BlockUpgradeCost(st.BlockMass(tech), sg.TechLevel(tech).F(), v) * float64(sg.Number))
|
||||
|
||||
sg.StateUpgrade = &su
|
||||
return sg
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
e "galaxy/error"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
g "galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBlockUpgradeCost(t *testing.T) {
|
||||
assert.Equal(t, 00.0, controller.BlockUpgradeCost(1, 1.0, 1.0))
|
||||
assert.Equal(t, 25.0, controller.BlockUpgradeCost(5, 1.0, 2.0))
|
||||
assert.Equal(t, 50.0, controller.BlockUpgradeCost(10, 1.0, 2.0))
|
||||
}
|
||||
|
||||
func TestGroupUpgradeCost(t *testing.T) {
|
||||
sg := &g.ShipGroup{
|
||||
Tech: map[g.Tech]g.Float{
|
||||
g.TechDrive: 1.0,
|
||||
g.TechWeapons: 1.0,
|
||||
g.TechShields: 1.0,
|
||||
g.TechCargo: 1.0,
|
||||
},
|
||||
Number: 1,
|
||||
}
|
||||
assert.Equal(t, 225.0, controller.GroupUpgradeCost(sg, Cruiser, 2.0, 2.0, 2.0, 2.0).UpgradeCost(1))
|
||||
}
|
||||
|
||||
func TestUpgradeMaxShips(t *testing.T) {
|
||||
sg := &g.ShipGroup{
|
||||
Tech: map[g.Tech]g.Float{
|
||||
g.TechDrive: 1.0,
|
||||
g.TechWeapons: 1.0,
|
||||
g.TechShields: 1.0,
|
||||
g.TechCargo: 1.0,
|
||||
},
|
||||
Number: 10,
|
||||
}
|
||||
uc := controller.GroupUpgradeCost(sg, Cruiser, 2.0, 2.0, 2.0, 2.0)
|
||||
assert.Equal(t, uint(4), uc.UpgradeMaxShips(1000))
|
||||
}
|
||||
|
||||
func TestCurrentUpgradingLevel(t *testing.T) {
|
||||
sg := &g.ShipGroup{
|
||||
StateUpgrade: nil,
|
||||
}
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechDrive))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechWeapons))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechShields))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechCargo))
|
||||
|
||||
sg.StateUpgrade = &g.InUpgrade{
|
||||
UpgradeTech: []g.UpgradePreference{
|
||||
{Tech: g.TechDrive, Level: 1.5, Cost: 100.1},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, 1.5, controller.CurrentUpgradingLevel(sg, g.TechDrive))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechWeapons))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechShields))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechCargo))
|
||||
|
||||
sg.StateUpgrade.UpgradeTech = append(sg.StateUpgrade.UpgradeTech, g.UpgradePreference{Tech: g.TechCargo, Level: 2.2, Cost: 200.2})
|
||||
assert.Equal(t, 1.5, controller.CurrentUpgradingLevel(sg, g.TechDrive))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechWeapons))
|
||||
assert.Equal(t, 0.0, controller.CurrentUpgradingLevel(sg, g.TechShields))
|
||||
assert.Equal(t, 2.2, controller.CurrentUpgradingLevel(sg, g.TechCargo))
|
||||
}
|
||||
|
||||
func TestFutureUpgradeLevel(t *testing.T) {
|
||||
assert.Equal(t, 0.0, controller.FutureUpgradeLevel(2.0, 2.0, 2.0))
|
||||
assert.Equal(t, 0.0, controller.FutureUpgradeLevel(2.0, 2.0, 3.0))
|
||||
assert.Equal(t, 1.5, controller.FutureUpgradeLevel(1.5, 2.0, 3.0))
|
||||
assert.Equal(t, 2.0, controller.FutureUpgradeLevel(2.5, 1.0, 2.0))
|
||||
assert.Equal(t, 2.5, controller.FutureUpgradeLevel(2.5, 1.0, 0.0))
|
||||
}
|
||||
|
||||
func TestUpgradeGroupPreference(t *testing.T) {
|
||||
sg := g.ShipGroup{
|
||||
Number: 4,
|
||||
Tech: g.TechSet{
|
||||
g.TechDrive: 1.0,
|
||||
g.TechWeapons: 1.0,
|
||||
g.TechShields: 1.0,
|
||||
g.TechCargo: 1.0,
|
||||
},
|
||||
}
|
||||
assert.Nil(t, sg.StateUpgrade)
|
||||
sg = controller.UpgradeGroupPreference(sg, Cruiser, g.TechDrive, 0)
|
||||
assert.Nil(t, sg.StateUpgrade)
|
||||
|
||||
sg = controller.UpgradeGroupPreference(sg, Cruiser, g.TechDrive, 2.0)
|
||||
assert.NotNil(t, sg.StateUpgrade)
|
||||
assert.Equal(t, 300., sg.StateUpgrade.TechCost(g.TechDrive))
|
||||
assert.Equal(t, 300., sg.StateUpgrade.Cost())
|
||||
|
||||
sg = controller.UpgradeGroupPreference(sg, Cruiser, g.TechWeapons, 2.0)
|
||||
assert.NotNil(t, sg.StateUpgrade)
|
||||
assert.Equal(t, 300., sg.StateUpgrade.TechCost(g.TechWeapons))
|
||||
assert.Equal(t, 600., sg.StateUpgrade.Cost())
|
||||
|
||||
sg = controller.UpgradeGroupPreference(sg, Cruiser, g.TechShields, 2.0)
|
||||
assert.NotNil(t, sg.StateUpgrade)
|
||||
assert.Equal(t, 300., sg.StateUpgrade.TechCost(g.TechShields))
|
||||
assert.Equal(t, 900., sg.StateUpgrade.Cost())
|
||||
|
||||
sg = controller.UpgradeGroupPreference(sg, Cruiser, g.TechCargo, 2.0)
|
||||
assert.NotNil(t, sg.StateUpgrade)
|
||||
assert.Equal(t, 0., sg.StateUpgrade.TechCost(g.TechCargo))
|
||||
assert.Equal(t, 900., sg.StateUpgrade.Cost())
|
||||
}
|
||||
|
||||
func TestShipGroupUpgrade(t *testing.T) {
|
||||
c, g := newCache()
|
||||
// group #1 - in_orbit, free to upgrade
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 10))
|
||||
// group #2 - in_space
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
|
||||
c.ShipGroup(1).StateInSpace = &InSpace
|
||||
// group #3 - in_orbit, foreign planet
|
||||
assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
|
||||
c.ShipGroup(2).Destination = R1_Planet_1_num
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(UnknownRace, c.ShipGroup(0).ID, "DRIVE", 0),
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_Extinct.Name, c.ShipGroup(0).ID, "DRIVE", 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, uuid.New(), "DRIVE", 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(1).ID, "DRIVE", 0),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(2).ID, "DRIVE", 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotOwned))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "GUN", 0),
|
||||
e.GenericErrorText(e.ErrInputTechUnknown))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "CARGO", 0),
|
||||
e.GenericErrorText(e.ErrInputUpgradeShipTechNotUsed))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "ALL", 2.0),
|
||||
e.GenericErrorText(e.ErrInputUpgradeParameterNotAllowed))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "DRIVE", 2.0),
|
||||
e.GenericErrorText(e.ErrInputUpgradeTechLevelInsufficient))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "DRIVE", 1.1),
|
||||
e.GenericErrorText(e.ErrInputUpgradeShipsAlreadyUpToDate))
|
||||
|
||||
c.RaceTechLevel(Race_0_idx, game.TechDrive, 10.0)
|
||||
assert.Equal(t, 10.0, c.Race(Race_0_idx).TechLevel(game.TechDrive))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "DRIVE", 10.0),
|
||||
e.GenericErrorText(e.ErrUpgradeInsufficientResources))
|
||||
|
||||
assert.NoError(t, g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(0).ID, "DRIVE", 1.3))
|
||||
assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State())
|
||||
assert.Equal(t, uint(6), c.ShipGroup(0).Number)
|
||||
assert.Equal(t, game.StateUpgrade, c.ShipGroup(3).State())
|
||||
assert.Equal(t, uint(4), c.ShipGroup(3).Number)
|
||||
assert.NotNil(t, c.ShipGroup(3).StateUpgrade)
|
||||
assert.Equal(t, 1.3, c.ShipGroup(3).StateUpgrade.UpgradeTech[0].Level.F())
|
||||
assert.Equal(t, "DRIVE", c.ShipGroup(3).StateUpgrade.UpgradeTech[0].Tech.String())
|
||||
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUpgrade(Race_0.Name, c.ShipGroup(3).ID, "DRIVE", 1.3),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math/big"
|
||||
"slices"
|
||||
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type VoteGroup struct {
|
||||
RaceIndex []int
|
||||
Sum float64
|
||||
}
|
||||
|
||||
type VoteNode struct {
|
||||
ID int
|
||||
Ally bool
|
||||
Next *VoteNode
|
||||
}
|
||||
|
||||
func (n VoteNode) String() string {
|
||||
lh, rh := " ", "."
|
||||
if n.Ally {
|
||||
lh, rh = "{", "}"
|
||||
}
|
||||
return fmt.Sprintf("%s%d%s", lh, n.ID, rh)
|
||||
}
|
||||
|
||||
func (c *Cache) TurnAcceptWinners(v []int) {
|
||||
if c.g.Finished() {
|
||||
panic("game is already has its winner(s)")
|
||||
}
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
for _, ri := range v {
|
||||
c.g.Winner = append(c.g.Winner, c.g.Race[ri].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) TurnCalculateVotes() []int {
|
||||
raceVotes := c.votesByRace()
|
||||
calc := GroupVotes(raceVotes, VotingGraph(c.g.Race, c.RaceIndex))
|
||||
|
||||
c.g.Votes = 0
|
||||
for ri, votes := range raceVotes {
|
||||
v := game.F(votes)
|
||||
c.g.Race[ri].Votes = v
|
||||
c.g.Votes += v
|
||||
}
|
||||
|
||||
return votingWinners(calc, c.g.Votes.F())
|
||||
}
|
||||
|
||||
func VotingGraph(races []game.Race, raceIndex func(uuid.UUID) int) map[int]*VoteNode {
|
||||
nodes := make(map[int]*VoteNode, len(races))
|
||||
for ri := range races {
|
||||
if races[ri].Extinct {
|
||||
continue
|
||||
}
|
||||
r := &races[ri]
|
||||
if _, ok := nodes[ri]; !ok {
|
||||
nodes[ri] = &VoteNode{
|
||||
ID: ri,
|
||||
}
|
||||
}
|
||||
if r.VoteFor != r.ID {
|
||||
vid := raceIndex(r.VoteFor)
|
||||
if !races[vid].Extinct {
|
||||
if _, ok := nodes[vid]; !ok {
|
||||
nodes[vid] = &VoteNode{
|
||||
ID: vid,
|
||||
}
|
||||
}
|
||||
nodes[ri].Next = nodes[vid]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (c *Cache) votesByRace() map[int]float64 {
|
||||
result := make(map[int]float64)
|
||||
for i := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[i]
|
||||
if !p.Owned() {
|
||||
continue
|
||||
}
|
||||
ri := c.RaceIndex(*p.Owner)
|
||||
planetVotes := p.Votes()
|
||||
result[ri] += planetVotes
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GroupVotes(raceVotes map[int]float64, nodes map[int]*VoteNode) []*VoteGroup {
|
||||
votes := maps.Clone(raceVotes)
|
||||
result := make([]*VoteGroup, 0)
|
||||
chains := VotingChains(nodes)
|
||||
chainingRaces := make(map[int]bool)
|
||||
|
||||
for i := range chains {
|
||||
chain := chains[i]
|
||||
if len(chain) == 0 {
|
||||
panic("voters chain is empty")
|
||||
}
|
||||
vg := &VoteGroup{}
|
||||
for j := range chain {
|
||||
node := &chain[j]
|
||||
if node.Ally || j == len(chain)-1 {
|
||||
vg.RaceIndex = append(vg.RaceIndex, node.ID)
|
||||
}
|
||||
vg.Sum += votes[node.ID]
|
||||
votes[node.ID] = 0
|
||||
chainingRaces[node.ID] = true
|
||||
}
|
||||
// find a non-ally group (single race) which already have its votes and merge with a new VoteGroup instead of adding to result
|
||||
if i := slices.IndexFunc(result, func(v *VoteGroup) bool { return len(v.RaceIndex) == 1 && v.RaceIndex[0] == vg.RaceIndex[0] }); i >= 0 && len(vg.RaceIndex) == 1 {
|
||||
result[i].Sum += vg.Sum
|
||||
} else {
|
||||
result = append(result, vg)
|
||||
}
|
||||
}
|
||||
|
||||
for ri, votes := range votes {
|
||||
if _, ok := chainingRaces[ri]; !ok && votes > 0 {
|
||||
result = append(result, &VoteGroup{RaceIndex: []int{ri}, Sum: votes})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func VotingChains(nodes map[int]*VoteNode) [][]VoteNode {
|
||||
visited := make(map[int]bool)
|
||||
result := make([][]VoteNode, 0)
|
||||
raceIds := slices.Collect(maps.Keys(nodes))
|
||||
slices.Sort(raceIds)
|
||||
for _, rid := range raceIds {
|
||||
n := nodes[rid]
|
||||
if v, ok := visited[n.ID]; (ok && v) || n.Next == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
slow, fast := n, n
|
||||
cycled := false
|
||||
var cycleBound *VoteNode
|
||||
for slow != nil && fast != nil && fast.Next != nil {
|
||||
slow = slow.Next
|
||||
fast = fast.Next.Next
|
||||
|
||||
if slow == fast {
|
||||
slow = n
|
||||
|
||||
for slow != fast {
|
||||
slow = slow.Next
|
||||
fast = fast.Next
|
||||
}
|
||||
|
||||
cycled = true
|
||||
cycleBound = slow
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var current *VoteNode
|
||||
if cycled && !visited[slow.ID] {
|
||||
result = append(result, make([]VoteNode, 0))
|
||||
|
||||
result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: slow.ID, Ally: true})
|
||||
visited[slow.ID] = true
|
||||
|
||||
current = slow.Next
|
||||
|
||||
for current != slow {
|
||||
visited[current.ID] = true
|
||||
result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: current.ID, Ally: true})
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
if n == slow {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
current = n
|
||||
var finish *VoteNode
|
||||
if cycleBound != nil {
|
||||
if cycleBound == current.Next {
|
||||
finish = current
|
||||
} else {
|
||||
finish = cycleBound
|
||||
}
|
||||
} else {
|
||||
finish = nil
|
||||
}
|
||||
|
||||
if finish != current {
|
||||
result = append(result, make([]VoteNode, 0))
|
||||
}
|
||||
|
||||
for current != finish {
|
||||
visited[current.ID] = current.ID != n.ID && current.Next != nil
|
||||
result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: current.ID, Ally: false})
|
||||
current = current.Next
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func votingWinners(calc []*VoteGroup, sumVotes float64) []int {
|
||||
slices.SortFunc(calc, func(a, b *VoteGroup) int { return cmp.Compare(b.Sum, a.Sum) })
|
||||
|
||||
topVoter := calc[0]
|
||||
maxVotes := &big.Rat{}
|
||||
maxVotes.SetFloat64(topVoter.Sum)
|
||||
|
||||
winVotes := &big.Rat{}
|
||||
winVotes.SetFloat64(sumVotes)
|
||||
winVotes = winVotes.Mul(winVotes, big.NewRat(2, 3))
|
||||
|
||||
if maxVotes.Cmp(winVotes) >= 0 {
|
||||
return topVoter.RaceIndex
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package controller_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"galaxy/game/internal/controller"
|
||||
"galaxy/game/internal/model/game"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVotesByRace(t *testing.T) {
|
||||
c, _ := newCache()
|
||||
|
||||
c.MustPlanet(R0_Planet_0_num).Size = 450.
|
||||
c.MustPlanet(R0_Planet_0_num).Population = 450.
|
||||
|
||||
c.MustPlanet(R1_Planet_1_num).Size = 900.
|
||||
c.MustPlanet(R1_Planet_1_num).Population = 900.
|
||||
|
||||
c.MustPlanet(R0_Planet_2_num).Size = 330.
|
||||
c.MustPlanet(R0_Planet_2_num).Population = 330.
|
||||
|
||||
vbr := c.VotesByRace()
|
||||
assert.Len(t, vbr, 2)
|
||||
assert.Contains(t, vbr, Race_0_idx)
|
||||
assert.Equal(t, 0.78, vbr[Race_0_idx])
|
||||
assert.Contains(t, vbr, Race_1_idx)
|
||||
assert.Equal(t, 0.9, vbr[Race_1_idx])
|
||||
}
|
||||
|
||||
func prepareRaces() ([]game.Race, func(u uuid.UUID) int) {
|
||||
races := make([]game.Race, 20)
|
||||
raceIndex := make(map[uuid.UUID]int)
|
||||
for i := range len(races) {
|
||||
races[i].ID = uuid.New()
|
||||
races[i].VoteFor = races[i].ID
|
||||
raceIndex[races[i].ID] = i
|
||||
}
|
||||
// 0 -> 1 -> 2 -> 3
|
||||
// ^ |
|
||||
// 5 -> 6 -> 4 <--'
|
||||
races[0].VoteFor = races[1].ID
|
||||
races[1].VoteFor = races[2].ID
|
||||
races[2].VoteFor = races[3].ID
|
||||
races[3].VoteFor = races[4].ID
|
||||
races[4].VoteFor = races[2].ID
|
||||
races[5].VoteFor = races[6].ID
|
||||
races[6].VoteFor = races[4].ID
|
||||
// 7 -> 10 -> 11
|
||||
// ^
|
||||
// 8 -> 9
|
||||
races[7].VoteFor = races[10].ID
|
||||
races[10].VoteFor = races[11].ID
|
||||
races[8].VoteFor = races[9].ID
|
||||
races[9].VoteFor = races[10].ID
|
||||
// 12 -> 13
|
||||
// 13 -> 12
|
||||
races[12].VoteFor = races[13].ID
|
||||
races[13].VoteFor = races[12].ID
|
||||
// 14 -> 15 -> 16
|
||||
// ^ |
|
||||
// 17 <--'
|
||||
races[14].VoteFor = races[15].ID
|
||||
races[15].VoteFor = races[16].ID
|
||||
races[16].VoteFor = races[17].ID
|
||||
races[17].VoteFor = races[15].ID
|
||||
// 19 -> 13
|
||||
races[19].VoteFor = races[13].ID
|
||||
|
||||
return races, func(u uuid.UUID) int { return raceIndex[u] }
|
||||
}
|
||||
|
||||
func TestVotingGraph(t *testing.T) {
|
||||
races, raceIndex := prepareRaces()
|
||||
|
||||
voteNodes := controller.VotingGraph(races, raceIndex)
|
||||
|
||||
assert.Len(t, voteNodes, len(races))
|
||||
for i := range voteNodes {
|
||||
n := voteNodes[i]
|
||||
switch i {
|
||||
case 0:
|
||||
assert.Equal(t, voteNodes[1], n.Next)
|
||||
case 1:
|
||||
assert.Equal(t, voteNodes[2], n.Next)
|
||||
case 2:
|
||||
assert.Equal(t, voteNodes[3], n.Next)
|
||||
case 3:
|
||||
assert.Equal(t, voteNodes[4], n.Next)
|
||||
case 4:
|
||||
assert.Equal(t, voteNodes[2], n.Next)
|
||||
case 5:
|
||||
assert.Equal(t, voteNodes[6], n.Next)
|
||||
case 6:
|
||||
assert.Equal(t, voteNodes[4], n.Next)
|
||||
case 7:
|
||||
assert.Equal(t, voteNodes[10], n.Next)
|
||||
case 8:
|
||||
assert.Equal(t, voteNodes[9], n.Next)
|
||||
case 9:
|
||||
assert.Equal(t, voteNodes[10], n.Next)
|
||||
case 10:
|
||||
assert.Equal(t, voteNodes[11], n.Next)
|
||||
case 11:
|
||||
assert.Nil(t, n.Next)
|
||||
case 12:
|
||||
assert.Equal(t, voteNodes[13], n.Next)
|
||||
case 13:
|
||||
assert.Equal(t, voteNodes[12], n.Next)
|
||||
case 14:
|
||||
assert.Equal(t, voteNodes[15], n.Next)
|
||||
case 15:
|
||||
assert.Equal(t, voteNodes[16], n.Next)
|
||||
case 16:
|
||||
assert.Equal(t, voteNodes[17], n.Next)
|
||||
case 17:
|
||||
assert.Equal(t, voteNodes[15], n.Next)
|
||||
case 18:
|
||||
assert.Nil(t, n.Next)
|
||||
case 19:
|
||||
assert.Equal(t, voteNodes[13], n.Next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVotingChains(t *testing.T) {
|
||||
races, raceIndex := prepareRaces()
|
||||
nodes := controller.VotingGraph(races, raceIndex)
|
||||
|
||||
vc := controller.VotingChains(nodes)
|
||||
|
||||
assert.Len(t, vc, 7)
|
||||
for i := range vc {
|
||||
n := vc[i]
|
||||
switch i {
|
||||
case 0:
|
||||
assert.Len(t, n, 3)
|
||||
assert.Equal(t, 2, n[0].ID)
|
||||
assert.Equal(t, 3, n[1].ID)
|
||||
assert.Equal(t, 4, n[2].ID)
|
||||
assert.True(t, n[0].Ally)
|
||||
assert.True(t, n[1].Ally)
|
||||
assert.True(t, n[2].Ally)
|
||||
case 1:
|
||||
assert.Len(t, n, 2)
|
||||
assert.Equal(t, 0, n[0].ID)
|
||||
assert.Equal(t, 1, n[1].ID)
|
||||
assert.False(t, n[0].Ally)
|
||||
assert.False(t, n[1].Ally)
|
||||
case 2:
|
||||
assert.Len(t, n, 2)
|
||||
assert.Equal(t, 5, n[0].ID)
|
||||
assert.Equal(t, 6, n[1].ID)
|
||||
assert.False(t, n[0].Ally)
|
||||
assert.False(t, n[1].Ally)
|
||||
case 3:
|
||||
assert.Len(t, n, 3)
|
||||
assert.Equal(t, 7, n[0].ID)
|
||||
assert.Equal(t, 10, n[1].ID)
|
||||
assert.Equal(t, 11, n[2].ID)
|
||||
assert.False(t, n[0].Ally)
|
||||
assert.False(t, n[1].Ally)
|
||||
assert.False(t, n[2].Ally)
|
||||
case 4:
|
||||
assert.Len(t, n, 4)
|
||||
assert.Equal(t, 8, n[0].ID)
|
||||
assert.Equal(t, 9, n[1].ID)
|
||||
assert.Equal(t, 10, n[2].ID)
|
||||
assert.Equal(t, 11, n[3].ID)
|
||||
assert.False(t, n[0].Ally)
|
||||
assert.False(t, n[1].Ally)
|
||||
assert.False(t, n[2].Ally)
|
||||
assert.False(t, n[3].Ally)
|
||||
case 5:
|
||||
assert.Len(t, n, 2)
|
||||
assert.Equal(t, 12, n[0].ID)
|
||||
assert.Equal(t, 13, n[1].ID)
|
||||
assert.True(t, n[0].Ally)
|
||||
assert.True(t, n[1].Ally)
|
||||
case 6:
|
||||
assert.Len(t, n, 3)
|
||||
assert.Equal(t, 15, n[0].ID)
|
||||
assert.Equal(t, 16, n[1].ID)
|
||||
assert.Equal(t, 17, n[2].ID)
|
||||
assert.True(t, n[0].Ally)
|
||||
assert.True(t, n[1].Ally)
|
||||
assert.True(t, n[2].Ally)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupVotes(t *testing.T) {
|
||||
races, raceIndex := prepareRaces()
|
||||
raceVotes := make(map[int]float64)
|
||||
// [1] = 0.24
|
||||
raceVotes[0] = 0.11
|
||||
raceVotes[1] = 0.13
|
||||
|
||||
// [2,3,4] = 0.69
|
||||
raceVotes[2] = 0.22
|
||||
raceVotes[3] = 0.23
|
||||
raceVotes[4] = 0.24
|
||||
|
||||
// [6] = 0.71
|
||||
raceVotes[5] = 0.35
|
||||
raceVotes[6] = 0.36
|
||||
|
||||
// [11] = 0.843
|
||||
raceVotes[7] = 0.41
|
||||
raceVotes[9] = 0.42
|
||||
raceVotes[10] = 0.013
|
||||
|
||||
// [12,13] = 0.52
|
||||
raceVotes[12] = 0.52
|
||||
raceVotes[13] = 0.
|
||||
|
||||
// [14] = 1.04
|
||||
raceVotes[14] = 1.04
|
||||
|
||||
// [15,16,17] = 2.49
|
||||
raceVotes[15] = 1.15
|
||||
raceVotes[16] = 0.16
|
||||
raceVotes[17] = 1.18
|
||||
|
||||
// [18] = 3.18
|
||||
raceVotes[18] = 3.18
|
||||
|
||||
// [19] = 0.019
|
||||
raceVotes[19] = 0.019
|
||||
|
||||
calc := controller.GroupVotes(raceVotes, controller.VotingGraph(races, raceIndex))
|
||||
|
||||
assert.Len(t, calc, 9)
|
||||
for i := range calc {
|
||||
vg := calc[i]
|
||||
switch i {
|
||||
case 0:
|
||||
assert.ElementsMatch(t, []int{2, 3, 4}, vg.RaceIndex)
|
||||
assert.Equal(t, 0.69, vg.Sum)
|
||||
case 4:
|
||||
assert.ElementsMatch(t, []int{12, 13}, vg.RaceIndex)
|
||||
assert.Equal(t, 0.52, vg.Sum)
|
||||
case 5:
|
||||
assert.ElementsMatch(t, []int{15, 16, 17}, vg.RaceIndex)
|
||||
assert.InDelta(t, 2.49, vg.Sum, 0.001)
|
||||
default:
|
||||
assert.Len(t, vg.RaceIndex, 1)
|
||||
switch ri := vg.RaceIndex[0]; ri {
|
||||
case 1:
|
||||
assert.Equal(t, 0.24, vg.Sum)
|
||||
case 6:
|
||||
assert.Equal(t, 0.71, vg.Sum)
|
||||
case 11:
|
||||
assert.Equal(t, 0.843, vg.Sum)
|
||||
case 14:
|
||||
assert.Equal(t, 1.04, vg.Sum)
|
||||
case 18:
|
||||
assert.Equal(t, 3.18, vg.Sum)
|
||||
case 19:
|
||||
assert.Equal(t, 0.019, vg.Sum)
|
||||
default:
|
||||
assert.Failf(t, "unexpected group", "id=%v sum=%f", vg.RaceIndex, vg.Sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVotingWinners(t *testing.T) {
|
||||
gameVotes := 100.0
|
||||
var vg []*controller.VoteGroup
|
||||
var winners []int
|
||||
|
||||
vg = []*controller.VoteGroup{
|
||||
{Sum: 4.0, RaceIndex: []int{0}},
|
||||
{Sum: 66.65, RaceIndex: []int{1, 2}},
|
||||
{Sum: 5.0, RaceIndex: []int{3}},
|
||||
{Sum: 25.0, RaceIndex: []int{4, 5, 6}},
|
||||
}
|
||||
winners = controller.VotingWinners(vg, gameVotes)
|
||||
assert.Len(t, winners, 0)
|
||||
|
||||
vg = []*controller.VoteGroup{
|
||||
{Sum: 4.0, RaceIndex: []int{0}},
|
||||
{Sum: 66.666666666666666, RaceIndex: []int{1, 2}},
|
||||
{Sum: 5.0, RaceIndex: []int{3}},
|
||||
{Sum: 22.0, RaceIndex: []int{4, 5, 6}},
|
||||
}
|
||||
winners = controller.VotingWinners(vg, gameVotes)
|
||||
assert.ElementsMatch(t, winners, []int{1, 2})
|
||||
|
||||
vg = []*controller.VoteGroup{
|
||||
{Sum: 4.0, RaceIndex: []int{0}},
|
||||
{Sum: 3.33, RaceIndex: []int{1, 2}},
|
||||
{Sum: 66.67, RaceIndex: []int{3}},
|
||||
{Sum: 25.0, RaceIndex: []int{4, 5, 6}},
|
||||
}
|
||||
winners = controller.VotingWinners(vg, gameVotes)
|
||||
assert.ElementsMatch(t, winners, []int{3})
|
||||
}
|
||||
Reference in New Issue
Block a user