Ejemplo n.º 1
0
def admin_loadgame():
    """ Fetch a game object and return it as JSON """

    uuid = request.form.get("uuid", None)
    game = None

    if uuid:
        # Attempt to load the game whose id is in the URL query string
        game = Game.load(uuid)

    if game:
        board = game.state.board()
        g = dict(
            uuid = game.uuid,
            timestamp = Alphabet.format_timestamp(game.timestamp),
            player0 = game.player_ids[0],
            player1 = game.player_ids[1],
            robot_level = game.robot_level,
            ts_last_move = Alphabet.format_timestamp(game.ts_last_move),
            irack0 = game.initial_racks[0],
            irack1 = game.initial_racks[1],
            prefs = game._preferences,
            over = game.is_over(),
            moves = [ (m.player, m.move.summary(board),
                m.rack, Alphabet.format_timestamp(m.ts)) for m in game.moves ]
        )
    else:
        g = None

    return jsonify(game = g)
Ejemplo n.º 2
0
def admin_loadgame():
    """ Fetch a game object and return it as JSON """

    uuid = request.form.get("uuid", None)
    game = None

    if uuid:
        # Attempt to load the game whose id is in the URL query string
        game = Game.load(uuid)

    if game:
        board = game.state.board()
        g = dict(uuid=game.uuid,
                 timestamp=Alphabet.format_timestamp(game.timestamp),
                 player0=game.player_ids[0],
                 player1=game.player_ids[1],
                 robot_level=game.robot_level,
                 ts_last_move=Alphabet.format_timestamp(game.ts_last_move),
                 irack0=game.initial_racks[0],
                 irack1=game.initial_racks[1],
                 prefs=game._preferences,
                 over=game.is_over(),
                 moves=[(m.player, m.move.summary(board), m.rack,
                         Alphabet.format_timestamp(m.ts)) for m in game.moves])
    else:
        g = None

    return jsonify(game=g)
Ejemplo n.º 3
0
 def finalize_score(self):
     """ When game is completed, update scores with the tiles left """
     if self._game_resigned:
         # In case of a resignation, the resigning player has already lost all points
         return
     for ix in range(2):
         # Add the score of the opponent's tiles
         self._scores[ix] += Alphabet.score(self._racks[1 - ix].contents())
         # Subtract the score of the player's own tiles
         self._scores[ix] -= Alphabet.score(self._racks[ix].contents())
         if self._scores[ix] < 0:
             self._scores[ix] = 0
Ejemplo n.º 4
0
    def __init__(self, state):

        # List of valid, candidate moves
        self._candidates = []
        self._state = state
        self._board = state.board()
        # The rack that the autoplayer has to work with
        self._rack = state.player_rack().contents()

        # Calculate a bit pattern representation of the rack
        if u'?' in self._rack:
            # Wildcard in rack: all letters allowed
            self._rack_bit_pattern = Alphabet.all_bits_set()
        else:
            # No wildcard: limits the possibilities of covering squares
            self._rack_bit_pattern = Alphabet.bit_pattern(self._rack)
Ejemplo n.º 5
0
def admin_fetchgames():
    """ Return a JSON representation of all finished games """
    # noinspection PyPep8
    q = GameModel.query(GameModel.over == True).order(GameModel.ts_last_move)
    gamelist = []
    for gm in q.fetch():
        gamelist.append(
            dict(id=gm.key.id(),
                 ts=Alphabet.format_timestamp(gm.timestamp),
                 lm=Alphabet.format_timestamp(gm.ts_last_move or gm.timestamp),
                 p0=None if gm.player0 is None else gm.player0.id(),
                 p1=None if gm.player1 is None else gm.player1.id(),
                 rl=gm.robot_level,
                 s0=gm.score0,
                 s1=gm.score1,
                 pr=gm.prefs))
    return jsonify(gamelist=gamelist)
Ejemplo n.º 6
0
    def __init__(self, copy = None):

        if copy is None:
            # Get a full bag from the Alphabet; this varies between languages
            self._tiles = Alphabet.full_bag()
        else:
            # Copy constructor: initialize from another Bag
            self._tiles = copy._tiles
Ejemplo n.º 7
0
 def read_word(self):
     if self._index >= self._len:
         self._eof = True
         return False
     self._nxt = self._list[self._index]
     self._key = Alphabet.sortkey(self._nxt)
     self._index += 1
     return True
