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 all 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
125 changes: 122 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,118 @@ 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) {
let player1 = gameQueue.shift();
let player2 = gameQueue.shift();

// player1 또는 2가 OFFLINE이라면 큐에서 제거시키고 다시 매칭
while (player1 && player1.channelSocketId === null) {
player1 = gameQueue.shift();
}

while (player2 && player2.channelSocketId === null) {
player2 = gameQueue.shift();
}

if (
!player1 ||
!player2 ||
player1.channelSocketId === null ||
player2.channelSocketId === null
)
throw new BadRequestException(
tomatozil marked this conversation as resolved.
Show resolved Hide resolved
`게임 매칭 큐에 유저가 2명 이상이어야 합니다`,
);

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,
);
}
}

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 +336,6 @@ export class GameService {
`시간초과로 유효하지 않은 요청입니다`,
);
}

await this.gameInvitationRepository.softDelete(invitationId);
}

Expand Down
Loading