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


1920

私は最近2048年のゲームに遭遇しました。類似のタイルを4つの方向のいずれかに移動して結合し、「より大きな」タイルを作成します。移動するたびに、新しいタイルが空の位置にランダムに表示され、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
}

私がやっていることは、いつでも、タイルを値2とマージしようとする4ことです。つまり、24タイルを最小限にしようとしています。この方法で試してみると、他のすべてのタイルが自動的にマージされており、戦略は良いようです。

しかし、実際にこのアルゴリズムを使用すると、ゲームが終了するまでに約4000ポイントしか得られません。最大点AFAIKは20,000ポイントをわずかに超えており、私の現在のスコアよりもはるかに大きいです。上記よりも良いアルゴリズムはありますか?


84
これは役立つかもしれません!ov3y.github.io/2048-AI
cegprakash

5
@ nitish712ちなみに、choose the move with large number of mergesローカル最適化にすぐにつながるため、アルゴリズムは貪欲です
Khaled.K

21
500-InternalServerError @:場合は、私は、アルファ-ベータゲーム木の剪定でAIを実装していた、新しいブロックがadversarially配置されていると仮定されるだろう。これは最悪の場合の想定ですが、役立つ場合があります。
Charles

6
ハイスコ​​アを狙う時間がないときの気晴らし:できるだけ低いスコアを取得するようにしてください。理論的には、2秒と4秒が交互になります。
Mark Hurd

7
この質問の正当性についての議論は、メタ上で見つけることができます:meta.stackexchange.com/questions/227266/...
イェルーンVannevel

回答:


1266

expectimaxを使用して2048 AIを開発しました@ovolveのアルゴリズムで使用されるミニマックス検索の代わりに、最適化。AIは、可能なすべての動きに対して最大化を実行し、次にすべての可能なタイルスポーンに対する期待値を実行します(タイルの確率で重み付けされます。つまり、4の場合は10%、2の場合は90%)。私が知る限り、expectimaxの最適化を排除することは(非常にありそうもないブランチを削除する場合を除いて)不可能なので、使用されるアルゴリズムは慎重に最適化されたブルートフォース検索です。

パフォーマンス

デフォルトの構成(最大検索深度8)のAIは、ボード位置の複雑さに応じて、移動を実行するのに10ミリ秒から200ミリ秒かかります。テストでは、AIはゲーム全体で1秒あたり5〜10回の平均移動速度を達成します。検索の深さが6移動に制限されている場合、AIは1秒あたり20以上の移動を簡単に実行できるため、興味深いウォッチングが可能になります。

AIのスコアパフォーマンスを評価するために、AIを100回実行しました(リモートコントロールを介してブラウザーゲームに接続しました)。各タイルについて、そのタイルが少なくとも1回達成されたゲームの比率を次に示します。

2048: 100%
4096: 100%
8192: 100%
16384: 94%
32768: 36%

すべての実行での最小スコアは124024でした。達成された最大スコアは794076でした。スコアの中央値は387222です。AIは2048タイルの取得に失敗していません(したがって、100ゲームに1回でもゲームに負けませんでした)。実際、すべての実行で少なくとも8192タイルを達成しました!

これが最良の実行のスクリーンショットです。

32768タイル、スコア794076

このゲームは96分で27830動き、つまり1秒あたり平均4.8動きかかりました。

実装

私のアプローチでは、ボード全体(16エントリ)を単一の64ビット整数(タイルはニブル、つまり4ビットチャンク)としてエンコードします。64ビットマシンでは、これによりボード全体を1つのマシンレジスタで渡すことができます。

ビットシフト操作は、個々の行と列を抽出するために使用されます。単一の行または列は16ビットの数量であるため、サイズが65536のテーブルは、単一の行または列を操作する変換をエンコードできます。たとえば、移動は、各移動が単一の行または列にどのように影響するかを説明する事前計算された「移動効果テーブル」への4つのルックアップとして実装されます(たとえば、「右移動」テーブルには、「1122-> 0023」というエントリが含まれています。行[2,2,4,4]は、右に移動すると行[0,0,4,8]になります)。