Ejemplo n.º 8
0
    def __init__(self, state, robot_level=0):

        # List of valid, candidate moves
        self._candidates = []
        self._state = state
        self._board = state.board()
        # The rack that the autoplayer has to work with
        self._rack = state.player_rack().contents()
        self._robot_level = robot_level

        # Calculate a bit pattern representation of the rack
        if u'?' in self._rack:
            # Wildcard in rack: all letters allowed
            self._rack_bit_pattern = Alphabet.all_bits_set()
        else:
            # No wildcard: limits the possibilities of covering squares
            self._rack_bit_pattern = Alphabet.bit_pattern(self._rack)
Ejemplo n.º 9
0
def admin_fetchgames():
    """ Return a JSON representation of all finished games """
    q = GameModel.query(GameModel.over == True).order(GameModel.ts_last_move)
    gamelist = []
    for gm in q.fetch():
        gamelist.append(dict(
            id = gm.key.id(),
            ts = Alphabet.format_timestamp(gm.timestamp),
            lm = Alphabet.format_timestamp(gm.ts_last_move or gm.timestamp),
            p0 = None if gm.player0 is None else gm.player0.id(),
            p1 = None if gm.player1 is None else gm.player1.id(),
            rl = gm.robot_level,
            s0 = gm.score0,
            s1 = gm.score1,
            pr = gm.prefs
        ))
    return jsonify(gamelist = gamelist)
Ejemplo n.º 10
0
    def _append_final_adjustments(self, movelist):
        """ Appends final score adjustment transactions to the given movelist """

        # Lastplayer is the player who finished the game
        lastplayer = self.moves[-1].player if self.moves else 0

        if not self.state.is_resigned():

            # If the game did not end by resignation, check for a timeout
            overtime = self.overtime()
            adjustment = list(self.overtime_adjustment())
            sc = self.state.scores()

            if any(overtime[ix] >= Game.MAX_OVERTIME for ix in range(2)): # 10 minutes overtime
                # Game ended with a loss on overtime
                ix = 0 if overtime[0] >= Game.MAX_OVERTIME else 1
                adjustment[1 - ix] = 0
                # Adjust score of losing player down by 100 points
                adjustment[ix] = - min(100, sc[ix])
                # If losing player is still winning on points, add points to the
                # winning player so that she leads by one point
                if sc[ix] + adjustment[ix] >= sc[1 - ix]:
                    adjustment[1 - ix] = sc[ix] + adjustment[ix] + 1 - sc[1 - ix]
            else:
                # Normal end of game
                opp_rack = self.state.rack(1 - lastplayer)
                opp_score = Alphabet.score(opp_rack)
                last_rack = self.state.rack(lastplayer)
                last_score = Alphabet.score(last_rack)
                if not last_rack:
                    # Won with an empty rack: Add double the score of the losing rack
                    movelist.append((1 - lastplayer, (u"", u"--", 0)))
                    movelist.append((lastplayer, (u"", u"2 * " + opp_rack, 2 * opp_score)))
                else:
                    # The game has ended by passes: each player gets her own rack subtracted
                    movelist.append((1 - lastplayer, (u"", opp_rack, -1 * opp_score)))
                    movelist.append((lastplayer, (u"", last_rack, -1 * last_score)))

            # If this is a timed game, add eventual overtime adjustment
            if tuple(adjustment) != (0, 0):
                movelist.append((1 - lastplayer, (u"", u"TIME", adjustment[1 - lastplayer])))
                movelist.append((lastplayer, (u"", u"TIME", adjustment[lastplayer])))

        # Add a synthetic "game over" move
        movelist.append((1 - lastplayer, (u"", u"OVER", 0)))
Ejemplo n.º 11
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 = 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
                    # Don't need a sorted result
                    matches = self.DAWG.find_matches(query, sort=False)
                    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
Ejemplo n.º 12
0
 def read_word(self):
     """ Read lines until we have a legal word or EOF """
     while True:
         try:
             line = next(self._fin).strip()
         except StopIteration:
             # We're done with this file
             self._eof = True
             return False
         if line and len(line) < MAXLEN:
             # Valid word
             self._nxt = line
             self._key = Alphabet.sortkey(line)
             return True
