package handler import ( "errors" "net/http" "os" "strings" "galaxy/model/order" "galaxy/model/report" "galaxy/model/rest" e "galaxy/error" "galaxy/game/internal/model/game" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "github.com/google/uuid" ) // Engine is the set of operations the HTTP handlers invoke on the game engine. // Its sole production implementation is *controller.Service; the interface // exists so the transport layer can be exercised against a lightweight fake // without standing up real storage. Methods return domain types — handlers own // the projection into the REST wire shapes. type Engine interface { GenerateGame(gameID uuid.UUID, races []string) (game.State, error) GenerateTurn() (game.State, error) GameState() (game.State, error) BanishRace(actor string) error LoadReport(actor string, turn uint) (*report.Report, 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) } // ResolveStoragePath returns the engine storage path resolved from // STORAGE_PATH (preferred, historical name) or GAME_STATE_PATH (canonical // name written by Runtime Manager). It returns an error when neither // variable is set; callers are expected to fail fast at startup. func ResolveStoragePath() (string, error) { if v := strings.TrimSpace(os.Getenv("STORAGE_PATH")); v != "" { return v, nil } if v := strings.TrimSpace(os.Getenv("GAME_STATE_PATH")); v != "" { return v, nil } return "", errors.New("storage path is not set: provide STORAGE_PATH or GAME_STATE_PATH") } // stateResponse projects the engine's domain game.State into the REST // StateResponse wire shape. func stateResponse(s game.State) rest.StateResponse { result := &rest.StateResponse{ ID: s.ID, Turn: s.Turn, Stage: s.Stage, Finished: s.Finished, Players: make([]rest.PlayerState, len(s.Players)), } for i := range s.Players { result.Players[i].ID = s.Players[i].ID result.Players[i].RaceName = s.Players[i].RaceName result.Players[i].Planets = s.Players[i].Planets result.Players[i].Population = s.Players[i].Population.F() result.Players[i].Extinct = s.Players[i].Extinct } return *result } // errorResponse renders err onto c and reports whether the caller // should stop further processing. The HTTP status is selected by the // GenericError shelf (see pkg/error for the taxonomy): // // - validator.ValidationErrors (request struct binding) → 400 with // {"error": ...}. // - GenericError, ErrGameNotInitialized → 501 with no body. // - GenericError on the internal shelf (1xxx) → 500 with // {"generic_error", "code"}. // - GenericError on the input-validation shelf (2xxx) or the // game-state shelf (3xxx) → 400 with {"generic_error", "code"}. // - everything else (non-GenericError) → 500 with {"error": ...}. func errorResponse(c *gin.Context, err error) bool { if err == nil { return false } if v, ok := err.(validator.ValidationErrors); ok { c.JSON(http.StatusBadRequest, gin.H{"error": v.Error()}) return true } if ge, ok := errors.AsType[*e.GenericError](err); ok { switch { case ge.Code == e.ErrGameNotInitialized: c.Status(http.StatusNotImplemented) case e.IsInputCode(ge.Code), e.IsGameStateCode(ge.Code): c.JSON(http.StatusBadRequest, gin.H{"generic_error": ge.Error(), "code": ge.Code}) default: c.JSON(http.StatusInternalServerError, gin.H{"generic_error": ge.Error(), "code": ge.Code}) } } else { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return true }