WIP : before-cqrs

This commit is contained in:
2024-06-11 14:57:02 -04:00
parent 5777d06fc8
commit 0d4c0e008c
22 changed files with 750 additions and 168 deletions
+16 -4
View File
@@ -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": [
"<node_internals>/**"
],
"type": "node",
"console": "internalConsole",
"outputCapture": "std",
"internalConsoleOptions": "openOnSessionStart"
}
]
}
+10
View File
@@ -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",
+1
View File
@@ -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"
},
+8 -4
View File
@@ -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;
}
}
+7
View File
@@ -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,
],
+4 -2
View File
@@ -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();
}
}
+14 -2
View File
@@ -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;
+118 -3
View File
@@ -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<Card>) {}
async loadCards(): Promise<void> {
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<Effect[]> {
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<EffectCondition> {
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;
}
}
+29 -5
View File
@@ -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);
+48
View File
@@ -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 = {
+91
View File
@@ -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,
},
],
},
});
@@ -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;
+141
View File
@@ -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<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;
}
@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,
@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<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) {
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;
}
}
+58 -25
View File
@@ -1,19 +1,58 @@
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<typeof DatabaseObjectDto>[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<typeof DatabaseObjectDto>[0]) {
constructor(data: Team & ConstructorParameters<typeof DatabaseObjectDto>[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<typeof DatabaseObjectDto>[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 {
constructor(
@@ -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<typeof DatabaseObjectDto>[0],
) {
constructor(data: Game & ConstructorParameters<typeof DatabaseObjectDto>[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[];
}
+3 -1
View File
@@ -1 +1,3 @@
export class UpdateGameDto {}
export class UpdateGameDto {
packs: string[];
}
-66
View File
@@ -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<string, any>) {
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);
}
}
+2 -2
View File
@@ -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';
-40
View File
@@ -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<Game>) {}
async create(owner: User): Promise<DatabaseObjectDto> {
const createdGame = new this.gameModel();
createdGame.owner = owner;
const gamedata = await createdGame.save();
return new DatabaseObjectDto(gamedata);
}
async findAll(): Promise<ReadGameDto[]> {
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`;
}
}
+32
View File
@@ -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<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;
})();
}
}
+56 -12
View File
@@ -5,34 +5,71 @@ import { User } from 'src/users/schemas/user.entity';
export type GameDocument = HydratedDocument<Game>;
@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);
@@ -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;
+110
View File
@@ -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<Game>,
@InjectModel(Card.name) private cardModel: Model<Card>,
) {}
async create(owner: User): Promise<DatabaseObjectDto> {
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<ReadGameDto[]> {
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<ReadGameDto> {
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();
}
}