文章 cheng yu · 1 小时 前 8m read

让我们与机器抗争吧!

https://media.giphy.com/media/Nxu57gIbNuYOQ/giphy.gif

别急,别急,我可不是在鼓吹那种科幻片里常见的“反机器战争”,为了阻止奥创或天网统治世界。 还没到那一步,还没到那一步 🤔

我邀请大家通过使用 ObjectScript 并嵌入 Python 来制作一款非常简单的游戏,以此向机器发起挑战。

我必须说,InterSystems IRIS 上的“嵌入式 Python”功能让我超级兴奋,它为创建精彩的应用打开了无数可能性,简直令人难以置信。

让我们来制作一个井字棋游戏吧,规则非常简单,我相信大家都知道怎么玩。

在孩子们还没有手机或平板电脑的年代,正是这个游戏让我在童年时期与家人长途驾车旅行时免于无聊——没有什么比在模糊的车窗玻璃上挑战兄弟姐妹来几局更让人乐在其中的了。

所以,系好安全带,我们开始吧!

规则

如前所述,规则非常简单:

  • 每局仅限2名玩家
  • 在3×3的网格上轮流下棋
  • 人类玩家始终代表字母X,电脑代表字母O
  • 玩家只能将字母填入空格中
  • 最先在水平、垂直或对角线上连成3个相同字母的一方获胜
  • 当9个空格全部被填满时,即为平局,比赛结束

https://media4.giphy.com/media/3oriNKQe0D6uQVjcIM/giphy.gif?cid=790b761123702fb0ddd8e14b01746685cc0059bac0bc66e9&rid=giphy.gif&ct=g

所有机制和规则都将用ObjectScript编写,而电脑玩家的机制将用Python编写。

让我们动手实践吧

我们将通过一个全局变量来控制棋盘,其中每一行对应一个节点,每一列对应一枚棋子。

我们的第一步是初始化棋盘,为了方便起见,我将直接在全局变量中预先设置好节点(A、B和C三行)以及3个棋子:

/// Iniciate a New Game
ClassMethod NewGame() As %Status
{
  Set sc = $$$OK
  Kill ^TicTacToe
  Set ^TicTacToe("A") = "^^"
  Set ^TicTacToe("B") = "^^"
  Set ^TicTacToe("C") = "^^"
  Return sc
}

此时,我们将创建一个方法来将字母填入空格中,为此,每位玩家将指定棋盘上空格的位置。

每行对应一个字母,每列对应一个数字;例如,要将字母 X 放置在中间位置,我们就向该方法传递 B2 和字母 X。

ClassMethod MakeMove(move As %String, player As %String) As %Boolean
{
  Set $Piece(^TicTacToe($Extract(move,1,1)),"^",$Extract(move,2,2)) = player
}

让我们验证坐标是否有效,我认为最简单的方法是使用正则表达式:

ClassMethod CheckMoveIsValid(move As %String) As %Boolean
{
  Set regex = ##class(%Regex.Matcher).%New("(A|B|C){1}[0-9]{1}")
  Set regex.Text = $ZCONVERT(move,"U")
  Return regex.Locate()
}

我们需要确保所选区域为空

ClassMethod IsSpaceFree(move As %String) As %Boolean
{
  Quit ($Piece(^TicTacToe($Extract(move,1,1)),"^",$Extract(move,2,2)) = "")
}

太棒了!

现在让我们检查是否有玩家赢下这一局,或者比赛是否已经结束,为此我们来创建一个名为 CheckGameResult 的方法。

首先,我们检查是否有玩家通过横向连线获胜,我们将使用一个包含各行的列表,并通过简单的 $Find 方法来解决

    Set lines = $ListBuild("A","B","C")
    // Check Horizontal
    For i = 1:1:3 {
      Set line = ^TicTacToe($List(lines, i))
      If (($Find(line,"X^X^X")>0)||($Find(line,"O^O^O")>0)) {
        Return $Piece(^TicTacToe($List(lines, i)),"^", 1)_" won"
      }
    }

再用另一个For循环检查竖向情况

