2048游戏的最佳算法是什么?

时间:2014-03-12 05:37:21

标签: algorithm logic artificial-intelligence 2048

我最近偶然发现了游戏2048。您可以通过在四个方向中的任何一个方向上移动它们来合并相似的图块,以制作“更大”每次移动后,新的图块会显示在随机空位置,其值为24。当所有框都填满且没有可以合并图块的移动时,或者您创建值为2048的图块时,游戏会终止。

一,我需要遵循明确的战略来实现目标。所以,我想为它编写一个程序。

我目前的算法:

while (!game_over) {
    for each possible move:
        count_no_of_merges_for_2-tiles and 4-tiles
    choose the move with a large number of merges
}

我正在做的是在任何时候,我会尝试将图块与值24合并,也就是说,我尝试2和{{1}瓷砖,尽可能少。如果我这样试试,所有其他牌都会自动合并,策略似乎很好。

但是,当我实际使用这个算法时,我只能在游戏结束前获得大约4000点。 AFAIK的最高分数略高于20,000分,远高于我目前的分数。有比上面更好的算法吗?

12 个答案:

答案 0 :(得分:1243)

我是其他人在此主题中提到的AI程序的作者。您可以在action中查看AI或阅读source

目前,该计划在我的笔记本电脑上的浏览器中以javascript运行大约90%的赢率,每次移动约有100毫秒的思考时间,所以虽然不完美(还是!)但表现相当不错。

由于游戏是一个离散的状态空间,完美的信息,像国际象棋和棋子这样的回合制游戏,我使用的方法已经被证明适用于那些游戏,即minimax searchalpha-beta pruning。由于那里已经有很多关于该算法的信息,我只是谈谈我在static evaluation function中使用的两个主要启发式方法,它们形式化了其他人在这里表达的许多直觉。

单调性

此启发式尝试确保切片的值沿左/右和上/下方向都增加或减少。这种启发式捕获了许多其他人提到的直觉,即更高价值的瓷砖应该聚集在一个角落里。它通常会阻止较小的有价值的瓷砖变得孤立,并使电路板保持整齐有序,较小的瓷砖层叠并填充到较大的瓷砖中。

这里是完美单调网格的屏幕截图。我通过运行算法并使用eval函数设置忽略其他启发式算法来获得此结果,并且只考虑单调性。

A perfectly monotonic 2048 board

光滑

上述启发式单独倾向于创建其中相邻图块的值减小的结构,但是当然为了合并,相邻图块需要是相同的值。因此,平滑度启发式测量仅测量相邻图块之间的值差异,尝试最小化此计数。

黑客新闻的评论者根据图论给出an interesting formalization这个想法。

这里是this excellent parody fork友情提供的完美平滑网格的屏幕截图。

A perfectly smooth 2048 board

免费瓷砖

最后,由于游戏牌太过狭窄,选项很快就会耗尽,因此免费牌位太少会受到惩罚。

那就是它!在优化这些标准的同时搜索游戏空间会产生非常好的性能。使用这样的通用方法而不是明确编码的移动策略的一个优点是该算法通常可以找到有趣且意想不到的解决方案。如果你观看它,它往往会产生令人惊讶但有效的动作,比如突然切换它所构建的墙壁或角落。

编辑:

这里展示了这种方法的力量。我打开了瓷砖值(所以它在达到2048后继续运行),这是八次试验后的最佳结果。

4096

是的,这是一个4096和2048. =)这意味着它在同一块板上实现了三次难以捉摸的2048瓦片。

答案 1 :(得分:131)

我对这个包含没有硬编码智能的游戏的AI的想法感兴趣(即没有启发式,评分功能等)。 AI应“知道”仅游戏规则,“弄清楚”游戏玩法。这与大多数人工智能(如本线程中的人工智能)形成鲜明对比,其中游戏玩法基本上是由表示人类对游戏理解的评分函数引导的蛮力。

AI算法