Ejemplo n.º 13
0
 def client_state(self):
     """ Create a package of information for the client about the current state """
     reply = dict()
     if self.state.is_game_over():
         # The game is now over - one of the players finished it
         reply["result"] = Error.GAME_OVER # Not really an error
         num_moves = 1
         if self.last_move is not None:
             # Show the autoplayer move if it was the last move in the game
             reply["lastmove"] = self.last_move.details()
             num_moves = 2 # One new move to be added to move list
         newmoves = [(player, m.summary(self.state.board())) for player, m in self.moves[-num_moves:]]
         # Lastplayer is the player who finished the game
         lastplayer = self.moves[-1][0]
         if not self.resigned:
             # If the game did not end by resignation,
             # account for the losing rack
             rack = self.state._racks[1 - lastplayer].contents()
             # Subtract the score of the losing rack from the losing player
             newmoves.append((1 - lastplayer, (u"", rack, -1 * Alphabet.score(rack))))
             # Add the score of the losing rack to the winning player
             newmoves.append((lastplayer, (u"", rack, 1 * Alphabet.score(rack))))
         # Add a synthetic "game over" move
         newmoves.append((1 - lastplayer, (u"", u"OVER", 0)))
         reply["newmoves"] = newmoves
         reply["bag"] = "" # Bag is now empty, by definition
         reply["xchg"] = False # Exchange move not allowed
     else:
         # Game is still in progress
         reply["result"] = 0 # Indicate no error
         reply["rack"] = self.state.player_rack().details()
         reply["lastmove"] = self.last_move.details()
         reply["newmoves"] = [(player, m.summary(self.state.board())) for player, m in self.moves[-2:]]
         reply["bag"] = self.display_bag()
         reply["xchg"] = self.state.is_exchange_allowed()
     reply["scores"] = self.state.scores()
     return reply
Ejemplo n.º 14
0
    def finalize_score(self, lost_on_overtime = None, overtime_adjustment = None):
        """ When game is completed, calculate the final score adjustments """

        if self._game_resigned:
            # In case of a resignation, the resigning player has already lost all points
            return

        sc = self._scores
        adj = self._adj_scores

        if lost_on_overtime is not None:
            # One of the players lost on overtime
            player = lost_on_overtime
            # Subtract 100 points from the player
            adj[player] = - min(100, sc[player])
            # If not enough to make the other player win, add to the other player
            if sc[player] + adj[player] >= sc[1 - player]:
                adj[1 - player] = sc[player] + adj[player] + 1 - sc[1 - player]
            # There is no consideration of rack leave in this case
            return

        if any(self._racks[ix].is_empty() for ix in range(2)):
            # Normal win by one of the players
            for ix in range(2):
                # Add double the score of the opponent's tiles (will be zero for the losing player)
                adj[ix] = 2 * Alphabet.score(self.rack(1 - ix))
        else:
            # Game expired by passes
            for ix in range(2):
                # Subtract the score of the player's own tiles
                adj[ix] = - Alphabet.score(self.rack(ix))

        # Apply overtime adjustment, if any
        if overtime_adjustment is not None:
            for ix in range(2):
                adj[ix] += overtime_adjustment[ix]
Ejemplo n.º 15
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
Ejemplo n.º 16
0
 def read_word(self):
     """ Read lines until we have a legal word or EOF """
     while True:
         try:
             line = self._fin.next()
         except StopIteration:
             # We're done with this file
             self._eof = True
             return False
         if line.endswith(u'\r\n'):
             # Cut off trailing CRLF (Windows-style)
             line = line[0:-2]
         elif line.endswith(u'\n'):
             # Cut off trailing LF (Unix-style)
             line = line[0:-1]
         if line and len(line) < MAXLEN:
             # Valid word
             self._nxt = line
             self._key = Alphabet.sortkey(line)
             return True
Ejemplo n.º 17
0
 def done(self):
     """ Called when the whole navigation is done """
     self._result.sort(key=lambda x: (-len(x), Alphabet.sortkey(x)))
