问题描述
我正在尝试通过这里的代码在 javscript 中自学 minimax:https://github.com/beaucarnes/fcc-project-tutorials/blob/master/tictactoe/7/script.js
和视频:https://youtu.be/P2TcQ3h0ipQ?t=2334
这是函数:
function minimax(newBoard,player) {
var availSpots = emptySquares();
if (checkWin(newBoard,huPlayer)) {
return {score: -10};
} else if (checkWin(newBoard,aiPlayer)) {
return {score: 10};
} else if (availSpots.length === 0) {
return {score: 0};
}
var moves = [];
for (var i = 0; i < availSpots.length; i++) {
var move = {};
move.index = newBoard[availSpots[i]];
newBoard[availSpots[i]] = player;
if (player == aiPlayer) {
var result = minimax(newBoard,huPlayer);
move.score = result.score;
} else {
var result = minimax(newBoard,aiPlayer);
move.score = result.score;
}
newBoard[availSpots[i]] = move.index;
moves.push(move);
}
var bestMove;
if(player === aiPlayer) {
var bestscore = -10000;
for(var i = 0; i < moves.length; i++) {
if (moves[i].score > bestscore) {
bestscore = moves[i].score;
bestMove = i;
}
}
} else {
var bestscore = 10000;
for(var i = 0; i < moves.length; i++) {
if (moves[i].score < bestscore) {
bestscore = moves[i].score;
bestMove = i;
}
}
}
return moves[bestMove];
}
我想我理解其中的大部分内容,但有一些空白阻碍了我完全理解它。
据我所知,minimax(newBoard,player)
首先获取可以进行移动的位置,并建立一种对最终结果进行排名的方法。
然后它创建一个数组 moves
,名为 move
的对象将进入该数组。 for 循环为每个可用位置获取一个 move
对象。
每个 move
对象通过 move.index = newBoard[availSpots[i]];
获得一个名为 index 的属性
-
newBoard[availSpots[i]]
是否只是表示 可用的种类。说在一个有 可用点[4,5,6]
,newBoard[availSpots[0]]
是4
和 因此第一个移动对象的索引属性为 4?代码的新部分是
newBoard[availSpots[i]] = player
-- 是 这意味着玩家的图标被标记在newBoard[4]
?
之后,有一个 if else
语句将 .score
属性添加到 move
对象。
- 但后来我看到了
newBoard[availSpots[i]] = move.index
,它反过来了 我们之前所做的 - 为什么会这样?
然后将最新的走法推入 moves
数组,并从数组中循环遍历分数以找到 best move
的走法。
我很难看到这一切是如何运作的。我尝试放入 console.log 并且我的 repl.it 失败了...
- 是不是因为编译器正在尝试数十种排列 将它们全部记录下来会很丑吗?多少个动作 电脑要试试?
最后:
我在网上浏览了大量 minimax 资源,所以我希望有人能提供帮助——他们似乎都掩盖了这一点。我看过:
https://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-1-introduction/
https://learnersbucket.com/tutorials/js-projects/tic-tac-toe-game-in-javascript-with-bot/
https://steveafrost.com/articles/discovering-the-minimax-algorithm/
谢谢!
解决方法
让我们从您的最后一个问题开始。
既然是回合制游戏,那么代码中的电脑在哪里“玩”另一边以获得终端值?
不是,不在这段代码中。此函数有一个目的,即使用极小极大算法选择最佳移动。它通过操作棋盘对象,设置值并重置它们,找到位置的分数来实现。其他代码必须处理 IO。因此请务必记住,此代码只是整个井字游戏系统的一部分。
这种理解可能有助于澄清一些其他问题。
说在有可用位置 [4,5,6]
的棋盘中,newBoard[availSpots[0]]
是 4
并且因此第一个移动对象的索引属性为 4 是否正确?
虽然您可以这样想,但这段代码中没有任何内容来描述如何表示板。因此,虽然它可能是正方形 1
- 9
,但它不一定是。它们可以是 a1
、a2
、a3
、b1
、b2
、b3
、c1
、c2
,c3
。或者完全可以有另一种表示。我们所知道的是,板子有一些我们可以用 []
(数字、字符串,甚至可能是符号)引用的属性,而 availSpots
是这些值的数组。很明显,它代表了可用的那些。
代码的新部分是 newBoard[availSpots[i]] = player
-- 这是否意味着玩家的图标被标记在 newBoard[4]
中?
这意味着棋盘的内部数据结构现在可以识别具有给定单元格的玩家。同样,此代码仅用于选择最佳移动。但它对图标或板的可见表示一无所知。请注意,董事会的这种状态是暂时的;棋盘的操纵只是为了帮助计算最佳移动。其他代码实际上会将它计算的移动应用到正在进行的棋盘上。
之后,有一个 if else
语句将 .score
属性添加到 move
对象。
但后来我看到了 newBoard[availSpots[i]] = move.index
,这与我们之前所做的相反——这是为什么?
它正在测试棋盘,逐个可用的移动,以找到最好的。它通过移动,计算结果分数,然后重置该移动以尝试不同的移动来做到这一点。在计算走法时,我们可能会递归调用 minmax
,然后它会依次尝试它的 走法,这可以扩展多达九层深,棋盘上的每个单元格一层.
因此如果当前板看起来像
X O O
4 5 6
X O X
我们会得到以下分析:
max min max
X O O
4 5 6
X O X
| X O O
+--> X 5 6 (score 10)
| X O X
|
| X O O
+--> 4 X 6 (score 10)
| X O X
|
| X O O
+--> 4 5 X
X O X
| X O O
+--> O 5 X
| X O X
| | X O O
| +--> O X O (score 10)
| X O X
|
+--> X O O
4 O X (score -10)
X O X
我们尝试4
的可用移动X
,发现它的分数是+10
,然后将4
重置为默认值,所以我们可以为 5
尝试 X
,得分也为 10
。我们再次将 5
重置为其默认值。然后我们尝试 6
为 X
。为了得分,我们必须更深入,首先尝试 4
为 O
。这需要我们更深入,我们将 5
设置为 X
。这具有价值 +10
。我们重置它,重置 4
并使用 5
重试 O
,其得分为 -10
。跟随minimax,我们可以发现X O O / 4 5 X / X O X
的值为-10
,我们已经看到X O O / X 5 6 / X O X
和X O O / 4 X 6 / X O X
的得分都是+10
,所以我们会选择其中之一。 (通过这个算法,第一个,但更有趣的算法可能会在同样好的动作中随机选择。)
我尝试放入一个 console.log 并且我的 repl.it 失败了...
是不是因为编译器正在尝试数十种排列并且将它们全部记录下来会很丑陋?计算机需要尝试多少个动作?
我们必须看看你做了什么来测试这个,但不,这个游戏很简单,你应该永远不会用完这些计算中的任何资源。总游戏数少于 9!
-- 即 362880
--。所以我猜你没有正确登录。
在这部分代码中:
var moves = [];
for (var i = 0; i < availSpots.length; i++) {
var move = {};
move.index = newBoard[availSpots[i]];
newBoard[availSpots[i]] = player;
if (player == aiPlayer) {
var result = minimax(newBoard,huPlayer);
move.score = result.score;
} else {
var result = minimax(newBoard,aiPlayer);
move.score = result.score;
}
newBoard[availSpots[i]] = move.index;
moves.push(move);
}
从您绘制的大地图中跟进:
我们得到一个名为 moves
的数组,然后从 availSpots
中的第一个条目 [4,6]
开始,我们创建一个名为 move
的对象
move.index = newBoard[availSpots[i]];
表示 move
的索引属性为 4。
所以我可以设想:var move = {index:4}
然后我们有:newBoard[availSpots[i]] = player
这意味着newBoard = X,O,X,6,X
然后在 if
语句之后,我们得到 var move = {index:4,score:10};
最后,newBoard[availSpots[i]] = move.index
表示返回原始状态:
newBoard = X,4,X
并在按下 moves.push(move)
时:
我们得到 moves = [move {index:4 score:10}};]
当 for
循环中的 i = 1 时,我们最终得到
moves = [move {index:4 score:10}},move {index:5 score:10}};]
但我的理解在这里崩溃了。如果 i = 2,var move = {index:6}
那么,newBoard = X,X
然而,var move = {index:6}
不会产生一个终端值,那么代码从现在开始做什么?
代码告诉我们:
if (player == aiPlayer) {
var result = minimax(newBoard,aiPlayer);
move.score = result.score;
}
但此时没有分数,因为没有最终值。我知道必须执行 else
语句,因为 newBoard
还没有进入原始状态 newBoard = X,X
,这还不会执行:newBoard[availSpots[i]] = move.index
所以在newBoard = X,X
上调用了minimax,并且有一个新的availSpots
,即[4,5]
。这次 player
是 O,因为在代码的其他地方,player
在检查板后发生了变化,所以 newBoard = X,X
。
newBoard = X,X
也没有终止值,因此该过程再次从 if
语句开始。
- 是否公平地说只有有分数才能下线
minimax
被执行,move.score = result.score
并且代码可以 继续将move
推送到数组?
这里有很多未说出口的交错,这让我很难理清头绪。
在初始点,当棋盘有 3 个可用空间时,不仅两个点(4 和 5)会有终值,而且要获得点 6 的终值,代码必须计算最佳在 2 个可用空间的数组中进行选择。
达到最终值所需的时间越长,路径数就越多。在为您找到最佳走法之前,代码必须为所有其他选择计算出最佳走法。
-
第一个动作后,我猜代码要经过8!或 40320 游戏,我想它会越接近我们的结局越快? 放
console.log
的好地方在哪里 在幕后进行? -
代码中有很多未提及的地方。例如,如果
minimax()
无法返回分数,它会继续为其他人返回分数 边,并且有多组moves
数组被 为终端值之前的每个可用空间生成 成立。有人如何考虑这一切,当console.log
-ing 会让人不知所措吗?
我自己制作了井字棋盘,所以我希望通过学习这个 minimax()
背后的思维过程,我可以进行一些调整并将其应用到我自己的,但它比它更难看起来。
- 最后,在计算
var bestMove
这里最新的moves[i].score
取代了之前的最好成绩,是否安全 假设代码会玩它遇到的第一个最佳移动, 并且永远不会在中间,因为这些动作可能相等,但是 从不满足if (moves[i].score > bestScore) {bestScore = moves[i].score}
?