Skip to content

Commit

Permalink
Merge pull request #268 from NewCodes7/feature-be-#264
Browse files Browse the repository at this point in the history
[BE] feat#264 μ„œλ°”μ΄λ²Œ λͺ¨λ“œ κ΅¬ν˜„ 및 ν…ŒμŠ€νŠΈ μ½”λ“œ κ°œμ„ 
  • Loading branch information
NewCodes7 authored Nov 26, 2024
2 parents fc1e977 + a2ea180 commit f12302b
Show file tree
Hide file tree
Showing 20 changed files with 986 additions and 835 deletions.
4 changes: 4 additions & 0 deletions BE/src/common/constants/game-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum GameMode {
RANKING = 'RANKING',
SURVIVAL = 'SURVIVAL',
}
2 changes: 1 addition & 1 deletion BE/src/common/filters/ws-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class WsExceptionFilter extends BaseWsExceptionFilter {

// ValidationPipeμ—μ„œ λ°œμƒν•œ μ—λŸ¬ 처리
if (exception instanceof GameWsException) {
this.logger.error(`Validation Error: ${JSON.stringify(exception.message)}`);
this.logger.error(`Validation Error: ${JSON.stringify(exception.message)}`, exception.stack);

client.emit('exception', {
eventName: exception.eventName,
Expand Down
3 changes: 2 additions & 1 deletion BE/src/game/dto/create-game.dto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { IsIn, IsInt, IsString, Max, MaxLength, Min, MinLength } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { WsException } from '@nestjs/websockets';
import { GameMode } from '../../common/constants/game-mode';

export class CreateGameDto {
@IsString()
Expand All @@ -9,7 +10,7 @@ export class CreateGameDto {
title: string;

@IsString()
@IsIn(['RANKING', 'SURVIVAL'])
@IsIn([GameMode.RANKING, GameMode.SURVIVAL])
gameMode: string;

@Type(() => Number)
Expand Down
3 changes: 2 additions & 1 deletion BE/src/game/dto/update-room-option.dto.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { IsIn, IsInt, IsString, Length, Max, MaxLength, Min, MinLength } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { WsException } from '@nestjs/websockets';
import { GameMode } from '../../common/constants/game-mode';

export class UpdateRoomOptionDto {
@IsString()
@Length(6, 6, { message: 'PINλ²ˆν˜ΈλŠ” 6μžλ¦¬μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€.' })
gameId: string;

@IsString()
@IsIn(['RANKING', 'SURVIVAL'])
@IsIn([GameMode.RANKING, GameMode.SURVIVAL])
gameMode: string;

@IsString()
Expand Down
32 changes: 27 additions & 5 deletions BE/src/game/redis/subscribers/player.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,33 @@ export class PlayerSubscriber extends RedisSubscriber {
}

private async handlePlayerPosition(playerId: string, playerData: any, server: Server) {
server.to(playerData.gameId).emit(SocketEvents.UPDATE_POSITION, {
playerId,
playerPosition: [parseFloat(playerData.positionX), parseFloat(playerData.positionY)]
});
this.logger.verbose(`Player position updated: ${playerId}`);
const { gameId, positionX, positionY } = playerData;
const playerPosition = [parseFloat(positionX), parseFloat(positionY)];
const updateData = { playerId, playerPosition };

const isAlivePlayer = await this.redis.hget(REDIS_KEY.PLAYER(playerId), 'isAlive');

if (isAlivePlayer === '1') {
server.to(gameId).emit(SocketEvents.UPDATE_POSITION, updateData);
} else if (isAlivePlayer === '0') {
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
const deadPlayers = await Promise.all(
players.map(async (id) => {
const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive');
return { id, isAlive };
})
);

deadPlayers
.filter(player => player.isAlive === '0')
.forEach(player => {
server.to(player.id).emit(SocketEvents.UPDATE_POSITION, updateData);
});
}

this.logger.verbose(
`[updatePosition] RoomId: ${gameId} | playerId: ${playerId} | isAlive: ${isAlivePlayer === '1' ? 'μƒμ‘΄μž' : 'κ΄€μ „μž'} | position: [${positionX}, ${positionY}]`
);
}

private async handlePlayerDisconnect(playerId: string, playerData: any, server: Server) {
Expand Down
4 changes: 2 additions & 2 deletions BE/src/game/redis/subscribers/scoring.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ export class ScoringSubscriber extends RedisSubscriber {
});

await this.updateQuizState(gameId, quiz.quizNum);
this.logger.verbose(`endQuizTime: ${gameId} - ${quiz.quizNum}`);
this.logger.verbose(`[endQuizTime] RoomId: ${gameId} | quizNum: ${quiz.quizNum}`);
}

private async updateQuizState(gameId: string, quizNum: number) {
await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${quizNum}:end`);
await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), `${quizNum}:end`); // timer.subscriber.ts ꡬ독 ν•Έλ“€λŸ¬ μ‹€ν–‰
await this.redis.set(REDIS_KEY.ROOM_TIMER(gameId), 'timer', 'EX', '10', 'NX');
}
}
51 changes: 40 additions & 11 deletions BE/src/game/redis/subscribers/timer.subscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Redis from 'ioredis';
import { Server } from 'socket.io';
import { REDIS_KEY } from '../../../common/constants/redis-key.constant';
import SocketEvents from '../../../common/constants/socket-events';
import { GameMode } from '../../../common/constants/game-mode';

@Injectable()
export class TimerSubscriber extends RedisSubscriber {
Expand Down Expand Up @@ -46,40 +47,68 @@ export class TimerSubscriber extends RedisSubscriber {

const sockets = await server.in(gameId).fetchSockets();
const clients = sockets.map((socket) => socket.id);

const correctPlayers = [];
const inCorrectPlayers = [];

// ν”Œλ ˆμ΄μ–΄ λ‹΅μ•ˆ 처리
for (const clientId of clients) {
const player = await this.redis.hgetall(REDIS_KEY.PLAYER(clientId));

if (player.isAlive === '0') {
continue;
}

const selectAnswer = this.calculateAnswer(player.positionX, player.positionY);

await this.redis.set(`${REDIS_KEY.PLAYER(clientId)}:Changes`, 'AnswerCorrect');
if (selectAnswer.toString() === quiz.answer) {
correctPlayers.push(clientId);
await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '1' });
await this.redis.hset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '1' });
} else {
await this.redis.hmset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '0' });
inCorrectPlayers.push(clientId);
await this.redis.hset(REDIS_KEY.PLAYER(clientId), { isAnswerCorrect: '0' });
}
}

// 점수 μ—…λ°μ΄νŠΈ
for (const clientId of correctPlayers) {
await this.redis.zincrby(
REDIS_KEY.ROOM_LEADERBOARD(gameId),
1000 / correctPlayers.length,
clientId
);
const gameMode = await this.redis.hget(REDIS_KEY.ROOM(gameId), 'gameMode');
const leaderboardKey = REDIS_KEY.ROOM_LEADERBOARD(gameId);

if (gameMode === GameMode.RANKING) {
const score = 1000 / correctPlayers.length;
correctPlayers.forEach(clientId => {
this.redis.zincrby(leaderboardKey, score, clientId);
});
} else if (gameMode === GameMode.SURVIVAL) {
correctPlayers.forEach(clientId => {
this.redis.zadd(leaderboardKey, 1, clientId);
});
inCorrectPlayers.forEach(clientId => {
this.redis.zadd(leaderboardKey, 0, clientId);
this.redis.hset(REDIS_KEY.PLAYER(clientId), { isAlive: '0' });
});
}

await this.redis.publish(`scoring:${gameId}`, clients.length.toString());
this.logger.verbose(`채점: ${gameId} - ${clients.length}`);

this.logger.verbose(
`[Quiz] Room: ${gameId} | gameMode: ${gameMode === GameMode.SURVIVAL ? 'μ„œλ°”μ΄λ²Œ' : 'λž­ν‚Ή'} | totalPlayers: ${clients.length} | ${gameMode === GameMode.SURVIVAL ? `μƒμ‘΄μž: ${correctPlayers.length}λͺ…` : `μ •λ‹΅μž: ${correctPlayers.length}λͺ…`}`
);
}

private async handleNextQuiz(gameId: string, currentQuizNum: number, server: Server) {
const newQuizNum = currentQuizNum + 1;
const quizList = await this.redis.smembers(REDIS_KEY.ROOM_QUIZ_SET(gameId));

if (quizList.length <= newQuizNum) {
// 생쑴 λͺ¨λ“œμ—μ„œ λͺ¨λ‘ νƒˆλ½ν•˜μ§„ μ•Šμ•˜λŠ”μ§€ 체크
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
const alivePlayers = players.filter(async (id) => {
const isAlive = await this.redis.hget(REDIS_KEY.PLAYER(id), 'isAlive');
return isAlive === '1';
});

if (quizList.length <= newQuizNum || alivePlayers.length === 0) {
const leaderboard = await this.redis.zrange(
REDIS_KEY.ROOM_LEADERBOARD(gameId),
0,
Expand All @@ -90,7 +119,7 @@ export class TimerSubscriber extends RedisSubscriber {
server.to(gameId).emit(SocketEvents.END_GAME, {
host: leaderboard[0]
});
this.logger.verbose(`endGame: ${leaderboard[0]}`);
this.logger.verbose(`[endGame]: ${gameId}`);
return;
}

Expand Down
29 changes: 24 additions & 5 deletions BE/src/game/service/game.chat.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,38 @@ export class GameChatService {
timestamp: new Date()
})
);
this.logger.verbose(`μ±„νŒ… 전솑: ${gameId} - ${clientId} (${player.playerName}) = ${message}`);

this.logger.verbose(
`[chatMessage] Room: ${gameId} | playerId: ${clientId} | playerName: ${player.playerName} | isAlive: ${player.isAlive ? 'μƒμ‘΄μž' : 'κ΄€μ „μž'} | Message: ${message}`
);
}

async subscribeChatEvent(server: Server) {
const chatSubscriber = this.redis.duplicate();
chatSubscriber.psubscribe('chat:*');

chatSubscriber.on('pmessage', async (_pattern, channel, message) => {
console.log(`channel: ${channel}`); // channel: chat:317172
console.log(`message: ${message}`); // message: {"playerId":"8CT28Iw5FgjgPHNyAAAs","playerName":"Player1","message":"Hello, everyone!","timestamp":"2024-11-14T08:32:38.617Z"}
const gameId = channel.split(':')[1];
const gameId = channel.split(':')[1]; // ex. channel: chat:317172
const chatMessage = JSON.parse(message);
server.to(gameId).emit(SocketEvents.CHAT_MESSAGE, chatMessage);

const playerKey = REDIS_KEY.PLAYER(chatMessage.playerId);
const isAlivePlayer = await this.redis.hget(playerKey, 'isAlive');

if (isAlivePlayer === '1') {
server.to(gameId).emit(SocketEvents.CHAT_MESSAGE, chatMessage);
return;
}

// 죽은 μ‚¬λžŒμ˜ μ±„νŒ…μ€ 죽은 μ‚¬λžŒλΌλ¦¬λ§Œ λ³Ό 수 μžˆλ„λ‘ 처리
const players = await this.redis.smembers(REDIS_KEY.ROOM_PLAYERS(gameId));
await Promise.all(players.map(async (playerId) => {
const playerKey = REDIS_KEY.PLAYER(playerId);
const isAlive = await this.redis.hget(playerKey, 'isAlive');

if (isAlive === '0') {
server.to(playerId).emit(SocketEvents.CHAT_MESSAGE, chatMessage);
}
}));
});
}
}
13 changes: 7 additions & 6 deletions BE/src/game/service/game.room.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class GameRoomService {
const currentRoomPins = await this.redis.smembers(REDIS_KEY.ACTIVE_ROOMS);
const roomId = generateUniquePin(currentRoomPins);

await this.redis.hmset(REDIS_KEY.ROOM(roomId), {
await this.redis.hset(REDIS_KEY.ROOM(roomId), {
host: clientId,
status: 'waiting',
title: gameConfig.title,
Expand Down Expand Up @@ -69,12 +69,13 @@ export class GameRoomService {
const positionY = Math.random();

await this.redis.set(`${playerKey}:Changes`, 'Join');
await this.redis.hmset(playerKey, {
await this.redis.hset(playerKey, {
playerName: dto.playerName,
positionX: positionX.toString(),
positionY: positionY.toString(),
disconnected: '0',
gameId: dto.gameId
gameId: dto.gameId,
isAlive: '1'
});

await this.redis.zadd(REDIS_KEY.ROOM_LEADERBOARD(dto.gameId), 0, clientId);
Expand Down Expand Up @@ -113,7 +114,7 @@ export class GameRoomService {
this.gameValidator.validatePlayerIsHost(SocketEvents.UPDATE_ROOM_OPTION, room, clientId);

await this.redis.set(`${roomKey}:Changes`, 'Option');
await this.redis.hmset(roomKey, {
await this.redis.hset(roomKey, {
title: title,
gameMode: gameMode,
maxPlayerCount: maxPlayerCount.toString(),
Expand All @@ -132,7 +133,7 @@ export class GameRoomService {
this.gameValidator.validatePlayerIsHost(SocketEvents.UPDATE_ROOM_QUIZSET, room, clientId);

await this.redis.set(`${roomKey}:Changes`, 'Quizset');
await this.redis.hmset(roomKey, {
await this.redis.hset(roomKey, {
quizSetId: quizSetId.toString(),
quizCount: quizCount.toString()
});
Expand All @@ -152,7 +153,7 @@ export class GameRoomService {
// ν”Œλ ˆμ΄μ–΄ 제거
pipeline.srem(REDIS_KEY.ROOM_PLAYERS(roomId), clientId);
// 1. ν”Œλ ˆμ΄μ–΄ μƒνƒœλ₯Ό 'disconnected'둜 λ³€κ²½ν•˜κ³  TTL μ„€μ •
pipeline.hmset(REDIS_KEY.PLAYER(clientId), {
pipeline.hset(REDIS_KEY.PLAYER(clientId), {
disconnected: '1',
disconnectedAt: Date.now().toString()
});
Expand Down
14 changes: 7 additions & 7 deletions BE/src/game/service/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class GameService {
this.gameValidator.validatePlayerInRoom(SocketEvents.UPDATE_POSITION, gameId, player);

await this.redis.set(`${playerKey}:Changes`, 'Position');
await this.redis.hmset(playerKey, {
await this.redis.hset(playerKey, {
positionX: newPosition[0].toString(),
positionY: newPosition[1].toString()
});
Expand Down Expand Up @@ -87,13 +87,13 @@ export class GameService {
...selectedQuizList.map((quiz) => quiz.id)
);
for (const quiz of selectedQuizList) {
await this.redis.hmset(REDIS_KEY.ROOM_QUIZ(gameId, quiz.id), {
await this.redis.hset(REDIS_KEY.ROOM_QUIZ(gameId, quiz.id), {
quiz: quiz.quiz,
answer: quiz.choiceList.find((choice) => choice.isAnswer).order,
limitTime: quiz.limitTime.toString(),
choiceCount: quiz.choiceList.length.toString()
});
await this.redis.hmset(
await this.redis.hset(
REDIS_KEY.ROOM_QUIZ_CHOICES(gameId, quiz.id),
quiz.choiceList.reduce(
(acc, choice) => {
Expand All @@ -113,15 +113,15 @@ export class GameService {

// κ²Œμž„μ΄ μ‹œμž‘λ˜μ—ˆμŒμ„ μ•Œλ¦Ό
await this.redis.set(`${roomKey}:Changes`, 'Start');
await this.redis.hmset(roomKey, {
await this.redis.hset(roomKey, {
status: 'playing'
});

// 첫 ν€΄μ¦ˆ κ±Έμ–΄μ£ΌκΈ°
await this.redis.set(REDIS_KEY.ROOM_CURRENT_QUIZ(gameId), '-1:end'); // 0:start, 0:end, 1:start, 1:end
await this.redis.set(REDIS_KEY.ROOM_TIMER(gameId), 'timer', 'EX', 3);

this.logger.verbose(`κ²Œμž„ μ‹œμž‘: ${gameId}`);
this.logger.verbose(`κ²Œμž„ μ‹œμž‘ (gameId: ${gameId}) (gameMode: ${room.gameMode})`);
}

async subscribeRedisEvent(server: Server) {
Expand All @@ -143,12 +143,12 @@ export class GameService {
const players = await this.redis.smembers(roomPlayersKey);
if (host === clientId && players.length > 0) {
const newHost = await this.redis.srandmember(REDIS_KEY.ROOM_PLAYERS(playerData.gameId));
await this.redis.hmset(roomKey, {
await this.redis.hset(roomKey, {
host: newHost
});
}
await this.redis.set(`${playerKey}:Changes`, 'Disconnect', 'EX', 600); // ν•΄λ‹Ήν”Œλ ˆμ΄μ–΄μ˜ 변화정보 10λΆ„ 후에 μ‚­μ œ
await this.redis.hmset(playerKey, {
await this.redis.hset(playerKey, {
disconnected: '1'
});

Expand Down
6 changes: 1 addition & 5 deletions BE/src/user/entities/user-quiz-archive.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import { Column, Entity, JoinColumn, ManyToOne } from 'typeorm';
import { BaseModel } from '../../common/entity/base.entity';
import { QuizSetModel } from '../../quiz-set/entities/quiz-set.entity';
import { UserModel } from './user.entity';

export enum GameMode {
SURVIVAL = 'SURVIVAL',
RANKING = 'RANKING'
}
import { GameMode } from '../../common/constants/game-mode';

@Entity('user_quiz_archive')
export class UserQuizArchiveModel extends BaseModel {
Expand Down
Loading

0 comments on commit f12302b

Please sign in to comment.