🎮

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

2022/03/28に公開

はじめに

こんにちは。本シリーズはLINE Developers CommunityYoutubeチャンネル内で公開されている0からのプログラミングシリーズとの連動企画です。

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

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

https://youtu.be/Wd0NLRLMJs8

Vol.1の手順書はこちら

https://zenn.dev/func/articles/9afbd735856527

プログラミングをやったことの無い方はそのPCで今日から始めよう!【0からのプログラミングシリーズ】を、

https://www.youtube.com/watch?v=6ZGbbBg9iuY&list=PLFA0577mKojXugUw40Th2CWq2_xYyVBMR&index=5

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

https://youtu.be/Tjiy5Q8oI5s

本シリーズでわかること

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

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

本記事で解説すること

  • 開発環境の準備
  • Botの設定
  • メインプログラム実行前の準備
  • メインプログラムの実行
  • 遊びかた
  • コード解説
    • Messaging APIの実装
    • 盤面状況の保存とテキスト化
    • ゲーム開始
    • 石を置き、はさんだ相手方の石をひっくり返す処理
    • ゲーム終了、パスの処理

開発環境の準備

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

プログラムページを開く

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

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

Botの設定

開発を始める前に、LINE DevelopersでBotの挙動の設定を変更しておきましょう。

チャネルのMessaging APIタブを開き、下までスクロールします。

今回の変更はLINE Developersではなく、LINE Official Account Manager側で行う必要があります。(1つのチャネルを2つの管理画面で管理します。)

「グループ・複数人・トークの〜」の横の「編集」をクリックし、遷移先で「グループ・複数人トークへの参加を許可する」をチェックします。

続いて応答メッセージも編集をクリックし、オフに設定します。

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

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

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

メインプログラムの実行

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

遊び方

  • 人vs人でリバーシが遊べるBotです。CPUは搭載していません。
  • 1人でも動作確認は可能です。その場合、黒番と白番を両方一人で担当することになります。
  • LINEグループに追加することで、2人で遊ぶことができます。

開始

「開始」と呼びかけることでいつでも盤面を初期状態にリセットし、黒の先行でゲームを開始できます。

石の置き方

行と列をテキストでBotに送信することで石を置くことができます。例えば左上は1行目1列目なので「11」と続けて送信します。置いてもひっくり返らない場所には置けません。

終了、パス

石を置く場所が無くなるとゲームが終了し、どちらが勝ったかをBotが教えてくれます。「終了」と呼びかけることで途中でゲームを終了し勝者を知ることも可能です。

「パス」と呼びかけることでパスも可能です。(手番が入れ替わります)

コードの解説

ソースコードの内、リバーシの実装の要点を解説します。

Messaging APIの実装

公式のPython用LINE Bot SDKでは、以下のような実装でイベントを受信することが可能になります。今回はMessageEventつまりユーザーがBotにテキストを送信した場合のみ処理を行い、その他のイベント(友だち追加や写真送信等)に関しては処理しません。

MessageEventの場合はevent.message.textに送信されたメッセージが入りますので、それにより開始や終了の処理を行います。

# ユーザーがLINE Botに送信した情報を受け取り、handlerに渡す
@app.route("/", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        print("Invalid signature. Please check your channel access token/channel secret.")
        abort(400)

    return 'OK'# ユーザーがBotにテキストを送信した時の処理。
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    global board_as_array
    global is_black_active

    # メッセージを格納する配列。3通まで同時に送ることが可能
    message_to_user = []
    # ゲームを最初からスタート
    if event.message.text == '開始':
        ...
    else event.message.text == '終了'
        ...# ユーザーに送信するメッセージが存在しない場合
    if len(message_to_user) == 0:
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='意味がわからないよ。'),
        )
    # メッセージが存在する場合は送信
    else:
        line_bot_api.reply_message(
            event.reply_token,
            message_to_user,
        )

盤面状況の保存とテキスト化

盤面は本来であればDB等に保存しますが、今回は簡便性のためグローバル変数に整数の配列の形で格納しています。関数convert_board_to_stringで配列に行数、列数を付与したテキストに変換しています。

# リバーシの初期配置。0は空、1が黒、2が白
board_as_array = [
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 2, 1, 0, 0, 0],
        [0, 0, 0, 1, 2, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
    ]
# 手番を格納。黒ならTrue
is_black_active = True

# 盤面を整数の配列からテキストを生成し返す関数
def convert_board_to_string():
    global board_as_array
    # 列名を最上段に表示
    board_str = '  12345678\n  ーーーーーーーー\n'
    # 行ごとに処理
    for i, row in enumerate(board_as_array):
        # 各行左に行名を表示。幅を合わせるため半角を全角に変換
        row_str = ('%d|' %
                   (i + 1)).translate(str.maketrans('12345678', '12345678'))
        # 0 → 「_」 1 → 「●」 2 → 「○」に変換
        for col in row:
            row_str = row_str + str(col).translate(str.maketrans('012', '_●○'))
        row_str = row_str + '\n'
        board_str = board_str + row_str

    return board_str

ゲーム開始

「開始」と呼びかけられると、石を初期状態に戻し、テキスト化した盤面と手番を返信します。

# 初期配置に戻すための関数
def reset_board():
    # グローバル変数を利用する宣言
    global board_as_array
    global is_black_active

    # 盤面を整数の二次元配列で定義
    board_as_array = [
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 2, 1, 0, 0, 0],
        [0, 0, 0, 1, 2, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
    ]
    is_black_active = True
    
