From 33efa860657c6ba913dfadc360be6cf3d1560bea Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Thu, 2 Oct 2025 23:22:44 +0300 Subject: [PATCH] cmd: merge ship types --- pkg/error/generic.go | 6 ++++ pkg/error/input.go | 8 +++++ pkg/game/cmd_ship_type.go | 16 ++++++++++ pkg/game/cmd_ship_type_test.go | 24 +++++++++++++++ pkg/model/game/ship.go | 55 ++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+) diff --git a/pkg/error/generic.go b/pkg/error/generic.go index 5bbaf83..1c2761e 100644 --- a/pkg/error/generic.go +++ b/pkg/error/generic.go @@ -13,12 +13,14 @@ const ( ErrDeleteShipTypeExistingGroup = 5000 ErrDeleteShipTypePlanetProduction = 5001 ErrDeleteSciencePlanetProduction = 5002 + ErrMergeShipTypeNotEqual = 5003 ) const ( ErrInputUnknownRace int = 3000 + iota ErrInputEntityTypeNameInvalid ErrInputEntityTypeNameDuplicate + ErrInputEntityTypeNameEquality ErrInputEntityNotExists ErrInputEntityNotOwned ErrInputPlanetNumber @@ -47,6 +49,8 @@ func GenericErrorText(code int) string { return "Name has invalid length or symbols" case ErrInputEntityTypeNameDuplicate: return "Name already exists" + case ErrInputEntityTypeNameEquality: + return "Names should differ" case ErrInputEntityNotExists: return "Entity does not exists" case ErrInputEntityNotOwned: @@ -77,6 +81,8 @@ func GenericErrorText(code int) string { return "Science proportions sum should be equal 1" case ErrInputProductionInvalid: return "Invalid Production type" + case ErrMergeShipTypeNotEqual: + return "Source and target ship types are not the same" default: return fmt.Sprintf("Undescribed error with code %d", code) } diff --git a/pkg/error/input.go b/pkg/error/input.go index 22a2128..1abd291 100644 --- a/pkg/error/input.go +++ b/pkg/error/input.go @@ -12,6 +12,10 @@ func NewEntityTypeNameDuplicateError(arg ...any) error { return newGenericError(ErrInputEntityTypeNameDuplicate, arg...) } +func NewEntityTypeNameEqualityError(arg ...any) error { + return newGenericError(ErrInputEntityTypeNameEquality, arg...) +} + func NewEntityNotExistsError(arg ...any) error { return newGenericError(ErrInputEntityNotExists, arg...) } @@ -59,3 +63,7 @@ func NewScienceSumValuesError(arg ...any) error { func NewProductionInvalidError(arg ...any) error { return newGenericError(ErrInputProductionInvalid, arg...) } + +func NewMergeShipTypeNotEqualError(arg ...any) error { + return newGenericError(ErrMergeShipTypeNotEqual, arg...) +} diff --git a/pkg/game/cmd_ship_type.go b/pkg/game/cmd_ship_type.go index 6954471..d2f6fe3 100644 --- a/pkg/game/cmd_ship_type.go +++ b/pkg/game/cmd_ship_type.go @@ -18,6 +18,22 @@ func createShipType(r Repo, g game.Game, race, typeName string, d, w, s, c float return r.SaveState(g) } +func MergeShipType(configure func(*Param), race, source, target string) (err error) { + control(configure, func(c *ctrl) { + c.execute(func(r Repo, g game.Game) { + err = mergeShipType(r, g, race, source, target) + }) + }) + return +} + +func mergeShipType(r Repo, g game.Game, race, source, target string) error { + if err := g.MergeShipType(race, source, target); err != nil { + return err + } + return r.SaveState(g) +} + func DeleteShipType(configure func(*Param), race, typeName string) (err error) { control(configure, func(c *ctrl) { c.execute(func(r Repo, g game.Game) { diff --git a/pkg/game/cmd_ship_type_test.go b/pkg/game/cmd_ship_type_test.go index 70ed4ce..d633994 100644 --- a/pkg/game/cmd_ship_type_test.go +++ b/pkg/game/cmd_ship_type_test.go @@ -91,3 +91,27 @@ func TestCreateShipTypeValidation(t *testing.T) { } }) } + +func TestMergeShipType(t *testing.T) { + race := "race_01" + g(t, func(p func(*game.Param), g func() mg.Game) { + err := game.CreateShipType(p, race, "Drone", 1, 0, 0, 0, 0) + assert.NoError(t, err) + err = game.CreateShipType(p, race, "Spy", 1, 0, 0, 0, 0) + assert.NoError(t, err) + err = game.CreateShipType(p, race, "Cruiser", 15, 15, 15, 0, 1) + assert.NoError(t, err) + err = game.MergeShipType(p, race, "Sky", "Drone") + assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) + err = game.MergeShipType(p, race, "Spy", "Freighter") + assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) + err = game.MergeShipType(p, race, "Spy", "Drone") + assert.NoError(t, err) + st, err := g().ShipTypes(race) + assert.NoError(t, err) + assert.Len(t, st, 2) + err = game.MergeShipType(p, race, "Drone", "Cruiser") + assert.ErrorContains(t, err, e.GenericErrorText(e.ErrMergeShipTypeNotEqual)) + // TODO: test group/production changed + }) +} diff --git a/pkg/model/game/ship.go b/pkg/model/game/ship.go index a0b819a..7bf0f33 100644 --- a/pkg/model/game/ship.go +++ b/pkg/model/game/ship.go @@ -44,6 +44,14 @@ type Fleet struct { ShipGroups []ShipGroup `json:"group"` } +func (st ShipType) Equal(o ShipType) bool { + return st.Drive == o.Drive && + st.Weapons == o.Weapons && + st.Armament == o.Armament && + st.Shields == o.Shields && + st.Cargo == o.Cargo +} + func (st ShipType) EmptyMass() float64 { shipMass := st.Drive + st.Shields + st.Cargo + st.WeaponsMass() return shipMass @@ -197,6 +205,53 @@ func (g Game) createShipTypeInternal(ri int, name string, d, w, s, c float64, a return nil } +func (g Game) MergeShipType(race, name, targetName string) error { + ri, err := g.raceIndex(race) + if err != nil { + return err + } + return g.mergeShipTypeInternal(ri, name, targetName) +} + +func (g Game) mergeShipTypeInternal(ri int, name, targetName string) error { + st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }) + if st < 0 { + return e.NewEntityNotExistsError("source ship type %w", name) + } + if name == targetName { + return e.NewEntityTypeNameEqualityError("ship type %q", targetName) + } + tt := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == targetName }) + if tt < 0 { + return e.NewEntityNotExistsError("target ship type %w", name) + } + if !g.Race[ri].ShipTypes[st].Equal(g.Race[ri].ShipTypes[tt]) { + return e.NewMergeShipTypeNotEqualError() + } + + // 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.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 + } + } + + // switch ship groups to the new type + for sg := range g.Race[ri].ShipGroups { + if g.Race[ri].ShipGroups[sg].TypeID == g.Race[ri].ShipTypes[st].ID { + g.Race[ri].ShipGroups[sg].TypeID = g.Race[ri].ShipTypes[tt].ID + } + } + + // remove the source type + g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...) + + return nil +} + func checkShipTypeValues(d, w, s, c float64, a int) error { if !checkShipTypeValueDWSC(d) { return e.NewDriveValueError(d)