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
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)
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)
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
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)
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)
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()
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)
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
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