ゲーム「2048」をPythonで自作してAIに解かせる(3) ~ついに「2048」をAIに解かせる~

ゲーム「2048」をPythonで自作し、自作のAIに解かせます。
これ自体はありふれたテーマで、ググれば色々出てくるのですが、僕みたいな初心者向けに書かれた詳しい記事は見つからなかったので、敢えて書いてみることにしました。

この記事では、ついにPythonでAIを自作し「2048」を解かせます。

なおこの記事は続編です。前の記事は以下のリンクからどうぞ。

[伝説のゲーム「2048」をPythonで自作してAIに解かせる(1) ~Webページ上の「2048」をプログラミングで操ってみる~][1]
[1]:https://qiita.com/masa_ramen/items/b497e547a1298a905a2e
[伝説のゲーム「2048」をPythonで自作してAIに解かせる(2) ~Pythonで「2048」を自作する~][2]
[2]:https://qiita.com/masa_ramen/items/ac31db0d20ad88cff09a

# 環境
- MacBook Pro (15-inch, 2016)
- OS: macOS High Sierra (10.13.5)
- Python 3.6.4

```
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.13.5
BuildVersion:	17F77
$ python --version
Python 3.6.4 :: Anaconda custom (64-bit)
```

# 前記事までのおさらい
前回までの記事で、
①Seleniumというライブラリを使って[Web上の「2048」][3]を制御する方法を学び、
②「2048」を自作
しました。
[3]:https://gabrielecirulli.github.io/2048/

また、AIに「2048」を解かせるためには
**盤面を認識して、
ある方向を送信した時の結果（=「1手先」の盤面）を計算して、
それがどれくらい「良い」かを評価し、一番「良い手」を選び続ける
プログラム**
の作成が必要であるとの結論に至りました。

では、順を追って実装していきましょう。

# 盤面を認識する
盤面の認識には、Seleniumライブラリを使用します。
特定のクラスをもつ要素を抜き出すメソッドを利用し、タイルの数字（「2048」の盤面の数字のこと）を抜き出します。
ここではterminalを使って試してみましょう。

```
$ python
Python 3.6.4 |Anaconda custom (64-bit)| (default, Jan 16 2018, 12:04:33) 
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>　from selenium import webdriver
>>>　browser = webdriver.Firefox(executable_path="/Users/hoge/geckodriver/geckodriver")# geckodriverのpathを書く
```

以上のように実行すれば、Firefoxが起動します。
ここで、webdriver.Firefox（）の返り値を調べてみましょう。

```
>>>　type(browser)
<class 'selenium.webdriver.firefox.webdriver.WebDriver'>
```
というわけで、Webdriverというデータ型でした。
このオブジェクトには、特定のクラスをもつ要素を抜き出すメソッド`find_element_by_class_name('name')`があります。

よって、タイルの数字に対応するHTMLの部分が分かれば、上のメソッドによって数字を抜き出せます。
それを探しましょう。

Firefoxで「2048」のページを開き、右クリックから「要素を調査」を選択します。
（Chromeの場合は、右クリックの後「検証」を選択します。）
すると、下の図1のようにタイルの数字の場所がわかります。


<img width="965" alt="スクリーンショット 2018-08-16 17.34.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/164223/eed62fe8-33c6-8997-752b-720dffe8f7e6.png">
図1. Chromeで「2048」のページを「検証」した図

どうやら、'tile-container'の中の'tile-position-3-1'を調べれば良さそうです。
実際にこれを抜き出してみましょう。

terminalで以下のように実行してください。
なお、以下の操作では（図1のような場面で）一番上の行の右から二番目の「2」を抜き出しています。
これは3列目の1行目なので、HTMLでは「tile-position-3-1」に対応します。
みなさんが実行する際には、タイルの「2」の位置に応じてコードの
`>>>　tile_3_1 = tile_container_elem.find_elements_by_class_name('tile-position-3-1') # ここはタイルの位置に応じて変更してください
`の部分を書き換えてください。

