LoginSignup
0
0

More than 1 year has passed since last update.

DjangoとDocker練習OA16o3o0 Webブラウザ越しに2人対戦できる〇×ゲームを作ろう! Vuetify編

Last updated at Posted at 2022-05-07

サンプルを見る

📖 この記事のゴール

目標

フロントエンドに Vuetify を使って、
1人2役で2窓で遊ぶ 〇×ゲーム(Tic tac toe)を作りたい

〇×ゲームの思考エンジンは 前の記事で作った物を流用する

情報

この記事は Lesson 1. から順に全部やってこないと ソースが足りず実行できないので注意されたい

What is This is
Lesson 1. 📖 DjangoとDockerでゲーム対局サーバーを作ろう!

この記事のアーキテクチャ:

What is This is
OS Windows10
Container Docker
Database Postgresql, (Redis)
Program Language Python 3
Web framework Django
Auth allauth
Frontend Vuetify
Data format JSON
Others (Socket), Web socket
Editor Visual Studio Code (以下 VSCode と表記)

以下、参考にした元記事は 📖Django Channels and WebSockets だ。
わたしの記事は単に やってみた ぐらいの位置づけだ

ディレクトリ構成を抜粋すると 以下のようになっている

    ├── 📂 src1                            # あなたのDjangoサーバー開発用ディレクトリー。任意の名前
    │   ├── 📂 apps1
    │   │   ├── 📂 accounts_vol1o0    # アプリケーション
    │   │   ├── 📂 portal_v1                # アプリケーション
    │   │   ├── 📂 practice_vol1o0              # アプリケーション
    │   │   ├── 📂 tic_tac_toe_vol1o0           # アプリケーション
    │   │   └── 📂 tic_tac_toe_vol2o0           # アプリケーション
    │   │       ├── 📂 migrations
    │   │       │   └── 📄 __init__.py
    │   │       ├── 📂 static
    │   │       │   └── 📂 tic_tac_toe_vol2o0
    │   │       │       └── 📂 think
    │   │       │           ├── 📂 concepts
    │   │       │           │   └── 📄 ver1o0.js
    │   │       │           ├── 📂 engine
    │   │       │           │   └── 📄 ver1o0.js
    │   │       │           ├── 📂 judge_ctrl
    │   │       │           │   └── 📄 ver1o0.js
    │   │       │           ├── 📂 position
    │   │       │           │   └── 📄 ver1o0.js
    │   │       │           ├── 📂 things
    │   │       │           │   └── 📄 ver1o0.js
    │   │       │           └── 📂 user_ctrl
    │   │       │               └── 📄 ver1o0.js
    │   │       ├── 📂 templates
    │   │       │   └── 📂 tic_tac_toe_vol2o0
    │   │       │       └── 📂 think
    │   │       │           └── 📂 engine_manual
    │   │       │               └── 📄 ver1o0.html
    │   │       ├── 📂 views
    │   │       │   └── 📂 think
    │   │       │       └── 📂 engine_manual
    │   │       │           └── 📂 ver1o0
    │   │       │               ├── 📄 __init__.py
    │   │       │               └── 📄 v_render.py
    │   │       ├── 📄 __init__.py
    │   │       ├── 📄 admin.py
    │   │       ├── 📄 apps.py
    │   │       └── 📄 tests.py
    │   ├── 📂 data
    │   ├── 📂 project1                  # プロジェクト
    │   │   ├── 📄 __init__.py
    │   │   ├── 📄 asgi.py
    │   │   ├── 📄 settings_secrets_example.txt
    │   │   ├── 📄 settings.py
    │   │   ├── 📄 urls_accounts_vol1o0.py
    │   │   ├── 📄 urls_practice.py
    │   │   ├── 📄 urls_tic_tac_toe_v1.py
    │   │   ├── 📄 urls_tic_tac_toe_v2.py
    │   │   ├── 📄 urls.py
    │   │   ├── 📄 ws_urls_tic_tac_toe_v1.py
    │   │   └── 📄 wsgi.py
    │   ├── 📂 project2                  # プロジェクト
    │   ├── 🐳 docker-compose-project2.yml
    │   ├── 🐳 docker-compose.yml
    │   ├── 🐳 Dockerfile
    │   ├── 📄 manage.py
    │   └── 📄 requirements.txt
    ├── 📂 src1_meta
    │   ├── 📂 data
    │   │   └── 📄 urls.csv
    │   └── 📂 scripts
    │       └── 📂 auto_generators
    │           └── 📄 urls.py
    ├── 📂 src2_local                      # Djangoとは関係ないもの
    │    ├── 📂 sockapp1
    │    └── 📂 websockapp1
    └── 📄 .gitignore

実装手順

Step [OA16o3o0g1o0] Dockerコンテナの起動

👇 (していなければ) Docker コンテナを起動しておいてほしい

# docker-compose.yml ファイルを置いてあるディレクトリーへ移動してほしい
cd src1

# Docker コンテナ起動
docker-compose up

Step [OA16o3o0g2o0]

Moved to [OA16o3o_1o0g_1o0]

Step [OA16o3o0g3o0] メッセージ受信器 - msg/message_receiver/v1o0.js ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                └── 📂 static
                    └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                        └── 📂 msg
                            └── 📂 message_receiver
👉                              └── 📄 ver1o0.js
// BOF [OA16o3o0g3o0]

/**
 * メッセージ受信器
 */
class MessageReceiver {
    constructor() {
        this._messageListeners = {};
    }

    addMessageListener(name, setMessage) {
        this._messageListeners[name] = setMessage;
    }

    /**
     * サーバーからクライアントへ送られてきたメッセージをセットする関数を返します
     * @returns 関数
     */
    execute(message) {
        // イベント名 (Server to client)
        let event = message["event"];

        // let description = `[Server] event:${event}`;
        // const keys = Object.keys(message);
        // keys.forEach((key) => {
        //     if (key != "event" && key != "type") {
        //         description += ` ${key}:${message[key]}`;
        //     }
        // });
        // description += ` type:${message["type"]}`;
        // console.log(description);

        if (event in this._messageListeners) {
            // 実行
            const setMessage = this._messageListeners[event];
            setMessage(message);
        } else {
            // Undefined behavior
            console.log(`(ignored) [Server] ${event}`);
        }
    }
}

// EOF [OA16o3o0g3o0]

Step [OA16o3o0g4o0] Webソケット接続の実装 - gui/connection/v1o0.js ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                └── 📂 static
                    └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                        ├── 📂 gui
                        │   └── 📂 connection
👉                      │       └── 📄 ver1o0.js
                        └── 📂 msg
                            └── 📂 message_receiver
                                └── 📄 ver1o0.js
// BOF [OA16o3o0g4o0]

// 参考にした記事
// -------------
// 📖[Django Channels and WebSockets](https://blog.logrocket.com/django-channels-and-websockets/)

/**
 * 接続
 */
class Connection {
    /**
     * ウェブソケット
     */
    #webSock1;

