package game import ( "encoding/json" "fmt" "scrabble/backend/internal/engine" ) // movePayload is the JSON stored in game_moves.payload. It holds the decoded, // dictionary-independent values needed both to replay the game through the engine // and to render history / emit GCG without a dictionary (docs/ARCHITECTURE.md // ยง9.1): the acting player's rack before the move, and per action the play's // direction, main-word anchor, placed tiles and formed words, or an exchange's // swapped tiles. type movePayload struct { Rack []string `json:"rack"` Dir string `json:"dir,omitempty"` MainRow int `json:"main_row,omitempty"` MainCol int `json:"main_col,omitempty"` Tiles []tilePayload `json:"tiles,omitempty"` Words []string `json:"words,omitempty"` Exchanged []string `json:"exchanged,omitempty"` } // tilePayload is one placed tile in a play payload. type tilePayload struct { Row int `json:"row"` Col int `json:"col"` Letter string `json:"letter"` Blank bool `json:"blank,omitempty"` } // buildPayload assembles the journal payload from the engine's decoded record, // the acting rack captured before the move, and (for an exchange) the swapped // tiles. func buildPayload(rec engine.MoveRecord, rackBefore, exchanged []string) movePayload { p := movePayload{Rack: rackBefore} switch rec.Action { case engine.ActionPlay: p.Dir = rec.Dir.String() p.MainRow = rec.MainRow p.MainCol = rec.MainCol p.Words = rec.Words p.Tiles = make([]tilePayload, len(rec.Tiles)) for i, t := range rec.Tiles { p.Tiles[i] = tilePayload{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank} } case engine.ActionExchange: p.Exchanged = exchanged } return p } // marshal renders the payload as the JSON text stored in the column. func (p movePayload) marshal() (string, error) { b, err := json.Marshal(p) if err != nil { return "", fmt.Errorf("game: marshal move payload: %w", err) } return string(b), nil } // parsePayload parses a stored payload back into its decoded fields. func parsePayload(s string) (movePayload, error) { var p movePayload if err := json.Unmarshal([]byte(s), &p); err != nil { return movePayload{}, fmt.Errorf("game: parse move payload: %w", err) } return p, nil } // tileRecords converts payload tiles back into engine TileRecords for replay and // history. func (p movePayload) tileRecords() []engine.TileRecord { out := make([]engine.TileRecord, len(p.Tiles)) for i, t := range p.Tiles { out[i] = engine.TileRecord{Row: t.Row, Col: t.Col, Letter: t.Letter, Blank: t.Blank} } return out } // direction parses the stored "H"/"V" into an engine.Direction. func (p movePayload) direction() engine.Direction { if p.Dir == engine.Vertical.String() { return engine.Vertical } return engine.Horizontal }