示例#1
0
 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())
示例#2
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)
示例#3
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)
示例#4
0
    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()
示例#5
0
    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
示例#6
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)
示例#7
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)
示例#8
0
 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()
示例#9
0
    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
示例#10
0
    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()
示例#11
0
    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