8881214213
Mechanical, behaviour-preserving removal of Stage N / TODO-N / phase (RN) references from comments, doc-comments, service READMEs, the current-state docs (ARCHITECTURE, FUNCTIONAL+_ru, TESTING, UI_DESIGN), config-file comments, and the .fbs/.proto schema comments. PLAN.md / PRERELEASE.md / CLAUDE.md keep the stage history. - Rename the only stage-named identifiers: registerStage8 -> registerSocialOps, registerStage11 -> registerLinkOps (gateway transcode). - Split stage6_test.go: TestEmailLoginFlow -> email_test.go, TestGuestAutoMatchLeavesNoStats (+ provisionGuest) -> account_test.go. - Regenerated proto bindings (push.pb.go, telegram_grpc.pb.go) from the de-staged .proto comments; FB Go/TS bindings unchanged (flatc strips schema comments). go build/vet/gofmt clean across modules; integration typecheck and pnpm check green.
151 lines
5.2 KiB
Go
151 lines
5.2 KiB
Go
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, 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
|
|
}
|