我可以使用什么算法进行井字游戏来确定AI的“最佳动作”?

时间:2008-09-24 05:26:28

标签: algorithm artificial-intelligence tic-tac-toe

在一个井字游戏的实施中,我认为具有挑战性的部分是确定机器最佳动作。

可以采用哪些算法?我正在研究从简单到复杂的实现。我该如何处理这部分问题?

10 个答案:

答案 0 :(得分:55)

维基百科推出完美游戏(每次赢或平)的策略看似简单的伪代码:

  

引自Wikipedia (Tic Tac Toe#Strategy)

     

如果玩家在Newell和Simon的1972年抽签中选择了每个回合中的第一个可用移动,那么玩家可以玩完美的Tic-tac-toe游戏(赢得或者至少是抽奖) tac-toe计划。[6]

     
      
  1. 胜利:如果你连续两个,那就玩第三个连续三个。

  2.   
  3. 阻止:如果对手连续两个,则玩第三个阻挡他们。

  4.   
  5. Fork:创造一个可以通过两种方式获胜的机会。

  6.   
  7. Block Opponent's Fork:

         

    选项1:连续创建两个以强制执行   对手进入防守,只要多久   因为它不会导致他们创造   叉子或胜利。例如,如果“X”   有一个角落,“O”有中心,和   “X”也有相反的角落,   “O”一定不能发挥作用   赢得。 (在这里打一个角落   scenario为“X”创建一个fork   胜利。)

         

    选项2:如果有配置   对手可以分叉,阻挡   那个叉子。

  8.   
  9. 中心:扮演中心。

  10.   
  11. 对面角落:如果对手在角落,则对手   角。

  12.   
  13. 空角:空角落。

  14.   
  15. 空荡荡的一面:空洞的一面。

  16.   

认识到“叉子”情况的样子可以按照建议的蛮力方式完成。

注意:一个“完美”的对手是一个很好的运动,但最终不值得'玩'反对。但是,您可以改变上述优先级,为对手的性格提供特有的弱点。

答案 1 :(得分:37)

你需要什么(对于井口棋或像国际象棋这样更难的游戏)是minimax algorithm或其稍微复杂的变体alpha-beta pruning。普通的天真极小极大对于像tic-tac-toe这样小的搜索空间的游戏来说会很好。

简而言之,您想要做的不是寻找对您有最佳结果的举动,而是寻找尽可能好的最坏结果的举动。如果你认为你的对手正在以最佳状态进行比赛,那么你必须假设他们会采取对你来说最糟糕的举动,因此你必须采取最小化其最大增益的举动。

答案 2 :(得分:14)

生成每一块可能的电路板并根据它后来在树上生成的电路板对其进行评分的强力方法不需要太多记忆,特别是一旦你认识到90度电路板旋转是多余的,就像翻转一样垂直,水平和对角轴。

一旦达到这一点,树形图中的数据量就会少于1k来描述结果,因此是计算机的最佳移动。

- 亚当

答案 3 :(得分:7)

Tic-tac-toe的典型算法应如下所示:

Board:代表董事会的九元素向量。我们存储2(表示 空白),3(表示X)或5(表示O)。 转弯:表示即将播放的游戏移动的整数。 第一步将由1表示,最后由9表示。

算法

主算法使用三个函数。

Make2:如果电路板的中心方块为空,即board[5]=2,则返回5。否则,此函数将返回任何非角落方(2, 4, 6 or 8)

Posswin(p):如果玩家p无法在下次行动中获胜,则返回0;否则,它返回构成获胜动作的平方数。此功能将使程序既赢又赢得对手。此功能通过检查每个行,列和对角线来操作。通过将每个方块的值乘以整行(或列或对角线),可以检查获胜的可能性。如果产品为183 x 3 x 2),那么X就可以获胜。如果产品是505 x 5 x 2),则O可以获胜。如果找到获胜行(列或对角线),则可以确定其中的空白正方形,并且此函数返回该正方形的数量。

Go (n):在方块n中移动。如果Turn为奇数,此过程将板[n]设置为3,如果Turn为偶数,则将板X设置为5。它也会增加一个。

