これまで作っていた麻雀ゲームをステートレスになるように
改良できたので早速Web上にアップしたいな〜と思っていろいろやりました。
一応公開まで出来たのでURLも置いときます。
良ければ触ってみてください!!
https://lonlymahjong.herokuapp.com/
アプリケーションプラットフォーム(サーバー)はHerokuを利用しました。
AWSなら現職でも使う機会があっていいかなーとも思ったんですが、
お金かかるから辞めました。(そのくらい払えよって話ですが…)
せっかくなのでALL0円でやります!
#使用技術
一旦開発にあたり使用した技術を整理します。
◇開発言語
Python3
◇サーバーサイドフレームワーク
Flask
◇フロントエンドフレームワーク
なし
◇開発OS
mac
◇IDE
pycharm
◇ソース管理
GitHub
◇アプリケーションプラットフォーム
Heroku
◇グラフィックコンポーネント
irasutoya
フロントエンドフレームワーク「なし」ってのが時代に逆行していて
我ながら潔いです…。
#Herokuのアカウント作成
Herokuのアカウント作成は0円です。
AWSみたいにクレジットカードの登録とかもいりません。
Herokuなら無料でも個人の趣味で行う分には不満に思うことはなさそうです。
参考)山崎屋の技術メモさん
では早速登録の方法を。
といっても難しいことは何もなくHerokuのサイト
に行ってポチポチ必要な情報を入力するだけです。
先程も言いましたがクレジットカードの登録とかは不要なので、
気軽にできますね♪
#Herokuにアプリをアップロード
早速Herokuに作成したアプリをアップロードします。
アップロードの方法ですが、2種類あります。
①Heroku CLIをインストールして作業端末からアプリをアップロード
②GitHubで管理しているソースからHerokuにアップロード
僕が作ったアプリはGitHubにてソース管理しているので
②の方法で行きたいと思います。
ブラウザをぽちぽち押してるだけでできますしね。
先程のアカウント作成後のページ(ダッシュボード)にて
「新しいアプリを作成する」(Create new app)を押して
作成するアプリの情報を入力する画面を出します。
といってもここではまだ
アプリの名前とサーバーのリージョン選択だけですが。
ここで入力したアプリ名がのちのちURLにもなります。
今回はひとりで永遠に麻雀するだけなので「lonlymahjong」にしました。
今流行りのぼっちですね。
んで、「Create app」を押してアプリの登録を行います。
アップロードしたいプロジェクトと「Connect」!
うまくいくと「Automatic deploys」「Manual deploy」が出てきます。
とりあえず「Manual deploy」しましょう。
Manual deployの「Deploy Branch」を押すと…
はい!失敗しました!!
#requirements.txtをつくろう
失敗した原因はrequirements.txtを用意していなかったからです。
requirements.txtは、アプリ環境の設定ファイルです。
こいつが無いとさすがのHerokuも何をしたらいいかわからないです。
ということでこのファイルをpycharmから作ります。
pycharmの「ターミナル」を押してターミナルを起動。
起動されたターミナルで
pip freeze > requirements.txt
と入力するだけです。
無事、requirements.txtが作成できましたー。
早速GitHubにrequirements.txtをアップしてもっかいデプロイします!
Your app was successfully deployed.
って出てます。
成功したみたいです!
早速「View」を押して動作確認しましょう!
#Herokuのログを見よう
Webサーバーは動いてるみたいなのでエラーログを確認します。
Herokuのアプリログは自分の作業端末のターミナルからの確認となります。
###CLIをインストール
まずは、Heroku CLIをインストールします。
いろいろなやり方があるみたいですが、僕はインストーラーでインストールしました。
###CLIからログイン
インストールできたらターミナルで、
$ heroku login
を入力してログインします。そうすると
heroku: Press any key to open up the browser to login or q to exit:
と帰ってくるので何も入力せずEnter
ブラウザが起動するのでそこで「Log in」ボタンを押せばログインとなります。
###CLIからログを見る
ログインできたら早速ログを見ます。
ログを見るコマンドは $ heroku logs
です。
ただし、ここで注意です。
今回Herokuへのアップロードは②GitHubで管理しているソースからHerokuにアップロード
を採用しているので、heroku logsだけではだめです。
--appオプションをつけて、どのアプリかを指定する必要があります。
参考)@ynunokawaさん
今回作成したアプリ名はlonlymahjong なので、
$ heroku logs --app lonlymahjong
というコマンドを打てばOKです。
ログを確認できました!
desc="No web processes running"
って出てますね。
web processesがNo runningのようですね。
なのでrunningしてもらうようにしましょう。
#Procfileをつくろう
失敗した原因はProcfileを用意していなかったからです。
Procfileは、アプリ起動コマンドを定義するファイルです。
こいつが無いとさすがのHerokuも何をしたらいいかわからないです。
ということでこのファイルをpycharmから作ります。
っと言ってもrequirements.txtみたいに特にコマンドがあるわけでもなく
Procfileというファイルをルートディレクトリ直下に作成し、
web processesの実行コマンドを書くだけです。なので、
web: python main.py
と書くだけでOK!
早速GitHubにProcfileをアップしてもっかいデプロイします!
失敗だ!\(*^◯^*)/めげずにログを見ると
Web process failed to bind to $PORT within 60 seconds of launch
となってます。
つまり どういうことだってばよ…!?
#ポートを指定しよう
いろいろ調べてみましたが、アプリケーションで設定しているポートがHerokuが設定するポート(5000)
と異なるとこのエラーが出るみたいですね。
なので、アプリケーションで設定しているポートを変更します。
この変更は、flaskフレームワークのapp起動を行っているpythonのプログラムになります。
if __name__ == '__main__':
#app.run(port=8080) 下記に変更
port = int(os.environ.get("PORT", 5000))
app.run(host='0.0.0.0', port=port)
早速GitHubに修正したファイルをアップしてもっかいデプロイします!
できた…。
三度目?の正直でようやくできました…。
疲れた…。
#感想
なんとか公開までできました☺
Herokuすごいですね〜。ここまで出来て無料でいいのでしょうか…。
あとirasutoya。
こっちはいまさら僕がなにか言うまでもないですね(笑)
でも、牌を捨てるたびにサーバーまで処理が行くのはあまり良くないですね。
HerokuのリージョンがUSAなので、毎回アメリカまで通信が行くことになりますし…。
なので今度はフロントエンドフレームワークを学んでレスポンスがすくなるようにVerUpしていきたいです。
ReactかVueかAngularか…。
どれにしようかな〜。
#おまけ
参考までに今回作成したプログラムものっけときます。
何かのお役に立てれば幸いです。
mahjong/
├static/
│ ├css/
│ │└main.css
│ └pic/
│ └https://www.irasutoya.com/
├templates/
│ └main.html
├Procfile
├requirements.txt
├main.py
└mahjong.py
from flask import Flask, render_template, request
import mahjong
import os
app = Flask(__name__)
# 配牌
@app.route('/')
def main():
yamahai = mahjong.create_yamahai() # 山牌
tehai = [yamahai.pop(0) for i in range(13)] # 配牌
tehai.sort(key=lambda hai: hai.sort_info()) # 理牌
tehai.append(yamahai.pop(0)) # 自摸
return render_template('main.html', tehai=tehai, win=mahjong.judge(tehai))
# 自摸
@app.route('/change', methods=['POST'])
def change():
dahai = mahjong.tile_from_pic(request.form['dahai']) # 打牌
sutehai = [mahjong.tile_from_pic(pic) for pic in request.form.getlist('sutehai')] # 捨て牌
tehai = [mahjong.tile_from_pic(pic) for pic in request.form.getlist('tehai')] # 手牌
tehai.remove(dahai)# 手牌から打牌を削除
sutehai.append(dahai)# 捨て牌に打牌を追加
yamahai = mahjong.create_yamahai() # 山牌再作成
for tile in sutehai + tehai: # 山牌から捨て牌と手牌を削除
yamahai.remove(tile)
tehai.sort(key=lambda hai: hai.sort_info()) # 理牌
tehai.append(yamahai.pop(0)) # 自摸
return render_template('main.html', tehai=tehai, sutehai=sutehai, win=mahjong.judge(tehai))
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
app.run(host='0.0.0.0', port=port)
import random
import copy
import re
# 麻雀牌のクラス
class Tile:
SUUPAI = 'pinzu', 'manzu', 'souzu'
JIHAI = 'sufonpai', 'sangenpai'
WINDS = '東南西北'
COLORS = '白發中'
def __init__(self, kind, value):
self.kind = kind # 麻雀牌の種類(萬子・筒子・索子・四風牌・三元牌)
self.value = value # 麻雀牌の値(1~9 東南西北白発中)
self.pic = f'{kind}_{value}.png' # 画像ファイル名
def __repr__(self):
return self.pic
def __eq__(self, other):
if not isinstance(other, Tile):
return False
return self.pic == other.pic
def __hash__(self):
return hash(self.pic)
def sort_info(self):
if Tile.SUUPAI[0] == self.kind:
return f'0_{self.value}'
elif Tile.SUUPAI[1] == self.kind:
return f'1_{self.value}'
elif Tile.SUUPAI[2] == self.kind:
return f'2_{self.value}'
elif Tile.JIHAI[0] == self.kind:
return f'3_{self.value}'
elif Tile.JIHAI[1] == self.kind:
return f'4_{self.value}'
# 山牌 シャッフルされた136個のTileオブジェクトリストを返却
def create_yamahai():
tiles = [Tile(kind, str(value))
for kind in Tile.SUUPAI
for value in range(1, 1 + 9)]
tiles += [Tile(Tile.JIHAI[0], value)
for value, label in enumerate(Tile.WINDS, 1)]
tiles += [Tile(Tile.JIHAI[1], value)
for value, label in enumerate(Tile.COLORS, 1)]
tiles *= 4
random.shuffle(tiles)
random.shuffle(tiles)
random.shuffle(tiles)
return tiles
# pic(画像ファイル名)からTileオブジェクトを作成
def tile_from_pic(pic):
s = re.search(r'^(.+)_(.+)\.png$', pic).groups()
return Tile(s[0], s[1])
# 通常あがり牌
class Agari:
def __init__(self, janto, mentsu1, mentsu2, mentsu3, mentsu4):
self.janto = janto
self.mentsu1 = mentsu1
self.mentsu2 = mentsu2
self.mentsu3 = mentsu3
self.mentsu4 = mentsu4
def __repr__(self):
return f'[{repr(self.janto[0])},{repr(self.janto[1])}],' \
f'[{repr(self.mentsu1.tiles[0])},{repr(self.mentsu1.tiles[1])},{repr(self.mentsu1.tiles[2])}],' \
f'[{repr(self.mentsu2.tiles[0])},{repr(self.mentsu2.tiles[1])},{repr(self.mentsu2.tiles[2])}],' \
f'[{repr(self.mentsu3.tiles[0])},{repr(self.mentsu3.tiles[1])},{repr(self.mentsu3.tiles[2])}],' \
f'[{repr(self.mentsu4.tiles[0])},{repr(self.mentsu4.tiles[1])},{repr(self.mentsu4.tiles[2])}]'
class Janto:
def __init__(self, tiles):
self.tiles = tiles
class Mentsu:
KIND = 'syuntsu', 'koutsu'
def __init__(self, kind, tiles):
self.kind = kind
self.tiles = tiles
class NoMentsu(Exception):
pass
# 七対子
class Titoitsu:
def __init__(self, l_toitsu):
self.l_toitsu = l_toitsu
def __repr__(self):
return f'[{repr(self.l_toitsu[0])},{repr(self.l_toitsu[0])}],' \
f'[{repr(self.l_toitsu[1])},{repr(self.l_toitsu[1])}],' \
f'[{repr(self.l_toitsu[2])},{repr(self.l_toitsu[2])}],' \
f'[{repr(self.l_toitsu[3])},{repr(self.l_toitsu[3])}],' \
f'[{repr(self.l_toitsu[4])},{repr(self.l_toitsu[4])}],' \
f'[{repr(self.l_toitsu[5])},{repr(self.l_toitsu[5])}],' \
f'[{repr(self.l_toitsu[6])},{repr(self.l_toitsu[6])}]'
# 国士無双
class Kokushimusou:
def __init__(self, l_tile):
self.l_tile = l_tile
def __repr__(self):
return f'{self.l_tile}'
# あがり判定
def judge(tehai):
agari_hai = []
# 雀頭の種類
l_janto = sorted([x for x in set(tehai) if tehai.count(x) >= 2], key=lambda hai: f'{hai.kind}{hai.value}')
if len(l_janto) == 0:
return agari_hai
# 国士無双
if check_kokushimusou(tehai, l_janto):
return Kokushimusou(tehai)
# 七対子
if len(l_janto) == 7:
agari_hai.append(Titoitsu(l_janto))
# 通常役
for janto in l_janto:
mentsu_kouho = copy.deepcopy(tehai)
mentsu_kouho.remove(janto)
mentsu_kouho.remove(janto)
mentsu_kouho.sort(key=lambda hai: f'{hai.kind}{hai.value}')
# 刻子の種類
l_koutsu = sorted([x for x in set(mentsu_kouho) if mentsu_kouho.count(x) >= 3],
key=lambda hai: f'{hai.kind}{hai.value}')
# 刻子が0個のパターン
agari_hai.extend(agari_koutsu0(mentsu_kouho, janto))
# 刻子が1個のパターン
agari_hai.extend(agari_koutsu1(mentsu_kouho, janto, l_koutsu))
# 刻子が2個のパターン
agari_hai.extend(agari_koutsu2(mentsu_kouho, janto, l_koutsu))
# 刻子が3個のパターン
agari_hai.extend(agari_koutsu3(mentsu_kouho, janto, l_koutsu))
# 刻子が4個のパターン
agari_hai.extend(agari_koutsu4(janto, l_koutsu))
return len(agari_hai) > 0
# 刻子が0個のあがりパターン
def agari_koutsu0(mentsu_kouho, janto):
try:
hanteiyou = copy.deepcopy(mentsu_kouho)
first = find_one_syuntu(hanteiyou)
hanteiyou.remove(first.tiles[0])
hanteiyou.remove(first.tiles[1])
hanteiyou.remove(first.tiles[2])
second = find_one_syuntu(hanteiyou)
hanteiyou.remove(second.tiles[0])
hanteiyou.remove(second.tiles[1])
hanteiyou.remove(second.tiles[2])
third = find_one_syuntu(hanteiyou)
hanteiyou.remove(third.tiles[0])
hanteiyou.remove(third.tiles[1])
hanteiyou.remove(third.tiles[2])
fourth = find_one_syuntu(hanteiyou)
return [Agari([janto for x in range(2)], first, second, third, fourth)]
except NoMentsu:
return []
# 刻子が1個のあがりパターン
def agari_koutsu1(mentsu_kouho, janto, l_koutsu):
if len(l_koutsu) < 1:
return []
result = []
for koutsu in l_koutsu:
try:
hanteiyou = copy.deepcopy(mentsu_kouho)
first = Mentsu(Mentsu.KIND[1], [koutsu for x in range(3)])
hanteiyou.remove(first.tiles[0])
hanteiyou.remove(first.tiles[1])
hanteiyou.remove(first.tiles[2])
second = find_one_syuntu(hanteiyou)
hanteiyou.remove(second.tiles[0])
hanteiyou.remove(second.tiles[1])
hanteiyou.remove(second.tiles[2])
third = find_one_syuntu(hanteiyou)
hanteiyou.remove(third.tiles[0])
hanteiyou.remove(third.tiles[1])
hanteiyou.remove(third.tiles[2])
fourth = find_one_syuntu(hanteiyou)
result.append(Agari([janto for x in range(2)], first, second, third, fourth))
except NoMentsu:
continue
return result
# 刻子が2個のあがりパターン
def agari_koutsu2(mentsu_kouho, janto, l_koutsu):
if len(l_koutsu) < 2:
return []
result = []
for i in range(len(l_koutsu) - 1):
for j in range(i + 1, len(l_koutsu)):
try:
hanteiyou = copy.deepcopy(mentsu_kouho)
first = Mentsu(Mentsu.KIND[1], [l_koutsu[i] for x in range(3)])
hanteiyou.remove(first.tiles[0])
hanteiyou.remove(first.tiles[1])
hanteiyou.remove(first.tiles[2])
second = Mentsu(Mentsu.KIND[1], [l_koutsu[j] for x in range(3)])
hanteiyou.remove(second.tiles[0])
hanteiyou.remove(second.tiles[1])
hanteiyou.remove(second.tiles[2])
third = find_one_syuntu(hanteiyou)
hanteiyou.remove(third.tiles[0])
hanteiyou.remove(third.tiles[1])
hanteiyou.remove(third.tiles[2])
fourth = find_one_syuntu(hanteiyou)
result.append(Agari([janto for x in range(2)], first, second, third, fourth))
except NoMentsu:
continue
return result
# 刻子が3個のあがりパターン
def agari_koutsu3(mentsu_kouho, janto, l_koutsu):
if len(l_koutsu) != 3:
return []
try:
hanteiyou = copy.deepcopy(mentsu_kouho)
first = Mentsu(Mentsu.KIND[1], [l_koutsu[0] for x in range(3)])
hanteiyou.remove(first.tiles[0])
hanteiyou.remove(first.tiles[1])
hanteiyou.remove(first.tiles[2])
second = Mentsu(Mentsu.KIND[1], [l_koutsu[1] for x in range(3)])
hanteiyou.remove(second.tiles[0])
hanteiyou.remove(second.tiles[1])
hanteiyou.remove(second.tiles[2])
third = Mentsu(Mentsu.KIND[1], [l_koutsu[2] for x in range(3)])
hanteiyou.remove(third.tiles[0])
hanteiyou.remove(third.tiles[1])
hanteiyou.remove(third.tiles[2])
fourth = find_one_syuntu(hanteiyou)
return [Agari([janto for x in range(2)], first, second, third, fourth)]
except NoMentsu:
return []
# 刻子が4個のあがりパターン
def agari_koutsu4(janto, l_koutsu):
if len(l_koutsu) != 4:
return []
return [Agari([janto for x in range(2)], Mentsu(Mentsu.KIND[1], [l_koutsu[0] for x in range(3)]),
Mentsu(Mentsu.KIND[1], [l_koutsu[1] for x in range(3)]),
Mentsu(Mentsu.KIND[1], [l_koutsu[2] for x in range(3)]),
Mentsu(Mentsu.KIND[1], [l_koutsu[3] for x in range(3)]))]
# 国士無双のチェック(前提として雀頭があること)
def check_kokushimusou(tehai, l_koutsu):
if len(l_koutsu) != 1:
return []
if Tile(Tile.SUUPAI[0], '1') in tehai \
and Tile(Tile.SUUPAI[0], '9') in tehai \
and Tile(Tile.SUUPAI[1], '1') in tehai \
and Tile(Tile.SUUPAI[1], '9') in tehai \
and Tile(Tile.SUUPAI[2], '1') in tehai \
and Tile(Tile.SUUPAI[2], '9') in tehai \
and Tile(Tile.JIHAI[0], Tile.WINDS[0]) in tehai \
and Tile(Tile.JIHAI[0], Tile.WINDS[1]) in tehai \
and Tile(Tile.JIHAI[0], Tile.WINDS[2]) in tehai \
and Tile(Tile.JIHAI[0], Tile.WINDS[3]) in tehai \
and Tile(Tile.JIHAI[1], Tile.COLORS[0]) in tehai \
and Tile(Tile.JIHAI[1], Tile.COLORS[1]) in tehai \
and Tile(Tile.JIHAI[1], Tile.COLORS[2]) in tehai:
return True
# 順子をひとつ見つける
def find_one_syuntu(hanteiyou):
hanteiyou.sort(key=lambda hai: f'{hai.kind}{hai.value}')
for hanteiyou_one_tile in hanteiyou:
syuntsu_kouho = create_syuntsu(hanteiyou_one_tile)
if syuntsu_kouho is None:
continue
if syuntsu_kouho[1] in hanteiyou and syuntsu_kouho[2] in hanteiyou:
return Mentsu(Mentsu.KIND[0], syuntsu_kouho)
raise NoMentsu()
# 自身を一番最初とした順子を返却
def create_syuntsu(tile):
if tile.kind in Tile.SUUPAI and int(tile.value) <= 7:
return [Tile(tile.kind, str(value))
for value in range(int(tile.value), int(tile.value) + 3)]
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet"href="/static/css/main.css">
<title>ひとりまーじゃん</title>
</head>
<body>
{% if win %}
<div class="win">
<a href="/"><img src=/static/pic/win.png></a>
</div>
{% else %}
<div class="sutehai">
{% for tile in sutehai %}{% if ((loop.index - 1) % 9) == 0 %}<br>{% endif %}<img src=/static/pic/{{tile.pic}}>{% endfor %}
</div>
{% endif %}
<div class="tehai">{% for tile in tehai %}{% if loop.last %} {% endif %}<a onclick="change.dahai.value = '{{tile.pic}}';change.submit();"><img src=/static/pic/{{tile.pic}}></a>{% endfor %}</div>
<form name="change" action="{{ url_for('change') }}" method=post>
{% for tile in sutehai %}
<input name="sutehai" type="hidden" value="{{tile.pic}}">
{% endfor %}
{% for tile in tehai %}
<input name="tehai" type="hidden" value="{{tile.pic}}">
{% endfor %}
<input name="dahai" type="hidden" value="">
</form>
</body>
</html>
body{
background-color: mediumaquamarine;
}
.sutehai{
text-align : left;
margin-left : 100px;
margin-top : 100px;
}
.tehai{
position : absolute;
bottom : 0;
margin-bottom : 30px;
}