diff --git a/apps/client/src/hooks/useSocketSubscriptions.ts b/apps/client/src/hooks/useSocketSubscriptions.ts index 8663c7b..ad75a40 100644 --- a/apps/client/src/hooks/useSocketSubscriptions.ts +++ b/apps/client/src/hooks/useSocketSubscriptions.ts @@ -16,6 +16,38 @@ setScenePhase: (phase: appTypes.ScenePhase) => void; }; +type AppSocketHandlers = { + handleConnect: (id: string) => void; + handleRoomUpdate: (updatedRoom: roomTypes.Room) => void; + handleGameStart: () => void; + handleGameResult: (payload: GameResultPayload) => void; +}; + +const registerConnectionSubscriptions = ({ handleConnect }: AppSocketHandlers): void => { + socketManager.common.onConnect(handleConnect); +}; + +const unregisterConnectionSubscriptions = ({ handleConnect }: AppSocketHandlers): void => { + socketManager.common.offConnect(handleConnect); +}; + +const registerRoomSubscriptions = ({ handleRoomUpdate }: AppSocketHandlers): void => { + socketManager.lobby.onRoomUpdate(handleRoomUpdate); +}; + +const unregisterRoomSubscriptions = ({ handleRoomUpdate }: AppSocketHandlers): void => { + socketManager.lobby.offRoomUpdate(handleRoomUpdate); +}; + +const registerGameSubscriptions = ({ handleGameStart, handleGameResult }: AppSocketHandlers): void => { + socketManager.game.onceGameStart(handleGameStart); + socketManager.game.onGameResult(handleGameResult); +}; + +const unregisterGameSubscriptions = ({ handleGameResult }: AppSocketHandlers): void => { + socketManager.game.offGameResult(handleGameResult); +}; + /** アプリ共通のソケット購読を登録しクリーンアップするフック */ export const useSocketSubscriptions = ({ completeJoinRequest, @@ -25,36 +57,37 @@ setScenePhase, }: UseSocketSubscriptionsParams): void => { useEffect(() => { - const handleConnect = (id: string) => { + const handlers: AppSocketHandlers = { + handleConnect: (id: string) => { setMyId(id); - }; + }, - const handleRoomUpdate = (updatedRoom: roomTypes.Room) => { + handleRoomUpdate: (updatedRoom: roomTypes.Room) => { completeJoinRequest(); setRoom(updatedRoom); setScenePhase(appConsts.ScenePhase.LOBBY); - }; + }, - const handleGameStart = () => { + handleGameStart: () => { setGameResult(null); setScenePhase(appConsts.ScenePhase.PLAYING); - }; + }, - const handleGameResult = (payload: GameResultPayload) => { + handleGameResult: (payload: GameResultPayload) => { setGameResult(payload); setScenePhase(appConsts.ScenePhase.RESULT); + }, }; - socketManager.common.onConnect(handleConnect); - socketManager.lobby.onRoomUpdate(handleRoomUpdate); - socketManager.game.onceGameStart(handleGameStart); - socketManager.game.onGameResult(handleGameResult); + registerConnectionSubscriptions(handlers); + registerRoomSubscriptions(handlers); + registerGameSubscriptions(handlers); return () => { completeJoinRequest(); - socketManager.common.offConnect(handleConnect); - socketManager.lobby.offRoomUpdate(handleRoomUpdate); - socketManager.game.offGameResult(handleGameResult); + unregisterConnectionSubscriptions(handlers); + unregisterRoomSubscriptions(handlers); + unregisterGameSubscriptions(handlers); }; }, [completeJoinRequest, setGameResult, setMyId, setRoom, setScenePhase]); }; diff --git a/apps/client/src/scenes/game/entities/player/PlayerView.ts b/apps/client/src/scenes/game/entities/player/PlayerView.ts index 9c55c23..0740720 100644 --- a/apps/client/src/scenes/game/entities/player/PlayerView.ts +++ b/apps/client/src/scenes/game/entities/player/PlayerView.ts @@ -6,6 +6,8 @@ import { Assets, Sprite, Texture } from "pixi.js"; import { config } from "@client/config"; +const ENABLE_DEBUG_LOG = import.meta.env.DEV; + export class PlayerView { public readonly displayObject: Sprite; @@ -54,7 +56,6 @@ } /** BASE_URL対応のURLで画像を読み込み、スプライトに反映する */ - /** BASE_URL対応のURLで画像を読み込み、スプライトに反映する */ private async applyTexture(imageFileName: string): Promise { try { const imageUrl = `${import.meta.env.BASE_URL}${imageFileName}`; @@ -69,10 +70,11 @@ this.displayObject.width = PLAYER_RADIUS_PX * 2 * scaleRate; this.displayObject.height = PLAYER_RADIUS_PX * 2 * scaleRate; - // 👇 ちゃんとこの処理が実行されているか、ブラウザのログに出す! - console.log( - `🎨 画像を ${scaleRate} 倍のサイズ(${this.displayObject.width})に拡大しました!`, - ); + if (ENABLE_DEBUG_LOG) { + console.log( + `[PlayerView] 画像を ${scaleRate} 倍のサイズ(${this.displayObject.width})に拡大`, + ); + } } catch (error) { console.error( `[PlayerView] 画像の読み込みに失敗: ${imageFileName}`, diff --git a/apps/client/src/scenes/result/ResultScene.tsx b/apps/client/src/scenes/result/ResultScene.tsx index d801ebd..e3225de 100644 --- a/apps/client/src/scenes/result/ResultScene.tsx +++ b/apps/client/src/scenes/result/ResultScene.tsx @@ -4,6 +4,7 @@ * 順位,チーム名,塗り率の3項目をテーブル形式で描画する */ import type { GameResultPayload } from "@repo/shared"; +import type { CSSProperties } from "react"; type Props = { result: GameResultPayload | null; @@ -11,6 +12,56 @@ const formatPaintRate = (value: number): string => `${value.toFixed(1)}%`; +const ROW_GRID_TEMPLATE = "120px 1fr 180px"; + +const ROOT_STYLE: CSSProperties = { + width: "100vw", + height: "100dvh", + background: "#111", + color: "white", + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "24px", + boxSizing: "border-box", +}; + +const TITLE_STYLE: CSSProperties = { + margin: "0 0 20px 0", + fontSize: "clamp(1.6rem, 4vw, 2.2rem)", +}; + +const TABLE_STYLE: CSSProperties = { + width: "100%", + maxWidth: "720px", + border: "1px solid #444", + borderRadius: "8px", + overflow: "hidden", +}; + +const HEADER_ROW_STYLE: CSSProperties = { + display: "grid", + gridTemplateColumns: ROW_GRID_TEMPLATE, + background: "#222", + padding: "12px 16px", + fontWeight: "bold", +}; + +const RIGHT_ALIGN_STYLE: CSSProperties = { textAlign: "right" }; + +const RATE_STYLE: CSSProperties = { + textAlign: "right", + fontVariantNumeric: "tabular-nums", +}; + +const getBodyRowStyle = (index: number): CSSProperties => ({ + display: "grid", + gridTemplateColumns: ROW_GRID_TEMPLATE, + padding: "12px 16px", + borderTop: "1px solid #333", + background: index % 2 === 0 ? "#171717" : "#1d1d1d", +}); + /** 最終結果データを受け取り,順位一覧を表示する */ export const ResultScene = ({ result }: Props) => { if (!result) { @@ -18,58 +69,21 @@ } return ( -
-

結果発表

+
+

結果発表

-
-
+
+
順位 チーム名 - 塗り率 + 塗り率
{result.rankings.map((row, index) => ( -
+
{row.rank}位 {row.teamName} - + {formatPaintRate(row.paintRate)}
diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index d737722..b3bc22b 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -4,7 +4,7 @@ */ import { createHttpServer } from "./network/bootstrap/createHttpServer"; import { boot } from "./network/bootstrap/boot"; -import { config } from "@server/config"; +import { config } from "./config"; // サーバー待受ポート const PORT = process.env.PORT || config.NETWORK_CONFIG.DEV_SERVER_PORT; diff --git a/packages/shared/src/protocol/eventPayloadMaps.ts b/packages/shared/src/protocol/eventPayloadMaps.ts new file mode 100644 index 0000000..406c312 --- /dev/null +++ b/packages/shared/src/protocol/eventPayloadMaps.ts @@ -0,0 +1,66 @@ +/** + * eventPayloadMaps + * イベント方向ごとのペイロード対応表を定義する + * イベント名定数とペイロード型の対応契約を集約する + */ +import { SocketEvents } from "./socketEvents"; +import type { + BombPlacedPayload, + CurrentPlayersPayload, + GameResultPayload, + GameStartPayload, + JoinRoomPayload, + MovePayload, + NewPlayerPayload, + PingPayload, + PongPayload, + RemovePlayerPayload, + RoomJoinRejectedPayload, + RoomUpdatePayload, + UpdateMapCellsPayload, + UpdatePlayersPayload, +} from "./eventPayloads"; + +/** 接続ライフサイクルイベントのペイロード対応表 */ +export type ConnectionLifecycleEventPayloadMap = { + [SocketEvents.CONNECT]: undefined; + [SocketEvents.DISCONNECT]: undefined; +}; + +/** クライアントからサーバーへ送信するイベントごとのペイロード対応表 */ +export type ClientToServerEventPayloadMap = { + [SocketEvents.JOIN_ROOM]: JoinRoomPayload; + [SocketEvents.START_GAME]: undefined; + [SocketEvents.READY_FOR_GAME]: undefined; + [SocketEvents.MOVE]: MovePayload; + [SocketEvents.PLACE_BOMB]: BombPlacedPayload; + [SocketEvents.PING]: PingPayload; +}; + +/** サーバーからクライアントへ送信するイベントごとのペイロード対応表 */ +export type ServerToClientEventPayloadMap = { + [SocketEvents.ROOM_JOIN_REJECTED]: RoomJoinRejectedPayload; + [SocketEvents.ROOM_UPDATE]: RoomUpdatePayload; + [SocketEvents.GAME_START]: GameStartPayload; + [SocketEvents.CURRENT_PLAYERS]: CurrentPlayersPayload; + [SocketEvents.NEW_PLAYER]: NewPlayerPayload; + [SocketEvents.UPDATE_PLAYERS]: UpdatePlayersPayload; + [SocketEvents.REMOVE_PLAYER]: RemovePlayerPayload; + [SocketEvents.UPDATE_MAP_CELLS]: UpdateMapCellsPayload; + [SocketEvents.BOMB_PLACED]: BombPlacedPayload; + [SocketEvents.PONG]: PongPayload; + [SocketEvents.GAME_END]: undefined; + [SocketEvents.GAME_RESULT]: GameResultPayload; +}; + +/** 接続ライフサイクルイベントのペイロード型を取得するユーティリティ */ +export type ConnectionLifecyclePayloadOf = + ConnectionLifecycleEventPayloadMap[TEvent]; + +/** クライアント送信イベントのペイロード型を取得するユーティリティ */ +export type ClientToServerPayloadOf = + ClientToServerEventPayloadMap[TEvent]; + +/** サーバー送信イベントのペイロード型を取得するユーティリティ */ +export type ServerToClientPayloadOf = + ServerToClientEventPayloadMap[TEvent]; diff --git a/packages/shared/src/protocol/eventPayloads.ts b/packages/shared/src/protocol/eventPayloads.ts new file mode 100644 index 0000000..ede59e7 --- /dev/null +++ b/packages/shared/src/protocol/eventPayloads.ts @@ -0,0 +1,29 @@ +/** + * eventPayloads + * ソケットイベントで送受信するペイロード型の集約エントリ + * 機能別に分割した型を再公開して契約参照を一本化する + */ + +/** 共通イベントのペイロード型を再公開する */ +export type { PingPayload, PongPayload } from "./payloads/commonPayloads"; + +/** ロビーイベントのペイロード型を再公開する */ +export type { + JoinRoomPayload, + RoomJoinRejectedPayload, + RoomUpdatePayload, +} from "./payloads/lobbyPayloads"; + +/** ゲームイベントのペイロード型を再公開する */ +export type { + UpdatePlayersPayload, + CurrentPlayersPayload, + UpdateMapCellsPayload, + NewPlayerPayload, + RemovePlayerPayload, + GameStartPayload, + MovePayload, + BombPlacedPayload, + GameResultPayload, + GameResultRanking, +} from "./payloads/gamePayloads"; diff --git a/packages/shared/src/protocol/events.ts b/packages/shared/src/protocol/events.ts index c409c8d..4415c64 100644 --- a/packages/shared/src/protocol/events.ts +++ b/packages/shared/src/protocol/events.ts @@ -1,152 +1,37 @@ /** * events - * ソケット通信で利用するイベント名定数を定義する - * クライアントとサーバー間のイベント契約を共有する - */ -import type { TickData } from "../domains/game/game.type"; -import type { MovePayload as PlayerMovePayload, PlayerData } from "../domains/player/player.type"; -import type * as roomTypes from "../domains/room/room.type"; - -/** ソケットイベント名の一覧定数 */ -export const SocketEvents = { - // 接続・切断イベント名 - CONNECT: "connect", - DISCONNECT: "disconnect", - - // ロビー・ルーム関連イベント名 - JOIN_ROOM: "join-room", - ROOM_JOIN_REJECTED: "room-join-rejected", - ROOM_UPDATE: "room-update", - START_GAME: "start-game", - GAME_START: "game-start", - READY_FOR_GAME: "ready-for-game", - - // ゲームプレイ関連イベント名 - CURRENT_PLAYERS: "current_players", - NEW_PLAYER: "new_player", - // 1ティック分のプレイヤー状態を配列でまとめて通知する - UPDATE_PLAYERS: "update_players", - REMOVE_PLAYER: "remove_player", - MOVE: "move", - PLACE_BOMB: "place-bomb", - UPDATE_MAP_CELLS: "update_map_cells", - BOMB_PLACED: "bomb-placed", - - // 時間同期・ゲーム進行関連 - PING: "ping", // クライアントからの時刻同期リクエスト(ラグ計算用) - PONG: "pong", // サーバーからの現在時刻レスポンス - GAME_END: "game-end", // 3分経過時のゲーム終了通知 - GAME_RESULT: "game-result", // 3分終了時の最終結果通知 -} as const; - -/** - * ------------------------------------------------------------ - * ペイロード型定義 - * ------------------------------------------------------------ + * ソケット通信で利用する公開契約を再公開するエントリ + * 分割したイベント名,ペイロード,対応表,ユーティリティ型を集約する */ -/** UPDATE_PLAYERS イベントで送受信するプレイヤー差分配列 */ -export type UpdatePlayersPayload = TickData["playerUpdates"]; +/** ソケットイベント名定数を再公開する */ +export { SocketEvents } from "./socketEvents"; -/** CURRENT_PLAYERS イベントで送受信するプレイヤー一覧 */ -export type CurrentPlayersPayload = TickData["playerUpdates"]; +/** 基本ペイロード型を再公開する */ +export type { + UpdatePlayersPayload, + CurrentPlayersPayload, + UpdateMapCellsPayload, + NewPlayerPayload, + RemovePlayerPayload, + GameStartPayload, + MovePayload, + BombPlacedPayload, + PingPayload, + PongPayload, + JoinRoomPayload, + RoomJoinRejectedPayload, + RoomUpdatePayload, + GameResultPayload, + GameResultRanking, +} from "./eventPayloads"; -/** UPDATE_MAP_CELLS イベントで送受信するマップ差分配列 */ -export type UpdateMapCellsPayload = TickData["cellUpdates"]; - -/** NEW_PLAYER イベントで送受信するプレイヤー情報 */ -export type NewPlayerPayload = PlayerData; - -/** REMOVE_PLAYER イベントで送受信するプレイヤーID */ -export type RemovePlayerPayload = PlayerData["id"]; - -/** GAME_START イベントで送受信するゲーム開始情報 */ -export type GameStartPayload = { startTime: number }; - -/** MOVE イベントで送受信する移動入力情報 */ -export type MovePayload = PlayerMovePayload; - -/** PLACE_BOMB / BOMB_PLACED イベントで送受信する爆弾情報 */ -export type BombPlacedPayload = { - x: number; - y: number; - explodeAtElapsedMs: number; -}; - -/** PING イベントで送受信する時刻同期リクエスト */ -export type PingPayload = number; - -/** PONG イベントで送受信する時刻同期レスポンス */ -export type PongPayload = { - clientTime: number; - serverTime: number; -}; - -/** GAME_RESULT イベントで送受信するランキング1行 */ -export type GameResultRanking = { - rank: number; - teamId: number; - teamName: string; - paintRate: number; -}; - -/** GAME_RESULT イベントで送受信する最終結果 */ -export type GameResultPayload = { - rankings: GameResultRanking[]; -}; - -/** - * ------------------------------------------------------------ - * イベント方向ごとのペイロード対応表 - * ------------------------------------------------------------ - */ - -/** 接続ライフサイクルイベントのペイロード対応表 */ -export type ConnectionLifecycleEventPayloadMap = { - [SocketEvents.CONNECT]: undefined; - [SocketEvents.DISCONNECT]: undefined; -}; - -/** クライアントからサーバーへ送信するイベントごとのペイロード対応表 */ -export type ClientToServerEventPayloadMap = { - [SocketEvents.JOIN_ROOM]: roomTypes.JoinRoomPayload; - [SocketEvents.START_GAME]: undefined; - [SocketEvents.READY_FOR_GAME]: undefined; - [SocketEvents.MOVE]: MovePayload; - [SocketEvents.PLACE_BOMB]: BombPlacedPayload; - [SocketEvents.PING]: PingPayload; -}; - -/** サーバーからクライアントへ送信するイベントごとのペイロード対応表 */ -export type ServerToClientEventPayloadMap = { - [SocketEvents.ROOM_JOIN_REJECTED]: roomTypes.JoinRoomRejectedPayload; - [SocketEvents.ROOM_UPDATE]: roomTypes.Room; - [SocketEvents.GAME_START]: GameStartPayload; - [SocketEvents.CURRENT_PLAYERS]: CurrentPlayersPayload; - [SocketEvents.NEW_PLAYER]: NewPlayerPayload; - [SocketEvents.UPDATE_PLAYERS]: UpdatePlayersPayload; - [SocketEvents.REMOVE_PLAYER]: RemovePlayerPayload; - [SocketEvents.UPDATE_MAP_CELLS]: UpdateMapCellsPayload; - [SocketEvents.BOMB_PLACED]: BombPlacedPayload; - [SocketEvents.PONG]: PongPayload; - [SocketEvents.GAME_END]: undefined; - [SocketEvents.GAME_RESULT]: GameResultPayload; -}; - -/** - * ------------------------------------------------------------ - * イベント名からペイロード型を引くユーティリティ - * ------------------------------------------------------------ - */ - -/** 接続ライフサイクルイベントのペイロード型を取得するユーティリティ */ -export type ConnectionLifecyclePayloadOf = - ConnectionLifecycleEventPayloadMap[TEvent]; - -/** クライアント送信イベントのペイロード型を取得するユーティリティ */ -export type ClientToServerPayloadOf = - ClientToServerEventPayloadMap[TEvent]; - -/** サーバー送信イベントのペイロード型を取得するユーティリティ */ -export type ServerToClientPayloadOf = - ServerToClientEventPayloadMap[TEvent]; +/** イベント方向ごとのペイロード対応表とユーティリティ型を再公開する */ +export type { + ConnectionLifecycleEventPayloadMap, + ConnectionLifecyclePayloadOf, + ClientToServerEventPayloadMap, + ClientToServerPayloadOf, + ServerToClientEventPayloadMap, + ServerToClientPayloadOf, +} from "./eventPayloadMaps"; diff --git a/packages/shared/src/protocol/payloads/commonPayloads.ts b/packages/shared/src/protocol/payloads/commonPayloads.ts new file mode 100644 index 0000000..a3f4d73 --- /dev/null +++ b/packages/shared/src/protocol/payloads/commonPayloads.ts @@ -0,0 +1,14 @@ +/** + * commonPayloads + * 接続と時刻同期で利用する共通ペイロード型を定義する + * ソケット通信の基盤イベントで使う契約を集約する + */ + +/** PING イベントで送受信する時刻同期リクエスト */ +export type PingPayload = number; + +/** PONG イベントで送受信する時刻同期レスポンス */ +export type PongPayload = { + clientTime: number; + serverTime: number; +}; diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts new file mode 100644 index 0000000..54c8396 --- /dev/null +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -0,0 +1,48 @@ +/** + * gamePayloads + * ゲーム進行イベントで利用するペイロード型を定義する + * プレイヤー差分,マップ差分,開始終了系の契約を集約する + */ +import type { TickData } from "../../domains/game/game.type"; +import type { MovePayload as PlayerMovePayload, PlayerData } from "../../domains/player/player.type"; + +/** GAME_RESULT イベントで送受信するランキング1行 */ +export type GameResultRanking = { + rank: number; + teamId: number; + teamName: string; + paintRate: number; +}; + +/** GAME_RESULT イベントで送受信する最終結果 */ +export type GameResultPayload = { + rankings: GameResultRanking[]; +}; + +/** UPDATE_PLAYERS イベントで送受信するプレイヤー差分配列 */ +export type UpdatePlayersPayload = TickData["playerUpdates"]; + +/** CURRENT_PLAYERS イベントで送受信するプレイヤー一覧 */ +export type CurrentPlayersPayload = TickData["playerUpdates"]; + +/** UPDATE_MAP_CELLS イベントで送受信するマップ差分配列 */ +export type UpdateMapCellsPayload = TickData["cellUpdates"]; + +/** NEW_PLAYER イベントで送受信するプレイヤー情報 */ +export type NewPlayerPayload = PlayerData; + +/** REMOVE_PLAYER イベントで送受信するプレイヤーID */ +export type RemovePlayerPayload = PlayerData["id"]; + +/** GAME_START イベントで送受信するゲーム開始情報 */ +export type GameStartPayload = { startTime: number }; + +/** MOVE イベントで送受信する移動入力情報 */ +export type MovePayload = PlayerMovePayload; + +/** PLACE_BOMB / BOMB_PLACED イベントで送受信する爆弾情報 */ +export type BombPlacedPayload = { + x: number; + y: number; + explodeAtElapsedMs: number; +}; diff --git a/packages/shared/src/protocol/payloads/lobbyPayloads.ts b/packages/shared/src/protocol/payloads/lobbyPayloads.ts new file mode 100644 index 0000000..3f5cdcb --- /dev/null +++ b/packages/shared/src/protocol/payloads/lobbyPayloads.ts @@ -0,0 +1,15 @@ +/** + * lobbyPayloads + * ロビー関連イベントで利用するペイロード型を定義する + * ルーム参加前後の契約を共有する + */ +import type * as roomTypes from "../../domains/room/room.type"; + +/** JOIN_ROOM イベントで送受信するルーム参加情報 */ +export type JoinRoomPayload = roomTypes.JoinRoomPayload; + +/** ROOM_JOIN_REJECTED イベントで送受信する参加拒否情報 */ +export type RoomJoinRejectedPayload = roomTypes.JoinRoomRejectedPayload; + +/** ROOM_UPDATE イベントで送受信するルーム状態情報 */ +export type RoomUpdatePayload = roomTypes.Room; diff --git a/packages/shared/src/protocol/socketEvents.ts b/packages/shared/src/protocol/socketEvents.ts new file mode 100644 index 0000000..2517acf --- /dev/null +++ b/packages/shared/src/protocol/socketEvents.ts @@ -0,0 +1,36 @@ +/** + * socketEvents + * ソケット通信で利用するイベント名定数を定義する + * クライアントとサーバー間で共通利用する契約名を集約する + */ + +/** ソケットイベント名の一覧定数 */ +export const SocketEvents = { + // 接続・切断イベント名 + CONNECT: "connect", + DISCONNECT: "disconnect", + + // ロビー・ルーム関連イベント名 + JOIN_ROOM: "join-room", + ROOM_JOIN_REJECTED: "room-join-rejected", + ROOM_UPDATE: "room-update", + START_GAME: "start-game", + GAME_START: "game-start", + READY_FOR_GAME: "ready-for-game", + + // ゲームプレイ関連イベント名 + CURRENT_PLAYERS: "current_players", + NEW_PLAYER: "new_player", + UPDATE_PLAYERS: "update_players", + REMOVE_PLAYER: "remove_player", + MOVE: "move", + PLACE_BOMB: "place-bomb", + UPDATE_MAP_CELLS: "update_map_cells", + BOMB_PLACED: "bomb-placed", + + // 時間同期・ゲーム進行関連 + PING: "ping", + PONG: "pong", + GAME_END: "game-end", + GAME_RESULT: "game-result", +} as const;