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)
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)
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
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)
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)
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
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
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)
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)
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)))
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
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
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
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]
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 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
def done(self): """ Called when the whole navigation is done """ self._result.sort(key=lambda x: (-len(x), Alphabet.sortkey(x)))
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)
def subtract_rack(self, rack): """ Subtract all tiles in the rack from the bag """ self._tiles = Alphabet.string_subtract(self._tiles, rack)
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)
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())
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
def _sorted(l): """ Return a list of (prefix, node) tuples sorted by prefix """ return sorted(l, key=lambda x: Alphabet.sortkey(x[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)
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
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)
def done(self): """ Called when the whole navigation is done """ self._result.sort(key = lambda x: (-len(x), Alphabet.sortkey(x)))
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)
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)
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
def _sorted(l): """ Return a list of (prefix, node) tuples sorted by prefix """ return sorted(l, key = lambda x: Alphabet.sortkey(x[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)
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)