我发现了一个简单但令人惊讶的优秀游戏算法:为了确定给定棋盘的下一步动作,AI使用随机动作在游戏中玩游戏,直到游戏结束。这样做了几次,同时跟踪最终比赛得分。然后计算每次开始移动的平均最终得分 。选择平均最终得分最高的开始行动作为下一步行动。

每次移动仅运行100次(即在记忆游戏中),AI在80%的时间内达到2048瓦,在50%的时间内达到4096瓦。使用10000次运行可获得2048瓦100%,4096瓦70%,以及8192瓦约1%。

See it in action

最佳成绩如下所示:

best score

关于这个算法的一个有趣的事实是,虽然随机游戏不足为奇,但选择最好(或最差)的动作可以带来非常好的游戏玩法:典型的AI游戏可以达到70000点并持续3000步然而,来自任何给定位置的记忆内随机游戏在死亡前的大约40次额外移动中平均产生340个额外点。 (您可以通过运行AI并打开调试控制台来自行查看。)

此图表说明了这一点:蓝线显示每次移动后的董事会得分。红线显示该算法从该位置开始的最佳随机运行结束游戏得分。本质上,红色值将蓝色值“向上”拉向它们,因为它们是算法的最佳猜测。有趣的是,红线在每个点的蓝线上方稍微高一点,但蓝线继续增加越来越多。

scoring graph

我觉得非常令人惊讶的是,算法并不需要实际预见到良好的游戏玩法,以便选择产生它的动作。

稍后搜索我发现此算法可能被归类为Pure Monte Carlo Tree Search算法。

实施和链接

