diff --git a/apps/server/src/application/coordinators/startGameCoordinator.ts b/apps/server/src/application/coordinators/startGameCoordinator.ts index c1fd7b4..51c2d2c 100644 --- a/apps/server/src/application/coordinators/startGameCoordinator.ts +++ b/apps/server/src/application/coordinators/startGameCoordinator.ts @@ -4,6 +4,7 @@ */ import { type StartGameOutputPort } from "@server/domains/game/application/ports/gameUseCasePorts"; import type { FieldSizePreset } from "@repo/shared"; +import { config } from "@server/config"; import type { StartGameCoordinatorDeps } from "./coordinatorDeps"; import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; import { @@ -70,13 +71,19 @@ return; } + const resolvedFieldSizePreset: FieldSizePreset = + requestedFieldSizePreset + ?? updatedRoom.fieldSizePreset + ?? config.GAME_CONFIG.DEFAULT_FIELD_PRESET; + updatedRoom.fieldSizePreset = resolvedFieldSizePreset; + logEvent(logScopes.GAME_USE_CASE, { event: gameUseCaseLogEvents.START_GAME, result: logResults.ACCEPTED, roomId: updatedRoom.roomId, socketId: ownerId, totalPlayers: updatedRoom.players.length, - fieldSizePreset: requestedFieldSizePreset, + fieldSizePreset: resolvedFieldSizePreset, }); const humanPlayerIds = updatedRoom.players.map((player) => player.id); @@ -104,6 +111,7 @@ startGameUseCase({ roomId: updatedRoom.roomId, + fieldSizePreset: resolvedFieldSizePreset, playerIds: sessionPlayerIds, playerNamesById, gameSession: gameManager, diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index c6aa198..1061248 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -10,6 +10,7 @@ import { GameRoomSession } from "./application/services/GameRoomSession"; import type { GameSessionCallbacks } from "./application/services/GameRoomSession"; import type { ActiveBombRegistration } from "./application/ports/gameUseCasePorts"; +import type { GameFieldConfig } from "./application/ports/gameUseCasePorts"; import { GameSessionLifecycleService } from "./application/services/GameSessionLifecycleService"; import { GamePlayerOperationService } from "./application/services/GamePlayerOperationService"; @@ -44,6 +45,10 @@ return this.lifecycleService.getRoomStartTime(); } + getRoomFieldConfig(): GameFieldConfig | undefined { + return this.lifecycleService.getRoomFieldConfig(); + } + // プレイヤー登録解除処理 removePlayer(id: string) { this.playerOperationService.removePlayer(id); @@ -67,11 +72,13 @@ startRoomSession( playerIds: string[], playerNamesById: Record, + fieldConfig: GameFieldConfig, callbacks: GameSessionCallbacks, ) { this.lifecycleService.startRoomSession( playerIds, playerNamesById, + fieldConfig, callbacks, ); } diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index e23c295..adadf12 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -17,23 +17,32 @@ PongPayload, RemovePlayerPayload, UpdatePlayersPayload, + FieldSizePreset, } from "@repo/shared"; import type { GameSessionCallbacks } from "../services/GameRoomSession"; +/** ゲーム開始時に固定するフィールド設定情報 */ +export type GameFieldConfig = { + fieldSizePreset: FieldSizePreset; +}; + /** ゲーム開始ユースケースが利用するゲーム管理入力ポート */ export interface StartGamePort { startRoomSession( playerIds: string[], playerNamesById: Record, + fieldConfig: GameFieldConfig, callbacks: GameSessionCallbacks, ): void; getRoomStartTime(): number | undefined; + getRoomFieldConfig(): GameFieldConfig | undefined; } /** 準備完了ユースケースが利用するゲーム状態参照入力ポート */ export interface ReadyForGamePort { getRoomPlayers(): domain.game.player.PlayerData[]; getRoomStartTime(): number | undefined; + getRoomFieldConfig(): GameFieldConfig | undefined; } /** 移動入力ユースケースが利用するプレイヤー操作入力ポート */ diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts index dd009b1..056e8aa 100644 --- a/apps/server/src/domains/game/application/services/GameRoomSession.ts +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -22,6 +22,7 @@ } from "../../entities/player/playerMovement.js"; import { buildGameResultPayload } from "./gameResultCalculator.js"; import { TeamAssignmentService } from "../services/TeamAssignmentService.js"; +import type { GameFieldConfig } from "../ports/gameUseCasePorts"; /** GameRoomSession のコールバック集合 */ export type GameSessionCallbacks = { @@ -40,15 +41,18 @@ private gameLoop: GameLoop | null = null; private startTime: number | undefined; private startDelayTimer: NodeJS.Timeout | null = null; + private fieldConfig: GameFieldConfig; constructor( private roomId: string, playerIds: string[], playerNamesById: Record, + fieldConfig: GameFieldConfig, ) { this.players = new Map(); this.mapStore = new MapStore(); this.bombStateStore = new BombStateStore(); + this.fieldConfig = fieldConfig; playerIds.forEach((playerId) => { // 現在のプレイヤー構成から人数が最も少ないチームを算出する @@ -166,6 +170,11 @@ return this.startTime; } + /** 現在セッションで確定したフィールド設定を返す */ + public getFieldConfig(): GameFieldConfig { + return this.fieldConfig; + } + public getPlayers(): Player[] { return Array.from(this.players.values()); } diff --git a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts index 76ba85c..dbbc2f2 100644 --- a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts +++ b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts @@ -3,7 +3,10 @@ * ゲームセッションの開始,参照,終了時クリーンアップを管理する */ import { config } from "@server/config"; -import type { ActiveBombRegistration } from "../ports/gameUseCasePorts"; +import type { + ActiveBombRegistration, + GameFieldConfig, +} from "../ports/gameUseCasePorts"; import type { domain, GameResultPayload, @@ -35,6 +38,10 @@ return this.sessionRef.current?.getPlayers() ?? []; } + public getRoomFieldConfig(): GameFieldConfig | undefined { + return this.sessionRef.current?.getFieldConfig(); + } + public shouldBroadcastBombPlaced(dedupeKey: string, nowMs: number): boolean { return ( this.sessionRef.current?.shouldBroadcastBombPlaced(dedupeKey, nowMs) ?? @@ -74,6 +81,7 @@ public startRoomSession( playerIds: string[], playerNamesById: Record, + fieldConfig: GameFieldConfig, callbacks: GameSessionCallbacks, ) { if (this.sessionRef.current) { @@ -90,6 +98,7 @@ this.roomId, playerIds, playerNamesById, + fieldConfig, ); this.activePlayerIds.clear(); diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index f61ceda..b1ef6f3 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -4,6 +4,7 @@ */ import type { ReadyForGamePort } from "../ports/gameUseCasePorts"; import type { GameOutputPort } from "../ports/gameUseCasePorts"; +import { config } from "@server/config"; import { logEvent } from "@server/logging/logger"; import { gameUseCaseLogEvents, logResults, logScopes } from "@server/logging/index"; @@ -47,7 +48,14 @@ return; } - output.publishGameStartToSocket({ startTime, serverNow: Date.now() }); + const fieldConfig = gameManager.getRoomFieldConfig(); + + output.publishGameStartToSocket({ + startTime, + serverNow: Date.now(), + fieldSizePreset: + fieldConfig?.fieldSizePreset ?? config.GAME_CONFIG.DEFAULT_FIELD_PRESET, + }); logEvent(logScopes.GAME_USE_CASE, { event: gameUseCaseLogEvents.GAME_START, result: logResults.EMITTED, diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index b0a9b7d..9cf60b2 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -4,6 +4,7 @@ */ import type { BombPlacementPort, + GameFieldConfig, StartGameOutputPort, StartGamePort, } from "../ports/gameUseCasePorts"; @@ -17,6 +18,7 @@ type StartGameUseCaseParams = { roomId: string; + fieldSizePreset: GameFieldConfig["fieldSizePreset"]; playerIds: string[]; playerNamesById: Record; gameSession: StartGamePort; @@ -28,7 +30,7 @@ type TickUpdatePublishParams = { roomId: string; output: StartGameOutputPort; - tickData: Parameters[2] extends { + tickData: Parameters[3] extends { onTick: (data: infer TTickData) => void; } ? TTickData @@ -56,6 +58,7 @@ /** ゲームセッション開始とティック通知,終了通知を実行する */ export const startGameUseCase = ({ roomId, + fieldSizePreset, playerIds, playerNamesById, gameSession, @@ -76,30 +79,42 @@ }); }; - gameSession.startRoomSession(playerIds, playerNamesById, { - onTick: (tickData) => { - publishTickUpdates({ roomId, output, tickData }); + gameSession.startRoomSession( + playerIds, + playerNamesById, + { + fieldSizePreset, }, - onGameEnd: (resultPayload) => { - logEvent(logScopes.GAME_USE_CASE, { - event: gameUseCaseLogEvents.GAME_END, - result: logResults.EMITTED, - roomId, - reason: "duration_elapsed", - }); - output.publishGameEndToRoom(roomId); - output.publishGameResultToRoom(roomId, resultPayload); - onGameEnd(); + { + onTick: (tickData) => { + publishTickUpdates({ roomId, output, tickData }); + }, + onGameEnd: (resultPayload) => { + logEvent(logScopes.GAME_USE_CASE, { + event: gameUseCaseLogEvents.GAME_END, + result: logResults.EMITTED, + roomId, + reason: "duration_elapsed", + }); + output.publishGameEndToRoom(roomId); + output.publishGameResultToRoom(roomId, resultPayload); + onGameEnd(); + }, + onBotPlaceBomb: handleBotBombAction, + onBotBombHit: handleBotBombHit, + onHurricanePlayerHit: (targetPlayerId) => { + output.publishHurricaneHitToRoom(roomId, { + playerId: targetPlayerId, + }); + }, }, - onBotPlaceBomb: handleBotBombAction, - onBotBombHit: handleBotBombHit, - onHurricanePlayerHit: (targetPlayerId) => { - output.publishHurricaneHitToRoom(roomId, { - playerId: targetPlayerId, - }); - }, - }); + ); const startTime = gameSession.getRoomStartTime() || Date.now(); - output.publishGameStartToRoom(roomId, { startTime, serverNow: Date.now() }); + const fieldConfig = gameSession.getRoomFieldConfig(); + output.publishGameStartToRoom(roomId, { + startTime, + serverNow: Date.now(), + fieldSizePreset: fieldConfig?.fieldSizePreset ?? fieldSizePreset, + }); }; diff --git a/apps/server/src/domains/room/application/services/RoomJoinService.ts b/apps/server/src/domains/room/application/services/RoomJoinService.ts index 979b5b2..2dc89ab 100644 --- a/apps/server/src/domains/room/application/services/RoomJoinService.ts +++ b/apps/server/src/domains/room/application/services/RoomJoinService.ts @@ -21,6 +21,7 @@ players: [], status: domain.room.RoomPhase.WAITING, maxPlayers: config.GAME_CONFIG.MAX_PLAYERS_PER_ROOM, + fieldSizePreset: config.GAME_CONFIG.DEFAULT_FIELD_PRESET, }; this.rooms.set(roomId, room); logEvent(logScopes.ROOM_JOIN_SERVICE, { diff --git a/apps/server/src/network/validation/socketPayloadValidators.ts b/apps/server/src/network/validation/socketPayloadValidators.ts index e5d3538..1c82b78 100644 --- a/apps/server/src/network/validation/socketPayloadValidators.ts +++ b/apps/server/src/network/validation/socketPayloadValidators.ts @@ -6,6 +6,7 @@ domain, PlaceBombPayload, BombHitReportPayload, + StartGameRequestPayload, } from "@repo/shared"; import type { PingPayload } from "@repo/shared"; import { isPlaceBombPayload as isValidPlaceBombPayload } from "@server/domains/game/entities/bomb/bombPayloadValidation"; @@ -57,7 +58,7 @@ /** START_GAMEイベントのペイロードが開始要求情報であるか判定する */ export const isStartGamePayload = ( value: unknown, -): value is { targetPlayerCount?: number; fieldSizePreset?: string } => { +): value is StartGameRequestPayload => { if (typeof value !== "object" || value === null) { return false; } diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index e178d72..f3670ce 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -38,12 +38,22 @@ }, } as const; +/** フィールドサイズ種別のキー型 */ +export type FieldSizePreset = keyof typeof FIELD_PRESETS; + +/** フィールドサイズ種別から実グリッドサイズを解決する */ +export const resolveFieldGridSize = (preset: FieldSizePreset) => { + const selectedPreset = FIELD_PRESETS[preset]; + return { + cols: selectedPreset.aoiCols * AOI_CELL_SIZE, + rows: selectedPreset.aoiRows * AOI_CELL_SIZE, + }; +}; + /** 既定で利用するフィールドサイズ種別 */ const DEFAULT_FIELD_PRESET = "MEDIUM" as const; -const defaultFieldPreset = FIELD_PRESETS[DEFAULT_FIELD_PRESET]; -const defaultGridCols = defaultFieldPreset.aoiCols * AOI_CELL_SIZE; -const defaultGridRows = defaultFieldPreset.aoiRows * AOI_CELL_SIZE; +const defaultGridSize = resolveFieldGridSize(DEFAULT_FIELD_PRESET); /** ゲーム全体で利用する共有設定値 */ export const GAME_CONFIG = { @@ -64,8 +74,8 @@ DEFAULT_FIELD_PRESET, // グリッド(マス)設定(クライアント/サーバー契約) - GRID_COLS: defaultGridCols, // 横のマス数(グリッド単位) - GRID_ROWS: defaultGridRows, // 縦のマス数(グリッド単位) + GRID_COLS: defaultGridSize.cols, // 横のマス数(グリッド単位) + GRID_ROWS: defaultGridSize.rows, // 縦のマス数(グリッド単位) // プレイヤー挙動設定(内部座標はグリッド単位、契約値) PLAYER_RADIUS: 0.5, // プレイヤー半径(グリッド単位、目安: 0.05〜0.2) diff --git a/packages/shared/src/config/index.ts b/packages/shared/src/config/index.ts index 3d3230d..f21dee7 100644 --- a/packages/shared/src/config/index.ts +++ b/packages/shared/src/config/index.ts @@ -6,6 +6,10 @@ /** ゲーム全体の共有設定値を再公開する */ export { GAME_CONFIG } from "./gameConfig"; +/** フィールドサイズ種別から実グリッドサイズを解決する関数を再公開する */ +export { resolveFieldGridSize } from "./gameConfig"; +/** フィールドサイズ種別のキー型を再公開する */ +export type { FieldSizePreset } from "./gameConfig"; /** チーム名配列を再公開する */ export { TEAM_NAMES } from "./gameConfig"; /** 未確定 teamId の既定値を再公開する */ diff --git a/packages/shared/src/domains/room/room.type.ts b/packages/shared/src/domains/room/room.type.ts index 6fad7d7..22d8039 100644 --- a/packages/shared/src/domains/room/room.type.ts +++ b/packages/shared/src/domains/room/room.type.ts @@ -4,6 +4,8 @@ * 参加状態とイベントペイロード契約を集約する */ +import type { FieldSizePreset } from "../../config/gameConfig"; + /** ルーム進行フェーズ状態型 */ export type RoomPhase = "waiting" | "playing" | "result"; @@ -22,6 +24,7 @@ players: RoomMember[]; status: RoomPhase; maxPlayers: number; + fieldSizePreset: FieldSizePreset; } /** ルーム参加時に送信するペイロード */ diff --git a/packages/shared/src/protocol/payloads/gamePayloads.ts b/packages/shared/src/protocol/payloads/gamePayloads.ts index 68139ba..01218a0 100644 --- a/packages/shared/src/protocol/payloads/gamePayloads.ts +++ b/packages/shared/src/protocol/payloads/gamePayloads.ts @@ -9,6 +9,10 @@ MovePayload as PlayerMovePayload, PlayerData, } from "../../domains/game/player/player.type"; +import type { FieldSizePreset } from "../../config/gameConfig"; + +/** start-game で利用するフィールドサイズ種別を再公開する型別名 */ +export type { FieldSizePreset } from "../../config/gameConfig"; /** game-result イベントで送受信するランキング1行 */ export type GameResultRanking = { @@ -101,12 +105,11 @@ startTime: number; /** ペイロード送信時のサーバー時刻(クライアント側クロックオフセット補正用, ms) */ serverNow: number; + /** 今回のゲームで採用するフィールドサイズ種別 */ + fieldSizePreset: FieldSizePreset; }; /** start-game イベントで受信するゲーム開始要求 */ -export type FieldSizePreset = "SMALL" | "MEDIUM" | "LARGE" | "XLARGE"; - -/** start-game イベントで受信するゲーム開始要求 */ export type StartGameRequestPayload = { targetPlayerCount?: number; fieldSizePreset?: FieldSizePreset;