Ejemplo n.º 18
0
def _run_stats(from_time, to_time):
    """ Runs a process to update user statistics and Elo ratings """

    logging.info(u"Generating stats from {0} to {1}".format(from_time, to_time))

    if from_time is None or to_time is None:
        # Time range must be specified
        return

    if from_time >= to_time:
        # Null time range
        return

    # Iterate over all finished games within the time span in temporal order
    q = GameModel.query(GameModel.over == True).order(GameModel.ts_last_move) \
        .filter(GameModel.ts_last_move > from_time) \
        .filter(GameModel.ts_last_move <= to_time)

    # The accumulated user statistics
    users = dict()

    def _init_stat(user_id, robot_level):
        """ Returns the newest StatsModel instance available for the given user """
        return StatsModel.newest_before(from_time, user_id, robot_level)

    cnt = 0
    ts_last_processed = None

    try:
        # Use i as a progress counter
        for i, gm in enumerate(q):
            ts = Alphabet.format_timestamp(gm.timestamp)
            lm = Alphabet.format_timestamp(gm.ts_last_move or gm.timestamp)
            p0 = None if gm.player0 is None else gm.player0.id()
            p1 = None if gm.player1 is None else gm.player1.id()
            robot_game = (p0 is None) or (p1 is None)
            if robot_game:
                rl = gm.robot_level
            else:
                rl = 0
            s0 = gm.score0
            s1 = gm.score1

            if (s0 == 0) and (s1 == 0):
                # When a game ends by resigning immediately,
                # make sure that the weaker player
                # doesn't get Elo points for a draw; in fact,
                # ignore such a game altogether in the statistics
                continue

            if p0 is None:
                k0 = "robot-" + str(rl)
            else:
                k0 = p0
            if p1 is None:
                k1 = "robot-" + str(rl)
            else:
                k1 = p1

            if k0 in users:
                urec0 = users[k0]
            else:
                users[k0] = urec0 = _init_stat(p0, rl if p0 is None else 0)
            if k1 in users:
                urec1 = users[k1]
            else:
                users[k1] = urec1 = _init_stat(p1, rl if p1 is None else 0)
            # Number of games played
            urec0.games += 1
            urec1.games += 1
            if not robot_game:
                urec0.human_games += 1
                urec1.human_games += 1
            # Total scores
            urec0.score += s0
            urec1.score += s1
            urec0.score_against += s1
            urec1.score_against += s0
            if not robot_game:
                urec0.human_score += s0
                urec1.human_score += s1
                urec0.human_score_against += s1
                urec1.human_score_against += s0
            # Wins and losses
            if s0 > s1:
                urec0.wins += 1
                urec1.losses += 1
            elif s1 > s0:
                urec1.wins += 1
                urec0.losses += 1
            if not robot_game:
                if s0 > s1:
                    urec0.human_wins += 1
                    urec1.human_losses += 1
                elif s1 > s0:
                    urec1.human_wins += 1
                    urec0.human_losses += 1
            # Find out whether players are established or beginners
            est0 = urec0.games > ESTABLISHED_MARK
            est1 = urec1.games > ESTABLISHED_MARK
            # Save the Elo point state used in the calculation
            gm.elo0, gm.elo1 = urec0.elo, urec1.elo
            # Compute the Elo points of both players
            adj = _compute_elo((urec0.elo, urec1.elo), s0, s1, est0, est1)
            # When an established player is playing a beginning (provisional) player,
            # leave the Elo score of the established player unchanged
            # Adjust player 0
            if est0 and not est1:
                adj = (0, adj[1])
            gm.elo0_adj = adj[0]
            urec0.elo += adj[0]
            # Adjust player 1
            if est1 and not est0:
                adj = (adj[0], 0)
            gm.elo1_adj = adj[1]
            urec1.elo += adj[1]
            # If not a robot game, compute the human-only Elo
            if not robot_game:
                gm.human_elo0, gm.human_elo1 = urec0.human_elo, urec1.human_elo
                adj = _compute_elo((urec0.human_elo, urec1.human_elo), s0, s1, est0, est1)
                # Adjust player 0
                if est0 and not est1:
                    adj = (0, adj[1])
                gm.human_elo0_adj = adj[0]
                urec0.human_elo += adj[0]
                # Adjust player 1
                if est1 and not est0:
                    adj = (adj[0], 0)
                gm.human_elo1_adj = adj[1]
                urec1.human_elo += adj[1]
            # Save the game object with the new Elo adjustment statistics
            gm.put()
            # Save the last processed timestamp
            ts_last_processed = lm
            cnt += 1
            # Report on our progress
            if (i + 1) % 1000 == 0:
                logging.info(u"Processed {0} games".format(i + 1))

    except DeadlineExceededError as ex:
        # Hit deadline: save the stuff we already have and
        # defer a new task to continue where we left off
        logging.info(u"Deadline exceeded in stats loop after {0} games and {1} users"
            .format(cnt, len(users)))
        logging.info(u"Resuming from timestamp {0}".format(ts_last_processed))
        if ts_last_processed is not None:
            _write_stats(ts_last_processed, users)
        deferred.defer(deferred_stats,
            from_time = ts_last_processed or from_time, to_time = to_time)
        # Normal return prevents this task from being run again
        return

    except Exception as ex:
        logging.info(u"Exception in stats loop: {0}".format(ex))
        # Avoid having the task retried
        raise deferred.PermanentTaskFailure()

    # Completed without incident
    logging.info(u"Normal completion of stats for {1} games and {0} users".format(len(users), cnt))

    _write_stats(to_time, users)
