Skip to content

Commit

Permalink
Merge pull request #93 from tscenping/jangcho
Browse files Browse the repository at this point in the history
feat(game): add game queue matchmaking logics
  • Loading branch information
tomatozil authored Dec 21, 2023
2 parents fcc43e4 + fe25b07 commit fb7bcd5
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 3 deletions.
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(
`게임 매칭 큐에 유저가 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

0 comments on commit fb7bcd5

Please sign in to comment.