Beispiel #1
0
    def _generate_candidates(self):
        """ Generate a fresh candidate list """

        self.candidates = []
        # Start by generating all possible permutations of the
        # rack that form left parts of words, ordering them by length
        if len(self._rack) > 1:
            lpn = LeftPermutationNavigator(self._rack)
            Wordbase.dawg().navigate(lpn)
        else:
            lpn = None

        # Generate moves in one-dimensional space by looking at each axis
        # (row or column) on the board separately

        if self._board.is_empty():
            # Special case for first move: only consider the vertical
            # central axis (any move played there can identically be
            # played horizontally), and with only one anchor in the
            # middle square
            axis = self._axis_from_column(Board.SIZE // 2)
            axis.init_crosschecks()
            # Mark the center anchor
            axis.mark_anchor(Board.SIZE // 2)
            axis.generate_moves(lpn)
        else:
            # Normal move: go through all 15 (row) + 15 (column) axes and generate
            # valid moves within each of them
            for r in range(Board.SIZE):
                axis = self._axis_from_row(r)
                axis.init_crosschecks()
                axis.generate_moves(lpn)
            for c in range(Board.SIZE):
                axis = self._axis_from_column(c)
                axis.init_crosschecks()
                axis.generate_moves(lpn)
        # Delete the reference to LeftPermutationNavigator to save memory
        lpn = None
Beispiel #2
0
    def _gen_moves_from_anchor(self, index, maxleft, lpn):
        """ Find valid moves emanating (on the left and right) from this anchor """

        if maxleft == 0 and index > 0 and not self.is_empty(index - 1):
            # We have a left part already on the board: try to complete it
            leftpart = u''
            ix = index
            while ix > 0 and not self.is_empty(ix - 1):
                leftpart = self._sq[ix - 1]._letter + leftpart
                ix -= 1
            # Use the ExtendRightNavigator to find valid words with this left part
            nav = LeftFindNavigator(leftpart)
            Wordbase.dawg().navigate(nav)
            ns = nav.state()
            if ns is not None:
                # We found a matching prefix in the graph
                matched, prefix, nextnode = ns
                # assert matched == leftpart
                nav = ExtendRightNavigator(self, index, self._rack)
                Navigation(nav).resume(prefix, nextnode, leftpart)
            return

        # We are not completing an existing left part
        # Begin by extending an empty prefix to the right, i.e. placing
        # tiles on the anchor square itself and to its right
        nav = ExtendRightNavigator(self, index, self._rack)
        Wordbase.dawg().navigate(nav)

        if maxleft > 0 and lpn is not None:
            # Follow this by an effort to permute left prefixes into the open space
            # to the left of the anchor square
            for leftlen in range(1, maxleft + 1):
                lplist = lpn.leftparts(leftlen)
                if lplist is not None:
                    for leftpart, rackleave, prefix, nextnode in lplist:
                        nav = ExtendRightNavigator(self, index, rackleave)
                        Navigation(nav).resume(prefix, nextnode, leftpart)
Beispiel #3
0
    def _generate_candidates(self):
        """ Generate a fresh candidate list """

        self._candidates = []
        # Start by generating all possible permutations of the
        # rack that form left parts of words, ordering them by length
        if len(self._rack) > 1:
            lpn = LeftPermutationNavigator(self._rack)
            Wordbase.dawg().navigate(lpn)
        else:
            lpn = None

        # Generate moves in one-dimensional space by looking at each axis
        # (row or column) on the board separately

        if self._board.is_empty():
            # Special case for first move: only consider the vertical
            # central axis (any move played there can identically be
            # played horizontally), and with only one anchor in the
            # middle square
            axis = self._axis_from_column(Board.SIZE // 2)
            axis.init_crosschecks()
            # Mark the center anchor
            axis.mark_anchor(Board.SIZE // 2)
            axis.generate_moves(lpn)
        else:
            # Normal move: go through all 15 (row) + 15 (column) axes and generate
            # valid moves within each of them
            for r in range(Board.SIZE):
                axis = self._axis_from_row(r)
                axis.init_crosschecks()
                axis.generate_moves(lpn)
            for c in range(Board.SIZE):
                axis = self._axis_from_column(c)
                axis.init_crosschecks()
                axis.generate_moves(lpn)
Beispiel #4
0
    def init_crosschecks(self):
        """ Calculate and return a list of cross-check bit patterns for the indicated axis """

        # The cross-check set is the set of letters that can appear in a square
        # and make cross words (above/left and/or below/right of the square) valid
        board = self._autoplayer.board()
        # Prepare to visit all squares on the axis
        x, y = self.coordinate_of(0)
        xd, yd = self.coordinate_step()
        # Fetch the default cross-check bits, which depend on the rack.
        # If the rack contains a wildcard (blank tile), the default cc set
        # contains all letters in the Alphabet. Otherwise, it contains the
        # letters in the rack.
        all_cc = self._autoplayer.rack_bit_pattern()
        # Go through the open squares and calculate their cross-checks
        for ix in range(Board.SIZE):
            cc = all_cc # Start with the default cross-check set
            if not board.is_covered(x, y):
                if self.is_horizontal():
                    above = board.letters_above(x, y)
                    below = board.letters_below(x, y)
                else:
                    above = board.letters_left(x, y)
                    below = board.letters_right(x, y)
                query = u'' if not above else above
                query += u'?'
                if below:
                    query += below
                if len(query) > 1:
                    # Nontrivial cross-check: Query the word database for words that fit this pattern
                    matches = Wordbase.dawg().find_matches(query, sort = False) # Don't need a sorted result
                    bits = 0
                    if matches:
                        cix = 0 if not above else len(above)
                        # Note the set of allowed letters here
                        bits = Alphabet.bit_pattern([wrd[cix] for wrd in matches])
                    # Reduce the cross-check set by intersecting it with the allowed set.
                    # If the cross-check set and the rack have nothing in common, this
                    # will lead to the square being marked as closed, which saves
                    # calculation later on
                    cc &= bits
            # Initialize the square
            self._sq[ix].init(self._autoplayer, x, y, cc)
            # Keep track of empty squares within the axis in a bit pattern for speed
            if self._sq[ix].is_empty():
                self._empty_bits |= (1 << ix)
            x += xd
            y += yd
Beispiel #5
0
def lookup_word(db, w, at_sentence_start):
    """ Lookup simple or compound word in database and return its meanings """
    # Start with a simple lookup
    m = db.meanings(w)
    if at_sentence_start or not m:
        # No meanings found in database, or at sentence start
        # Try a lowercase version of the word, if different
        lower_w = w.lower()
        if lower_w != w:
            # Do another lookup, this time for lowercase only
            if not m:
                m = db.meanings(lower_w)
            else:
                m.extend(db.meanings(lower_w))

        if not m and (lower_w != w or w[0] == '['):
            # Still nothing: check abbreviations
            m = lookup_abbreviation(w)
            if not m and w[0] == '[':
                # Could be an abbreviation with periods at the start of a sentence:
                # Lookup a lowercase version
                m = lookup_abbreviation(lower_w)
            if m and w[0] == '[':
                # Remove brackets from known abbreviations
                w = w[1:-1]

        if not m:
            # Still nothing: check compound words
            cw = Wordbase.dawg().slice_compound_word(lower_w)
            if cw:
                # This looks like a compound word:
                # use the meaning of its last part
                prefix = "-".join(cw[0:-1])
                m = db.meanings(cw[-1])
                m = [ (prefix + "-" + stem, ix, wtype, wcat, prefix + "-" + wform, gform)
                    for stem, ix, wtype, wcat, wform, gform in m]
    return (w, m)
Beispiel #6
0
    def _lookup(w, at_sentence_start, lookup):
        """ Lookup a simple or compound word in the database and return its meaning(s) """

        def lookup_abbreviation(w):
            """ Lookup abbreviation from abbreviation list """
            # Remove brackets, if any, before lookup
            clean_w = w[1:-1] if w[0] == '[' else w
            # Return a single-entity list with one meaning
            m = Abbreviations.DICT.get(clean_w, None)
            return None if m is None else [ BIN_Meaning._make(m) ]

        # Start with a simple lookup
        m = lookup(w)

        if at_sentence_start or not m:
            # No meanings found in database, or at sentence start
            # Try a lowercase version of the word, if different
            lower_w = w.lower()
            if lower_w != w:
                # Do another lookup, this time for lowercase only
                if not m:
                    m = lookup(lower_w)
                else:
                    m.extend(lookup(lower_w))

            if not m and (lower_w != w or w[0] == '['):
                # Still nothing: check abbreviations
                m = lookup_abbreviation(w)
                if not m and w[0] == '[':
                    # Could be an abbreviation with periods at the start of a sentence:
                    # Lookup a lowercase version
                    m = lookup_abbreviation(lower_w)
                if m and w[0] == '[':
                    # Remove brackets from known abbreviations
                    w = w[1:-1]

            if not m and BIN_Db._ADJECTIVE_TEST in lower_w:
                # Not found: Check whether this might be an adjective
                # ending in 'legur'/'leg'/'legt'/'legir'/'legar' etc.
                for aend, beyging in AdjectiveTemplate.ENDINGS:
                    if lower_w.endswith(aend) and len(lower_w) > len(aend):
                        prefix = lower_w[0 : len(lower_w) - len(aend)]
                        # Construct an adjective descriptor
                        if m is None:
                            m = []
                        m.append(BIN_Meaning(prefix + "legur", 0, "lo", "alm", lower_w, beyging))

            if not m:
                # Still nothing: check compound words
                cw = Wordbase.dawg().slice_compound_word(w)
                if not cw and lower_w != w:
                    # If not able to slice in original case, try lower case
                    cw = Wordbase.dawg().slice_compound_word(lower_w)
                if cw:
                    # This looks like a compound word:
                    # use the meaning of its last part
                    prefix = "-".join(cw[0:-1])
                    m = lookup(cw[-1])
                    m = [ BIN_Meaning(prefix + "-" + r.stofn, r.utg, r.ordfl, r.fl,
                            prefix + "-" + r.ordmynd, r.beyging)
                            for r in m]

            if not m and lower_w.startswith('ó'):
                # Check whether an adjective without the 'ó' prefix is found in BÍN
                # (i.e. create 'óhefðbundinn' from 'hefðbundinn')
                suffix = lower_w[1:]
                if suffix:
                    om = lookup(suffix)
                    if om:
                        m = [ BIN_Meaning("ó" + r.stofn, r.utg, r.ordfl, r.fl,
                                "ó" + r.ordmynd, r.beyging)
                                for r in om if r.ordfl == "lo" ]

        # noinspection PyRedundantParentheses
        return (w, m)
Beispiel #7
0
 def _load(self):
     """ Load word lists into memory from static preprocessed text files """
     if self._dawg is not None:
         # Already loaded, nothing to do
         return
     self._dawg = Wordbase.dawg()
Beispiel #8
0
    def _lookup(w, at_sentence_start, auto_uppercase, lookup):
        """ Lookup a simple or compound word in the database and return its meaning(s) """
        def lookup_abbreviation(w):
            """ Lookup abbreviation from abbreviation list """
            # Remove brackets, if any, before lookup
            if w[0] == '[':
                clean_w = w[1:-1]
                # Check for abbreviation that also ended a sentence and
                # therefore had its end period cut off
                if not clean_w.endswith('.'):
                    clean_w += '.'
            else:
                clean_w = w
            # Return a single-entity list with one meaning
            m = Abbreviations.DICT.get(clean_w, None)
            return None if m is None else [BIN_Meaning._make(m)]

        # Start with a straightforward lookup of the word

        if auto_uppercase and w.islower():
            if len(w) == 1:
                # Special case for single letter words:
                # if they exist in BÍN, don't convert them
                m = lookup(w)
                if not m:
                    # If they don't exist in BÍN, treat them as uppercase
                    # abbreviations (probably middle names)
                    w = w.upper() + '.'
            else:
                # Check whether this word has an uppercase form in the database
                w_upper = w.capitalize()
                m = lookup(w_upper)
                if m:
                    # Yes: assume it should be uppercase
                    w = w_upper
                    at_sentence_start = False  # No need for special case here
                else:
                    # No: go for the regular lookup
                    m = lookup(w)
        else:
            m = lookup(w)

        if at_sentence_start or not m:
            # No meanings found in database, or at sentence start
            # Try a lowercase version of the word, if different
            lower_w = w.lower()
            if lower_w != w:
                # Do another lookup, this time for lowercase only
                if not m:
                    # This is a word that contains uppercase letters
                    # and was not found in BÍN in its original form
                    # Try an abbreviation before doing a lowercase lookup
                    # (since some abbreviations are also words, i.e. LÍN)
                    m = lookup_abbreviation(w)
                    if not m:
                        m = lookup(lower_w)
                    elif w[0] == '[':
                        # Remove brackets from known abbreviations
                        w = w[1:-1]
                else:
                    m.extend(lookup(lower_w))

        if m:
            # Most common path out of this function
            return (w, m)

        if (lower_w != w or w[0] == '['):
            # Still nothing: check abbreviations
            m = lookup_abbreviation(w)
            if not m and w[0] == '[':
                # Could be an abbreviation with periods at the start of a sentence:
                # Lookup a lowercase version
                m = lookup_abbreviation(lower_w)
            if m and w[0] == '[':
                # Remove brackets from known abbreviations
                w = w[1:-1]

        if not m and BIN_Db._ADJECTIVE_TEST in lower_w:
            # Not found: Check whether this might be an adjective
            # ending in 'legur'/'leg'/'legt'/'legir'/'legar' etc.
            llw = len(lower_w)
            for aend, beyging in AdjectiveTemplate.ENDINGS:
                if lower_w.endswith(aend) and llw > len(aend):
                    prefix = lower_w[0:llw - len(aend)]
                    # Construct an adjective descriptor
                    if m is None:
                        m = []
                    m.append(
                        BIN_Meaning(prefix + "legur", 0, "lo", "alm", lower_w,
                                    beyging))
            if lower_w.endswith("lega") and llw > 4:
                # For words ending with "lega", add a possible adverb meaning
                if m is None:
                    m = []
                m.append(BIN_Meaning(lower_w, 0, "ao", "ob", lower_w, "-"))

        if not m:
            # Still nothing: check compound words
            cw = Wordbase.dawg().slice_compound_word(w)
            if not cw and lower_w != w:
                # If not able to slice in original case, try lower case
                cw = Wordbase.dawg().slice_compound_word(lower_w)
            if cw:
                # This looks like a compound word:
                # use the meaning of its last part
                prefix = "-".join(cw[0:-1])
                m = lookup(cw[-1])
                if lower_w != w and not at_sentence_start:
                    # If this is an uppercase word in the middle of a
                    # sentence, allow only nouns as possible interpretations
                    # (it wouldn't be correct to capitalize verbs, adjectives, etc.)
                    m = [mm for mm in m if mm.ordfl in {"kk", "kvk", "hk"}]
                m = BIN_Db.prefix_meanings(m, prefix)

        if not m and lower_w.startswith('ó'):
            # Check whether an adjective without the 'ó' prefix is found in BÍN
            # (i.e. create 'óhefðbundinn' from 'hefðbundinn')
            suffix = lower_w[1:]
            if suffix:
                om = lookup(suffix)
                if om:
                    m = [
                        BIN_Meaning("ó" + r.stofn, r.utg, r.ordfl, r.fl,
                                    "ó" + r.ordmynd, r.beyging) for r in om
                        if r.ordfl == "lo"
                    ]

        if not m and auto_uppercase and w.islower():
            # If no meaning found and we're auto-uppercasing,
            # convert this to upper case (could very well be a name
            # of a person or entity)
            w = w.capitalize()

        # noinspection PyRedundantParentheses
        return (w, m)
Beispiel #9
0
    def check_legality(self, state):
        """ Check whether this move is legal on the board """

        # Must cover at least one square
        if len(self._covers) < 1:
            return Error.NULL_MOVE
        if len(self._covers) > Rack.MAX_TILES:
            return Error.TOO_MANY_TILES_PLAYED
        if state.is_game_over():
            return Error.GAME_OVER

        rack = state.player_rack()
        board = state.board()
        row = 0
        col = 0
        horiz = True
        vert = True
        first = True
        # All tiles played must be in the rack
        played = u''.join([c.tile for c in self._covers])
        if not rack.contains(played):
            return Error.TILE_NOT_IN_RACK
        # The tiles covered by the move must be purely horizontal or purely vertical
        for c in self._covers:
            if first:
                row = c.row
                col = c.col
                first = False
            else:
                if c.row != row:
                    horiz = False
                if c.col != col:
                    vert = False
        if (not horiz) and (not vert):
            # Spread all over: not legal
            return Error.DISJOINT
        # If only one cover, use the orientation of the longest word formed
        if len(self._covers) == 1:
            # In the case of a tied length, we use horizontal
            self._horizontal = len(board.letters_left(row, col)) + len(board.letters_right(row, col)) >= \
                len(board.letters_above(row, col)) + len(board.letters_below(row, col))
            horiz = self._horizontal
            vert = not horiz
        # The move is purely horizontal or vertical
        if horiz:
            self._covers.sort(key = lambda x: x.col) # Sort in ascending column order
            self._horizontal = True
        else:
            self._covers.sort(key = lambda x: x.row) # Sort in ascending row order
            self._horizontal = False
        # Check whether eventual missing squares in the move sequence are already covered
        row = 0
        col = 0
        first = True
        for c in self._covers:
            if board.is_covered(c.row, c.col):
                # We already have a tile in the square: illegal play
                return Error.SQUARE_ALREADY_OCCUPIED
            # If there is a gap between this cover and the last one,
            # make sure all intermediate squares are covered
            if first:
                self._row = c.row
                self._col = c.col
                first = False
            else:
                if horiz:
                    # Horizontal: check squares within row
                    for ix in range(col + 1, c.col):
                        if not board.is_covered(c.row, ix):
                            # Found gap: illegal play
                            return Error.HAS_GAP
                else:
                    # assert vert
                    # Vertical: check squares within column
                    for ix in range(row + 1, c.row):
                        if not board.is_covered(ix, c.col):
                            # Found gap: illegal play
                            return Error.HAS_GAP
            row = c.row
            col = c.col
        # Find the start and end of the word that is being formed, including
        # tiles aready on the board
        if horiz:
            # Look for the beginning
            while self._col > 0 and board.is_covered(self._row, self._col - 1):
                self._col -= 1
            # Look for the end
            while col + 1 < Board.SIZE and board.is_covered(self._row, col + 1):
                col += 1
            # Now we know the length
            self._numletters = col - self._col + 1
        else:
            # Look for the beginning
            while self._row > 0 and board.is_covered(self._row - 1, self._col):
                self._row -= 1
            # Look for the end
            while row + 1 < Board.SIZE and board.is_covered(row + 1, self._col):
                row += 1
            # Now we know the length
            self._numletters = row - self._row + 1

        # Assemble the resulting word
        self._word = u''
        self._tiles = u''
        cix = 0

        for ix in range(self._numletters):

            def add(cix):
                ltr = self._covers[cix].letter
                tile = self._covers[cix].tile
                self._word += ltr
                self._tiles += tile + (ltr if tile == u'?' else u'')

            if horiz:
                if cix < len(self._covers) and self._col + ix == self._covers[cix].col:
                    # This is one of the new letters
                    add(cix)
                    cix += 1
                else:
                    # This is a letter that was already on the board
                    ltr = board.letter_at(self._row, self._col + ix)
                    self._word += ltr
                    self._tiles += ltr
            else:
                if cix < len(self._covers) and self._row + ix == self._covers[cix].row:
                    # This is one of the new letters
                    add(cix)
                    cix += 1
                else:
                    # This is a letter that was already on the board
                    ltr = board.letter_at(self._row + ix, self._col)
                    self._word += ltr
                    self._tiles += ltr

        # Check whether the word is in the dictionary
        if self._word not in Wordbase.dawg():
            # print(u"Word '{0}' not found in dictionary".format(self._word))
            return (Error.WORD_NOT_IN_DICTIONARY, self._word)
        # Check that the play is adjacent to some previously placed tile
        # (unless this is the first move, i.e. the board is empty)
        if board.is_empty():
            # Must go through the center square
            center = False
            for c in self._covers:
                if c.row == Board.SIZE // 2 and c.col == Board.SIZE // 2:
                    center = True
                    break
            if not center:
                return Error.FIRST_MOVE_NOT_IN_CENTER
        else:
            # Must be adjacent to something already on the board
            if not any([board.has_adjacent(c.row, c.col) for c in self._covers]):
                return Error.NOT_ADJACENT
            # Check all cross words formed by the new tiles
            for c in self._covers:
                if self._horizontal:
                    cross = board.letters_above(c.row, c.col) + c.letter + board.letters_below(c.row, c.col)
                else:
                    cross = board.letters_left(c.row, c.col) + c.letter + board.letters_right(c.row, c.col)
                if len(cross) > 1 and cross not in Wordbase.dawg():
                    return (Error.CROSS_WORD_NOT_IN_DICTIONARY, cross)
        # All checks pass: the play is legal
        return Error.LEGAL
Beispiel #10
0
class Axis:
    """ Represents a one-dimensional axis on the board, either
        horizontal or vertical. This is used to find legal moves
        for an AutoPlayer.
    """

    DAWG = Wordbase.dawg()

    def __init__(self, autoplayer, index, horizontal):

        self._autoplayer = autoplayer
        self._sq = [None] * Board.SIZE
        for i in range(Board.SIZE):
            self._sq[i] = Square()
        self._index = index
        self._horizontal = horizontal
        self._rack = autoplayer.rack()
        # Bit pattern representing empty squares on this axis
        self._empty_bits = 0

    def is_horizontal(self):
        """ Is this a horizontal (row) axis? """
        return self._horizontal

    def is_vertical(self):
        """ Is this a vertical (column) axis? """
        return not self._horizontal

    def coordinate_of(self, index):
        """ Return the co-ordinate on the board of a square within this axis """
        return (self._index, index) if self._horizontal else (index,
                                                              self._index)

    def coordinate_step(self):
        """ How to move along this axis on the board, (row,col) """
        return (0, 1) if self._horizontal else (1, 0)

    def letter_at(self, index):
        """ Return the letter at the index """
        return self._sq[index].letter()

    def is_open(self, index):
        """ Is the square at the index open (i.e. can a tile be placed there?) """
        return self._sq[index].is_open()

    def is_open_for(self, index, letter):
        """ Is the square at the index open for this letter? """
        return self._sq[index].is_open_for(letter)

    def is_empty(self, index):
        """ Is the square at the index empty? """
        return bool(self._empty_bits & (1 << index))

    def mark_anchor(self, index):
        """ Force the indicated square to be an anchor. Used in first move
            to mark the center square. """
        self._sq[index].mark_anchor()

    def init_crosschecks(self):
        """ Calculate and return a list of cross-check bit patterns for the indicated axis """

        # The cross-check set is the set of letters that can appear in a square
        # and make cross words (above/left and/or below/right of the square) valid
        board = self._autoplayer.board()
        # Prepare to visit all squares on the axis
        x, y = self.coordinate_of(0)
        xd, yd = self.coordinate_step()
        # Fetch the default cross-check bits, which depend on the rack.
        # If the rack contains a wildcard (blank tile), the default cc set
        # contains all letters in the Alphabet. Otherwise, it contains the
        # letters in the rack.
        all_cc = self._autoplayer.rack_bit_pattern()
        # Go through the open squares and calculate their cross-checks
        for ix in range(Board.SIZE):
            cc = all_cc  # Start with the default cross-check set
            if not board.is_covered(x, y):
                if self.is_horizontal():
                    above = board.letters_above(x, y)
                    below = board.letters_below(x, y)
                else:
                    above = board.letters_left(x, y)
                    below = board.letters_right(x, y)
                query = above or u''
                query += u'?'
                if below:
                    query += below
                if len(query) > 1:
                    # Nontrivial cross-check: Query the word database for words that fit this pattern
                    matches = self.DAWG.find_matches(
                        query, sort=False)  # Don't need a sorted result
                    bits = 0
                    if matches:
                        cix = len(above) if above else 0
                        # Note the set of allowed letters here
                        bits = Alphabet.bit_pattern(
                            [wrd[cix] for wrd in matches])
                    # Reduce the cross-check set by intersecting it with the allowed set.
                    # If the cross-check set and the rack have nothing in common, this
                    # will lead to the square being marked as closed, which saves
                    # calculation later on
                    cc &= bits
            # Initialize the square
            self._sq[ix].init(self._autoplayer, x, y, cc)
            # Keep track of empty squares within the axis in a bit pattern for speed
            if self._sq[ix].is_empty():
                self._empty_bits |= (1 << ix)
            x += xd
            y += yd

    def _gen_moves_from_anchor(self, index, maxleft, lpn):
        """ Find valid moves emanating (on the left and right) from this anchor """
        if maxleft == 0 and index > 0 and not self.is_empty(index - 1):
            # We have a left part already on the board: try to complete it
            leftpart = u''
            ix = index
            while ix > 0 and not self.is_empty(ix - 1):
                leftpart = self._sq[ix - 1]._letter + leftpart
                ix -= 1
            # Use the ExtendRightNavigator to find valid words with this left part
            nav = LeftFindNavigator(leftpart)
            self.DAWG.navigate(nav)
            ns = nav.state()
            if ns is not None:
                # We found a matching prefix in the graph
                _, prefix, nextnode = ns
                # assert matched == leftpart
                nav = ExtendRightNavigator(self, index, self._rack)
                self.DAWG.resume_navigation(nav, prefix, nextnode, leftpart)
            return

        # We are not completing an existing left part
        # Begin by extending an empty prefix to the right, i.e. placing
        # tiles on the anchor square itself and to its right
        nav = ExtendRightNavigator(self, index, self._rack)
        self.DAWG.navigate(nav)

        if maxleft > 0 and lpn is not None:
            # Follow this by an effort to permute left prefixes into the open space
            # to the left of the anchor square
            for leftlen in range(1, maxleft + 1):
                lplist = lpn.leftparts(leftlen)
                if lplist is not None:
                    for leftpart, rackleave, prefix, nextnode in lplist:
                        nav = ExtendRightNavigator(self, index, rackleave)
                        self.DAWG.resume_navigation(nav, prefix, nextnode,
                                                    leftpart)

    def generate_moves(self, lpn):
        """ Find all valid moves on this axis by attempting to place tiles
            at and around all anchor squares """
        last_anchor = -1
        lenrack = len(self._rack)
        for i in range(Board.SIZE):
            if self._sq[i].is_anchor():
                # Count the consecutive open, non-anchor squares on the left of the anchor
                opensq = 0
                left = i
                while left > 0 and left > (last_anchor +
                                           1) and self._sq[left - 1].is_open():
                    opensq += 1
                    left -= 1
                # We have a maximum left part length of min(opensq, lenrack-1) as the anchor
                # square itself must always be filled from the rack
                self._gen_moves_from_anchor(i, min(opensq, lenrack - 1), lpn)
                last_anchor = i
    def check_legality(self, state):
        """ Check whether this move is legal on the board """

        # Must cover at least one square
        if len(self._covers) < 1:
            return Error.NULL_MOVE
        if len(self._covers) > Rack.MAX_TILES:
            return Error.TOO_MANY_TILES_PLAYED
        if state.is_game_over():
            return Error.GAME_OVER

        rack = state.player_rack()
        board = state.board()
        row = 0
        col = 0
        horiz = True
        vert = True
        first = True
        # All tiles played must be in the rack
        played = u''.join([c.tile for c in self._covers])
        if not rack.contains(played):
            return Error.TILE_NOT_IN_RACK
        # The tiles covered by the move must be purely horizontal or purely vertical
        for c in self._covers:
            if first:
                row = c.row
                col = c.col
                first = False
            else:
                if c.row != row:
                    horiz = False
                if c.col != col:
                    vert = False
        if (not horiz) and (not vert):
            # Spread all over: not legal
            return Error.DISJOINT
        # If only one cover, use the orientation of the longest word formed
        if len(self._covers) == 1:
            # In the case of a tied length, we use horizontal
            self._horizontal = len(board.letters_left(row, col)) + len(board.letters_right(row, col)) >= \
                len(board.letters_above(row, col)) + len(board.letters_below(row, col))
            horiz = self._horizontal
        # The move is purely horizontal or vertical
        if horiz:
            self._covers.sort(
                key=lambda x: x.col)  # Sort in ascending column order
            self._horizontal = True
        else:
            self._covers.sort(
                key=lambda x: x.row)  # Sort in ascending row order
            self._horizontal = False
        # Check whether eventual missing squares in the move sequence are already covered
        row = 0
        col = 0
        first = True
        for c in self._covers:
            if board.is_covered(c.row, c.col):
                # We already have a tile in the square: illegal play
                return Error.SQUARE_ALREADY_OCCUPIED
            # If there is a gap between this cover and the last one,
            # make sure all intermediate squares are covered
            if first:
                self._row = c.row
                self._col = c.col
                first = False
            else:
                if horiz:
                    # Horizontal: check squares within row
                    for ix in range(col + 1, c.col):
                        if not board.is_covered(c.row, ix):
                            # Found gap: illegal play
                            return Error.HAS_GAP
                else:
                    # Vertical: check squares within column
                    for ix in range(row + 1, c.row):
                        if not board.is_covered(ix, c.col):
                            # Found gap: illegal play
                            return Error.HAS_GAP
            row = c.row
            col = c.col
        # Find the start and end of the word that is being formed, including
        # tiles aready on the board
        if horiz:
            # Look for the beginning
            while self._col > 0 and board.is_covered(self._row, self._col - 1):
                self._col -= 1
            # Look for the end
            while col + 1 < Board.SIZE and board.is_covered(
                    self._row, col + 1):
                col += 1
            # Now we know the length
            self._numletters = col - self._col + 1
        else:
            # Look for the beginning
            while self._row > 0 and board.is_covered(self._row - 1, self._col):
                self._row -= 1
            # Look for the end
            while row + 1 < Board.SIZE and board.is_covered(
                    row + 1, self._col):
                row += 1
            # Now we know the length
            self._numletters = row - self._row + 1

        # Assemble the resulting word
        self._word = u''
        self._tiles = u''
        cix = 0

        for ix in range(self._numletters):

            def add(cix):
                ltr = self._covers[cix].letter
                tile = self._covers[cix].tile
                self._word += ltr
                self._tiles += tile + (ltr if tile == u'?' else u'')

            if horiz:
                if cix < len(self._covers
                             ) and self._col + ix == self._covers[cix].col:
                    # This is one of the new letters
                    add(cix)
                    cix += 1
                else:
                    # This is a letter that was already on the board
                    ltr = board.letter_at(self._row, self._col + ix)
                    self._word += ltr
                    self._tiles += ltr
            else:
                if cix < len(self._covers
                             ) and self._row + ix == self._covers[cix].row:
                    # This is one of the new letters
                    add(cix)
                    cix += 1
                else:
                    # This is a letter that was already on the board
                    ltr = board.letter_at(self._row + ix, self._col)
                    self._word += ltr
                    self._tiles += ltr

        # Check whether the word is in the dictionary
        if self._word not in Wordbase.dawg():
            # print(u"Word '{0}' not found in dictionary".format(self._word))
            return (Error.WORD_NOT_IN_DICTIONARY, self._word)
        # Check that the play is adjacent to some previously placed tile
        # (unless this is the first move, i.e. the board is empty)
        if board.is_empty():
            # Must go through the center square
            center = False
            for c in self._covers:
                if c.row == Board.SIZE // 2 and c.col == Board.SIZE // 2:
                    center = True
                    break
            if not center:
                return Error.FIRST_MOVE_NOT_IN_CENTER
        else:
            # Must be adjacent to something already on the board
            if not any(
                [board.has_adjacent(c.row, c.col) for c in self._covers]):
                return Error.NOT_ADJACENT
            # Check all cross words formed by the new tiles
            for c in self._covers:
                if self._horizontal:
                    cross = board.letters_above(
                        c.row, c.col) + c.letter + board.letters_below(
                            c.row, c.col)
                else:
                    cross = board.letters_left(
                        c.row, c.col) + c.letter + board.letters_right(
                            c.row, c.col)
                if len(cross) > 1 and cross not in Wordbase.dawg():
                    return (Error.CROSS_WORD_NOT_IN_DICTIONARY, cross)
        # All checks pass: the play is legal
        return Error.LEGAL