def add_slots(self, num_slots):
        """Build and execute DB API transaction to add slots to a player.

        If player doesn't exist, build and execute DB API transaction to
        initialize the player row and then retrieve it.

        Args:
            player_id: Hashed player name.

        Returns:
            If successful: boolean True or a positive integer indicating the number
                of rows affected.
            If unsuccessful: boolean False or a zero-value integer.
            Note: Doesn't explicitly return latest player stats; if successful the
                updated results are available in the object's attributes.

        Raises:
            RuntimeError: There was an issue updating or retrieving the player's
                row.
        """

        transaction = []
        # Test that query generation is successful. Necessary as query generation
        # will fail if, for example, the player already has max slots
        updated_player = self.player
        updated_player['slots'] = self.player['slots'] + num_slots
        update_player_query = player.update(updated_player)
        if update_player_query:
            transaction.extend(update_player_query)
        else:
            logger.error(
                "Unable to update player! (continuing without update!)")

        # Get player after updating slots
        transaction.extend(player.get(self.player['id']))

        # Run transaction
        trans_id = str(uuid.uuid4())
        results = self._execute_db_transaction(trans_id, transaction)
        # Since a query that returns no rows will return a 0, explicitly check for
        # the False keyword value
        if results is False:
            raise RuntimeError("Unable to add slots to player %s!" % player['id'])
        return results
    def play_stage(self):
        """Build and execute DB API transaction to simulate player playing a stage.

        Note: Stamina is not currently validated.

        Returns:
            If successful: boolean True or a positive integer indicating the number
                of rows affected.
            If unsuccessful: boolean False or a zero-value integer.
            Note: Doesn't explicitly return latest cardlist/player stats;
                if successful the updated results are available in the
                object's attributes.

        Raises:
            RuntimeError: There was an issue with the database transaction
                required to evolve the card.
        """

        # Obviously this could be a call out to a key/value store to get a constantly
        # updating chance of drops
        loot_table = self.cfg['loot_tables']['std']  # Standard loot table
        num_rounds = 5  # rounds in this level

        transaction = []
        # Test to see if the player failed the stage
        if random.random() <= self.cfg['stage']['failure_chance']:

            # Roll for card drops
            for i in range(num_rounds):
                if (len(self.cards) + len(transaction)) < self.player['slots']:
                    #logger.debug(" Playing round %d" % i)
                    card_type = None
                    # Roll d100
                    roll = random.random()
                    if roll <= loot_table['drop_chance']:
                        # This can be replaced with a more advanced probabilistic function, just
                        # random for now
                        card_type = random.randint(loot_table['min'],
                                                   loot_table['max'])
                        transaction.extend(card.create(self.player['id'],
                                                       card_type))
                    loot_msg = " Round %2d: Rolled %.2f/%.2f for player %d, dropped card %s"
                    logger.info(loot_msg, i, roll, loot_table['drop_chance'],
                                            self.player['id'], str(card_type))
                else:
                    full_msg = "****Player (%d) doesn't have any more slots! Discarding remaining drops..."
                    logger.warning(full_msg, self.player['id'])
                    break
            logger.info(" Player completed stage - %2d loot cards acquired.",
                        len(transaction))

            # Assume player took a friend along, give them friend points
            updated_player = self.player.copy()
            updated_player['points'] = self.player['points'] + self.cfg[
                'stage']['points_per_run']
            # Test that query generation is successful. Necessary as query generation
            # will fail if, for example, the player already has max friend points
            update_player_query = player.update(updated_player)
            if update_player_query:
                transaction.extend(update_player_query)
            else:
                logger.error(
                    "Unable to update player! (continuing without update!)")

            # After updates, get the latest player/cardlist
            transaction.extend(player.get(self.player['id']))
            transaction.extend(card.get_all(self.player['id']))

            # Run transaction
            trans_id = str(uuid.uuid4())
            results = self._execute_db_transaction(trans_id, transaction)

            # Since a query that returns no rows will return a 0, explicitly check for
            # the False keyword value
            if results is False:
                raise RuntimeError("Unable to Play Stage for player %s!" %
                                   self.player['id'])
            return results
        else:
            logger.info("  Player failed stage!")
            return False