ec435c0e7f
- backend/go.mod pins gitea.iliadenisov.ru/developer/scrabble-solver v1.0.0; the engine's imports use the published module path; go.work drops the solver replace (GOPRIVATE fetches it directly from Gitea). The solver's wordlist/dictdawg are now public packages. - CI (go-unit, integration): drop the solver sibling-clone, set GOPRIVATE, and download the dictionary DAWG release artifact (scrabble-dawg-<DICT_VERSION>.tar.gz from the new scrabble-dictionary repo) for BACKEND_DICT_DIR. - Docs: ARCHITECTURE §5/§11/§13/§14 + backend/README updated to the published-module + release-artifact model. PLAN.md re-scoped Stage 14 to the split and added Stages 15 (deploy infra & test contour), 16 (prod contour), 17 (dual Telegram bots); TODO-1/TODO-2 marked done.
117 lines
4.4 KiB
Go
117 lines
4.4 KiB
Go
// 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"
|
|
|
|
"gitea.iliadenisov.ru/developer/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}
|
|
}
|
|
|
|
// ParseVariant maps a stable label produced by Variant.String back to its
|
|
// Variant, or returns ErrUnknownVariant. It is the inverse the game domain uses
|
|
// to read a persisted variant.
|
|
func ParseVariant(s string) (Variant, error) {
|
|
for _, v := range Variants() {
|
|
if v.String() == s {
|
|
return v, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("%w: %q", ErrUnknownVariant, s)
|
|
}
|
|
|
|
// 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")
|
|
// ErrUnknownDropoutTiles is returned by ParseDropoutTiles for a label that is
|
|
// neither "remove" nor "return".
|
|
ErrUnknownDropoutTiles = errors.New("engine: unknown drop-out tile disposition")
|
|
// 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")
|
|
)
|