web-dev-qa-db-ja.com

2048ゲームに最適なアルゴリズムは何ですか?

私は最近ゲームに出会いました 2048 。あなたは "より大きな"タイルを作るために四方のいずれかにそれらを動かすことによって同様のタイルを併合します。移動のたびに、新しいタイルが2または4の値を持つランダムな空の位置に表示されます。すべてのボックスがいっぱいになり、タイルを結合できる移動がない場合、ゲームは終了します。または、値が2048のタイルを作成します。

1つは、目標を達成するために明確に定義された戦略に従う必要があります。だから、私はそれのためにプログラムを書くことを考えました。

私の現在のアルゴリズム:

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の値でマージしようとしています。つまり、できるだけ24のタイルを使用するようにしています。この方法で試してみると、他のすべてのタイルは自動的にマージされていたので、戦略は良いようです。

しかし、実際にこのアルゴリズムを使用すると、ゲームが終了する前に4000ポイントしか獲得できません。最高得点AFAIKは20,000点を少し上回っています。これは私の現在の得点よりもはるかに大きいです。上記より優れたアルゴリズムはありますか?

1855
nitish712

私は、このスレッドで他の人が言及したAIプログラムの著者です。 AIは action で表示するか、 source で読むことができます。

現在、このプログラムは、1回の移動で約100ミリ秒の思考時間を与えられると、ラップトップのブラウザーでjavascriptで実行される約90%の勝率を達成します。

ゲームは離散状態空間、完璧な情報、チェスやチェッカーのようなターンベースのゲームであるため、これらのゲームで機能することが証明されている同じ方法、つまり minimaxsearch with alpha-beta pruning 。そのアルゴリズムについてはすでに多くの情報があるので、 静的評価関数 で使用し、他の人が持っている多くの直観を形式化する2つの主なヒューリスティックについて説明しますここに表現されます。

単調性

このヒューリスティックは、タイルの値がすべて左/右および上/下の両方向に沿って増加または減少することを保証しようとします。このヒューリスティックだけでも、他の多くの人が言及している、より価値の高いタイルはコーナーにクラスター化する必要があるという直感を捉えています。通常、小さな値のタイルが孤立するのを防ぎ、小さなタイルがカスケードして大きなタイルを埋めてボードを非常に整理します。

これは、完全に単調なグリッドのスクリーンショットです。これは、eval関数を設定してアルゴリズムを実行し、他のヒューリスティックを無視して単調性のみを考慮して取得しました。

A perfectly monotonic 2048 board

滑らかさ

上記のヒューリスティックだけでは、隣接するタイルの値が減少する構造を作成する傾向がありますが、もちろんマージするには、隣接するタイルが同じ値である必要があります。したがって、スムーズヒューリスティックは、隣接するタイル間の値の差を測定し、このカウントを最小化しようとします。

Hacker Newsのコメント者は、グラフ理論の観点からこのアイデアを 興味深い形式化 で示しました。

この優れたパロディフォーク のおかげで、完全に滑らかなグリッドのスクリーンショットを次に示します。

A perfectly smooth 2048 board

フリータイル

そして最後に、ゲームボードがamp屈になりすぎるとオプションがすぐになくなるため、無料のタイルが少なすぎるとペナルティがあります。

以上です!これらの基準を最適化しながらゲーム空間を検索すると、非常に優れたパフォーマンスが得られます。明示的にコード化された移動戦略ではなく、このような一般化されたアプローチを使用する利点の1つは、アルゴリズムが興味深く予期しない解決策を見つけることができることです。走るのを見ると、それが構築する壁やコーナーを突然切り替えるなど、驚くほど効果的な動きをすることがよくあります。

編集:

このアプローチの威力を示すデモを次に示します。タイルの値に上限を設けなかったため(2048に達した後も継続しました)、8回の試行後の最良の結果が得られました。

4096

はい、それは2048と並んで4096です。=)それは、同じボード上で3回、捉えにくい2048タイルを達成したことを意味します。

1244
ovolve

ハードコーディングされたインテリジェンスを含まない(つまり、ヒューリスティック、スコアリング関数などを含まない)このゲームのAIのアイデアに興味を持ちました。 AIは"know"ゲームルールのみ、"figure out"ゲームプレイのみを行う必要があります。これは、ゲームのプレイが人間のゲームの理解を表すスコアリング機能によって基本的に総当たり操作されるほとんどのAI(このスレッドのような)とは対照的です。

