-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathboggle.py
More file actions
147 lines (115 loc) · 4.54 KB
/
boggle.py
File metadata and controls
147 lines (115 loc) · 4.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/python
"""Solve Boggle game
## Usage: boggle [OPTIONS] [LETTERS]...
Options:
--size INTEGER
--help
## Example
`boggle lnto epro stie nesi`
Default is a 4x4 board. Option -size overrides
If no letters are given, random letters are chosen.
Note: Enter Qu as if it were a single letter.
The trie.py program is provided to format dictionaries.
"""
import click
from itertools import repeat
import random
from helpers import normalize_qu, boggle_dice
from trie import Trie, TrieNode
DIRECTIONS = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
@click.command()
@click.option("--size", type=int, default=4)
@click.argument("letters", nargs=-1, type=str)
def cli(letters, size):
"""Run the Boggle solver from the command line."""
game = Boggle(letters=letters, size=size)
game.display_board()
words = game.find_words()
click.secho(f"{len(words)} words found:", fg="yellow")
click.secho(sorted(words, key=len))
class Boggle:
"""solve the classic Boggle word game by Parker Brothers"""
# the boggle dictionary is large, and slow to load.
# Share a single copy among all instances.
dictionary = Trie()
def __init__(self, size=4, letters=None):
self.size = size
if self.size < 2:
raise ValueError("Board size too small")
if letters:
self.board = self.form_board(self.load(letters))
else:
self.board = self.form_board(self.generate_random_boggle_letters())
self.visited = self.form_board(repeat(False))
if not self.dictionary:
self.dictionary = Trie.load_from_file()
def load(self, raw_chars):
"""Parse user input into boggle-normalized letters."""
cleaned_chars = [ch.lower() for ch in "".join(raw_chars) if ch.isalpha()]
boggle_chars = list(normalize_qu(cleaned_chars))
if len(boggle_chars) != self.size * self.size:
raise ValueError(
f"{len(boggle_chars)} letters cannot be formatted into a {self.size}x{self.size} board"
)
yield from boggle_chars
def form_board(self, letter):
"""make a square matrix from a generator of letters"""
return [[next(letter) for _ in range(self.size)] for _ in range(self.size)]
def generate_random_boggle_letters(self):
"""Yield random letters by rolling each Boggle cube in shuffled order."""
cubes = list(boggle_dice) # copy to avoid modifying the original
random.shuffle(cubes)
while True:
for cube in cubes:
yield random.choice(cube)
def find_words(self):
"""Return the set of all dictionary words found on the board."""
found_words = set()
for i in range(self.size):
for j in range(self.size):
first_letter = self.board[i][j]
candidates = self.dictionary.root.children
if first_letter in candidates:
self.search_word(
i, j, candidates[first_letter], first_letter, found_words
)
return found_words
def search_word(self, x, y, node, path, found_words):
"""Recursively explore adjacent cells to find words via DFS."""
is_on_grid = (0 <= x < self.size) and (0 <= y < self.size)
if not is_on_grid or self.visited[x][y]:
return
if node.is_end_of_word and len(path) > 2:
found_words.add(path)
self.visited[x][y] = True
for dx, dy in DIRECTIONS:
nx, ny = x + dx, y + dy
is_on_grid = (0 <= nx < self.size) and (0 <= ny < self.size)
if not is_on_grid:
continue
next_letter = self.board[nx][ny]
if next_letter in node.children and not self.visited[nx][ny]:
self.search_word(
nx,
ny,
node.children[next_letter],
path + next_letter,
found_words,
)
self.visited[x][y] = False
def to_dict(self):
"""Return the board and found words as a serializable dictionary."""
words = self.find_words()
return {
"board": self.board,
"size": self.size,
"words": sorted(words, key=len),
"count": len(words),
}
def display_board(self):
"""Print the board grid to the terminal."""
for row in self.board:
click.echo(" ".join(row))
click.echo()
if __name__ == "__main__":
cli()