VercelでLineボットを作って総理大臣の名前を暗記するゲームをやってみる

今までQiitaに3つ記事を書いたんですけど、そいつらが時代に取り残されている気がしてたんですよ。エラストテネスとか紀元前3世紀じゃねえか。だから今回はちょっと若くなろうと思って、Lineボットを作ってみました。内容はシンプルです。スクレイピングしてきた内閣総理大臣の一覧を使って、古今東西・内閣総理大臣の名前ゲームをしてくれるチャットボットです。

<img alt="IMG_6951.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2866483/bf3351fa-d7c3-429f-4fb5-99c8371c8de4.png" width=25%>
<img alt="IMG_6952.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2866483/44c00ced-aef9-a649-a398-2e43ae0befc9.png" width=25%>

ボットが句読点打ってる時点で若くないかもしれんが、気にせず説明していくよ！

## 構想

こちらのサイトを参照して、やってみようかと思いました。

https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d

実は当初、しりとりBOTを作るつもりでした。でも、しりとりできるだけの名詞の一覧は件数が膨大だし奥が深い。それにこのVercel、現時点では無料プランで素晴らしい使い心地なのですが、DBはないのでデータ格納先は別で探す必要があります(Herokuは有料化するらしいし)。今回は試しにやりたいだけなので、特定のテーマに沿った言葉の一覧を言い合う、古今東西ゲームに落ち着きました。それでも何らかの意義は持たせたいので、受験勉強などで暗記したい需要がありそうなテーマを選んだというわけです。(政治的意図は無いです。)

<img alt="renkei2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2866483/e2632f65-89eb-bc86-8edb-ecf1cda6736e.png" width=80%>

連携のイメージはこんな感じ。VercelはGitHubのアカウントがあればワンクリックでアカウント作成できて、一瞬でリポジトリと連携してデプロイできます。そして、Line Developerアカウントで、Messaging APIの設定をして、そこにVercelのWebhook URLを登録するという形です。総理大臣一覧はテーブル構造が微妙なWikipediaからではなくADEAC様からいただきました。目録・一覧はクリエイティブコモンズライセンスとのこと。データ取得は最初に一度やっただけです。

## データ取得

今回はそのつど連携する必要はないデータのため、別処理で作成しました。もし連携するのであれば、APIがあるものを利用し、なるべくスクレイピングするべきではないですね。PythonとBeautifulSoupでやりました。

```python getdata.py
from urllib.request import urlopen
from bs4 import BeautifulSoup
import json

html = urlopen("https://trc-adeac.trc.co.jp/Html/SystemRef/nsd.html")
bsObj = BeautifulSoup(html, "html.parser")
table = bsObj.findAll("table")[0]
rows = table.findAll("tr")
names=[]
for row in rows:
    cells = row.findAll(["td","th"])
    names.append(cells[1].get_text())

uniquenames = set(names)
uniquenames.remove('姓名')
uniquenames.remove('年号')
tmpdata=[]
for name in uniquenames:
    tmpdata.append({"name":name.replace("　",""), "flag":0})
with open("./api/souri.json",'w',encoding='utf-8') as file:
    json.dump(tmpdata,file,indent=4,ensure_ascii=False)
```

参照先の表では、求めたい名前は2列目にありました。2列目だけを取得し、全角スペースとヘダーを除いて、重複(2期以上の総理経験者)をset()でユニークにしています。BOTのアプリで使うためのフラグを付けてJson形式で保存。これを実行すると、以下のような形になりました(抜粋)。
```json souri.json
[
    {
        "name": "宇野宗佑",
        "flag": 0
    },
    {
        "name": "芦田均",
        "flag": 0
    },
```

## Vercel設定

こちらのサイトを参考にさせていただきました。

https://qiita.com/n0bisuke/items/a5f49193fa68f28a7f25

環境は以下です。少し上記よりも新しいバージョンと思いますが、問題なく動いています。
node: 16.17.1
npm: 8.15.0 
@line/bot-sdk: 7.5.2
express: 4.18.1
lowdb: 3.0.0

Vercel.json、上記参考サイトから書き換えたのはregionsのみです。デフォルトだと北米でした。

```json vercel.json
{
    "version": 2,
    "regions": [
        "hnd1"
    ],
    "routes": [
        { "src": "/", "dest": "api/server.js" },
        { "src": "/webhook", "dest": "api/server.js" }
    ]
}
```

先ほど作成したsouri.jsonをこれで読み込みます。ローカルのパス指定だとVercelで動かないので、サーバー上のURLの指定に変えないといけないですね。

```javascript server.js
const __dirname = dirname(fileURLToPath(import.meta.url))
const orgFile =  join(__dirname,'souri.json')
const orgData = JSON.parse(fs.readFileSync(orgFile, 'utf8'))
```