    /**
     * 生成
     *
     * @param {string} roomName - 部屋名
     * @param {strint} connectionString - Webソケット接続文字列
     * @param {MessageReceiver} messageReceiver - メッセージ受信器
     * @param {function} onOpenWebSocket - Webソケットを開かれたとき
     * @param {function} onCloseWebSocket - Webソケットが閉じられたとき。 例: サーバー側にエラーがあって接続が切れたりなど
     * @param {function} onWebSocketError - Webソケットエラー時のメッセージ
     * @param {function} onRetryWaiting - 再接続のためのインターバルの定期的なメッセージ
     * @param {function} onGiveUp - 再接続を諦めたとき
     */
    constructor(roomName, connectionString, messageReceiver, onOpenWebSocket, onCloseWebSocket, onWebSocketError, onRetryWaiting, onGiveUp) {
        // console.log(`[Connection constructor] roomName=[${roomName}] connectionString=[${connectionString}]`);

        // 部屋名
        this._roomName = roomName;

        // 接続文字列
        this._connectionString = connectionString;

        // リトライ回数
        this._retryCount = 0;
        // リトライ上限
        this._retryMax = 10;

        // 再接続のために記憶しておきます
        this._onOpenWebSocket = onOpenWebSocket;
        this._onCloseWebSocket = onCloseWebSocket;
        this._messageReceiver = messageReceiver;
        this._onWebSocketError = onWebSocketError;
        this._onRetryWaiting = onRetryWaiting;
        this._onGiveUp = onGiveUp;
    }

    /**
     * メッセージ送信
     */
    send(response) {
        const message = JSON.stringify(response)
        console.log(`[Client] ${message}`)
        this.#webSock1.send(message);
    }

    /**
     * 接続
     */
    connect() {
        // console.log(`[Connection#connect] Start this._connectionString=[${this._connectionString}]`);

        // Webソケット
        //
        // * 生成と同時に接続が始まる。そこで例外が起こるとキャッチはできないし、なぜか後続の処理に続かず関数を抜けてしまう
        // * 1回切りの使い捨て。再接続機能は無い
        // * 再接続したいときは、再生成する
        try {
            this.#webSock1 = new WebSocket(this._connectionString);
            // 以下、接続に成功

            // イベントハンドラを毎回設定し直してください
            this.#webSock1.onopen = this._onOpenWebSocket;
            this.#webSock1.onclose = this._onCloseWebSocket;

            // 設定: サーバーからメッセージを受信したとき
            this.#webSock1.onmessage = (e) => {
                // JSON を解析、メッセージだけ抽出
                let data1 = JSON.parse(e.data);
                let message = data1["message"];

                console.log(`[Server] ${JSON.stringify(message)}`)
                this._messageReceiver.execute(message);
            };

            this.#webSock1.addEventListener("open", (event1) => {
                // console.log("[Connection connect] WebSockets connection created.");
                // 再接続カウンターをリセットします
                this._retryCount = 0;
            });
            this.#webSock1.addEventListener("error", (event1) => {
                this._onWebSocketError(event1);
            });

            // 状態を表示
            if (this.#webSock1.readyState == WebSocket.CONNECTING) {
                // 未接続
                // console.log("[Connection connect] Connecting socket.");
            } else if (this.#webSock1.readyState == WebSocket.OPEN) {
                // console.log("[Connection connect] Open socket.");
                this.#webSock1.onopen();
            } else if (this.#webSock1.readyState == WebSocket.CLOSING) {
                // console.log("[Connection connect] Closing socket.");
            } else if (this.#webSock1.readyState == WebSocket.CLOSED) {
                // サーバーが落ちたりしたときは、ここ
                // console.log("[Connection connect] Closed socket.");

                // 再接続のリトライを書くタイミングはここです
                this.reconnect();
            } else {
                // console.log(`[Connection connect] #webSock1.readyState=${this.#webSock1.readyState}`);
            }
        } catch (exception) {
            // キャッチで捕まえられない
            // console.log(`[Connection connect] exception:${exception}`);
        }
    }

    /**
     * 再接続
     */
    reconnect() {
        if (this._retryMax <= this._retryCount) {
            // 諦めます
            console.log(`[Connection reconnect] Give up`);
            this._onGiveUp();
            return;
        }

        console.log(`[Connection reconnect] Start...`);

        // 再接続のインターバルの開始を通知しますが、実際に接続するのは5秒後です
        // 最初は 0 回目なので、表示を考慮して +1 の下駄を履かせます
        this._onRetryWaiting(true, this._retryCount + 1, this._retryMax);

        setTimeout(() => {
            console.log(`[Connection reconnect] Try... to:${this._connectionString}`);

            // 事前にカウントを上げておきます
            this._retryCount += 1;
            // 再接続のインターバルの終了を通知しますが、実際には接続を開始します
            this._onRetryWaiting(false, this._retryCount, this._retryMax);
            // 接続
            this.connect();
            console.log(`[Connection reconnect] End. retried:${this._retryCount}/${this._retryMax}`);
        }, 5000);
    }
}

// EOF [OA16o3o0g4o0]

Step [OA16o3o0g5o0] 対局申込画面作成 - gui/match_application/v1o0.html ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                └── 📂 templates
                    └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                        └── 📂 gui
                            └── 📂 match_application