首先,我创建了一个可以seen in action here的JavaScript版本。这个版本可以在适当的时间内运行100次。打开控制台以获取额外信息。 (source

后来,为了更多地使用@nneonneo高度优化的基础架构并在C ++中实现我的版本。如果你有耐心,这个版本允许每次移动最多100000次运行甚至1000000次运行。提供建筑说明。它在控制台中运行,并且还具有远程控制以播放Web版本。 (source

结果

令人惊讶的是,增加跑步次数并没有大幅提升比赛效果。这个策略似乎有一个限制,大约80000点4096瓦和所有较小的瓦,非常接近实现8192瓦。将运行次数从100增加到100000会增加达到此分数限制的赔率(从5%降至40%),但不能突破它。

在关键位置附近临时增加10000次运行并设法突破此障碍,不到1%的时间达到最大分数129892和8192瓦。

改进

在实施此算法后,我尝试了许多改进,包括使用最小或最大分数,或min,max和avg的组合。我也试过使用深度:我没有尝试每次移动K次运行,而是尝试了每次移动的K移动 list 给定长度(例如“向上,向上,向左”)并选择第一步最佳得分移动名单。

后来我实施了一个评分树,该树考虑了在给定移动列表后能够进行移动的条件概率。

然而,这些想法都没有比简单的第一个想法显示出任何真正的优势。我把这些想法的代码留在了C ++代码中。

我确实添加了一个“深度搜索”机制,当任何一次运行意外到达下一个最高的磁贴时,它会暂时将运行次数增加到1000000。这提供了时间的改善。

我很想知道是否有人有其他改进想法来保持AI的域独立性。

2048变种和克隆

为了好玩,我还implemented the AI as a bookmarklet,加入了游戏的控件。这允许AI与原始游戏和许多变体一起使用。

由于AI的域独立性,这是可能的。一些变体非常不同,例如六角形克隆。

答案 2 :(得分:123)

编辑:这是一个天真的算法,对人类有意识的思维过程进行建模,与搜索所有可能性的AI相比,得到的结果非常微弱,因为它只能看到前面的一个区块。它是在响应时间表的早期提交的。

我已经改进了算法并击败了游戏!它可能会因为接近末尾的简单坏运而失败(你被迫向下移动,你永远不应该这样做,并且会出现一个最高的区域。只是试着保持顶行填充,所以向左移动不会打破模式),但基本上你最终有一个固定的部分和一个移动部分来玩。这是你的目标:

Ready to finish

这是我默认选择的型号。

1024 512 256 128
  8   16  32  64
  4   2   x   x
  x   x   x   x

选择的角落是任意的,你基本上从不按一个键(禁止移动),如果你这样做,你再次按相反的方法并尝试修复它。对于将来的图块,模型总是希望下一个随机图块为2,并显示在当前模型的另一侧(第一行不完整,右下角,第一行完成后,左下角)角)。

这是算法。大约80%的胜利(似乎总是有可能赢得更多"专业和#34; AI技术,但我不确定这一点。)

initiateModel();

while(!game_over)
{    
    checkCornerChosen(); // Unimplemented, but it might be an improvement to change the reference point

    for each 3 possible move:
        evaluateResult()
    execute move with best score
    if no move is available, execute forbidden move and undo, recalculateModel()
 }

 evaluateResult() {
     calculatesBestCurrentModel()
     calculates distance to chosen model
     stores result
 }

 calculateBestCurrentModel() {
      (according to the current highest tile acheived and their distribution)
  }

关于缺失步骤的几点建议。这里:model change

由于靠近预期模型的运气,模型已经改变。 AI试图实现的模型是

 512 256 128  x
  X   X   x   x
  X   X   x   x
  x   x   x   x

到达那里的链条已成为:

 512 256  64  O
  8   16  32  O
  4   x   x   x
  x   x   x   x

O表示禁止的空格......

所以它会向右按,然后向右按,然后(向右或向上取决于4创建的位置)然后将继续完成链直到它:

Chain completed

所以现在模型和链条又回到了:

 512 256 128  64
  4   8  16   32
  X   X   x   x
  x   x   x   x

第二个指针,它运气不好,其主要位置已被采用。它可能会失败,但它仍然可以实现它:

Enter image description here

这里的模型和链是:

  O 1024 512 256
  O   O   O  128
  8  16   32  64
  4   x   x   x

当它设法达到128时,它会再次获得一整行:

  O 1024 512 256
  x   x  128 128
  x   x   x   x
  x   x   x   x

答案 3 :(得分:94)

我在这里复制post on my blog

的内容

我提出的解决方案非常简单,易于实施。虽然它已达到131040的分数。提出了算法性能的几个基准。

Score

算法

启发式评分算法

我的算法所依据的假设相当简单:如果你想获得更高的分数,董事会必须保持尽可能整洁。特别地,最佳设置由瓦片值的线性和单调递减顺序给出。 这种直觉也会给你一个瓦片值的上限:s其中n是棋盘上的瓦片数。

(如果需要随机生成4个图块而不是2个图块,则有可能到达131072图块)

以下图片显示了组织电路板的两种可能方式:

enter image description here

为了以单调递减顺序强制执行瓦片的排序,得分si计算为板上线性化值的总和乘以具有公共比率r <1的几何序列的值。

s

s

可以一次评估多个线性路径,最终得分将是任何路径的最高得分。

决策规则

实施的决策规则并不十分明智,Python中的代码如下所示:

@staticmethod
def nextMove(board,recursion_depth=3):
    m,s = AI.nextMoveRecur(board,recursion_depth,recursion_depth)
    return m

@staticmethod
def nextMoveRecur(board,depth,maxDepth,base=0.9):
    bestScore = -1.
    bestMove = 0
    for m in range(1,5):
        if(board.validMove(m)):
            newBoard = copy.deepcopy(board)
            newBoard.move(m,add_tile=True)

            score = AI.evaluate(newBoard)
            if depth != 0:
                my_m,my_s = AI.nextMoveRecur(newBoard,depth-1,maxDepth)
                score += my_s*pow(base,maxDepth-depth+1)

            if(score > bestScore):
                bestMove = m
                bestScore = score
    return (bestMove,bestScore);

minmax或Expectiminimax的实现肯定会改进算法。显然更多 复杂的决策规则会减慢算法的速度,需要一些时间才能实现。我将在不久的将来尝试实现minimax。 (敬请关注)

基准

  • T1 - 121次测试 - 8条不同的路径 - r = 0.125
  • T2 - 122次测试 - 8种不同的路径 - r = 0.25
  • T3 - 132次测试 - 8种不同的路径 - r = 0.5
  • T4 - 211测试 - 2个不同的路径 - r = 0.125
  • T5 - 274次测试 - 2种不同的路径 - r = 0.25
  • T6 - 211测试 - 2个不同的路径 - r = 0.5

enter image description here enter image description here enter image description here enter image description here

如果是T2,十分之四的测试会生成4096个平铺,平均分为s 42000

代码

可以在以下链接的GiHub上找到代码:https://github.com/Nicola17/term2048-AI 它基于term2048,用Python编写。我将尽快在C ++中实现更高效的版本。

答案 4 :(得分:37)

我的尝试像上面的其他解决方案一样使用expectimax,但没有位板。 Nneonneo的解决方案可以检查1000万次移动,大约深度为4,剩下6个瓷砖,4个移动可能(2 * 6 * 4) 4 。在我的情况下,这个深度需要很长时间来探索,我根据剩下的免费瓷砖数量调整expectimax搜索的深度:

depth = free > 7 ? 1 : (free > 4 ? 2 : 3)

电路板的分数是用免费瓷砖的数量的平方和2D网格的点积的加权和来计算的:

[[10,8,7,6.5],
 [.5,.7,1,3],
 [-.5,-1.5,-1.8,-2],
 [-3.8,-3.7,-3.5,-3]]

强制从左上方的瓷砖中以某种蛇形式下降瓷砖。

下面或github上的代码:

&#13;
&#13;
var n = 4,
	M = new MatrixTransform(n);

var ai = {weights: [1, 1], depth: 1}; // depth=1 by default, but we adjust it on every prediction according to the number of free tiles

var snake= [[10,8,7,6.5],
            [.5,.7,1,3],
            [-.5,-1.5,-1.8,-2],
            [-3.8,-3.7,-3.5,-3]]
snake=snake.map(function(a){return a.map(Math.exp)})

initialize(ai)

function run(ai) {
	var p;
	while ((p = predict(ai)) != null) {
		move(p, ai);
	}
	//console.log(ai.grid , maxValue(ai.grid))
	ai.maxValue = maxValue(ai.grid)
	console.log(ai)
}

function initialize(ai) {
	ai.grid = [];
	for (var i = 0; i < n; i++) {
		ai.grid[i] = []
		for (var j = 0; j < n; j++) {
			ai.grid[i][j] = 0;
		}
	}
	rand(ai.grid)
	rand(ai.grid)
	ai.steps = 0;
}

function move(p, ai) { //0:up, 1:right, 2:down, 3:left
	var newgrid = mv(p, ai.grid);
	if (!equal(newgrid, ai.grid)) {
		//console.log(stats(newgrid, ai.grid))
		ai.grid = newgrid;
		try {
			rand(ai.grid)
			ai.steps++;
		} catch (e) {
			console.log('no room', e)
		}
	}
}

function predict(ai) {
	var free = freeCells(ai.grid);
	ai.depth = free > 7 ? 1 : (free > 4 ? 2 : 3);
	var root = {path: [],prob: 1,grid: ai.grid,children: []};
	var x = expandMove(root, ai)
	//console.log("number of leaves", x)
	//console.log("number of leaves2", countLeaves(root))
	if (!root.children.length) return null
	var values = root.children.map(expectimax);
	var mx = max(values);
	return root.children[mx[1]].path[0]

}

function countLeaves(node) {
	var x = 0;
	if (!node.children.length) return 1;
	for (var n of node.children)
		x += countLeaves(n);
	return x;
}

function expectimax(node) {
	if (!node.children.length) {
		return node.score
	} else {
		var values = node.children.map(expectimax);
		if (node.prob) { //we are at a max node
			return Math.max.apply(null, values)
		} else { // we are at a random node
			var avg = 0;
			for (var i = 0; i < values.length; i++)
				avg += node.children[i].prob * values[i]
			return avg / (values.length / 2)
		}
	}
}

function expandRandom(node, ai) {
	var x = 0;
	for (var i = 0; i < node.grid.length; i++)
		for (var j = 0; j < node.grid.length; j++)
			if (!node.grid[i][j]) {
				var grid2 = M.copy(node.grid),
					grid4 = M.copy(node.grid);
				grid2[i][j] = 2;
				grid4[i][j] = 4;
				var child2 = {grid: grid2,prob: .9,path: node.path,children: []};
				var child4 = {grid: grid4,prob: .1,path: node.path,children: []}
				node.children.push(child2)
				node.children.push(child4)
				x += expandMove(child2, ai)
				x += expandMove(child4, ai)
			}
	return x;
}

function expandMove(node, ai) { // node={grid,path,score}
	var isLeaf = true,
		x = 0;
	if (node.path.length < ai.depth) {
		for (var move of[0, 1, 2, 3]) {
			var grid = mv(move, node.grid);
			if (!equal(grid, node.grid)) {
				isLeaf = false;
				var child = {grid: grid,path: node.path.concat([move]),children: []}
				node.children.push(child)
				x += expandRandom(child, ai)
			}
		}
	}
	if (isLeaf) node.score = dot(ai.weights, stats(node.grid))
	return isLeaf ? 1 : x;
}



var cells = []
var table = document.querySelector("table");
for (var i = 0; i < n; i++) {
	var tr = document.createElement("tr");
	cells[i] = [];
	for (var j = 0; j < n; j++) {
		cells[i][j] = document.createElement("td");
		tr.appendChild(cells[i][j])
	}
	table.appendChild(tr);
}

function updateUI(ai) {
	cells.forEach(function(a, i) {
		a.forEach(function(el, j) {
			el.innerHTML = ai.grid[i][j] || ''
		})
	});
}


updateUI(ai);
updateHint(predict(ai));

function runAI() {
	var p = predict(ai);
	if (p != null && ai.running) {
		move(p, ai);
		updateUI(ai);
		updateHint(p);
		requestAnimationFrame(runAI);
	}
}
runai.onclick = function() {
	if (!ai.running) {
		this.innerHTML = 'stop AI';
		ai.running = true;
		runAI();
	} else {
		this.innerHTML = 'run AI';
		ai.running = false;
		updateHint(predict(ai));
	}
}


function updateHint(dir) {
	hintvalue.innerHTML = ['↑', '→', '↓', '←'][dir] || '';
}

document.addEventListener("keydown", function(event) {
	if (!event.target.matches('.r *')) return;
	event.preventDefault(); // avoid scrolling
	if (event.which in map) {
		move(map[event.which], ai)
		console.log(stats(ai.grid))
		updateUI(ai);
		updateHint(predict(ai));
	}
})
var map = {
	38: 0, // Up
	39: 1, // Right
	40: 2, // Down
	37: 3, // Left
};
init.onclick = function() {
	initialize(ai);
	updateUI(ai);
	updateHint(predict(ai));
}


function stats(grid, previousGrid) {

	var free = freeCells(grid);

	var c = dot2(grid, snake);

	return [c, free * free];
}

function dist2(a, b) { //squared 2D distance
	return Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2)
}

function dot(a, b) {
	var r = 0;
	for (var i = 0; i < a.length; i++)
		r += a[i] * b[i];
	return r
}

function dot2(a, b) {
	var r = 0;
	for (var i = 0; i < a.length; i++)
		for (var j = 0; j < a[0].length; j++)
			r += a[i][j] * b[i][j]
	return r;
}

function product(a) {
	return a.reduce(function(v, x) {
		return v * x
	}, 1)
}

function maxValue(grid) {
	return Math.max.apply(null, grid.map(function(a) {
		return Math.max.apply(null, a)
	}));
}

function freeCells(grid) {
	return grid.reduce(function(v, a) {
		return v + a.reduce(function(t, x) {
			return t + (x == 0)
		}, 0)
	}, 0)
}

function max(arr) { // return [value, index] of the max
	var m = [-Infinity, null];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] > m[0]) m = [arr[i], i];
	}
	return m
}

