diff --git a/internal/controller/ballte_test.go b/internal/controller/battle_test.go similarity index 83% rename from internal/controller/ballte_test.go rename to internal/controller/battle_test.go index 3c11cd4..3fe3aee 100644 --- a/internal/controller/ballte_test.go +++ b/internal/controller/battle_test.go @@ -67,16 +67,16 @@ func TestEffectiveDefence(t *testing.T) { } func TestCollectPlanetGroups(t *testing.T) { - c, g := newCache() + c, _ := newCache() - assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) // 0 - assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 1 - assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 2 - g.ShipGroups[2].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} // 2 -> In_Space - assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 3 - g.ShipGroups[3].Destination = R1_Planet_1_num // 3 -> Planet_1 - assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 4 - g.ShipGroups[4].Destination = R0_Planet_0_num // 4 -> Planet_0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 10)) // 1 #0 + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 3)) // 2 #1 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // 3 #2 + c.ShipGroup(2).StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} // 3 #2 -> In_Space + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) // 4 #3 + c.ShipGroup(3).Destination = R1_Planet_1_num // 4 #3 -> Planet_1 + assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 5 #4 + c.ShipGroup(4).Destination = R0_Planet_0_num // 5 #4 -> Planet_0 planetGroups := controller.CollectPlanetGroups(c) @@ -101,7 +101,7 @@ func TestFilterBattleOpponents(t *testing.T) { assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 15)) // 2 undefeatedShip := ship undefeatedShip.Shields = 100 - assert.NoError(t, c.CreateShipType(Race_1.Name, undefeatedShip.Name, undefeatedShip.Drive, int(undefeatedShip.Armament), undefeatedShip.Weapons, undefeatedShip.Shields, undefeatedShip.Cargo)) + assert.NoError(t, c.CreateShipType(Race_1_idx, undefeatedShip.Name, undefeatedShip.Drive, int(undefeatedShip.Armament), undefeatedShip.Weapons, undefeatedShip.Shields, undefeatedShip.Cargo)) assert.NoError(t, c.CreateShips(Race_1_idx, undefeatedShip.Name, R1_Planet_1_num, 1)) // 3 cacheProbability := make(map[int]map[int]float64) @@ -121,10 +121,10 @@ func TestFilterBattleOpponents(t *testing.T) { assert.True(t, controller.FilterBattleOpponents(c, 1, 0, cacheProbability)) // Test: reace reations - assert.NoError(t, c.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationPeace)) + assert.NoError(t, c.UpdateRelation(Race_0_idx, Race_1_idx, game.RelationPeace)) assert.True(t, controller.FilterBattleOpponents(c, 0, 2, cacheProbability)) assert.True(t, controller.FilterBattleOpponents(c, 2, 0, cacheProbability)) - assert.NoError(t, c.UpdateRelation(Race_0.Name, Race_1.Name, game.RelationWar)) + assert.NoError(t, c.UpdateRelation(Race_0_idx, Race_1_idx, game.RelationWar)) assert.LessOrEqual(t, controller.DestructionProbability(Cruiser.Weapons, 1, undefeatedShip.Shields, 1, undefeatedShip.EmptyMass()), 0.) assert.True(t, controller.FilterBattleOpponents(c, 1, 3, cacheProbability)) diff --git a/internal/controller/cache.go b/internal/controller/cache.go index 66f9812..3403042 100644 --- a/internal/controller/cache.go +++ b/internal/controller/cache.go @@ -11,6 +11,7 @@ import ( type Cache struct { g *game.Game cacheRaceIndexByID map[uuid.UUID]int + cacheFleetIndexByID map[uuid.UUID]int raceIndexByShipGroupIndex map[int]int shipClassByShipGroupIndex map[int]*game.ShipType planetByPlanetNumber map[uint]*game.Planet @@ -27,53 +28,9 @@ func NewCache(g *game.Game) *Cache { return c } -func (c *Cache) Relation(r1, r2 int) game.Relation { - if c.cacheRelation == nil { - c.cacheRelation = make(map[int]map[int]game.Relation) - for r1 := range c.g.Race { - for r2 := range c.g.Race { - if r1 == r2 { - continue - } - rel := slices.IndexFunc(c.g.Race[r1].Relations, func(r game.RaceRelation) bool { return r.RaceID == c.g.Race[r2].ID }) - if rel < 0 { - panic(fmt.Sprintf("Relation: opponent not found idx=%d", r2)) - } - c.updateRelationCache(r1, r2, c.g.Race[r1].Relations[rel].Relation) - // if _, ok := c.cacheRelation[r1]; !ok { - // c.cacheRelation[r1] = make(map[int]game.Relation) - // } - // c.cacheRelation[r1][r2] = c.g.Race[r1].Relations[rel].Relation - } - } - - } - if _, ok := c.cacheRelation[r1]; !ok { - panic(fmt.Sprintf("Relation: no left race idx=%d", r1)) - } - if v, ok := c.cacheRelation[r1][r2]; !ok { - panic(fmt.Sprintf("Relation: no right race idx=%d", r2)) - } else { - return v - } -} - -func (c *Cache) updateRelationCache(r1, r2 int, rel game.Relation) { - if r1 == r2 { - return - } - if c.cacheRelation == nil { - c.cacheRelation = make(map[int]map[int]game.Relation) - } - if _, ok := c.cacheRelation[r1]; !ok { - c.cacheRelation[r1] = make(map[int]game.Relation) - } - c.cacheRelation[r1][r2] = rel -} - func (c *Cache) ShipGroupShipClass(groupIndex int) *game.ShipType { - if c.shipClassByShipGroupIndex == nil { - c.fillShipsAndGroups() + if c.shipClassByShipGroupIndex == nil || len(c.shipClassByShipGroupIndex) == 0 { + c.cacheShipsAndGroups() } c.validateShipGroupIndex(groupIndex) if v, ok := c.shipClassByShipGroupIndex[groupIndex]; ok { @@ -97,21 +54,7 @@ func (c *Cache) RaceIndex(ID uuid.UUID) int { } } -func (c *Cache) unsafeDeleteShipGroup(i int) { - c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...) - delete(c.raceIndexByShipGroupIndex, i) - delete(c.shipClassByShipGroupIndex, i) -} - -// Internal - -func (c *Cache) validateShipGroupIndex(i int) { - if i >= len(c.g.ShipGroups) { - panic(fmt.Sprintf("group index out of groups len: %d >= %d", i, len(c.g.ShipGroups))) - } -} - -func (c *Cache) fillShipsAndGroups() { +func (c *Cache) cacheShipsAndGroups() { if c.raceIndexByShipGroupIndex != nil { clear(c.raceIndexByShipGroupIndex) } else { @@ -133,6 +76,24 @@ func (c *Cache) fillShipsAndGroups() { } } +func (c *Cache) cacheShipGroup(groupIndex, ri int, class *game.ShipType) { + if c.raceIndexByShipGroupIndex != nil { + c.raceIndexByShipGroupIndex[groupIndex] = ri + } + if c.shipClassByShipGroupIndex != nil { + c.shipClassByShipGroupIndex[groupIndex] = class + } +} + +func (c *Cache) invalidateShipGroupCache() { + if c.raceIndexByShipGroupIndex != nil { + clear(c.raceIndexByShipGroupIndex) + } + if c.shipClassByShipGroupIndex != nil { + clear(c.shipClassByShipGroupIndex) + } +} + // Helpers func ShipClassIndex(g *game.Game, ri int, classID uuid.UUID) (int, bool) { diff --git a/internal/controller/controller.go b/internal/controller/controller.go index b33f2a0..93b072f 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -31,7 +31,6 @@ type Repo interface { } type Controller struct { - param Param Repo Repo Cache *Cache } @@ -54,11 +53,16 @@ func NewController(config Config) (*Controller, error) { return nil, err } return &Controller{ - param: *c, - Repo: r, + Repo: r, }, nil } +func NewRepoController(r Repo) *Controller { + return &Controller{ + Repo: r, + } +} + func (c *Controller) ExecuteState(consumer func(Repo)) error { if err := c.Repo.Lock(); err != nil { return fmt.Errorf("execute: lock failed: %s", err) diff --git a/internal/controller/controller_export_test.go b/internal/controller/controller_export_test.go new file mode 100644 index 0000000..07718e3 --- /dev/null +++ b/internal/controller/controller_export_test.go @@ -0,0 +1,64 @@ +package controller + +import ( + "fmt" + "iter" + + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/model/game" +) + +func (c *Cache) Race(i int) game.Race { + c.validateRaceIndex(i) + return c.g.Race[i] +} + +func (c *Cache) ListShipGroups(ri int) iter.Seq[*game.ShipGroup] { + return c.listShipGroups(ri) +} + +func (c *Cache) ListFleets(ri int) iter.Seq[*game.Fleet] { + return c.listFleets(ri) +} + +func (c *Cache) MustFleetID(ri int, name string) uuid.UUID { + for f := range c.listFleets(ri) { + if f.Name == name { + return f.ID + } + } + panic("fleet not found") +} + +func (c *Cache) MustShipGroup(ri int, index uint) *game.ShipGroup { + for sg := range c.listShipGroups(ri) { + if sg.Index == index { + return sg + } + } + panic(fmt.Sprintf("race i=%d have no group i=%d", ri, index)) +} + +func (c *Cache) MustShipClass(ri int, name string) *game.ShipType { + st, _, ok := c.ShipClass(ri, name) + if !ok { + panic("ship class not foind") + } + return st +} + +func (c *Cache) PutPopulation(pn uint, v float64) { + c.putPopulation(pn, v) +} + +func (c *Cache) PutColonists(pn uint, v float64) { + c.putColonists(pn, v) +} + +func (c *Cache) PutMaterial(pn uint, v float64) { + c.putMaterial(pn, v) +} + +func (c *Cache) RacetTechLevel(ri int, t game.Tech, v float64) { + c.racetTechLevel(ri, t, v) +} diff --git a/internal/controller/controller_helper.go b/internal/controller/controller_helper.go index d2db3f3..f4caaa9 100644 --- a/internal/controller/controller_helper.go +++ b/internal/controller/controller_helper.go @@ -11,3 +11,10 @@ func validateTypeName(v string) (string, bool) { // TODO: special symbols AND include error check in all user-input test return s, false } + +func maxUint(a, b uint) uint { + if b > a { + return b + } + return a +} diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 5190647..02be5a4 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -90,18 +90,22 @@ func newGame() *game.Game { }, }, } - 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 } -func newCache() (*controller.Cache, *game.Game) { +func newCache() (*controller.Cache, *controller.Controller) { g := newGame() c := controller.NewCache(g) - return c, g + assertNoError(c.CreateShipType(Race_0_idx, Race_0_Gunship, 60, 3, 30, 100, 0)) + assertNoError(c.CreateShipType(Race_0_idx, Race_0_Freighter, 8, 0, 0, 2, 10)) + assertNoError(c.CreateShipType(Race_0_idx, ShipType_Cruiser, Cruiser.Drive, int(Cruiser.Armament), Cruiser.Weapons, Cruiser.Shields, Cruiser.Cargo)) + + assertNoError(c.CreateShipType(Race_1_idx, Race_1_Gunship, 60, 3, 30, 100, 0)) + assertNoError(c.CreateShipType(Race_1_idx, Race_1_Freighter, 8, 0, 0, 2, 10)) + assertNoError(c.CreateShipType(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.) + + ctl := controller.NewRepoController(nil) + ctl.Cache = c + + return c, ctl } diff --git a/internal/controller/fleet.go b/internal/controller/fleet.go new file mode 100644 index 0000000..e09a9a6 --- /dev/null +++ b/internal/controller/fleet.go @@ -0,0 +1,285 @@ +package controller + +import ( + "fmt" + "iter" + "math" + "slices" + + "github.com/google/uuid" + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" +) + +func (c *Cache) FleetState(fleetID uuid.UUID) (game.ShipGroupState, *uint, *game.InSpace) { + fi := c.MustFleetIndex(fleetID) + ri := c.RaceIndex(c.g.Fleets[fi].OwnerID) + var state *game.ShipGroupState + var onPlanet *uint + var is *game.InSpace + for sg := range c.FleetGroups(ri, fi) { + if state == nil { + s := sg.State() + state = &s + if planet, ok := sg.OnPlanet(); ok { + onPlanet = &planet + } + is = sg.StateInSpace + continue + } + if *state != sg.State() { + panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different states", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) + } + if planet, ok := sg.OnPlanet(); ok && onPlanet != nil && *onPlanet != planet { + for sg := range c.FleetGroups(ri, fi) { + fmt.Println("group", sg.Index, "fleet", sg.FleetID, c.g.Fleets[fi].Name, "state", sg.State(), "on", sg.Destination) + } + panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q are on different planets: %d <> %d", c.g.Race[ri].Name, c.g.Fleets[fi].Name, *onPlanet, planet)) + } + if (is == nil && sg.StateInSpace != nil) || (is != nil && sg.StateInSpace == nil) { + panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q on_planet and in_space at the same time", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) + } + if is != nil && sg.StateInSpace != nil && !is.Equal(*sg.StateInSpace) { + panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different is_space states", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) + } + } + if state == nil { + panic(fmt.Sprintf("FleetState: race's %q fleet %q has no ships", c.g.Race[ri].Name, c.g.Fleets[fi].Name)) + } + return *state, onPlanet, is +} + +// TODO: Hello! Wanna know fleet's speed? Good. Implement & test this func first. +func (c *Cache) FleetSpeed(fl game.Fleet) float64 { + result := math.MaxFloat64 + for sg := range c.ShipGroupsIndex() { + if c.ShipGroup(sg).FleetID == nil || *c.ShipGroup(sg).FleetID != fl.ID { + continue + } + st := c.ShipGroupShipClass(sg) + // st := g.mustShipType(g.ShipGroups[sg].TypeID) + typeSpeed := c.ShipGroup(sg).Speed(st) + if typeSpeed < result { + result = typeSpeed + } + } + return result +} + +func (c *Controller) JoinShipGroupToFleet(raceName, fleetName string, group, count uint) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.JoinShipGroupToFleet(ri, fleetName, group, count) +} + +func (c *Cache) JoinShipGroupToFleet(ri int, fleetName string, groupIndex, quantity uint) (err error) { + c.validateRaceIndex(ri) + name, ok := validateTypeName(fleetName) + if !ok { + return e.NewEntityTypeNameValidationError("%q", name) + } + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + + // sgi := -1 + // var maxIndex uint + // for i, sg := range g.listIndexShipGroups(ri) { + // if sgi < 0 && sg.Index == groupIndex { + // sgi = i + // } + // if sg.Index > maxIndex { + // maxIndex = sg.Index + // } + // } + // if sgi < 0 { + // return e.NewEntityNotExistsError("group #%d", groupIndex) + // } + + if c.ShipGroup(sgi).State() != game.StateInOrbit { + return e.NewShipsBusyError() + } + + if c.ShipGroup(sgi).Number < quantity { + return e.NewJoinFleetGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) + } + + fi, ok := c.fleetIndex(ri, name) + if !ok { + fi, err = c.createFleet(ri, name) + if err != nil { + return err + } + } else { + state, onPlanet, _ := c.FleetState(c.g.Fleets[fi].ID) + if state != game.StateInOrbit || *onPlanet != c.ShipGroup(sgi).Destination { + return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName) + } + } + + // FIXME: if g.ShipGroups[sgi].FleetID != nil { // delete old fleet if empty, ALSO mind breaking group } + if quantity > 0 && quantity < c.ShipGroup(sgi).Number { + nsgi, err := c.breakGroupSafe(ri, groupIndex, quantity) + if err != nil { + return err + } + sgi = nsgi + // newGroup := g.ShipGroups[sgi] + // newGroup.Number -= quantity + // g.ShipGroups[sgi].Number = quantity + // newGroup.Index = maxIndex + 1 + // g.ShipGroups = append(g.ShipGroups, newGroup) + } + + // g.ShipGroups[sgi].FleetID = &g.Fleets[fi].ID + c.ShipGroupFleet(sgi, &c.g.Fleets[fi].ID) + return nil +} + +func (c *Controller) JoinFleets(raceName, fleetSourceName, fleetTargetName string) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.JoinFleets(ri, fleetSourceName, fleetTargetName) +} + +func (c *Cache) JoinFleets(ri int, fleetSourceName, fleetTargetName string) (err error) { + fiSource, ok := c.fleetIndex(ri, fleetSourceName) + if !ok { + return e.NewEntityNotExistsError("source fleet %s", fleetSourceName) + } + fiTarget, ok := c.fleetIndex(ri, fleetTargetName) + if !ok { + return e.NewEntityNotExistsError("target fleet %s", fleetTargetName) + } + srcState, planet1, _ := c.FleetState(c.g.Fleets[fiSource].ID) + tgtState, planet2, _ := c.FleetState(c.g.Fleets[fiTarget].ID) + if srcState != game.StateInOrbit || srcState != tgtState || *planet1 != *planet2 { + return e.NewShipsNotOnSamePlanetError() + } + for sg := range c.listShipGroups(ri) { + if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fiSource].ID { + sg.FleetID = &c.g.Fleets[fiTarget].ID + } + } + return c.deleteFleetSafe(ri, fleetSourceName) +} + +func (c *Cache) createFleet(ri int, name string) (int, error) { + c.validateRaceIndex(ri) + n, ok := validateTypeName(name) + if !ok { + return 0, e.NewEntityTypeNameValidationError("%q", n) + } + if _, ok := c.fleetIndex(ri, n); ok { + return 0, e.NewEntityTypeNameDuplicateError("fleet %w", n) + } + fleets := slices.Clone(c.g.Fleets) + fleets = append(fleets, game.Fleet{ + ID: uuid.New(), + OwnerID: c.g.Race[ri].ID, + Name: n, + }) + c.g.Fleets = fleets + i := len(c.g.Fleets) - 1 + if c.cacheFleetIndexByID != nil { + c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i + } + return i, nil +} + +func (c *Cache) deleteFleetSafe(ri int, name string) error { + fi, ok := c.fleetIndex(ri, name) + if !ok { + return e.NewEntityNotExistsError("fleet %s", name) + } + for sg := range c.listShipGroups(ri) { + if sg.FleetID != nil && *(sg.FleetID) == c.g.Fleets[fi].ID { + return e.NewEntityInUseError("fleet %s: race %s, group #%d", name, c.g.Race[ri].Name, sg.Number) + } + } + if c.cacheFleetIndexByID != nil { + delete(c.cacheFleetIndexByID, c.g.Fleets[fi].ID) + } + c.g.Fleets = append(c.g.Fleets[:fi], c.g.Fleets[fi+1:]...) + return nil +} + +// Internal funcs + +func (c *Cache) FleetIndex(ID uuid.UUID) (int, bool) { + if c.cacheFleetIndexByID == nil { + c.cacheFleetIndexByID = make(map[uuid.UUID]int) + for i := range c.g.Fleets { + c.cacheFleetIndexByID[c.g.Fleets[i].ID] = i + } + } + if v, ok := c.cacheFleetIndexByID[ID]; ok { + return v, true + } else { + return -1, false + } +} + +// TODO: rename / [fleetIndex] +func (c *Cache) MustFleetIndex(ID uuid.UUID) int { + if v, ok := c.FleetIndex(ID); ok { + return v + } else { + panic(fmt.Sprintf("fleet not found by ID=%v", ID)) + } +} + +func (c *Cache) FleetGroups(ri, fi int) iter.Seq[*game.ShipGroup] { + c.validateRaceIndex(ri) + c.validateFleetIndex(fi) + return func(yield func(*game.ShipGroup) bool) { + for sg := range c.listShipGroups(ri) { + if sg.FleetID != nil && *sg.FleetID == c.g.Fleets[fi].ID { + if !yield(sg) { + break + } + } + } + } +} + +func (c *Cache) listFleets(ri int) iter.Seq[*game.Fleet] { + c.validateRaceIndex(ri) + return func(yield func(*game.Fleet) bool) { + for i := range c.g.Fleets { + if c.g.Fleets[i].OwnerID == c.g.Race[ri].ID { + if !yield(&c.g.Fleets[i]) { + return + } + } + } + } +} + +func (c *Cache) fleetIndex(ri int, name string) (int, bool) { + c.validateRaceIndex(ri) + if i := slices.IndexFunc(c.g.Fleets, func(f game.Fleet) bool { return f.OwnerID == c.g.Race[ri].ID && f.Name == name }); i < 0 { + return -1, false + } else { + return i, true + } +} + +func (c *Cache) mustFleetIndex(ri int, name string) int { + if v, ok := c.fleetIndex(ri, name); ok { + return v + } else { + panic(fmt.Sprintf("fleet not found: race_idx=%d name=%q", ri, name)) + } +} + +func (c *Cache) validateFleetIndex(i int) { + if i >= len(c.g.Fleets) { + panic(fmt.Sprintf("race index out of range: %d >= %d", i, len(c.g.Fleets))) + } +} diff --git a/internal/controller/fleet_send.go b/internal/controller/fleet_send.go new file mode 100644 index 0000000..1d8e180 --- /dev/null +++ b/internal/controller/fleet_send.go @@ -0,0 +1,89 @@ +package controller + +import ( + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/util" +) + +func (c *Controller) SendFleet(raceName, fleetName string, planetNumber uint) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + fi, ok := c.Cache.fleetIndex(ri, fleetName) + if !ok { + return e.NewEntityNotExistsError("fleet %q", fleetName) + } + return c.Cache.SendFleet(ri, fi, planetNumber) +} + +func (c *Cache) SendFleet(ri, fi int, planetNumber uint) error { + c.validateRaceIndex(ri) + c.validateFleetIndex(fi) + state, sourcePlanet, _ := c.FleetState(c.g.Fleets[fi].ID) + if game.StateInOrbit != state && game.StateLaunched != state { + return e.NewShipsBusyError() + } + + p1, ok := c.Planet(*sourcePlanet) + // p1, ok := PlanetByNum(g, *sourcePlanet) + if !ok { + return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) + } + p2, ok := c.Planet(planetNumber) + // p2, ok := PlanetByNum(g, planetNumber) + if !ok { + return e.NewEntityNotExistsError("destination planet #%d", planetNumber) + } + rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) + if rangeToDestination > c.g.Race[ri].FlightDistance() { + return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) + } + + for sg := range c.FleetGroups(ri, fi) { + st := c.ShipType(ri, sg.TypeID) + // st, ok := ShipClass(g, ri, sg.TypeID) + // if !ok { + // return e.NewGameStateError("not found: ShipType ID=%v", sg.TypeID) + // } + if st.DriveBlockMass() == 0 { + return e.NewSendShipHasNoDrivesError("Class=%s", st.Name) + } + } + + if *sourcePlanet == planetNumber { + c.UnsendFleet(ri, fi) + return nil + } + + c.LaunchFleet(ri, fi, planetNumber) + + return nil +} + +func (c *Cache) LaunchFleet(ri, fi int, destination uint) { + c.validateRaceIndex(ri) + c.validateFleetIndex(fi) + for sg := range c.FleetGroups(ri, fi) { + c.LaunchShips(*sg, destination) + // sgi := slices.IndexFunc(g.ShipGroups, func(s ShipGroup) bool { return sg.Index == s.Index }) + // if sgi < 0 { + // panic(fmt.Sprintf("LauncgFleet: cannot find ship group index=%d", sg.Index)) + // } + // g.ShipGroups[sgi] = c.LaunchShips(sg, destination) + } +} + +func (c *Cache) UnsendFleet(ri, fi int) { + c.validateRaceIndex(ri) + c.validateFleetIndex(fi) + for sg := range c.FleetGroups(ri, fi) { + c.UnsendShips(*sg) + // sgi := slices.IndexFunc(g.ShipGroups, func(s ShipGroup) bool { return sg.Index == s.Index }) + // if sgi < 0 { + // panic(fmt.Sprintf("UnsendFleet: cannot find ship group index=%d", sg.Index)) + // } + // g.ShipGroups[sgi] = c.UnsendShips(sg) + } +} diff --git a/internal/controller/fleet_send_test.go b/internal/controller/fleet_send_test.go new file mode 100644 index 0000000..ad28259 --- /dev/null +++ b/internal/controller/fleet_send_test.go @@ -0,0 +1,74 @@ +package controller_test + +import ( + "slices" + "testing" + + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/stretchr/testify/assert" +) + +func TestSendFleet(t *testing.T) { + c, g := newCache() + // group #1 - in_orbit Planet_0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) + // group #2 - in_space (later) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) + // group #3 - in_orbit Planet_0, unmovable + g.CreateShipType(Race_0.Name, "Fortress", 0, 50, 30, 100, 0) + assert.NoError(t, c.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1)) + // group #4 - in_orbit Planet_0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) + + // ensure race has no Fleets + assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 0) + + fleetSending := "R0_Fleet_one" + fleetInSpace := "R0_Fleet_inSpace" + fleetUnmovable := "R0_Fleet_unmovable" + + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSending, 1, 0)) + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSending, 3, 0)) + + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetInSpace, 2, 0)) + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetUnmovable, 3, 0)) + // group #2 - in_space + c.ShipGroup(1).StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} + // g.ShipGroups[1].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} + + assert.ErrorContains(t, + g.SendFleet("UnknownRace", fleetSending, 2), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.SendFleet(Race_0.Name, "UnknownFleet", 2), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.SendFleet(Race_0.Name, fleetInSpace, 2), + e.GenericErrorText(e.ErrShipsBusy)) + assert.ErrorContains(t, + g.SendFleet(Race_0.Name, fleetSending, 200), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.SendFleet(Race_0.Name, fleetSending, 3), + e.GenericErrorText(e.ErrSendUnreachableDestination)) + assert.ErrorContains(t, + g.SendFleet(Race_0.Name, fleetUnmovable, 2), + e.GenericErrorText(e.ErrSendShipHasNoDrives)) + + assert.NoError(t, g.SendFleet(Race_0.Name, fleetSending, 2)) + // fi := slices.IndexFunc(slices.Collect(c.ListFleets(Race_0_idx)), func(f *game.Fleet) bool { return f.Name == fleetSending }) + state, _, _ := c.FleetState(c.MustFleetID(Race_0_idx, fleetSending)) + assert.Equal(t, game.StateLaunched, state) + for sg := range c.FleetGroups(Race_0_idx, c.MustFleetIndex(c.MustFleetID(Race_0_idx, fleetSending))) { + assert.Equal(t, game.StateLaunched, sg.State()) + } + + assert.NoError(t, g.SendFleet(Race_0.Name, fleetSending, 0)) + // fi = slices.IndexFunc(slices.Collect(c.ListFleets(Race_0_idx)), func(f *game.Fleet) bool { return f.Name == fleetSending }) + state, _, _ = c.FleetState(c.MustFleetID(Race_0_idx, fleetSending)) + assert.Equal(t, game.StateInOrbit, state) + for sg := range c.FleetGroups(Race_0_idx, c.MustFleetIndex(c.MustFleetID(Race_0_idx, fleetSending))) { + assert.Equal(t, game.StateInOrbit, sg.State()) + } +} diff --git a/internal/controller/fleet_test.go b/internal/controller/fleet_test.go new file mode 100644 index 0000000..829ea1a --- /dev/null +++ b/internal/controller/fleet_test.go @@ -0,0 +1,137 @@ +package controller_test + +import ( + "slices" + "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) { + c, g := newCache() + var groupIndex uint = 1 + + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, " ", groupIndex, 0), + e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)) + + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, "Unnamed", groupIndex, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + + // creating ShipGroup + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) + + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, "Unnamed", groupIndex, 6), + e.GenericErrorText(e.ErrJoinFleetGroupNumberNotEnough)) + + // ensure race has no Fleets + assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 0) + + fleetOne := "R0_Fleet_one" + fleetTwo := "R0_Fleet_two" + + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) + fleets := slices.Collect(c.ListFleets(Race_0_idx)) + groups := slices.Collect(c.ListShipGroups(Race_0_idx)) + assert.Len(t, groups, 1) + gi := 0 + assert.Len(t, fleets, 1) + assert.Equal(t, fleets[0].Name, fleetOne) + state, _, _ := c.FleetState(fleets[0].ID) + assert.Equal(t, game.StateInOrbit, state) + + assert.NotNil(t, groups[gi].FleetID) + assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) + + // create another ShipGroup + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) + groupIndex = 2 + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetTwo, groupIndex, 2)) + fleets = slices.Collect(c.ListFleets(Race_0_idx)) + groups = slices.Collect(c.ListShipGroups(Race_0_idx)) + assert.Len(t, groups, 3) + assert.Len(t, fleets, 2) + assert.Equal(t, fleets[1].Name, fleetTwo) + state, _, _ = c.FleetState(fleets[1].ID) + assert.Equal(t, game.StateInOrbit, state) + + gi = 2 + assert.Len(t, groups, 3) + assert.NotNil(t, groups[gi].FleetID) + assert.Equal(t, fleets[1].ID, *groups[gi].FleetID) + assert.Equal(t, uint(2), groups[gi].Number) + assert.Equal(t, uint(3), groups[gi].Index) + + gi = 1 + assert.Nil(t, groups[gi].FleetID) + assert.Equal(t, uint(1), groups[gi].Number) + assert.Equal(t, uint(2), groups[gi].Index) + + groupIndex = groups[gi].Index + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) + fleets = slices.Collect(c.ListFleets(Race_0_idx)) + assert.Len(t, fleets, 2) + groups = slices.Collect(c.ListShipGroups(Race_0_idx)) + assert.NotNil(t, groups[gi].FleetID) + assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) + state, _, _ = c.FleetState(fleets[0].ID) + assert.Equal(t, game.StateInOrbit, state) + + // group not In_Orbit + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7)) + gi = 3 + c.ShipGroup(gi).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 1, + } + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, fleetOne, c.ShipGroup(gi).Index, 0), + e.GenericErrorText(e.ErrShipsBusy)) + c.ShipGroup(gi).StateInSpace = nil + + // existing fleet not on the same planet or in_orbit + c.ShipGroup(0).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 1, + } + c.ShipGroup(2).StateInSpace = c.ShipGroup(0).StateInSpace + assert.ErrorContains(t, + g.JoinShipGroupToFleet(Race_0.Name, fleetOne, c.ShipGroup(gi).Index, 0), + e.GenericErrorText(e.ErrShipsNotOnSamePlanet)) +} + +func TestJoinFleets(t *testing.T) { + c, g := newCache() + // creating ShipGroup #1 at Planet_0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1 + // creating ShipGroup #2 at Planet_2 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 2)) // group #2 + // creating ShipGroup #3 at Planet_0 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // group #3 + + // ensure race has no Fleets + assert.Len(t, slices.Collect(c.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/controller/generate_turn.go b/internal/controller/generate_turn.go index b806351..cb694ef 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -12,7 +12,7 @@ func MakeTurn(c *Controller, r Repo, g *game.Game) error { g.Age += 1 // 01. Корабли, где это возможно, объединяются в группы. - game.JoinEqualGroups(g) + c.Cache.CmdJoinEqualGroups() // 02. Враждующие корабли вступают в схватку. battles := ProduceBattles(c.Cache) diff --git a/internal/controller/planet.go b/internal/controller/planet.go index 9547fe0..92250b9 100644 --- a/internal/controller/planet.go +++ b/internal/controller/planet.go @@ -27,3 +27,42 @@ func (c *Cache) MustPlanet(planetNumber uint) *game.Planet { panic(fmt.Sprintf("Planet: not found by number=%d", planetNumber)) } } + +// Свободный производственный потенциал (L) +// промышленность * 0.75 + население * 0.25 +// за вычетом затрат, расходуемых в течение хода на модернизацию кораблей +func (c *Cache) PlanetProductionCapacity(planetNumber uint) float64 { + p := c.MustPlanet(planetNumber) + // p, err := g.PlanetByNumber(planetNumber) + // if err != nil { + // panic(err) + // } + var busyResources float64 + for sg := range c.g.ShipsInUpgrade(p.Number) { + busyResources += sg.StateUpgrade.Cost() + } + return game.PlanetProduction(p.Industry, p.Population) - busyResources +} + +// Internal fincs + +func (c *Cache) putPopulation(pn uint, v float64) { + c.MustPlanet(pn).Population = v +} + +func (c *Cache) putColonists(pn uint, v float64) { + c.MustPlanet(pn).Colonists = v +} + +func (c *Cache) putMaterial(pn uint, v float64) { + c.MustPlanet(pn).Material = v +} + +func UnloadColonists(p game.Planet, v float64) game.Planet { + p.Population += v * 8 + if p.Population > p.Size { + p.Colonists += (p.Population - p.Size) / 8 + p.Population = p.Size + } + return p +} diff --git a/internal/controller/race.go b/internal/controller/race.go index fdc87d1..c5e28f1 100644 --- a/internal/controller/race.go +++ b/internal/controller/race.go @@ -1,6 +1,7 @@ package controller import ( + "fmt" "slices" e "github.com/iliadenisov/galaxy/internal/error" @@ -8,69 +9,129 @@ import ( "github.com/iliadenisov/galaxy/internal/model/game" ) -// func (c *Cache) CmdRelation(hostRace, opponentRace string) (game.RaceRelation, error) { -// ri, err := c.raceIndex(hostRace) -// if err != nil { -// return game.RaceRelation{}, err -// } -// other, err := c.raceIndex(opponentRace) -// if err != nil { -// return game.RaceRelation{}, err -// } -// if ri == other { -// return game.RaceRelation{ -// RaceID: c.g.Race[ri].ID, -// Relation: game.RelationPeace, -// }, nil -// } -// rel := slices.IndexFunc(c.g.Race[ri].Relations, func(r game.RaceRelation) bool { return r.RaceID == c.g.Race[other].ID }) -// if rel < 0 { -// return game.RaceRelation{}, e.NewGameStateError("Relation: opponent not found") -// } -// return c.g.Race[ri].Relations[rel], nil +func (c Controller) Relation(from, to string) (game.Relation, error) { + r1, err := c.Cache.raceIndex(from) + if err != nil { + return game.Relation(""), err + } + r2, err := c.Cache.raceIndex(to) + if err != nil { + return game.Relation(""), err + } + return c.Cache.Relation(r1, r2), nil +} -// // return g.relationInternal(ri, other) -// } - -func (c *Cache) UpdateRelation(race, opponent string, rel game.Relation) error { - r1, err := c.raceIndex(race) +func (c Controller) UpdateRelation(race, opponent string, rel game.Relation) error { + ri, err := c.Cache.raceIndex(race) if err != nil { return err } - var r2 int + var other int if race == opponent { - r2 = r1 - } else if r2, err = c.raceIndex(opponent); err != nil { + other = ri + } else if other, err = c.Cache.raceIndex(opponent); err != nil { return err } if err != nil { return err } - if r2 == r1 { - return nil - } - for o := range c.g.Race[r1].Relations { - if c.g.Race[r1].Relations[o].RaceID == c.g.Race[r2].ID { - c.g.Race[r1].Relations[o].Relation = rel - if c.cacheRelation != nil { - c.updateRelationCache(r1, r2, rel) + return c.Cache.UpdateRelation(ri, other, rel) +} + +func (c *Cache) Relation(r1, r2 int) game.Relation { + if c.cacheRelation == nil { + c.cacheRelation = make(map[int]map[int]game.Relation) + for r1 := range c.g.Race { + for r2 := range c.g.Race { + if r1 == r2 { + continue + } + rel := slices.IndexFunc(c.g.Race[r1].Relations, func(r game.RaceRelation) bool { return r.RaceID == c.g.Race[r2].ID }) + if rel < 0 { + panic(fmt.Sprintf("Relation: opponent not found idx=%d", r2)) + } + c.updateRelationCache(r1, r2, c.g.Race[r1].Relations[rel].Relation) } + } + + } + if _, ok := c.cacheRelation[r1]; !ok { + panic(fmt.Sprintf("Relation: no left race idx=%d", r1)) + } + if v, ok := c.cacheRelation[r1][r2]; !ok { + panic(fmt.Sprintf("Relation: no right race idx=%d", r2)) + } else { + return v + } +} + +func (c *Cache) updateRelationCache(r1, r2 int, rel game.Relation) { + if r1 == r2 { + return + } + if c.cacheRelation == nil { + c.cacheRelation = make(map[int]map[int]game.Relation) + } + if _, ok := c.cacheRelation[r1]; !ok { + c.cacheRelation[r1] = make(map[int]game.Relation) + } + c.cacheRelation[r1][r2] = rel +} + +func (c *Cache) GiveVotes(race, recipient string) error { + ri, err := c.raceIndex(race) + if err != nil { + return err + } + rec, err := c.raceIndex(recipient) + if err != nil { + return err + } + c.g.Race[ri].Vote = c.g.Race[rec].ID + return nil +} + +func (c *Cache) Voted(ri int) int { + c.validateRaceIndex(ri) + return c.RaceIndex(c.g.Race[ri].Vote) +} + +func (c *Cache) UpdateRelation(ri, other int, rel game.Relation) (err error) { + defer func() { + if err == nil && c.cacheRelation != nil { + c.updateRelationCache(ri, other, rel) + } + }() + for o := range c.g.Race[ri].Relations { + switch { + case ri == other: + c.g.Race[ri].Relations[o].Relation = rel + case c.g.Race[ri].Relations[o].RaceID == c.g.Race[other].ID: + c.g.Race[ri].Relations[o].Relation = rel return nil } - // switch { - // case r1 == r2: - // c.g.Race[r1].Relations[o].Relation = rel - // case c.g.Race[r1].Relations[o].RaceID == c.g.Race[r2].ID: - // c.g.Race[r1].Relations[o].Relation = rel - // return nil - // } } - return e.NewGameStateError("UpdateRelation: opponent not found") - // if r1 != r2 { - // return e.NewGameStateError("UpdateRelation: opponent not found") + if ri != other { + err = e.NewGameStateError("UpdateRelation: opponent not found") + } + return + + // for o := range c.g.Race[r1].Relations { + // if c.g.Race[r1].Relations[o].RaceID == c.g.Race[r2].ID { + // c.g.Race[r1].Relations[o].Relation = rel + // if c.cacheRelation != nil { + // c.updateRelationCache(r1, r2, rel) + // } + // return nil + // } // } - // return nil - // return g.updateRelationInternal(ri, other, rel) + // return e.NewGameStateError("UpdateRelation: opponent not found") +} + +func (c *Cache) validateRaceIndex(i int) { + if i >= len(c.g.Race) { + panic(fmt.Sprintf("race index out of range: %d >= %d", i, len(c.g.Race))) + } } func (c *Cache) raceIndex(name string) (int, error) { @@ -80,3 +141,8 @@ func (c *Cache) raceIndex(name string) (int, error) { } return i, nil } + +func (c *Cache) racetTechLevel(ri int, t game.Tech, v float64) { + c.validateFleetIndex(ri) + c.g.Race[ri].Tech = c.g.Race[ri].Tech.Set(t, v) +} diff --git a/internal/controller/race_test.go b/internal/controller/race_test.go new file mode 100644 index 0000000..a1d7814 --- /dev/null +++ b/internal/controller/race_test.go @@ -0,0 +1,22 @@ +package controller_test + +import ( + "testing" + + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/stretchr/testify/assert" +) + +func TestGiveVotes(t *testing.T) { + c, _ := newCache() + + assert.Equal(t, c.Voted(Race_0_idx), Race_0_idx) + assert.Equal(t, c.Voted(Race_1_idx), Race_1_idx) + + assert.NoError(t, c.GiveVotes(Race_0.Name, Race_1.Name)) + assert.Equal(t, Race_1_idx, c.Voted(Race_0_idx)) + assert.Equal(t, Race_1_idx, c.Voted(Race_1_idx)) + + assert.ErrorContains(t, c.GiveVotes("UnknownRace", Race_1.Name), e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, c.GiveVotes(Race_0.Name, "UnknownRace"), e.GenericErrorText(e.ErrInputUnknownRace)) +} diff --git a/internal/controller/ship_class.go b/internal/controller/ship_class.go index ed759f0..44de6e1 100644 --- a/internal/controller/ship_class.go +++ b/internal/controller/ship_class.go @@ -1,6 +1,7 @@ package controller import ( + "fmt" "slices" "github.com/google/uuid" @@ -8,6 +9,14 @@ import ( "github.com/iliadenisov/galaxy/internal/model/game" ) +func (c *Controller) CreateShipType(raceName, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.CreateShipType(ri, typeName, drive, ammo, weapons, shileds, cargo) +} + func (c *Cache) ShipClass(ri int, name string) (*game.ShipType, int, bool) { i := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == name }) if i < 0 { @@ -16,25 +25,17 @@ func (c *Cache) ShipClass(ri int, name string) (*game.ShipType, int, bool) { return &c.g.Race[ri].ShipTypes[i], i, true } -func (c *Cache) CreateShipType(raceName, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error { - ri, err := c.raceIndex(raceName) - if err != nil { +func (c *Cache) CreateShipType(ri int, typeName string, drive float64, ammo int, weapons, shileds, cargo float64) error { + c.validateRaceIndex(ri) + if err := checkShipTypeValues(drive, ammo, weapons, shileds, cargo); err != nil { return err } - _, err = c.createShipTypeInternal(ri, typeName, drive, ammo, weapons, shileds, cargo) - return err -} - -func (c *Cache) createShipTypeInternal(ri int, name string, drive float64, ammo int, weapons, shileds, cargo float64) (int, error) { - if err := checkShipTypeValues(drive, ammo, weapons, shileds, cargo); err != nil { - return -1, err - } - n, ok := validateTypeName(name) + n, ok := validateTypeName(typeName) if !ok { - return -1, e.NewEntityTypeNameValidationError("%q", n) + return e.NewEntityTypeNameValidationError("%q", n) } - if st := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == name }); st >= 0 { - return -1, e.NewEntityTypeNameDuplicateError("ship type %w", c.g.Race[ri].ShipTypes[st].Name) + if st := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == typeName }); st >= 0 { + return e.NewEntityTypeNameDuplicateError("ship type %w", c.g.Race[ri].ShipTypes[st].Name) } c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes, game.ShipType{ ID: uuid.New(), @@ -47,7 +48,124 @@ func (c *Cache) createShipTypeInternal(ri int, name string, drive float64, ammo Armament: uint(ammo), }, }) - return len(c.g.Race[ri].ShipTypes) - 1, nil + return nil +} + +func (c *Controller) MergeShipType(race, name, targetName string) error { + ri, err := c.Cache.raceIndex(race) + if err != nil { + return err + } + return c.Cache.MergeShipType(ri, name, targetName) +} + +func (c *Cache) MergeShipType(ri int, name, targetName string) error { + c.validateRaceIndex(ri) + st, sti, ok := c.ShipClass(ri, name) + + // st := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == name }) + if !ok { + return e.NewEntityNotExistsError("source ship type %w", name) + } + if name == targetName { + return e.NewEntityTypeNameEqualityError("ship type %q", targetName) + } + tt, _, ok := c.ShipClass(ri, name) + // tt := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == targetName }) + if !ok { + return e.NewEntityNotExistsError("target ship type %w", name) + } + if !st.Equal(*tt) { + return e.NewMergeShipTypeNotEqualError() + } + + // switch planet productions to the new type + for pl := range c.g.Map.Planet { + if c.g.Map.Planet[pl].Owner == c.g.Race[ri].ID && + c.g.Map.Planet[pl].Production.Type == game.ProductionShip && + c.g.Map.Planet[pl].Production.SubjectID != nil && + *c.g.Map.Planet[pl].Production.SubjectID == st.ID { + + c.g.Map.Planet[pl].Production.SubjectID = &tt.ID + } + } + + // switch ship groups to the new type + for sg := range c.g.ShipGroups { + if c.g.ShipGroups[sg].OwnerID == c.g.Race[ri].ID && c.g.ShipGroups[sg].TypeID == st.ID { + c.g.ShipGroups[sg].TypeID = tt.ID + } + } + + // remove the source type + c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes[:sti], c.g.Race[ri].ShipTypes[sti+1:]...) + + return nil +} + +func (c *Controller) DeleteShipType(raceName, typeName string) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.DeleteShipType(ri, typeName) +} + +func (c *Cache) DeleteShipType(ri int, name string) error { + c.validateRaceIndex(ri) + st, i, ok := c.ShipClass(ri, name) + if !ok { + return e.NewEntityNotExistsError("ship type %w", name) + } + // st := slices.IndexFunc(c.g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.Name == name }) + // if st < 0 { + // return e.NewEntityNotExistsError("ship type %w", name) + // } + if pl := slices.IndexFunc(c.g.Map.Planet, func(p game.Planet) bool { + return p.Production.Type == game.ProductionShip && + p.Production.SubjectID != nil && + st.ID == *p.Production.SubjectID + }); pl >= 0 { + return e.NewDeleteShipTypePlanetProductionError(c.g.Map.Planet[pl].Name) + } + + for sg := range c.listShipGroups(ri) { + if sg.TypeID == st.ID { + return e.NewDeleteShipTypeExistingGroupError("group: %v", sg.Index) + } + } + c.g.Race[ri].ShipTypes = append(c.g.Race[ri].ShipTypes[:i], c.g.Race[ri].ShipTypes[i+1:]...) + // FIXME: update cache ON ALL FUNCs IN THIS FILE + return nil +} + +// ShipTypes used for tests only +func (c *Controller) ShipTypes(race string) ([]*game.ShipType, error) { + ri, err := c.Cache.raceIndex(race) + if err != nil { + return nil, err + } + return c.Cache.ShipTypes(ri), nil +} + +// ShipTypes used for tests only +func (c *Cache) ShipTypes(ri int) []*game.ShipType { + c.validateRaceIndex(ri) + result := make([]*game.ShipType, len(c.g.Race[ri].ShipTypes)) + for i := range c.g.Race[ri].ShipTypes { + result[i] = &c.g.Race[ri].ShipTypes[i] + } + return result +} + +func (c *Cache) ShipType(ri int, ID uuid.UUID) *game.ShipType { + c.validateRaceIndex(ri) + for i := range c.g.Race[ri].ShipTypes { + if c.g.Race[ri].ShipTypes[i].ID == ID { + return &c.g.Race[ri].ShipTypes[i] + } + } + panic(fmt.Sprintf("ship_class not found: race_id=%d id=%v", ri, ID)) } func checkShipTypeValues(d float64, a int, w, s, c float64) error { diff --git a/internal/controller/ship_group.go b/internal/controller/ship_group.go index 2e98ae8..b1be59b 100644 --- a/internal/controller/ship_group.go +++ b/internal/controller/ship_group.go @@ -3,7 +3,10 @@ package controller import ( "fmt" "iter" + "maps" + "slices" + "github.com/google/uuid" e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/model/game" ) @@ -23,8 +26,9 @@ func (c *Cache) CreateShips(ri int, shipTypeName string, planetNumber uint, quan return e.NewEntityNotOwnedError("planet #%d", planetNumber) } + // FIXME: move maxindex to appendShipGroup nextIndex := c.ShipGroupMaxIndex(ri) + 1 - c.g.ShipGroups = append(c.g.ShipGroups, game.ShipGroup{ + c.appendShipGroup(ri, class, &game.ShipGroup{ Index: nextIndex, OwnerID: c.g.Race[ri].ID, TypeID: class.ID, @@ -37,12 +41,6 @@ func (c *Cache) CreateShips(ri int, shipTypeName string, planetNumber uint, quan game.TechCargo: c.g.Race[ri].TechLevel(game.TechCargo), }, }) - if c.raceIndexByShipGroupIndex != nil { - c.raceIndexByShipGroupIndex[len(c.g.ShipGroups)-1] = ri - } - if c.shipClassByShipGroupIndex != nil { - c.shipClassByShipGroupIndex[len(c.g.ShipGroups)-1] = class - } return nil } @@ -52,6 +50,19 @@ func (c *Cache) ShipGroup(groupIndex int) *game.ShipGroup { return &c.g.ShipGroups[groupIndex] } +func (c *Cache) ShipGroupFleet(groupIndex int, fID *uuid.UUID) { + c.validateShipGroupIndex(groupIndex) + c.g.ShipGroups[groupIndex].FleetID = fID +} + +func (c *Cache) ShipGroupShipsNumber(groupIndex int, number uint) { + c.validateShipGroupIndex(groupIndex) + if c.g.ShipGroups[groupIndex].Number > 0 { + c.g.ShipGroups[groupIndex].Load = c.g.ShipGroups[groupIndex].Load / float64(c.g.ShipGroups[groupIndex].Number) * float64(number) + } + c.g.ShipGroups[groupIndex].Number = number +} + func (c *Cache) ShipGroupsIndex() iter.Seq[int] { return func(yield func(int) bool) { for i := range c.g.ShipGroups { @@ -73,10 +84,10 @@ func (c *Cache) ShipGroupMaxIndex(ri int) uint { } func (c *Cache) ShipGroupOwnerRaceIndex(groupIndex int) int { - if c.raceIndexByShipGroupIndex == nil { - c.fillShipsAndGroups() - } c.validateShipGroupIndex(groupIndex) + if len(c.raceIndexByShipGroupIndex) == 0 { + c.cacheShipsAndGroups() + } if v, ok := c.raceIndexByShipGroupIndex[groupIndex]; ok { return v } else { @@ -105,3 +116,527 @@ func (c *Cache) DeleteKilledShipGroups() { } } } + +func (c *Controller) JoinEqualGroups(raceName string) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + c.Cache.JoinEqualGroups(ri) + return nil +} + +func (c *Cache) CmdJoinEqualGroups() { + for i := range c.g.Race { + c.JoinEqualGroups(i) + } +} + +func (c *Cache) JoinEqualGroups(ri int) { + c.validateRaceIndex(ri) + shipGroups := slices.Collect(c.listShipGroups(ri)) + origin := len(shipGroups) + if origin < 2 { + return + } + for i := 0; i < len(shipGroups)-1; i++ { + for j := len(shipGroups) - 1; j > i; j-- { + if shipGroups[i].Equal(*shipGroups[j]) { + shipGroups[i].Index = maxUint(shipGroups[i].Index, shipGroups[j].Index) + shipGroups[i].Number += shipGroups[j].Number + shipGroups = append(shipGroups[:j], shipGroups[j+1:]...) + } + } + } + if len(shipGroups) == origin { + return + } + + toDelete := make([]int, 0) + for i := range c.ShipGroupsIndex() { + if c.ShipGroup(i).OwnerID == c.g.Race[ri].ID { + toDelete = append(toDelete, i) + } + } + + // c.g.ShipGroups = slices.DeleteFunc(c.g.ShipGroups, func(v game.ShipGroup) bool { return v.OwnerID == c.g.Race[ri].ID }) + for _, idx := range toDelete { + c.unsafeDeleteShipGroup(idx) + } + + // c.g.ShipGroups = append(c.g.ShipGroups, shipGroups...) + for i := range shipGroups { + c.g.ShipGroups = append(c.g.ShipGroups, *shipGroups[i]) + } + + c.invalidateShipGroupCache() +} + +func (c *Controller) BreakGroup(raceName string, groupIndex, quantity uint) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.BreakGroup(ri, groupIndex, quantity) +} + +func (c *Controller) DisassembleGroup(raceName string, groupIndex, quantity uint) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.DisassembleGroup(ri, groupIndex, quantity) +} + +func (c *Cache) DisassembleGroup(ri int, groupIndex, quantity uint) error { + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + + if c.ShipGroup(sgi).State() != game.StateInOrbit { + return e.NewShipsBusyError() + } + + if c.ShipGroup(sgi).Number < quantity { + return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) + } + + p, ok := c.Planet(c.ShipGroup(sgi).Destination) + if !ok { + return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + } + + // pl := slices.IndexFunc(g.Map.Planet, func(p game.Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) + // if pl < 0 { + // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + // } + + st := c.ShipGroupShipClass(sgi) + // var sti int + // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { + // // hard to test, need manual game data invalidation + // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) + // } + + if quantity > 0 && quantity < c.ShipGroup(sgi).Number { + // make new group for disassembly + nsgi, err := c.breakGroupSafe(ri, groupIndex, quantity) + if err != nil { + return err + } + sgi = nsgi + } + + if c.ShipGroup(sgi).CargoType != nil { + ct := *c.ShipGroup(sgi).CargoType + load := c.ShipGroup(sgi).Load + switch ct { + case game.CargoColonist: + if p.Owner == c.g.Race[ri].ID { + pn := UnloadColonists(*p, load) + p = &pn + } + case game.CargoMaterial: + p.Material += load + case game.CargoCapital: + p.Capital += load + } + } + + p.Material += c.ShipGroup(sgi).EmptyMass(st) + + // g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) + c.unsafeDeleteShipGroup(sgi) + + return nil +} + +func (c *Controller) LoadCargo(raceName string, groupIndex uint, cargoType string, ships uint, quantity float64) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + ct, ok := game.CargoTypeSet[cargoType] + if !ok { + return e.NewCargoTypeInvalidError(cargoType) + } + return c.Cache.LoadCargo(ri, groupIndex, ct, ships, quantity) +} + +// Корабль может нести только один тип груза одновременно. +// Возможные типы груза - это колонисты, сырье и промышленность. +// Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется. +func (c *Cache) LoadCargo(ri int, groupIndex uint, ct game.CargoType, ships uint, quantity float64) error { + if ships == 0 && quantity > 0 { + return e.NewCargoQuantityWithoutGroupBreakError() + } + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + if c.ShipGroup(sgi).State() != game.StateInOrbit { + return e.NewShipsBusyError() + } + p, ok := c.Planet(c.ShipGroup(sgi).Destination) + if !ok { + return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + } + + // pl := slices.IndexFunc(g.Map.Planet, func(p game.Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) + // if pl < 0 { + // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + // } + if p.Owner != uuid.Nil && p.Owner != c.g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet #%d", p.Number) + } + st := c.ShipGroupShipClass(sgi) + // var sti int + // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { + // // hard to test, need manual game data invalidation + // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) + // } + if st.Cargo < 1 { + return e.NewNoCargoBayError("ship_type %q", st.Name) + } + if c.ShipGroup(sgi).CargoType != nil && *c.ShipGroup(sgi).CargoType != ct { + return e.NewCargoLoadNotEqualError("cargo: %v", *c.ShipGroup(sgi).CargoType) + } + if ships > 0 && ships < c.ShipGroup(sgi).Number { + nsgi, err := c.breakGroupSafe(ri, groupIndex, ships) + if err != nil { + return err + } + sgi = nsgi + } + capacity := c.ShipGroup(sgi).CargoCapacity(st) + freeShipGroupCargoLoad := capacity - c.ShipGroup(sgi).Load + if freeShipGroupCargoLoad == 0 { + return e.NewCargoLoadNoSpaceLeftError() + } + var availableOnPlanet *float64 + switch ct { + case game.CargoMaterial: + availableOnPlanet = &p.Material + case game.CargoCapital: + availableOnPlanet = &p.Capital + case game.CargoColonist: + availableOnPlanet = &p.Colonists + default: + return e.NewGameStateError("CargoType not accepted: %v", ct) + } + if quantity > *availableOnPlanet || *availableOnPlanet == 0 { + return e.NewCargoLoadNotEnoughError("planet: #%d, %s=%.03f", p.Number, ct, *availableOnPlanet) + } + toBeLoaded := quantity + if quantity == 0 { + toBeLoaded = *availableOnPlanet + } + if toBeLoaded > freeShipGroupCargoLoad { + toBeLoaded = freeShipGroupCargoLoad + } + *availableOnPlanet = *availableOnPlanet - toBeLoaded + c.ShipGroup(sgi).Load += toBeLoaded + if c.ShipGroup(sgi).Load > 0 { + c.ShipGroup(sgi).CargoType = &ct + } + return nil +} + +func (c *Controller) UnloadCargo(raceName string, groupIndex uint, ships uint, quantity float64) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.UnloadCargo(ri, groupIndex, ships, quantity) +} + +// Промышленность и Сырье могут быть выгружены на любой планете. +// Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты. +func (c *Cache) UnloadCargo(ri int, groupIndex uint, ships uint, quantity float64) error { + c.validateRaceIndex(ri) + if ships == 0 && quantity > 0 { + return e.NewCargoQuantityWithoutGroupBreakError() + } + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + if c.ShipGroup(sgi).State() != game.StateInOrbit { + return e.NewShipsBusyError() + } + st := c.ShipGroupShipClass(sgi) + // var sti int + // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st game.ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { + // // hard to test, need manual game data invalidation + // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) + // } + if st.Cargo < 1 { + return e.NewNoCargoBayError("ship_type %q", st.Name) + } + if c.ShipGroup(sgi).CargoType == nil || c.ShipGroup(sgi).Load == 0 { + return e.NewCargoUnloadEmptyError() + } + ct := *c.ShipGroup(sgi).CargoType + p, ok := c.Planet(c.ShipGroup(sgi).Destination) + if !ok { + return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + } + // pl := slices.IndexFunc(g.Map.Planet, func(p game.Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) + // if pl < 0 { + // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + // } + if ct == game.CargoColonist { + if p.Owner != uuid.Nil && p.Owner != c.g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet #%d unload %v", p.Number, ct) + } + if p.Owner == uuid.Nil { + p.Owner = c.g.Race[ri].ID + } + } + var availableOnPlanet *float64 + switch ct { + case game.CargoMaterial: + availableOnPlanet = &p.Material + case game.CargoCapital: + availableOnPlanet = &p.Capital + case game.CargoColonist: + availableOnPlanet = &p.Colonists + default: + return e.NewGameStateError("CargoType not accepted: %v", ct) + } + if ships > 0 && ships < c.ShipGroup(sgi).Number { + nsgi, err := c.breakGroupSafe(ri, groupIndex, ships) + if err != nil { + return err + } + sgi = nsgi + } + toBeUnloaded := quantity + if quantity == 0 { + toBeUnloaded = c.ShipGroup(sgi).Load + } + if toBeUnloaded > c.ShipGroup(sgi).Load { + return e.NewCargoUnoadNotEnoughError("load: %.03f", c.ShipGroup(sgi).Load) + } + *availableOnPlanet += toBeUnloaded + c.ShipGroup(sgi).Load -= toBeUnloaded + if c.ShipGroup(sgi).Load == 0 { + c.ShipGroup(sgi).CargoType = nil + } + return nil +} +func (c *Controller) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + riAccept, err := c.Cache.raceIndex(raceAcceptor) + if err != nil { + return err + } + return c.Cache.GiveawayGroup(ri, riAccept, groupIndex, quantity) +} + +func (c *Cache) GiveawayGroup(ri, riAccept int, groupIndex, quantity uint) (err error) { + if ri == riAccept { + return e.NewSameRaceError(c.g.Race[riAccept].Name) + } + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + if c.ShipGroup(sgi).Number < quantity { + return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) + } + + st := c.ShipGroupShipClass(sgi) + // var sti int + // if sti = slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.ID == c.ShipGroup(sgi).TypeID }); sti < 0 { + // // hard to test, need manual game data invalidation + // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) + // } + + var stAcc int + if stAcc = slices.IndexFunc(c.g.Race[riAccept].ShipTypes, func(v game.ShipType) bool { return v.Name == st.Name }); stAcc >= 0 && + !st.Equal(c.g.Race[riAccept].ShipTypes[stAcc]) { + return e.NewGiveawayGroupShipsTypeNotEqualError("race %w, ship type %w", c.g.Race[riAccept].Name, c.g.Race[riAccept].ShipTypes[stAcc].Name) + } + if stAcc < 0 { + err = c.CreateShipType(riAccept, + st.Name, + st.Drive, + int(st.Armament), + st.Weapons, + st.Shields, + st.Cargo) + stAcc = len(c.g.Race[ri].ShipTypes) - 1 + if err != nil { + return err + } + } + + // maxIndex := c.ShipGroupMaxIndex(riAccept) + // var maxIndex uint + // for sg := range g.listShipGroups(riAccept) { + // if sg.Index > maxIndex { + // maxIndex = sg.Index + // } + // } + + c.appendShipGroup(ri, st, &game.ShipGroup{ + // Index: maxIndex + 1, + OwnerID: c.g.Race[riAccept].ID, + TypeID: c.g.Race[riAccept].ShipTypes[stAcc].ID, + Number: uint(quantity), + + CargoType: c.ShipGroup(sgi).CargoType, + Load: c.ShipGroup(sgi).Load, + + Tech: maps.Clone(c.ShipGroup(sgi).Tech), + + Destination: c.ShipGroup(sgi).Destination, + StateInSpace: c.ShipGroup(sgi).StateInSpace, + StateUpgrade: c.ShipGroup(sgi).StateUpgrade, + }) + + // g.ShipGroups = append(g.ShipGroups, game.ShipGroup{ + // Index: maxIndex + 1, + // OwnerID: g.Race[riAccept].ID, + // TypeID: g.Race[riAccept].ShipTypes[stAcc].ID, + // Number: uint(quantity), + + // CargoType: c.ShipGroup(sgi).CargoType, + // Load: c.ShipGroup(sgi).Load, + + // Tech: maps.Clone(c.ShipGroup(sgi).Tech), + + // Destination: c.ShipGroup(sgi).Destination, + // StateInSpace: c.ShipGroup(sgi).StateInSpace, + // StateUpgrade: c.ShipGroup(sgi).StateUpgrade, + // }) + + if quantity == 0 || quantity == c.ShipGroup(sgi).Number { + c.unsafeDeleteShipGroup(sgi) + // g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) + } else { + c.ShipGroup(sgi).Number -= quantity + } + + return nil +} + +func (c *Cache) BreakGroup(ri int, groupIndex, quantity uint) error { + c.validateRaceIndex(ri) + sgi := -1 + for i := range c.ShipGroupsIndex() { + if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).Index == groupIndex { + sgi = i + break + } + } + if sgi < 0 { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + + if c.ShipGroup(sgi).State() != game.StateInOrbit { + return e.NewShipsBusyError() + } + + if c.ShipGroup(sgi).Number < quantity { + return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) + } + + if quantity == 0 || quantity == c.ShipGroup(sgi).Number { + c.ShipGroupFleet(sgi, nil) + } else { + if _, err := c.breakGroupSafe(ri, groupIndex, quantity); err != nil { + return err + } + } + + return nil +} + +func (c *Cache) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int, error) { + c.validateRaceIndex(ri) + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return -1, e.NewEntityNotExistsError("group #%d", groupIndex) + } + // maxIndex := c.ShipGroupMaxIndex(ri) + if c.ShipGroup(sgi).Number < newGroupShips { + return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", c.ShipGroup(sgi).Index, c.ShipGroup(sgi).Number, newGroupShips) + } + newGroup := *c.ShipGroup(sgi) + if c.ShipGroup(sgi).CargoType != nil { + newGroup.Load = c.ShipGroup(sgi).Load / float64(c.ShipGroup(sgi).Number) * float64(newGroupShips) + // c.ShipGroup(sgi).Load -= newGroup.Load + } + newGroup.Number = newGroupShips + c.ShipGroupShipsNumber(sgi, c.ShipGroup(sgi).Number-newGroup.Number) + // c.ShipGroup(sgi).Number -= newGroup.Number + // newGroup.Index = maxIndex + 1 + newGroup.FleetID = nil + st := c.ShipGroupShipClass(sgi) + // c.g.ShipGroups = append(c.g.ShipGroups, newGroup) + return c.appendShipGroup(ri, st, &newGroup), nil +} + +// Internal funcs + +func (c *Cache) appendShipGroup(ri int, class *game.ShipType, sg *game.ShipGroup) int { + c.validateRaceIndex(ri) + sg.Index = c.ShipGroupMaxIndex(ri) + 1 + c.g.ShipGroups = append(c.g.ShipGroups, *sg) + i := len(c.g.ShipGroups) - 1 + c.cacheShipGroup(i, ri, class) + return i +} + +func (c *Cache) raceShipGroupIndex(ri int, index uint) (int, bool) { + c.validateRaceIndex(ri) + for i := range c.ShipGroupsIndex() { + if c.ShipGroupOwnerRaceIndex(i) == ri && c.ShipGroup(i).Index == index { + return i, true + } + } + return -1, false +} + +func (c *Cache) listShipGroups(ri int) iter.Seq[*game.ShipGroup] { + c.validateRaceIndex(ri) + return func(yield func(*game.ShipGroup) bool) { + for i := range c.g.ShipGroups { + if ri == c.ShipGroupOwnerRaceIndex(i) { + if !yield(&c.g.ShipGroups[i]) { + return + } + } + } + } +} + +func (c *Cache) shipGroupsInUpgrade(planetNumber uint) iter.Seq[*game.ShipGroup] { + return func(yield func(*game.ShipGroup) bool) { + for sg := range c.g.ShipGroups { + if c.g.ShipGroups[sg].Destination == planetNumber && c.g.ShipGroups[sg].State() == game.StateUpgrade { + if !yield(&c.g.ShipGroups[sg]) { + break + } + } + } + } +} + +func (c *Cache) unsafeDeleteShipGroup(i int) { + c.g.ShipGroups = append(c.g.ShipGroups[:i], c.g.ShipGroups[i+1:]...) + delete(c.raceIndexByShipGroupIndex, i) + delete(c.shipClassByShipGroupIndex, i) +} + +func (c *Cache) validateShipGroupIndex(i int) { + if i >= len(c.g.ShipGroups) { + panic(fmt.Sprintf("group index out of range: %d >= %d", i, len(c.g.ShipGroups))) + } +} diff --git a/internal/controller/ship_group_send.go b/internal/controller/ship_group_send.go new file mode 100644 index 0000000..5074b56 --- /dev/null +++ b/internal/controller/ship_group_send.go @@ -0,0 +1,126 @@ +package controller + +import ( + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/iliadenisov/galaxy/internal/util" +) + +func (c *Controller) SendGroup(raceName string, groupIndex, planetNumber, quantity uint) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.SendGroup(ri, groupIndex, planetNumber, quantity) +} + +func (c *Cache) SendGroup(ri int, groupIndex, planetNumber, quantity uint) error { + c.validateRaceIndex(ri) + + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + st := c.ShipGroupShipClass(sgi) + + // 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] + + if st.DriveBlockMass() == 0 { + return e.NewSendShipHasNoDrivesError() + } + + sourcePlanet, ok := c.ShipGroup(sgi).OnPlanet() + if !ok { + return e.NewShipsBusyError() + } + + if c.ShipGroup(sgi).Number < quantity { + return e.NewBeakGroupNumberNotEnoughError("%d<%d", c.ShipGroup(sgi).Number, quantity) + } + + p1 := c.MustPlanet(sourcePlanet) + // p1, ok := PlanetByNum(g, sourcePlanet) + // if !ok { + // return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) + // } + p2 := c.MustPlanet(planetNumber) + // p2, ok := PlanetByNum(g, planetNumber) + // if !ok { + // return e.NewEntityNotExistsError("destination planet #%d", planetNumber) + // } + rangeToDestination := util.ShortDistance(c.g.Map.Width, c.g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) + if rangeToDestination > c.g.Race[ri].FlightDistance() { + return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) + } + + if quantity > 0 && quantity < c.ShipGroup(sgi).Number { + nsgi, err := c.breakGroupSafe(ri, groupIndex, quantity) + if err != nil { + return err + } + sgi = nsgi + } + + if sourcePlanet == planetNumber { + c.UnsendShips(*c.ShipGroup(sgi)) + c.JoinEqualGroups(ri) + return nil + } + + c.LaunchShips(*c.ShipGroup(sgi), planetNumber) + + return nil +} + +func (c *Cache) LaunchShips(sg game.ShipGroup, destination uint) *game.ShipGroup { + for i := range c.ShipGroupsIndex() { + if c.ShipGroup(i).OwnerID == sg.OwnerID && c.ShipGroup(i).Index == sg.Index { + state := c.g.ShipGroups[i].State() + if state != game.StateInOrbit && state != game.StateLaunched { + panic("state invalid") + } + c.g.ShipGroups[i] = LaunchShips(sg, destination) + return &c.g.ShipGroups[i] + } + } + panic("ship group not found") +} + +func (c *Cache) UnsendShips(sg game.ShipGroup) *game.ShipGroup { + for i := range c.ShipGroupsIndex() { + if c.ShipGroup(i).OwnerID == sg.OwnerID && c.ShipGroup(i).Index == sg.Index { + c.g.ShipGroups[i] = UnsendShips(sg) + return &c.g.ShipGroups[i] + } + } + panic("ship group not found") +} + +func LaunchShips(sg game.ShipGroup, destination uint) game.ShipGroup { + sg.StateInSpace = &game.InSpace{ + Origin: sg.Destination, + } + sg.Destination = destination + return sg +} + +func UnsendShips(sg game.ShipGroup) game.ShipGroup { + sg.Destination = sg.StateInSpace.Origin + sg.StateInSpace = nil + return sg +} diff --git a/internal/controller/ship_group_send_test.go b/internal/controller/ship_group_send_test.go new file mode 100644 index 0000000..9f121e9 --- /dev/null +++ b/internal/controller/ship_group_send_test.go @@ -0,0 +1,70 @@ +package controller_test + +import ( + "slices" + "testing" + + e "github.com/iliadenisov/galaxy/internal/error" + + "github.com/iliadenisov/galaxy/internal/model/game" + "github.com/stretchr/testify/assert" +) + +func TestSendGroup(t *testing.T) { + c, g := newCache() + // group #1 - in_orbit, free to upgrade + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 10)) + // group #2 - in_space + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) + // g.ShipGroups[1].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} + c.ShipGroup(1).StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} + // group #3 - in_orbit, unmovable + g.CreateShipType(Race_0.Name, "Fortress", 0, 50, 30, 100, 0) + assert.NoError(t, c.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1)) + + assert.ErrorContains(t, + g.SendGroup("UnknownRace", 1, 2, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.SendGroup(Race_0.Name, 555, 2, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.SendGroup(Race_0.Name, 1, 222, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.SendGroup(Race_0.Name, 2, 1, 0), + e.GenericErrorText(e.ErrShipsBusy)) + assert.ErrorContains(t, + g.SendGroup(Race_0.Name, 3, 2, 0), + e.GenericErrorText(e.ErrSendShipHasNoDrives)) + assert.ErrorContains(t, + g.SendGroup(Race_0.Name, 1, 2, 100), + e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) + assert.ErrorContains(t, + g.SendGroup(Race_0.Name, 1, 3, 0), + e.GenericErrorText(e.ErrSendUnreachableDestination)) + + assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 3)) // send 3 of 10 + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, uint(7), c.ShipGroup(0).Number) + assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State()) + assert.Equal(t, uint(3), c.ShipGroup(3).Number) + assert.Equal(t, game.StateLaunched, c.ShipGroup(3).State()) + + assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 2)) // un-send 2 of 3 + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, uint(9), c.MustShipGroup(Race_0_idx, 5).Number) + assert.Equal(t, game.StateInOrbit, c.MustShipGroup(Race_0_idx, 5).State()) + assert.Equal(t, uint(1), c.MustShipGroup(Race_0_idx, 4).Number) + assert.Equal(t, game.StateLaunched, c.MustShipGroup(Race_0_idx, 4).State()) + + assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 0)) // un-send the rest 1 + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 3) + assert.Equal(t, uint(10), c.MustShipGroup(Race_0_idx, 5).Number) + assert.Equal(t, game.StateInOrbit, c.MustShipGroup(Race_0_idx, 5).State()) + + assert.NoError(t, g.SendGroup(Race_0.Name, 5, 2, 0)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 3) + assert.Equal(t, uint(10), c.MustShipGroup(Race_0_idx, 5).Number) + assert.Equal(t, game.StateLaunched, c.MustShipGroup(Race_0_idx, 5).State()) +} diff --git a/internal/controller/ship_group_test.go b/internal/controller/ship_group_test.go new file mode 100644 index 0000000..18dcd94 --- /dev/null +++ b/internal/controller/ship_group_test.go @@ -0,0 +1,539 @@ +package controller_test + +import ( + "slices" + "testing" + + "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" +) + +func TestCreateShips(t *testing.T) { + c, _ := newCache() + + assert.ErrorContains(t, + c.CreateShips(Race_0_idx, "Unknown_Ship_Type", R0_Planet_0_num, 2), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + c.CreateShips(Race_0_idx, Race_0_Gunship, R1_Planet_1_num, 2), + e.GenericErrorText(e.ErrInputEntityNotOwned)) + + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 1) + + assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1)) + assert.Len(t, slices.Collect(c.ListShipGroups(1)), 1) + + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 2) + + assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1)) + assert.Len(t, slices.Collect(c.ListShipGroups(1)), 2) +} + +func TestJoinEqualGroups(t *testing.T) { + c, _ := newCache() + + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // 1 -> 2 + assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1)) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) // (2) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) // (3) + assert.NoError(t, c.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1)) + + // g.Race[Race_0_idx].SetTechLevel(game.TechDrive, 1.5) + c.RacetTechLevel(Race_0_idx, game.TechDrive, 1.5) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 9)) // 4 -> 6 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) // 5 -> 7 + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 4)) // (6) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 4)) // (7) + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) + + // g.Race[Race_1_idx].SetTechLevel(game.TechShields, 2.0) + c.RacetTechLevel(Race_1_idx, game.TechShields, 2.0) + assert.NoError(t, c.CreateShips(1, Race_1_Freighter, R1_Planet_1_num, 1)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_1_idx)), 3) + + c.JoinEqualGroups(Race_0_idx) + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_1_idx)), 3) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 4) + + shipTypeID := func(ri int, name string) uuid.UUID { + class, _, ok := c.ShipClass(ri, name) + if !ok { + t.Fatalf("ShipType not found: %s", name) + return uuid.Nil + } + return class.ID + } + + for sg := range c.ListShipGroups(Race_0_idx) { + switch { + 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.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.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.TechLevel(game.TechDrive) == 1.5: + assert.Equal(t, uint(13), sg.Number) + assert.Equal(t, uint(6), sg.Index) + default: + t.Error("not all ship groups covered") + } + } +} + +func TestBreakGroup(t *testing.T) { + c, g := newCache() + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 13)) // group #1 (0) + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 7)) // group #2 (1) - In_Space + c.ShipGroup(1).StateInSpace = &game.InSpace{ + Origin: 1, + Range: 1, + } + + fleet := "R0_Fleet" + assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleet, 1, 0)) + + assert.ErrorContains(t, + g.BreakGroup("UnknownRace", 1, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.BreakGroup(Race_0.Name, 555, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.BreakGroup(Race_0.Name, 1, 17), + e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) + assert.ErrorContains(t, + g.BreakGroup(Race_0.Name, 2, 0), + e.GenericErrorText(e.ErrShipsBusy)) + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 2) + assert.Len(t, slices.Collect(c.ListFleets(Race_0_idx)), 1) + + // group #1 -> group #3 (5 new, 8 left) + assert.NoError(t, c.BreakGroup(Race_0_idx, 1, 5)) // group #3 (2) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 3) + assert.Equal(t, uint(8), c.ShipGroup(0).Number) + assert.NotNil(t, c.ShipGroup(0).FleetID) + assert.Equal(t, uint(5), c.ShipGroup(2).Number) + assert.Equal(t, uint(3), c.ShipGroup(2).Index) + assert.Nil(t, c.ShipGroup(2).FleetID) + assert.Nil(t, c.ShipGroup(2).CargoType) + + // group #1 -> group #4 (2 new, 6 left) + c.ShipGroup(0).CargoType = game.CargoColonist.Ref() + c.ShipGroup(0).Load = 32.8 // 8 ships + assert.NoError(t, c.BreakGroup(Race_0_idx, 1, 2)) // group #4 (3) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, uint(6), c.ShipGroup(0).Number) + assert.NotNil(t, c.ShipGroup(0).FleetID) + assert.Equal(t, uint(2), c.ShipGroup(3).Number) + assert.Equal(t, uint(4), c.ShipGroup(3).Index) + assert.Nil(t, c.ShipGroup(3).FleetID) + assert.NoError(t, c.JoinShipGroupToFleet(Race_0_idx, fleet, 4, 0)) + assert.NotNil(t, c.ShipGroup(3).FleetID) + + assert.Equal(t, game.CargoColonist.Ref(), c.ShipGroup(0).CargoType) + assert.Equal(t, 24.6, number.Fixed3(c.ShipGroup(0).Load)) + assert.Equal(t, game.CargoColonist.Ref(), c.ShipGroup(3).CargoType) + assert.Equal(t, 8.2, number.Fixed3(c.ShipGroup(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(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, uint(6), c.ShipGroup(0).Number) + assert.Nil(t, c.ShipGroup(0).FleetID) + + // group #4 -> ALL off the fleet + assert.NoError(t, g.BreakGroup(Race_0.Name, 4, 0)) // group #1 (0) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, uint(2), c.ShipGroup(3).Number) + assert.Nil(t, c.ShipGroup(3).FleetID) +} + +func TestGiveawayGroup(t *testing.T) { + c, g := newCache() + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 11)) // group #1 (0) + assert.NoError(t, c.CreateShips(Race_1_idx, ShipType_Cruiser, R1_Planet_1_num, 23)) // group #1 (1) + + assert.NoError(t, c.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, c.ShipGroup(2).FleetID) + c.ShipGroup(2).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 31.337, + } + c.ShipGroup(2).CargoType = game.CargoMaterial.Ref() + c.ShipGroup(2).Load = 1.234 + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 2) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_1_idx)), 1) + + assert.ErrorContains(t, + g.GiveawayGroup("UnknownRace", Race_1.Name, 2, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.GiveawayGroup(Race_0.Name, "UnknownRace", 2, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.GiveawayGroup(Race_0.Name, Race_0.Name, 2, 0), + e.GenericErrorText(e.ErrInputSameRace)) + assert.ErrorContains(t, + g.GiveawayGroup(Race_0.Name, Race_1.Name, 555, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.GiveawayGroup(Race_0.Name, Race_1.Name, 2, 18), + e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) + assert.ErrorContains(t, + g.GiveawayGroup(Race_0.Name, Race_1.Name, 1, 0), + e.GenericErrorText(e.ErrGiveawayGroupShipsTypeNotEqual)) + + assert.NoError(t, g.GiveawayGroup(Race_0.Name, Race_1.Name, 2, 11)) // group #2 (3) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 2) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_1_idx)), 2) + // sto := slices.IndexFunc(g.Race[Race_0_idx].ShipTypes, func(st game.ShipType) bool { return st.Name == Race_0_Gunship }) + // sti := slices.IndexFunc(g.Race[Race_1_idx].ShipTypes, func(st game.ShipType) bool { return st.Name == Race_0_Gunship }) + + c.MustShipClass(Race_0_idx, Race_0_Gunship) + assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Name, c.MustShipClass(Race_0_idx, Race_0_Gunship).Name) + assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Drive, c.MustShipClass(Race_0_idx, Race_0_Gunship).Drive) + assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Weapons, c.MustShipClass(Race_0_idx, Race_0_Gunship).Weapons) + assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Shields, c.MustShipClass(Race_0_idx, Race_0_Gunship).Shields) + assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Cargo, c.MustShipClass(Race_0_idx, Race_0_Gunship).Cargo) + assert.Equal(t, c.MustShipClass(Race_1_idx, Race_0_Gunship).Armament, c.MustShipClass(Race_0_idx, Race_0_Gunship).Armament) + assert.Equal(t, c.ShipGroup(2).State(), c.ShipGroup(3).State()) + assert.Equal(t, c.ShipGroup(2).CargoType, c.ShipGroup(3).CargoType) + assert.Equal(t, c.ShipGroup(2).Load, c.ShipGroup(3).Load) + assert.Equal(t, c.ShipGroup(2).TechLevel(game.TechDrive), c.ShipGroup(3).TechLevel(game.TechDrive)) + assert.Equal(t, c.ShipGroup(2).TechLevel(game.TechWeapons), c.ShipGroup(3).TechLevel(game.TechWeapons)) + assert.Equal(t, c.ShipGroup(2).TechLevel(game.TechShields), c.ShipGroup(3).TechLevel(game.TechShields)) + assert.Equal(t, c.ShipGroup(2).TechLevel(game.TechCargo), c.ShipGroup(3).TechLevel(game.TechCargo)) + assert.Equal(t, c.ShipGroup(2).Destination, c.ShipGroup(3).Destination) + assert.Equal(t, c.ShipGroup(2).StateInSpace, c.ShipGroup(3).StateInSpace) + assert.Equal(t, c.ShipGroup(2).StateUpgrade, c.ShipGroup(3).StateUpgrade) + assert.Equal(t, c.ShipGroup(3).OwnerID, Race_1_ID) + assert.Equal(t, c.ShipGroup(3).TypeID, c.MustShipClass(Race_1_idx, Race_0_Gunship).ID) + assert.Equal(t, c.ShipGroup(3).Number, uint(11)) + assert.Nil(t, c.ShipGroup(3).FleetID) + + assert.NoError(t, g.GiveawayGroup(Race_1.Name, Race_0.Name, 2, 11)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 3) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_1_idx)), 1) +} + +func TestLoadCargo(t *testing.T) { + c, g := newCache() + + // 1: idx = 0 / Ready to load + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + + // 2: idx = 1 / Has no cargo bay + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) + + // 3: idx = 2 / In_Space + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) + c.ShipGroup(2).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 31.337, + } + + // 4: idx = 3 / loaded with COL + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(3).CargoType = game.CargoColonist.Ref() + c.ShipGroup(3).Load = 1.234 + + // 5: idx = 4 / on foreign planet + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(4).Destination = R1_Planet_1_num + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 5) + + // tests + assert.ErrorContains(t, + g.LoadCargo("UnknownRace", 1, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, "GOLD", 0, 0), + e.GenericErrorText(e.ErrInputCargoTypeInvalid)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 555, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 3, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrShipsBusy)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 5, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputEntityNotOwned)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 2, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputNoCargoBay)) + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 4, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputCargoLoadNotEqual)) + + // initial planet is empty + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputCargoLoadNotEnough)) + // add cargo to planet + // g.Map.Planet[0].Material = 100 + c.PutMaterial(R0_Planet_0_num, 100) + // not enough on the planet + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 11, 101), + e.GenericErrorText(e.ErrInputCargoLoadNotEnough)) + // quantity > ships + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 1), + e.GenericErrorText(e.ErrInputCargoQuantityWithoutGroupBreak)) + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 5) + + // break group and load maximum + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 2, 0)) + assert.Equal(t, 58.0, c.MustPlanet(R0_Planet_0_num).Material) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 6) + assert.Nil(t, c.ShipGroup(0).CargoType) + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(5).CargoType) + assert.Equal(t, uint(9), c.ShipGroup(0).Number) + assert.Equal(t, 0.0, c.ShipGroup(0).Load) + assert.Equal(t, uint(2), c.ShipGroup(5).Number) + assert.Equal(t, 42.0, c.ShipGroup(5).Load) + + // break group and load limited + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 2, 18)) + assert.Equal(t, 40.0, c.MustPlanet(R0_Planet_0_num).Material) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) + assert.Nil(t, c.ShipGroup(0).CargoType) + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(6).CargoType) + assert.Equal(t, uint(7), c.ShipGroup(0).Number) + assert.Equal(t, 0.0, c.ShipGroup(0).Load) + assert.Equal(t, uint(2), c.ShipGroup(6).Number) + assert.Equal(t, 18.0, c.ShipGroup(6).Load) + + // add cargo to planet + // g.Map.Planet[0].Material = 100 + c.PutMaterial(R0_Planet_0_num, 100) + // loading all available cargo + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 0)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) + assert.Equal(t, 0.0, c.MustPlanet(R0_Planet_0_num).Material) + assert.Equal(t, 100.0, c.ShipGroup(0).Load) // free: 131.0 + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType) + + // add cargo to planet + // g.Map.Planet[0].Material = 200 + c.PutMaterial(R0_Planet_0_num, 200) + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 11, 31)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) + assert.Equal(t, 169.0, c.MustPlanet(R0_Planet_0_num).Material) + assert.Equal(t, 131.0, c.ShipGroup(0).Load) // free: 100.0 + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType) + + // load to maximum cargo space left + assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 11, 0)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) + assert.Equal(t, 153.0, c.MustPlanet(R0_Planet_0_num).Material) + assert.Equal(t, 147.0, c.ShipGroup(0).Load) // free: 0.0 + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(0).CargoType) + + // ship group is full + assert.ErrorContains(t, + g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 0), + e.GenericErrorText(e.ErrInputCargoLoadNoSpaceLeft)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) +} + +func TestUnloadCargo(t *testing.T) { + c, g := newCache() + + // 1: idx = 0 / empty + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) + + // 2: idx = 1 / Has no cargo bay + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 1)) + + // 3: idx = 2 / In_Space + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) + c.ShipGroup(2).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 31.337, + } + + // 4: idx = 3 / loaded with COL + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(3).CargoType = game.CargoColonist.Ref() + c.ShipGroup(3).Load = 1.234 + + // 5: idx = 4 / on foreign planet / loaded with COL + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(4).Destination = R1_Planet_1_num + c.ShipGroup(4).CargoType = game.CargoColonist.Ref() + c.ShipGroup(4).Load = 1.234 + + // 6: idx = 5 / on foreign planet / loaded with MAT + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(5).Destination = R1_Planet_1_num + c.ShipGroup(5).CargoType = game.CargoMaterial.Ref() + c.ShipGroup(5).Load = 100.0 + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 6) + + // tests + assert.ErrorContains(t, + g.UnloadCargo("UnknownRace", 1, 0, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 555, 0, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 3, 0, 0), + e.GenericErrorText(e.ErrShipsBusy)) + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 2, 0, 0), + e.GenericErrorText(e.ErrInputNoCargoBay)) + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 1, 0, 0), + e.GenericErrorText(e.ErrInputCargoUnloadEmpty)) + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 5, 0, 0), + e.GenericErrorText(e.ErrInputEntityNotOwned)) + c.ShipGroup(0).CargoType = game.CargoColonist.Ref() + c.ShipGroup(0).Load = 100 + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 1, 11, 101), + e.GenericErrorText(e.ErrInputCargoUnoadNotEnough)) + assert.ErrorContains(t, + g.UnloadCargo(Race_0.Name, 1, 0, 1), + e.GenericErrorText(e.ErrInputCargoQuantityWithoutGroupBreak)) + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 6) + + // unload MAT on foreign planet / break group + assert.NoError(t, g.UnloadCargo(Race_0.Name, 6, 3, 0)) + assert.Equal(t, 27.273, number.Fixed3(c.MustPlanet(R1_Planet_1_num).Material)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 7) + assert.Equal(t, uint(3), c.ShipGroup(6).Number) + assert.Nil(t, c.ShipGroup(6).CargoType) + assert.Equal(t, 0.0, c.ShipGroup(6).Load) + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(5).CargoType) + assert.Equal(t, uint(8), c.ShipGroup(5).Number) + assert.Equal(t, 72.727, number.Fixed3(c.ShipGroup(5).Load)) + + // unload MAT on foreign planet / break group / limited MAT + assert.NoError(t, g.UnloadCargo(Race_0.Name, 6, 3, 20.0)) + assert.Equal(t, 47.273, number.Fixed3(c.MustPlanet(R1_Planet_1_num).Material)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 8) + assert.Equal(t, uint(3), c.ShipGroup(7).Number) + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(7).CargoType) + assert.Equal(t, 7.273, number.Fixed3(c.ShipGroup(7).Load)) + assert.Equal(t, game.CargoMaterial.Ref(), c.ShipGroup(5).CargoType) + assert.Equal(t, uint(5), c.ShipGroup(5).Number) + assert.Equal(t, 45.455, number.Fixed3(c.ShipGroup(5).Load)) + + // unload ALL + assert.NoError(t, g.UnloadCargo(Race_0.Name, 1, 0, 0)) + assert.Equal(t, 100.0, number.Fixed3(c.MustPlanet(R0_Planet_0_num).Colonists)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 8) + assert.Equal(t, uint(10), c.ShipGroup(0).Number) + assert.Nil(t, c.ShipGroup(0).CargoType) + assert.Equal(t, 0.0, number.Fixed3(c.ShipGroup(0).Load)) +} + +func TestDisassembleGroup(t *testing.T) { + c, g := newCache() + + // 1: idx = 0 / empty + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) + + // 2: idx = 1 / In_Space + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 7)) + c.ShipGroup(1).StateInSpace = &game.InSpace{ + Origin: 2, + Range: 31.337, + } + + // 3: idx = 2 / loaded with COL + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 10)) + c.ShipGroup(2).CargoType = game.CargoColonist.Ref() + c.ShipGroup(2).Load = 80.0 + + // 4: idx = 3 / on foreign planet / loaded with MAT + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(3).Destination = R1_Planet_1_num + c.ShipGroup(3).CargoType = game.CargoMaterial.Ref() + c.ShipGroup(3).Load = 100.0 + + // 5: idx = 4 / on foreign planet / loaded with COL + assert.NoError(t, c.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) + c.ShipGroup(4).Destination = R1_Planet_1_num + c.ShipGroup(4).CargoType = game.CargoColonist.Ref() + c.ShipGroup(4).Load = 2.345 + + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 5) + + // tests + assert.ErrorContains(t, + g.DisassembleGroup("UnknownRace", 1, 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.DisassembleGroup(Race_0.Name, 555, 0), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.DisassembleGroup(Race_0.Name, 2, 0), + e.GenericErrorText(e.ErrShipsBusy)) + assert.ErrorContains(t, + g.DisassembleGroup(Race_0.Name, 3, 12), + e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) + + groupEmptyMass := c.ShipGroup(4).EmptyMass(c.MustShipClass(Race_0_idx, Race_0_Freighter)) + // groupEmptyMass := c.ShipGroup(4).EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) + // groupLoadCOL := c.ShipGroup(4).Load + planetMAT := c.MustPlanet(R1_Planet_1_num).Material + planetCOL := c.MustPlanet(R1_Planet_1_num).Colonists + + assert.NoError(t, g.DisassembleGroup(Race_0.Name, 5, 0)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, planetMAT+groupEmptyMass, c.MustPlanet(R1_Planet_1_num).Material) + assert.Equal(t, planetCOL, c.MustPlanet(R1_Planet_1_num).Colonists) + + groupEmptyMass = c.ShipGroup(3).EmptyMass(c.MustShipClass(Race_0_idx, Race_0_Freighter)) + // groupEmptyMass = c.ShipGroup(3).EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) + groupLoadMAT := c.ShipGroup(3).Load + planetMAT = c.MustPlanet(R1_Planet_1_num).Material + assert.NoError(t, g.DisassembleGroup(Race_0.Name, 4, 0)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 3) + assert.Equal(t, planetMAT+groupEmptyMass+groupLoadMAT, c.MustPlanet(R1_Planet_1_num).Material) + + groupEmptyMass = c.ShipGroup(2).EmptyMass(c.MustShipClass(Race_0_idx, Race_0_Freighter)) + // groupEmptyMass = c.ShipGroup(2).EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) + planetMAT = c.MustPlanet(R0_Planet_0_num).Material + planetCOL = c.MustPlanet(R0_Planet_0_num).Colonists + planetPOP := 90.0 + + // g.Map.Planet[0].Population = planetPOP + c.PutPopulation(R0_Planet_0_num, planetPOP) + var shipsDisassembling uint = 3 + groupEmptyMass = groupEmptyMass / float64(c.ShipGroup(2).Number) * float64(shipsDisassembling) + newGroupUnloadedCOL := c.ShipGroup(2).Load / float64(c.ShipGroup(2).Number) * float64(shipsDisassembling) + expectPOPIncrease := newGroupUnloadedCOL * 8 + freePOPLeft := c.MustPlanet(R0_Planet_0_num).Size - c.MustPlanet(R0_Planet_0_num).Population + expectAddedCOL := (expectPOPIncrease - freePOPLeft) / 8 + expectAddedPOP := freePOPLeft + assert.NoError(t, g.DisassembleGroup(Race_0.Name, 3, shipsDisassembling)) + assert.Len(t, slices.Collect(c.ListShipGroups(Race_0_idx)), 3) + assert.Equal(t, planetCOL+expectAddedCOL, c.MustPlanet(R0_Planet_0_num).Colonists) + assert.Equal(t, planetPOP+expectAddedPOP, c.MustPlanet(R0_Planet_0_num).Population) + assert.Equal(t, planetMAT+groupEmptyMass, c.MustPlanet(R0_Planet_0_num).Material) + assert.Equal(t, uint(7), c.ShipGroup(2).Number) + assert.Equal(t, 56.0, c.ShipGroup(2).Load) +} diff --git a/internal/controller/ship_group_upgrade.go b/internal/controller/ship_group_upgrade.go new file mode 100644 index 0000000..9bb836a --- /dev/null +++ b/internal/controller/ship_group_upgrade.go @@ -0,0 +1,215 @@ +package controller + +import ( + "github.com/google/uuid" + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" +) + +func (c *Controller) UpgradeGroup(raceName string, groupIndex uint, techInput string, limitShips uint, limitLevel float64) error { + ri, err := c.Cache.raceIndex(raceName) + if err != nil { + return err + } + return c.Cache.UpgradeGroup(ri, groupIndex, techInput, limitShips, limitLevel) +} + +func (c *Cache) UpgradeGroup(ri int, groupIndex uint, techInput string, limitShips uint, limitLevel float64) error { + c.validateRaceIndex(ri) + sgi, ok := c.raceShipGroupIndex(ri, groupIndex) + if !ok { + return e.NewEntityNotExistsError("group #%d", groupIndex) + } + st := c.ShipGroupShipClass(sgi) + + // 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 == c.ShipGroup(sgi).TypeID }); sti < 0 { + // // hard to test, need manual game data invalidation + // return e.NewGameStateError("not found: ShipType ID=%v", c.ShipGroup(sgi).TypeID) + // } + // st := g.Race[ri].ShipTypes[sti] + + if s := c.ShipGroup(sgi).State(); s != game.StateInOrbit && s != game.StateUpgrade { + return e.NewShipsBusyError() + } + + pl := c.MustPlanet(c.ShipGroup(sgi).Destination) + + // pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == c.ShipGroup(sgi).Destination }) + // if pl < 0 { + // return e.NewGameStateError("planet #%d", c.ShipGroup(sgi).Destination) + // } + if pl.Owner != uuid.Nil && pl.Owner != c.g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet #%d for upgrade group #%d", pl.Number, groupIndex) + } + + upgradeValidTech := map[string]game.Tech{ + game.TechDrive.String(): game.TechDrive, + game.TechWeapons.String(): game.TechWeapons, + game.TechShields.String(): game.TechShields, + game.TechCargo.String(): game.TechCargo, + game.TechAll.String(): game.TechAll, + } + + techRequest, ok := upgradeValidTech[techInput] + if !ok { + return e.NewTechUnknownError(techInput) + } + + var blockMasses map[game.Tech]float64 = map[game.Tech]float64{ + game.TechDrive: st.DriveBlockMass(), + game.TechWeapons: st.WeaponsBlockMass(), + game.TechShields: st.ShieldsBlockMass(), + game.TechCargo: st.CargoBlockMass(), + } + + switch { + case techRequest != game.TechAll && blockMasses[techRequest] == 0: + return e.NewUpgradeShipTechNotUsedError() + case techRequest == game.TechAll && limitLevel != 0: + return e.NewUpgradeParameterNotAllowedError("tech=%s max_level=%f", techRequest.String(), limitLevel) + } + + targetLevel := make(map[game.Tech]float64) + var sumLevels float64 + for _, tech := range []game.Tech{game.TechDrive, game.TechWeapons, game.TechShields, game.TechCargo} { + if techRequest == game.TechAll || tech == techRequest { + if c.g.Race[ri].TechLevel(tech) < limitLevel { + return e.NewUpgradeTechLevelInsufficientError("%s=%.03f < %.03f", tech.String(), c.g.Race[ri].TechLevel(tech), limitLevel) + } + targetLevel[tech] = game.FutureUpgradeLevel(c.g.Race[ri].TechLevel(tech), c.ShipGroup(sgi).TechLevel(tech), limitLevel) + } else { + targetLevel[tech] = game.CurrentUpgradingLevel(c.g.ShipGroups[sgi], tech) + } + sumLevels += targetLevel[tech] + } + + productionCapacity := c.PlanetProductionCapacity(pl.Number) + if c.ShipGroup(sgi).State() == game.StateUpgrade { + // to calculate actual capacity we must substract upgrade cost of selected group, if is upgrade state + productionCapacity -= c.ShipGroup(sgi).StateUpgrade.Cost() + } + uc := game.GroupUpgradeCost(*(c.ShipGroup(sgi)), *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.TechCargo]) + costForShip := uc.UpgradeCost(1) + if costForShip == 0 { + return e.NewUpgradeShipsAlreadyUpToDateError("%#v", targetLevel) + } + + shipsToUpgrade := c.ShipGroup(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 = game.GroupUpgradeCost(*(c.ShipGroup(sgi)), *st, targetLevel[game.TechDrive], targetLevel[game.TechWeapons], targetLevel[game.TechShields], targetLevel[game.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 < c.ShipGroup(sgi).Number { + if c.ShipGroup(sgi).State() == game.StateUpgrade { + return e.NewUpgradeGroupBreakNotAllowedError("ships=%d max=%d", c.ShipGroup(sgi).Number, maxUpgradableShips) + } + nsgi, err := c.breakGroupSafe(ri, groupIndex, maxUpgradableShips) + if err != nil { + return err + } + sgi = nsgi + } + + // finally, fill group upgrade prefs + for tech := range targetLevel { + if targetLevel[tech] > 0 { + c.upgradeShipGroup(sgi, tech, targetLevel[tech]) + } + } + + return nil +} + +// func CurrentUpgradingLevel(sg game.ShipGroup, tech game.Tech) float64 { +// if sg.StateUpgrade == nil { +// return 0 +// } +// ti := slices.IndexFunc(sg.StateUpgrade.UpgradeTech, func(pref game.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 (c *Cache) upgradeShipGroup(sgi int, tech game.Tech, v float64) { + sg := *(c.ShipGroup(sgi)) + st := c.ShipGroupShipClass(sgi) + // if v <= 0 || st.BlockMass(tech) == 0 || sg.TechLevel(tech) >= v { + // return + // } + // var su game.InUpgrade + // if sg.StateUpgrade != nil { + // su = *sg.StateUpgrade + // } else { + // su = game.InUpgrade{UpgradeTech: []game.UpgradePreference{}} + // } + // ti := slices.IndexFunc(su.UpgradeTech, func(pref game.UpgradePreference) bool { return pref.Tech == tech }) + // if ti < 0 { + // su.UpgradeTech = append(su.UpgradeTech, game.UpgradePreference{Tech: tech}) + // ti = len(su.UpgradeTech) - 1 + // } + // su.UpgradeTech[ti].Level = v + // su.UpgradeTech[ti].Cost = game.BlockUpgradeCost(st.BlockMass(tech), sg.TechLevel(tech), v) * float64(sg.Number) + + // sg.StateUpgrade = &su + + c.g.ShipGroups[sgi] = game.UpgradeGroupPreference(sg, *st, tech, v) +} diff --git a/internal/controller/ship_group_upgrade_test.go b/internal/controller/ship_group_upgrade_test.go new file mode 100644 index 0000000..0dc1748 --- /dev/null +++ b/internal/controller/ship_group_upgrade_test.go @@ -0,0 +1,170 @@ +package controller_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) { + c, g := newCache() + // group #1 - in_orbit, free to upgrade + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 10)) + // group #2 - in_space + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) + c.ShipGroup(1).StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} + // group #3 - in_orbit, foreign planet + assert.NoError(t, c.CreateShips(Race_0_idx, ShipType_Cruiser, R0_Planet_0_num, 1)) + c.ShipGroup(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) + c.RacetTechLevel(Race_0_idx, game.TechDrive, 10.0) + assert.Equal(t, 10.0, c.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(c.ListShipGroups(Race_0_idx)), 4) + assert.Equal(t, uint(8), c.ShipGroup(0).Number) + assert.Equal(t, uint(2), c.ShipGroup(3).Number) + assert.Equal(t, game.StateInOrbit, c.ShipGroup(0).State()) + assert.Equal(t, game.StateUpgrade, c.ShipGroup(3).State()) + + assert.ErrorContains(t, + g.UpgradeGroup(Race_0.Name, 4, "DRIVE", 1, 1.3), + e.GenericErrorText(e.ErrInputUpgradeGroupBreakNotAllowed)) +} diff --git a/internal/game/cmd_group.go b/internal/game/cmd_group.go index 4d09f02..775dd62 100644 --- a/internal/game/cmd_group.go +++ b/internal/game/cmd_group.go @@ -8,14 +8,14 @@ import ( func JoinEqualGroups(configure func(*controller.Param), race string) (err error) { control(configure, func(c *controller.Controller) { c.ExecuteGame(func(r controller.Repo, g *game.Game) { - err = joinEqualGroups(r, g, race) + err = joinEqualGroups(c, r, g, race) }) }) return } -func joinEqualGroups(r controller.Repo, g *game.Game, race string) error { - if err := g.JoinEqualGroups(race); err != nil { +func joinEqualGroups(c *controller.Controller, r controller.Repo, g *game.Game, race string) error { + if err := c.JoinEqualGroups(race); err != nil { return err } return r.SaveState(g) diff --git a/internal/game/cmd_group_test.go b/internal/game/cmd_group_test.go index d7d6bd2..d33e158 100644 --- a/internal/game/cmd_group_test.go +++ b/internal/game/cmd_group_test.go @@ -5,12 +5,11 @@ import ( "github.com/iliadenisov/galaxy/internal/controller" "github.com/iliadenisov/galaxy/internal/game" - mg "github.com/iliadenisov/galaxy/internal/model/game" "github.com/stretchr/testify/assert" ) func TestJoinEqualGroups(t *testing.T) { - g(t, func(p func(*controller.Param), g func() *mg.Game) { + c(t, func(p func(*controller.Param), g func() *controller.Controller) { err := game.JoinEqualGroups(p, "race_01") assert.NoError(t, err) }) diff --git a/internal/game/cmd_ship_type.go b/internal/game/cmd_ship_type.go index 2792803..17b4448 100644 --- a/internal/game/cmd_ship_type.go +++ b/internal/game/cmd_ship_type.go @@ -5,17 +5,17 @@ import ( "github.com/iliadenisov/galaxy/internal/model/game" ) -func CreateShipType(configure func(*controller.Param), race, typeName string, drive, weapons, shields, cargo float64, armament int) (err error) { +func CreateShipType(configure func(*controller.Param), race, typeName string, drive float64, ammo int, weapons, shields, cargo float64) (err error) { control(configure, func(c *controller.Controller) { c.ExecuteGame(func(r controller.Repo, g *game.Game) { - err = createShipType(r, g, race, typeName, drive, weapons, shields, cargo, armament) + err = createShipType(c, r, g, race, typeName, drive, ammo, weapons, shields, cargo) }) }) return } -func createShipType(r controller.Repo, g *game.Game, race, typeName string, d, w, s, c float64, a int) error { - if err := g.CreateShipType(race, typeName, d, w, s, c, a); err != nil { +func createShipType(c *controller.Controller, r controller.Repo, g *game.Game, race, typeName string, drive float64, ammo int, weapons, shields, cargo float64) error { + if err := c.CreateShipType(race, typeName, drive, ammo, weapons, shields, cargo); err != nil { return err } return r.SaveState(g) @@ -24,14 +24,14 @@ func createShipType(r controller.Repo, g *game.Game, race, typeName string, d, w func MergeShipType(configure func(*controller.Param), race, source, target string) (err error) { control(configure, func(c *controller.Controller) { c.ExecuteGame(func(r controller.Repo, g *game.Game) { - err = mergeShipType(r, g, race, source, target) + err = mergeShipType(c, r, g, race, source, target) }) }) return } -func mergeShipType(r controller.Repo, g *game.Game, race, source, target string) error { - if err := g.MergeShipType(race, source, target); err != nil { +func mergeShipType(c *controller.Controller, r controller.Repo, g *game.Game, race, source, target string) error { + if err := c.MergeShipType(race, source, target); err != nil { return err } return r.SaveState(g) @@ -40,14 +40,14 @@ func mergeShipType(r controller.Repo, g *game.Game, race, source, target string) func DeleteShipType(configure func(*controller.Param), race, typeName string) (err error) { control(configure, func(c *controller.Controller) { c.ExecuteGame(func(r controller.Repo, g *game.Game) { - err = deleteShipType(r, g, race, typeName) + err = deleteShipType(c, r, g, race, typeName) }) }) return } -func deleteShipType(r controller.Repo, g *game.Game, race, typeName string) error { - if err := g.DeleteShipType(race, typeName); err != nil { +func deleteShipType(c *controller.Controller, r controller.Repo, g *game.Game, race, typeName string) error { + if err := c.DeleteShipType(race, typeName); err != nil { return err } return r.SaveState(g) diff --git a/internal/game/cmd_ship_type_test.go b/internal/game/cmd_ship_type_test.go index 9e03dc5..146b83c 100644 --- a/internal/game/cmd_ship_type_test.go +++ b/internal/game/cmd_ship_type_test.go @@ -15,14 +15,14 @@ import ( func TestCreateShipType(t *testing.T) { race := "race_01" typeName := "Drone" - g(t, func(p func(*controller.Param), g func() *mg.Game) { + c(t, func(p func(*controller.Param), ctrl func() *controller.Controller) { err := game.DeleteShipType(p, race, typeName) assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) err = game.CreateShipType(p, unknownRaceName, " "+typeName+" ", 1, 0, 0, 0, 0) // TODO: test on dead race assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) err = game.CreateShipType(p, race, " "+typeName+" ", 1, 0, 0, 0, 0) assert.NoError(t, err) - st, err := g().ShipTypes(race) + st, err := ctrl().ShipTypes(race) assert.NoError(t, err) assert.Len(t, st, 1) assert.Equal(t, st[0].Name, typeName) @@ -36,7 +36,7 @@ func TestCreateShipType(t *testing.T) { assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) err = game.DeleteShipType(p, race, typeName) assert.NoError(t, err) - st, err = g().ShipTypes(race) + st, err = ctrl().ShipTypes(race) assert.NoError(t, err) assert.Len(t, st, 0) }) @@ -82,12 +82,12 @@ func TestCreateShipTypeValidation(t *testing.T) { g(t, func(p func(*controller.Param), g func() *mg.Game) { for i, tc := range table { if tc.err == "" { - err := game.CreateShipType(p, race, tc.name+strconv.Itoa(i), tc.d, tc.w, tc.s, tc.c, tc.a) + err := game.CreateShipType(p, race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c) assert.NoError(t, err) - err = game.CreateShipType(p, race, tc.name+strconv.Itoa(i), tc.d, tc.w, tc.s, tc.c, tc.a) + err = game.CreateShipType(p, race, tc.name+strconv.Itoa(i), tc.d, tc.a, tc.w, tc.s, tc.c) assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityTypeNameDuplicate)) } else { - err := game.CreateShipType(p, race, tc.name, tc.d, tc.w, tc.s, tc.c, tc.a) + err := game.CreateShipType(p, race, tc.name, tc.d, tc.a, tc.w, tc.s, tc.c) assert.ErrorContains(t, err, tc.err) } } @@ -96,7 +96,7 @@ func TestCreateShipTypeValidation(t *testing.T) { func TestMergeShipType(t *testing.T) { race := "race_01" - g(t, func(p func(*controller.Param), g func() *mg.Game) { + c(t, func(p func(*controller.Param), ctrl func() *controller.Controller) { 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) @@ -109,7 +109,7 @@ func TestMergeShipType(t *testing.T) { assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputEntityNotExists)) err = game.MergeShipType(p, race, "Spy", "Drone") assert.NoError(t, err) - st, err := g().ShipTypes(race) + st, err := ctrl().ShipTypes(race) assert.NoError(t, err) assert.Len(t, st, 2) err = game.MergeShipType(p, race, "Drone", "Cruiser") diff --git a/internal/game/cmd_war_peace.go b/internal/game/cmd_war_peace.go index 4abd0a8..96a291b 100644 --- a/internal/game/cmd_war_peace.go +++ b/internal/game/cmd_war_peace.go @@ -7,20 +7,20 @@ import ( func DeclareWar(configure func(*controller.Param), from, to string) (err error) { control(configure, func(c *controller.Controller) { - c.ExecuteGame(func(r controller.Repo, g *game.Game) { err = updateRelation(r, g, from, to, game.RelationWar) }) + c.ExecuteGame(func(r controller.Repo, g *game.Game) { err = updateRelation(c, r, g, from, to, game.RelationWar) }) }) return } func DeclarePeace(configure func(*controller.Param), from, to string) (err error) { control(configure, func(c *controller.Controller) { - c.ExecuteGame(func(r controller.Repo, g *game.Game) { err = updateRelation(r, g, from, to, game.RelationPeace) }) + c.ExecuteGame(func(r controller.Repo, g *game.Game) { err = updateRelation(c, r, g, from, to, game.RelationPeace) }) }) return } -func updateRelation(r controller.Repo, g *game.Game, hostRace, opponentRace string, rel game.Relation) error { - if err := g.UpdateRelation(hostRace, opponentRace, rel); err != nil { +func updateRelation(c *controller.Controller, r controller.Repo, g *game.Game, hostRace, opponentRace string, rel game.Relation) error { + if err := c.UpdateRelation(hostRace, opponentRace, rel); err != nil { return err } return r.SaveState(g) diff --git a/internal/game/cmd_war_peace_test.go b/internal/game/cmd_war_peace_test.go index 9c6a25d..e587851 100644 --- a/internal/game/cmd_war_peace_test.go +++ b/internal/game/cmd_war_peace_test.go @@ -12,33 +12,33 @@ import ( ) func TestDeclarePeaceAndWarSingle(t *testing.T) { - g(t, func(f func(*controller.Param), g func() *mg.Game) { + c(t, func(f func(*controller.Param), ctrl func() *controller.Controller) { hostRace := "race_05" opponentRace := "race_01" - r, err := g().Relation(hostRace, opponentRace) + r, err := ctrl().Relation(hostRace, opponentRace) assert.NoError(t, err) - assert.Equal(t, mg.RelationWar, r.Relation) + assert.Equal(t, mg.RelationWar, r) - r, err = g().Relation(unknownRaceName, opponentRace) // TODO: test on dead race + r, err = ctrl().Relation(unknownRaceName, opponentRace) // TODO: test on dead race assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) - r, err = g().Relation(hostRace, unknownRaceName) // TODO: test on dead race + r, err = ctrl().Relation(hostRace, unknownRaceName) // TODO: test on dead race assert.ErrorContains(t, err, e.GenericErrorText(e.ErrInputUnknownRace)) assert.NoError(t, game.DeclarePeace(f, hostRace, opponentRace)) - r, err = g().Relation(hostRace, opponentRace) + r, err = ctrl().Relation(hostRace, opponentRace) assert.NoError(t, err) - assert.Equal(t, mg.RelationPeace, r.Relation) + assert.Equal(t, mg.RelationPeace, r) assert.NoError(t, game.DeclareWar(f, hostRace, opponentRace)) - r, err = g().Relation(hostRace, opponentRace) + r, err = ctrl().Relation(hostRace, opponentRace) assert.NoError(t, err) - assert.Equal(t, mg.RelationWar, r.Relation) + assert.Equal(t, mg.RelationWar, r) }) } func TestDeclarePeaceAndWarAll(t *testing.T) { - g(t, func(f func(*controller.Param), g func() *mg.Game) { + c(t, func(f func(*controller.Param), ctrl func() *controller.Controller) { hostRace := "race_07" for i := range testRaceCount { @@ -46,9 +46,9 @@ func TestDeclarePeaceAndWarAll(t *testing.T) { if opponentRace == hostRace { continue } - r, err := g().Relation(hostRace, opponentRace) + r, err := ctrl().Relation(hostRace, opponentRace) assert.NoError(t, err) - assert.Equal(t, mg.RelationWar, r.Relation) + assert.Equal(t, mg.RelationWar, r) } assert.NoError(t, game.DeclarePeace(f, hostRace, hostRace)) @@ -58,9 +58,9 @@ func TestDeclarePeaceAndWarAll(t *testing.T) { if opponentRace == hostRace { continue } - r, err := g().Relation(hostRace, opponentRace) + r, err := ctrl().Relation(hostRace, opponentRace) assert.NoError(t, err) - assert.Equal(t, mg.RelationPeace, r.Relation) + assert.Equal(t, mg.RelationPeace, r) } assert.NoError(t, game.DeclareWar(f, hostRace, hostRace)) @@ -70,9 +70,9 @@ func TestDeclarePeaceAndWarAll(t *testing.T) { if opponentRace == hostRace { continue } - r, err := g().Relation(hostRace, opponentRace) + r, err := ctrl().Relation(hostRace, opponentRace) assert.NoError(t, err) - assert.Equal(t, mg.RelationWar, r.Relation) + assert.Equal(t, mg.RelationWar, r) } }) } diff --git a/internal/game/controller_test.go b/internal/game/controller_test.go index e12d542..45f1b36 100644 --- a/internal/game/controller_test.go +++ b/internal/game/controller_test.go @@ -52,3 +52,33 @@ func g(t *testing.T, f func(p func(*controller.Param), g func() *mg.Game)) { } f(p, g) } + +func c(t *testing.T, f func(p func(*controller.Param), g func() *controller.Controller)) { + root, cleanup := util.CreateWorkDir(t) + defer cleanup() + races := make([]string, testRaceCount) + for i := range testRaceCount { + races[i] = raceNum(i) + } + p := func(p *controller.Param) { p.StoragePath = root } + _, err := game.GenerateGame(p, races) + if err != nil { + assert.FailNow(t, "c: GenerateGame", err) + return + } + g := func() *controller.Controller { + c, err := controller.NewController(p) + if err != nil { + assert.FailNow(t, "c: NewController", err) + return nil + } + g, err := game.LoadState(p) + if err != nil { + assert.FailNow(t, "c: LoadState", err) + return nil + } + c.Cache = controller.NewCache(g) + return c + } + f(p, g) +} diff --git a/internal/model/game/fleet.go b/internal/model/game/fleet.go index 26dc0bb..b9946f5 100644 --- a/internal/model/game/fleet.go +++ b/internal/model/game/fleet.go @@ -1,14 +1,16 @@ package game -import ( - "fmt" - "iter" - "math" - "slices" +import "github.com/google/uuid" - "github.com/google/uuid" - e "github.com/iliadenisov/galaxy/internal/error" -) +// import ( +// "fmt" +// "iter" +// "math" +// "slices" + +// "github.com/google/uuid" +// e "github.com/iliadenisov/galaxy/internal/error" +// ) type Fleet struct { ID uuid.UUID `json:"id"` @@ -16,231 +18,231 @@ type Fleet struct { Name string `json:"name"` } -func FleetState(g *Game, fleetID uuid.UUID) (ShipGroupState, *uint, *InSpace) { - fi := slices.IndexFunc(g.Fleets, func(f Fleet) bool { return f.ID == fleetID }) - if fi < 0 { - panic("FleetState: fleet id not found: " + fleetID.String()) - } - ri := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == g.Fleets[fi].OwnerID }) - if ri < 0 { - panic("FleetState: race id not found: " + g.Fleets[fi].OwnerID.String()) - } - var state *ShipGroupState - var onPlanet *uint - var is *InSpace - for sg := range FleetGroups(g, ri, fi) { - if state == nil { - s := sg.State() - state = &s - if planet, ok := sg.OnPlanet(); ok { - onPlanet = &planet - } - is = sg.StateInSpace - continue - } - if *state != sg.State() { - panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different states", g.Race[ri].Name, g.Fleets[fi].Name)) - } - if planet, ok := sg.OnPlanet(); ok && onPlanet != nil && *onPlanet != planet { - for sg := range FleetGroups(g, ri, fi) { - fmt.Println("group", sg.Index, "fleet", sg.FleetID, g.Fleets[fi].Name, "state", sg.State(), "on", sg.Destination) - } - panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q are on different planets: %d <> %d", g.Race[ri].Name, g.Fleets[fi].Name, *onPlanet, planet)) - } - if (is == nil && sg.StateInSpace != nil) || (is != nil && sg.StateInSpace == nil) { - panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q on_planet and in_space at the same time", g.Race[ri].Name, g.Fleets[fi].Name)) - } - if is != nil && sg.StateInSpace != nil && !is.Equal(*sg.StateInSpace) { - panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different is_space states", g.Race[ri].Name, g.Fleets[fi].Name)) - } - } - if state == nil { - panic(fmt.Sprintf("FleetState: race's %q fleet %q has no ships", g.Race[ri].Name, g.Fleets[fi].Name)) - } - return *state, onPlanet, is -} +// func FleetState(g *Game, fleetID uuid.UUID) (ShipGroupState, *uint, *InSpace) { +// fi := slices.IndexFunc(g.Fleets, func(f Fleet) bool { return f.ID == fleetID }) +// if fi < 0 { +// panic("FleetState: fleet id not found: " + fleetID.String()) +// } +// ri := slices.IndexFunc(g.Race, func(r Race) bool { return r.ID == g.Fleets[fi].OwnerID }) +// if ri < 0 { +// panic("FleetState: race id not found: " + g.Fleets[fi].OwnerID.String()) +// } +// var state *ShipGroupState +// var onPlanet *uint +// var is *InSpace +// for sg := range FleetGroups(g, ri, fi) { +// if state == nil { +// s := sg.State() +// state = &s +// if planet, ok := sg.OnPlanet(); ok { +// onPlanet = &planet +// } +// is = sg.StateInSpace +// continue +// } +// if *state != sg.State() { +// panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different states", g.Race[ri].Name, g.Fleets[fi].Name)) +// } +// if planet, ok := sg.OnPlanet(); ok && onPlanet != nil && *onPlanet != planet { +// for sg := range FleetGroups(g, ri, fi) { +// fmt.Println("group", sg.Index, "fleet", sg.FleetID, g.Fleets[fi].Name, "state", sg.State(), "on", sg.Destination) +// } +// panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q are on different planets: %d <> %d", g.Race[ri].Name, g.Fleets[fi].Name, *onPlanet, planet)) +// } +// if (is == nil && sg.StateInSpace != nil) || (is != nil && sg.StateInSpace == nil) { +// panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q on_planet and in_space at the same time", g.Race[ri].Name, g.Fleets[fi].Name)) +// } +// if is != nil && sg.StateInSpace != nil && !is.Equal(*sg.StateInSpace) { +// panic(fmt.Sprintf("FleetState: one or more ships in race's %q fleet %q has different is_space states", g.Race[ri].Name, g.Fleets[fi].Name)) +// } +// } +// if state == nil { +// panic(fmt.Sprintf("FleetState: race's %q fleet %q has no ships", g.Race[ri].Name, g.Fleets[fi].Name)) +// } +// return *state, onPlanet, is +// } -// TODO: Hello! Wanna know fleet's speed? Good. Implement & test this func first. -func (g Game) FleetSpeed(fl Fleet) float64 { - result := math.MaxFloat64 - for sg := range g.ShipGroups { - if g.ShipGroups[sg].FleetID == nil || *g.ShipGroups[sg].FleetID != fl.ID { - continue - } - st := g.mustShipType(g.ShipGroups[sg].TypeID) - typeSpeed := g.ShipGroups[sg].Speed(st) - if typeSpeed < result { - result = typeSpeed - } - } - return result -} +// // TODO: Hello! Wanna know fleet's speed? Good. Implement & test this func first. +// func (g Game) FleetSpeed(fl Fleet) float64 { +// result := math.MaxFloat64 +// for sg := range g.ShipGroups { +// if g.ShipGroups[sg].FleetID == nil || *g.ShipGroups[sg].FleetID != fl.ID { +// continue +// } +// st := g.mustShipType(g.ShipGroups[sg].TypeID) +// typeSpeed := g.ShipGroups[sg].Speed(st) +// if typeSpeed < result { +// result = typeSpeed +// } +// } +// return result +// } -func (g *Game) JoinShipGroupToFleet(raceName, fleetName string, group, count uint) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - return g.joinShipGroupToFleetInternal(ri, fleetName, group, count) -} +// func (g *Game) JoinShipGroupToFleet(raceName, fleetName string, group, count uint) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// 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) 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, groupIndex, quantity uint) (err error) { - name, ok := validateTypeName(fleetName) - if !ok { - return e.NewEntityTypeNameValidationError("%q", name) - } - sgi := -1 - var maxIndex uint - for i, sg := range g.listIndexShipGroups(ri) { - if sgi < 0 && sg.Index == groupIndex { - sgi = i - } - if sg.Index > maxIndex { - maxIndex = sg.Index - } - } - if sgi < 0 { - return e.NewEntityNotExistsError("group #%d", groupIndex) - } +// func (g *Game) joinShipGroupToFleetInternal(ri int, fleetName string, groupIndex, quantity uint) (err error) { +// name, ok := validateTypeName(fleetName) +// if !ok { +// return e.NewEntityTypeNameValidationError("%q", name) +// } +// sgi := -1 +// var maxIndex uint +// for i, sg := range g.listIndexShipGroups(ri) { +// if sgi < 0 && sg.Index == groupIndex { +// sgi = i +// } +// if sg.Index > maxIndex { +// maxIndex = sg.Index +// } +// } +// if sgi < 0 { +// return e.NewEntityNotExistsError("group #%d", groupIndex) +// } - if g.ShipGroups[sgi].State() != StateInOrbit { - return e.NewShipsBusyError() - } +// if g.ShipGroups[sgi].State() != StateInOrbit { +// return e.NewShipsBusyError() +// } - if g.ShipGroups[sgi].Number < quantity { - return e.NewJoinFleetGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) - } +// if g.ShipGroups[sgi].Number < quantity { +// return e.NewJoinFleetGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) +// } - fi := g.fleetIndex(ri, name) - if fi < 0 { - fi, err = g.createFleet(ri, name) - if err != nil { - return err - } - } else { - state, onPlanet, _ := FleetState(g, g.Fleets[fi].ID) - if state != StateInOrbit || *onPlanet != g.ShipGroups[sgi].Destination { - return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName) - } - } +// fi := g.fleetIndex(ri, name) +// if fi < 0 { +// fi, err = g.createFleet(ri, name) +// if err != nil { +// return err +// } +// } else { +// state, onPlanet, _ := FleetState(g, g.Fleets[fi].ID) +// if state != StateInOrbit || *onPlanet != g.ShipGroups[sgi].Destination { +// return e.NewShipsNotOnSamePlanetError("fleet: %s", fleetName) +// } +// } - // FIXME: if g.ShipGroups[sgi].FleetID != nil { // delete old fleet if empty, ALSO mind breaking group } - if quantity > 0 && quantity < g.ShipGroups[sgi].Number { - // nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) - // if err != nil { - // return err - // } - // sgi = nsgi - newGroup := g.ShipGroups[sgi] - newGroup.Number -= quantity - g.ShipGroups[sgi].Number = quantity - newGroup.Index = maxIndex + 1 - g.ShipGroups = append(g.ShipGroups, newGroup) - } +// // FIXME: if g.ShipGroups[sgi].FleetID != nil { // delete old fleet if empty, ALSO mind breaking group } +// if quantity > 0 && quantity < g.ShipGroups[sgi].Number { +// // nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) +// // if err != nil { +// // return err +// // } +// // sgi = nsgi +// newGroup := g.ShipGroups[sgi] +// newGroup.Number -= quantity +// g.ShipGroups[sgi].Number = quantity +// newGroup.Index = maxIndex + 1 +// g.ShipGroups = append(g.ShipGroups, newGroup) +// } - g.ShipGroups[sgi].FleetID = &g.Fleets[fi].ID - return nil -} +// g.ShipGroups[sgi].FleetID = &g.Fleets[fi].ID +// return nil +// } -func (g *Game) createFleet(ri int, name string) (int, error) { - n, ok := validateTypeName(name) - if !ok { - return 0, e.NewEntityTypeNameValidationError("%q", n) - } - if fl := g.fleetIndex(ri, n); fl >= 0 { - return 0, e.NewEntityTypeNameDuplicateError("fleet %w", g.Fleets[fl].Name) - } - fleets := slices.Clone(g.Fleets) - fleets = append(fleets, Fleet{ - ID: uuid.New(), - OwnerID: g.Race[ri].ID, - Name: n, - }) - g.Fleets = fleets - return len(g.Fleets) - 1, nil -} +// func (g *Game) createFleet(ri int, name string) (int, error) { +// n, ok := validateTypeName(name) +// if !ok { +// return 0, e.NewEntityTypeNameValidationError("%q", n) +// } +// if fl := g.fleetIndex(ri, n); fl >= 0 { +// return 0, e.NewEntityTypeNameDuplicateError("fleet %w", g.Fleets[fl].Name) +// } +// fleets := slices.Clone(g.Fleets) +// fleets = append(fleets, Fleet{ +// ID: uuid.New(), +// OwnerID: g.Race[ri].ID, +// Name: n, +// }) +// g.Fleets = fleets +// 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) - } - srcState, planet1, _ := FleetState(g, g.Fleets[fiSource].ID) - tgtState, planet2, _ := FleetState(g, g.Fleets[fiTarget].ID) - if srcState != StateInOrbit || srcState != tgtState || *planet1 != *planet2 { - 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) 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) +// } +// srcState, planet1, _ := FleetState(g, g.Fleets[fiSource].ID) +// tgtState, planet2, _ := FleetState(g, g.Fleets[fiTarget].ID) +// if srcState != StateInOrbit || srcState != tgtState || *planet1 != *planet2 { +// 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) 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) 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) { - if !yield(fl) { - return - } - } - } -} +// func (g Game) listFleets(ri int) iter.Seq[Fleet] { +// return func(yield func(Fleet) bool) { +// for _, fl := range g.listIndexFleets(ri) { +// if !yield(fl) { +// return +// } +// } +// } +// } -func (g Game) listIndexFleets(ri int) iter.Seq2[int, Fleet] { - return func(yield func(int, Fleet) bool) { - for i := range g.Fleets { - if g.Fleets[i].OwnerID == g.Race[ri].ID { - if !yield(i, g.Fleets[i]) { - return - } - } - } - } -} +// func (g Game) listIndexFleets(ri int) iter.Seq2[int, Fleet] { +// return func(yield func(int, Fleet) bool) { +// for i := range g.Fleets { +// if g.Fleets[i].OwnerID == g.Race[ri].ID { +// if !yield(i, g.Fleets[i]) { +// return +// } +// } +// } +// } +// } -func FleetGroups(g *Game, ri, fi int) iter.Seq[ShipGroup] { - if len(g.Fleets) < fi+1 { - panic(fmt.Sprintf("FleetGroups: game fleets index %d invalid: len=%d", fi, len(g.Fleets))) - } - return func(yield func(ShipGroup) bool) { - for sg := range g.listShipGroups(ri) { - if sg.FleetID != nil && *sg.FleetID == g.Fleets[fi].ID { - if !yield(sg) { - break - } - } - } - } -} +// func FleetGroups(g *Game, ri, fi int) iter.Seq[ShipGroup] { +// if len(g.Fleets) < fi+1 { +// panic(fmt.Sprintf("FleetGroups: game fleets index %d invalid: len=%d", fi, len(g.Fleets))) +// } +// return func(yield func(ShipGroup) bool) { +// for sg := range g.listShipGroups(ri) { +// if sg.FleetID != nil && *sg.FleetID == g.Fleets[fi].ID { +// if !yield(sg) { +// break +// } +// } +// } +// } +// } diff --git a/internal/model/game/fleet_send.go b/internal/model/game/fleet_send.go index 0de948a..c7aae5a 100644 --- a/internal/model/game/fleet_send.go +++ b/internal/model/game/fleet_send.go @@ -1,80 +1,80 @@ package game -import ( - "fmt" - "slices" +// import ( +// "fmt" +// "slices" - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/iliadenisov/galaxy/internal/util" -) +// e "github.com/iliadenisov/galaxy/internal/error" +// "github.com/iliadenisov/galaxy/internal/util" +// ) -func (g *Game) SendFleet(raceName, fleetName string, planetNumber uint) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - fi := g.fleetIndex(ri, fleetName) - if fi < 0 { - return e.NewEntityNotExistsError("fleet %q", fleetName) - } - return g.sendFleetInternal(ri, fi, planetNumber) -} +// func (g *Game) SendFleet(raceName, fleetName string, planetNumber uint) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// fi := g.fleetIndex(ri, fleetName) +// if fi < 0 { +// return e.NewEntityNotExistsError("fleet %q", fleetName) +// } +// return g.sendFleetInternal(ri, fi, planetNumber) +// } -func (g *Game) sendFleetInternal(ri, fi int, planetNumber uint) error { - state, sourcePlanet, _ := FleetState(g, g.Fleets[fi].ID) - if StateInOrbit != state && StateLaunched != state { - return e.NewShipsBusyError() - } +// func (g *Game) sendFleetInternal(ri, fi int, planetNumber uint) error { +// state, sourcePlanet, _ := FleetState(g, g.Fleets[fi].ID) +// if StateInOrbit != state && StateLaunched != state { +// return e.NewShipsBusyError() +// } - p1, ok := PlanetByNum(g, *sourcePlanet) - if !ok { - return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) - } - p2, ok := PlanetByNum(g, planetNumber) - if !ok { - return e.NewEntityNotExistsError("destination planet #%d", planetNumber) - } - rangeToDestination := util.ShortDistance(g.Map.Width, g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) - if rangeToDestination > g.Race[ri].FlightDistance() { - return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) - } +// p1, ok := PlanetByNum(g, *sourcePlanet) +// if !ok { +// return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) +// } +// p2, ok := PlanetByNum(g, planetNumber) +// if !ok { +// return e.NewEntityNotExistsError("destination planet #%d", planetNumber) +// } +// rangeToDestination := util.ShortDistance(g.Map.Width, g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) +// if rangeToDestination > g.Race[ri].FlightDistance() { +// return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) +// } - for sg := range FleetGroups(g, ri, fi) { - st, ok := ShipClass(g, ri, sg.TypeID) - if !ok { - return e.NewGameStateError("not found: ShipType ID=%v", sg.TypeID) - } - if st.DriveBlockMass() == 0 { - return e.NewSendShipHasNoDrivesError("Class=%s", st.Name) - } - } +// for sg := range FleetGroups(g, ri, fi) { +// st, ok := ShipClass(g, ri, sg.TypeID) +// if !ok { +// return e.NewGameStateError("not found: ShipType ID=%v", sg.TypeID) +// } +// if st.DriveBlockMass() == 0 { +// return e.NewSendShipHasNoDrivesError("Class=%s", st.Name) +// } +// } - if *sourcePlanet == planetNumber { - UnsendFleet(g, ri, fi) - return nil - } +// if *sourcePlanet == planetNumber { +// UnsendFleet(g, ri, fi) +// return nil +// } - LaunchFleet(g, ri, fi, planetNumber) +// LaunchFleet(g, ri, fi, planetNumber) - return nil -} +// return nil +// } -func LaunchFleet(g *Game, ri, fi int, destination uint) { - for sg := range FleetGroups(g, ri, fi) { - sgi := slices.IndexFunc(g.ShipGroups, func(s ShipGroup) bool { return sg.Index == s.Index }) - if sgi < 0 { - panic(fmt.Sprintf("LauncgFleet: cannot find ship group index=%d", sg.Index)) - } - g.ShipGroups[sgi] = LaunchShips(sg, destination) - } -} +// func LaunchFleet(g *Game, ri, fi int, destination uint) { +// for sg := range FleetGroups(g, ri, fi) { +// sgi := slices.IndexFunc(g.ShipGroups, func(s ShipGroup) bool { return sg.Index == s.Index }) +// if sgi < 0 { +// panic(fmt.Sprintf("LauncgFleet: cannot find ship group index=%d", sg.Index)) +// } +// g.ShipGroups[sgi] = LaunchShips(sg, destination) +// } +// } -func UnsendFleet(g *Game, ri, fi int) { - for sg := range FleetGroups(g, ri, fi) { - sgi := slices.IndexFunc(g.ShipGroups, func(s ShipGroup) bool { return sg.Index == s.Index }) - if sgi < 0 { - panic(fmt.Sprintf("UnsendFleet: cannot find ship group index=%d", sg.Index)) - } - g.ShipGroups[sgi] = UnsendShips(sg) - } -} +// func UnsendFleet(g *Game, ri, fi int) { +// for sg := range FleetGroups(g, ri, fi) { +// sgi := slices.IndexFunc(g.ShipGroups, func(s ShipGroup) bool { return sg.Index == s.Index }) +// if sgi < 0 { +// panic(fmt.Sprintf("UnsendFleet: cannot find ship group index=%d", sg.Index)) +// } +// g.ShipGroups[sgi] = UnsendShips(sg) +// } +// } diff --git a/internal/model/game/fleet_send_test.go b/internal/model/game/fleet_send_test.go index 4e51b4a..f4c3f3b 100644 --- a/internal/model/game/fleet_send_test.go +++ b/internal/model/game/fleet_send_test.go @@ -1,73 +1,73 @@ package game_test -import ( - "slices" - "testing" +// import ( +// "slices" +// "testing" - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/stretchr/testify/assert" -) +// e "github.com/iliadenisov/galaxy/internal/error" +// "github.com/iliadenisov/galaxy/internal/model/game" +// "github.com/stretchr/testify/assert" +// ) -func TestSendFleet(t *testing.T) { - g := newGame() - // group #1 - in_orbit Planet_0 - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) - // group #2 - in_space (later) - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) - // group #3 - in_orbit Planet_0, unmovable - g.CreateShipType(Race_0.Name, "Fortress", 0, 30, 100, 0, 50) - assert.NoError(t, g.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1)) - // group #4 - in_orbit Planet_0 - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) +// func TestSendFleet(t *testing.T) { +// g := newGame() +// // group #1 - in_orbit Planet_0 +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) +// // group #2 - in_space (later) +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) +// // group #3 - in_orbit Planet_0, unmovable +// g.CreateShipType(Race_0.Name, "Fortress", 0, 50, 30, 100, 0) +// assert.NoError(t, g.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1)) +// // group #4 - in_orbit Planet_0 +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 2)) - // ensure race has no Fleets - assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 0) +// // ensure race has no Fleets +// assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 0) - fleetSending := "R0_Fleet_one" - fleetInSpace := "R0_Fleet_inSpace" - fleetUnmovable := "R0_Fleet_unmovable" +// fleetSending := "R0_Fleet_one" +// fleetInSpace := "R0_Fleet_inSpace" +// fleetUnmovable := "R0_Fleet_unmovable" - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSending, 1, 0)) - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSending, 3, 0)) +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSending, 1, 0)) +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetSending, 3, 0)) - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetInSpace, 2, 0)) - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetUnmovable, 3, 0)) - // group #2 - in_space - g.ShipGroups[1].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetInSpace, 2, 0)) +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetUnmovable, 3, 0)) +// // group #2 - in_space +// g.ShipGroups[1].StateInSpace = &game.InSpace{Origin: 2, Range: 1.23} - assert.ErrorContains(t, - g.SendFleet("UnknownRace", fleetSending, 2), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.SendFleet(Race_0.Name, "UnknownFleet", 2), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.SendFleet(Race_0.Name, fleetInSpace, 2), - e.GenericErrorText(e.ErrShipsBusy)) - assert.ErrorContains(t, - g.SendFleet(Race_0.Name, fleetSending, 200), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.SendFleet(Race_0.Name, fleetSending, 3), - e.GenericErrorText(e.ErrSendUnreachableDestination)) - assert.ErrorContains(t, - g.SendFleet(Race_0.Name, fleetUnmovable, 2), - e.GenericErrorText(e.ErrSendShipHasNoDrives)) +// assert.ErrorContains(t, +// g.SendFleet("UnknownRace", fleetSending, 2), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.SendFleet(Race_0.Name, "UnknownFleet", 2), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.SendFleet(Race_0.Name, fleetInSpace, 2), +// e.GenericErrorText(e.ErrShipsBusy)) +// assert.ErrorContains(t, +// g.SendFleet(Race_0.Name, fleetSending, 200), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.SendFleet(Race_0.Name, fleetSending, 3), +// e.GenericErrorText(e.ErrSendUnreachableDestination)) +// assert.ErrorContains(t, +// g.SendFleet(Race_0.Name, fleetUnmovable, 2), +// e.GenericErrorText(e.ErrSendShipHasNoDrives)) - assert.NoError(t, g.SendFleet(Race_0.Name, fleetSending, 2)) - fi := slices.IndexFunc(slices.Collect(g.ListFleets(Race_0_idx)), func(f game.Fleet) bool { return f.Name == fleetSending }) - state, _, _ := game.FleetState(g, g.Fleets[fi].ID) - assert.Equal(t, game.StateLaunched, state) - for sg := range game.FleetGroups(g, Race_0_idx, fi) { - assert.Equal(t, game.StateLaunched, sg.State()) - } +// assert.NoError(t, g.SendFleet(Race_0.Name, fleetSending, 2)) +// fi := slices.IndexFunc(slices.Collect(g.ListFleets(Race_0_idx)), func(f game.Fleet) bool { return f.Name == fleetSending }) +// state, _, _ := game.FleetState(g, g.Fleets[fi].ID) +// assert.Equal(t, game.StateLaunched, state) +// for sg := range game.FleetGroups(g, Race_0_idx, fi) { +// assert.Equal(t, game.StateLaunched, sg.State()) +// } - assert.NoError(t, g.SendFleet(Race_0.Name, fleetSending, 0)) - fi = slices.IndexFunc(slices.Collect(g.ListFleets(Race_0_idx)), func(f game.Fleet) bool { return f.Name == fleetSending }) - state, _, _ = game.FleetState(g, g.Fleets[fi].ID) - assert.Equal(t, game.StateInOrbit, state) - for sg := range game.FleetGroups(g, Race_0_idx, fi) { - assert.Equal(t, game.StateInOrbit, sg.State()) - } -} +// assert.NoError(t, g.SendFleet(Race_0.Name, fleetSending, 0)) +// fi = slices.IndexFunc(slices.Collect(g.ListFleets(Race_0_idx)), func(f game.Fleet) bool { return f.Name == fleetSending }) +// state, _, _ = game.FleetState(g, g.Fleets[fi].ID) +// assert.Equal(t, game.StateInOrbit, state) +// for sg := range game.FleetGroups(g, Race_0_idx, fi) { +// assert.Equal(t, game.StateInOrbit, sg.State()) +// } +// } diff --git a/internal/model/game/fleet_test.go b/internal/model/game/fleet_test.go index f91a853..e552c31 100644 --- a/internal/model/game/fleet_test.go +++ b/internal/model/game/fleet_test.go @@ -1,136 +1,136 @@ package game_test -import ( - "slices" - "testing" +// import ( +// "slices" +// "testing" - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/iliadenisov/galaxy/internal/model/game" - "github.com/stretchr/testify/assert" -) +// 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 := newGame() - var groupIndex uint = 1 +// func TestJoinShipGroupToFleet(t *testing.T) { +// g := newGame() +// var groupIndex uint = 1 - assert.ErrorContains(t, - g.JoinShipGroupToFleet(Race_0.Name, " ", groupIndex, 0), - e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)) +// assert.ErrorContains(t, +// g.JoinShipGroupToFleet(Race_0.Name, " ", groupIndex, 0), +// e.GenericErrorText(e.ErrInputEntityTypeNameInvalid)) - assert.ErrorContains(t, - g.JoinShipGroupToFleet(Race_0.Name, "Unnamed", groupIndex, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.JoinShipGroupToFleet(Race_0.Name, "Unnamed", groupIndex, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) - // creating ShipGroup - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) +// // creating ShipGroup +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 5)) - assert.ErrorContains(t, - g.JoinShipGroupToFleet(Race_0.Name, "Unnamed", groupIndex, 6), - e.GenericErrorText(e.ErrJoinFleetGroupNumberNotEnough)) +// assert.ErrorContains(t, +// g.JoinShipGroupToFleet(Race_0.Name, "Unnamed", groupIndex, 6), +// e.GenericErrorText(e.ErrJoinFleetGroupNumberNotEnough)) - // ensure race has no Fleets - assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 0) +// // ensure race has no Fleets +// assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 0) - fleetOne := "R0_Fleet_one" - fleetTwo := "R0_Fleet_two" +// fleetOne := "R0_Fleet_one" +// fleetTwo := "R0_Fleet_two" - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) - fleets := slices.Collect(g.ListFleets(Race_0_idx)) - 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) - state, _, _ := game.FleetState(g, fleets[0].ID) - assert.Equal(t, game.StateInOrbit, state) +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) +// fleets := slices.Collect(g.ListFleets(Race_0_idx)) +// 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) +// state, _, _ := game.FleetState(g, fleets[0].ID) +// assert.Equal(t, game.StateInOrbit, state) - assert.NotNil(t, groups[gi].FleetID) - assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) +// assert.NotNil(t, groups[gi].FleetID) +// assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) - // create another ShipGroup - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) - groupIndex = 2 - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetTwo, groupIndex, 2)) - fleets = slices.Collect(g.ListFleets(Race_0_idx)) - 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) - state, _, _ = game.FleetState(g, fleets[1].ID) - assert.Equal(t, game.StateInOrbit, state) +// // create another ShipGroup +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) +// groupIndex = 2 +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetTwo, groupIndex, 2)) +// fleets = slices.Collect(g.ListFleets(Race_0_idx)) +// 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) +// state, _, _ = game.FleetState(g, fleets[1].ID) +// assert.Equal(t, game.StateInOrbit, state) - assert.NotNil(t, groups[gi].FleetID) - assert.Equal(t, fleets[1].ID, *groups[gi].FleetID) - assert.Equal(t, uint(2), groups[gi].Number) - assert.Equal(t, uint(2), groups[gi].Index) +// assert.NotNil(t, groups[gi].FleetID) +// assert.Equal(t, fleets[1].ID, *groups[gi].FleetID) +// assert.Equal(t, uint(2), groups[gi].Number) +// assert.Equal(t, uint(2), groups[gi].Index) - gi = 2 - assert.Nil(t, groups[gi].FleetID) - assert.Equal(t, uint(1), groups[gi].Number) - assert.Equal(t, uint(3), groups[gi].Index) +// gi = 2 +// assert.Nil(t, groups[gi].FleetID) +// assert.Equal(t, uint(1), groups[gi].Number) +// assert.Equal(t, uint(3), groups[gi].Index) - groupIndex = groups[gi].Index - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) - fleets = slices.Collect(g.ListFleets(Race_0_idx)) - assert.Len(t, fleets, 2) - groups = slices.Collect(g.ListShipGroups(Race_0_idx)) - assert.NotNil(t, groups[gi].FleetID) - assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) - state, _, _ = game.FleetState(g, fleets[0].ID) - assert.Equal(t, game.StateInOrbit, state) +// groupIndex = groups[gi].Index +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleetOne, groupIndex, 0)) +// fleets = slices.Collect(g.ListFleets(Race_0_idx)) +// assert.Len(t, fleets, 2) +// groups = slices.Collect(g.ListShipGroups(Race_0_idx)) +// assert.NotNil(t, groups[gi].FleetID) +// assert.Equal(t, fleets[0].ID, *groups[gi].FleetID) +// state, _, _ = game.FleetState(g, fleets[0].ID) +// assert.Equal(t, game.StateInOrbit, state) - // 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].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].StateInSpace = nil +// // 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].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].StateInSpace = nil - // existing fleet not on the same planet or in_orbit - g.ShipGroups[0].StateInSpace = &game.InSpace{ - Origin: 2, - Range: 1, - } - g.ShipGroups[2].StateInSpace = g.ShipGroups[0].StateInSpace - assert.ErrorContains(t, - g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0), - e.GenericErrorText(e.ErrShipsNotOnSamePlanet)) -} +// // existing fleet not on the same planet or in_orbit +// g.ShipGroups[0].StateInSpace = &game.InSpace{ +// Origin: 2, +// Range: 1, +// } +// g.ShipGroups[2].StateInSpace = g.ShipGroups[0].StateInSpace +// assert.ErrorContains(t, +// g.JoinShipGroupToFleet(Race_0.Name, fleetOne, g.ShipGroups[gi].Index, 0), +// e.GenericErrorText(e.ErrShipsNotOnSamePlanet)) +// } -func TestJoinFleets(t *testing.T) { - g := newGame() - // creating ShipGroup #1 at Planet_0 - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1 - // creating ShipGroup #2 at Planet_2 - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 2)) // group #2 - // creating ShipGroup #3 at Planet_0 - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_0_num, 3)) // group #3 +// func TestJoinFleets(t *testing.T) { +// g := newGame() +// // creating ShipGroup #1 at Planet_0 +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) // group #1 +// // creating ShipGroup #2 at Planet_2 +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Gunship, R0_Planet_2_num, 2)) // group #2 +// // creating ShipGroup #3 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) +// // 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" +// 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.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)) -} +// 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/game_export_test.go b/internal/model/game/game_export_test.go index 2b6968a..fd81624 100644 --- a/internal/model/game/game_export_test.go +++ b/internal/model/game/game_export_test.go @@ -3,15 +3,17 @@ package game import "iter" func (g *Game) CreateShips(ri int, shipTypeName string, planetNumber uint, quantity int) error { - return g.createShips(ri, shipTypeName, int(planetNumber), quantity) + return nil // g.createShips(ri, shipTypeName, int(planetNumber), quantity) } func (g Game) ListShipGroups(ri int) iter.Seq[ShipGroup] { - return g.listShipGroups(ri) + return func(yield func(ShipGroup) bool) {} + // return g.listShipGroups(ri) } func (g Game) ListFleets(ri int) iter.Seq[Fleet] { - return g.listFleets(ri) + return func(yield func(Fleet) bool) {} + // return g.listFleets(ri) } func (g Game) MustPlanetByNumber(num uint) Planet { diff --git a/internal/model/game/game_test.go b/internal/model/game/game_test.go index 4999729..25e25ee 100644 --- a/internal/model/game/game_test.go +++ b/internal/model/game/game_test.go @@ -90,12 +90,12 @@ func newGame() *game.Game { }, }, } - 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_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.) + // 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 a5693bc..b3d832f 100644 --- a/internal/model/game/group.go +++ b/internal/model/game/group.go @@ -2,13 +2,9 @@ package game import ( "fmt" - "iter" - "maps" "math" - "slices" "github.com/google/uuid" - e "github.com/iliadenisov/galaxy/internal/error" "github.com/iliadenisov/galaxy/internal/number" ) @@ -21,7 +17,7 @@ const ( ) var ( - cargoTypeSet map[string]CargoType = map[string]CargoType{ + CargoTypeSet map[string]CargoType = map[string]CargoType{ CargoColonist.String(): CargoColonist, CargoMaterial.String(): CargoMaterial, CargoCapital.String(): CargoCapital, @@ -236,517 +232,514 @@ func (sg ShipGroup) BombingPower(st *ShipType) float64 { // JoinEqualGroups iterates over all races and joins their respective equal ship groups. // Used in turn production. -func JoinEqualGroups(g *Game) { - for i := range g.Race { - g.joinEqualGroupsInternal(i) - } -} +// func JoinEqualGroups(g *Game) { +// // for i := range g.Race { +// // g.joinEqualGroupsInternal(i) +// // } +// } -func (g *Game) JoinEqualGroups(raceName string) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - g.joinEqualGroupsInternal(ri) - return nil -} +// func (g *Game) JoinEqualGroups(raceName string) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// g.joinEqualGroupsInternal(ri) +// return nil +// } -func (g *Game) BreakGroup(raceName string, groupIndex, quantity uint) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - return g.breakGroupInternal(ri, groupIndex, quantity) -} +// func (g *Game) joinEqualGroupsInternal(ri int) { +// shipGroups := slices.Collect(maps.Values(maps.Collect(g.listIndexShipGroups(ri)))) +// origin := len(shipGroups) +// if origin < 2 { +// return +// } +// for i := 0; i < len(shipGroups)-1; i++ { +// for j := len(shipGroups) - 1; j > i; j-- { +// if shipGroups[i].Equal(shipGroups[j]) { +// shipGroups[i].Index = maxUint(shipGroups[i].Index, shipGroups[j].Index) +// shipGroups[i].Number += shipGroups[j].Number +// shipGroups = append(shipGroups[:j], shipGroups[j+1:]...) +// } +// } +// } +// if len(shipGroups) == origin { +// return +// } +// g.ShipGroups = slices.DeleteFunc(g.ShipGroups, func(v ShipGroup) bool { return v.OwnerID == g.Race[ri].ID }) +// g.ShipGroups = append(g.ShipGroups, shipGroups...) +// } -func (g *Game) DisassembleGroup(raceName string, groupIndex, quantity uint) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - return g.disassembleGroupInternal(ri, groupIndex, quantity) -} +// func (g *Game) BreakGroup(raceName string, groupIndex, quantity uint) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// return g.breakGroupInternal(ri, groupIndex, quantity) +// } -func (g *Game) disassembleGroupInternal(ri int, groupIndex, quantity uint) error { - sgi := -1 - var maxIndex uint - for i, sg := range g.listIndexShipGroups(ri) { - if sgi < 0 && sg.Index == groupIndex { - sgi = i - } - if sg.Index > maxIndex { - maxIndex = sg.Index - } - } - if sgi < 0 { - return e.NewEntityNotExistsError("group #%d", groupIndex) - } +// func (g *Game) DisassembleGroup(raceName string, groupIndex, quantity uint) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// return g.disassembleGroupInternal(ri, groupIndex, quantity) +// } - if g.ShipGroups[sgi].State() != StateInOrbit { - return e.NewShipsBusyError() - } +// func (g *Game) disassembleGroupInternal(ri int, groupIndex, quantity uint) error { +// sgi := -1 +// var maxIndex uint +// for i, sg := range g.listIndexShipGroups(ri) { +// if sgi < 0 && sg.Index == groupIndex { +// sgi = i +// } +// if sg.Index > maxIndex { +// maxIndex = sg.Index +// } +// } +// if sgi < 0 { +// return e.NewEntityNotExistsError("group #%d", groupIndex) +// } - if g.ShipGroups[sgi].Number < quantity { - return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) - } +// 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 }) - if pl < 0 { - return e.NewGameStateError("planet #%d", g.ShipGroups[sgi].Destination) - } - 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.ShipGroups[sgi].Number < quantity { +// return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) +// } - if quantity > 0 && quantity < g.ShipGroups[sgi].Number { - // make new group for disassembly - nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) - if err != nil { - return err - } - sgi = nsgi - } +// 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) +// } +// 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.ShipGroups[sgi].CargoType != nil { - ct := *g.ShipGroups[sgi].CargoType - load := g.ShipGroups[sgi].Load - switch ct { - case CargoColonist: - if g.Map.Planet[pl].Owner == g.Race[ri].ID { - g.Map.Planet[pl] = UnloadColonists(g.Map.Planet[pl], load) - } - case CargoMaterial: - g.Map.Planet[pl].Material += load - case CargoCapital: - g.Map.Planet[pl].Capital += load - } - } +// if quantity > 0 && quantity < g.ShipGroups[sgi].Number { +// // make new group for disassembly +// nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) +// if err != nil { +// return err +// } +// sgi = nsgi +// } - g.Map.Planet[pl].Material += g.ShipGroups[sgi].EmptyMass(&g.Race[ri].ShipTypes[sti]) +// if g.ShipGroups[sgi].CargoType != nil { +// ct := *g.ShipGroups[sgi].CargoType +// load := g.ShipGroups[sgi].Load +// switch ct { +// case CargoColonist: +// if g.Map.Planet[pl].Owner == g.Race[ri].ID { +// g.Map.Planet[pl] = UnloadColonists(g.Map.Planet[pl], load) +// } +// case CargoMaterial: +// g.Map.Planet[pl].Material += load +// case CargoCapital: +// g.Map.Planet[pl].Capital += load +// } +// } - g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) +// g.Map.Planet[pl].Material += g.ShipGroups[sgi].EmptyMass(&g.Race[ri].ShipTypes[sti]) - return nil -} +// g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) -func (g *Game) LoadCargo(raceName string, groupIndex uint, cargoType string, ships uint, 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, ships, quantity) -} +// return nil +// } -func (g *Game) UnloadCargo(raceName string, groupIndex uint, ships uint, quantity float64) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - return g.unloadCargoInternal(ri, groupIndex, ships, quantity) -} +// func (g *Game) LoadCargo(raceName string, groupIndex uint, cargoType string, ships uint, 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, ships, quantity) +// } -// Промышленность и Сырье могут быть выгружены на любой планете. -// Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты. -func (g *Game) unloadCargoInternal(ri int, groupIndex uint, ships uint, quantity float64) error { - if ships == 0 && quantity > 0 { - return e.NewCargoQuantityWithoutGroupBreakError() - } - 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() != StateInOrbit { - return e.NewShipsBusyError() - } - 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.NewNoCargoBayError("ship_type %q", g.Race[ri].ShipTypes[sti].Name) - } - if g.ShipGroups[sgi].CargoType == nil || g.ShipGroups[sgi].Load == 0 { - return e.NewCargoUnloadEmptyError() - } - ct := *g.ShipGroups[sgi].CargoType - 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 ct == CargoColonist { - if g.Map.Planet[pl].Owner != uuid.Nil && g.Map.Planet[pl].Owner != g.Race[ri].ID { - return e.NewEntityNotOwnedError("planet #%d unload %v", g.Map.Planet[pl].Number, ct) - } - if g.Map.Planet[pl].Owner == uuid.Nil { - g.Map.Planet[pl].Owner = g.Race[ri].ID - } - } - 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 ships > 0 && ships < g.ShipGroups[sgi].Number { - nsgi, err := g.breakGroupSafe(ri, groupIndex, ships) - if err != nil { - return err - } - sgi = nsgi - } - toBeUnloaded := quantity - if quantity == 0 { - toBeUnloaded = g.ShipGroups[sgi].Load - } - if toBeUnloaded > g.ShipGroups[sgi].Load { - return e.NewCargoUnoadNotEnoughError("load: %.03f", g.ShipGroups[sgi].Load) - } - *availableOnPlanet += toBeUnloaded - g.ShipGroups[sgi].Load -= toBeUnloaded - if g.ShipGroups[sgi].Load == 0 { - g.ShipGroups[sgi].CargoType = nil - } - return nil -} +// func (g *Game) UnloadCargo(raceName string, groupIndex uint, ships uint, quantity float64) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// return g.unloadCargoInternal(ri, groupIndex, ships, quantity) +// } -/* - TODO: multi-command processing: Game data should NOT be changed when error is returned -*/ -// Корабль может нести только один тип груза одновременно. -// Возможные типы груза - это колонисты, сырье и промышленность. -// Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется. -func (g *Game) loadCargoInternal(ri int, groupIndex uint, ct CargoType, ships uint, quantity float64) error { - if ships == 0 && quantity > 0 { - return e.NewCargoQuantityWithoutGroupBreakError() - } - 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() != StateInOrbit { - 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 != uuid.Nil && 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.NewNoCargoBayError("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) - } - if ships > 0 && ships < g.ShipGroups[sgi].Number { - nsgi, err := g.breakGroupSafe(ri, groupIndex, ships) - if err != nil { - return err - } - sgi = nsgi - } - 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 - if g.ShipGroups[sgi].Load > 0 { - g.ShipGroups[sgi].CargoType = &ct - } - return nil -} +// // Промышленность и Сырье могут быть выгружены на любой планете. +// // Колонисты могут быть высажены только на планеты, принадлежащие Вам или на необитаемые планеты. +// func (g *Game) unloadCargoInternal(ri int, groupIndex uint, ships uint, quantity float64) error { +// if ships == 0 && quantity > 0 { +// return e.NewCargoQuantityWithoutGroupBreakError() +// } +// 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() != StateInOrbit { +// return e.NewShipsBusyError() +// } +// 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.NewNoCargoBayError("ship_type %q", g.Race[ri].ShipTypes[sti].Name) +// } +// if g.ShipGroups[sgi].CargoType == nil || g.ShipGroups[sgi].Load == 0 { +// return e.NewCargoUnloadEmptyError() +// } +// ct := *g.ShipGroups[sgi].CargoType +// 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 ct == CargoColonist { +// if g.Map.Planet[pl].Owner != uuid.Nil && g.Map.Planet[pl].Owner != g.Race[ri].ID { +// return e.NewEntityNotOwnedError("planet #%d unload %v", g.Map.Planet[pl].Number, ct) +// } +// if g.Map.Planet[pl].Owner == uuid.Nil { +// g.Map.Planet[pl].Owner = g.Race[ri].ID +// } +// } +// 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 ships > 0 && ships < g.ShipGroups[sgi].Number { +// nsgi, err := g.breakGroupSafe(ri, groupIndex, ships) +// if err != nil { +// return err +// } +// sgi = nsgi +// } +// toBeUnloaded := quantity +// if quantity == 0 { +// toBeUnloaded = g.ShipGroups[sgi].Load +// } +// if toBeUnloaded > g.ShipGroups[sgi].Load { +// return e.NewCargoUnoadNotEnoughError("load: %.03f", g.ShipGroups[sgi].Load) +// } +// *availableOnPlanet += toBeUnloaded +// g.ShipGroups[sgi].Load -= toBeUnloaded +// if g.ShipGroups[sgi].Load == 0 { +// g.ShipGroups[sgi].CargoType = nil +// } +// return nil +// } -func (g *Game) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - riAccept, err := g.raceIndex(raceAcceptor) - if err != nil { - return err - } - return g.giveawayGroupInternal(ri, riAccept, groupIndex, quantity) -} +// // Корабль может нести только один тип груза одновременно. +// // Возможные типы груза - это колонисты, сырье и промышленность. +// // Груз может быть доставлен на борт корабля с Вашей или не занятой планеты, на которой он имеется. +// func (g *Game) loadCargoInternal(ri int, groupIndex uint, ct CargoType, ships uint, quantity float64) error { +// if ships == 0 && quantity > 0 { +// return e.NewCargoQuantityWithoutGroupBreakError() +// } +// 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() != StateInOrbit { +// 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 != uuid.Nil && 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.NewNoCargoBayError("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) +// } +// if ships > 0 && ships < g.ShipGroups[sgi].Number { +// nsgi, err := g.breakGroupSafe(ri, groupIndex, ships) +// if err != nil { +// return err +// } +// sgi = nsgi +// } +// 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 +// if g.ShipGroups[sgi].Load > 0 { +// g.ShipGroups[sgi].CargoType = &ct +// } +// return nil +// } -func (g *Game) giveawayGroupInternal(ri, riAccept int, groupIndex, quantity uint) (err error) { - if ri == riAccept { - return e.NewSameRaceError(g.Race[riAccept].Name) - } - 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].Number < quantity { - return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) - } +// func (g *Game) GiveawayGroup(raceName, raceAcceptor string, groupIndex, quantity uint) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// riAccept, err := g.raceIndex(raceAcceptor) +// if err != nil { +// return err +// } +// return g.giveawayGroupInternal(ri, riAccept, groupIndex, quantity) +// } - 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) - } +// func (g *Game) giveawayGroupInternal(ri, riAccept int, groupIndex, quantity uint) (err error) { +// if ri == riAccept { +// return e.NewSameRaceError(g.Race[riAccept].Name) +// } +// 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].Number < quantity { +// return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) +// } - var stAcc int - if stAcc = slices.IndexFunc(g.Race[riAccept].ShipTypes, func(st ShipType) bool { return st.Name == g.Race[ri].ShipTypes[sti].Name }); stAcc >= 0 && - !g.Race[ri].ShipTypes[sti].Equal(g.Race[riAccept].ShipTypes[stAcc]) { - return e.NewGiveawayGroupShipsTypeNotEqualError("race %w, ship type %w", g.Race[riAccept].Name, g.Race[riAccept].ShipTypes[stAcc].Name) - } - if stAcc < 0 { - stAcc, err = g.createShipTypeInternal(riAccept, - g.Race[ri].ShipTypes[sti].Name, - g.Race[ri].ShipTypes[sti].Drive, - g.Race[ri].ShipTypes[sti].Weapons, - g.Race[ri].ShipTypes[sti].Shields, - g.Race[ri].ShipTypes[sti].Cargo, - int(g.Race[ri].ShipTypes[sti].Armament)) - if err != nil { - return err - } - } +// 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) +// } - var maxIndex uint - for sg := range g.listShipGroups(riAccept) { - if sg.Index > maxIndex { - maxIndex = sg.Index - } - } +// var stAcc int +// if stAcc = slices.IndexFunc(g.Race[riAccept].ShipTypes, func(st ShipType) bool { return st.Name == g.Race[ri].ShipTypes[sti].Name }); stAcc >= 0 && +// !g.Race[ri].ShipTypes[sti].Equal(g.Race[riAccept].ShipTypes[stAcc]) { +// return e.NewGiveawayGroupShipsTypeNotEqualError("race %w, ship type %w", g.Race[riAccept].Name, g.Race[riAccept].ShipTypes[stAcc].Name) +// } +// if stAcc < 0 { +// stAcc, err = g.createShipTypeInternal(riAccept, +// g.Race[ri].ShipTypes[sti].Name, +// g.Race[ri].ShipTypes[sti].Drive, +// g.Race[ri].ShipTypes[sti].Weapons, +// g.Race[ri].ShipTypes[sti].Shields, +// g.Race[ri].ShipTypes[sti].Cargo, +// int(g.Race[ri].ShipTypes[sti].Armament)) +// if err != nil { +// return err +// } +// } - g.ShipGroups = append(g.ShipGroups, ShipGroup{ - Index: maxIndex + 1, - OwnerID: g.Race[riAccept].ID, - TypeID: g.Race[riAccept].ShipTypes[stAcc].ID, - Number: uint(quantity), +// var maxIndex uint +// for sg := range g.listShipGroups(riAccept) { +// if sg.Index > maxIndex { +// maxIndex = sg.Index +// } +// } - CargoType: g.ShipGroups[sgi].CargoType, - Load: g.ShipGroups[sgi].Load, +// g.ShipGroups = append(g.ShipGroups, ShipGroup{ +// Index: maxIndex + 1, +// OwnerID: g.Race[riAccept].ID, +// TypeID: g.Race[riAccept].ShipTypes[stAcc].ID, +// Number: uint(quantity), - Tech: maps.Clone(g.ShipGroups[sgi].Tech), +// CargoType: g.ShipGroups[sgi].CargoType, +// Load: g.ShipGroups[sgi].Load, - Destination: g.ShipGroups[sgi].Destination, - StateInSpace: g.ShipGroups[sgi].StateInSpace, - StateUpgrade: g.ShipGroups[sgi].StateUpgrade, - }) +// Tech: maps.Clone(g.ShipGroups[sgi].Tech), - if quantity == 0 || quantity == g.ShipGroups[sgi].Number { - g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) - } else { - g.ShipGroups[sgi].Number -= quantity - } +// Destination: g.ShipGroups[sgi].Destination, +// StateInSpace: g.ShipGroups[sgi].StateInSpace, +// StateUpgrade: g.ShipGroups[sgi].StateUpgrade, +// }) - return nil -} +// if quantity == 0 || quantity == g.ShipGroups[sgi].Number { +// g.ShipGroups = append(g.ShipGroups[:sgi], g.ShipGroups[sgi+1:]...) +// } else { +// g.ShipGroups[sgi].Number -= quantity +// } -func (g *Game) breakGroupInternal(ri int, groupIndex, quantity uint) error { - sgi := -1 - var maxIndex uint - for i, sg := range g.listIndexShipGroups(ri) { - if sgi < 0 && sg.Index == groupIndex { - sgi = i - } - if sg.Index > maxIndex { - maxIndex = sg.Index - } - } - if sgi < 0 { - return e.NewEntityNotExistsError("group #%d", groupIndex) - } +// return nil +// } - if g.ShipGroups[sgi].State() != StateInOrbit { - return e.NewShipsBusyError() - } +// func (g *Game) breakGroupInternal(ri int, groupIndex, quantity uint) error { +// sgi := -1 +// var maxIndex uint +// for i, sg := range g.listIndexShipGroups(ri) { +// if sgi < 0 && sg.Index == groupIndex { +// sgi = i +// } +// if sg.Index > maxIndex { +// maxIndex = sg.Index +// } +// } +// if sgi < 0 { +// return e.NewEntityNotExistsError("group #%d", groupIndex) +// } - if g.ShipGroups[sgi].Number < quantity { - return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) - } +// if g.ShipGroups[sgi].State() != StateInOrbit { +// return e.NewShipsBusyError() +// } - if quantity == 0 || quantity == g.ShipGroups[sgi].Number { - g.ShipGroups[sgi].FleetID = nil - } else { - if _, err := g.breakGroupSafe(ri, groupIndex, quantity); err != nil { - return err - } - } +// if g.ShipGroups[sgi].Number < quantity { +// return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) +// } - return nil -} +// if quantity == 0 || quantity == g.ShipGroups[sgi].Number { +// g.ShipGroups[sgi].FleetID = nil +// } else { +// if _, err := g.breakGroupSafe(ri, groupIndex, quantity); err != nil { +// return err +// } +// } -func (g *Game) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int, error) { - sgi := -1 - var maxIndex uint - for i, sg := range g.listIndexShipGroups(ri) { - if sgi < 0 && sg.Index == groupIndex { - sgi = i - } - if sg.Index > maxIndex { - maxIndex = sg.Index - } - } - if sgi < 0 { - return -1, e.NewEntityNotExistsError("group #%d", groupIndex) - } - if g.ShipGroups[sgi].Number < newGroupShips { - return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", g.ShipGroups[sgi].Index, g.ShipGroups[sgi].Number, newGroupShips) - } - newGroup := g.ShipGroups[sgi] - if g.ShipGroups[sgi].CargoType != nil { - newGroup.Load = g.ShipGroups[sgi].Load / float64(g.ShipGroups[sgi].Number) * float64(newGroupShips) - g.ShipGroups[sgi].Load -= newGroup.Load - } - newGroup.Number = newGroupShips - g.ShipGroups[sgi].Number -= newGroup.Number - newGroup.Index = maxIndex + 1 - newGroup.FleetID = nil - g.ShipGroups = append(g.ShipGroups, newGroup) - return len(g.ShipGroups) - 1, nil -} +// return nil +// } -func (g *Game) joinEqualGroupsInternal(ri int) { - shipGroups := slices.Collect(maps.Values(maps.Collect(g.listIndexShipGroups(ri)))) - origin := len(shipGroups) - if origin < 2 { - return - } - for i := 0; i < len(shipGroups)-1; i++ { - for j := len(shipGroups) - 1; j > i; j-- { - if shipGroups[i].Equal(shipGroups[j]) { - shipGroups[i].Index = maxUint(shipGroups[i].Index, shipGroups[j].Index) - shipGroups[i].Number += shipGroups[j].Number - shipGroups = append(shipGroups[:j], shipGroups[j+1:]...) - } - } - } - if len(shipGroups) == origin { - return - } - g.ShipGroups = slices.DeleteFunc(g.ShipGroups, func(v ShipGroup) bool { return v.OwnerID == g.Race[ri].ID }) - g.ShipGroups = append(g.ShipGroups, shipGroups...) -} +// func (g *Game) breakGroupSafe(ri int, groupIndex uint, newGroupShips uint) (int, error) { +// sgi := -1 +// var maxIndex uint +// for i, sg := range g.listIndexShipGroups(ri) { +// if sgi < 0 && sg.Index == groupIndex { +// sgi = i +// } +// if sg.Index > maxIndex { +// maxIndex = sg.Index +// } +// } +// if sgi < 0 { +// return -1, e.NewEntityNotExistsError("group #%d", groupIndex) +// } +// if g.ShipGroups[sgi].Number < newGroupShips { +// return -1, e.NewBreakGroupIllegalNumberError("group #%d ships: %d -> %d", g.ShipGroups[sgi].Index, g.ShipGroups[sgi].Number, newGroupShips) +// } +// newGroup := g.ShipGroups[sgi] +// if g.ShipGroups[sgi].CargoType != nil { +// newGroup.Load = g.ShipGroups[sgi].Load / float64(g.ShipGroups[sgi].Number) * float64(newGroupShips) +// g.ShipGroups[sgi].Load -= newGroup.Load +// } +// newGroup.Number = newGroupShips +// g.ShipGroups[sgi].Number -= newGroup.Number +// newGroup.Index = maxIndex + 1 +// newGroup.FleetID = nil +// g.ShipGroups = append(g.ShipGroups, newGroup) +// return len(g.ShipGroups) - 1, nil +// } -func (g *Game) createShips(ri int, shipTypeName string, planetNumber int, quantity int) error { - st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == shipTypeName }) - if st < 0 { - return e.NewEntityNotExistsError("ship type %w", shipTypeName) - } - pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(planetNumber) }) - if pl < 0 { - return e.NewEntityNotExistsError("planet #%d", planetNumber) - } - if g.Map.Planet[pl].Owner != g.Race[ri].ID { - return e.NewEntityNotOwnedError("planet #%d", planetNumber) - } +// func (g *Game) createShips(ri int, shipTypeName string, planetNumber int, quantity int) error { +// st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == shipTypeName }) +// if st < 0 { +// return e.NewEntityNotExistsError("ship type %w", shipTypeName) +// } +// pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == uint(planetNumber) }) +// if pl < 0 { +// return e.NewEntityNotExistsError("planet #%d", planetNumber) +// } +// if g.Map.Planet[pl].Owner != g.Race[ri].ID { +// return e.NewEntityNotOwnedError("planet #%d", planetNumber) +// } - var maxIndex uint - for _, sg := range g.listIndexShipGroups(ri) { - if sg.Index > maxIndex { - maxIndex = sg.Index - } - } - g.ShipGroups = append(g.ShipGroups, ShipGroup{ - Index: maxIndex + 1, - OwnerID: g.Race[ri].ID, - TypeID: g.Race[ri].ShipTypes[st].ID, - Destination: g.Map.Planet[pl].Number, - Number: uint(quantity), - 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 -} +// var maxIndex uint +// for _, sg := range g.listIndexShipGroups(ri) { +// if sg.Index > maxIndex { +// maxIndex = sg.Index +// } +// } +// g.ShipGroups = append(g.ShipGroups, ShipGroup{ +// Index: maxIndex + 1, +// OwnerID: g.Race[ri].ID, +// TypeID: g.Race[ri].ShipTypes[st].ID, +// Destination: g.Map.Planet[pl].Number, +// Number: uint(quantity), +// 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 +// } -func (g Game) listShipGroups(ri int) iter.Seq[ShipGroup] { - return func(yield func(ShipGroup) bool) { - for _, sg := range g.listIndexShipGroups(ri) { - if !yield(sg) { - return - } - } - } -} +// func (g Game) listShipGroups(ri int) iter.Seq[ShipGroup] { +// return func(yield func(ShipGroup) bool) { +// for _, sg := range g.listIndexShipGroups(ri) { +// if !yield(sg) { +// return +// } +// } +// } +// } -func (g Game) listIndexShipGroups(ri int) iter.Seq2[int, ShipGroup] { - return func(yield func(int, ShipGroup) bool) { - for i := range g.ShipGroups { - if g.ShipGroups[i].OwnerID == g.Race[ri].ID { - if !yield(i, g.ShipGroups[i]) { - return - } - } - } - } -} +// func (g Game) listIndexShipGroups(ri int) iter.Seq2[int, ShipGroup] { +// return func(yield func(int, ShipGroup) bool) { +// for i := range g.ShipGroups { +// if g.ShipGroups[i].OwnerID == g.Race[ri].ID { +// if !yield(i, g.ShipGroups[i]) { +// return +// } +// } +// } +// } +// } -func MustShipGroup(g *Game, ri int, index uint) ShipGroup { - for sg := range g.listShipGroups(ri) { - if sg.Index == index { - return sg - } - } - panic(fmt.Sprintf("race i=%d have no group i=%d", ri, index)) -} +// func MustShipGroup(g *Game, ri int, index uint) ShipGroup { +// for sg := range g.listShipGroups(ri) { +// if sg.Index == index { +// return sg +// } +// } +// panic(fmt.Sprintf("race i=%d have no group i=%d", ri, index)) +// } -func maxUint(a, b uint) uint { - if b > a { - return b - } - return a -} +// func maxUint(a, b uint) uint { +// if b > a { +// return b +// } +// return a +// } diff --git a/internal/model/game/group_send.go b/internal/model/game/group_send.go index 0850e43..ea1d18e 100644 --- a/internal/model/game/group_send.go +++ b/internal/model/game/group_send.go @@ -1,93 +1,93 @@ package game -import ( - "slices" +// import ( +// "slices" - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/iliadenisov/galaxy/internal/util" -) +// e "github.com/iliadenisov/galaxy/internal/error" +// "github.com/iliadenisov/galaxy/internal/util" +// ) -func (g *Game) SendGroup(raceName string, groupIndex, planetNumber, quantity uint) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - return g.sendGroupInternal(ri, groupIndex, planetNumber, quantity) -} +// func (g *Game) SendGroup(raceName string, groupIndex, planetNumber, quantity uint) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// return g.sendGroupInternal(ri, groupIndex, planetNumber, quantity) +// } -func (g *Game) sendGroupInternal(ri int, groupIndex, planetNumber, quantity uint) 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) - } +// func (g *Game) sendGroupInternal(ri int, groupIndex, planetNumber, quantity uint) 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] +// 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] - if st.DriveBlockMass() == 0 { - return e.NewSendShipHasNoDrivesError() - } +// if st.DriveBlockMass() == 0 { +// return e.NewSendShipHasNoDrivesError() +// } - sourcePlanet, ok := g.ShipGroups[sgi].OnPlanet() - if !ok { - return e.NewShipsBusyError() - } +// sourcePlanet, ok := g.ShipGroups[sgi].OnPlanet() +// if !ok { +// return e.NewShipsBusyError() +// } - if g.ShipGroups[sgi].Number < quantity { - return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) - } +// if g.ShipGroups[sgi].Number < quantity { +// return e.NewBeakGroupNumberNotEnoughError("%d<%d", g.ShipGroups[sgi].Number, quantity) +// } - p1, ok := PlanetByNum(g, sourcePlanet) - if !ok { - return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) - } - p2, ok := PlanetByNum(g, planetNumber) - if !ok { - return e.NewEntityNotExistsError("destination planet #%d", planetNumber) - } - rangeToDestination := util.ShortDistance(g.Map.Width, g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) - if rangeToDestination > g.Race[ri].FlightDistance() { - return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) - } +// p1, ok := PlanetByNum(g, sourcePlanet) +// if !ok { +// return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) +// } +// p2, ok := PlanetByNum(g, planetNumber) +// if !ok { +// return e.NewEntityNotExistsError("destination planet #%d", planetNumber) +// } +// rangeToDestination := util.ShortDistance(g.Map.Width, g.Map.Height, p1.X, p1.Y, p2.X, p2.Y) +// if rangeToDestination > g.Race[ri].FlightDistance() { +// return e.NewSendUnreachableDestinationError("range=%.03f", rangeToDestination) +// } - if quantity > 0 && quantity < g.ShipGroups[sgi].Number { - nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) - if err != nil { - return err - } - sgi = nsgi - } +// if quantity > 0 && quantity < g.ShipGroups[sgi].Number { +// nsgi, err := g.breakGroupSafe(ri, groupIndex, quantity) +// if err != nil { +// return err +// } +// sgi = nsgi +// } - if sourcePlanet == planetNumber { - g.ShipGroups[sgi] = UnsendShips(g.ShipGroups[sgi]) - g.joinEqualGroupsInternal(ri) - return nil - } +// if sourcePlanet == planetNumber { +// g.ShipGroups[sgi] = UnsendShips(g.ShipGroups[sgi]) +// g.joinEqualGroupsInternal(ri) +// return nil +// } - g.ShipGroups[sgi] = LaunchShips(g.ShipGroups[sgi], planetNumber) +// g.ShipGroups[sgi] = LaunchShips(g.ShipGroups[sgi], planetNumber) - return nil -} +// return nil +// } -func LaunchShips(sg ShipGroup, destination uint) ShipGroup { - sg.StateInSpace = &InSpace{ - Origin: sg.Destination, - } - sg.Destination = destination - return sg -} +// func LaunchShips(sg ShipGroup, destination uint) ShipGroup { +// sg.StateInSpace = &InSpace{ +// Origin: sg.Destination, +// } +// sg.Destination = destination +// return sg +// } -func UnsendShips(sg ShipGroup) ShipGroup { - sg.Destination = sg.StateInSpace.Origin - sg.StateInSpace = nil - return sg -} +// func UnsendShips(sg ShipGroup) ShipGroup { +// sg.Destination = sg.StateInSpace.Origin +// sg.StateInSpace = nil +// return sg +// } diff --git a/internal/model/game/group_send_test.go b/internal/model/game/group_send_test.go index 417accd..4efcca2 100644 --- a/internal/model/game/group_send_test.go +++ b/internal/model/game/group_send_test.go @@ -1,69 +1,69 @@ package game_test -import ( - "slices" - "testing" +// import ( +// "slices" +// "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/iliadenisov/galaxy/internal/model/game" +// "github.com/stretchr/testify/assert" +// ) -func TestSendGroup(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, unmovable - g.CreateShipType(Race_0.Name, "Fortress", 0, 30, 100, 0, 50) - assert.NoError(t, g.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1)) +// func TestSendGroup(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, unmovable +// g.CreateShipType(Race_0.Name, "Fortress", 0, 50, 30, 100, 0) +// assert.NoError(t, g.CreateShips(Race_0_idx, "Fortress", R0_Planet_0_num, 1)) - assert.ErrorContains(t, - g.SendGroup("UnknownRace", 1, 2, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.SendGroup(Race_0.Name, 555, 2, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.SendGroup(Race_0.Name, 1, 222, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.SendGroup(Race_0.Name, 2, 1, 0), - e.GenericErrorText(e.ErrShipsBusy)) - assert.ErrorContains(t, - g.SendGroup(Race_0.Name, 3, 2, 0), - e.GenericErrorText(e.ErrSendShipHasNoDrives)) - assert.ErrorContains(t, - g.SendGroup(Race_0.Name, 1, 2, 100), - e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) - assert.ErrorContains(t, - g.SendGroup(Race_0.Name, 1, 3, 0), - e.GenericErrorText(e.ErrSendUnreachableDestination)) +// assert.ErrorContains(t, +// g.SendGroup("UnknownRace", 1, 2, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.SendGroup(Race_0.Name, 555, 2, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.SendGroup(Race_0.Name, 1, 222, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.SendGroup(Race_0.Name, 2, 1, 0), +// e.GenericErrorText(e.ErrShipsBusy)) +// assert.ErrorContains(t, +// g.SendGroup(Race_0.Name, 3, 2, 0), +// e.GenericErrorText(e.ErrSendShipHasNoDrives)) +// assert.ErrorContains(t, +// g.SendGroup(Race_0.Name, 1, 2, 100), +// e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) +// assert.ErrorContains(t, +// g.SendGroup(Race_0.Name, 1, 3, 0), +// e.GenericErrorText(e.ErrSendUnreachableDestination)) - assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 3)) // send 3 of 10 - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) - assert.Equal(t, uint(7), g.ShipGroups[0].Number) - assert.Equal(t, game.StateInOrbit, g.ShipGroups[0].State()) - assert.Equal(t, uint(3), g.ShipGroups[3].Number) - assert.Equal(t, game.StateLaunched, g.ShipGroups[3].State()) +// assert.NoError(t, g.SendGroup(Race_0.Name, 1, 2, 3)) // send 3 of 10 +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) +// assert.Equal(t, uint(7), g.ShipGroups[0].Number) +// assert.Equal(t, game.StateInOrbit, g.ShipGroups[0].State()) +// assert.Equal(t, uint(3), g.ShipGroups[3].Number) +// assert.Equal(t, game.StateLaunched, g.ShipGroups[3].State()) - assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 2)) // un-send 2 of 3 - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) - assert.Equal(t, uint(9), game.MustShipGroup(g, Race_0_idx, 5).Number) - assert.Equal(t, game.StateInOrbit, game.MustShipGroup(g, Race_0_idx, 5).State()) - assert.Equal(t, uint(1), game.MustShipGroup(g, Race_0_idx, 4).Number) - assert.Equal(t, game.StateLaunched, game.MustShipGroup(g, Race_0_idx, 4).State()) +// assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 2)) // un-send 2 of 3 +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) +// assert.Equal(t, uint(9), game.MustShipGroup(g, Race_0_idx, 5).Number) +// assert.Equal(t, game.StateInOrbit, game.MustShipGroup(g, Race_0_idx, 5).State()) +// assert.Equal(t, uint(1), game.MustShipGroup(g, Race_0_idx, 4).Number) +// assert.Equal(t, game.StateLaunched, game.MustShipGroup(g, Race_0_idx, 4).State()) - assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 0)) // un-send the rest 1 - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) - assert.Equal(t, uint(10), game.MustShipGroup(g, Race_0_idx, 5).Number) - assert.Equal(t, game.StateInOrbit, game.MustShipGroup(g, Race_0_idx, 5).State()) +// assert.NoError(t, g.SendGroup(Race_0.Name, 4, 0, 0)) // un-send the rest 1 +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) +// assert.Equal(t, uint(10), game.MustShipGroup(g, Race_0_idx, 5).Number) +// assert.Equal(t, game.StateInOrbit, game.MustShipGroup(g, Race_0_idx, 5).State()) - assert.NoError(t, g.SendGroup(Race_0.Name, 5, 2, 0)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) - assert.Equal(t, uint(10), game.MustShipGroup(g, Race_0_idx, 5).Number) - assert.Equal(t, game.StateLaunched, game.MustShipGroup(g, Race_0_idx, 5).State()) -} +// assert.NoError(t, g.SendGroup(Race_0.Name, 5, 2, 0)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) +// assert.Equal(t, uint(10), game.MustShipGroup(g, Race_0_idx, 5).Number) +// assert.Equal(t, game.StateLaunched, game.MustShipGroup(g, Race_0_idx, 5).State()) +// } diff --git a/internal/model/game/group_test.go b/internal/model/game/group_test.go index 7bccb45..af803bd 100644 --- a/internal/model/game/group_test.go +++ b/internal/model/game/group_test.go @@ -2,13 +2,10 @@ package game_test import ( "math/rand/v2" - "slices" "testing" "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" ) @@ -259,517 +256,517 @@ func TestShipGroupEqual(t *testing.T) { assert.True(t, left.Equal(right)) } -func TestCreateShips(t *testing.T) { - g := newGame() - - assert.ErrorContains(t, - g.CreateShips(Race_0_idx, "Unknown_Ship_Type", R0_Planet_0_num, 2), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.CreateShips(Race_0_idx, Race_0_Gunship, R1_Planet_1_num, 2), - e.GenericErrorText(e.ErrInputEntityNotOwned)) - - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 1) - - assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1)) - assert.Len(t, slices.Collect(g.ListShipGroups(1)), 1) - - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) - - assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1)) - assert.Len(t, slices.Collect(g.ListShipGroups(1)), 2) -} - -func TestJoinEqualGroups(t *testing.T) { - 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)) - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) // (2) - 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].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) - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 4)) // (7) - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) - - 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) - - assert.NoError(t, g.JoinEqualGroups(Race_0.Name)) - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 3) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) - - shipTypeID := func(ri int, name string) uuid.UUID { - st := slices.IndexFunc(g.Race[ri].ShipTypes, func(v game.ShipType) bool { return v.Name == name }) - if st < 0 { - t.Fatalf("ShipType not found: %s", name) - return uuid.Nil - } - return g.Race[ri].ShipTypes[st].ID - } - - for sg := range g.ListShipGroups(Race_0_idx) { - switch { - 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.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.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.TechLevel(game.TechDrive) == 1.5: - assert.Equal(t, uint(13), sg.Number) - assert.Equal(t, uint(6), sg.Index) - default: - t.Error("not all ship groups covered") - } - } -} - -func TestBreakGroup(t *testing.T) { - 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].StateInSpace = &game.InSpace{ - Origin: 1, - Range: 1, - } - - fleet := "R0_Fleet" - assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleet, 1, 0)) - - assert.ErrorContains(t, - g.BreakGroup("UnknownRace", 1, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.BreakGroup(Race_0.Name, 555, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.BreakGroup(Race_0.Name, 1, 17), - e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) - assert.ErrorContains(t, - g.BreakGroup(Race_0.Name, 2, 0), - e.GenericErrorText(e.ErrShipsBusy)) - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) - assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 1) - - // group #1 -> group #3 (5 new, 8 left) - assert.NoError(t, g.BreakGroup(Race_0.Name, 1, 5)) // group #3 (2) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) - assert.Equal(t, uint(8), g.ShipGroups[0].Number) - assert.NotNil(t, g.ShipGroups[0].FleetID) - 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) - assert.NotNil(t, g.ShipGroups[0].FleetID) - assert.Equal(t, uint(2), g.ShipGroups[3].Number) - assert.Equal(t, uint(4), g.ShipGroups[3].Index) - assert.Nil(t, g.ShipGroups[3].FleetID) - 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) - assert.Equal(t, uint(6), g.ShipGroups[0].Number) - assert.Nil(t, g.ShipGroups[0].FleetID) - - // group #4 -> ALL off the fleet - assert.NoError(t, g.BreakGroup(Race_0.Name, 4, 0)) // group #1 (0) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) - assert.Equal(t, uint(2), g.ShipGroups[3].Number) - assert.Nil(t, g.ShipGroups[3].FleetID) -} - -func TestGiveawayGroup(t *testing.T) { - 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].StateInSpace = &game.InSpace{ - Origin: 2, - Range: 31.337, - } - g.ShipGroups[2].CargoType = game.CargoMaterial.Ref() - g.ShipGroups[2].Load = 1.234 - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 1) - - assert.ErrorContains(t, - g.GiveawayGroup("UnknownRace", Race_1.Name, 2, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.GiveawayGroup(Race_0.Name, "UnknownRace", 2, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.GiveawayGroup(Race_0.Name, Race_0.Name, 2, 0), - e.GenericErrorText(e.ErrInputSameRace)) - assert.ErrorContains(t, - g.GiveawayGroup(Race_0.Name, Race_1.Name, 555, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.GiveawayGroup(Race_0.Name, Race_1.Name, 2, 18), - e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) - assert.ErrorContains(t, - g.GiveawayGroup(Race_0.Name, Race_1.Name, 1, 0), - e.GenericErrorText(e.ErrGiveawayGroupShipsTypeNotEqual)) - - assert.NoError(t, g.GiveawayGroup(Race_0.Name, Race_1.Name, 2, 11)) // group #2 (3) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 2) - sto := slices.IndexFunc(g.Race[Race_0_idx].ShipTypes, func(st game.ShipType) bool { return st.Name == Race_0_Gunship }) - sti := slices.IndexFunc(g.Race[Race_1_idx].ShipTypes, func(st game.ShipType) bool { return st.Name == Race_0_Gunship }) - assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Name, g.Race[Race_0_idx].ShipTypes[sto].Name) - assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Drive, g.Race[Race_0_idx].ShipTypes[sto].Drive) - assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Weapons, g.Race[Race_0_idx].ShipTypes[sto].Weapons) - 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].CargoType, g.ShipGroups[3].CargoType) - assert.Equal(t, g.ShipGroups[2].Load, g.ShipGroups[3].Load) - 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].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)) - assert.Nil(t, g.ShipGroups[3].FleetID) - - assert.NoError(t, g.GiveawayGroup(Race_1.Name, Race_0.Name, 2, 11)) - 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 := newGame() - - // 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].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)) - 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 - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 5) - - // tests - assert.ErrorContains(t, - g.LoadCargo("UnknownRace", 1, game.CargoMaterial.String(), 0, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 1, "GOLD", 0, 0), - e.GenericErrorText(e.ErrInputCargoTypeInvalid)) - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 555, game.CargoMaterial.String(), 0, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 3, game.CargoMaterial.String(), 0, 0), - e.GenericErrorText(e.ErrShipsBusy)) - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 5, game.CargoMaterial.String(), 0, 0), - e.GenericErrorText(e.ErrInputEntityNotOwned)) - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 2, game.CargoMaterial.String(), 0, 0), - e.GenericErrorText(e.ErrInputNoCargoBay)) - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 4, game.CargoMaterial.String(), 0, 0), - e.GenericErrorText(e.ErrInputCargoLoadNotEqual)) - - // initial planet is empty - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 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(), 11, 101), - e.GenericErrorText(e.ErrInputCargoLoadNotEnough)) - // quantity > ships - assert.ErrorContains(t, - g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 1), - e.GenericErrorText(e.ErrInputCargoQuantityWithoutGroupBreak)) - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 5) - - // break group and load maximum - assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 2, 0)) - assert.Equal(t, 58.0, g.Map.Planet[0].Material) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 6) - assert.Nil(t, g.ShipGroups[0].CargoType) - assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[5].CargoType) - assert.Equal(t, uint(9), g.ShipGroups[0].Number) - assert.Equal(t, 0.0, g.ShipGroups[0].Load) - assert.Equal(t, uint(2), g.ShipGroups[5].Number) - assert.Equal(t, 42.0, g.ShipGroups[5].Load) - - // break group and load limited - assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 2, 18)) - assert.Equal(t, 40.0, g.Map.Planet[0].Material) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) - assert.Nil(t, g.ShipGroups[0].CargoType) - assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[6].CargoType) - assert.Equal(t, uint(7), g.ShipGroups[0].Number) - assert.Equal(t, 0.0, g.ShipGroups[0].Load) - assert.Equal(t, uint(2), g.ShipGroups[6].Number) - assert.Equal(t, 18.0, g.ShipGroups[6].Load) - - // add cargo to planet - g.Map.Planet[0].Material = 100 - // loading all available cargo - assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 0)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) - 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(), 11, 31)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) - 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(), 11, 0)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) - assert.Equal(t, 153.0, g.Map.Planet[0].Material) - assert.Equal(t, 147.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, 0), - e.GenericErrorText(e.ErrInputCargoLoadNoSpaceLeft)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) -} - -func TestUnloadCargo(t *testing.T) { - 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 / 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].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)) - g.ShipGroups[3].CargoType = game.CargoColonist.Ref() - g.ShipGroups[3].Load = 1.234 - - // 5: idx = 4 / on foreign planet / loaded with COL - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) - g.ShipGroups[4].Destination = R1_Planet_1_num - g.ShipGroups[4].CargoType = game.CargoColonist.Ref() - g.ShipGroups[4].Load = 1.234 - - // 6: idx = 5 / on foreign planet / loaded with MAT - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) - g.ShipGroups[5].Destination = R1_Planet_1_num - g.ShipGroups[5].CargoType = game.CargoMaterial.Ref() - g.ShipGroups[5].Load = 100.0 - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 6) - - // tests - assert.ErrorContains(t, - g.UnloadCargo("UnknownRace", 1, 0, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 555, 0, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 3, 0, 0), - e.GenericErrorText(e.ErrShipsBusy)) - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 2, 0, 0), - e.GenericErrorText(e.ErrInputNoCargoBay)) - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 1, 0, 0), - e.GenericErrorText(e.ErrInputCargoUnloadEmpty)) - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 5, 0, 0), - e.GenericErrorText(e.ErrInputEntityNotOwned)) - g.ShipGroups[0].CargoType = game.CargoColonist.Ref() - g.ShipGroups[0].Load = 100 - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 1, 11, 101), - e.GenericErrorText(e.ErrInputCargoUnoadNotEnough)) - assert.ErrorContains(t, - g.UnloadCargo(Race_0.Name, 1, 0, 1), - e.GenericErrorText(e.ErrInputCargoQuantityWithoutGroupBreak)) - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 6) - - // unload MAT on foreign planet / break group - assert.NoError(t, g.UnloadCargo(Race_0.Name, 6, 3, 0)) - assert.Equal(t, 27.273, number.Fixed3(g.Map.Planet[1].Material)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) - assert.Equal(t, uint(3), g.ShipGroups[6].Number) - assert.Nil(t, g.ShipGroups[6].CargoType) - assert.Equal(t, 0.0, g.ShipGroups[6].Load) - assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[5].CargoType) - assert.Equal(t, uint(8), g.ShipGroups[5].Number) - assert.Equal(t, 72.727, number.Fixed3(g.ShipGroups[5].Load)) - - // unload MAT on foreign planet / break group / limited MAT - assert.NoError(t, g.UnloadCargo(Race_0.Name, 6, 3, 20.0)) - assert.Equal(t, 47.273, number.Fixed3(g.Map.Planet[1].Material)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 8) - assert.Equal(t, uint(3), g.ShipGroups[7].Number) - assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[7].CargoType) - assert.Equal(t, 7.273, number.Fixed3(g.ShipGroups[7].Load)) - assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[5].CargoType) - assert.Equal(t, uint(5), g.ShipGroups[5].Number) - assert.Equal(t, 45.455, number.Fixed3(g.ShipGroups[5].Load)) - - // unload ALL - assert.NoError(t, g.UnloadCargo(Race_0.Name, 1, 0, 0)) - assert.Equal(t, 100.0, number.Fixed3(g.Map.Planet[0].Colonists)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 8) - assert.Equal(t, uint(10), g.ShipGroups[0].Number) - assert.Nil(t, g.ShipGroups[0].CargoType) - assert.Equal(t, 0.0, number.Fixed3(g.ShipGroups[0].Load)) -} - -func TestDisassembleGroup(t *testing.T) { - 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].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)) - g.ShipGroups[2].CargoType = game.CargoColonist.Ref() - g.ShipGroups[2].Load = 80.0 - - // 4: idx = 3 / on foreign planet / loaded with MAT - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) - g.ShipGroups[3].Destination = R1_Planet_1_num - g.ShipGroups[3].CargoType = game.CargoMaterial.Ref() - g.ShipGroups[3].Load = 100.0 - - // 5: idx = 4 / on foreign planet / loaded with COL - assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) - g.ShipGroups[4].Destination = R1_Planet_1_num - g.ShipGroups[4].CargoType = game.CargoColonist.Ref() - g.ShipGroups[4].Load = 2.345 - - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 5) - - // tests - assert.ErrorContains(t, - g.DisassembleGroup("UnknownRace", 1, 0), - e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, - g.DisassembleGroup(Race_0.Name, 555, 0), - e.GenericErrorText(e.ErrInputEntityNotExists)) - assert.ErrorContains(t, - g.DisassembleGroup(Race_0.Name, 2, 0), - e.GenericErrorText(e.ErrShipsBusy)) - assert.ErrorContains(t, - g.DisassembleGroup(Race_0.Name, 3, 12), - e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) - - groupEmptyMass := g.ShipGroups[4].EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) - // groupLoadCOL := g.ShipGroups[4].Load - planetMAT := g.Map.Planet[1].Material - planetCOL := g.Map.Planet[1].Colonists - - assert.NoError(t, g.DisassembleGroup(Race_0.Name, 5, 0)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) - assert.Equal(t, planetMAT+groupEmptyMass, g.Map.Planet[1].Material) - assert.Equal(t, planetCOL, g.Map.Planet[1].Colonists) - - groupEmptyMass = g.ShipGroups[3].EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) - groupLoadMAT := g.ShipGroups[3].Load - planetMAT = g.Map.Planet[1].Material - assert.NoError(t, g.DisassembleGroup(Race_0.Name, 4, 0)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) - assert.Equal(t, planetMAT+groupEmptyMass+groupLoadMAT, g.Map.Planet[1].Material) - - groupEmptyMass = g.ShipGroups[2].EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) - planetMAT = g.Map.Planet[0].Material - planetCOL = g.Map.Planet[0].Colonists - planetPOP := 90.0 - g.Map.Planet[0].Population = planetPOP - var shipsDisassembling uint = 3 - groupEmptyMass = groupEmptyMass / float64(g.ShipGroups[2].Number) * float64(shipsDisassembling) - newGroupUnloadedCOL := g.ShipGroups[2].Load / float64(g.ShipGroups[2].Number) * float64(shipsDisassembling) - expectPOPIncrease := newGroupUnloadedCOL * 8 - freePOPLeft := g.Map.Planet[0].Size - g.Map.Planet[0].Population - expectAddedCOL := (expectPOPIncrease - freePOPLeft) / 8 - expectAddedPOP := freePOPLeft - assert.NoError(t, g.DisassembleGroup(Race_0.Name, 3, shipsDisassembling)) - assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) - assert.Equal(t, planetCOL+expectAddedCOL, g.Map.Planet[0].Colonists) - assert.Equal(t, planetPOP+expectAddedPOP, g.Map.Planet[0].Population) - assert.Equal(t, planetMAT+groupEmptyMass, g.Map.Planet[0].Material) - assert.Equal(t, uint(7), g.ShipGroups[2].Number) - assert.Equal(t, 56.0, g.ShipGroups[2].Load) -} +// func TestCreateShips(t *testing.T) { +// g := newGame() + +// assert.ErrorContains(t, +// g.CreateShips(Race_0_idx, "Unknown_Ship_Type", R0_Planet_0_num, 2), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.CreateShips(Race_0_idx, Race_0_Gunship, R1_Planet_1_num, 2), +// e.GenericErrorText(e.ErrInputEntityNotOwned)) + +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 1)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 1) + +// assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Freighter, R1_Planet_1_num, 1)) +// assert.Len(t, slices.Collect(g.ListShipGroups(1)), 1) + +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) + +// assert.NoError(t, g.CreateShips(Race_1_idx, Race_1_Gunship, R1_Planet_1_num, 1)) +// assert.Len(t, slices.Collect(g.ListShipGroups(1)), 2) +// } + +// func TestJoinEqualGroups(t *testing.T) { +// 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)) +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 6)) // (2) +// 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].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) +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 4)) // (7) + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) + +// 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) + +// assert.NoError(t, g.JoinEqualGroups(Race_0.Name)) + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 3) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) + +// shipTypeID := func(ri int, name string) uuid.UUID { +// st := slices.IndexFunc(g.Race[ri].ShipTypes, func(v game.ShipType) bool { return v.Name == name }) +// if st < 0 { +// t.Fatalf("ShipType not found: %s", name) +// return uuid.Nil +// } +// return g.Race[ri].ShipTypes[st].ID +// } + +// for sg := range g.ListShipGroups(Race_0_idx) { +// switch { +// 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.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.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.TechLevel(game.TechDrive) == 1.5: +// assert.Equal(t, uint(13), sg.Number) +// assert.Equal(t, uint(6), sg.Index) +// default: +// t.Error("not all ship groups covered") +// } +// } +// } + +// func TestBreakGroup(t *testing.T) { +// 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].StateInSpace = &game.InSpace{ +// Origin: 1, +// Range: 1, +// } + +// fleet := "R0_Fleet" +// assert.NoError(t, g.JoinShipGroupToFleet(Race_0.Name, fleet, 1, 0)) + +// assert.ErrorContains(t, +// g.BreakGroup("UnknownRace", 1, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.BreakGroup(Race_0.Name, 555, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.BreakGroup(Race_0.Name, 1, 17), +// e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) +// assert.ErrorContains(t, +// g.BreakGroup(Race_0.Name, 2, 0), +// e.GenericErrorText(e.ErrShipsBusy)) + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) +// assert.Len(t, slices.Collect(g.ListFleets(Race_0_idx)), 1) + +// // group #1 -> group #3 (5 new, 8 left) +// assert.NoError(t, g.BreakGroup(Race_0.Name, 1, 5)) // group #3 (2) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) +// assert.Equal(t, uint(8), g.ShipGroups[0].Number) +// assert.NotNil(t, g.ShipGroups[0].FleetID) +// 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) +// assert.NotNil(t, g.ShipGroups[0].FleetID) +// assert.Equal(t, uint(2), g.ShipGroups[3].Number) +// assert.Equal(t, uint(4), g.ShipGroups[3].Index) +// assert.Nil(t, g.ShipGroups[3].FleetID) +// 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) +// assert.Equal(t, uint(6), g.ShipGroups[0].Number) +// assert.Nil(t, g.ShipGroups[0].FleetID) + +// // group #4 -> ALL off the fleet +// assert.NoError(t, g.BreakGroup(Race_0.Name, 4, 0)) // group #1 (0) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) +// assert.Equal(t, uint(2), g.ShipGroups[3].Number) +// assert.Nil(t, g.ShipGroups[3].FleetID) +// } + +// func TestGiveawayGroup(t *testing.T) { +// 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].StateInSpace = &game.InSpace{ +// Origin: 2, +// Range: 31.337, +// } +// g.ShipGroups[2].CargoType = game.CargoMaterial.Ref() +// g.ShipGroups[2].Load = 1.234 + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 1) + +// assert.ErrorContains(t, +// g.GiveawayGroup("UnknownRace", Race_1.Name, 2, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.GiveawayGroup(Race_0.Name, "UnknownRace", 2, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.GiveawayGroup(Race_0.Name, Race_0.Name, 2, 0), +// e.GenericErrorText(e.ErrInputSameRace)) +// assert.ErrorContains(t, +// g.GiveawayGroup(Race_0.Name, Race_1.Name, 555, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.GiveawayGroup(Race_0.Name, Race_1.Name, 2, 18), +// e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) +// assert.ErrorContains(t, +// g.GiveawayGroup(Race_0.Name, Race_1.Name, 1, 0), +// e.GenericErrorText(e.ErrGiveawayGroupShipsTypeNotEqual)) + +// assert.NoError(t, g.GiveawayGroup(Race_0.Name, Race_1.Name, 2, 11)) // group #2 (3) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 2) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_1_idx)), 2) +// sto := slices.IndexFunc(g.Race[Race_0_idx].ShipTypes, func(st game.ShipType) bool { return st.Name == Race_0_Gunship }) +// sti := slices.IndexFunc(g.Race[Race_1_idx].ShipTypes, func(st game.ShipType) bool { return st.Name == Race_0_Gunship }) +// assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Name, g.Race[Race_0_idx].ShipTypes[sto].Name) +// assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Drive, g.Race[Race_0_idx].ShipTypes[sto].Drive) +// assert.Equal(t, g.Race[Race_1_idx].ShipTypes[sti].Weapons, g.Race[Race_0_idx].ShipTypes[sto].Weapons) +// 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].CargoType, g.ShipGroups[3].CargoType) +// assert.Equal(t, g.ShipGroups[2].Load, g.ShipGroups[3].Load) +// 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].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)) +// assert.Nil(t, g.ShipGroups[3].FleetID) + +// assert.NoError(t, g.GiveawayGroup(Race_1.Name, Race_0.Name, 2, 11)) +// 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 := newGame() + +// // 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].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)) +// 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 + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 5) + +// // tests +// assert.ErrorContains(t, +// g.LoadCargo("UnknownRace", 1, game.CargoMaterial.String(), 0, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 1, "GOLD", 0, 0), +// e.GenericErrorText(e.ErrInputCargoTypeInvalid)) +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 555, game.CargoMaterial.String(), 0, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 3, game.CargoMaterial.String(), 0, 0), +// e.GenericErrorText(e.ErrShipsBusy)) +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 5, game.CargoMaterial.String(), 0, 0), +// e.GenericErrorText(e.ErrInputEntityNotOwned)) +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 2, game.CargoMaterial.String(), 0, 0), +// e.GenericErrorText(e.ErrInputNoCargoBay)) +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 4, game.CargoMaterial.String(), 0, 0), +// e.GenericErrorText(e.ErrInputCargoLoadNotEqual)) + +// // initial planet is empty +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 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(), 11, 101), +// e.GenericErrorText(e.ErrInputCargoLoadNotEnough)) +// // quantity > ships +// assert.ErrorContains(t, +// g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 1), +// e.GenericErrorText(e.ErrInputCargoQuantityWithoutGroupBreak)) + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 5) + +// // break group and load maximum +// assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 2, 0)) +// assert.Equal(t, 58.0, g.Map.Planet[0].Material) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 6) +// assert.Nil(t, g.ShipGroups[0].CargoType) +// assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[5].CargoType) +// assert.Equal(t, uint(9), g.ShipGroups[0].Number) +// assert.Equal(t, 0.0, g.ShipGroups[0].Load) +// assert.Equal(t, uint(2), g.ShipGroups[5].Number) +// assert.Equal(t, 42.0, g.ShipGroups[5].Load) + +// // break group and load limited +// assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 2, 18)) +// assert.Equal(t, 40.0, g.Map.Planet[0].Material) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) +// assert.Nil(t, g.ShipGroups[0].CargoType) +// assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[6].CargoType) +// assert.Equal(t, uint(7), g.ShipGroups[0].Number) +// assert.Equal(t, 0.0, g.ShipGroups[0].Load) +// assert.Equal(t, uint(2), g.ShipGroups[6].Number) +// assert.Equal(t, 18.0, g.ShipGroups[6].Load) + +// // add cargo to planet +// g.Map.Planet[0].Material = 100 +// // loading all available cargo +// assert.NoError(t, g.LoadCargo(Race_0.Name, 1, game.CargoMaterial.String(), 0, 0)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) +// 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(), 11, 31)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) +// 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(), 11, 0)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) +// assert.Equal(t, 153.0, g.Map.Planet[0].Material) +// assert.Equal(t, 147.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, 0), +// e.GenericErrorText(e.ErrInputCargoLoadNoSpaceLeft)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) +// } + +// func TestUnloadCargo(t *testing.T) { +// 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 / 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].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)) +// g.ShipGroups[3].CargoType = game.CargoColonist.Ref() +// g.ShipGroups[3].Load = 1.234 + +// // 5: idx = 4 / on foreign planet / loaded with COL +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) +// g.ShipGroups[4].Destination = R1_Planet_1_num +// g.ShipGroups[4].CargoType = game.CargoColonist.Ref() +// g.ShipGroups[4].Load = 1.234 + +// // 6: idx = 5 / on foreign planet / loaded with MAT +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) +// g.ShipGroups[5].Destination = R1_Planet_1_num +// g.ShipGroups[5].CargoType = game.CargoMaterial.Ref() +// g.ShipGroups[5].Load = 100.0 + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 6) + +// // tests +// assert.ErrorContains(t, +// g.UnloadCargo("UnknownRace", 1, 0, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 555, 0, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 3, 0, 0), +// e.GenericErrorText(e.ErrShipsBusy)) +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 2, 0, 0), +// e.GenericErrorText(e.ErrInputNoCargoBay)) +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 1, 0, 0), +// e.GenericErrorText(e.ErrInputCargoUnloadEmpty)) +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 5, 0, 0), +// e.GenericErrorText(e.ErrInputEntityNotOwned)) +// g.ShipGroups[0].CargoType = game.CargoColonist.Ref() +// g.ShipGroups[0].Load = 100 +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 1, 11, 101), +// e.GenericErrorText(e.ErrInputCargoUnoadNotEnough)) +// assert.ErrorContains(t, +// g.UnloadCargo(Race_0.Name, 1, 0, 1), +// e.GenericErrorText(e.ErrInputCargoQuantityWithoutGroupBreak)) + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 6) + +// // unload MAT on foreign planet / break group +// assert.NoError(t, g.UnloadCargo(Race_0.Name, 6, 3, 0)) +// assert.Equal(t, 27.273, number.Fixed3(g.Map.Planet[1].Material)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 7) +// assert.Equal(t, uint(3), g.ShipGroups[6].Number) +// assert.Nil(t, g.ShipGroups[6].CargoType) +// assert.Equal(t, 0.0, g.ShipGroups[6].Load) +// assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[5].CargoType) +// assert.Equal(t, uint(8), g.ShipGroups[5].Number) +// assert.Equal(t, 72.727, number.Fixed3(g.ShipGroups[5].Load)) + +// // unload MAT on foreign planet / break group / limited MAT +// assert.NoError(t, g.UnloadCargo(Race_0.Name, 6, 3, 20.0)) +// assert.Equal(t, 47.273, number.Fixed3(g.Map.Planet[1].Material)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 8) +// assert.Equal(t, uint(3), g.ShipGroups[7].Number) +// assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[7].CargoType) +// assert.Equal(t, 7.273, number.Fixed3(g.ShipGroups[7].Load)) +// assert.Equal(t, game.CargoMaterial.Ref(), g.ShipGroups[5].CargoType) +// assert.Equal(t, uint(5), g.ShipGroups[5].Number) +// assert.Equal(t, 45.455, number.Fixed3(g.ShipGroups[5].Load)) + +// // unload ALL +// assert.NoError(t, g.UnloadCargo(Race_0.Name, 1, 0, 0)) +// assert.Equal(t, 100.0, number.Fixed3(g.Map.Planet[0].Colonists)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 8) +// assert.Equal(t, uint(10), g.ShipGroups[0].Number) +// assert.Nil(t, g.ShipGroups[0].CargoType) +// assert.Equal(t, 0.0, number.Fixed3(g.ShipGroups[0].Load)) +// } + +// func TestDisassembleGroup(t *testing.T) { +// 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].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)) +// g.ShipGroups[2].CargoType = game.CargoColonist.Ref() +// g.ShipGroups[2].Load = 80.0 + +// // 4: idx = 3 / on foreign planet / loaded with MAT +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) +// g.ShipGroups[3].Destination = R1_Planet_1_num +// g.ShipGroups[3].CargoType = game.CargoMaterial.Ref() +// g.ShipGroups[3].Load = 100.0 + +// // 5: idx = 4 / on foreign planet / loaded with COL +// assert.NoError(t, g.CreateShips(Race_0_idx, Race_0_Freighter, R0_Planet_0_num, 11)) +// g.ShipGroups[4].Destination = R1_Planet_1_num +// g.ShipGroups[4].CargoType = game.CargoColonist.Ref() +// g.ShipGroups[4].Load = 2.345 + +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 5) + +// // tests +// assert.ErrorContains(t, +// g.DisassembleGroup("UnknownRace", 1, 0), +// e.GenericErrorText(e.ErrInputUnknownRace)) +// assert.ErrorContains(t, +// g.DisassembleGroup(Race_0.Name, 555, 0), +// e.GenericErrorText(e.ErrInputEntityNotExists)) +// assert.ErrorContains(t, +// g.DisassembleGroup(Race_0.Name, 2, 0), +// e.GenericErrorText(e.ErrShipsBusy)) +// assert.ErrorContains(t, +// g.DisassembleGroup(Race_0.Name, 3, 12), +// e.GenericErrorText(e.ErrBeakGroupNumberNotEnough)) + +// groupEmptyMass := g.ShipGroups[4].EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) +// // groupLoadCOL := g.ShipGroups[4].Load +// planetMAT := g.Map.Planet[1].Material +// planetCOL := g.Map.Planet[1].Colonists + +// assert.NoError(t, g.DisassembleGroup(Race_0.Name, 5, 0)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 4) +// assert.Equal(t, planetMAT+groupEmptyMass, g.Map.Planet[1].Material) +// assert.Equal(t, planetCOL, g.Map.Planet[1].Colonists) + +// groupEmptyMass = g.ShipGroups[3].EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) +// groupLoadMAT := g.ShipGroups[3].Load +// planetMAT = g.Map.Planet[1].Material +// assert.NoError(t, g.DisassembleGroup(Race_0.Name, 4, 0)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) +// assert.Equal(t, planetMAT+groupEmptyMass+groupLoadMAT, g.Map.Planet[1].Material) + +// groupEmptyMass = g.ShipGroups[2].EmptyMass(&g.Race[Race_0_idx].ShipTypes[Race_0_Freighter_idx]) +// planetMAT = g.Map.Planet[0].Material +// planetCOL = g.Map.Planet[0].Colonists +// planetPOP := 90.0 +// g.Map.Planet[0].Population = planetPOP +// var shipsDisassembling uint = 3 +// groupEmptyMass = groupEmptyMass / float64(g.ShipGroups[2].Number) * float64(shipsDisassembling) +// newGroupUnloadedCOL := g.ShipGroups[2].Load / float64(g.ShipGroups[2].Number) * float64(shipsDisassembling) +// expectPOPIncrease := newGroupUnloadedCOL * 8 +// freePOPLeft := g.Map.Planet[0].Size - g.Map.Planet[0].Population +// expectAddedCOL := (expectPOPIncrease - freePOPLeft) / 8 +// expectAddedPOP := freePOPLeft +// assert.NoError(t, g.DisassembleGroup(Race_0.Name, 3, shipsDisassembling)) +// assert.Len(t, slices.Collect(g.ListShipGroups(Race_0_idx)), 3) +// assert.Equal(t, planetCOL+expectAddedCOL, g.Map.Planet[0].Colonists) +// assert.Equal(t, planetPOP+expectAddedPOP, g.Map.Planet[0].Population) +// assert.Equal(t, planetMAT+groupEmptyMass, g.Map.Planet[0].Material) +// assert.Equal(t, uint(7), g.ShipGroups[2].Number) +// assert.Equal(t, 56.0, g.ShipGroups[2].Load) +// } diff --git a/internal/model/game/group_upgrade.go b/internal/model/game/group_upgrade.go index ee63d65..83b7fb4 100644 --- a/internal/model/game/group_upgrade.go +++ b/internal/model/game/group_upgrade.go @@ -4,9 +4,6 @@ import ( "maps" "math" "slices" - - "github.com/google/uuid" - e "github.com/iliadenisov/galaxy/internal/error" ) type UpgradeCalc struct { @@ -49,157 +46,157 @@ func GroupUpgradeCost(sg ShipGroup, st ShipType, drive, weapons, shields, 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) 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) - } +// 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] +// 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) - } +// 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() - } +// 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, - } +// 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) - } +// 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(), - } +// 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) - } +// 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] - } +// 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) - } +// 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 - } +// shipsToUpgrade := g.ShipGroups[sgi].Number +// // НЕ БОЛЕЕ УКАЗАННОГО +// if limitShips > 0 && shipsToUpgrade > limitShips { +// shipsToUpgrade = limitShips +// } - maxUpgradableShips := uc.UpgradeMaxShips(productionCapacity) +// maxUpgradableShips := uc.UpgradeMaxShips(productionCapacity) - /* - 1. считаем стоимость модернизации одного корабля - 2. считаем сколько кораблей можно модернизировать - 3. если не хватает даже на 1 корабль, ограничиваемся одним кораблём и пересчитываем коэффициент пропорционально массе блоков - 4. иначе, считаем истинное количество кораблей с учётом ограничения maxShips - */ - blockMassSum := st.EmptyMass() +// /* +// 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 - } +// 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) - } +// // 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 - } +// // 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]) - } - } +// // 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 -} +// return nil +// } func CurrentUpgradingLevel(sg ShipGroup, tech Tech) float64 { if sg.StateUpgrade == nil { diff --git a/internal/model/game/group_upgrade_test.go b/internal/model/game/group_upgrade_test.go index 57a3f21..3737cb6 100644 --- a/internal/model/game/group_upgrade_test.go +++ b/internal/model/game/group_upgrade_test.go @@ -1,10 +1,8 @@ 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" ) @@ -111,59 +109,59 @@ func TestUpgradeGroupPreference(t *testing.T) { 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 +// 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)) +// 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)) +// 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.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)) -} +// assert.ErrorContains(t, +// g.UpgradeGroup(Race_0.Name, 4, "DRIVE", 1, 1.3), +// e.GenericErrorText(e.ErrInputUpgradeGroupBreakNotAllowed)) +// } diff --git a/internal/model/game/race.go b/internal/model/game/race.go index 5e85355..4e5fca4 100644 --- a/internal/model/game/race.go +++ b/internal/model/game/race.go @@ -1,10 +1,10 @@ package game import ( - "slices" + // "slices" "github.com/google/uuid" - e "github.com/iliadenisov/galaxy/internal/error" + // e "github.com/iliadenisov/galaxy/internal/error" ) type Race struct { @@ -38,7 +38,7 @@ func (r Race) TechLevel(t Tech) float64 { return r.Tech.Value(t) } -// TODO: refactor to separate method with *Race as parameter +// TODO: remove func, move to Cache func (r *Race) SetTechLevel(t Tech, v float64) { r.Tech = r.Tech.Set(t, v) } @@ -51,74 +51,74 @@ func (r Race) VisibilityDistance() float64 { return r.TechLevel(TechDrive) * 30 } -func (g Game) Relation(hostRace, opponentRace string) (RaceRelation, error) { - ri, err := g.raceIndex(hostRace) - if err != nil { - return RaceRelation{}, err - } - other, err := g.raceIndex(opponentRace) - if err != nil { - return RaceRelation{}, err - } - return g.relationInternal(ri, other) -} +// func (g Game) Relation(hostRace, opponentRace string) (RaceRelation, error) { +// ri, err := g.raceIndex(hostRace) +// if err != nil { +// return RaceRelation{}, err +// } +// other, err := g.raceIndex(opponentRace) +// if err != nil { +// return RaceRelation{}, err +// } +// return g.relationInternal(ri, other) +// } -func (g Game) UpdateRelation(race, opponent string, rel Relation) error { - ri, err := g.raceIndex(race) - if err != nil { - return err - } - var other int - if race == opponent { - other = ri - } else if other, err = g.raceIndex(opponent); err != nil { - return err - } - if err != nil { - return err - } - return g.updateRelationInternal(ri, other, rel) -} +// func (g Game) UpdateRelation(race, opponent string, rel Relation) error { +// ri, err := g.raceIndex(race) +// if err != nil { +// return err +// } +// var other int +// if race == opponent { +// other = ri +// } else if other, err = g.raceIndex(opponent); err != nil { +// return err +// } +// if err != nil { +// return err +// } +// return g.updateRelationInternal(ri, other, rel) +// } -func (g *Game) GiveVotes(race, recipient string) error { - ri, err := g.raceIndex(race) - if err != nil { - return err - } - rec, err := g.raceIndex(recipient) - if err != nil { - return err - } - g.Race[ri].Vote = g.Race[rec].ID - return nil -} +// func (g *Game) GiveVotes(race, recipient string) error { +// ri, err := g.raceIndex(race) +// if err != nil { +// return err +// } +// rec, err := g.raceIndex(recipient) +// if err != nil { +// return err +// } +// g.Race[ri].Vote = g.Race[rec].ID +// return nil +// } -func (g Game) relationInternal(ri, other int) (RaceRelation, error) { - if ri == other { - return RaceRelation{ - RaceID: g.Race[ri].ID, - Relation: RelationPeace, - }, nil - } - rel := slices.IndexFunc(g.Race[ri].Relations, func(r RaceRelation) bool { return r.RaceID == g.Race[other].ID }) - if rel < 0 { - return RaceRelation{}, e.NewGameStateError("Relation: opponent not found") - } - return g.Race[ri].Relations[rel], nil -} +// func (g Game) relationInternal(ri, other int) (RaceRelation, error) { +// if ri == other { +// return RaceRelation{ +// RaceID: g.Race[ri].ID, +// Relation: RelationPeace, +// }, nil +// } +// rel := slices.IndexFunc(g.Race[ri].Relations, func(r RaceRelation) bool { return r.RaceID == g.Race[other].ID }) +// if rel < 0 { +// return RaceRelation{}, e.NewGameStateError("Relation: opponent not found") +// } +// return g.Race[ri].Relations[rel], nil +// } -func (g Game) updateRelationInternal(ri, other int, rel Relation) error { - for o := range g.Race[ri].Relations { - switch { - case ri == other: - g.Race[ri].Relations[o].Relation = rel - case g.Race[ri].Relations[o].RaceID == g.Race[other].ID: - g.Race[ri].Relations[o].Relation = rel - return nil - } - } - if ri != other { - return e.NewGameStateError("UpdateRelation: opponent not found") - } - return nil -} +// func (g Game) updateRelationInternal(ri, other int, rel Relation) error { +// for o := range g.Race[ri].Relations { +// switch { +// case ri == other: +// g.Race[ri].Relations[o].Relation = rel +// case g.Race[ri].Relations[o].RaceID == g.Race[other].ID: +// g.Race[ri].Relations[o].Relation = rel +// return nil +// } +// } +// if ri != other { +// return e.NewGameStateError("UpdateRelation: opponent not found") +// } +// return nil +// } diff --git a/internal/model/game/race_test.go b/internal/model/game/race_test.go index 05559ea..5a2cdf6 100644 --- a/internal/model/game/race_test.go +++ b/internal/model/game/race_test.go @@ -1,22 +1 @@ package game_test - -import ( - "testing" - - e "github.com/iliadenisov/galaxy/internal/error" - "github.com/stretchr/testify/assert" -) - -func TestGiveVotes(t *testing.T) { - g := newGame() - - assert.Equal(t, g.Race[Race_0_idx].ID, g.Race[Race_0_idx].Vote) - assert.Equal(t, g.Race[Race_1_idx].ID, g.Race[Race_1_idx].Vote) - - assert.NoError(t, g.GiveVotes(Race_0.Name, Race_1.Name)) - assert.Equal(t, g.Race[Race_1_idx].ID, g.Race[Race_0_idx].Vote) - assert.Equal(t, g.Race[Race_1_idx].ID, g.Race[Race_1_idx].Vote) - - assert.ErrorContains(t, g.GiveVotes("UnknownRace", Race_1.Name), e.GenericErrorText(e.ErrInputUnknownRace)) - assert.ErrorContains(t, g.GiveVotes(Race_0.Name, "UnknownRace"), e.GenericErrorText(e.ErrInputUnknownRace)) -} diff --git a/internal/model/game/ship.go b/internal/model/game/ship.go index 0090ff4..bd9b76b 100644 --- a/internal/model/game/ship.go +++ b/internal/model/game/ship.go @@ -102,116 +102,116 @@ func (g Game) shipTypesInternal(ri int) []ShipType { return g.Race[ri].ShipTypes } -func (g Game) DeleteShipType(raceName, typeName string) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - return g.deleteShipTypeInternal(ri, typeName) -} +// func (g Game) DeleteShipType(raceName, typeName string) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// return g.deleteShipTypeInternal(ri, typeName) +// } -func (g Game) deleteShipTypeInternal(ri int, name string) error { - st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }) - if st < 0 { - return e.NewEntityNotExistsError("ship type %w", name) - } - if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { - return p.Production.Type == ProductionShip && - p.Production.SubjectID != nil && - g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID - }); pl >= 0 { - return e.NewDeleteShipTypePlanetProductionError(g.Map.Planet[pl].Name) - } - for sg := range g.listShipGroups(ri) { - if sg.TypeID == g.Race[ri].ShipTypes[st].ID { - return e.NewDeleteShipTypeExistingGroupError("group: %v", sg.Index) - } - } - g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...) - return nil -} +// func (g Game) deleteShipTypeInternal(ri int, name string) error { +// st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }) +// if st < 0 { +// return e.NewEntityNotExistsError("ship type %w", name) +// } +// if pl := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { +// return p.Production.Type == ProductionShip && +// p.Production.SubjectID != nil && +// g.Race[ri].ShipTypes[st].ID == *p.Production.SubjectID +// }); pl >= 0 { +// return e.NewDeleteShipTypePlanetProductionError(g.Map.Planet[pl].Name) +// } +// for sg := range g.listShipGroups(ri) { +// if sg.TypeID == g.Race[ri].ShipTypes[st].ID { +// return e.NewDeleteShipTypeExistingGroupError("group: %v", sg.Index) +// } +// } +// g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...) +// return nil +// } // TODO: D A W S C -func (g Game) CreateShipType(raceName, typeName string, d, w, s, c float64, a int) error { - ri, err := g.raceIndex(raceName) - if err != nil { - return err - } - _, err = g.createShipTypeInternal(ri, typeName, d, w, s, c, a) - return err -} +// func (g Game) CreateShipType(raceName, typeName string, d, w, s, c float64, a int) error { +// ri, err := g.raceIndex(raceName) +// if err != nil { +// return err +// } +// _, err = g.createShipTypeInternal(ri, typeName, d, w, s, c, a) +// return err +// } -func (g Game) createShipTypeInternal(ri int, name string, d, w, s, c float64, a int) (int, error) { - if err := checkShipTypeValues(d, w, s, c, a); err != nil { - return -1, err - } - n, ok := validateTypeName(name) - if !ok { - return -1, e.NewEntityTypeNameValidationError("%q", n) - } - if st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }); st >= 0 { - return -1, e.NewEntityTypeNameDuplicateError("ship type %w", g.Race[ri].ShipTypes[st].Name) - } - g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes, ShipType{ - ID: uuid.New(), - ShipTypeReport: ShipTypeReport{ - Name: n, - Drive: d, - Weapons: w, - Shields: s, - Cargo: c, - Armament: uint(a), - }, - }) - return len(g.Race[ri].ShipTypes) - 1, nil -} +// func (g Game) createShipTypeInternal(ri int, name string, d, w, s, c float64, a int) (int, error) { +// if err := checkShipTypeValues(d, w, s, c, a); err != nil { +// return -1, err +// } +// n, ok := validateTypeName(name) +// if !ok { +// return -1, e.NewEntityTypeNameValidationError("%q", n) +// } +// if st := slices.IndexFunc(g.Race[ri].ShipTypes, func(st ShipType) bool { return st.Name == name }); st >= 0 { +// return -1, e.NewEntityTypeNameDuplicateError("ship type %w", g.Race[ri].ShipTypes[st].Name) +// } +// g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes, ShipType{ +// ID: uuid.New(), +// ShipTypeReport: ShipTypeReport{ +// Name: n, +// Drive: d, +// Weapons: w, +// Shields: s, +// Cargo: c, +// Armament: uint(a), +// }, +// }) +// return len(g.Race[ri].ShipTypes) - 1, 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) 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() - } +// 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.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 - } - } +// // 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.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 +// } +// } - // switch ship groups to the new type - for sg := range g.ShipGroups { - if g.ShipGroups[sg].OwnerID == g.Race[ri].ID && g.ShipGroups[sg].TypeID == g.Race[ri].ShipTypes[st].ID { - g.ShipGroups[sg].TypeID = g.Race[ri].ShipTypes[tt].ID - } - } +// // switch ship groups to the new type +// for sg := range g.ShipGroups { +// if g.ShipGroups[sg].OwnerID == g.Race[ri].ID && g.ShipGroups[sg].TypeID == g.Race[ri].ShipTypes[st].ID { +// g.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:]...) +// // remove the source type +// g.Race[ri].ShipTypes = append(g.Race[ri].ShipTypes[:st], g.Race[ri].ShipTypes[st+1:]...) - return nil -} +// return nil +// } func checkShipTypeValues(d, w, s, c float64, a int) error { if !checkShipTypeValueDWSC(d) {