diff --git a/apps/server/src/domains/game/GameManager.ts b/apps/server/src/domains/game/GameManager.ts index 43eee46..cd8ab82 100644 --- a/apps/server/src/domains/game/GameManager.ts +++ b/apps/server/src/domains/game/GameManager.ts @@ -1,16 +1,13 @@ -import { GameLoop, type TickData } from "./GameLoop"; +import { type TickData } from "./GameLoop"; import { Player } from "./entities/Player.js"; -import { PlayerRegistry } from "./application/services/PlayerRegistry"; import { GameSessionService } from "./application/services/GameSessionService"; // プレイヤー集合の生成・更新・参照管理クラス export class GameManager { - private playerRegistry: PlayerRegistry; private gameSessionService: GameSessionService; constructor() { - this.playerRegistry = new PlayerRegistry(); - this.gameSessionService = new GameSessionService(this.playerRegistry.getPlayersRef()); + this.gameSessionService = new GameSessionService(); } // 外部(GameHandlerなど)から開始時刻を取得できるようにする @@ -18,19 +15,14 @@ return this.gameSessionService.getRoomStartTime(roomId); } - // 新規プレイヤー登録と初期位置設定処理 - addPlayer(id: string): Player { - return this.playerRegistry.addPlayer(id); - } - // プレイヤー登録解除処理 removePlayer(id: string) { - this.playerRegistry.removePlayer(id); + this.gameSessionService.removePlayer(id); } // 指定プレイヤー座標更新処理 movePlayer(id: string, x: number, y: number) { - this.playerRegistry.movePlayer(id, x, y); + this.gameSessionService.movePlayer(id, x, y); } /** @@ -48,10 +40,8 @@ this.gameSessionService.startGameLoop(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.gameSessionService.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..2d41bd2 100644 --- a/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts +++ b/apps/server/src/domains/game/application/ports/gameUseCasePorts.ts @@ -2,7 +2,6 @@ import type { playerTypes } from "@repo/shared"; export interface StartGamePort { - addPlayer(id: string): void; startGameLoop( roomId: string, playerIds: string[], @@ -13,7 +12,7 @@ } export interface ReadyForGamePort { - getPlayersByIds(playerIds: string[]): playerTypes.PlayerData[]; + getRoomPlayers(roomId: string): playerTypes.PlayerData[]; getRoomStartTime(roomId: string): number | undefined; } 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..f347020 --- /dev/null +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -0,0 +1,106 @@ +import { config } from "@repo/shared"; +import { logEvent } from "@server/logging/logEvent"; +import { GameLoop, type TickData } from "../../GameLoop"; +import { Player } from "../../entities/Player.js"; +import { MapStore } from "../../states/MapStore"; + +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 = new Player(playerId); + player.x = config.GAME_CONFIG.GRID_COLS / 2; + player.y = config.GAME_CONFIG.GRID_ROWS / 2; + 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 (typeof x !== "number" || typeof y !== "number" || isNaN(x) || isNaN(y)) { + logEvent("GameRoomSession", { + event: "MOVE", + result: "ignored_invalid_payload", + roomId: this.roomId, + socketId: id, + }); + return; + } + + player.x = x; + player.y = 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/GameSessionService.ts b/apps/server/src/domains/game/application/services/GameSessionService.ts index 48191cf..53e9075 100644 --- a/apps/server/src/domains/game/application/services/GameSessionService.ts +++ b/apps/server/src/domains/game/application/services/GameSessionService.ts @@ -1,22 +1,23 @@ import { config } from "@repo/shared"; -import { GameLoop, type TickData } from "../../GameLoop"; -import { Player } from "../../entities/Player.js"; -import { MapStore } from "../../states/MapStore"; +import { type TickData } from "../../GameLoop"; import { logEvent } from "@server/logging/logEvent"; +import { GameRoomSession } from "./GameRoomSession"; export class GameSessionService { - private mapStores: Map; - private gameLoops: Map; - private roomStartTimes: Map; + private sessions: Map; + private playerToRoom: Map; - constructor(private players: Map) { - this.mapStores = new Map(); - this.gameLoops = new Map(); - this.roomStartTimes = new Map(); + constructor() { + this.sessions = new Map(); + this.playerToRoom = new Map(); } public getRoomStartTime(roomId: string): number | undefined { - return this.roomStartTimes.get(roomId); + return this.sessions.get(roomId)?.getStartTime(); + } + + public getRoomPlayers(roomId: string) { + return this.sessions.get(roomId)?.getPlayers() ?? []; } public startGameLoop( @@ -25,7 +26,7 @@ onTick: (data: TickData) => void, onGameEnd: () => void ) { - if (this.gameLoops.has(roomId)) { + if (this.sessions.has(roomId)) { logEvent("GameSessionService", { event: "START_GAME_LOOP", result: "ignored_already_running", @@ -35,27 +36,19 @@ } 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 session = new GameRoomSession(roomId, playerIds); - const loop = new GameLoop( - roomId, - tickRate, - playerIds, - this.players, - mapStore, - onTick, - () => { - this.roomStartTimes.delete(roomId); - this.gameLoops.delete(roomId); - this.mapStores.delete(roomId); - onGameEnd(); - } - ); + playerIds.forEach((playerId) => { + this.playerToRoom.set(playerId, roomId); + }); - loop.start(); - this.gameLoops.set(roomId, loop); + 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", @@ -63,4 +56,58 @@ playerCount: playerIds.length, }); } + + 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, + }); + } + } + + private clearRoomPlayerIndex(roomId: string): void { + Array.from(this.playerToRoom.entries()).forEach(([playerId, mappedRoomId]) => { + if (mappedRoomId === roomId) { + this.playerToRoom.delete(playerId); + } + }); + } } 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/readyForGameUseCase.ts b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts index 1084d78..067a1e2 100644 --- a/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/readyForGameUseCase.ts @@ -5,7 +5,6 @@ type ReadyForGameUseCaseParams = { socketId: string; roomId?: string; - playerIds: string[]; gameManager: ReadyForGamePort; publishCurrentPlayers: (players: playerTypes.PlayerData[]) => void; publishGameStart: (payload: { startTime: number }) => void; @@ -14,12 +13,21 @@ export const readyForGameUseCase = ({ socketId, roomId, - playerIds, gameManager, publishCurrentPlayers, publishGameStart, }: ReadyForGameUseCaseParams) => { - const roomPlayers = gameManager.getPlayersByIds(playerIds); + if (!roomId) { + publishCurrentPlayers([]); + logEvent("GameUseCase", { + event: "READY_FOR_GAME", + result: "ignored_missing_room", + socketId, + }); + return; + } + + const roomPlayers = gameManager.getRoomPlayers(roomId); publishCurrentPlayers(roomPlayers); logEvent("GameUseCase", { @@ -30,15 +38,6 @@ 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; diff --git a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts index aa45777..835e92a 100644 --- a/apps/server/src/domains/game/application/useCases/startGameUseCase.ts +++ b/apps/server/src/domains/game/application/useCases/startGameUseCase.ts @@ -55,10 +55,6 @@ const playerIds = room.players.map((p: { id: string }) => p.id); - room.players.forEach((p: { id: string }) => { - gameManager.addPlayer(p.id); - }); - gameManager.startGameLoop( room.roomId, playerIds, diff --git a/apps/server/src/network/handlers/game/registerGameHandlers.ts b/apps/server/src/network/handlers/game/registerGameHandlers.ts index 859a451..29d3511 100644 --- a/apps/server/src/network/handlers/game/registerGameHandlers.ts +++ b/apps/server/src/network/handlers/game/registerGameHandlers.ts @@ -59,14 +59,10 @@ // 参加者の準備完了通知を受けて現在状態を返す 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,