diff --git a/game/internal/controller/controller.go b/game/internal/controller/controller.go index 3bd57d9..542ba59 100644 --- a/game/internal/controller/controller.go +++ b/game/internal/controller/controller.go @@ -38,6 +38,10 @@ type Repo interface { // SaveBattle stores a new battle protocol and battle meta data for turn t SaveBattle(uint, *report.BattleReport, *game.BattleMeta) error + // LoadBattle reads battle's protocol for turn t and battle id. + // Returns false if battle with such id was never stored at turn t + LoadBattle(t uint, id uuid.UUID) (*report.BattleReport, bool, error) + // SaveBombing stores all prodused bombings for turn t SaveBombings(uint, []*game.Bombing) error @@ -143,6 +147,14 @@ func FetchOrder(configure func(*Param), actor string, turn uint) (order *order.U return ec.fetchOrder(actor, turn) } +func FetchBattle(configure func(*Param), turn uint, ID uuid.UUID) (b *report.BattleReport, exists bool, err error) { + ec, err := NewRepoController(configure) + if err != nil { + return nil, false, err + } + return ec.fetchBattle(turn, ID) +} + func BanishRace(configure func(*Param), actor string) error { ec, err := NewRepoController(configure) if err != nil { @@ -261,6 +273,14 @@ func (ec *RepoController) fetchOrder(actor string, turn uint) (order *order.User return } +func (ec *RepoController) fetchBattle(turn uint, ID uuid.UUID) (order *report.BattleReport, exists bool, err error) { + err = ec.executeSafe(func(t uint, c *Controller) error { + order, exists, err = ec.Repo.LoadBattle(turn, ID) + return err + }) + return +} + func (ec *RepoController) loadReport(actor string, turn uint) (r *report.Report, err error) { execErr := ec.executeSafe(func(t uint, c *Controller) (exErr error) { id, exErr := c.RaceID(actor) diff --git a/game/internal/repo/game.go b/game/internal/repo/game.go index a61811a..f466c3a 100644 --- a/game/internal/repo/game.go +++ b/game/internal/repo/game.go @@ -13,6 +13,7 @@ package repo import ( "encoding/json" "fmt" + "slices" "galaxy/model/order" "galaxy/model/report" @@ -117,9 +118,25 @@ func loadMeta(s Storage) (*game.GameMeta, error) { return result, nil } -func saveMeta(s Storage, t uint, gm *game.GameMeta) error { +func loadTurnMeta(s Storage, turn uint) (*game.GameMeta, error) { + var result *game.GameMeta = new(game.GameMeta) + path := fmt.Sprintf("%s/%s", TurnDir(turn), metaPath) + exist, err := s.Exists(path) + if err != nil { + return nil, NewStorageError(err) + } + if !exist { + return result, nil + } + if err := s.ReadSafe(path, result); err != nil { + return nil, NewStorageError(err) + } + return result, nil +} + +func saveMeta(s Storage, turn uint, gm *game.GameMeta) error { // save turn's meta - path := fmt.Sprintf("%s/%s", TurnDir(t), metaPath) + path := fmt.Sprintf("%s/%s", TurnDir(turn), metaPath) if err := s.Write(path, gm); err != nil { return NewStorageError(err) } @@ -131,27 +148,43 @@ func saveMeta(s Storage, t uint, gm *game.GameMeta) error { return nil } -func (r *repo) SaveBattle(t uint, b *report.BattleReport, m *game.BattleMeta) error { +func (r *repo) LoadBattle(turn uint, id uuid.UUID) (*report.BattleReport, bool, error) { + meta, err := loadTurnMeta(r.s, turn) + if err != nil { + return nil, false, err + } + i := slices.IndexFunc(meta.Battles, func(m game.BattleMeta) bool { return m.BattleID == id }) + if i < 0 { + return nil, false, nil + } + result, err := loadBattle(r.s, turn, meta.Battles[i].BattleID) + if err != nil { + return nil, false, err + } + return result, true, nil +} + +func (r *repo) SaveBattle(turn uint, b *report.BattleReport, m *game.BattleMeta) error { meta, err := loadMeta(r.s) if err != nil { return err } - err = saveBattle(r.s, t, b) + err = saveBattle(r.s, turn, b) if err != nil { return err } meta.Battles = append(meta.Battles, *m) - return saveMeta(r.s, t, meta) + return saveMeta(r.s, turn, meta) } -func saveBattle(s Storage, t uint, b *report.BattleReport) error { - path := fmt.Sprintf("%s/battle/%s.json", TurnDir(t), b.ID.String()) +func saveBattle(s Storage, turn uint, b *report.BattleReport) error { + path := fmt.Sprintf("%s/battle/%s.json", TurnDir(turn), b.ID.String()) exist, err := s.Exists(path) if err != nil { return NewStorageError(err) } if exist { - return NewStateError(fmt.Sprintf("battle %v for turn %d already has been saved", b.ID, t)) + return NewStateError(fmt.Sprintf("battle %v for turn %d already has been saved", b.ID, turn)) } if err := s.Write(path, b); err != nil { return NewStorageError(err) @@ -159,7 +192,23 @@ func saveBattle(s Storage, t uint, b *report.BattleReport) error { return nil } -func (r *repo) SaveBombings(t uint, b []*game.Bombing) error { +func loadBattle(s Storage, turn uint, id uuid.UUID) (*report.BattleReport, error) { + path := fmt.Sprintf("%s/battle/%s.json", TurnDir(turn), id.String()) + exist, err := s.Exists(path) + if err != nil { + return nil, NewStorageError(err) + } + if !exist { + return nil, NewStateError(fmt.Sprintf("battle %v for turn %d never was saved", id, turn)) + } + result := new(report.BattleReport) + if err := s.ReadSafe(path, result); err != nil { + return nil, NewStorageError(err) + } + return result, nil +} + +func (r *repo) SaveBombings(turn uint, b []*game.Bombing) error { meta, err := loadMeta(r.s) if err != nil { return err @@ -167,11 +216,11 @@ func (r *repo) SaveBombings(t uint, b []*game.Bombing) error { for i := range b { meta.Bombings = append(meta.Bombings, *b[i]) } - return saveMeta(r.s, t, meta) + return saveMeta(r.s, turn, meta) } -func (r *repo) SaveReport(t uint, rep *report.Report) error { - return saveReport(r.s, t, rep) +func (r *repo) SaveReport(turn uint, rep *report.Report) error { + return saveReport(r.s, turn, rep) } func saveReport(s Storage, t uint, v *report.Report) error { @@ -182,12 +231,12 @@ func saveReport(s Storage, t uint, v *report.Report) error { return nil } -func (r *repo) LoadReport(t uint, id uuid.UUID) (*report.Report, error) { - return loadReport(r.s, t, id) +func (r *repo) LoadReport(turn uint, id uuid.UUID) (*report.Report, error) { + return loadReport(r.s, turn, id) } -func loadReport(s Storage, t uint, id uuid.UUID) (*report.Report, error) { - path := ReportDir(t, id) +func loadReport(s Storage, turn uint, id uuid.UUID) (*report.Report, error) { + path := ReportDir(turn, id) result := new(report.Report) exist, err := s.Exists(path) if err != nil { diff --git a/game/internal/router/handler/battle.go b/game/internal/router/handler/battle.go new file mode 100644 index 0000000..4322b23 --- /dev/null +++ b/game/internal/router/handler/battle.go @@ -0,0 +1,37 @@ +package handler + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +func BattleHandler(c *gin.Context, executor CommandExecutor) { + turn := c.Param("turn") + t, err := strconv.Atoi(turn) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + if t < 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "turn number can't be negative"}) + return + } + id := c.Param("uuid") + battleID, err := uuid.Parse(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + r, exists, err := executor.FetchBattle(uint(t), battleID) + if errorResponse(c, err) { + return + } + if !exists { + c.JSON(http.StatusNotFound, gin.H{"error": "unknown battle"}) + return + } + c.JSON(http.StatusOK, r) +} diff --git a/game/internal/router/handler/handler.go b/game/internal/router/handler/handler.go index ca4ad6d..4be6c8b 100644 --- a/game/internal/router/handler/handler.go +++ b/game/internal/router/handler/handler.go @@ -17,6 +17,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" + "github.com/google/uuid" ) type CommandExecutor interface { @@ -29,6 +30,7 @@ type CommandExecutor interface { Execute(cmd ...Command) error ValidateOrder(actor string, cmd ...order.DecodableCommand) (*order.UserGamesOrder, error) FetchOrder(actor string, turn uint) (*order.UserGamesOrder, bool, error) + FetchBattle(turn uint, ID uuid.UUID) (*report.BattleReport, bool, error) } type Command func(controller.Ctrl) error @@ -86,6 +88,10 @@ func (e *executor) FetchOrder(actor string, turn uint) (*order.UserGamesOrder, b return controller.FetchOrder(e.cfg, actor, turn) } +func (e *executor) FetchBattle(turn uint, ID uuid.UUID) (*report.BattleReport, bool, error) { + return controller.FetchBattle(e.cfg, turn, ID) +} + func (e *executor) GenerateGame(races []string) (rest.StateResponse, error) { s, err := controller.GenerateGame(e.cfg, races) if err != nil { diff --git a/game/internal/router/router.go b/game/internal/router/router.go index 0ff10a2..15efea9 100644 --- a/game/internal/router/router.go +++ b/game/internal/router/router.go @@ -76,6 +76,7 @@ func setupRouter(executor handler.CommandExecutor) *gin.Engine { groupV1.GET("/report", func(ctx *gin.Context) { handler.ReportHandler(ctx, executor) }) groupV1.PUT("/order", func(ctx *gin.Context) { handler.PutOrderHandler(ctx, executor) }) groupV1.GET("/order", func(ctx *gin.Context) { handler.GetOrderHandler(ctx, executor) }) + groupV1.GET("/battle/:turn/:uuid", func(ctx *gin.Context) { handler.BattleHandler(ctx, executor) }) // /command is reserved for future use; any API request for orders should use /order groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) }) diff --git a/game/internal/router/router_helper_test.go b/game/internal/router/router_helper_test.go index 4ab9b98..e42b90d 100644 --- a/game/internal/router/router_helper_test.go +++ b/game/internal/router/router_helper_test.go @@ -68,6 +68,10 @@ func (e *dummyExecutor) FetchOrder(actor string, turn uint) (*order.UserGamesOrd return e.FetchOrderResult, e.FetchOrderOK, e.FetchOrderErr } +func (e *dummyExecutor) FetchBattle(turn uint, ID uuid.UUID) (*report.BattleReport, bool, error) { + return nil, false, nil +} + func (e *dummyExecutor) Execute(command ...handler.Command) error { e.CommandsExecuted = len(command) return nil diff --git a/pkg/model/report/battle.go b/pkg/model/report/battle.go index d7a92f8..be1f9cb 100644 --- a/pkg/model/report/battle.go +++ b/pkg/model/report/battle.go @@ -7,31 +7,53 @@ import ( ) type BattleReport struct { - ID uuid.UUID `json:"id"` - Planet uint `json:"planet"` - PlanetName string `json:"planetName"` - Races map[int]uuid.UUID `json:"races"` - Ships map[int]BattleReportGroup `json:"ships"` - Protocol []BattleActionReport `json:"protocol"` + // Battle unique ID + ID uuid.UUID `json:"id"` + // Planet number + Planet uint `json:"planet"` + // Planet name at battle start + PlanetName string `json:"planetName"` + // Races participating map: + Races map[int]uuid.UUID `json:"races"` + // Ships Groups participating map: + Ships map[int]BattleReportGroup `json:"ships"` + // Battle's firing protocol + Protocol []BattleActionReport `json:"protocol"` } type BattleReportGroup struct { - InBattle bool `json:"inBattle"` - Number uint `json:"num"` - NumberLeft uint `json:"numLeft"` - LoadQuantity Float `json:"loadQuantity"` - Tech map[string]Float `json:"tech"` - Race string `json:"race"` - ClassName string `json:"className"` - LoadType string `json:"loadType"` + // Name of the race + Race string `json:"race"` + // Name of the Ship Class. + // By design, ship's info MUST be present in Game's Repors in 'LocalShipClass' or 'OtherShipClass' + ClassName string `json:"className"` + // Ship Group's technologies mapping + Tech map[string]Float `json:"tech"` + // Initial number of ships in this group + Number uint `json:"num"` + // Number of ships left after battle + NumberLeft uint `json:"numLeft"` + // Type of cargo loaded + LoadType string `json:"loadType"` + // Quantity of cargo loaded + LoadQuantity Float `json:"loadQuantity"` + // A Race with its ships can be in Peace state with all participants, + // so no shots will be fired and no damage taken, participating only as viewer + // when InBattle=false + InBattle bool `json:"inBattle"` } type BattleActionReport struct { - Attacker int `json:"a"` - AttackerShipClass int `json:"sa"` - Defender int `json:"d"` - DefenderShipClass int `json:"sd"` - Destroyed bool `json:"x"` + // `key` from BattleReport.Races map + Attacker int `json:"a"` + // `key` from BattleReport.Ships map + AttackerShipClass int `json:"sa"` + // `key` from BattleReport.Races map + Defender int `json:"d"` + // `key` from BattleReport.Ships map + DefenderShipClass int `json:"sd"` + // Was ship destroyed after attack or survived under shields + Destroyed bool `json:"x"` } func (b BattleReport) MarshalBinary() (data []byte, err error) {