Deno DeployとWebSocketを使ったオンライン四目並べゲームの作成



# なぜオンライン四目並べか

Deno DeployについてLTする機会があったので
以前作ったChatGPT DALL-E3で美少女四目並べ[(note記事)](https://note.com/akb428/n/nb7a42f412c6d)を元に
Deno Deploy＋Websocketでオンライン四目並べができるゲームを作成しました。

# できたもの
![YonmokuOnlineQiita.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/d171866e-a5fe-9bac-8240-2bb277a5a58f.gif)

# 概要
プロジェクトの目的: このプロジェクトは、DenoとWebSocketを用いたリアルタイムWebアプリケーションの開発過程とその技術的な側面を理解することを目的としています。

アプリケーションの概要: 開発されたアプリケーションは、オンラインでプレイできる四目並べゲームです。プレイヤーはリアルタイムで駒を配置し、ゲームの進行を他のプレイヤーと共有します。

技術スタック: 主な技術として、フロントエンドにはJavaScriptとHTML5 Canvasを、バックエンドにはDenoとWebSocketプロトコルを使用しています。また、データ管理とリアルタイム通信にはSupabaseを活用しています。

ポイント: リアルタイムWebアプリケーションの開発には、ユーザー間のインタラクションの即時性とデータの同期が重要です。この記事では、これらの要素を如何にして実現しているかに焦点を当てます。


# 使用したライブラリ

## Deno
- dotenv
- Oak (Webミドルウェアフレームワーク）
- supabase-js
- dejs (テンプレートエンジン)
## JavaScript
　未使用

# Deno Deployについて

Deno Deployは、Deno(TypeScript)で書かれたWEBアプリケーションをデプロイするためのサービスです。このサービスは、無料で1日最大10万リクエストまで利用可能で、HTTPSとカスタムドメインにも対応しています。
Deno Deployにはドメイン名の変更機能やカスタムドメインへの変更機能があり、SSL証明書の自動設定も無料で提供されています​​​​​​。便利機能としては、リアルタイムログの閲覧、サーバーレスタイプでの動作、リリースリビジョンの保持などがあります​
https://qiita.com/AKB428/items/ebebed20dbebd2b9ab78


# WebSocket通信の詳細

## websocket通知されるアクション

- キャラクター選択　（キャラクターの選択は対戦ゲーム雰囲気の演出のみ）
- ゲームの開始 (2名揃ったらサーバーからPush）
- ケーム中　駒をおく（自分の駒を送信、相手の駒を受信）
- ゲーム終了

![スクリーンショット 2023-12-19 13.19.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/55f0624a-672d-42e2-842f-d30383b7975f.png)


## クライアント側の処理
ウェブソケットの接続は、JavaScriptを使用して WebSocket オブジェクトを作成することで開始されます。
接続が開始されると、クライアントはサーバーにメッセージを送信できます。
クライアントはサーバーからのメッセージをリアルタイムで受信し、適切に処理します。

## サーバー側の処理
サーバーは、DenoのWebSocket APIを利用してクライアントからの接続を受け入れます。
クライアントからのデータを受信した際には、適切な応答や処理を行い、必要に応じてクライアントにデータを送信します。
ゲームの状態更新やプレイヤー間のアクションをリアルタイムで処理し、反映させます。

## 通信の流れ
クライアントが行動（例: 石を置く、キャラクターを選択する）を行う。
行動データはWebSocketを介してサーバーに送信されます。
サーバーはこのデータを処理し、ゲームの状態を更新。
更新されたゲーム状態は、同じ部屋にいる他のクライアントにブロードキャストされます。

## Deno&Deno Deployは標準でWebSocketに対応している

![スクリーンショット 2023-12-19 12.22.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/9ac43c2d-7699-a15f-6e00-81238d3e20f1.png)

https://deno.com/blog/deploy-streams
https://developer.mozilla.org/ja/docs/Web/API/WebSockets_API/Writing_a_WebSocket_server_in_JavaScript_Deno

## websocket クライアント側

```javascript:game.js
const hostname = window.location.hostname;
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${hostname}/ws?roomId=${roomInfo.room_id}`;
console.log(wsUrl);
const socket = new WebSocket(wsUrl);

socket.onopen = function (e) {
    console.log("[open] Connection established");
};

socket.onmessage = function (event) {
    const data = JSON.parse(event.data);
    console.log(data);
```

## websocketサーバー側(deno oakを使用)

```typescript:server.ts
async function handleWebSocket(ctx: Context) {
    const sock = await ctx.upgrade();
    sockets.add(sock);
    const roomId = ctx.request.url.searchParams.get('roomId') || 'defaultRoom';

    if (!socketsOfRoom[roomId]) {
        socketsOfRoom[roomId] = [];
    }
    socketsOfRoom[roomId].push(sock);
    //console.log(socketsOfRoom);

    sock.onopen = () => {
        console.log("WebSocket opened");
        //sock.send(JSON.stringify({ role: role }));
    }

    sock.onclose = () => {
```


# ルームの概念とWebSocketの制御

３者のブラウザは同一ルーム[akb73]にいる

![スクリーンショット 2023-12-19 1.11.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/490dcfef-90b0-4ae0-7f34-dc3ee08c558d.png)

四目オンラインには複数のルーム(=ゲームボード)が存在する

![Untitled diagram-2023-12-18-175104.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/12ce624e-aed6-6e8d-5d30-04a0ec09ccdb.png)


ルームAの更新をルームBのプレイヤーに伝えてはならない
![ルーム概念-2023-12-22-133600.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/e91573ff-6c05-f591-f686-a212147deb20.png)



同一ルームの情報しかブロードキャストしないようにする制御が必要

![四目ルームごとのブロードキャスト-2023-12-18-162232.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/f77d18ad-8c25-3c36-a46e-2c4de3ff2645.png)

## (解決方法)websocket受信時にsocketをハッシュマップで持つ(Deno)

```typescript:server.ts
async function handleWebSocket(ctx: Context) {
    const sock = await ctx.upgrade();
    sockets.add(sock);
    const roomId = ctx.request.url.searchParams.get('roomId') || 'defaultRoom';

    if (!socketsOfRoom[roomId]) {
        socketsOfRoom[roomId] = [];
    }
    socketsOfRoom[roomId].push(sock);
    //console.log(socketsOfRoom);

    sock.onopen = () => {
        console.log("WebSocket opened");
        //sock.send(JSON.stringify({ role: role }));
    }

```

## 特定のルームへブロードキャスト(Deno)

```typescript:server.ts
function broadcastToRoom(roomId: string, message: string) {
    const socketsInRoom = socketsOfRoom[roomId];
    if (socketsInRoom) {
        for (const socket of socketsInRoom) {
            if (socket.readyState === WebSocket.OPEN) {
                socket.send(message);
            }
        }
    }
}
```

完成したルーム制御図
![四目ルーム通知-2023-12-19-041501.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/217685fc-c7cf-0445-6642-491ca28133da.png)


# 同一の部屋のクライアントがDeno Deployのリージョンをまたがる問題

![yonmokuRegion違い-2023-12-22-130811.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/20d8174a-f93e-bca7-6e84-7d0c31ece6b1.png)

Player1が先手、Playser2が後手、Player3が試合閲覧者の場合に
全員が同一ルームにいてもPlayer1~3がどこのリージョンのどのマシンにいるかがわからないので各々のサーバーがもっているWebsocketを同期できない。

## Deno Deployではリージョン固定を制御できない

![スクリーンショット 2023-12-10 22.48.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/2482f7f1-ab3e-7ba5-c627-6ac6fae9b6d4.png)

## Supabase側でボードの駒の動き等を集約してポーリングする

https://supabase.com/docs/guides/realtime

Supabaseのリアルタイム機能ではテーブルの更新をキャッチしてサーバ側に通知してくれることができる。そのためDenoDeployのどのリージョンやどのマシンインスタンスの更新でもSupabase側でキャッチできるのでSupabaseに接続しているDenoDeployサーバ群は共通の更新情報をうけとることができる！🥰🥰🥰🥰

![yonmoku1-2023-12-18-160414.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/da5d8e14-1eab-e89f-6520-f7a14943a27d.png)

Supabaseのテーブル設定でリアルタイム機能をONにする

![スクリーンショット 2023-12-12 0.20.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/276ab397-222c-b367-0818-32300a412f9b.png)


## supabaseのリアルタイム監視機能を使う(deno実装)

```typescript:server.ts
// 特定のルームIDのgame_statesテーブルの変更を購読
//https://supabase.com/docs/guides/realtime/postgres-changes
supabase
    .channel('game_states_channel')
    .on("postgres_changes",
        {
            event: "INSERT",
            schema: "public",
            table: `game_states`, // DBのテーブル名
            // filter: `room_id=eq.${roomId}` 
        },
        (payload) => {
            //console.log('データが変更されました:', payload);
            const roomId = payload.new.room_id;
            const player_id = parseInt(payload.new.player_id);
            const placeStone = payload.new.action.placeStone;
            const playerColor = PlayerColor[player_id];
            const message = JSON.stringify({ action: WsSendAction.PlaceStone, playerId: player_id, playerColor: playerColor, placeStoneXY: placeStone });
            console.log(`broadcastToRoom ${roomId} ${message}`);
            broadcastToRoom(roomId, message);
        })
    .subscribe();

```

- eventに*をつけるとすべてのイベントをキャッチできます
- filterを使うと特定の条件の行のみ絞れます
- テーブルの更新を一括で受信するのでサーバー側がどれだけ負荷に耐えられるかは不明、場合によってはメッセージキューシステムを別に挟む必要があるかも 


# テストプレイ

サンドボックスとしてルームakb21-50を開放
- スマートフォン表示でやってください
- デフォルトキャラクター以外を選択しないとバグります
- 先手と後手がブッキングしないように2名でやるときはP1かP2をきめておいてください
- なんかうまく通信できなかったらリロードでなんとかなります
- 1名で2役やるときはブラウザはわけてください（cookie制御があるため）

https://yonmoku.deno.dev/room/akb21
https://yonmoku.deno.dev/room/akb50


# その他
- ローカル開発ではMacのセキュリティの制限なのかport80で起動しないとwebsocketがうまく通信できなかった（開発環境のデフォは8080など）
- ChatGPT4を使いながらコーディングしたがDenoの環境周り、supabaseのリアルタイム周りの情報は古すぎて役に立たなかった。

# 終わりに

Deno Deployでルームに対応したオンラインボードゲームの作成方法を紹介しました。
とりあえずこの方法でなんとかなると思います。
Deno　DeployもSupabaseも一定量は無料なので、Deno Deployに興味ある人は2つを組み合わせてオンライン将棋やオンライン麻雀を作ってみてはどうでしょうか。