Ejemplo n.º 19
0
 def subtract_rack(self, rack):
     """ Subtract all tiles in the rack from the bag """
     self._tiles = Alphabet.string_subtract(self._tiles, rack)
Ejemplo n.º 20
0
 def subtract_rack(self, rack):
     """ Subtract all tiles in the rack from the bag """
     self._tiles = Alphabet.string_subtract(self._tiles, rack)
Ejemplo n.º 21
0
 def subtract_board(self, board):
     """ Subtract all tiles on the board from the bag """
     board_tiles = u''.join(tile for row, col, tile, letter in board.enum_tiles())
     self._tiles = Alphabet.string_subtract(self._tiles, board_tiles)
Ejemplo n.º 22
0
 def is_full(self):
     """ Returns True if the bag is full, i.e. no tiles have been drawn """
     return self.num_tiles() == len(Alphabet.full_bag())
Ejemplo n.º 23
0
 def process(self, rack):
     """ Generate the data that will be shown to the user on the result page.
         This includes a list of permutations of the rack, as well as combinations
         of the rack with a single additional letter. High scoring words are also
         tabulated. """
     # Start with basic hygiene
     if not rack:
         return False
     rack = rack.strip()
     if not rack:
         return False
     # Make sure we reset all state in case we're called multiple times
     self._counter = 0
     self._allwords = [] # List of tuples: (word, score)
     self._highscore = 0
     self._highwords = []
     self._combinations = { }
     self._rack = u''
     self._pattern = False
     # Do a sanity check on the input by calculating its raw score, thereby
     # checking whether all the letters are valid
     score = 0
     rack_lower = u'' # Rack converted to lowercase
     wildcards = 0 # Number of wildcard characters
     # If the rack starts with an equals sign ('=') we do a pattern match
     # instead of a permutation search
     if rack[0] == u'=':
         self._pattern = True
         rack = rack[1:]
     # Sanitize the rack, converting upper case to lower case and
     # catching invalid characters
     try:
         for c in rack:
             ch = c
             if ch in Alphabet.upper:
                 # Uppercase: find corresponding lowercase letter
                 ch = Alphabet.lowercase(ch)
             if ch in u'?_*':
                 # This is one of the allowed wildcard characters
                 wildcards += 1
                 ch = u'?'
             else:
                 score += Alphabet.scores[ch]
             rack_lower += ch
     except KeyError:
         # A letter in the rack is not valid, even after conversion to lower case
         return False
     if not self._pattern and (wildcards > 2):
         # Too many wildcards in a permutation search - need to constrain result set size
         return False
     # The rack contains only valid letters
     self._rack = rack_lower
     # Generate combinations
     if not self._pattern and not wildcards:
         # If no wildcards given, check combinations with one additional letter
         query = self._rack + u'?'
         # Permute the rack with one additional letter
         p = self._word_db.find_permutations(query)
         # Check the permutations to find valid words and their scores
         if p is not None:
             for word in p:
                 # Only interested in full-length permutations, i.e. with the additional letter
                 if len(word) == len(query):
                     # Find out which letter was added
                     addedletter = Alphabet.string_subtract(word, self._rack)
                     self._add_combination(addedletter, word)
     # Check permutations
     # The shortest possible rack to check for permutations is 2 letters
     if len(self._rack) < 2:
         return True
     if self._pattern:
         # Use pattern matching
         p = self._word_db.find_matches(self._rack, True) # We'd like a sorted result
     else:
         # Find permutations
         p = self._word_db.find_permutations(self._rack)
     if p is None:
         return True
     for word in p:
         if len(word) < 2:
             # Don't show single letter words
             continue
         # Calculate the basic score of the word
         score = self.score(word)
         if wildcards and not self._pattern:
             # Complication: Make sure we don't count the score of the wildcard tile(s)
             wildchars = Alphabet.string_subtract(word, self._rack)
             # What we have left are the wildcard substitutes: subtract'em
             score -= self.score(wildchars)
         self._add_permutation(word, score)
     # Successful
     return True
