Author SHA1 Message Date
cblanken 0cdd3895ee Add multiprocessing for board solving 2022-09-01 12:42:02 -04:00
cblanken 50d4625b84 Add misc board files and rename dice file 2022-09-01 08:40:06 -04:00
cblanken c88f657113 Add README 2022-08-31 19:44:02 -04:00
cblanken 14a813ed74 Update boards dir with boards in csv format 2022-08-31 14:44:49 -04:00
cblanken 46dd069cfd Track words found in WordTree(s) for easier display 2022-08-31 14:28:30 -04:00
cblanken 31a307e83d Remove old boggler implementation
Update boggler cli to use WordTree implementation
2022-08-31 10:37:02 -04:00
cblanken bc4922dc55 Add printable board 2022-08-31 09:56:47 -04:00
cblanken 5f9a0d1990 Refactor to account for multi-letter nodes
Updates WordTree and WordNodes to build trees with WordNodes containing
blocks that have multiple letters such as "Qu".
2022-08-30 18:00:04 -04:00
cblanken dc532c1fdd Update board_randomizer.py to generate csv board files 2022-08-29 15:21:46 -04:00
cblanken 0f5e5af5ae Update boggler_utils main to accept board.csv file 2022-08-29 15:20:49 -04:00
cblanken 28b3d56880 Update dictionary type hinting 2022-08-26 19:51:52 -04:00
cblanken 79486d4217 Update board_randomizer and dice file 2022-08-26 19:37:32 -04:00
cblanken 6d6d56d1cb Add function to solve entire boggle board 2022-08-26 19:31:49 -04:00
cblanken 88b43d0679 Update WordTree and WordNode to use @property decorators 2022-08-26 16:12:11 -04:00
21 changed files with 706 additions and 260 deletions
+396
View File
@@ -0,0 +1,396 @@
# Boggler
A solver for the popular word game Boggle.
# Install
```bash
$ git clone https://github.com/cblanken/boggler.git
```
To use the script to solve a particular Boggle board configuration, you'll need to do a few things
1. Create `.csv` of the board state like so:
Here is an example `board.csv`. Note the orientation of the board does not matter.
```console
$ cat boards/b1.csv
s,a,i,p
l,qu,a,y
u,l,l,s
o,w,h,a
```
2. Find a dictionary wordlist file or create your own
The dictionary wordlist should have each word on a single line like so
```console
$ cat wordlists/wordlist.txt
a
aa
aaa
aah
aahed
aahing
aahs
aal
...
zwiebacks
zwieselite
zwinglian
zwinglianism
zwinglianist
zwitter
zwitterion
zwitterionic
```
3. Split the dictionary wordlist into separate files based on the first letter of each word.
To split an English wordlist the `split_wordlist_alpha.sh` script can be used like so:
```console
$ split_wordlist_alpha.sh my_wordlist.txt .
```
This will generate a series of sub-dictionaries for each letter in the alphabet in the current directory.
```console
$ ls
my_wordlist.txt words_d.txt words_h.txt words_l.txt words_p.txt words_t.txt words_x.txt
words_a.txt words_e.txt words_i.txt words_m.txt words_q.txt words_u.txt words_y.txt
words_b.txt words_f.txt words_j.txt words_n.txt words_r.txt words_v.txt words_z.txt
words_c.txt words_g.txt words_k.txt words_o.txt words_s.txt words_w.txt
```
# Usage
```console
$ python boggler.py
Usage: python3 boggler.py <BOARD_FILE> <WORDLISTS_DIR> [MAX_WORD_LENGTH]
$ python boggler.py board.csv wordlists/ 15
Reading in wordlists...
>> d: words_d.txt
>> e: words_e.txt
>> f: words_f.txt
>> m: words_m.txt
>> n: words_n.txt
>> o: words_o.txt
>> qu: words_q.txt
>> r: words_r.txt
>> s: words_s.txt
>> v: words_v.txt
>> y: words_y.txt
Generating WordTrees...
>> (0, 0): y
>> (0, 1): e
>> (0, 2): o
>> (0, 3): s
>> (1, 0): r
>> (1, 1): e
>> (1, 2): o
>> (1, 3): v
>> (2, 0): d
>> (2, 1): f
>> (2, 2): e
>> (2, 3): y
>> (3, 0): n
>> (3, 1): m
>> (3, 2): e
>> (3, 3): qu
BOARD
+---------------+
| Y | E | O | S |
+---------------+
| R | E | O | V |
+---------------+
| D | F | E | Y |
+---------------+
| N | M | E |QU |
+---------------+
Starting @ (0, 0)...
y : [(0, 0)]
yr : [(0, 0), (1, 0)]
ye : [(0, 0), (1, 1)]
yee : [(0, 0), (1, 1), (0, 1)]
yeo : [(0, 0), (1, 1), (0, 2)]
yed : [(0, 0), (1, 1), (2, 0)]
yee : [(0, 0), (1, 1), (2, 2)]
yer : [(0, 0), (1, 1), (1, 0)]
yere : [(0, 0), (1, 1), (1, 0), (0, 1)]
yerd : [(0, 0), (1, 1), (1, 0), (2, 0)]
yeo : [(0, 0), (1, 1), (1, 2)]
ye : [(0, 0), (0, 1)]
yee : [(0, 0), (0, 1), (1, 1)]
yer : [(0, 0), (0, 1), (1, 0)]
yerd : [(0, 0), (0, 1), (1, 0), (2, 0)]
yere : [(0, 0), (0, 1), (1, 0), (1, 1)]
yeo : [(0, 0), (0, 1), (1, 2)]
yeo : [(0, 0), (0, 1), (0, 2)]
Starting @ (0, 1)...
e : [(0, 1)]
ee : [(0, 1), (1, 1)]
eer : [(0, 1), (1, 1), (1, 0)]
eery : [(0, 1), (1, 1), (1, 0), (0, 0)]
er : [(0, 1), (1, 0)]
erd : [(0, 1), (1, 0), (2, 0)]
erf : [(0, 1), (1, 0), (2, 1)]
ere : [(0, 1), (1, 0), (1, 1)]
eo : [(0, 1), (1, 2)]
eos : [(0, 1), (1, 2), (0, 3)]
eof : [(0, 1), (1, 2), (2, 1)]
ey : [(0, 1), (0, 0)]
eyr : [(0, 1), (0, 0), (1, 0)]
eyre : [(0, 1), (0, 0), (1, 0), (1, 1)]
eye : [(0, 1), (0, 0), (1, 1)]
eyed : [(0, 1), (0, 0), (1, 1), (2, 0)]
eyer : [(0, 1), (0, 0), (1, 1), (1, 0)]
eo : [(0, 1), (0, 2)]
eos : [(0, 1), (0, 2), (0, 3)]
Starting @ (0, 2)...
o : [(0, 2)]
oos : [(0, 2), (1, 2), (0, 3)]
oof : [(0, 2), (1, 2), (2, 1)]
oe : [(0, 2), (1, 1)]
oer : [(0, 2), (1, 1), (1, 0)]
ovey : [(0, 2), (1, 3), (2, 2), (2, 3)]
oe : [(0, 2), (0, 1)]
oer : [(0, 2), (0, 1), (1, 0)]
os : [(0, 2), (0, 3)]
Starting @ (0, 3)...
s : [(0, 3)]
sv : [(0, 3), (1, 3)]
so : [(0, 3), (1, 2)]
sooey : [(0, 3), (1, 2), (0, 2), (1, 1), (0, 0)]
sooey : [(0, 3), (1, 2), (0, 2), (0, 1), (0, 0)]
soe : [(0, 3), (1, 2), (0, 1)]
soe : [(0, 3), (1, 2), (2, 2)]
sofer : [(0, 3), (1, 2), (2, 1), (1, 1), (1, 0)]
soy : [(0, 3), (1, 2), (2, 3)]
soe : [(0, 3), (1, 2), (1, 1)]
sov : [(0, 3), (1, 2), (1, 3)]
so : [(0, 3), (0, 2)]
sooey : [(0, 3), (0, 2), (1, 2), (0, 1), (0, 0)]
sooey : [(0, 3), (0, 2), (1, 2), (2, 2), (2, 3)]
sooey : [(0, 3), (0, 2), (1, 2), (1, 1), (0, 0)]
soe : [(0, 3), (0, 2), (1, 1)]
sov : [(0, 3), (0, 2), (1, 3)]
soe : [(0, 3), (0, 2), (0, 1)]
Starting @ (1, 0)...
r : [(1, 0)]
rye : [(1, 0), (0, 0), (1, 1)]
rye : [(1, 0), (0, 0), (0, 1)]
re : [(1, 0), (0, 1)]
ree : [(1, 0), (0, 1), (1, 1)]
reef : [(1, 0), (0, 1), (1, 1), (2, 1)]
reed : [(1, 0), (0, 1), (1, 1), (2, 0)]
rd : [(1, 0), (2, 0)]
rf : [(1, 0), (2, 1)]
re : [(1, 0), (1, 1)]
ree : [(1, 0), (1, 1), (0, 1)]
ref : [(1, 0), (1, 1), (2, 1)]
red : [(1, 0), (1, 1), (2, 0)]
ree : [(1, 0), (1, 1), (2, 2)]
reem : [(1, 0), (1, 1), (2, 2), (3, 1)]
reef : [(1, 0), (1, 1), (2, 2), (2, 1)]
Starting @ (1, 1)...
e : [(1, 1)]
ee : [(1, 1), (0, 1)]
eer : [(1, 1), (0, 1), (1, 0)]
eery : [(1, 1), (0, 1), (1, 0), (0, 0)]
ey : [(1, 1), (0, 0)]
eyr : [(1, 1), (0, 0), (1, 0)]
eyre : [(1, 1), (0, 0), (1, 0), (0, 1)]
eye : [(1, 1), (0, 0), (0, 1)]
eyer : [(1, 1), (0, 0), (0, 1), (1, 0)]
eo : [(1, 1), (0, 2)]
eos : [(1, 1), (0, 2), (0, 3)]
ef : [(1, 1), (2, 1)]
ed : [(1, 1), (2, 0)]
ee : [(1, 1), (2, 2)]
er : [(1, 1), (1, 0)]
ere : [(1, 1), (1, 0), (0, 1)]
erd : [(1, 1), (1, 0), (2, 0)]
erf : [(1, 1), (1, 0), (2, 1)]
eo : [(1, 1), (1, 2)]
eos : [(1, 1), (1, 2), (0, 3)]
eof : [(1, 1), (1, 2), (2, 1)]
Starting @ (1, 2)...
o : [(1, 2)]
oos : [(1, 2), (0, 2), (0, 3)]
oe : [(1, 2), (0, 1)]
oer : [(1, 2), (0, 1), (1, 0)]
os : [(1, 2), (0, 3)]
oe : [(1, 2), (2, 2)]
of : [(1, 2), (2, 1)]
ofer : [(1, 2), (2, 1), (1, 1), (1, 0)]
oy : [(1, 2), (2, 3)]
oe : [(1, 2), (1, 1)]
oer : [(1, 2), (1, 1), (1, 0)]
ovey : [(1, 2), (1, 3), (2, 2), (2, 3)]
Starting @ (1, 3)...
v : [(1, 3)]
vs : [(1, 3), (0, 3)]
vo : [(1, 3), (0, 2)]
voe : [(1, 3), (0, 2), (1, 1)]
voe : [(1, 3), (0, 2), (0, 1)]
vee : [(1, 3), (2, 2), (1, 1)]
veer : [(1, 3), (2, 2), (1, 1), (1, 0)]
veery : [(1, 3), (2, 2), (1, 1), (1, 0), (0, 0)]
vee : [(1, 3), (2, 2), (3, 2)]
vefry : [(1, 3), (2, 2), (2, 1), (1, 0), (0, 0)]
vo : [(1, 3), (1, 2)]
voe : [(1, 3), (1, 2), (0, 1)]
voe : [(1, 3), (1, 2), (2, 2)]
voe : [(1, 3), (1, 2), (1, 1)]
Starting @ (2, 0)...
d : [(2, 0)]
dr : [(2, 0), (1, 0)]
dry : [(2, 0), (1, 0), (0, 0)]
dree : [(2, 0), (1, 0), (0, 1), (1, 1)]
drey : [(2, 0), (1, 0), (0, 1), (0, 0)]
dree : [(2, 0), (1, 0), (1, 1), (0, 1)]
drey : [(2, 0), (1, 0), (1, 1), (0, 0)]
dree : [(2, 0), (1, 0), (1, 1), (2, 2)]
de : [(2, 0), (1, 1)]
dee : [(2, 0), (1, 1), (0, 1)]
deer : [(2, 0), (1, 1), (0, 1), (1, 0)]
dey : [(2, 0), (1, 1), (0, 0)]
def : [(2, 0), (1, 1), (2, 1)]
dee : [(2, 0), (1, 1), (2, 2)]
deem : [(2, 0), (1, 1), (2, 2), (3, 1)]
der : [(2, 0), (1, 1), (1, 0)]
dere : [(2, 0), (1, 1), (1, 0), (0, 1)]
derf : [(2, 0), (1, 1), (1, 0), (2, 1)]
dn : [(2, 0), (3, 0)]
dm : [(2, 0), (3, 1)]
Starting @ (2, 1)...
f : [(2, 1)]
fe : [(2, 1), (1, 1)]
fee : [(2, 1), (1, 1), (0, 1)]
feer : [(2, 1), (1, 1), (0, 1), (1, 0)]
fey : [(2, 1), (1, 1), (0, 0)]
feyer : [(2, 1), (1, 1), (0, 0), (0, 1), (1, 0)]
fed : [(2, 1), (1, 1), (2, 0)]
fedn : [(2, 1), (1, 1), (2, 0), (3, 0)]
fee : [(2, 1), (1, 1), (2, 2)]
fer : [(2, 1), (1, 1), (1, 0)]
fere : [(2, 1), (1, 1), (1, 0), (0, 1)]
ferd : [(2, 1), (1, 1), (1, 0), (2, 0)]
fr : [(2, 1), (1, 0)]
fry : [(2, 1), (1, 0), (0, 0)]
free : [(2, 1), (1, 0), (0, 1), (1, 1)]
freed : [(2, 1), (1, 0), (0, 1), (1, 1), (2, 0)]
frey : [(2, 1), (1, 0), (0, 1), (0, 0)]
free : [(2, 1), (1, 0), (1, 1), (0, 1)]
frey : [(2, 1), (1, 0), (1, 1), (0, 0)]
fred : [(2, 1), (1, 0), (1, 1), (2, 0)]
free : [(2, 1), (1, 0), (1, 1), (2, 2)]
fo : [(2, 1), (1, 2)]
foo : [(2, 1), (1, 2), (0, 2)]
foe : [(2, 1), (1, 2), (0, 1)]
foe : [(2, 1), (1, 2), (2, 2)]
foy : [(2, 1), (1, 2), (2, 3)]
foe : [(2, 1), (1, 2), (1, 1)]
fm : [(2, 1), (3, 1)]
fn : [(2, 1), (3, 0)]
fe : [(2, 1), (3, 2)]
fee : [(2, 1), (3, 2), (2, 2)]
fey : [(2, 1), (3, 2), (2, 3)]
fem : [(2, 1), (3, 2), (3, 1)]
feme : [(2, 1), (3, 2), (3, 1), (2, 2)]
fe : [(2, 1), (2, 2)]
fee : [(2, 1), (2, 2), (1, 1)]
feed : [(2, 1), (2, 2), (1, 1), (2, 0)]
feer : [(2, 1), (2, 2), (1, 1), (1, 0)]
feere : [(2, 1), (2, 2), (1, 1), (1, 0), (0, 1)]
fee : [(2, 1), (2, 2), (3, 2)]
fem : [(2, 1), (2, 2), (3, 1)]
feme : [(2, 1), (2, 2), (3, 1), (3, 2)]
fey : [(2, 1), (2, 2), (2, 3)]
Starting @ (2, 2)...
e : [(2, 2)]
eo : [(2, 2), (1, 2)]
eos : [(2, 2), (1, 2), (0, 3)]
eof : [(2, 2), (1, 2), (2, 1)]
ee : [(2, 2), (1, 1)]
eer : [(2, 2), (1, 1), (1, 0)]
eery : [(2, 2), (1, 1), (1, 0), (0, 0)]
evoe : [(2, 2), (1, 3), (0, 2), (1, 1)]
evoe : [(2, 2), (1, 3), (0, 2), (0, 1)]
evoe : [(2, 2), (1, 3), (1, 2), (0, 1)]
evoe : [(2, 2), (1, 3), (1, 2), (1, 1)]
ee : [(2, 2), (3, 2)]
em : [(2, 2), (3, 1)]
emf : [(2, 2), (3, 1), (2, 1)]
eme : [(2, 2), (3, 1), (3, 2)]
ef : [(2, 2), (2, 1)]
ey : [(2, 2), (2, 3)]
eye : [(2, 2), (2, 3), (3, 2)]
Starting @ (2, 3)...
y : [(2, 3)]
yo : [(2, 3), (1, 2)]
yoe : [(2, 3), (1, 2), (0, 1)]
yoe : [(2, 3), (1, 2), (2, 2)]
yoe : [(2, 3), (1, 2), (1, 1)]
yquem : [(2, 3), (3, 3), (2, 2), (3, 1)]
yquem : [(2, 3), (3, 3), (3, 2), (3, 1)]
ye : [(2, 3), (3, 2)]
yee : [(2, 3), (3, 2), (2, 2)]
ye : [(2, 3), (2, 2)]
yeo : [(2, 3), (2, 2), (1, 2)]
yee : [(2, 3), (2, 2), (1, 1)]
yee : [(2, 3), (2, 2), (3, 2)]
Starting @ (3, 0)...
n : [(3, 0)]
nd : [(3, 0), (2, 0)]
nm : [(3, 0), (3, 1)]
Starting @ (3, 1)...
m : [(3, 1)]
mf : [(3, 1), (2, 1)]
mfr : [(3, 1), (2, 1), (1, 0)]
mfd : [(3, 1), (2, 1), (2, 0)]
md : [(3, 1), (2, 0)]
me : [(3, 1), (2, 2)]
meo : [(3, 1), (2, 2), (1, 2)]
mee : [(3, 1), (2, 2), (1, 1)]
meed : [(3, 1), (2, 2), (1, 1), (2, 0)]
meer : [(3, 1), (2, 2), (1, 1), (1, 0)]
mev : [(3, 1), (2, 2), (1, 3)]
mee : [(3, 1), (2, 2), (3, 2)]
mn : [(3, 1), (3, 0)]
me : [(3, 1), (3, 2)]
mee : [(3, 1), (3, 2), (2, 2)]
Starting @ (3, 2)...
e : [(3, 2)]
ee : [(3, 2), (2, 2)]
ef : [(3, 2), (2, 1)]
ey : [(3, 2), (2, 3)]
eye : [(3, 2), (2, 3), (2, 2)]
em : [(3, 2), (3, 1)]
emf : [(3, 2), (3, 1), (2, 1)]
eme : [(3, 2), (3, 1), (2, 2)]
emeer : [(3, 2), (3, 1), (2, 2), (1, 1), (1, 0)]
Starting @ (3, 3)...
qu : [(3, 3)]
que : [(3, 3), (2, 2)]
queer : [(3, 3), (2, 2), (1, 1), (1, 0)]
queery : [(3, 3), (2, 2), (1, 1), (1, 0), (0, 0)]
quem : [(3, 3), (2, 2), (3, 1)]
queme : [(3, 3), (2, 2), (3, 1), (3, 2)]
quey : [(3, 3), (2, 2), (2, 3)]
que : [(3, 3), (3, 2)]
quey : [(3, 3), (3, 2), (2, 3)]
quem : [(3, 3), (3, 2), (3, 1)]
queme : [(3, 3), (3, 2), (3, 1), (2, 2)]
```
+3 -2
View File
@@ -4,9 +4,10 @@ from random import randint, shuffle
from math import sqrt from math import sqrt
def read_dice_file(path): def read_dice_file(path):
# TODO: Factor in 'qu' die face
'''Return list of die strings from file ignoring all comments (#)''' '''Return list of die strings from file ignoring all comments (#)'''
with open(path, 'r+', encoding="utf-8") as file: with open(path, 'r+', encoding="utf-8") as file:
return [''.join(line.rstrip().split(',')) for line in file.readlines() if line[0] != "#"] return [line.rstrip().split(',') for line in file.readlines() if line[0] != "#"]
def roll_die(die: str): def roll_die(die: str):
'''Return a face of the given die string to simulate rolling a die''' '''Return a face of the given die string to simulate rolling a die'''
@@ -27,4 +28,4 @@ if __name__ == '__main__':
# Format dice rolls for boards file # Format dice rolls for boards file
board_size = int(sqrt(len(rolls))) board_size = int(sqrt(len(rolls)))
for i in range(0, len(dice_strings), board_size): for i in range(0, len(dice_strings), board_size):
print("".join(rolls[i:i+board_size])) print(",".join(rolls[i:i+board_size]))
+4
View File
@@ -0,0 +1,4 @@
s,a,i,p
l,qu,a,y
u,l,l,s
o,w,h,a
1 s a i p
2 l qu a y
3 u l l s
4 o w h a
-4
View File
@@ -1,4 +0,0 @@
aucd
eegh
hios
trea
+4
View File
@@ -0,0 +1,4 @@
t,h,a,e
o,p,j,v
f,e,i,t
n,e,v,i
1 t h a e
2 o p j v
3 f e i t
4 n e v i
+4
View File
@@ -0,0 +1,4 @@
w,a,n,c
j,i,e,u
y,g,o,e
i,l,e,c
1 w a n c
2 j i e u
3 y g o e
4 i l e c
-4
View File
@@ -1,4 +0,0 @@
vdau
ptuo
ohxr
wlhn
+4
View File
@@ -0,0 +1,4 @@
y,o,o,a
p,l,n,v
t,i,o,qu
n,f,u,t
1 y o o a
2 p l n v
3 t i o qu
4 n f u t
-4
View File
@@ -1,4 +0,0 @@
nxaa
vatr
ieba
iurn
+4
View File
@@ -0,0 +1,4 @@
o,d,r,s
a,h,e,s
e,t,i,h
u,r,e,o
1 o d r s
2 a h e s
3 e t i h
4 u r e o
-4
View File
@@ -1,4 +0,0 @@
vdat
pteo
ohnr
wlan
+4
View File
@@ -0,0 +1,4 @@
e,t,p,a
b,m,v,g
c,s,e,n
x,g,d,t
1 e t p a
2 b m v g
3 c s e n
4 x g d t
-5
View File
@@ -1,5 +0,0 @@
nbapi
satrk
iebae
turnl
icqme
+4
View File
@@ -0,0 +1,4 @@
c,e,s,o
e,j,d,h
s,n,r,h
w,c,r,y
1 c e s o
2 e j d h
3 s n r h
4 w c r y
-6
View File
@@ -1,6 +0,0 @@
nbapir
satrkp
iebaez
turnls
icqmet
nynjro
+4
View File
@@ -0,0 +1,4 @@
l,e,e,e
b,k,p,o
h,u,u,i
y,t,t,s
1 l e e e
2 b k p o
3 h u u i
4 y t t s
+4
View File
@@ -0,0 +1,4 @@
y,e,o,s
r,e,o,v
d,f,e,y
n,m,e,qu
1 y e o s
2 r e o v
3 d f e y
4 n m e qu
+4
View File
@@ -0,0 +1,4 @@
e,w,t,m
s,p,m,r
h,y,o,x
y,t,e,w
1 e w t m
2 s p m r
3 h y o x
4 y t e w
+28 -106
View File
@@ -1,111 +1,33 @@
"""Boggler Demo"""
import sys import sys
from dictionary_utils import WordTree from pathlib import Path
from copy import deepcopy from boggler_utils import BoggleBoard, build_full_boggle_tree, read_boggle_file
def read_boggle_file(file):
'''Return list of rows from Boggle board file'''
with open(file, 'r', encoding='utf-8') as file:
return [x.rstrip() for x in file.readlines()]
def read_wordlist(file):
'''Return dictionary of words with associated word count (1 by default)'''
with open(file, 'r', encoding='utf-8') as file:
return {k:1 for k in file.read().split()}
def get_adjacent_indexes(board, row, col):
'''Return adjecency list for matrix (Boggle board) of size `row x col`'''
max_height = len(board) - 1
max_width = len(board[0]) - 1
indexes = []
if row > 0:
indexes.append((row-1, col)) # up
if col > 0:
indexes.append((row-1, col-1)) # up-left
if col < max_width:
indexes.append((row-1, col+1)) # up-right
if row < max_height:
indexes.append((row+1, col)) # down
if col > 0:
indexes.append((row+1, col-1)) # down-left
if col < max_width:
indexes.append((row+1, col+1)) # down-right
if col > 0:
indexes.append((row, col-1)) # left
if col < max_width:
indexes.append((row, col+1)) # right
return indexes
def find_words_by_index(board, wordlist, row, col, depth, adjacent_indexes, prefix = '', path = [], found_words = {}, prefixes = None, max_depth = 5, prefix_len = 0):
if depth == max_depth:
return {}
if prefixes != None and depth == prefix_len and prefix not in prefixes:
return {}
prefix = prefix + board[row][col]
found_words = deepcopy(found_words)
path = deepcopy(path)
path.append((row, col))
if prefix in wordlist:
found_words[prefix] = 1
adjacents = [x for x in adjacent_indexes[(row, col)] if x not in path]
for adj in adjacents:
new_words = find_words_by_index(board,
wordlist,
adj[0],
adj[1],
depth+1,
adjacent_indexes,
prefix,
path,
found_words,
prefixes,
max_depth,
prefix_len)
for k, v in new_words.items():
if k in wordlist and k in found_words:
# Add and increment word counts
#foundWords[k] = foundWords[k] + v
found_words[k] = found_words[k]
elif k in wordlist.keys():
found_words[k] = v
return found_words
def find_words(board, adjacent_indexes, wordlist_path, max_depth = 5, prefixes = None, prefix_len = 0):
words = {}
for row in range(len(board)):
for col in range(len(board[0])):
# Find words for each row/col starting points
wordlist = read_wordlist(wordlist_path + 'words_' + board[row][col] + '.txt')
newWords = find_words_by_index(board, wordlist, row, col, 0, adjacent_indexes, prefixes = prefixes, max_depth = max_depth, prefix_len = prefix_len)
for w,v in newWords.items():
# Add and increment word counts
words[w] = words[w] + v if w in words.keys() else v
return words
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) != 5: if len(sys.argv) < 3 or len(sys.argv) > 4:
print('Usage: python3 blogger.py <BOARD_FILE> <MAX_WORD_LENGTH> <PREFIX_FILE> <WORDLISTS>') print('Usage: python3 boggler.py <BOARD_FILE> <WORDLISTS_DIR> [MAX_WORD_LENGTH]')
else: sys.exit(1)
board = read_boggle_file(sys.argv[1])
print(board)
adjacentIndexes = {}
for r in range(len(board)):
for c in range(len(board)):
adjacentIndexes[(r,c)] = get_adjacent_indexes(board, r, c)
adjacentLetters = { board = read_boggle_file(Path(sys.argv[1]))
k:[board[pos[0]][pos[1]] for pos in v] for (k,v) in adjacentIndexes.items() if len(sys.argv) == 3:
} boggle_board = BoggleBoard(board)
print(boggle_board)
boggle_tree = build_full_boggle_tree(boggle_board, Path(sys.argv[2]))
elif len(sys.argv) == 4:
try :
boggle_board = BoggleBoard(board, int(sys.argv[3]))
print(boggle_board)
boggle_tree = build_full_boggle_tree(boggle_board, Path(sys.argv[2]))
maxSize = int(sys.argv[2], 10) print("\nBOARD")
prefixes = read_wordlist(sys.argv[3]) print(boggle_board)
words = find_words(board, adjacentIndexes, sys.argv[4], maxSize, prefixes, 4)
for w in words: for start_pos, tree in boggle_tree.items():
print(w) print(f"\nStarting @ {start_pos}...")
for word in tree.word_paths:
print(f"{word[0]: <{boggle_board.max_word_len}}: {word[1]}")
except ValueError as e:
print("The [MAX_WORD_LENGTH] argument must be an integer.")
print("Please try again.")
sys.exit(1)
+236 -121
View File
@@ -1,15 +1,18 @@
'''Boggler Utils''' '''Boggler Utils'''
from __future__ import annotations from __future__ import annotations
from os import path
from multiprocessing import Pool from multiprocessing import Pool
import sys
class BoardCell: class BoardCell:
'''Boggle Board cell''' '''Boggle Board cell'''
def __init__(self, row: int, col: int, letter: str, adjacent_cells: list[BoardCell] = None) -> BoardCell: def __init__(self, row: int, col: int, letters: str,
adjacent_cells: list[BoardCell] = None) -> BoardCell:
self.__row: int = row self.__row: int = row
self.__col: int = col self.__col: int = col
self.__pos: (int, int) = (self.__row, self.__col) self.__pos: (int, int) = (self.__row, self.__col)
self.__letter: str = letter self.__letters: str = letters
self.__adjacent_cells: list[BoardCell] = adjacent_cells self.__adjacent_cells: list[BoardCell] = adjacent_cells
@property @property
@@ -28,9 +31,9 @@ class BoardCell:
return self.__pos return self.__pos
@property @property
def letter(self) -> str: def letters(self) -> str:
'''Getter for letter property''' '''Getter for letter property'''
return self.__letter return self.__letters
@property @property
def adjacent_cells(self) -> list[BoardCell]: def adjacent_cells(self) -> list[BoardCell]:
@@ -42,31 +45,48 @@ class BoardCell:
self.__adjacent_cells = value self.__adjacent_cells = value
def __str__(self): def __str__(self):
return f"({self.__row}, {self.__col}: {self.__letter} | {self.__adjacent_cells})" return f"({self.__row}, {self.__col}): {self.__letters}"
def __repr__(self):
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
class BoggleBoard: class BoggleBoard:
'''Boggle board structure''' '''Boggle board structure'''
def __init__(self, board: list[list[str]], max_word_len: int = 14) -> BoggleBoard: def __init__(self, board: list[list[str]], max_word_len: int = 14) -> BoggleBoard:
self.__height: int = len(board) self.__height: int = len(board)
self.__width: int = len(board[0]) if self.__height > 0 else 0 self.__width: int = len(board[0]) if self.__height > 0 else 0
self.__board_list = board
self.__board: dict[(int, int), BoardCell] = {} self.__board: dict[(int, int), BoardCell] = {}
# max word length is limited by size of the board # Max word length is limited by size of the board
self.__max_word_len = min(max_word_len, self.__width * self.__height) self.__max_word_len = min(max_word_len, self.__width * self.__height)
# Generate BoardCell for each position on the board # Generate BoardCell for each position on the board
for row in range(0, self.__height): for row in range(0, self.__height):
for col in range(0, self.__width): for col in range(0, self.__width):
#self.adjacency_dict[(row, col)] = self.__get_adjacent_indexes(row, col)
self.__board[(row, col)] = BoardCell(row, col, board[row][col]) self.__board[(row, col)] = BoardCell(row, col, board[row][col])
# Update adjacent cell references for each BoardCell # Update adjacent cell references for each BoardCell
for cell in self.__board.values(): for cell in self.__board.values():
adjacent_indexes = self.__get_adjacent_indexes(cell.row, cell.col) adjacent_indexes = self.__get_adjacent_indexes(cell.row, cell.col)
#print(cell)
#print(adjacent_indexes)
cell.adjacent_cells = [self.__board[(x[0], x[1])] for x in adjacent_indexes] cell.adjacent_cells = [self.__board[(x[0], x[1])] for x in adjacent_indexes]
def __str__(self):
flattened_board_list = [y for x in self.__board_list for y in x]
max_len = len(max(flattened_board_list, key=len))
max_len = max_len + 1 if max_len % 2 == 0 else max_len + 2 # keep header_len odd
header_len = self.__width * (max_len + 1) - 1
head = "-" * header_len
header = f"+{head}+\n"
body = ""
for row in self.__board_list:
body += "|"
for col in row:
body += f"{col.upper(): ^{max_len}}|"
body += "\n"
body += header
return f"{header}{body}"
@property @property
def height(self) -> int: def height(self) -> int:
'''Getter for height property''' '''Getter for height property'''
@@ -76,12 +96,14 @@ class BoggleBoard:
def width(self) -> int: def width(self) -> int:
'''Getter for width property''' '''Getter for width property'''
return self.__width return self.__width
@property @property
def max_word_len(self) -> int: def max_word_len(self) -> int:
'''Getter for maximum word length property''' '''Getter for maximum word length property'''
return self.__max_word_len return self.__max_word_len
@property @property
def board(self) -> dict((int, int), BoardCell): def board(self) -> dict[(int, int), BoardCell]:
'''Getter for board property''' '''Getter for board property'''
return self.__board return self.__board
@@ -112,16 +134,46 @@ class BoggleBoard:
class WordNode: class WordNode:
'''A node describing a single letter in a WordTree.''' '''A node describing a single letter in a WordTree.'''
def __init__(self, char: str, is_word: bool, parent: WordNode = None, children: dict[WordNode] = None, board_pos = None) -> WordNode: def __init__(self, letters: str, is_word: bool = False, parent: WordNode = None,
self.char = char children: dict[str, WordNode] = None, board_pos = None) -> WordNode:
self.is_word = is_word self.__letters = letters
self.children = children if children is not None else {} self.__is_word = is_word
self.parent = parent self.__children = children if children is not None else {}
self.board_pos = board_pos self.__parent = parent
self.__board_pos = board_pos
@property
def letters(self) -> str:
'''Getter for letter property'''
return self.__letters
@property
def is_word(self) -> bool:
'''Getter for is_word property'''
return self.__is_word
@is_word.setter
def is_word(self, value):
self.__is_word = value
@property
def children(self) -> dict[str, WordNode]:
'''Getter for children property'''
return self.__children
@property
def parent(self) -> WordNode:
'''Getter for parent property'''
return self.__parent
@property
def board_pos(self) -> (int, int):
'''Getter for board_pos property'''
return self.__board_pos
def add_child_node(self, node): def add_child_node(self, node):
'''Add child node to `children` dictionary, indexed by the nodes' `char`''' '''Add child node to `children` dictionary, indexed by the nodes' `letter`'''
self.children[node.char] = node self.children[node.letters] = node
@property @property
def path(self) -> list[WordNode]: def path(self) -> list[WordNode]:
@@ -135,72 +187,147 @@ class WordNode:
return path return path
def get_word(self, board: BoggleBoard) -> str:
'''Return word or word fragment based on the WordNode's path property'''
return "".join([board.board[x].letters for x in self.path[::-1]])
def __str__(self): def __str__(self):
# return f"WordNode: {self.char}, {self.is_word}, {self.children}" return f"WordNode: {self.letters}, {self.is_word}, {self.board_pos}"
return f"WordNode: {self.char}, {self.is_word}"
def __repr__(self): def __repr__(self):
return self.__str__() return f"WordNode: {self.letters}, {self.is_word}, {self.children}"
class WordTree: class WordTree:
'''A tree populated by WordNode(s) to complete words from a given root letter and wordlist''' '''A tree populated by WordNode(s) to complete words from a given root letter and wordlist'''
def __init__(self, alphabet: str, root: str, words: list[str] = None, max_word_len = 16) -> WordTree: def __init__(self, alphabet: str, root: WordNode, words: list[str] = None,
self.alphabet = alphabet max_word_len = 16) -> WordTree:
self.wordlist = words self.__alphabet: str = alphabet
self.root = root self.__wordlist: list[str] = words
self.max_word_len = max_word_len self.__root: WordNode = root
self.tree = {} self.__max_word_len: int = max_word_len
self.__tree: dict[str, WordNode] = {}
self.__word_paths: dict[str, list[WordNode]] = []
# Generate root node # Generate root node
self.tree[root] = WordNode(root, False) self.__tree[root.letters] = root
self.active_node: WordNode = self.tree[root] self.active_node: WordNode = self.__tree[root.letters]
# Populate tree from wordlist # Populate tree from wordlist
if words is not None: if words is not None:
for word in words: for word in words:
self.__insert_word(word) self.__insert_word(word)
@property
def alphabet(self) -> str:
'''Getter for alphabet property'''
return self.__alphabet
@property
def wordlist(self) -> list[str]:
'''Getter for wordlist property'''
return self.__wordlist
@property
def root(self) -> WordNode:
'''Getter for root property'''
return self.__root
@property
def max_word_len(self) -> int:
'''Getter for max_word_len property'''
return self.__max_word_len
@property
def tree(self) -> dict[str, WordNode]:
'''Getter for tree property'''
return self.__tree
@property
def word_paths(self) -> dict[(int, int), WordNode]:
'''Getter for leaf_nodes property'''
return self.__word_paths
@word_paths.setter
def word_paths(self, value):
self.__word_paths = value
def __str__(self): def __str__(self):
return ", ".join(self.wordlist) return ", ".join(self.wordlist)
def insert_letter(self, letter: str, parent: WordNode, is_word: bool = False, children: dict[WordNode] = None, board_pos = None): def insert_node(self, letters: str, parent: WordNode, is_word: bool = False,
'''Create WordNode for `letter` and into WordTree under `parent`''' children: dict[str, WordNode] = None, board_pos = None):
node = WordNode(letter, is_word, parent, children, board_pos) '''Create WordNode for `letters` and into WordTree under `parent`'''
node = WordNode(letters, is_word, parent, children, board_pos)
parent.add_child_node(node) parent.add_child_node(node)
def __insert_word(self, word: str) -> bool: def __insert_word(self, word: str) -> bool:
'''Returns True if word was inserted into the tree, otherwise False''' '''Returns True if the word could be inserted into the tree with the given alphabet,
if word is None or word[0] != self.root: otherwise returns False'''
# Insert root node
prefix = word[0:len(self.root.letters)] # prefix = first letter block
if word is None or prefix != self.root.letters:
return False return False
curr_node = self.tree[word[0]] curr_node = self.tree[prefix]
# print(word[0]) word_len = len(word)
for letter in word[1:self.max_word_len]:
# Insert new WordNode for each letter that doesen't already exist in the tree # Insert remaining nodes as letter groups based on alphabet
if letter not in curr_node.children: skip_cnt = 0
self.insert_letter(letter, curr_node) for i, letter in enumerate(word[len(prefix):self.max_word_len]):
# print(letter) # Skip word letter iterations for length of a previously inserted letter group
curr_node = curr_node.children[letter] if skip_cnt > 0:
# Mark the last node of the word skip_cnt -= 1
continue
try:
alpha_index = [x[0] for x in self.alphabet].index(letter)
except ValueError: # letters not in given alphabet
return False
# Insert letter (single)
if letter == self.alphabet[alpha_index] and letter not in curr_node.children:
self.insert_node(letter, curr_node)
curr_node = curr_node.children[letter]
# Letter already exists
elif letter == self.alphabet[alpha_index]:
curr_node = curr_node.children[letter]
# Check for letter groups (like "Qu") starting with `letter` in alphabet
else:
alpha = self.alphabet[alpha_index]
# Check that letter group is shorter than and matches in the word remainder
if len(alpha) < word_len - i and alpha == word[i+1:i+1+len(alpha)]:
if alpha not in curr_node.children:
self.insert_node(alpha, curr_node)
curr_node = curr_node.children[alpha]
else: # node already exist
curr_node = curr_node.children[alpha]
skip_cnt = len(alpha) - 1
#print(f"LONG GROUP: {alpha}; {word}")
else:
return False
# Mark the last node as a word
curr_node.is_word = len(word) <= self.max_word_len curr_node.is_word = len(word) <= self.max_word_len
print(f"Added: {word[:self.max_word_len]}") #print(f"Added: {word[:self.max_word_len]}")
return True return True
def search(self, word: str) -> WordNode: def search(self, word: str, curr_node = None) -> WordNode:
'''Return True if a given word is in the tree otherwise return False''' '''Return leaf (WordNode) if a given word is in the tree otherwise return None'''
if len(word) == 0 or word is None: if len(word) == 0 or word is None:
return False return curr_node
curr_node = self.tree[word[0]] if curr_node is None:
for char in word[1:]: curr_node = self.root
if char not in curr_node.children: for letters in curr_node.children:
return None if letters == word[len(curr_node.letters):len(curr_node.letters) + len(letters)]:
return self.search(word[len(curr_node.letters):], curr_node.children[letters])
curr_node = curr_node.children[char]
# Currently only returns single path for word
# TODO: return all possible paths, maybe create minature WordTree? Or just of list of paths.
return curr_node return curr_node
def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, depth: int = 0) -> WordTree: def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, depth: int = 1) -> WordTree:
'''Return subtree of board given a particular root (first letter). '''Return subtree of board given a particular root (first letter).
Keyword arguments: Keyword arguments:
@@ -213,96 +340,84 @@ class WordTree:
# TODO rework active_node refs for recursion so don't have to be reset to parent at every point of return # TODO rework active_node refs for recursion so don't have to be reset to parent at every point of return
if self.active_node.is_word and len(self.active_node.children) == 0: if self.active_node.is_word and len(self.active_node.children) == 0:
word_path = subtree.active_node.path[::-1] word_path = subtree.active_node.path[::-1]
print("WORD FOUND:", "".join([board.board[x].letter for x in word_path]), word_path) subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
#print("1: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path)
self.active_node = self.active_node.parent self.active_node = self.active_node.parent
subtree.active_node = subtree.active_node.parent subtree.active_node = subtree.active_node.parent
return return
elif self.active_node.is_word: elif self.active_node.is_word:
word_path = subtree.active_node.path[::-1] word_path = subtree.active_node.path[::-1]
print("WORD FOUND:", "".join([board.board[x].letter for x in word_path]), word_path) subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
elif depth > self.max_word_len: #print("2: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path)
print(f"MAX DEPTH REACHED! Depth = {depth}")
self.active_node = self.active_node.parent if depth >= self.max_word_len or depth >= board.max_word_len:
#print(f"MAX DEPTH REACHED! Depth = {depth}")
subtree.active_node = subtree.active_node.parent subtree.active_node = subtree.active_node.parent
self.active_node = self.active_node.parent
return return
# Branch for each adjacent board cell # Branch for each adjacent board cell
for cell in board_cell.adjacent_cells: for cell in board_cell.adjacent_cells:
# Check dictionary and exclude nodes already in path # Check dictionary and exclude nodes already in path
print("Cell:", cell.letter, cell.pos, subtree.active_node.path) if cell.letters in self.active_node.children and cell.pos not in subtree.active_node.path:
if cell.letter in self.active_node.children and cell.pos not in subtree.active_node.path: new_node = WordNode(cell.letters, self.active_node.is_word, subtree.active_node, board_pos = cell.pos)
# print(cell)
print(self.active_node)
#subtree.insert_letter(cell.letter, subtree.active_node, self.active_node.is_word, board_pos=cell.pos)
new_node = WordNode(cell.letter, self.active_node.is_word, subtree.active_node, board_pos = cell.pos)
subtree.active_node.add_child_node(new_node) subtree.active_node.add_child_node(new_node)
subtree.active_node = subtree.active_node.children[cell.letter] # update subtree pointer subtree.active_node = subtree.active_node.children[cell.letters] # update subtree pointer
self.active_node = self.active_node.children[cell.letter] # update dictionary tree pointer self.active_node = self.active_node.children[cell.letters] # update dictionary tree pointer
self.build_boggle_tree(board, board.board[cell.pos], subtree) self.build_boggle_tree(board, board.board[cell.pos], subtree, depth=depth+1)
#print(f"depth: {depth}")
print(f"DONE SEARCHING at {subtree.active_node}")
self.active_node = self.active_node.parent self.active_node = self.active_node.parent
subtree.active_node = subtree.active_node.parent subtree.active_node = subtree.active_node.parent
return subtree return subtree
def build_boggle_tree(args):
'''Build boggle tree from arguments for process Pool'''
(alphabet, board, cell, wordlist) = args
root_node = WordNode(cell.letters)
dict_tree = WordTree(alphabet, root_node, wordlist)
sub_tree = WordTree(alphabet, WordNode(cell.letters, False, board_pos=cell.pos))
return dict_tree.build_boggle_tree(board, cell, sub_tree)
def build_full_boggle_tree(board: BoggleBoard, wordlist_path: str) -> dict[str, WordTree]:
'''Return dictionary of WordTree(s) for every letter on a BoggleBoard'''
alphabet = sorted(set([cell.letters for cell in board.board.values()]))
board_tree = {}
index = {}
print("Reading in wordlists...")
for letter in alphabet:
filename = "words_" + letter[0] + ".txt"
print(f">> {letter}: {filename}")
wordlist = read_wordlist(path.join(path.abspath(wordlist_path), filename))
index[letter] = wordlist
print("Generating WordTrees...")
params = [ [alphabet, board, cell, index[cell.letters] ] for cell in board.board.values()]
with Pool(processes=len(board.board)) as pool:
for i, res in enumerate(pool.map(build_boggle_tree, params)):
board_tree[params[i][2].pos] = res
return board_tree
def read_wordlist(file): def read_wordlist(file):
'''Return dictionary of words with associated word count (1 by default)''' '''Return dictionary of words with associated word count (1 by default)'''
with open(file, 'r', encoding='utf-8') as file: with open(file, 'r', encoding='utf-8') as file:
return {k:1 for k in file.read().split()} return {k:1 for k in file.read().split()}
def read_boggle_file(file):
'''Return list of rows from Boggle board csv file'''
with open(file, 'r', encoding='utf-8') as file:
return [x.rstrip().split(',') for x in file.readlines()]
if __name__ == "__main__": if __name__ == "__main__":
# sample_dict = [ #b2 = read_boggle_file(sys.argv[1])
# 'aero', b2 = read_boggle_file("boards/b1.csv")
# 'anger',
# 'ant',
# 'ape',
# 'argue',
# 'ash',
# 'assuage',
# 'aunt',
# 'auspice',
# 'bigger',
# 'bit',
# 'bite',
# 'biter',
# 'cart',
# 'cast',
# ]
sample_dict = read_wordlist("wordlists/dwyl/words_a.txt")
tree = WordTree("abcdefghijklmnopqrstuvwxyz", "a", sample_dict)
n1 = tree.search('anger')
#print(n1.path)
#print(tree.search('argues'))
print("=" * 50)
b1 = [
"aucd",
"eegh",
"hios",
"trea"
]
b2 = [
"iats",
"osep",
"tras",
"yegc"
]
b1_board = BoggleBoard(b1)
b2_board = BoggleBoard(b2) b2_board = BoggleBoard(b2)
boggle_tree = build_full_boggle_tree(b2_board, "wordlists/dwyl")
print("\nBOARD")
print(b2_board)
b1_tree = WordTree("abcdefghijklmnopqrstuvwxyz", "a") w1 = boggle_tree[(0,0)].word_paths
b1_tree.tree["a"].board_pos = (3,3) for w in w1:
print(f"{w[0]: <{b2_board.max_word_len}}: {w[1]}")
b2_tree = WordTree("abcdefghijklmnopqrstuvwxyz", "a")
b2_tree.tree["a"].board_pos = (0,1)
#sub_tree = tree.build_boggle_tree(b1_board, b1_board.board[(0,0)], b1_tree)
sub_tree = tree.build_boggle_tree(b2_board, b2_board.board[(2,2)], b2_tree)
#sub_tree = tree.build_board_sub_tree(b1_board, b1_board.board[(3, 3)])
#print(sub_tree.search("anger"))
Regular → Executable
+1 -2
View File
@@ -1,5 +1,4 @@
front,right,back,left,top,bottom (start with 'one of' the first alphabetical chars # front,right,back,left,top,bottom (start with 'one of' the first alphabetical chars
-----------
a,h,s,p,o,c a,h,s,p,o,c
e,r,l,i,x,d e,r,l,i,x,d
a,g,a,e,e,a a,g,a,e,e,a
1 front,right,back,left,top,bottom (start with 'one of' the first alphabetical chars # front right back left top bottom (start with 'one of' the first alphabetical chars
-----------
2 a,h,s,p,o,c a h s p o c
3 e,r,l,i,x,d e r l i x d
4 a,g,a,e,e,a a g a e e a