👉                              └── 📄 ver1o0.html
<!-- BOF [OA16o3o0g5o0] -->
{% load static %} {# 👈あとで static "URL" を使うので load static します #}
<!DOCTYPE html>
<html>
    <head>
        <link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
        <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" />
        <link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet" />
        <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet" />
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui" />
        <title>Tic Tac Toe</title>
    </head>
    <body>
        <div id="app">
            <v-app>
                <v-main>
                    <v-container fluid>
                        <h1>Welcome to Tic Tac Toe Game</h1>
                        <v-form method="POST">
                            {% csrf_token %}

                            <!-- `po_` は POST送信するパラメーター名の目印 -->
                            <!-- 部屋名 -->
                            <v-text-field name="po_room_name" required v-model="roomName.value" :rules="roomName.rules" counter="25" hint="A-Z, a-z, 0-9, No number at the beginning. Max 25 characters" label="Room name"></v-text-field>

                            <!--
                                自分の番。 "X" か "O"。 機能拡張も想定

                                * 戻り値をオブジェクトのまま受け取りたいときは、タグの属性として return-object を付ける
                            -->
                            <v-select name="po_my_turn" v-model="visitor.value" :items="visitor.select" item-text="text" item-value="value" label="Your turn" persistent-hint single-line></v-select>

                            <v-btn block elevation="2" type="submit"> Start Game </v-btn>
                        </v-form>
                    </v-container>
                </v-main>
            </v-app>
        </div>

        <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
        <script>
            new Vue({
                el: "#app",
                vuetify: new Vuetify(),
                data: {
                    // 部屋名
                    roomName: {
                        value: "Elephant",
                        rules: [
                            (v) => v.length <= 25 || "Max 25 characters", // 文字数上限
                            (value) => {
                                const pattern = /^[A-Za-z][A-Za-z0-9]*$/; // 正規表現で指定
                                return pattern.test(value) || "Invalid format";
                            },
                        ],
                    },
                    // 入場者
                    visitor: {
                        // `dj_` は Djangoでレンダーするパラメーター名の目印
                        value: "{{dj_visitor_value}}",
                        select: JSON.parse("{{dj_visitor_select | escapejs}}"),
                    },
                },
            });
        </script>
    </body>
</html>
<!-- EOF [OA16o3o0g5o0] -->

Step [OA16o3o0g6o0] 対局画面作成 - gui/playing/v1o0.html ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                └── 📂 templates
                    └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                        └── 📂 gui
                            ├── 📂 match_application
                            │   └── 📄 ver1o0.html
                            └── 📂 playing
👉                              └── 📄 ver1o0.html
<!-- BOF [OA16o3o0g6o0] -->
{% load static %} {# 👈あとで static "URL" を使うので load static します #}
<!DOCTYPE html>
<html>
    <head>
        <link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
        <link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet" />
        <link href="https://cdn.jsdelivr.net/npm/@mdi/font@6.x/css/materialdesignicons.min.css" rel="stylesheet" />
        <link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet" />
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui" />
        <title>Tic Tac Toe</title>
    </head>
    <body>
        <div id="app">
            <v-app>
                <v-main>
                    <v-container>
                        <h1>TIC TAC TOE</h1>
                        <h3>Welcome to room_{{dj_room_name}}</h3>
                    </v-container>

                    <form name="form1" method="POST">
                        {% csrf_token %}
                        <v-container>
                            <v-row justify="center" dense>
                                <v-col>
                                    {% comment %} Vue で二重波括弧(braces)は変数の展開に使っていることから、 Python のテンプレートに二重波括弧を変数の展開に使わないよう verbatim で指示します。 {% endcomment %}
                                    <!-- -->
                                    {% verbatim %}
                                    <v-btn id="square0" v-on:click="clickSquare(0)">{{label0}}</v-btn>
                                    <v-btn id="square1" v-on:click="clickSquare(1)">{{label1}}</v-btn>
                                    <v-btn id="square2" v-on:click="clickSquare(2)">{{label2}}</v-btn>
                                    {% endverbatim %}
                                </v-col>
                            </v-row>
                            <v-row justify="center" dense>
                                <v-col>
                                    {% verbatim %}
                                    <v-btn id="square3" v-on:click="clickSquare(3)">{{label3}}</v-btn>
                                    <v-btn id="square4" v-on:click="clickSquare(4)">{{label4}}</v-btn>
                                    <v-btn id="square5" v-on:click="clickSquare(5)">{{label5}}</v-btn>
                                    {% endverbatim %}
                                </v-col>
                            </v-row>
                            <v-row justify="center" dense>
                                <v-col>
                                    {% verbatim %}
                                    <v-btn id="square6" v-on:click="clickSquare(6)">{{label6}}</v-btn>
                                    <v-btn id="square7" v-on:click="clickSquare(7)">{{label7}}</v-btn>
                                    <v-btn id="square8" v-on:click="clickSquare(8)">{{label8}}</v-btn>
                                    {% endverbatim %}
                                </v-col>
                            </v-row>
                        </v-container>

                        <!-- `po_` は POST送信するパラメーター名の目印 -->
                        <input type="hidden" name="po_room_name" value="{{dj_room_name}}" />
                        <input type="hidden" name="po_my_turn" value="{{dj_my_turn}}" />
                    </form>
                    {% block footer_section1 %}
                    <!-- ボタン等を追加したいなら、ここに挿しこめる -->
                    {% endblock footer_section1 %}
                    <v-container>
                        {% verbatim %}
                        <v-alert type="success" v-show="isGameover">{{gameover_message}}</v-alert>
                        {% endverbatim %}
                        <v-alert type="info" v-show="isYourTurn">Your turn. Place your move <strong>{{dj_my_turn}}</strong></v-alert>
                        <v-alert type="warning" v-show="isItOpponentsTurnToMove">Wait for other to place the move</v-alert>
                        {% verbatim %}
                        <v-alert type="warning" v-show="isReconnecting">Reconnecting... {{reconnectionCount}}/{{reconnectionMax}}</v-alert>
                        {% endverbatim %}
                        <v-alert type="error" v-show="isSocketClosed">Lost connection</v-alert>
                    </v-container>
                </v-main>
            </v-app>
        </div>

        <script src="{% static 'tic_tac_toe_vol2o0/think/things/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/think/concepts/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/think/position/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/think/user_ctrl/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/think/judge_ctrl/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/think/engine/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/gui/connection/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/message_receiver/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/messages/end/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/messages/moved/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/messages/start/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/events/do_move/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/events/draw/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/events/start/ver1o0.js' %}"></script>
        <script src="{% static 'tic_tac_toe_vol2o0/msg/c2s_json_gen/events/won/ver1o0.js' %}"></script>
        <!--            ================================================================
                        1
        1. src1/apps1/tic_tac_toe_vol2o0/static/tic_tac_toe_vol2o0/msg/c2s_json_gen/events/won/ver1o0.js
                                         ===============================================================
        -->

        <script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
        <script>
            // 部屋名
            const roomName = document.forms["form1"]["po_room_name"].value;
            // 接続文字列
            // `dj_` は Djangoでレンダーするパラメーター名の目印
            const connectionString = `ws://${window.location.host}{{dj_path_of_ws_playing}}${roomName}/`;
            //                        ----]----------------------]-------------------------------------
            //                        1    2                      3
            // 1. プロトコル(Web socket)
            // 2. ホスト アドレス
            // 3. パス
            console.log(`[HTML] roomName=${roomName} connectionString=${connectionString}`);

            // クライアントが、サーバーからのメッセージ受信
            const messageReceiver = new MessageReceiver();
            // 対局開始時
            messageReceiver.addMessageListener("S2C_Start", (message)=>{
                vue1.onStart();
            });
            // 対局終了時
            messageReceiver.addMessageListener("S2C_End", (message)=>{
                // 勝者
                let winner = message["winner"];
                vue1.onGameover(winner);
            });
            // 指し手受信時
            messageReceiver.addMessageListener("S2C_Moved", (message)=>{
                // 升番号
                let sq = parseInt(message["sq"]);
                // 手番。 "X" か "O"
                let piece_moved = message["piece"];

                if (piece_moved != vue1.engine.position.turn.me) {
                    // 相手の手番なら、自動で動かします
                    vue1.engine.userCtrl.doMove(vue1.engine.position, piece_moved, sq);

                    // 「相手の手番で動かした」アラートの取りさげ
                    vue1.isItOpponentsTurnToMove = false;
                }

                // ゲームオーバー判定
                vue1.engine.judgeCtrl.doJudge(vue1.engine.position);
            });

            // 接続
            var connection = new Connection(
                roomName,
                connectionString,
                // サーバーからのメッセージを受信したとき
                messageReceiver,
                // Webソケットを開かれたとき
                () => {
                    // console.log("WebSockets connection created.");
                    let message = new EvtStart().createMessage();
                    connection.send(message.asJsObject());
                },
                // Webソケットが閉じられたとき
                (exception) => {
                    // console.log(`Socket is closed. Reconnect it. ${exception.reason}`);
                    // 再接続の初回トライを書いてよいのはこのタイミングです
                    connection.reconnect();
                },
                // エラー時
                (exception) => {
                    console.log(`Socket is error. ${exception.reason}`);
                },
                /**
                 * 再接続のためのインターバルの通知
                 *
                 * アラートが、接続中に短く非表示、次の接続までの待機中に長く表示と、逆になっているが、そうしないと表示が短くなってしまう
                 *
                 * @param {bool} isBeginWait - 次の再接続までの待ち時間に入ったら真
                 * @param {int} retryCount - リトライ回数
                 * @param {int} retryMax - リトライ回数上限
                 */
                (isBeginWait, retryCount, retryMax)=>{
                    // console.log(`WebSockets reconnection isBeginWait:${isBeginWait}`);
                    vue1.isReconnecting = isBeginWait;
                    vue1.reconnectionCount = retryCount;
                    vue1.reconnectionMax = retryMax;
                },
                /**
                 * 再接続を諦めたとき
                 */
                ()=>{
                    vue1.isSocketClosed = true;
                });

            let vue1 = new Vue({
                el: "#app",
                vuetify: new Vuetify(),
                data: {
                    // 思考エンジン
                    engine: new Engine(
                        // `po_` は POST送信するパラメーター名の目印
                        // 自分の番。 X か O
                        document.forms["form1"]["po_my_turn"].value,
                        // ユーザーコントロール
                        new UserCtrl(
                            /**
                             * onDidMove - 駒を置いたあと
                             *
                             * * 手番がひっくり返っていることに注意してください
                             *
                             * @param {int} sq - マス番号
                             * @param {string} pieceMoved - 動かした駒
                             */
                            (sq, pieceMoved) => {
                                // ボタンのラベルを更新
                                vue1.setLabelOfButton(sq, pieceMoved);

                                // 自分の指し手なら送信
                                if (vue1.engine.position.turn.me == pieceMoved) {
                                    const message = new EvtDoMove(()=>{
                                        return [sq, pieceMoved];
                                    }).createMessage();
                                    connection.send(message.asJsObject());
                                }

                                // 手番の再描画を通知
                                vue1.raiseMyTurnChanged();
                            }
                        ),
                        // 審判コントロール
                        new JudgeCtrl(
                            /**
                             * onDoJudge - 判断したとき
                             *
                             * @param {*} gameoverSet - ゲームオーバー集合
                             */
                            (gameoverSet) => {
                                vue1.engine.gameoverSet = gameoverSet;
                                let response;

                                switch (gameoverSet.value) {
                                    case GameoverSet.won:
                                        // 自分が勝ったとき
                                        message = new EvtWon(()=>{
                                            const winner = vue1.engine.position.turn.me;
                                            return winner;
                                        }).createMessage();
                                        connection.send(message.asJsObject());
                                        break;
                                    case GameoverSet.draw:
                                        // 引き分けたとき
                                        message = new EvtDraw().createMessage();
                                        connection.send(message.asJsObject());
                                        break;
                                    case GameoverSet.lost:
                                        // 自分が負けたとき
                                        break;
                                    case GameoverSet.none:
                                        // なんでもなかったとき
                                        break;
                                    default:
                                        throw new Error(`Unexpected gameoverSet.value=${gameoverSet.value}`);
                                }
                            }
                        )
                    ),
                    label0: PC_EMPTY_LABEL,
                    label1: PC_EMPTY_LABEL,
                    label2: PC_EMPTY_LABEL,
                    label3: PC_EMPTY_LABEL,
                    label4: PC_EMPTY_LABEL,
                    label5: PC_EMPTY_LABEL,
                    label6: PC_EMPTY_LABEL,
                    label7: PC_EMPTY_LABEL,
                    label8: PC_EMPTY_LABEL,
                    isReconnecting: false,
                    reconnectionCount: 0,
                    reconnectionMax: 0,
                    isSocketClosed: false,
                    isYourTurn: false,
                    isGameover: false,
                    // 「相手の手番に着手しないでください」というアラートの可視性
                    isItOpponentsTurnToMove: false,
                    roomState: new RoomState(RoomState.none,(oldValue, newValue)=>{
                        // changeRoomState
                        vue1.raiseRoomStateChanged();
                    }),
                    gameover_message : "",
                    messages: {
                        draw: "It's a draw.",
                        youLost: "You lost.",
                        youWon: "You won!",
                        {% block appendix_message %}
                        // メッセージを追加したければ、ここに挿しこめる
                        {% endblock appendix_message %}
                    }
                },
                // page loaded
                mounted: () => {
                    // * ここで vue1 はまだ初期化されていない
                    // * ここで this は Window を指している
                },
                methods: {
                    // 対局開始時
                    onStart() {
                        // 「相手の手番に着手しないでください」というアラートの非表示
                        this.isItOpponentsTurnToMove = false;

                        // 先に 対局中状態 にしておいてから、エンジンをスタートさせてください
                        this.roomState.value = RoomState.playing;
                        this.engine.start();
                        this.raisePositionChanged();


                        // ボタンのラベルをクリアー
                        for (let sq = 0; sq < BOARD_AREA; sq += 1) {
                            this.setLabelOfButton(sq, "");
                        }

                        // ダンプ
                        // this.dump();
                    },
                    /**
                     * 升ボタンをクリックしたとき
                     * @param {*} sq - Square; 0 <= sq
                     */
                    clickSquare(sq) {
                        if (this.engine.gameoverSet.value != GameoverSet.none) {
                            // Ban on illegal move
                            return;
                        }

                        if (this.engine.position.board.getPieceBySq(sq) == PC_EMPTY) {
                            if (!this.engine.position.turn.isMe) {
                                // Wait for other to place the move
                                this.isItOpponentsTurnToMove = true;
                            } else {
                                if (this.engine.gameoverSet.value != GameoverSet.none) {
                                    // ゲームオーバー後に駒を置いてはいけません
                                    return;
                                }

                                // 自分の一手
                                this.engine.userCtrl.doMove(this.engine.position, this.engine.position.turn.me, parseInt(sq));
                            }
                        }
                    },
                    /**
                     * 対局は終了しました
                     */
                    onGameover(winner) {
                        this.engine.winner = winner;
                        this.roomState.value = RoomState.none; // 画面を対局終了状態へ

                        this.gameover_message = this.createGameoverMessage();
                    },
                    /**
                     * ゲームオーバー時のメッセージ作成
                     */
                    createGameoverMessage() {
                        {% block create_gameover_message %}
                        // 返却値を変えたいなら、ここに挿しこめる
                        {% endblock create_gameover_message %}

                        switch (this.engine.gameoverSet.value) {
                            case GameoverSet.draw:
                                return this.messages.draw;
                            case GameoverSet.won:
                                return this.messages.youWon;
                            case GameoverSet.lost:
                                return this.messages.youLost;
                            case GameoverSet.none:
                                // ここに来るのはおかしい
                                return "";
                            default:
                                throw `unknown this.engine.gameoverSet.value = ${this.engine.gameoverSet.value}`;
                        }
                    },
                    /**
                     * 升ボタンのラベルの設定
                     *
                     * @param {number} sq - Square; 0 <= sq
                     * @param {*} piece - text
                     */
                    setLabelOfButton(sq, piece) {
                        switch (sq) {
                            case 0:
                                this.label0 = piece;
                                break;
                            case 1:
                                this.label1 = piece;
                                break;
                            case 2:
                                this.label2 = piece;
                                break;
                            case 3:
                                this.label3 = piece;
                                break;
                            case 4:
                                this.label4 = piece;
                                break;
                            case 5:
                                this.label5 = piece;
                                break;
                            case 6:
                                this.label6 = piece;
                                break;
                            case 7:
                                this.label7 = piece;
                                break;
                            case 8:
                                this.label8 = piece;
                                break;
                            default:
                                alert(`[Error] sq=${sq}`);
                                break;
                        }
                    },
                    /**
                     * (1) 対局中か
                     * (2) 自分の手番か
                     */
                    updateYourTurn(){
                        let isYourTurn = this.roomState.value == RoomState.playing && this.engine.position.turn.isMe;

                        {% block isYourTurn_patch1 %}
                        // 条件を追加したいなら、ここに挿しこめる
                        {% endblock isYourTurn_patch1 %}

                        // v-show="" は複雑なメソッドを指定すると動かないようなので、プロパティにします
                        this.isYourTurn = isYourTurn;
                    },
                    raiseRoomStateChanged() {
                        this.isGameover = this.roomState.value == RoomState.none;

                        this.updateYourTurn();
                    },
                    raiseMyTurnChanged() {
                        this.updateYourTurn();
                    },
                    raisePositionChanged() {
                        this.raiseMyTurnChanged();
                    },
                    {% block methods_footer %}
                    // メソッドを追加したければ、ここに挿しこめる
                    {% endblock methods_footer %}
                    /**
                     * ダンプ
                     */
                    dump() {
                        console.log(`[DUMP] vue1\n${this.engine.dump("")}`)
                    },
                },
            });

            connection.connect();
        </script>
    </body>
</html>
<!-- EOF [OA16o3o0g6o0] -->

Step [OA16o3o0g7o0] 対局画面作成 - gui/playing/ver1o1o0.html.txt ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                └── 📂 templates
                    └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                        └── 📂 gui
                            ├── 📂 match_application
                            │   └── 📄 ver1o0.html
                            └── 📂 playing
                                ├── 📄 ver1o0.html
👉                              └── 📄 ver1o1o0.html.txt

👆 自動フォーマットされてくないので、拡張子をテキストファイルにしておく

<!-- BOF [OA16o3o0g7o0] -->
{% extends "tic_tac_toe_vol2o0/gui/playing/ver1o0.html" %}
{#          ------------------------------------------
            1
1. src1/apps1/tic_tac_toe_vol2o0/templates/tic_tac_toe_vol2o0/gui/playing/ver1o0.html
                                           ------------------------------------------

    自動フォーマットしないでください
    Do not auto fomatting
#}

{% block footer_section1 %}
    <v-container>
        <v-btn block elevation="2" v-on:click="clickPlayAgain()" :disabled="isDisabledPlayAgainButton()"> Play again </v-btn>
    </v-container>
{% endblock footer_section1 %}

{% block methods_footer %}
    /**
     *
     */
    clickPlayAgain() {
        console.log(`Play Again`);

        // 対局開始時
        this.onStart();
    },
    /**
     * Play again ボタンは非表示か
     */
    isDisabledPlayAgainButton() {
        switch (this.roomState.value) {
            case RoomState.none: // ゲームオーバー中
                return false; // Enable
            default:
                return true; // Disable
        }
    },
{% endblock methods_footer %}
<!-- EOF [OA16o3o0g7o0] -->

Step [OA16o3o0g8o0] 通信プロトコル作成 - gui/message_manager/v1o0.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                └── 📂 websocks
                    └── 📂 gui
                        └── 📂 message_manager
👉                          └── 📄 ver1o0.py
# BOF [OA16o3o0g8o0]

class MessageManager():
    """[OA16o3o0g8o0] 〇×ゲーム v2 メッセージ駆動"""

    def __init__(self):
        self._message_handlers = {}

    def addMessageHandler(self, eventName, message_handler):
        self._message_handlers[eventName] = message_handler

    async def execute(self, scope, doc_received):
        """クライアントからサーバーへ送られてきた変数を解析し、
        サーバーからクライアントへ送信するメッセージの作成"""

        # ログインしていなければ AnonymousUser
        # user = scope["user"]
        # print(f"[MessageManager execute] user=[{user}]")

        # イベント名 (Client to server)
        event = doc_received.get("event", None)

        if(event in self._message_handlers):
            message_handler = self._message_handlers[event]
            if message_handler is None:
                raise ValueError(
                    f"[MessageManager execute] message handler not found. event:{event}")

            response_json = await message_handler.on_message_received(scope, doc_received)
            return response_json

        raise ValueError(
            f"[MessageManager execute] unknown event:{event}")


# EOF [OA16o3o0g8o0]

Step [OA16o3o0g9o0] Webソケットの通信プロトコル作成 - gui/consumer/v1o0.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 consumer
👉                      │   └── 📄 ver1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0g9o0]

# 参考にした記事
# -------------
# 📖[Django Channels and WebSockets](https: // blog.logrocket.com/django-channels-and-websockets/)
import json
from channels.generic.websocket import AsyncJsonWebsocketConsumer


class ConsumerBase(AsyncJsonWebsocketConsumer):
    """[OA16o3o0g9o0] Webソケット用コンシューマー"""

    def __init__(self):
        super().__init__()

    async def connect(self):
        """接続"""
        print("Connect")
        # ログインしていれば、ユーザーのPrimaryKeyは以下で取得可能。ログインしていなければ None
        # print(f'Connect self.scope["user"].pk={self.scope["user"].pk}')

        self.room_name = self.scope['url_route']['kwargs']['kw_room_name']
        self.room_group_name = f'room_{self.room_name}'

        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )
        await self.accept()

    async def disconnect(self, close_code):
        """切断"""
        print("Disconnected")
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    async def receive(self, text_data):
        """クライアントからのメッセージの受信"""

        # ちゃんと動いているようなら消す
        # print(f"[ConsumerBase receive] text_data={text_data}")

        doc_received = json.loads(text_data)

        response = await self.on_receive(doc_received)

        # 部屋のメンバーに一斉送信します
        await self.channel_layer.group_send(self.room_group_name, response)

    async def on_receive(self, doc_received):
        """クライアントからメッセージを受信したとき

        Returns
        -------
        response
        """
        return {}  # Empty

    async def send_message(self, message):
        """メッセージ送信"""
        await self.send(text_data=json.dumps({
            "message": message,
        }))

# EOF [OA16o3o0g9o0]

Step [OA16o3o0gA10o_1o0] ハンドラー作成 - gui/c2s_handlers/end/v1o0.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 c2s_handlers
                        │   └── 📂 end
👉                      │       └── 📄 ver1o0.py
                        ├── 📂 consumer
                        │   └── 📄 ver1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA10o_1o0]