function min(arr) { // return [value, index] of the min
	var m = [Infinity, null];
	for (var i = 0; i < arr.length; i++) {
		if (arr[i] < m[0]) m = [arr[i], i];
	}
	return m
}

function maxScore(nodes) {
	var min = {
		score: -Infinity,
		path: []
	};
	for (var node of nodes) {
		if (node.score > min.score) min = node;
	}
	return min;
}


function mv(k, grid) {
	var tgrid = M.itransform(k, grid);
	for (var i = 0; i < tgrid.length; i++) {
		var a = tgrid[i];
		for (var j = 0, jj = 0; j < a.length; j++)
			if (a[j]) a[jj++] = (j < a.length - 1 && a[j] == a[j + 1]) ? 2 * a[j++] : a[j]
		for (; jj < a.length; jj++)
			a[jj] = 0;
	}
	return M.transform(k, tgrid);
}

function rand(grid) {
	var r = Math.floor(Math.random() * freeCells(grid)),
		_r = 0;
	for (var i = 0; i < grid.length; i++) {
		for (var j = 0; j < grid.length; j++) {
			if (!grid[i][j]) {
				if (_r == r) {
					grid[i][j] = Math.random() < .9 ? 2 : 4
				}
				_r++;
			}
		}
	}
}

function equal(grid1, grid2) {
	for (var i = 0; i < grid1.length; i++)
		for (var j = 0; j < grid1.length; j++)
			if (grid1[i][j] != grid2[i][j]) return false;
	return true;
}