AIアルゴリズム

シンプルでありながら驚くほど良いプレイングアルゴリズムを見つけました:特定のボードの次の動きを決定するために、AIはランダムな動きを使用してメモリ内でゲームをプレイしますゲームは終わりだ。これは、ゲーム終了スコアを追跡しながら数回行われます。次に、平均終了スコア開始移動ごとが計算されます。最も高い平均終了スコアを持つ開始移動が、次の移動として選択されます。

1回の移動あたり100回の実行(メモリゲームでの実行)で、AIは80%の2048タイルと50%の4096タイルを達成します。 10000回実行すると、2048タイルが100%、4096タイルが70%、8192タイルが約1%になります。

実際の動作を参照

最高の得点は次のとおりです。

best score

このアルゴリズムの興味深い事実は、ランダムプレイゲームは驚くほどひどく悪いものですが、最高の(または最低の)動きを選択することは非常に良いゲームプレイにつながることです:典型的なAIゲームは70000ポイントと最後の3000の動きに到達できます任意の位置からのメモリ内ランダムプレイゲームでは、死ぬまでに約40回の追加移動で平均340ポイントが追加されます。 (AIを実行してデバッグコンソールを開くと、これを自分で確認できます。)

このグラフはこの点を示しています。青い線は、各移動後のボードスコアを示しています。赤い線は、その位置からのアルゴリズムのbestランダムランエンドゲームスコアを示しています。本質的に、赤の値はアルゴリズムの最良の推測であるため、青の値を上に「引っ張って」います。赤い線が各ポイントで青い線の上にほんの少しだけあるのを見るのは面白いですが、青い線はどんどん増え続けています。

scoring graph

アルゴリズムは、それを生み出す動きを選択するために実際に良いゲームプレイを予測する必要がないことは非常に驚くべきことです。

後で検索すると、このアルゴリズムは Pure Monte Carlo Tree Search アルゴリズムとして分類される可能性があることがわかりました。

実装とリンク

