359
302

More than 5 years have passed since last update.

python知識ゼロからポケモンの名前でしりとりするslackbotを作ったノウハウのすべて

Last updated at Posted at 2018-06-30

こんなのです

pokeshiri1.gif

ソースはgithubで公開してますhttps://github.com/wagase/pokeshiri
よかったら「いいね」してください。

環境

OS:windows
言語:python

筆者について

趣味でプログラム書いてるにわか。
htmlとcssとjavascriptくらいはかける。
python知識ゼロ

Twitterフォローされると喜びます
https://twitter.com/wagase
よろしくお願いします

なぜpython?

話題だから

なぜslackbot?

開発経験ゼロだから気軽に作れるのがよかった

開発環境構築

知識ゼロだからまずpythonという言語の仕様を学ぶ

参考にしたサイト
https://www.pythonweb.jp/tutorial/

斜め読みしながらわからないところはググる

pythonをwindowsにインストール

https://www.python.org/
Python 3.6.5にしました
Download for windowsからpython-3.6.5.exeを取得して実行
Add Python to PATHチェックする
コンソールでHello pythonくらいはprintできた

Visual Studio Codeでpythonの設定をする

拡張機能「python」で検索して
Python
Python for VSCode
をインストール
pylintがないとかいわれるのでインストール
pip install pylint
これでいい感じに開発できるようになった

slackbotの作り方をググる

参考にしたもの
Pythonを使ったSlackBotの作成方法
PythonのslackbotライブラリでSlackボットを作る
↑この記事をみながら
pip3 install slackbotを実行
slackbotの登録
https://my.slack.com/services/new/bot
にいって書いてある項目を埋めるだけ
API トークンをコピーしておく

いざ実行

上記の参考サイトを丸パクリして
python run.py
をしてみる
Appのところにあるbotがアクティブ表示になった!
リプライすると「何言ってんだこいつ」って返ってくる!!
実行できた!

あとはゴリゴリプログラミングする

githubで公開中
https://github.com/wagase/pokeshiri
わからないところはググる

ポケモンのデータはこちらからお借りしました

全ポケモンのJSONデータ
https://github.com/kotofurumiya/pokemon_data/
ありがとうございます

ログ出力で参考にしたもの

Pythonでお手軽にかっこよくlogging

機能とか

ポケモンじゃないときは

image.png

ンで終わるポケモンはペナルティ

image.png
しりとりだから怒られる

しりとりになってないとペナルティ

image.png
slackのシステム的に1対Nのしりとりを想定してるため多少まちがっても
ゲームオーバーにはならない

一度登場したポケモンを言うとペナルティ

image.png

困ったときはヒントで答えてない一覧をだせる

image.png
もちろん一度言うとリストから消える

知らないポケモンがでたときに詳細表示機能で教えてもらえる

image.png
全ポケモンのJSONデータ作者さんに感謝

ランキング機能

image.png
登場回数順にランキングを表示。
ル攻めすると勝てるのでルチャブルは受け攻め強い

リセット機能

image.png
ただのリセット。ゲームリスタートの意味。
リセットするとペナルティと今まで言ったポケモンを忘れる。
リセットしてもランキングは消さない

ログ表示機能

image.png
今までの記録を教えてくれる

ソース

ソースはgithubで公開してます
https://github.com/wagase/pokeshiri

<追記 date=20180705>
この記事に下記にコピペしているソースは
特定のポケモンやある条件で発生するバグがあります
github上では見つけ次第修正していますがこの記事でも同じように修正するのは記事の趣旨とは異なるため
あえて初期のままにすることにしました
(初学者はこういう書き方をしてしまうとか反面教師にもなるかと思います)
</追記>

my_mention.py
# -*- coding: utf-8 -*-
from slackbot.bot import respond_to     # @botname: で反応するデコーダ
from slackbot.bot import listen_to      # チャネル内発言で反応するデコーダ
from slackbot.bot import default_reply  # 該当する応答がない場合に反応するデコーダ
from libs import my_functions           # 自作関数の読み込み
from libs import log

# 何回呼ばれたかカウントしたい
maincount = 0
resetcount = 0
hintcount = 0
detailcount = 0
rankingcount = 0
nomalcount = 0
errorcount = 0
notpokecount = 0

