在井字棋实现我猜测,挑战性的部分是确定由机器打出了最好的举措。
什么是可以追求的算法? 我在寻找到从简单到复杂的实现。 我怎么会去解决这个问题的一部分?
在井字棋实现我猜测,挑战性的部分是确定由机器打出了最好的举措。
什么是可以追求的算法? 我在寻找到从简单到复杂的实现。 我怎么会去解决这个问题的一部分?
维基百科玩一场完美的比赛(赢或领带每次)的战略似乎是简单的伪代码:
引用自维基百科(井字#策略)
玩家可以玩井字棋的一场完美的比赛(赢或者,至少,画出),如果他们选择从下面的列表,每个回合第一个可用的举动,如在纽维尔使用和西蒙的1972年井字棋程序。[6]
WIN:如果连续有两个,打第三次拿到三连胜。
块:如果对手有两成一排,打第三阻止他们。
叉:创建一个机会,在这里你可以用两种方法来赢得。
阻止对手的叉:
选项1:连续创建两个对手迫使防守,只要它不创造他们叉子或获奖结果。 例如,如果“X”有一个角落,“O”的圆心,而“X”具有相反的角落里为好,“O”为了赢得一定不能玩一个角落。 (打在这种情况下一个角落里创造了“X”,赢得了叉。)
选项2:如果有一种配置,其中对手可以fork,阻止叉。
中心:打中锋。
对角:如果对手是在角落里,打对角。
空角:玩一个空的角落。
空方:玩的空方。
认识到什么是“叉”的情况看起来像一个强力的方式可以做到的建议。
注:一个“完美”的对手是一个很好的锻炼,但最终不值得“玩”反对。 你可以,但是,改变上述各优先给予特征的弱点对手的个性。
你需要什么(对于井字棋或一个更为艰难的比赛就像国际象棋)是极大极小算法 ,或其稍微复杂的变体, α-β剪枝 。 普通天真极大极小将与小井字棋搜索领域的游戏规则做精,虽然。
概括地说,你想要做的不是搜索具有最适合你的可能结果的举动,而是为了在最坏可能的结果是尽可能好招。 如果你认为你的对手是打最佳,你必须承担他们将采取最糟糕的是你中招了,所以你必须要最大限度地减少他们的最大增益的举动。
生成每一个可能的电路板,并根据其后来产生进一步下跌的树并不需要多大的内存,尤其是当你认识到90度的旋转板是多余的,因为是围绕垂直翻转板得分它的蛮力方法,水平,和对角轴。
一旦你到这一点,有一些像不到1K的数据在一个树形图来描述的结果,从而为电脑的最佳举措。
-亚当
对于井字棋典型的算法中应该是这样的:
板:表示板上的九元向量。 我们存储2(表示空白),3(指示X)或5(指示O)。 打开:一个整数,指示将要玩的游戏的哪些举动。 第1招将1来表示,最后由9。
算法
主要的算法使用三个功能。
Make2:返回5如果电路板的中心正方形是空白即,如果板[5] = 2。 否则,此函数返回的任何非角方(2,4,6或8)。
Posswin(P):返回值为0,玩家P不能在他的下一步行动赢; 否则返回构成一个成功的举动平方数。 此功能将使程序都取胜,以阻止对手获胜。 该功能通过检查每一行,列和对角线的动作。 通过对一整行(或列或对角线)每平方的值相乘,一赢的可能性可以被检查。 如果产品18(3×3×2),则X可以获胜。 如果产品50(5×5×2),然后与O可以获胜。 如果找到了赢球的行(列或对角线),在它的空白方能够确定和正方形的数量由这个函数返回。
去(N):使方n中的举动。 此过程集合板[n]至3如果打开是奇数,或5如果打开是偶数。 它还通过一个递增转。
该算法对每个移动内置的策略。 它使奇数移动,如果它扮演X,偶数的举动,如果它起着O.
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.
我已经用它。 让我知道你们的感受。
既然你只处理的可能位置的3x3矩阵,这将会是很容易只写通过所有可能的搜索,而无需占用你的计算能力。 对于每一个开放的空间,通过计算后标志着空间(递归,我会说),然后使用以质取胜的最可能的举动所有可能的结果。
优化这将是徒劳无功的,真的。 虽然有些难办可能是:
您可以让AI发挥自身在一些样本游戏学习的榜样。 使用监督学习算法,以帮助它一起。
不使用游戏区的尝试。
注意:当你有双和叉子,检查你的双给对手double.if它给,检查你的新的强制性点包括在你的叉子清单。
排名每个数字分值的平方。 如果一个正方形取,移动到下一个选择(排序按排名降序排列)。 你将需要选择一个策略(有换去第一和三个(我认为)的第二两个主要的)。 从技术上讲,你可以只编写所有的策略,然后选择一个随机的。 这将使一个难以预料的对手。
这个答案假定您了解实现的P1完美的算法,并讨论了如何实现对普通的人类的球员,谁就会犯一些错误更普遍比其他条件的胜利。
当然,游戏应该以平局结束时,如果两个球员发挥最佳。 在一个人的水平,P1在一个角落里玩更经常产生获胜。 无论出于何种原因,心理,P2为诱饵,以为在玩心不那么重要,这是不幸的他们,因为它不创造P1游戏胜出的唯一回应。
如果P2 不正确的中心块,P1应该起到相反的角落里,因为再次,无论出于何种心理原因,P2会喜欢玩一个角落,这又产生一个失败的主板为他们的对称性。
对于任何举动P1可就起步之举,有一招P2可能会做出将创建P1的胜利,如果双方球员此后发挥最佳。 在这个意义上P1可以玩的地方。 边缘举动是在这个意义上,可能的反应,此举动的最大部分产生平局最弱的,但仍存在,这将创造P1一场胜利回应。
经验(更准确地说,闲谈)的最佳起点P1移动似乎是第一个弯道,第二中心,和最后一条边。
您可以添加,亲自或通过GUI的下一个挑战,是不显示板。 一个人绝对可以记住所有的状态,但更大的挑战导致对称板,它需要较少的努力,记住的偏好,导致我在第一个分支中列出的错误。
我有很多在聚会的乐趣,我知道了。
下面是考虑所有可能的移动解决方案,以及各自的后果移动以确定最佳的举措。
我们需要一个代表线路板数据结构的研究。 我们将代表董事会二维数组。 外阵列表示整个板,和一个内阵列表示一个行。 下面是一个空板的状态。
_gameBoard: [
[“”, “”, “”],
[“”, “”, “”],
[“”, “”, “”]
]
我们将填充板用“X”和“O”字符。
下一步,我们将需要一个可检查的结果的功能。 该功能将检查字符的继承。 什么都板的状态,其结果是4个选项:要么不完整,播放器X韩元,球员Ø赢或平局。 函数应该返回这是董事会的状态。
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} }
现在我们可以添加getBestMove功能,我们提供任何给定板,和下一个符号,该函数将运行检查与功能的getResult所有可能的行动。 如果它是一个双赢它会给它一个得分1.如果它是一个松散的,将得到-1分,平局一定的分值为0,如果它是不确定的,我们将在getBestMove功能递归找出得分的下一步行动。 由于下一个动作是oponent的,他的胜利是失去当前玩家,比分将被否定。 在最后可能的移动接收得分要么1,0或-1,我们可以把移动进行排序,并返回得分最高的举动。
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] }
算法在行动 , Github上 , 在讲解过程中更多的细节