// Package engine is the backend's in-process bridge to the scrabble-solver // library. It catalogues the playable variants, loads versioned dictionaries // into a registry of solvers, and exposes a pure rules engine (the in-memory // Game) that drives a match through legal plays, passes, exchanges and // resignations while detecting the end of the game. // // Two invariants shape the package. First, the solver speaks alphabet-index // bytes that are meaningful only alongside the matching ruleset; every value // that leaves the engine for persistence or display is decoded to concrete // characters (see decode.go and docs/ARCHITECTURE.md §9.1), so archived games // replay independently of any dictionary. Second, the engine owns rules and // scoring only: turn scheduling, the 24-hour timeout, persistence and transport // belong to the game domain in a later stage. package engine import ( "errors" "fmt" "scrabble-solver/rules" ) // Variant identifies a Scrabble variant the backend offers. Each maps to a // scrabble-solver ruleset and a committed dictionary. type Variant uint8 const ( // VariantEnglish is standard English Scrabble (the SOWPODS dictionary). VariantEnglish Variant = iota // VariantRussianScrabble is Russian Scrabble. VariantRussianScrabble // VariantErudit is the Russian "Эрудит" variant. VariantErudit ) // String returns the variant's stable identifier, used in logs and as a metadata // label on persisted games. func (v Variant) String() string { switch v { case VariantEnglish: return "english" case VariantRussianScrabble: return "russian_scrabble" case VariantErudit: return "erudit" } return "unknown" } // ruleset returns the scrabble-solver ruleset backing the variant and true, or // (nil, false) for an unrecognised variant. func (v Variant) ruleset() (*rules.Ruleset, bool) { switch v { case VariantEnglish: return rules.English(), true case VariantRussianScrabble: return rules.RussianScrabble(), true case VariantErudit: return rules.Erudit(), true } return nil, false } // Variants returns the variants the backend offers, in catalogue order. func Variants() []Variant { return []Variant{VariantEnglish, VariantRussianScrabble, VariantErudit} } // Ruleset returns the scrabble-solver ruleset for variant. It needs no // dictionary, so it supports dictionary-independent board replay (see // ReplayBoard) from a finished game's variant metadata alone. func Ruleset(v Variant) (*rules.Ruleset, error) { rs, ok := v.ruleset() if !ok { return nil, fmt.Errorf("%w: %d", ErrUnknownVariant, v) } return rs, nil } // Sentinel errors returned across the engine. Callers match them with // errors.Is; the wrapped detail carries the offending value. var ( // ErrUnknownVariant is returned for a variant the engine does not recognise. ErrUnknownVariant = errors.New("engine: unknown variant") // ErrUnknownVersion is returned when no dictionary is registered for a // (variant, version) pair. ErrUnknownVersion = errors.New("engine: unknown dictionary version") // ErrIllegalPlay wraps a solver validation failure: off-board geometry, a // word absent from the dictionary, or a play that does not connect. ErrIllegalPlay = errors.New("engine: illegal play") // ErrTilesNotOnRack is returned when a play or exchange references tiles the // acting player does not hold. ErrTilesNotOnRack = errors.New("engine: tiles not on the player's rack") // ErrNotEnoughTilesToExchange is returned when an exchange is attempted while // the bag holds fewer tiles than a full rack. ErrNotEnoughTilesToExchange = errors.New("engine: not enough tiles in the bag to exchange") // ErrNothingToExchange is returned for an exchange of zero tiles. ErrNothingToExchange = errors.New("engine: exchange requires at least one tile") // ErrGameOver is returned when a transition is attempted on a finished game. ErrGameOver = errors.New("engine: game is over") )