diff --git a/apps/client/src/scenes/result/ResultScene.tsx b/apps/client/src/scenes/result/ResultScene.tsx index 14388dd..d801ebd 100644 --- a/apps/client/src/scenes/result/ResultScene.tsx +++ b/apps/client/src/scenes/result/ResultScene.tsx @@ -1,3 +1,8 @@ +/** + * ResultScene + * ゲーム終了後の順位一覧を表示する結果画面コンポーネント + * 順位,チーム名,塗り率の3項目をテーブル形式で描画する + */ import type { GameResultPayload } from "@repo/shared"; type Props = { @@ -6,6 +11,7 @@ const formatPaintRate = (value: number): string => `${value.toFixed(1)}%`; +/** 最終結果データを受け取り,順位一覧を表示する */ export const ResultScene = ({ result }: Props) => { if (!result) { return
結果を読み込み中...
; diff --git a/apps/server/src/domains/game/application/services/GameRoomSession.ts b/apps/server/src/domains/game/application/services/GameRoomSession.ts index 5f044c2..02847cb 100644 --- a/apps/server/src/domains/game/application/services/GameRoomSession.ts +++ b/apps/server/src/domains/game/application/services/GameRoomSession.ts @@ -8,7 +8,6 @@ logResults, logScopes, } from "@server/logging/index"; -import { config } from "@repo/shared"; import type { gameTypes, GameResultPayload } from "@repo/shared"; import { GameLoop } from "../../loop/GameLoop"; import { Player } from "../../entities/player/Player.js"; @@ -18,12 +17,11 @@ isValidPosition, setPlayerPosition, } from "../../entities/player/playerMovement.js"; +import { buildGameResultPayload } from "./gameResultCalculator.js"; // 💡 追加: チーム割り当てサービスをインポート import { TeamAssignmentService } from "../services/TeamAssignmentService.js"; -const TEAM_NAMES = ["赤チーム", "青チーム", "緑チーム", "黄チーム"] as const; - /** ルーム単位のゲーム状態とループ進行を保持するセッションクラス */ export class GameRoomSession { private players: Map; @@ -68,7 +66,7 @@ this.mapStore, onTick, () => { - const resultPayload = this.buildGameResultPayload(); + const resultPayload = buildGameResultPayload(this.mapStore.getGridColorsSnapshot()); this.dispose(); onGameEnd(resultPayload); }, @@ -125,49 +123,4 @@ } this.players.clear(); } - - private buildGameResultPayload(): GameResultPayload { - const { TEAM_COUNT } = config.GAME_CONFIG; - const gridColors = this.mapStore.getGridColorsSnapshot(); - const totalCells = gridColors.length; - const paintedCounts = new Array(TEAM_COUNT).fill(0); - - gridColors.forEach((teamId) => { - if (!Number.isInteger(teamId) || teamId < 0 || teamId >= TEAM_COUNT) { - return; - } - - paintedCounts[teamId] += 1; - }); - - const rankings = paintedCounts - .map((paintedCellCount, teamId) => ({ - rank: 0, - teamId, - teamName: TEAM_NAMES[teamId] ?? `チーム${teamId + 1}`, - paintRate: totalCells > 0 ? (paintedCellCount / totalCells) * 100 : 0, - })) - .sort((a, b) => { - if (b.paintRate !== a.paintRate) { - return b.paintRate - a.paintRate; - } - - return a.teamId - b.teamId; - }); - - let currentRank = 0; - let previousPaintRate: number | null = null; - const epsilon = 1e-9; - - rankings.forEach((item, index) => { - if (previousPaintRate === null || Math.abs(item.paintRate - previousPaintRate) > epsilon) { - currentRank = index + 1; - previousPaintRate = item.paintRate; - } - - item.rank = currentRank; - }); - - return { rankings }; - } } diff --git a/apps/server/src/domains/game/application/services/gameResultCalculator.ts b/apps/server/src/domains/game/application/services/gameResultCalculator.ts new file mode 100644 index 0000000..0117fde --- /dev/null +++ b/apps/server/src/domains/game/application/services/gameResultCalculator.ts @@ -0,0 +1,52 @@ +/** + * gameResultCalculator + * マップ塗り状態から最終順位ペイロードを算出する純関数を提供する + * 塗り率計算,同率順位付け,チーム名解決を一箇所で扱う + */ +import { config } from "@repo/shared"; +import type { GameResultPayload } from "@repo/shared"; + +/** グリッド色配列からゲーム結果ペイロードを生成する */ +export const buildGameResultPayload = (gridColors: number[]): GameResultPayload => { + const { TEAM_COUNT } = config.GAME_CONFIG; + const totalCells = gridColors.length; + const paintedCounts = new Array(TEAM_COUNT).fill(0); + + gridColors.forEach((teamId) => { + if (!Number.isInteger(teamId) || teamId < 0 || teamId >= TEAM_COUNT) { + return; + } + + paintedCounts[teamId] += 1; + }); + + const rankings = paintedCounts + .map((paintedCellCount, teamId) => ({ + rank: 0, + teamId, + teamName: config.TEAM_NAMES[teamId] ?? `チーム${teamId + 1}`, + paintRate: totalCells > 0 ? (paintedCellCount / totalCells) * 100 : 0, + })) + .sort((a, b) => { + if (b.paintRate !== a.paintRate) { + return b.paintRate - a.paintRate; + } + + return a.teamId - b.teamId; + }); + + let currentRank = 0; + let previousPaintRate: number | null = null; + const epsilon = 1e-9; + + rankings.forEach((item, index) => { + if (previousPaintRate === null || Math.abs(item.paintRate - previousPaintRate) > epsilon) { + currentRank = index + 1; + previousPaintRate = item.paintRate; + } + + item.rank = currentRank; + }); + + return { rankings }; +}; diff --git a/packages/shared/src/config/gameConfig.ts b/packages/shared/src/config/gameConfig.ts index 51ca802..76463ce 100644 --- a/packages/shared/src/config/gameConfig.ts +++ b/packages/shared/src/config/gameConfig.ts @@ -7,7 +7,7 @@ export const GAME_CONFIG = { // ゲーム設定 MAX_PLAYERS_PER_ROOM: 100, // ルーム収容人数設定 - GAME_DURATION_SEC: 180, // 1ゲームの制限時間(3分 = 180秒) + GAME_DURATION_SEC: 30, // 1ゲームの制限時間(3分 = 180秒) // UI表示更新設定 TIMER_DISPLAY_UPDATE_MS: 250, // 残り時間表示の更新間隔(ms) @@ -62,6 +62,9 @@ MAP_BORDER_COLOR: 0xff4444, // プレイ領域外枠の色 } as const; +/** teamId インデックス順のチーム名配列 */ +export const TEAM_NAMES = ["赤チーム", "青チーム", "緑チーム", "黄チーム"] as const; + /** TEAM_COUNT と TEAM_COLORS の整合性を検証する */ export const validateTeamConfig = (): void => { const { TEAM_COUNT, TEAM_COLORS } = GAME_CONFIG; @@ -70,6 +73,12 @@ `GAME_CONFIG mismatch: TEAM_COLORS length (${TEAM_COLORS.length}) must equal TEAM_COUNT (${TEAM_COUNT})`, ); } + + if (TEAM_NAMES.length !== TEAM_COUNT) { + throw new Error( + `GAME_CONFIG mismatch: TEAM_NAMES length (${TEAM_NAMES.length}) must equal TEAM_COUNT (${TEAM_COUNT})`, + ); + } }; /** teamId が有効範囲内かを検証する */ diff --git a/packages/shared/src/config/index.ts b/packages/shared/src/config/index.ts index e34af47..92611fe 100644 --- a/packages/shared/src/config/index.ts +++ b/packages/shared/src/config/index.ts @@ -1,3 +1,4 @@ export { GAME_CONFIG } from "./gameConfig"; +export { TEAM_NAMES } from "./gameConfig"; export { validateTeamConfig, assertValidTeamId } from "./gameConfig"; export { NETWORK_CONFIG } from "./networkConfig";