function conv44valid(a, b) {
	var r = 0;
	for (var i = 0; i < 4; i++)
		for (var j = 0; j < 4; j++)
			r += a[i][j] * b[3 - i][3 - j]
	return r
}

function MatrixTransform(n) {
	var g = [],
		ig = [];
	for (var i = 0; i < n; i++) {
		g[i] = [];
		ig[i] = [];
		for (var j = 0; j < n; j++) {
			g[i][j] = [[j, i],[i, n-1-j],[j, n-1-i],[i, j]]; // transformation matrix in the 4 directions g[i][j] = [up, right, down, left]
			ig[i][j] = [[j, i],[i, n-1-j],[n-1-j, i],[i, j]]; // the inverse tranformations
		}
	}
	this.transform = function(k, grid) {
		return this.transformer(k, grid, g)
	}
	this.itransform = function(k, grid) { // inverse transform
		return this.transformer(k, grid, ig)
	}
	this.transformer = function(k, grid, mat) {
		var newgrid = [];
		for (var i = 0; i < grid.length; i++) {
			newgrid[i] = [];
			for (var j = 0; j < grid.length; j++)
				newgrid[i][j] = grid[mat[i][j][k][0]][mat[i][j][k][1]];
		}
		return newgrid;
	}
	this.copy = function(grid) {
		return this.transform(3, grid)
	}
}
&#13;
body {
	font-family: Arial;
}
table, th, td {
	border: 1px solid black;
	margin: 0 auto;
	border-collapse: collapse;
}
td {
	width: 35px;
	height: 35px;
	text-align: center;
}
button {
	margin: 2px;
	padding: 3px 15px;
	color: rgba(0,0,0,.9);
}
.r {
	display: flex;
	align-items: center;
	justify-content: center;
	margin: .2em;
	position: relative;
}
#hintvalue {
	font-size: 1.4em;
	padding: 2px 8px;
	display: inline-flex;
	justify-content: center;
	width: 30px;
}
&#13;
<table title="press arrow keys"></table>
<div class="r">
    <button id=init>init</button>
    <button id=runai>run AI</button>
    <span id="hintvalue" title="Best predicted move to do, use your arrow keys" tabindex="-1"></span>