最初に ここで実際に見られます にできるJavaScriptバージョンを作成しました。このバージョンでは、適切な時間で何百回も実行できます。追加情報については、コンソールを開きます。 ( ソース

後で、さらにいくつかを試すために、@ nneonneoの高度に最適化されたインフラストラクチャを使用し、C++でバージョンを実装しました。このバージョンでは、1回の移動につき最大100000回、忍耐力があれば1000000回まで実行できます。提供される構築手順。コンソールで実行され、Webバージョンを再生するためのリモートコントロールもあります。 ( ソース

結果

驚くべきことに、実行回数を増やしてもゲームプレイが劇的に改善されることはありません。この戦略には、4096タイルと8192タイルの達成に非常に近いすべての小さなタイルで約80000ポイントに制限があるようです。実行回数を100から100000に増やすと、このスコア制限に到達する(5%から40%に達する)が、突破しないoddsが増えます。

10000を実行すると、重要な位置の近くで一時的に1000000に増加し、この障壁を1%未満の時間で破って最大スコア129892と8192タイルを達成しました。

改善点

このアルゴリズムを実装した後、最小または最大スコア、または最小、最大、平均の組み合わせを使用するなど、多くの改善を試みました。また、深さを使用してみました:移動ごとにKの移動を試みる代わりに、移動ごとにKの移動を試みましたリスト与えられた長さ(たとえば、「上、上、左」)の最初の移動を選択します最高得点移動リスト。

後で、特定の移動リストの後に移動できる条件付き確率を考慮に入れたスコアリングツリーを実装しました。

ただし、これらのアイデアはいずれも、単純な最初のアイデアに勝る真の利点を示していません。これらのアイデアのコードは、C++コードでコメントアウトしました。

いずれかの実行が誤って次に高いタイルに到達したときに、実行数を一時的に1000000に増やす「ディープ検索」メカニズムを追加しました。これにより、時間が改善されました。

AIのドメイン非依存性を維持する他の改善アイデアを誰かが持っているかどうか聞いてみたいです。

2048のバリアントとクローン

ただの楽しみのために、私は AIをブックマークレットとして実装 をして、ゲームのコントロールに接続しました。これにより、AIは元のゲームおよびの多くのバリアントで動作できます。

これは、AIのドメインに依存しない性質により可能です。六角形のクローンなど、いくつかのバリアントは非常に明確です。

132
Ronenz

編集: これは単純なアルゴリズムで、人間の意識的思考プロセスをモデル化したもので、AIと比較して非常に弱い結果になります。回答の早い時期に提出されました。

私はアルゴリズムを洗練し、ゲームを破った!それは終わりに近づくという単純な不運のせいで失敗するかもしれません(あなたは絶対にしてはいけない、あなたが下に動かされるべきではなく、タイルはあなたの最も高いところにあるべきところに現れます。パターンを壊す)、しかし基本的にあなたが遊ぶための固定部分と可動部分を持つことになる。これがあなたの目的です:

Ready to finish

これが私がデフォルトで選んだモデルです。

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

選択されたコーナーは任意です、あなたは基本的に1つのキーを押すことは禁じられています(禁止された移動)、そしてあなたがそうするならば、あなたはもう一度反対を押してそれを修正しようとします。将来のタイルでは、モデルは常に次のランダムタイルが2であり、現在のモデルの反対側に表示されることを期待します(最初の行が完成していない間、最初の行が完成すると左下に表示されます)。コーナー)。

これがアルゴリズムです。約80%が勝ちます(もっと "プロの" 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
123
Daren

私はここに私のブログの 投稿の内容をコピーします


私が提案する解決策は非常に単純で実装が簡単です。しかし、それは131040のスコアに達しました。アルゴリズム性能のいくつかのベンチマークが提示されています。

Score

アルゴリズム

ヒューリスティックスコアリングアルゴリズム

私のアルゴリズムが基づいているという仮定はかなり単純です:あなたがより高いスコアを達成したいならば、ボードはできるだけきちんとしておかなければなりません。特に、最適設定は、タイル値の線形単調減少順によって与えられる。この直感から、タイル値の上限もわかります。 s nはボード上のタイルの数です。

(必要に応じて2タイルではなく4タイルがランダムに生成された場合、131072タイルに到達する可能性があります)

次の図に、ボードを編成する2つの方法を示します。

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を実装すると、確実にアルゴリズムが改善されます。明らかに、より洗練された決定規則はアルゴリズムを減速させ、それは実行されるためにいくらかの時間を必要とするでしょう。私は近い将来ミニマックスの実行を試みるでしょう。 (乞うご期待)

基準

  • 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 hereenter image description hereenter image description hereenter image description here

T2の場合、10回のうち4回のテストで4096タイルが生成され、平均スコアは s 42000

コード

このコードはGiHubの次のリンクにあります。 https://github.com/Nicola17/term2048-AI これは term2048 に基づいており、Pythonで書かれています。できるだけ早くC++でより効率的なバージョンを実装します。

94
Nicola Pezzotti

私の試みは、上記の他の解決策のようにexpectimaxを使いますが、ビットボードは使いません。 Nneonneoの解決策は、残りの6つのタイルと4つの移動が可能な約4の深さである1000万の移動をチェックすることができます(2 * 6 * 4)4。私の場合、この深さは調査に時間がかかりすぎる、私は残っている空きタイルの数に従ってexpectimax検索の深さを調整する:

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

ボードの得点は、フリータイルの数の2乗と2次元グリッドの内積との加重和で計算されます。

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

左上のタイルから蛇のようにタイルを降順に整理することを強制します。

以下のコードまたは github

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)
        }
}
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;
}
<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>
37
caub

私はこのスレッドで言及されている他のどのプログラムよりも優れた得点2048コントローラの作者です。コントローラの効率的な実装は github で利用可能です。 別のレポ - コントローラの状態評価関数を訓練するために使用されるコードもあります。トレーニング方法は paper に記載されています。

コントローラは、 時間差学習 (強化学習技法)の変形によって、(人間2048の専門知識なしで)ゼロから学習された状態評価関数を用いたexpectimax検索を使用する。状態値関数は n-タプルネットワーク を使用します。これは基本的にボード上で観測されたパターンの重み付き線形関数です。それは合計で 10億以上の重み を含みました。

パフォーマンス

1手の時/秒: 609104 (100ゲームの平均)

10移動/秒の場合: 589355 (300ゲームの平均)

3プライで(約1500移動/秒): 511759 (平均1000ゲーム)

10移動/秒のタイル統計は次のとおりです。

2048: 100%
4096: 100%
8192: 100%
16384: 97%
32768: 64%
32768,16384,8192,4096: 10%