スコアリングもテーブルルックアップを使用して行われます。テーブルには、すべての可能な行/列で計算されたヒューリスティックスコアが含まれ、ボードの結果のスコアは、各行と列のテーブル値の合計です。

このボード表現は、動きとスコアリングのためのテーブルルックアップアプローチとともに、AIが短期間に膨大な数のゲームステートを検索できるようにします(2011年中頃のラップトップの1つのコアで毎秒10,000,000ゲームステート以上)。

expectimax検索自体は、「期待」ステップ(すべての可能なタイルスポーンの場所と値をテストし、最適化されたスコアに各可能性の確率で重み付け)と「最大化」ステップ(すべての可能な動きをテスト)を交互に繰り返す再帰的検索としてコード化されます。そして、最高のスコアを持つものを選択します)。ツリー検索は、以前に表示された位置(転置テーブルを使用)を見つけたとき、事前定義された深さ制限に達したとき、または可能性が非常に低いボード状態に達したときに終了します(たとえば、6 "4"タイルを取得して到達した場合)開始位置から続けて)。典型的な探索の深さは4-8手です。

経験則

最適化アルゴリズムを有利な位置に向けるために、いくつかのヒューリスティックが使用されます。ヒューリスティックの正確な選択は、アルゴリズムのパフォーマンスに大きな影響を与えます。さまざまなヒューリスティックが重み付けされ、位置スコアに結合されます。これにより、特定のボードポジションの「良さ」が決まります。次に、最適化検索は、すべての可能なボードポジションの平均スコアを最大化することを目指します。実際のスコアは、ゲームによって示されるように、ボードスコアの計算には使用されません。これは、タイルをマージするために重み付けが高すぎるためです(マージの遅延により大きなメリットが得られる場合)。

当初、私は2つの非常に単純なヒューリスティックを使用して、開いた四角形とエッジに大きな値を持つ「ボーナス」を付与しました。これらのヒューリスティックはかなりうまく機能し、頻繁に16384を達成しましたが、32768にはなりませんでした。

PetrMorávek(@xificurk)が私のAIを採用し、2つの新しいヒューリスティックを追加しました。最初のヒューリスティックは、ランクが増加するにつれて増加する非単調な行と列を持つペナルティであり、小さな数の非単調な行がスコアに強く影響しないことを保証しますが、大きな数の非単調な行はスコアに大きな影響を与えます。2番目のヒューリスティックは、オープンスペースに加えて、潜在的なマージ(隣接する等しい値)の数をカウントしました。これらの2つのヒューリスティックは、アルゴリズムを単調なボード(マージがより簡単)に向けて、および多数のマージがあるボードポジションに向けて(効果を高めるために可能な限りマージを揃えるように推奨する)のに役立ちました。

さらに、Petrは、「メタ最適化」戦略(CMA-ESと呼ばれるアルゴリズムを使用)を使用してヒューリスティックな重みも最適化しました。この場合、重み自体が調整され、可能な限り高い平均スコアが得られました。

これらの変更の影響は非常に重要です。アルゴリズムは、時間の約13%で16384タイルを達成することから、時間の90%以上で達成することになり、アルゴリズムは、時間の3分の1を超えて32768を達成し始めました(これまでのヒューリスティックは、一度に32768タイルを作成することはありませんでした)。 。

ヒューリスティックスにはまだ改善の余地があると思います。このアルゴリズムはまだ「最適」ではありませんが、かなり近づいているように感じます。


AIがゲームの3分の1以上で32768タイルを達成したことは、大きなマイルストーンです。人間のプレイヤーが公式ゲームで32768を達成した(つまり、savestatesやundoなどのツールを使用しなかった)かどうかを聞いて驚かされます。65536タイルは手の届くところにあると思います!

自分でAIを試すことができます。コードはhttps://github.com/nneonneo/2048-aiで入手できます


12
@RobL:2は90%の確率で出現します。4は10%の確率で出現します。それはソースコードにありますvar value = Math.random() < 0.9 ? 2 : 4;
nneonneo 14