该算法针对每次移动都有内置策略。它使奇数编号 如果它播放Turn = 1 Go(1) (upper left corner). Turn = 2 If Board[5] is blank, Go(5), else Go(1). Turn = 3 If Board[9] is blank, Go(9), else Go(3). Turn = 4 If Posswin(X) is not 0, then Go(Posswin(X)) i.e. [ block opponent’s win], else Go(Make2). Turn = 5 if Posswin(X) is not 0 then Go(Posswin(X)) [i.e. win], else if Posswin(O) is not 0, then Go(Posswin(O)) [i.e. block win], else if Board[7] is blank, then Go(7), else Go(3). [to explore other possibility if there be any ]. Turn = 6 If Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else Go(Make2). Turn = 7 If Posswin(X) is not 0 then Go(Posswin(X)), else if Posswin(X) is not 0, then Go(Posswin(O)) else go anywhere that is blank. Turn = 8 if Posswin(O) is not 0 then Go(Posswin(O)), else if Posswin(X) is not 0, then Go(Posswin(X)), else go anywhere that is blank. Turn = 9 Same as Turn=7. 则移动,如果它播放O,则移动偶数移动。

{{1}}

我用过它。让我知道你们的感受。

答案 4 :(得分:6)

由于您只处理可能位置的3x3矩阵,因此只需编写搜索所有可能性而不会增加计算能力。对于每个开放空间,在标记该空间之后计算所有可能的结果(递归地,我会说),然后使用最有可能获胜的移动。

实际上,优化这将是浪费精力。虽然一些简单的可能是:

  • 首先检查可能的胜利 另一支队伍阻挡了第一支队伍 你找到了(如果有2个游戏 无论如何)。
  • 如果它打开,请始终占据中心位置 (以前的规则没有 候选)。
  • 在两侧前角(再次, 如果以前的规则是空的)

答案 5 :(得分:3)

你可以在一些示例游戏中让人工智能游戏来学习。使用有监督的学习算法,以帮助它。

答案 6 :(得分:3)

不使用游戏区域的尝试。

  1. 赢得(你的双倍)
  2. 如果没有,不要输(对手的双倍)
  3. 如果没有,你是否已经有一个分叉(有双倍)
  4. 如果没有,如果对手有叉子
    1. 在阻塞点搜索可能的双重和分叉(终极胜利)
    2. 如果不是在阻挡点搜索叉子(这会让对手失去最多的可能性)
    3. 如果不仅仅是阻止点(不会丢失)
  5. 如果没有搜索double和fork(终极胜利)
  6. 如果不是只搜索能给对手带来最大损失的叉子
  7. 如果不是只搜索双
  8. 如果不是死路,领带,随机。
  9. 如果不是(这意味着你的第一步)
    1. 如果这是游戏的第一步;
      1. 给予对手最大失败的可能性(算法只会导致对手有7个失分点的角落)
      2. 或者随意打破无聊。
    2. 如果这是游戏的第二次移动;
      1. 只找到不失分(给出更多选项)
      2. 或找到此列表中获胜机会最多的点(可能很无聊,因为它只会产生所有角落或相邻的角落或中心)
  10. 注意:当你有双倍和分叉时,检查你的双人是否给对手一个双倍。如果它给出,检查你的新强制点是否包含在你的叉子列表中。

答案 7 :(得分:0)

使用数字分数对每个方块进行排名。如果采用正方形,则转到下一个选择(按等级按降序排序)。你需要选择一个策略(首先是两个主要的策略,第二个是(我认为)第二个)。从技术上讲,您可以编写所有策略,然后随机选择一个。这将使一个不太可预测的对手。

答案 8 :(得分:0)

这个答案假设你理解为P1实现完美的算法,并讨论如何在对抗普通人类玩家的条件下获胜,谁会比其他人更常犯错误。

如果两名球员都发挥最佳状态,那么比赛当然应该以平局结束。在人类层面上,P1在角落里比赛会更频繁地产生胜利。无论出于何种心理原因,P2都会认为在中心打球并不重要,这对他们来说是不幸的,因为这是唯一没有为P1赢得胜利的回应。

如果P2 在中心正确阻挡,P1应该在相反的角落,因为无论出于何种心理原因,P2都会更喜欢角球的对称性,这又会产生一个失败的棋盘对他们来说。

对于P1可能进行的任何移动开始移动,如果两个玩家此后都以最佳方式进行游戏,则可以进行P2移动,这将为P1创造胜利。在这个意义上,P1可以在任何地方发挥。边缘移动是最弱的,因为对此移动的可能响应的最大部分产生了平局,但是仍然存在将为P1创造胜利的响应。

根据经验(更确切地说,有趣的是)最好的P1开始动作似乎是第一个角落,第二个中心和最后一个边缘。

