Python
Slack
slackbot
しりとり
ポケモン

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


こんなのです

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

だれか解決方法知ってたら教えてください(小声)

</追記>


あとがき

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

pythonは本当に学習コストが少ないと思いました

JSONの読み込みとかリストの並び替えとか辞書の扱い方とかググればすぐにサンプルコードがでてきます。

またググればでてくるポケモンデータJSONのすごさにちょっと感動

ポケモンデータJSON作者様にこのうえない謝辞をおくります。

以上

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

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

Twitterフォローされると喜びます

https://twitter.com/wagase

よろしくお願いします