Skip to content

Commit

Permalink
[BE] 사용자는 각 단계의 결과를 확인할 수 있다. (#136)
Browse files Browse the repository at this point in the history
* feat: 게임 진행 시 turnchanged 이벤트

* docs: swagger 작성

* chore: 보이스 처리 서버에 먼저 보내기
  • Loading branch information
student079 authored Nov 21, 2024
1 parent f5c334d commit c4cb5d4
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 12 deletions.
17 changes: 17 additions & 0 deletions be/gameServer/src/modules/games/dto/voice-processing-result.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';

export class VoiceProcessingResultDto {
@ApiProperty({
example: 'player1',
type: String,
description: '플레이어 닉네임',
})
playerNickname: string;

@ApiProperty({
example: 'SUCCESS',
type: String,
description: '결과',
})
result: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ApiProperty } from '@nestjs/swagger';

export class VoiceResultFromServerDto {
@ApiProperty({
example: '6f42377f-42ea-42cc-ac1a-b5d2b99d4ced',
type: String,
description: '게임 방 ID',
})
roomId: string;

@ApiProperty({
example: 'player1',
type: String,
description: '해당 단계를 수행하는 플레이어 닉네임',
})
playerNickname: string;

@ApiProperty({
example: 92,
type: Number,
description: '발음게임 점수',
required: false,
})
pronounceScore?: number;

@ApiProperty({
example: 'A#3',
type: String,
description: '음정게임 평균 음',
required: false,
})
averageNote?: string;
}
69 changes: 65 additions & 4 deletions be/gameServer/src/modules/games/games-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ const SAMPLE_DATA = [
];

export function createTurnData(
roomData: RoomDataDto,
roomId: string,
gameData: GameDataDto,
): TurnDataDto {
const gameModes = [GameMode.PRONUNCIATION, GameMode.CLEOPATRA];
const gameMode = gameModes[Math.floor(Math.random() * gameModes.length)];

if (gameMode === GameMode.CLEOPATRA) {
return {
roomId: roomData.roomId,
roomId: roomId,
playerNickname: gameData.currentPlayer,
gameMode,
timeLimit: 7,
Expand All @@ -47,7 +47,7 @@ export function createTurnData(
SAMPLE_DATA[Math.floor(Math.random() * SAMPLE_DATA.length)];

return {
roomId: roomData.roomId,
roomId: roomId,
playerNickname: gameData.currentPlayer,
gameMode,
timeLimit: randomSentence.timeLimit,
Expand All @@ -60,7 +60,15 @@ export function selectCurrentPlayer(
previousPlayers: string[],
): string {
let candidates = alivePlayers;
if (previousPlayers[0] === previousPlayers[1]) {

if (candidates.length === 0) {
return null;
}

if (
previousPlayers.length >= 2 &&
previousPlayers[0] === previousPlayers[1]
) {
candidates = alivePlayers.filter((player) => player !== previousPlayers[0]);
}
const randomIndex = Math.floor(Math.random() * candidates.length);
Expand All @@ -81,3 +89,56 @@ export function removePlayerFromGame(
(player: string) => player !== playerNickname,
);
}

export function noteToNumber(note: string): number {
const matches = note.match(/([A-G]#?)(\d+)/);
if (!matches) return null;

const [, noteName, octave] = matches;
const noteBase = {
C: 0,
'C#': 1,
D: 2,
'D#': 3,
E: 4,
F: 5,
'F#': 6,
G: 7,
'G#': 8,
A: 9,
'A#': 10,
B: 11,
}[noteName];

return noteBase + (parseInt(octave) + 1) * 12;
}

export function numberToNote(number: number): string {
const notes = [
'C',
'C#',
'D',
'D#',
'E',
'F',
'F#',
'G',
'G#',
'A',
'A#',
'B',
];
const octave = Math.floor(number / 12) - 1;
const noteIndex = Math.round(number) % 12;
return `${notes[noteIndex]}${octave}`;
}

export function updatePreviousPlayers(
gameData: GameDataDto,
playerNickname: string,
): void {
if (gameData.previousPlayers.length >= 2) {
gameData.previousPlayers.shift(); // 맨 앞의 플레이어 제거
}
gameData.previousPlayers.push(playerNickname);
}
115 changes: 108 additions & 7 deletions be/gameServer/src/modules/games/games.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SubscribeMessage,
ConnectedSocket,
OnGatewayDisconnect,
MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { RedisService } from '../../redis/redis.service';
Expand All @@ -12,12 +13,15 @@ import { WsExceptionsFilter } from '../../common/filters/ws-exceptions.filter';
import { RoomDataDto } from '../rooms/dto/room-data.dto';
import { GameDataDto } from './dto/game-data.dto';
import { TurnDataDto } from './dto/turn-data.dto';
import { VoiceResultFromServerDto } from './dto/Voice-result-from-server.dto';
import { ErrorResponse } from '../rooms/dto/error-response.dto';
import {
createTurnData,
selectCurrentPlayer,
checkPlayersReady,
removePlayerFromGame,
noteToNumber,
updatePreviousPlayers,
} from './games-utils';

const VOICE_SERVERS = 'voice-servers';
Expand Down Expand Up @@ -94,9 +98,13 @@ export class GamesGateway implements OnGatewayDisconnect {
};
await this.redisService.set(`game:${roomId}`, JSON.stringify(gameData));

const turnData: TurnDataDto = createTurnData(roomData, gameData);
const turnData: TurnDataDto = createTurnData(roomId, gameData);

this.server.to(VOICE_SERVERS).emit('turnChanged', turnData);
await new Promise<void>((resolve) => {
this.server.to(VOICE_SERVERS).emit('turnChanged', turnData, () => {
resolve();
});
});
this.logger.log('Turn data sent to voice servers:', turnData);
this.server.to(roomId).emit('turnChanged', turnData);
this.logger.log('Turn data sent to clients in room:', roomId);
Expand All @@ -113,11 +121,104 @@ export class GamesGateway implements OnGatewayDisconnect {
}
}

// // 음성 처리 결과 수신
// socket.on("voiceResult", (result) => {
// console.log("Voice result received:", result);
// io.to(result.roomId).emit("voiceProcessingResult", result);
// });
@SubscribeMessage('voiceResult')
async handleVoiceResult(
@MessageBody() voiceResultFromServerDto: VoiceResultFromServerDto,
@ConnectedSocket() client: Socket,
) {
try {
const { roomId, playerNickname, averageNote, pronounceScore } =
voiceResultFromServerDto;

this.logger.log(
`Received voice result for roomId: ${roomId}, player: ${playerNickname}`,
);

const gameDataString = await this.redisService.get<string>(
`game:${roomId}`,
);
if (!gameDataString) {
return client.emit('error', { message: `game ${roomId} not found` });
}

const gameData: GameDataDto = JSON.parse(gameDataString);

if (averageNote) {
const note = noteToNumber(averageNote);
this.logger.log(
`Processing averageNote for player ${playerNickname}: ${note}`,
);
if (gameData.previousPitch < note) {
this.logger.log(
`Success: Player ${playerNickname} has a higher note (${note}) than required pitch.`,
);
this.server.to(roomId).emit('voiceProcessingResult', {
playerNickname,
result: 'SUCCESS',
});
gameData.previousPitch = note;
} else {
this.logger.log(
`Failure: Player ${playerNickname} failed to meet the required pitch.`,
);
this.server.to(roomId).emit('voiceProcessingResult', {
playerNickname,
result: 'FAILURE',
});
removePlayerFromGame(gameData, playerNickname);
}
} else if (pronounceScore) {
this.logger.log(
`Processing pronounceScore for player ${playerNickname}: ${pronounceScore}`,
);
if (pronounceScore >= 98) {
this.server.to(roomId).emit('voiceProcessingResult', {
playerNickname,
result: 'SUCCESS',
});
} else {
this.server.to(roomId).emit('voiceProcessingResult', {
playerNickname,
result: 'FAILURE',
});
removePlayerFromGame(gameData, playerNickname);
}
}
updatePreviousPlayers(gameData, playerNickname);
gameData.currentTurn++;
this.logger.log(`Turn updated: ${gameData.currentTurn}`);
gameData.currentPlayer = selectCurrentPlayer(
gameData.alivePlayers,
gameData.previousPlayers,
);

if (gameData.currentPlayer === null) {
// 게임종료
}

this.logger.log(
`Saving updated game data to Redis for roomId: ${roomId}`,
);
await this.redisService.set(`game:${roomId}`, JSON.stringify(gameData));

const turnData: TurnDataDto = createTurnData(roomId, gameData);

await new Promise<void>((resolve) => {
this.server.to(VOICE_SERVERS).emit('turnChanged', turnData, () => {
resolve();
});
});
this.logger.log('Turn data sent to voice servers:', turnData);

this.server.to(roomId).emit('turnChanged', turnData);
this.logger.log('Turn data sent to clients in room:', roomId);
} catch (error) {
this.logger.error('Error handling voiceResult:', error);
client.emit('error', { message: 'Internal server error' });

// 오류 일때는 일단 성공
}
}

@SubscribeMessage('disconnect')
async handleDisconnect(@ConnectedSocket() client: Socket) {
Expand Down
15 changes: 15 additions & 0 deletions be/gameServer/src/modules/games/games.websocket.emit.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Controller, Post } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { TurnDataDto } from './dto/turn-data.dto';
import { VoiceProcessingResultDto } from './dto/voice-processing-result.dto';

@ApiTags('Rooms (WebSocket: 서버에서 발행하는 이벤트)')
@Controller('rooms')
Expand All @@ -17,4 +18,18 @@ export class GamesWebSocketEmitController {
turnChanged() {
return;
}

@Post('voiceProcessingResult')
@ApiOperation({
summary: '채점 결과',
description:
'해당 단계를 수행한 playerNickname과 result(SUCCESS, FAILURE)을 전달합니다.',
})
@ApiResponse({
description: '채점 결과',
type: VoiceProcessingResultDto,
})
voiceProcessingResult() {
return;
}
}
15 changes: 14 additions & 1 deletion be/gameServer/src/modules/games/games.websocket.on.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Controller, Post } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
import { VoiceResultFromServerDto } from './dto/Voice-result-from-server.dto';

@ApiTags('Rooms (WebSocket: 서버에서 수신하는 이벤트)')
@Controller('rooms')
Expand All @@ -14,4 +15,16 @@ export class GamesWebSocketOnController {
// This method does not execute any logic. It's for Swagger documentation only.
return;
}

@Post('voiceResult')
@ApiOperation({
summary: '음성 처리서버에서 처리한 결과 받기',
description:
'wss://clovapatra.com/rooms 에서 "voiceResult" 이벤트를 emit해 사용합니다.',
})
@ApiBody({ type: VoiceResultFromServerDto })
voiceResult() {
// This method does not execute any logic. It's for Swagger documentation only.
return;
}
}

0 comments on commit c4cb5d4

Please sign in to comment.