</div>
&#13;
&#13;
&#13;

答案 5 :(得分:27)

我认为我发现了一种效果很好的算法,因为我经常达到10000以上的分数,我个人最好的是16000左右。我的解决方案并不是要将最大数字保持在一个角落,而是保持在最上面的一行。

请参阅以下代码:

while( !game_over ) {
    move_direction=up;
    if( !move_is_possible(up) ) {
        if( move_is_possible(right) && move_is_possible(left) ){
            if( number_of_empty_cells_after_moves(left,up) > number_of_empty_cells_after_moves(right,up) ) 
                move_direction = left;
            else
                move_direction = right;
        } else if ( move_is_possible(left) ){
            move_direction = left;
        } else if ( move_is_possible(right) ){
            move_direction = right;
        } else {
            move_direction = down;
        }
    }
    do_move(move_direction);
}

答案 6 :(得分:25)

此游戏here已有AI实施。摘自README:

  

该算法是迭代加深深度的第一个alpha-beta搜索。评估函数尝试使行和列保持单调(全部减少或增加),同时最小化网格上的图块数。

还有关于此算法的Hacker News的讨论,您可能会发现它很有用。

答案 7 :(得分:23)

<强>算法

while(!game_over)
{
    for each possible move:
        evaluate next state

    choose the maximum evaluation
}

