cmd: upgrade group

This commit is contained in:
Ilia Denisov
2026-01-04 19:22:06 +02:00
parent 6157c07a35
commit c6e1cb5cdf
17 changed files with 918 additions and 245 deletions
+1 -1
View File
@@ -70,7 +70,7 @@ func (g *Game) joinShipGroupToFleetInternal(ri int, fleetName string, group, cou
return e.NewEntityNotExistsError("group #%d", group)
}
if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil {
if g.ShipGroups[sgi].State() != StateInOrbit {
return e.NewShipsBusyError()
}
+8 -4
View File
@@ -5,11 +5,12 @@ import (
"testing"
e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
func TestJoinShipGroupToFleet(t *testing.T) {
g := copyGame()
g := newGame()
var groupIndex uint = 1
assert.ErrorContains(t,
@@ -85,11 +86,14 @@ func TestJoinShipGroupToFleet(t *testing.T) {
// group not In_Orbit
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7))
gi = 3
g.ShipGroups[gi].State = "In_Space"
g.ShipGroups[gi].StateInSpace = &game.InSpace{
Origin: 2,
Range: 1,
}
assert.ErrorContains(t,
g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0),
e.GenericErrorText(e.ErrShipsBusy))
g.ShipGroups[gi].State = "In_Orbit"
g.ShipGroups[gi].StateInSpace = nil
// existing fleet not on the same planet or in_orbit
g.Fleets[0].Destination = R0_Planet_2_num
@@ -100,7 +104,7 @@ func TestJoinShipGroupToFleet(t *testing.T) {
}
func TestJoinFleets(t *testing.T) {
g := copyGame()
g := newGame()
// creating ShipGroup at Planet_0
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1
// creating ShipGroup at Planet_2
+39
View File
@@ -2,6 +2,9 @@ package game
import (
"encoding/json"
"fmt"
"iter"
"maps"
"slices"
"strings"
@@ -9,6 +12,22 @@ import (
e "github.com/iliadenisov/galaxy/internal/error"
)
type TechSet map[Tech]float64
func (ts TechSet) Value(t Tech) float64 {
if v, ok := ts[t]; ok {
return v
} else {
panic(fmt.Sprintf("TechSet: Value: %s's value not set", t.String()))
}
}
func (ts TechSet) Set(t Tech, v float64) TechSet {
m := maps.Clone(ts)
m[t] = v
return m
}
type Game struct {
ID uuid.UUID `json:"id"`
Age uint `json:"turn"` // Game's turn number
@@ -29,6 +48,26 @@ func (g Game) Votes(raceID uuid.UUID) float64 {
return pop / 1000.
}
func (g Game) PlanetByNumber(number uint) (Planet, error) {
pi := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == number })
if pi < 0 {
return Planet{}, e.NewGameStateError("PlanetByNumber: planet with number=%d not found", number)
}
return g.Map.Planet[pi], nil
}
func (g Game) ShipsInUpgrade(planetNumber uint) iter.Seq[ShipGroup] {
return func(yield func(ShipGroup) bool) {
for sg := range g.ShipGroups {
if g.ShipGroups[sg].Destination == planetNumber && g.ShipGroups[sg].State() == StateUpgrade {
if !yield(g.ShipGroups[sg]) {
break
}
}
}
}
}
func (g Game) raceIndex(name string) (int, error) {
i := slices.IndexFunc(g.Race, func(r Race) bool { return r.Name == name })
if i < 0 {
+51 -40
View File
@@ -10,37 +10,26 @@ import (
var (
Race_0 = game.Race{
ID: uuid.New(),
Name: "Race_0",
Drive: 1.1,
Weapons: 1.2,
Shields: 1.3,
Cargo: 1.4,
ID: uuid.New(),
Name: "Race_0",
Tech: map[game.Tech]float64{
game.TechDrive: 1.1,
game.TechWeapons: 1.2,
game.TechShields: 1.3,
game.TechCargo: 1.4,
},
}
Race_1 = game.Race{
ID: uuid.New(),
Name: "Race_1",
Drive: 2.1,
Weapons: 2.2,
Shields: 2.3,
Cargo: 2.4,
}
Map = game.Map{
Width: 10,
Height: 10,
Planet: []game.Planet{
controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 0, 0, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
ID: uuid.New(),
Name: "Race_1",
Tech: map[game.Tech]float64{
game.TechDrive: 2.1,
game.TechWeapons: 2.2,
game.TechShields: 2.3,
game.TechCargo: 2.4,
},
}
Game = &game.Game{
Race: []game.Race{
Race_0,
Race_1,
},
Map: Map,
}
Race_0_idx = 0
Race_0_Gunship = "R0_Gunship"
Race_0_Freighter = "R0_Freighter"
@@ -59,25 +48,47 @@ var (
Race_1_Cruiser_idx = 2
ShipType_Cruiser = "Cruiser"
Cruiser = game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Cruiser",
Drive: 15,
Armament: 1,
Weapons: 15,
Shields: 15,
Cargo: 0,
},
}
)
func init() {
assertNoError(Game.CreateShipType(Race_0.Name, Race_0_Gunship, 60, 30, 100, 0, 3))
assertNoError(Game.CreateShipType(Race_0.Name, Race_0_Freighter, 8, 0, 2, 10, 0))
assertNoError(Game.CreateShipType(Race_0.Name, ShipType_Cruiser, 15, 15, 15, 0, 1))
assertNoError(Game.CreateShipType(Race_1.Name, Race_1_Gunship, 60, 30, 100, 0, 3))
assertNoError(Game.CreateShipType(Race_1.Name, Race_1_Freighter, 8, 0, 2, 10, 0))
assertNoError(Game.CreateShipType(Race_1.Name, ShipType_Cruiser, 15, 15, 15, 0, 2)) // same name - different type
}
func assertNoError(err error) {
if err != nil {
panic(fmt.Sprintf("init assertion failed: %v", err))
}
}
func copyGame() *game.Game {
g := *Game
return &g
func newGame() *game.Game {
g := &game.Game{
Race: []game.Race{
Race_0,
Race_1,
},
Map: game.Map{
Width: 10,
Height: 10,
Planet: []game.Planet{
controller.NewPlanet(R0_Planet_0_num, "Planet_0", Race_0.ID, 0, 0, 100, 100, 100, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R1_Planet_1_num, "Planet_1", Race_1.ID, 1, 1, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
controller.NewPlanet(R0_Planet_2_num, "Planet_2", Race_0.ID, 2, 2, 100, 0, 0, 0, game.ProductionNone.AsType(uuid.Nil)),
},
},
}
assertNoError(g.CreateShipType(Race_0.Name, Race_0_Gunship, 60, 30, 100, 0, 3))
assertNoError(g.CreateShipType(Race_0.Name, Race_0_Freighter, 8, 0, 2, 10, 0))
assertNoError(g.CreateShipType(Race_0.Name, ShipType_Cruiser, Cruiser.Drive, Cruiser.Weapons, Cruiser.Shields, Cruiser.Cargo, int(Cruiser.Armament)))
assertNoError(g.CreateShipType(Race_1.Name, Race_1_Gunship, 60, 30, 100, 0, 3))
assertNoError(g.CreateShipType(Race_1.Name, Race_1_Freighter, 8, 0, 2, 10, 0))
assertNoError(g.CreateShipType(Race_1.Name, ShipType_Cruiser, 15, 15, 15, 0, 2)) // same name - different type (why.)
return g
}
+115 -39
View File
@@ -1,6 +1,7 @@
package game
import (
"fmt"
"iter"
"maps"
"math"
@@ -35,50 +36,128 @@ func (ct CargoType) String() string {
return string(ct)
}
type ShipGroupState string
const (
StateInOrbit ShipGroupState = "In_Orbit"
StateLaunched ShipGroupState = "Launched"
StateInSpace ShipGroupState = "In_Space"
StateUpgrade ShipGroupState = "Upgrade"
StateTransfer ShipGroupState = "Transfer_Status"
)
type InSpace struct {
Origin uint `json:"origin"`
// zero is for Launched status
Range float64 `json:"range"`
}
type InUpgrade struct {
UpgradeTech []UpgradePreference `json:"preference"`
}
func (iu InUpgrade) Cost() float64 {
var sum float64
for i := range iu.UpgradeTech {
sum += iu.UpgradeTech[i].Cost
}
return sum
}
func (iu InUpgrade) TechCost(t Tech) float64 {
for i := range iu.UpgradeTech {
if iu.UpgradeTech[i].Tech == t {
return iu.UpgradeTech[i].Cost
}
}
return 0.
}
type UpgradePreference struct {
Tech Tech `json:"tech"`
Level float64 `json:"level"`
Cost float64 `json:"cost"`
}
type Tech string
const (
TechAll Tech = "ALL"
TechDrive Tech = "DRIVE"
TechWeapons Tech = "WEAPONS"
TechShields Tech = "SHIELDS"
TechCargo Tech = "CARGO"
)
func (t Tech) String() string {
return string(t)
}
type ShipGroup struct {
Index uint `json:"index"` // FIXME: use UUID for Group Index (ordered)
OwnerID uuid.UUID `json:"ownerId"` // Race link
TypeID uuid.UUID `json:"typeId"` // ShipType link
FleetID *uuid.UUID `json:"fleetId,omitempty"` // Fleet link
Number uint `json:"number"` // Number (quantity) ships of specific ShipType
State string `json:"state"` // TODO: kinda enum: In_Orbit, In_Space, Transfer_State, Upgrade
CargoType *CargoType `json:"loadType,omitempty"`
Load float64 `json:"load"` // Cargo loaded - "Масса груза"
Drive float64 `json:"drive"`
Weapons float64 `json:"weapons"`
Shields float64 `json:"shields"`
Cargo float64 `json:"cargo"`
Tech TechSet `json:"tech"`
// TODO: TEST: Destination, Origin, Range
Destination uint `json:"destination"`
Origin *uint `json:"origin,omitempty"`
Range *float64 `json:"range,omitempty"`
Destination uint `json:"destination"`
StateInSpace *InSpace `json:"stateInSpace,omitempty"`
StateUpgrade *InUpgrade `json:"stateUpgrade,omitempty"`
}
func (sg ShipGroup) TechLevel(t Tech) float64 {
return sg.Tech.Value(t)
}
// TODO: refactor to separate method with *ShipGroup as parameter
func (sg *ShipGroup) SetTechLevel(t Tech, v float64) {
sg.Tech = sg.Tech.Set(t, v)
}
func (sg ShipGroup) State() ShipGroupState {
switch {
case sg.StateInSpace == nil && sg.StateUpgrade == nil:
return StateInOrbit
case sg.StateInSpace != nil && sg.StateUpgrade == nil:
if sg.StateInSpace.Range > 0 {
return StateInSpace
}
return StateLaunched
case sg.StateUpgrade != nil && sg.StateInSpace == nil:
return StateUpgrade
default:
panic(fmt.Sprintf("ambigous group state: in_space=%#v upgrage=%#v", sg.StateInSpace, sg.StateUpgrade))
}
}
func (sg ShipGroup) Equal(other ShipGroup) bool {
return sg.OwnerID == other.OwnerID &&
sg.TypeID == other.TypeID &&
sg.FleetID == other.FleetID &&
sg.Drive == other.Drive &&
sg.Weapons == other.Weapons &&
sg.Shields == other.Shields &&
sg.Cargo == other.Cargo &&
sg.TechLevel(TechDrive) == other.TechLevel(TechDrive) &&
sg.TechLevel(TechWeapons) == other.TechLevel(TechWeapons) &&
sg.TechLevel(TechShields) == other.TechLevel(TechShields) &&
sg.TechLevel(TechCargo) == other.TechLevel(TechCargo) &&
sg.CargoType == other.CargoType &&
sg.Load == other.Load &&
sg.State == other.State
sg.State() == other.State()
}
// Грузоподъёмность
func (sg ShipGroup) CargoCapacity(st *ShipType) float64 {
return sg.Cargo * (st.Cargo + (st.Cargo*st.Cargo)/20) * float64(sg.Number)
return sg.TechLevel(TechCargo) * (st.Cargo + (st.Cargo*st.Cargo)/20) * float64(sg.Number)
}
// Масса перевозимого груза -
// общее количество единиц груза, деленное на технологический уровень Грузоперевозок
func (sg ShipGroup) CarryingMass() float64 {
return sg.Load / sg.Cargo
return sg.Load / sg.TechLevel(TechCargo)
}
// Масса группы без учёта груза
@@ -95,7 +174,7 @@ func (sg ShipGroup) FullMass(st *ShipType) float64 {
// Эффективность двигателя -
// равна мощности Двигателей, умноженной на технологический уровень блока Двигателей
func (sg ShipGroup) DriveEffective(st *ShipType) float64 {
return st.Drive * sg.Drive
return st.Drive * sg.TechLevel(TechDrive)
}
// Корабли перемещаются за один ход на количество световых лет, равное
@@ -105,29 +184,29 @@ func (sg ShipGroup) Speed(st *ShipType) float64 {
}
func (sg ShipGroup) UpgradeDriveCost(st *ShipType, drive float64) float64 {
return (1 - sg.Drive/drive) * 10 * st.Drive
return (1 - sg.TechLevel(TechDrive)/drive) * 10 * st.Drive
}
// TODO: test on other values
func (sg ShipGroup) UpgradeWeaponsCost(st *ShipType, weapons float64) float64 {
return (1 - sg.Weapons/weapons) * 10 * st.WeaponsMass()
return (1 - sg.TechLevel(TechWeapons)/weapons) * 10 * st.WeaponsBlockMass()
}
func (sg ShipGroup) UpgradeShieldsCost(st *ShipType, shields float64) float64 {
return (1 - sg.Shields/shields) * 10 * st.Shields
return (1 - sg.TechLevel(TechShields)/shields) * 10 * st.Shields
}
func (sg ShipGroup) UpgradeCargoCost(st *ShipType, cargo float64) float64 {
return (1 - sg.Cargo/cargo) * 10 * st.Cargo
return (1 - sg.TechLevel(TechCargo)/cargo) * 10 * st.Cargo
}
// Мощность бомбардировки
// TODO: maybe rounding must be done only for display?
func (sg ShipGroup) BombingPower(st *ShipType) float64 {
// return math.Sqrt(sg.Type.Weapons * sg.Weapons)
result := (math.Sqrt(st.Weapons*sg.Weapons)/10. + 1.) *
result := (math.Sqrt(st.Weapons*sg.TechLevel(TechWeapons))/10. + 1.) *
st.Weapons *
sg.Weapons *
sg.TechLevel(TechWeapons) *
float64(st.Armament) *
float64(sg.Number)
return number.Fixed3(result)
@@ -173,7 +252,7 @@ func (g *Game) disassembleGroupInternal(ri int, groupIndex, quantity uint) error
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil {
if g.ShipGroups[sgi].State() != StateInOrbit {
return e.NewShipsBusyError()
}
@@ -257,7 +336,7 @@ func (g *Game) unloadCargoInternal(ri int, groupIndex uint, ships uint, quantity
if sgi < 0 {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil {
if g.ShipGroups[sgi].State() != StateInOrbit {
return e.NewShipsBusyError()
}
var sti int
@@ -336,7 +415,7 @@ func (g *Game) loadCargoInternal(ri int, groupIndex uint, ct CargoType, ships ui
if sgi < 0 {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil {
if g.ShipGroups[sgi].State() != StateInOrbit {
return e.NewShipsBusyError()
}
pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == g.ShipGroups[sgi].Destination })
@@ -464,19 +543,15 @@ func (g *Game) giveawayGroupInternal(ri, riAccept int, groupIndex, quantity uint
OwnerID: g.Race[riAccept].ID,
TypeID: g.Race[riAccept].ShipTypes[stAcc].ID,
Number: uint(quantity),
State: g.ShipGroups[sgi].State,
CargoType: g.ShipGroups[sgi].CargoType,
Load: g.ShipGroups[sgi].Load,
Drive: g.ShipGroups[sgi].Drive,
Weapons: g.ShipGroups[sgi].Weapons,
Shields: g.ShipGroups[sgi].Shields,
Cargo: g.ShipGroups[sgi].Cargo,
Tech: maps.Clone(g.ShipGroups[sgi].Tech),
Destination: g.ShipGroups[sgi].Destination,
Origin: g.ShipGroups[sgi].Origin,
Range: g.ShipGroups[sgi].Range,
Destination: g.ShipGroups[sgi].Destination,
StateInSpace: g.ShipGroups[sgi].StateInSpace,
StateUpgrade: g.ShipGroups[sgi].StateUpgrade,
})
if quantity == 0 || quantity == g.ShipGroups[sgi].Number {
@@ -503,7 +578,7 @@ func (g *Game) breakGroupInternal(ri int, groupIndex, quantity uint) error {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil {
if g.ShipGroups[sgi].State() != StateInOrbit {
return e.NewShipsBusyError()
}
@@ -599,11 +674,12 @@ func (g *Game) createShips(ri int, shipTypeName string, planetNumber int, quanti
TypeID: g.Race[ri].ShipTypes[st].ID,
Destination: g.Map.Planet[pl].Number,
Number: uint(quantity),
State: "In_Orbit",
Drive: g.Race[ri].Drive,
Weapons: g.Race[ri].Weapons,
Shields: g.Race[ri].Shields,
Cargo: g.Race[ri].Cargo,
Tech: map[Tech]float64{
TechDrive: g.Race[ri].TechLevel(TechDrive),
TechWeapons: g.Race[ri].TechLevel(TechWeapons),
TechShields: g.Race[ri].TechLevel(TechShields),
TechCargo: g.Race[ri].TechLevel(TechCargo),
},
})
return nil
}
+98 -109
View File
@@ -24,12 +24,13 @@ func TestCargoCapacity(t *testing.T) {
},
}
sg := game.ShipGroup{
Number: 1,
State: "In_Orbit",
Drive: 1.5,
Weapons: 1.1,
Shields: 2.0,
Cargo: 1.0,
Number: 1,
Tech: map[game.Tech]float64{
game.TechDrive: 1.5,
game.TechWeapons: 1.1,
game.TechShields: 2.0,
game.TechCargo: 1.0,
},
}
assert.Equal(t, expectCapacity, sg.CargoCapacity(&ship))
}
@@ -52,13 +53,14 @@ func TestCarryingAndFullMass(t *testing.T) {
},
}
sg := &game.ShipGroup{
Number: 1,
State: "In_Orbit",
Drive: 1.0,
Weapons: 1.0,
Shields: 1.0,
Cargo: 1.0,
Load: 0.0,
Number: 1,
Tech: map[game.Tech]float64{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
Load: 0.0,
}
em := Freighter.EmptyMass()
assert.Equal(t, 0.0, sg.CarryingMass())
@@ -68,7 +70,7 @@ func TestCarryingAndFullMass(t *testing.T) {
assert.Equal(t, 10.0, sg.CarryingMass())
assert.Equal(t, em+10.0, sg.FullMass(Freighter))
sg.Cargo = 2.5
sg.SetTechLevel(game.TechCargo, 2.5)
assert.Equal(t, 4.0, sg.CarryingMass())
assert.Equal(t, em+4.0, sg.FullMass(Freighter))
}
@@ -85,21 +87,22 @@ func TestSpeed(t *testing.T) {
},
}
sg := &game.ShipGroup{
Number: 1,
State: "In_Orbit",
Drive: 1.0,
Weapons: 1.0,
Shields: 1.0,
Cargo: 1.0,
Load: 0.0,
Number: 1,
Tech: map[game.Tech]float64{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
Load: 0.0,
}
assert.Equal(t, 8.0, sg.Speed(Freighter))
sg.Load = 5.0
assert.Equal(t, 6.4, sg.Speed(Freighter))
sg.Drive = 1.5
sg.SetTechLevel(game.TechDrive, 1.5)
assert.Equal(t, 9.6, sg.Speed(Freighter))
sg.Load = 10
sg.Cargo = 1.5
sg.SetTechLevel(game.TechCargo, 1.5)
assert.Equal(t, 9.0, sg.Speed(Freighter))
}
@@ -114,45 +117,19 @@ func TestBombingPower(t *testing.T) {
},
}
sg := game.ShipGroup{
Number: 1,
State: "In_Orbit",
Drive: 1.0,
Weapons: 1.0,
Shields: 1.0,
Cargo: 1.0,
Number: 1,
Tech: map[game.Tech]float64{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
}
expectedBombingPower := 139.295
result := sg.BombingPower(&Gunship)
assert.Equal(t, expectedBombingPower, result)
}
func TestUpgradeCost(t *testing.T) {
Cruiser := game.ShipType{
ShipTypeReport: game.ShipTypeReport{
Name: "Cruiser",
Drive: 15,
Armament: 1,
Weapons: 15,
Shields: 15,
Cargo: 0,
},
}
sg := game.ShipGroup{
Number: 1,
State: "In_Orbit",
Drive: 1.0,
Weapons: 1.0,
Shields: 1.0,
Cargo: 1.0,
}
upgradeCost := sg.UpgradeDriveCost(&Cruiser, 2.0) +
sg.UpgradeWeaponsCost(&Cruiser, 2.0) +
sg.UpgradeShieldsCost(&Cruiser, 2.0) +
sg.UpgradeCargoCost(&Cruiser, 2.0)
assert.Equal(t, 225., upgradeCost)
}
func TestDriveEffective(t *testing.T) {
tc := []struct {
driveShipType float64
@@ -178,12 +155,13 @@ func TestDriveEffective(t *testing.T) {
},
}
sg := game.ShipGroup{
Number: rand.UintN(4) + 1,
State: "In_Orbit",
Drive: tc[i].driveTech,
Weapons: rand.Float64()*5 + 1,
Shields: rand.Float64()*5 + 1,
Cargo: rand.Float64()*5 + 1,
Number: rand.UintN(4) + 1,
Tech: map[game.Tech]float64{
game.TechDrive: tc[i].driveTech,
game.TechWeapons: rand.Float64()*5 + 1,
game.TechShields: rand.Float64()*5 + 1,
game.TechCargo: rand.Float64()*5 + 1,
},
}
assert.Equal(t, tc[i].expectDriveEffective, sg.DriveEffective(&someShip))
}
@@ -201,13 +179,14 @@ func TestShipGroupEqual(t *testing.T) {
OwnerID: uuid.New(),
TypeID: uuid.New(),
FleetID: &fleetId,
State: "In_Orbit",
CargoType: &mat,
Load: 123.45,
Drive: 1.0,
Weapons: 1.0,
Shields: 1.0,
Cargo: 1.0,
Tech: map[game.Tech]float64{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
}
// essential properties
@@ -230,7 +209,10 @@ func TestShipGroupEqual(t *testing.T) {
assert.False(t, left.Equal(right))
right = *left
left.State = "In_Space"
left.StateInSpace = &game.InSpace{
Origin: 1,
Range: 1,
}
assert.False(t, left.Equal(right))
right = *left
@@ -246,19 +228,23 @@ func TestShipGroupEqual(t *testing.T) {
assert.False(t, left.Equal(right))
right = *left
left.Drive = 1.1
left.SetTechLevel(game.TechDrive, 1.1)
assert.Equal(t, 1.1, left.TechLevel(game.TechDrive))
assert.False(t, left.Equal(right))
right = *left
left.Weapons = 1.1
left.SetTechLevel(game.TechWeapons, 1.1)
assert.Equal(t, 1.1, left.TechLevel(game.TechWeapons))
assert.False(t, left.Equal(right))
right = *left
left.Shields = 1.1
left.SetTechLevel(game.TechShields, 1.1)
assert.Equal(t, 1.1, left.TechLevel(game.TechShields))
assert.False(t, left.Equal(right))
right = *left
left.Cargo = 1.1
left.SetTechLevel(game.TechCargo, 1.1)
assert.Equal(t, 1.1, left.TechLevel(game.TechCargo))
assert.False(t, left.Equal(right))
// non-essential properties
@@ -271,7 +257,7 @@ func TestShipGroupEqual(t *testing.T) {
}
func TestCreateShips(t *testing.T) {
g := copyGame()
g := newGame()
assert.ErrorContains(t,
g.CreateShips(Race_0_idx, "Unknown_Ship_Type", R0_Planet_0_num, 2),
@@ -294,7 +280,7 @@ func TestCreateShips(t *testing.T) {
}
func TestJoinEqualGroups(t *testing.T) {
g := copyGame()
g := newGame()
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // 1 -> 2
assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1))
@@ -302,7 +288,7 @@ func TestJoinEqualGroups(t *testing.T) {
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) // (3)
assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1))
g.Race[Race_0_idx].Drive = 1.5
g.Race[Race_0_idx].SetTechLevel(game.TechDrive, 1.5)
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 9)) // 4 -> 6
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) // 5 -> 7
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 4)) // (6)
@@ -310,7 +296,7 @@ func TestJoinEqualGroups(t *testing.T) {
assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7)
g.Race[Race_1_idx].Shields = 2.0
g.Race[Race_1_idx].SetTechLevel(game.TechShields, 2.0)
assert.NoError(t, g.CreateShips(1, Race_1_Freighter, R1_Planet_1_num, 1))
assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 3)
@@ -330,16 +316,16 @@ func TestJoinEqualGroups(t *testing.T) {
for sg := range g.ListShipGroups(Race_0_idx) {
switch {
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Freighter) && sg.Drive == 1.1:
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(2), sg.Index)
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Freighter) && sg.Drive == 1.5:
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(7), sg.Index)
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Gunship) && sg.Drive == 1.1:
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(3), sg.Index)
case sg.TypeID == shipTypeID(Race_0_idx, Race_0_Gunship) && sg.Drive == 1.5:
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(6), sg.Index)
default:
@@ -349,10 +335,13 @@ func TestJoinEqualGroups(t *testing.T) {
}
func TestBreakGroup(t *testing.T) {
g := copyGame()
g := newGame()
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 13)) // group #1 (0)
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7)) // group #2 (1) - In_Space
g.ShipGroups[1].State = "In_Space"
g.ShipGroups[1].StateInSpace = &game.InSpace{
Origin: 1,
Range: 1,
}
fleet := "R0_Fleet"
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleet, 1, 0))
@@ -415,17 +404,17 @@ func TestBreakGroup(t *testing.T) {
}
func TestGiveawayGroup(t *testing.T) {
g := copyGame()
g := newGame()
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 11)) // group #1 (0)
assert.NoError(t, g.CreateShips(Race_1_idx, ShipType_Cruiser, R1_Planet_1_num, 23)) // group #1 (1)
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 17)) // group #2 (2) - In_Space
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "R0_Fleet", 2, 0))
assert.NotNil(t, g.ShipGroups[2].FleetID)
g.ShipGroups[2].Origin = &R0_Planet_2_num
rng := 31.337
g.ShipGroups[2].Range = &rng
g.ShipGroups[2].State = "In_Space"
g.ShipGroups[2].StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
g.ShipGroups[2].CargoType = game.CargoMaterial.Ref()
g.ShipGroups[2].Load = 1.234
@@ -462,16 +451,16 @@ func TestGiveawayGroup(t *testing.T) {
assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Shields, g.Race[Race_0_idx].ShipTypes[sto].Shields)
assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Cargo, g.Race[Race_0_idx].ShipTypes[sto].Cargo)
assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Armament, g.Race[Race_0_idx].ShipTypes[sto].Armament)
assert.Equal(t, g.ShipGroups[2].State, g.ShipGroups[3].State)
assert.Equal(t, g.ShipGroups[2].State(), g.ShipGroups[3].State())
assert.Equal(t, g.ShipGroups[2].CargoType, g.ShipGroups[3].CargoType)
assert.Equal(t, g.ShipGroups[2].Load, g.ShipGroups[3].Load)
assert.Equal(t, g.ShipGroups[2].Drive, g.ShipGroups[3].Drive)
assert.Equal(t, g.ShipGroups[2].Weapons, g.ShipGroups[3].Weapons)
assert.Equal(t, g.ShipGroups[2].Shields, g.ShipGroups[3].Shields)
assert.Equal(t, g.ShipGroups[2].Cargo, g.ShipGroups[3].Cargo)
assert.Equal(t, g.ShipGroups[2].TechLevel(game.TechDrive), g.ShipGroups[3].TechLevel(game.TechDrive))
assert.Equal(t, g.ShipGroups[2].TechLevel(game.TechWeapons), g.ShipGroups[3].TechLevel(game.TechWeapons))
assert.Equal(t, g.ShipGroups[2].TechLevel(game.TechShields), g.ShipGroups[3].TechLevel(game.TechShields))
assert.Equal(t, g.ShipGroups[2].TechLevel(game.TechCargo), g.ShipGroups[3].TechLevel(game.TechCargo))
assert.Equal(t, g.ShipGroups[2].Destination, g.ShipGroups[3].Destination)
assert.Equal(t, g.ShipGroups[2].Origin, g.ShipGroups[3].Origin)
assert.Equal(t, g.ShipGroups[2].Range, g.ShipGroups[3].Range)
assert.Equal(t, g.ShipGroups[2].StateInSpace, g.ShipGroups[3].StateInSpace)
assert.Equal(t, g.ShipGroups[2].StateUpgrade, g.ShipGroups[3].StateUpgrade)
assert.Equal(t, g.ShipGroups[3].OwnerID, g.Race[Race_1_idx].ID)
assert.Equal(t, g.ShipGroups[3].TypeID, g.Race[Race_1_idx].ShipTypes[sti].ID)
assert.Equal(t, g.ShipGroups[3].Number, uint(11))
@@ -483,7 +472,7 @@ func TestGiveawayGroup(t *testing.T) {
}
func TestLoadCargo(t *testing.T) {
g := copyGame()
g := newGame()
// 1: idx = 0 / Ready to load
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
@@ -493,10 +482,10 @@ func TestLoadCargo(t *testing.T) {
// 3: idx = 2 / In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
g.ShipGroups[2].Origin = &R0_Planet_2_num
rng := 31.337
g.ShipGroups[2].Range = &rng
g.ShipGroups[2].State = "In_Space"
g.ShipGroups[2].StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
// 4: idx = 3 / loaded with COL
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
@@ -603,7 +592,7 @@ func TestLoadCargo(t *testing.T) {
}
func TestUnloadCargo(t *testing.T) {
g := copyGame()
g := newGame()
// 1: idx = 0 / empty
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
@@ -613,10 +602,10 @@ func TestUnloadCargo(t *testing.T) {
// 3: idx = 2 / In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
g.ShipGroups[2].Origin = &R0_Planet_2_num
rng := 31.337
g.ShipGroups[2].Range = &rng
g.ShipGroups[2].State = "In_Space"
g.ShipGroups[2].StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
// 4: idx = 3 / loaded with COL
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11))
@@ -699,17 +688,17 @@ func TestUnloadCargo(t *testing.T) {
}
func TestDisassembleGroup(t *testing.T) {
g := copyGame()
g := newGame()
// 1: idx = 0 / empty
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
// 2: idx = 1 / In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
g.ShipGroups[1].Origin = &R0_Planet_2_num
rng := 31.337
g.ShipGroups[1].Range = &rng
g.ShipGroups[1].State = "In_Space"
g.ShipGroups[1].StateInSpace = &game.InSpace{
Origin: 2,
Range: 31.337,
}
// 3: idx = 2 / loaded with COL
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
+246
View File
@@ -0,0 +1,246 @@
package game
import (
"maps"
"math"
"slices"
"github.com/google/uuid"
e "github.com/iliadenisov/galaxy/internal/error"
)
type UpgradeCalc struct {
Cost map[Tech]float64
}
func (uc UpgradeCalc) UpgradeCost(ships uint) float64 {
var sum float64
for v := range maps.Values(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 ShipGroup, st ShipType, drive, weapons, shields, cargo float64) UpgradeCalc {
uc := &UpgradeCalc{Cost: make(map[Tech]float64)}
if drive > 0 {
uc.Cost[TechDrive] = BlockUpgradeCost(st.DriveBlockMass(), sg.TechLevel(TechDrive), drive)
}
if weapons > 0 {
uc.Cost[TechWeapons] = BlockUpgradeCost(st.WeaponsBlockMass(), sg.TechLevel(TechWeapons), weapons)
}
if shields > 0 {
uc.Cost[TechShields] = BlockUpgradeCost(st.ShieldsBlockMass(), sg.TechLevel(TechShields), shields)
}
if cargo > 0 {
uc.Cost[TechCargo] = BlockUpgradeCost(st.CargoBlockMass(), sg.TechLevel(TechCargo), cargo)
}
return *uc
}
func (g *Game) UpgradeGroup(raceName string, groupIndex uint, techInput string, limitShips uint, limitLevel float64) error {
ri, err := g.raceIndex(raceName)
if err != nil {
return err
}
return g.upgradeGroupInternal(ri, groupIndex, techInput, limitShips, limitLevel)
}
func (g *Game) upgradeGroupInternal(ri int, groupIndex uint, techInput string, limitShips uint, limitLevel float64) error {
sgi := -1
for i, sg := range g.listIndexShipGroups(ri) {
if sgi < 0 && sg.Index == groupIndex {
sgi = i
}
}
if sgi < 0 {
return e.NewEntityNotExistsError("group #%d", groupIndex)
}
var sti int
if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == g.ShipGroups[sgi].TypeID }); sti < 0 {
// hard to test, need manual game data invalidation
return e.NewGameStateError("not found: ShipType ID=%v", g.ShipGroups[sgi].TypeID)
}
st := g.Race[ri].ShipTypes[sti]
pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == g.ShipGroups[sgi].Destination })
if pl < 0 {
return e.NewGameStateError("planet #%d", g.ShipGroups[sgi].Destination)
}
if g.Map.Planet[pl].Owner != uuid.Nil && g.Map.Planet[pl].Owner != g.Race[ri].ID {
return e.NewEntityNotOwnedError("planet #%d for upgrade group #%d", g.Map.Planet[pl].Number, groupIndex)
}
if g.ShipGroups[sgi].State() != StateInOrbit && g.ShipGroups[sgi].State() != StateUpgrade {
return e.NewShipsBusyError()
}
upgradeValidTech := map[string]Tech{
TechDrive.String(): TechDrive,
TechWeapons.String(): TechWeapons,
TechShields.String(): TechShields,
TechCargo.String(): TechCargo,
TechAll.String(): TechAll,
}
techRequest, ok := upgradeValidTech[techInput]
if !ok {
return e.NewTechUnknownError(techInput)
}
var blockMasses map[Tech]float64 = map[Tech]float64{
TechDrive: st.DriveBlockMass(),
TechWeapons: st.WeaponsBlockMass(),
TechShields: st.ShieldsBlockMass(),
TechCargo: st.CargoBlockMass(),
}
switch {
case techRequest != TechAll && blockMasses[techRequest] == 0:
return e.NewUpgradeShipTechNotUsedError()
case techRequest == TechAll && limitLevel != 0:
return e.NewUpgradeParameterNotAllowedError("tech=%s max_level=%f", techRequest.String(), limitLevel)
}
targetLevel := make(map[Tech]float64)
var sumLevels float64
for _, tech := range []Tech{TechDrive, TechWeapons, TechShields, TechCargo} {
if techRequest == TechAll || tech == techRequest {
if g.Race[ri].TechLevel(tech) < limitLevel {
return e.NewUpgradeTechLevelInsufficientError("%s=%.03f < %.03f", tech.String(), g.Race[ri].TechLevel(tech), limitLevel)
}
targetLevel[tech] = FutureUpgradeLevel(g.Race[ri].TechLevel(tech), g.ShipGroups[sgi].TechLevel(tech), limitLevel)
} else {
targetLevel[tech] = CurrentUpgradingLevel(g.ShipGroups[sgi], tech)
}
sumLevels += targetLevel[tech]
}
productionCapacity := PlanetProductionCapacity(g, g.Map.Planet[pl].Number)
if g.ShipGroups[sgi].State() == StateUpgrade {
// to calculate actual capacity we must substract upgrade cost of selected group, if is upgrade state
productionCapacity -= g.ShipGroups[sgi].StateUpgrade.Cost()
}
uc := GroupUpgradeCost(g.ShipGroups[sgi], st, targetLevel[TechDrive], targetLevel[TechWeapons], targetLevel[TechShields], targetLevel[TechCargo])
costForShip := uc.UpgradeCost(1)
if costForShip == 0 {
return e.NewUpgradeShipsAlreadyUpToDateError("%#v", targetLevel)
}
shipsToUpgrade := g.ShipGroups[sgi].Number
// НЕ БОЛЕЕ УКАЗАННОГО
if limitShips > 0 && shipsToUpgrade > limitShips {
shipsToUpgrade = limitShips
}
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(g.ShipGroups[sgi], st, targetLevel[TechDrive], targetLevel[TechWeapons], targetLevel[TechShields], targetLevel[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 < g.ShipGroups[sgi].Number {
if g.ShipGroups[sgi].State() == StateUpgrade {
return e.NewUpgradeGroupBreakNotAllowedError("ships=%d max=%d", g.ShipGroups[sgi].Number, maxUpgradableShips)
}
nsgi, err := g.breakGroupSafe(ri, groupIndex, maxUpgradableShips)
if err != nil {
return err
}
sgi = nsgi
}
// finally, fill group upgrade prefs
for tech := range targetLevel {
if targetLevel[tech] > 0 {
g.ShipGroups[sgi] = UpgradeGroupPreference(g.ShipGroups[sgi], st, tech, targetLevel[tech])
}
}
return nil
}
func CurrentUpgradingLevel(sg ShipGroup, tech Tech) float64 {
if sg.StateUpgrade == nil {
return 0
}
ti := slices.IndexFunc(sg.StateUpgrade.UpgradeTech, func(pref UpgradePreference) bool { return pref.Tech == tech })
if ti >= 0 {
return sg.StateUpgrade.UpgradeTech[ti].Level
}
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 ShipGroup, st ShipType, tech Tech, v float64) ShipGroup {
if v <= 0 || st.BlockMass(tech) == 0 || sg.TechLevel(tech) >= v {
return sg
}
var su InUpgrade
if sg.StateUpgrade != nil {
su = *sg.StateUpgrade
} else {
su = InUpgrade{UpgradeTech: []UpgradePreference{}}
}
ti := slices.IndexFunc(su.UpgradeTech, func(pref UpgradePreference) bool { return pref.Tech == tech })
if ti < 0 {
su.UpgradeTech = append(su.UpgradeTech, UpgradePreference{Tech: tech})
ti = len(su.UpgradeTech) - 1
}
su.UpgradeTech[ti].Level = v
su.UpgradeTech[ti].Cost = BlockUpgradeCost(st.BlockMass(tech), sg.TechLevel(tech), v) * float64(sg.Number)
sg.StateUpgrade = &su
return sg
}
+169
View File
@@ -0,0 +1,169 @@
package game_test
import (
"slices"
"testing"
e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
func TestBlockUpgradeCost(t *testing.T) {
assert.Equal(t, 00.0, game.BlockUpgradeCost(1, 1.0, 1.0))
assert.Equal(t, 25.0, game.BlockUpgradeCost(5, 1.0, 2.0))
assert.Equal(t, 50.0, game.BlockUpgradeCost(10, 1.0, 2.0))
}
func TestGroupUpgradeCost(t *testing.T) {
sg := game.ShipGroup{
Tech: map[game.Tech]float64{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
Number: 1,
}
assert.Equal(t, 225.0, game.GroupUpgradeCost(sg, Cruiser, 2.0, 2.0, 2.0, 2.0).UpgradeCost(1))
}
func TestUpgradeMaxShips(t *testing.T) {
sg := game.ShipGroup{
Tech: map[game.Tech]float64{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
Number: 10,
}
uc := game.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 := &game.ShipGroup{
StateUpgrade: nil,
}
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechDrive))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechWeapons))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechShields))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechCargo))
sg.StateUpgrade = &game.InUpgrade{
UpgradeTech: []game.UpgradePreference{
{Tech: game.TechDrive, Level: 1.5, Cost: 100.1},
},
}
assert.Equal(t, 1.5, game.CurrentUpgradingLevel(*sg, game.TechDrive))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechWeapons))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechShields))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechCargo))
sg.StateUpgrade.UpgradeTech = append(sg.StateUpgrade.UpgradeTech, game.UpgradePreference{Tech: game.TechCargo, Level: 2.2, Cost: 200.2})
assert.Equal(t, 1.5, game.CurrentUpgradingLevel(*sg, game.TechDrive))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechWeapons))
assert.Equal(t, 0.0, game.CurrentUpgradingLevel(*sg, game.TechShields))
assert.Equal(t, 2.2, game.CurrentUpgradingLevel(*sg, game.TechCargo))
}
func TestFutureUpgradeLevel(t *testing.T) {
assert.Equal(t, 0.0, game.FutureUpgradeLevel(2.0, 2.0, 2.0))
assert.Equal(t, 0.0, game.FutureUpgradeLevel(2.0, 2.0, 3.0))
assert.Equal(t, 1.5, game.FutureUpgradeLevel(1.5, 2.0, 3.0))
assert.Equal(t, 2.0, game.FutureUpgradeLevel(2.5, 1.0, 2.0))
assert.Equal(t, 2.5, game.FutureUpgradeLevel(2.5, 1.0, 0.0))
}
func TestUpgradeGroupPreference(t *testing.T) {
sg := game.ShipGroup{
Number: 4,
Tech: game.TechSet{
game.TechDrive: 1.0,
game.TechWeapons: 1.0,
game.TechShields: 1.0,
game.TechCargo: 1.0,
},
}
assert.Nil(t, sg.StateUpgrade)
sg = game.UpgradeGroupPreference(sg, Cruiser, game.TechDrive, 0)
assert.Nil(t, sg.StateUpgrade)
sg = game.UpgradeGroupPreference(sg, Cruiser, game.TechDrive, 2.0)
assert.NotNil(t, sg.StateUpgrade)
assert.Equal(t, 300., sg.StateUpgrade.TechCost(game.TechDrive))
assert.Equal(t, 300., sg.StateUpgrade.Cost())
sg = game.UpgradeGroupPreference(sg, Cruiser, game.TechWeapons, 2.0)
assert.NotNil(t, sg.StateUpgrade)
assert.Equal(t, 300., sg.StateUpgrade.TechCost(game.TechWeapons))
assert.Equal(t, 600., sg.StateUpgrade.Cost())
sg = game.UpgradeGroupPreference(sg, Cruiser, game.TechShields, 2.0)
assert.NotNil(t, sg.StateUpgrade)
assert.Equal(t, 300., sg.StateUpgrade.TechCost(game.TechShields))
assert.Equal(t, 900., sg.StateUpgrade.Cost())
sg = game.UpgradeGroupPreference(sg, Cruiser, game.TechCargo, 2.0)
assert.NotNil(t, sg.StateUpgrade)
assert.Equal(t, 0., sg.StateUpgrade.TechCost(game.TechCargo))
assert.Equal(t, 900., sg.StateUpgrade.Cost())
}
func TestUpgradeGroup(t *testing.T) {
g := newGame()
// group #1 - in_orbit, free to upgrade
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 10))
// group #2 - in_space
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
g.ShipGroups[1].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23}
// group #3 - in_orbit, foreign planet
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
g.ShipGroups[2].Destination = R1_Planet_1_num
assert.ErrorContains(t,
g.UpgradeGroup("UnknownRace", 1, "DRIVE", 0, 0),
e.GenericErrorText(e.ErrInputUnknownRace))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 555, "DRIVE", 0, 0),
e.GenericErrorText(e.ErrInputEntityNotExists))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 2, "DRIVE", 0, 0),
e.GenericErrorText(e.ErrShipsBusy))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 3, "DRIVE", 0, 0),
e.GenericErrorText(e.ErrInputEntityNotOwned))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 1, "GUN", 0, 0),
e.GenericErrorText(e.ErrInputTechUnknown))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 1, "CARGO", 0, 0),
e.GenericErrorText(e.ErrInputUpgradeShipTechNotUsed))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 1, "ALL", 0, 2.0),
e.GenericErrorText(e.ErrInputUpgradeParameterNotAllowed))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 1, "DRIVE", 0, 2.0),
e.GenericErrorText(e.ErrInputUpgradeTechLevelInsufficient))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 1, "DRIVE", 0, 1.1),
e.GenericErrorText(e.ErrInputUpgradeShipsAlreadyUpToDate))
g.Race[Race_0_idx].SetTechLevel(game.TechDrive, 10.0)
assert.Equal(t, 10.0, g.Race[Race_0_idx].TechLevel(game.TechDrive))
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 1, "DRIVE", 0, 10.0),
e.GenericErrorText(e.ErrUpgradeInsufficientResources))
assert.NoError(t, g.UpgradeGroup(Race_0.Name, 1, "DRIVE", 2, 1.2))
assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4)
assert.Equal(t, uint(8), g.ShipGroups[0].Number)
assert.Equal(t, uint(2), g.ShipGroups[3].Number)
assert.Equal(t, game.StateInOrbit, g.ShipGroups[0].State())
assert.Equal(t, game.StateUpgrade, g.ShipGroups[3].State())
assert.ErrorContains(t,
g.UpgradeGroup(Race_0.Name, 4, "DRIVE", 1, 1.3),
e.GenericErrorText(e.ErrInputUpgradeGroupBreakNotAllowed))
}
+24 -7
View File
@@ -25,10 +25,10 @@ type UninhabitedPlanet struct {
type PlanetReport struct {
UninhabitedPlanet
Industry float64 `json:"industry"` // I - Промышленность
Population float64 `json:"population"` // P - Население
Colonists float64 `json:"colonists"` // COL C - Количество колонистов
Production ProductionType `json:"production"` // TODO: internal/report format
Industry float64 `json:"industry"` // I - Промышленность
Population float64 `json:"population"` // P - Население
Colonists float64 `json:"colonists"` // COL C - Количество колонистов
Production Production `json:"production"` // TODO: internal/report format
// Параметр "L" - Свободный производственный потенциал
}
@@ -42,13 +42,30 @@ type PlanetReportForeign struct {
PlanetReport
}
// Свободный производственный потенциал (L)
// промышленность * 0.75 + население * 0.25
// TODO: за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
// TODO: delete func
func (p Planet) ProductionCapacity() float64 {
return p.Industry*0.75 + p.Population*0.25
}
// Свободный производственный потенциал (L)
// промышленность * 0.75 + население * 0.25
// за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
func PlanetProductionCapacity(g *Game, planetNumber uint) float64 {
p, err := g.PlanetByNumber(planetNumber)
if err != nil {
panic(err)
}
var busyResources float64
for sg := range g.ShipsInUpgrade(p.Number) {
busyResources += sg.StateUpgrade.Cost()
}
return PlanetProduction(p.Industry, p.Population) - busyResources
}
func PlanetProduction(industry, population float64) float64 {
return industry*0.75 + population*0.25
}
// Производство промышленности
// TODO: test on real values
func (p *Planet) IncreaseIndustry() {
+22
View File
@@ -0,0 +1,22 @@
package game_test
import (
"testing"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert"
)
func TestPlanetProduction(t *testing.T) {
assert.Equal(t, 1000., game.PlanetProduction(1000., 1000.))
assert.Equal(t, 750., game.PlanetProduction(1000., 0.))
assert.Equal(t, 250., game.PlanetProduction(0., 1000.))
}
func TestPlanetProductionCapacity(t *testing.T) {
g := newGame()
assert.NoError(t, g.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1))
assert.Equal(t, 100., game.PlanetProductionCapacity(g, R0_Planet_0_num))
g.ShipGroups[0] = game.UpgradeGroupPreference(g.ShipGroups[0], Cruiser, game.TechDrive, 1.6)
assert.Equal(t, 53.125, game.PlanetProductionCapacity(g, R0_Planet_0_num))
}
+23 -23
View File
@@ -7,34 +7,34 @@ import (
e "github.com/iliadenisov/galaxy/internal/error"
)
type PlanetProduction string
type ProductionType string
const (
ProductionNone PlanetProduction = "-"
ProductionMaterial PlanetProduction = "MAT" // Сырьё
ProductionCapital PlanetProduction = "CAP" // Промышленность
ProductionNone ProductionType = "-"
ProductionMaterial ProductionType = "MAT" // Сырьё
ProductionCapital ProductionType = "CAP" // Промышленность
ResearchDrive PlanetProduction = "DRIVE"
ResearchWeapons PlanetProduction = "WEAPONS"
ResearchShields PlanetProduction = "SHIELDS"
ResearchCargo PlanetProduction = "CARGO"
ResearchDrive ProductionType = "DRIVE"
ResearchWeapons ProductionType = "WEAPONS"
ResearchShields ProductionType = "SHIELDS"
ResearchCargo ProductionType = "CARGO"
ResearchScience PlanetProduction = "SCIENCE"
ProductionShip PlanetProduction = "SHIP"
ResearchScience ProductionType = "SCIENCE"
ProductionShip ProductionType = "SHIP"
)
type ProductionType struct {
Production PlanetProduction `json:"type"`
SubjectID *uuid.UUID `json:"subjectId"`
Progress *float64 `json:"progress"`
type Production struct {
Type ProductionType `json:"type"`
SubjectID *uuid.UUID `json:"subjectId"`
Progress *float64 `json:"progress"`
}
func (p PlanetProduction) AsType(subject uuid.UUID) ProductionType {
func (p ProductionType) AsType(subject uuid.UUID) Production {
switch p {
case ResearchScience, ProductionShip:
return ProductionType{Production: p, SubjectID: &subject}
return Production{Type: p, SubjectID: &subject}
default:
return ProductionType{Production: p, SubjectID: nil}
return Production{Type: p, SubjectID: nil}
}
}
@@ -43,8 +43,8 @@ func (g Game) PlanetProduction(raceName string, planetNumber int, prodType, subj
if err != nil {
return err
}
var prod PlanetProduction
switch PlanetProduction(prodType) {
var prod ProductionType
switch ProductionType(prodType) {
case ProductionMaterial:
prod = ProductionMaterial
case ProductionCapital:
@@ -67,7 +67,7 @@ func (g Game) PlanetProduction(raceName string, planetNumber int, prodType, subj
return g.planetProductionInternal(ri, planetNumber, prod, subject)
}
func (g Game) planetProductionInternal(ri int, number int, prod PlanetProduction, subj string) error {
func (g Game) planetProductionInternal(ri int, number int, prod ProductionType, subj string) error {
if number < 0 {
return e.NewPlanetNumberError(number)
}
@@ -95,7 +95,7 @@ func (g Game) planetProductionInternal(ri int, number int, prod PlanetProduction
if i < 0 {
return e.NewEntityNotExistsError("ship type %w", subj)
}
if g.Map.Planet[i].Production.Production == ProductionShip &&
if g.Map.Planet[i].Production.Type == ProductionShip &&
g.Map.Planet[i].Production.SubjectID != nil &&
*g.Map.Planet[i].Production.SubjectID == g.Race[ri].ShipTypes[i].ID {
// Planet already produces this ship type, keeping progress intact
@@ -105,7 +105,7 @@ func (g Game) planetProductionInternal(ri int, number int, prod PlanetProduction
var progress float64 = 0.
g.Map.Planet[i].Production.Progress = &progress
}
if g.Map.Planet[i].Production.Production == ProductionShip {
if g.Map.Planet[i].Production.Type == ProductionShip {
if g.Map.Planet[i].Production.SubjectID == nil {
return e.NewGameStateError("planet #%d produces ship but SubjectID is empty", g.Map.Planet[i].Number)
}
@@ -122,7 +122,7 @@ func (g Game) planetProductionInternal(ri int, number int, prod PlanetProduction
extra := mat * progress
g.Map.Planet[i].Material += extra
}
g.Map.Planet[i].Production.Production = prod
g.Map.Planet[i].Production.Type = prod
g.Map.Planet[i].Production.SubjectID = subjectID
return nil
}
+15 -7
View File
@@ -1,6 +1,8 @@
package game
import "github.com/google/uuid"
import (
"github.com/google/uuid"
)
type Race struct {
ID uuid.UUID `json:"id"`
@@ -10,10 +12,7 @@ type Race struct {
Vote uuid.UUID `json:"vote"`
Relations []RaceRelation `json:"relations"`
Drive float64 `json:"drive"`
Weapons float64 `json:"weapons"`
Shields float64 `json:"shields"`
Cargo float64 `json:"cargo"`
Tech TechSet `json:"tech"`
Sciences []Science `json:"science,omitempty"`
@@ -32,10 +31,19 @@ type RaceRelation struct {
Relation Relation `json:"relation"`
}
func (r Race) TechLevel(t Tech) float64 {
return r.Tech.Value(t)
}
// TODO: refactor to separate method with *Race as parameter
func (r *Race) SetTechLevel(t Tech, v float64) {
r.Tech = r.Tech.Set(t, v)
}
func (r Race) FlightDistance() float64 {
return r.Drive * 40
return r.TechLevel(TechDrive) * 40
}
func (r Race) VisibilityDistance() float64 {
return r.Drive * 30
return r.TechLevel(TechDrive) * 30
}
+1 -1
View File
@@ -51,7 +51,7 @@ func (g Game) deleteScienceInternal(ri int, name string) error {
return e.NewEntityNotExistsError("science %w", name)
}
if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool {
return p.Production.Production == ResearchScience &&
return p.Production.Type == ResearchScience &&
p.Production.SubjectID != nil &&
*p.Production.SubjectID == g.Race[ri].Sciences[sc].ID
}); pl >= 0 {
+33 -6
View File
@@ -35,18 +35,45 @@ func (st ShipType) Equal(o ShipType) bool {
st.Cargo == o.Cargo
}
func (st ShipType) EmptyMass() float64 {
shipMass := st.Drive + st.Shields + st.Cargo + st.WeaponsMass()
return shipMass
func (st ShipType) BlockMass(t Tech) float64 {
switch t {
case TechDrive:
return st.DriveBlockMass()
case TechWeapons:
return st.WeaponsBlockMass()
case TechShields:
return st.ShieldsBlockMass()
case TechCargo:
return st.CargoBlockMass()
default:
panic("BlockMass: unexpectec tech: " + t.String())
}
}
func (st ShipType) WeaponsMass() float64 {
func (st ShipType) DriveBlockMass() float64 {
return st.Drive
}
func (st ShipType) WeaponsBlockMass() float64 {
if st.Armament == 0 || st.Weapons == 0 {
return 0
}
return float64(st.Armament+1) * (st.Weapons / 2)
}
func (st ShipType) ShieldsBlockMass() float64 {
return st.Shields
}
func (st ShipType) CargoBlockMass() float64 {
return st.Cargo
}
func (st ShipType) EmptyMass() float64 {
shipMass := st.DriveBlockMass() + st.ShieldsBlockMass() + st.CargoBlockMass() + st.WeaponsBlockMass()
return shipMass
}
// ProductionCost returns Material (MAT) and Population (POP) to produce this [ShipType]
func (st ShipType) ProductionCost() (mat float64, pop float64) {
mat = st.EmptyMass()
@@ -89,7 +116,7 @@ func (g Game) deleteShipTypeInternal(ri int, name string) error {
return e.NewEntityNotExistsError("ship type %w", name)
}
if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool {
return p.Production.Production == ProductionShip &&
return p.Production.Type == ProductionShip &&
p.Production.SubjectID != nil &&
g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID
}); pl >= 0 {
@@ -160,7 +187,7 @@ func (g Game) mergeShipTypeInternal(ri int, name, targetName string) error {
// switch planet productions to the new type
for pl := range g.Map.Planet {
if g.Map.Planet[pl].Owner == g.Race[ri].ID &&
g.Map.Planet[pl].Production.Production == ProductionShip &&
g.Map.Planet[pl].Production.Type == ProductionShip &&
g.Map.Planet[pl].Production.SubjectID != nil &&
*g.Map.Planet[pl].Production.SubjectID == g.Race[ri].ShipTypes[st].ID {
g.Map.Planet[pl].Production.SubjectID = &g.Race[ri].ShipTypes[tt].ID