From 87291d2760b15c6755a2a896a5926eded33ebcf5 Mon Sep 17 00:00:00 2001 From: IliaDenisov Date: Thu, 12 Feb 2026 14:27:56 +0300 Subject: [PATCH] feat: turn generate api --- internal/controller/cache.go | 4 ++ internal/controller/controller.go | 52 +++++++++++++----- internal/controller/controller_test.go | 20 ++++--- internal/controller/generate_game.go | 1 + internal/controller/generate_game_test.go | 10 +++- internal/controller/generate_turn.go | 10 ++-- internal/model/rest/init.go | 6 -- internal/router/handler/handler.go | 27 +++++++-- internal/router/handler/init.go | 6 +- internal/router/handler/turn.go | 17 ++++++ internal/router/init_test.go | 5 +- internal/router/router.go | 1 + internal/router/router_export_test.go | 1 + internal/router/router_helper_test.go | 10 ++-- internal/router/status_test.go | 6 +- internal/router/turn_test.go | 67 +++++++++++++++++++++++ 16 files changed, 192 insertions(+), 51 deletions(-) create mode 100644 internal/router/handler/turn.go create mode 100644 internal/router/turn_test.go diff --git a/internal/controller/cache.go b/internal/controller/cache.go index 1f7defb..60d9e4c 100644 --- a/internal/controller/cache.go +++ b/internal/controller/cache.go @@ -28,6 +28,10 @@ func NewCache(g *game.Game) *Cache { return c } +func (c *Cache) StageCommand() { + c.g.Stage++ +} + func (c Cache) Stage() uint { return c.g.Stage } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 70f1a21..7447c42 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -70,19 +70,31 @@ type Ctrl interface { PlanetRouteRemove(actor, loadType string, origin uint) error } -func GenerateGame(configure func(*Param), races []string) (ID uuid.UUID, err error) { +func GenerateGame(configure func(*Param), races []string) (s game.State, err error) { ec, err := NewRepoController(configure) if err != nil { - return uuid.Nil, err + return game.State{}, err } if err = ec.Repo.Lock(); err != nil { return } defer func() { err = errors.Join(err, ec.Repo.Release()) + if err == nil { + s, err = GameState(configure) + } }() - ID, err = NewGame(ec.Repo, races) + _, err = NewGame(ec.Repo, races) + return +} + +func GenerateTurn(configure func(*Param)) (err error) { + ec, err := NewRepoController(configure) + if err != nil { + return err + } + err = ec.ExecuteLocked(func(c *Controller) error { return c.MakeTurn() }) return } @@ -96,6 +108,9 @@ func ExecuteCommand(configure func(*Param), consumer func(c Ctrl) error) (err er func GameState(configure func(*Param)) (s game.State, err error) { ec, err := NewRepoController(configure) + if err != nil { + return game.State{}, err + } g, err := ec.Repo.LoadStateSafe() if err != nil { @@ -123,7 +138,7 @@ type RepoController struct { Repo Repo } -func (ec *RepoController) ExecuteCommand(consumer func(c *Controller) error) (err error) { +func (ec *RepoController) ExecuteLocked(consumer func(*Controller) error) (err error) { if err := ec.Repo.Lock(); err != nil { return err } @@ -136,15 +151,21 @@ func (ec *RepoController) ExecuteCommand(consumer func(c *Controller) error) (er return err } - err = consumer(NewGameController(g)) - - if err == nil { - g.Stage += 1 - ec.Repo.SaveLastState(g) - } + err = consumer(ec.NewGameController(g)) return } +func (ec *RepoController) ExecuteCommand(consumer func(*Controller) error) (err error) { + return ec.ExecuteLocked(func(c *Controller) error { + err = consumer(c) + if err == nil { + c.Cache.StageCommand() + err = c.saveState() + } + return err + }) +} + func NewRepoController(config Configurer) (*RepoController, error) { c := &Param{ StoragePath: ".", @@ -161,14 +182,19 @@ func NewRepoController(config Configurer) (*RepoController, error) { }, nil } -func NewGameController(g *game.Game) *Controller { +func (ec *RepoController) NewGameController(g *game.Game) *Controller { return &Controller{ - Cache: NewCache(g), + RepoController: ec, + Cache: NewCache(g), } } +func (c *Controller) saveState() error { + return c.Repo.SaveLastState(c.Cache.g) +} + type Controller struct { - Repo Repo + *RepoController Cache *Cache } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 53de96d..2aca071 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -129,17 +129,19 @@ func newGame() *game.Game { } func newCache() (*controller.Cache, *controller.Controller) { - ctl := controller.NewGameController(newGame()) - c := ctl.Cache - assertNoError(c.ShipClassCreate(Race_0_idx, Race_0_Gunship, 60, 3, 30, 100, 0)) - assertNoError(c.ShipClassCreate(Race_0_idx, Race_0_Freighter, 8, 0, 0, 2, 10)) - assertNoError(c.ShipClassCreate(Race_0_idx, ShipType_Cruiser, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F())) + ctl := &controller.Controller{ + RepoController: nil, + Cache: controller.NewCache(newGame()), + } + assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, Race_0_Gunship, 60, 3, 30, 100, 0)) + assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, Race_0_Freighter, 8, 0, 0, 2, 10)) + assertNoError(ctl.Cache.ShipClassCreate(Race_0_idx, ShipType_Cruiser, Cruiser.Drive.F(), int(Cruiser.Armament), Cruiser.Weapons.F(), Cruiser.Shields.F(), Cruiser.Cargo.F())) - assertNoError(c.ShipClassCreate(Race_1_idx, Race_1_Gunship, 60, 3, 30, 100, 0)) - assertNoError(c.ShipClassCreate(Race_1_idx, Race_1_Freighter, 8, 0, 0, 2, 10)) - assertNoError(c.ShipClassCreate(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.) + assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, Race_1_Gunship, 60, 3, 30, 100, 0)) + assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, Race_1_Freighter, 8, 0, 0, 2, 10)) + assertNoError(ctl.Cache.ShipClassCreate(Race_1_idx, ShipType_Cruiser, 15, 2, 15, 15, 0)) // same name - different type (why.) - return c, ctl + return ctl.Cache, ctl } func floatRef(v float64) *game.Float { diff --git a/internal/controller/generate_game.go b/internal/controller/generate_game.go index aed531f..eb99ed8 100644 --- a/internal/controller/generate_game.go +++ b/internal/controller/generate_game.go @@ -61,6 +61,7 @@ func buildGameOnMap(races []string, m generator.Map) (*game.Game, error) { ID: raceID, Name: races[i], VoteFor: raceID, + TTL: 10, Tech: game.NewTechSet(), } gameMap.Planet = append(gameMap.Planet, NewPlanet( diff --git a/internal/controller/generate_game_test.go b/internal/controller/generate_game_test.go index 2ec7a69..7a0e51d 100644 --- a/internal/controller/generate_game_test.go +++ b/internal/controller/generate_game_test.go @@ -3,6 +3,7 @@ package controller_test import ( "fmt" "path/filepath" + "strings" "testing" "github.com/google/uuid" @@ -41,6 +42,7 @@ func TestNewGame(t *testing.T) { for r := range g.Race { assert.NotEqual(t, uuid.Nil, g.Race[r].ID) assert.Equal(t, players-1, len(g.Race[r].Relations)) + assert.Equal(t, uint(10), g.Race[r].TTL) for i := range g.Race[r].Relations { assert.NotEqual(t, uuid.Nil, g.Race[r].Relations[i].RaceID) if g.Race[r].Relations[i].RaceID == g.Race[r].ID { @@ -52,7 +54,13 @@ func TestNewGame(t *testing.T) { numShuffled := false for i := range g.Map.Planet { - numShuffled = numShuffled || g.Map.Planet[i].Number != uint(i) + p := &g.Map.Planet[i] + if strings.HasPrefix(p.Name, "HW") || strings.HasPrefix(p.Name, "DW") { + assert.True(t, p.Owned()) + assert.NotNil(t, p.Owner) + assert.NotEqual(t, uuid.Nil, *p.Owner) + } + numShuffled = numShuffled || p.Number != uint(i) } assert.True(t, numShuffled) diff --git a/internal/controller/generate_turn.go b/internal/controller/generate_turn.go index 54761f4..eb4a195 100644 --- a/internal/controller/generate_turn.go +++ b/internal/controller/generate_turn.go @@ -9,7 +9,7 @@ import ( "github.com/iliadenisov/galaxy/internal/model/report" ) -func MakeTurn(c *Controller, r Repo) error { +func (c *Controller) MakeTurn() error { // Next turn c.Cache.g.Turn += 1 c.Cache.g.Stage = 0 @@ -61,7 +61,7 @@ func MakeTurn(c *Controller, r Repo) error { // Store bombings bombingReport := make([]*report.Bombing, len(bombings)) if len(bombings) > 0 { - if err := r.SaveBombings(c.Cache.g.Turn, bombings); err != nil { + if err := c.Repo.SaveBombings(c.Cache.g.Turn, bombings); err != nil { return err } for i := range bombings { @@ -101,7 +101,7 @@ func MakeTurn(c *Controller, r Repo) error { } report := TransformBattle(c.Cache, b) - if err := r.SaveBattle(c.Cache.g.Turn, report, &battleMeta[i]); err != nil { + if err := c.Repo.SaveBattle(c.Cache.g.Turn, report, &battleMeta[i]); err != nil { return err } battleReport[i] = report @@ -112,12 +112,12 @@ func MakeTurn(c *Controller, r Repo) error { c.Cache.DeleteKilledShipGroups() // Store game state for the new turn and 'current' state as well - if err := r.SaveNewTurn(c.Cache.g.Turn, c.Cache.g); err != nil { + if err := c.Repo.SaveNewTurn(c.Cache.g.Turn, c.Cache.g); err != nil { return err } for rep := range c.Cache.Report(c.Cache.g.Turn, battleReport, bombingReport) { - if err := r.SaveReport(c.Cache.g.Turn, rep); err != nil { + if err := c.Repo.SaveReport(c.Cache.g.Turn, rep); err != nil { return err } } diff --git a/internal/model/rest/init.go b/internal/model/rest/init.go index 3283fa7..3464553 100644 --- a/internal/model/rest/init.go +++ b/internal/model/rest/init.go @@ -1,7 +1,5 @@ package rest -import "github.com/google/uuid" - type Init struct { Races []Race `json:"races" binding:"required,gte=10"` } @@ -9,7 +7,3 @@ type Init struct { type Race struct { Name string `json:"name" binding:"required,notblank"` } - -type InitResponse struct { - UUID uuid.UUID `json:"uuid"` -} diff --git a/internal/router/handler/handler.go b/internal/router/handler/handler.go index b29a84d..e9edd8d 100644 --- a/internal/router/handler/handler.go +++ b/internal/router/handler/handler.go @@ -7,14 +7,15 @@ import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" - "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/controller" e "github.com/iliadenisov/galaxy/internal/error" + "github.com/iliadenisov/galaxy/internal/model/game" "github.com/iliadenisov/galaxy/internal/model/rest" ) type CommandExecutor interface { - GenerateGame([]string) (uuid.UUID, error) + GenerateGame([]string) (rest.StateResponse, error) + GenerateTurn() (rest.StateResponse, error) GameState() (rest.StateResponse, error) Execute(cmd ...Command) error } @@ -50,8 +51,20 @@ func (e *executor) Execute(command ...Command) error { }) } -func (e *executor) GenerateGame(races []string) (uuid.UUID, error) { - return controller.GenerateGame(e.cfg, races) +func (e *executor) GenerateGame(races []string) (rest.StateResponse, error) { + s, err := controller.GenerateGame(e.cfg, races) + if err != nil { + return rest.StateResponse{}, err + } + return stateResponse(s), nil +} + +func (e *executor) GenerateTurn() (rest.StateResponse, error) { + err := controller.GenerateTurn(e.cfg) + if err != nil { + return rest.StateResponse{}, err + } + return e.GameState() } func (e *executor) GameState() (rest.StateResponse, error) { @@ -59,6 +72,10 @@ func (e *executor) GameState() (rest.StateResponse, error) { if err != nil { return rest.StateResponse{}, err } + return stateResponse(s), nil +} + +func stateResponse(s game.State) rest.StateResponse { result := &rest.StateResponse{ ID: s.ID, Turn: s.Turn, @@ -70,7 +87,7 @@ func (e *executor) GameState() (rest.StateResponse, error) { result.Players[i].Name = s.Players[i].Name result.Players[i].Extinct = s.Players[i].Extinct } - return *result, nil + return *result } func errorResponded(c *gin.Context, err error) bool { diff --git a/internal/router/handler/init.go b/internal/router/handler/init.go index 1b46633..4e8d41c 100644 --- a/internal/router/handler/init.go +++ b/internal/router/handler/init.go @@ -18,12 +18,10 @@ func InitHandler(c *gin.Context, executor CommandExecutor) { races[i] = init.Races[i].Name } - uuid, err := executor.GenerateGame(races) + s, err := executor.GenerateGame(races) if errorResponded(c, err) { return } - c.JSON(http.StatusCreated, rest.InitResponse{ - UUID: uuid, - }) + c.JSON(http.StatusCreated, s) } diff --git a/internal/router/handler/turn.go b/internal/router/handler/turn.go new file mode 100644 index 0000000..8af1834 --- /dev/null +++ b/internal/router/handler/turn.go @@ -0,0 +1,17 @@ +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func TurnHandler(c *gin.Context, executor CommandExecutor) { + state, err := executor.GenerateTurn() + + if errorResponded(c, err) { + return + } + + c.JSON(http.StatusOK, state) +} diff --git a/internal/router/init_test.go b/internal/router/init_test.go index f37a18f..e577cb5 100644 --- a/internal/router/init_test.go +++ b/internal/router/init_test.go @@ -28,9 +28,10 @@ func TestInit(t *testing.T) { r.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code, w.Body) - var initResponse rest.InitResponse + var initResponse rest.StateResponse assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse)) - assert.NoError(t, uuid.Validate(initResponse.UUID.String())) + assert.NoError(t, uuid.Validate(initResponse.ID.String())) + assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String())) } func TestInitValidators(t *testing.T) { diff --git a/internal/router/router.go b/internal/router/router.go index 8eeefc2..f3fdeb3 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -67,6 +67,7 @@ func setupRouter(executor handler.CommandExecutor) *gin.Engine { groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) }) groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) }) groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) }) + groupV1.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) }) return r } diff --git a/internal/router/router_export_test.go b/internal/router/router_export_test.go index bc4ede7..883fea2 100644 --- a/internal/router/router_export_test.go +++ b/internal/router/router_export_test.go @@ -6,5 +6,6 @@ import ( ) func SetupRouter(e handler.CommandExecutor) *gin.Engine { + gin.SetMode(gin.TestMode) return setupRouter(e) } diff --git a/internal/router/router_helper_test.go b/internal/router/router_helper_test.go index 797743e..4cb8d18 100644 --- a/internal/router/router_helper_test.go +++ b/internal/router/router_helper_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "github.com/gin-gonic/gin" - "github.com/google/uuid" "github.com/iliadenisov/galaxy/internal/model/rest" "github.com/iliadenisov/galaxy/internal/router" "github.com/iliadenisov/galaxy/internal/router/handler" @@ -19,8 +18,12 @@ func (e *dummyExecutor) Execute(command ...handler.Command) error { return nil } -func (e *dummyExecutor) GenerateGame(races []string) (uuid.UUID, error) { - return uuid.New(), nil +func (e *dummyExecutor) GenerateGame(races []string) (rest.StateResponse, error) { + return rest.StateResponse{}, nil +} + +func (e *dummyExecutor) GenerateTurn() (rest.StateResponse, error) { + return rest.StateResponse{}, nil } func (e *dummyExecutor) GameState() (rest.StateResponse, error) { @@ -32,7 +35,6 @@ func setupRouter() *gin.Engine { } func setupRouterExecutor(e handler.CommandExecutor) *gin.Engine { - gin.SetMode(gin.TestMode) return router.SetupRouter(e) } diff --git a/internal/router/status_test.go b/internal/router/status_test.go index 3104143..19fb7e3 100644 --- a/internal/router/status_test.go +++ b/internal/router/status_test.go @@ -28,9 +28,10 @@ func TestGetStatus(t *testing.T) { r.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code, w.Body) - var initResponse rest.InitResponse + var initResponse rest.StateResponse assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse)) - assert.NoError(t, uuid.Validate(initResponse.UUID.String())) + assert.NoError(t, uuid.Validate(initResponse.ID.String())) + assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String())) w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/api/v1/status", nil) @@ -40,6 +41,7 @@ func TestGetStatus(t *testing.T) { var stateResponse rest.StateResponse assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &stateResponse)) assert.NoError(t, uuid.Validate(stateResponse.ID.String())) + assert.Equal(t, initResponse.ID, stateResponse.ID) assert.Equal(t, uint(0), stateResponse.Turn) assert.Equal(t, uint(0), stateResponse.Stage) assert.Len(t, stateResponse.Players, 10) diff --git a/internal/router/turn_test.go b/internal/router/turn_test.go new file mode 100644 index 0000000..b0a754b --- /dev/null +++ b/internal/router/turn_test.go @@ -0,0 +1,67 @@ +package router_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + "github.com/iliadenisov/galaxy/internal/controller" + "github.com/iliadenisov/galaxy/internal/model/rest" + "github.com/iliadenisov/galaxy/internal/router" + "github.com/iliadenisov/galaxy/internal/router/handler" + "github.com/iliadenisov/galaxy/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestGetTurn(t *testing.T) { + root, cleanup := util.CreateWorkDir(t) + defer cleanup() + + r := router.SetupRouter(handler.NewDefaultConfigExecutor(func(p *controller.Param) { p.StoragePath = root })) + + // create game + + payload := generateInitRequest(10) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload)) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code, w.Body) + var initResponse rest.StateResponse + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &initResponse)) + assert.NoError(t, uuid.Validate(initResponse.ID.String())) + assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String())) + assert.Equal(t, uint(0), initResponse.Turn) + assert.Equal(t, uint(0), initResponse.Stage) + + // generate next turn + + w = httptest.NewRecorder() + req, _ = http.NewRequest("PUT", "/api/v1/turn", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, w.Body) + + var turnResponse rest.StateResponse + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &turnResponse)) + assert.NoError(t, uuid.Validate(turnResponse.ID.String())) + assert.Equal(t, initResponse.ID, turnResponse.ID) + assert.Equal(t, uint(1), turnResponse.Turn) + assert.Equal(t, uint(0), turnResponse.Stage) + + // validate status + + w = httptest.NewRecorder() + req, _ = http.NewRequest("GET", "/api/v1/status", nil) + r.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, w.Body) + var stateResponse rest.StateResponse + assert.NoError(t, json.Unmarshal(w.Body.Bytes(), &stateResponse)) + assert.Equal(t, initResponse.ID, stateResponse.ID) + assert.Equal(t, uint(1), stateResponse.Turn) + assert.Equal(t, uint(0), stateResponse.Stage) +}