<强>评价

Evaluation =
    128 (Constant)
    + (Number of Spaces x 128)
    + Sum of faces adjacent to a space { (1/face) x 4096 }
    + Sum of other faces { log(face) x 4 }
    + (Number of possible next moves x 256)
    + (Number of aligned values x 2)

评估详情

128 (Constant)

这是一个常量,用作基线和其他用途,如测试。

+ (Number of Spaces x 128)

更多空格使状态更灵活,我们乘以128(这是中位数),因为填充128个面的网格是最佳的不可能状态。

+ Sum of faces adjacent to a space { (1/face) x 4096 }

在这里,我们评估有可能进行合并的面部,通过向后评估它们,图块2变为值2048,而图块2048被评估为2。

+ Sum of other faces { log(face) x 4 }

在这里我们仍然需要检查堆叠值,但是以较小的方式不会中断灵活性参数,所以我们得到{x in [4,44]}的总和。

+ (Number of possible next moves x 256)

如果一个州有更多的可能过渡自由,那就更灵活了。

+ (Number of aligned values x 2)

这是对在该状态下进行合并的可能性的简化检查,而不进行预测。

注意:常量可以调整..

答案 8 :(得分:11)

这不是对OP问题的直接回答,这是我迄今为止尝试解决相同问题并获得一些结果的更多东西(实验),并且我想分享一些观察,我我很好奇,如果我们能从中得到进一步的见解。

我刚尝试使用alpha-beta修剪我的minimax实现,搜索树深度截止为3和5.我试图解决4x4网格的相同问题,作为 edX课程ColumbiaX的项目任务: CSMM.101x人工智能(AI)