For j = 1:1:3 {
      If (($Piece(^TicTacToe($List(lines, 1)),"^",j)'="") &&
        ($Piece(^TicTacToe($List(lines, 1)),"^",j)=$Piece(^TicTacToe($List(lines, 2)),"^",j)) &&
        ($Piece(^TicTacToe($List(lines, 2)),"^",j)=$Piece(^TicTacToe($List(lines, 3)),"^",j))) {
        Return $Piece(^TicTacToe($List(lines, 1)),"^",j)_" won"
      }
    }

检查对角线:

    If (($Piece(^TicTacToe($List(lines, 2)),"^",2)'="") &&
      (
        (($Piece(^TicTacToe($List(lines, 1)),"^",1)=$Piece(^TicTacToe($List(lines, 2)),"^",2)) &&
          ($Piece(^TicTacToe($List(lines, 2)),"^",2)=$Piece(^TicTacToe($List(lines, 3)),"^",3)))||
        (($Piece(^TicTacToe($List(lines, 1)),"^",3)=$Piece(^TicTacToe($List(lines, 2)),"^",2)) &&
        ($Piece(^TicTacToe($List(lines, 2)),"^",2)=$Piece(^TicTacToe($List(lines, 3)),"^",1)))
      )) {
      Return ..WhoWon($Piece(^TicTacToe($List(lines, 2)),"^",2))
    }

最后,我们检查是否出现平局

    Set gameStatus = ""
    For i = 1:1:3 {
      For j = 1:1:3 {
        Set:($Piece(^TicTacToe($List(lines, i)),"^",j)="") gameStatus = "Not Done"
      }
    }
    Set:(gameStatus = "") gameStatus = "Draw"

太棒了!

是时候组装这台机器了

让我们来创建对手吧,我们需要设计一个算法,能够计算出所有可行的走法,并利用某种评估标准来判断哪一步是最佳走法。

理想的做法是使用一种名为MiniMax的决策算法(维基百科:MiniMax)

https://media3.giphy.com/media/WhTC5v5qQP4yAUvGKz/giphy.gif?cid=ecf05e47cx92yiew8vsig62tjq738xf7hfde0a2ygyfdl0xt&rid=giphy.gif&ct=g

MiniMax算法是博弈论、决策理论和人工智能中使用的一种决策规则。

基本上,我们需要在预判对手可能采取的行动的基础上掌握如何下棋,并争取获得最佳局面。

具体来说,我们以当前局面为起点,递归地检查每位玩家行动后的结果:若计算机获胜,则计分+1;若失败,则计分-1;若打平,则计分0。

如果游戏尚未结束,我们就根据当前游戏状态打开另一棵决策树。随后,我们找出对计算机而言价值最大、对对手而言价值最小的走法。

参见下图,共有3种可选走法:B2、C1和C3。

如果选择 C1 或 C3,对手在下回合有机会获胜;但如果选择 B2,无论对手选择哪一步,机器都将赢得这局。

minimaxp

这就像是手握时间之石,试图寻找最佳的时间线。

https://pa1.narvii.com/7398/463c11d54d8203aac94cda3c906c40efccf5fd77r1-460-184_hq.gif

转换为 Python

ClassMethod ComputerMove() As %String [ Language = python ]
{
  import iris
  from math import inf as infinity
  computerLetter = "O"
  playerLetter = "X"

  def isBoardFull(board):
    for i in range(0, 8):
      if isSpaceFree(board, i):
        return False
    return True

  def makeMove(board, letter, move):
    board[move] = letter

  def isWinner(brd, let):
    # check horizontals
    if ((brd[0] == brd[1] == brd[2] == let) or \
      (brd[3] == brd[4] == brd[5] == let) or \
      (brd[6] == brd[7] == brd[8] == let)):
        return True
    # check verticals
    if ((brd[0] == brd[3] == brd[6] == let) or \
        (brd[1] == brd[4] == brd[7] == let) or \
        (brd[2] == brd[5] == brd[8] == let)):
        return True
    # check diagonals
    if ((brd[0] == brd[4] == brd[8] == let) or \
        (brd[2] == brd[4] == brd[6] == let)):
        return True
    return False

  def isSpaceFree(board, move):
    #Retorna true se o espaco solicitado esta livre no quadro
    if(board[move] == ''):
      return True
    else:
      return False

  def copyGameState(board):
    dupeBoard = []
    for i in board:
      dupeBoard.append(i)
    return dupeBoard

  def getBestMove(state, player):
    done = "Done" if isBoardFull(state) else ""
    if done == "Done" and isWinner(state, computerLetter): # If Computer won
      return 1
    elif done == "Done" and isWinner(state, playerLetter): # If Human won
      return -1
    elif done == "Done":    # Draw condition
      return 0

    # Minimax Algorithm
    moves = []
    empty_cells = []
    for i in range(0,9):
      if state[i] == '':
        empty_cells.append(i)

    for empty_cell in empty_cells:
      move = {}
      move['index'] = empty_cell
      new_state = copyGameState(state)
      makeMove(new_state, player, empty_cell)

      if player == computerLetter:
          result = getBestMove(new_state, playerLetter)
          move['score'] = result
      else:
          result = getBestMove(new_state, computerLetter)
          move['score'] = result

      moves.append(move)

    # Find best move
    best_move = None
    if player == computerLetter:
        best = -infinity
        for move in moves:
            if move['score'] > best:
                best = move['score']
                best_move = move['index']
    else:
        best = infinity
        for move in moves:
            if move['score'] < best:
                best = move['score']
                best_move = move['index']

    return best_move

  lines = ['A', 'B', 'C']
  game = []
  current_game_state = iris.gref("^TicTacToe")

  for line in lines:
    for cell in current_game_state[line].split("^"):
      game.append(cell)

  cellNumber = getBestMove(game, computerLetter)
  next_move = lines[int(cellNumber/3)]+ str(int(cellNumber%3)+1)
  return next_move
}

首先,我将全局数据转换为一个简单的数组,忽略行和列,将其扁平化以便于处理。

在分析每一步时,我们会调用 copyGameState 方法,顾名思义,该方法会复制当前时刻的游戏状态,随后我们在此状态上应用迷你最大法(MiniMax)。

getBestMove 方法将通过递归调用,直至游戏结束并确定胜者或平局。

首先对空格进行映射,并交替验证双方玩家的每一步棋的结果。

结果将存储在 move['score'] 中,以便在检查所有可能性后,找出最佳走法。

希望你玩得开心,可以通过使用阿尔法-贝塔剪枝(维基百科:AlphaBeta Pruning)或神经网络等算法来提升智能水平,只是要小心别让“天网”复活了。

https://media4.giphy.com/media/mBpthYTk5rfbZvdtIy/giphy.gif?cid=790b761181bf3c36d85a50b84ced8ac3c6c937987b7b0516&rid=giphy.gif&ct=g

欢迎随时留言或提问。

各位,就到这里啦

完整源代码: InterSystems Iris 2021.1.0PYTHON 版本