-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[BE] 게임 시작시, 현재 단계의 진행을 결정한다. (#119)
* feat: startGame 이벤트 turnChanged 이벤트 발행 * test: startGame 유닛 테스트 * docs: swagger 정리
- Loading branch information
1 parent
002f38f
commit cc6b23a
Showing
9 changed files
with
399 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export class GameDataDto { | ||
gameId: string; | ||
alivePlayers: string[]; | ||
currentTurn: number; | ||
currentPlayer: string; | ||
previousPitch: number; | ||
previousPlayers: string[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
|
||
export enum GameMode { | ||
PRONUNCIATION = 'PRONUNCIATION', | ||
CLEOPATRA = 'CLEOPATRA', | ||
} | ||
|
||
export class TurnDataDto { | ||
@ApiProperty({ | ||
example: '6f42377f-42ea-42cc-ac1a-b5d2b99d4ced', | ||
type: String, | ||
description: '게임 방 ID', | ||
}) | ||
roomId: string; | ||
|
||
@ApiProperty({ | ||
example: 'player1', | ||
type: String, | ||
description: '해당 단계를 수행하는 플레이어 닉네임', | ||
}) | ||
playerNickname: string; | ||
|
||
@ApiProperty({ | ||
example: GameMode.PRONUNCIATION, | ||
type: String, | ||
description: '해당 단계 게임 모드', | ||
}) | ||
gameMode: GameMode; | ||
|
||
@ApiProperty({ | ||
example: 10, | ||
type: Number, | ||
description: '해당 단계 게임 모드를 수행할 때의 제한시간', | ||
}) | ||
timeLimit: number; | ||
|
||
@ApiProperty({ | ||
example: | ||
'도토리가 문을 도로록, 드르륵, 두루룩 열었는가? 드로록, 두루륵, 두르룩 열었는가.', | ||
type: String, | ||
description: '게임모드가 PRONUNCIATION일 때 가사', | ||
required: false, | ||
}) | ||
lyrics?: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { GameMode, TurnDataDto } from './dto/turn-data.dto'; | ||
import { RoomDataDto } from './../rooms/dto/room-data.dto'; | ||
import { GameDataDto } from './dto/game-data.dto'; | ||
|
||
export function createTurnData( | ||
roomData: RoomDataDto, | ||
gameData: GameDataDto, | ||
): TurnDataDto { | ||
const gameModes = [GameMode.PRONUNCIATION, GameMode.CLEOPATRA]; | ||
const gameMode = gameModes[Math.floor(Math.random() * gameModes.length)]; | ||
|
||
let timeLimit: number; | ||
if (gameMode === GameMode.CLEOPATRA) { | ||
timeLimit = 10; | ||
} else { | ||
// 데이터에 따라 바뀜 | ||
timeLimit = 15; | ||
} | ||
|
||
let lyrics: string | undefined; | ||
if (gameMode === GameMode.PRONUNCIATION) { | ||
// 데이터에 따라 바뀜 | ||
lyrics = '테스트테스트테스트테스트테스트테스트테스트테스트테스트테스트'; | ||
} | ||
|
||
return { | ||
roomId: roomData.roomId, | ||
playerNickname: gameData.currentPlayer, | ||
gameMode: gameMode, | ||
timeLimit: timeLimit, | ||
lyrics: lyrics, | ||
}; | ||
} | ||
|
||
export function selectCurrentPlayer(alivePlayers: string[]): string { | ||
const randomIndex = Math.floor(Math.random() * alivePlayers.length); | ||
return alivePlayers[randomIndex]; | ||
} | ||
|
||
export function checkPlayersReady(roomData: RoomDataDto): boolean { | ||
return roomData.players | ||
.filter((player) => player.playerNickname !== roomData.hostNickname) | ||
.every((player) => player.isReady); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
import { Test, TestingModule } from '@nestjs/testing'; | ||
import { GamesGateway } from './games.gateway'; | ||
import { RedisService } from '../../redis/redis.service'; | ||
import { Logger } from '@nestjs/common'; | ||
import { Server, Socket } from 'socket.io'; | ||
import { RoomDataDto } from '../rooms/dto/room-data.dto'; | ||
|
||
describe('GamesGateway', () => { | ||
let gateway: GamesGateway; | ||
let redisService: RedisService; | ||
let mockServer: Server; | ||
let mockHostClient: Socket; | ||
let mockLogger: Logger; | ||
|
||
beforeEach(async () => { | ||
const redisServiceMock = { | ||
set: jest.fn(), | ||
get: jest.fn(), | ||
delete: jest.fn(), | ||
}; | ||
|
||
const module: TestingModule = await Test.createTestingModule({ | ||
providers: [ | ||
GamesGateway, | ||
{ provide: RedisService, useValue: redisServiceMock }, | ||
{ provide: Logger, useValue: mockLogger }, | ||
], | ||
}).compile(); | ||
|
||
gateway = module.get<GamesGateway>(GamesGateway); | ||
redisService = module.get<RedisService>(RedisService); | ||
|
||
mockServer = { | ||
to: jest.fn().mockReturnValue({ | ||
emit: jest.fn(), | ||
}), | ||
} as unknown as Server; | ||
|
||
mockHostClient = { | ||
emit: jest.fn(), | ||
data: { | ||
roomId: 'test-room-id', | ||
playerNickname: 'hostPlayer', | ||
}, | ||
} as unknown as Socket; | ||
|
||
gateway.server = mockServer; | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe('handleStartGame', () => { | ||
it('모든 조건이 충족되면 게임을 시작해야 한다.', async () => { | ||
const roomData: RoomDataDto = { | ||
roomId: 'test-room-id', | ||
roomName: 'testRoomName', | ||
hostNickname: 'hostPlayer', | ||
players: [ | ||
{ playerNickname: 'hostPlayer', isReady: true }, | ||
{ playerNickname: 'player1', isReady: true }, | ||
], | ||
status: 'waiting', | ||
}; | ||
|
||
jest | ||
.spyOn(redisService, 'get') | ||
.mockResolvedValueOnce(JSON.stringify(roomData)); | ||
|
||
await gateway.handleStartGame(mockHostClient); | ||
|
||
expect(redisService.get).toHaveBeenCalledWith('room:test-room-id'); | ||
expect(redisService.set).toHaveBeenCalledWith( | ||
'room:test-room-id', | ||
JSON.stringify({ ...roomData, status: 'progress' }), | ||
'roomUpdate', | ||
); | ||
}); | ||
|
||
it('모든 플레이어가 준비되지 않았을 경우 오류를 반환해야 한다.', async () => { | ||
const roomData: RoomDataDto = { | ||
roomId: 'test-room-id', | ||
roomName: 'testRoomName', | ||
hostNickname: 'hostPlayer', | ||
players: [ | ||
{ playerNickname: 'hostPlayer', isReady: true }, | ||
{ playerNickname: 'player1', isReady: false }, | ||
], | ||
status: 'waiting', | ||
}; | ||
|
||
jest | ||
.spyOn(redisService, 'get') | ||
.mockResolvedValueOnce(JSON.stringify(roomData)); | ||
|
||
await gateway.handleStartGame(mockHostClient); | ||
|
||
expect(redisService.get).toHaveBeenCalledWith('room:test-room-id'); | ||
expect(mockHostClient.emit).toHaveBeenCalledWith( | ||
'error', | ||
'All players must be ready to start the game', | ||
); | ||
}); | ||
|
||
it('호스트가 아닌 사용자가 게임 시작을 요청할 경우 오류를 반환해야 한다.', async () => { | ||
mockHostClient.data.playerNickname = 'notHostPlayer'; | ||
|
||
const roomData: RoomDataDto = { | ||
roomId: 'test-room-id', | ||
roomName: 'testRoomName', | ||
hostNickname: 'hostPlayer', | ||
players: [ | ||
{ playerNickname: 'hostPlayer', isReady: true }, | ||
{ playerNickname: 'player1', isReady: true }, | ||
], | ||
status: 'waiting', | ||
}; | ||
|
||
jest | ||
.spyOn(redisService, 'get') | ||
.mockResolvedValueOnce(JSON.stringify(roomData)); | ||
|
||
await gateway.handleStartGame(mockHostClient); | ||
|
||
expect(redisService.get).toHaveBeenCalledWith('room:test-room-id'); | ||
expect(mockHostClient.emit).toHaveBeenCalledWith( | ||
'error', | ||
'Only the host can start the game', | ||
); | ||
}); | ||
|
||
it('Redis에서 방 정보를 찾을 수 없을 경우 오류를 반환해야 한다.', async () => { | ||
jest.spyOn(redisService, 'get').mockResolvedValueOnce(null); | ||
|
||
await gateway.handleStartGame(mockHostClient); | ||
|
||
expect(redisService.get).toHaveBeenCalledWith('room:test-room-id'); | ||
expect(mockHostClient.emit).toHaveBeenCalledWith( | ||
'error', | ||
'Room not found', | ||
); | ||
}); | ||
|
||
it('Redis에서 예외가 발생할 경우 오류를 반환해야 한다.', async () => { | ||
jest | ||
.spyOn(redisService, 'get') | ||
.mockRejectedValueOnce(new Error('Redis error')); | ||
|
||
await gateway.handleStartGame(mockHostClient); | ||
|
||
expect(mockHostClient.emit).toHaveBeenCalledWith('error', { | ||
message: 'Failed to start the game', | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,12 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { RedisModule } from '../../redis/redis.module'; | ||
import { GamesWebSocketEmitController } from './games.websocket.emit.controller'; | ||
import { GamesWebSocketOnController } from './games.websocket.on.controller'; | ||
import { GamesGateway } from './games.gateway'; | ||
|
||
@Module({ | ||
imports: [RedisModule], | ||
providers: [GamesGateway], | ||
controllers: [], | ||
controllers: [GamesWebSocketEmitController, GamesWebSocketOnController], | ||
}) | ||
export class GamesModule {} |
Oops, something went wrong.