diff --git a/apps/server/src/application/coordinators/disconnectCoordinator.ts b/apps/server/src/application/coordinators/disconnectCoordinator.ts new file mode 100644 index 0000000..dcb2d76 --- /dev/null +++ b/apps/server/src/application/coordinators/disconnectCoordinator.ts @@ -0,0 +1,48 @@ +/** + * disconnectCoordinator + * DISCONNECTイベントの調停を行い,ゲーム離脱処理とルーム離脱処理を順序実行する + */ +import { + type DisconnectPlayerPort, + type GameOutputPort, +} from "@server/domains/game/application/ports/gameUseCasePorts"; +import type { + DisconnectRoomPort, + FindRoomByPlayerPort, + RoomOutputPort, +} from "@server/domains/room/application/ports/roomUseCasePorts"; +import { disconnectUseCase } from "@server/domains/game/application/useCases/disconnectUseCase"; +import { roomDisconnectUseCase } from "@server/domains/room/application/useCases/roomDisconnectUseCase"; + +/** 切断調停で利用する入力ポートと出力ポートの契約 */ +export type DisconnectCoordinatorParams = { + socketId: string; + gameManager: DisconnectPlayerPort; + roomManager: DisconnectRoomPort & FindRoomByPlayerPort; + gameOutput: Pick; + roomOutput: Pick; +}; + +/** 切断時にゲーム処理とルーム処理を調停し,一貫した離脱処理を実行する */ +export const disconnectCoordinator = ({ + socketId, + gameManager, + roomManager, + gameOutput, + roomOutput, +}: DisconnectCoordinatorParams) => { + const roomId = roomManager.getRoomByPlayerId(socketId)?.roomId; + + disconnectUseCase({ + gameManager, + roomId, + playerId: socketId, + output: gameOutput, + }); + + roomDisconnectUseCase({ + roomManager, + socketId, + output: roomOutput, + }); +}; diff --git a/apps/server/src/application/coordinators/readyForGameCoordinator.ts b/apps/server/src/application/coordinators/readyForGameCoordinator.ts new file mode 100644 index 0000000..407dc01 --- /dev/null +++ b/apps/server/src/application/coordinators/readyForGameCoordinator.ts @@ -0,0 +1,34 @@ +/** + * readyForGameCoordinator + * READY_FOR_GAMEイベントの調停を行い,所属ルーム解決と準備状態通知を橋渡しする + */ +import { + type GameOutputPort, + type ReadyForGamePort, + type ReadyForGameRoomPort, +} from "@server/domains/game/application/ports/gameUseCasePorts"; +import { readyForGameUseCase } from "@server/domains/game/application/useCases/readyForGameUseCase"; + +type ReadyForGameCoordinatorParams = { + socketId: string; + gameManager: ReadyForGamePort; + roomManager: ReadyForGameRoomPort; + output: Pick; +}; + +/** READY_FOR_GAME受信時に所属ルームを解決し,準備状態ユースケースを実行する */ +export const readyForGameCoordinator = ({ + socketId, + gameManager, + roomManager, + output, +}: ReadyForGameCoordinatorParams) => { + const room = roomManager.getRoomByPlayerId(socketId); + + readyForGameUseCase({ + socketId, + roomId: room?.roomId, + gameManager, + output, + }); +}; diff --git a/apps/server/src/application/coordinators/startGameCoordinator.ts b/apps/server/src/application/coordinators/startGameCoordinator.ts new file mode 100644 index 0000000..87220ce --- /dev/null +++ b/apps/server/src/application/coordinators/startGameCoordinator.ts @@ -0,0 +1,84 @@ +/** + * startGameCoordinator + * START_GAMEイベントの調停を行い,ルーム状態更新とゲーム開始処理を橋渡しする + */ +import { + type GameOutputPort, + type StartGamePort, + type StartGameRoomPort, +} from "@server/domains/game/application/ports/gameUseCasePorts"; +import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; +import { logEvent } from "@server/logging/logEvent"; +import { roomConsts } from "@repo/shared"; + +type StartGameCoordinatorParams = { + ownerId: string; + gameManager: StartGamePort; + roomManager: StartGameRoomPort; + output: Pick< + GameOutputPort, + | "publishUpdatePlayerToRoom" + | "publishMapCellUpdatesToRoom" + | "publishGameEndToRoom" + | "publishGameStartToRoom" + >; +}; + +/** START_GAME受信時にルーム状態遷移を判定し,ゲーム開始ユースケースを実行する */ +export const startGameCoordinator = ({ + ownerId, + gameManager, + roomManager, + output, +}: StartGameCoordinatorParams) => { + const room = roomManager.getRoomByOwnerId(ownerId); + if (!room) { + logEvent("GameUseCase", { + event: "START_GAME", + result: "ignored_no_room", + socketId: ownerId, + }); + return; + } + + if (room.status === roomConsts.RoomPhase.PLAYING) { + logEvent("GameUseCase", { + event: "START_GAME", + result: "ignored_already_playing", + roomId: room.roomId, + socketId: ownerId, + }); + return; + } + + const updatedRoom = roomManager.markRoomPlaying(room.roomId); + if (!updatedRoom) { + logEvent("GameUseCase", { + event: "START_GAME", + result: "ignored_room_not_found", + roomId: room.roomId, + socketId: ownerId, + }); + return; + } + + logEvent("GameUseCase", { + event: "START_GAME", + result: "accepted", + roomId: updatedRoom.roomId, + socketId: ownerId, + totalPlayers: updatedRoom.players.length, + }); + + const playerIds = updatedRoom.players.map((player) => player.id); + + startGameUseCase({ + roomId: updatedRoom.roomId, + playerIds, + gameManager, + onGameEnd: () => { + roomManager.markRoomWaiting(updatedRoom.roomId); + }, + output, + }); +}; diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index 118abb7..c6f5a6f 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -1,3 +1,7 @@ +/** + * GameManager + * ゲームセッション集合の生成,更新,参照管理を統括する + */ import { type TickData } from "./loop/GameLoop"; import { Player } from "./entities/player/Player.js"; import { GameRoomSession } from "./application/services/GameRoomSession"; @@ -5,6 +9,7 @@ import { GamePlayerOperationService } from "./application/services/GamePlayerOperationService"; // プレイヤー集合の生成・更新・参照管理クラス +/** ゲームセッションのライフサイクルとプレイヤー操作を統括するマネージャ */ export class GameManager { private sessions: Map; private playerToRoom: Map; diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index 659e636..c3f8e0f 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -1,6 +1,11 @@ +/** + * gameUseCasePorts + * ゲーム系ユースケースが利用する入力ポートと出力ポートの契約を定義する + */ import type { TickData } from "../../loop/GameLoop"; import type { gridMapTypes, playerTypes, roomTypes } from "@repo/shared"; +/** ゲーム開始ユースケースが利用するゲーム管理入力ポート */ export interface StartGamePort { startRoomSession( roomId: string, @@ -11,19 +16,35 @@ getRoomStartTime(roomId: string): number | undefined; } +/** ゲーム開始調停で利用するルーム管理入力ポート */ +export interface StartGameRoomPort { + getRoomByOwnerId(ownerId: string): roomTypes.Room | undefined; + markRoomPlaying(roomId: string): roomTypes.Room | undefined; + markRoomWaiting(roomId: string): roomTypes.Room | undefined; +} + +/** 準備完了調停で利用するルーム解決入力ポート */ +export interface ReadyForGameRoomPort { + getRoomByPlayerId(playerId: string): roomTypes.Room | undefined; +} + +/** 準備完了ユースケースが利用するゲーム状態参照入力ポート */ export interface ReadyForGamePort { getRoomPlayers(roomId: string): playerTypes.PlayerData[]; getRoomStartTime(roomId: string): number | undefined; } +/** 移動入力ユースケースが利用するプレイヤー操作入力ポート */ export interface MovePlayerPort { movePlayer(id: string, x: number, y: number): void; } +/** 切断ユースケースが利用するプレイヤー削除入力ポート */ export interface DisconnectPlayerPort { removePlayer(id: string): void; } +/** ゲーム系ユースケースが利用する送信出力ポート */ export interface GameOutputPort { publishPongToSocket(payload: { clientTime: number; serverTime: number }): void; publishUpdatePlayerToRoom(roomId: roomTypes.Room["roomId"], playerData: playerTypes.PlayerData): void; diff --git a/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts b/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts index 9c039c7..e6af25b 100644 --- a/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts +++ b/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts @@ -1,9 +1,14 @@ +/** + * GamePlayerOperationService + * ゲームセッション内のプレイヤー移動と離脱操作を管理する + */ import { logEvent } from "@server/logging/logEvent"; import { GameRoomSession } from "./GameRoomSession"; type SessionStore = Map; type PlayerRoomIndex = Map; +/** プレイヤー移動とセッション離脱処理を提供するサービス */ export class GamePlayerOperationService { constructor( private sessions: SessionStore, diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts index 4ac08b7..4e31055 100644 --- a/apps/server/src/domains/game/application/services/GameRoomSession.ts +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -1,3 +1,7 @@ +/** + * GameRoomSession + * 1ルーム分のゲーム進行状態とゲームループ実行を管理する + */ import { logEvent } from "@server/logging/logEvent"; import { GameLoop, type TickData } from "../../loop/GameLoop"; import { Player } from "../../entities/player/Player.js"; @@ -8,6 +12,7 @@ setPlayerPosition, } from "../../entities/player/playerMovement.js"; +/** ルーム単位のゲーム状態とループ進行を保持するセッションクラス */ export class GameRoomSession { private players: Map; private mapStore: MapStore; diff --git a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts index 333d259..0b22870 100644 --- a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts +++ b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts @@ -1,3 +1,7 @@ +/** + * GameSessionLifecycleService + * ゲームセッションの開始,参照,終了時クリーンアップを管理する + */ import { config } from "@repo/shared"; import { type TickData } from "../../loop/GameLoop"; import { logEvent } from "@server/logging/logEvent"; @@ -6,6 +10,7 @@ type SessionStore = Map; type PlayerRoomIndex = Map; +/** ゲームセッションのライフサイクル操作を提供するサービス */ export class GameSessionLifecycleService { constructor( private sessions: SessionStore, diff --git a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts index 865ec74..bb6ca19 100644 --- a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts @@ -1,3 +1,7 @@ +/** + * disconnectUseCase + * 切断したプレイヤーをゲーム状態から除外し,必要に応じて通知を行う + */ import type { DisconnectPlayerPort, GameOutputPort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; @@ -8,6 +12,7 @@ output: Pick; }; +/** プレイヤー切断時の状態更新と通知を実行する */ export const disconnectUseCase = ({ gameManager, roomId, diff --git a/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts b/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts index 9bcece1..400b1ae 100644 --- a/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/movePlayerUseCase.ts @@ -1,3 +1,7 @@ +/** + * movePlayerUseCase + * プレイヤー移動入力を受け取り,ゲーム管理へ反映する + */ import type { playerTypes } from "@repo/shared"; import type { MovePlayerPort } from "../ports/gameUseCasePorts"; @@ -7,6 +11,7 @@ move: playerTypes.MovePayload; }; +/** プレイヤー移動入力をゲーム管理へ委譲する */ export const movePlayerUseCase = ({ gameManager, playerId, diff --git a/apps/server/src/domains/game/application/useCases/pingUseCase.ts b/apps/server/src/domains/game/application/useCases/pingUseCase.ts index 42e8fbd..c357d66 100644 --- a/apps/server/src/domains/game/application/useCases/pingUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/pingUseCase.ts @@ -1,3 +1,7 @@ +/** + * pingUseCase + * PING受信時に時刻情報付きPONGを返して遅延計測を支援する + */ import type { GameOutputPort } from "../ports/gameUseCasePorts"; type PingUseCaseParams = { @@ -5,6 +9,7 @@ output: Pick; }; +/** クライアント時刻を受け取りサーバー時刻付きで応答する */ export const pingUseCase = ({ clientTime, output, diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index f8a72c0..f4696ad 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -1,3 +1,7 @@ +/** + * readyForGameUseCase + * READY_FOR_GAME受信時に現在プレイヤー状態と開始時刻を返却する + */ import type { ReadyForGamePort } from "../ports/gameUseCasePorts"; import type { GameOutputPort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; @@ -9,6 +13,7 @@ output: Pick; }; +/** 準備完了通知に対してルーム状態を返却し,開始済みなら開始時刻も通知する */ export const readyForGameUseCase = ({ socketId, roomId, diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index e492412..f4075f0 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -1,12 +1,15 @@ -import { roomConsts } from "@repo/shared"; -import { RoomManager } from "@server/domains/room/RoomManager"; +/** + * startGameUseCase + * ルーム内プレイヤーでゲームセッションを開始し,進行イベントを通知する + */ import type { GameOutputPort, StartGamePort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; type StartGameUseCaseParams = { - ownerId: string; + roomId: string; + playerIds: string[]; gameManager: StartGamePort; - roomManager: RoomManager; + onGameEnd: () => void; output: Pick< GameOutputPort, | "publishUpdatePlayerToRoom" @@ -16,68 +19,38 @@ >; }; +/** ゲームセッション開始とティック通知,終了通知を実行する */ export const startGameUseCase = ({ - ownerId, + roomId, + playerIds, gameManager, - roomManager, + onGameEnd, output, }: StartGameUseCaseParams) => { - const room = roomManager.getRoomByOwnerId(ownerId); - if (!room) { - logEvent("GameUseCase", { - event: "START_GAME", - result: "ignored_no_room", - socketId: ownerId, - }); - return; - } - - if (room.status === roomConsts.RoomPhase.PLAYING) { - logEvent("GameUseCase", { - event: "START_GAME", - result: "ignored_already_playing", - roomId: room.roomId, - socketId: ownerId, - }); - return; - } - - logEvent("GameUseCase", { - event: "START_GAME", - result: "accepted", - roomId: room.roomId, - socketId: ownerId, - totalPlayers: room.players.length, - }); - - room.status = roomConsts.RoomPhase.PLAYING; - - const playerIds = room.players.map((p: { id: string }) => p.id); - gameManager.startRoomSession( - room.roomId, + roomId, playerIds, (tickData) => { tickData.players.forEach((playerData) => { - output.publishUpdatePlayerToRoom(room.roomId, playerData); + output.publishUpdatePlayerToRoom(roomId, playerData); }); if (tickData.cellUpdates.length > 0) { - output.publishMapCellUpdatesToRoom(room.roomId, tickData.cellUpdates); + output.publishMapCellUpdatesToRoom(roomId, tickData.cellUpdates); } }, () => { logEvent("GameUseCase", { event: "GAME_END", result: "emitted", - roomId: room.roomId, + roomId, reason: "duration_elapsed", }); - output.publishGameEndToRoom(room.roomId); - room.status = roomConsts.RoomPhase.WAITING; + output.publishGameEndToRoom(roomId); + onGameEnd(); } ); - const startTime = gameManager.getRoomStartTime(room.roomId) || Date.now(); - output.publishGameStartToRoom(room.roomId, { startTime }); + const startTime = gameManager.getRoomStartTime(roomId) || Date.now(); + output.publishGameStartToRoom(roomId, { startTime }); }; diff --git a/apps/server/src/domains/game/entities/map/MapStore.ts b/apps/server/src/domains/game/entities/map/MapStore.ts index 15b03f8..cd244c9 100644 --- a/apps/server/src/domains/game/entities/map/MapStore.ts +++ b/apps/server/src/domains/game/entities/map/MapStore.ts @@ -1,9 +1,13 @@ -// apps/server/src/domains/game/entities/map/MapStore.ts +/** + * MapStore + * 塗り状態グリッドと差分更新キューを保持して提供する + */ import type { gridMapTypes } from "@repo/shared"; import { createInitialGridColors } from "./mapGrid.js"; import { paintCellIfChanged } from "./mapPainting.js"; import { drainPendingUpdates } from "./mapUpdates.js"; +/** ルーム内マップの塗り状態と更新差分を管理するストア */ export class MapStore { // 全マスの現在の色(teamId)を保持 private gridColors: number[]; @@ -17,7 +21,7 @@ } /** - * マスを塗り、色が変化した場合のみ差分キューに追加する + * マスを塗り,色が変化した場合のみ差分キューに追加する */ public paintCell(index: number, teamId: number): void { paintCellIfChanged({ @@ -29,7 +33,7 @@ } /** - * 溜まっている差分を取得し、キューをクリアする(ループ送信時に使用) + * 溜まっている差分を取得し,キューをクリアする(ループ送信時に使用) */ public getAndClearUpdates(): gridMapTypes.CellUpdate[] { return drainPendingUpdates(this.pendingUpdates); diff --git a/apps/server/src/domains/game/entities/map/mapGrid.ts b/apps/server/src/domains/game/entities/map/mapGrid.ts index c08cc21..f230acf 100644 --- a/apps/server/src/domains/game/entities/map/mapGrid.ts +++ b/apps/server/src/domains/game/entities/map/mapGrid.ts @@ -1,5 +1,10 @@ +/** + * mapGrid + * マップ配列の初期状態を生成する + */ import { config } from "@repo/shared"; +/** マップ全セルを未塗り状態で初期化した配列を返す */ export const createInitialGridColors = (): number[] => { const totalCells = config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS; return new Array(totalCells).fill(-1); diff --git a/apps/server/src/domains/game/entities/map/mapPainting.ts b/apps/server/src/domains/game/entities/map/mapPainting.ts index c03eab1..8ffe9fb 100644 --- a/apps/server/src/domains/game/entities/map/mapPainting.ts +++ b/apps/server/src/domains/game/entities/map/mapPainting.ts @@ -1,3 +1,7 @@ +/** + * mapPainting + * マップセルの塗り更新と差分追加処理を提供する + */ import type { gridMapTypes } from "@repo/shared"; type PaintCellParams = { @@ -7,6 +11,7 @@ teamId: number; }; +/** マップセルの色が変わった場合のみ差分へ追加する */ export const paintCellIfChanged = ({ gridColors, pendingUpdates, diff --git a/apps/server/src/domains/game/entities/map/mapUpdates.ts b/apps/server/src/domains/game/entities/map/mapUpdates.ts index 091ed0b..9c51618 100644 --- a/apps/server/src/domains/game/entities/map/mapUpdates.ts +++ b/apps/server/src/domains/game/entities/map/mapUpdates.ts @@ -1,5 +1,10 @@ +/** + * mapUpdates + * マップ差分キューの取り出しとクリア処理を提供する + */ import type { gridMapTypes } from "@repo/shared"; +/** 差分キューを配列として返却し,キューを空にする */ export const drainPendingUpdates = ( pendingUpdates: gridMapTypes.CellUpdate[] ): gridMapTypes.CellUpdate[] => { diff --git a/apps/server/src/domains/game/entities/player/Player.ts b/apps/server/src/domains/game/entities/player/Player.ts index 6e16617..68a033e 100644 --- a/apps/server/src/domains/game/entities/player/Player.ts +++ b/apps/server/src/domains/game/entities/player/Player.ts @@ -1,7 +1,12 @@ +/** + * Player + * サーバー側で保持するプレイヤー状態モデルを定義する + */ import type { playerTypes } from "@repo/shared"; import { config } from "@repo/shared"; // サーバー側保持プレイヤー状態モデル +/** サーバー側プレイヤー座標と所属チームを保持するエンティティ */ export class Player implements playerTypes.PlayerData { public id: string; public x: number = 0; diff --git a/apps/server/src/domains/game/entities/player/playerMovement.ts b/apps/server/src/domains/game/entities/player/playerMovement.ts index b7cd590..a53a88b 100644 --- a/apps/server/src/domains/game/entities/player/playerMovement.ts +++ b/apps/server/src/domains/game/entities/player/playerMovement.ts @@ -1,9 +1,15 @@ +/** + * playerMovement + * プレイヤー座標の検証と更新処理を提供する + */ import { Player } from "./Player.js"; +/** 座標値が有限数かを判定する */ export const isValidPosition = (x: number, y: number): boolean => { return Number.isFinite(x) && Number.isFinite(y); }; +/** プレイヤー座標を新しい値へ更新する */ export const setPlayerPosition = ( player: Player, x: number, diff --git a/apps/server/src/domains/game/entities/player/playerPosition.ts b/apps/server/src/domains/game/entities/player/playerPosition.ts index f0c825a..0a2112c 100644 --- a/apps/server/src/domains/game/entities/player/playerPosition.ts +++ b/apps/server/src/domains/game/entities/player/playerPosition.ts @@ -1,6 +1,11 @@ +/** + * playerPosition + * プレイヤー座標からマップ上のセル位置を解決する + */ import { gridMapLogic } from "@repo/shared"; import { Player } from "./Player.js"; +/** プレイヤー座標に対応するグリッドインデックスを返す */ export const getPlayerGridIndex = (player: Player): number | null => { return gridMapLogic.getGridIndexFromPosition(player.x, player.y); }; diff --git a/apps/server/src/domains/game/entities/player/playerSpawn.ts b/apps/server/src/domains/game/entities/player/playerSpawn.ts index 064a690..c09e4ae 100644 --- a/apps/server/src/domains/game/entities/player/playerSpawn.ts +++ b/apps/server/src/domains/game/entities/player/playerSpawn.ts @@ -1,6 +1,11 @@ +/** + * playerSpawn + * プレイヤー初期生成時のスポーン座標設定を提供する + */ import { config } from "@repo/shared"; import { Player } from "./Player.js"; +/** プレイヤーを生成し,初期スポーン座標を設定して返す */ export const createSpawnedPlayer = (id: string): Player => { const player = new Player(id); player.x = config.GAME_CONFIG.GRID_COLS / 2; diff --git a/apps/server/src/domains/game/loop/GameLoop.ts b/apps/server/src/domains/game/loop/GameLoop.ts index de3617c..c5a9f9e 100644 --- a/apps/server/src/domains/game/loop/GameLoop.ts +++ b/apps/server/src/domains/game/loop/GameLoop.ts @@ -1,3 +1,7 @@ +/** + * GameLoop + * ルーム単位の定周期更新を実行し,プレイヤー状態とマップ差分を集約する + */ import { Player } from "../entities/player/Player.js"; import { MapStore } from "../entities/map/MapStore"; import { getPlayerGridIndex } from "../entities/player/playerPosition.js"; @@ -6,6 +10,7 @@ import { logEvent } from "@server/logging/logEvent"; // コールバックで渡すデータの型定義 +/** 1ティック分のプレイヤー情報とマップ差分を表すデータ */ export interface TickData { players: { id: string; @@ -16,6 +21,7 @@ cellUpdates: gridMapTypes.CellUpdate[]; } +/** ルーム内ゲーム進行を定周期で実行するループ管理クラス */ export class GameLoop { private loopId: NodeJS.Timeout | null = null; private startTime: number = 0; diff --git a/apps/server/src/domains/room/RoomManager.ts b/apps/server/src/domains/room/RoomManager.ts index 4197846..47a86ed 100644 --- a/apps/server/src/domains/room/RoomManager.ts +++ b/apps/server/src/domains/room/RoomManager.ts @@ -3,6 +3,7 @@ * ルーム状態の保持とルーム操作サービスへの委譲を担うマネージャ */ import type { roomTypes } from "@repo/shared"; +import { roomConsts } from "@repo/shared"; import { RoomJoinService } from "./application/services/RoomJoinService"; import { RoomExitService } from "./application/services/RoomExitService"; import { RoomQueryService } from "./application/services/RoomQueryService"; @@ -45,4 +46,26 @@ public getRoomByPlayerId(playerId: string): roomTypes.Room | undefined { return this.roomQueryService.getRoomByPlayerId(playerId); } + + // ルーム状態をPLAYINGへ更新する + public markRoomPlaying(roomId: string): roomTypes.Room | undefined { + const room = this.roomQueryService.getRoomById(roomId); + if (!room) { + return undefined; + } + + room.status = roomConsts.RoomPhase.PLAYING; + return room; + } + + // ルーム状態をWAITINGへ更新する + public markRoomWaiting(roomId: string): roomTypes.Room | undefined { + const room = this.roomQueryService.getRoomById(roomId); + if (!room) { + return undefined; + } + + room.status = roomConsts.RoomPhase.WAITING; + return room; + } } \ No newline at end of file diff --git a/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts b/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts index defacee..c0e5a7b 100644 --- a/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts +++ b/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts @@ -25,3 +25,8 @@ export interface DisconnectRoomPort { removePlayer(socketId: string): roomTypes.Room[]; } + +/** 切断調停で利用するプレイヤー所属ルーム参照ポート */ +export interface FindRoomByPlayerPort { + getRoomByPlayerId(playerId: string): roomTypes.Room | undefined; +} diff --git a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts index 8ab6699..4f9a1fd 100644 --- a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts +++ b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts @@ -14,10 +14,10 @@ roomManager: JoinRoomPort; socketId: string; data: roomTypes.JoinRoomPayload; - output: RoomOutputPort; + output: Pick; }; -/** 参加イベントを受け取り,ルーム更新を配信する */ +/** 参加イベントを受け取り,参加可否を判定する */ export const joinRoomUseCase = ({ roomManager, socketId, @@ -50,7 +50,5 @@ return joinResult; } - output.publishRoomUpdateToRoom(roomId, joinResult.room); - return joinResult; }; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 77f2750..f7138f6 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,3 +1,7 @@ +/** + * index + * サーバー起動時にHTTPサーバー生成とブート処理を実行する + */ import { createHttpServer } from "./network/bootstrap/createHttpServer"; import { boot } from "./network/bootstrap/boot"; import { config } from "@repo/shared"; diff --git a/apps/server/src/logging/logEvent.ts b/apps/server/src/logging/logEvent.ts index 626324c..c521ab8 100644 --- a/apps/server/src/logging/logEvent.ts +++ b/apps/server/src/logging/logEvent.ts @@ -1,3 +1,7 @@ +/** + * logEvent + * 共通ログ出力で利用するイベントログ関数を提供する + */ type LogEventPayload = { event: string; result: string; @@ -6,6 +10,7 @@ [key: string]: unknown; }; +/** スコープ名とイベント情報を標準出力へ記録する */ export const logEvent = (scope: string, payload: LogEventPayload) => { console.log(`[${scope}]`, payload); }; diff --git a/apps/server/src/network/SocketManager.ts b/apps/server/src/network/SocketManager.ts index 39372ed..279a30c 100644 --- a/apps/server/src/network/SocketManager.ts +++ b/apps/server/src/network/SocketManager.ts @@ -3,28 +3,35 @@ * Socket.IO接続ハンドラの登録を初期化するマネージャ */ import { Server } from "socket.io"; -import { GameManager } from "@server/domains/game/GameManager"; -import { RoomManager } from "@server/domains/room/RoomManager"; +import type { + SocketConnectionManagerBundle, + SocketConnectionGamePort, + SocketConnectionRoomPort, +} from "./types/connectionPorts"; import { registerConnectionHandlers } from "./handlers/registerConnectionHandlers"; /** Socket.IOの接続ハンドラ登録を統括する */ export class SocketManager { private io: Server; - private gameManager: GameManager; - private roomManager: RoomManager; + private managers: SocketConnectionManagerBundle; - constructor(io: Server, gameManager: GameManager, roomManager: RoomManager) { + constructor( + io: Server, + gameManager: SocketConnectionGamePort, + roomManager: SocketConnectionRoomPort + ) { this.io = io; - this.gameManager = gameManager; - this.roomManager = roomManager; + this.managers = { + gameManager, + roomManager, + }; } public initialize() { // 接続時に必要な各ドメインハンドラを登録する registerConnectionHandlers({ io: this.io, - gameManager: this.gameManager, - roomManager: this.roomManager, + ...this.managers, }); } } \ No newline at end of file diff --git a/apps/server/src/network/handlers/GameHandler.ts b/apps/server/src/network/handlers/GameHandler.ts index 3cefc15..c2be16e 100644 --- a/apps/server/src/network/handlers/GameHandler.ts +++ b/apps/server/src/network/handlers/GameHandler.ts @@ -4,5 +4,3 @@ */ /** ゲームイベント受信ハンドラ登録関数を再公開する */ export { registerGameHandlers } from "./game/registerGameHandlers"; -/** ゲーム切断処理ハンドラを再公開する */ -export { handleGameDisconnect } from "./game/handleGameDisconnect"; diff --git a/apps/server/src/network/handlers/RoomHandler.ts b/apps/server/src/network/handlers/RoomHandler.ts index 4ff4894..9710aaa 100644 --- a/apps/server/src/network/handlers/RoomHandler.ts +++ b/apps/server/src/network/handlers/RoomHandler.ts @@ -4,5 +4,3 @@ */ /** ルームイベント受信ハンドラ登録関数を再公開する */ export { registerRoomHandlers } from "./room/registerRoomHandlers"; -/** ルーム切断処理ハンドラを再公開する */ -export { handleRoomDisconnect } from "./room/handleRoomDisconnect"; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index 3af44f2..98d1c63 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -5,8 +5,8 @@ import { Server } from "socket.io"; import { protocol } from "@repo/shared"; import type { gridMapTypes, playerTypes, roomTypes } from "@repo/shared"; -import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; import type { GameOutputPort } from "@server/domains/game/application/ports/gameUseCasePorts"; +import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; import type { CommonHandlerContext } from "../CommonHandler"; type RoomId = roomTypes.Room["roomId"]; diff --git a/apps/server/src/network/handlers/game/handleGameDisconnect.ts b/apps/server/src/network/handlers/game/handleGameDisconnect.ts deleted file mode 100644 index 77af1c8..0000000 --- a/apps/server/src/network/handlers/game/handleGameDisconnect.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * handleGameDisconnect - * ゲーム切断ユースケースを呼び出してプレイヤー離脱を配信する - */ -import { Server } from "socket.io"; -import { GameManager } from "@server/domains/game/GameManager"; -import { disconnectUseCase } from "@server/domains/game/application/useCases/disconnectUseCase"; -import { createGameDisconnectOutputAdapter } from "./createGameOutputAdapter"; - -/** 切断したプレイヤーをゲーム管理から除外し通知する */ -export const handleGameDisconnect = ( - io: Server, - gameManager: GameManager, - roomId: string | undefined, - playerId: string -) => { - const gameDisconnectOutputAdapter = createGameDisconnectOutputAdapter(io); - - disconnectUseCase({ - gameManager, - roomId, - playerId, - output: gameDisconnectOutputAdapter, - }); -}; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index f64b1b0..8b6ada2 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -3,24 +3,29 @@ * ゲーム関連イベントの受信ハンドラを登録する */ import { Server, Socket } from "socket.io"; -import { GameManager } from "@server/domains/game/GameManager"; -import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; -import { pingUseCase } from "@server/domains/game/application/useCases/pingUseCase"; -import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; -import { readyForGameUseCase } from "@server/domains/game/application/useCases/readyForGameUseCase"; +import { readyForGameCoordinator } from "@server/application/coordinators/readyForGameCoordinator"; +import { startGameCoordinator } from "@server/application/coordinators/startGameCoordinator"; +import type { + MovePlayerPort, + ReadyForGamePort, + ReadyForGameRoomPort, + StartGamePort, + StartGameRoomPort, +} from "@server/domains/game/application/ports/gameUseCasePorts"; import { movePlayerUseCase } from "@server/domains/game/application/useCases/movePlayerUseCase"; -import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; -import { createGameOutputAdapter } from "./createGameOutputAdapter"; +import { pingUseCase } from "@server/domains/game/application/useCases/pingUseCase"; import { logEvent } from "@server/logging/logEvent"; +import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; import { isMovePayload, isPingPayload } from "@server/network/validation/socketPayloadValidators"; +import { createGameOutputAdapter } from "./createGameOutputAdapter"; /** ゲームイベントの購読とユースケース呼び出しを設定する */ export const registerGameHandlers = ( io: Server, socket: Socket, - gameManager: GameManager, - roomManager: RoomManager + gameManager: StartGamePort & ReadyForGamePort & MovePlayerPort, + roomManager: StartGameRoomPort & ReadyForGameRoomPort ) => { const common = createCommonHandlerContext(io, socket); const gameOutputAdapter = createGameOutputAdapter(common); @@ -44,7 +49,7 @@ // オーナー開始要求に応じてゲーム進行ユースケースを起動する socket.on(protocol.SocketEvents.START_GAME, () => { - startGameUseCase({ + startGameCoordinator({ ownerId: socket.id, gameManager, roomManager, @@ -54,12 +59,10 @@ // 参加者の準備完了通知を受けて現在状態を返す socket.on(protocol.SocketEvents.READY_FOR_GAME, () => { - const roomId = Array.from(socket.rooms).find((room) => room !== socket.id); - - readyForGameUseCase({ + readyForGameCoordinator({ socketId: socket.id, - roomId, gameManager, + roomManager, output: gameOutputAdapter, }); }); diff --git a/apps/server/src/network/handlers/registerConnectionHandlers.ts b/apps/server/src/network/handlers/registerConnectionHandlers.ts index fa871f9..fc7fad4 100644 --- a/apps/server/src/network/handlers/registerConnectionHandlers.ts +++ b/apps/server/src/network/handlers/registerConnectionHandlers.ts @@ -3,18 +3,21 @@ * 接続時にルームとゲームの各ハンドラを登録する */ import { Server, Socket } from "socket.io"; -import { GameManager } from "@server/domains/game/GameManager"; -import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; -import { registerRoomHandlers, handleRoomDisconnect } from "./RoomHandler"; -import { registerGameHandlers, handleGameDisconnect } from "./GameHandler"; +import { disconnectCoordinator } from "@server/application/coordinators/disconnectCoordinator"; import { logEvent } from "@server/logging/logEvent"; - -type RegisterConnectionHandlersParams = { - io: Server; - gameManager: GameManager; - roomManager: RoomManager; -}; +import { registerGameHandlers } from "./GameHandler"; +import { registerRoomHandlers } from "./RoomHandler"; +import { createGameDisconnectOutputAdapter } from "./game/createGameOutputAdapter"; +import { createRoomDisconnectOutputAdapter } from "./room/createRoomOutputAdapter"; +import type { + ConnectionGamePort, + ConnectionRoomPort, + DisconnectCoordinatorPortBundle, + DisconnectGamePort, + DisconnectRoomHandlerPort, + RegisterConnectionHandlersParams, +} from "../types/connectionPorts"; /** ソケット接続と切断イベントに対する共通ハンドラを登録する */ export const registerConnectionHandlers = ({ @@ -22,6 +25,19 @@ gameManager, roomManager, }: RegisterConnectionHandlersParams) => { + const gameDisconnectOutputAdapter = createGameDisconnectOutputAdapter(io); + const roomDisconnectOutputAdapter = createRoomDisconnectOutputAdapter(io); + const connectionGameManager: ConnectionGamePort = gameManager; + const disconnectGameManager: DisconnectGamePort = gameManager; + const connectionRoomManager: ConnectionRoomPort = roomManager; + const disconnectRoomManager: DisconnectRoomHandlerPort = roomManager; + const disconnectPorts: DisconnectCoordinatorPortBundle = { + gameManager: disconnectGameManager, + roomManager: disconnectRoomManager, + gameOutput: gameDisconnectOutputAdapter, + roomOutput: roomDisconnectOutputAdapter, + }; + io.on(protocol.SocketEvents.CONNECT, (socket: Socket) => { // 接続ログを記録してドメイン別ハンドラを登録する logEvent("Network", { @@ -30,8 +46,8 @@ socketId: socket.id, }); - registerRoomHandlers(io, socket, roomManager); - registerGameHandlers(io, socket, gameManager, roomManager); + registerRoomHandlers(io, socket, connectionRoomManager); + registerGameHandlers(io, socket, connectionGameManager, connectionRoomManager); socket.on(protocol.SocketEvents.DISCONNECT, () => { // 切断ログ記録後にドメイン別の後処理を実行する @@ -41,10 +57,10 @@ socketId: socket.id, }); - const roomId = roomManager.getRoomByPlayerId(socket.id)?.roomId; - - handleGameDisconnect(io, gameManager, roomId, socket.id); - handleRoomDisconnect(io, socket, roomManager); + disconnectCoordinator({ + socketId: socket.id, + ...disconnectPorts, + }); }); }); }; diff --git a/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts b/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts index 932d2d2..90aacab 100644 --- a/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts +++ b/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts @@ -4,9 +4,9 @@ */ import { Server } from "socket.io"; import { protocol } from "@repo/shared"; -import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; import type { roomTypes } from "@repo/shared"; import type { RoomOutputPort } from "@server/domains/room/application/ports/roomUseCasePorts"; +import { createEmitToRoom } from "@server/network/adapters/socketEmitters"; import type { CommonHandlerContext } from "../CommonHandler"; type RoomId = roomTypes.Room["roomId"]; diff --git a/apps/server/src/network/handlers/room/handleRoomDisconnect.ts b/apps/server/src/network/handlers/room/handleRoomDisconnect.ts deleted file mode 100644 index 998b0f9..0000000 --- a/apps/server/src/network/handlers/room/handleRoomDisconnect.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * handleRoomDisconnect - * ルーム切断ユースケースを呼び出してルーム状態更新を配信する - */ -import { Server, Socket } from "socket.io"; -import { RoomManager } from "@server/domains/room/RoomManager"; -import { roomDisconnectUseCase } from "@server/domains/room/application/useCases/roomDisconnectUseCase"; -import { createRoomDisconnectOutputAdapter } from "./createRoomOutputAdapter"; - -/** 切断ソケットのルーム離脱処理を実行して更新通知する */ -export const handleRoomDisconnect = ( - io: Server, - socket: Socket, - roomManager: RoomManager -) => { - const roomDisconnectOutputAdapter = createRoomDisconnectOutputAdapter(io); - - roomDisconnectUseCase({ - roomManager, - socketId: socket.id, - output: roomDisconnectOutputAdapter, - }); -}; diff --git a/apps/server/src/network/handlers/room/registerRoomHandlers.ts b/apps/server/src/network/handlers/room/registerRoomHandlers.ts index d5729b1..c4b42b9 100644 --- a/apps/server/src/network/handlers/room/registerRoomHandlers.ts +++ b/apps/server/src/network/handlers/room/registerRoomHandlers.ts @@ -3,25 +3,25 @@ * ルーム参加イベントの受信ハンドラを登録する */ import { Server, Socket } from "socket.io"; -import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; +import type { JoinRoomPort } from "@server/domains/room/application/ports/roomUseCasePorts"; import { joinRoomUseCase } from "@server/domains/room/application/useCases/joinRoomUseCase"; -import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; -import { createRoomOutputAdapter } from "./createRoomOutputAdapter"; -import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; import { logEvent } from "@server/logging/logEvent"; +import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; +import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; +import { createRoomOutputAdapter } from "./createRoomOutputAdapter"; /** ルーム参加イベントを検証して参加ユースケースへ連携する */ export const registerRoomHandlers = ( io: Server, socket: Socket, - roomManager: RoomManager + roomManager: JoinRoomPort ) => { const common = createCommonHandlerContext(io, socket); const roomOutputAdapter = createRoomOutputAdapter(common); // 参加要求のペイロード検証と参加処理を実行する - socket.on(protocol.SocketEvents.JOIN_ROOM, (data: unknown) => { + socket.on(protocol.SocketEvents.JOIN_ROOM, async (data: unknown) => { if (!isJoinRoomPayload(data)) { logEvent("Network", { event: "JOIN_ROOM", @@ -61,7 +61,7 @@ return; case "joined": - socket.join(roomId); + await socket.join(roomId); roomOutputAdapter.publishRoomUpdateToRoom(roomId, joinResult.room); logEvent("RoomUseCase", { event: "ROOM_UPDATE", diff --git a/apps/server/src/network/types/connectionPorts.ts b/apps/server/src/network/types/connectionPorts.ts new file mode 100644 index 0000000..dcb61a3 --- /dev/null +++ b/apps/server/src/network/types/connectionPorts.ts @@ -0,0 +1,62 @@ +/** + * connectionPorts + * ネットワーク接続処理で利用するポート型を提供する + */ +import type { Server } from "socket.io"; +import type { + DisconnectPlayerPort, + MovePlayerPort, + ReadyForGamePort, + ReadyForGameRoomPort, + StartGamePort, + StartGameRoomPort, +} from "@server/domains/game/application/ports/gameUseCasePorts"; +import type { + DisconnectRoomPort, + FindRoomByPlayerPort, + JoinRoomPort, +} from "@server/domains/room/application/ports/roomUseCasePorts"; +import type { DisconnectCoordinatorParams } from "../../application/coordinators/disconnectCoordinator"; + +/** 接続時のゲーム処理で利用する入力ポート集合 */ +export type ConnectionGamePort = + & StartGamePort + & ReadyForGamePort + & MovePlayerPort; + +/** 接続時のルーム処理で利用する入力ポート集合 */ +export type ConnectionRoomPort = + & JoinRoomPort + & StartGameRoomPort + & ReadyForGameRoomPort; + +/** ソケット接続全体で利用するゲーム管理ポート集合 */ +export type SocketConnectionGamePort = + & ConnectionGamePort + & DisconnectPlayerPort; + +/** ソケット接続全体で利用するルーム管理ポート集合 */ +export type SocketConnectionRoomPort = + & ConnectionRoomPort + & DisconnectRoomPort + & FindRoomByPlayerPort; + +/** ソケット接続ハンドラで受け取るマネージャ依存の束 */ +export type SocketConnectionManagerBundle = { + gameManager: SocketConnectionGamePort; + roomManager: SocketConnectionRoomPort; +}; + +/** 切断時のゲーム処理で利用する入力ポート */ +export type DisconnectGamePort = DisconnectPlayerPort; + +/** 切断時のルーム処理で利用する入力ポート集合 */ +export type DisconnectRoomHandlerPort = DisconnectRoomPort & FindRoomByPlayerPort; + +/** 切断調停処理へ受け渡す依存集合 */ +export type DisconnectCoordinatorPortBundle = Omit; + +/** 接続ハンドラ登録関数が受け取る入力パラメータ */ +export type RegisterConnectionHandlersParams = SocketConnectionManagerBundle & { + io: Server; +}; diff --git a/test/load-bot.constants.ts b/test/load-bot.constants.ts index 12216fd..61fae65 100644 --- a/test/load-bot.constants.ts +++ b/test/load-bot.constants.ts @@ -4,7 +4,7 @@ export const URL = NETWORK_CONFIG.PROD_SERVER_URL; export const DEV_URL = NETWORK_CONFIG.DEV_SERVER_URL; -export const BOTS = 20; +export const BOTS = 10; export const DURATION_MS = Infinity; export const JOIN_DELAY_MS = 25; export const MOVE_TICK_MS = GAME_CONFIG.PLAYER_POSITION_UPDATE_MS;