Ejemplo n.º 24
0
 def _sorted(l):
     """ Return a list of (prefix, node) tuples sorted by prefix """
     return sorted(l, key=lambda x: Alphabet.sortkey(x[0]))
Ejemplo n.º 25
0
 def subtract_board(self, board):
     """ Subtract all tiles on the board from the bag """
     board_tiles = u''.join(
         tile for row, col, tile, letter in board.enum_tiles())
     self._tiles = Alphabet.string_subtract(self._tiles, board_tiles)
Ejemplo n.º 26
0
 def process(self, rack):
     """ Generate the data that will be shown to the user on the result page.
         This includes a list of permutations of the rack, as well as combinations
         of the rack with a single additional letter. High scoring words are also
         tabulated. """
     # Start with basic hygiene
     if not rack:
         return False
     rack = rack.strip()
     if not rack:
         return False
     # Make sure we reset all state in case we're called multiple times
     self._counter = 0
     self._allwords = [] # List of tuples: (word, score)
     self._highscore = 0
     self._highwords = []
     self._combinations = { }
     self._rack = ""
     self._pattern = False
     # Do a sanity check on the input by calculating its raw score, thereby
     # checking whether all the letters are valid
     score = 0
     rack_lower = "" # Rack converted to lowercase
     wildcards = 0 # Number of wildcard characters
     # If the rack starts with an equals sign ('=') we do a pattern match
     # instead of a permutation search
     if rack[0] == '=':
         self._pattern = True
         rack = rack[1:]
     # Sanitize the rack, converting upper case to lower case and
     # catching invalid characters
     try:
         for c in rack:
             ch = c
             if ch in Alphabet.upper:
                 # Uppercase: find corresponding lowercase letter
                 ch = Alphabet.lowercase(ch)
             if ch in '?_*':
                 # This is one of the allowed wildcard characters
                 wildcards += 1
                 ch = '?'
             else:
                 score += Alphabet.scores[ch]
             rack_lower += ch
     except KeyError:
         # A letter in the rack is not valid, even after conversion to lower case
         return False
     if not self._pattern and (wildcards > 2):
         # Too many wildcards in a permutation search - need to constrain result set size
         return False
     # The rack contains only valid letters
     self._rack = rack_lower
     # Generate combinations
     if not self._pattern and not wildcards:
         # If no wildcards given, check combinations with one additional letter
         query = self._rack + '?'
         # Permute the rack with one additional letter
         p = self._word_db.find_permutations(query)
         # Check the permutations to find valid words and their scores
         if p is not None:
             for word in p:
                 # Only interested in full-length permutations, i.e. with the additional letter
                 if len(word) == len(query):
                     # Find out which letter was added
                     addedletter = Alphabet.string_subtract(word, self._rack)
                     self._add_combination(addedletter, word)
     # Check permutations
     # The shortest possible rack to check for permutations is 2 letters
     if len(self._rack) < 2:
         return True
     if self._pattern:
         # Use pattern matching
         p = self._word_db.find_matches(self._rack, True) # We'd like a sorted result
     else:
         # Find permutations
         p = self._word_db.find_permutations(self._rack)
     if p is None:
         return True
     for word in p:
         if len(word) < 2:
             # Don't show single letter words
             continue
         # Calculate the basic score of the word
         score = self.score(word)
         if wildcards and not self._pattern:
             # Complication: Make sure we don't count the score of the wildcard tile(s)
             wildchars = Alphabet.string_subtract(word, self._rack)
             # What we have left are the wildcard substitutes: subtract'em
             score -= self.score(wildchars)
         self._add_permutation(word, score)
     # Successful
     return True
Ejemplo n.º 27
0
 def start_time(self):
     """ Returns the timestamp of the game in a readable format """
     return u"" if self.timestamp is None else Alphabet.format_timestamp(self.timestamp)
Ejemplo n.º 28
0
 def done(self):
     """ Called when the whole navigation is done """
     self._result.sort(key = lambda x: (-len(x), Alphabet.sortkey(x)))
