【问题标题】:How to keep track of players' rankings?如何跟踪玩家的排名?
【发布时间】:2016-12-21 19:53:29
【问题描述】:

我有一个带有score 属性的Player 类:

class Player(game_engine.Player):

    def __init__(self, id):
        super().__init__(id)
        self.score = 0

随着玩家成功/失败完成目标,该分数会增加/减少。现在我需要告诉玩家他在玩家总数中的排名,比如

print('Your rank is {0} out of {1}')

首先我想有一个所有玩家的列表,以及每当玩家发生任何事情时:

  1. 我检查他的分数是增加还是减少
  2. 在列表中找到他
  3. 移动他,直到他的分数在正确的位置

但这会非常慢。可能有数十万玩家,玩家可以将自己的分数重置为0,这意味着我必须将堆栈中的每个人都移到他之后。即使找到玩家也是 O(n)。

我正在寻找的是一种高性能的解决方案。尽管应该使用常识,但 RAM 的使用并不那么重要。我怎样才能将系统改进得更快?

更新信息:每次玩家离开游戏服务器时,我都会使用 SQLAlchemy 将玩家的数据存储到 MySQL 数据库中,并在每次他加入服务器时加载它。这些是通过'player_join''player_leave' 事件处理的:

@Event('player_join')
def load_player(id):
    """Load player into the global players dict."""
    session = Session()
    query = session.query(Player).filter_by(id=id)
    players[id] = query.one_or_none() or Player(id=id)

@Event('player_leave')
def save_player(id):
    """Save player into the database."""
    session = Session()
    session.add(players[id])
    session.commit()

此外,玩家的分数会根据'player_kill' 事件更新:

@Event('player_kill')
def update_score(id, target_id):
    """Update players' scores upon a kill."""
    players[id].score += 2
    players[target_id].score -= 2

【问题讨论】:

  • 您使用哪个数据库?
  • @r-m-n 我正在使用 MySQL
  • 在某些数据库中可以使用 DENSE_RANK 窗口函数来完成,但 MySQL 不支持此函数。你可以试试这样dukesoftware00.blogspot.ru/2012/11/…
  • 我已经使用 redis 排序集处理了这个问题 - 例如,有一些简单的命令可以获取特定键的等级,并且 redis 将比您尝试构建自己的任何东西都快得多,而无需大量投资。
  • 你可以有一个每小时的任务来制作一个排序的分数列表,这将允许你快速确定当前的排名,比如缓存列表上的 bisect.bisect_left。分数列表不需要完整——也许保留每 1000 个分数。您可以去 db 获取实际顶级玩家的准确排名。

标签: python algorithm python-3.x ranking rank


【解决方案1】:

Redis 排序集有助于解决这种确切情况(文档使用排行榜作为示例用法)http://redis.io/topics/data-types-intro#redis-sorted-sets

  • 您关心的关键命令是 ZADD(更新玩家排名)和 ZRANK(获取特定玩家的排名)。这两种操作都是 O(log(N)) 复杂度。

Redis 可以作为玩家排名的缓存。当您的应用程序启动时,从 SQL 数据填充 redis。在mysql中更新玩家分数的时候也会更新redis。

如果您有多个服务器进程/线程并且它们可以同时触发玩家分数更新,那么您还应该考虑 mysql/redis 更新竞争条件,例如:

  • 仅从数据库触发器更新redis;或
  • 序列化玩家分数更新;或
  • 让数据暂时不同步,并在延迟后进行另一次缓存更新;或
  • 让数据暂时不同步,并以固定的时间间隔进行完整的缓存重建

【讨论】:

    【解决方案2】:

    您遇到的问题是您想要对数据库进行实时更新,这每次都需要一个数据库查询。如果你在内存中维护一个分数列表,并以更合理的频率更新它(比如一小时一次,甚至一分钟一次,如果你的玩家真的关心他们的排名),那么玩家仍然会体验到真实的——时间进度与分数排名,他们无法真正判断更新是否存在短暂的滞后。

    通过内存中得分的排序列表,您可以立即获得玩家的排名(我的意思是立即在内存中查找 O(lg n)),代价是缓存的内存,当然还有时间需要时更新缓存。与每次有人想要查看他们的排名时对 100k 条记录进行数据库查询相比,这是一个更好的选择。

    详述排序列表,你必须查询db才能得到它,但你可以继续使用它一段时间。也许您存储 last_update,并且仅当此列表“太旧”时才重新查询数据库。因此,您无需一直尝试更新即可快速更新,而只是感觉像实时一样。

    为了几乎立即找到某人的排名,您使用 bisect 模块,该模块支持在排序列表中进行二进制搜索。获得分数时会对其进行排序。

    from bisect import bisect_left
    
    # suppose scores are 1 through 10
    scores = range(1, 11)
    
    # get the insertion index for score 7
    # subtract it from len(scores) because bisect expects ascending sort
    # but you want a descending rank
    print len(scores) - bisect_left(scores, 7)
    

    这表示 7 分是排名 4,这是正确的。

    【讨论】:

    • "With a sorted list of scores in memory, you can instantly get the player's rank (where by instantly, I mean O(lg n) lookup in memory) at the cost of the memory to cache" 这几乎是我主要帖子中的问题(不是数据库问题,我在帖子中几乎没有提到数据库)。如何在保持排序的同时快速更新和查找列表?
    • 我更新了该部分的示例代码。如果缓存不是 100% 最新的数据库信息是一个破坏者,请告诉我,所以我可以删除它。我不确定这一点,这就是为什么我之前只是将其作为评论发布。
    【解决方案3】:

    可以使用 SQLAlchemy 的 sort_by 函数提取此类信息。如果您执行如下查询:

    leaderboard = session.query(Player).order_by(Player.score).all()
    

    您将获得按得分排序的玩家列表。请记住,每次执行此操作时,您都会对数据库执行 I/O,这可能会相当慢,而不是保存数据 python 变量。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-15
      • 2017-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多