diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index 637ded1..a91dce1 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -3,8 +3,10 @@ * ゲーム系ユースケースが利用する入力ポートと出力ポートの契約を定義する */ import type { + BombPlacedPayload, gameTypes, playerTypes, + PlaceBombPayload, roomTypes, CurrentPlayersPayload, GameResultPayload, @@ -49,6 +51,11 @@ movePlayer(id: string, x: number, y: number): void; } +/** 爆弾設置ユースケースが利用するルーム解決入力ポート */ +export interface PlaceBombRoomPort { + getRoomByPlayerId(playerId: string): roomTypes.Room | undefined; +} + /** 切断ユースケースが利用するプレイヤー削除入力ポート */ export interface DisconnectPlayerPort { removePlayer(id: string): void; @@ -70,5 +77,19 @@ publishGameStartToRoom(roomId: roomTypes.Room["roomId"], payload: GameStartPayload): void; publishCurrentPlayersToSocket(players: CurrentPlayersPayload): void; publishGameStartToSocket(payload: GameStartPayload): void; + publishBombPlacedToRoom(roomId: roomTypes.Room["roomId"], payload: BombPlacedPayload): void; publishPlayerRemovedToRoom(roomId: roomTypes.Room["roomId"], removedPlayerId: RemovePlayerPayload): void; } + +/** 爆弾設置ユースケースが利用する爆弾状態管理入力ポート */ +export interface BombPlacementStorePort { + shouldBroadcastBombPlaced(roomId: string, dedupeKey: string, nowMs: number): boolean; + issueServerBombId(roomId: string): string; +} + +/** 爆弾設置ユースケースの入力値 */ +export type PlaceBombInput = { + socketId: string; + payload: PlaceBombPayload; + nowMs: number; +}; diff --git a/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts b/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts new file mode 100644 index 0000000..2e4b822 --- /dev/null +++ b/apps/server/src/domains/game/application/useCases/placeBombUseCase.ts @@ -0,0 +1,41 @@ +/** + * placeBombUseCase + * 爆弾設置入力を検証済み前提で処理し,ルーム配信を実行する + */ +import type { + BombPlacementStorePort, + GameOutputPort, + PlaceBombInput, + PlaceBombRoomPort, +} from "../ports/gameUseCasePorts"; + +type PlaceBombUseCaseParams = { + roomResolver: PlaceBombRoomPort; + bombStore: BombPlacementStorePort; + input: PlaceBombInput; + output: Pick; +}; + +/** 爆弾設置入力を重複排除と採番付きでルームへ配信する */ +export const placeBombUseCase = ({ + roomResolver, + bombStore, + input, + output, +}: PlaceBombUseCaseParams): void => { + const roomId = roomResolver.getRoomByPlayerId(input.socketId)?.roomId; + if (!roomId) { + return; + } + + const dedupeKey = `${input.socketId}:${input.payload.requestId}`; + if (!bombStore.shouldBroadcastBombPlaced(roomId, dedupeKey, input.nowMs)) { + return; + } + + output.publishBombPlacedToRoom(roomId, { + ...input.payload, + bombId: bombStore.issueServerBombId(roomId), + ownerId: input.socketId, + }); +}; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts index bfa4b8f..3594a06 100644 --- a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -5,6 +5,7 @@ import { Server } from "socket.io"; import { protocol } from "@repo/shared"; import type { + BombPlacedPayload, GameStartPayload, GameResultPayload, PongPayload, @@ -53,6 +54,9 @@ publishGameStartToSocket: (payload: GameStartPayload) => { common.emitToSocket(protocol.SocketEvents.GAME_START, payload); }, + publishBombPlacedToRoom: (roomId: RoomId, payload: BombPlacedPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.BOMB_PLACED, payload); + }, }; }; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 75ea885..577b8cb 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -7,13 +7,16 @@ import { readyForGameCoordinator } from "@server/application/coordinators/readyForGameCoordinator"; import { startGameCoordinator } from "@server/application/coordinators/startGameCoordinator"; import type { + BombPlacementStorePort, MovePlayerPort, + PlaceBombRoomPort, ReadyForGamePort, ReadyForGameRoomPort, StartGamePort, StartGameRoomPort, } from "@server/domains/game/application/ports/gameUseCasePorts"; import { movePlayerUseCase } from "@server/domains/game/application/useCases/movePlayerUseCase"; +import { placeBombUseCase } from "@server/domains/game/application/useCases/placeBombUseCase"; import { pingUseCase } from "@server/domains/game/application/useCases/pingUseCase"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; import { isMovePayload, isPingPayload, isPlaceBombPayload } from "@server/network/validation/socketPayloadValidators"; @@ -34,7 +37,7 @@ io: Server, socket: Socket, gameManager: StartGamePort & ReadyForGamePort & MovePlayerPort, - roomManager: StartGameRoomPort & ReadyForGameRoomPort + roomManager: StartGameRoomPort & ReadyForGameRoomPort & PlaceBombRoomPort ) => { const common = createCommonHandlerContext(io, socket); const gameOutputAdapter = createGameOutputAdapter(common); @@ -107,23 +110,20 @@ return; } - const roomId = roomManager.getRoomByPlayerId(socket.id)?.roomId; - if (!roomId) { - return; - } - - const nowMs = Date.now(); - const dedupeKey = `${socket.id}:${data.requestId}`; - if (!shouldBroadcastBombPlaced(roomId, dedupeKey, nowMs)) { - return; - } - - const payload = { - ...data, - bombId: issueServerBombId(roomId), - ownerId: socket.id, + const bombStore: BombPlacementStorePort = { + shouldBroadcastBombPlaced, + issueServerBombId, }; - common.emitToRoom(roomId, protocol.SocketEvents.BOMB_PLACED, payload); + placeBombUseCase({ + roomResolver: roomManager, + bombStore, + input: { + socketId: socket.id, + payload: data, + nowMs: Date.now(), + }, + output: gameOutputAdapter, + }); }); };