From c4cb5d4f18921774a7e6c2286b846063aae0f2a8 Mon Sep 17 00:00:00 2001 From: student079 <102852555+student079@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:29:46 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=8A=94=20?= =?UTF-8?q?=EA=B0=81=20=EB=8B=A8=EA=B3=84=EC=9D=98=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=EB=A5=BC=20=ED=99=95=EC=9D=B8=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8B=A4.=20(#136)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 게임 진행 시 turnchanged 이벤트 * docs: swagger 작성 * chore: 보이스 처리 서버에 먼저 보내기 --- .../games/dto/voice-processing-result.dto.ts | 17 +++ .../games/dto/voice-result-from-server.dto.ts | 33 +++++ .../src/modules/games/games-utils.ts | 69 ++++++++++- .../src/modules/games/games.gateway.ts | 115 ++++++++++++++++-- .../games/games.websocket.emit.controller.ts | 15 +++ .../games/games.websocket.on.controller.ts | 15 ++- 6 files changed, 252 insertions(+), 12 deletions(-) create mode 100644 be/gameServer/src/modules/games/dto/voice-processing-result.dto.ts create mode 100644 be/gameServer/src/modules/games/dto/voice-result-from-server.dto.ts diff --git a/be/gameServer/src/modules/games/dto/voice-processing-result.dto.ts b/be/gameServer/src/modules/games/dto/voice-processing-result.dto.ts new file mode 100644 index 0000000..1ffb50e --- /dev/null +++ b/be/gameServer/src/modules/games/dto/voice-processing-result.dto.ts @@ -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; +} diff --git a/be/gameServer/src/modules/games/dto/voice-result-from-server.dto.ts b/be/gameServer/src/modules/games/dto/voice-result-from-server.dto.ts new file mode 100644 index 0000000..3bfc4b7 --- /dev/null +++ b/be/gameServer/src/modules/games/dto/voice-result-from-server.dto.ts @@ -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; +} diff --git a/be/gameServer/src/modules/games/games-utils.ts b/be/gameServer/src/modules/games/games-utils.ts index 101bf9f..c120ab3 100644 --- a/be/gameServer/src/modules/games/games-utils.ts +++ b/be/gameServer/src/modules/games/games-utils.ts @@ -28,7 +28,7 @@ const SAMPLE_DATA = [ ]; export function createTurnData( - roomData: RoomDataDto, + roomId: string, gameData: GameDataDto, ): TurnDataDto { const gameModes = [GameMode.PRONUNCIATION, GameMode.CLEOPATRA]; @@ -36,7 +36,7 @@ export function createTurnData( if (gameMode === GameMode.CLEOPATRA) { return { - roomId: roomData.roomId, + roomId: roomId, playerNickname: gameData.currentPlayer, gameMode, timeLimit: 7, @@ -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, @@ -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); @@ -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); +} diff --git a/be/gameServer/src/modules/games/games.gateway.ts b/be/gameServer/src/modules/games/games.gateway.ts index fb58b00..7e57efb 100644 --- a/be/gameServer/src/modules/games/games.gateway.ts +++ b/be/gameServer/src/modules/games/games.gateway.ts @@ -4,6 +4,7 @@ import { SubscribeMessage, ConnectedSocket, OnGatewayDisconnect, + MessageBody, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { RedisService } from '../../redis/redis.service'; @@ -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'; @@ -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((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); @@ -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( + `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((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) { diff --git a/be/gameServer/src/modules/games/games.websocket.emit.controller.ts b/be/gameServer/src/modules/games/games.websocket.emit.controller.ts index a1805e0..f217adc 100644 --- a/be/gameServer/src/modules/games/games.websocket.emit.controller.ts +++ b/be/gameServer/src/modules/games/games.websocket.emit.controller.ts @@ -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') @@ -17,4 +18,18 @@ export class GamesWebSocketEmitController { turnChanged() { return; } + + @Post('voiceProcessingResult') + @ApiOperation({ + summary: '채점 결과', + description: + '해당 단계를 수행한 playerNickname과 result(SUCCESS, FAILURE)을 전달합니다.', + }) + @ApiResponse({ + description: '채점 결과', + type: VoiceProcessingResultDto, + }) + voiceProcessingResult() { + return; + } } diff --git a/be/gameServer/src/modules/games/games.websocket.on.controller.ts b/be/gameServer/src/modules/games/games.websocket.on.controller.ts index 721c4fc..3e13e3e 100644 --- a/be/gameServer/src/modules/games/games.websocket.on.controller.ts +++ b/be/gameServer/src/modules/games/games.websocket.on.controller.ts @@ -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') @@ -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; + } }