我应用了几个启发式评估函数的凸组合(尝试了不同的启发式权重),主要来自直觉和上面讨论的那些:

  1. 单调性
  2. 可用空间
  3. 在我的情况下,计算机播放器是完全随机的,但我仍然采用对抗设置并将AI播放器代理实现为最大播放器。

    我有4x4网格用于玩游戏。

    观察:

    如果我为第一个启发式函数或第二个启发式函数分配了太多权重,那么AI玩家获得的分数都很低。我玩启发函数的许多可能的重量分配并采用凸组合,但很少AI玩家能够得分2048.大多数时候它停在1024或512.

    我也尝试了角落启发式,但由于某种原因它会使结果变得更糟,任何直觉都是为什么?

    另外,我尝试将搜索深度截止值从3增加到5(由于搜索空间超过允许的时间,即使使用修剪,我也无法增加它)并添加了一个查看值的启发式算法如果它们是合并的,那么相邻的瓷砖会给出更多的分数,但我仍然无法获得2048.

    我认为最好使用Expectimax而不是minimax,但我仍然只想用minimax解决这个问题并获得高分,例如2048或4096.我不确定我是否遗漏了任何东西。

    下面的动画显示了AI代理人与电脑玩家玩游戏的最后几个步骤:

    enter image description here

    任何见解都会非常有用,提前谢谢。 (这是我的博客文章的链接:https://sandipanweb.wordpress.com/2017/03/06/using-minimax-with-alpha-beta-pruning-and-heuristic-evaluation-to-solve-2048-game-with-computer/和youtube视频:https://www.youtube.com/watch?v=VnVFilfZ0r4

    下面的动画显示了游戏的最后几个步骤,其中AI玩家代理可以获得2048分,这次也增加了绝对值启发式:

    enter image description here

    以下数据显示了玩家AI代理人探索的游戏树,假设计算机只是一个步骤的对手:

    enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

答案 9 :(得分:9)

我在Haskell写了一个2048求解器,主要是因为我现在正在学习这门语言。

我对游戏的实现与实际游戏略有不同,因为新的牌总是'2'(而不是90%2和10%4)。并且新的图块不是随机的,但始终是左上角的第一个可用图块。此变体也称为Det 2048

因此,这个解算器是确定性的。

我使用了一种有利于空磁贴的详尽算法。对于深度1-4,它的执行速度非常快,但在深度为5时,每次移动的速度相当慢,大约为1秒。

以下是实现求解算法的代码。网格表示为16个长度的整数数组。只需计算空方格的数量即可完成评分。

bestMove :: Int -> [Int] -> Int
bestMove depth grid = maxTuple [ (gridValue depth (takeTurn x grid), x) | x <- [0..3], takeTurn x grid /= [] ]

gridValue :: Int -> [Int] -> Int
gridValue _ [] = -1
gridValue 0 grid = length $ filter (==0) grid  -- <= SCORING
gridValue depth grid = maxInList [ gridValue (depth-1) (takeTurn x grid) | x <- [0..3] ]

我认为它的简单性非常成功。从空网格开始并在深度5处求解时达到的结果是:

Move 4006
[2,64,16,4]
[16,4096,128,512]
[2048,64,1024,16]
[2,4,16,2]

Game Over

可在此处找到源代码:https://github.com/popovitsj/2048-haskell

答案 10 :(得分:6)

此算法不是赢得游戏的最佳选择,但就性能和所需代码量而言,它是相当优化的:

  if(can move neither right, up or down)
    direction = left
  else
  {
    do
    {
      direction = random from (right, down, up)
    }
    while(can not move in "direction")
  }

答案 11 :(得分:4)

许多其他答案使用人工智能计算昂贵的搜索可能的未来,启发式,学习等。这些令人印象深刻,可能是正确的前进方向,但我希望提出另一个想法。

模拟游戏中优秀玩家使用的策略。

例如:

13 14 15 16
12 11 10  9
 5  6  7  8
 4  3  2  1

按照上面显示的顺序读取方块,直到下一个方块值大于当前值。这提出了尝试将具有相同值的另一个图块合并到此正方形中的问题。

要解决这个问题,他们有2种不移动或更糟的移动方式,并且检查两种可能性可能会立即显示更多问题,这会形成一个依赖项列表,每个问题都需要首先解决另一个问题。在决定我的下一步行动时,我认为我内部有这种链或者在某些情况下有依赖树,特别是卡住了。

瓷砖需要与邻居合并,但太小:将另一个邻居与此合并。

更大的瓷砖:增加较小的周围瓷砖的值。

等...

整个方法可能比这更复杂,但并不复杂。可能是这种机械感觉缺乏分数,重量,神经元和对可能性的深入探索。可能性的树木甚至需要足够大才能需要任何分支。