Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(game): add game queue matchmaking logics #93

Merged
merged 7 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/common/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const EVENT_GAME_INVITATION_REPLY = 'gameInvitationReply';

export const EVENT_SERVER_GAME_READY = 'serverGameReady';

export const EVENT_GAME_MATCHED = 'gameMatched';

export const EVENT_GAME_START = 'gameStart';

export const EVENT_MATCH_SCORE = 'matchScore';
Expand Down
6 changes: 6 additions & 0 deletions src/game/dto/emit-event-invitation-reaponse.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class EmitEventInvitationReplyDto {
targetUserId: number;
targetUserChannelSocketId: string;
isAccepted: boolean;
gameId: number | null;
}
5 changes: 5 additions & 0 deletions src/game/dto/emit-event-matchmaking-param.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class EmitEventMatchmakingReplyDto {
targetUserId: number;
targetUserChannelSocketId: string;
gameId: number | null;
}
6 changes: 6 additions & 0 deletions src/game/dto/match-game-delete-param.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GameType } from 'src/common/enum';

export class gameMatchDeleteParamDto {
userId: number;
gameType: GameType;
}
6 changes: 6 additions & 0 deletions src/game/dto/match-game-param.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { GameType } from 'src/common/enum';

export class gameMatchStartParamDto {
userId: number;
gameType: GameType;
}
30 changes: 30 additions & 0 deletions src/game/game.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Post,
Expand All @@ -17,6 +18,9 @@ import { PositiveIntPipe } from '../common/pipes/positiveInt.pipe';
import { AuthGuard } from '@nestjs/passport';
import { DeleteGameInvitationParamDto } from './dto/delete-invitation-param.dto';
import { acceptGameParamDto } from './dto/accept-game-param.dto';
import { gameMatchStartParamDto } from './dto/match-game-param.dto';
import { GameType } from 'src/common/enum';
import { gameMatchDeleteParamDto } from './dto/match-game-delete-param.dto';

@Controller('game')
@ApiTags('game')
Expand Down Expand Up @@ -86,4 +90,30 @@ export class GameController {
deleteInvitationParamDto,
);
}

@Post('/match')
async gameMatchStart(
@GetUser() user: User,
@Body('gameType')
gameType: GameType,
) {
const gameMatchStartDto: gameMatchStartParamDto = {
userId: user.id,
gameType: gameType,
};
await this.gameService.gameMatchStart(gameMatchStartDto);
}

@Delete('/match/:gameType')
async gameMatchCancel(
@GetUser() user: User,
@Param('gameType')
gameType: GameType,
) {
const gameDeleteMatchDto: gameMatchDeleteParamDto = {
userId: user.id,
gameType: gameType,
};
await this.gameService.gameMatchCancel(gameDeleteMatchDto);
}
}
16 changes: 16 additions & 0 deletions src/game/game.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
EVENT_MATCH_SCORE,
EVENT_MATCH_STATUS,
EVENT_SERVER_GAME_READY,
EVENT_GAME_MATCHED,
} from '../common/events';
import { ChannelsGateway } from '../channels/channels.gateway';
import { EmitEventInvitationReplyDto } from './dto/emit-event-invitation-reply.dto';
Expand All @@ -30,6 +31,7 @@ import { GameStatus, UserStatus } from '../common/enum';
import { EmitEventMatchStatusDto } from './dto/emit-event-match-status.dto';
import { EmitEventMatchScoreParamDto } from './dto/emit-event-match-score-param.dto';
import { EmitEventServerGameReadyParamDto } from './dto/emit-event-server-game-ready-param.dto';
import { EmitEventMatchmakingReplyDto } from './dto/emit-event-matchmaking-param.dto';

@WebSocketGateway({ namespace: 'game' })
@UseFilters(WsExceptionFilter)
Expand Down Expand Up @@ -119,6 +121,20 @@ export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
});
}

async sendMatchmakingReply(
sendMatchmakingReplyDto: EmitEventMatchmakingReplyDto,
) {
const targetUserChannelSocketId =
sendMatchmakingReplyDto.targetUserChannelSocketId;
const gameId = sendMatchmakingReplyDto.gameId;

this.channelsGateway.server
.to(targetUserChannelSocketId)
.emit(EVENT_GAME_MATCHED, {
gameId: gameId,
});
}