(最後の行はボード上に同時に与えられたタイルを持つことを意味します)。

3プライの場合

2048: 100%
4096: 100%
8192: 100%
16384: 96%
32768: 54%
32768,16384,8192,4096: 8%

しかし、私はそれが65536タイルを取得するのを見たことがありません。

31
cauchy

私はよくうまくいくアルゴリズムを見つけたと思います、私はしばしば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);
}
27

このゲームにはすでにAIの実装があります ここ 。 READMEからの抜粋:

このアルゴリズムは、深さ優先のアルファ - ベータ検索を繰り返し深くするものです。評価関数は、グリッド上のタイルの数を最小限に抑えながら、行と列を単調(すべて減少または増加)に保とうとします。

Hacker News にこのアルゴリズムについての議論もありますが、これは役に立つかもしれません。

25
baltazar

アルゴリズム

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)

これは、先読みせずに、その状態内でマージが発生する可能性を簡単にチェックしたものです。

注:定数は調整できます。

23
Khaled.K

これはOPの質問に対する直接的な答えではありません、これは私がこれまでに同じ問題を解決し、いくつかの結果を得て共有したいという観察を得たものです。これからのさらなる洞察。

私は、3と5で検索ツリーの深さをカットオフして、アルファベータを刈り込んでミニマックスの実装を試したところです。私は、 edXコースColumbiaX:CSMM.101xのプロジェクト割り当てとして、4x4グリッドの同じ問題を解決しようとしました。人工知能(AI)

私は、主に直感と上記で論じたものから、2つのヒューリスティック評価関数の凸型の組み合わせ(さまざまなヒューリスティック重みを試しました)を適用しました。

  1. 単調性
  2. 利用可能な空き容量

私の場合、コンピュータプレイヤーは完全にランダムですが、それでも私は敵対的な設定を想定し、最大プレイヤーとしてAIプレイヤーエージェントを実装しました。

私はゲームをプレイするための4×4グリッドを持っています。

観察:

最初のヒューリスティック関数または2番目のヒューリスティック関数にあまりに多くの重みを割り当てた場合、どちらの場合もAIプレイヤーが獲得するスコアは低くなります。私はヒューリスティック関数に可能な限り多くのウェイト割り当てをして、凸型の組み合わせにしましたが、AIプレーヤーが2048を獲得できることはめったにありません。ほとんどの場合、1024または512で止まります。

私はまたコーナーヒューリスティックを試みました、しかし、それは何らかの理由で結果をより悪くします、なぜか直観?

また、検索深度のカットオフを3から5に増やそうとし(剪定を行ってもスペースを検索することが許容時間を超えているのでこれ以上増やすことはできません)、隣接タイルの値を調べてそれらがマージ可能であるなら、より多くのポイントが、それでも私は2048年を得ることができません。

Minimaxの代わりにExpectimaxを使用する方が良いと思いますが、それでもこの問題を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-コンピューター付きゲーム/ および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

11
Sandipan Dey

私は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

9
wvdz

このアルゴリズムはゲームに勝つためには最適ではありませんが、パフォーマンスと必要なコード量の点ではかなり最適です。

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

他の答えの多くは、可能な未来、ヒューリスティック、学習などの計算上高価な検索でAIを使います。これらは印象的でおそらく正しい方法ですが、私は別のアイデアを投稿したいと思います。

ゲームの優れたプレイヤーが使用する戦略の種類をモデル化します。

例えば:

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

次の二乗値が現在のものより大きくなるまで、上に示した順序で四角形を読みます。これは、この正方形に同じ値の別のタイルをマージしようとするという問題を提示します。

この問題を解決するためには、2つの方法があります。残されていないか、さらに悪いことには両方の可能性を調べると、より多くの問題がすぐに明らかになります。私は次の動きを決めるとき、特に立ち往生しているとき、私はこの連鎖、あるいはある場合には依存関係の木を内部的に持っていると思います。


タイルは隣人とマージする必要がありますが小さすぎます:別の隣人とこのタイルをマージします。

邪魔にならない大きいタイル:小さい周囲のタイルの値を大きくします。

等...


アプローチ全体はおそらくこれよりも複雑ですが、それほど複雑ではありません。スコア、ウェイト、ニューロン、および可能性の詳細な検索が欠けているという点で、このメカニズムになる可能性があります。可能性の木は少しでも分岐を必要とするのに十分大きくする必要があります。

4
alan2here