LINE Botで友だちと遊べるリバーシ開発(3) 手順書


はじめに

こんにちは。本シリーズはLINE Developers Communityの 内で公開されている との連動企画です。

クマくん、ファンク先生と一緒に楽しく学ぼう!

動画では愉快な先生と一緒に楽しい&分かりやすい解説付きで学べます。是非動画と一緒にお楽しみ下さい♪

https://youtu.be/cNrrGGXawOk

手順書(1)はこちら

手順書(2)はこちら

プログラミングをやったことの無い方は を、

少しだけやったことがある方はMr.パイソンの格付けチェックである程度知識がついているかをチェックしてから本シリーズに進むことをオススメします。

https://youtu.be/Tjiy5Q8oI5s

本シリーズでわかること

はじめてのアプリ作成と題して、LINE Botで友だちと遊べるリバーシを開発する手順を学ぶことが出来ます。

なお、本シリーズでは特に断りの無い限り「アプリ」 = 「LINE Bot」を指します。

本記事で解説すること

  • 開発環境の準備
  • メインプログラム実行前の準備
  • メインプログラムの実行
  • 前回との変更点
  • コード解説
    • あああああ

開発環境の準備

本シリーズではGoogle Colabを開発環境として利用します。以下のリンクをクリックしてください。

プログラムページを開く

※ 別タブで開いて下さい。

こちらのGoogle Colabノートブックは筆者が作成したものです。開いた後は、メニューの「ドライブにコピーを作成」をクリックして、ご自身のドライブに保存し、それを編集するようにして下さい。

メインプログラム実行前の準備

以前の記事を参考に3つのキーを入力します。入力が終わったらngrokのコードブロックまで順に実行します。

3つ目のブロックを実行すると下部にURLが表示されます。URLの「http」を「https」に変更し、LINE Developers、Messaging APIタブのWebhook URLにコピーし、保存します。

メインプログラムの実行

「アプリの準備」と「メインプログラムの実行」のブロックの再生ボタンをクリックし、メインプログラムを実行します。

アプリの準備段階で、実行結果に警告が発生し、「RESTART RUNTIME」というボタンが表示されることがあります。この場合はボタンをクリックした後、サイドキーの入力とngrokの準備、実行を行う必要があります。

前回との違い

前回はテキストベースでキーボードで座標を入力して石を置くという面倒な手順でしたが、今回はイメージマップで遊べるようになり、置きたい場所をタップすることで石を置くことが出来るようになりました。

これにより、直感的に遊べるようになっています。

その他の部分に関しては変わりません。

コードの解説

ソースコードの内、前回と変わった部分の解説を行います。

初期盤面

前回までは配列に盤面の状況を保存していましたが、それに加え、画像でも保存するように変更しました。画像はImageクラスを利用し、滑らかな描画を行うためaggdrawを利用して線や石を描画します。

後述するImagemapMessageでは画像サイズを複数用意する必要があるのですが、筆者が利用するスマホからのアクセスでは700pxが要求されたため、リサイズの処理を省略するため700pxで作成しています。

よってマスは87.5px、石はパッディングを5px取ることから77.5pxというサイズになります。

from PIL import Image
import aggdraw

略

# 盤面画像
board_image = Image.init()

# 盤面サイズから描画に必要な各値を計算。1マスは各辺87.5px、石は67.5px。
REVERSI_BOARD_SIZE = 700
REVERSI_SQUARE_SIZE = REVERSI_BOARD_SIZE / 8
REVERSI_STONE_PADDING = 10
REVERSI_STONE_SIZE = REVERSI_SQUARE_SIZE - REVERSI_STONE_PADDING * 2

# 初期配置に戻すための関数
def reset_board():# 盤面の画像を生成する
    pen_black = aggdraw.Pen("black", 5)

    # 背景の緑画像
    board_image = Image.new('RGB', (REVERSI_BOARD_SIZE, REVERSI_BOARD_SIZE), "green")
    canvas = aggdraw.Draw(board_image)
    # 枠線
    canvas.rectangle((0, 0, REVERSI_BOARD_SIZE, REVERSI_BOARD_SIZE), pen_black)

    # マスの区切り線
    for i in range(8):
        h_line_point = (0, REVERSI_SQUARE_SIZE * i, REVERSI_BOARD_SIZE, REVERSI_SQUARE_SIZE * i)
        v_line_point = (REVERSI_SQUARE_SIZE * i, 0, REVERSI_SQUARE_SIZE * i, REVERSI_BOARD_SIZE)

        canvas.line(h_line_point, pen_black)
        canvas.line(v_line_point, pen_black)

    # 初期の石4個。
    brush_black = aggdraw.Brush("black")
    brush_white = aggdraw.Brush("white")

    canvas.ellipse((REVERSI_SQUARE_SIZE * 4 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE * 3 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE *
                   4 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE, REVERSI_SQUARE_SIZE * 3 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE), brush_black)
    canvas.ellipse((REVERSI_SQUARE_SIZE * 3 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE * 4 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE *
                   3 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE, REVERSI_SQUARE_SIZE * 4 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE), brush_black)
    canvas.ellipse((REVERSI_SQUARE_SIZE * 3 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE * 3 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE *
                   3 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE, REVERSI_SQUARE_SIZE * 3 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE), brush_white)
    canvas.ellipse((REVERSI_SQUARE_SIZE * 4 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE * 4 + REVERSI_STONE_PADDING, REVERSI_SQUARE_SIZE *
                   4 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE, REVERSI_SQUARE_SIZE * 4 + REVERSI_STONE_PADDING + REVERSI_STONE_SIZE), brush_white)

    canvas.flush()

