Skip to content

Commit dac1f21

Browse files
committed
feat(other): Create word_search algorithm
1 parent b77e6ad commit dac1f21

File tree

1 file changed

+395
-0
lines changed

1 file changed

+395
-0
lines changed

other/word_search.py

+395
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
"""
2+
Creates a random wordsearch with eight different directions
3+
that are best described as compass locations.
4+
5+
@
6+
"""
7+
8+
9+
from random import choice, randint, shuffle
10+
11+
# The words to display on the word search -
12+
# can be made dynamic by randonly selecting a certain number of
13+
# words from a predefined word file, while ensuring the character
14+
# count fits within the matrix size (n x m)
15+
WORDS = ["cat", "dog", "snake", "fish"]
16+
17+
WIDTH = 10
18+
HEIGHT = 10
19+
20+
21+
class WordSearch:
22+
"""
23+
>>> ws = WordSearch(WORDS, WIDTH, HEIGHT)
24+
>>> ws.board # doctest: +ELLIPSIS
25+
[[None, ..., None], ..., [None, ..., None]]
26+
>>> ws.generate_board()
27+
"""
28+
29+
def __init__(self, words: list[str], width: int, height: int) -> None:
30+
self.words = words
31+
self.width = width
32+
self.height = height
33+
34+
# Board matrix holding each letter
35+
self.board: list[list[str | None]] = [[None] * width for _ in range(height)]
36+
37+
def insert_north(self, word: str, rows: list[int], cols: list[int]) -> None:
38+
"""
39+
>>> ws = WordSearch(WORDS, 3, 3)
40+
>>> ws.insert_north("cat", [2], [2])
41+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
42+
[[None, None, 't'],
43+
[None, None, 'a'],
44+
[None, None, 'c']]
45+
>>> ws.insert_north("at", [0, 1, 2], [2, 1])
46+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
47+
[[None, 't', 't'],
48+
[None, 'a', 'a'],
49+
[None, None, 'c']]
50+
"""
51+
word_length = len(word)
52+
# Attempt to insert the word into each row and when successful, exit
53+
for row in rows:
54+
# Check if there is space above the row to fit in the word
55+
if word_length > row + 1:
56+
continue
57+
58+
# Attempt to insert the word into each column
59+
for col in cols:
60+
# Only check to be made here is if there are existing letters
61+
# above the column that will be overwritten
62+
letters_above = [self.board[row - i][col] for i in range(word_length)]
63+
if all(letter is None for letter in letters_above):
64+
# Successful, insert the word north
65+
for i in range(word_length):
66+
self.board[row - i][col] = word[i]
67+
return
68+
69+
def insert_north_east(self, word: str, rows: list[int], cols: list[int]) -> None:
70+
"""
71+
>>> ws = WordSearch(WORDS, 3, 3)
72+
>>> ws.insert_north_east("cat", [2], [0])
73+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
74+
[[None, None, 't'],
75+
[None, 'a', None],
76+
['c', None, None]]
77+
>>> ws.insert_north_east("at", [0, 1], [2, 1, 0])
78+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
79+
[[None, 't', 't'],
80+
['a', 'a', None],
81+
['c', None, None]]
82+
"""
83+
word_length = len(word)
84+
# Attempt to insert the word into each row and when successful, exit
85+
for row in rows:
86+
# Check if there is space for the word above the row
87+
if word_length > row + 1:
88+
continue
89+
90+
# Attempt to insert the word into each column
91+
for col in cols:
92+
# Check if there is space to the right of the word as well as above
93+
if word_length + col > self.width:
94+
continue
95+
96+
# Check if there are existing letters
97+
# to the right of the column that will be overwritten
98+
letters_diagonal_left = [
99+
self.board[row - i][col + i] for i in range(word_length)
100+
]
101+
if all(letter is None for letter in letters_diagonal_left):
102+
# Successful, insert the word north
103+
for i in range(word_length):
104+
self.board[row - i][col + i] = word[i]
105+
return
106+
107+
def insert_east(self, word: str, rows: list[int], cols: list[int]) -> None:
108+
"""
109+
>>> ws = WordSearch(WORDS, 3, 3)
110+
>>> ws.insert_east("cat", [1], [0])
111+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
112+
[[None, None, None],
113+
['c', 'a', 't'],
114+
[None, None, None]]
115+
>>> ws.insert_east("at", [1, 0], [2, 1, 0])
116+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
117+
[[None, 'a', 't'],
118+
['c', 'a', 't'],
119+
[None, None, None]]
120+
"""
121+
word_length = len(word)
122+
# Attempt to insert the word into each row and when successful, exit
123+
for row in rows:
124+
# Attempt to insert the word into each column
125+
for col in cols:
126+
# Check if there is space to the right of the word
127+
if word_length + col > self.width:
128+
continue
129+
130+
# Check if there are existing letters
131+
# to the right of the column that will be overwritten
132+
letters_left = [self.board[row][col + i] for i in range(word_length)]
133+
if all(letter is None for letter in letters_left):
134+
# Successful, insert the word north
135+
for i in range(word_length):
136+
self.board[row][col + i] = word[i]
137+
return
138+
139+
def insert_south_east(self, word: str, rows: list[int], cols: list[int]) -> None:
140+
"""
141+
>>> ws = WordSearch(WORDS, 3, 3)
142+
>>> ws.insert_south_east("cat", [0], [0])
143+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
144+
[['c', None, None],
145+
[None, 'a', None],
146+
[None, None, 't']]
147+
>>> ws.insert_south_east("at", [1, 0], [2, 1, 0])
148+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
149+
[['c', None, None],
150+
['a', 'a', None],
151+
[None, 't', 't']]
152+
"""
153+
word_length = len(word)
154+
# Attempt to insert the word into each row and when successful, exit
155+
for row in rows:
156+
# Check if there is space for the word above the row
157+
if word_length + row > self.height:
158+
continue
159+
160+
# Attempt to insert the word into each column
161+
for col in cols:
162+
# Check if there is space to the right of the word as well as above
163+
if word_length + col > self.width:
164+
continue
165+
166+
# Check if there are existing letters
167+
# to the right of the column that will be overwritten
168+
letters_diagonal_left = [
169+
self.board[row + i][col + i] for i in range(word_length)
170+
]
171+
if all(letter is None for letter in letters_diagonal_left):
172+
# Successful, insert the word north
173+
for i in range(word_length):
174+
self.board[row + i][col + i] = word[i]
175+
return
176+
177+
def insert_south(self, word: str, rows: list[int], cols: list[int]) -> None:
178+
"""
179+
>>> ws = WordSearch(WORDS, 3, 3)
180+
>>> ws.insert_south("cat", [0], [0])
181+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
182+
[['c', None, None],
183+
['a', None, None],
184+
['t', None, None]]
185+
>>> ws.insert_south("at", [2, 1, 0], [0, 1, 2])
186+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
187+
[['c', None, None],
188+
['a', 'a', None],
189+
['t', 't', None]]
190+
"""
191+
word_length = len(word)
192+
# Attempt to insert the word into each row and when successful, exit
193+
for row in rows:
194+
# Check if there is space below the row to fit in the word
195+
if word_length + row > self.height:
196+
continue
197+
198+
# Attempt to insert the word into each column
199+
for col in cols:
200+
# Only check to be made here is if there are existing letters
201+
# below the column that will be overwritten
202+
letters_below = [self.board[row + i][col] for i in range(word_length)]
203+
if all(letter is None for letter in letters_below):
204+
# Successful, insert the word south
205+
for i in range(word_length):
206+
self.board[row + i][col] = word[i]
207+
return
208+
209+
def insert_south_west(self, word: str, rows: list[int], cols: list[int]) -> None:
210+
"""
211+
>>> ws = WordSearch(WORDS, 3, 3)
212+
>>> ws.insert_south_west("cat", [0], [2])
213+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
214+
[[None, None, 'c'],
215+
[None, 'a', None],
216+
['t', None, None]]
217+
>>> ws.insert_south_west("at", [1, 2], [2, 1, 0])
218+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
219+
[[None, None, 'c'],
220+
[None, 'a', 'a'],
221+
['t', 't', None]]
222+
"""
223+
word_length = len(word)
224+
# Attempt to insert the word into each row and when successful, exit
225+
for row in rows:
226+
# Check if there is space for the word above the row
227+
if word_length + row > self.height:
228+
continue
229+
230+
# Attempt to insert the word into each column
231+
for col in cols:
232+
# Check if there is space to the right of the word as well as above
233+
if word_length > col + 1:
234+
continue
235+
236+
# Check if there are existing letters
237+
# to the right of the column that will be overwritten
238+
letters_diagonal_left = [
239+
self.board[row + i][col - i] for i in range(word_length)
240+
]
241+
if all(letter is None for letter in letters_diagonal_left):
242+
# Successful, insert the word north
243+
for i in range(word_length):
244+
self.board[row + i][col - i] = word[i]
245+
return
246+
247+
def insert_west(self, word: str, rows: list[int], cols: list[int]) -> None:
248+
"""
249+
>>> ws = WordSearch(WORDS, 3, 3)
250+
>>> ws.insert_west("cat", [1], [2])
251+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
252+
[[None, None, None],
253+
['t', 'a', 'c'],
254+
[None, None, None]]
255+
>>> ws.insert_west("at", [1, 0], [1, 2, 0])
256+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
257+
[['t', 'a', None],
258+
['t', 'a', 'c'],
259+
[None, None, None]]
260+
"""
261+
word_length = len(word)
262+
# Attempt to insert the word into each row and when successful, exit
263+
for row in rows:
264+
# Attempt to insert the word into each column
265+
for col in cols:
266+
# Check if there is space to the left of the word
267+
if word_length > col + 1:
268+
continue
269+
270+
# Check if there are existing letters
271+
# to the left of the column that will be overwritten
272+
letters_left = [self.board[row][col - i] for i in range(word_length)]
273+
if all(letter is None for letter in letters_left):
274+
# Successful, insert the word north
275+
for i in range(word_length):
276+
self.board[row][col - i] = word[i]
277+
return
278+
279+
def insert_north_west(self, word: str, rows: list[int], cols: list[int]) -> None:
280+
"""
281+
>>> ws = WordSearch(WORDS, 3, 3)
282+
>>> ws.insert_north_west("cat", [2], [2])
283+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
284+
[['t', None, None],
285+
[None, 'a', None],
286+
[None, None, 'c']]
287+
>>> ws.insert_north_west("at", [1, 2], [0, 1])
288+
>>> ws.board # doctest: +NORMALIZE_WHITESPACE
289+
[['t', None, None],
290+
['t', 'a', None],
291+
[None, 'a', 'c']]
292+
"""
293+
word_length = len(word)
294+
# Attempt to insert the word into each row and when successful, exit
295+
for row in rows:
296+
# Check if there is space for the word above the row
297+
if word_length > row + 1:
298+
continue
299+
300+
# Attempt to insert the word into each column
301+
for col in cols:
302+
# Check if there is space to the right of the word as well as above
303+
if word_length > col + 1:
304+
continue
305+
306+
# Check if there are existing letters
307+
# to the right of the column that will be overwritten
308+
letters_diagonal_left = [
309+
self.board[row - i][col - i] for i in range(word_length)
310+
]
311+
if all(letter is None for letter in letters_diagonal_left):
312+
# Successful, insert the word north
313+
for i in range(word_length):
314+
self.board[row - i][col - i] = word[i]
315+
return
316+
317+
def generate_board(self) -> None:
318+
"""
319+
Generates a board with a random direction for each word.
320+
321+
>>> wt = WordSearch(WORDS, WIDTH, HEIGHT)
322+
>>> wt.generate_board()
323+
>>> len(list(filter(lambda word: word is not None, sum(wt.board, start=[])))
324+
... ) == sum(map(lambda word: len(word), WORDS))
325+
True
326+
"""
327+
directions = (
328+
self.insert_north,
329+
self.insert_north_east,
330+
self.insert_east,
331+
self.insert_south_east,
332+
self.insert_south,
333+
self.insert_south_west,
334+
self.insert_west,
335+
)
336+
for word in self.words:
337+
# Shuffle the row order and column order that is used when brute forcing
338+
# the insertion of the word
339+
rows, cols = list(range(self.height)), list(range(self.width))
340+
shuffle(rows)
341+
shuffle(cols)
342+
343+
# Insert the word via the direction
344+
choice(directions)(word, rows, cols)
345+
346+
347+
def visualise_word_search(
348+
board: list[list[str | None]] | None = None, *, add_fake_chars: bool = True
349+
) -> None:
350+
"""
351+
Graphically displays the word search in the terminal.
352+
353+
>>> ws = WordSearch(WORDS, 5, 5)
354+
>>> ws.insert_north("cat", [4], [4])
355+
>>> visualise_word_search(
356+
... ws.board, add_fake_chars=False) # doctest: +NORMALIZE_WHITESPACE
357+
# # # # #
358+
# # # # #
359+
# # # # t
360+
# # # # a
361+
# # # # c
362+
>>> ws.insert_north_east("snake", [4], [4, 3, 2, 1, 0])
363+
>>> visualise_word_search(
364+
... ws.board, add_fake_chars=False) # doctest: +NORMALIZE_WHITESPACE
365+
# # # # e
366+
# # # k #
367+
# # a # t
368+
# n # # a
369+
s # # # c
370+
"""
371+
if board is None:
372+
word_search = WordSearch(WORDS, WIDTH, HEIGHT)
373+
word_search.generate_board()
374+
board = word_search.board
375+
376+
result = ""
377+
for row in range(len(board)):
378+
for col in range(len(board[0])):
379+
character = "#"
380+
if (letter := board[row][col]) is not None:
381+
character = letter
382+
# Empty char, so add a fake char
383+
elif add_fake_chars:
384+
character = chr(randint(97, 122))
385+
result += f"{character} "
386+
result += "\n"
387+
print(result, end="")
388+
389+
390+
if __name__ == "__main__":
391+
import doctest
392+
393+
doctest.testmod()
394+
395+
visualise_word_search()

0 commit comments

Comments
 (0)