mirror of
https://codeberg.org/cblanken/boggler.git
synced 2026-07-26 11:35:10 -04:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a80111072f | ||
|
|
fa01d442a5 | ||
|
|
733c1474b0 | ||
|
|
54f79c0afe | ||
|
|
9f364ee90e | ||
|
|
e219954ba6 | ||
|
|
f4accfe959 | ||
|
|
bc70b88bf7 | ||
|
|
21aab46b96 | ||
|
|
f18c30cb60 | ||
|
|
da915b723c | ||
|
|
d5a102713d | ||
|
|
c488263eea | ||
|
|
fe1f1c2b97 | ||
|
|
d2584686a3 | ||
|
|
8f75604d15 | ||
|
|
96da732dd9 | ||
|
|
ec53042d18 | ||
|
|
5c18a4aebf | ||
|
|
b197253d86 | ||
|
|
67f8c439b0 | ||
|
|
8c41ad212d | ||
|
|
94800909b4 | ||
|
|
f24b996411 | ||
|
|
e4df95cd08 | ||
|
|
0e9b120e94 | ||
|
|
276c27f1ce | ||
|
|
d30c44af3c | ||
|
|
5da33cf02c | ||
|
|
600efc19c2 | ||
|
|
cdb1542184 | ||
|
|
795f1540b4 | ||
|
|
91390ea425 | ||
|
|
2a6a27b0b8 | ||
|
|
b445071ccc | ||
|
|
9c8e94964c | ||
|
|
2fd5b05717 | ||
|
|
0cdd3895ee | ||
|
|
50d4625b84 | ||
|
|
c88f657113 | ||
|
|
14a813ed74 | ||
|
|
46dd069cfd | ||
|
|
31a307e83d | ||
|
|
bc4922dc55 | ||
|
|
5f9a0d1990 | ||
|
|
dc532c1fdd | ||
|
|
0f5e5af5ae | ||
|
|
28b3d56880 | ||
|
|
79486d4217 | ||
|
|
6d6d56d1cb | ||
|
|
88b43d0679 |
@@ -1 +1,9 @@
|
|||||||
|
# Byte-compiled / optimized
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Env
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
Copyright © 2022 Cameron Blankenbuehler
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
# Boggler
|
||||||
|
A solver for the popular word game Boggle.
|
||||||
|
|
||||||
|
# Install
|
||||||
|
```console
|
||||||
|
pip install boggler
|
||||||
|
```
|
||||||
|
OR
|
||||||
|
```console
|
||||||
|
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 board.csv
|
||||||
|
y,e,o,s
|
||||||
|
r,e,o,v
|
||||||
|
d,f,e,y
|
||||||
|
n,m,e,qu
|
||||||
|
```
|
||||||
|
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)]
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
The included [wordlists](src/boggler/wordlists) are covered by their respective licenses. All other files MIT © Cameron Blankenbuehler
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
'''Module to generate random Boggle boards for testing'''
|
|
||||||
from sys import argv
|
|
||||||
from random import randint, shuffle
|
|
||||||
from math import sqrt
|
|
||||||
|
|
||||||
def read_dice_file(path):
|
|
||||||
'''Return list of die strings from file ignoring all comments (#)'''
|
|
||||||
with open(path, 'r+', encoding="utf-8") as file:
|
|
||||||
return [''.join(line.rstrip().split(',')) for line in file.readlines() if line[0] != "#"]
|
|
||||||
|
|
||||||
def roll_die(die: str):
|
|
||||||
'''Return a face of the given die string to simulate rolling a die'''
|
|
||||||
return str(die[randint(0, len(die) - 1)])
|
|
||||||
|
|
||||||
def roll_dice(dice: list[str]):
|
|
||||||
'''Return a random roll for each die'''
|
|
||||||
return [ roll_die(die) for die in dice ]
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
if len(argv) != 2:
|
|
||||||
print('Usage: python3 board_randomizer.py <dice_file>')
|
|
||||||
else:
|
|
||||||
dice_strings = read_dice_file(argv[1])
|
|
||||||
shuffle(dice_strings)
|
|
||||||
rolls = roll_dice(dice_strings)
|
|
||||||
|
|
||||||
# Format dice rolls for boards file
|
|
||||||
board_size = int(sqrt(len(rolls)))
|
|
||||||
for i in range(0, len(dice_strings), board_size):
|
|
||||||
print("".join(rolls[i:i+board_size]))
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
aucd
|
|
||||||
eegh
|
|
||||||
hios
|
|
||||||
trea
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
vdau
|
|
||||||
ptuo
|
|
||||||
ohxr
|
|
||||||
wlhn
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
nxaa
|
|
||||||
vatr
|
|
||||||
ieba
|
|
||||||
iurn
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
vdat
|
|
||||||
pteo
|
|
||||||
ohnr
|
|
||||||
wlan
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
nbapi
|
|
||||||
satrk
|
|
||||||
iebae
|
|
||||||
turnl
|
|
||||||
icqme
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
nbapir
|
|
||||||
satrkp
|
|
||||||
iebaez
|
|
||||||
turnls
|
|
||||||
icqmet
|
|
||||||
nynjro
|
|
||||||
-111
@@ -1,111 +0,0 @@
|
|||||||
import sys
|
|
||||||
from dictionary_utils import WordTree
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
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 len(sys.argv) != 5:
|
|
||||||
print('Usage: python3 blogger.py <BOARD_FILE> <MAX_WORD_LENGTH> <PREFIX_FILE> <WORDLISTS>')
|
|
||||||
else:
|
|
||||||
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 = {
|
|
||||||
k:[board[pos[0]][pos[1]] for pos in v] for (k,v) in adjacentIndexes.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
maxSize = int(sys.argv[2], 10)
|
|
||||||
prefixes = read_wordlist(sys.argv[3])
|
|
||||||
words = find_words(board, adjacentIndexes, sys.argv[4], maxSize, prefixes, 4)
|
|
||||||
for w in words:
|
|
||||||
print(w)
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
'''Boggler Utils'''
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
from multiprocessing import Pool
|
|
||||||
|
|
||||||
class BoardCell:
|
|
||||||
'''Boggle Board cell'''
|
|
||||||
def __init__(self, row: int, col: int, letter: str, adjacent_cells: list[BoardCell] = None) -> BoardCell:
|
|
||||||
self.__row: int = row
|
|
||||||
self.__col: int = col
|
|
||||||
self.__pos: (int, int) = (self.__row, self.__col)
|
|
||||||
self.__letter: str = letter
|
|
||||||
self.__adjacent_cells: list[BoardCell] = adjacent_cells
|
|
||||||
|
|
||||||
@property
|
|
||||||
def row(self) -> int:
|
|
||||||
'''Getter for row property'''
|
|
||||||
return self.__row
|
|
||||||
|
|
||||||
@property
|
|
||||||
def col(self) -> int:
|
|
||||||
'''Getter for col property'''
|
|
||||||
return self.__col
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pos(self) -> (int, int):
|
|
||||||
'''Getter for pos property'''
|
|
||||||
return self.__pos
|
|
||||||
|
|
||||||
@property
|
|
||||||
def letter(self) -> str:
|
|
||||||
'''Getter for letter property'''
|
|
||||||
return self.__letter
|
|
||||||
|
|
||||||
@property
|
|
||||||
def adjacent_cells(self) -> list[BoardCell]:
|
|
||||||
'''Getter for adjacent_cells property'''
|
|
||||||
return self.__adjacent_cells
|
|
||||||
|
|
||||||
@adjacent_cells.setter
|
|
||||||
def adjacent_cells(self, value):
|
|
||||||
self.__adjacent_cells = value
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"({self.__row}, {self.__col}: {self.__letter} | {self.__adjacent_cells})"
|
|
||||||
|
|
||||||
class BoggleBoard:
|
|
||||||
'''Boggle board structure'''
|
|
||||||
def __init__(self, board: list[list[str]], max_word_len: int = 14) -> BoggleBoard:
|
|
||||||
self.__height: int = len(board)
|
|
||||||
self.__width: int = len(board[0]) if self.__height > 0 else 0
|
|
||||||
self.__board: dict[(int, int), BoardCell] = {}
|
|
||||||
|
|
||||||
# max word length is limited by size of the board
|
|
||||||
self.__max_word_len = min(max_word_len, self.__width * self.__height)
|
|
||||||
|
|
||||||
# Generate BoardCell for each position on the board
|
|
||||||
for row in range(0, self.__height):
|
|
||||||
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])
|
|
||||||
|
|
||||||
# Update adjacent cell references for each BoardCell
|
|
||||||
for cell in self.__board.values():
|
|
||||||
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]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def height(self) -> int:
|
|
||||||
'''Getter for height property'''
|
|
||||||
return self.__height
|
|
||||||
|
|
||||||
@property
|
|
||||||
def width(self) -> int:
|
|
||||||
'''Getter for width property'''
|
|
||||||
return self.__width
|
|
||||||
@property
|
|
||||||
def max_word_len(self) -> int:
|
|
||||||
'''Getter for maximum word length property'''
|
|
||||||
return self.__max_word_len
|
|
||||||
@property
|
|
||||||
def board(self) -> dict((int, int), BoardCell):
|
|
||||||
'''Getter for board property'''
|
|
||||||
return self.__board
|
|
||||||
|
|
||||||
def __get_adjacent_indexes(self, row, col):
|
|
||||||
'''Return adjecency list for board of size `row x col`'''
|
|
||||||
indexes = []
|
|
||||||
if row > 0:
|
|
||||||
indexes.append((row-1, col)) # up
|
|
||||||
if col > 0:
|
|
||||||
indexes.append((row-1, col-1)) # up-left
|
|
||||||
if col < self.width - 1:
|
|
||||||
indexes.append((row-1, col+1)) # up-right
|
|
||||||
if row < self.height - 1:
|
|
||||||
indexes.append((row+1, col)) # down
|
|
||||||
if col > 0:
|
|
||||||
indexes.append((row+1, col-1)) # down-left
|
|
||||||
if col < self.width - 1:
|
|
||||||
indexes.append((row+1, col+1)) # down-right
|
|
||||||
if col > 0:
|
|
||||||
indexes.append((row, col-1)) # left
|
|
||||||
if col < self.width - 1:
|
|
||||||
indexes.append((row, col+1)) # right
|
|
||||||
return indexes
|
|
||||||
|
|
||||||
def get_cell(self, row: int, col: int) -> str:
|
|
||||||
'''Return the value at the specified row x column'''
|
|
||||||
return self.__board[(row, col)]
|
|
||||||
|
|
||||||
class WordNode:
|
|
||||||
'''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:
|
|
||||||
self.char = char
|
|
||||||
self.is_word = is_word
|
|
||||||
self.children = children if children is not None else {}
|
|
||||||
self.parent = parent
|
|
||||||
self.board_pos = board_pos
|
|
||||||
|
|
||||||
def add_child_node(self, node):
|
|
||||||
'''Add child node to `children` dictionary, indexed by the nodes' `char`'''
|
|
||||||
self.children[node.char] = node
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self) -> list[WordNode]:
|
|
||||||
'''Return list of nodes from current node to the root'''
|
|
||||||
curr_node = self
|
|
||||||
path = []
|
|
||||||
path.append(curr_node.board_pos)
|
|
||||||
while curr_node.parent is not None:
|
|
||||||
path.append(curr_node.parent.board_pos)
|
|
||||||
curr_node = curr_node.parent
|
|
||||||
|
|
||||||
return path
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
# return f"WordNode: {self.char}, {self.is_word}, {self.children}"
|
|
||||||
return f"WordNode: {self.char}, {self.is_word}"
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.__str__()
|
|
||||||
|
|
||||||
class WordTree:
|
|
||||||
'''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:
|
|
||||||
self.alphabet = alphabet
|
|
||||||
self.wordlist = words
|
|
||||||
self.root = root
|
|
||||||
self.max_word_len = max_word_len
|
|
||||||
self.tree = {}
|
|
||||||
|
|
||||||
# Generate root node
|
|
||||||
self.tree[root] = WordNode(root, False)
|
|
||||||
self.active_node: WordNode = self.tree[root]
|
|
||||||
|
|
||||||
# Populate tree from wordlist
|
|
||||||
if words is not None:
|
|
||||||
for word in words:
|
|
||||||
self.__insert_word(word)
|
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return ", ".join(self.wordlist)
|
|
||||||
|
|
||||||
def insert_letter(self, letter: str, parent: WordNode, is_word: bool = False, children: dict[WordNode] = None, board_pos = None):
|
|
||||||
'''Create WordNode for `letter` and into WordTree under `parent`'''
|
|
||||||
node = WordNode(letter, is_word, parent, children, board_pos)
|
|
||||||
parent.add_child_node(node)
|
|
||||||
|
|
||||||
def __insert_word(self, word: str) -> bool:
|
|
||||||
'''Returns True if word was inserted into the tree, otherwise False'''
|
|
||||||
if word is None or word[0] != self.root:
|
|
||||||
return False
|
|
||||||
curr_node = self.tree[word[0]]
|
|
||||||
# print(word[0])
|
|
||||||
for letter in word[1:self.max_word_len]:
|
|
||||||
# Insert new WordNode for each letter that doesen't already exist in the tree
|
|
||||||
if letter not in curr_node.children:
|
|
||||||
self.insert_letter(letter, curr_node)
|
|
||||||
# print(letter)
|
|
||||||
curr_node = curr_node.children[letter]
|
|
||||||
# Mark the last node of the word
|
|
||||||
curr_node.is_word = len(word) <= self.max_word_len
|
|
||||||
print(f"Added: {word[:self.max_word_len]}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def search(self, word: str) -> WordNode:
|
|
||||||
'''Return True if a given word is in the tree otherwise return False'''
|
|
||||||
if len(word) == 0 or word is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
curr_node = self.tree[word[0]]
|
|
||||||
for char in word[1:]:
|
|
||||||
if char not in curr_node.children:
|
|
||||||
return None
|
|
||||||
|
|
||||||
curr_node = curr_node.children[char]
|
|
||||||
|
|
||||||
return curr_node
|
|
||||||
|
|
||||||
def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, depth: int = 0) -> WordTree:
|
|
||||||
'''Return subtree of board given a particular root (first letter).
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
board -- the board the new tree is based on
|
|
||||||
board_node -- a pointer on the board where new branch nodes can be inserted into the tree
|
|
||||||
dict_node -- a pointer on the dictionary where nodes are read from for validation
|
|
||||||
subtree -- the partial tree passed to the next recursive step for generating branches
|
|
||||||
'''
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
word_path = subtree.active_node.path[::-1]
|
|
||||||
print("WORD FOUND:", "".join([board.board[x].letter for x in word_path]), word_path)
|
|
||||||
self.active_node = self.active_node.parent
|
|
||||||
subtree.active_node = subtree.active_node.parent
|
|
||||||
return
|
|
||||||
elif self.active_node.is_word:
|
|
||||||
word_path = subtree.active_node.path[::-1]
|
|
||||||
print("WORD FOUND:", "".join([board.board[x].letter for x in word_path]), word_path)
|
|
||||||
elif depth > self.max_word_len:
|
|
||||||
print(f"MAX DEPTH REACHED! Depth = {depth}")
|
|
||||||
self.active_node = self.active_node.parent
|
|
||||||
subtree.active_node = subtree.active_node.parent
|
|
||||||
return
|
|
||||||
|
|
||||||
# Branch for each adjacent board cell
|
|
||||||
for cell in board_cell.adjacent_cells:
|
|
||||||
# Check dictionary and exclude nodes already in path
|
|
||||||
print("Cell:", cell.letter, cell.pos, subtree.active_node.path)
|
|
||||||
if cell.letter in self.active_node.children and cell.pos not in subtree.active_node.path:
|
|
||||||
# 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 = subtree.active_node.children[cell.letter] # update subtree pointer
|
|
||||||
self.active_node = self.active_node.children[cell.letter] # update dictionary tree pointer
|
|
||||||
self.build_boggle_tree(board, board.board[cell.pos], subtree)
|
|
||||||
|
|
||||||
print(f"DONE SEARCHING at {subtree.active_node}")
|
|
||||||
|
|
||||||
self.active_node = self.active_node.parent
|
|
||||||
subtree.active_node = subtree.active_node.parent
|
|
||||||
return subtree
|
|
||||||
|
|
||||||
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()}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# sample_dict = [
|
|
||||||
# 'aero',
|
|
||||||
# '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)
|
|
||||||
|
|
||||||
b1_tree = WordTree("abcdefghijklmnopqrstuvwxyz", "a")
|
|
||||||
b1_tree.tree["a"].board_pos = (3,3)
|
|
||||||
|
|
||||||
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"))
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
front,right,back,left,top,bottom (start with 'one of' the first alphabetical chars
|
|
||||||
-----------
|
|
||||||
a,h,s,p,o,c
|
|
||||||
e,r,l,i,x,d
|
|
||||||
a,g,a,e,e,a
|
|
||||||
e,y,t,l,r,t
|
|
||||||
e,o,t,s,s,i
|
|
||||||
d,r,y,v,l,e
|
|
||||||
h,n,l,n,z,r
|
|
||||||
e,r,w,t,v,h
|
|
||||||
h,i,u,n,m,qu
|
|
||||||
e,e,h,n,w,g
|
|
||||||
d,t,y,t,i,s
|
|
||||||
a,b,b,o,o,j
|
|
||||||
e,i,u,n,s,e
|
|
||||||
a,s,p,f,k,f
|
|
||||||
c,m,u,o,i,t
|
|
||||||
a,w,t,o,o,t
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "boggler"
|
||||||
|
version = "2.0.1"
|
||||||
|
authors = [
|
||||||
|
{ name="Cameron Blankenbuehler", email="cameron.blankenbuehler@gmail.com" },
|
||||||
|
]
|
||||||
|
description = "Utilities for solving the Boggle word game."
|
||||||
|
readme="README.md"
|
||||||
|
license = { file="LICENSE" }
|
||||||
|
requires-python = ">=3.7"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/cblanken/boggler"
|
||||||
|
"Bug Tracker" = "https://github.com/cblanken/boggler/issues"
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
if [ -z $1 ] || [ -z $2 ]; then
|
|
||||||
echo "Usage: ./filter_prefix.sh WORDLIST OUT_DIR"
|
|
||||||
exit 0
|
|
||||||
else
|
|
||||||
grep -P "^a" $1 > "$2/words_a.txt"
|
|
||||||
grep -P "^b" $1 > "$2/words_b.txt"
|
|
||||||
grep -P "^c" $1 > "$2/words_c.txt"
|
|
||||||
grep -P "^d" $1 > "$2/words_d.txt"
|
|
||||||
grep -P "^e" $1 > "$2/words_e.txt"
|
|
||||||
grep -P "^f" $1 > "$2/words_f.txt"
|
|
||||||
grep -P "^g" $1 > "$2/words_g.txt"
|
|
||||||
grep -P "^h" $1 > "$2/words_h.txt"
|
|
||||||
grep -P "^i" $1 > "$2/words_i.txt"
|
|
||||||
grep -P "^j" $1 > "$2/words_j.txt"
|
|
||||||
grep -P "^k" $1 > "$2/words_k.txt"
|
|
||||||
grep -P "^l" $1 > "$2/words_l.txt"
|
|
||||||
grep -P "^m" $1 > "$2/words_m.txt"
|
|
||||||
grep -P "^n" $1 > "$2/words_n.txt"
|
|
||||||
grep -P "^o" $1 > "$2/words_o.txt"
|
|
||||||
grep -P "^p" $1 > "$2/words_p.txt"
|
|
||||||
grep -P "^q" $1 > "$2/words_q.txt"
|
|
||||||
grep -P "^r" $1 > "$2/words_r.txt"
|
|
||||||
grep -P "^s" $1 > "$2/words_s.txt"
|
|
||||||
grep -P "^t" $1 > "$2/words_t.txt"
|
|
||||||
grep -P "^u" $1 > "$2/words_u.txt"
|
|
||||||
grep -P "^v" $1 > "$2/words_v.txt"
|
|
||||||
grep -P "^w" $1 > "$2/words_w.txt"
|
|
||||||
grep -P "^x" $1 > "$2/words_x.txt"
|
|
||||||
grep -P "^y" $1 > "$2/words_y.txt"
|
|
||||||
grep -P "^z" $1 > "$2/words_z.txt"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
'''Module to generate random Boggle boards for testing'''
|
||||||
|
from sys import argv, stderr
|
||||||
|
from random import randint, shuffle
|
||||||
|
from math import sqrt, floor
|
||||||
|
from os import path
|
||||||
|
|
||||||
|
def read_dice_file(dice_path: str):
|
||||||
|
'''Return list of die strings from file ignoring all comments (#)'''
|
||||||
|
with open(path.abspath(dice_path), 'r+', encoding="utf-8") as file:
|
||||||
|
return [line.rstrip().split(',') for line in file.readlines() if line[0] != "#"]
|
||||||
|
|
||||||
|
def roll_die(die: str):
|
||||||
|
'''Return a face of the given die string to simulate rolling a die'''
|
||||||
|
return str(die[randint(0, len(die) - 1)])
|
||||||
|
|
||||||
|
def roll_dice(dice: list[str]):
|
||||||
|
'''Return a random roll for each die'''
|
||||||
|
return [ roll_die(die) for die in dice ]
|
||||||
|
|
||||||
|
def get_random_board(dice: list[str]):
|
||||||
|
shuffle(dice)
|
||||||
|
rolls = roll_dice(dice)
|
||||||
|
|
||||||
|
# Format dice rolls for boards file
|
||||||
|
board_size = int(floor(sqrt(len(rolls))))
|
||||||
|
board = []
|
||||||
|
for i in range(0, len(dice), board_size):
|
||||||
|
board.append(rolls[i:i+board_size])
|
||||||
|
return board
|
||||||
|
|
||||||
|
def get_random_board_csv(dice: list[str]):
|
||||||
|
board = get_random_board(dice)
|
||||||
|
board = [",".join(row) for row in board]
|
||||||
|
return board
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(argv) != 2:
|
||||||
|
print('Usage: python3 board_randomizer.py <dice_file>')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
dice = read_dice_file(path.abspath(argv[1]))
|
||||||
|
board = get_random_board_csv(dice)
|
||||||
|
for r in range(0, int(floor(sqrt(len(dice))))):
|
||||||
|
print(board[r])
|
||||||
|
except Exception as e:
|
||||||
|
print("Argument must be a valid file path!", file=stderr)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
s,a,i,p
|
||||||
|
l,qu,a,y
|
||||||
|
u,l,l,s
|
||||||
|
o,w,h,a
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
t,h,a,e
|
||||||
|
o,p,j,v
|
||||||
|
f,e,i,t
|
||||||
|
n,e,v,i
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
w,a,n,c
|
||||||
|
j,i,e,u
|
||||||
|
y,g,o,e
|
||||||
|
i,l,e,c
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
y,o,o,a
|
||||||
|
p,l,n,v
|
||||||
|
t,i,o,qu
|
||||||
|
n,f,u,t
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
o,d,r,s
|
||||||
|
a,h,e,s
|
||||||
|
e,t,i,h
|
||||||
|
u,r,e,o
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
e,t,p,a
|
||||||
|
b,m,v,g
|
||||||
|
c,s,e,n
|
||||||
|
x,g,d,t
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
c,e,s,o
|
||||||
|
e,j,d,h
|
||||||
|
s,n,r,h
|
||||||
|
w,c,r,y
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
l,e,e,e
|
||||||
|
b,k,p,o
|
||||||
|
h,u,u,i
|
||||||
|
y,t,t,s
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
y,e,o,s
|
||||||
|
r,e,o,v
|
||||||
|
d,f,e,y
|
||||||
|
n,m,e,qu
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
e,w,t,m
|
||||||
|
s,p,m,r
|
||||||
|
h,y,o,x
|
||||||
|
y,t,e,w
|
||||||
|
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
"""Boggler Demo"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from boggler_utils import BoggleBoard, build_full_boggle_tree, read_boggle_file
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) < 3 or len(sys.argv) > 4:
|
||||||
|
print('Usage: python3 boggler.py <BOARD_FILE> <WORDLISTS_DIR> [MAX_WORD_LENGTH]')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
board = read_boggle_file(Path(sys.argv[1]))
|
||||||
|
if len(sys.argv) == 3:
|
||||||
|
boggle_board = BoggleBoard(board)
|
||||||
|
elif len(sys.argv) == 4:
|
||||||
|
try:
|
||||||
|
boggle_board = BoggleBoard(board, int(sys.argv[3]))
|
||||||
|
except ValueError:
|
||||||
|
print("Invalid MAX_WORD_LENGTH. Please try again with a valid integer.")
|
||||||
|
sys.exit(1)
|
||||||
|
try:
|
||||||
|
boggle_tree = build_full_boggle_tree(boggle_board, Path(sys.argv[2]))
|
||||||
|
|
||||||
|
print("\nBOARD")
|
||||||
|
print(boggle_board)
|
||||||
|
|
||||||
|
for start_pos, tree in boggle_tree.items():
|
||||||
|
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)
|
||||||
Executable
+429
@@ -0,0 +1,429 @@
|
|||||||
|
'''Boggler Utils'''
|
||||||
|
from __future__ import annotations
|
||||||
|
from os import path
|
||||||
|
from multiprocessing import Pool
|
||||||
|
import functools
|
||||||
|
import operator
|
||||||
|
|
||||||
|
class BoardCell:
|
||||||
|
'''Boggle Board cell'''
|
||||||
|
def __init__(self, row: int, col: int, letters: str,
|
||||||
|
adjacent_cells: list[BoardCell] = None) -> BoardCell:
|
||||||
|
self.__row: int = row
|
||||||
|
self.__col: int = col
|
||||||
|
self.__pos: (int, int) = (self.__row, self.__col)
|
||||||
|
self.__letters: str = letters
|
||||||
|
self.__adjacent_cells: list[BoardCell] = adjacent_cells
|
||||||
|
|
||||||
|
@property
|
||||||
|
def row(self) -> int:
|
||||||
|
'''Getter for row property'''
|
||||||
|
return self.__row
|
||||||
|
|
||||||
|
@property
|
||||||
|
def col(self) -> int:
|
||||||
|
'''Getter for col property'''
|
||||||
|
return self.__col
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pos(self) -> (int, int):
|
||||||
|
'''Getter for pos property'''
|
||||||
|
return self.__pos
|
||||||
|
|
||||||
|
@property
|
||||||
|
def letters(self) -> str:
|
||||||
|
'''Getter for letter property'''
|
||||||
|
return self.__letters
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adjacent_cells(self) -> list[BoardCell]:
|
||||||
|
'''Getter for adjacent_cells property'''
|
||||||
|
return self.__adjacent_cells
|
||||||
|
|
||||||
|
@adjacent_cells.setter
|
||||||
|
def adjacent_cells(self, value):
|
||||||
|
self.__adjacent_cells = value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"({self.__row}, {self.__col}): {self.__letters}"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
|
||||||
|
|
||||||
|
class BoggleBoard:
|
||||||
|
'''Boggle board structure'''
|
||||||
|
def __init__(self, board: list[list[str]], max_word_len: int = 14) -> BoggleBoard:
|
||||||
|
self.__height: int = len(board)
|
||||||
|
self.__width: int = len(board[0]) if self.__height > 0 else 0
|
||||||
|
self.__board_list = board
|
||||||
|
self.__board: dict[(int, int), BoardCell] = {}
|
||||||
|
|
||||||
|
# Max word length is limited by size of the board
|
||||||
|
self.__max_word_len = min(max_word_len, self.__width * self.__height)
|
||||||
|
|
||||||
|
# Generate BoardCell for each position on the board
|
||||||
|
for row in range(0, self.__height):
|
||||||
|
for col in range(0, self.__width):
|
||||||
|
self.__board[(row, col)] = BoardCell(row, col, board[row][col])
|
||||||
|
|
||||||
|
# Update adjacent cell references for each BoardCell
|
||||||
|
for cell in self.__board.values():
|
||||||
|
adjacent_indexes = self.__get_adjacent_indexes(cell.row, cell.col)
|
||||||
|
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
|
||||||
|
def height(self) -> int:
|
||||||
|
'''Getter for height property'''
|
||||||
|
return self.__height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
'''Getter for width property'''
|
||||||
|
return self.__width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_word_len(self) -> int:
|
||||||
|
'''Getter for maximum word length property'''
|
||||||
|
return self.__max_word_len
|
||||||
|
|
||||||
|
@property
|
||||||
|
def board(self) -> dict[(int, int), BoardCell]:
|
||||||
|
'''Getter for board property'''
|
||||||
|
return self.__board
|
||||||
|
|
||||||
|
def __get_adjacent_indexes(self, row, col):
|
||||||
|
'''Return adjecency list for board of size `row x col`'''
|
||||||
|
indexes = []
|
||||||
|
if row > 0:
|
||||||
|
indexes.append((row-1, col)) # up
|
||||||
|
if col > 0:
|
||||||
|
indexes.append((row-1, col-1)) # up-left
|
||||||
|
if col < self.width - 1:
|
||||||
|
indexes.append((row-1, col+1)) # up-right
|
||||||
|
if row < self.height - 1:
|
||||||
|
indexes.append((row+1, col)) # down
|
||||||
|
if col > 0:
|
||||||
|
indexes.append((row+1, col-1)) # down-left
|
||||||
|
if col < self.width - 1:
|
||||||
|
indexes.append((row+1, col+1)) # down-right
|
||||||
|
if col > 0:
|
||||||
|
indexes.append((row, col-1)) # left
|
||||||
|
if col < self.width - 1:
|
||||||
|
indexes.append((row, col+1)) # right
|
||||||
|
return indexes
|
||||||
|
|
||||||
|
def get_cell(self, row: int, col: int) -> str:
|
||||||
|
'''Return the value at the specified row x column'''
|
||||||
|
return self.__board[(row, col)]
|
||||||
|
|
||||||
|
class WordNode:
|
||||||
|
'''A node describing a single letter in a WordTree.'''
|
||||||
|
def __init__(self, letters: str, is_word: bool = False, parent: WordNode = None,
|
||||||
|
children: dict[str, WordNode] = None, board_pos = None) -> WordNode:
|
||||||
|
self.__letters = letters
|
||||||
|
self.__is_word = is_word
|
||||||
|
self.__children = children if children is not None else {}
|
||||||
|
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):
|
||||||
|
'''Add child node to `children` dictionary, indexed by the nodes' `letter`'''
|
||||||
|
self.children[node.letters] = node
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path(self) -> list[WordNode]:
|
||||||
|
'''Return list of nodes from current node to the root'''
|
||||||
|
curr_node = self
|
||||||
|
path = []
|
||||||
|
path.append(curr_node.board_pos)
|
||||||
|
while curr_node.parent is not None:
|
||||||
|
path.append(curr_node.parent.board_pos)
|
||||||
|
curr_node = curr_node.parent
|
||||||
|
|
||||||
|
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):
|
||||||
|
return f"WordNode: {self.letters}, {self.is_word}, {self.board_pos}"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"WordNode: {self.letters}, {self.is_word}, {self.children}"
|
||||||
|
|
||||||
|
class WordTree:
|
||||||
|
'''A tree populated by WordNode(s) to complete words from a given root letter and wordlist'''
|
||||||
|
def __init__(self, alphabet: list, root: WordNode, words: list[str] = None,
|
||||||
|
max_word_len = 16) -> WordTree:
|
||||||
|
self.__alphabet: list = alphabet
|
||||||
|
self.__wordlist: list[str] = words
|
||||||
|
self.__root: WordNode = root
|
||||||
|
self.__max_word_len: int = max_word_len
|
||||||
|
self.__tree: dict[str, WordNode] = {}
|
||||||
|
self.__word_paths: dict[str, list[WordNode]] = []
|
||||||
|
|
||||||
|
# Generate root node
|
||||||
|
self.__tree[root.letters] = root
|
||||||
|
self.active_node: WordNode = self.__tree[root.letters]
|
||||||
|
|
||||||
|
# Populate tree from wordlist
|
||||||
|
if words is not None:
|
||||||
|
for word in words:
|
||||||
|
self.__insert_word(word)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alphabet(self) -> list:
|
||||||
|
'''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):
|
||||||
|
return ", ".join(self.wordlist)
|
||||||
|
|
||||||
|
def insert_node(self, letters: str, parent: WordNode, is_word: bool = False,
|
||||||
|
children: dict[str, WordNode] = None, board_pos = None):
|
||||||
|
'''Create WordNode for `letters` and into WordTree under `parent`'''
|
||||||
|
node = WordNode(letters, is_word, parent, children, board_pos)
|
||||||
|
parent.add_child_node(node)
|
||||||
|
|
||||||
|
def __insert_word(self, word: str) -> bool:
|
||||||
|
'''Returns True if the word could be inserted into the tree with the given alphabet,
|
||||||
|
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
|
||||||
|
curr_node = self.tree[prefix]
|
||||||
|
word_len = len(word)
|
||||||
|
|
||||||
|
# Insert remaining nodes as letter groups based on alphabet
|
||||||
|
skip_cnt = 0
|
||||||
|
for i, letter in enumerate(word[len(prefix):self.max_word_len]):
|
||||||
|
# Skip word letter iterations for length of a previously inserted letter group
|
||||||
|
if skip_cnt > 0:
|
||||||
|
skip_cnt -= 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
alpha_index = 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
|
||||||
|
return True
|
||||||
|
|
||||||
|
def search(self, word: str, curr_node = None) -> WordNode:
|
||||||
|
'''Return leaf (WordNode) if a given word is in the tree otherwise return None'''
|
||||||
|
if len(word) == 0 or word is None:
|
||||||
|
return curr_node
|
||||||
|
|
||||||
|
if curr_node is None:
|
||||||
|
curr_node = self.root
|
||||||
|
for letters in curr_node.children:
|
||||||
|
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])
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, word_len: int = 0) -> WordTree:
|
||||||
|
'''Return subtree of board given a particular root (first letter).
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
board -- the board the new tree is based on
|
||||||
|
board_node -- a pointer on the board where new branch nodes can be inserted into the tree
|
||||||
|
dict_node -- a pointer on the dictionary where nodes are read from for validation
|
||||||
|
subtree -- the partial tree passed to the next recursive step for generating branches
|
||||||
|
'''
|
||||||
|
|
||||||
|
if word_len > self.max_word_len or word_len >= board.max_word_len:
|
||||||
|
#print(f"MAX DEPTH REACHED! Depth = {depth}")
|
||||||
|
subtree.active_node = subtree.active_node.parent
|
||||||
|
self.active_node = self.active_node.parent
|
||||||
|
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:
|
||||||
|
word_path = subtree.active_node.path[::-1]
|
||||||
|
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
|
||||||
|
subtree.active_node = subtree.active_node.parent
|
||||||
|
return
|
||||||
|
elif self.active_node.is_word:
|
||||||
|
word_path = subtree.active_node.path[::-1]
|
||||||
|
subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
|
||||||
|
#print("2: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path)
|
||||||
|
|
||||||
|
# Branch for each adjacent board cell
|
||||||
|
for cell in board_cell.adjacent_cells:
|
||||||
|
# Check dictionary and exclude nodes already in path
|
||||||
|
if cell.letters 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)
|
||||||
|
subtree.active_node.add_child_node(new_node)
|
||||||
|
subtree.active_node = subtree.active_node.children[cell.letters] # update subtree pointer
|
||||||
|
self.active_node = self.active_node.children[cell.letters] # update dictionary tree pointer
|
||||||
|
self.build_boggle_tree(board, board.board[cell.pos], subtree, word_len=word_len+len(board_cell.letters))
|
||||||
|
|
||||||
|
self.active_node = self.active_node.parent
|
||||||
|
subtree.active_node = subtree.active_node.parent
|
||||||
|
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 letters in alphabet:
|
||||||
|
if letters == "":
|
||||||
|
# Skip wordlist read for blocks with empty string
|
||||||
|
index[""] = {}
|
||||||
|
continue
|
||||||
|
elif letters[0] in index:
|
||||||
|
index[letters] = index[letters[0]]
|
||||||
|
continue
|
||||||
|
|
||||||
|
filename = "words_" + letters[0] + ".txt"
|
||||||
|
try:
|
||||||
|
wordlist = read_wordlist(path.join(path.abspath(wordlist_path), filename))
|
||||||
|
index[letters] = wordlist
|
||||||
|
print(f">> {letters}: {filename}")
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f">> {letters}: -- Skipping -- no wordlist found for {letters}")
|
||||||
|
index[letters] = {}
|
||||||
|
|
||||||
|
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)):
|
||||||
|
print(f">> {params[i][2]}")
|
||||||
|
board_tree[params[i][2].pos] = res
|
||||||
|
|
||||||
|
return board_tree
|
||||||
|
|
||||||
|
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 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()]
|
||||||
|
|
||||||
|
def find_paths_by_word(board_letters, dictionary_path, max_len):
|
||||||
|
'''Return list of paths by word'''
|
||||||
|
boggle_board = BoggleBoard(board_letters, max_len)
|
||||||
|
boggle_tree = build_full_boggle_tree(boggle_board, dictionary_path)
|
||||||
|
paths_by_word = functools.reduce(operator.iconcat, [x.word_paths for x in boggle_tree.values()], [])
|
||||||
|
return paths_by_word
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Listed alphabetically
|
||||||
|
a,a,c,i,o,t
|
||||||
|
a,b,i,l,t,y
|
||||||
|
a,b,j,m,o,qu
|
||||||
|
a,c,d,e,m,p
|
||||||
|
a,c,e,l,r,s
|
||||||
|
a,d,e,n,v,z
|
||||||
|
a,h,m,o,r,s
|
||||||
|
b,i,f,o,r,x
|
||||||
|
d,e,n,o,s,w
|
||||||
|
d,k,n,o,t,u
|
||||||
|
e,e,f,h,i,y
|
||||||
|
e,g,k,l,u,y
|
||||||
|
e,g,i,n,t,v
|
||||||
|
e,h,i,n,p,s
|
||||||
|
e,l,p,s,t,u
|
||||||
|
g,i,l,r,u,w
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
# Listed alphabetically
|
||||||
|
a,a,e,e,g,n
|
||||||
|
a,b,b,j,o,o
|
||||||
|
a,c,h,o,p,s
|
||||||
|
a,f,f,k,p,s
|
||||||
|
a,o,o,t,t,w
|
||||||
|
c,i,m,o,t,u
|
||||||
|
d,e,i,l,r,x
|
||||||
|
d,e,l,r,v,y
|
||||||
|
d,i,s,t,t,y
|
||||||
|
e,e,g,h,n,w
|
||||||
|
e,e,i,n,s,u
|
||||||
|
e,h,r,t,v,w
|
||||||
|
e,i,o,s,s,t
|
||||||
|
e,l,r,t,t,y
|
||||||
|
h,i,m,n,qu,u
|
||||||
|
h,l,n,n,r,z
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
# Listed alphabetically
|
||||||
|
a,a,a,f,r,s
|
||||||
|
a,a,e,e,e,e
|
||||||
|
a,a,f,i,r,s
|
||||||
|
a,d,e,n,n,n
|
||||||
|
a,e,e,e,e,m
|
||||||
|
a,e,e,g,m,u
|
||||||
|
a,e,g,m,n,n
|
||||||
|
a,f,i,r,s,y
|
||||||
|
an,er,he,in,qu,th
|
||||||
|
b,j,k,qu,x,z
|
||||||
|
c,c,e,n,s,t
|
||||||
|
c,e,i,i,l,t
|
||||||
|
c,e,i,l,p,t
|
||||||
|
c,e,i,p,s,t
|
||||||
|
d,d,h,n,o,t
|
||||||
|
d,h,h,l,n,o
|
||||||
|
d,h,l,n,o,r
|
||||||
|
d,l,o,h,h,r
|
||||||
|
e,i,i,i,t,t
|
||||||
|
e,m,o,t,t,t
|
||||||
|
e,n,s,s,s,u
|
||||||
|
f,i,p,r,s,y
|
||||||
|
g,o,r,r,v,w
|
||||||
|
i,k,l,qu,u,w
|
||||||
|
n,o,o,t,u,w
|
||||||
|
o,o,o,t,t,u
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
# Listed alphabetically
|
||||||
|
_,_,_,e,i,o
|
||||||
|
a,a,a,f,r,s
|
||||||
|
a,a,e,e,e,e
|
||||||
|
a,a,e,e,o,o
|
||||||
|
a,a,f,i,r,s
|
||||||
|
a,b,d,e,i,o
|
||||||
|
a,d,e,n,n,n
|
||||||
|
a,e,e,e,e,m
|
||||||
|
a,e,e,g,m,u
|
||||||
|
a,e,g,m,n,n
|
||||||
|
a,e,i,l,m,n
|
||||||
|
a,e,i,n,o,u
|
||||||
|
a,f,i,r,s,y
|
||||||
|
an,er,he,in,qu,th
|
||||||
|
b,b,b,j,j,z
|
||||||
|
c,c,e,n,s,t
|
||||||
|
c,d,d,l,n,n
|
||||||
|
c,e,e,i,t,t
|
||||||
|
c,e,i,p,s,t
|
||||||
|
c,f,g,n,u,w
|
||||||
|
d,d,h,n,o,t
|
||||||
|
d,h,h,l,o,r
|
||||||
|
d,h,h,n,o,w
|
||||||
|
d,h,l,n,o,r
|
||||||
|
e,h,i,l,r,s
|
||||||
|
e,i,i,l,s,t
|
||||||
|
e,i,l,p,s,t
|
||||||
|
e,m,o,t,t,t
|
||||||
|
e,n,s,s,s,u
|
||||||
|
g,o,r,r,v,w
|
||||||
|
h,i,r,s,t,v
|
||||||
|
h,o,p,r,s,t
|
||||||
|
i,p,r,s,y,y
|
||||||
|
j,k,qu,w,x,z
|
||||||
|
n,o,o,t,u,w
|
||||||
|
o,o,o,t,t,u
|
||||||
|
@@ -0,0 +1,82 @@
|
|||||||
|
# Made from 6x6 dice repeated to reach 9x9 (81) dice rolls
|
||||||
|
_,_,_,e,i,o
|
||||||
|
a,a,a,f,r,s
|
||||||
|
a,a,e,e,e,e
|
||||||
|
a,a,e,e,o,o
|
||||||
|
a,a,f,i,r,s
|
||||||
|
a,b,d,e,i,o
|
||||||
|
a,d,e,n,n,n
|
||||||
|
a,e,e,e,e,m
|
||||||
|
a,e,e,g,m,u
|
||||||
|
a,e,g,m,n,n
|
||||||
|
a,e,i,l,m,n
|
||||||
|
a,e,i,n,o,u
|
||||||
|
a,f,i,r,s,y
|
||||||
|
an,er,he,in,qu,th
|
||||||
|
b,b,b,j,j,z
|
||||||
|
c,c,e,n,s,t
|
||||||
|
c,d,d,l,n,n
|
||||||
|
c,e,e,i,t,t
|
||||||
|
c,e,i,p,s,t
|
||||||
|
c,f,g,n,u,w
|
||||||
|
d,d,h,n,o,t
|
||||||
|
d,h,h,l,o,r
|
||||||
|
d,h,h,n,o,w
|
||||||
|
d,h,l,n,o,r
|
||||||
|
e,h,i,l,r,s
|
||||||
|
e,i,i,l,s,t
|
||||||
|
e,i,l,p,s,t
|
||||||
|
e,m,o,t,t,t
|
||||||
|
e,n,s,s,s,u
|
||||||
|
g,o,r,r,v,w
|
||||||
|
h,i,r,s,t,v
|
||||||
|
h,o,p,r,s,t
|
||||||
|
i,p,r,s,y,y
|
||||||
|
j,k,qu,w,x,z
|
||||||
|
n,o,o,t,u,w
|
||||||
|
o,o,o,t,t,u
|
||||||
|
_,_,_,e,i,o
|
||||||
|
a,a,a,f,r,s
|
||||||
|
a,a,e,e,e,e
|
||||||
|
a,a,e,e,o,o
|
||||||
|
a,a,f,i,r,s
|
||||||
|
a,b,d,e,i,o
|
||||||
|
a,d,e,n,n,n
|
||||||
|
a,e,e,e,e,m
|
||||||
|
a,e,e,g,m,u
|
||||||
|
a,e,g,m,n,n
|
||||||
|
a,e,i,l,m,n
|
||||||
|
a,e,i,n,o,u
|
||||||
|
a,f,i,r,s,y
|
||||||
|
an,er,he,in,qu,th
|
||||||
|
b,b,b,j,j,z
|
||||||
|
c,c,e,n,s,t
|
||||||
|
c,d,d,l,n,n
|
||||||
|
c,e,e,i,t,t
|
||||||
|
c,e,i,p,s,t
|
||||||
|
c,f,g,n,u,w
|
||||||
|
d,d,h,n,o,t
|
||||||
|
d,h,h,l,o,r
|
||||||
|
d,h,h,n,o,w
|
||||||
|
d,h,l,n,o,r
|
||||||
|
e,h,i,l,r,s
|
||||||
|
e,i,i,l,s,t
|
||||||
|
e,i,l,p,s,t
|
||||||
|
e,m,o,t,t,t
|
||||||
|
e,n,s,s,s,u
|
||||||
|
g,o,r,r,v,w
|
||||||
|
h,i,r,s,t,v
|
||||||
|
h,o,p,r,s,t
|
||||||
|
i,p,r,s,y,y
|
||||||
|
j,k,qu,w,x,z
|
||||||
|
n,o,o,t,u,w
|
||||||
|
o,o,o,t,t,u
|
||||||
|
e,m,o,t,t,t
|
||||||
|
e,n,s,s,s,u
|
||||||
|
g,o,r,r,v,w
|
||||||
|
h,i,r,s,t,v
|
||||||
|
h,o,p,r,s,t
|
||||||
|
i,p,r,s,y,y
|
||||||
|
j,k,qu,w,x,z
|
||||||
|
n,o,o,t,u,w
|
||||||
|
o,o,o,t,t,u
|
||||||
|
Executable
+35
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ -z $1 ] || [ -z $2 ]; then
|
||||||
|
echo "Usage: WORDLIST OUT_DIR"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
rg "(^a[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_a.txt"
|
||||||
|
rg "(^b[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_b.txt"
|
||||||
|
rg "(^c[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_c.txt"
|
||||||
|
rg "(^d[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_d.txt"
|
||||||
|
rg "(^e[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_e.txt"
|
||||||
|
rg "(^f[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_f.txt"
|
||||||
|
rg "(^g[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_g.txt"
|
||||||
|
rg "(^h[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_h.txt"
|
||||||
|
rg "(^i[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_i.txt"
|
||||||
|
rg "(^j[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_j.txt"
|
||||||
|
rg "(^k[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_k.txt"
|
||||||
|
rg "(^l[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_l.txt"
|
||||||
|
rg "(^m[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_m.txt"
|
||||||
|
rg "(^n[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_n.txt"
|
||||||
|
rg "(^o[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_o.txt"
|
||||||
|
rg "(^p[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_p.txt"
|
||||||
|
rg "(^q[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_q.txt"
|
||||||
|
rg "(^r[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_r.txt"
|
||||||
|
rg "(^s[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_s.txt"
|
||||||
|
rg "(^t[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_t.txt"
|
||||||
|
rg "(^u[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_u.txt"
|
||||||
|
rg "(^v[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_v.txt"
|
||||||
|
rg "(^w[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_w.txt"
|
||||||
|
rg "(^x[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_x.txt"
|
||||||
|
rg "(^y[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_y.txt"
|
||||||
|
rg "(^z[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_z.txt"
|
||||||
|
touch "$2/words__.txt"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user