package engine import ( "fmt" "strings" ) // AlphabetEntry is one letter of a variant's alphabet: its alphabet-index byte, the // concrete character and its tile point value. It is the dictionary-independent display // table the edge sends to the client (Stage 13), produced from the variant's solver // ruleset (its alphabet and value table) and so pinned by the solver version, not by any // dictionary. type AlphabetEntry struct { // Index is the alphabet-index byte the wire uses for this letter (0..Size-1). Index byte // Letter is the concrete character, in the case the solver ruleset emits (lower). Letter string // Value is the tile's point score. Value int } // BlankIndex is the wire sentinel for a blank tile inside an alphabet-index sequence (a // rack or an exchange list). It is out of range of every offered variant's alphabet (the // largest has 33 letters), so it never collides with a real letter index. A placed blank // instead travels as an ordinary tile carrying its designated letter's index alongside a // separate blank flag. The constant is untyped so it serves both byte (FlatBuffers ubyte) // and int (the gateway/backend JSON edge) call sites. const BlankIndex = 0xFF // variantCodec is the cached per-variant alphabet data backing the wire helpers: the // ordered display table and a case-insensitive letter→index lookup. Both are derived once // from the solver ruleset (see variantCodecs). type variantCodec struct { table []AlphabetEntry letterToIndex map[string]byte } // variantCodecs holds one codec per offered variant, built once at package load from each // ruleset's alphabet and value table. The rulesets are needed only here (not per request), // so the hot path never rebuilds them. var variantCodecs = buildVariantCodecs() func buildVariantCodecs() map[Variant]*variantCodec { m := make(map[Variant]*variantCodec, len(Variants())) for _, v := range Variants() { rs, ok := v.ruleset() if !ok { continue } size := rs.Alphabet.Size() table := make([]AlphabetEntry, size) lut := make(map[string]byte, size) for i := range size { ch, err := rs.Alphabet.Character(byte(i)) if err != nil { // An offered variant's alphabet never yields a bad index; skip defensively. continue } table[i] = AlphabetEntry{Index: byte(i), Letter: ch, Value: rs.Values[i]} lut[strings.ToLower(ch)] = byte(i) } m[v] = &variantCodec{table: table, letterToIndex: lut} } return m } // AlphabetTable returns a copy of variant's full alphabet as an ordered (index, letter, // value) table, or ErrUnknownVariant. Entry i has Index i, so the slice doubles as an // index→(letter, value) lookup. It needs no dictionary — the data comes from the solver // ruleset alone — so it is safe to build for any offered variant and is the same table the // client caches for display while live play exchanges bare indices. func AlphabetTable(v Variant) ([]AlphabetEntry, error) { c, ok := variantCodecs[v] if !ok { return nil, fmt.Errorf("%w: %d", ErrUnknownVariant, v) } out := make([]AlphabetEntry, len(c.table)) copy(out, c.table) return out, nil } // LetterForIndex maps one alphabet index to its concrete letter for variant. It is the // wire-decode primitive for a placed tile (a blank carries its designated letter's index). // An out-of-range index is an illegal play. func LetterForIndex(v Variant, idx int) (string, error) { c, ok := variantCodecs[v] if !ok { return "", fmt.Errorf("%w: %d", ErrUnknownVariant, v) } if idx < 0 || idx >= len(c.table) { return "", fmt.Errorf("%w: alphabet index %d for %s", ErrIllegalPlay, idx, v) } return c.table[idx].Letter, nil } // EncodeRack maps a decoded rack (the Game.Hand form: concrete letters with "?" for an // undesignated blank) to wire alphabet indices, using BlankIndex for each blank. It backs // the per-player state view, whose rack the client renders via the cached table. func EncodeRack(v Variant, letters []string) ([]int, error) { c, ok := variantCodecs[v] if !ok { return nil, fmt.Errorf("%w: %d", ErrUnknownVariant, v) } out := make([]int, len(letters)) for i, l := range letters { if l == blankLetter { out[i] = BlankIndex continue } idx, ok := c.letterToIndex[strings.ToLower(l)] if !ok { return nil, fmt.Errorf("%w: rack letter %q for %s", ErrTilesNotOnRack, l, v) } out[i] = int(idx) } return out, nil } // DecodeTiles maps a wire rack/exchange index list back to the decoded letter form ("?" // for a blank, BlankIndex), for handing to the existing letter-based exchange path. func DecodeTiles(v Variant, idx []int) ([]string, error) { out := make([]string, len(idx)) for i, x := range idx { if x == BlankIndex { out[i] = blankLetter continue } l, err := LetterForIndex(v, x) if err != nil { return nil, fmt.Errorf("%w (exchange)", err) } out[i] = l } return out, nil } // DecodeWord maps a sequence of alphabet indices to a concrete word (word-check carries no // blanks). The client constrains input to the variant's alphabet, so every index is a real // letter. func DecodeWord(v Variant, idx []int) (string, error) { var sb strings.Builder for _, x := range idx { l, err := LetterForIndex(v, x) if err != nil { return "", fmt.Errorf("%w (word check)", err) } sb.WriteString(l) } return sb.String(), nil }