Chomp游戏的算法

Gia*_*nio 10 algorithm heuristics minimax

我正在为Chomp游戏编写一个程序.你可以在维基百科上阅读游戏的描述,但无论如何我都会简要描述一下.

我们在尺寸为nxm的巧克力棒上玩,即酒吧分为nxm正方形.在每个回合中,当前玩家选择一个正方形并吃掉所选正方形下方和右侧的所有内容.因此,例如,以下是有效的第一步:

在此输入图像描述

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

关于AI部分,我使用了具有深度截断的minimax算法.但是我无法想出合适的位置评估功能.结果是,通过我的评估功能,人类玩家很容易赢得我的计划.

谁能:

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

Jul*_*les 9

你的电路板有多大?

如果您的电路板相当小,那么您可以通过动态编程完全解决游戏问题.在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))
Run Code Online (Sandbox Code Playgroud)

函数wins(board)计算董事会是否为获胜职位.电路板表示是一组元组(x,y),表示片(x,y)是否仍在电路板上.函数移动计算一次移动可到达的板的列表.

wins函数背后的逻辑就像这样.如果我们的移动板是空的,那么另一个玩家必须吃掉最后一块,所以我们赢了.如果董事会不是空的那么我们可以赢,如果any我们可以做的移动使得最终的位置是一个失败的位置(即没有获胜ie not wins(move)),因为那时我们让另一个玩家陷入失败的位置.

您还需要memoize helper函数来缓存结果:

def memoize(f):
    cache = dict()
    def memof(x):
        try: return cache[x]
        except:
            cache[x] = f(x)
            return cache[x]
    return memof
Run Code Online (Sandbox Code Playgroud)

通过缓存,我们只计算一次给定位置的赢家,即使此位置可以多种方式到达.例如,如果第一个玩家在他的第一个动作中吃掉所有剩余的行,则可以获得单行巧克力的位置,但是也可以通过许多其他一系列动作获得.计算一次又一次地在单行板上获胜的人是浪费的,所以我们缓存结果.这提高了从东西渐近性能像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))
Run Code Online (Sandbox Code Playgroud)

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

这是minimax的推导

我们将从minimax开始:

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
Run Code Online (Sandbox Code Playgroud)

我们可以删除深度检查以进行完整搜索:

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
Run Code Online (Sandbox Code Playgroud)

因为游戏结束,启发式将返回-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
Run Code Online (Sandbox Code Playgroud)

你可以看到这相当于:

def minimax(board):
  if not board: return True
  return any([not minimax(move) for move in moves(board)])
Run Code Online (Sandbox Code Playgroud)

如果我们开始使用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)
Run Code Online (Sandbox Code Playgroud)

搜索以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)
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

我们可以再次从-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
Run Code Online (Sandbox Code Playgroud)

所以你可以看到这相当于使用任何一个生成器,它一旦找到True值就停止迭代,而不是总是计算整个子列表:

def alphabeta(board):
  if not board: return True
  return any(not alphabeta(move) for move in moves(board))
Run Code Online (Sandbox Code Playgroud)

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

所以你有它,胜利功能只是伪装的字母搜索.我们用于获胜的下一个技巧是记住它.在游戏编程中,这将使用"转置表"来调用.所以wins函数正在使用转置表进行字母搜索.当然,直接写下这个算法更简单,而不是通过这个推导;)