@respond_to(r'.+')
def mention_func(message):
    global maincount
    global resetcount
    global hintcount
    global detailcount
    global rankingcount
    global nomalcount
    global errorcount
    global notpokecount
    maincount = maincount +1
    req=message.body['text']
    log.logger.info("["+str(maincount)+"] :総実行回数【"+str(req)+"】:受け取ったメッセージ")
    if req == "リセット" or req == "reset":
        resetcount = resetcount +1
        my_functions.reset()
        message.send("リセットしました")
    elif req == "log" or req == "ログ" or req == "記録" :
        message.send("["+str(maincount)+"] :総実行回数")
        message.send("["+str(resetcount)+"] :総リセット回数")
        message.send("["+str(hintcount)+"] :総ヒント回数")
        message.send("["+str(detailcount)+"] :総詳細表示回数")
        message.send("["+str(rankingcount)+"] :総ランキング表示回数")
        message.send("["+str(notpokecount)+"] :総ポケモンじゃなくね?回数")
        message.send("["+str(nomalcount)+"] :総しりとり成立回数")
        message.send("["+str(errorcount)+"] :総しりとり不成立回数")
    elif req == "ランキング" or req == "ranking" :
        rankingcount = rankingcount +1
        message.send(my_functions.remarkRanking())
    elif req[:4] == "ヒント|" or req[:4] == "ヒント|"  or req[:4] == "hint":
        hintcount = hintcount +1
        log.logger.info("["+str(hintcount)+"] :ヒント回数")
        hint = my_functions.hint(req[4:5])
        message.send(str(hint))
    elif req[:3] == "詳細|" or req[:3] == "詳細|" :
        detailcount = detailcount +1
        log.logger.info("["+str(detailcount)+"] :詳細表示回数")
        if my_functions.checkExistenceAllPoke(req[3:len(req)]) :
            message.send(my_functions.getpokedetail(req[3:len(req)]))
        else:
            message.send("よくわかりませんでした"+req[3:len(req)])
    else:
        if my_functions.checkExistencePoke(req) :
            my_functions.memoryRemark(req)
            IsShiritoriOK = True
            # すでに言ったことがあるかどうか
            if my_functions.checkExistencereq(req) :
                IsShiritoriOK = False
                message.send(my_functions.countreqstock(req))
            # しりとりになってるかどうか
            if not my_functions.checkTruelastword(req) :
                IsShiritoriOK = False
                message.send(my_functions.forgivelastword(req))
            if IsShiritoriOK :
                nomalcount = nomalcount +1
                log.logger.info("["+str(nomalcount)+"] :しりとり成立回数")
            else :
                errorcount = errorcount +1
                log.logger.info("["+str(errorcount)+"] :しりとり不成立回数")
            my_functions.reqstockappend(req)
            ret = my_functions.shiritori(req)
            log.logger.info("【"+str(ret)+"】:返答")
        else :
            notpokecount = notpokecount +1
            ret = "ポケモンじゃなくね?"
            log.logger.info("["+str(notpokecount)+"] :ポケモンじゃなくね?回数")
        message.send(ret)
my_functions.py
# -*- coding: utf-8 -*-
import json
import random
import collections


def mid(text,s,e):
    return text[s-1:s+e-1]

def left(text,e):
    return text[:e]

def right(text,s):
    return text[-s:]

# pokemon_data.jsonを読み取ってポケモンの名前だけにする
def getpokenamelist():
    dic = {}
    for key in POKEDATA:
        if not key["no"] in dic.keys() :
            dic[key["no"]]=key["name"]
    return dic

# 辞書{'ア':['アーボ','アーボック'....],'イ':['イシツブテ','イワーク'....].....} の形にするが最後にンがつくものは除外
def makekanalistNotnn():
    kanalist = {}
    for i in range(1,len(KATAKANA)+1):
        kanas = []
        j = 1
        for key in POKENAMELIST:
            if left(POKENAMELIST[key],1) == mid(KATAKANA,i,1) and right(POKENAMELIST[key],1) != "ン" :
                kanas.append(POKENAMELIST[key])
                j = j +1
        kanalist[mid(KATAKANA,i,1)] = kanas
    return kanalist

