diff --git a/internal/error/generic.go b/internal/error/generic.go index 284873f..6ca66b5 100644 --- a/internal/error/generic.go +++ b/internal/error/generic.go @@ -40,6 +40,11 @@ const ( ErrInputShipTypeZeroValues ErrInputScienceSumValues ErrInputProductionInvalid + ErrInputCargoTypeInvalid + ErrInputCargoLoadNotEnough + ErrInputCargoLoadNotEqual + ErrInputCargoLoadNoCargoBay + ErrInputCargoLoadNoSpaceLeft ) func GenericErrorText(code int) string { @@ -92,6 +97,16 @@ func GenericErrorText(code int) string { return "Science proportions sum should be equal 1" case ErrInputProductionInvalid: return "Invalid Production type" + case ErrInputCargoTypeInvalid: + return "Invalid cargo type" + case ErrInputCargoLoadNotEnough: + return "Not enough cargo to load" + case ErrInputCargoLoadNotEqual: + return "Ship(s) already loaded with another cargo" + case ErrInputCargoLoadNoCargoBay: + return "Ship type is not designed to carry cargo" + case ErrInputCargoLoadNoSpaceLeft: + return "No space left on the ships to load cargo" case ErrMergeShipTypeNotEqual: return "Source and target ship types are not the same" case ErrJoinFleetGroupNumberNotEnough: diff --git a/internal/error/input.go b/internal/error/input.go index 6445ef8..fb421b2 100644 --- a/internal/error/input.go +++ b/internal/error/input.go @@ -3,7 +3,7 @@ package error func NewRaceUnknownError(arg ...any) error { return newGenericError(ErrInputUnknownRace, arg...) } -func NewInputSameRaceError(arg ...any) error { +func NewSameRaceError(arg ...any) error { // TODO: check all possible commands return newGenericError(ErrInputSameRace, arg...) } @@ -72,6 +72,26 @@ func NewProductionInvalidError(arg ...any) error { return newGenericError(ErrInputProductionInvalid, arg...) } +func NewCargoTypeInvalidError(arg ...any) error { + return newGenericError(ErrInputCargoTypeInvalid, arg...) +} + +func NewCargoLoadNotEnoughError(arg ...any) error { + return newGenericError(ErrInputCargoLoadNotEnough, arg...) +} + +func NewCargoLoadNotEqualError(arg ...any) error { + return newGenericError(ErrInputCargoLoadNotEqual, arg...) +} + +func NewCargoLoadNoCargoBayError(arg ...any) error { + return newGenericError(ErrInputCargoLoadNoCargoBay, arg...) +} + +func NewCargoLoadNoSpaceLeftError(arg ...any) error { + return newGenericError(ErrInputCargoLoadNoSpaceLeft, arg...) +} + func NewMergeShipTypeNotEqualError(arg ...any) error { return newGenericError(ErrMergeShipTypeNotEqual, arg...) } diff --git a/internal/model/game/group.go b/internal/model/game/group.go index 7667b7b..60ab39c 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -14,16 +14,27 @@ import ( type CargoType string const ( - // CargoNone CargoType = "-" CargoColonist CargoType = "COL" // Колонисты CargoMaterial CargoType = "MAT" // Сырьё CargoCapital CargoType = "CAP" // Промышленность ) +var ( + cargoTypeSet map[string]CargoType = map[string]CargoType{ + CargoColonist.String(): CargoColonist, + CargoMaterial.String(): CargoMaterial, + CargoCapital.String(): CargoCapital, + } +) + func (ct CargoType) Ref() *CargoType { return &ct } +func (ct CargoType) String() string { + return string(ct) +} + type ShipGroup struct { Index uint `json:"index"` // FIXME: use UUID for Group Index (ordered) OwnerID uuid.UUID `json:"ownerId"` // Race link @@ -134,6 +145,84 @@ func (g *Game) BreakGroup(raceName string, groupIndex, quantity uint) error { return g.breakGroupInternal(ri, groupIndex, quantity) } +func (g *Game) LoadCargo(raceName string, groupIndex uint, cargoType string, quantity float64) error { + ri, err := g.raceIndex(raceName) + if err != nil { + return err + } + ct, ok := cargoTypeSet[cargoType] + if !ok { + return e.NewCargoTypeInvalidError(cargoType) + } + return g.loadCargoInternal(ri, groupIndex, ct, quantity) +} + +func (g *Game) loadCargoInternal(ri int, groupIndex uint, ct CargoType, quantity 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) + } + if g.ShipGroups[sgi].State != "In_Orbit" || g.ShipGroups[sgi].Origin != nil || g.ShipGroups[sgi].Range != nil { + return e.NewShipsBusyError() + } + 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 != g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet %#d", g.Map.Planet[pl].Number) + } + 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) + } + if g.Race[ri].ShipTypes[sti].Cargo < 1 { + return e.NewCargoLoadNoCargoBayError("ship_type %q", g.Race[ri].ShipTypes[sti].Name) + } + if g.ShipGroups[sgi].CargoType != nil && *g.ShipGroups[sgi].CargoType != ct { + return e.NewCargoLoadNotEqualError("cargo: %v", *g.ShipGroups[sgi].CargoType) + } + capacity := g.ShipGroups[sgi].CargoCapacity(&g.Race[ri].ShipTypes[sti]) + freeShipGroupCargoLoad := capacity - g.ShipGroups[sgi].Load + if freeShipGroupCargoLoad == 0 { + return e.NewCargoLoadNoSpaceLeftError() + } + var availableOnPlanet *float64 + switch ct { + case CargoMaterial: + availableOnPlanet = &g.Map.Planet[pl].Material + case CargoCapital: + availableOnPlanet = &g.Map.Planet[pl].Capital + case CargoColonist: + availableOnPlanet = &g.Map.Planet[pl].Colonists + default: + return e.NewGameStateError("CargoType not accepted: %v", ct) + } + if quantity > *availableOnPlanet || *availableOnPlanet == 0 { + return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", g.Map.Planet[pl].Number, ct, *availableOnPlanet) + } + toBeLoaded := quantity + if quantity == 0 { + toBeLoaded = *availableOnPlanet + } + if toBeLoaded > freeShipGroupCargoLoad { + toBeLoaded = freeShipGroupCargoLoad + } + *availableOnPlanet = *availableOnPlanet - toBeLoaded + g.ShipGroups[sgi].Load += toBeLoaded + // fmt.Println("capacity:", capacity, "loaded:", g.ShipGroups[sgi].Load, "free:", capacity-g.ShipGroups[sgi].Load) + if g.ShipGroups[sgi].Load > 0 { + g.ShipGroups[sgi].CargoType = &ct + } + return nil +} + func (g *Game) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { ri, err := g.raceIndex(raceName) if err != nil { @@ -148,7 +237,7 @@ func (g *Game) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity func (g *Game) giveawayGroupInternal(ri, riAccept int, groupIndex, quantity uint) (err error) { if ri == riAccept { - return e.NewInputSameRaceError(g.Race[riAccept].Name) + return e.NewSameRaceError(g.Race[riAccept].Name) } sgi := -1 for i, sg := range g.listIndexShipGroups(ri) { @@ -250,8 +339,12 @@ func (g *Game) breakGroupInternal(ri int, groupIndex, quantity uint) error { g.ShipGroups[sgi].FleetID = nil } else { newGroup := g.ShipGroups[sgi] + if g.ShipGroups[sgi].CargoType != nil { + newGroup.Load = g.ShipGroups[sgi].Load / float64(g.ShipGroups[sgi].Number) * float64(quantity) + g.ShipGroups[sgi].Load -= newGroup.Load + } newGroup.Number = quantity - g.ShipGroups[sgi].Number -= quantity + g.ShipGroups[sgi].Number -= newGroup.Number newGroup.Index = maxIndex + 1 newGroup.FleetID = nil g.ShipGroups = append(g.ShipGroups, newGroup) diff --git a/internal/model/game/group_test.go b/internal/model/game/group_test.go index 96466ba..5d52af7 100644 --- a/internal/model/game/group_test.go +++ b/internal/model/game/group_test.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/number" "github.com/stretchr/testify/assert" ) @@ -380,8 +381,11 @@ func TestBreakGroup(t *testing.T) { assert.Equal(t, uint(5), g.ShipGroups[2].Number) assert.Equal(t, uint(3), g.ShipGroups[2].Index) assert.Nil(t, g.ShipGroups[2].FleetID) + assert.Nil(t, g.ShipGroups[2].CargoType) // group #1 -> group #4 (2 new, 6 left) + g.ShipGroups[0].CargoType = game.CargoColonist.Ref() + g.ShipGroups[0].Load = 32.8 // 8 ships assert.NoError(t, g.BreakGroup(Race_0.Name, 1, 2)) // group #4 (3) assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) assert.Equal(t, uint(6), g.ShipGroups[0].Number) @@ -392,6 +396,11 @@ func TestBreakGroup(t *testing.T) { assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleet, 4, 0)) assert.NotNil(t, g.ShipGroups[3].FleetID) + assert.Equal(t, game.CargoColonist.Ref(), g.ShipGroups[0].CargoType) + assert.Equal(t, 24.6, number.Fixed3(g.ShipGroups[0].Load)) + assert.Equal(t, game.CargoColonist.Ref(), g.ShipGroups[3].CargoType) + assert.Equal(t, 8.2, number.Fixed3(g.ShipGroups[3].Load)) + // group #1 -> MAX 6 off the fleet assert.NoError(t, g.BreakGroup(Race_0.Name, 1, 6)) // group #1 (0) assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) @@ -472,3 +481,87 @@ func TestGiveawayGroup(t *testing.T) { assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 1) } + +func TestLoadCargo(t *testing.T) { + g := copyGame() + + // 1: idx = 0 / Ready to load + assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + + // 2: idx = 1 / Has no cargo bay + assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) + + // 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" + + // 4: idx = 3 / loaded with COL + assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + g.ShipGroups[3].CargoType = game.CargoColonist.Ref() + g.ShipGroups[3].Load = 1.234 + + // 5: idx = 4 / on foreign planet + assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + g.ShipGroups[4].Destination = R1_Planet_1_num + + // tests + assert.ErrorContains(t, + g.LoadCargo("UnknownRace", 1, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, "GOLD", 0), + e.GenericErrorText(e.ErrInputCargoTypeInvalid)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 555, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 3, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrShipsBusy)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 5, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputEntityNotOwned)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 2, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputCargoLoadNoCargoBay)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 4, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputCargoLoadNotEqual)) + + // initial planet is empty + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputCargoLoadNotEnough)) + // add cargo to planet + g.Map.Planet[0].Material = 100 + // not enough on the planet + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 101), + e.GenericErrorText(e.ErrInputCargoLoadNotEnough)) + + // loading all available cargo + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0)) + assert.Equal(t, 0.0, g.Map.Planet[0].Material) + assert.Equal(t, 100.0, g.ShipGroups[0].Load) // free: 131.0 + assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[0].CargoType) + + // add cargo to planet + g.Map.Planet[0].Material = 200 + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 31)) + assert.Equal(t, 169.0, g.Map.Planet[0].Material) + assert.Equal(t, 131.0, g.ShipGroups[0].Load) // free: 100.0 + assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[0].CargoType) + + // load to maximum cargo space left + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0)) + assert.Equal(t, 69.0, g.Map.Planet[0].Material) + assert.Equal(t, 231.0, g.ShipGroups[0].Load) // free: 0.0 + assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[0].CargoType) + + // ship group is full + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0), + e.GenericErrorText(e.ErrInputCargoLoadNoSpaceLeft)) +}