diff --git a/internal/error/generic.go b/internal/error/generic.go index f3e16e4..eec0a66 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -15,6 +15,9 @@ const ( ErrDeleteSciencePlanetProduction = 5002 ErrMergeShipTypeNotEqual = 5003 ErrJoinFleetGroupNumberNotEnough = 5004 + ErrEntityInUse = 5005 + ErrShipsBusy = 5006 + ErrShipsNotOnSamePlanet = 5007 ) const ( @@ -56,6 +59,8 @@ func GenericErrorText(code int) string { return "Entity does not exists" case ErrInputEntityNotOwned: return "Entity is not owned" + case ErrEntityInUse: + return "Entity currently in use" case ErrInputPlanetNumber: return "Invalid Planet number" case ErrInputDriveValue: @@ -86,6 +91,10 @@ func GenericErrorText(code int) string { return "Source and target ship types are not the same" case ErrJoinFleetGroupNumberNotEnough: return "Not enough ships in the group to join a fleet" + case ErrShipsBusy: + return "Ships currently not in orbit or free to use" + case ErrShipsNotOnSamePlanet: + return "Ships not on the same planet" default: return fmt.Sprintf("Undescribed error with code %d", code) } diff --git a/internal/error/input.go b/internal/error/input.go index 32ffe7b..7ed7286 100644 --- a/internal/error/input.go +++ b/internal/error/input.go @@ -24,6 +24,10 @@ func NewEntityNotOwnedError(arg ...any) error { return newGenericError(ErrInputEntityNotOwned, arg...) } +func NewEntityInUseError(arg ...any) error { + return newGenericError(ErrEntityInUse, arg...) +} + func NewPlanetNumberError(arg ...any) error { return newGenericError(ErrInputPlanetNumber, arg...) } @@ -71,3 +75,11 @@ func NewMergeShipTypeNotEqualError(arg ...any) error { func NewJoinFleetGroupNumberNotEnoughError(arg ...any) error { return newGenericError(ErrJoinFleetGroupNumberNotEnough, arg...) } + +func NewShipsBusyError(arg ...any) error { + return newGenericError(ErrShipsBusy, arg...) +} + +func NewShipsNotOnSamePlanetError(arg ...any) error { + return newGenericError(ErrShipsNotOnSamePlanet, arg...) +} diff --git a/internal/model/game/fleet.go b/internal/model/game/fleet.go index ee115f2..bb6be8e 100644 --- a/internal/model/game/fleet.go +++ b/internal/model/game/fleet.go @@ -13,6 +13,10 @@ type Fleet struct { ID uuid.UUID `json:"id"` OwnerID uuid.UUID `json:"ownerId"` Name string `json:"name"` + + Destination uint `json:"destination"` + Origin *uint `json:"origin,omitempty"` + Range *float64 `json:"range,omitempty"` } // TODO: Hello! Wanna know fleet's speed? Good. Implement & test this func first. @@ -39,6 +43,14 @@ func (g *Game) JoinShipGroupToFleet(raceName, fleetName string, group, count uin return g.joinShipGroupToFleetInternal(ri, fleetName, group, count) } +func (g *Game) JoinFleets(raceName, fleetSourceName, fleetTargetName string) error { + ri, err := g.raceIndex(raceName) + if err != nil { + return err + } + return g.joinFleetsInternal(ri, fleetSourceName, fleetTargetName) +} + func (g *Game) joinShipGroupToFleetInternal(ri int, fleetName string, group, count uint) (err error) { name, ok := validateTypeName(fleetName) if !ok { @@ -58,21 +70,27 @@ 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 { + return e.NewShipsBusyError() + } + if g.ShipGroups[sgi].Number < count { return e.NewJoinFleetGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, count) } fi := g.fleetIndex(ri, name) if fi < 0 { - fi, err = g.createFleet(ri, name) + fi, err = g.createFleet(ri, name, g.ShipGroups[sgi].Destination) if err != nil { return err } + } else { + if g.Fleets[fi].Destination != g.ShipGroups[sgi].Destination || g.Fleets[fi].Origin != nil || g.Fleets[fi].Range != nil { + return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName) + } } - if count > 0 && - g.ShipGroups[sgi].Number != count { - + if count > 0 && g.ShipGroups[sgi].Number != count { newGroup := g.ShipGroups[sgi] newGroup.Number -= count g.ShipGroups[sgi].Number = count @@ -84,11 +102,7 @@ func (g *Game) joinShipGroupToFleetInternal(ri int, fleetName string, group, cou return nil } -func (g Game) fleetIndex(ri int, name string) int { - return slices.IndexFunc(g.Fleets, func(f Fleet) bool { return f.OwnerID == g.Race[ri].ID && f.Name == name }) -} - -func (g *Game) createFleet(ri int, name string) (int, error) { +func (g *Game) createFleet(ri int, name string, planetNumber uint) (int, error) { n, ok := validateTypeName(name) if !ok { return 0, e.NewEntityTypeNameValidationError("%q", n) @@ -97,13 +111,54 @@ func (g *Game) createFleet(ri int, name string) (int, error) { return 0, e.NewEntityTypeNameDuplicateError("fleet %w", g.Fleets[fl].Name) } g.Fleets = append(g.Fleets, Fleet{ - ID: uuid.New(), - OwnerID: g.Race[ri].ID, - Name: n, + ID: uuid.New(), + OwnerID: g.Race[ri].ID, + Name: n, + Destination: planetNumber, }) return len(g.Fleets) - 1, nil } +func (g *Game) joinFleetsInternal(ri int, fleetSourceName, fleetTargetName string) (err error) { + fiSource := g.fleetIndex(ri, fleetSourceName) + if fiSource < 0 { + return e.NewEntityNotExistsError("source fleet %s", fleetSourceName) + } + fiTarget := g.fleetIndex(ri, fleetTargetName) + if fiTarget < 0 { + return e.NewEntityNotExistsError("target fleet %s", fleetTargetName) + } + if g.Fleets[fiSource].Destination != g.Fleets[fiTarget].Destination || + g.Fleets[fiSource].Origin != nil || g.Fleets[fiTarget].Origin != nil || + g.Fleets[fiSource].Range != nil || g.Fleets[fiTarget].Range != nil { + return e.NewShipsNotOnSamePlanetError() + } + for sgi, sg := range g.listIndexShipGroups(ri) { + if sg.FleetID != nil && *sg.FleetID == g.Fleets[fiSource].ID { + g.ShipGroups[sgi].FleetID = &g.Fleets[fiTarget].ID + } + } + return g.deleteFleetSafe(ri, fleetSourceName) +} + +func (g *Game) deleteFleetSafe(ri int, name string) error { + fi := g.fleetIndex(ri, name) + if fi < 0 { + return e.NewEntityNotExistsError("fleet %s", name) + } + for sgi := range g.ShipGroups { + if g.ShipGroups[sgi].FleetID != nil && *g.ShipGroups[sgi].FleetID == g.Fleets[fi].ID { + return e.NewEntityInUseError("fleet %s: race %s, group #%d", name, g.Race[ri].Name, g.ShipGroups[sgi].Number) + } + } + g.Fleets = append(g.Fleets[:fi], g.Fleets[fi+1:]...) + return nil +} + +func (g Game) fleetIndex(ri int, name string) int { + return slices.IndexFunc(g.Fleets, func(f Fleet) bool { return f.OwnerID == g.Race[ri].ID && f.Name == name }) +} + func (g Game) listFleets(ri int) iter.Seq[Fleet] { return func(yield func(Fleet) bool) { for _, fl := range g.listIndexFleets(ri) { diff --git a/internal/model/game/fleet_test.go b/internal/model/game/fleet_test.go index 0704bb7..d40f11e 100644 --- a/internal/model/game/fleet_test.go +++ b/internal/model/game/fleet_test.go @@ -35,12 +35,15 @@ func TestJoinShipGroupToFleet(t *testing.T) { assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) fleets := slices.Collect(g.ListFleets(Race_0_idx)) - assert.Len(t, fleets, 1) - assert.Equal(t, fleets[0].Name, fleetOne) - groups := slices.Collect(g.ListShipGroups(Race_0_idx)) assert.Len(t, groups, 1) gi := 0 + assert.Len(t, fleets, 1) + assert.Equal(t, fleets[0].Name, fleetOne) + assert.Equal(t, fleets[0].Destination, groups[gi].Destination) + assert.Nil(t, fleets[0].Origin) + assert.Nil(t, fleets[0].Range) + assert.NotNil(t, groups[gi].FleetID) assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) @@ -49,12 +52,15 @@ func TestJoinShipGroupToFleet(t *testing.T) { groupIndex = 2 assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetTwo, groupIndex, 2)) fleets = slices.Collect(g.ListFleets(Race_0_idx)) - assert.Len(t, fleets, 2) - assert.Equal(t, fleets[1].Name, fleetTwo) groups = slices.Collect(g.ListShipGroups(Race_0_idx)) assert.Len(t, groups, 3) - gi = 1 + assert.Len(t, fleets, 2) + assert.Equal(t, fleets[1].Name, fleetTwo) + assert.Equal(t, fleets[1].Destination, groups[gi].Destination) + assert.Nil(t, fleets[1].Origin) + assert.Nil(t, fleets[1].Range) + assert.NotNil(t, groups[gi].FleetID) assert.Equal(t, fleets[1].ID, *groups[gi].FleetID) assert.Equal(t, uint(2), groups[gi].Number) @@ -72,4 +78,55 @@ func TestJoinShipGroupToFleet(t *testing.T) { groups = slices.Collect(g.ListShipGroups(Race_0_idx)) assert.NotNil(t, groups[gi].FleetID) assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) + assert.Equal(t, fleets[0].Destination, groups[gi].Destination) + assert.Nil(t, fleets[0].Origin) + assert.Nil(t, fleets[0].Range) + + // 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" + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0), + e.GenericErrorText(e.ErrShipsBusy)) + g.ShipGroups[gi].State = "In_Orbit" + + // existing fleet not on the same planet or in_orbit + g.Fleets[0].Destination = R0_Planet_2_num + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0), + e.GenericErrorText(e.ErrShipsNotOnSamePlanet)) + g.Fleets[0].Destination = R0_Planet_0_num +} + +func TestJoinFleets(t *testing.T) { + g := copyGame() + // 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 + assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 2)) // group #2 + // creating ShipGroup at Planet_0 + assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // group #3 + + // ensure race has no Fleets + assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 0) + + fleetPlanet2 := "R0_Fleet_On_Planet_2" + fleetSource := "R0_Fleet_one" + fleetTarget := "R0_Fleet_two" + + assert.ErrorContains(t, + g.JoinFleets(Race_0.Name, fleetSource, fleetTarget), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSource, 1, 0)) + assert.ErrorContains(t, + g.JoinFleets(Race_0.Name, fleetSource, fleetTarget), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetTarget, 3, 0)) + assert.NoError(t, g.JoinFleets(Race_0.Name, fleetSource, fleetTarget)) + + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetPlanet2, 2, 0)) + assert.ErrorContains(t, + g.JoinFleets(Race_0.Name, fleetPlanet2, fleetTarget), + e.GenericErrorText(e.ErrShipsNotOnSamePlanet)) } diff --git a/internal/model/game/group.go b/internal/model/game/group.go index 894afee..f83a60c 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -24,8 +24,8 @@ type ShipGroup struct { Index uint `json:"index"` // Group index (ordered) OwnerID uuid.UUID `json:"ownerId"` // Race link TypeID uuid.UUID `json:"typeId"` // ShipType link - FleetID *uuid.UUID `json:"fleetId,omitempty"` // ShipType link - Number uint `json:"number"` // Number (quantity) ships of Type + 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"` @@ -36,8 +36,10 @@ type ShipGroup struct { Shields float64 `json:"shields"` Cargo float64 `json:"cargo"` - // TODO: append AND TEST: Destination, Origin, Range - Destination uint `json:"destination"` + // TODO: TEST: Destination, Origin, Range + Destination uint `json:"destination"` + Origin *uint `json:"origin,omitempty"` + Range *float64 `json:"range,omitempty"` } func (sg ShipGroup) Equal(other ShipGroup) bool {