51 Commits
Author SHA1 Message Date
cblanken a80111072f Bump release version 2022-12-30 20:55:44 -05:00
cblanken fa01d442a5 Fix bug in board randomizer
- The board randomizer did not account for dice files that don't contain an exact integer squared number of dice. For example the 5x5 dice file contains 26 possible dice since 1 extra die is provided.
- The fix takes the square root of the number of dice and takes the floor of that number as the total number of dice to choose.
2022-12-30 19:54:00 -05:00
cblanken 733c1474b0 Add 5x5 dice file 2022-12-30 19:51:41 -05:00
cblanken 54f79c0afe Remove dead code from boggler.py 2022-11-16 20:37:30 -05:00
cblanken 9f364ee90e Remove old dictionary word prefix lists 2022-11-16 20:36:55 -05:00
cblanken e219954ba6 Add function to flatten boggle board results into 1D list 2022-10-10 12:00:42 -04:00
cblanken f4accfe959 Update wordlist splitting script
- use ripgrep to exclude proper nouns (with capitals) output only the
  capture group
2022-09-28 13:01:39 -04:00
cblanken bc70b88bf7 Bump version 2.0.0 2022-09-26 11:44:51 -04:00
cblanken 21aab46b96 Refactor board_randomizer
- Make file read separate and return "dice" or a list of strings from
  dice handling functions for use in downstream programs
2022-09-26 11:40:36 -04:00
cblanken f18c30cb60 Rename Super Big Boggle dice file 2022-09-23 10:20:48 -04:00
cblanken da915b723c Update wordlist splitting script
- Force lowercase on split
- Only match single (non compound) words
- Don't match punctuation
- Sort and uniquify
2022-09-23 10:20:48 -04:00
cblanken d5a102713d Fix die letters for 'New Boggle' dice 2022-09-23 10:20:38 -04:00
cblanken c488263eea Bump version and update license 2022-09-21 10:15:14 -04:00
cblanken fe1f1c2b97 Fix MAX_WORD_LENGTH parsing 2022-09-21 10:04:02 -04:00
cblanken d2584686a3 Fix max word length
- Previously the max word length parameter was checking the tree depths
  instead of the length of the word