# [OA16o3o_2o0g1o_1o0] Endメッセージ
from apps1.tic_tac_toe_vol2o0.views.msg.s2c_json_gen.messages.end.ver1o0 import EndS2cMessage
#          ------------------                                     ------        -------------
#          11                                                     12            2
#    -------------------------------------------------------------------
#    10
# 10, 12. ディレクトリー
# 11. アプリケーション
# 2. `12.` に含まれる __init__.py にさらに含まれるクラス


class EndC2sHandler:

    async def on_message_received(self, scope, doc_received):
        """対局終了時"""
        return EndS2cMessage({
            # TODO 現状、勝者は、クライアント側から送ってきたものをそのまま返しているが、勝敗判定のロジックはサーバー側に置きたい
            "player1": doc_received.get("winner", None)
        }).asDict()

# EOF [OA16o3o0gA10o_1o0]

Step [OA16o3o0gA10o_2o0] ハンドラー作成 - gui/c2s_handlers/move/v1o0.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 c2s_handlers
                        │   ├── 📂 end
                        │   │   └── 📄 ver1o0.py
                        │   └── 📂 move
👉                      │       └── 📄 ver1o0.py
                        ├── 📂 consumer
                        │   └── 📄 ver1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA10o_2o0]

# [OA16o3o_2o0g1o_2o0] Movedメッセージ
from apps1.tic_tac_toe_vol2o0.views.msg.s2c_json_gen.messages.moved.ver1o0 import MovedS2cMessage


