4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

discord.pyの開発が終了してしまった

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

今回作るもの

最近何かと話題のWordleを作ります.
本家同様5文字の英単語を6回の回数制限の中推測させるルールで作りました.

実際に作ったもの

画像と簡単な説明 ![動作テスト.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/241801/849aa82d-1913-7c47-bc72-b06459e90e15.jpeg) `/start`で単語リスト(txtファイル)から単語の選定と変数の初期化 `/answer`とオプションにansがあり5文字を入力し送信すると結果を表示し位置と文字があっていれば緑,文字が合っていれば赤で文字が表示されます.

pycordの基本

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

基本的な雛形

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)


tokenguild_idに関してはgithubで管理する上で間違えて晒すのを防止するために別ファイルに記入し読み取るようにしているだけで[:-1]等もAtomでtxtファイルを変更すると改行されてしまうためです.

実行結果

雛形.jpg
みればわかりますがほぼdiscord.pyの感覚で書けます.

ButtonやView

あと今回はボタンを用いていますのでそれに関しても簡単な雛形を紹介します.

参考にさせてもらった動画

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
当然ボタンを押したときの処理を書いてないので押しても何も起きません.また引数により押せないように無効化することや位置を指定することもできますが長くなるので端折ります.(趣旨が変わるのもある)

今回作ったもののソース解説

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

wordleのクラス

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.correctself.existに結果を格納している.

正誤判定

正解の単語と入力された単語単純に比較し合っているかを確認する
単語は合っているけど位置は違う場合の判定では,正解の単語一文字に対し入力された単語を一文字ずつ比較し判定.
どちらも結果をリストに格納する.

/startコマンド

@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コマンド

@bot.slash_command(guild_ids=[guild_id],description='回答*要/startコマンド')
async def answer(
    ctx,
    ans:Option(str,'任意の5文字を入力してください')
):
    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

ソースコード
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,'任意の5文字を入力してください')
):
    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)

最後に

今回はpycordで話題のWordleを作ってみました.
シンプルなルールのおかげで比較的スパゲッティコードにならずに済んだ気がします.(多分)
クラスも使ってみたいと思っていたので,正しい使い方ができてるかわかりませんがなんとなく利点を生かして使えて(?)よかったです.
最後の方は体力が尽きて雑になっているので質問があればできる限り返答などしますので気軽にどうぞ

注意点

wordlist.txtは5文字の英単語を改行すれば認識してくれるはずです.
英単語リストは自分で作成してください.(使用した単語リストの著作権周りがよくわからなかったため)

引用元

ButtonやViewの使い方

Pycordの環境構築など

@melonadeさんの記事を参考にさせていただきました.

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?