35
現在Cudaに移植しているため、GPUはさらに高速で動作します!
nimsson 2014

25
@nneonneo私はあなたのコードをemscriptenでjavascriptに移植しました、そしてそれ今やブラウザでとてもうまくいきます!コンパイルする必要もなく、すべてを見る必要もなく、見るのがクールです... Firefoxでは、パフォーマンスは非常に優れています...
reverse_engineer

7
4x4グリッドの理論上の制限は、実際には65536ではなく131072です。ただし、適切な瞬間に4を取得する必要があります(つまり、ボード全体が4 .. 65536で一度に満たされる-15のフィールドが占有されます)。あなたが実際に組み合わせることができるように瞬間。
Bodo Thiesen、2015

5
@nneonneo 60%のゲームで32kに到達するAIをチェックすることをお勧めします:github.com/aszczepanski/2048
cauchy

1253

私は、他の人がこのスレッドで言及したAIプログラムの作成者です。AIの動作を表示したり、ソースを読んだりできます。

現在、このプログラムは、私のラップトップのブラウザーでJavaScriptで実行される約90%の勝率を達成しています。1移動あたりの思考時間は約100ミリ秒なので、完全ではありませんが(まだ!)

ゲームは離散状態空間、完全な情報、チェスやチェッカーなどのターンベースのゲームなので、これらのゲームで機能することが証明されているのと同じ方法、つまりアルファベータ剪定によるミニマックス 検索を使用しました。そのアルゴリズムについてはすでに多くの情報があるので、静的評価関数で使用する2つの主なヒューリスティックについて説明します。これは、他の人々がここで表現した直観の多くを形式化しています。

単調性

このヒューリスティックは、タイルの値がすべて、左/右方向と上/下方向の両方に沿って増加または減少していることを確認しようとします。このヒューリスティックだけでも、他の多くの人が述べた直感を捕らえます。より高い値のタイルは隅に集めるべきです。通常、小さい値のタイルが孤立するのを防ぎ、ボードを整理して、小さいタイルを重ねて大きいタイルに埋めます。

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

完全に単調な2048ボード

滑らかさ

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

Hacker Newsのコメンターは、グラフ理論の観点からこのアイデアを興味深い形式化しまし

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

完全に滑らかな2048ボード

無料のタイル

そして最後に、ゲームボードが窮屈になりすぎるとオプションがすぐになくなるため、フリータイルが少なすぎるとペナルティが生じます。

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

編集:

これは、このアプローチの威力のデモンストレーションです。私はタイル値のキャップを外し(2048に達した後も継続しました)、8回の試行後の最良の結果がここにあります。

4096

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


89
「2」と「4」のタイルを配置したコンピューターを「対戦相手」として扱うことができます。
Wei Yen

29
@WeiYenもちろん、コンピューターが意図的にスコアを最小化するのではなく、特定の確率でランダムにタイルを配置するため、それをminmax問題と見なしてもゲームロジックに忠実ではありません。
koo

57
AIがランダムにタイルを配置しても、目標は失うことではありません。不運になることは、相手があなたのために最悪の動きを選択するのと同じことです。「最小」の部分は、不運になる可能性のあるひどい動きがないように、保守的にプレイすることを意味します。
FryGuy 2014年

196
2048のフォークを作成するというアイデアがありました。コンピューターは、2と4をランダムに配置する代わりに、AIをランダムに使用して、値を配置する場所を決定します。結果:まったく不可能。ここで試すことができます:sztupy.github.io/2048-Hard
SztupY

30
@SztupYうわー、これは悪です。qntm.org/hatetris Hatetris を思い出します。これは、あなたの状況を最も改善する部分を配置しようとします。
Patashu、2014年

145

ハードコードされたインテリジェンスを含まないこのゲームのAIのアイデアに興味を持った(つまり、ヒューリスティックス、スコアリング関数など)。AIはゲームルールのみを「認識」し、ゲームプレイを「把握」する必要があります。これは、ほとんどのAI(このスレッドのAIと同様)とは対照的です。この場合、ゲームプレイは、本質的に、ゲームに対する人間の理解を表すスコアリング関数によって力ずくで操作されます。