您可以亲自或通过GUI添加的下一个挑战是不显示电路板。一个人绝对可以记住所有的状态,但是增加的挑战导致对对称板的偏好,这需要更少的记忆力,导致我在第一个分支中概述的错误。

我知道,我在派对上玩得很开心。

答案 9 :(得分:0)

这是一个解决方案,它考虑所有可能的移动,以及每次移动确定最佳移动的后果。

我们需要一个代表董事会的数据结构。我们将用二维数组代表电路板。外部数组表示整个板,内部数组表示一行。这是一块空板的状态。

_gameBoard: [
    [“”, “”, “”],
    [“”, “”, “”],
    [“”, “”, “”]
]

我们将使用' x'填充电路板。和' o'字符。

接下来,我们需要一个可以检查结果的函数。该函数将检查一系列字符。无论董事会状态如何,结果都是4个选项之一:不完整,玩家X获胜,玩家O获胜或平局。该函数应该返回哪个是电路板的状态。



const SYMBOLS = {
  x:'X',
  o:'O'
}
const RESULT = {
  incomplete: 0,
  playerXWon: SYMBOLS.x,
  playerOWon: SYMBOLS.o,
  tie: 3
}
  function getResult(board){
      // returns an object with the result

      let result = RESULT.incomplete
      if (moveCount(board)<5){
        {result}
      }

      function succession (line){
        return (line === symbol.repeat(3))
      }

      let line

      //first we check row, then column, then diagonal
      for (var i = 0 ; i<3 ; i++){
        line = board[i].join('')
        if(succession(line)){
          result = symbol;
          return {result};
        }
      }

      for (var j=0 ; j<3; j++){
        let column = [board[0][j],board[1][j],board[2][j]]
        line = column.join('')
        if(succession(line)){
          result = symbol
          return {result};
        }
      }

      let diag1 = [board[0][0],board[1][1],board[2][2]]
      line = diag1.join('')
      if(succession(line)){
        result = symbol
        return {result};
      }

      let diag2 = [board[0][2],board[1][1],board[2][0]]
      line = diag2.join('')
      if(succession(line)){
        result = symbol
        return {result};
      }

      //Check for tie
      if (moveCount(board)==9){
        result=RESULT.tie
        return {result}
      }

      return {result}
    }
&#13;
&#13;
&#13;

现在我们可以添加getBestMove函数,我们提供任何给定的板,并且下一个符号,该函数将使用getResult函数检查所有可能的移动。如果它是一个胜利,它将给它一个得分1.如果它松散它会得到-1的分数,一个平局将获得0分。如果它未确定我们将getBestMove函数递归到弄清楚下一步行动的得分。由于下一步是对手,他的胜利是当前球员的失利,而得分将被否定。最后,可能的移动得分为1,0或-1,我们可以对移动进行排序,并返回得分最高的移动。

&#13;
&#13;
  function getBestMove (board, symbol){

    function copyBoard(board) {
      let copy = []
       for (let row = 0 ; row<3 ; row++){
        copy.push([])
        for (let column = 0 ; column<3 ; column++){
          copy[row][column] = board[row][column]
        }
      }
      return copy
    }

    function getAvailableMoves (board) {
      let availableMoves = []
      for (let row = 0 ; row<3 ; row++){
        for (let column = 0 ; column<3 ; column++){
          if (board[row][column]===""){
            availableMoves.push({row, column})
          }
        }
      }
      return availableMoves
    }

    let availableMoves = getAvailableMoves(board)

    let availableMovesAndScores = []

    for (var i=0 ; i<availableMoves.length ; i++){
      let move = availableMoves[i]
      let newBoard = copyBoard(board)
      newBoard = applyMove(newBoard,move, symbol)
      result = getResult(newBoard,symbol).result
      let score
      if (result == RESULT.tie) {score = 0}
      else if (result == symbol) {
        score = 1
      }
      else {
        let otherSymbol = (symbol==SYMBOLS.x)? SYMBOLS.o : SYMBOLS.x
        nextMove = getBestMove(newBoard, otherSymbol)
        score = - (nextMove.score)
      }
      if(score === 1)
        return {move, score}
      availableMovesAndScores.push({move, score})
    }

    availableMovesAndScores.sort((moveA, moveB )=>{
        return moveB.score - moveA.score
      })
    return availableMovesAndScores[0]
  }
&#13;
&#13;
&#13;

Algorithm in actionGithubExplaining the process in more details