@SubscribeMessage('gameRequest')
async prepareGame(
@ConnectedSocket() client: Socket,
Expand Down
115 changes: 112 additions & 3 deletions src/game/game.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ import { GameGateway } from './game.gateway';
import { CreateInitialGameParamDto } from './dto/create-initial-game-param.dto';
import { GameInvitationRepository } from './game-invitation.repository';
import { DeleteGameInvitationParamDto } from './dto/delete-invitation-param.dto';
import { GameStatus, UserStatus } from '../common/enum';
import { GameStatus, GameType, UserStatus } from '../common/enum';
import { acceptGameParamDto } from './dto/accept-game-param.dto';
import { EmitEventInvitationReplyDto } from './dto/emit-event-invitation-reply.dto';
import { EmitEventInvitationReplyDto } from './dto/emit-event-invitation-reaponse.dto';
import { User } from '../users/entities/user.entity';
import * as moment from 'moment';
import { BlocksRepository } from '../users/blocks.repository';
import { gameMatchStartParamDto } from './dto/match-game-param.dto';
import { EmitEventMatchmakingReplyDto } from './dto/emit-event-matchmaking-param.dto';
import { gameMatchDeleteParamDto } from './dto/match-game-delete-param.dto';

@Injectable()
export class GameService {
private gameQueue: Record<string, User[]> = {
LADDER: [],
NORMAL_MATCHING: [],
SPECIAL_MATCHING: [],
};
constructor(
private readonly gameRepository: GameRepository,
private readonly gameInvitationRepository: GameInvitationRepository,
Expand Down Expand Up @@ -190,6 +198,108 @@ export class GameService {
);
}

async gameMatchStart(gameMatchStartParamDto: gameMatchStartParamDto) {
const userId = gameMatchStartParamDto.userId;
const gameType = gameMatchStartParamDto.gameType;

// 유저가 존재하는지
const user = await this.checkUserExist(userId);

if (user.status !== UserStatus.ONLINE || !user.channelSocketId)
throw new BadRequestException(
`유저 ${user.id} 는 OFFLINE 상태이거나 게임중입니다`,
);
if (
gameType !== GameType.LADDER &&
gameType !== GameType.NORMAL_MATCHING &&
gameType !== GameType.SPECIAL_MATCHING
) {
throw new BadRequestException(`지원하지 않는 게임 타입입니다`);
}

const gameQueue = this.gameQueue[gameType];

// 이미 큐에 존재하는지
const index = gameQueue.indexOf(user);
if (index > -1)
throw new BadRequestException(
`유저 ${user.id} 는 이미 매칭 큐에 존재합니다`,
);

gameQueue.push(user);

if (gameQueue.length === 2) {
const player1 = gameQueue[0];
const player2 = gameQueue[1];

if (
player1.channelSocketId === null ||
player2.channelSocketId === null
)
throw new BadRequestException(
tomatozil marked this conversation as resolved.
Show resolved Hide resolved
`유저 ${player1.id} 는 OFFLINE 상태입니다`,
);

const gameDto = new CreateInitialGameParamDto(
player1.id,
player2.id,
gameType,
GameStatus.WAITING,
);
const game = await this.gameRepository.createGame(gameDto);

const invitationReplyToPlayer1Dto: EmitEventMatchmakingReplyDto = {
targetUserId: player1.id,
targetUserChannelSocketId: player1.channelSocketId,
gameId: game.id,
};
const invitationReplyToPlayer2Dto: EmitEventMatchmakingReplyDto = {
targetUserId: player2.id,
targetUserChannelSocketId: player2.channelSocketId,
gameId: game.id,
};

await this.gameGateway.sendMatchmakingReply(
invitationReplyToPlayer1Dto,
);
await this.gameGateway.sendMatchmakingReply(
invitationReplyToPlayer2Dto,
);
gameQueue.splice(0, 2);
}
}

async gameMatchCancel(gameMatchCancelParamDto: gameMatchDeleteParamDto) {
const userId = gameMatchCancelParamDto.userId;
const gameType = gameMatchCancelParamDto.gameType;

// 유저가 존재하는지
const user = await this.checkUserExist(userId);

if (user.status !== UserStatus.ONLINE || !user.channelSocketId)
throw new BadRequestException(
`유저 ${user.id} 는 OFFLINE 상태이거나 게임중입니다`,
);

if (
gameType !== GameType.LADDER &&
gameType !== GameType.NORMAL_MATCHING &&
gameType !== GameType.SPECIAL_MATCHING
) {
throw new BadRequestException(`지원하지 않는 게임 타입입니다`);
}

const gameQueue = this.gameQueue[gameType];

const index = gameQueue.indexOf(user);
if (index > -1) {
gameQueue.splice(index, 1);
} else if (index === -1)
throw new BadRequestException(
`유저 ${user.id} 는 매칭 큐에 존재하지 않습니다`,
);
}

async deleteInvitationByInvitingUserId(
deleteInvitationParamDto: DeleteGameInvitationParamDto,
) {
Expand All @@ -216,7 +326,6 @@ export class GameService {
`시간초과로 유효하지 않은 요청입니다`,
);
}

await this.gameInvitationRepository.softDelete(invitationId);
}

Expand Down
Loading