package server import ( "strings" "scrabble/backend/internal/account" "scrabble/backend/internal/engine" "scrabble/backend/internal/game" "scrabble/backend/internal/lobby" "scrabble/backend/internal/social" ) // The JSON DTOs below are the gateway<->backend REST contract. They are explicit // (the domain/engine structs are never serialised directly) and mirror the // FlatBuffers edge tables (pkg/fbs) the gateway transcodes to and from. // sessionResponse is the credential returned by every auth endpoint. type sessionResponse struct { Token string `json:"token"` UserID string `json:"user_id"` IsGuest bool `json:"is_guest"` DisplayName string `json:"display_name"` } // okResponse is a simple success acknowledgement. type okResponse struct { OK bool `json:"ok"` } // resolveResponse maps a session token to its account. type resolveResponse struct { UserID string `json:"user_id"` } // profileResponse is the authenticated account's own profile. type profileResponse struct { UserID string `json:"user_id"` DisplayName string `json:"display_name"` PreferredLanguage string `json:"preferred_language"` TimeZone string `json:"time_zone"` HintBalance int `json:"hint_balance"` BlockChat bool `json:"block_chat"` BlockFriendRequests bool `json:"block_friend_requests"` IsGuest bool `json:"is_guest"` } // tileDTO is one placed (or to-place) tile. type tileDTO struct { Row int `json:"row"` Col int `json:"col"` Letter string `json:"letter"` Blank bool `json:"blank"` } // moveRecordDTO is a decoded move (a committed play, or a hint preview). type moveRecordDTO struct { Player int `json:"player"` Action string `json:"action"` Dir string `json:"dir"` MainRow int `json:"main_row"` MainCol int `json:"main_col"` Tiles []tileDTO `json:"tiles"` Words []string `json:"words"` Count int `json:"count"` Score int `json:"score"` Total int `json:"total"` } // seatDTO is one seat's public standing. type seatDTO struct { Seat int `json:"seat"` AccountID string `json:"account_id"` Score int `json:"score"` HintsUsed int `json:"hints_used"` IsWinner bool `json:"is_winner"` } // gameDTO is the shared game summary. type gameDTO struct { ID string `json:"id"` Variant string `json:"variant"` DictVersion string `json:"dict_version"` Status string `json:"status"` Players int `json:"players"` ToMove int `json:"to_move"` TurnTimeoutSecs int `json:"turn_timeout_secs"` MoveCount int `json:"move_count"` EndReason string `json:"end_reason"` Seats []seatDTO `json:"seats"` } // moveResultDTO is the outcome of a committed move. type moveResultDTO struct { Move moveRecordDTO `json:"move"` Game gameDTO `json:"game"` } // stateDTO is a player's view of a game. type stateDTO struct { Game gameDTO `json:"game"` Seat int `json:"seat"` Rack []string `json:"rack"` BagLen int `json:"bag_len"` HintsRemaining int `json:"hints_remaining"` } // matchDTO reports whether the caller has been paired into a game. type matchDTO struct { Matched bool `json:"matched"` Game *gameDTO `json:"game,omitempty"` } // chatDTO is one stored chat message or nudge. type chatDTO struct { ID string `json:"id"` GameID string `json:"game_id"` SenderID string `json:"sender_id"` Kind string `json:"kind"` Body string `json:"body"` CreatedAtUnix int64 `json:"created_at_unix"` } // errorResponse is the uniform error envelope. type errorResponse struct { Error errorBody `json:"error"` } type errorBody struct { Code string `json:"code"` Message string `json:"message"` } // sessionResponseFor builds the credential payload for a minted session. func sessionResponseFor(token string, acc account.Account) sessionResponse { return sessionResponse{ Token: token, UserID: acc.ID.String(), IsGuest: acc.IsGuest, DisplayName: acc.DisplayName, } } // profileResponseFor projects an account into its profile DTO. func profileResponseFor(acc account.Account) profileResponse { return profileResponse{ UserID: acc.ID.String(), DisplayName: acc.DisplayName, PreferredLanguage: acc.PreferredLanguage, TimeZone: acc.TimeZone, HintBalance: acc.HintBalance, BlockChat: acc.BlockChat, BlockFriendRequests: acc.BlockFriendRequests, IsGuest: acc.IsGuest, } } // gameDTOFromGame projects a game.Game into its DTO. func gameDTOFromGame(g game.Game) gameDTO { seats := make([]seatDTO, 0, len(g.Seats)) for _, s := range g.Seats { seats = append(seats, seatDTO{ Seat: s.Seat, AccountID: s.AccountID.String(), Score: s.Score, HintsUsed: s.HintsUsed, IsWinner: s.IsWinner, }) } return gameDTO{ ID: g.ID.String(), Variant: g.Variant.String(), DictVersion: g.DictVersion, Status: g.Status, Players: g.Players, ToMove: g.ToMove, TurnTimeoutSecs: int(g.TurnTimeout.Seconds()), MoveCount: g.MoveCount, EndReason: g.EndReason, Seats: seats, } } // moveRecordDTOFrom projects an engine move record into its DTO. func moveRecordDTOFrom(m engine.MoveRecord) moveRecordDTO { tiles := make([]tileDTO, 0, len(m.Tiles)) for _, t := range m.Tiles { tiles = append(tiles, tileDTO{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank}) } return moveRecordDTO{ Player: m.Player, Action: m.Action.String(), Dir: m.Dir.String(), MainRow: m.MainRow, MainCol: m.MainCol, Tiles: tiles, Words: m.Words, Count: m.Count, Score: m.Score, Total: m.Total, } } // moveResultDTOFrom projects a committed move result into its DTO. func moveResultDTOFrom(r game.MoveResult) moveResultDTO { return moveResultDTO{Move: moveRecordDTOFrom(r.Move), Game: gameDTOFromGame(r.Game)} } // stateDTOFrom projects a player's state view into its DTO. func stateDTOFrom(v game.StateView) stateDTO { return stateDTO{ Game: gameDTOFromGame(v.Game), Seat: v.Seat, Rack: v.Rack, BagLen: v.BagLen, HintsRemaining: v.HintsRemaining, } } // matchDTOFrom projects an enqueue/poll result into its DTO. func matchDTOFrom(r lobby.EnqueueResult) matchDTO { if !r.Matched { return matchDTO{Matched: false} } g := gameDTOFromGame(r.Game) return matchDTO{Matched: true, Game: &g} } // chatDTOFrom projects a chat message into its DTO. func chatDTOFrom(m social.Message) chatDTO { return chatDTO{ ID: m.ID.String(), GameID: m.GameID.String(), SenderID: m.SenderID.String(), Kind: m.Kind, Body: m.Body, CreatedAtUnix: m.CreatedAt.Unix(), } } // parseDirection maps the wire direction string to an engine.Direction. func parseDirection(s string) (engine.Direction, bool) { switch strings.ToUpper(strings.TrimSpace(s)) { case "H": return engine.Horizontal, true case "V": return engine.Vertical, true default: return 0, false } }