Ejemplo n.º 29
0
 def end_time(self):
     """ Returns the time of the last move in a readable format """
     return u"" if self.ts_last_move is None else Alphabet.format_timestamp(self.ts_last_move)
Ejemplo n.º 30
0
 def end_time(self):
     """ Returns the time of the last move in a readable format """
     return u"" if self.ts_last_move is None else Alphabet.format_timestamp(
         self.ts_last_move)
Ejemplo n.º 31
0
    def list(cls, nick_from, nick_to, max_len = 100):
        """ Query for a list of users within a nickname range """

        nick_from = u"a" if nick_from is None else Alphabet.tolower(nick_from)
        nick_to = u"ö" if nick_to is None else Alphabet.tolower(nick_to)
        counter = 0

        try:
            o_from = Alphabet.full_order.index(nick_from[0])
        except:
            o_from = 0
        try:
            o_to = Alphabet.full_order.index(nick_to[0])
        except:
            o_to = len(Alphabet.full_order) - 1

        # We do this by issuing a series of queries, each returning
        # nicknames beginning with a particular letter.
        # These shenanigans are necessary because NDB maintains its string
        # indexes by Unicode ordinal index, which is quite different from
        # the actual sort collation order we need. Additionally, the
        # indexes are case-sensitive while our query boundaries are not.

        # Prepare the list of query letters
        q_letters = []

        for i in range(o_from, o_to + 1):
            # Append the lower case letter
            q_letters.append(Alphabet.full_order[i])
            # Append the upper case letter
            q_letters.append(Alphabet.full_upper[i])

        # For aesthetic cleanliness, sort the query letters (in Unicode order)
        q_letters.sort()

        count = 0
        for q_from in q_letters:

            q_to = unichr(ord(q_from) + 1)

            # logging.info(u"Issuing user query from '{0}' to '{1}'".format(q_from, q_to).encode('latin-1'))
            q = cls.query(ndb.AND(UserModel.nickname >= q_from, UserModel.nickname < q_to))

            CHUNK_SIZE = 1000 # Individual letters contain >600 users as of 2015-02-12
            # logging.info(u"Fetching chunk of {0} users".format(CHUNK_SIZE).encode('latin-1'))
            for um in iter_q(q, chunk_size = CHUNK_SIZE):
                if not um.inactive:
                    # This entity matches: return a dict describing it
                    yield dict(
                        id = um.key.id(),
                        nickname = um.nickname,
                        prefs = um.prefs,
                        timestamp = um.timestamp,
                        ready = um.ready,
                        ready_timed = um.ready_timed,
                        human_elo = um.human_elo
                    )
                    count += 1
                    if max_len and count >= max_len:
                        # Reached limit: done
                        return
Ejemplo n.º 32
0
 def _sorted(l):
     """ Return a list of (prefix, node) tuples sorted by prefix """
     return sorted(l, key = lambda x: Alphabet.sortkey(x[0]))
