【Python】Seleniumでやさしい神経衰弱ゲームを最速攻略する🎮

弊社[mori_goq](https://qiita.com/mori_goq)の作成した[やさしい神経衰弱ゲーム](https://qiita.com/mori_goq/items/5607f99ca7eb0e615cbe)をやってみたところ、過去にワギャンランド🦖で鍛えたはずの記憶力の衰えを実感し、悔しくなったため技術力で攻略してやろうと思いました。
![スクリーンショット 2023-01-29 18.28.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359978/a1dbd6e3-5767-deeb-cd22-8f10b328a949.png)

## 攻略本を読む📕
リポジトリという名の攻略本が公開されているので読み解いていきます。

https://github.com/mori-goq/easy-memory-game

### シャッフルの待機時間⏳

```tsx
const SHUFFLE_COUNT = 3
const SHUFFLE_DURATION = 500

// シャッフル
for (let _ of [...new Array(SHUFFLE_COUNT)]) {
  cardList = arrayShuffle(cardList)
  // シャッフルの動作を待つ
  await new Promise(resolve => setTimeout(resolve, SHUFFLE_DURATION + 200))
}
```

ゲームスタートボタンをクリックして、シャッフルは700ms毎に3回行われるようなので、シャッフル後にカードの絵柄がクリックできるようになるのは2.1秒後ということになります。  
ただし、実際に配置が決定するのは3周目で`cardList = arrayShuffle(cardList)`が行われたタイミングなので、1.4秒後に決定していることになるはず。  
2.1秒後にカードの配置を調べていたら若干のタイムロスが発生するため、上記の1.4秒経過時点でカードがどこに設置されてるのか確認する処理を行なっておくことが、最速攻略のためには重要そうです🃏

### カードの配置を調べる🕵️
```tsx
<div class="box card-board">
  {#each cardList as card (card.id)}
    <div animate:flip="{{ duration: SHUFFLE_DURATION }}">
      <button
        type="button"
        class="mb-0 button card-button"
        data-is-front={card.isChecked || card.isMatch}
        disabled={isLockCard || card.isChecked || card.isMatch}
        on:click={() => handleClickCard(card.id)}
      >
        <img src={card.imageSrc} class="card-image" alt="#" />
        <img src={imageBack} class="card-image" alt="#" />
      </button>
    </div>
  {/each}
</div>
```
表側の絵柄は`.card-board`の`.card-button > img[1]`のファイル名で判別することができます。  
下記スクショのファイル名の頭2文字さえわかればOKです🙆
![スクリーンショット 2023-01-29 18.29.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359978/71c44f7c-a2ab-b0ab-3f53-5661e8f3899a.png)

https://github.com/mori-goq/easy-memory-game/tree/main/src/lib/images

## 攻略🥷
まずは結果をご覧ください。
*画像が小さいので気になる方は後述のコードをお試しください*
![easy-memory-game.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/359978/b2d239ed-5f7f-29f1-b96c-b49388fd9f55.gif)
🎉🎉🎉**Congratulations**🎉🎉🎉
```
処理にかかった時間:0.11477184295654297
クリックにかかった時間:0.29099583625793457
ページ開いてからの時間:2.433328866958618
```

### コード⌨️
```python
import re
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager

driver = webdriver.Chrome(ChromeDriverManager().install())

# やさしい神経衰弱ゲームのページを開く
driver.get('https://easy-memory-game.vercel.app/')

# ページ開いてからの時間計測
all_start = time.time()

# ゲームスタート！
start_button = driver.find_element(By.XPATH, '//button[contains(@class, "is-primary")]')
start_button.click()

# 待機時間
WAIT_TIME = 2.1
FIRST_TIME = 1.4

# 1.4秒待ってカードの位置を調べる
time.sleep(FIRST_TIME)

# 処理前の時間
start = time.time()

# 左上からカードの絵柄を調べる
imgs = driver.find_elements(By.XPATH, '//button[contains(@class, "card-button")]/img[1]')
click_list = [{
    'index': i,
    'name': re.match('.*?(\d+-).*', card.get_attribute('src')).group(1)
} for i, card in enumerate(imgs)]

# ファイル名順にソートして、クリックする順番を決定
click_list = sorted(click_list, key=lambda x: x['name'])

# 処理後の時間
end = time.time()

# 処理時間にかかった時間を算出
diff = end - start
print(f'処理にかかった時間:{diff}')

# 残待機時間(0.7秒)から処理時間引いた分待機する
time.sleep(WAIT_TIME - FIRST_TIME - diff)

# クリックの処理の時間計測
start = time.time()

# クリックあるのみ
for item in click_list:
    driver.find_elements(By.XPATH, f'//button[contains(@class, "card-button")]')[item['index']].click()

end = time.time()

print(f'クリックにかかった時間:{end - start}')
print(f'ページ開いてからの時間:{end - all_start}')
```

<details>
<summary>一応、requirements.txtの中身も。</summary>

```
async-generator==1.10
attrs==22.2.0
certifi==2022.12.7
charset-normalizer==3.0.1
exceptiongroup==1.1.0
h11==0.14.0
idna==3.4
outcome==1.2.0
packaging==23.0
PySocks==1.7.1
python-dotenv==0.21.1
requests==2.28.2
selenium==4.8.0
sniffio==1.3.0
sortedcontainers==2.4.0
tqdm==4.64.1
trio==0.22.0
trio-websocket==0.9.2
urllib3==1.26.14
webdriver-manager==3.8.5
wsproto==1.2.0
```
</details>

### 解説🙏
大体は[攻略本を読む](#%E6%94%BB%E7%95%A5%E6%9C%AC%E3%82%92%E8%AA%AD%E3%82%80)に書いてる通りですが、絵柄を調べるところだけ解説しておきます。（大したことはしてないですが）

`.card-button`を1つずつ順番に調べていき、絵柄のファイル名(`name`)とindex値(`index`)を保持します。  
全部書くと長いので、今回は3絵柄とします。（2絵柄だと逆にわかりにくいので）
```python
click_list = [
    {
        'index': 0,
        'name': '03-'
    },
    {
        'index': 1,
        'name': '01-'
    },
    {
        'index': 2,
        'name': '03-'
    },
    {
        'index': 3,
        'name': '02-'
    },
    {
        'index': 4,
        'name': '02-'
    },
    {
        'index': 5,
        'name': '01-'
    }
]
```

そして`name`の値でソートをかけます。
```python
click_list = [
    {
        'index': 1,
        'name': '01-'
    },
    {
        'index': 5,
        'name': '01-'
    },
    {
        'index': 3,
        'name': '02-'
    },
    {
        'index': 4,
        'name': '02-'
    },
    {
        'index': 0,
        'name': '03-'
    },
    {
        'index': 2,
        'name': '03-'
    }
]
```
こうすることで絵柄のペアごとにクリックする順番が確定できます。  

上記の場合、こんな感じ。
1. `.card-button[1].click()`→`.card-button[5].click()`で絵柄マッチ
2. `.card-button[3].click()`→`.card-button[4].click()`で絵柄マッチ
3. `.card-button[0].click()`→`.card-button[2].click()`で絵柄マッチ

## まとめ🍰
まだ試していないことがいくつかあり、これが最速ではない気はしていますが、ぱっと見では0秒で終えてるのでひとまずはここまでとします。  
また機会があれば改善していくつもりです。

## 最後に
### GoQSystemでは一緒に働いてくれる仲間を募集中です！
ご興味がある方は以下リンクよりご確認ください。

https://goqsystem.recruit.site/?qiita=1d1ca3f4d7dd60b12c42