見出し画像

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

こんなのです

画像1

ソースは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ライブラリで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]

機能とか

ポケモンじゃないときは

画像2



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

画像3


しりとりだから怒られる

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

画像4

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

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

画像5

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

画像6


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

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

画像7


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

ランキング機能

画像8


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

リセット機能

画像9


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

ログ表示機能

画像10


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

# ソース

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

この記事に下記にコピペしているソースは
特定のポケモンやある条件で発生するバグがあります
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
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ファイルのありか
)

`
その他

ニドラン♀とかニドラン♂が入力できない問題
slackの仕様なのか♀が「:女性のマーク:」っていう絵文字になるんですけど

画像11


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

↑これ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
よろしくお願いします

以下投げ銭用コメント

ここから先は

53字

¥ 100

投げ銭用に有料記事設定をしていますが 記事は基本的にすべて無料で読めます