Python+Flask+Herokuで麻雀アプリを作りたい（作った）

これまで作っていた麻雀ゲームをステートレスになるように
改良できたので早速Web上にアップしたいな〜と思っていろいろやりました。

一応公開まで出来たのでURLも置いときます。
良ければ触ってみてください！！
https://lonlymahjong.herokuapp.com/

↓こんな感じのを作りました〜。
<img width="1090" alt="スクリーンショット 2019-06-12 22.49.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/f84ec93e-ffb8-8476-9d30-13b1ec469310.png">






アプリケーションプラットフォーム（サーバー）はHerokuを利用しました。
AWSなら現職でも使う機会があっていいかなーとも思ったんですが、
お金かかるから辞めました。（そのくらい払えよって話ですが…）
せっかくなのでALL０円でやります！

#使用技術
一旦開発にあたり使用した技術を整理します。

◇開発言語
　Python3
◇サーバーサイドフレームワーク
　Flask
◇フロントエンドフレームワーク
　なし
◇開発OS
　mac
◇IDE
　pycharm
◇ソース管理
　GitHub
◇アプリケーションプラットフォーム
　Heroku
◇グラフィックコンポーネント
　[irasutoya](https://www.irasutoya.com/)

フロントエンドフレームワーク「なし」ってのが時代に逆行していて
我ながら潔いです…。

#Herokuのアカウント作成
Herokuのアカウント作成は[０円](https://jp.heroku.com/pricing)です。
[AWSみたいに](https://aws.amazon.com/jp/aws-jp-faq/prospects/#free-noncard)クレジットカードの登録とかもいりません。
Herokuなら無料でも個人の趣味で行う分には不満に思うことはなさそうです。
参考）[山崎屋の技術メモさん](https://www.shookuro.com/entry/2018/05/02/133225)

では早速登録の方法を。
といっても難しいことは何もなく[Herokuのサイト](https://jp.heroku.com/)
に行ってポチポチ必要な情報を入力するだけです。
先程も言いましたがクレジットカードの登録とかは不要なので、
気軽にできますね♪

<img width="1128" alt="スクリーンショット 2019-06-08 15.45.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/ba38d6ba-6471-6e37-fc71-a9401a3a1659.png">

<img width="1128" alt="スクリーンショット 2019-06-08 15.45.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/41102123-c233-37ed-712a-7592fa017327.png">

<img width="1128" alt="スクリーンショット 2019-06-08 15.48.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/bc4ac6eb-bb85-83f0-dc24-f2339faea85e.png">
アカウント作成できましたー！


#Herokuにアプリをアップロード
早速Herokuに作成したアプリをアップロードします。
アップロードの方法ですが、２種類あります。
　①Heroku CLIをインストールして作業端末からアプリをアップロード
　②GitHubで管理しているソースからHerokuにアップロード
僕が作ったアプリは[GitHubにてソース管理](https://github.com/kph7m)しているので
②の方法で行きたいと思います。
ブラウザをぽちぽち押してるだけでできますしね。

先程のアカウント作成後のページ（ダッシュボード）にて
「新しいアプリを作成する」(Create new app)を押して
作成するアプリの情報を入力する画面を出します。
<img width="1188" alt="スクリーンショット 2019-06-10 3.07.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/590d79e2-3895-a15f-3d36-33c0da8efc64.png">

といってもここではまだ
アプリの名前とサーバーのリージョン選択だけですが。
ここで入力したアプリ名がのちのちURLにもなります。
今回はひとりで永遠に麻雀するだけなので「lonlymahjong」にしました。
今流行りのぼっちですね。
んで、「Create app」を押してアプリの登録を行います。

<img width="1188" alt="スクリーンショット 2019-06-10 3.16.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/98f2f33f-e82e-a1fb-dca3-821cddffd921.png">
「Deploy」タグを押すと見慣れた[GitHubのアイコン](https://octodex.github.com/)があるのでこれを押して
GitHubのアカウントと紐づけます。
<img width="612" alt="スクリーンショット 2019-06-08 15.58.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/018b2003-90a4-6cdf-4272-a0e1812397a5.png">
いろいろうにゃうにゃやって（すいません、どうやるか忘れました…。）
<img width="1188" alt="スクリーンショット 2019-06-10 3.29.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/66103c5d-13b9-b476-8fb6-d47437667669.png">
自身のGitHubアカウントが表示されればOKです。

んで、後は「Search」を押して
<img width="1188" alt="スクリーンショット 2019-06-10 3.31.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/5cfd7428-a4b7-d00a-ff1c-6d16556c7ef9.png">

アップロードしたいプロジェクトと「Connect」！

<img width="1188" alt="スクリーンショット 2019-06-10 3.34.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/467e5aa6-1184-167c-6b28-12388c270bb8.png">

うまくいくと「Automatic deploys」「Manual deploy」が出てきます。
とりあえず「Manual deploy」しましょう。

Manual deployの「Deploy Branch」を押すと…

<img width="1188" alt="スクリーンショット 2019-06-10 3.44.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/1043d073-3612-cfa1-9ecd-5d73c0bcd132.png">

はい！失敗しました！！

#requirements.txtをつくろう
失敗した原因はrequirements.txtを用意していなかったからです。
requirements.txtは、アプリ環境の設定ファイルです。
こいつが無いとさすがのHerokuも何をしたらいいかわからないです。
ということでこのファイルをpycharmから作ります。

pycharmの「ターミナル」を押してターミナルを起動。
起動されたターミナルで
pip freeze > requirements.txt
と入力するだけです。
<img width="1318" alt="スクリーンショット 2019-06-10 3.54.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/9c9afcdb-ef9d-a779-51d1-7841a8be192c.png">
無事、requirements.txtが作成できましたー。
早速GitHubにrequirements.txtをアップしてもっかいデプロイします！

<img width="1188" alt="スクリーンショット 2019-06-10 4.02.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/3961cb69-e1d3-2fe3-b507-19cde0b52b3b.png">
おお、なんか上手く行きそう。

<img width="1188" alt="スクリーンショット 2019-06-10 4.03.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/b92ac9fb-ffd0-bf21-e4b3-19b78fec98a3.png">

`Your app was successfully deployed.`　って出てます。
成功したみたいです！
早速「View」を押して動作確認しましょう！

<img width="1188" alt="スクリーンショット 2019-06-10 4.05.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/f1bf8287-73e4-c0c6-f999-2fadf75883be.png">
失敗だ！
なんでじゃ…。なにがあかんのや…。


#Herokuのログを見よう
Webサーバーは動いてるみたいなのでエラーログを確認します。
Herokuのアプリログは自分の作業端末のターミナルからの確認となります。

###CLIをインストール
まずは、[Heroku CLIをインストール](https://devcenter.heroku.com/articles/heroku-cli#download-and-install)します。
いろいろなやり方があるみたいですが、僕はインストーラーでインストールしました。

###CLIからログイン
インストールできたらターミナルで、
`$ heroku login`
を入力してログインします。そうすると
`heroku: Press any key to open up the browser to login or q to exit:`
と帰ってくるので何も入力せずEnter
ブラウザが起動するのでそこで「Log in」ボタンを押せばログインとなります。

<img width="1058" alt="スクリーンショット 2019-06-10 4.22.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/07aad31e-7753-fd14-d30d-290b51bcff49.png">
<img width="697" alt="スクリーンショット 2019-06-10 4.26.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/cb8d1e84-cf2d-97ee-7ab9-eeb6b78785e3.png">

###CLIからログを見る
ログインできたら早速ログを見ます。
ログを見るコマンドは　`$ heroku logs`　です。
ただし、ここで注意です。
今回Herokuへのアップロードは②GitHubで管理しているソースからHerokuにアップロード
を採用しているので、heroku logsだけではだめです。
--appオプションをつけて、どのアプリかを指定する必要があります。
参考）[@ynunokawaさん](https://qiita.com/ynunokawa/items/036b284d5f1cee947b32)
今回作成したアプリ名はlonlymahjong　なので、
`$ heroku logs --app lonlymahjong`
というコマンドを打てばOKです。

<img width="823" alt="スクリーンショット 2019-06-10 4.42.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/e81991c6-8246-0cd5-7f45-d396901700be.png">

ログを確認できました！
`desc="No web processes running"`
って出てますね。
web processesがNo runningのようですね。
なのでrunningしてもらうようにしましょう。

#Procfileをつくろう
失敗した原因はProcfileを用意していなかったからです。
Procfileは、アプリ起動コマンドを定義するファイルです。
こいつが無いとさすがのHerokuも何をしたらいいかわからないです。
ということでこのファイルをpycharmから作ります。
<img width="990" alt="スクリーンショット 2019-06-10 4.49.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/0ca64d7e-21ae-63f7-c45b-9c0d9426bd19.png">

っと言ってもrequirements.txtみたいに特にコマンドがあるわけでもなく
Procfileというファイルをルートディレクトリ直下に作成し、
web processesの実行コマンドを書くだけです。なので、
`web: python main.py`
と書くだけでOK！

早速GitHubにProcfileをアップしてもっかいデプロイします！

<img width="1188" alt="スクリーンショット 2019-06-10 4.05.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/f1bf8287-73e4-c0c6-f999-2fadf75883be.png">
失敗だ！＼(*^◯^*)／

<img width="823" alt="スクリーンショット 2019-06-10 5.01.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/1b8b98bd-454f-70a6-6872-69f14128166a.png">

めげずにログを見ると
`Web process failed to bind to $PORT within 60 seconds of launch`
となってます。
つまり どういうことだってばよ…!?

#ポートを指定しよう
いろいろ調べてみましたが、アプリケーションで設定しているポートがHerokuが設定するポート（5000）
と異なるとこのエラーが出るみたいですね。
なので、アプリケーションで設定しているポートを変更します。
この変更は、flaskフレームワークのapp起動を行っているpythonのプログラムになります。

```main.py
if __name__ == '__main__':
    #app.run(port=8080)　　下記に変更
    port = int(os.environ.get("PORT", 5000))
    app.run(host='0.0.0.0', port=port)

```
参考）[@Masa79さん](https://qiita.com/Masa79/items/9a7c5d8f54483e71abbc)
　　　[@ekzemplaroさん](https://qiita.com/ekzemplaro/items/b25033bc03ef45938623)

早速GitHubに修正したファイルをアップしてもっかいデプロイします！

<img width="1107" alt="スクリーンショット 2019-06-10 5.20.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363095/deb75a3d-2e43-ac06-ad34-beea43449b83.png">

できた…。
三度目？の正直でようやくできました…。
疲れた…。

#感想
なんとか公開までできました☺
Herokuすごいですね〜。ここまで出来て無料でいいのでしょうか…。
あと[irasutoya](https://www.irasutoya.com/)。
こっちはいまさら僕がなにか言うまでもないですね（笑）

でも、牌を捨てるたびにサーバーまで処理が行くのはあまり良くないですね。
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
```

```main.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)

```

```mahjong.py　麻雀部分の処理　あがり判定とか
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}'


# 山牌　シャッフルされた１３６個の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}')

        # 刻子が０個のパターン
        agari_hai.extend(agari_koutsu0(mentsu_kouho, janto))

        # 刻子が１個のパターン
        agari_hai.extend(agari_koutsu1(mentsu_kouho, janto, l_koutsu))

        # 刻子が２個のパターン
        agari_hai.extend(agari_koutsu2(mentsu_kouho, janto, l_koutsu))

        # 刻子が３個のパターン
        agari_hai.extend(agari_koutsu3(mentsu_kouho, janto, l_koutsu))

        # 刻子が４個のパターン
        agari_hai.extend(agari_koutsu4(janto, l_koutsu))

    return len(agari_hai) > 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 []


# 刻子が１個のあがりパターン
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


# 刻子が２個のあがりパターン
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


# 刻子が３個のあがりパターン
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 []


# 刻子が４個のあがりパターン
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)]
```

```main.html
<!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 %}&nbsp;{% 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>
```

```main.css
body{
    background-color: mediumaquamarine;
}
.sutehai{
    text-align  : left;
    margin-left : 100px;
    margin-top  : 100px;
}
.tehai{
    position      : absolute;
    bottom        : 0;
    margin-bottom : 30px;
}

```