# 辞書{'ア':['アーボ','アーボック'....],'イ':['イシツブテ','イワーク'....].....} の形にするが「ン」で終わるやつを取得
def makekanalistGetnn():
    kanalist = {}
    for i in range(1,len(KATAKANA)+1):
        kanas = []
        j = 1
        for key in POKENAMELIST:
            if left(POKENAMELIST[key],1) == mid(KATAKANA,i,1) and right(POKENAMELIST[key],1) == "ン" :
                kanas.append(POKENAMELIST[key])
                j = j +1
        kanalist[mid(KATAKANA,i,1)] = kanas
    return kanalist

# makekanalistNotnnのリストから指定した文字で始まるポケモンを適当に選ぶ
def pokechoice(kana):
    val =""
    if len(stock[kana]) == 0 :
        if len(nstock[kana]) != 0 :
            val = random.choice(nstock[kana])
            memoryRemark(val)
            delnstock(kana,val)
        val= val + "・・・もう【"+kana+"】から始まるポケモンは答えられないよ。負けました。リセットしてね"
        if penalty !=0 :
            val= val +" ペナルティ合計は(" +str(penalty) +"回)でした"
    else :
        val = random.choice(stock[kana])
        memorylastword(val)
        reqstockappend(val)
        delstock(kana,val)
        rest = len(stock[kana])
        memoryRemark(val)
        val = val + "・・・【"+kana+"】のこり【"+str(rest)+"】" + "次のことばは【"+getshiri(val)+"】です"
    return val

# そのポケモンがしりとりで存在するかどうか
def checkExistencePoke(req):
    if req in POKENAMELIST.values():
        return True
    else :
        return False

# そのポケモンがそもそも存在するかどうか
def checkExistenceAllPoke(req):
    for key in POKEDATA:
        if req == key["name"] :
            return True
    return False

# しりとりメソッド
def shiritori(req):
    atama = left(req,1)
    shiri = getshiri(req)
    if shiri == "ン":
        global penalty
        penalty = penalty +1
        return "「ン」で終わるやつはだめだよ ペナルティ(" +str(penalty) +"回)"
    else :
        if req in stock[atama]:
            delstock(atama,req)
        return pokechoice(shiri)

# 末尾の文字を調整する
def getshiri(req):
    shiri = right(req,1)
    # ミミッキュ対策
    if shiri in "ァィゥェォッャュョヮヵヶ" :
        shiri = shiri.replace("ァ","ア")
        shiri = shiri.replace("ィ","イ")
        shiri = shiri.replace("ゥ","ウ")
        shiri = shiri.replace("ェ","エ")
        shiri = shiri.replace("ォ","オ")
        shiri = shiri.replace("ッ","ツ")
        shiri = shiri.replace("ャ","ヤ")
        shiri = shiri.replace("ュ","ユ")
        shiri = shiri.replace("ョ","ヨ")
        shiri = shiri.replace("ヮ","ワ")
        shiri = shiri.replace("ヵ","カ")
        shiri = shiri.replace("ヶ","ケ")
    # 長音対策
    if shiri == "ー" :
        shiri = mid(req,len(req)-1,1)
    return shiri

# 一度いったやつはストックから消す
def delstock(kana,val):
    stock[kana].remove(val)

# 一度いったやつはストックから消す
def delnstock(kana,val):
    nstock[kana].remove(val)

# 一度言われたやつを覚える
def reqstockappend(req):
    reqstock.append(req)

# 一度言われたことがあるかどうかしらべる
def checkExistencereq(req):
    if req in reqstock:
        return True
    else:
        return False

# 何回言われてるか調べて返す
def countreqstock(req):
    global penalty
    penalty = penalty +1
    return req + "は【" + str(reqstock.count(req)+1) + "】回目だよ。できれば違うやつ言ってね ペナルティ(" +str(penalty) +"回)"

# リセット
def reset():
    global stock
    global nstock
    global lastWord
    global reqstock
    global penalty 
    penalty = 0
    lastWord =""
    stock = makekanalistNotnn()
    nstock = makekanalistGetnn()
    reqstock.clear()

# ヒント
def hint(req):
    global penalty
    if req in stock:
        penalty = penalty + 1
        return stock[req]
    else:
        return "カタカナ一文字でお願いします"

# 詳細機能
def getpokedetail(req):
    ret = ""
    for key in POKEDATA:
        if key["name"] == req :
            ret = ret + str(key) + "\n"
    return ret

# 最後の文字を覚える
def memorylastword(req):
    global lastWord
    lastWord = getshiri(req)

