-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathwordlebot.py
296 lines (224 loc) · 8.24 KB
/
wordlebot.py
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
# -*- coding: utf-8 -*-
"""
@author: pheuer
"""
import os
import pandas as pd
import numpy as np
import time
import matplotlib.pyplot as plt
def _load_wordlist(path=os.path.join(os.getcwd(), 'wordlist', 'words.txt')):
"""
Read in the word list from CSV
Word list contains five-letter words and their frequencies
"""
df = pd.read_csv(path, delimiter='\t')
wordlist = [list(row) for row in df.values]
return wordlist
class WordleBot:
def __init__(self, firstword=None, answer=None, verbose=None):
self.wordlist = _load_wordlist()
if firstword is None:
self.word = 'earth'
else:
self.word = firstword
# Used for autoplay
self.answer = answer
self.status = 'playing'
self.quit = False
self.turn = 0
if answer is not None:
if verbose is None:
self.verbose = False
else:
self.verbose = True
while self.quit is False:
self.auto_loop()
else:
self.verbose=True
while self.quit is False:
self.loop()
self.summary()
def printout(self, text):
if self.verbose:
print(text)
def loop(self):
"""
The main game loop
"""
print("*************************************")
print(f"Next word: {self.word}")
# Get user input
result = self.get_input()
self.process_input(result)
# Choose a new word
self.choose_word()
# Increment the turn counter
self.turn += 1
def get_input(self):
"""
Collect results from the current word from the user
"""
no_input=True
while no_input:
user_input = input("g=green, y=yellow, x=black, q=quit "
"n=not in word list, w=won, l=lost\n"
"Input the pattern for the last word:")
# Validate the input string as either a single letter command
# or a 5-letter string of wordle results
no_input=False
if len(user_input) == 1:
if user_input not in 'qnwl':
print("Invalid input")
no_input=True
if len(user_input) == 5:
if not set(user_input).issubset(set('gyx')):
print("Invalid input")
no_input=True
return user_input
def process_input(self, user_input):
"""
Take input results (as a string of characters)
"""
# Go through the input letter-by-letter
for i in range(len(user_input)):
letter = self.word[i]
result = user_input[i]
if result == 'g':
# Eliminate words that don't have this letter at this location
self.wordlist = [s for s in self.wordlist if s[0][i]==letter]
elif result == 'y':
# Eliminate words that don't contain this letter
self.wordlist = [s for s in self.wordlist if letter in set(s[0])]
# Eliminate words that have this letter at this location
self.wordlist = [s for s in self.wordlist if s[0][i]!=letter]
elif result == 'x':
# Eliminate words that contain this letter
self.wordlist = [s for s in self.wordlist if letter not in set(s[0])]
elif result == 'n':
# Remove this word from the word list and move on
self.wordlist = [s for s in self.wordlist if s is not self.word]
break
elif result == 'q':
# Quit the loop
self.quit=True
elif result == 'w':
self.status='won'
self.quit=True
elif result == 'l':
self.status = 'lost'
self.quit=True
def choose_word(self):
"""
Choose the next word to play
"""
wordlist = np.array(self.wordlist)
# Arrays of the words and their frequencies
words = wordlist[:,0]
freq = wordlist[:,1].astype('int64')
# An array of the number of unique letters in each word
unique_letters = np.array([len(set(s)) for s in words])
if self.turn <= 2:
fitness = freq*unique_letters
else:
fitness = freq
i = np.argmax(fitness)
self.word = words[i]
def get_input_auto(self):
"""
Given an answer word, return the correct wordle string
"""
wordlist = np.array(self.wordlist)
words = set(wordlist[:,0])
# Check if word is in word list
if self.word not in words:
return 'n'
result = ''
for i in range(len(self.word)):
if self.word[i] == self.answer[i]:
result += 'g'
elif self.word[i] in self.answer:
result += 'y'
else:
result += 'x'
if result == 'ggggg':
return 'w'
return result
def auto_loop(self):
"""
The main game loop for automated play
"""
self.printout("*************************************")
self.printout(f"Answer: {self.answer}")
self.printout(f"Word: {self.word}")
# Get user input
result = self.get_input_auto()
self.printout(f"Result: {result}")
self.process_input(result)
# Choose a new word
self.choose_word()
# Increment the turn counter
self.turn += 1
if self.turn >= 7:
self.status = 'lost'
self.quit=True
def summary(self):
if self.status == 'won':
self.printout(f"WORDLEBOT STRIKES AGAIN ({self.turn}/6)")
@property
def score(self):
"""
Returns the score for a completed game
lost -> zero points
won -> 7 - turns
so
(6/6) = 1 point
(4/6) = 3 points
(1/6) = 6 points
"""
if self.status == 'lost':
return 0
elif self.status == 'won':
return 7 - self.turn
else:
return np.nan
def benchmark_wordlebot(num=300):
"""
Test wordlebot on a random sample of 'num' words
"""
# Load the wordlist
wordlist = _load_wordlist()
words = np.array(wordlist)[:,0]
freq = np.array(wordlist)[:,1].astype('int64')
# Draw a distribution of 'num' words
p = freq/np.sum(freq)
i = np.random.choice(np.arange(words.size), size=num, replace=False, p=p)
words = words[i]
# Create an array to store the scores
results = np.zeros(num)
# Run wordlebot auto mode for each answer word
t0 = time.time()
for i, answer in enumerate(words):
if i % 50 == 0:
print(f"Word {i}/{num}")
# Run wordlebot
g = WordleBot(answer=answer)
# Store the score
results[i] = g.turn
elapsed_time = (time.time()- t0)*1e3
time_per_word = elapsed_time/num
total_score = np.mean(results)
print(f"Total score: {total_score:.2f}/6 ({time_per_word:.1f} ms/word)")
fig, ax = plt.subplots(figsize=(6,6))
ax.tick_params(axis='both', labelsize=12)
ax.set_xticks([0,1,2,3,4,5,6])
ax.set_xticklabels(['L', "1/6", "2/6", "3/6", "4/6", "5/6", "6/6"])
ax.hist(results, bins=np.arange(8)-0.5)
ax.set_xlabel("Score", fontsize=16)
ax.set_ylabel("Test words", fontsize=16)
ax.set_title(f"Mean score: {total_score:.2f}/6 on {num} words\n({time_per_word:.1f} ms/word)", fontsize=16)
fig.savefig('results.png', dpi=200)
if __name__ == '__main__':
#g = WordleBot(answer='audio', verbose=True)
g = WordleBot()
#benchmark_wordlebot(num=500)