```
$ python
Python 3.6.4 |Anaconda custom (64-bit)| (default, Jan 16 2018, 12:04:33) 
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>　from selenium import webdriver
>>>　browser = webdriver.Firefox(executable_path="/Users/hoge/geckodriver/geckodriver")# geckodriverのpathを書く
>>>　browser.get('https://gabrielecirulli.github.io/2048/')
>>>　html_elem = browser.find_element_by_tag_name('html')
>>>　tile_container_elem = browser.find_element_by_class_name('tile-container')
>>>　tile_3_1 = tile_container_elem.find_elements_by_class_name('tile-position-3-1') # ここはタイルの位置に応じて変更してください
>>> print(tile_3_1[0].text) #　タイルの数字をprint
2
```

これでタイルの数字を抜き出せるようになりました。
実際のプログラムでは、タイルが存在しない場合にも対応できるように

```python:get_tile_example.py
    try:
        tile_1_1 = tile_container_elem.find_elements_by_class_name('tile-position-1-1')
        try:
            board['1-1'] = tile_1_1[2].text
        except:
            board['1-1'] = tile_1_1[0].text
    except:
        logging.debug('1-1: None')
        board['1-1'] = '0'
```

などと書きます。
ここでは、取得したタイルの数字をboardという辞書に格納しています。
boardには

```python:board.py
board = {'1-1':'0','1-2':'0','1-3':'0','1-4':'0',\
    '2-1':'0','2-2':'0','2-3':'0','2-4':'0',\
    '3-1':'0','3-2':'0','3-3':'0','3-4':'0',\
    '4-1':'0','4-2':'0','4-3':'0','4-4':'0'}# 初期設定は全て0
```

のように各タイルの位置と数字を格納していきます。

なお、上記と全く同様な方法で、現在獲得しているスコアの値も抜き出せます。

# 一気に実装する
ここまでくれば、あとは地道に頑張るだけです。

プログラムの流れは以下のようになっています。
①Seleniumを使用してFirefoxを起動、「2048」のページへ移動
②ページの更新を待つために0.3秒待つ
③現在の局面を取得し、`board`に格納
④もし「上」方向を送信したらどんな局面になるか計算（＝「1手先」を読む）
⑤その局面を評価関数に渡し、その局面の「良さ」を数値に変換
⑥同様に、「右」「下」「左」についても④と⑤を実行
⑦最も「良い」と判断された方向を送信（＝評価値が最も高い手を選ぶ）
⑧現在のスコアを取得、表示
⑨②〜⑧を繰り返す

これを愚直に書いていけば、[こんな風に][4]して「2048」を解くAIの完成です！
[4]:https://twitter.com/masa_ramen/status/962957952683450368

# コード
流石に長すぎるので、GitHubを使わせてください。
以下でコードをダウンロードできます。
GitHubを使ったことがない方は、緑色の「Clone or download」ボタンから「Download Zip」を選ぶとダウンロードできます。
python_2048_solve1.pyというファイルです。
966行目の
`browser = webdriver.Firefox(executable_path="/Users/hoge/geckodriver/geckodriver")# geckodriverのpathを書く`
をご自身のパスに書き換えて実行してみてください。

https://github.com/masa-ramen/python_2048.git

# 評価関数を変える
今回の記事では、評価関数の中身については全く触れませんでした。
みなさん、ぜひ評価関数の部分を変更して実行してみてください。
もしよければ、本記事のコメント欄でそのAIの最高スコアを報告してくれたら嬉しいです。
（例：「右」を押した回数を評価値にしたら○○○○点とれました）

# 参考文献
1） 2048 （2048が実際に遊べます。iPhone6とMacBook Proで動作確認しました。2018/8/9にアクセスを確認しました。）
https://gabrielecirulli.github.io/2048/
2） Al Sweigart 著、相川　愛三 訳(2017)　『退屈なことはPythonにやらせよう ―ノンプログラマーにもできる自動化処理プログラミング』 オライリージャパン