Ejemplo n.º 33
0
def _run_stats(from_time, to_time):
    """ Runs a process to update user statistics and Elo ratings """

    logging.info(u"Generating stats from {0} to {1}".format(
        from_time, to_time))

    if from_time is None or to_time is None:
        # Time range must be specified
        return

    if from_time >= to_time:
        # Null time range
        return

    # Iterate over all finished games within the time span in temporal order
    q = GameModel.query(ndb.AND(GameModel.ts_last_move > from_time, GameModel.ts_last_move <= to_time)) \
        .order(GameModel.ts_last_move) \
        .filter(GameModel.over == True)

    # The accumulated user statistics
    users = dict()

    def _init_stat(user_id, robot_level):
        """ Returns the newest StatsModel instance available for the given user """
        return StatsModel.newest_before(from_time, user_id, robot_level)

    cnt = 0
    ts_last_processed = None

    try:
        # Use i as a progress counter
        i = 0
        for gm in iter_q(q, chunk_size=250):
            i += 1
            lm = Alphabet.format_timestamp(gm.ts_last_move or gm.timestamp)
            p0 = None if gm.player0 is None else gm.player0.id()
            p1 = None if gm.player1 is None else gm.player1.id()
            robot_game = (p0 is None) or (p1 is None)
            if robot_game:
                rl = gm.robot_level
            else:
                rl = 0
            s0 = gm.score0
            s1 = gm.score1

            if (s0 == 0) and (s1 == 0):
                # When a game ends by resigning immediately,
                # make sure that the weaker player
                # doesn't get Elo points for a draw; in fact,
                # ignore such a game altogether in the statistics
                continue

            if p0 is None:
                k0 = "robot-" + str(rl)
            else:
                k0 = p0
            if p1 is None:
                k1 = "robot-" + str(rl)
            else:
                k1 = p1

            if k0 in users:
                urec0 = users[k0]
            else:
                users[k0] = urec0 = _init_stat(p0, rl if p0 is None else 0)
            if k1 in users:
                urec1 = users[k1]
            else:
                users[k1] = urec1 = _init_stat(p1, rl if p1 is None else 0)
            # Number of games played
            urec0.games += 1
            urec1.games += 1
            if not robot_game:
                urec0.human_games += 1
                urec1.human_games += 1
            # Total scores
            urec0.score += s0
            urec1.score += s1
            urec0.score_against += s1
            urec1.score_against += s0
            if not robot_game:
                urec0.human_score += s0
                urec1.human_score += s1
                urec0.human_score_against += s1
                urec1.human_score_against += s0
            # Wins and losses
            if s0 > s1:
                urec0.wins += 1
                urec1.losses += 1
            elif s1 > s0:
                urec1.wins += 1
                urec0.losses += 1
            if not robot_game:
                if s0 > s1:
                    urec0.human_wins += 1
                    urec1.human_losses += 1
                elif s1 > s0:
                    urec1.human_wins += 1
                    urec0.human_losses += 1
            # Find out whether players are established or beginners
            est0 = urec0.games > ESTABLISHED_MARK
            est1 = urec1.games > ESTABLISHED_MARK
            # Save the Elo point state used in the calculation
            gm.elo0, gm.elo1 = urec0.elo, urec1.elo
            # Compute the Elo points of both players
            adj = _compute_elo((urec0.elo, urec1.elo), s0, s1, est0, est1)
            # When an established player is playing a beginning (provisional) player,
            # leave the Elo score of the established player unchanged
            # Adjust player 0
            if est0 and not est1:
                adj = (0, adj[1])
            gm.elo0_adj = adj[0]
            urec0.elo += adj[0]
            # Adjust player 1
            if est1 and not est0:
                adj = (adj[0], 0)
            gm.elo1_adj = adj[1]
            urec1.elo += adj[1]
            # If not a robot game, compute the human-only Elo
            if not robot_game:
                gm.human_elo0, gm.human_elo1 = urec0.human_elo, urec1.human_elo
                adj = _compute_elo((urec0.human_elo, urec1.human_elo), s0, s1,
                                   est0, est1)
                # Adjust player 0
                if est0 and not est1:
                    adj = (0, adj[1])
                gm.human_elo0_adj = adj[0]
                urec0.human_elo += adj[0]
                # Adjust player 1
                if est1 and not est0:
                    adj = (adj[0], 0)
                gm.human_elo1_adj = adj[1]
                urec1.human_elo += adj[1]
            # Save the game object with the new Elo adjustment statistics
            gm.put()
            # Save the last processed timestamp
            ts_last_processed = lm
            cnt += 1
            # Report on our progress
            if i % 1000 == 0:
                logging.info(u"Processed {0} games".format(i))

    except DeadlineExceededError as ex:
        # Hit deadline: save the stuff we already have and
        # defer a new task to continue where we left off
        logging.info(
            u"Deadline exceeded in stats loop after {0} games and {1} users".
            format(cnt, len(users)))
        logging.info(u"Resuming from timestamp {0}".format(ts_last_processed))
        if ts_last_processed is not None:
            _write_stats(ts_last_processed, users)
        deferred.defer(deferred_stats,
                       from_time=ts_last_processed or from_time,
                       to_time=to_time)
        # Normal return prevents this task from being run again
        return

    except Exception as ex:
        logging.info(u"Exception in stats loop: {0}".format(ex))
        # Avoid having the task retried
        raise deferred.PermanentTaskFailure()

    # Completed without incident
    logging.info(
        u"Normal completion of stats for {1} games and {0} users".format(
            len(users), cnt))

    _write_stats(to_time, users)
Ejemplo n.º 34
0
 def start_time(self):
     """ Returns the timestamp of the game in a readable format """
     return u"" if self.timestamp is None else Alphabet.format_timestamp(
         self.timestamp)