AIアルゴリズム

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

1回の移動あたり100回の実行(つまり、メモリゲームで)で、AIは2048タイルを80%の時間、4096タイルを50%の時間で達成します。10000ランを使用すると、2048タイルが100%、4096タイルが70%、8192タイルが約1%取得されます。

実際に見る

最高のスコアがここに表示されます:

最高のスコア

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

このグラフは、この点を示しています。青い線は、各移動後のボードのスコアを示しています。赤い線は、その位置からのアルゴリズムの最高のランダムラン終了ゲームのスコアを示しています。本質的に、赤の値は青の値を上向きに「引き」、アルゴリズムの最良の推測であるためです。赤い線が各点で青い線のほんの少し上にあるのは興味深いことですが、青い線はますます増え続けています。

スコアリンググラフ

アルゴリズムが実際に良い動きを予測して、それを生み出す動きを選択する必要がないことは、かなり驚くべきことです。

後で検索すると、このアルゴリズムが純粋なモンテカルロツリー検索アルゴリズムに分類される可能性があることがわかりました。

実装とリンク

まず、ここで実際に動作するJavaScriptバージョンを作成しました。このバージョンでは、数百回の実行を適切な時間で実行できます。詳細については、コンソールを開いてください。(ソース

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

結果

驚いたことに、実行回数を増やしても、ゲームのプレイは大幅には改善されません。この戦略には、4096タイルとそれより小さいすべてのタイルで約80000ポイントの制限があり、8192タイルの達成に非常に近いようです。100から100000までのランの数を増やすと増加オッズ(5%から40%)、このスコアの限界になってそれを突破しないのです。

10000ランを実行すると、クリティカルポジションの近くで一時的に1000000に増加し、このバリアを1%未満の時間で破ることができ、最大スコア129892と8192タイルを達成しました。

改善点

このアルゴリズムを実装した後、私は最小または最大スコア、または最小、最大、および平均の組み合わせの使用を含む多くの改善を試みました。深度も使用してみました。移動ごとにKランを試行する代わりに、所定の長さ(「上、上、左」など)の移動リストごとにK移動を試行し、最高スコアの移動リストの最初の移動を選択しました。

後で、与えられたムーブリストの後にムーブをプレイできるという条件付き確率を考慮したスコアリングツリーを実装しました。

ただし、これらのアイデアのいずれも、単純な最初のアイデアよりも優れた点はありませんでした。これらのアイデアのコードは、C ++コードでコメント化したままにしました。

「ディープサーチ」メカニズムを追加しました。これにより、ランのいずれかが誤って2番目に高いタイルに到達したときに、ラン数が一時的に1000000に増加しました。これは時間の改善をもたらしました。

AIのドメイン非依存性を維持する他の改善アイデアがあるかどうか知りたいです。

2048バリアントとクローン

面白くするために、AIをブックマークレットとして実装し、ゲームのコントロールにフックしました。これにより、AIは元のゲームとその多くのバリアントで動作することができます。

これは、AIのドメインに依存しない性質により可能です。Hexagonalクローンなど、一部のバリアントはまったく異なります。


7
+1。AIの学生として、これは本当に興味深いものでした。空き時間にこれをよく見ていきます。
アイザック

4
これは素晴らしいです!expectimaxの優れたヒューリスティック関数の重みの最適化に何時間も費やしただけで、これを3分で実装すると、これで完全に破壊されます。
ブレンダン・アナブル2014年

8
モンテカルロシミュレーションの素晴らしい使い方。
nneonneo 2014年

5
この演奏を見ていると悟りが必要です。これはすべてのヒューリスティックを吹き飛ばしますが、それでも機能します。おめでとう !
ステフェイン・グーリッホン

4
断然、ここで最も興味深いソリューション。
シバウ、2015

126

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

アルゴリズムを改良し、ゲームを打ち負かしました!最後の近くの単純な不運のために失敗する可能性があります(絶対に下に移動する必要があり、絶対に行わないでください。最高の位置にタイルが表示されます。一番上の行を塗りつぶすようにしてください。左に移動しても移動しません。パターンを破る)が、基本的には、固定部分と移動する部分があります。これはあなたの目的です:

終了する準備ができました

これは私がデフォルトで選択したモデルです。

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)
  }

