diff --git a/apps/server/src/application/coordinators/startGameCoordinator.ts b/apps/server/src/application/coordinators/startGameCoordinator.ts index 08f8fa4..85fc8dc 100644 --- a/apps/server/src/application/coordinators/startGameCoordinator.ts +++ b/apps/server/src/application/coordinators/startGameCoordinator.ts @@ -2,10 +2,7 @@ * startGameCoordinator * START_GAMEイベントの調停を行い,ルーム状態更新とゲーム開始処理を橋渡しする */ -import { - type GameOutputPort, - type BombOutputPort, -} from "@server/domains/game/application/ports/gameUseCasePorts"; +import { type StartGameOutputPort } from "@server/domains/game/application/ports/gameUseCasePorts"; import type { StartGameCoordinatorDeps } from "./coordinatorDeps"; import { startGameUseCase } from "@server/domains/game/application/useCases/startGameUseCase"; import { createBalancedSessionPlayerIds } from "@server/domains/game/application/services/BotRosterService"; @@ -19,18 +16,7 @@ type StartGameCoordinatorParams = { ownerId: string; } & StartGameCoordinatorDeps & { - output: Pick< - GameOutputPort, - | "publishUpdatePlayersToSocket" - | "publishMapCellUpdatesToRoom" - | "publishGameEndToRoom" - | "publishGameResultToRoom" - | "publishGameStartToRoom" - > & - Pick< - BombOutputPort, - "publishBombPlacedToOthersInRoom" | "publishBombPlacedAckToSocket" - >; + output: StartGameOutputPort; }; /** START_GAME受信時にルーム状態遷移を判定し,ゲーム開始ユースケースを実行する */ @@ -100,7 +86,8 @@ roomId: updatedRoom.roomId, playerIds: sessionPlayerIds, recipientPlayerIds: humanPlayerIds, - gameManager, + gameSession: gameManager, + bombStore: gameManager, onGameEnd: () => { roomManager.markRoomWaiting(updatedRoom.roomId); }, diff --git a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts index a6eefb5..7bf53c8 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -27,8 +27,6 @@ onBotPlaceBomb?: (ownerId: string, payload: PlaceBombPayload) => void, ): void; getRoomStartTime(): number | undefined; - shouldBroadcastBombPlaced(dedupeKey: string, nowMs: number): boolean; - issueServerBombId(): string; } /** 準備完了ユースケースが利用するゲーム状態参照入力ポート */ @@ -88,6 +86,20 @@ ): void; } +/** start-game 系フローで利用する送信出力ポート */ +export type StartGameOutputPort = Pick< + GameOutputPort, + | "publishUpdatePlayersToSocket" + | "publishMapCellUpdatesToRoom" + | "publishGameEndToRoom" + | "publishGameResultToRoom" + | "publishGameStartToRoom" +> & + Pick< + BombOutputPort, + "publishBombPlacedToOthersInRoom" | "publishBombPlacedAckToSocket" + >; + /** 爆弾設置ユースケースが利用する爆弾状態入力ポート */ export interface BombPlacementPort { shouldBroadcastBombPlaced(dedupeKey: string, nowMs: number): boolean; diff --git a/apps/server/src/domains/game/application/services/BotAiService.ts b/apps/server/src/domains/game/application/services/BotAiService.ts index 96132c7..0818cba 100644 --- a/apps/server/src/domains/game/application/services/BotAiService.ts +++ b/apps/server/src/domains/game/application/services/BotAiService.ts @@ -4,7 +4,7 @@ */ import type { PlaceBombPayload } from "@repo/shared"; import { config } from "@server/config"; -import { isBotPlayerId } from "./BotRosterService"; +import type { BotPlayerId } from "./BotRosterService"; import type { Player } from "../../entities/player/Player"; type BotState = { @@ -120,9 +120,10 @@ /** Botの移動・爆弾行動を管理するサービス */ export class BotAiService { - private states = new Map(); + private states = new Map(); public decide( + botPlayerId: BotPlayerId, player: Player, gridColors: number[], nowMs: number, @@ -133,7 +134,7 @@ const currentCol = clamp(Math.floor(player.x), 0, GRID_COLS - 1); const currentRow = clamp(Math.floor(player.y), 0, GRID_ROWS - 1); - const currentState = this.states.get(player.id) ?? { + const currentState = this.states.get(botPlayerId) ?? { targetCol: currentCol, targetRow: currentRow, lastBombPlacedAtMs: Number.NEGATIVE_INFINITY, @@ -167,13 +168,13 @@ ) { const nextBombSeq = currentState.bombSeq + 1; placeBombPayload = { - requestId: `bot-${player.id}-${nextBombSeq}`, + requestId: `bot-${botPlayerId}-${nextBombSeq}`, x: moved.nextX, y: moved.nextY, explodeAtElapsedMs: elapsedMs + BOMB_FUSE_MS, }; - this.states.set(player.id, { + this.states.set(botPlayerId, { targetCol: nextTarget.col, targetRow: nextTarget.row, bombSeq: nextBombSeq, @@ -187,7 +188,7 @@ }; } - this.states.set(player.id, { + this.states.set(botPlayerId, { targetCol: nextTarget.col, targetRow: nextTarget.row, bombSeq: currentState.bombSeq, @@ -205,5 +206,3 @@ this.states.clear(); } } - -export { isBotPlayerId }; diff --git a/apps/server/src/domains/game/application/services/BotBombActionService.ts b/apps/server/src/domains/game/application/services/BotBombActionService.ts new file mode 100644 index 0000000..26d8c14 --- /dev/null +++ b/apps/server/src/domains/game/application/services/BotBombActionService.ts @@ -0,0 +1,36 @@ +/** + * BotBombActionService + * Bot由来の爆弾設置アクションを既存ユースケースへ橋渡しする + */ +import type { PlaceBombPayload } from "@repo/shared"; +import type { + BombPlacementPort, + StartGameOutputPort, +} from "../ports/gameUseCasePorts"; +import { placeBombUseCase } from "../useCases/placeBombUseCase"; + +type CreateBotBombActionHandlerParams = { + roomId: string; + bombStore: BombPlacementPort; + output: StartGameOutputPort; +}; + +/** Bot爆弾アクションを処理するコールバックを生成する */ +export const createBotBombActionHandler = ({ + roomId, + bombStore, + output, +}: CreateBotBombActionHandlerParams) => { + return (ownerId: string, payload: PlaceBombPayload): void => { + placeBombUseCase({ + roomId, + bombStore, + input: { + socketId: ownerId, + payload, + nowMs: Date.now(), + }, + output, + }); + }; +}; diff --git a/apps/server/src/domains/game/application/services/BotRosterService.ts b/apps/server/src/domains/game/application/services/BotRosterService.ts index 8c5e665..2c4e953 100644 --- a/apps/server/src/domains/game/application/services/BotRosterService.ts +++ b/apps/server/src/domains/game/application/services/BotRosterService.ts @@ -5,9 +5,20 @@ import { config } from "@server/config"; const BOT_PLAYER_ID_PREFIX = "bot:"; +declare const botPlayerIdBrand: unique symbol; + +export type BotPlayerId = string & { readonly [botPlayerIdBrand]: true }; + +/** BotプレイヤーIDを生成する */ +export const createBotPlayerId = ( + roomId: string, + serialNumber: number, +): BotPlayerId => { + return `${BOT_PLAYER_ID_PREFIX}${roomId}:${serialNumber}` as BotPlayerId; +}; /** BotプレイヤーIDかどうかを判定する */ -export const isBotPlayerId = (playerId: string): boolean => { +export const isBotPlayerId = (playerId: string): playerId is BotPlayerId => { return playerId.startsWith(BOT_PLAYER_ID_PREFIX); }; @@ -38,7 +49,7 @@ } const botIds = Array.from({ length: requiredBotCount }, (_, index) => { - return `${BOT_PLAYER_ID_PREFIX}${roomId}:${index + 1}`; + return createBotPlayerId(roomId, index + 1); }); return [...humanPlayerIds, ...botIds]; diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index 9956be5..9000d3d 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -3,8 +3,8 @@ * ルーム内プレイヤーでゲームセッションを開始し,進行イベントを通知する */ import type { - BombOutputPort, - GameOutputPort, + BombPlacementPort, + StartGameOutputPort, StartGamePort, } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logger"; @@ -13,7 +13,7 @@ logResults, logScopes, } from "@server/logging/index"; -import { placeBombUseCase } from "./placeBombUseCase"; +import { createBotBombActionHandler } from "../services/BotBombActionService"; const excludeRecipientFromPlayerUpdates = < TPlayerUpdate extends { id: string }, @@ -30,20 +30,10 @@ roomId: string; playerIds: string[]; recipientPlayerIds?: string[]; - gameManager: StartGamePort; + gameSession: StartGamePort; + bombStore: BombPlacementPort; onGameEnd: () => void; - output: Pick< - GameOutputPort, - | "publishUpdatePlayersToSocket" - | "publishMapCellUpdatesToRoom" - | "publishGameEndToRoom" - | "publishGameResultToRoom" - | "publishGameStartToRoom" - > & - Pick< - BombOutputPort, - "publishBombPlacedToOthersInRoom" | "publishBombPlacedAckToSocket" - >; + output: StartGameOutputPort; }; /** ゲームセッション開始とティック通知,終了通知を実行する */ @@ -51,13 +41,19 @@ roomId, playerIds, recipientPlayerIds, - gameManager, + gameSession, + bombStore, onGameEnd, output, }: StartGameUseCaseParams) => { const updateRecipients = recipientPlayerIds ?? playerIds; + const handleBotBombAction = createBotBombActionHandler({ + roomId, + bombStore, + output, + }); - gameManager.startRoomSession( + gameSession.startRoomSession( playerIds, (tickData) => { if (tickData.playerUpdates.length > 0) { @@ -90,20 +86,9 @@ output.publishGameResultToRoom(roomId, resultPayload); onGameEnd(); }, - (ownerId, payload) => { - placeBombUseCase({ - roomId, - bombStore: gameManager, - input: { - socketId: ownerId, - payload, - nowMs: Date.now(), - }, - output, - }); - }, + handleBotBombAction, ); - const startTime = gameManager.getRoomStartTime() || Date.now(); + const startTime = gameSession.getRoomStartTime() || Date.now(); output.publishGameStartToRoom(roomId, { startTime }); }; diff --git a/apps/server/src/domains/game/loop/GameLoop.ts b/apps/server/src/domains/game/loop/GameLoop.ts index 7c32b3f..339c05d 100644 --- a/apps/server/src/domains/game/loop/GameLoop.ts +++ b/apps/server/src/domains/game/loop/GameLoop.ts @@ -13,10 +13,8 @@ logResults, logScopes, } from "@server/logging/index"; -import { - BotAiService, - isBotPlayerId, -} from "../application/services/BotAiService"; +import { BotAiService } from "../application/services/BotAiService"; +import { isBotPlayerId } from "../application/services/BotRosterService"; import { setPlayerPosition } from "../entities/player/playerMovement.js"; /** ルーム内ゲーム進行を定周期で実行するループ管理クラス */ @@ -111,21 +109,26 @@ } private processSingleTick(): void { - const changedPlayers: gameTypes.TickData["playerUpdates"] = []; - const activePlayerIds = new Set(); const nowMs = performance.now(); const elapsedMs = Math.max( 0, Math.round(nowMs - this.startMonotonicTimeMs), ); const gridColorsSnapshot = this.mapStore.getGridColorsSnapshot(); + this.updateBotPlayers(nowMs, elapsedMs, gridColorsSnapshot); + const tickData = this.buildTickData(); + this.onTick(tickData); + } - // 1. 各プレイヤーの座標処理とマス塗りの判定 + private updateBotPlayers( + nowMs: number, + elapsedMs: number, + gridColorsSnapshot: number[], + ): void { this.players.forEach((player) => { - activePlayerIds.add(player.id); - if (isBotPlayerId(player.id)) { const decision = this.botAiService.decide( + player.id, player, gridColorsSnapshot, nowMs, @@ -137,7 +140,27 @@ this.onBotPlaceBomb(player.id, decision.placeBombPayload); } } + }); + } + private buildTickData(): gameTypes.TickData { + const activePlayerIds = new Set(); + const playerUpdates = this.collectChangedPlayerUpdates(activePlayerIds); + this.cleanupInactivePlayerSnapshots(activePlayerIds); + + return { + playerUpdates, + cellUpdates: this.mapStore.getAndClearUpdates(), + }; + } + + private collectChangedPlayerUpdates( + activePlayerIds: Set, + ): gameTypes.TickData["playerUpdates"] { + const changedPlayers: gameTypes.TickData["playerUpdates"] = []; + + this.players.forEach((player) => { + activePlayerIds.add(player.id); const gridIndex = getPlayerGridIndex(player); if (gridIndex !== null) { this.mapStore.paintCell(gridIndex, player.teamId); @@ -162,21 +185,15 @@ } }); - // ルームから離脱したプレイヤーの送信状態をクリーンアップする + return changedPlayers; + } + + private cleanupInactivePlayerSnapshots(activePlayerIds: Set): void { Array.from(this.lastSentPlayers.keys()).forEach((playerId) => { if (!activePlayerIds.has(playerId)) { this.lastSentPlayers.delete(playerId); } }); - - // 2. マスの差分(Diff)を取得 - const cellUpdates = this.mapStore.getAndClearUpdates(); - - // 3. 通信層(GameHandler)へデータを渡す - this.onTick({ - playerUpdates: changedPlayers, - cellUpdates: cellUpdates, - }); } stop() {