From 0b8c53cedf0c844024547a41c14c6f9affe549cf Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Tue, 6 Jan 2026 07:16:27 +0200 Subject: [PATCH] cmd: add/remove route --- internal/model/game/game_export_test.go | 8 ++ internal/model/game/group_send.go | 2 +- internal/model/game/planet.go | 3 +- internal/model/game/route.go | 116 ++++++++++++++++++++++++ internal/model/game/route_test.go | 91 +++++++++++++++++++ 5 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 internal/model/game/route.go create mode 100644 internal/model/game/route_test.go diff --git a/internal/model/game/game_export_test.go b/internal/model/game/game_export_test.go index 8bb5e5d..2b6968a 100644 --- a/internal/model/game/game_export_test.go +++ b/internal/model/game/game_export_test.go @@ -13,3 +13,11 @@ func (g Game) ListShipGroups(ri int) iter.Seq[ShipGroup] { func (g Game) ListFleets(ri int) iter.Seq[Fleet] { return g.listFleets(ri) } + +func (g Game) MustPlanetByNumber(num uint) Planet { + p, err := g.PlanetByNumber(num) + if err != nil { + panic(err) + } + return p +} diff --git a/internal/model/game/group_send.go b/internal/model/game/group_send.go index 532da0e..0850e43 100644 --- a/internal/model/game/group_send.go +++ b/internal/model/game/group_send.go @@ -48,7 +48,7 @@ func (g *Game) sendGroupInternal(ri int, groupIndex, planetNumber, quantity uint p1, ok := PlanetByNum(g, sourcePlanet) if !ok { - return e.NewGameStateError("source planet #%d does not exists", g.ShipGroups[sgi].Destination) + return e.NewGameStateError("source planet #%d does not exists", sourcePlanet) } p2, ok := PlanetByNum(g, planetNumber) if !ok { diff --git a/internal/model/game/planet.go b/internal/model/game/planet.go index 3ba1f8c..e910dd8 100644 --- a/internal/model/game/planet.go +++ b/internal/model/game/planet.go @@ -33,7 +33,8 @@ type PlanetReport struct { } type Planet struct { - Owner uuid.UUID `json:"owner"` // FIXME: nil value when no owner + Owner uuid.UUID `json:"owner"` // FIXME: nil value when no owner + Route map[RouteType]uint `json:"route"` PlanetReport } diff --git a/internal/model/game/route.go b/internal/model/game/route.go new file mode 100644 index 0000000..ca53950 --- /dev/null +++ b/internal/model/game/route.go @@ -0,0 +1,116 @@ +package game + +import ( + "fmt" + "slices" + + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/util" +) + +type RouteType string + +const ( + RouteMaterial RouteType = "MAT" // Сырьё + RouteCapital RouteType = "CAP" // Промышленность + RouteColonist RouteType = "COL" // Колонисты + RouteEmpty RouteType = "EMP" // Пустые корабли +) + +var ( + routeTypeSet map[string]RouteType = map[string]RouteType{ + RouteMaterial.String(): RouteMaterial, + RouteCapital.String(): RouteCapital, + RouteColonist.String(): RouteColonist, + RouteEmpty.String(): RouteEmpty, + } +) + +func (rt RouteType) Ref() *RouteType { + return &rt +} + +func (rt RouteType) String() string { + return string(rt) +} + +func (g *Game) SetRoute(raceName, loadType string, origin, destination uint) error { + ri, err := g.raceIndex(raceName) + if err != nil { + return err + } + rt, ok := routeTypeSet[loadType] + if !ok { + return e.NewCargoTypeInvalidError(loadType) + } + return g.setRouteInternal(ri, rt, origin, destination) +} + +func (g *Game) RemoveRoute(raceName, loadType string, origin uint) error { + ri, err := g.raceIndex(raceName) + if err != nil { + return err + } + rt, ok := routeTypeSet[loadType] + if !ok { + return e.NewCargoTypeInvalidError(loadType) + } + return g.removeRouteInternal(ri, rt, origin) +} + +func (g *Game) setRouteInternal(ri int, rt RouteType, origin, destination uint) error { + p1, ok := PlanetByNum(g, origin) + if !ok { + return e.NewEntityNotExistsError("origin planet #%d", origin) + } + if p1.Owner != g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet #%d", origin) + } + p2, ok := PlanetByNum(g, destination) + if !ok { + return e.NewEntityNotExistsError("destination planet #%d", destination) + } + 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) + } + + SetPlanetRoute(g, rt, origin, destination) + + return nil +} + +func (g *Game) removeRouteInternal(ri int, rt RouteType, origin uint) error { + p1, ok := PlanetByNum(g, origin) + if !ok { + return e.NewEntityNotExistsError("origin planet #%d", origin) + } + if p1.Owner != g.Race[ri].ID { + return e.NewEntityNotOwnedError("planet #%d", origin) + } + + RemovePlanetRoute(g, rt, origin) + + return nil +} + +func SetPlanetRoute(g *Game, rt RouteType, origin, destination uint) { + pi := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == origin }) + if pi < 0 { + panic(fmt.Sprintf("SetPlanetRoute: origin planet #%d not found", origin)) + } + if g.Map.Planet[pi].Route == nil { + g.Map.Planet[pi].Route = make(map[RouteType]uint) + } + g.Map.Planet[pi].Route[rt] = destination +} + +func RemovePlanetRoute(g *Game, rt RouteType, origin uint) { + pi := slices.IndexFunc(g.Map.Planet, func(p Planet) bool { return p.Number == origin }) + if pi < 0 { + panic(fmt.Sprintf("RemovePlanetRoute: origin planet #%d not found", origin)) + } + if g.Map.Planet[pi].Route != nil { + delete(g.Map.Planet[pi].Route, rt) + } +} diff --git a/internal/model/game/route_test.go b/internal/model/game/route_test.go new file mode 100644 index 0000000..9a2d8b0 --- /dev/null +++ b/internal/model/game/route_test.go @@ -0,0 +1,91 @@ +package game_test + +import ( + "testing" + + e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" + + "github.com/stretchr/testify/assert" +) + +func TestSetRoute(t *testing.T) { + g := newGame() + + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteMaterial) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteEmpty) + + assert.NoError(t, g.SetRoute(Race_0.Name, "COL", 0, 2)) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteMaterial) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteEmpty) + + assert.NoError(t, g.SetRoute(Race_0.Name, "MAT", 0, 2)) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteMaterial) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteEmpty) + + assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", 0, 2)) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteMaterial) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteEmpty) + + assert.NoError(t, g.SetRoute(Race_0.Name, "EMP", 0, 2)) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteMaterial) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteEmpty) + + assert.ErrorContains(t, + g.SetRoute("UnknownRace", "COL", 0, 2), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.SetRoute(Race_0.Name, "IND", 0, 2), + e.GenericErrorText(e.ErrInputCargoTypeInvalid)) + assert.ErrorContains(t, + g.SetRoute(Race_0.Name, "COL", 500, 2), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.SetRoute(Race_0.Name, "COL", 1, 2), + e.GenericErrorText(e.ErrInputEntityNotOwned)) + assert.ErrorContains(t, + g.SetRoute(Race_0.Name, "COL", 0, 3), + e.GenericErrorText(e.ErrSendUnreachableDestination)) +} + +func TestRemoveRoute(t *testing.T) { + g := newGame() + + assert.NoError(t, g.SetRoute(Race_0.Name, "COL", 0, 2)) + assert.NoError(t, g.SetRoute(Race_0.Name, "CAP", 0, 2)) + assert.NoError(t, g.SetRoute(Race_0.Name, "EMP", 2, 0)) + + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + assert.Contains(t, g.MustPlanetByNumber(2).Route, game.RouteEmpty) + + assert.NoError(t, g.RemoveRoute(Race_0.Name, "COL", 0)) + assert.NotContains(t, g.MustPlanetByNumber(0).Route, game.RouteColonist) + assert.Contains(t, g.MustPlanetByNumber(0).Route, game.RouteCapital) + + assert.NoError(t, g.RemoveRoute(Race_0.Name, "EMP", 2)) + assert.NotContains(t, g.MustPlanetByNumber(2).Route, game.RouteEmpty) + + assert.ErrorContains(t, + g.RemoveRoute("UnknownRace", "COL", 0), + e.GenericErrorText(e.ErrInputUnknownRace)) + assert.ErrorContains(t, + g.RemoveRoute(Race_0.Name, "IND", 0), + e.GenericErrorText(e.ErrInputCargoTypeInvalid)) + assert.ErrorContains(t, + g.RemoveRoute(Race_0.Name, "COL", 500), + e.GenericErrorText(e.ErrInputEntityNotExists)) + assert.ErrorContains(t, + g.RemoveRoute(Race_0.Name, "COL", 1), + e.GenericErrorText(e.ErrInputEntityNotOwned)) +}