不足しているステップに関するいくつかのポインタ。ここに:モデルチェンジ

予想されるモデルに近づく運が良かったため、モデルが変更されました。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が作成された場所に応じて右または上)押して、次のようになるまでチェーンを完了します。

チェーン完成

したがって、モデルとチェーンは次のように戻ります。

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

2つ目のポイントは、運が悪かったことと、その主な地位を獲得したことです。失敗する可能性がありますが、それでも達成できます。

ここに画像の説明を入力してください

ここにモデルとチェーンがあります:

  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

execute move with best score可能な次の状態から最高のスコアをどのように評価できますか?
Khaled.K 2014年

ヒューリスティックは、evaluateResult基本的には可能な限り最良のシナリオに最も近づくように定義されています。
Daren

@ダーレン私はあなたの詳細をお待ちしています
ashu

@ashu現在作業中です。予期しない状況により、終了する時間がありません。一方、私はアルゴリズムを改善し、75%の時間で解決しました。
Daren

13
この戦略で本当に気に入っているのは、手動でゲームをプレイするときにそれを使用できることです。これにより、最大37kポイントを獲得できました。
頭足類2014

94

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


私が提案するソリューションは、非常にシンプルで実装が簡単です。しかし、それは131040のスコアに達しています。アルゴリズムのパフォーマンスのいくつかのベンチマークが示されています。

スコア

アルゴリズム

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

私のアルゴリズムのベースとなる仮定はかなり単純です。より高いスコアを達成したい場合は、ボードをできるだけ整頓しておく必要があります。特に、最適な設定は、タイル値の線形かつ単調な減少順序によって与えられます。この直感により、タイル値の上限も得られますs。nはボード上のタイルの数です。

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

ボードを整理する2つの可能な方法を次の図に示します。

ここに画像の説明を入力してください

タイルの順序を単調な減少順に強制するために、スコア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

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください

T2の場合、10回に4回のテストで平均スコアs42000の4096タイルが生成されます。

コード

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


悪くありません、あなたのイラストは、マージベクトルを評価に取り入れることについてのアイデアを私に与えました
Khaled.K

こんにちは。GitHubページで提供される手順がプロジェクトに適用されることを確認しますか?私はそれを試してみたいのですが、それはAI自動実行ではなく、元のプレイアブルゲームの説明のようです。それらを更新できますか?ありがとう。
JDガンボア2018年

41

私の試みでは、上記の他のソリューションと同様にexpectimaxを使用していますが、ビットボードは使用していません。Nneonneoのソリューションは、10ミリオンの動きをチェックできます。これは、深さ約4で、残りのタイルは6タイルで、4動きが可能です(2 * 6 * 4)4。私の場合、この深さを探索するには時間がかかりすぎるので、残っている空きタイルの数に応じてexpectimax検索の深さを調整します。

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

ボードのスコアは、フリータイルの数の2乗と2Dグリッドのドット積の加重和で計算されます。