イメージマップの送信と画像の用意

イメージマップは一般的に画像を背景とし、その上にタップ可能な領域が設定されたものです。LINE Messaging APIでも利用することができます。

ドキュメント

気をつけなければいけない仕様があり、以下のように対処しています。

タップ可能な領域は20個まで

空きマス全てをタップ可能にするのではなく、事前に置くことが出来るかをチェックしTrueの時のみ領域を設定

背景の画像は1040、700、460、300、240の各サイズが要求される可能性があり、それぞれ用意する必要がある

Messaging APIのイメージマップは以下のような仕様になっており、base_urlにサイズが付与されたURLで画像がリクエストされます。

この際の処理は同一なため、正規表現を利用して一つの関数で受け、リクエストされたサイズに画像をリサイズして返しています。

背景の画像がキャッシュされる

イメージマップがリクエストした画像はキャッシュされてしまうため、画像のURLにタイムスタンプを付与し、都度違うURLを生成しています。こちらも正規表現で可変のURLを処理できるようになっています。


from werkzeug.routing import BaseConverter
import time

app = Flask(__name__)


# アクセスのコントロールに正規表現を使えるようにする
class RegexConverter(BaseConverter):
    def __init__(self, url_map, *items):
        super(RegexConverter, self).__init__(url_map)
        self.regex = items[0]


app.url_map.converters['regex'] = RegexConverter

略

# LINE側でのキャッシュを防止するため、毎回違うURLを生成
@app.route('/resized_image/<regex("[0-9]*"):timestamp>/<regex("[0-9]*"):size>')
def resized_image(timestamp, size):
    global board_image

    img_bytes = BytesIO()

    # 指定サイズがREVERSI_BOARD_SIZE以外の場合のみリサイズ
    if int(size) != REVERSI_BOARD_SIZE:
        resized_image = board_image.resize((int(size), int(size)))
        resized_image.save(img_bytes, format='PNG')
    else:
        board_image.save(img_bytes, format='PNG')

    # PNG画像を生成
    return send_file(
        BytesIO(img_bytes.getvalue()),
        mimetype='image/png',
        as_attachment=False,
        download_name='image.png')
	
	
# 整数の配列からイメージマップを生成し返す関数
def convert_board_to_imagemap():
    global board_as_array
    global is_black_active

    # タップ可能エリアを生成。MAX20個のためひっくり返る場所のみ
    actions = []
    for i in range(8):
        for j in range(8):
            if put_stone_then_return_flip_count(i, j, True):
                actions.append(MessageImagemapAction(
                    text='%i%i' % (i + 1, j + 1),
                    area=ImagemapArea(
                        x=j * 130,
                        y=i * 130,
                        width=130,
                        height=130
                    )
                ))

    # イメージマップメッセージを生成。背景画像はキャッシュ防止の為UNIXタイムを利用し毎回違うURLに。
    message = ImagemapSendMessage(
        base_url='%s/resized_image/%s' % (request.url_root.replace(
            'http://', 'https://'), int(time.time() * 1000)),
        alt_text='リバーシの盤面',
        base_size=BaseSize(height=1040, width=1040),
        actions=actions
    )

    return message

関数put_stone_then_return_flip_countの変更

石を置ける場所をチェックするため、関数put_stone_then_return_flip_countに引数を1つ追加しました。これにより、実際にはひっくり返さず、ひっくり返る数のみを取得することでタップ可能領域の設置判断を行っています。

# 引数の行、列に石を置き、ひっくり返った数を返す関数。is_just_countがTrueの場合はひっくり返さずに数を返す
- def put_stone_then_return_flip_count(row, col):
+ def put_stone_then_return_flip_count(row, col, is_just_count):
     # チェック
+     if is_just_count:
+         return len(flipped_pos) > 0

まとめ

本記事ではリバーシをテキストベースからイメージマップで遊べるよう改良しました。これにより見た目がリッチになっただけではなく、石を置くマスの指定をテキスト入力ではなくタップで行えるようになり、利便性が向上しています。

なお、本シリーズで解説した構成ではGoogle Colabが起動中しか遊べず、1つのゲームしか状況を保存できません。サービスが一度終了されると保存された状況もリセットされてしまいます。

実際にゲームを公開するためにはGoogle Colabではなくクラウドへのデプロイ(アップロード)やデータベースの利用が必要になります。

LINE Botをクラウド上で運用する手法やデータベースについての詳細は以下の動画で解説されていますので是非そちらを御覧ください。

その他にもLINE Developers Communityでは様々な動画が公開されております。是非チャンネル登録のうえ、お楽しみ下さい。