## 会話設計

ざっくり以下のパターンで作りました。

|状況|残り|インプット|返事|
|---|---|---|---|
|勝負前  |-  |「古今東西」  |ゲーム開始。最初の1件をランダムで返事  |
|勝負前  |-  |それ以外の言葉  |「古今東西」でゲームを始める案内  |
|勝負中  |3以上  |正解回答  |正解分を一覧から減らして、次の1件をランダムで返事  |
|勝負中  |2件  |正解回答  |正解分を一覧から減らし、最後の1件を返事しゲーム終了  |
|勝負中  |1件  |正解回答  |最後がプレイヤーなら、プレイヤーの勝ちでゲーム終了 |
|勝負中  |2以上  |「スキップ」  |次の1件をランダムで返事しゲーム続行  |
|勝負中  |1件  |「スキップ」  |最後の1件を返事したらゲーム終了  |
|勝負中  |-  |使用済み回答 |使用済みと告げ、残り件数と「降参」か「スキップ」案内  |
|勝負中  |-  |「降参」  |ゲーム終了しプレイ前に戻す  |
|勝負中  |-  |それ以外の言葉  |残り件数と、「降参」か「スキップ」の案内  |
|勝負中|-  |放置  |セッションが切れたら自動で最初から |

これを繰り返すと、総理大臣の名前が覚えられます。拡張するとしたら、答えた総理の紹介データへのリンクを追記したりすることもできますし、他のテーマの名称一覧を追加して、冒頭に別ゲームを選べるようにしてもいいかと。

## テストで見つかった問題とその対処

- まず気が付いたのは、放置していた時にカウントが元に戻ってしまうことでした。しばらく経ってから続きをしようとしても、最初の言葉が返ってくる。デフォルト寿命は300000ミリ秒(5分)だと思います。初期化前に告知(push)してあげた方が良いかもしれません。

- さらに、他の人に試してみてもらったところ、同時プレイ時の問題が分かりました。このゲーム、上記の設計の通り、ゲームが始まると「プレイ中」のフラグを立てて動いています。しかし、その内容は、同時にプレイしている他の人も更新できてしまいます。すでに言った単語のフラグも同様ですね。セッション管理が必要になります。

1個目の問題は、実際のゲームでもすぐ回答するものだし、致命的ではないかもしれません。でも2個目はゲーム体験自体が損なわれるため、仮管理でも対処が必要です。以下をやることにしました。

- アクセスしてきたLineのユーザーごとにレコードを保存する
- そのレコード内に名前一覧を持たせ、言った言葉を更新する
- ゲーム終了したらユーザーのレコードを削除する

Vercel上のみでライトに完結させたいのですが、DBを使わないとなると、JavaScriptの配列をこねくり回すことになります。どんどんコードが煩雑になっていき、一晩寝てから考えを改めました。軽量DB機能として「lowdb」を使うことにします。

https://github.com/typicode/lowdb

```javascript
import { Low, JSONFileSync } from 'lowdb'
const file = "/tmp/users.json"
const adapter = new JSONFileSync(file)
const db = new Low(adapter)

await db.read()
db.data ||= { posts: [] }             // Node >= 15.x
const { posts } = db.data
posts.push({user:userId,nameLists:orgData})
await db.write()
```

上記ユーザーIDは、Lineのメッセージイベントのevent.source.userIdです。これをキーにして、なんちゃってセッション管理をします。このデータに対して、上記の会話設計に応じたCRUD処理を行えば大丈夫でした。いちおう、LowDBを使う上で引っかかったポイントは以下です。

- lowdbはESMパッケージのため、import構文しか受け付けない。元サイトのrequireを書き換え 
- Vercel上のjsonファイルは通常書き込み不可になるので"/temp/user.json"とtemp下を指定


## 感想

今回は以上です。若者になったつもりでLineボットのやり方を勉強してみたのですが、書いたコードの量はそこまで多くなくて、環境の作り方や、もっと手前のアイデアで悩むことの方が多かったです。JavaScriptの配列と非同期の構文でかなり精神力を消耗しましたが、それについても、楽をするツールを早めに探すべきでした。今後の技術者は、そういった環境面についての勉強が重要になっていくんでしょうね。

おまけ。Stable Diffusionで生成した内閣総理大臣の一覧(呪文：List of Prime Ministers of Japan, grid, black and white, caricature style)をBOTのプロフィールにセットしました。

![list.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2866483/5e23bfaf-ed57-4f64-ac57-f252513e4f2f.jpeg
)

セキュリティ・スケール面での考慮をしていないのでBOTは公開できませんが、
トークン等を消した参考テンプレートとして、ソースを以下にアップしておきました。
https://github.com/KentAnak/WordGameBotTemplate