DiscordBotでヒットアンドブロー

今回はPythonのDiscord.pyを使ってNintendoSwitchの「世界のアソビ大全」にもある、「ヒットアンドブロー」というゲームのBotを作ってみた
[ヒットアンドブローとは](https://ja.wikipedia.org/wiki/%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC%E3%83%9E%E3%82%A4%E3%83%B3%E3%83%89)

#流れ
##[DEVELOPER PORTAL](https://discord.com/developers/applications/) から、新しいアプリケーションを作成
これについては、[この記事](https://qiita.com/1ntegrale9/items/cb285053f2fa5d0cccdf)でとても詳しく書かれているので割愛

## プログラムを作成

まずプログラムはこちら

```Python3:main.py
# インストールした discord.py を読み込む
import discord
from discord.ext import commands
import random


class Room():
    def __init__(self,hard=False):
        self.ans=""
        if hard:
            for i in range(4):
                self.ans=self.ans+str(random.randrange(0,10))
        else :
            l=['0','1','2','3','4','5','6','7','8','9']
            random.shuffle(l)
            for i in range(4):
                self.ans=self.ans+l[i]
            
        self.history=[]
        
    def step(self,req):
        brow=0
        hit=0
        
        for index,value in enumerate(req):
            if self.ans[index]==value:
                brow+=1
            elif self.ans.find(value)!=-1:
                hit+=1
                
        return hit,brow

# 自分のBotのアクセストークンに置き換えてください
TOKEN = 'アクセストークン'

# 接続に必要なオブジェクトを生成
client = discord.Client()

rooms={0:"example"}


# 起動時に動作する処理
@client.event
async def on_ready():
    # 起動したらターミナルにログイン通知が表示される
    print('ログインしました')

# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    
    # メッセージ送信者がBotだった場合は無視する
    if message.author.bot:
        return
    
    print(message.author.name+"<"+message.content)
    
    # 「/neko」と発言したら「にゃーん」が返る処理
    if message.content == '/neko':
        await message.channel.send('にゃーん')
    
    #ヘルプ
    if message.content == '/help':
        a="〜ヘルプ〜\n「/help」コマンド一覧\n「/rule」このゲームのルールを教えるよ\n「/start」ゲームをスタート(重複不可)するよ\n「/start-hard」ゲームをスタート(重複可)するよ(テスト機能)\n「/see 〜」 「〜」が答えとあっているか調べるよ\n「/history」今までのゲームの記録を調べるよ\n「/exit」ゲームを中断するよ"
        await message.channel.send(a)
        
    #ルール説明
    if message.content == '/rule':
        a="〜ルール〜\n1.僕が４桁の番号を決めるよ\n2.「/see 〜」と言ってくれれば僕がその数が正解とどのくらいあっているか判定するよ\n　・数字だけがあっている：ヒット\n　・数字も場所もあっている：ブロー\nこれを繰り返して、僕の数字を当ててね！"
        await message.channel.send(a)
    
    #ゲーム開始(重複なし)
    if message.content=="/start":
        if message.channel.id in rooms:
            await message.channel.send('ここのルームはいまゲーム中だよ')
            return
        
        rooms[message.channel.id]=Room()
        await message.channel.send('スタート！')
        
    #ゲーム開始(重複可)
    if message.content=="/start-hard":
        if message.channel.id in rooms:
            await message.channel.send('ここのルームはいまゲーム中だよ')
            return
        
        rooms[message.channel.id]=Room(hard=True)
        await message.channel.send('スタート！頑張って！')
        
    #正誤判定
    if(message.content[0:4]=="/see") and message.channel.id in rooms:
        
        req=message.content[4:]
        req=req.replace(" ","")
        if len(req)!=4:
            await message.channel.send('４桁の番号だよ')
            return
            
        hit,brow=rooms[message.channel.id].step(req)
        
        rooms[message.channel.id].history.append({'request':req,'hit':hit,'brow':brow})
                
        await message.channel.send('リクエスト：'+req+'\n結果：{}ヒット {}ブロー'.format(hit,brow))
                
        if req==rooms[message.channel.id].ans:
            await message.channel.send('正解だね！')
            say="今までの記録だよ\n質問回数:{}回| 数字 | ヒット | ブロー |\n".format(len(rooms[message.channel.id].history))
            for i in rooms[message.channel.id].history:
                say=say+"| {} |　 {} 　|　 {} 　|\n".format(i["request"],i["hit"],i["brow"])
                
            await message.channel.send(say)
            del rooms[message.channel.id]
            
    #離脱
    if message.content=="/exit" and message.channel.id in rooms:
        await message.channel.send("ゲーム終了！\n答え："+rooms[message.channel.id].ans)
        del rooms[message.channel.id]
        
    #履歴
    if message.content=="/history" and message.channel.id in rooms:
        say="今までの記録だよ\n質問回数:{}回| 数字 | ヒット | ブロー |\n".format(len(rooms[message.channel.id].history))
        for i in rooms[message.channel.id].history:
            say=say+"| {} |　 {} 　|　 {} 　|\n".format(i["request"],i["hit"],i["brow"])
        await message.channel.send(say)
            
    #チート
    if message.content=="/Please Show Us Answer" and message.channel.id in rooms:
        await message.channel.send(rooms[message.channel.id].ans)

# Botの起動とDiscordサーバーへの接続
client.run(TOKEN)
```

小分けしながら説明

### 基本

```Python
import discord
from discord.ext import commands
import random

[...]

# 自分のBotのアクセストークンに置き換えてください
TOKEN = 'アクセストークン'

# 接続に必要なオブジェクトを生成
client = discord.Client()

rooms={0:"example"}

[...]

# Botの起動とDiscordサーバーへの接続
client.run(TOKEN)
```

これがこのプログラムの基本になる部分
・必要なモジュールのインストール
・Botと接続(?)して定義をしたあと、実行する
辞書型の変数`rooms`は後で使う

###クラス定義

```Python3
class Room():
    def __init__(self,hard=False):
        self.ans=""
        if hard:
            for i in range(4):
                self.ans=self.ans+str(random.randrange(0,10))
        else :
            l=['0','1','2','3','4','5','6','7','8','9']
            random.shuffle(l)
            for i in range(4):
                self.ans=self.ans+l[i]
            
        self.history=[]
        
    def step(self,req):
        brow=0
        hit=0
        
        for index,value in enumerate(req):
            if self.ans[index]==value:
                brow+=1
            elif self.ans.find(value)!=-1:
                hit+=1
                
        return hit,brow
```

これは、ゲームを動かす部屋のクラス

#### __ init __()
※タイトルの「__」のところに半角スペース入ってます。コピペしないように

この関数は大きく分けて２つ
・ハードモード(重複あり)かを判定し、それに応じてそのゲームにおける答えを決める
・予測の記録`history`の初期化

答えは、
重複なしのとき、0から９までの数字が１つずつ入ったリストをシャッフルして、その先頭４つを取る。
重複可のときは１桁ずつランダムに決める

####step(req):
相手の予想`req`を引数にとり、それと答えがどのくらい一致しているかを判定
(スペルミスは勘弁)

場所も数字もあっている：browを+1
場所は違うが数字はあっている：hitを+1


###メッセージ受信時
```Python
# メッセージ受信時に動作する処理
@client.event
async def on_message(message):
    
    # メッセージ送信者がBotだった場合は無視する
    if message.author.bot:
        return
    
    print(message.author.name+"<"+message.content)
    
    # 「/neko」と発言したら「にゃーん」が返る処理
    if message.content == '/neko':
        await message.channel.send('にゃーん')
    
    #ヘルプ
    if message.content == '/help':
        a="〜ヘルプ〜\n「/help」コマンド一覧\n「/rule」このゲームのルールを教えるよ\n「/start」ゲームをスタート(重複不可)するよ\n「/start-hard」ゲームをスタート(重複可)するよ(テスト機能)\n「/see 〜」 「〜」が答えとあっているか調べるよ\n「/history」今までのゲームの記録を調べるよ\n「/exit」ゲームを中断するよ"
        await message.channel.send(a)
        
    #ルール説明
    if message.content == '/rule':
        a="〜ルール〜\n1.僕が４桁の番号を決めるよ\n2.「/see 〜」と言ってくれれば僕がその数が正解とどのくらいあっているか判定するよ\n　・数字だけがあっている：ヒット\n　・数字も場所もあっている：ブロー\nこれを繰り返して、僕の数字を当ててね！"
        await message.channel.send(a)
    
    #ゲーム開始(重複なし)
    if message.content=="/start":
        if message.channel.id in rooms:
            await message.channel.send('ここのルームはいまゲーム中だよ')
            return
        
        rooms[message.channel.id]=Room()
        await message.channel.send('スタート！')
        
    #ゲーム開始(重複可)
    if message.content=="/start-hard":
        if message.channel.id in rooms:
            await message.channel.send('ここのルームはいまゲーム中だよ')
            return
        
        rooms[message.channel.id]=Room(hard=True)
        await message.channel.send('スタート！頑張って！')
        
    #正誤判定
    if(message.content[0:4]=="/see") and message.channel.id in rooms:
        
        req=message.content[4:]
        req=req.replace(" ","")
        if len(req)!=4:
            await message.channel.send('４桁の番号だよ')
            return
            
        hit,brow=rooms[message.channel.id].step(req)
        
        rooms[message.channel.id].history.append({'request':req,'hit':hit,'brow':brow})
                
        await message.channel.send('リクエスト：'+req+'\n結果：{}ヒット {}ブロー'.format(hit,brow))
                
        if req==rooms[message.channel.id].ans:
            await message.channel.send('正解だね！')
            say="今までの記録だよ\n質問回数:{}回| 数字 | ヒット | ブロー |\n".format(len(rooms[message.channel.id].history))
            for i in rooms[message.channel.id].history:
                say=say+"| {} |　 {} 　|　 {} 　|\n".format(i["request"],i["hit"],i["brow"])
                
            await message.channel.send(say)
            del rooms[message.channel.id]
            
    #離脱
    if message.content=="/exit" and message.channel.id in rooms:
        await message.channel.send("ゲーム終了！\n答え："+rooms[message.channel.id].ans)
        del rooms[message.channel.id]
        
    #履歴
    if message.content=="/history" and message.channel.id in rooms:
        say="今までの記録だよ\n質問回数:{}回| 数字 | ヒット | ブロー |\n".format(len(rooms[message.channel.id].history))
        for i in rooms[message.channel.id].history:
            say=say+"| {} |　 {} 　|　 {} 　|\n".format(i["request"],i["hit"],i["brow"])
        await message.channel.send(say)
            
    #チート
    if message.content=="/Please Show Us Answer" and message.channel.id in rooms:
        await message.channel.send(rooms[message.channel.id].ans)
```

#### 「/help」「/rule」「/neko」
コマンドに応じた返答(テンプレ)を返す

#### 「/start」「/start-hard」
最初に出てきた変数`rooms`に発言のあった部屋のIDをキーに、Roomクラスのインスタンスを新しく追加する。
もしその部屋がすでにゲーム中ならば(キーが存在するならば)、その旨を伝えて弾く
また、「/start-hard」の場合は重複を可能にさせる

#### 「/see 〜」
このBotの一番大事な部分

ステップ１
まず「/see」の部分を削除し、余分なスペースを排除
そして、残った文字の長さが4でなければ、メッセージを送って無視する(矛盾は気にしない)

ステップ２
そしたら、予想と答えがどのくらいあっているのか判定、結果を`history`に保存。
結果を発言し、もし予想と答えが完全に一致していれば、今までの履歴を表示してから、`rooms`からそのゲームのデータをキーごと消す(=ゲーム終了)。

####　「/history」
今までの予想と結果を表示する。これが考察において一番重要だったり
自分の部屋のゲームデータから、`history`の内容を確認して、それらを順に表示させる

#### 「/exit」
どうしてもわからないときの救済措置
答えを表示して、終了処理。

##Herokuにデプロイ

Herokuにアカウントがあることは前提に。

###プロジェクトを作成
```bash
$ heroku login
$ heroku create (プロジェクト名(任意))
```
まずはログイン。
その後のコマンドを実行すると、何やらふたつのURLが出てくる。どうやらサーバーのURLとGit用のURLらしい。

### 必要なファイルを用意
必要なファイルは以下の４つ。
・main.py (プログラム本体)
・Procfile　(よくわかんないけど多分プログラム識別用のキーとそのときのコマンド)
・requirements.txt (使うモジュール一覧)
・runtime.txt (Pythonのバージョン)

中身はこんな感じ

```text:Procfile
NumberQuiz: python main.py
```

```text:requirements.txt
discord.py
```

```text:runtime.txt
python-3.7.3
```

###デプロイ
あとはデプロイして動かすだけ

```bash
$ git init
$ git remote (heroku create　で出てきたURLのうち、「.git」で終わっている方)
$ git add .
$ git commit -m "first commit"
$ git push heroku master
```

これでHerokuにデプロイ完了
あとは、

```bash
$ eroku ps:scale NumberQuiz=1
```

これで動作開始。

もしログを見たいときは、

```bash
$ heroku logs #その時点でのログ
$ heroku logs --tail #リアルタイム
```
これで大丈夫


#最後に
初めてちょっとしたゲームBotを作ってみて楽しかった。
一番手こずったのはゲームをルーム別に管理するところでしたね。でも思いついたときは嬉しかった。

Herokuって難しそうで結構簡単なんだな