patternpythonMinor
Word Search Generator
Viewed 0 times
wordsearchgenerator
Problem
Last night I thought a word search would be fairly interesting to try. I'm not sure how proper ones run, but my one kind of works through the brute force method, where it'll place a letter in one spot (the first letter of a random word), and branch out in all directions while narrowing down the list of matching words until it finds a match (although if there's remaining words, there's a chance it'll continue as to not end up with all the shortest words).
Each attempt is an iteration, so obviously the more iterations, the more chance the grid will be filled with words.
It'll also generate similar words based on the words added to the search, to throw the user off a little (eg.
Instead of storing the values in a 2d grid, I stored them in a list, and used a separate function to calculate the next index when given an index and direction, and it checks that it's still on the grid and not gone off the side.
Examples (with using a list with all the english words):
```
14x14 output with 10 iterations:
w x w r r m j b v y x c k c
b h y d y s e e a i i z o s
o a n a l n t x s e j y c a
o r a t i o n a l s a a l f
b g u u n c o m b i n e d y
r z w e f e n n t e i x c z
t q r c r f s l n l k a m g
x u u f q s f w a c e i l o
v a z s o z w h r n o c w v
p l e l k t z v i r o e w j
i l c n f q r w a a v o k u
h s e i b l y u h r r c l u
a h k i t p b x o o w r r u
w x n t g f n g m j h s w e
Words: celt, mohair, rationals, ruers, uncombined
Solutions:
rationals: (1, 3) right
celt: (11, 8) left-up
mohair: (8, 13) up
ruers: (1, 3) down-right
uncombined: (3, 4) right
14x14 output with 20 iterations (in debug mode to remove extra letters):
n
w e s s
a p n y m y t s
l m r i
u o c
c l o v e s b k h
e y l e c
n l e a
i i s e
p k
Each attempt is an iteration, so obviously the more iterations, the more chance the grid will be filled with words.
It'll also generate similar words based on the words added to the search, to throw the user off a little (eg.
testing could also generate tesing, temtin, test, stin etc).Instead of storing the values in a 2d grid, I stored them in a list, and used a separate function to calculate the next index when given an index and direction, and it checks that it's still on the grid and not gone off the side.
Examples (with using a list with all the english words):
```
14x14 output with 10 iterations:
w x w r r m j b v y x c k c
b h y d y s e e a i i z o s
o a n a l n t x s e j y c a
o r a t i o n a l s a a l f
b g u u n c o m b i n e d y
r z w e f e n n t e i x c z
t q r c r f s l n l k a m g
x u u f q s f w a c e i l o
v a z s o z w h r n o c w v
p l e l k t z v i r o e w j
i l c n f q r w a a v o k u
h s e i b l y u h r r c l u
a h k i t p b x o o w r r u
w x n t g f n g m j h s w e
Words: celt, mohair, rationals, ruers, uncombined
Solutions:
rationals: (1, 3) right
celt: (11, 8) left-up
mohair: (8, 13) up
ruers: (1, 3) down-right
uncombined: (3, 4) right
14x14 output with 20 iterations (in debug mode to remove extra letters):
n
w e s s
a p n y m y t s
l m r i
u o c
c l o v e s b k h
e y l e c
n l e a
i i s e
p k
Solution
- Introduction
This is well-documented code but it is complex and awkward to use. Some work is needed to make it simpler.
The key problem, it seems to me, is that the caller is required to proceed in three steps: (i) create the
WordSearch object; (ii) load the list of words by calling the format_input_list method; (iii) call the generate method.It is not clear to me that anything is gained by this division into steps, so if I were writing it I would redesign the interface so that everything is done in one step. An additional advantage of this is that the user only has to read one docstring.
- Review
-
The code could easily be made portable to Python 3 by using the
print function instead of the print statement, and using range instead of xrange.-
width and height would be better parameter names than x and y.-
The default values to the constructor (x=0 and y=0) are unhelpful. Choosing values for parameters requires experience with an API, but you can't get experience with an API until you've chosen values for parameters. Good defaults help beginners get out of this cycle.
-
There's no need for
kwargs in __init__. Specify all keyword arguments explicitly:def __init__(self, width=10, height=10, difficulty=5):-
The
__repr__ is unhelpful too:>>> WordSearch()
Did this work or not? It is easier to work with objects that are self-describing.
-
The name
format_input_list is misleading: it does not actually format anything.-
word_length_min is not actually the minimum word length, but one less than the minimum. Possibly word_length_min
-
direction_coordinate is way too complicated. There are three kinds of decoding going here: (i) location identifiers to location coordinates (ii) direction numbers to direction names; (iii) direction names to directions. The fact that you need debug_grid is a sign that you've over-complicated things.
I would avoid (i) by representing the grid as a list of lists instead of a single list; and I would avoid (ii) by working directly with direction names instead of direction numbers. Using this approach you could cut out direction_coordinate completely.
To implement (iii), the code builds a decoding table every time direction_coordinate is called. But the decoding table is the same every time, so this is a waste. It would be better to make this a global variable, as in my revised code below.
-
Representing difficulty levels as numbers is hard to understand and needlessly complex (because there have to be encoding and decoding steps). It would be clearer to represent difficulty levels as collections of directions:
EASY = 'down right'.split()
MEDIUM = 'up down left right'.split()
HARD = 'up down left right upleft upright downleft downright'.split()
Using this approach, you could cut out get_difficulty completely.
-
The string 'abcdefghijklmnopqrstuvwxyz' is built into Python as string.ascii_lowercase.
- Revised code
This doesn't implement everything that the original code does (in particular, it doesn't populate the empty squares); the idea here is to give you an idea of how to go about keeping the code simple and short:
import random
import string
LETTERS = set(string.ascii_lowercase)
EMPTY = '.'
DIRECTIONS = dict(up=(-1,0), down=(1,0), left=(0,-1), right=(0,1),
upleft=(-1,-1), upright=(-1,1), downleft=(1,-1),
downright=(1,1))
class WordSearch:
"""A word search puzzle.
Arguments to the constructor:
width Width of the puzzle (default: 10).
height Height of the puzzle (default: 10).
word_list List of words to use (default: None).
word_file File to load words from (if word_list is None).
min_len Minimum length of words (default: 3).
max_len Maximum length of words (default: None).
directions Iterable of names of allowed directions (default: all eight).
density Stop generating when this density is reached (default: .7).
"""
def __init__(self, width=10, height=10, word_list=None, word_file=None,
min_len=3, max_len=None, directions=DIRECTIONS, density=.7):
# Check arguments and load word list.
if max_len is None:
max_len = min(width, height)
if word_list is None:
if word_file is None:
raise ValueError("neither word_list nor word_file specified")
word_list = []
with open(word_file) as f:
for line in f:
word = line.strip()
if set(word) = target_cells:
break
def __repr__(self):
grid = (''.join(row) for row in self.grid)
words = ('{} at ({},{}) {}'.format(*w) for w in self.words)
indent = lambda lines: '\n'.join(' ' + line for line in lines)
return "Grid:\n{}\nWords:\n{}".format(indent(grid), indent(words))
For example:
``>>> WordSearch(word_
Code Snippets
def __init__(self, width=10, height=10, difficulty=5):>>> WordSearch()
<__main__.WordSearch object at 0x10442f278>EASY = 'down right'.split()
MEDIUM = 'up down left right'.split()
HARD = 'up down left right upleft upright downleft downright'.split()import random
import string
LETTERS = set(string.ascii_lowercase)
EMPTY = '.'
DIRECTIONS = dict(up=(-1,0), down=(1,0), left=(0,-1), right=(0,1),
upleft=(-1,-1), upright=(-1,1), downleft=(1,-1),
downright=(1,1))
class WordSearch:
"""A word search puzzle.
Arguments to the constructor:
width Width of the puzzle (default: 10).
height Height of the puzzle (default: 10).
word_list List of words to use (default: None).
word_file File to load words from (if word_list is None).
min_len Minimum length of words (default: 3).
max_len Maximum length of words (default: None).
directions Iterable of names of allowed directions (default: all eight).
density Stop generating when this density is reached (default: .7).
"""
def __init__(self, width=10, height=10, word_list=None, word_file=None,
min_len=3, max_len=None, directions=DIRECTIONS, density=.7):
# Check arguments and load word list.
if max_len is None:
max_len = min(width, height)
if word_list is None:
if word_file is None:
raise ValueError("neither word_list nor word_file specified")
word_list = []
with open(word_file) as f:
for line in f:
word = line.strip()
if set(word) <= LETTERS and min_len <= len(word) <= max_len:
word_list.append(word)
else:
# Take a copy so that we can shuffle it without updating
# the original.
word_list = word_list[:]
random.shuffle(word_list)
# Initially empty grid and list of words.
self.grid = [[EMPTY] * width for _ in range(height)]
self.words = []
# Generate puzzle by adding words from word_list until either
# the word list is exhausted or the target density is reached.
filled_cells = 0
target_cells = width * height * density
for word in word_list:
# List of candidate positions as tuples (i, j, d) where
# (i, j) is the coordinate of the first letter and d is
# the direction.
candidates = []
for d in directions:
di, dj = DIRECTIONS[d]
for i in range(max(0, 0 - len(word) * di),
min(height, height - len(word) * di)):
for j in range(max(0, 0 - len(word) * dj),
min(width, width - len(word) * dj)):
for k, letter in enumerate(word):
g = self.grid[i + k * di][j + k * dj]
if g != letter and g != EMPTY:
break
else:
candidates.append((i, j, d))
if candidates:
i, j, d = random.choice(candidates)
>>> WordSearch(word_file='/usr/share/dict/words', density=.9)
Grid:
..tsoob.a.
ejectedus.
vlns.in.s.
iaaceclyed
biohloptne
rrmipogrtk
aeppragiir
tsesortpna
owarlikegp
sessilitys
Words:
warlike at (8,1) right
assenting at (0,8) down
vibrato at (2,0) down
stilt at (0,3) downright
chip at (3,3) down
sparked at (9,9) up
boost at (0,6) left
sessility at (9,0) right
serial at (7,1) up
ejected at (1,0) right
goes at (5,6) upleft
rose at (7,5) left
unclip at (1,7) downleft
moan at (5,2) up
clog at (3,3) downright
tripe at (4,7) down
ropy at (6,4) upright
pat at (5,4) downrightContext
StackExchange Code Review Q#92649, answer score: 6
Revisions (0)
No revisions yet.