// Package rules describes a Scrabble variant: board geometry, premium-square layout, // the letter alphabet, per-letter tile values and bag counts, blanks, rack size and // the all-tiles bonus. English() returns standard English Scrabble; the Ruleset type // is general enough for other variants such as Russian "Эрудит" (same board, different // tile values/counts and alphabet). package rules import ( "fmt" "strings" "github.com/iliadenisov/alphabet" ) // Premium is the bonus kind of a board square. type Premium uint8 const ( None Premium = iota DL // double letter TL // triple letter DW // double word TW // triple word ) // LetterMult is the multiplier a premium applies to the tile placed on it. func (p Premium) LetterMult() int { switch p { case DL: return 2 case TL: return 3 default: return 1 } } // WordMult is the multiplier a premium applies to a word passing through it. func (p Premium) WordMult() int { switch p { case DW: return 2 case TW: return 3 default: return 1 } } // Ruleset is a complete description of a Scrabble variant. type Ruleset struct { Name string Rows, Cols int Alphabet alphabet.Indexer // letter alphabet (no separator) Values []int // tile value per letter index; len == Alphabet.Size() Counts []int // bag count per letter index; len == Alphabet.Size() Blanks int // number of blank tiles in the bag RackSize int // tiles drawn to a full rack Bingo int // bonus for using the whole rack in one play Center int // row-major index of the centre square (first-move anchor) premiums []Premium // row-major premium per square } // Premium returns the premium of square (r, c). func (rs *Ruleset) Premium(r, c int) Premium { return rs.premiums[r*rs.Cols+c] } // PremiumAt returns the premium of the row-major square index i. func (rs *Ruleset) PremiumAt(i int) Premium { return rs.premiums[i] } // Size returns the number of letters in the alphabet (excluding blanks). func (rs *Ruleset) Size() int { return rs.Alphabet.Size() } // Validate checks that the slices are consistent with the alphabet and board. func (rs *Ruleset) Validate() error { n := rs.Alphabet.Size() if len(rs.Values) != n { return fmt.Errorf("rules %q: %d values for %d letters", rs.Name, len(rs.Values), n) } if len(rs.Counts) != n { return fmt.Errorf("rules %q: %d counts for %d letters", rs.Name, len(rs.Counts), n) } if len(rs.premiums) != rs.Rows*rs.Cols { return fmt.Errorf("rules %q: %d premiums for a %dx%d board", rs.Name, len(rs.premiums), rs.Rows, rs.Cols) } if rs.Center < 0 || rs.Center >= rs.Rows*rs.Cols { return fmt.Errorf("rules %q: centre %d out of range", rs.Name, rs.Center) } return nil } // standardBoard is the classic 15x15 premium layout: T=triple word, D=double word, // t=triple letter, d=double letter, .=plain, *=centre (a double word). const standardBoard = `T..d...T...d..T .D...t...t...D. ..D...d.d...D.. d..D...d...D..d ....D.....D.... .t...t...t...t. ..d...d.d...d.. T..d...*...d..T ..d...d.d...d.. .t...t...t...t. ....D.....D.... d..D...d...D..d ..D...d.d...D.. .D...t...t...D. T..d...T...d..T` // parsePremiums turns a board template into a premium grid and the centre index. func parsePremiums(s string) (rows, cols int, prem []Premium, center int) { lines := strings.Split(strings.TrimSpace(s), "\n") rows = len(lines) cols = len(strings.TrimRight(lines[0], "\r")) prem = make([]Premium, rows*cols) center = -1 for r, line := range lines { line = strings.TrimRight(line, "\r") for c := 0; c < cols && c < len(line); c++ { var p Premium switch line[c] { case 'd': p = DL case 't': p = TL case 'D': p = DW case 'T': p = TW case '*': // centre square, a double word p = DW center = r*cols + c case '+': // centre square with no premium center = r*cols + c } prem[r*cols+c] = p } } return rows, cols, prem, center } // FromTemplate builds a ruleset from a premium-layout template (see standardBoard for // the character legend; '+' marks a centre square with no premium). It returns an error // if the resulting ruleset is inconsistent. func FromTemplate(name string, idx alphabet.Indexer, values, counts []int, blanks, rackSize, bingo int, template string) (*Ruleset, error) { rows, cols, prem, center := parsePremiums(template) rs := &Ruleset{ Name: name, Rows: rows, Cols: cols, Alphabet: idx, Values: values, Counts: counts, Blanks: blanks, RackSize: rackSize, Bingo: bingo, Center: center, premiums: prem, } if err := rs.Validate(); err != nil { return nil, err } return rs, nil } // English returns the standard English Scrabble ruleset (15x15, the classic premium // layout, English tile values and distribution, 2 blanks, a 7-tile rack and a 50-point // bingo bonus). func English() *Ruleset { rs, err := FromTemplate("English Scrabble", alphabet.Latin(), // a b c d e f g h i j k l m n o p q r s t u v w x y z []int{1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10}, []int{9, 2, 2, 4, 12, 2, 3, 2, 9, 1, 1, 4, 2, 6, 8, 2, 1, 6, 4, 6, 4, 2, 2, 1, 2, 1}, 2, 7, 50, standardBoard) if err != nil { panic(err) // a programming error in this package, not a runtime condition } return rs } // eruditBoard is the standard 15x15 layout but with a non-doubling centre ('+'), as in // the Russian "Эрудит" variant. const eruditBoard = `T..d...T...d..T .D...t...t...D. ..D...d.d...D.. d..D...d...D..d ....D.....D.... .t...t...t...t. ..d...d.d...d.. T..d...+...d..T ..d...d.d...d.. .t...t...t...t. ....D.....D.... d..D...d...D..d ..D...d.d...D.. .D...t...t...D. T..d...T...d..T` // russian returns the embedded 33-letter Russian alphabet (а..я including ё at index 6). func russian() alphabet.Indexer { return alphabet.Embedded(alphabet.Langs.LangRu) } // RussianScrabble returns the Russian Scrabble ruleset: the 33-letter alphabet, the // standard board, 2 blanks, a 7-tile rack and a 50-point bonus. func RussianScrabble() *Ruleset { rs, err := FromTemplate("Russian Scrabble", russian(), // а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я []int{1, 3, 1, 3, 2, 1, 3, 5, 5, 1, 4, 2, 2, 2, 1, 1, 2, 1, 1, 1, 2, 10, 5, 5, 5, 8, 10, 10, 4, 3, 8, 8, 3}, []int{8, 2, 4, 2, 4, 8, 1, 1, 2, 5, 1, 4, 4, 3, 5, 10, 4, 5, 5, 5, 4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 2}, 2, 7, 50, standardBoard) if err != nil { panic(err) } return rs } // Erudit returns the Russian "Эрудит" ruleset. Ё carries no tiles (count 0); fold Ё→Е // when preparing the dictionary (see wordlist.FoldYo). The centre square does not double // the word, there are 3 blanks (each scoring 0), and the all-tiles bonus is 15. func Erudit() *Ruleset { rs, err := FromTemplate("Эрудит", russian(), // а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я []int{1, 3, 2, 3, 2, 1, 0, 5, 5, 1, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 10, 5, 10, 5, 10, 10, 10, 5, 5, 10, 10, 3}, []int{10, 3, 5, 3, 5, 9, 0, 2, 2, 8, 4, 6, 4, 5, 8, 10, 6, 6, 6, 5, 3, 1, 2, 1, 2, 1, 1, 1, 2, 2, 1, 1, 3}, 3, 7, 15, eruditBoard) if err != nil { panic(err) } return rs }