# ユーザーがBotにテキストを送信した時の処理。
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):# ゲームを最初からスタート
    if event.message.text == '開始':
        # ボードを初期配置にリセット
        reset_board()
        # 盤面
        message_to_user.append(TextSendMessage(text=convert_board_to_string()))
        message_to_user.append(TextSendMessage(text='スタート。あなたが黒で先行だよ。'))

石を置き、はさんだ相手方の石をひっくり返す処理

行と列をそれぞれ取得し、盤内であれば関数put_stone_then_return_flip_countを呼び出してその場所に置いた場合にひっくり返る数を取得します。

1個以上ひっくり返る場合は盤面を更新し、新しい盤面と入れ替えた手番を返信します。

全てのマスに石が置かれていればゲームの結果も合わせて返信します。

# 引数の行、列に石を置き、ひっくり返った数を返す関数
def put_stone_then_return_flip_count(row, col):
    global board_as_array
    global is_black_active

    # 置こうとする場所が空でない場合は置けないので0を返す
    if board_as_array[row][col] != 0:
        return 0

    # 相手方の石の色を格納。黒の手番なら白(2)、逆なら黒(1)
    target_color = 2 if is_black_active else 1

    # ひっくり返った石の座標を格納する変数
    flipped_pos = []
    # 探索の方向を定義
    process_directions = [
        [0, -1], # 左
        [1, 0], # 下
        [0, 1], # 右
        [-1, 0], # 上
        [1, -1], # 左下
        [1, 1], # 右下
        [-1, 1], # 右上
        [-1, -1], # 左上
        ]
    
    # 各方向において石を一つずつチェックする
    for direction in process_directions:
        # 探索の基準座標
        current_pos = [row, col]
        # ひっくり返る石の座標
        target_stones = []

        while True:
            # directionの方向へ一つチェックする座標をずらす
            current_pos = [current_pos[0] + direction[0], current_pos[1] + direction[1]]
            # チェックしようとする座標が負の値や8以上の場合は盤外なのでループを抜ける
            if not 0 <= current_pos[0] < 8 or not 0 <= current_pos[1] < 8:
                break
            
            # 空の場合はループを抜ける
            if board_as_array[current_pos[0]][current_pos[1]] == 0:
                break
            # 相手方の石の場合はひっくり返る可能性があるため一旦target_stonesに格納
            elif board_as_array[current_pos[0]][current_pos[1]] == target_color:
                target_stones.append(current_pos)
            # 自分の石の場合
            else:
                # target_stonesが空で無ければひっくり返る石がある
                if len(target_stones) > 0:
                    # flipped_posにひっくり返る座標を追加
                    flipped_pos = flipped_pos + target_stones
                break
    
    # ひっくり返る石がなければ0を返す
    if len(flipped_pos) == 0:
        return 0
    # ひっくり返る石がある場合
    else:
        # 石をまとめてひっくり返す
        for pos in flipped_pos:
            board_as_array[pos[0]][pos[1]] = 1 if is_black_active else 2
        
        # ひっくり返る = 置けるということなので座標に石を追加
        board_as_array[row][col] = 1 if is_black_active else 2

        # ひっくり返った数を返却
        return len(flipped_pos)
	
# ユーザーがBotにテキストを送信した時の処理。
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):# 石を置く処理。行と列を送ることで石を置くことが可能。4行目5列の場合は「45」と送信
    elif event.message.text.isnumeric() and 11 <= int(event.message.text) <= 88:
        # ユーザーが送信した座標に石を置き、返ってくる値が1以上の場合
        if put_stone_then_return_flip_count(int(event.message.text[0]) - 1, int(event.message.text[1]) - 1) > 0:
            # 手番を変更
            is_black_active = not is_black_active
            message_to_user.append(TextSendMessage(text=convert_board_to_string()))

            # 盤面の空マスの数をカウント
            count_empty = sum(board_as_array, []).count(0)
            # 0の場合はゲームオーバー処理
            if count_empty == 0:
                message_to_user.append(TextSendMessage(text=finish_then_return_result()))
            # それ以外は次の手番を案内
            else:
                message_to_user.append(TextSendMessage(text='%sの番だよ。' % ('黒' if is_black_active else '白')))
        # 置いても一つもひっくり返らない場合
        else:
            message_to_user.append(TextSendMessage(text=convert_board_to_string()))
            message_to_user.append(TextSendMessage(text='そこには置けないよ。'))

ゲーム終了、パスの処理

手番プレイヤーの石を置ける場所が存在しない場合のため、パスを受け付けます。手番を入れ替え、盤面と新たな手番を返信します。

ゲームを途中で終了したい場合のため、強制終了も実装しています。

# ユーザーがBotにテキストを送信した時の処理。
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):# 置けない場合のパスの処理
    elif event.message.text == 'パス':    
        # 手番を変更
        is_black_active = not is_black_active
        message_to_user.append(TextSendMessage(text=convert_board_to_string()))
        message_to_user.append(TextSendMessage(text='パスしたよ。%sの番だよ。' % ('黒' if is_black_active else '白')))
    # ゲームを手動で終了
    elif event.message.text == '終了':
        message_to_user.append(TextSendMessage(text=finish_then_return_result()))
        message_to_user.append(TextSendMessage(text='また遊んでね!'))

まとめ

本記事ではLINEで遊べるリバーシを解説しました。

次回はリッチメニューを利用して開始をボタンで行えるようにする、盤面をタップ可能にしてタップで石を置けるようにする、等ユーザビリティを高める実装を解説します。

Discussion