/
main.go
283 lines (252 loc) · 7.21 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"sort"
"strings"
"github.com/shiblon/findwords/board"
"github.com/shiblon/findwords/index"
)
// Flags
var (
recognizerFile = flag.String("recognizer", "dicts/wordswithfriends.mealy",
"Serialized Mealy machine to use as a word recognizer.")
)
// Directions
const (
RIGHT = iota
DOWN
)
// Produce all valid "left" indices for a particular search query.
// What determines whether an index is valid is whether it can form the start
// of a complete word. This basically means "any cell with nothing on the
// left".
//
// A cell that has *something* on the left of it cannot form the beginning of a
// word because it must have a prefix (the stuff on the left) to be a word.
//
// Given a query like ..AD.., this will produce the indices 0, 1, 2, 5
// (because indices 3 and 4 are 'D' and the '.' after it, which can't be the
// beginning of a word, but index 2 can, since a word can start with 'AD' in
// this scenario).
func GetSubSuffixes(info index.AllowedInfo) <-chan int {
out := make(chan int)
emit := func(left int) {
if info.AnchoredSubSequence(left, len(info.Draws)) {
out <- left
}
}
go func() {
defer close(out)
for left := 0; left < len(info.Constraints); left++ {
emit(left)
for left < len(info.Constraints) && !info.Draws[left] {
left++
}
}
}()
return out
}
// Endpoints, left inclusive, right exclusive: [left, right)
type Endpoints struct {
left int
right int
}
// Get all allowed subconstraints from an initial constraint.
//
// This constitutes peeling off from the left and right whatever can be peeled
// off to form a new sub constraint.
func GetSubConstraints(info index.AllowedInfo) <-chan Endpoints {
out := make(chan Endpoints)
emit := func(left, right int) {
if info.AnchoredSubSequence(left, right) {
out <- Endpoints{left, right}
}
}
go func() {
defer close(out)
for left := range GetSubSuffixes(info) {
emit(left, len(info.Constraints))
for right := left + 1; right < len(info.Constraints); right++ {
// We can't peel off a fixed tile - it must form part of the
// word.
if !info.Draws[right] {
continue
}
emit(left, right)
}
}
}()
return out
}
// Create a map from byte to count, given a string containing all tiles
// (including '.' for blank tiles).
func TileCounts(available string) (counts map[byte]int) {
counts = make(map[byte]int)
for _, c := range strings.ToUpper(available) {
counts[byte(c)]++
}
return
}
type foundword struct {
word string
line int
direction int
start int
score int
blanks []int
}
func (self foundword) String() string {
switch self.direction {
case RIGHT:
return fmt.Sprintf("%s (%d): %d, %d across, %v",
self.word, self.score, self.line, self.start, self.blanks)
case DOWN:
return fmt.Sprintf("%s (%d): %d, %d down, %v",
self.word, self.score, self.start, self.line, self.blanks)
}
return fmt.Sprintf("%s (%d): direction? line %d, start %d, %v",
self.word, self.score, self.line, self.start, self.blanks)
}
type foundwords []foundword
func (self foundwords) Len() int {
return len(self)
}
func (self foundwords) Less(a, b int) bool {
// Sort by score.
return self[a].score < self[b].score
}
func (self foundwords) Swap(a, b int) {
self[a], self[b] = self[b], self[a]
}
// Given a line-oriented query (find me a word that matches this sort of line),
// produce all valid words on that line.
func LineWords(line, direction int, idx index.Index, lineQuery []string, available map[byte]int) <-chan foundword {
allowed := idx.GetAllowedLetters(lineQuery, available)
out := make(chan foundword)
go func() {
defer close(out)
for left := range GetSubSuffixes(allowed) {
subinfo := allowed.MakeSuffix(left)
if !subinfo.PossiblePrefix() {
continue
}
for seq := range idx.ConstrainedSequences(subinfo) {
out <- foundword{
word: string(seq),
start: left,
line: line,
direction: direction,
}
}
}
}()
return out
}
// Given a blank board, find all words we can play with our given tiles, from the center.
//
// Since the center spot is always required, we just always start our words
// from there. No word will ever be long enough to go off the edge of the
// board, since that is 8 tiles away and we only ever have 7.
//
// Also, direction is not important because the board has 4-way symmetry. So,
// we always choose 7,7,RIGHT.
func InitialWords(idx index.Index, available map[byte]int) <-chan foundword {
allowed := index.NewUnanchoredAllowedInfo(
[]string{".", ".", ".", ".", ".", ".", "."},
[]bool{true, true, true, true, true, true, true},
available)
out := make(chan foundword)
go func() {
defer close(out)
for left := 0; left < 7; left++ {
subinfo := allowed.MakeSuffix(left)
for seq := range idx.ConstrainedSequences(subinfo) {
out <- foundword{
word: string(seq),
start: 7,
line: 7,
direction: RIGHT,
}
}
}
}()
return out
}
// Get all words that can be formed on this board with the available tiles, and
// score all of them.
//
// Empty boards are also allowed, in which case all words formable with the
// available tiles are used, starting at 7,7 and going to the right.
func BoardWords(board board.Board, idx index.Index, available map[byte]int) <-chan foundword {
out := make(chan foundword)
go func() {
defer close(out)
if board.IsEmpty() {
for found := range InitialWords(idx, available) {
found.score = board.ScoreRowPlacement(7, found.start, found.word)
out <- found
}
return
}
for row := 0; row < 15; row++ {
q := board.RowQuery(row)
for found := range LineWords(row, RIGHT, idx, q, available) {
// TODO: Here and below, find all possible placements of blanks
// for this word, and produce all such words, scoring them
// differently (a blank on a TL tile is different than a real
// letter).
found.score = board.ScoreRowPlacement(row, found.start, found.word)
out <- found
}
}
for col := 0; col < 15; col++ {
q := board.ColQuery(col)
for found := range LineWords(col, DOWN, idx, q, available) {
found.score = board.ScoreColPlacement(col, found.start, found.word)
out <- found
}
}
}()
return out
}
func main() {
flag.Parse()
// Read the recognizer.
fmt.Print("Reading recognizer...")
rfile, err := os.Open(*recognizerFile)
if err != nil {
log.Fatalf("Failed to open '%v': %v", *recognizerFile, err)
}
idx, err := index.ReadFrom(rfile)
if err != nil {
log.Fatalf("Failed to read recognizer from '%v': %v", *recognizerFile, err)
}
fmt.Println("DONE")
// Read the board.
boardbytes, err := ioutil.ReadFile(flag.Arg(0))
if err != nil {
log.Fatalf("Could not load board file '%v': %v", flag.Arg(0), err)
}
board := board.NewFromString(string(boardbytes))
available := TileCounts(flag.Arg(1))
// Show what board and tiles we are working with.
fmt.Println(board)
for k, v := range available {
fmt.Printf("%v: %v ", string(k), v)
}
fmt.Println()
// Get and score all words:
allwords := make([]foundword, 0, 500)
for word := range BoardWords(board, idx, available) {
allwords = append(allwords, word)
}
// Sort by score, ascending.
sort.Sort(foundwords(allwords))
for _, w := range allwords {
fmt.Println(w)
}
}