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";