def current(cls): """ Obtain the current game state """ user = User.current() user_id = None if user is None else user.id() if not user_id: # No game state found return None if user_id in Game._cache: return Game._cache[user_id] # No game in cache: attempt to find one in the database uuid = GameModel.find_live_game(user_id) if uuid is None: # Not found in persistent storage: create a new game return cls.new(user.nickname()) # Load from persistent storage return cls.load(uuid, user.nickname())
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 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 _store_locked(self): """ Store the game after having acquired the object lock """ assert self.uuid is not None gm = GameModel(id = self.uuid) gm.timestamp = self.timestamp gm.ts_last_move = self.ts_last_move gm.set_player(0, self.player_ids[0]) gm.set_player(1, self.player_ids[1]) gm.irack0 = self.initial_racks[0] gm.irack1 = self.initial_racks[1] gm.rack0 = self.state.rack(0) gm.rack1 = self.state.rack(1) gm.over = self.is_over() sc = self.final_scores() # Includes adjustments if game is over gm.score0 = sc[0] gm.score1 = sc[1] gm.to_move = len(self.moves) % 2 gm.robot_level = self.robot_level gm.prefs = self._preferences tile_count = 0 movelist = [] best_word = [None, None] best_word_score = [0, 0] player = 0 for m in self.moves: mm = MoveModel() coord, tiles, score = m.move.summary(self.state.board()) if coord: # Regular move: count the tiles actually laid down tile_count += m.move.num_covers() # Keep track of best words laid down by each player if score > best_word_score[player]: best_word_score[player] = score best_word[player] = tiles mm.coord = coord mm.tiles = tiles mm.score = score mm.rack = m.rack mm.timestamp = m.ts movelist.append(mm) player = 1 - player gm.moves = movelist gm.tile_count = tile_count # Update the database entity gm.put() # Storing a game that is now over: update the player statistics as well if self.is_over(): pid_0 = self.player_ids[0] pid_1 = self.player_ids[1] u0 = User.load(pid_0) if pid_0 else None u1 = User.load(pid_1) if pid_1 else None if u0: mod_0 = u0.adjust_highest_score(sc[0], self.uuid) mod_0 |= u0.adjust_best_word(best_word[0], best_word_score[0], self.uuid) if mod_0: # Modified: store the updated user entity u0.update() if u1: mod_1 = u1.adjust_highest_score(sc[1], self.uuid) mod_1 |= u1.adjust_best_word(best_word[1], best_word_score[1], self.uuid) if mod_1: # Modified: store the updated user entity u1.update()
def _load_locked(cls, uuid, use_cache = True): """ Load an existing game from cache or persistent storage under lock """ gm = GameModel.fetch(uuid, use_cache) if gm is None: # A game with this uuid is not found in the database: give up return None # Initialize a new Game instance with a pre-existing uuid game = cls(uuid) # Set the timestamps game.timestamp = gm.timestamp game.ts_last_move = gm.ts_last_move if game.ts_last_move is None: # If no last move timestamp, default to the start of the game game.ts_last_move = game.timestamp # Initialize the preferences game._preferences = gm.prefs # Initialize a fresh, empty state with no tiles drawn into the racks game.state = State(drawtiles = False) # A player_id of None means that the player is an autoplayer (robot) game.player_ids[0] = None if gm.player0 is None else gm.player0.id() game.player_ids[1] = None if gm.player1 is None else gm.player1.id() game.robot_level = gm.robot_level # Load the initial racks game.initial_racks[0] = gm.irack0 game.initial_racks[1] = gm.irack1 # Load the current racks game.state.set_rack(0, gm.rack0) game.state.set_rack(1, gm.rack1) # Process the moves player = 0 # mx = 0 # Move counter for debugging/logging for mm in gm.moves: # mx += 1 # logging.info(u"Game move {0} tiles '{3}' score is {1}:{2}".format(mx, game.state._scores[0], game.state._scores[1], mm.tiles).encode("latin-1")) m = None if mm.coord: # Normal tile move # Decode the coordinate: A15 = horizontal, 15A = vertical if mm.coord[0] in Board.ROWIDS: row = Board.ROWIDS.index(mm.coord[0]) col = int(mm.coord[1:]) - 1 horiz = True else: row = Board.ROWIDS.index(mm.coord[-1]) col = int(mm.coord[0:-1]) - 1 horiz = False # The tiles string may contain wildcards followed by their meaning # Remove the ? marks to get the "plain" word formed if mm.tiles is not None: m = Move(mm.tiles.replace(u'?', u''), row, col, horiz) m.make_covers(game.state.board(), mm.tiles) elif mm.tiles[0:4] == u"EXCH": # Exchange move m = ExchangeMove(mm.tiles[5:]) elif mm.tiles == u"PASS": # Pass move m = PassMove() elif mm.tiles == u"RSGN": # Game resigned m = ResignMove(- mm.score) assert m is not None if m: # Do a "shallow apply" of the move, which updates # the board and internal state variables but does # not modify the bag or the racks game.state.apply_move(m, True) # Append to the move history game.moves.append(MoveTuple(player, m, mm.rack, mm.timestamp)) player = 1 - player # Find out what tiles are now in the bag game.state.recalc_bag() # Account for the final tiles in the rack and overtime, if any if game.is_over(): game.finalize_score() if not gm.over: # The game was not marked as over when we loaded it from # the datastore, but it is over now. One of the players must # have lost on overtime. We need to update the persistent state. game._store_locked() return game
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 _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 store(self): """ Store the game state in persistent storage """ assert self.uuid is not None user = User.current() if user is None: # No current user: can't store game assert False return gm = GameModel(id = self.uuid) gm.set_player(self.player_index, user.id()) gm.set_player(1 - self.player_index, None) gm.rack0 = self.state._racks[0].contents() gm.rack1 = self.state._racks[1].contents() gm.score0 = self.state.scores()[0] gm.score1 = self.state.scores()[1] gm.to_move = len(self.moves) % 2 gm.over = self.state.is_game_over() movelist = [] for player, m in self.moves: mm = MoveModel() coord, tiles, score = m.summary(self.state.board()) mm.coord = coord mm.tiles = tiles mm.score = score movelist.append(mm) gm.moves = movelist gm.put()
def load(cls, uuid, username): """ Load an already existing game from persistent storage """ gm = GameModel.fetch(uuid) if gm is None: # A game with this uuid is not found in the database: give up return None # Initialize a new Game instance with a pre-existing uuid game = cls(uuid) game.username = username game.state = State(drawtiles = False) if gm.player0 is None: # Player 0 is an Autoplayer game.player_index = 1 # Human (local) player is 1 else: assert gm.player1 is None game.player_index = 0 # Human (local) player is 0 game.state.set_player_name(game.player_index, username) game.state.set_player_name(1 - game.player_index, u"Netskrafl") # Load the current racks game.state._racks[0].set_tiles(gm.rack0) game.state._racks[1].set_tiles(gm.rack1) # Process the moves player = 0 for mm in gm.moves: m = None if mm.coord: # Normal tile move # Decode the coordinate: A15 = horizontal, 15A = vertical if mm.coord[0] in Board.ROWIDS: row = Board.ROWIDS.index(mm.coord[0]) col = int(mm.coord[1:]) - 1 horiz = True else: row = Board.ROWIDS.index(mm.coord[-1]) col = int(mm.coord[0:-1]) - 1 horiz = False # The tiles string may contain wildcards followed by their meaning # Remove the ? marks to get the "plain" word formed m = Move(mm.tiles.replace(u'?', u''), row, col, horiz) m.make_covers(game.state.board(), mm.tiles) elif mm.tiles[0:4] == u"EXCH": # Exchange move m = ExchangeMove(mm.tiles[5:]) elif mm.tiles == u"PASS": # Pass move m = PassMove() elif mm.tiles == u"RSGN": # Game resigned m = ResignMove(- mm.score) assert m is not None if m: # Do a "shallow apply" of the move, which updates # the board and internal state variables but does # not modify the bag or the racks game.state.apply_move(m, True) # Append to the move history game.moves.append((player, m)) player = 1 - player # If the moves were correctly applied, the scores should match assert game.state._scores[0] == gm.score0 assert game.state._scores[1] == gm.score1 # Find out what tiles are now in the bag game.state.recalc_bag() # Cache the game so it can be looked up by user id user = User.current() if user is not None: Game._cache[user.id()] = game return game
def _store_locked(self): """ Store the game after having acquired the object lock """ assert self.uuid is not None gm = GameModel(id=self.uuid) gm.timestamp = self.timestamp gm.ts_last_move = self.ts_last_move gm.set_player(0, self.player_ids[0]) gm.set_player(1, self.player_ids[1]) gm.irack0 = self.initial_racks[0] gm.irack1 = self.initial_racks[1] gm.rack0 = self.state.rack(0) gm.rack1 = self.state.rack(1) gm.over = self.is_over() sc = self.final_scores() # Includes adjustments if game is over gm.score0 = sc[0] gm.score1 = sc[1] gm.to_move = len(self.moves) % 2 gm.robot_level = self.robot_level gm.prefs = self._preferences tile_count = 0 movelist = [] best_word = [None, None] best_word_score = [0, 0] player = 0 for m in self.moves: mm = MoveModel() coord, tiles, score = m.move.summary(self.state) if coord: # Regular move: count the tiles actually laid down tile_count += m.move.num_covers() # Keep track of best words laid down by each player if score > best_word_score[player]: best_word_score[player] = score best_word[player] = tiles mm.coord = coord mm.tiles = tiles mm.score = score mm.rack = m.rack mm.timestamp = m.ts movelist.append(mm) player = 1 - player gm.moves = movelist gm.tile_count = tile_count # Update the database entity gm.put() # Storing a game that is now over: update the player statistics as well if self.is_over(): pid_0 = self.player_ids[0] pid_1 = self.player_ids[1] u0 = User.load(pid_0) if pid_0 else None u1 = User.load(pid_1) if pid_1 else None if u0: mod_0 = u0.adjust_highest_score(sc[0], self.uuid) mod_0 |= u0.adjust_best_word(best_word[0], best_word_score[0], self.uuid) if mod_0: # Modified: store the updated user entity u0.update() if u1: mod_1 = u1.adjust_highest_score(sc[1], self.uuid) mod_1 |= u1.adjust_best_word(best_word[1], best_word_score[1], self.uuid) if mod_1: # Modified: store the updated user entity u1.update()
def _load_locked(cls, uuid, use_cache=True): """ Load an existing game from cache or persistent storage under lock """ gm = GameModel.fetch(uuid, use_cache) if gm is None: # A game with this uuid is not found in the database: give up return None # Initialize a new Game instance with a pre-existing uuid game = cls(uuid) # Set the timestamps game.timestamp = gm.timestamp game.ts_last_move = gm.ts_last_move if game.ts_last_move is None: # If no last move timestamp, default to the start of the game game.ts_last_move = game.timestamp # Initialize the preferences game._preferences = gm.prefs # Initialize a fresh, empty state with no tiles drawn into the racks game.state = State(drawtiles=False, tileset=game.tileset) # A player_id of None means that the player is an autoplayer (robot) game.player_ids[0] = None if gm.player0 is None else gm.player0.id() game.player_ids[1] = None if gm.player1 is None else gm.player1.id() game.robot_level = gm.robot_level # Load the initial racks game.initial_racks[0] = gm.irack0 game.initial_racks[1] = gm.irack1 # Load the current racks game.state.set_rack(0, gm.rack0) game.state.set_rack(1, gm.rack1) # Process the moves player = 0 # mx = 0 # Move counter for debugging/logging for mm in gm.moves: # mx += 1 # logging.info(u"Game move {0} tiles '{3}' score is {1}:{2}".format(mx, game.state._scores[0], game.state._scores[1], mm.tiles).encode("latin-1")) m = None if mm.coord: # Normal tile move # Decode the coordinate: A15 = horizontal, 15A = vertical if mm.coord[0] in Board.ROWIDS: row = Board.ROWIDS.index(mm.coord[0]) col = int(mm.coord[1:]) - 1 horiz = True else: row = Board.ROWIDS.index(mm.coord[-1]) col = int(mm.coord[0:-1]) - 1 horiz = False # The tiles string may contain wildcards followed by their meaning # Remove the ? marks to get the "plain" word formed if mm.tiles is not None: m = Move(mm.tiles.replace(u'?', u''), row, col, horiz) m.make_covers(game.state.board(), mm.tiles) elif mm.tiles[0:4] == u"EXCH": # Exchange move m = ExchangeMove(mm.tiles[5:]) elif mm.tiles == u"PASS": # Pass move m = PassMove() elif mm.tiles == u"RSGN": # Game resigned m = ResignMove(-mm.score) assert m is not None if m: # Do a "shallow apply" of the move, which updates # the board and internal state variables but does # not modify the bag or the racks game.state.apply_move(m, True) # Append to the move history game.moves.append(MoveTuple(player, m, mm.rack, mm.timestamp)) player = 1 - player # Find out what tiles are now in the bag game.state.recalc_bag() # Account for the final tiles in the rack and overtime, if any if game.is_over(): game.finalize_score() if not gm.over: # The game was not marked as over when we loaded it from # the datastore, but it is over now. One of the players must # have lost on overtime. We need to update the persistent state. game._store_locked() return game