0
0

More than 3 years have passed since last update.

DiscordBotでヒットアンドブロー

Posted at

今回はPythonのDiscord.pyを使ってNintendoSwitchの「世界のアソビ大全」にもある、「ヒットアンドブロー」というゲームのBotを作ってみた
ヒットアンドブローとは

流れ

DEVELOPER PORTAL から、新しいアプリケーションを作成

これについては、この記事でとても詳しく書かれているので割愛

プログラムを作成

まずプログラムはこちら

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.僕が4桁の番号を決めるよ\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('4桁の番号だよ')
            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)

小分けしながら説明

基本

import discord
from discord.ext import commands
import random

[...]

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

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

rooms={0:"example"}

[...]

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

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

クラス定義

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 __()

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

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

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

step(req):

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

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

メッセージ受信時

# メッセージ受信時に動作する処理
@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.僕が4桁の番号を決めるよ\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('4桁の番号だよ')
            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の一番大事な部分

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

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

 「/history」

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

「/exit」

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

Herokuにデプロイ

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

プロジェクトを作成

$ heroku login
$ heroku create (プロジェクト名(任意))

まずはログイン。
その後のコマンドを実行すると、何やらふたつのURLが出てくる。どうやらサーバーのURLとGit用のURLらしい。

必要なファイルを用意

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

中身はこんな感じ

Procfile
NumberQuiz: python main.py
requirements.txt
discord.py
runtime.txt
python-3.7.3

デプロイ

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

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

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

$ eroku ps:scale NumberQuiz=1

これで動作開始。

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

$ heroku logs #その時点でのログ
$ heroku logs --tail #リアルタイム

これで大丈夫

最後に

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

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

0
0
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
0
0