discordのコマンド機能を使ってNumer0nみたいなゲームを作ってみた　

#はじめに
この記事は[リンク情報システム](https://www.lis.co.jp/)の「[2020新春アドベントカレンダー TechConnect!](https://qiita.com/dgkz/items/5131349d56354b33ee97)」のリレー記事です。
engineer.hanzomonのグループメンバーによってリレーされます。

(リンク情報システムのFacebookは[こちらです](https://ja-jp.facebook.com/lis.co.jp/))

16日目担当の@o-changです。
あけましてもう一か月ですね。本年もよい年になりますように！

#前回のあらすじ

[前回のアドベントカレンダー](https://qiita.com/dgkz/items/2862d5e6b77203a126b3)にも参加させていただいているのですが、そこで投稿させていただいた[私の記事](https://qiita.com/o-chang/items/e45fb7074654f8eb26ea)にありがたいことにコメントをいただきまして、コマンド拡張機能があることを知りました。
・・・ということはもちろんコマンド拡張機能を使って記事を書くしかないよなぁ！？ということで本記事もdircord.pyを使用してdiscordbotを作成する記事となっております。

さて、前回のアドベントカレンダーの中で一際目を引くのは、私の同期達が執筆いたしましたこちらの記事です。
[JavaでNumer0nみたいなゲームを作ってみた](https://qiita.com/k-igarashi214/items/e006bd24dc6fba05a47d#_reference-dd5c94e0041cd62b75b0)
偉大なる二人の作者：@k-igarashi214 @k-nakamura0420

こちらの記事の中で、お二人はこのような問題点を述べられています。

>`改行を99回繰り返した文字列を生成しています。
>自分のナンバーを設定した後にそれを画面上に表示させないために使います。`

この問題も解決できるのです。そう、discordならね。
というわけでこちらの記事を元に、discordbotでNumer0nっぽいゲームを実装してみましょう！
お二人とも、ネタの使用許可ありがとうございます！

・・・ここで一つ謝罪点があります。
お二人が記事内で主題とされていた、「StreamAPI」に代わるものを見つけて使用することができず、同じネタを使った別物みたいになってしまいました。申し訳ありません・・・

#前提
ざっくりとした要件は以下の通りです
①Numer0nっぽいゲームを作る！
②コマンド拡張機能を使い倒す！
③相手に自分が入力した数字が見えず、かついつでも自分は確認できる！

botの開発環境や実行環境については前回の記事を参考にしていただけると幸いです。

#実装
※※※例外処理等実装しておりません。再現される場合はお気を付けください。※※※
※※※また、全文貼るにはソースが長い~~かつ突貫で作ったので結構酷い~~ため、解説部のみ一部抜粋とさせていただいています。※※※
discord.pyの機能を中心に解説させていただきます。
参考はこちら→([コマンド拡張機能の公式ドキュメント](https://discordpy.readthedocs.io/ja/latest/ext/commands/commands.html))
###botの起動部
```discordbot.py
# コマンド拡張機能
from discord.ext import commands
# discordのAPI
import discord
# GameCogの取り込み(./cogsにファイルを置いています)
import cogs.GameCog as GameCog

# コマンドプレフィックスを設定
bot = commands.Bot(command_prefix='$')

# ゲーム起動時に発生するイベント
@bot.event
async def on_ready():
    print('-----')
    print(bot.user.name)
    print(bot.user.id)
    print('-----')
    text = f'Logged on as {bot.user}!'
    GameCog.setup(bot)
    channel = bot.get_channel(チャンネルID(数値))
    await channel.send('start success')

# botの起動
bot.run('botのアクセストークン(文字列)')
```

まず重要なのが

```discordbot.py
from discord.ext import commands

bot = commands.Bot(command_prefix='$')
```

ここで、discordbotのコマンド拡張機能を読み込み、botの生成を行っています。
起動時のイベントとしてコグを読み込む形式にしました。
「command_prefix」とはコマンドの頭文字になるキーワードで、以下のようにコメント本体の前につける記号になります。

![command_prefix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/9dcb5982-d78b-ed8e-d7a3-1f538045f632.png)


```discordbot.py
GameCog.setup(bot)
```
ここはコグの読み込みを行っている部分です。
>``Bot開発においてコマンドやリスナー、いくつかの状態を一つのクラスにまとめてしまいたい場合があるでしょう。コグはそれを実現したものです。``
([コグ(Cog)の公式ドキュメント](https://discordpy.readthedocs.io/ja/latest/ext/commands/cogs.html#cogs)より引用)

とあるように、クラスとしてbotのコマンド等の管理ができる機能がコグです。
ぶっちゃけあまり有用に使用できてはいませんが・・・中身は後述するクラスにて解説します。
ここでbotを引数にsetupメソッドを起動することでbotがコグを読み込んでくれるということが重要です。
###GameCogクラス

```GameCog.py
# exitの実装
import sys

# コグクラス
class GameCog(commands.Cog):
    # TestCogクラスのコンストラクタ。Botを受取り、インスタンス変数として保持。
    def __init__(self, bot):
        self.bot = bot
        self.playerFlag = False
        self.gameFlag = False
    # 起動確認のコマンド
    @commands.command()
        async def test(self, ctx):
        await ctx.send('ok!')

        # ゲーム終了のコマンド
        @commands.command()
        async def exit(self, ctx):
            await ctx.send("ﾉｼ")
            sys.exit()

        # ゲーム開始管理のコマンドグループ
        @commands.group()
            async def game(self, ctx):

        # サブコマンド未指定時のメッセージ
        if ctx.invoked_subcommand is None:
            await ctx.send('サブコマンドを指定してください。')

        # ゲーム参加コマンド
        @game.command()
        async def join(self, ctx):
            if self.gameFlag:
                errTxt = "現在ゲーム中です\n"
                await ctx.send(errTxt)

            # プレイヤー1生成前
            elif not self.playerFlag:
                self.playerFlag = True

                # プレイヤー1を生成する
                self.player1 = Player(ctx.author)
                startTxt = "1P:" + ctx.author.name + "\n"
                startTxt += "「二人目のプレイヤーも$game join」を実行してください"
                await ctx.send(startTxt)

            # プレイヤー1生成後
            else:
                self.playerFlag = False

                # プレイヤー2を生成する
                self.player2 = Player(ctx.author)
                playTxt = "2P:" + ctx.author.name + "\n"
                playTxt += "「$game start」を実行してください"
                await ctx.send(playTxt)
            @game.command()
            async def start(self, ctx):
                if self.gameFlag:
                    errTxt = "現在ゲーム中です\n"
                    await ctx.send(errTxt)
                else:
                    self.channel = ctx
                    self.gameFlag = True
                    # 1Pにダイレクトメールを送る
                    await self.player1.getUser().send("「$number」の後に半角で3桁の数字を入力してね")

    # 以下、Numer0nのゲーム部分が続きますが紙面の都合で省略します。

# Bot本体側からコグを読み込む際に呼び出される関数。メンバ関数ではないです。
def setup(bot):
    # cogクラスにbotを渡してインスタンス化
    bot.add_cog(GameCog(bot))
```

Player()は後述するPlayerクラスのコンストラクタです。
ここでは三点説明します。

```GameCog.py
def __init__(self, bot):
    self.bot = bot

def setup(bot):
    bot.add_cog(GameCog(bot))
```
この**setup**メソッドを関数外に宣言し、botを引数で与えて呼び出すことで、コグのコンストラクタにアクセスしています。
そして**add_cog**メソッドでコグがbotに追加され、外部から利用できるようになります。

```GameCog.py
@commands.command()
async def test(self, ctx):
    await ctx.send('ok!')

# ゲーム開始管理のコマンドグループ
@commands.group()
async def game(self, ctx):

    # サブコマンド未指定時のメッセージ
    if ctx.invoked_subcommand is None:
        await ctx.send('サブコマンドを指定してください。')

# ゲーム参加コマンド
@game.command()
async def join(self, ctx):
    # 省略
```
**@commands.comman**を指定することでコマンドの定義ができます。
「async def hoge」で指定した「hoge」の部分がコマンドの本体になり、↑に貼った画像のように、プレフィックスと合わせることでメソッド内部の処理が行われます。
動きの中身は前回解説した

```discordbot.py
@client.event
async def on_message(message):
```
の動きと大差ないため、解説はそちらをご覧ください。

コマンド拡張機能で個人的に注目度が高いのが「@commands.group」の動きで、これはコマンドをネストすることができる機能です。
@commands.commandと同じで「async def game」のgame部分をプレフィックスと合わせて使用する点に加え、引数としてグループ内の別のコマンドを与えることができます。
今回の場合は「**@game.command**」が使用されている箇所がこれに該当します。
使用例は以下の通りです。

![group例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/a1244610-7fdd-a5a9-7103-1871684d2bfb.png)


ぶっちゃけ全く上手く使えてはいないのですが、単純に同じ機能をひとまとめにできるというだけでも個人的には注目度が高かったですね。
一応呼び出し順は親コマンドの処理→子コマンドの処理であるため、親コマンドで常にログをチャンネルに表示する・・・なんて使い方も便利そうだなと思いました。一つのイベントで複数のdiscord.pyのメソッドを使いたいときに便利そうです。

最後に、コマンドの引数にある「**ctx**」こいつについてです。
こいつは[コンテクスト(公式ドキュメント)](https://discordpy.readthedocs.io/ja/latest/ext/commands/api.html#discord.ext.commands.Context)と呼ばれる引数で、コマンドの引数には必ず与える必要があります。
受け取ったコマンドが入力された場所や、入力したアカウントの情報などが保持されているクラスです。

今回使ったのは

>・Context.send(txt)
 ・Context.author

この二つです。Context.send()は引数で与えた文字列をコマンドが入力されたチャンネルに発言するというメソッドです。単純な応答に使えます。

```GameCog.py
@commands.command()
async def test(self, ctx):
    await ctx.send('ok!')
```

ここなんかで使用していますね。動きは最初に載せた画像の通りです。
次にContext.authorですが、これはコマンドを入力したユーザー情報のクラスです。
今回は「$game join」コマンドを入力したユーザーの情報を取得し、インスタンス変数として保持し、様々な箇所で使用しています。

```GameCog.py
        @game.command()
        async def join(self, ctx):
        # 中略
                # プレイヤー1を生成する
                self.player1 = Player(ctx.author)
                startTxt = "1P:" + ctx.author.name + "\n"
                startTxt += "「二人目のプレイヤーも$game join」を実行してください"
                await ctx.send(startTxt)

        @game.command()
        async def start(self, ctx):
        # 中略
            else:
                self.channel = ctx
                self.gameFlag = True
                # 1Pにダイレクトメールを送る
                await self.player1.getUser().send("「$number」の後に半角で3桁の数字を入力してね")
```
Context.author.nameでユーザー名を取得できる他、Context.author.send()を使用することで、ユーザーにDMを送信することができます。

↓DMを受け取る

![DM起動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/08a891f0-4754-b0eb-21ea-0b72f9c7c51a.png)

↓DM画面

![DM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/51a265a4-de4b-aba3-c72e-4253579c6bff.png)

このContext.authorクラスをインスタンスとして保持しておくとコマンド送信者以外にDMを送るなんて操作もできるので、**$game start**は1Pと2P、どちらが送信しても1PからDMを送れるようになっています。
そしてbotのコマンドはDMからでも同様に受け付けますので、自身の数字をDMで送ることで、相手には見られないし自分はいつでも確認できるようになります。


###計算等を行うその他クラス
こちらはdiscordというよりPythonの機能で、基本的に[JavaでNumer0nみたいなゲームを作ってみた](https://qiita.com/k-igarashi214/items/e006bd24dc6fba05a47d#_reference-dd5c94e0041cd62b75b0)と同じような構成で作成させており、discord.pyの機能は特に使ってないため、説明は省きます。
入力文字を~~力技で~~1文字ずつ比較することで重複チェックやEAT-BITEの判断も行っています。
・・・というのは半分ほんと半分建前で、ちょっとソースコードがあまりにも酷いので省略させていただきたいという気持ち・・・後日整備してまた載せられたら載せたい・・・ ~~のでストック是非よろしくお願いします~~ 
前回のお二人の記事と基本的なアルゴリズムは変わらないので参考にしていただければ・・・！


#ゲームを見てみよう！
最後に少しだけゲーム画面の紹介をさせていただきます。
ちなみにこれ書いてる時間に暇な人がいなかったので悲しいことにどちらも私のアカウントです；；

![ゲーム開始.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/62dbc631-3ab5-eba7-a597-676466f5729d.png)

お互いが参加表明すると、1PにDMが送信されます。

![DM2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/d5a7161e-32b1-990d-9526-b4b133bcef69.png)

1Pが自分の数字をDMで送信すると2PにDMが届きます(画面わかりにくいですがDM画面です)

![サブDM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/85fabb52-c897-6fc6-a581-cf3d4e3d9521.png)

(ちなみにサブはスマホで操作しております。画像のサイズがバラバラで申し訳ない・・・)
そして2Pが数字を送るとゲームが開始されます。
しかしその間何もメッセージもない虚無な時間になるので、前述したグループ機能を使ってゲーム開始したチャンネルにメッセージを送信するようにした方がよかったなと今更ながら反省しております。今後の課題ですね。

![ゲーム画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/453534/1b9c3d2c-29fc-ac69-c9d7-7f3d37fec2d0.png)


ゲームの推移はこの通りです。
これなら離れた友達ともいつでも遊べますね！！！！！！！！！！！！

#今後の課題
突貫工事で作成したこともあり、第三者が$game startしても動き出します←
このへんの明らかなバグを一通り修正したり、ヘルプコマンドを導入して、ゆくゆくはbotの公開なんかもできたらいいな・・・と思っております。
また、DM限定でコマンドを受け取る**@commands.dm_only**なんてのもあるんですが、全然うまく使えなかったので要勉強です。




#おわりに
改めて快くネタの使用許可をくださったお二人には感謝を述べさせていただきます。
本当にありがとうございます！そして再現度は低くなってしまって申し訳ございません・・・！

明日の記事は@shinmoさんです！よろしくお願いします！

アドベントカレンダーも最後の一週です。最後までよろしくお願いします！

---
[リンク情報システム株式会社](https://www.lis.co.jp)では一緒に働く仲間を[随時募集](https://www.lis.co.jp/employ_top)しています！
また、お仕事のご依頼、ビジネスパートナー様も募集しております。[お気軽にご連絡](https://www.lis.co.jp/inquiry)ください。