class MoveC2sHandler:

    async def on_message_received(self, scope, doc_received):
        """駒を置いたとき"""
        return MovedS2cMessage({
            # `s2c_` は サーバーからクライアントへ送る変数の目印
            "sq1": doc_received.get("sq", None),
            "piece1": doc_received.get("piece", None),
        }).asDict()

# EOF [OA16o3o0gA10o_2o0]

Step [OA16o3o0gA10o_3o0] ハンドラー作成 - gui/c2s_handlers/start/v1o0.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 c2s_handlers
                        │   ├── 📂 end
                        │   │   └── 📄 ver1o0.py
                        │   ├── 📂 move
                        │   │   └── 📄 ver1o0.py
                        │   └── 📂 start
👉                      │       └── 📄 ver1o0.py
                        ├── 📂 consumer
                        │   └── 📄 ver1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA10o_3o0]

# [OA16o3o_2o0g1o_3o0] Startメッセージ
from apps1.tic_tac_toe_vol2o0.views.msg.s2c_json_gen.messages.start.ver1o0 import StartS2cMessage


class StartC2sHandler:
    async def on_message_received(self, scope, doc_received):
        """対局開始時"""
        return StartS2cMessage({}).asDict()

# EOF [OA16o3o0gA10o_3o0]

Step [OA16o3o0gA10o0] Webソケットの通信プロトコル作成 - gui/consumer/ver1o1o0.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 consumer
                        │   ├── 📄 ver1o0.py