- This fix acounts for multi-letter blocks like 'Qu', 'Tr' etc.
2022-09-21 09:45:09 -04:00
cblanken 8f75604d15 Handle blocks with empty strings and non-existant wordlists 2022-09-20 22:45:57 -04:00
cblanken 96da732dd9 Update board_randomizer module
- Fix typo: randimizer to randomizer
- Add module function to get direct 2d array of board values
2022-09-20 15:58:26 -04:00
cblanken ec53042d18 Update and move word splitting script 2022-09-20 13:21:57 -04:00
cblankenandGitHub 5c18a4aebf Update README.md 2022-09-15 14:06:55 -04:00
cblanken b197253d86 Add 9x9 dice file 2022-09-15 13:44:14 -04:00
cblanken 67f8c439b0 Update release version 2022-09-15 13:44:14 -04:00
cblanken 8c41ad212d Add missing __init__.py for PyPI package 2022-09-15 13:44:14 -04:00
cblanken 94800909b4 Update project version number 2022-09-15 13:44:14 -04:00
cblanken f24b996411 Update README.md 2022-09-15 13:44:14 -04:00
cblanken e4df95cd08 Update .gitignore 2022-09-15 13:44:14 -04:00
cblanken 0e9b120e94 Reorganize files into PyPI package format 2022-09-15 13:44:14 -04:00
cblanken 276c27f1ce Add pyproject.toml for PyPI 2022-09-15 13:44:13 -04:00
cblanken d30c44af3c Add MIT LICENSE 2022-09-15 13:43:55 -04:00
cblanken 5da33cf02c Update dice files 2022-09-07 11:27:09 -04:00
cblanken 600efc19c2 Remove old demo code from boggler_utils.py 2022-09-07 10:29:56 -04:00
cblanken cdb1542184 Fix bug loading duplicate wordlist for multi-letter blocks 2022-09-07 10:28:12 -04:00
cblanken 795f1540b4 Generate '_' words file for 6x6 board 2022-09-06 15:34:01 -04:00
cblanken 91390ea425 Add empty _ word files
The "_" is used to support the "blank" or "block" characters in Super Big Boggle (6x6).
2022-09-06 15:28:15 -04:00
cblanken 2a6a27b0b8 Update 4x4 dice and add 6x6 dice 2022-09-06 15:27:19 -04:00
cblanken b445071ccc Merge branch 'multiprocessing' 2022-09-06 15:10:27 -04:00
cblanken 9c8e94964c Add multiprocessing for board solving 2022-09-06 15:08:26 -04:00
cblanken 2fd5b05717 Update .gitignore 2022-09-01 15:29:37 -04:00
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
96 changed files with 1208 additions and 59272 deletions
+8
View File
@@ -1 +1,9 @@
# Byte-compiled / optimized
__pycache__/
*.py[cod]
# Distribution / packaging
dist/
# Env
.vscode
+8
View File
@@ -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.
+403
View File
@@ -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
-30
View File
@@ -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]))
-4
View File
@@ -1,4 +0,0 @@
aucd
eegh
hios
trea
-4
View File
@@ -1,4 +0,0 @@
vdau
ptuo
ohxr
wlhn
-4
View File
@@ -1,4 +0,0 @@
nxaa
vatr
ieba
iurn
-4
View File
@@ -1,4 +0,0 @@
vdat
pteo
ohnr
wlan
-5
View File
@@ -1,5 +0,0 @@
nbapi
satrk
iebae
turnl
icqme
-6
View File
@@ -1,6 +0,0 @@
nbapir
satrkp
iebaez
turnls
icqmet
nynjro
-111
View File
@@ -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)
-308
View File
@@ -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"))
-18
View File
@@ -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
+23
View File
@@ -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"
-34
View File
@@ -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
View File
+48
View File
@@ -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)
+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
@@ -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
@@ -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
@@ -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
@@ -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
+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
+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
+34
View File
@@ -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)
+429
View File
@@ -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
+17
View File
@@ -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
1 # Listed alphabetically
2 a,a,c,i,o,t
3 a,b,i,l,t,y
4 a,b,j,m,o,qu
5 a,c,d,e,m,p
6 a,c,e,l,r,s
7 a,d,e,n,v,z
8 a,h,m,o,r,s
9 b,i,f,o,r,x
10 d,e,n,o,s,w
11 d,k,n,o,t,u
12 e,e,f,h,i,y
13 e,g,k,l,u,y
14 e,g,i,n,t,v
15 e,h,i,n,p,s
16 e,l,p,s,t,u
17 g,i,l,r,u,w
+17
View File
@@ -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
1 # Listed alphabetically
2 a,a,e,e,g,n
3 a,b,b,j,o,o
4 a,c,h,o,p,s
5 a,f,f,k,p,s
6 a,o,o,t,t,w
7 c,i,m,o,t,u
8 d,e,i,l,r,x
9 d,e,l,r,v,y
10 d,i,s,t,t,y
11 e,e,g,h,n,w
12 e,e,i,n,s,u
13 e,h,r,t,v,w
14 e,i,o,s,s,t
15 e,l,r,t,t,y
16 h,i,m,n,qu,u
17 h,l,n,n,r,z
+27
View File
@@ -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
1 # Listed alphabetically
2 a,a,a,f,r,s
3 a,a,e,e,e,e
4 a,a,f,i,r,s
5 a,d,e,n,n,n
6 a,e,e,e,e,m
7 a,e,e,g,m,u
8 a,e,g,m,n,n
9 a,f,i,r,s,y
10 an,er,he,in,qu,th
11 b,j,k,qu,x,z
12 c,c,e,n,s,t
13 c,e,i,i,l,t
14 c,e,i,l,p,t
15 c,e,i,p,s,t
16 d,d,h,n,o,t
17 d,h,h,l,n,o
18 d,h,l,n,o,r
19 d,l,o,h,h,r
20 e,i,i,i,t,t
21 e,m,o,t,t,t
22 e,n,s,s,s,u
23 f,i,p,r,s,y
24 g,o,r,r,v,w
25 i,k,l,qu,u,w
26 n,o,o,t,u,w
27 o,o,o,t,t,u
+37
View File
@@ -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
1 # Listed alphabetically
2 _,_,_,e,i,o
3 a,a,a,f,r,s
4 a,a,e,e,e,e
5 a,a,e,e,o,o
6 a,a,f,i,r,s
7 a,b,d,e,i,o
8 a,d,e,n,n,n
9 a,e,e,e,e,m
10 a,e,e,g,m,u
11 a,e,g,m,n,n
12 a,e,i,l,m,n
13 a,e,i,n,o,u
14 a,f,i,r,s,y
15 an,er,he,in,qu,th
16 b,b,b,j,j,z
17 c,c,e,n,s,t
18 c,d,d,l,n,n
19 c,e,e,i,t,t
20 c,e,i,p,s,t
21 c,f,g,n,u,w
22 d,d,h,n,o,t
23 d,h,h,l,o,r
24 d,h,h,n,o,w
25 d,h,l,n,o,r
26 e,h,i,l,r,s
27 e,i,i,l,s,t
28 e,i,l,p,s,t
29 e,m,o,t,t,t
30 e,n,s,s,s,u
31 g,o,r,r,v,w
32 h,i,r,s,t,v
33 h,o,p,r,s,t
34 i,p,r,s,y,y
35 j,k,qu,w,x,z
36 n,o,o,t,u,w
37 o,o,o,t,t,u
+82
View File
@@ -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
1 # Made from 6x6 dice repeated to reach 9x9 (81) dice rolls
2 _,_,_,e,i,o
3 a,a,a,f,r,s
4 a,a,e,e,e,e
5 a,a,e,e,o,o
6 a,a,f,i,r,s
7 a,b,d,e,i,o
8 a,d,e,n,n,n
9 a,e,e,e,e,m
10 a,e,e,g,m,u
11 a,e,g,m,n,n
12 a,e,i,l,m,n
13 a,e,i,n,o,u
14 a,f,i,r,s,y
15 an,er,he,in,qu,th
16 b,b,b,j,j,z
17 c,c,e,n,s,t
18 c,d,d,l,n,n
19 c,e,e,i,t,t
20 c,e,i,p,s,t
21 c,f,g,n,u,w
22 d,d,h,n,o,t
23 d,h,h,l,o,r
24 d,h,h,n,o,w
25 d,h,l,n,o,r
26 e,h,i,l,r,s
27 e,i,i,l,s,t
28 e,i,l,p,s,t
29 e,m,o,t,t,t
30 e,n,s,s,s,u
31 g,o,r,r,v,w
32 h,i,r,s,t,v
33 h,o,p,r,s,t
34 i,p,r,s,y,y
35 j,k,qu,w,x,z
36 n,o,o,t,u,w
37 o,o,o,t,t,u
38 _,_,_,e,i,o
39 a,a,a,f,r,s
40 a,a,e,e,e,e
41 a,a,e,e,o,o
42 a,a,f,i,r,s
43 a,b,d,e,i,o
44 a,d,e,n,n,n
45 a,e,e,e,e,m
46 a,e,e,g,m,u
47 a,e,g,m,n,n
48 a,e,i,l,m,n
49 a,e,i,n,o,u
50 a,f,i,r,s,y
51 an,er,he,in,qu,th
52 b,b,b,j,j,z
53 c,c,e,n,s,t
54 c,d,d,l,n,n
55 c,e,e,i,t,t
56 c,e,i,p,s,t
57 c,f,g,n,u,w
58 d,d,h,n,o,t
59 d,h,h,l,o,r
60 d,h,h,n,o,w
61 d,h,l,n,o,r
62 e,h,i,l,r,s
63 e,i,i,l,s,t
64 e,i,l,p,s,t
65 e,m,o,t,t,t
66 e,n,s,s,s,u
67 g,o,r,r,v,w
68 h,i,r,s,t,v
69 h,o,p,r,s,t
70 i,p,r,s,y,y
71 j,k,qu,w,x,z
72 n,o,o,t,u,w
73 o,o,o,t,t,u
74 e,m,o,t,t,t
75 e,n,s,s,s,u
76 g,o,r,r,v,w
77 h,i,r,s,t,v
78 h,o,p,r,s,t
79 i,p,r,s,y,y
80 j,k,qu,w,x,z
81 n,o,o,t,u,w
82 o,o,o,t,t,u
+35
View File
@@ -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