# しりとりになってるか調べる
def checkTruelastword(req):
    if lastWord != left(req,1) and not lastWord=="":
        return False
    else :
        return True

# しりとりになってないメッセージ
def forgivelastword(req):
    global penalty
    penalty = penalty + 1
    return req + "はしりとりになってないよ。できれば【"+lastWord+"】から始まるやつ言ってほしかったな ペナルティ(" +str(penalty) +"回)"


# 呼ばれたものを記憶
def memoryRemark(req):
    remarkstock.append(req)

# ランキングカウント
def remarkRanking():
    ret =""
    i = 0
    c = collections.Counter(remarkstock)
    for item in c.most_common() :
        i=i+1
        if i > 5 :
            break
        ret = ret + str(item[0]) + " " + str(item[1]) + "回"+ "\n"
    return ret



# 定数群
KATAKANA = "アイウエオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモヤユヨラリルレロワヲンヴ"
POKEDATA = json.load(open("data/pokemon_data.json","r",encoding="utf-8"))
POKENAMELIST = getpokenamelist()

# 変数群
stock = makekanalistNotnn()
nstock = makekanalistGetnn()
remarkstock=[]
reqstock =[]
lastWord =""
penalty =0
log.py
import logging
logger = logging.getLogger(__name__)
_detail_formatting = '[%(asctime)s] %(module)s.%(funcName)s %(levelname)s -> %(message)s'

logging.basicConfig(
    level=logging.DEBUG,
    format=_detail_formatting, # 出力のformatも変えられる
    filename="./pokeshiri.log", # logファイルのありか
)

その他

ニドラン♀とかニドラン♂が入力できない問題

<追記 date=20180705>
slackの仕様なのか♀が「:女性のマーク:」っていう絵文字になるんですけど(困惑)
image.png
だれか解決方法知ってたら教えてください(小声)
</追記>

<追記 date=20190819>
↑これslack公式に問い合わせたところ♀が勝手に:女性のマーク:になるのは仕様で
設定等で勝手に変わらないようには現状できないとのことでした。
要望は出しておきました。
</追記>

おまけ 

Amazon linux (EC2 t2.micro)での実行方法 無料

pythonとpipとslackbotをインストール

sudo git clone https://github.com/yyuu/pyenv.git /usr/bin/.pyenv
cd /usr/bin/.pyenv
pyenv install 3.6.5
pyenv global 3.6.5
sudo apt-get install python3-pip
sudo yum install -y python36u-pip
pip3 install slackbot

上記はpythonにわかが2018/07頃に実行したhistoryです。
正確な情報は自分で調べることをおすすめします。

pokeshiriのモジュールを適当なところにおいて

nohup python -u run.py >out.log

でサーバーで実行し続けてくれます。
止めたいときは

ps -C python
kill 番号

でいけます

Herokuで実行する方法 無料

Herokuのアカウントを作る
https://signup.heroku.com/login

HerokuCLIをインストールしてパスを通す
https://devcenter.heroku.com/articles/getting-started-with-python#set-up
パスはC:\Program Files\heroku\bin
ここでいいはず

herokuコマンドでherokuのgitに上げる

heroku login
heroku create {名前}
heroku git:clone -a {名前}
cd {名前}

作業フォルダ(名前つけたやつ)にpokeshiriのモジュールをおいて

git add .
git commit -am "init"
git push heroku master

これでpythonが動く環境ができて勝手にデプロイしてくれる
なお外部依存のモジュールは
requirements.txtに書く必要があるのでrootにコミットしておく

requirements.txt
slackbot==0.5.3

実行するには

heroku run nohup python -u run.py >out.log

止めるには

heroku ps
heroku kill {実行名}

でいけました

あとがき

python知識ゼロからポケモンの名前でしりとりするslackbotを作ったノウハウのすべてでした
pythonは本当に学習コストが少ないと思いました
JSONの読み込みとかリストの並び替えとか辞書の扱い方とかググればすぐにサンプルコードがでてきます。
またググればでてくるポケモンデータJSONのすごさにちょっと感動
ポケモンデータJSON作者様にこのうえない謝辞をおくります。

以上
ありがとうございました。

よかったら「いいね」してください。

Twitterフォローされると喜びます
https://twitter.com/wagase
よろしくお願いします

359
302
6

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
359
302