👉                      │   └── 📄 ver1o1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA10o0]

# [OA16o3o0g9o0] 〇×ゲーム2.0巻 - Webソケット コンシューマー1.0版
from apps1.tic_tac_toe_vol2o0.websocks.gui.consumer.ver1o0 import ConsumerBase
#          ------------------                       ------        ------------
#          11                                       12            2
#    -----------------------------------------------
#    10
# 10, 12. ディレクトリー
# 11. アプリケーション
# 2. `12.` に含まれる __init__.py にさらに含まれるクラス

# [OA16o3o0g8o0] 〇×ゲーム2.0巻 - WebソケットGUI メッセージ駆動 1.0版
from apps1.tic_tac_toe_vol2o0.websocks.gui.message_manager.ver1o0 import MessageManager

# [OA16o3o0gA10o_1o0] 〇×ゲーム2.0巻 - WebソケットGUI Endメッセージハンドラー 1.0版
from apps1.tic_tac_toe_vol2o0.websocks.gui.c2s_handlers.end.ver1o0 import EndC2sHandler

# [OA16o3o0gA10o_2o0] 〇×ゲーム2.0巻 - WebソケットGUI Moveメッセージハンドラー 1.0版
from apps1.tic_tac_toe_vol2o0.websocks.gui.c2s_handlers.move.ver1o0 import MoveC2sHandler

# [OA16o3o0gA10o_3o0] 〇×ゲーム2.0巻 - WebソケットGUI Startメッセージハンドラー 1.0版
from apps1.tic_tac_toe_vol2o0.websocks.gui.c2s_handlers.start.ver1o0 import StartC2sHandler


class ConsumerCustom(ConsumerBase):
    """[OA16o3o0gA10o0] Webソケット用コンシューマー 1.1.0版"""

    def __init__(self):
        super().__init__()

        self._messageManager = MessageManager()
        self._messageManager.addMessageHandler('C2S_End', EndC2sHandler())
        self._messageManager.addMessageHandler('C2S_Moved', MoveC2sHandler())
        self._messageManager.addMessageHandler('C2S_Start', StartC2sHandler())

    async def on_receive(self, doc_received):
        """クライアントからメッセージを受信したとき

        Returns
        -------
        response
        """
        return await self._messageManager.execute(self.scope, doc_received)

# EOF [OA16o3o0gA10o0]

Step [OA16o3o0gA11o0] ビュー作成 - gui/match_application/v1o0 フォルダー

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                ├── 📂 views
                │   └── 📂 gui
                │       └── 📂 match_application
                │           └── 📂 ver1o0
👉              │               └── 📄 __init__.py
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 consumer
                        │   ├── 📄 ver1o0.py
                        │   └── 📄 ver1o1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA11o0]

import json


class MatchApplicationView():
    """[OA16o3o0gA11o0] 対局申込ビュー"""

    # 対局申込 - 訪問後
    open_context = {
        # `dj_` は Djangoでレンダーするパラメーター名の目印
        # 入場者データ
        "dj_visitor_value": "X",
        # Python と JavaScript 間で配列データを渡すために JSON 文字列形式にします
        "dj_visitor_select": json.dumps([
            {"text": "X", "value": "X"},
            {"text": "O", "value": "O"},
        ]),
    }

    playing_web_path = "/tic-tac-toe/vol2.0/playing/ver1.0/{0}/?&myturn={1}"
    #                                   ^ two
    #                   ---------------------------------------------------
    #                   1
    # 1. `http://example.com:8000/tic-tac-toe/vol2.0/playing/ver1.0/Elephant/?&myturn=X`
    #                            ------------------------------------------------------

    template_path = "tic_tac_toe_vol2o0/gui/match_application/ver1o0.html"
    #                               ^two
    #                ----------------------------------------------------
    #                1
    # 1. src1/apps1/tic_tac_toe_vol2o0/templates/tic_tac_toe_vol2o0/gui/match_application/ver1o0.html
    #                                            ----------------------------------------------------

    @staticmethod
    def render(request):
        """描画"""

        # 以下のファイルはあとで作ります
        from .v_render import render_match_application
        #    ---------        ------------------------
        #    1                2
        # 1. `src1/apps1/tic_tac_toe_vol2o0/views/gui/match_application/ver1o0/v_render.py`
        #                                                                      --------
        # 2. `1.` に含まれる関数

        return render_match_application(
            request,
            MatchApplicationView.playing_web_path,
            MatchApplicationView.template_path,
            MatchApplicationView.on_sent,
            MatchApplicationView.open)

    @staticmethod
    def on_sent(request):
        """送信後"""
        pass

    @staticmethod
    def open(request):
        """訪問後"""
        return MatchApplicationView.open_context

# EOF [OA16o3o0gA11o0]

Step [OA16o3o0gA12o0] ビュー作成 - gui/match_application/v_render.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                ├── 📂 views
                │   └── 📂 gui
                │       └── 📂 match_application
                │           └── 📂 ver1o0
                │               ├── 📄 __init__.py
👉              │               └── 📄 v_render.py
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 consumer
                        │   ├── 📄 ver1o0.py
                        │   └── 📄 ver1o1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA12o0]

from django.shortcuts import render, redirect


def render_match_application(request, playing_web_path, match_application_tp, on_sent, on_open):
    """[OA16o3o0gA12o0] 対局申込 - 描画

    Parameters
    ----------
    match_application_tp : str
        Template path
    """
    if request.method == "POST":
        # 送信後
        on_sent(request)

        # `po_` は POST送信するパラメーター名の目印
        po_room_name = request.POST.get("po_room_name")
        # 自分の番。 "X" か "O"。 機能拡張も想定
        my_turn = request.POST.get("po_my_turn")

        # TODO バリデーションチェックしたい

        return redirect(playing_web_path.format(po_room_name, my_turn))

    # 訪問後
    context = on_open(request)

    return render(request, match_application_tp, context)

# EOF [OA16o3o0gA12o0]

Step [OA16o3o0gA13o0] ビュー作成 - gui/playing/v1o0 フォルダー

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                ├── 📂 views
                │   └── 📂 gui
                │       ├── 📂 match_application
                │       │   └── 📂 ver1o0
                │       │       ├── 📄 __init__.py
                │       │       └── 📄 v_render.py
                │       └── 📂 playing
                │           └── 📂 ver1o0
👉              │               └── 📄 __init__.py
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 consumer
                        │   ├── 📄 ver1o0.py
                        │   └── 📄 ver1o1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA13o0]

