def is_valid(self, move: Move): """ Validate a move. :param move: The move to be validated. :return: boolean True if move is valid, otherwise a MoveValidationError is raised giving the reason for the move being invalid. """ if move.is_valid is not None: return move.is_valid move.is_valid = False if move.direction == Direction.NOT_APPLICABLE: # we must be passing or swapping letters move.is_valid = True return True # Player must have passed or exchanged num_tiles = len(move.tiles) start_index = move.start_index # check we are playing a sensible number of tiles: if num_tiles < 1 or num_tiles > RACK_SIZE: raise MoveValidationError('move has invalid number of tiles') # get row or column as appropriate (columns come back transposed to a row): row = move.row # check we're not on a sentinel square if start_index < 1 or start_index >= BOARD_SIZE: raise MoveValidationError("starting square is off the board") if not row.square_is_empty(start_index): raise MoveValidationError("starting square is not free") if len(move.played_squares) < num_tiles: raise MoveValidationError("not enough free spaces") if not self.has_valid_hook(row, move.played_squares): raise MoveValidationError( "word must join an existing word or start square") row.place_tiles(move.played_squares, move.tiles) if not self.crosswords_valid(row): row.remove_tiles(move.played_squares) raise MoveValidationError("invalid cross-word formed") word_formed = row.word_at(start_index) if word_formed not in self.lexicon: row.remove_tiles(move.played_squares) raise MoveValidationError("'" + word_formed + "'is not a valid word") move.calculate_score() move.is_valid = True return True
def play_on_square(self, row, index, played_tiles, rack: Rack): valid_moves = [] # get all the letters we could play on this square without making nonsense in the corresponding column: valid_cross_plays = [chr(64 + x) for x in range(1, 27) if read_bit(row.this_row_crosschecks[index], x)] # filter that to ones we can actually use with tiles from our rack (all of them if we have a blank!) valid_tiles = valid_cross_plays if '@' in rack else [x for x in valid_cross_plays if x in rack] # for each of the playable tiles, stick it in the square for tile_letter in valid_tiles: tile = rack.get_tile(tile_letter) played_tiles[index] = tile row.existing_letters[index] = ord(tile)-64 if row.word_at(index) in self.game.lexicon: new_move = Move(row, np.where(played_tiles)[0][0], [tile for tile in played_tiles if tile]) new_move.played_squares = np.where(played_tiles)[0] new_move.calculate_score() #DEBUG: #print(self.name+": Considering move: "+str(new_move)) valid_moves.append(new_move) if len(rack) > 0: # if we still have tiles left # try extending into the next square on the left, only if we've made a middle part of a real word: if self.game.lexicon.contains_infix(row.word_at(index)): valid_moves.extend(self.extend_left(index, played_tiles, row, rack)) # and if we've made the start of a word yet, try extending that to the right if self.game.lexicon.contains_prefix(row.word_at(index)): valid_moves.extend(self.extend_right(index, played_tiles, row, rack)) # return the tile to the rack rack.add_tile(tile) # set board square back to no letter row.existing_letters[index] = 0 # remove tile from list of played tiles: played_tiles[index] = None return valid_moves