HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Word Search Generator

Submitted by: @import:stackexchange-codereview··
0
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. 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


  1. 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.

  1. 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.

  1. 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) downright

Context

StackExchange Code Review Q#92649, answer score: 6

Revisions (0)

No revisions yet.