diff --git a/apps/server/src/domains/game/GameLoop.ts b/apps/server/src/domains/game/GameLoop.ts deleted file mode 100644 index a36b44b..0000000 --- a/apps/server/src/domains/game/GameLoop.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Player } from "./entities/Player.js"; -import { MapStore } from "./states/MapStore"; -import { gridMapLogic, config } from "@repo/shared"; -import type { gridMapTypes } from "@repo/shared"; -import { logEvent } from "@server/logging/logEvent"; - -// コールバックで渡すデータの型定義 -export interface TickData { - players: { - id: string; - x: number; - y: number; - teamId: number; - }[]; - cellUpdates: gridMapTypes.CellUpdate[]; -} - -export class GameLoop { - private loopId: NodeJS.Timeout | null = null; - private startTime: number = 0; - - constructor( - private roomId: string, - private tickRate: number, - private playerIds: string[], - private players: Map, - private mapStore: MapStore, - private onTick: (data: TickData) => void, - private onGameEnd: () => void // ゲーム終了時のコールバック - ) {} - - start() { - // 既にループが回っている場合は何もしない - if (this.loopId) return; - - this.startTime = Date.now(); - - this.loopId = setInterval(() => { - // 時間経過のチェック - const elapsedTimeMs = Date.now() - this.startTime; - if (elapsedTimeMs >= config.GAME_CONFIG.GAME_DURATION_SEC * 1000) { - // ゲーム終了時にループを止めて終了処理へ - this.stop(); - this.onGameEnd(); - return; // 今回のフレームの座標更新はスキップ - } - - const playersData: TickData["players"] = []; - - // 1. 各プレイヤーの座標処理とマス塗りの判定 - this.playerIds.forEach(id => { - const player = this.players.get(id); - if (!player) return; - - const gridIndex = gridMapLogic.getGridIndexFromPosition(player.x, player.y); - if (gridIndex !== null) { - this.mapStore.paintCell(gridIndex, player.teamId); - } - - // 送信用のプレイヤーデータを構築 - playersData.push({ - id: player.id, - x: player.x, - y: player.y, - teamId: player.teamId, - }); - }); - - // 2. マスの差分(Diff)を取得 - const cellUpdates = this.mapStore.getAndClearUpdates(); - - // 3. 通信層(GameHandler)へデータを渡す - this.onTick({ - players: playersData, - cellUpdates: cellUpdates, - }); - - }, this.tickRate); - - logEvent("GameLoop", { - event: "GAME_LOOP", - result: "started", - roomId: this.roomId, - tickRate: this.tickRate, - }); - } - - stop() { - if (this.loopId) { - clearInterval(this.loopId); - this.loopId = null; - logEvent("GameLoop", { - event: "GAME_LOOP", - result: "stopped", - roomId: this.roomId, - }); - } - } -} \ No newline at end of file diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index 43eee46..118abb7 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -1,36 +1,36 @@ -import { GameLoop, type TickData } from "./GameLoop"; -import { Player } from "./entities/Player.js"; -import { PlayerRegistry } from "./application/services/PlayerRegistry"; -import { GameSessionService } from "./application/services/GameSessionService"; +import { type TickData } from "./loop/GameLoop"; +import { Player } from "./entities/player/Player.js"; +import { GameRoomSession } from "./application/services/GameRoomSession"; +import { GameSessionLifecycleService } from "./application/services/GameSessionLifecycleService"; +import { GamePlayerOperationService } from "./application/services/GamePlayerOperationService"; // プレイヤー集合の生成・更新・参照管理クラス export class GameManager { - private playerRegistry: PlayerRegistry; - private gameSessionService: GameSessionService; + private sessions: Map; + private playerToRoom: Map; + private lifecycleService: GameSessionLifecycleService; + private playerOperationService: GamePlayerOperationService; constructor() { - this.playerRegistry = new PlayerRegistry(); - this.gameSessionService = new GameSessionService(this.playerRegistry.getPlayersRef()); + this.sessions = new Map(); + this.playerToRoom = new Map(); + this.lifecycleService = new GameSessionLifecycleService(this.sessions, this.playerToRoom); + this.playerOperationService = new GamePlayerOperationService(this.sessions, this.playerToRoom); } // 外部(GameHandlerなど)から開始時刻を取得できるようにする getRoomStartTime(roomId: string): number | undefined { - return this.gameSessionService.getRoomStartTime(roomId); - } - - // 新規プレイヤー登録と初期位置設定処理 - addPlayer(id: string): Player { - return this.playerRegistry.addPlayer(id); + return this.lifecycleService.getRoomStartTime(roomId); } // プレイヤー登録解除処理 removePlayer(id: string) { - this.playerRegistry.removePlayer(id); + this.playerOperationService.removePlayer(id); } // 指定プレイヤー座標更新処理 movePlayer(id: string, x: number, y: number) { - this.playerRegistry.movePlayer(id, x, y); + this.playerOperationService.movePlayer(id, x, y); } /** @@ -39,19 +39,17 @@ * @param playerIds このルームに参加しているプレイヤーのIDリスト * @param onTick 毎フレーム実行される送信用のコールバック関数 */ - startGameLoop( + startRoomSession( roomId: string, playerIds: string[], onTick: (data: TickData) => void, onGameEnd: () => void ) { - this.gameSessionService.startGameLoop(roomId, playerIds, onTick, onGameEnd); + this.lifecycleService.startRoomSession(roomId, playerIds, onTick, onGameEnd); } - // 指定ID配列のプレイヤーを取得 - getPlayersByIds(playerIds: string[]) { - return playerIds - .map((playerId) => this.playerRegistry.getPlayer(playerId)) - .filter((player): player is Player => player !== undefined); + // 指定ルームのプレイヤーを取得 + getRoomPlayers(roomId: string): Player[] { + return this.lifecycleService.getRoomPlayers(roomId); } } \ 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 851750a..659e636 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -1,9 +1,8 @@ -import type { TickData } from "../../GameLoop"; -import type { playerTypes } from "@repo/shared"; +import type { TickData } from "../../loop/GameLoop"; +import type { gridMapTypes, playerTypes, roomTypes } from "@repo/shared"; export interface StartGamePort { - addPlayer(id: string): void; - startGameLoop( + startRoomSession( roomId: string, playerIds: string[], onTick: (data: TickData) => void, @@ -13,7 +12,7 @@ } export interface ReadyForGamePort { - getPlayersByIds(playerIds: string[]): playerTypes.PlayerData[]; + getRoomPlayers(roomId: string): playerTypes.PlayerData[]; getRoomStartTime(roomId: string): number | undefined; } @@ -24,3 +23,17 @@ 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; + publishMapCellUpdatesToRoom( + roomId: roomTypes.Room["roomId"], + cellUpdates: gridMapTypes.CellUpdate[] + ): void; + publishGameEndToRoom(roomId: roomTypes.Room["roomId"]): void; + publishGameStartToRoom(roomId: roomTypes.Room["roomId"], payload: { startTime: number }): void; + publishCurrentPlayersToSocket(players: playerTypes.PlayerData[]): void; + publishGameStartToSocket(payload: { startTime: number }): void; + publishPlayerRemovedToRoom(roomId: roomTypes.Room["roomId"], removedPlayerId: string): void; +} diff --git a/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts b/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts new file mode 100644 index 0000000..9c039c7 --- /dev/null +++ b/apps/server/src/domains/game/application/services/GamePlayerOperationService.ts @@ -0,0 +1,58 @@ +import { logEvent } from "@server/logging/logEvent"; +import { GameRoomSession } from "./GameRoomSession"; + +type SessionStore = Map; +type PlayerRoomIndex = Map; + +export class GamePlayerOperationService { + constructor( + private sessions: SessionStore, + private playerToRoom: PlayerRoomIndex + ) {} + + public movePlayer(id: string, x: number, y: number): void { + const roomId = this.playerToRoom.get(id); + if (!roomId) { + logEvent("GameSessionService", { + event: "MOVE", + result: "ignored_player_not_in_session", + socketId: id, + }); + return; + } + + this.sessions.get(roomId)?.movePlayer(id, x, y); + } + + public removePlayer(id: string): void { + const roomId = this.playerToRoom.get(id); + if (!roomId) { + logEvent("GameSessionService", { + event: "REMOVE_PLAYER", + result: "ignored_player_not_in_session", + socketId: id, + }); + return; + } + + const session = this.sessions.get(roomId); + if (!session) { + this.playerToRoom.delete(id); + return; + } + + const removed = session.removePlayer(id); + this.playerToRoom.delete(id); + + if (removed && session.isEmpty()) { + session.dispose(); + this.sessions.delete(roomId); + logEvent("GameSessionService", { + event: "REMOVE_PLAYER", + result: "session_disposed_empty_room", + roomId, + socketId: id, + }); + } + } +} \ No newline at end of file diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts new file mode 100644 index 0000000..4ac08b7 --- /dev/null +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -0,0 +1,107 @@ +import { logEvent } from "@server/logging/logEvent"; +import { GameLoop, type TickData } from "../../loop/GameLoop"; +import { Player } from "../../entities/player/Player.js"; +import { MapStore } from "../../entities/map/MapStore"; +import { createSpawnedPlayer } from "../../entities/player/playerSpawn.js"; +import { + isValidPosition, + setPlayerPosition, +} from "../../entities/player/playerMovement.js"; + +export class GameRoomSession { + private players: Map; + private mapStore: MapStore; + private gameLoop: GameLoop | null = null; + private startTime: number | undefined; + + constructor(private roomId: string, playerIds: string[]) { + this.players = new Map(); + this.mapStore = new MapStore(); + + playerIds.forEach((playerId) => { + const player = createSpawnedPlayer(playerId); + this.players.set(playerId, player); + }); + } + + public start( + tickRate: number, + onTick: (data: TickData) => void, + onGameEnd: () => void + ): void { + if (this.gameLoop) { + return; + } + + this.startTime = Date.now(); + this.gameLoop = new GameLoop( + this.roomId, + tickRate, + this.getPlayerIds(), + this.players, + this.mapStore, + onTick, + () => { + this.dispose(); + onGameEnd(); + } + ); + + this.gameLoop.start(); + } + + public movePlayer(id: string, x: number, y: number): void { + const player = this.players.get(id); + if (!player) { + logEvent("GameRoomSession", { + event: "MOVE", + result: "ignored_player_not_found", + roomId: this.roomId, + socketId: id, + }); + return; + } + + if (!isValidPosition(x, y)) { + logEvent("GameRoomSession", { + event: "MOVE", + result: "ignored_invalid_payload", + roomId: this.roomId, + socketId: id, + }); + return; + } + + setPlayerPosition(player, x, y); + } + + public removePlayer(id: string): boolean { + return this.players.delete(id); + } + + public getStartTime(): number | undefined { + return this.startTime; + } + + public getPlayers(): Player[] { + return Array.from(this.players.values()); + } + + public hasPlayer(id: string): boolean { + return this.players.has(id); + } + + public getPlayerIds(): string[] { + return Array.from(this.players.keys()); + } + + public isEmpty(): boolean { + return this.players.size === 0; + } + + public dispose(): void { + this.gameLoop?.stop(); + this.gameLoop = null; + this.startTime = undefined; + } +} \ No newline at end of file diff --git a/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts new file mode 100644 index 0000000..333d259 --- /dev/null +++ b/apps/server/src/domains/game/application/services/GameSessionLifecycleService.ts @@ -0,0 +1,67 @@ +import { config } from "@repo/shared"; +import { type TickData } from "../../loop/GameLoop"; +import { logEvent } from "@server/logging/logEvent"; +import { GameRoomSession } from "./GameRoomSession"; + +type SessionStore = Map; +type PlayerRoomIndex = Map; + +export class GameSessionLifecycleService { + constructor( + private sessions: SessionStore, + private playerToRoom: PlayerRoomIndex + ) {} + + public getRoomStartTime(roomId: string): number | undefined { + return this.sessions.get(roomId)?.getStartTime(); + } + + public getRoomPlayers(roomId: string) { + return this.sessions.get(roomId)?.getPlayers() ?? []; + } + + public startRoomSession( + roomId: string, + playerIds: string[], + onTick: (data: TickData) => void, + onGameEnd: () => void + ) { + if (this.sessions.has(roomId)) { + logEvent("GameSessionService", { + event: "START_GAME_LOOP", + result: "ignored_already_running", + roomId, + }); + return; + } + + const tickRate = config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS; + const session = new GameRoomSession(roomId, playerIds); + + playerIds.forEach((playerId) => { + this.playerToRoom.set(playerId, roomId); + }); + + this.sessions.set(roomId, session); + session.start(tickRate, onTick, () => { + this.clearRoomPlayerIndex(roomId); + this.sessions.delete(roomId); + onGameEnd(); + }); + + logEvent("GameSessionService", { + event: "START_GAME_LOOP", + result: "started", + roomId, + playerCount: playerIds.length, + }); + } + + private clearRoomPlayerIndex(roomId: string): void { + Array.from(this.playerToRoom.entries()).forEach(([playerId, mappedRoomId]) => { + if (mappedRoomId === roomId) { + this.playerToRoom.delete(playerId); + } + }); + } +} \ No newline at end of file diff --git a/apps/server/src/domains/game/application/services/GameSessionService.ts b/apps/server/src/domains/game/application/services/GameSessionService.ts deleted file mode 100644 index 48191cf..0000000 --- a/apps/server/src/domains/game/application/services/GameSessionService.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { config } from "@repo/shared"; -import { GameLoop, type TickData } from "../../GameLoop"; -import { Player } from "../../entities/Player.js"; -import { MapStore } from "../../states/MapStore"; -import { logEvent } from "@server/logging/logEvent"; - -export class GameSessionService { - private mapStores: Map; - private gameLoops: Map; - private roomStartTimes: Map; - - constructor(private players: Map) { - this.mapStores = new Map(); - this.gameLoops = new Map(); - this.roomStartTimes = new Map(); - } - - public getRoomStartTime(roomId: string): number | undefined { - return this.roomStartTimes.get(roomId); - } - - public startGameLoop( - roomId: string, - playerIds: string[], - onTick: (data: TickData) => void, - onGameEnd: () => void - ) { - if (this.gameLoops.has(roomId)) { - logEvent("GameSessionService", { - event: "START_GAME_LOOP", - result: "ignored_already_running", - roomId, - }); - return; - } - - const tickRate = config.GAME_CONFIG.PLAYER_POSITION_UPDATE_MS; - this.roomStartTimes.set(roomId, Date.now()); - const mapStore = this.mapStores.get(roomId) ?? new MapStore(); - this.mapStores.set(roomId, mapStore); - - const loop = new GameLoop( - roomId, - tickRate, - playerIds, - this.players, - mapStore, - onTick, - () => { - this.roomStartTimes.delete(roomId); - this.gameLoops.delete(roomId); - this.mapStores.delete(roomId); - onGameEnd(); - } - ); - - loop.start(); - this.gameLoops.set(roomId, loop); - logEvent("GameSessionService", { - event: "START_GAME_LOOP", - result: "started", - roomId, - playerCount: playerIds.length, - }); - } -} diff --git a/apps/server/src/domains/game/application/services/PlayerRegistry.ts b/apps/server/src/domains/game/application/services/PlayerRegistry.ts deleted file mode 100644 index fc263ce..0000000 --- a/apps/server/src/domains/game/application/services/PlayerRegistry.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { config } from "@repo/shared"; -import { Player } from "../../entities/Player.js"; -import { logEvent } from "@server/logging/logEvent"; - -export class PlayerRegistry { - private players: Map; - - constructor() { - this.players = new Map(); - } - - public addPlayer(id: string): Player { - const player = new Player(id); - player.x = config.GAME_CONFIG.GRID_COLS / 2; - player.y = config.GAME_CONFIG.GRID_ROWS / 2; - this.players.set(id, player); - logEvent("PlayerRegistry", { - event: "PLAYER_ADD", - result: "added", - socketId: id, - totalPlayers: this.players.size, - }); - return player; - } - - public removePlayer(id: string) { - const existed = this.players.delete(id); - if (existed) { - logEvent("PlayerRegistry", { - event: "PLAYER_REMOVE", - result: "removed", - socketId: id, - totalPlayers: this.players.size, - }); - } else { - logEvent("PlayerRegistry", { - event: "PLAYER_REMOVE", - result: "ignored_not_found", - socketId: id, - }); - } - } - - public getPlayer(id: string): Player | undefined { - return this.players.get(id); - } - - public movePlayer(id: string, x: number, y: number) { - const player = this.players.get(id); - if (player) { - logEvent("PlayerRegistry", { - event: "MOVE", - result: "received", - socketId: id, - x: Math.round(x), - y: Math.round(y), - }); - if (typeof x !== "number" || typeof y !== "number" || isNaN(x) || isNaN(y)) { - logEvent("PlayerRegistry", { - event: "MOVE", - result: "ignored_invalid_payload", - socketId: id, - }); - return; - } - player.x = x; - player.y = y; - } else { - logEvent("PlayerRegistry", { - event: "MOVE", - result: "ignored_player_not_found", - socketId: id, - }); - } - } - - public getPlayersRef(): Map { - return this.players; - } -} diff --git a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts index ce9ce89..865ec74 100644 --- a/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/disconnectUseCase.ts @@ -1,19 +1,25 @@ -import type { DisconnectPlayerPort } from "../ports/gameUseCasePorts"; +import type { DisconnectPlayerPort, GameOutputPort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; type DisconnectUseCaseParams = { gameManager: DisconnectPlayerPort; + roomId?: string; playerId: string; - publishPlayerRemoved: (playerId: string) => void; + output: Pick; }; export const disconnectUseCase = ({ gameManager, + roomId, playerId, - publishPlayerRemoved, + output, }: DisconnectUseCaseParams) => { gameManager.removePlayer(playerId); - publishPlayerRemoved(playerId); + + if (roomId) { + output.publishPlayerRemovedToRoom(roomId, playerId); + } + logEvent("GameUseCase", { event: "DISCONNECT", result: "player_removed", diff --git a/apps/server/src/domains/game/application/useCases/pingUseCase.ts b/apps/server/src/domains/game/application/useCases/pingUseCase.ts index d71fb20..42e8fbd 100644 --- a/apps/server/src/domains/game/application/useCases/pingUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/pingUseCase.ts @@ -1,13 +1,15 @@ +import type { GameOutputPort } from "../ports/gameUseCasePorts"; + type PingUseCaseParams = { clientTime: number; - publishPong: (payload: { clientTime: number; serverTime: number }) => void; + output: Pick; }; export const pingUseCase = ({ clientTime, - publishPong, + output, }: PingUseCaseParams) => { - publishPong({ + output.publishPongToSocket({ clientTime, serverTime: Date.now(), }); diff --git a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index 1084d78..f8a72c0 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -1,26 +1,32 @@ import type { ReadyForGamePort } from "../ports/gameUseCasePorts"; -import type { playerTypes } from "@repo/shared"; +import type { GameOutputPort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; type ReadyForGameUseCaseParams = { socketId: string; roomId?: string; - playerIds: string[]; gameManager: ReadyForGamePort; - publishCurrentPlayers: (players: playerTypes.PlayerData[]) => void; - publishGameStart: (payload: { startTime: number }) => void; + output: Pick; }; export const readyForGameUseCase = ({ socketId, roomId, - playerIds, gameManager, - publishCurrentPlayers, - publishGameStart, + output, }: ReadyForGameUseCaseParams) => { - const roomPlayers = gameManager.getPlayersByIds(playerIds); - publishCurrentPlayers(roomPlayers); + if (!roomId) { + output.publishCurrentPlayersToSocket([]); + logEvent("GameUseCase", { + event: "READY_FOR_GAME", + result: "ignored_missing_room", + socketId, + }); + return; + } + + const roomPlayers = gameManager.getRoomPlayers(roomId); + output.publishCurrentPlayersToSocket(roomPlayers); logEvent("GameUseCase", { event: "READY_FOR_GAME", @@ -30,21 +36,12 @@ totalPlayers: roomPlayers.length, }); - if (!roomId) { - logEvent("GameUseCase", { - event: "READY_FOR_GAME", - result: "ignored_missing_room", - socketId, - }); - return; - } - const startTime = gameManager.getRoomStartTime(roomId); if (!startTime) { return; } - publishGameStart({ startTime }); + output.publishGameStartToSocket({ startTime }); logEvent("GameUseCase", { event: "GAME_START", result: "emitted", diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index aa45777..e492412 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -1,27 +1,26 @@ import { roomConsts } from "@repo/shared"; -import type { gridMapTypes, playerTypes } from "@repo/shared"; import { RoomManager } from "@server/domains/room/RoomManager"; -import type { StartGamePort } from "../ports/gameUseCasePorts"; +import type { GameOutputPort, StartGamePort } from "../ports/gameUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; type StartGameUseCaseParams = { ownerId: string; gameManager: StartGamePort; roomManager: RoomManager; - publishUpdatePlayer: (roomId: string, playerData: playerTypes.PlayerData) => void; - publishMapCellUpdates: (roomId: string, cellUpdates: gridMapTypes.CellUpdate[]) => void; - publishGameEnd: (roomId: string) => void; - publishGameStart: (roomId: string, payload: { startTime: number }) => void; + output: Pick< + GameOutputPort, + | "publishUpdatePlayerToRoom" + | "publishMapCellUpdatesToRoom" + | "publishGameEndToRoom" + | "publishGameStartToRoom" + >; }; export const startGameUseCase = ({ ownerId, gameManager, roomManager, - publishUpdatePlayer, - publishMapCellUpdates, - publishGameEnd, - publishGameStart, + output, }: StartGameUseCaseParams) => { const room = roomManager.getRoomByOwnerId(ownerId); if (!room) { @@ -55,20 +54,16 @@ const playerIds = room.players.map((p: { id: string }) => p.id); - room.players.forEach((p: { id: string }) => { - gameManager.addPlayer(p.id); - }); - - gameManager.startGameLoop( + gameManager.startRoomSession( room.roomId, playerIds, (tickData) => { tickData.players.forEach((playerData) => { - publishUpdatePlayer(room.roomId, playerData); + output.publishUpdatePlayerToRoom(room.roomId, playerData); }); if (tickData.cellUpdates.length > 0) { - publishMapCellUpdates(room.roomId, tickData.cellUpdates); + output.publishMapCellUpdatesToRoom(room.roomId, tickData.cellUpdates); } }, () => { @@ -78,11 +73,11 @@ roomId: room.roomId, reason: "duration_elapsed", }); - publishGameEnd(room.roomId); + output.publishGameEndToRoom(room.roomId); room.status = roomConsts.RoomPhase.WAITING; } ); const startTime = gameManager.getRoomStartTime(room.roomId) || Date.now(); - publishGameStart(room.roomId, { startTime }); + output.publishGameStartToRoom(room.roomId, { startTime }); }; diff --git a/apps/server/src/domains/game/entities/Player.ts b/apps/server/src/domains/game/entities/Player.ts deleted file mode 100644 index 6e16617..0000000 --- a/apps/server/src/domains/game/entities/Player.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { playerTypes } from "@repo/shared"; -import { config } from "@repo/shared"; - -// サーバー側保持プレイヤー状態モデル -export class Player implements playerTypes.PlayerData { - public id: string; - public x: number = 0; - public y: number = 0; - public teamId: number; - - constructor(id: string) { - this.id = id; - - // GAME_CONFIGからチーム数を動的に取得して割り当て - const teamCount = config.GAME_CONFIG.TEAM_COLORS.length; - this.teamId = Math.floor(Math.random() * teamCount); - } -} \ No newline at end of file diff --git a/apps/server/src/domains/game/entities/map/MapStore.ts b/apps/server/src/domains/game/entities/map/MapStore.ts new file mode 100644 index 0000000..15b03f8 --- /dev/null +++ b/apps/server/src/domains/game/entities/map/MapStore.ts @@ -0,0 +1,37 @@ +// apps/server/src/domains/game/entities/map/MapStore.ts +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[]; + // 次回の送信ループで送る差分リスト + private pendingUpdates: gridMapTypes.CellUpdate[]; + + constructor() { + // 初期状態は -1 (無色) などで初期化 + this.gridColors = createInitialGridColors(); + this.pendingUpdates = []; + } + + /** + * マスを塗り、色が変化した場合のみ差分キューに追加する + */ + public paintCell(index: number, teamId: number): void { + paintCellIfChanged({ + gridColors: this.gridColors, + pendingUpdates: this.pendingUpdates, + index, + teamId, + }); + } + + /** + * 溜まっている差分を取得し、キューをクリアする(ループ送信時に使用) + */ + public getAndClearUpdates(): gridMapTypes.CellUpdate[] { + return drainPendingUpdates(this.pendingUpdates); + } +} \ No newline at end of file diff --git a/apps/server/src/domains/game/entities/map/mapGrid.ts b/apps/server/src/domains/game/entities/map/mapGrid.ts new file mode 100644 index 0000000..c08cc21 --- /dev/null +++ b/apps/server/src/domains/game/entities/map/mapGrid.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..c03eab1 --- /dev/null +++ b/apps/server/src/domains/game/entities/map/mapPainting.ts @@ -0,0 +1,22 @@ +import type { gridMapTypes } from "@repo/shared"; + +type PaintCellParams = { + gridColors: number[]; + pendingUpdates: gridMapTypes.CellUpdate[]; + index: number; + teamId: number; +}; + +export const paintCellIfChanged = ({ + gridColors, + pendingUpdates, + index, + teamId, +}: PaintCellParams): void => { + if (gridColors[index] === teamId) { + return; + } + + gridColors[index] = teamId; + pendingUpdates.push({ index, teamId }); +}; diff --git a/apps/server/src/domains/game/entities/map/mapUpdates.ts b/apps/server/src/domains/game/entities/map/mapUpdates.ts new file mode 100644 index 0000000..091ed0b --- /dev/null +++ b/apps/server/src/domains/game/entities/map/mapUpdates.ts @@ -0,0 +1,9 @@ +import type { gridMapTypes } from "@repo/shared"; + +export const drainPendingUpdates = ( + pendingUpdates: gridMapTypes.CellUpdate[] +): gridMapTypes.CellUpdate[] => { + const updates = [...pendingUpdates]; + pendingUpdates.length = 0; + return updates; +}; diff --git a/apps/server/src/domains/game/entities/player/Player.ts b/apps/server/src/domains/game/entities/player/Player.ts new file mode 100644 index 0000000..6e16617 --- /dev/null +++ b/apps/server/src/domains/game/entities/player/Player.ts @@ -0,0 +1,18 @@ +import type { playerTypes } from "@repo/shared"; +import { config } from "@repo/shared"; + +// サーバー側保持プレイヤー状態モデル +export class Player implements playerTypes.PlayerData { + public id: string; + public x: number = 0; + public y: number = 0; + public teamId: number; + + constructor(id: string) { + this.id = id; + + // GAME_CONFIGからチーム数を動的に取得して割り当て + const teamCount = config.GAME_CONFIG.TEAM_COLORS.length; + this.teamId = Math.floor(Math.random() * teamCount); + } +} \ No newline at end of file diff --git a/apps/server/src/domains/game/entities/player/playerMovement.ts b/apps/server/src/domains/game/entities/player/playerMovement.ts new file mode 100644 index 0000000..b7cd590 --- /dev/null +++ b/apps/server/src/domains/game/entities/player/playerMovement.ts @@ -0,0 +1,14 @@ +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, + y: number +): void => { + player.x = x; + player.y = y; +}; diff --git a/apps/server/src/domains/game/entities/player/playerPosition.ts b/apps/server/src/domains/game/entities/player/playerPosition.ts new file mode 100644 index 0000000..f0c825a --- /dev/null +++ b/apps/server/src/domains/game/entities/player/playerPosition.ts @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..064a690 --- /dev/null +++ b/apps/server/src/domains/game/entities/player/playerSpawn.ts @@ -0,0 +1,9 @@ +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; + player.y = config.GAME_CONFIG.GRID_ROWS / 2; + return player; +}; diff --git a/apps/server/src/domains/game/loop/GameLoop.ts b/apps/server/src/domains/game/loop/GameLoop.ts new file mode 100644 index 0000000..de3617c --- /dev/null +++ b/apps/server/src/domains/game/loop/GameLoop.ts @@ -0,0 +1,100 @@ +import { Player } from "../entities/player/Player.js"; +import { MapStore } from "../entities/map/MapStore"; +import { getPlayerGridIndex } from "../entities/player/playerPosition.js"; +import { config } from "@repo/shared"; +import type { gridMapTypes } from "@repo/shared"; +import { logEvent } from "@server/logging/logEvent"; + +// コールバックで渡すデータの型定義 +export interface TickData { + players: { + id: string; + x: number; + y: number; + teamId: number; + }[]; + cellUpdates: gridMapTypes.CellUpdate[]; +} + +export class GameLoop { + private loopId: NodeJS.Timeout | null = null; + private startTime: number = 0; + + constructor( + private roomId: string, + private tickRate: number, + private playerIds: string[], + private players: Map, + private mapStore: MapStore, + private onTick: (data: TickData) => void, + private onGameEnd: () => void // ゲーム終了時のコールバック + ) {} + + start() { + // 既にループが回っている場合は何もしない + if (this.loopId) return; + + this.startTime = Date.now(); + + this.loopId = setInterval(() => { + // 時間経過のチェック + const elapsedTimeMs = Date.now() - this.startTime; + if (elapsedTimeMs >= config.GAME_CONFIG.GAME_DURATION_SEC * 1000) { + // ゲーム終了時にループを止めて終了処理へ + this.stop(); + this.onGameEnd(); + return; // 今回のフレームの座標更新はスキップ + } + + const playersData: TickData["players"] = []; + + // 1. 各プレイヤーの座標処理とマス塗りの判定 + this.playerIds.forEach(id => { + const player = this.players.get(id); + if (!player) return; + + const gridIndex = getPlayerGridIndex(player); + if (gridIndex !== null) { + this.mapStore.paintCell(gridIndex, player.teamId); + } + + // 送信用のプレイヤーデータを構築 + playersData.push({ + id: player.id, + x: player.x, + y: player.y, + teamId: player.teamId, + }); + }); + + // 2. マスの差分(Diff)を取得 + const cellUpdates = this.mapStore.getAndClearUpdates(); + + // 3. 通信層(GameHandler)へデータを渡す + this.onTick({ + players: playersData, + cellUpdates: cellUpdates, + }); + + }, this.tickRate); + + logEvent("GameLoop", { + event: "GAME_LOOP", + result: "started", + roomId: this.roomId, + tickRate: this.tickRate, + }); + } + + stop() { + if (this.loopId) { + clearInterval(this.loopId); + this.loopId = null; + logEvent("GameLoop", { + event: "GAME_LOOP", + result: "stopped", + roomId: this.roomId, + }); + } + } +} \ No newline at end of file diff --git a/apps/server/src/domains/game/states/MapStore.ts b/apps/server/src/domains/game/states/MapStore.ts deleted file mode 100644 index 108308a..0000000 --- a/apps/server/src/domains/game/states/MapStore.ts +++ /dev/null @@ -1,36 +0,0 @@ -// apps/server/src/states/MapStore.ts -import type { gridMapTypes } from "@repo/shared"; -import { config } from "@repo/shared"; - -export class MapStore { - // 全マスの現在の色(teamId)を保持 - private gridColors: number[]; - // 次回の送信ループで送る差分リスト - private pendingUpdates: gridMapTypes.CellUpdate[]; - - constructor() { - // 初期状態は -1 (無色) などで初期化 - const totalCells = config.GAME_CONFIG.GRID_COLS * config.GAME_CONFIG.GRID_ROWS; - this.gridColors = new Array(totalCells).fill(-1); - this.pendingUpdates = []; - } - - /** - * マスを塗り、色が変化した場合のみ差分キューに追加する - */ - public paintCell(index: number, teamId: number): void { - if (this.gridColors[index] !== teamId) { - this.gridColors[index] = teamId; - this.pendingUpdates.push({ index, teamId }); - } - } - - /** - * 溜まっている差分を取得し、キューをクリアする(ループ送信時に使用) - */ - public getAndClearUpdates(): gridMapTypes.CellUpdate[] { - const updates = [...this.pendingUpdates]; - this.pendingUpdates = []; - return updates; - } -} \ 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 b00b3c8..defacee 100644 --- a/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts +++ b/apps/server/src/domains/room/application/ports/roomUseCasePorts.ts @@ -10,6 +10,12 @@ status: "joined" | "duplicate" | "full"; }; +/** ルームユースケースが利用する出力ポート */ +export interface RoomOutputPort { + publishRoomUpdateToRoom(roomId: roomTypes.Room["roomId"], room: roomTypes.Room): void; + publishJoinRejectedToSocket(payload: roomTypes.JoinRoomRejectedPayload): void; +} + /** ルーム参加ユースケースが利用する参加操作ポート */ export interface JoinRoomPort { addPlayerToRoom(roomId: string, socketId: string, playerName: string): JoinRoomResult; diff --git a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts index ecca7bd..8ab6699 100644 --- a/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts +++ b/apps/server/src/domains/room/application/useCases/joinRoomUseCase.ts @@ -3,14 +3,18 @@ * ルーム参加要求を処理し,状態更新を配信するユースケース */ import type { roomTypes } from "@repo/shared"; -import type { JoinRoomPort, JoinRoomResult } from "../ports/roomUseCasePorts"; +import type { + JoinRoomPort, + JoinRoomResult, + RoomOutputPort, +} from "../ports/roomUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; type JoinRoomUseCaseParams = { roomManager: JoinRoomPort; socketId: string; data: roomTypes.JoinRoomPayload; - publishRoomUpdate: (roomId: roomTypes.Room["roomId"], room: roomTypes.Room) => void; + output: RoomOutputPort; }; /** 参加イベントを受け取り,ルーム更新を配信する */ @@ -18,7 +22,7 @@ roomManager, socketId, data, - publishRoomUpdate, + output, }: JoinRoomUseCaseParams): JoinRoomResult => { const { roomId, playerName } = data; logEvent("RoomUseCase", { @@ -31,6 +35,11 @@ const joinResult = roomManager.addPlayerToRoom(roomId, socketId, playerName); if (joinResult.status !== "joined") { + output.publishJoinRejectedToSocket({ + roomId, + reason: joinResult.status, + }); + logEvent("RoomUseCase", { event: "JOIN_ROOM", result: "rejected", @@ -41,7 +50,7 @@ return joinResult; } - publishRoomUpdate(roomId, joinResult.room); + output.publishRoomUpdateToRoom(roomId, joinResult.room); return joinResult; }; diff --git a/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts b/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts index 1f3a4e1..18b7aaf 100644 --- a/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts +++ b/apps/server/src/domains/room/application/useCases/roomDisconnectUseCase.ts @@ -3,20 +3,20 @@ * 切断時のルーム退出処理と状態更新配信を行うユースケース */ import type { DisconnectRoomPort } from "../ports/roomUseCasePorts"; -import type { roomTypes } from "@repo/shared"; +import type { RoomOutputPort } from "../ports/roomUseCasePorts"; import { logEvent } from "@server/logging/logEvent"; type RoomDisconnectUseCaseParams = { roomManager: DisconnectRoomPort; socketId: string; - publishRoomUpdate: (roomId: roomTypes.Room["roomId"], room: roomTypes.Room) => void; + output: Pick; }; /** 切断ソケットを各ルームから退出させ,更新ルームを配信する */ export const roomDisconnectUseCase = ({ roomManager, socketId, - publishRoomUpdate, + output, }: RoomDisconnectUseCaseParams) => { const updatedRooms = roomManager.removePlayer(socketId); logEvent("RoomUseCase", { @@ -27,7 +27,7 @@ }); updatedRooms.forEach((room) => { - publishRoomUpdate(room.roomId, room); + output.publishRoomUpdateToRoom(room.roomId, room); logEvent("RoomUseCase", { event: "ROOM_UPDATE", result: "emitted", diff --git a/apps/server/src/network/handlers/game/createGameEventPublisher.ts b/apps/server/src/network/handlers/game/createGameEventPublisher.ts deleted file mode 100644 index 8d36b1e..0000000 --- a/apps/server/src/network/handlers/game/createGameEventPublisher.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * createGameEventPublisher - * ゲーム系ユースケースから利用する送信関数群を生成する - */ -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 { CommonHandlerContext } from "../CommonHandler"; - -type RoomId = roomTypes.Room["roomId"]; -type SocketId = playerTypes.PlayerData["id"]; -type PongPayload = { clientTime: number; serverTime: number }; -type GameStartPayload = { startTime: number }; -type CurrentPlayersPayload = playerTypes.PlayerData[]; -type UpdatePlayerPayload = playerTypes.PlayerData; -type MapCellUpdatesPayload = gridMapTypes.CellUpdate[]; - -/** ゲーム進行中イベントの送信インターフェース */ -export type GameEventPublisher = { - publishPongToSocket: (payload: PongPayload) => void; - publishUpdatePlayerToRoom: (roomId: RoomId, playerData: UpdatePlayerPayload) => void; - publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: MapCellUpdatesPayload) => void; - publishGameEndToRoom: (roomId: RoomId) => void; - publishGameStartToRoom: (roomId: RoomId, payload: GameStartPayload) => void; - publishCurrentPlayersToSocket: (players: CurrentPlayersPayload) => void; - publishGameStartToSocket: (payload: GameStartPayload) => void; -}; - -/** 切断時に配信するゲームイベントの送信インターフェース */ -export type GameDisconnectPublisher = { - publishPlayerRemovedToRoom: (roomId: RoomId, removedPlayerId: SocketId) => void; -}; - -/** 共通送信コンテキストからゲームイベント送信関数群を生成する */ -export const createGameEventPublisher = (common: CommonHandlerContext): GameEventPublisher => { - return { - publishPongToSocket: (payload: PongPayload) => { - common.emitToSocket(protocol.SocketEvents.PONG, payload); - }, - publishUpdatePlayerToRoom: (roomId: RoomId, playerData: UpdatePlayerPayload) => { - common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_PLAYER, playerData); - }, - publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: MapCellUpdatesPayload) => { - common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, cellUpdates); - }, - publishGameEndToRoom: (roomId: RoomId) => { - common.emitToRoom(roomId, protocol.SocketEvents.GAME_END); - }, - publishGameStartToRoom: (roomId: RoomId, payload: GameStartPayload) => { - common.emitToRoom(roomId, protocol.SocketEvents.GAME_START, payload); - }, - publishCurrentPlayersToSocket: (players: CurrentPlayersPayload) => { - common.emitToSocket(protocol.SocketEvents.CURRENT_PLAYERS, players); - }, - publishGameStartToSocket: (payload: GameStartPayload) => { - common.emitToSocket(protocol.SocketEvents.GAME_START, payload); - }, - }; -}; - -/** ゲーム切断時の送信関数群を生成する */ -export const createGameDisconnectPublisher = (io: Server): GameDisconnectPublisher => { - const emitToRoom = createEmitToRoom(io); - - return { - publishPlayerRemovedToRoom: (roomId: RoomId, removedPlayerId: SocketId) => { - emitToRoom(roomId, protocol.SocketEvents.REMOVE_PLAYER, removedPlayerId); - }, - }; -}; diff --git a/apps/server/src/network/handlers/game/createGameOutputAdapter.ts b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts new file mode 100644 index 0000000..3af44f2 --- /dev/null +++ b/apps/server/src/network/handlers/game/createGameOutputAdapter.ts @@ -0,0 +1,62 @@ +/** + * createGameOutputAdapter + * ゲーム系ユースケースから利用する送信関数群を生成する + */ +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 type { CommonHandlerContext } from "../CommonHandler"; + +type RoomId = roomTypes.Room["roomId"]; +type SocketId = playerTypes.PlayerData["id"]; +type PongPayload = { clientTime: number; serverTime: number }; +type GameStartPayload = { startTime: number }; +type CurrentPlayersPayload = playerTypes.PlayerData[]; +type UpdatePlayerPayload = playerTypes.PlayerData; +type MapCellUpdatesPayload = gridMapTypes.CellUpdate[]; + +/** ゲーム出力アダプターのインターフェース */ +export type GameOutputAdapter = Omit; + +/** ゲーム切断時の出力アダプターのインターフェース */ +export type GameDisconnectOutputAdapter = Pick; + +/** 共通送信コンテキストからゲーム出力アダプターを生成する */ +export const createGameOutputAdapter = (common: CommonHandlerContext): GameOutputAdapter => { + return { + publishPongToSocket: (payload: PongPayload) => { + common.emitToSocket(protocol.SocketEvents.PONG, payload); + }, + publishUpdatePlayerToRoom: (roomId: RoomId, playerData: UpdatePlayerPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_PLAYER, playerData); + }, + publishMapCellUpdatesToRoom: (roomId: RoomId, cellUpdates: MapCellUpdatesPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.UPDATE_MAP_CELLS, cellUpdates); + }, + publishGameEndToRoom: (roomId: RoomId) => { + common.emitToRoom(roomId, protocol.SocketEvents.GAME_END); + }, + publishGameStartToRoom: (roomId: RoomId, payload: GameStartPayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.GAME_START, payload); + }, + publishCurrentPlayersToSocket: (players: CurrentPlayersPayload) => { + common.emitToSocket(protocol.SocketEvents.CURRENT_PLAYERS, players); + }, + publishGameStartToSocket: (payload: GameStartPayload) => { + common.emitToSocket(protocol.SocketEvents.GAME_START, payload); + }, + }; +}; + +/** ゲーム切断時の送信関数群を生成する */ +export const createGameDisconnectOutputAdapter = (io: Server): GameDisconnectOutputAdapter => { + const emitToRoom = createEmitToRoom(io); + + return { + publishPlayerRemovedToRoom: (roomId: RoomId, removedPlayerId: SocketId) => { + emitToRoom(roomId, protocol.SocketEvents.REMOVE_PLAYER, removedPlayerId); + }, + }; +}; diff --git a/apps/server/src/network/handlers/game/handleGameDisconnect.ts b/apps/server/src/network/handlers/game/handleGameDisconnect.ts index 0a7be66..77af1c8 100644 --- a/apps/server/src/network/handlers/game/handleGameDisconnect.ts +++ b/apps/server/src/network/handlers/game/handleGameDisconnect.ts @@ -5,7 +5,7 @@ import { Server } from "socket.io"; import { GameManager } from "@server/domains/game/GameManager"; import { disconnectUseCase } from "@server/domains/game/application/useCases/disconnectUseCase"; -import { createGameDisconnectPublisher } from "./createGameEventPublisher"; +import { createGameDisconnectOutputAdapter } from "./createGameOutputAdapter"; /** 切断したプレイヤーをゲーム管理から除外し通知する */ export const handleGameDisconnect = ( @@ -14,17 +14,12 @@ roomId: string | undefined, playerId: string ) => { - const gameDisconnectPublisher = createGameDisconnectPublisher(io); + const gameDisconnectOutputAdapter = createGameDisconnectOutputAdapter(io); disconnectUseCase({ gameManager, + roomId, playerId, - publishPlayerRemoved: (removedPlayerId) => { - if (!roomId) { - return; - } - - gameDisconnectPublisher.publishPlayerRemovedToRoom(roomId, removedPlayerId); - }, + output: gameDisconnectOutputAdapter, }); }; diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 859a451..f64b1b0 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -6,13 +6,12 @@ import { GameManager } from "@server/domains/game/GameManager"; import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; -import type { playerTypes } 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 { movePlayerUseCase } from "@server/domains/game/application/useCases/movePlayerUseCase"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; -import { createGameEventPublisher } from "./createGameEventPublisher"; +import { createGameOutputAdapter } from "./createGameOutputAdapter"; import { logEvent } from "@server/logging/logEvent"; import { isMovePayload, isPingPayload } from "@server/network/validation/socketPayloadValidators"; @@ -24,7 +23,7 @@ roomManager: RoomManager ) => { const common = createCommonHandlerContext(io, socket); - const gamePublisher = createGameEventPublisher(common); + const gameOutputAdapter = createGameOutputAdapter(common); // 遅延計測用のPINGを検証しPONGを返す socket.on(protocol.SocketEvents.PING, (clientTime: unknown) => { @@ -39,7 +38,7 @@ pingUseCase({ clientTime, - publishPong: gamePublisher.publishPongToSocket, + output: gameOutputAdapter, }); }); @@ -49,27 +48,19 @@ ownerId: socket.id, gameManager, roomManager, - publishUpdatePlayer: gamePublisher.publishUpdatePlayerToRoom, - publishMapCellUpdates: gamePublisher.publishMapCellUpdatesToRoom, - publishGameEnd: gamePublisher.publishGameEndToRoom, - publishGameStart: gamePublisher.publishGameStartToRoom, + output: gameOutputAdapter, }); }); // 参加者の準備完了通知を受けて現在状態を返す socket.on(protocol.SocketEvents.READY_FOR_GAME, () => { const roomId = Array.from(socket.rooms).find((room) => room !== socket.id); - const playerIds = roomId - ? (roomManager.getRoomById(roomId)?.players ?? []).map((p) => p.id) - : []; readyForGameUseCase({ socketId: socket.id, roomId, - playerIds, gameManager, - publishCurrentPlayers: gamePublisher.publishCurrentPlayersToSocket, - publishGameStart: gamePublisher.publishGameStartToSocket, + output: gameOutputAdapter, }); }); diff --git a/apps/server/src/network/handlers/room/createRoomEventPublisher.ts b/apps/server/src/network/handlers/room/createRoomEventPublisher.ts deleted file mode 100644 index 5e2bf4d..0000000 --- a/apps/server/src/network/handlers/room/createRoomEventPublisher.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * createRoomEventPublisher - * ルーム系ユースケースから利用する送信関数を生成する - */ -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 { CommonHandlerContext } from "../CommonHandler"; - -type RoomId = roomTypes.Room["roomId"]; -type RoomUpdatePayload = roomTypes.Room; - -/** ルーム更新イベントの送信インターフェース */ -export type RoomEventPublisher = { - publishRoomUpdate: (roomId: RoomId, room: RoomUpdatePayload) => void; - publishJoinRejected: (payload: roomTypes.JoinRoomRejectedPayload) => void; -}; - -/** 共通送信コンテキストからルームイベント送信関数を生成する */ -export const createRoomEventPublisher = ( - common: CommonHandlerContext -): RoomEventPublisher => { - return { - publishRoomUpdate: (roomId: RoomId, room: RoomUpdatePayload) => { - common.emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); - }, - publishJoinRejected: (payload: roomTypes.JoinRoomRejectedPayload) => { - common.emitToSocket(protocol.SocketEvents.ROOM_JOIN_REJECTED, payload); - }, - }; -}; - -/** 切断時のルーム更新送信関数を生成する */ -export const createRoomDisconnectPublisher = (io: Server): RoomEventPublisher => { - const emitToRoom = createEmitToRoom(io); - - return { - publishRoomUpdate: (roomId: RoomId, room: RoomUpdatePayload) => { - emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); - }, - publishJoinRejected: () => { - return; - }, - }; -}; diff --git a/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts b/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts new file mode 100644 index 0000000..932d2d2 --- /dev/null +++ b/apps/server/src/network/handlers/room/createRoomOutputAdapter.ts @@ -0,0 +1,43 @@ +/** + * createRoomOutputAdapter + * ルーム系ユースケースから利用する送信関数を生成する + */ +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 type { CommonHandlerContext } from "../CommonHandler"; + +type RoomId = roomTypes.Room["roomId"]; +type RoomUpdatePayload = roomTypes.Room; + +/** ルーム出力アダプターのインターフェース */ +export type RoomOutputAdapter = RoomOutputPort; + +/** 共通送信コンテキストからルーム出力アダプターを生成する */ +export const createRoomOutputAdapter = ( + common: CommonHandlerContext +): RoomOutputAdapter => { + return { + publishRoomUpdateToRoom: (roomId: RoomId, room: RoomUpdatePayload) => { + common.emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); + }, + publishJoinRejectedToSocket: (payload: roomTypes.JoinRoomRejectedPayload) => { + common.emitToSocket(protocol.SocketEvents.ROOM_JOIN_REJECTED, payload); + }, + }; +}; + +/** 切断時のルーム出力アダプターを生成する */ +export const createRoomDisconnectOutputAdapter = ( + io: Server +): Pick => { + const emitToRoom = createEmitToRoom(io); + + return { + publishRoomUpdateToRoom: (roomId: RoomId, room: RoomUpdatePayload) => { + emitToRoom(roomId, protocol.SocketEvents.ROOM_UPDATE, room); + }, + }; +}; diff --git a/apps/server/src/network/handlers/room/handleRoomDisconnect.ts b/apps/server/src/network/handlers/room/handleRoomDisconnect.ts index c711d20..998b0f9 100644 --- a/apps/server/src/network/handlers/room/handleRoomDisconnect.ts +++ b/apps/server/src/network/handlers/room/handleRoomDisconnect.ts @@ -5,7 +5,7 @@ import { Server, Socket } from "socket.io"; import { RoomManager } from "@server/domains/room/RoomManager"; import { roomDisconnectUseCase } from "@server/domains/room/application/useCases/roomDisconnectUseCase"; -import { createRoomDisconnectPublisher } from "./createRoomEventPublisher"; +import { createRoomDisconnectOutputAdapter } from "./createRoomOutputAdapter"; /** 切断ソケットのルーム離脱処理を実行して更新通知する */ export const handleRoomDisconnect = ( @@ -13,11 +13,11 @@ socket: Socket, roomManager: RoomManager ) => { - const roomDisconnectPublisher = createRoomDisconnectPublisher(io); + const roomDisconnectOutputAdapter = createRoomDisconnectOutputAdapter(io); roomDisconnectUseCase({ roomManager, socketId: socket.id, - publishRoomUpdate: roomDisconnectPublisher.publishRoomUpdate, + output: roomDisconnectOutputAdapter, }); }; diff --git a/apps/server/src/network/handlers/room/registerRoomHandlers.ts b/apps/server/src/network/handlers/room/registerRoomHandlers.ts index d1000ce..d5729b1 100644 --- a/apps/server/src/network/handlers/room/registerRoomHandlers.ts +++ b/apps/server/src/network/handlers/room/registerRoomHandlers.ts @@ -5,10 +5,9 @@ import { Server, Socket } from "socket.io"; import { RoomManager } from "@server/domains/room/RoomManager"; import { protocol } from "@repo/shared"; -import type { roomTypes } from "@repo/shared"; import { joinRoomUseCase } from "@server/domains/room/application/useCases/joinRoomUseCase"; import { createCommonHandlerContext } from "@server/network/handlers/CommonHandler"; -import { createRoomEventPublisher } from "./createRoomEventPublisher"; +import { createRoomOutputAdapter } from "./createRoomOutputAdapter"; import { isJoinRoomPayload } from "@server/network/validation/socketPayloadValidators"; import { logEvent } from "@server/logging/logEvent"; @@ -19,7 +18,7 @@ roomManager: RoomManager ) => { const common = createCommonHandlerContext(io, socket); - const roomPublisher = createRoomEventPublisher(common); + const roomOutputAdapter = createRoomOutputAdapter(common); // 参加要求のペイロード検証と参加処理を実行する socket.on(protocol.SocketEvents.JOIN_ROOM, (data: unknown) => { @@ -33,24 +32,17 @@ } const { roomId } = data; - let joinedRoom: roomTypes.Room | null = null; const joinResult = joinRoomUseCase({ roomManager, socketId: socket.id, data, - publishRoomUpdate: (_roomId, room) => { - joinedRoom = room; - }, + output: roomOutputAdapter, }); // 参加拒否時は理由を通知する switch (joinResult.status) { case "full": - roomPublisher.publishJoinRejected({ - roomId, - reason: "full", - }); logEvent("Network", { event: "JOIN_ROOM", result: "rejected_room_full", @@ -60,10 +52,6 @@ return; case "duplicate": - roomPublisher.publishJoinRejected({ - roomId, - reason: "duplicate", - }); logEvent("Network", { event: "JOIN_ROOM", result: "rejected_duplicate", @@ -74,7 +62,7 @@ case "joined": socket.join(roomId); - roomPublisher.publishRoomUpdate(roomId, joinResult.room); + roomOutputAdapter.publishRoomUpdateToRoom(roomId, joinResult.room); logEvent("RoomUseCase", { event: "ROOM_UPDATE", result: "emitted",