
ChatGPT DALL-E3で作るオンライン四目並べゲーム
以前作ったChatGPT DALL-E3で美少女四目並べ(note記事)を元に
Deno Deploy+Supabase+Websocketでオンライン四目並べができるゲームを作成しました。サーバー費用は無料枠内で構築できます。
できたもの

なぜオンライン四目並べか
最終的には美少女麻雀ゲームをChatGPT+DALL-E3で作りたいのでテスト的意味合いが強い
四目並べゲームは作成し終わっていたのでロジック的にChatGPTがコードは全部書いてくれることが実証できていたので。
ChatGPT DALL-E3でのキャラクターメイク
1名の美少女キャラ(しずく)はある程度固定での出力するためのプロフィールを作れたのでさくさく作成できたのですが、二人目のキャラクターメイクは挫折しました。髪型や目の色でキャラクターのデザインはある程度個性化できますが、何名もキャラクターデザインを固定して作成するのは骨が折れそうだなと思いました。
諦めて骸骨忍者とか作りましたが、テイストがバラバラなのでドット絵で全キャラをあわせる・・など同じゲームという表現をキャラクターデザインに持たせる工夫は必要かもしれません。

今回はキャラクターを選択してもなんの効果もないですが、対戦ボードゲームにありがちな、先読み特殊技や2回駒を動かせるなどの特殊技能を実装すると面白いと思います。
画面デザイン(GPT4-Vでラフを添付)
iPadでラフを書いてChatGPTにアップロードして仕様を伝えます。

ラフ画像とともにこんな感じで仕様を伝えればコードを書いてくれます
先手後手とキャラクターの選択「プレイヤー設定」の仕様を伝えます
まずは理解してレビューをお願いします
※ここでは先手後手とキャラクターの設定を総称して「プレイヤー設定」と呼ぶ
# 画面イメージ
想定画面を添付します。
手書きの部分が今回の部分です。
## 画面要素
- gameInfoMsg: rooms.statusに応じたゲームの状態を伝える 「ユーザーを選択してください」「Player2が入出しました」
- characterSelect: キャラクターを選択するプルダウン characters変数から取得する
- characterImage1: 選択したキャラクターを動的に表示する。画像サイズはまちまちなので画面からはみ出さないよう制御
- ゲームを開始: キャラクターの選択が終わったことをサーバーに伝える /wsへwebsocket送信する
## プレイヤー設定のクライアントのデータ保存
プレイヤー設定はクライアントにはクッキーで保持する
期限は生成して1時間
クッキーのイメージ
rooms : {
roomId : {playserId: 1 , characher: shizuku}
}
roomIdには実際のroomIdが入る
ゲームロジック部分
基本的には「こういうゲームつくりたいんやが・・」とChatGPT先生に聞きながら作ることになりますが、Deno、Supabaseのような比較的新しく技術進化が早い言語やソフトウェアは古い情報を返すことも多かったです。技術的こだわりがないなら枯れた言語かつGPTが得意そうなPythonやnode.jsが良いかと思います。nodeならvercel使えますし。
使用したライブラリ
Deno
dotenv
Oak (Webミドルウェアフレームワーク)
supabase-js
dejs (テンプレートエンジン)
JavaScript
未使用
サーバーインフラ
Deno Deploy = Deno言語(TypeScript)でサーバーサイドのコードを書いてデプロイ、一定使用数まで無料。git push origin main でデプロイできるので便利。
Supabase = PostgreSQLで対戦やルームのデータ同期を行うために使う。一定使用数まで無料。
WebSocket
オンライン対戦ゲームをWEBで実装する上でコアとなる部分になります。
Denoサーバー側でポーリングとPUSHを行います。
websocket通知されるアクション
キャラクター選択 (キャラクターの選択は対戦ゲーム雰囲気の演出のみ)
ゲームの開始 (2名揃ったらサーバーからPush)
ケーム中 駒をおく(自分の駒を送信、相手の駒を受信)
ゲーム終了

websocket クライアント側
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を使用)
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の制御
3者のブラウザは同一ルーム[akb73]にいる

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

ルームAの更新をルームBのプレイヤーに伝えてはならない

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

(解決方法)websocket受信時にsocketをハッシュマップで持つ(Deno)
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)
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);
}
}
}
}

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

Player1が先手、Playser2が後手、Player3が試合閲覧者の場合に
全員が同一ルームにいてもPlayer1~3がどこのリージョンのどのマシンにいるかがわからないので各々のサーバーがもっているWebsocketを同期できない。
Supabase側でボードの駒の動き等を集約してポーリングする
Supabaseのリアルタイム機能ではテーブルの更新をキャッチしてサーバ側に通知してくれることができる。そのためDenoDeployのどのリージョンやどのマシンインスタンスの更新でもSupabase側でキャッチできるのでSupabaseに接続しているDenoDeployサーバ群は共通の更新情報をうけとることができる!🥰🥰🥰🥰

supabaseのリアルタイム監視機能を使う(deno実装)
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を使うと特定の条件の行のみ絞れます
テーブルの更新を一括で受信するのでサーバー側がどれだけ負荷に耐えられるかは不明、場合によってはメッセージキューシステムを別に挟む必要があるかも
終わりに
ChatGPT DALL-E3とDeno Deployでルームに対応したオンラインボードゲームの作成方法を紹介しました。
とりあえずこの方法でなんとかなると思います。
Deno DeployもSupabaseも一定量は無料なので、Deno Deployに興味ある人は2つを組み合わせてDALL-E3でキャラクターを作成しつつオンライン将棋やオンライン麻雀を作ってみてはどうでしょうか。