class PlayingView():
    """[OA16o3o0gA13o0] 対局中ビュー"""

    # 駒
    expected_pieces = ['X', 'O']

    # 〇×ゲーム2.0巻 対局中 Webソケット
    web_socket_path = "/tic-tac-toe/v2/playing/"
    #                                ^two
    #                  ------------------------
    #                  1
    # 1. `ws://example.com:8000/tic-tac-toe/v2/playing/`
    #                          ------------------------

    # 〇×ゲーム2.0巻 対局中 HTML 1.1.0版
    template_path = "tic_tac_toe_vol2o0/gui/playing/ver1o1o0.html.txt"
    #                               ^two
    #                ------------------------------------------------
    #                1
    # 1. `src1/apps1/tic_tac_toe_vol2o0/templates/tic_tac_toe_vol2o0/gui/playing/ver1o1o0.html.txt`
    #                                             ------------------------------------------------

    @staticmethod
    def render(request, kw_room_name):
        """描画"""

        # 以下のファイルはあとで作ります
        from .v_render import render_playing
        #    ---------        --------------
        #    1                2
        # 1. `src1/apps1/tic_tac_toe_vol2o0/views/gui/playing/ver1o0/v_render.py`
        #                                                            --------
        # 2. `1.` に含まれる関数

        return render_playing(
            request,
            kw_room_name,
            PlayingView.web_socket_path,
            PlayingView.template_path,
            PlayingView.on_update,
            PlayingView.expected_pieces)

    @staticmethod
    def on_update(request):
        """訪問後または送信後"""
        # 拡張したい挙動があれば、ここに書く
        pass

# EOF [OA16o3o0gA13o0]

Step [OA16o3o0gA14o0] ビュー作成 - gui/playing/v1o0/v_render.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        └── 📂 apps1
            └── 📂 tic_tac_toe_vol2o0    # アプリケーション
                ├── 📂 static
                │   └── 📂 tic_tac_toe_vol2o0
                │       ├── 📂 gui
                │       │   └── 📂 connection
                │       │       └── 📄 ver1o0.js
                │       └── 📂 msg
                │           └── 📂 message_receiver
                │               └── 📄 ver1o0.js
                ├── 📂 templates
                │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
                │       └── 📂 gui
                │           ├── 📂 match_application
                │           │   └── 📄 ver1o0.html
                │           └── 📂 playing
                │               ├── 📄 ver1o0.html
                │               └── 📄 ver1o1o0.html.txt
                ├── 📂 views
                │   └── 📂 gui
                │       ├── 📂 match_application
                │       │   └── 📂 ver1o0
                │       │       ├── 📄 __init__.py
                │       │       └── 📄 v_render.py
                │       └── 📂 playing
                │           └── 📂 ver1o0
                │               ├── 📄 __init__.py
👉              │               └── 📄 v_render.py
                └── 📂 websocks
                    └── 📂 gui
                        ├── 📂 consumer
                        │   ├── 📄 ver1o0.py
                        │   └── 📄 ver1o1o0.py
                        └── 📂 message_manager
                            └── 📄 ver1o0.py
# BOF [OA16o3o0gA14o0]

from django.http import Http404
from django.shortcuts import render


def render_playing(request, kw_room_name, wsp_playing, playing_tp, on_update, expected_pieces):
    """[OA16o3o0gA14o0] 対局中 - 描画

    Parameters
    ----------
    wsp_playing : str
        Webソケットパス
    playing_tp : str
        Template path
    """

    my_turn = request.GET.get("myturn")
    if my_turn not in expected_pieces:
        raise Http404(f"My piece '{my_turn}' does not exists")

    on_update(request)

    # `dj_` は Djangoでレンダーするパラメーター名の目印
    context = {
        "dj_room_name": kw_room_name,
        "dj_my_turn": my_turn,
        "dj_path_of_ws_playing": wsp_playing,
    }
    return render(request, playing_tp, context)

# EOF [OA16o3o0gA14o0]

Step [OA16o3o0gA15o0]

Merged to [OA16o3o0gA15o1o0]

Step [OA16o3o0gA15o1o0] ルート編集 - urls.csv ファイル

👇 以下の既存ファイルの末尾に追記してほしい

    ├── 📂 src1
    │   └── 📂 apps1
    │       └── 📂 tic_tac_toe_vol2o0    # アプリケーション
    │           ├── 📂 static
    │           │   └── 📂 tic_tac_toe_vol2o0
    │           │       ├── 📂 gui
    │           │       │   └── 📂 connection
    │           │       │       └── 📄 ver1o0.js
    │           │       └── 📂 msg
    │           │           └── 📂 message_receiver
    │           │               └── 📄 ver1o0.js
    │           ├── 📂 templates
    │           │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
    │           │       └── 📂 gui
    │           │           ├── 📂 match_application
    │           │           │   └── 📄 ver1o0.html
    │           │           └── 📂 playing
    │           │               ├── 📄 ver1o0.html
    │           │               └── 📄 ver1o1o0.html.txt
    │           ├── 📂 views
    │           │   └── 📂 gui
    │           │       ├── 📂 match_application
    │           │       │   └── 📂 ver1o0
    │           │       │       ├── 📄 __init__.py
    │           │       │       └── 📄 v_render.py
    │           │       └── 📂 playing
    │           │           └── 📂 ver1o0
    │           │               ├── 📄 __init__.py
    │           │               └── 📄 v_render.py
    │           └── 📂 websocks
    │               └── 📂 gui
    │                   ├── 📂 consumer
    │                   │   ├── 📄 ver1o0.py
    │                   │   └── 📄 ver1o1o0.py
    │                   └── 📂 message_manager
    │                       └── 📄 ver1o0.py
    └── 📂 src1_meta
        └── 📂 data
👉          └── 📄 urls.csv
...略... file,path,name,comment,module,class,alias,method
...略...


../src1/project1/urls_tic_tac_toe_vol2o0_autogen.py,tic-tac-toe/vol2.0/match-application/ver1.0/,,"OA16o3o0gA15o1o0 〇×ゲーム2.0巻 対局申込中1.0版",apps1.tic_tac_toe_vol2o0.views.gui.match_application.ver1o0,MatchApplicationView,,render
../src1/project1/urls_tic_tac_toe_vol2o0_autogen.py,tic-tac-toe/vol2.0/playing/ver1.0/<str:kw_room_name>/,,"OA16o3o0gA15o1o0 〇×ゲーム2.0巻 対局中1.0版",apps1.tic_tac_toe_vol2o0.views.gui.playing.ver1o0,PlayingView,,render

Step [OA16o3o_2o0g5o2o0] ルート編集 - コマンド打鍵

👇 以下のコマンドを打鍵してほしい

cd ../src1_meta
python -m scripts.auto_generators.urls
cd ../src1
docker-compose restart
  • ディレクトリーは、がんばって移動してほしい
  • スクリプトについて See also: O3o2o_1o0g2o0
  • 設定ファイルを変更したら、サーバーの再起動が必要

Step [OA16o3o0gA16o0] Webソケット用ルート新規作成 - ws_urls_tic_tac_toe_v2.py ファイル

👇 以下のファイルを新規作成してほしい

    └── 📂 src1
        ├── 📂 apps1
        │   └── 📂 tic_tac_toe_vol2o0    # アプリケーション
        │       ├── 📂 static
        │       │   └── 📂 tic_tac_toe_vol2o0
        │       │       ├── 📂 gui
        │       │       │   └── 📂 connection
        │       │       │       └── 📄 ver1o0.js
        │       │       └── 📂 msg
        │       │           └── 📂 message_receiver
        │       │               └── 📄 ver1o0.js
        │       ├── 📂 templates
        │       │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
        │       │       └── 📂 gui
        │       │           ├── 📂 match_application
        │       │           │   └── 📄 ver1o0.html
        │       │           └── 📂 playing
        │       │               ├── 📄 ver1o0.html
        │       │               └── 📄 ver1o1o0.html.txt
        │       ├── 📂 views
        │       │   └── 📂 gui
        │       │       ├── 📂 match_application
        │       │       │   └── 📂 ver1o0
        │       │       │       ├── 📄 __init__.py
        │       │       │       └── 📄 v_render.py
        │       │       └── 📂 playing
        │       │           └── 📂 ver1o0
        │       │               ├── 📄 __init__.py
        │       │               └── 📄 v_render.py
        │       └── 📂 websocks
        │           └── 📂 gui
        │               ├── 📂 consumer
        │               │   ├── 📄 ver1o0.py
        │               │   └── 📄 ver1o1o0.py
        │               └── 📂 message_manager
        │                   └── 📄 ver1o0.py
        └── 📂 project1                      # プロジェクト
            ├── 📄 urls_tic_tac_toe_v2.py
