From c6e1cb5cdff6035b4049501c9b3a4445748b6873 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Sun, 4 Jan 2026 19:22:06 +0200 Subject: [PATCH] cmd: upgrade group --- internal/controller/generate.go | 18 +- internal/error/generic.go | 27 +++ internal/error/input.go | 36 ++++ internal/model/game/fleet.go | 2 +- internal/model/game/fleet_test.go | 12 +- internal/model/game/game.go | 39 ++++ internal/model/game/game_test.go | 91 ++++---- internal/model/game/group.go | 154 ++++++++++---- internal/model/game/group_test.go | 207 +++++++++--------- internal/model/game/group_upgrade.go | 246 ++++++++++++++++++++++ internal/model/game/group_upgrade_test.go | 169 +++++++++++++++ internal/model/game/planet.go | 31 ++- internal/model/game/planet_test.go | 22 ++ internal/model/game/production.go | 46 ++-- internal/model/game/race.go | 22 +- internal/model/game/science.go | 2 +- internal/model/game/ship.go | 39 +++- 17 files changed, 918 insertions(+), 245 deletions(-) create mode 100644 internal/model/game/group_upgrade.go create mode 100644 internal/model/game/group_upgrade_test.go create mode 100644 internal/model/game/planet_test.go diff --git a/internal/controller/generate.go b/internal/controller/generate.go index ddb42d7..9d51330 100644 --- a/internal/controller/generate.go +++ b/internal/controller/generate.go @@ -57,13 +57,15 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { } relations[i] = game.RaceRelation{RaceID: raceID, Relation: game.RelationWar} g.Race[i] = game.Race{ - ID: raceID, - Name: races[i], - Vote: raceID, - Drive: 1, - Weapons: 1, - Shields: 1, - Cargo: 1, + ID: raceID, + Name: races[i], + Vote: raceID, + Tech: map[game.Tech]float64{ + game.TechDrive: 1, + game.TechWeapons: 1, + game.TechShields: 1, + game.TechCargo: 1, + }, } gameMap.Planet = append(gameMap.Planet, NewPlanet( planetCount, @@ -125,7 +127,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { 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{ Owner: owner, PlanetReport: game.PlanetReport{ diff --git a/internal/error/generic.go b/internal/error/generic.go index 54122a4..3dad8ee 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -20,6 +20,8 @@ const ( ErrShipsBusy = 5007 ErrShipsNotOnSamePlanet = 5008 ErrGiveawayGroupShipsTypeNotEqual = 5009 + ErrUpgradeGroupNumberNotEnough = 5010 + ErrUpgradeInsufficientResources = 5011 ) const ( @@ -49,6 +51,13 @@ const ( ErrInputCargoUnloadEmpty ErrInputCargoUnoadNotEnough ErrInputBreakGroupIllegalNumber + ErrInputTechUnknown + ErrInputTechInvalidMixing + ErrInputUpgradeShipTechNotUsed + ErrInputUpgradeParameterNotAllowed + ErrInputUpgradeShipsAlreadyUpToDate + ErrInputUpgradeGroupBreakNotAllowed + ErrInputUpgradeTechLevelInsufficient ) func GenericErrorText(code int) string { @@ -131,6 +140,24 @@ func GenericErrorText(code int) string { return "Ships not on the same planet" case ErrGiveawayGroupShipsTypeNotEqual: 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: return fmt.Sprintf("Undescribed error with code %d", code) } diff --git a/internal/error/input.go b/internal/error/input.go index 94e6922..2494b17 100644 --- a/internal/error/input.go +++ b/internal/error/input.go @@ -131,3 +131,39 @@ func NewShipsNotOnSamePlanetError(arg ...any) error { func NewGiveawayGroupShipsTypeNotEqualError(arg ...any) error { 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...) +} diff --git a/internal/model/game/fleet.go b/internal/model/game/fleet.go index 2161266..84b4730 100644 --- a/internal/model/game/fleet.go +++ b/internal/model/game/fleet.go @@ -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() } diff --git a/internal/model/game/fleet_test.go b/internal/model/game/fleet_test.go index d40f11e..5762d3b 100644 --- a/internal/model/game/fleet_test.go +++ b/internal/model/game/fleet_test.go @@ -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 diff --git a/internal/model/game/game.go b/internal/model/game/game.go index 4a398b8..e75e6ab 100644 --- a/internal/model/game/game.go +++ b/internal/model/game/game.go @@ -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 { diff --git a/internal/model/game/game_test.go b/internal/model/game/game_test.go index 0a1be9f..be4ebae 100644 --- a/internal/model/game/game_test.go +++ b/internal/model/game/game_test.go @@ -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 } diff --git a/internal/model/game/group.go b/internal/model/game/group.go index 7a131ae..2bff133 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -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 } diff --git a/internal/model/game/group_test.go b/internal/model/game/group_test.go index f81d6a2..d074314 100644 --- a/internal/model/game/group_test.go +++ b/internal/model/game/group_test.go @@ -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)) diff --git a/internal/model/game/group_upgrade.go b/internal/model/game/group_upgrade.go new file mode 100644 index 0000000..ee63d65 --- /dev/null +++ b/internal/model/game/group_upgrade.go @@ -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 +} diff --git a/internal/model/game/group_upgrade_test.go b/internal/model/game/group_upgrade_test.go new file mode 100644 index 0000000..57a3f21 --- /dev/null +++ b/internal/model/game/group_upgrade_test.go @@ -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)) +} diff --git a/internal/model/game/planet.go b/internal/model/game/planet.go index 56c1a62..83b2ee0 100644 --- a/internal/model/game/planet.go +++ b/internal/model/game/planet.go @@ -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() { diff --git a/internal/model/game/planet_test.go b/internal/model/game/planet_test.go new file mode 100644 index 0000000..91cc8a4 --- /dev/null +++ b/internal/model/game/planet_test.go @@ -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)) +} diff --git a/internal/model/game/production.go b/internal/model/game/production.go index 3f11d71..c45b508 100644 --- a/internal/model/game/production.go +++ b/internal/model/game/production.go @@ -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 } diff --git a/internal/model/game/race.go b/internal/model/game/race.go index dcc63b6..58b8729 100644 --- a/internal/model/game/race.go +++ b/internal/model/game/race.go @@ -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 } diff --git a/internal/model/game/science.go b/internal/model/game/science.go index 805df86..884ae92 100644 --- a/internal/model/game/science.go +++ b/internal/model/game/science.go @@ -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 { diff --git a/internal/model/game/ship.go b/internal/model/game/ship.go index 73d0510..10991af 100644 --- a/internal/model/game/ship.go +++ b/internal/model/game/ship.go @@ -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