Chomp 游戏的算法

Posted

技术标签:

【中文标题】Chomp 游戏的算法【英文标题】:Algorithm for the game of Chomp 【发布时间】:2011-10-13 11:24:28 【问题描述】:

我正在为 Chomp 游戏编写程序。你可以在Wikipedia阅读游戏的描述,不过我还是会简单描述一下。

我们在一块尺寸为 n x m 的巧克力棒上玩耍,即巧克力棒被分成 n x m 个正方形。在每一回合,当前玩家选择一个方格并吃掉所选方格下方和右侧的所有东西。因此,例如,以下是有效的第一步:

目的是迫使你的对手吃掉最后一块巧克力(它是有毒的)。

关于 AI 部分,我使用了带有深度截断的极小极大算法。但是我想不出一个合适的位置评估函数。结果是,使用我的评估函数,人类玩家很容易战胜我的程序。

任何人都可以:

建议一个好的位置评估函数或 提供一些有用的参考或 建议替代算法?

【问题讨论】:

最好在:gamedev.stackexchange.com 【参考方案1】:

你的板子有多大?

如果您的棋盘相当小,那么您可以使用动态编程准确地解决游戏。在 Python 中:

n,m = 6,6
init = frozenset((x,y) for x in range(n) for y in range(m))

def moves(board):
    return [frozenset([(x,y) for (x,y) in board if x < px or y < py]) for (px,py) in board]

@memoize
def wins(board):
    if not board: return True
    return any(not wins(move) for move in moves(board))

wins(board) 函数计算棋盘是否为获胜位置。棋盘表示是一组元组 (x,y),指示棋子 (x,y) 是否仍在棋盘上。函数 move 计算一次可到达的棋盘列表。

wins 函数背后的逻辑是这样工作的。如果棋盘在我们移动时是空的,那么其他玩家一定吃掉了最后一块,所以我们赢了。如果棋盘不是空的,那么如果有any 移动我们可以赢,我们可以这样做,结果位置是一个失败的位置(即不赢,即not wins(move)),因为这样我们让另一个玩家进入了一个失败的位置.

您还需要缓存结果的 memoize 辅助函数:

def memoize(f):
    cache = dict()
    def memof(x):
        try: return cache[x]
        except:
            cache[x] = f(x)
            return cache[x]
    return memof

通过缓存,我们只计算给定位置的获胜者一次,即使该位置可以通过多种方式到达。例如,如果第一个玩家在第一个动作中吃掉所有剩余的巧克力排,则可以获得单排巧克力的位置,但也可以通过许多其他系列的动作来获得。一次又一次地计算谁在单排板上获胜是很浪费的,所以我们缓存了结果。这将渐近性能从O((n*m)^(n+m)) 提高到O((n+m)!/(n!m!)),这是一个巨大的改进,但对于大型电路板来说仍然很慢。

为了方便,这里有一个调试打印功能:

def show(board):
    for x in range(n):
        print '|' + ''.join('x ' if (x,y) in board else '  ' for y in range(m))

这段代码仍然相当慢,因为代码没有以任何方式优化(这是 Python ......)。如果你用 C 或 Java 高效地编写它,你可能可以将性能提高 100 倍以上。您应该能够轻松处理 10x10 板,并且您可能最多可以处理 15x15 板。您还应该使用不同的板表示,例如位板。如果您使用多个处理器,也许您甚至可以将其加速 1000 倍。

这是从极小极大的推导

我们将从极小极大开始:

def minimax(board, depth):
  if depth > maxdepth: return heuristic(board)
  else:
    alpha = -1
    for move in moves(board):
      alpha = max(alpha, -minimax(move, depth-1))
    return alpha

我们可以去掉深度检查来做一个完整的搜索:

def minimax(board):
  if game_ended(board): return heuristic(board)
  else:
    alpha = -1
    for move in moves(board):
      alpha = max(alpha, -minimax(move))
    return alpha

由于游戏结束,启发式将返回 -1 或 1,具体取决于哪个玩家获胜。如果我们将 -1 表示为 false,将 1 表示为 true,那么 max(a,b) 变为 a or b-a 变为 not a

def minimax(board):
  if game_ended(board): return heuristic(board)
  else:
    alpha = False
    for move in moves(board):
      alpha = alpha or not minimax(move)
    return alpha

你可以看到这相当于:

def minimax(board):
  if not board: return True
  return any([not minimax(move) for move in moves(board)])

