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
+7 -5
View File
@@ -60,10 +60,12 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) {
ID: raceID, ID: raceID,
Name: races[i], Name: races[i],
Vote: raceID, Vote: raceID,
Drive: 1, Tech: map[game.Tech]float64{
Weapons: 1, game.TechDrive: 1,
Shields: 1, game.TechWeapons: 1,
Cargo: 1, game.TechShields: 1,
game.TechCargo: 1,
},
} }
gameMap.Planet = append(gameMap.Planet, NewPlanet( gameMap.Planet = append(gameMap.Planet, NewPlanet(
planetCount, planetCount,
@@ -125,7 +127,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) {
return g, nil return g, nil
} }
func NewPlanet(num uint, name string, owner uuid.UUID, x, y, size, pop, ind, res float64, prod game.ProductionType) game.Planet { func NewPlanet(num uint, name string, owner uuid.UUID, x, y, size, pop, ind, res float64, prod game.Production) game.Planet {
return game.Planet{ return game.Planet{
Owner: owner, Owner: owner,
PlanetReport: game.PlanetReport{ PlanetReport: game.PlanetReport{
+27
View File
@@ -20,6 +20,8 @@ const (
ErrShipsBusy = 5007 ErrShipsBusy = 5007
ErrShipsNotOnSamePlanet = 5008 ErrShipsNotOnSamePlanet = 5008
ErrGiveawayGroupShipsTypeNotEqual = 5009 ErrGiveawayGroupShipsTypeNotEqual = 5009
ErrUpgradeGroupNumberNotEnough = 5010
ErrUpgradeInsufficientResources = 5011
) )
const ( const (
@@ -49,6 +51,13 @@ const (
ErrInputCargoUnloadEmpty ErrInputCargoUnloadEmpty
ErrInputCargoUnoadNotEnough ErrInputCargoUnoadNotEnough
ErrInputBreakGroupIllegalNumber ErrInputBreakGroupIllegalNumber
ErrInputTechUnknown
ErrInputTechInvalidMixing
ErrInputUpgradeShipTechNotUsed
ErrInputUpgradeParameterNotAllowed
ErrInputUpgradeShipsAlreadyUpToDate
ErrInputUpgradeGroupBreakNotAllowed
ErrInputUpgradeTechLevelInsufficient
) )
func GenericErrorText(code int) string { func GenericErrorText(code int) string {
@@ -131,6 +140,24 @@ func GenericErrorText(code int) string {
return "Ships not on the same planet" return "Ships not on the same planet"
case ErrGiveawayGroupShipsTypeNotEqual: case ErrGiveawayGroupShipsTypeNotEqual:
return "Ship type already defined with different specifications" return "Ship type already defined with different specifications"
case ErrInputTechUnknown:
return "Technology name unknown"
case ErrInputTechInvalidMixing:
return "Technologies list must containt only specific values"
case ErrInputUpgradeShipTechNotUsed:
return "Technology is not used with ship class"
case ErrInputUpgradeParameterNotAllowed:
return "Parameter not allowed for upgrade"
case ErrInputUpgradeShipsAlreadyUpToDate:
return "Ships already up to date, nothing to upgrade"
case ErrUpgradeGroupNumberNotEnough:
return "Not enough ships in the group to make an upgrade"
case ErrUpgradeInsufficientResources:
return "Insufficient planet production capacity"
case ErrInputUpgradeGroupBreakNotAllowed:
return "The Group is already in upgrade state and can't be divided to a smaller group"
case ErrInputUpgradeTechLevelInsufficient:
return "Insifficient Tech level for requested upgrade"
default: default:
return fmt.Sprintf("Undescribed error with code %d", code) return fmt.Sprintf("Undescribed error with code %d", code)
} }
+36
View File
@@ -131,3 +131,39 @@ func NewShipsNotOnSamePlanetError(arg ...any) error {
func NewGiveawayGroupShipsTypeNotEqualError(arg ...any) error { func NewGiveawayGroupShipsTypeNotEqualError(arg ...any) error {
return newGenericError(ErrGiveawayGroupShipsTypeNotEqual, arg...) return newGenericError(ErrGiveawayGroupShipsTypeNotEqual, arg...)
} }
func NewTechUnknownError(arg ...any) error {
return newGenericError(ErrInputTechUnknown, arg...)
}
func NewTechInvalidMixingError(arg ...any) error {
return newGenericError(ErrInputTechInvalidMixing, arg...)
}
func NewUpgradeShipTechNotUsedError(arg ...any) error {
return newGenericError(ErrInputUpgradeShipTechNotUsed, arg...)
}
func NewUpgradeParameterNotAllowedError(arg ...any) error {
return newGenericError(ErrInputUpgradeParameterNotAllowed, arg...)
}
func NewUpgradeShipsAlreadyUpToDateError(arg ...any) error {
return newGenericError(ErrInputUpgradeShipsAlreadyUpToDate, arg...)
}
func NewUpgradeGroupNumberNotEnoughError(arg ...any) error {
return newGenericError(ErrUpgradeGroupNumberNotEnough, arg...)
}
func NewUpgradeInsufficientResourcesError(arg ...any) error {
return newGenericError(ErrUpgradeInsufficientResources, arg...)
}
func NewUpgradeGroupBreakNotAllowedError(arg ...any) error {
return newGenericError(ErrInputUpgradeGroupBreakNotAllowed, arg...)
}
func NewUpgradeTechLevelInsufficientError(arg ...any) error {
return newGenericError(ErrInputUpgradeTechLevelInsufficient, arg...)
}
+1 -1
View File
@@ -70,7 +70,7 @@ func (g *Game) joinShipGroupToFleetInternal(ri int, fleetName string, group, cou
return e.NewEntityNotExistsError("group #%d", group) 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() return e.NewShipsBusyError()
} }
+8 -4
View File
@@ -5,11 +5,12 @@ import (
"testing" "testing"
e "github.com/iliadenisov/galaxy/internal/error" e "github.com/iliadenisov/galaxy/internal/error"
"github.com/iliadenisov/galaxy/internal/model/game"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestJoinShipGroupToFleet(t *testing.T) { func TestJoinShipGroupToFleet(t *testing.T) {
g := copyGame() g := newGame()
var groupIndex uint = 1 var groupIndex uint = 1
assert.ErrorContains(t, assert.ErrorContains(t,
@@ -85,11 +86,14 @@ func TestJoinShipGroupToFleet(t *testing.T) {
// group not In_Orbit // group not In_Orbit
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7)) assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7))
gi = 3 gi = 3
g.ShipGroups[gi].State = "In_Space" g.ShipGroups[gi].StateInSpace = &game.InSpace{
Origin: 2,
Range: 1,
}
assert.ErrorContains(t, assert.ErrorContains(t,
g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0), g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0),
e.GenericErrorText(e.ErrShipsBusy)) 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 // existing fleet not on the same planet or in_orbit
g.Fleets[0].Destination = R0_Planet_2_num g.Fleets[0].Destination = R0_Planet_2_num
@@ -100,7 +104,7 @@ func TestJoinShipGroupToFleet(t *testing.T) {
} }
func TestJoinFleets(t *testing.T) { func TestJoinFleets(t *testing.T) {
g := copyGame() g := newGame()
// creating ShipGroup at Planet_0 // creating ShipGroup at Planet_0
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1 assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1
// creating ShipGroup at Planet_2 // creating ShipGroup at Planet_2
+39
View File
@@ -2,6 +2,9 @@ package game
import ( import (
"encoding/json" "encoding/json"
"fmt"
"iter"
"maps"
"slices" "slices"
"strings" "strings"
@@ -9,6 +12,22 @@ import (
e "github.com/iliadenisov/galaxy/internal/error" 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 { type Game struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Age uint `json:"turn"` // Game's turn number Age uint `json:"turn"` // Game's turn number
@@ -29,6 +48,26 @@ func (g Game) Votes(raceID uuid.UUID) float64 {
return pop / 1000. 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) { func (g Game) raceIndex(name string) (int, error) {
i := slices.IndexFunc(g.Race, func(r Race) bool { return r.Name == name }) i := slices.IndexFunc(g.Race, func(r Race) bool { return r.Name == name })
if i < 0 { if i < 0 {
+46 -35
View File
@@ -12,35 +12,24 @@ var (
Race_0 = game.Race{ Race_0 = game.Race{
ID: uuid.New(), ID: uuid.New(),
Name: "Race_0", Name: "Race_0",
Drive: 1.1, Tech: map[game.Tech]float64{
Weapons: 1.2, game.TechDrive: 1.1,
Shields: 1.3, game.TechWeapons: 1.2,
Cargo: 1.4, game.TechShields: 1.3,
game.TechCargo: 1.4,
},
} }
Race_1 = game.Race{ Race_1 = game.Race{
ID: uuid.New(), ID: uuid.New(),
Name: "Race_1", Name: "Race_1",
Drive: 2.1, Tech: map[game.Tech]float64{
Weapons: 2.2, game.TechDrive: 2.1,
Shields: 2.3, game.TechWeapons: 2.2,
Cargo: 2.4, game.TechShields: 2.3,
} game.TechCargo: 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)),
}, },
} }
Game = &game.Game{
Race: []game.Race{
Race_0,
Race_1,
},
Map: Map,
}
Race_0_idx = 0 Race_0_idx = 0
Race_0_Gunship = "R0_Gunship" Race_0_Gunship = "R0_Gunship"
Race_0_Freighter = "R0_Freighter" Race_0_Freighter = "R0_Freighter"
@@ -59,17 +48,18 @@ var (
Race_1_Cruiser_idx = 2 Race_1_Cruiser_idx = 2
ShipType_Cruiser = "Cruiser" ShipType_Cruiser = "Cruiser"
)
func init() { Cruiser = game.ShipType{
assertNoError(Game.CreateShipType(Race_0.Name, Race_0_Gunship, 60, 30, 100, 0, 3)) ShipTypeReport: game.ShipTypeReport{
assertNoError(Game.CreateShipType(Race_0.Name, Race_0_Freighter, 8, 0, 2, 10, 0)) Name: "Cruiser",
assertNoError(Game.CreateShipType(Race_0.Name, ShipType_Cruiser, 15, 15, 15, 0, 1)) Drive: 15,
Armament: 1,
assertNoError(Game.CreateShipType(Race_1.Name, Race_1_Gunship, 60, 30, 100, 0, 3)) Weapons: 15,
assertNoError(Game.CreateShipType(Race_1.Name, Race_1_Freighter, 8, 0, 2, 10, 0)) Shields: 15,
assertNoError(Game.CreateShipType(Race_1.Name, ShipType_Cruiser, 15, 15, 15, 0, 2)) // same name - different type Cargo: 0,
},
} }
)
func assertNoError(err error) { func assertNoError(err error) {
if err != nil { if err != nil {
@@ -77,7 +67,28 @@ func assertNoError(err error) {
} }
} }
func copyGame() *game.Game { func newGame() *game.Game {
g := *Game g := &game.Game{
return &g 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
} }
+113 -37
View File
@@ -1,6 +1,7 @@
package game package game
import ( import (
"fmt"
"iter" "iter"
"maps" "maps"
"math" "math"
@@ -35,50 +36,128 @@ func (ct CargoType) String() string {
return string(ct) 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 { type ShipGroup struct {
Index uint `json:"index"` // FIXME: use UUID for Group Index (ordered) Index uint `json:"index"` // FIXME: use UUID for Group Index (ordered)
OwnerID uuid.UUID `json:"ownerId"` // Race link OwnerID uuid.UUID `json:"ownerId"` // Race link
TypeID uuid.UUID `json:"typeId"` // ShipType link TypeID uuid.UUID `json:"typeId"` // ShipType link
FleetID *uuid.UUID `json:"fleetId,omitempty"` // Fleet link FleetID *uuid.UUID `json:"fleetId,omitempty"` // Fleet link
Number uint `json:"number"` // Number (quantity) ships of specific ShipType 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"` CargoType *CargoType `json:"loadType,omitempty"`
Load float64 `json:"load"` // Cargo loaded - "Масса груза" Load float64 `json:"load"` // Cargo loaded - "Масса груза"
Drive float64 `json:"drive"` Tech TechSet `json:"tech"`
Weapons float64 `json:"weapons"`
Shields float64 `json:"shields"`
Cargo float64 `json:"cargo"`
// TODO: TEST: Destination, Origin, Range // TODO: TEST: Destination, Origin, Range
Destination uint `json:"destination"` Destination uint `json:"destination"`
Origin *uint `json:"origin,omitempty"` StateInSpace *InSpace `json:"stateInSpace,omitempty"`
Range *float64 `json:"range,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 { func (sg ShipGroup) Equal(other ShipGroup) bool {
return sg.OwnerID == other.OwnerID && return sg.OwnerID == other.OwnerID &&
sg.TypeID == other.TypeID && sg.TypeID == other.TypeID &&
sg.FleetID == other.FleetID && sg.FleetID == other.FleetID &&
sg.Drive == other.Drive && sg.TechLevel(TechDrive) == other.TechLevel(TechDrive) &&
sg.Weapons == other.Weapons && sg.TechLevel(TechWeapons) == other.TechLevel(TechWeapons) &&
sg.Shields == other.Shields && sg.TechLevel(TechShields) == other.TechLevel(TechShields) &&
sg.Cargo == other.Cargo && sg.TechLevel(TechCargo) == other.TechLevel(TechCargo) &&
sg.CargoType == other.CargoType && sg.CargoType == other.CargoType &&
sg.Load == other.Load && sg.Load == other.Load &&
sg.State == other.State sg.State() == other.State()
} }
// Грузоподъёмность // Грузоподъёмность
func (sg ShipGroup) CargoCapacity(st *ShipType) float64 { 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 { 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 { 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 { 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 // TODO: test on other values
func (sg ShipGroup) UpgradeWeaponsCost(st *ShipType, weapons float64) float64 { 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 { 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 { 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? // TODO: maybe rounding must be done only for display?
func (sg ShipGroup) BombingPower(st *ShipType) float64 { func (sg ShipGroup) BombingPower(st *ShipType) float64 {
// return math.Sqrt(sg.Type.Weapons * sg.Weapons) // 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 * st.Weapons *
sg.Weapons * sg.TechLevel(TechWeapons) *
float64(st.Armament) * float64(st.Armament) *
float64(sg.Number) float64(sg.Number)
return number.Fixed3(result) return number.Fixed3(result)
@@ -173,7 +252,7 @@ func (g *Game) disassembleGroupInternal(ri int, groupIndex, quantity uint) error
return e.NewEntityNotExistsError("group #%d", groupIndex) 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() return e.NewShipsBusyError()
} }
@@ -257,7 +336,7 @@ func (g *Game) unloadCargoInternal(ri int, groupIndex uint, ships uint, quantity
if sgi < 0 { if sgi < 0 {
return e.NewEntityNotExistsError("group #%d", groupIndex) 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() return e.NewShipsBusyError()
} }
var sti int var sti int
@@ -336,7 +415,7 @@ func (g *Game) loadCargoInternal(ri int, groupIndex uint, ct CargoType, ships ui
if sgi < 0 { if sgi < 0 {
return e.NewEntityNotExistsError("group #%d", groupIndex) 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() return e.NewShipsBusyError()
} }
pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == g.ShipGroups[sgi].Destination }) 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, OwnerID: g.Race[riAccept].ID,
TypeID: g.Race[riAccept].ShipTypes[stAcc].ID, TypeID: g.Race[riAccept].ShipTypes[stAcc].ID,
Number: uint(quantity), Number: uint(quantity),
State: g.ShipGroups[sgi].State,
CargoType: g.ShipGroups[sgi].CargoType, CargoType: g.ShipGroups[sgi].CargoType,
Load: g.ShipGroups[sgi].Load, Load: g.ShipGroups[sgi].Load,
Drive: g.ShipGroups[sgi].Drive, Tech: maps.Clone(g.ShipGroups[sgi].Tech),
Weapons: g.ShipGroups[sgi].Weapons,
Shields: g.ShipGroups[sgi].Shields,
Cargo: g.ShipGroups[sgi].Cargo,
Destination: g.ShipGroups[sgi].Destination, Destination: g.ShipGroups[sgi].Destination,
Origin: g.ShipGroups[sgi].Origin, StateInSpace: g.ShipGroups[sgi].StateInSpace,
Range: g.ShipGroups[sgi].Range, StateUpgrade: g.ShipGroups[sgi].StateUpgrade,
}) })
if quantity == 0 || quantity == g.ShipGroups[sgi].Number { 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) 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() 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, TypeID: g.Race[ri].ShipTypes[st].ID,
Destination: g.Map.Planet[pl].Number, Destination: g.Map.Planet[pl].Number,
Number: uint(quantity), Number: uint(quantity),
State: "In_Orbit", Tech: map[Tech]float64{
Drive: g.Race[ri].Drive, TechDrive: g.Race[ri].TechLevel(TechDrive),
Weapons: g.Race[ri].Weapons, TechWeapons: g.Race[ri].TechLevel(TechWeapons),
Shields: g.Race[ri].Shields, TechShields: g.Race[ri].TechLevel(TechShields),
Cargo: g.Race[ri].Cargo, TechCargo: g.Race[ri].TechLevel(TechCargo),
},
}) })
return nil return nil
} }
+91 -102
View File
@@ -25,11 +25,12 @@ func TestCargoCapacity(t *testing.T) {
} }
sg := game.ShipGroup{ sg := game.ShipGroup{
Number: 1, Number: 1,
State: "In_Orbit", Tech: map[game.Tech]float64{
Drive: 1.5, game.TechDrive: 1.5,
Weapons: 1.1, game.TechWeapons: 1.1,
Shields: 2.0, game.TechShields: 2.0,
Cargo: 1.0, game.TechCargo: 1.0,
},
} }
assert.Equal(t, expectCapacity, sg.CargoCapacity(&ship)) assert.Equal(t, expectCapacity, sg.CargoCapacity(&ship))
} }
@@ -53,11 +54,12 @@ func TestCarryingAndFullMass(t *testing.T) {
} }
sg := &game.ShipGroup{ sg := &game.ShipGroup{
Number: 1, Number: 1,
State: "In_Orbit", Tech: map[game.Tech]float64{
Drive: 1.0, game.TechDrive: 1.0,
Weapons: 1.0, game.TechWeapons: 1.0,
Shields: 1.0, game.TechShields: 1.0,
Cargo: 1.0, game.TechCargo: 1.0,
},
Load: 0.0, Load: 0.0,
} }
em := Freighter.EmptyMass() em := Freighter.EmptyMass()
@@ -68,7 +70,7 @@ func TestCarryingAndFullMass(t *testing.T) {
assert.Equal(t, 10.0, sg.CarryingMass()) assert.Equal(t, 10.0, sg.CarryingMass())
assert.Equal(t, em+10.0, sg.FullMass(Freighter)) 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, 4.0, sg.CarryingMass())
assert.Equal(t, em+4.0, sg.FullMass(Freighter)) assert.Equal(t, em+4.0, sg.FullMass(Freighter))
} }
@@ -86,20 +88,21 @@ func TestSpeed(t *testing.T) {
} }
sg := &game.ShipGroup{ sg := &game.ShipGroup{
Number: 1, Number: 1,
State: "In_Orbit", Tech: map[game.Tech]float64{
Drive: 1.0, game.TechDrive: 1.0,
Weapons: 1.0, game.TechWeapons: 1.0,
Shields: 1.0, game.TechShields: 1.0,
Cargo: 1.0, game.TechCargo: 1.0,
},
Load: 0.0, Load: 0.0,
} }
assert.Equal(t, 8.0, sg.Speed(Freighter)) assert.Equal(t, 8.0, sg.Speed(Freighter))
sg.Load = 5.0 sg.Load = 5.0
assert.Equal(t, 6.4, sg.Speed(Freighter)) 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)) assert.Equal(t, 9.6, sg.Speed(Freighter))
sg.Load = 10 sg.Load = 10
sg.Cargo = 1.5 sg.SetTechLevel(game.TechCargo, 1.5)
assert.Equal(t, 9.0, sg.Speed(Freighter)) assert.Equal(t, 9.0, sg.Speed(Freighter))
} }
@@ -115,44 +118,18 @@ func TestBombingPower(t *testing.T) {
} }
sg := game.ShipGroup{ sg := game.ShipGroup{
Number: 1, Number: 1,
State: "In_Orbit", Tech: map[game.Tech]float64{
Drive: 1.0, game.TechDrive: 1.0,
Weapons: 1.0, game.TechWeapons: 1.0,
Shields: 1.0, game.TechShields: 1.0,
Cargo: 1.0, game.TechCargo: 1.0,
},
} }
expectedBombingPower := 139.295 expectedBombingPower := 139.295
result := sg.BombingPower(&Gunship) result := sg.BombingPower(&Gunship)
assert.Equal(t, expectedBombingPower, result) 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) { func TestDriveEffective(t *testing.T) {
tc := []struct { tc := []struct {
driveShipType float64 driveShipType float64
@@ -179,11 +156,12 @@ func TestDriveEffective(t *testing.T) {
} }
sg := game.ShipGroup{ sg := game.ShipGroup{
Number: rand.UintN(4) + 1, Number: rand.UintN(4) + 1,
State: "In_Orbit", Tech: map[game.Tech]float64{
Drive: tc[i].driveTech, game.TechDrive: tc[i].driveTech,
Weapons: rand.Float64()*5 + 1, game.TechWeapons: rand.Float64()*5 + 1,
Shields: rand.Float64()*5 + 1, game.TechShields: rand.Float64()*5 + 1,
Cargo: rand.Float64()*5 + 1, game.TechCargo: rand.Float64()*5 + 1,
},
} }
assert.Equal(t, tc[i].expectDriveEffective, sg.DriveEffective(&someShip)) assert.Equal(t, tc[i].expectDriveEffective, sg.DriveEffective(&someShip))
} }
@@ -201,13 +179,14 @@ func TestShipGroupEqual(t *testing.T) {
OwnerID: uuid.New(), OwnerID: uuid.New(),
TypeID: uuid.New(), TypeID: uuid.New(),
FleetID: &fleetId, FleetID: &fleetId,
State: "In_Orbit",
CargoType: &mat, CargoType: &mat,
Load: 123.45, Load: 123.45,
Drive: 1.0, Tech: map[game.Tech]float64{
Weapons: 1.0, game.TechDrive: 1.0,
Shields: 1.0, game.TechWeapons: 1.0,
Cargo: 1.0, game.TechShields: 1.0,
game.TechCargo: 1.0,
},
} }
// essential properties // essential properties
@@ -230,7 +209,10 @@ func TestShipGroupEqual(t *testing.T) {
assert.False(t, left.Equal(right)) assert.False(t, left.Equal(right))
right = *left right = *left
left.State = "In_Space" left.StateInSpace = &game.InSpace{
Origin: 1,
Range: 1,
}
assert.False(t, left.Equal(right)) assert.False(t, left.Equal(right))
right = *left right = *left
@@ -246,19 +228,23 @@ func TestShipGroupEqual(t *testing.T) {
assert.False(t, left.Equal(right)) assert.False(t, left.Equal(right))
right = *left 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)) assert.False(t, left.Equal(right))
right = *left 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)) assert.False(t, left.Equal(right))
right = *left 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)) assert.False(t, left.Equal(right))
right = *left 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)) assert.False(t, left.Equal(right))
// non-essential properties // non-essential properties
@@ -271,7 +257,7 @@ func TestShipGroupEqual(t *testing.T) {
} }
func TestCreateShips(t *testing.T) { func TestCreateShips(t *testing.T) {
g := copyGame() g := newGame()
assert.ErrorContains(t, assert.ErrorContains(t,
g.CreateShips(Race_0_idx, "Unknown_Ship_Type", R0_Planet_0_num, 2), 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) { 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_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)) 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_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)) 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_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_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) 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) 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.NoError(t, g.CreateShips(1, Race_1_Freighter, R1_Planet_1_num, 1))
assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 3) 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) { for sg := range g.ListShipGroups(Race_0_idx) {
switch { 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(7), sg.Number)
assert.Equal(t, uint(2), sg.Index) 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(11), sg.Number)
assert.Equal(t, uint(7), sg.Index) 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(2), sg.Number)
assert.Equal(t, uint(3), sg.Index) 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(13), sg.Number)
assert.Equal(t, uint(6), sg.Index) assert.Equal(t, uint(6), sg.Index)
default: default:
@@ -349,10 +335,13 @@ func TestJoinEqualGroups(t *testing.T) {
} }
func TestBreakGroup(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_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 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" fleet := "R0_Fleet"
assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleet, 1, 0)) 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) { 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_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_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.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.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, "R0_Fleet", 2, 0))
assert.NotNil(t, g.ShipGroups[2].FleetID) assert.NotNil(t, g.ShipGroups[2].FleetID)
g.ShipGroups[2].Origin = &R0_Planet_2_num g.ShipGroups[2].StateInSpace = &game.InSpace{
rng := 31.337 Origin: 2,
g.ShipGroups[2].Range = &rng Range: 31.337,
g.ShipGroups[2].State = "In_Space" }
g.ShipGroups[2].CargoType = game.CargoMaterial.Ref() g.ShipGroups[2].CargoType = game.CargoMaterial.Ref()
g.ShipGroups[2].Load = 1.234 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].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].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.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].CargoType, g.ShipGroups[3].CargoType)
assert.Equal(t, g.ShipGroups[2].Load, g.ShipGroups[3].Load) 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].TechLevel(game.TechDrive), g.ShipGroups[3].TechLevel(game.TechDrive))
assert.Equal(t, g.ShipGroups[2].Weapons, g.ShipGroups[3].Weapons) assert.Equal(t, g.ShipGroups[2].TechLevel(game.TechWeapons), g.ShipGroups[3].TechLevel(game.TechWeapons))
assert.Equal(t, g.ShipGroups[2].Shields, g.ShipGroups[3].Shields) assert.Equal(t, g.ShipGroups[2].TechLevel(game.TechShields), g.ShipGroups[3].TechLevel(game.TechShields))
assert.Equal(t, g.ShipGroups[2].Cargo, g.ShipGroups[3].Cargo) 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].Destination, g.ShipGroups[3].Destination)
assert.Equal(t, g.ShipGroups[2].Origin, g.ShipGroups[3].Origin) assert.Equal(t, g.ShipGroups[2].StateInSpace, g.ShipGroups[3].StateInSpace)
assert.Equal(t, g.ShipGroups[2].Range, g.ShipGroups[3].Range) 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].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].TypeID, g.Race[Race_1_idx].ShipTypes[sti].ID)
assert.Equal(t, g.ShipGroups[3].Number, uint(11)) assert.Equal(t, g.ShipGroups[3].Number, uint(11))
@@ -483,7 +472,7 @@ func TestGiveawayGroup(t *testing.T) {
} }
func TestLoadCargo(t *testing.T) { func TestLoadCargo(t *testing.T) {
g := copyGame() g := newGame()
// 1: idx = 0 / Ready to load // 1: idx = 0 / Ready to load
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) 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 // 3: idx = 2 / In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
g.ShipGroups[2].Origin = &R0_Planet_2_num g.ShipGroups[2].StateInSpace = &game.InSpace{
rng := 31.337 Origin: 2,
g.ShipGroups[2].Range = &rng Range: 31.337,
g.ShipGroups[2].State = "In_Space" }
// 4: idx = 3 / loaded with COL // 4: idx = 3 / loaded with COL
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) 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) { func TestUnloadCargo(t *testing.T) {
g := copyGame() g := newGame()
// 1: idx = 0 / empty // 1: idx = 0 / empty
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) 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 // 3: idx = 2 / In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
g.ShipGroups[2].Origin = &R0_Planet_2_num g.ShipGroups[2].StateInSpace = &game.InSpace{
rng := 31.337 Origin: 2,
g.ShipGroups[2].Range = &rng Range: 31.337,
g.ShipGroups[2].State = "In_Space" }
// 4: idx = 3 / loaded with COL // 4: idx = 3 / loaded with COL
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) 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) { func TestDisassembleGroup(t *testing.T) {
g := copyGame() g := newGame()
// 1: idx = 0 / empty // 1: idx = 0 / empty
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10))
// 2: idx = 1 / In_Space // 2: idx = 1 / In_Space
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7))
g.ShipGroups[1].Origin = &R0_Planet_2_num g.ShipGroups[1].StateInSpace = &game.InSpace{
rng := 31.337 Origin: 2,
g.ShipGroups[1].Range = &rng Range: 31.337,
g.ShipGroups[1].State = "In_Space" }
// 3: idx = 2 / loaded with COL // 3: idx = 2 / loaded with COL
assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) 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))
}
+21 -4
View File
@@ -28,7 +28,7 @@ type PlanetReport struct {
Industry float64 `json:"industry"` // I - Промышленность Industry float64 `json:"industry"` // I - Промышленность
Population float64 `json:"population"` // P - Население Population float64 `json:"population"` // P - Население
Colonists float64 `json:"colonists"` // COL C - Количество колонистов Colonists float64 `json:"colonists"` // COL C - Количество колонистов
Production ProductionType `json:"production"` // TODO: internal/report format Production Production `json:"production"` // TODO: internal/report format
// Параметр "L" - Свободный производственный потенциал // Параметр "L" - Свободный производственный потенциал
} }
@@ -42,13 +42,30 @@ type PlanetReportForeign struct {
PlanetReport PlanetReport
} }
// Свободный производственный потенциал (L) // TODO: delete func
// промышленность * 0.75 + население * 0.25
// TODO: за вычетом затрат, расходуемых в течение хода на модернизацию кораблей
func (p Planet) ProductionCapacity() float64 { func (p Planet) ProductionCapacity() float64 {
return p.Industry*0.75 + p.Population*0.25 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 // TODO: test on real values
func (p *Planet) IncreaseIndustry() { 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))
}
+21 -21
View File
@@ -7,34 +7,34 @@ import (
e "github.com/iliadenisov/galaxy/internal/error" e "github.com/iliadenisov/galaxy/internal/error"
) )
type PlanetProduction string type ProductionType string
const ( const (
ProductionNone PlanetProduction = "-" ProductionNone ProductionType = "-"
ProductionMaterial PlanetProduction = "MAT" // Сырьё ProductionMaterial ProductionType = "MAT" // Сырьё
ProductionCapital PlanetProduction = "CAP" // Промышленность ProductionCapital ProductionType = "CAP" // Промышленность
ResearchDrive PlanetProduction = "DRIVE" ResearchDrive ProductionType = "DRIVE"
ResearchWeapons PlanetProduction = "WEAPONS" ResearchWeapons ProductionType = "WEAPONS"
ResearchShields PlanetProduction = "SHIELDS" ResearchShields ProductionType = "SHIELDS"
ResearchCargo PlanetProduction = "CARGO" ResearchCargo ProductionType = "CARGO"
ResearchScience PlanetProduction = "SCIENCE" ResearchScience ProductionType = "SCIENCE"
ProductionShip PlanetProduction = "SHIP" ProductionShip ProductionType = "SHIP"
) )
type ProductionType struct { type Production struct {
Production PlanetProduction `json:"type"` Type ProductionType `json:"type"`
SubjectID *uuid.UUID `json:"subjectId"` SubjectID *uuid.UUID `json:"subjectId"`
Progress *float64 `json:"progress"` Progress *float64 `json:"progress"`
} }
func (p PlanetProduction) AsType(subject uuid.UUID) ProductionType { func (p ProductionType) AsType(subject uuid.UUID) Production {
switch p { switch p {
case ResearchScience, ProductionShip: case ResearchScience, ProductionShip:
return ProductionType{Production: p, SubjectID: &subject} return Production{Type: p, SubjectID: &subject}
default: 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 { if err != nil {
return err return err
} }
var prod PlanetProduction var prod ProductionType
switch PlanetProduction(prodType) { switch ProductionType(prodType) {
case ProductionMaterial: case ProductionMaterial:
prod = ProductionMaterial prod = ProductionMaterial
case ProductionCapital: case ProductionCapital:
@@ -67,7 +67,7 @@ func (g Game) PlanetProduction(raceName string, planetNumber int, prodType, subj
return g.planetProductionInternal(ri, planetNumber, prod, subject) 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 { if number < 0 {
return e.NewPlanetNumberError(number) return e.NewPlanetNumberError(number)
} }
@@ -95,7 +95,7 @@ func (g Game) planetProductionInternal(ri int, number int, prod PlanetProduction
if i < 0 { if i < 0 {
return e.NewEntityNotExistsError("ship type %w", subj) 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 != nil &&
*g.Map.Planet[i].Production.SubjectID == g.Race[ri].ShipTypes[i].ID { *g.Map.Planet[i].Production.SubjectID == g.Race[ri].ShipTypes[i].ID {
// Planet already produces this ship type, keeping progress intact // 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. var progress float64 = 0.
g.Map.Planet[i].Production.Progress = &progress 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 { if g.Map.Planet[i].Production.SubjectID == nil {
return e.NewGameStateError("planet #%d produces ship but SubjectID is empty", g.Map.Planet[i].Number) 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 extra := mat * progress
g.Map.Planet[i].Material += extra 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 g.Map.Planet[i].Production.SubjectID = subjectID
return nil return nil
} }
+15 -7
View File
@@ -1,6 +1,8 @@
package game package game
import "github.com/google/uuid" import (
"github.com/google/uuid"
)
type Race struct { type Race struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
@@ -10,10 +12,7 @@ type Race struct {
Vote uuid.UUID `json:"vote"` Vote uuid.UUID `json:"vote"`
Relations []RaceRelation `json:"relations"` Relations []RaceRelation `json:"relations"`
Drive float64 `json:"drive"` Tech TechSet `json:"tech"`
Weapons float64 `json:"weapons"`
Shields float64 `json:"shields"`
Cargo float64 `json:"cargo"`
Sciences []Science `json:"science,omitempty"` Sciences []Science `json:"science,omitempty"`
@@ -32,10 +31,19 @@ type RaceRelation struct {
Relation Relation `json:"relation"` 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 { func (r Race) FlightDistance() float64 {
return r.Drive * 40 return r.TechLevel(TechDrive) * 40
} }
func (r Race) VisibilityDistance() float64 { 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) return e.NewEntityNotExistsError("science %w", name)
} }
if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { 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 != nil &&
*p.Production.SubjectID == g.Race[ri].Sciences[sc].ID *p.Production.SubjectID == g.Race[ri].Sciences[sc].ID
}); pl >= 0 { }); pl >= 0 {
+33 -6
View File
@@ -35,18 +35,45 @@ func (st ShipType) Equal(o ShipType) bool {
st.Cargo == o.Cargo st.Cargo == o.Cargo
} }
func (st ShipType) EmptyMass() float64 { func (st ShipType) BlockMass(t Tech) float64 {
shipMass := st.Drive + st.Shields + st.Cargo + st.WeaponsMass() switch t {
return shipMass 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 { if st.Armament == 0 || st.Weapons == 0 {
return 0 return 0
} }
return float64(st.Armament+1) * (st.Weapons / 2) 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] // ProductionCost returns Material (MAT) and Population (POP) to produce this [ShipType]
func (st ShipType) ProductionCost() (mat float64, pop float64) { func (st ShipType) ProductionCost() (mat float64, pop float64) {
mat = st.EmptyMass() mat = st.EmptyMass()
@@ -89,7 +116,7 @@ func (g Game) deleteShipTypeInternal(ri int, name string) error {
return e.NewEntityNotExistsError("ship type %w", name) return e.NewEntityNotExistsError("ship type %w", name)
} }
if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { 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 && p.Production.SubjectID != nil &&
g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID
}); pl >= 0 { }); pl >= 0 {
@@ -160,7 +187,7 @@ func (g Game) mergeShipTypeInternal(ri int, name, targetName string) error {
// switch planet productions to the new type // switch planet productions to the new type
for pl := range g.Map.Planet { for pl := range g.Map.Planet {
if g.Map.Planet[pl].Owner == g.Race[ri].ID && 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 != nil &&
*g.Map.Planet[pl].Production.SubjectID == g.Race[ri].ShipTypes[st].ID { *g.Map.Planet[pl].Production.SubjectID == g.Race[ri].ShipTypes[st].ID {
g.Map.Planet[pl].Production.SubjectID = &g.Race[ri].ShipTypes[tt].ID g.Map.Planet[pl].Production.SubjectID = &g.Race[ri].ShipTypes[tt].ID