👉          └── 📄 ws_urls_tic_tac_toe_v2.py
# BOF [OA16o3o0gA16o0]

# See also: 📖 [Channels - Consumers](https://channels.readthedocs.io/en/latest/topics/consumers.html)
from django.conf.urls import url

# [OA16o3o0gA16o0] 〇×ゲーム2.0巻 ウェブソケットGUIコンシューマー ver1.1.0
from apps1.tic_tac_toe_vol2o0.websocks.gui.consumer.ver1o1o0 import ConsumerCustom
#          ------------------                       --------        --------------
#          11                                       12              2
#    -------------------------------------------------------
#    10
# 10, 12. ディレクトリー
# 11. アプリケーション
# 2. `12.` に含まれる __init__.py にさらに含まれるクラス


websocket_urlpatterns = [

    # [OA16o3o0gA16o0] 〇×ゲーム v2
    url(r'^tic-tac-toe/v2/playing/(?P<kw_room_name>\w+)/$',
        # -----------------------------------------------
        # 1
        ConsumerCustom.as_asgi()),
    #   ------------------------
    #   2
    # 1. 例えば `ws://example.com/tic-tac-toe/v2/playing/Elephant/` のようなURLのパスの部分
    #                            --------------------------------
    #    kw_room_name は変数として渡される
    # 2. クラス名とメソッド。 URL を ASGI形式にする
]

# EOF [OA16o3o0gA16o0]

Step [OA16o3o0gA17o0] Webソケット用総合ルート編集 - asgi.py ファイル<その2>

👇以下の既存のファイルを編集してほしい

    └── 📂 src1
        ├── 📂 apps1
        │   └── 📂 tic_tac_toe_vol2o0    # アプリケーション
        │       ├── 📂 static
        │       │   └── 📂 tic_tac_toe_vol2o0
        │       │       ├── 📂 gui
        │       │       │   └── 📂 connection
        │       │       │       └── 📄 ver1o0.js
        │       │       └── 📂 msg
        │       │           └── 📂 message_receiver
        │       │               └── 📄 ver1o0.js
        │       ├── 📂 templates
        │       │   └── 📂 tic_tac_toe_vol2o0    # アプリケーションと同名
        │       │       └── 📂 gui
        │       │           ├── 📂 match_application
        │       │           │   └── 📄 ver1o0.html
        │       │           └── 📂 playing
        │       │               ├── 📄 ver1o0.html
        │       │               └── 📄 ver1o1o0.html.txt
        │       ├── 📂 views
        │       │   └── 📂 gui
        │       │       ├── 📂 match_application
        │       │       │   └── 📂 ver1o0
        │       │       │       ├── 📄 __init__.py
        │       │       │       └── 📄 v_render.py
        │       │       └── 📂 playing
        │       │           └── 📂 ver1o0
        │       │               ├── 📄 __init__.py
        │       │               └── 📄 v_render.py
        │       └── 📂 websocks
        │           └── 📂 gui
        │               ├── 📂 consumer
        │               │   ├── 📄 ver1o0.py
        │               │   └── 📄 ver1o1o0.py
        │               └── 📂 message_manager
        │                   └── 📄 ver1o0.py
        └── 📂 project1                      # プロジェクト
👉          ├── 📄 asgi.py
            ├── 📄 urls_tic_tac_toe_v2.py
            └── 📄 ws_urls_tic_tac_toe_v2.py
# ...略...


# * 以下を追加
# [OA16o3o0gA17o0] 〇×ゲーム v2
from . import ws_urls_tic_tac_toe_v2
#                                  ^two
#    -        ----------------------
#    1        2
# 1. 同じディレクトリー
# 2. `src1/projectN/ws_urls_tic_tac_toe_v2.py`
#                   ----------------------


# ...略...
# この辺に os.environ.setdefault(...)


# ...略...


# 複数のアプリケーションの websocket_urlpatterns をマージします
websocket_urlpatterns_merged = []


# ...略...


# * 以下を追加
# [OA16o3o0gA17o0] 〇×ゲーム v2
websocket_urlpatterns_merged.extend(
    ws_urls_tic_tac_toe_v2.websocket_urlpatterns)
#                        ^two

テスト手順

Step [OA16o3o0gA18o0] Web画面へアクセス

このゲームは2人用なので、Webページを2窓で開き、片方が X プレイヤー、もう片方が O プレイヤーとして遊んでください

📖 http://localhost:8000/tic-tac-toe/vol2.0/match-application/ver1.0/

後処理

Step [OA16o3o0gA19o0] ランチャーのリンク用データ追加 - finished-lessons.csv ファイル

👇 以下の既存ファイルの最終行に追記してほしい

    └── 📂 src1
        ├── 📂 apps1
        │   ├── 📂 portal_v1                        # アプリケーション
        │   │   └── 📂 data
👉      │   │       └── 📄 finished-lessons.csv
        │   └── 📂 tic_tac_toe_vol2o0                # アプリケーション
        │       ├── 📂 static
        │       │   └── 📂 tic_tac_toe_vol2o0
        │       │       ├── 📂 gui
        │       │       │   └── 📂 connection
        │       │       │       └── 📄 ver1o0.js
        │       │       └── 📂 msg
        │       │           └── 📂 message_receiver
        │       │               └── 📄 ver1o0.js
        │       ├── 📂 templates
        │       │   └── 📂 tic_tac_toe_vol2o0
        │       │       └── 📂 gui
        │       │           ├── 📂 match_application
        │       │           │   └── 📄 ver1o0.html
        │       │           └── 📂 playing
        │       │               ├── 📄 ver1o0.html
        │       │               └── 📄 ver1o1o0.html.txt
        │       ├── 📂 views
        │       │   └── 📂 gui
        │       │       ├── 📂 match_application
        │       │       │   └── 📂 ver1o0
        │       │       │       ├── 📄 __init__.py
        │       │       │       └── 📄 v_render.py
        │       │       └── 📂 playing
        │       │           └── 📂 ver1o0
        │       │               ├── 📄 __init__.py
        │       │               └── 📄 v_render.py
        │       └── 📂 websocks
        │           └── 📂 gui
        │               ├── 📂 consumer
        │               │   ├── 📄 ver1o0.py
        │               │   └── 📄 ver1o1o0.py
        │               └── 📂 message_manager
        │                   └── 📄 ver1o0.py
        └── 📂 project1                          # プロジェクト
            ├── 📄 asgi.py
            ├── 📄 urls_tic_tac_toe_v2.py
            └── 📄 ws_urls_tic_tac_toe_v2.py

👇 冗長なスペース,冗長なダブルクォーテーション,末尾のカンマ は止めてほしい

/tic-tac-toe/vol2.0/match-application/ver1.0/,OA16o3o0gA19o0 〇×ゲーム2.0巻 対局申込中1.0版

👇 ランチャーにリンクが追加されていることを確認してほしい

📖 http://localhost:8000/

次の記事

📖 Django さくらVPS 備忘録

参考にした記事

Vuetify 関連

📖 Vuetifyのv-selectにてitemsのキーがtextとvalueじゃないときの対処法

Web socket 関連

📖 Django Channels and WebSockets

メモリ関連

📖 メモリ管理

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0