Stage 13: alphabet on the wire (UI alphabet-agnostic, TODO-4)
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Tests · Go / test (push) Successful in 10s
Tests · Integration / integration (push) Successful in 12s
Tests · UI / test (push) Successful in 19s
Tests · Go / test (pull_request) Successful in 9s
Tests · Integration / integration (pull_request) Successful in 12s
Tests · UI / test (pull_request) Successful in 19s
Live play now exchanges per-variant alphabet indices instead of concrete letters (rack out; submit-play, evaluate, exchange, word-check in). The client caches each variant's (index, letter, value) table behind StateRequest.include_alphabet and renders the rack and blank chooser from it, dropping the hardcoded value/alphabet tables. History, the durable journal and GCG stay decoded concrete characters (ARCHITECTURE §9.1, unchanged). - pkg/fbs: new AlphabetEntry + PlayTile; StateView.rack -> [ubyte] + alphabet; StateRequest.include_alphabet; SubmitPlay/Eval tiles -> [PlayTile]; Exchange tiles + CheckWord word -> [ubyte] (committed Go + TS regenerated). - engine: AlphabetTable + a cached per-variant codec (LetterForIndex/EncodeRack/ DecodeTiles/DecodeWord) + BlankIndex sentinel; Go parity test. - backend server edge maps index<->letter (new thin game.Service.GameVariant); game.Service domain methods, engine.Game and the robot keep one letter-based play path. The gateway forwards indices verbatim (no alphabet table). - ui: lib/alphabet.ts in-memory cache; codec encodes/decodes indices; premiums.ts is geometry-only; the mock seeds a fixture table; the UI normalises display to upper case (codec + cache), leaving placement/board/checkword unchanged. Parity moved to the Go engine.AlphabetTable test; premiums.ts loses its value tables. Discharges TODO-4.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// The structs below mirror the backend's JSON DTOs (backend/internal/server
|
||||
@@ -47,7 +48,8 @@ type LinkResultResp struct {
|
||||
Profile *ProfileResp `json:"profile"`
|
||||
}
|
||||
|
||||
// TileJSON is one placed tile, used in both play requests and move responses.
|
||||
// TileJSON is one tile in a decoded move response (history, move result, hint); its Letter
|
||||
// is a concrete character (Stage 13 keeps the move journal in letters).
|
||||
type TileJSON struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
@@ -55,6 +57,15 @@ type TileJSON struct {
|
||||
Blank bool `json:"blank"`
|
||||
}
|
||||
|
||||
// PlayTileJSON is one inbound tile to place, addressed by alphabet index (Stage 13). For a
|
||||
// blank, Letter is the designated letter's index and Blank is true.
|
||||
type PlayTileJSON struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Letter int `json:"letter"`
|
||||
Blank bool `json:"blank"`
|
||||
}
|
||||
|
||||
// MoveRecordResp is a decoded move.
|
||||
type MoveRecordResp struct {
|
||||
Player int `json:"player"`
|
||||
@@ -99,13 +110,23 @@ type MoveResultResp struct {
|
||||
Game GameResp `json:"game"`
|
||||
}
|
||||
|
||||
// StateResp is a player's view of a game.
|
||||
// AlphabetEntryJSON is one letter of a variant's alphabet (its index, concrete letter and
|
||||
// tile value), present in StateResp only when the client requested it (Stage 13).
|
||||
type AlphabetEntryJSON struct {
|
||||
Index int `json:"index"`
|
||||
Letter string `json:"letter"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
|
||||
// StateResp is a player's view of a game. Rack carries wire alphabet indices (Stage 13);
|
||||
// Alphabet is present only when the request asked for it.
|
||||
type StateResp struct {
|
||||
Game GameResp `json:"game"`
|
||||
Seat int `json:"seat"`
|
||||
Rack []string `json:"rack"`
|
||||
BagLen int `json:"bag_len"`
|
||||
HintsRemaining int `json:"hints_remaining"`
|
||||
Game GameResp `json:"game"`
|
||||
Seat int `json:"seat"`
|
||||
Rack []int `json:"rack"`
|
||||
BagLen int `json:"bag_len"`
|
||||
HintsRemaining int `json:"hints_remaining"`
|
||||
Alphabet []AlphabetEntryJSON `json:"alphabet,omitempty"`
|
||||
}
|
||||
|
||||
// MatchResp reports an auto-match outcome.
|
||||
@@ -194,18 +215,25 @@ func (c *Client) Profile(ctx context.Context, userID string) (ProfileResp, error
|
||||
return out, err
|
||||
}
|
||||
|
||||
// SubmitPlay commits a placement on the player's turn.
|
||||
func (c *Client) SubmitPlay(ctx context.Context, userID, gameID, dir string, tiles []TileJSON) (MoveResultResp, error) {
|
||||
// SubmitPlay commits a placement on the player's turn. The tiles are addressed by alphabet
|
||||
// index (Stage 13).
|
||||
func (c *Client) SubmitPlay(ctx context.Context, userID, gameID, dir string, tiles []PlayTileJSON) (MoveResultResp, error) {
|
||||
var out MoveResultResp
|
||||
body := map[string]any{"dir": dir, "tiles": tiles}
|
||||
err := c.do(ctx, http.MethodPost, "/api/v1/user/games/"+url.PathEscape(gameID)+"/play", userID, "", body, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// GameState returns the player's view of a game.
|
||||
func (c *Client) GameState(ctx context.Context, userID, gameID string) (StateResp, error) {
|
||||
// GameState returns the player's view of a game. When includeAlphabet is set the backend
|
||||
// embeds the variant's alphabet table (Stage 13); the client asks for it on a per-variant
|
||||
// cache miss only.
|
||||
func (c *Client) GameState(ctx context.Context, userID, gameID string, includeAlphabet bool) (StateResp, error) {
|
||||
var out StateResp
|
||||
err := c.do(ctx, http.MethodGet, "/api/v1/user/games/"+url.PathEscape(gameID)+"/state", userID, "", nil, &out)
|
||||
path := "/api/v1/user/games/" + url.PathEscape(gameID) + "/state"
|
||||
if includeAlphabet {
|
||||
path += "?include_alphabet=true"
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, path, userID, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
@@ -278,8 +306,9 @@ func (c *Client) Pass(ctx context.Context, userID, gameID string) (MoveResultRes
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Exchange swaps the chosen rack tiles back into the bag.
|
||||
func (c *Client) Exchange(ctx context.Context, userID, gameID string, tiles []string) (MoveResultResp, error) {
|
||||
// Exchange swaps the chosen rack tiles back into the bag. Tiles are wire alphabet indices
|
||||
// (Stage 13; a blank is engine.BlankIndex).
|
||||
func (c *Client) Exchange(ctx context.Context, userID, gameID string, tiles []int) (MoveResultResp, error) {
|
||||
var out MoveResultResp
|
||||
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/exchange"), userID, "",
|
||||
map[string]any{"tiles": tiles}, &out)
|
||||
@@ -300,18 +329,24 @@ func (c *Client) Hint(ctx context.Context, userID, gameID string) (HintResultRes
|
||||
return out, err
|
||||
}
|
||||
|
||||
// Evaluate previews a tentative play's legality and score.
|
||||
func (c *Client) Evaluate(ctx context.Context, userID, gameID, dir string, tiles []TileJSON) (EvalResultResp, error) {
|
||||
// Evaluate previews a tentative play's legality and score. The tiles are addressed by
|
||||
// alphabet index (Stage 13).
|
||||
func (c *Client) Evaluate(ctx context.Context, userID, gameID, dir string, tiles []PlayTileJSON) (EvalResultResp, error) {
|
||||
var out EvalResultResp
|
||||
err := c.do(ctx, http.MethodPost, c.gamePath(gameID, "/evaluate"), userID, "",
|
||||
map[string]any{"dir": dir, "tiles": tiles}, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// CheckWord looks a word up in the game's pinned dictionary.
|
||||
func (c *Client) CheckWord(ctx context.Context, userID, gameID, word string) (WordCheckResp, error) {
|
||||
// CheckWord looks a word up in the game's pinned dictionary. The word is carried as
|
||||
// repeated ?idx= alphabet indices (Stage 13); the backend echoes the decoded concrete word.
|
||||
func (c *Client) CheckWord(ctx context.Context, userID, gameID string, word []int) (WordCheckResp, error) {
|
||||
var out WordCheckResp
|
||||
err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/check_word")+"?word="+url.QueryEscape(word), userID, "", nil, &out)
|
||||
q := url.Values{}
|
||||
for _, x := range word {
|
||||
q.Add("idx", strconv.Itoa(x))
|
||||
}
|
||||
err := c.do(ctx, http.MethodGet, c.gamePath(gameID, "/check_word")+"?"+q.Encode(), userID, "", nil, &out)
|
||||
return out, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user