如果我们从带有 alpha-beta 剪枝的 minimax 开始:

def alphabeta(board, alpha, beta):
  if game_ended(board): return heuristic(board)
  else:
    for move in moves(board):
      alpha = max(alpha, -alphabeta(move, -beta, -alpha))
      if alpha >= beta: break
    return alpha

// start the search:
alphabeta(initial_board, -1, 1)

搜索从 alpha = -1 和 beta = 1 开始。一旦 alpha 变为 1,循环就会中断。所以我们可以假设在递归调用中 alpha 保持 -1 而 beta 保持 1。所以代码等价于:

def alphabeta(board, alpha, beta):
  if game_ended(board): return heuristic(board)
  else:
    for move in moves(board):
      alpha = max(alpha, -alphabeta(move, -1, 1))
      if alpha == 1: break
    return alpha

// start the search:
alphabeta(initial_board, -1, 1)

所以我们可以简单地删除参数,因为它们总是作为相同的值传入:

def alphabeta(board):
  if game_ended(board): return heuristic(board)
  else:
    alpha = -1
    for move in moves(board):
      alpha = max(alpha, -alphabeta(move))
      if alpha == 1: break
    return alpha

// start the search:
alphabeta(initial_board)

我们可以再次从 -1 和 1 切换到布尔值:

def alphabeta(board):
  if game_ended(board): return heuristic(board)
  else:
    alpha = False
    for move in moves(board):
      alpha = alpha or not alphabeta(move))
      if alpha: break
    return alpha

所以你可以看到这等同于将 any 与生成器一起使用,生成器会在找到 True 值后立即停止迭代,而不是总是计算整个子列表:

def alphabeta(board):
  if not board: return True
  return any(not alphabeta(move) for move in moves(board))

请注意,这里我们使用any(not alphabeta(move) for move in moves(board)) 而不是any([not minimax(move) for move in moves(board)])。这加快了对合理尺寸电路板的搜索速度大约 10 倍。不是因为第一种形式更快,而是因为它允许我们在找到 True 值后立即跳过整个循环的其余部分,包括递归调用。

所以你有它,wins 功能只是变相的字母搜索。我们用于获胜的下一个技巧是记住它。在游戏编程中,这将被称为“转置表”。因此,wins 函数正在使用转置表进行字母搜索。当然,直接写下这个算法而不是通过这个推导更简单;)

【讨论】:

不错。真的让我想学习Python。在我看来,游戏主要归结为当只剩下几行和几列时你如何玩,尤其是最后一行和一列。如果解决整个棋盘需要太长时间,我建议只解决最终游戏案例。考虑到这个游戏的性质,你甚至可以让 AI 使用它的第一步将游戏移动到这个状态。我不相信这会损害它获胜的机会。 是的,这是一个绝妙的主意,尤其是当您与人类对战时。即使 AI 移动到的位置很可能是一个失败的位置(因为你从一开始就可以做出的几乎所有移动都是失败的),但人类肯定会犯错误,然后 AI 可以利用该错误来赢得比赛。跨度> 感谢您的精彩回答!如果我理解正确,您的建议相当于没有深度截断的极小值。但是,记忆当然是一个聪明的主意。也许我可以将字典缓存存储到文件中,以进一步加快速度。我用 Python 编程,但我不熟悉辅助函数。用户可以设置板尺寸,因此任何适合屏幕的尺寸都是可能的。我将尝试优化实现,对于大棋盘,使用 Jonathan 的随机游戏理念,直到游戏缩减为更小的棋盘。 是的,你说得对,类似于 minimax 是一种有效的查看方式;我添加了一个解释。【参考方案2】:

我认为这里不可能有一个好的位置评估功能,因为与国际象棋等游戏不同,除了输赢之外没有“进步”。***文章建议一个详尽的解决方案对于现代计算机是实用的,我想你会发现情况就是这样,只要有适当的记忆和优化。

您可能会感兴趣的相关游戏是Nim。

【讨论】:

以上是关于Chomp 游戏的算法的主要内容,如果未能解决你的问题,请参考以下文章

Chomp游戏(必胜策略分析)

ruby case option / gets.chomp游戏项目

网易游戏推荐算法工程师-「贵系内推|游戏篇」

允许测试人工智能算法的游戏[关闭]

模拟算法_掷骰子游戏&&猜数游戏

客户端-服务器游戏算法