[[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>


3
なぜこれに賛成票がないのかわからない。それはそのシンプルさのために本当に効果的です。
David Greydanus

おかげで、遅い答えとそれは本当にほとんどうまくいきません(ほとんどの場合[
1024、8192

空のスペースにどのように重みを付けましたか?
David Greydanus

1
それは単純でcost=1x(number of empty tiles)²+1xdotproduct(snakeWeights,grid)あり、私たちはこのコストを最大化しようとします
caub 2015

@Robustoに感謝します。いつかコードを改善する必要があります。コードは簡略化できます
caub

38

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

コントローラーは、時間差学習(強化学習手法)のバリアントによってゼロから(人間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タイルを取得することはありません。


4
かなり印象的な結果。しかし、あなたのプログラムがこれをどのように達成するかを説明するために回答を更新できますか(大まかに、簡単な言葉で...私は完全な詳細がここに投稿するには長すぎると確信しています)?学習アルゴリズムがどのように機能するかの大まかな説明のように?
Cedric Mamo

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

5
私は、これと「上、右、上、左、...」(そして必要に応じて下)というささいな周期的戦略とを比較して、100,000ゲームを実行しました。サイクリック戦略はの「平均タイルスコア」を完了しましたが770.6、これはちょうど得ました396.7。なぜだと思いますか?左か右がもっと合流する時でさえ、それはあまりにも多くのアップを行うと思います。
Thomas Ahle 14

1
タイルは、複数の方向にシフトされていない場合、互換性のない方法でスタックする傾向があります。一般に、サイクリック戦略を使用すると、中央のタイルが大きくなり、操縦がはるかに窮屈になります。
bcdan

25

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

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

また、このアルゴリズムについて有用なハッカーニュースに関するディスカッションもあります。


4
これが一番の答えになるはずですが、実装に関する詳細を追加するとよいでしょう。たとえば、ゲームボードのモデル化(グラフとして)、採用された最適化(タイル間の差の最小-最大)など
Alceu Costa

1
将来の読者のために:これは、著者(ovolve)がここで2番目に高い回答で説明したものと同じプログラムです。この回答、およびこのディスカッションでのovolveのプログラムに関するその他の言及により、ovolveが登場し、彼のアルゴリズムがどのように機能するかを記述しました。その答えは、今1200のスコアを持っている
MultiplyByZer0

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)

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

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


2
これを後で編集して、ライブコード@ nitish712を追加します
Khaled.K

9
このアルゴリズムのwin%は何ですか?
cegprakash 2014年

なぜあなたは必要constantですか?スコアの比較のみを行っている場合、それらの比較の結果にどのように影響しますか?
bcdan 2015年

@bcdanヒューリスティック(別名比較スコア)は、チェスヒューリスティックが機能する方法と同様に、将来の状態の期待値の比較に依存しますが、これは線形ヒューリスティックですが、次の最適なN移動を知るためのツリーを構築しないためです
Khaled.K

12

これはOPの質問への直接の回答ではありません。これは、これまでに同じ問題を解決するために試してみて、いくつかの結果を得て、共有したいいくつかの観察結果があります。これからのさらなる洞察。

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

私は主に直感と上で説明したものから、いくつかのヒューリスティック評価関数の凸型の組み合わせ(異なるヒューリスティックな重みを試してみました)を適用しました。

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

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

ゲームをプレイするための4x4グリッドがあります。

観察:

最初のヒューリスティック関数または2番目のヒューリスティック関数に割り当てた重みが多すぎると、AIプレーヤーが取得するスコアはどちらも低くなります。私はヒューリスティック関数への可能な多くの重み割り当てで遊んで、コンベックスコンビネーションを採用しましたが、AIプレーヤーが2048を獲得することは非常にまれです。ほとんどの場合、1024または512で停止します。

コーナーヒューリスティックも試してみましたが、何らかの理由で結果が悪化します。直感がなぜですか?

また、検索深度のカットオフを3から5に増やしてみました(プルーニングを使用しても、そのスペースの検索が許容時間を超えているため、これ以上増やすことはできません)。隣接するタイルの値を見て、マージ可能であればさらにポイントが増えますが、それでも2048を取得できません。

私はミニマックスの代わりにExpectimaxを使用する方が良いと思いますが、それでもこの問題をミニマックスだけで解決して、2048や4096などの高いスコアを取得したいと思います。何か欠けているかどうかわかりません。

以下のアニメーションは、AIエージェントがコンピュータープレーヤーでプレイしたゲームの最後の数ステップを示しています。

ここに画像の説明を入力してください

事前のおかげで、どんな洞察も本当にとても役に立ちます。(これは記事の私のブログ投稿のリンクです: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のスコアを取得できるゲームの最後の数ステップを示しています。今回は絶対値ヒューリスティックも追加しています。

ここに画像の説明を入力してください

次の図は、プレーヤーのAIエージェントが、コンピューターを1つのステップで敵対的であると想定して探索したゲームツリーを示しています。

ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください ここに画像の説明を入力してください


9

Haskellで2048ソルバーを書いたのは、主に今この言語を学んでいるからです。

私のゲームの実装は実際のゲームとは少し異なります。新しいタイルは常に「2」です(90%2および10%4ではありません)。そして、新しいタイルはランダムではありませんが、常に左上から最初に利用可能なタイルです。このバリアントは、Det 2048とも呼ばれます。

結果として、このソルバーは確定的です。

空のタイルを優先する徹底的なアルゴリズムを使用しました。深さ1〜4ではかなり高速に実行されますが、深さ5では、1移動あたり約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


実際のルールで拡張してみてください。Haskellの乱数発生器について学ぶのは良い挑戦です!
トーマスアーレ2014

私はHaskellがそれをしようとすることに非常に不満を感じましたが、おそらくもう一度試してみるつもりです!無作為化しないと、ゲームがかなり簡単になることがわかりました。
wvdz 14

ランダム化がなければ、常に16kまたは32kを取得する方法を見つけることができると確信しています。ただし、Haskellでのランダム化はそれほど悪くはありません。「シード」を回避する方法が必要です。明示的に行うか、Randomモナドを使用します。
トーマスアーレ2014

ランダムでないゲームで常に16k / 32kに到達するようにアルゴリズムを
改良

おっしゃるとおり、思ったより難しいです。私はこのシーケンスを見つけることができました:[UP、LEFT、LEFT、UP、LEFT、DOWN、LEFT]常にゲームに勝ちますが、2048を超えません(合法的な移動がない場合、サイクルアルゴリズムは単に次の時計回りの順序)
Thomas Ahle

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")
  }

10
random from (right, right, right, down, down, up) すべての動きが同じ確率であるとは限らないので、あなたはそれがよりうまく機能します。:)
Daren

3
実際、ゲームに完全に慣れていない場合は、基本的にこのアルゴリズムが行うように、3つのキーのみを使用することが本当に役立ちます。一見するとそれほど悪くはありません。
数字:

5
はい、それは私自身のゲームでの観察に基づいています。4番目の方向を使用する必要があるまで、ゲームは実際には何の観察もせずに自動的に解決します。この「AI」は、ブロックの正確な値を確認せずに512/1024に到達できるはずです。
API-Beast

3
適切なAIは、どうしても一方向にしか移動できない状態にならないようにします。
API-Beast

3
3つの方向のみを使用することは、実際には非常に適切な戦略です。2048年に近づき、手動でゲームをプレイしました。これを他の3つの動きの間で決定するための他の戦略と組み合わせると、非常に強力になる可能性があります。選択を3に減らすとパフォーマンスに大きな影響があることは言うまでもありません。
wvdz 14

4

他の答えの多くは、可能性のある未来、ヒューリスティック、学習などの計算コストの高い検索でAIを使用しています。これらは印象的で、おそらく正しい方法ですが、別のアイデアに貢献したいと思います。

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

例えば:

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

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

この問題を解決するには、2つの移動方法があり、それが放置されたり悪化したりすることはありません。両方の可能性を調べると、すぐに多くの問題が明らかになる可能性があります。これにより、依存関係のリストが形成されます。各問題は、最初に別の問題を解決する必要があります。次の手を決定するとき、特に行き詰まったときに、私はこのチェーンまたは場合によっては依存関係のツリーを内部に持っていると思います。


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

方法で大きいタイル:小さい周囲のタイルの値を増やします。

等...


アプローチ全体はこれよりも複雑になる可能性がありますが、それほど複雑ではありません。スコア、重み、ニューロン、そして可能性の深い探求に欠けている感覚で、これはこの機械的なものかもしれません。可能性のツリーは、分岐をまったく必要とするほど大きくなくてはなりません。


5
ヒューリスティックを使用したローカル検索について説明しています。それは行き詰まってしまうので、次の動きに備えて事前に計画する必要があります。これにより、ソリューションの検索とスコアリングにもつながります(決定するため)。したがって、これは他の提示されたソリューションと実際に違いはありません。
runDOSrun 2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.