【Python】discordでWordleを遊べるbotを作った

# はじめに
## discord.pyの開発が終了してしまった
この記事を閲覧してくださってるという方はすでにご存じだと思いますが，discord.pyの開発が終了してしまいました．（[詳細:github](https://gist.github.com/Rapptz/4a2f62751b9600a31a0d3c78100287f1)）
これに関する自分の意見などを述べるのはこの場では省きますが，こういった件からPythonでDiscordのbotを開発するのに使うライブラリは，discord.pyを中心としてフォークされ様々なものが出ています．
そんな中@melonadeさんの[こちらの記事](https://qiita.com/melonade/items/25c038e8e4e6800aa639)を参考に代替ライブラリとしてpycordを用いて今回のbot作りを行いました。

# 今回作るもの
最近何かと話題の[Wordle](https://www.nytimes.com/games/wordle/index.html)を作ります．
本家同様5文字の英単語を6回の回数制限の中推測させるルールで作りました．
## 実際に作ったもの
<details>
<summary>画像と簡単な説明</summary>
![動作テスト.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241801/849aa82d-1913-7c47-bc72-b06459e90e15.jpeg)
`/start`で単語リスト(txtファイル)から単語の選定と変数の初期化
`/answer`とオプションにansがあり５文字を入力し送信すると結果を表示し位置と文字があっていれば緑，文字が合っていれば赤で文字が表示されます．
</details>

## pycordの基本
実際に作成もののソースを紹介する前にpycordの基本を備忘録的な感じに簡単にまとめるとともに今回用いたButtonやViewも紹介します．
導入方法に関しては，最初に紹介した記事を書いていらっしゃる方が説明してくださっているのでそれを参考にしてください．すでに入れているdiscord.pyをアンインストールすることを忘れなければつまずかないはずです．(1敗)

### 基本的な雛形

```python
import discord

bot=discord.Bot()
guild_id=open('guild_id.txt','r',encoding='UTF-8').read()[:-1]

@bot.event
async def on_ready():
    print('Raedy...')

@bot.slash_command(guild_ids=[guild_id])
async def ping(ctx):
    await ctx.respond('pong')

token=open('token.txt','r',encoding='UTF-8').read()[:-1]
bot.run(token)


```

`token`や`guild_id`に関してはgithubで管理する上で間違えて晒すのを防止するために別ファイルに記入し読み取るようにしているだけで`[:-1]`等もAtomでtxtファイルを変更すると改行されてしまうためです．
### 実行結果
![雛形.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241801/4537d462-0bfa-0bac-705a-9ec78fe5fb6c.jpeg)
みればわかりますがほぼdiscord.pyの感覚で書けます．
### ButtonやView
あと今回はボタンを用いていますのでそれに関しても簡単な雛形を紹介します．
>[参考にさせてもらった動画](https://www.youtube.com/watch?v=kNUuYEWGOxA)

```python
import discord
from discord.ui import Button,View

bot=discord.Bot()
guild_id=open('guild_id.txt','r',encoding='UTF-8').read()[:-1]

@bot.event
async def on_ready():
    print('Raedy...')

@bot.slash_command(guild_ids=[guild_id])
async def ping(ctx):
    button = discord.ui.Button(label="pong",style=discord.ButtonStyle.green)
    view=View()
    view.add_item(button)
    await ctx.respond('pong',view=view)

token=open('token.txt','r',encoding='UTF-8').read()[:-1]
bot.run(token)
```

### 動作結果
![ボタンテスト.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241801/e724a866-db5a-39ab-3227-d964cfb880ab.jpeg)
当然ボタンを押したときの処理を書いてないので押しても何も起きません．また引数により押せないように無効化することや位置を指定することもできますが長くなるので端折ります．（趣旨が変わるのもある）

# 今回作ったもののソース解説
wordle自体ルールが簡単なため実装内容も単純です．（読みやすいとは言っていない）
流れとしては
`/start`コマンドを実行した際にグローバル変数のGameにwordleクラスのインスタンスを作成し，5つのボタンとゲームを開始したというメッセージを発言をさせる
`/answer ans`コマンドでans部分に5文字の英単語を入力させ，wordleクラスの`input_word`に引数として渡し判定を行う．
判定結果をリストに格納しているのでそれを参照しボタンの色を変えて答えとして発言させる．
その後5文字全てあっているか，回数制限を迎えてないか判定を行う


## wordleのクラス

```python
class wordle:
    def __init__(self):
        words=open('wordlist.txt','r',encoding='UTF-8').read().split()
        select=random.randint(0,len(words)-1)
        self.time=0
        self.word=words[select]
        print(self.word)

    def input_word(self,input):
        self.correct=[]
        self.exist=[]
        #位置文字ともに正解か判定
        for i in range(5):
            if(self.word[i]==input[i]):
                self.correct.append(i)
        #位置が正解か判定
        for w in self.word:
            for i in range(5):
                if(w==input[i]):
                    self.exist.append(i)
```

コンストラクタ部分では`wordlist.txt`から5文字の英単語を変数に格納し，単語を抽選
`input_word`のメソッド部分では正誤判定を行い結果を`self.correct`と`self.exist`に結果を格納している．
### 正誤判定
正解の単語と入力された単語単純に比較し合っているかを確認する
単語は合っているけど位置は違う場合の判定では，正解の単語一文字に対し入力された単語を一文字ずつ比較し判定．
どちらも結果をリストに格納する．

## /startコマンド

```python
@bot.slash_command(guild_ids=[guild_id],description='wordleを開始')
async def start(ctx):
    global Game
    Game=wordle()
    view=View()

    for i in range(5):
        view.add_item(discord.ui.Button(disabled=True,label=' ',style=discord.ButtonStyle.gray))
    await ctx.respond('ゲームを開始しました',view=view)
```

クラスをグローバル変数でインスタンス`global Game`を生成し上記で説明した初期化処理を行い，その後ボタンを5つ表示するためfor文を用いて`view`にボタンを追加する．
最後にゲームを開始しましたというメッセージとともにボタンを追加したviewを返信させる．

## /answerコマンド

```python
@bot.slash_command(guild_ids=[guild_id],description='回答＊要/startコマンド')
async def answer(
    ctx,
    ans:Option(str,'任意の５文字を入力してください')
):
    global Game
    view=View()
    gray=discord.ButtonStyle.gray   #gray:合っていない
    green=discord.ButtonStyle.green #green:位置文字共に合っている
    red=discord.ButtonStyle.red     #red:文字は含まれるが位置が違う
    styles=[gray,gray,gray,gray,gray]

    if(len(ans)!=5):
        await ctx.respond('5文字入力してください')
        return
    try:
        Game.input_word(ans)
    except:
        await ctx.respond('/startをしてゲームを開始してください')
        return
    #input_wordより得た結果をリストに格納
    for exist in Game.exist:styles[exist]=red
    for correct in Game.correct:styles[correct]=green
    #Viewにボタンを追加
    for i in range(5):
        view.add_item(discord.ui.Button(disabled=True,label=ans[i],style=styles[i]))
    #結果表示
    Game.time=Game.time+1
    await ctx.respond('結果 '+str(Game.time)+'/6',view=view)
    #クリアor失敗判定
    if(len(Game.correct)==5):
        await ctx.respond('クリア')
        del Game
        return
    if(Game.time==6):
        await ctx.respond('失敗 答え:'+Game.word)
        del Game
        return
```

例外処理を用いてインスタンスがなかったらゲームが開始されていないとエラーメッセージを送信させる．
あとは`input_word`の結果がリストに格納されているのでそこからボタンの色を決める．
あとは結果を送信なりゲームのクリア判定をしています（雑）

### 全体のコード
[GitHub](https://github.com/smoothie1023/wordle-forDiscord)
<details>
<summary>ソースコード</summary>
<div>

```python
import discord
import random
from discord.ui import Button,View
from discord.commands import Option

bot=discord.Bot()
guild_id=open('guild_id.txt','r',encoding='UTF-8').read()[:-1]

class wordle:
    def __init__(self):
        words=open('wordlist.txt','r',encoding='UTF-8').read().split()
        select=random.randint(0,len(words)-1)
        self.time=0
        self.word=words[select]
        print(self.word)

    def input_word(self,input):
        self.correct=[]
        self.exist=[]
        #位置文字ともに正解か判定
        for i in range(5):
            if(self.word[i]==input[i]):
                self.correct.append(i)
        #位置が正解か判定
        for w in self.word:
            for i in range(5):
                if(w==input[i]):
                    self.exist.append(i)

@bot.event
async def on_ready():
    print('Raedy...')

@bot.slash_command(guild_ids=[guild_id],description='wordleを開始')
async def start(ctx):
    global Game
    Game=wordle()
    view=View()

    for i in range(5):
        view.add_item(discord.ui.Button(disabled=True,label=' ',style=discord.ButtonStyle.gray))
    await ctx.respond('ゲームを開始しました',view=view)

@bot.slash_command(guild_ids=[guild_id],description='回答＊要/startコマンド')
async def answer(
    ctx,
    ans:Option(str,'任意の５文字を入力してください')
):
    global Game
    view=View()
    gray=discord.ButtonStyle.gray   #gray:合っていない
    green=discord.ButtonStyle.green #green:位置文字共に合っている
    red=discord.ButtonStyle.red     #red:文字は含まれるが位置が違う
    styles=[gray,gray,gray,gray,gray]

    if(len(ans)!=5):
        await ctx.respond('5文字入力してください')
        return
    try:
        Game.input_word(ans)
    except:
        await ctx.respond('/startをしてゲームを開始してください')
        return
    #input_wordより得た結果をリストに格納
    for exist in Game.exist:styles[exist]=red
    for correct in Game.correct:styles[correct]=green
    #Viewにボタンを追加
    for i in range(5):
        view.add_item(discord.ui.Button(disabled=True,label=ans[i],style=styles[i]))
    #結果表示
    Game.time=Game.time+1
    await ctx.respond('結果 '+str(Game.time)+'/6',view=view)
    #クリアor失敗判定
    if(len(Game.correct)==5):
        await ctx.respond('クリア')
        del Game
        return
    if(Game.time==6):
        await ctx.respond('失敗 答え:'+Game.word)
        del Game
        return

@bot.slash_command(guild_ids=[guild_id],description="ゲームを中断")
async def kill(ctx):
    global Game
    try:
        del Game
        await ctx.respond('ゲームを中断しました')
    except:
        await ctx.respond('ゲームは開始されていません')

token=open('token.txt','r',encoding='UTF-8').read()[:-1]
bot.run(token)
```

</div>
</details>

## 最後に
今回はpycordで話題のWordleを作ってみました．
シンプルなルールのおかげで比較的スパゲッティコードにならずに済んだ気がします．（多分）
クラスも使ってみたいと思っていたので，正しい使い方ができてるかわかりませんがなんとなく利点を生かして使えて(?)よかったです．
最後の方は体力が尽きて雑になっているので質問があればできる限り返答などしますので気軽にどうぞ
### 注意点
wordlist.txtは5文字の英単語を改行すれば認識してくれるはずです．
英単語リストは自分で作成してください．（使用した単語リストの著作権周りがよくわからなかったため）

# 引用元
## ButtonやViewの使い方
https://www.youtube.com/watch?v=kNUuYEWGOxA
## Pycordの環境構築など
@melonadeさんの記事を参考にさせていただきました．

https://qiita.com/melonade/items/25c038e8e4e6800aa639