package scrabble import ( "errors" "fmt" "sort" dawg "github.com/iliadenisov/dafsa" "gitea.iliadenisov.ru/developer/scrabble-solver/board" "gitea.iliadenisov.ru/developer/scrabble-solver/rack" "gitea.iliadenisov.ru/developer/scrabble-solver/rules" ) // Solver is the high-level entry point: it generates ranked plays and scores or // validates arbitrary plays for a ruleset over a dictionary. type Solver struct { rules *rules.Ruleset finder dawg.Finder gen *DAWGGenerator } // NewSolver returns a Solver for the ruleset over the dictionary finder. func NewSolver(rs *rules.Ruleset, finder dawg.Finder) *Solver { return &Solver{rules: rs, finder: finder, gen: NewDAWGGenerator(rs, finder)} } // Rules returns the solver's ruleset. func (s *Solver) Rules() *rules.Ruleset { return s.rules } // GenerateMoves returns every legal play for rack r on board b in the requested // orientations, ranked by descending score (ties broken deterministically by the move's // canonical key). func (s *Solver) GenerateMoves(b *board.Board, r rack.Rack, mode Mode) []Move { moves := s.gen.GenerateMoves(b, r, mode) sort.Slice(moves, func(i, j int) bool { if moves[i].Score != moves[j].Score { return moves[i].Score > moves[j].Score } return moves[i].Key() < moves[j].Key() }) return moves } // ScorePlay computes the words and score for placing tiles on b in direction dir. It // checks geometry only (see Evaluate); use ValidatePlay to also check the dictionary and // connectivity. func (s *Solver) ScorePlay(b *board.Board, dir Direction, tiles []Placement) (Move, error) { return Evaluate(b, s.rules, dir, tiles) } // ValidatePlay scores a play and verifies that every word it forms is in the dictionary // and that it connects to the board (or covers the centre on the first move). It returns // the scored move; the error is nil exactly when the play is legal. func (s *Solver) ValidatePlay(b *board.Board, dir Direction, tiles []Placement) (Move, error) { m, err := Evaluate(b, s.rules, dir, tiles) if err != nil { return Move{}, err } if len(m.Main.Letters) < 2 { return m, errors.New("scrabble: play forms no word of length 2 or more") } if s.finder.IndexOfB(m.Main.Letters) < 0 { return m, fmt.Errorf("scrabble: main word is not in the dictionary") } for _, cw := range m.Cross { if s.finder.IndexOfB(cw.Letters) < 0 { return m, fmt.Errorf("scrabble: a cross word is not in the dictionary") } } if !s.connected(b, m) { return m, errors.New("scrabble: play does not connect to the board") } return m, nil } // connected reports whether the play touches the existing position (or covers the centre // on the first move). func (s *Solver) connected(b *board.Board, m Move) bool { if b.IsEmpty() { cr, cc := s.rules.Center/s.rules.Cols, s.rules.Center%s.rules.Cols return wordCovers(m.Main, cr, cc) } // The main word incorporated an existing tile, or a new tile formed a cross word. return len(m.Main.Letters) > len(m.Tiles) || len(m.Cross) > 0 } func wordCovers(w Word, r, c int) bool { for i := range w.Letters { rr, cc := w.Row, w.Col if w.Dir == Horizontal { cc += i } else { rr += i } if rr == r && cc == c { return true } } return false }