Esempio n. 1
0
class CrosswordPuzzle(object):
    """A crossword puzzle grid that automatically generates puzzles.

    A `m` by `n` grid is populated with words from Wordnik's corpus. Currently
    the resulting crossword puzzle cannot have parallel, adjacent words like you
    would find in the NY TImes crossword.
    
    The grid itself is accessible through the __getitem__ method of the puzzle,
    which provides access to the individual Squares. Clues are taken from 
    Wordnik (definitions, example usages, synonyms, etc.) and are stored in a
    dictionary from clue positions to words and clues:
        self.clues[1, 'DOWN'] => ('cat', 'A small domestic animal')


    In order to create the puzzle you can use the populate_puzzle method, which
    uses Wordnik's Word of the Day as the first word and then adds the specified
    number of words to the puzzle. 
    """

    def __init__(self, rows=15, columns=15, api_key=None):
        """Create a `rows` X `columns` grid and initialize the clues dict.
        
        If `api_key` is not set then the key in config.py is tried.
        """
        self.grid = Grid(rows, columns)
        self.clues = {}
        api_key = api_key or config.WORDNIK_API_KEY
        if not api_key:
            raise WordnikAPIKeyError('Enter your Wordnik API key in config.py')
        self.wordnik = Wordnik(api_key)
        self._current_sq_id = 1  # To keep track of Square IDs

    def __str__(self):
        """Return the grid as a string."""
        return str(self.grid)

    def populate_puzzle(self, word_count):
        """Try to `word_count` words/clues. Return the number of words added."""
        words_added = 0
        if not self.clues:
            self.place_first_word()
            word_count -= 1
            words_added += 1

        for i in range(word_count):
            result = self.find_and_add_a_word()
            if result is None:
                s = 'Grid filled up after adding %d words.' % len(self.clues)
                print >> sys.stderr, s
                break
            else:
                words_added += 1

        self.finalize()
        return words_added

    def place_first_word(self, word=None):
        """Add the Wordnik Word of the Day as the first word in the puzzle.
        
        If no word is passed in, the Wordnik Word of the Day is used. 
        """
        if word is None:
            word = self.wordnik.word_of_the_day()['wordstring']

        #TODO: handle the WOTD being too long
        assert len(word) <= self.grid.num_columns, 'First word is too long.'
        span = [(0, n) for n in range(len(word))]
        self.add_word(word, span)
            
    def find_and_add_a_word(self):
        """Find a word in the Wordnik corpus that fits the puzzle and add it.
        
        If the search and addition are successful, return the wordstring. If
        not, return None.
        """
        open_spans = sorted(self.grid.open_spans(), key=len, reverse=True)
        for span in open_spans:
            query = ''.join([str(self.grid[m, n]) for (m, n) in span])
            query = query.replace(' ', '?')
            length = len(query)
            words = self.wordnik.word_search(query, max_length=length, 
                                             min_dictionary_count=1)
            if words:
                word = max(words, key=lambda w: w['count'])
                self.add_word(word['wordstring'], span)
                return word['wordstring']
        return None

    def store_clue(self, word, id_, direction, clue):
        """Store a word in self.clues. Call after putting word on the grid."""
        self.clues[id_, direction] = (word, clue)


    def add_word(self, word, span):
        """Place the word on the grid then add it and its clue to self.clues."""
        print >> sys.stderr, 'Placing word "%s".' % word
        self.put_word_on_grid(word, span)
        
        m, n = span[0][0], span[0][1]
        first_square = self.grid[m, n]
        if first_square.id_ is None:
            id_ = self._current_sq_id
            self._current_sq_id += 1
            first_square.id_ = id_
        else:
            id_ = first_square.id_
        definitions = self.wordnik.definitions(word)
        definition = random.choice(definitions)['text']
        direction = self.grid.get_span_direction(span)
        
        self.store_clue(word, id_, direction, definition)

    def put_word_on_grid(self, word, span):
        """Add the nth letter in `word` to the nth position in `span`.  """
        assert len(word) == len(span)
        assert len(span) > 1, "Can't insert word shorter than two letters."
        for i, char in enumerate(word):
            (m, n) = span[i]
            if self.grid[m, n].letter is None:
                self.grid[m, n].letter = char
            else:
                assert self.grid[m, n].letter == char

        # Black out open squares on either end of the word if they exist.
        direction = self.grid.get_span_direction(span)
        if direction == 'ACROSS':
            for (m, n) in ((m, n - 1), (m, n + 1)):
                if self.grid.are_valid_coordinates(m, n):
                    self.grid.blackout_square(m, n)

        elif direction == 'DOWN':
            for (m, n) in ((m - 1, n), (m + 1, n)):
                if self.grid.are_valid_coordinates(m, n):
                    self.grid.blackout_square(m, n)
        else:
            assert False, "Sanity check"

    def finalize(self):
        """Perform cleanup after all the words have been placed."""
        self.grid.blackout_all_open_squares()

    #
    # Gameplay related methods
    #
    @property
    def is_completed(self):
        """Return True if the user's entries match the correct letters."""
        for sq in self.grid:
            if sq.letter != sq.user_entry:
                return False
        return True

    def enter_from_user(self, m, n, letter):
        """Set the value of the square at (`m`, `n`) to `letter`."""
        sq = self.grid[m, n]
        assert not sq.is_blacked_out
        sq.letter = letter