From 0d4c0e008c107b18f47aa5d47e271e5cdb2d4d4e Mon Sep 17 00:00:00 2001 From: Legonzaur Date: Tue, 11 Jun 2024 14:57:02 -0400 Subject: [PATCH] WIP : before-cqrs --- .vscode/launch.json | 20 ++- package-lock.json | 10 ++ package.json | 1 + src/app.controller.ts | 12 +- src/app.module.ts | 7 + src/app.service.ts | 6 +- src/cards/cards.module.ts | 16 +- src/cards/cards.service.ts | 121 ++++++++++++++- src/cards/schemas/cards.schema.ts | 34 ++++- src/cards/schemas/cards.types.ts | 48 ++++++ src/cards/schemas/effect.schema.ts | 91 +++++++++++ .../games.controller.spec.ts | 2 +- src/games/controllers/games.controller.ts | 141 ++++++++++++++++++ src/games/dto/read-games.dto.ts | 83 +++++++---- src/games/dto/update-game.dto.ts | 4 +- src/games/games.controller.ts | 66 -------- src/games/games.module.ts | 4 +- src/games/games.service.ts | 40 ----- src/games/guards/game.guard.ts | 32 ++++ src/games/schemas/game.schema.ts | 68 +++++++-- .../games.lobby.service.spec.ts} | 2 +- src/games/services/games.lobby.service.ts | 110 ++++++++++++++ 22 files changed, 750 insertions(+), 168 deletions(-) create mode 100644 src/cards/schemas/effect.schema.ts rename src/games/{ => controllers}/games.controller.spec.ts (89%) create mode 100644 src/games/controllers/games.controller.ts delete mode 100644 src/games/games.controller.ts delete mode 100644 src/games/games.service.ts create mode 100644 src/games/guards/game.guard.ts rename src/games/{games.service.spec.ts => services/games.lobby.service.spec.ts} (88%) create mode 100644 src/games/services/games.lobby.service.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index c9b6fc5..7c5f392 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,10 +5,22 @@ "version": "0.2.0", "configurations": [ { - "command": "npm run start:dev -b swc", - "name": "Start Nest", + "name": "Launch via NPM", "request": "launch", - "type": "node-terminal" - }, + "runtimeArgs": [ + "run", + "start:dev", + "-b", + "swc" + ], + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "node", + "console": "internalConsole", + "outputCapture": "std", + "internalConsoleOptions": "openOnSessionStart" + } ] } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 873862a..d10355c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "connect-mongo": "^5.1.0", "express-session": "^1.18.0", "mongoose": "^8.4.1", + "mongoose-autopopulate": "^1.1.0", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1" }, @@ -7587,6 +7588,15 @@ "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose-autopopulate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongoose-autopopulate/-/mongoose-autopopulate-1.1.0.tgz", + "integrity": "sha512-nTlTMlu1fLQ1bmJT7ILKbZmPGt2fHErLO4UJwzMDsHSigjtUYz0l3nvFhg511QkOkZcKBRzOnPn3DmmLIUENzg==", + "license": "Apache 2.0", + "peerDependencies": { + "mongoose": "6.x || 7.x || 8.0.0-rc0 || 8.x" + } + }, "node_modules/mongoose/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 78fdabd..f1cbb9e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "connect-mongo": "^5.1.0", "express-session": "^1.18.0", "mongoose": "^8.4.1", + "mongoose-autopopulate": "^1.1.0", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1" }, diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..c0c5805 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,16 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Put } from '@nestjs/common'; import { AppService } from './app.service'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +@ApiTags('admin') @Controller() export class AppController { constructor(private readonly appService: AppService) {} - @Get() - getHello(): string { - return this.appService.getHello(); + @Put('loadCards') + @ApiOperation({ summary: 'Load cards into the database' }) + loadCards(): void { + this.appService.loadCards(); + return; } } diff --git a/src/app.module.ts b/src/app.module.ts index 8b20e93..cfdfb5d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,7 @@ import { AppService } from './app.service'; import { GamesModule } from './games/games.module'; import { UsersModule } from './users/users.module'; import { CardsService } from './cards/cards.service'; +import { CardsModule } from './cards/cards.module'; @Module({ imports: [ @@ -16,7 +17,13 @@ import { CardsService } from './cards/cards.service'; username: process.env.MONGO_USER, }, dbName: 'heron', + connectionFactory: (connection) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + connection.plugin(require('mongoose-autopopulate')); + return connection; + }, }), + CardsModule, GamesModule, UsersModule, ], diff --git a/src/app.service.ts b/src/app.service.ts index 927d7cc..2ff22cc 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,8 +1,10 @@ import { Injectable } from '@nestjs/common'; +import { CardsService } from './cards/cards.service'; @Injectable() export class AppService { - getHello(): string { - return 'Hello World!'; + constructor(private readonly cardService: CardsService) {} + loadCards(): void { + this.cardService.loadCards(); } } diff --git a/src/cards/cards.module.ts b/src/cards/cards.module.ts index f3c32d7..b220f6a 100644 --- a/src/cards/cards.module.ts +++ b/src/cards/cards.module.ts @@ -1,12 +1,24 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Card, CardSchema } from './schemas/cards.schema'; +import { CardsService } from './cards.service'; +// import { +// Effect, +// EffectCondition, +// EffectConditionSchema, +// EffectSchema, +// } from './schemas/effect.schema'; @Module({ imports: [ - MongooseModule.forFeature([{ name: Card.name, schema: CardSchema }]), + MongooseModule.forFeature([ + // { name: Effect.name, schema: EffectSchema }, + // { name: EffectCondition.name, schema: EffectConditionSchema }, + { name: Card.name, schema: CardSchema }, + ]), ], - exports: [MongooseModule], + providers: [CardsService], + exports: [MongooseModule, CardsService], }) export class CardsModule { schema = CardSchema; diff --git a/src/cards/cards.service.ts b/src/cards/cards.service.ts index 136936a..a6d5a9a 100644 --- a/src/cards/cards.service.ts +++ b/src/cards/cards.service.ts @@ -1,11 +1,26 @@ import { Injectable } from '@nestjs/common'; import path from 'node:path'; import fs from 'node:fs/promises'; +import { Effect } from './schemas/effect.schema'; +import { Card } from './schemas/cards.schema'; +import { Model } from 'mongoose'; +import { InjectModel } from '@nestjs/mongoose'; +import { + CardFaction, + CardParser, + CardRole, + EffectParser, + EffectType, + EffectTypeVerbose, +} from './schemas/cards.types'; +import { EffectCondition } from './schemas/effect.schema'; @Injectable() export class CardsService { + constructor(@InjectModel(Card.name) private cardModel: Model) {} + async loadCards(): Promise { - const cardPath = path.join(path.resolve(), 'cards'); + const cardPath = path.join(path.resolve(), 'src', 'json'); const files = await fs.readdir(cardPath); const processed = await Promise.all( files.map(async (f) => { @@ -20,7 +35,107 @@ export class CardsService { }), ); - const packs = Object.fromEntries(processed); - console.log(packs); + await Promise.all( + Object.entries(processed).map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async ([_, data]: [string, [string, CardParser[]]]) => { + await Promise.all( + data[1].map((c) => { + if (c.role == CardRole.FIRE_GEM || c.role == CardRole.MARKET) { + for (let i = 0; i < (c.init_amount ?? 1); i++) { + this.createCard(c, data[0]); + } + } else { + for (let j = 0; j < 4; j++) { + for (let i = 0; i < (c.init_amount ?? 1); i++) { + this.createCard(c, data[0], j); + } + } + } + }), + ); + }, + ), + ); + } + + private async createCard( + json: CardParser, + pack: string, + playerIndex?: number, + ) { + const createdCard = await this.cardModel.create({ + cardId: json.id, + role: json.role, + cardType: json.card_type, + faction: json.faction, + name: json.name, + cost: json.cost, + defense: json.defense, + guard: json.guard, + pack: pack, + playerIndex, + }); + if (json.effects) { + createdCard.effects = await this.createEffects(json.effects, createdCard); + } + createdCard.save(); + } + + private async createEffects( + json: EffectParser[], + card: Card, + ): Promise { + return await Promise.all( + json.map(async (e) => { + const createdEffect = { + card: card, + effect: e.effect, + amount: e.amount, + times: e.times ?? 1, + per: e.per, + faction: e.faction, + } as Effect; + if (e.sub_effects) { + createdEffect.subEffects = await this.createEffects( + e.sub_effects, + card, + ); + } else { + createdEffect.subEffects = []; + } + + if (e.effect_type) { + createdEffect.condition = await this.createEffectCondition( + e.effect_type, + card, + ); + } + return createdEffect; + }), + ); + } + + private async createEffectCondition( + json: EffectType | EffectTypeVerbose, + card: Card, + ): Promise { + const createdCondition = {} as EffectCondition; + if (typeof json === 'string') { + createdCondition.effectId = json; + if (json == EffectType.FACTION_COMBO) { + createdCondition.faction = card.faction; + } + createdCondition.amount = 1; + } else { + createdCondition.effectId = json.id; + createdCondition.amount = json.amount ?? 1; + createdCondition.cardId = json.card_id; + if (json.id == EffectType.FACTION_COMBO) { + createdCondition.faction = (json.faction ?? + card.faction) as CardFaction; + } + } + return createdCondition; } } diff --git a/src/cards/schemas/cards.schema.ts b/src/cards/schemas/cards.schema.ts index 2c6f65e..b296b57 100644 --- a/src/cards/schemas/cards.schema.ts +++ b/src/cards/schemas/cards.schema.ts @@ -1,18 +1,42 @@ -import { Schema, SchemaFactory } from '@nestjs/mongoose'; -import { CardFaction, CardRole, CardType, EffectParser } from './cards.types'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { CardFaction, CardRole, CardType } from './cards.types'; +import { Types } from 'mongoose'; +import { Effect } from './effect.schema'; @Schema() -export class Card { +export class Card extends Types.ObjectId { + @Prop({ type: Number }) cardId: number; + + @Prop({ type: String, enum: CardRole }) role: CardRole; + + @Prop() + playerIndex?: number; + + @Prop({ type: String, enum: CardType }) cardType: CardType; + + @Prop({ type: String, enum: CardFaction }) faction: CardFaction; + + @Prop({ type: String }) name: string; - initAmount: number; + + @Prop() cost?: number; + + @Prop() defense?: number; + + @Prop() guard?: boolean; - effects: EffectParser[]; + + @Prop({ type: [Effect], default: [], autopopulate: true }) + effects: Effect[]; + + @Prop() + pack: string; } export const CardSchema = SchemaFactory.createForClass(Card); diff --git a/src/cards/schemas/cards.types.ts b/src/cards/schemas/cards.types.ts index 78f27e0..fb2ca9c 100644 --- a/src/cards/schemas/cards.types.ts +++ b/src/cards/schemas/cards.types.ts @@ -5,6 +5,21 @@ export enum CardRole { PERSONAL = 'personal', MARKET = 'market', FIRE_GEM = 'fire_gem', + + HUNTER = 'hunter', + TRAVELER = 'traveler', + CLERIC = 'cleric', + FIGHTER = 'fighter', + RANGER = 'ranger', + THIEF = 'thief', + WIZARD = 'wizard', + + HALF_DEMON = 'half-demon', + DWARF = 'dwarf', + ELF = 'elf', + OGRE = 'ogre', + ORC = 'orc', + SMALLFOLK = 'smallfolk', } /** @@ -33,6 +48,7 @@ export enum CardFaction { } export enum CardEffects { + //Base GOLD = 'gold', DAMAGE = 'damage', HEAL = 'heal', @@ -47,14 +63,45 @@ export enum CardEffects { STACK_NEXT_ACTION_BOUGHT = 'stack_next_action_bought', STACK_NEXT_CARD_BOUGHT = 'stack_next_card_bought', PLAY_NEXT_CARD_BOUGHT = 'play_next_card_bought', + + //DLCs + BUY_FOR_FREE = 'buy_for_free', + + //Journeys + DAMAGE_ALL_CHAMPIONS = 'damage_all_champions', + + //Journeys: Hunters + DISCARD_X_AND_DRAW_X = 'discard_x_and_draw_x', + PREPARE_ANOTHER_CHAMPION = 'prepare_another_champion', + //Journey: Travelers + PREPARE_ALL_CHAMPIONS = 'prepare_all_champions', + CHEAPER_CHAMPION = 'cheaper_champion', + CHEAPER_CHAMPIONS_PASSIVE = 'cheaper_champions_passive', + CHEAPER_ACTION = 'cheaper_action', + CONTROL_OPPOSING_CHAMPION_PASSIVE = 'control_opposing_champion_passive', + + RESTACK_DISCARDED_ACTION = 'restack_discarded_action', + + //Ancestry + KEEP_IN_HAND = 'keep_in_hand', + BUY_GEM_FOR_FREE = 'buy_gem_for_free', + CHEAPER_SKILLS_PASSIVE = 'cheaper_skills_passive', + CHEAPER_CARD_IF_HIGHER_PRICE = 'cheaper_card_if_higher_price', + PICK_FACTION = 'pick_faction', } export enum Per { + //Base CHAMPION = 'champion', OTHER_CHAMPION = 'other_champion', OTHER_GUARD = 'other_guard', CARD_OF_SAME_FACTION = 'card_of_same_faction', OTHER_CARD_OF_SAME_FACTION = 'other_card_of_same_faction', + + //Journey: Travelers + STUNNED_CHAMPION = 'stunned_champion', + CHAMPION_OF_SAME_FACTION = 'champion_of_same_faction', + OTHER_KNIFE_PLAYED = 'other_knife_played', } export enum EffectType { @@ -85,6 +132,7 @@ export type EffectParser = { effect_type?: EffectType | EffectTypeVerbose; per?: Per; mutex?: number; + faction?: CardFaction; }; export type CardParser = { diff --git a/src/cards/schemas/effect.schema.ts b/src/cards/schemas/effect.schema.ts new file mode 100644 index 0000000..a29e1bc --- /dev/null +++ b/src/cards/schemas/effect.schema.ts @@ -0,0 +1,91 @@ +import mongoose, { Types } from 'mongoose'; +import { + CardEffects, + CardFaction, + EffectType, + EffectTypeSpecial, + Per, +} from './cards.types'; +import { Card } from './cards.schema'; +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; + +// @Schema() +export class EffectCondition { + @Prop({ type: String, enum: { ...EffectType, ...EffectTypeSpecial } }) + effectId: EffectType | EffectTypeSpecial; + + @Prop({ type: Number }) + amount: number; + + @Prop({ type: Number }) + cardId?: number; + + @Prop({ type: String, enum: CardFaction }) + faction?: CardFaction; +} + +// export const EffectConditionSchema = +// SchemaFactory.createForClass(EffectCondition); + +@Schema() +export class Effect extends Types.ObjectId { + @Prop({ type: String, enum: CardEffects }) + effect: CardEffects; + + @Prop({ type: Number, default: 1 }) + amount!: number; + + @Prop({ type: Number, default: 1 }) + times!: number; + + // @Prop({ type: [Effect], default: [], autopopulate: true }) + subEffects: Effect[]; + + @Prop({ type: EffectCondition, autopopulate: true }) + condition?: EffectCondition; + + @Prop({ type: String, enum: Per }) + per?: Per; + + // @Prop({ + // type: [ + // { + // type: mongoose.Schema.Types.ObjectId, + // ref: 'Effect', + // autopopulate: true, + // }, + // ], + // }) + mutex?: Effect[]; + + // @Prop({ + // type: mongoose.Schema.Types.ObjectId, + // ref: 'Card', + // autopopulate: true, + // }) + card: Card; + + @Prop({ type: String, enum: CardFaction }) + faction?: CardFaction; +} + +export const EffectSchema = SchemaFactory.createForClass(Effect); +EffectSchema.add({ + card: { + type: mongoose.Schema.Types.ObjectId, + ref: 'Card', + autopopulate: true, + }, +}); + +EffectSchema.add({ + mutex: { + type: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Effect', + autopopulate: true, + }, + ], + }, +}); diff --git a/src/games/games.controller.spec.ts b/src/games/controllers/games.controller.spec.ts similarity index 89% rename from src/games/games.controller.spec.ts rename to src/games/controllers/games.controller.spec.ts index d2eb47d..17b4de4 100644 --- a/src/games/games.controller.spec.ts +++ b/src/games/controllers/games.controller.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GamesController } from './games.controller'; -import { GamesService } from './games.service'; +import { GamesService } from '../services/games.lobby.service'; describe('GamesController', () => { let controller: GamesController; diff --git a/src/games/controllers/games.controller.ts b/src/games/controllers/games.controller.ts new file mode 100644 index 0000000..a66a305 --- /dev/null +++ b/src/games/controllers/games.controller.ts @@ -0,0 +1,141 @@ +import { + Controller, + Get, + Post, + Param, + Delete, + UseGuards, + Session, + HttpException, + HttpStatus, + Patch, + Body, +} from '@nestjs/common'; +import { GamesService } 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 { GameGuard } from '../guards/game.guard'; +import { UpdateGameDto } from '../dto/update-game.dto'; + +@ApiTags('lobby') +@Controller('games') +export class GamesController { + constructor(private readonly gamesService: GamesService) {} + + @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) { + 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, + @Param('id') id: string, + @Body() updateGameDto: UpdateGameDto, + ) { + 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, + @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, + @Param('id') id: string, + ) { + const game = await this.gamesService.findById(id); + 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(id); + return; + } +} diff --git a/src/games/dto/read-games.dto.ts b/src/games/dto/read-games.dto.ts index 1b446df..c25dc24 100644 --- a/src/games/dto/read-games.dto.ts +++ b/src/games/dto/read-games.dto.ts @@ -1,18 +1,57 @@ import { ApiProperty } from '@nestjs/swagger'; import { DatabaseObjectDto } from './database-object.dto'; -import { Container, Team } from '../schemas/game.schema'; -import { User } from 'src/users/schemas/user.entity'; +import { Game, Player, Team } from '../schemas/game.schema'; +import { Card } from 'src/cards/schemas/cards.schema'; + +export class ReadPlayerDto extends DatabaseObjectDto { + constructor( + data: Player & ConstructorParameters[0], + ) { + super(data); + this.user = new ReadUserDto(data.user); + this.board = new ReadContainerDto(data.board); + this.discard = new ReadContainerDto(data.discard); + this.hand = new ReadContainerDto(data.hand, true); + this.stack = new ReadContainerDto(data.stack, true); + } + user: ReadUserDto; + + board: ReadContainerDto; + + hand: ReadContainerDto; + + stack: ReadContainerDto; + + discard: ReadContainerDto; +} export class ReadTeamDto extends DatabaseObjectDto { - constructor(data: ConstructorParameters[0]) { + constructor(data: Team & ConstructorParameters[0]) { super(data); + this.health = data.health; + this.players = data.players.map((e) => { + return new ReadPlayerDto(e); + }); } + health: number; + players: ReadPlayerDto[]; } export class ReadContainerDto extends DatabaseObjectDto { - constructor(data: ConstructorParameters[0]) { + constructor( + data: { cards: Card[] } & ConstructorParameters< + typeof DatabaseObjectDto + >[0], + hidden = false, + ) { super(data); + if (hidden) { + this.cards = data.cards.map(() => null); + } else { + this.cards = data.cards.map((e) => e); + } } + cards: Card[]; } export class ReadUserDto extends DatabaseObjectDto { @@ -34,29 +73,20 @@ export class ReadUserDto extends DatabaseObjectDto { } export class ReadGameDto extends DatabaseObjectDto { - constructor( - data: { - teams: Team[]; - market: Container; - marketStack: Container; - fireGems: Container; - currentTurn?: Team; - started: boolean; - ended: boolean; - owner: User; - } & ConstructorParameters[0], - ) { + constructor(data: Game & ConstructorParameters[0]) { super(data); - this.teams = data.teams.map((e) => new ReadTeamDto(e)); - this.market = new ReadContainerDto(data.market); - this.marketStack = new ReadContainerDto(data.marketStack); - this.fireGems = new ReadContainerDto(data.fireGems); - this.started = data.started; - this.ended = data.ended; - this.owner = new ReadUserDto(data.owner); + const obj = data; + this.teams = obj.teams.map((e) => new ReadTeamDto(e)); + this.market = new ReadContainerDto(obj.market); + this.marketStack = new ReadContainerDto(obj.marketStack, true); + this.fireGems = new ReadContainerDto(obj.fireGems); + this.started = obj.started; + this.ended = obj.ended; + this.owner = new ReadUserDto(obj.owner); + this.packs = obj.packs; - if (data.currentTurn) { - this.currentTurn = new ReadTeamDto(data.currentTurn); + if (obj.currentTurn) { + this.currentTurn = new ReadTeamDto(obj.currentTurn); } } @@ -83,4 +113,7 @@ export class ReadGameDto extends DatabaseObjectDto { @ApiProperty() owner: ReadUserDto; + + @ApiProperty() + packs: string[]; } diff --git a/src/games/dto/update-game.dto.ts b/src/games/dto/update-game.dto.ts index 99f0499..bb84caa 100644 --- a/src/games/dto/update-game.dto.ts +++ b/src/games/dto/update-game.dto.ts @@ -1 +1,3 @@ -export class UpdateGameDto {} +export class UpdateGameDto { + packs: string[]; +} diff --git a/src/games/games.controller.ts b/src/games/games.controller.ts deleted file mode 100644 index 0d4a552..0000000 --- a/src/games/games.controller.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - Controller, - Get, - Post, - Param, - Delete, - UseGuards, - Session, - HttpException, - HttpStatus, -} from '@nestjs/common'; -import { GamesService } from './games.service'; -import { AuthGuard } from './guards/auth.guard'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { DatabaseObjectDto } from './dto/database-object.dto'; - -@Controller('games') -export class GamesController { - constructor(private readonly gamesService: GamesService) {} - - @Post() - @ApiOperation({ summary: 'Create game' }) - @ApiResponse({ - status: 201, - description: 'Game successfully created.', - type: DatabaseObjectDto, - }) - @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) { - const games = await this.gamesService.findByOwner(session.user._id); - if (games.length > 0) { - 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') - findOne(@Param('id') id: string) { - return this.gamesService.findById(id); - } - - // @Patch(':id') - // update(@Param('id') id: string, @Body() updateGameDto: UpdateGameDto) { - // return this.gamesService.update(+id, updateGameDto); - // } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.gamesService.remove(+id); - } -} diff --git a/src/games/games.module.ts b/src/games/games.module.ts index 7eb3d7d..df126c7 100644 --- a/src/games/games.module.ts +++ b/src/games/games.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { GamesService } from './games.service'; -import { GamesController } from './games.controller'; +import { GamesService } from './services/games.lobby.service'; +import { GamesController } from './controllers/games.controller'; import { Game, GameSchema } from './schemas/game.schema'; import { MongooseModule } from '@nestjs/mongoose'; import { UsersModule } from 'src/users/users.module'; diff --git a/src/games/games.service.ts b/src/games/games.service.ts deleted file mode 100644 index 8ae49bd..0000000 --- a/src/games/games.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DatabaseObjectDto } from './dto/database-object.dto'; -import { InjectModel } from '@nestjs/mongoose'; -import { Game } from './schemas/game.schema'; -import { Model } from 'mongoose'; -import { ReadGameDto } from './dto/read-games.dto'; -import { User } from 'src/users/schemas/user.entity'; - -@Injectable() -export class GamesService { - constructor(@InjectModel(Game.name) private gameModel: Model) {} - - async create(owner: User): Promise { - const createdGame = new this.gameModel(); - createdGame.owner = owner; - const gamedata = await createdGame.save(); - return new DatabaseObjectDto(gamedata); - } - - async findAll(): Promise { - const games = await this.gameModel.find().exec(); - return games.map((e) => new ReadGameDto(e)); - } - - findById(id: string) { - return this.gameModel.findById(id).exec(); - } - - findByOwner(_id: string) { - return this.gameModel.find({ owner: _id }).exec(); - } - - // update(id: number, updateGameDto: UpdateGameDto) { - // return `This action updates a #${id} game`; - // } - - remove(id: number) { - return `This action removes a #${id} game`; - } -} diff --git a/src/games/guards/game.guard.ts b/src/games/guards/game.guard.ts new file mode 100644 index 0000000..517ccb9 --- /dev/null +++ b/src/games/guards/game.guard.ts @@ -0,0 +1,32 @@ +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 | Observable { + const http = context.switchToHttp(); + const request = http.getRequest(); + 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).user) { + throw new HttpException('Please log in', HttpStatus.UNAUTHORIZED); + } + return true; + })(); + } +} diff --git a/src/games/schemas/game.schema.ts b/src/games/schemas/game.schema.ts index 3e5f637..3bbc9d8 100644 --- a/src/games/schemas/game.schema.ts +++ b/src/games/schemas/game.schema.ts @@ -5,34 +5,71 @@ import { User } from 'src/users/schemas/user.entity'; export type GameDocument = HydratedDocument; -@Schema() -export class Team extends Types.ObjectId {} - -export const TeamSchema = SchemaFactory.createForClass(Team); - @Schema() export class Container extends Types.ObjectId { - @Prop([{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }]) + @Prop({ + type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Card' }], + autopopulate: true, + }) cards: Card[]; } export const ContainerSchema = SchemaFactory.createForClass(Container); +@Schema() +export class Player extends Types.ObjectId { + @Prop({ + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + autopopulate: true, + }) + user: User; + + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) + board!: Container; + + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) + hand!: Container; + + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) + stack!: Container; + + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) + discard!: Container; +} + +export const PlayerSchema = SchemaFactory.createForClass(Player); + +@Schema() +export class Team extends Types.ObjectId { + @Prop({ type: Number, default: 50 }) + health: number; + + @Prop({ type: [{ type: Player }] }) + players: Player[]; +} + +export const TeamSchema = SchemaFactory.createForClass(Team); + @Schema() export class Game extends Types.ObjectId { - @Prop([Team]) + @Prop({ type: [{ type: Team, autopopulate: true }] }) teams: Team[]; - @Prop({ type: ContainerSchema, default: {} }) + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) market: Container; - @Prop({ type: ContainerSchema, default: {} }) + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) marketStack: Container; - @Prop({ type: ContainerSchema, default: {} }) + @Prop({ type: ContainerSchema, default: {}, autopopulate: true }) fireGems: Container; - @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'Game.teams' }) + @Prop({ + type: mongoose.Schema.Types.ObjectId, + ref: 'Team', + autopopulate: true, + }) currentTurn?: Team; @Prop({ default: false }) @@ -41,8 +78,15 @@ export class Game extends Types.ObjectId { @Prop({ default: false }) ended: boolean; - @Prop({ type: mongoose.Schema.Types.ObjectId, ref: 'User' }) + @Prop({ + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + autopopulate: true, + }) owner: User; + + @Prop({ type: [String], default: [] }) + packs: string[]; } export const GameSchema = SchemaFactory.createForClass(Game); diff --git a/src/games/games.service.spec.ts b/src/games/services/games.lobby.service.spec.ts similarity index 88% rename from src/games/games.service.spec.ts rename to src/games/services/games.lobby.service.spec.ts index c65ca09..b2b405f 100644 --- a/src/games/games.service.spec.ts +++ b/src/games/services/games.lobby.service.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { GamesService } from './games.service'; +import { GamesService } from './games.lobby.service'; describe('GamesService', () => { let service: GamesService; diff --git a/src/games/services/games.lobby.service.ts b/src/games/services/games.lobby.service.ts new file mode 100644 index 0000000..0ed6ad2 --- /dev/null +++ b/src/games/services/games.lobby.service.ts @@ -0,0 +1,110 @@ +import { Injectable } from '@nestjs/common'; +import { DatabaseObjectDto } from '../dto/database-object.dto'; +import { InjectModel } from '@nestjs/mongoose'; +import { Game, Player, Team } from '../schemas/game.schema'; +import { Model } from 'mongoose'; +import { ReadGameDto } from '../dto/read-games.dto'; +import { User } from 'src/users/schemas/user.entity'; +import { Card } from 'src/cards/schemas/cards.schema'; +import { CardRole } from 'src/cards/schemas/cards.types'; +import { UpdateGameDto } from '../dto/update-game.dto'; + +@Injectable() +export class GamesService { + constructor( + @InjectModel(Game.name) private gameModel: Model, + @InjectModel(Card.name) private cardModel: Model, + ) {} + + async create(owner: User): Promise { + const createdGame = new this.gameModel(); + createdGame.owner = owner; + createdGame.packs = [ + 'pack1_base_deck', + 'pack1_base_necros', + 'pack1_base_wild', + 'pack1_base_imperial', + 'pack1_base_guild', + ]; + createdGame.teams.push({ + players: [{ user: owner } as Player], + } as Team); + + const gamedata = await createdGame.save(); + return new ReadGameDto(gamedata); + } + + async findAll(): Promise { + const games = await this.gameModel.find().exec(); + return await Promise.all( + games.map(async (e) => { + return await new ReadGameDto(e); + }), + ); + } + + 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 { + const game = await this.gameModel.findOne({ owner: _id }).exec(); + if (!game) { + return null; + } + return new ReadGameDto(game); + } + + async update(id: string, updateGameDto: UpdateGameDto) { + const game = await this.gameModel.findById(id).exec(); + if (!game) { + return null; + } + game.packs = updateGameDto.packs; + return; + } + + async remove(id: string) { + return await this.gameModel.findByIdAndDelete(id); + } + + async start(id: string) { + const game = await this.gameModel.findById(id).exec(); + if (!game) { + return null; + } + game.fireGems.cards = await this.cardModel.find({ + pack: { $in: game.packs }, + role: CardRole.FIRE_GEM, + }); + + game.marketStack.cards = await this.cardModel.find({ + pack: { $in: game.packs }, + role: CardRole.MARKET, + }); + + 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: CardRole.PERSONAL, + playerIndex, + }) + .exec(); + playerIndex++; + }), + ), + ), + ); + game.started = true; + await game.save(); + } +}