-
Notifications
You must be signed in to change notification settings - Fork 0
/
words.lua
357 lines (283 loc) · 9.31 KB
/
words.lua
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
-- Help us solve Wordle. Perhaps
local dict = require('dict')
local Words = {}
Words.__index = Words
-- Define a new list of possible words, given as an `array`.
-- The default scoring function is `highestScoringWords()`, but this can
-- be changed by setting the optional `scoring_fn`.
--
function Words.new(array, scoring_fn)
local self = {}
self.bestWords = scoring_fn or Words.highestScoringWords
setmetatable(self, Words)
-- words is just a mapping from word to true
local words = {}
for _, word in pairs(array) do
words[word] = true
end
self.words = words
-- freqs is a mapping from letter to its frequency
self.freqs = {}
return self
end
-- Does the list of words contain the given word?
--
function Words:contains(word)
return (not(not self.words[word]))
end
-- Manually rescore all the words. Calculates frequency of each letter
-- and the total score of each word (ie the sum of its lstter frequencies).
-- Note that a letter will only count once in a word.
--
function Words:rescore()
local freqs = {}
for word, _ in pairs(self.words) do
-- First collect the letters - once each
local letters = {}
for i = 1, #word do
local letter = word:sub(i, i)
letters[letter] = true
end
-- Now increment the frequency scores
for letter, _ in pairs(letters) do
local freq = self.freqs[letter]
if freq == nil then
self.freqs[letter] = 1
else
self.freqs[letter] = freq + 1
end
end
end
end
-- What is the frequency of a given letter?
--
function Words.freq(self, letter)
return self.freqs[letter] or 0
end
-- Find the score of a given word. Each letter only counts once.
--
function Words:score(word)
-- First collect the letters
local letters = {}
for i = 1, #word do
letters[word:sub(i, i)] = true
end
-- Now add up the score
local score = 0
for letter, _ in pairs(letters) do
score = score + self.freqs[letter]
end
return score
end
-- Should we keep `subject` given that word `guess` results in `clue`?
-- The `clue` is a string of five characters:
-- `-` means the letter doesn't appear;
-- `a` means the letter appears, but not here (amber);
-- `g` means the letter appears here (green).
-- This is a class method, not an instance method.
--
function Words.keepGiven(subject, guess, clue)
for i = 1, #clue do
local light = clue:sub(i, i)
local letter = guess:sub(i, i)
-- If we have a green light, we'll reject the word if
-- corresponding letters in the subject and the guess
-- are different.
if light == "g" then
if subject:sub(i, i) ~= letter then
return false
end
end
-- If we have an amber light we'll reject the word if
-- the corresponding letters match, or if there's no
-- such letter elsewhere in the subject.
if light == "a" then
if subject:sub(i, i) == letter then
return false
else
local rest = subject:sub(1, i-1) .. subject:sub(i+1, #subject)
if rest:find(letter) == nil then
return false
end
end
end
-- If we have a blank light we'll reject the word if that
-- letter appears somewhere in the subject.
if light == "-" then
if subject:find(letter) ~= nil then
return false
end
end
end
return true
end
-- Given a `guess` and a `clue` eliminate all impossible words.
-- Returns a new instance of the class (with the reduced word list).
--
function Words:eliminateGiven(guess, clue)
local list = {}
for subject, _ in pairs(self.words) do
if Words.keepGiven(subject, guess, clue) then
table.insert(list, subject)
end
end
return Words.new(list)
end
-- Rescore the words and return: a list of the words with the best score,
-- a list of the words with the second-best score.
-- If there is no best or second-best scoring words the relevant lists
-- will be empty.
--
function Words:bestWords()
return self:highestScoringWords()
end
-- Rescore the words and return: a list of the words with the highest score,
-- and a list of the words with the second-highest score.
-- If there is no highest or second-highest scoring words the relevant lists
-- will be empty.
--
function Words:highestScoringWords()
self:rescore()
local highest_score = 0
local second_score = 0
local scores = {}
-- Create a map from score to words with that score,
-- and track the highest and second-highest scores.
for word in pairs(self.words) do
local word_score = self:score(word)
local equal_words = scores[word_score] or {}
table.insert(equal_words, word)
scores[word_score] = equal_words
if word_score > highest_score then
highest_score, second_score = word_score, highest_score
elseif word_score == highest_score then
-- Do nothing
elseif word_score > second_score then
second_score = word_score
end
end
-- Now we have that map, return the highest and second-highest scores and words.
local highest_words = {}
if highest_score > 0 then
highest_words = scores[highest_score]
end
local second_words = {}
if second_score > 0 then
second_words = scores[second_score]
end
return highest_words, second_words
end
-- Like `highestScoringWords`, but selects the lowest scoring and second-lowest
-- scoring words.
--
function Words:lowestScoringWords()
self:rescore()
local lowest_score = nil
local second_score = nil
local scores = {}
-- Create a map from score to words with that score,
-- and track the lowest and second-lowest scores.
for word in pairs(self.words) do
local word_score = self:score(word)
local equal_words = scores[word_score] or {}
table.insert(equal_words, word)
scores[word_score] = equal_words
if lowest_score == nil or word_score < lowest_score then
lowest_score, second_score = word_score, lowest_score
elseif word_score == lowest_score then
-- Do nothing
elseif second_score == nil or word_score < second_score then
second_score = word_score
end
end
-- Now we have that map, return the lowest and second-lowest scores and words.
local lowest_words = {}
if lowest_score ~= nil then
lowest_words = scores[lowest_score]
end
local second_words = {}
if second_score ~= nil then
second_words = scores[second_score]
end
return lowest_words, second_words
end
-- A scoring function to select the best and second best words randomly -
-- three of each
--
function Words:randomWords()
-- We don't want to select words twice, so in order to eliminate selected
-- words from our list we need to start with a copy of our word list and
-- use that to choose and eliminate from.
local words = {}
for word, _ in pairs(self.words) do
table.insert(words, word)
end
-- Select up to 3 words for the first list to return, and up to 3 words
-- for the second list.
local first_list = {}
for i = 1, math.min(#words, 3) do
local index = math.random(#words)
if #words == 1 then
index = 1
end
table.insert(first_list, words[index])
table.remove(words, index)
end
local second_list = {}
for i = 1, math.min(#words, 3) do
local index = math.random(#words)
if #words == 1 then
index = 1
end
table.insert(second_list, words[index])
table.remove(words, index)
end
return first_list, second_list
end
-- Pick the best word, or nil if there is none.
--
function Words:bestWord()
local best_words, second_words = self:bestWords()
if #best_words == nil then
return nil
else
return best_words[1]
end
end
-- Run the helper. The default scoring function can be changed by setting the
-- optional `scoring_fn`.
--
function Words.run(scoring_fn)
local words = Words.new(dict, scoring_fn)
local count = 1
while true do
io.write("\n" .. count .. " -------------\n")
-- Print the recommended words from best two categories
local best_words, second_words = words:bestWords()
if #best_words > 0 then
io.write("\n Recommended:\n")
for i = 1, #best_words do
io.write(" " .. best_words[i] .. "\n")
end
else
io.write("\nNo words to suggest!\n")
return
end
if #second_words > 0 then
io.write(" Also:\n")
for i = 1, #second_words do
io.write(" " .. second_words[i] .. "\n")
end
end
-- Get feedback and recalculate options
io.write("\n")
io.write(" Enter your guess: ")
local guess = io.read("*l")
io.write(" Enter the clue..: ")
local clue = io.read("*l")
words = words:eliminateGiven(guess, clue)
count = count + 1
end
end
-------------------------------
return Words