diff --git a/apps/server/src/application/coordinators/disconnectCoordinator.ts b/apps/server/src/application/coordinators/disconnectCoordinator.ts index 52acd07..c76880a 100644 --- a/apps/server/src/application/coordinators/disconnectCoordinator.ts +++ b/apps/server/src/application/coordinators/disconnectCoordinator.ts @@ -3,7 +3,7 @@ * DISCONNECTイベントの調停を行い,ゲーム離脱処理とルーム離脱処理を順序実行する */ import { - type BombRoomStateCleanupPort, + type BombCleanupPort, type DisconnectPlayerPort, type GameOutputPort, } from "@server/domains/game/application/ports/gameUseCasePorts"; @@ -19,7 +19,7 @@ /** 切断調停で利用する入力ポートと出力ポートの契約 */ export type DisconnectCoordinatorParams = { socketId: string; - gameManager: DisconnectPlayerPort & BombRoomStateCleanupPort; + gameManager: DisconnectPlayerPort & BombCleanupPort; roomManager: DisconnectRoomPort & FindRoomByPlayerPort & FindRoomByIdPort; gameOutput: Pick; roomOutput: Pick; diff --git a/apps/server/src/application/coordinators/readyForGameCoordinator.ts b/apps/server/src/application/coordinators/readyForGameCoordinator.ts index 407dc01..c87243b 100644 --- a/apps/server/src/application/coordinators/readyForGameCoordinator.ts +++ b/apps/server/src/application/coordinators/readyForGameCoordinator.ts @@ -4,15 +4,15 @@ */ import { type GameOutputPort, + type GameRoomLookupPort, 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; + roomManager: GameRoomLookupPort; output: Pick; }; diff --git a/apps/server/src/application/coordinators/startGameCoordinator.ts b/apps/server/src/application/coordinators/startGameCoordinator.ts index d96240a..766b626 100644 --- a/apps/server/src/application/coordinators/startGameCoordinator.ts +++ b/apps/server/src/application/coordinators/startGameCoordinator.ts @@ -3,7 +3,7 @@ * START_GAMEイベントの調停を行い,ルーム状態更新とゲーム開始処理を橋渡しする */ import { - type BombRoomStateCleanupPort, + type BombCleanupPort, type GameOutputPort, type StartGamePort, type StartGameRoomPort, @@ -15,7 +15,7 @@ type StartGameCoordinatorParams = { ownerId: string; - gameManager: StartGamePort & BombRoomStateCleanupPort; + gameManager: StartGamePort & BombCleanupPort; roomManager: StartGameRoomPort; output: Pick< GameOutputPort, diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index 4a4bdd4..3f86bcf 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -4,9 +4,8 @@ */ import type { gameTypes, GameResultPayload } from "@repo/shared"; import { - clearBombRoomState, - issueServerBombId, - shouldBroadcastBombPlaced, + BombRoomStateClearReason, + BombRoomStateStore, } from "./entities/bomb/BombRoomStateStore"; import { Player } from "./entities/player/Player.js"; import { GameRoomSession } from "./application/services/GameRoomSession"; @@ -21,6 +20,7 @@ private roomToPlayers: Map>; private lifecycleService: GameSessionLifecycleService; private playerOperationService: GamePlayerOperationService; + private bombRoomStateStore: BombRoomStateStore; constructor() { this.sessions = new Map(); @@ -28,6 +28,7 @@ this.roomToPlayers = new Map(); this.lifecycleService = new GameSessionLifecycleService(this.sessions, this.playerToRoom, this.roomToPlayers); this.playerOperationService = new GamePlayerOperationService(this.sessions, this.playerToRoom, this.roomToPlayers); + this.bombRoomStateStore = new BombRoomStateStore(); } // 外部(GameHandlerなど)から開始時刻を取得できるようにする @@ -67,16 +68,16 @@ // 爆弾設置イベントを配信すべきか判定し,配信時は重複排除状態を更新する shouldBroadcastBombPlaced(roomId: string, dedupeKey: string, nowMs: number): boolean { - return shouldBroadcastBombPlaced(roomId, dedupeKey, nowMs); + return this.bombRoomStateStore.shouldBroadcastBombPlaced(roomId, dedupeKey, nowMs); } // ルーム単位の連番からサーバー採番の爆弾IDを生成する issueServerBombId(roomId: string): string { - return issueServerBombId(roomId); + return this.bombRoomStateStore.issueServerBombId(roomId); } // 指定ルームの爆弾採番状態と重複排除状態を破棄する - clearBombRoomState(roomId: string, reason: "game-ended" | "room-deleted"): void { - clearBombRoomState(roomId, reason); + clearBombRoomState(roomId: string, reason: BombRoomStateClearReason): void { + this.bombRoomStateStore.clearBombRoomState(roomId, reason); } } \ No newline at end of file diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index 2fdae5b..a269b40 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -2,6 +2,7 @@ * gameUseCasePorts * ゲーム系ユースケースが利用する入力ポートと出力ポートの契約を定義する */ +import type { BombRoomStateClearReason } from "@server/domains/game/entities/bomb/BombRoomStateStore"; import type { BombPlacedPayload, gameTypes, @@ -35,8 +36,8 @@ markRoomWaiting(roomId: string): roomTypes.Room | undefined; } -/** 準備完了調停で利用するルーム解決入力ポート */ -export interface ReadyForGameRoomPort { +/** ゲーム系調停で利用するプレイヤー所属ルーム解決入力ポート */ +export interface GameRoomLookupPort { getRoomByPlayerId(playerId: string): roomTypes.Room | undefined; } @@ -51,11 +52,6 @@ movePlayer(id: string, x: number, y: number): void; } -/** 爆弾設置ユースケースが利用するルーム解決入力ポート */ -export interface PlaceBombRoomPort { - getRoomByPlayerId(playerId: string): roomTypes.Room | undefined; -} - /** 切断ユースケースが利用するプレイヤー削除入力ポート */ export interface DisconnectPlayerPort { removePlayer(id: string): void; @@ -81,22 +77,19 @@ publishPlayerRemovedToRoom(roomId: roomTypes.Room["roomId"], removedPlayerId: RemovePlayerPayload): void; } -/** 爆弾設置ユースケースが利用する爆弾状態管理入力ポート */ -export interface BombPlacementStorePort { +/** 爆弾設置ユースケースが利用する爆弾状態入力ポート */ +export interface BombPlacementPort { shouldBroadcastBombPlaced(roomId: string, dedupeKey: string, nowMs: number): boolean; issueServerBombId(roomId: string): string; } -/** 爆弾状態破棄理由を表す型 */ -export type BombRoomStateClearReason = "game-ended" | "room-deleted"; - /** 爆弾状態破棄ユースケースが利用する入力ポート */ -export interface BombRoomStateCleanupPort { +export interface BombCleanupPort { clearBombRoomState(roomId: string, reason: BombRoomStateClearReason): void; } /** 爆弾状態の参照更新と破棄を扱う統合入力ポート */ -export interface BombRoomStateStorePort extends BombPlacementStorePort, BombRoomStateCleanupPort {} +export interface BombStatePort extends BombPlacementPort, BombCleanupPort {} /** 爆弾設置ユースケースの入力値 */ export type PlaceBombInput = { diff --git a/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts b/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts index 2e4b822..a0e1290 100644 --- a/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts @@ -3,15 +3,15 @@ * 爆弾設置入力を検証済み前提で処理し,ルーム配信を実行する */ import type { - BombPlacementStorePort, + BombPlacementPort, GameOutputPort, + GameRoomLookupPort, PlaceBombInput, - PlaceBombRoomPort, } from "../ports/gameUseCasePorts"; type PlaceBombUseCaseParams = { - roomResolver: PlaceBombRoomPort; - bombStore: BombPlacementStorePort; + roomResolver: GameRoomLookupPort; + bombStore: BombPlacementPort; input: PlaceBombInput; output: Pick; }; diff --git a/apps/server/src/domains/game/entities/bomb/BombRoomStateStore.ts b/apps/server/src/domains/game/entities/bomb/BombRoomStateStore.ts index d305563..cd4e006 100644 --- a/apps/server/src/domains/game/entities/bomb/BombRoomStateStore.ts +++ b/apps/server/src/domains/game/entities/bomb/BombRoomStateStore.ts @@ -4,59 +4,65 @@ */ import { config } from "@repo/shared"; -type BombRoomStateClearReason = "game-ended" | "room-deleted"; +/** 爆弾状態破棄理由の識別子 */ +export type BombRoomStateClearReason = "game-ended" | "room-deleted"; -const roomBombDedupTable = new Map>(); -const roomBombSerialTable = new Map(); -const isBombRoomStateDebugEnabled = process.env.NODE_ENV !== "production"; +/** ルーム単位の爆弾重複排除状態と採番状態を保持するストア */ +export class BombRoomStateStore { + private roomBombDedupTable = new Map>(); + private roomBombSerialTable = new Map(); + private isBombRoomStateDebugEnabled = process.env.NODE_ENV !== "production"; -const cleanupExpiredBombDedup = (roomId: string, nowMs: number) => { - const roomTable = roomBombDedupTable.get(roomId); - if (!roomTable) return; + /** 爆弾設置イベントを配信すべきか判定し,配信時は重複排除状態を更新する */ + public shouldBroadcastBombPlaced(roomId: string, dedupeKey: string, nowMs: number): boolean { + this.cleanupExpiredBombDedup(roomId, nowMs); - roomTable.forEach((expiresAtMs, dedupeKey) => { - if (expiresAtMs <= nowMs) { - roomTable.delete(dedupeKey); + const roomTable = this.roomBombDedupTable.get(roomId) ?? new Map(); + if (roomTable.has(dedupeKey)) { + return false; } - }); - if (roomTable.size === 0) { - roomBombDedupTable.delete(roomId); - } -}; - -/** 爆弾設置イベントを配信すべきか判定し,配信時は重複排除状態を更新する */ -export const shouldBroadcastBombPlaced = (roomId: string, dedupeKey: string, nowMs: number): boolean => { - cleanupExpiredBombDedup(roomId, nowMs); - - const roomTable = roomBombDedupTable.get(roomId) ?? new Map(); - if (roomTable.has(dedupeKey)) { - return false; + const ttlMs = config.GAME_CONFIG.BOMB_FUSE_MS + config.GAME_CONFIG.BOMB_DEDUP_EXTRA_TTL_MS; + roomTable.set(dedupeKey, nowMs + ttlMs); + this.roomBombDedupTable.set(roomId, roomTable); + return true; } - const ttlMs = config.GAME_CONFIG.BOMB_FUSE_MS + config.GAME_CONFIG.BOMB_DEDUP_EXTRA_TTL_MS; - roomTable.set(dedupeKey, nowMs + ttlMs); - roomBombDedupTable.set(roomId, roomTable); - return true; -}; - -/** ルーム単位の連番からサーバー採番の爆弾IDを生成する */ -export const issueServerBombId = (roomId: string): string => { - const serial = (roomBombSerialTable.get(roomId) ?? 0) + 1; - roomBombSerialTable.set(roomId, serial); - return `${roomId}:${serial}`; -}; - -/** 指定ルームの爆弾採番状態と重複排除状態を破棄する */ -export const clearBombRoomState = (roomId: string, reason: BombRoomStateClearReason): void => { - const hadDedupState = roomBombDedupTable.delete(roomId); - const hadSerialState = roomBombSerialTable.delete(roomId); - - if (!isBombRoomStateDebugEnabled) { - return; + /** ルーム単位の連番からサーバー採番の爆弾IDを生成する */ + public issueServerBombId(roomId: string): string { + const serial = (this.roomBombSerialTable.get(roomId) ?? 0) + 1; + this.roomBombSerialTable.set(roomId, serial); + return `${roomId}:${serial}`; } - console.debug( - `[BombState] cleared room=${roomId} reason=${reason} dedup=${hadDedupState} serial=${hadSerialState}` - ); -}; + /** 指定ルームの爆弾採番状態と重複排除状態を破棄する */ + public clearBombRoomState(roomId: string, reason: BombRoomStateClearReason): void { + const hadDedupState = this.roomBombDedupTable.delete(roomId); + const hadSerialState = this.roomBombSerialTable.delete(roomId); + + if (!this.isBombRoomStateDebugEnabled) { + return; + } + + console.debug( + `[BombState] cleared room=${roomId} reason=${reason} dedup=${hadDedupState} serial=${hadSerialState}` + ); + } + + private cleanupExpiredBombDedup(roomId: string, nowMs: number): void { + const roomTable = this.roomBombDedupTable.get(roomId); + if (!roomTable) { + return; + } + + roomTable.forEach((expiresAtMs, dedupeKey) => { + if (expiresAtMs <= nowMs) { + roomTable.delete(dedupeKey); + } + }); + + if (roomTable.size === 0) { + this.roomBombDedupTable.delete(roomId); + } + } +} diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index ad9a157..c9ca666 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -7,11 +7,10 @@ import { readyForGameCoordinator } from "@server/application/coordinators/readyForGameCoordinator"; import { startGameCoordinator } from "@server/application/coordinators/startGameCoordinator"; import type { - BombRoomStateStorePort, + BombStatePort, + GameRoomLookupPort, MovePlayerPort, - PlaceBombRoomPort, ReadyForGamePort, - ReadyForGameRoomPort, StartGamePort, StartGameRoomPort, } from "@server/domains/game/application/ports/gameUseCasePorts"; @@ -35,8 +34,8 @@ export const registerGameHandlers = ( io: Server, socket: Socket, - gameManager: StartGamePort & ReadyForGamePort & MovePlayerPort & BombRoomStateStorePort, - roomManager: StartGameRoomPort & ReadyForGameRoomPort & PlaceBombRoomPort + gameManager: StartGamePort & ReadyForGamePort & MovePlayerPort & BombStatePort, + roomManager: StartGameRoomPort & GameRoomLookupPort ) => { const common = createCommonHandlerContext(io, socket); const gameOutputAdapter = createGameOutputAdapter(common); diff --git a/apps/server/src/network/types/connectionPorts.ts b/apps/server/src/network/types/connectionPorts.ts index 0ddfd48..6649026 100644 --- a/apps/server/src/network/types/connectionPorts.ts +++ b/apps/server/src/network/types/connectionPorts.ts @@ -4,11 +4,11 @@ */ import type { Server } from "socket.io"; import type { - BombRoomStateStorePort, + BombStatePort, DisconnectPlayerPort, + GameRoomLookupPort, MovePlayerPort, ReadyForGamePort, - ReadyForGameRoomPort, StartGamePort, StartGameRoomPort, } from "@server/domains/game/application/ports/gameUseCasePorts"; @@ -25,13 +25,13 @@ & StartGamePort & ReadyForGamePort & MovePlayerPort - & BombRoomStateStorePort; + & BombStatePort; /** 接続時のルーム処理で利用する入力ポート集合 */ export type ConnectionRoomPort = & JoinRoomPort & StartGameRoomPort - & ReadyForGameRoomPort; + & GameRoomLookupPort; /** ソケット接続全体で利用するゲーム管理ポート集合 */ export type SocketConnectionGamePort =