Feat : start games with sse
This commit is contained in:
Generated
+20
@@ -12,6 +12,7 @@
|
|||||||
"@nestjs/common": "^10.3.2",
|
"@nestjs/common": "^10.3.2",
|
||||||
"@nestjs/config": "^3.2.2",
|
"@nestjs/config": "^3.2.2",
|
||||||
"@nestjs/core": "^10.3.2",
|
"@nestjs/core": "^10.3.2",
|
||||||
|
"@nestjs/event-emitter": "^2.0.4",
|
||||||
"@nestjs/mapped-types": "^2.0.5",
|
"@nestjs/mapped-types": "^2.0.5",
|
||||||
"@nestjs/mongoose": "^10.0.6",
|
"@nestjs/mongoose": "^10.0.6",
|
||||||
"@nestjs/platform-express": "^10.3.2",
|
"@nestjs/platform-express": "^10.3.2",
|
||||||
@@ -1828,6 +1829,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/event-emitter": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter2": "6.4.9"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0",
|
||||||
|
"@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/mapped-types": {
|
"node_modules/@nestjs/mapped-types": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz",
|
||||||
@@ -5018,6 +5032,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter2": {
|
||||||
|
"version": "6.4.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
|
||||||
|
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/events": {
|
"node_modules/events": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"@nestjs/common": "^10.3.2",
|
"@nestjs/common": "^10.3.2",
|
||||||
"@nestjs/config": "^3.2.2",
|
"@nestjs/config": "^3.2.2",
|
||||||
"@nestjs/core": "^10.3.2",
|
"@nestjs/core": "^10.3.2",
|
||||||
|
"@nestjs/event-emitter": "^2.0.4",
|
||||||
"@nestjs/mapped-types": "^2.0.5",
|
"@nestjs/mapped-types": "^2.0.5",
|
||||||
"@nestjs/mongoose": "^10.0.6",
|
"@nestjs/mongoose": "^10.0.6",
|
||||||
"@nestjs/platform-express": "^10.3.2",
|
"@nestjs/platform-express": "^10.3.2",
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import { GamesModule } from './games/games.module';
|
|||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from './users/users.module';
|
||||||
import { CardsService } from './cards/cards.service';
|
import { CardsService } from './cards/cards.service';
|
||||||
import { CardsModule } from './cards/cards.module';
|
import { CardsModule } from './cards/cards.module';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
MongooseModule.forRoot(process.env.MONGO_ENDPOINT, {
|
MongooseModule.forRoot(process.env.MONGO_ENDPOINT, {
|
||||||
auth: {
|
auth: {
|
||||||
|
|||||||
@@ -1,141 +1,83 @@
|
|||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
|
||||||
Post,
|
|
||||||
Param,
|
|
||||||
Delete,
|
|
||||||
UseGuards,
|
|
||||||
Session,
|
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Patch,
|
Param,
|
||||||
Body,
|
Post,
|
||||||
|
Session,
|
||||||
|
Sse,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { GamesService } from '../services/games.lobby.service';
|
import { LobbyService } from '../services/games.lobby.service';
|
||||||
import { AuthGuard } from '../guards/auth.guard';
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { ReadGameDto } from '../dto/read-games.dto';
|
import { StartingDeckDto } from '../dto/update-game.dto';
|
||||||
import { GameGuard } from '../guards/game.guard';
|
import { Game, Player } from '../schemas/game.schema';
|
||||||
import { UpdateGameDto } from '../dto/update-game.dto';
|
import { Document } from 'mongoose';
|
||||||
|
import { GamePipe } from '../pipe/game.pipe';
|
||||||
|
|
||||||
@ApiTags('lobby')
|
import { ReadUserDto } from '../dto/read-games.dto';
|
||||||
@Controller('games')
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
function getPlayer(session: Record<string, any>, game: Game) {
|
||||||
|
const user = session.user as ReadUserDto;
|
||||||
|
if (!user) {
|
||||||
|
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
for (const team of game.teams) {
|
||||||
|
for (const player of team.players) {
|
||||||
|
if (player.user._id.toHexString() == user._id) {
|
||||||
|
return player as Document<Player> & Player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new HttpException(
|
||||||
|
'You are not part of this game',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags('game')
|
||||||
|
@Controller('games/:id')
|
||||||
export class GamesController {
|
export class GamesController {
|
||||||
constructor(private readonly gamesService: GamesService) {}
|
constructor(
|
||||||
|
private readonly lobbyService: LobbyService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post('changeStartingDeck')
|
||||||
@ApiOperation({ summary: 'Create game' })
|
@ApiOperation({ summary: 'Pick a different starting deck' })
|
||||||
@ApiResponse({
|
|
||||||
status: 201,
|
|
||||||
description: 'Game successfully created.',
|
|
||||||
type: ReadGameDto,
|
|
||||||
})
|
|
||||||
@ApiResponse({
|
|
||||||
status: 400,
|
|
||||||
description: 'You can only create one game at a time',
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 403, description: 'Forbidden.' })
|
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
async create(@Session() session: Record<string, any>) {
|
async changeStartingDeck(
|
||||||
const games = await this.gamesService.findByOwner(session.user._id);
|
|
||||||
if (games) {
|
|
||||||
throw new HttpException(
|
|
||||||
'You can only create one game at a time',
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.gamesService.create(session.user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({ summary: 'List all games' })
|
|
||||||
findAll() {
|
|
||||||
const games = this.gamesService.findAll();
|
|
||||||
return games;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get(':id')
|
|
||||||
@ApiOperation({ summary: 'Get game information' })
|
|
||||||
@UseGuards(GameGuard)
|
|
||||||
async findOne(@Param('id') id: string) {
|
|
||||||
const game = await this.gamesService.findById(id);
|
|
||||||
return game;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Patch(':id')
|
|
||||||
@ApiOperation({ summary: 'Edit game settings' })
|
|
||||||
@UseGuards(AuthGuard, GameGuard)
|
|
||||||
async update(
|
|
||||||
@Session() session: Record<string, any>,
|
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() updateGameDto: UpdateGameDto,
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
@Body() startingDeckDto: StartingDeckDto,
|
||||||
|
@Session() session: Record<string, string>,
|
||||||
) {
|
) {
|
||||||
const game = await this.gamesService.findById(id);
|
|
||||||
if (game.owner._id.toString() != session.user._id) {
|
|
||||||
throw new HttpException(
|
|
||||||
'You can only edit games you own',
|
|
||||||
HttpStatus.FORBIDDEN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this.gamesService.update(id, updateGameDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete(':id')
|
|
||||||
@ApiOperation({ summary: 'Delete game' })
|
|
||||||
@UseGuards(AuthGuard, GameGuard)
|
|
||||||
async remove(
|
|
||||||
@Session() session: Record<string, any>,
|
|
||||||
@Param('id') id: string,
|
|
||||||
) {
|
|
||||||
const game = await this.gamesService.findById(id);
|
|
||||||
if (game.owner._id.toString() != session.user._id) {
|
|
||||||
throw new HttpException(
|
|
||||||
'You can only delete games you own',
|
|
||||||
HttpStatus.FORBIDDEN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const deleted = await this.gamesService.remove(id);
|
|
||||||
if (deleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':id/start')
|
|
||||||
@ApiOperation({ summary: 'Start game' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 201,
|
|
||||||
description: 'Game successfully started.',
|
|
||||||
})
|
|
||||||
@ApiResponse({
|
|
||||||
status: 400,
|
|
||||||
description: 'Bad Request',
|
|
||||||
})
|
|
||||||
@ApiResponse({
|
|
||||||
status: 403,
|
|
||||||
description: 'You can only start games you own',
|
|
||||||
})
|
|
||||||
@UseGuards(AuthGuard, GameGuard)
|
|
||||||
async start(
|
|
||||||
@Session() session: Record<string, any>,
|
|
||||||
@Param('id') id: string,
|
|
||||||
) {
|
|
||||||
const game = await this.gamesService.findById(id);
|
|
||||||
if (game.started) {
|
if (game.started) {
|
||||||
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
|
||||||
}
|
|
||||||
if (game.teams.length < 2 && !process.env.DEV_MODE) {
|
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
'Not enough teams to start the game',
|
'You cannot do that after the game has started',
|
||||||
HttpStatus.BAD_REQUEST,
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (game.owner._id.toString() != session.user._id) {
|
const player = getPlayer(session, game);
|
||||||
throw new HttpException(
|
await this.lobbyService.changeStartingDeck(startingDeckDto, player, game);
|
||||||
'You can only start games you own',
|
}
|
||||||
HttpStatus.FORBIDDEN,
|
|
||||||
);
|
@Sse('/subscribe')
|
||||||
}
|
subscribe(
|
||||||
this.gamesService.start(id);
|
@Param('id') id: string,
|
||||||
return;
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
@Param('id', GamePipe) _game: Document<Game> & Game,
|
||||||
|
): Observable<{ data: string }> {
|
||||||
|
return fromEvent(this.eventEmitter, 'sse.game.' + id).pipe(
|
||||||
|
map((payload) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -1,17 +1,17 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { GamesController } from './games.controller';
|
import { LobbyController } from './games.lobby.controller';
|
||||||
import { GamesService } from '../services/games.lobby.service';
|
import { LobbyService } from '../services/games.lobby.service';
|
||||||
|
|
||||||
describe('GamesController', () => {
|
describe('GamesController', () => {
|
||||||
let controller: GamesController;
|
let controller: LobbyController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [GamesController],
|
controllers: [LobbyController],
|
||||||
providers: [GamesService],
|
providers: [LobbyService],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<GamesController>(GamesController);
|
controller = module.get<LobbyController>(LobbyController);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
UseGuards,
|
||||||
|
Session,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
Sse,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { LobbyService } from '../services/games.lobby.service';
|
||||||
|
import { AuthGuard } from '../guards/auth.guard';
|
||||||
|
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { ReadGameDto } from '../dto/read-games.dto';
|
||||||
|
import { UpdateGameDto } from '../dto/update-game.dto';
|
||||||
|
import { GamePipe } from '../pipe/game.pipe';
|
||||||
|
import { Game } from '../schemas/game.schema';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
import { Observable, fromEvent, map } from 'rxjs';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
@ApiTags('lobby')
|
||||||
|
@Controller('games')
|
||||||
|
export class LobbyController {
|
||||||
|
constructor(
|
||||||
|
private readonly gamesService: LobbyService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: 'Create game' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Game successfully created.',
|
||||||
|
type: ReadGameDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'You can only create one game at a time',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 403, description: 'Forbidden.' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async create(@Session() session: Record<string, any>) {
|
||||||
|
const games = await this.gamesService.findByOwner(session.user._id);
|
||||||
|
if (games) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only create one game at a time',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.gamesService.create(session.user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: 'List all games' })
|
||||||
|
findAll() {
|
||||||
|
const games = this.gamesService.findAll();
|
||||||
|
return games;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Sse('/subscribe')
|
||||||
|
subscribe(): Observable<{ data: string }> {
|
||||||
|
console.log('subscris');
|
||||||
|
return fromEvent(this.eventEmitter, 'sse.lobby').pipe(
|
||||||
|
map((payload) => {
|
||||||
|
return {
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: 'Get game information' })
|
||||||
|
async findOne(@Param('id') id: string, @Param('id', GamePipe) game: Game) {
|
||||||
|
return new ReadGameDto(game);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: 'Edit game settings' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Body() updateGameDto: UpdateGameDto,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.owner._id.toString() != session.user._id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only edit games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.gamesService.update(game, updateGameDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: 'Delete game' })
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async remove(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.owner._id.toString() != session.user._id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only delete games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const deleted = await this.gamesService.remove(game);
|
||||||
|
if (deleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/start')
|
||||||
|
@ApiOperation({ summary: 'Start game' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Game successfully started.',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'Bad Request',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: 'You can only start games you own',
|
||||||
|
})
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
async start(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Session() session: Record<string, any>,
|
||||||
|
@Param('id', GamePipe) game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (game.started) {
|
||||||
|
throw new HttpException('Game already started', HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
if (game.teams.length < 2 && !process.env.DEV_MODE) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Not enough teams to start the game',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (game.owner._id.toString() != session.user._id) {
|
||||||
|
throw new HttpException(
|
||||||
|
'You can only start games you own',
|
||||||
|
HttpStatus.FORBIDDEN,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.gamesService.start(game);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ export class ReadPlayerDto extends DatabaseObjectDto {
|
|||||||
this.discard = new ReadContainerDto(data.discard);
|
this.discard = new ReadContainerDto(data.discard);
|
||||||
this.hand = new ReadContainerDto(data.hand, true);
|
this.hand = new ReadContainerDto(data.hand, true);
|
||||||
this.stack = new ReadContainerDto(data.stack, true);
|
this.stack = new ReadContainerDto(data.stack, true);
|
||||||
|
this.race = data.race;
|
||||||
|
this.class = data.class;
|
||||||
}
|
}
|
||||||
user: ReadUserDto;
|
user: ReadUserDto;
|
||||||
|
|
||||||
@@ -23,6 +25,9 @@ export class ReadPlayerDto extends DatabaseObjectDto {
|
|||||||
stack: ReadContainerDto;
|
stack: ReadContainerDto;
|
||||||
|
|
||||||
discard: ReadContainerDto;
|
discard: ReadContainerDto;
|
||||||
|
|
||||||
|
class: string;
|
||||||
|
race: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ReadTeamDto extends DatabaseObjectDto {
|
export class ReadTeamDto extends DatabaseObjectDto {
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
export class UpdateGameDto {
|
export class UpdateGameDto {
|
||||||
packs: string[];
|
packs: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class StartingDeckDto {
|
||||||
|
class?: string;
|
||||||
|
race?: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { GamesService } from './services/games.lobby.service';
|
import { LobbyService } from './services/games.lobby.service';
|
||||||
import { GamesController } from './controllers/games.controller';
|
import { LobbyController } from './controllers/games.lobby.controller';
|
||||||
import { Game, GameSchema } from './schemas/game.schema';
|
import { Game, GameSchema } from './schemas/game.schema';
|
||||||
import { MongooseModule } from '@nestjs/mongoose';
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
import { UsersModule } from 'src/users/users.module';
|
import { UsersModule } from 'src/users/users.module';
|
||||||
import { CardsModule } from 'src/cards/cards.module';
|
import { CardsModule } from 'src/cards/cards.module';
|
||||||
|
import { GamesController } from './controllers/games.controller';
|
||||||
|
import { GameService } from './services/games.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -12,7 +14,7 @@ import { CardsModule } from 'src/cards/cards.module';
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]),
|
MongooseModule.forFeature([{ name: Game.name, schema: GameSchema }]),
|
||||||
],
|
],
|
||||||
controllers: [GamesController],
|
controllers: [LobbyController, GamesController],
|
||||||
providers: [GamesService],
|
providers: [LobbyService, GameService],
|
||||||
})
|
})
|
||||||
export class GamesModule {}
|
export class GamesModule {}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
CanActivate,
|
|
||||||
ExecutionContext,
|
|
||||||
HttpException,
|
|
||||||
HttpStatus,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Request } from 'express';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { GamesService } from '../services/games.lobby.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class GameGuard implements CanActivate {
|
|
||||||
constructor(private readonly gamesService: GamesService) {}
|
|
||||||
|
|
||||||
canActivate(
|
|
||||||
context: ExecutionContext,
|
|
||||||
): boolean | Promise<boolean> | Observable<boolean> {
|
|
||||||
const http = context.switchToHttp();
|
|
||||||
const request = http.getRequest<Request>();
|
|
||||||
return (async () => {
|
|
||||||
const game = await this.gamesService.findById(request.params.id);
|
|
||||||
if (!game) {
|
|
||||||
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
if (!(request.session as Record<string, any>).user) {
|
|
||||||
throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import {
|
||||||
|
PipeTransform,
|
||||||
|
Injectable,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Game } from '../schemas/game.schema';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GamePipe implements PipeTransform {
|
||||||
|
constructor(@InjectModel(Game.name) private gameModel: Model<Game>) {}
|
||||||
|
|
||||||
|
async transform(value: any) {
|
||||||
|
const game = await this.gameModel.findById(value);
|
||||||
|
if (!game) {
|
||||||
|
throw new HttpException('Game not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
import mongoose, { HydratedDocument, Types } from 'mongoose';
|
import mongoose, { HydratedDocument, Types } from 'mongoose';
|
||||||
import { Card } from 'src/cards/schemas/cards.schema';
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { CardRole } from 'src/cards/schemas/cards.types';
|
||||||
import { User } from 'src/users/schemas/user.entity';
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
|
|
||||||
export type GameDocument = HydratedDocument<Game>;
|
export type GameDocument = HydratedDocument<Game>;
|
||||||
@@ -25,6 +26,12 @@ export class Player extends Types.ObjectId {
|
|||||||
})
|
})
|
||||||
user: User;
|
user: User;
|
||||||
|
|
||||||
|
@Prop({ type: String, default: CardRole.PERSONAL })
|
||||||
|
class?: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
race?: string;
|
||||||
|
|
||||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||||
board!: Container;
|
board!: Container;
|
||||||
|
|
||||||
@@ -53,7 +60,7 @@ export const TeamSchema = SchemaFactory.createForClass(Team);
|
|||||||
|
|
||||||
@Schema()
|
@Schema()
|
||||||
export class Game extends Types.ObjectId {
|
export class Game extends Types.ObjectId {
|
||||||
@Prop({ type: [{ type: Team, autopopulate: true }] })
|
@Prop({ type: [{ type: TeamSchema, autopopulate: true }] })
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
|
|
||||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||||
@@ -65,11 +72,7 @@ export class Game extends Types.ObjectId {
|
|||||||
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
@Prop({ type: ContainerSchema, default: {}, autopopulate: true })
|
||||||
fireGems: Container;
|
fireGems: Container;
|
||||||
|
|
||||||
@Prop({
|
@Prop({ type: Team, autopopulate: true })
|
||||||
type: mongoose.Schema.Types.ObjectId,
|
|
||||||
ref: 'Team',
|
|
||||||
autopopulate: true,
|
|
||||||
})
|
|
||||||
currentTurn?: Team;
|
currentTurn?: Team;
|
||||||
|
|
||||||
@Prop({ default: false })
|
@Prop({ default: false })
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { GamesService } from './games.lobby.service';
|
import { LobbyService } from './games.lobby.service';
|
||||||
|
|
||||||
describe('GamesService', () => {
|
describe('GamesService', () => {
|
||||||
let service: GamesService;
|
let service: LobbyService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [GamesService],
|
providers: [LobbyService],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<GamesService>(GamesService);
|
service = module.get<LobbyService>(LobbyService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
import { DatabaseObjectDto } from '../dto/database-object.dto';
|
import { DatabaseObjectDto } from '../dto/database-object.dto';
|
||||||
import { InjectModel } from '@nestjs/mongoose';
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
import { Game, Player, Team } from '../schemas/game.schema';
|
import { Game, Player, Team } from '../schemas/game.schema';
|
||||||
import { Model } from 'mongoose';
|
import { Document, Model } from 'mongoose';
|
||||||
import { ReadGameDto } from '../dto/read-games.dto';
|
import { ReadContainerDto, ReadGameDto } from '../dto/read-games.dto';
|
||||||
import { User } from 'src/users/schemas/user.entity';
|
import { User } from 'src/users/schemas/user.entity';
|
||||||
import { Card } from 'src/cards/schemas/cards.schema';
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
import { CardRole } from 'src/cards/schemas/cards.types';
|
import { CardRole } from 'src/cards/schemas/cards.types';
|
||||||
import { UpdateGameDto } from '../dto/update-game.dto';
|
import { StartingDeckDto, UpdateGameDto } from '../dto/update-game.dto';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { GameService } from './games.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GamesService {
|
export class LobbyService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectModel(Game.name) private gameModel: Model<Game>,
|
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||||
@InjectModel(Card.name) private cardModel: Model<Card>,
|
@InjectModel(Card.name) private cardModel: Model<Card>,
|
||||||
|
private readonly gamesService: GameService,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(owner: User): Promise<DatabaseObjectDto> {
|
async create(owner: User): Promise<DatabaseObjectDto> {
|
||||||
@@ -31,7 +35,10 @@ export class GamesService {
|
|||||||
} as Team);
|
} as Team);
|
||||||
|
|
||||||
const gamedata = await createdGame.save();
|
const gamedata = await createdGame.save();
|
||||||
return new ReadGameDto(gamedata);
|
const dto = new ReadGameDto(gamedata);
|
||||||
|
this.eventEmitter.emit('sse.lobby', { type: 'create', ...dto });
|
||||||
|
|
||||||
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(): Promise<ReadGameDto[]> {
|
async findAll(): Promise<ReadGameDto[]> {
|
||||||
@@ -43,14 +50,6 @@ export class GamesService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string) {
|
|
||||||
const game = await this.gameModel.findById(id).exec();
|
|
||||||
if (!game) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new ReadGameDto(game);
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByOwner(_id: string): Promise<ReadGameDto> {
|
async findByOwner(_id: string): Promise<ReadGameDto> {
|
||||||
const game = await this.gameModel.findOne({ owner: _id }).exec();
|
const game = await this.gameModel.findOne({ owner: _id }).exec();
|
||||||
if (!game) {
|
if (!game) {
|
||||||
@@ -59,24 +58,71 @@ export class GamesService {
|
|||||||
return new ReadGameDto(game);
|
return new ReadGameDto(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, updateGameDto: UpdateGameDto) {
|
async update(game: Document<Game> & Game, updateGameDto: UpdateGameDto) {
|
||||||
const game = await this.gameModel.findById(id).exec();
|
|
||||||
if (!game) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
game.packs = updateGameDto.packs;
|
game.packs = updateGameDto.packs;
|
||||||
return;
|
const packs = await this.cardModel.distinct('pack');
|
||||||
}
|
if (updateGameDto.packs.every((e) => packs.includes(e))) {
|
||||||
|
await game.save();
|
||||||
async remove(id: string) {
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
return await this.gameModel.findByIdAndDelete(id);
|
type: 'update',
|
||||||
}
|
_id: game._id,
|
||||||
|
...updateGameDto,
|
||||||
async start(id: string) {
|
});
|
||||||
const game = await this.gameModel.findById(id).exec();
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
if (!game) {
|
type: 'update',
|
||||||
return null;
|
...updateGameDto,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
throw new HttpException('Pack not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(game: Document<Game> & Game) {
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
_id: game._id,
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'delete',
|
||||||
|
});
|
||||||
|
return await game.deleteOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(game: Document<Game> & Game) {
|
||||||
|
const random = Math.floor(Math.random() * game.teams.length);
|
||||||
|
game.currentTurn = game.teams[random];
|
||||||
|
await game.save();
|
||||||
|
game.started = true;
|
||||||
|
this.eventEmitter.emit('sse.lobby', {
|
||||||
|
type: 'start',
|
||||||
|
_id: game._id,
|
||||||
|
});
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'start',
|
||||||
|
game: new ReadGameDto(game),
|
||||||
|
});
|
||||||
|
let playerIndex = 0;
|
||||||
|
await Promise.all(
|
||||||
|
game.teams.map((team) =>
|
||||||
|
Promise.all(
|
||||||
|
team.players.map(async (player) => {
|
||||||
|
player.stack.cards = await this.cardModel
|
||||||
|
.find({
|
||||||
|
pack: { $in: game.packs },
|
||||||
|
role: { $in: [player.class, player.race] },
|
||||||
|
playerIndex,
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'populateContainer',
|
||||||
|
container: new ReadContainerDto(player.stack, true),
|
||||||
|
});
|
||||||
|
this.gamesService.shuffleContainer(player.stack, game);
|
||||||
|
playerIndex++;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
game.fireGems.cards = await this.cardModel.find({
|
game.fireGems.cards = await this.cardModel.find({
|
||||||
pack: { $in: game.packs },
|
pack: { $in: game.packs },
|
||||||
role: CardRole.FIRE_GEM,
|
role: CardRole.FIRE_GEM,
|
||||||
@@ -87,24 +133,51 @@ export class GamesService {
|
|||||||
role: CardRole.MARKET,
|
role: CardRole.MARKET,
|
||||||
});
|
});
|
||||||
|
|
||||||
let playerIndex = 0;
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
await Promise.all(
|
type: 'populateContainer',
|
||||||
game.teams.map((team) =>
|
container: new ReadContainerDto(game.fireGems),
|
||||||
Promise.all(
|
});
|
||||||
team.players.map(async (player) => {
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
player.stack.cards = await this.cardModel
|
type: 'populateContainer',
|
||||||
.find({
|
container: new ReadContainerDto(game.marketStack, true),
|
||||||
pack: { $in: game.packs },
|
});
|
||||||
role: CardRole.PERSONAL,
|
|
||||||
playerIndex,
|
await this.gamesService.shuffleContainer(game.marketStack, game);
|
||||||
})
|
|
||||||
.exec();
|
|
||||||
playerIndex++;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
game.started = true;
|
|
||||||
await game.save();
|
await game.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeStartingDeck(
|
||||||
|
data: StartingDeckDto,
|
||||||
|
player: Document<Player> & Player,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
const packs = await this.cardModel
|
||||||
|
.find({ pack: { $in: game.packs } })
|
||||||
|
.distinct('role');
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Object.values<string>(CardRole).includes(data.class) ||
|
||||||
|
!Object.values<string>(CardRole).includes(data.race) ||
|
||||||
|
data.race == CardRole.FIRE_GEM ||
|
||||||
|
data.race == CardRole.MARKET ||
|
||||||
|
data.class == CardRole.FIRE_GEM ||
|
||||||
|
data.class == CardRole.MARKET
|
||||||
|
) {
|
||||||
|
throw new HttpException('Invalid race or class', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
packs.includes(data.class as CardRole) &&
|
||||||
|
packs.includes(data.race as CardRole)
|
||||||
|
) {
|
||||||
|
player.class = data.class;
|
||||||
|
player.race = data.race;
|
||||||
|
await game.save();
|
||||||
|
this.eventEmitter.emit('sse.game.changeStartingDeck', {
|
||||||
|
_id: player._id,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new HttpException('Pack not found', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Container, Game } from '../schemas/game.schema';
|
||||||
|
import { Document, Model } from 'mongoose';
|
||||||
|
import { Card } from 'src/cards/schemas/cards.schema';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
function shuffle(a) {
|
||||||
|
let j, x, i;
|
||||||
|
for (i = a.length - 1; i > 0; i--) {
|
||||||
|
j = Math.floor(Math.random() * (i + 1));
|
||||||
|
x = a[i];
|
||||||
|
a[i] = a[j];
|
||||||
|
a[j] = x;
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GameService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(Game.name) private gameModel: Model<Game>,
|
||||||
|
@InjectModel(Card.name) private cardModel: Model<Card>,
|
||||||
|
private eventEmitter: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async shuffleContainer(container: Container, game: Document<Game> & Game) {
|
||||||
|
container.cards = shuffle(container.cards);
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'shuffleContainer',
|
||||||
|
container: container._id.toHexString(),
|
||||||
|
});
|
||||||
|
await game.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
async distributeCards(
|
||||||
|
cards: Card[],
|
||||||
|
from: Container,
|
||||||
|
to: Container,
|
||||||
|
game: Document<Game> & Game,
|
||||||
|
) {
|
||||||
|
if (cards.some((c) => !from.cards.includes(c))) {
|
||||||
|
throw new HttpException(
|
||||||
|
'Card not found in container',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const c of cards) {
|
||||||
|
const index = from.cards.indexOf(c);
|
||||||
|
from.cards.splice(index, 1);
|
||||||
|
to.cards.push(c);
|
||||||
|
}
|
||||||
|
this.eventEmitter.emit('sse.game.' + game._id.toHexString(), {
|
||||||
|
type: 'distributeCards',
|
||||||
|
from: from._id.toHexString(),
|
||||||
|
to: to._id.toHexString(),
|
||||||
|
cards: cards,
|
||||||
|
});
|
||||||
|
await game.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,7 +110,6 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const data2 = await req2.json();
|
const data2 = await req2.json();
|
||||||
console.log(data2);
|
|
||||||
const dto = new LoginDto(data2.id, data2.global_name, data2.avatar);
|
const dto = new LoginDto(data2.id, data2.global_name, data2.avatar);
|
||||||
const user = await this.getOrCreateUser(dto.userId, dto);
|
const user = await this.getOrCreateUser(dto.userId, dto);
|
||||||
session.user = user;
|
session.user = user;
|
||||||
|
|||||||
Reference in New Issue
Block a user