From bafe228911fb2185c28d46a2875ed6a495b2b52c Mon Sep 17 00:00:00 2001 From: legonzaur Date: Mon, 30 Dec 2024 18:16:25 +0100 Subject: [PATCH] feat: working on messages --- events | 2 +- package-lock.json | 17 ++++---- package.json | 2 +- src/app.module.ts | 12 +++++- src/games/controllers/games.controller.ts | 48 +++++++++++++++++++++-- src/games/controllers/lobby.controller.ts | 4 +- src/games/dto/messageRecieve.dto.ts | 7 ++++ src/games/entities/game.entity.ts | 36 ++++++++++++++--- src/games/entities/message.entity.ts | 32 +++++++++++++++ src/games/entities/player.entity.ts | 9 ++++- src/games/games.module.ts | 5 ++- src/games/services/games.service.ts | 45 ++++++++++++++++++++- src/games/services/lobby.service.ts | 6 +-- src/word/entities/topic.entity.ts | 2 +- src/word/entities/word.entity.ts | 2 +- src/word/services/word.service.ts | 2 +- src/word/words.module.ts | 1 + types.d.ts/express-session.d.ts | 2 +- 18 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 src/games/dto/messageRecieve.dto.ts create mode 100644 src/games/entities/message.entity.ts diff --git a/events b/events index cdddb9a..22656ba 160000 --- a/events +++ b/events @@ -1 +1 @@ -Subproject commit cdddb9a073ecc42938136427a4c378945b7742db +Subproject commit 22656ba60812db5471b61acc0e657d2f81ae0645 diff --git a/package-lock.json b/package-lock.json index b2be149..56d38eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "connect-redis": "^8.0.1", "express-session": "^1.18.0", "pg": "^8.13.1", - "ranjs": "^1.24.5", + "random": "^5.1.1", "redis": "^4.7.0", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1", @@ -10074,6 +10074,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/random": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/random/-/random-5.1.1.tgz", + "integrity": "sha512-iidvORUvXY1ItoYxO0eduHCKl22QV0G6460vRHe862dUagJKPhRyjUGwK8ioOCG4NRuFvExHFpqMngsnr2miwA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/random-bytes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", @@ -10101,12 +10110,6 @@ "node": ">= 0.6" } }, - "node_modules/ranjs": { - "version": "1.24.5", - "resolved": "https://registry.npmjs.org/ranjs/-/ranjs-1.24.5.tgz", - "integrity": "sha512-ZW4Bgc5jPRDYqBWuTcwS3GgSMemG5t+eltzS1aGIM7AVBx3JJymqqWN+TiKb5t0gfK//lsJUYPDkNsZ+oLIXIQ==", - "license": "MIT" - }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", diff --git a/package.json b/package.json index 7f41146..9758ab3 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "connect-redis": "^8.0.1", "express-session": "^1.18.0", "pg": "^8.13.1", - "ranjs": "^1.24.5", + "random": "^5.1.1", "redis": "^4.7.0", "reflect-metadata": "^0.2.1", "rxjs": "^7.8.1", diff --git a/src/app.module.ts b/src/app.module.ts index 0832907..03bbd25 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { Concept } from './games/entities/concept.entity'; import { ConceptInTurn } from './games/entities/concept_in_turn.entity'; import { ConfigService } from '@nestjs/config'; import { WordModule } from './word/words.module'; +import { Message } from './games/entities/message.entity'; const configService = new ConfigService(); @@ -31,7 +32,16 @@ const configService = new ConfigService(); username: configService.get('PG_USER'), database: 'concept', synchronize: configService.get('DEV_MODE') == 'true', - entities: [User, Player, Game, Concept, ConceptInTurn, Topic, Word], + entities: [ + User, + Player, + Game, + Message, + Concept, + ConceptInTurn, + Topic, + Word, + ], }), GamesModule, UsersModule, diff --git a/src/games/controllers/games.controller.ts b/src/games/controllers/games.controller.ts index 638deec..0cfb893 100644 --- a/src/games/controllers/games.controller.ts +++ b/src/games/controllers/games.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Get, HttpException, HttpStatus, Param, @@ -11,7 +12,7 @@ import { } from '@nestjs/common'; import { LobbyService } from '../services/lobby.service'; import { AuthGuard } from '../../users/guards/auth.guard'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Game } from '../entities/game.entity'; import { GamePipe, GameStartedPipe } from '../pipe/game.pipe'; @@ -20,6 +21,8 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { GameService } from '../services/games.service'; import { ConfigService } from '@nestjs/config'; import { SessionData } from 'express-session'; +import { WordResponseDTO } from 'events/dto/wordresponse.dto'; +import { WordRecieveDTO } from 'src/word/dto/wordrecieve.dto'; @ApiTags('game') @Controller('games/:id') @@ -60,7 +63,7 @@ export class GamesController { HttpStatus.BAD_REQUEST, ); } - if (game.owner.id != session.user.id) { + if (game.ownerId != session.user?.id) { throw new HttpException( 'You can only start games you own', HttpStatus.FORBIDDEN, @@ -88,12 +91,12 @@ export class GamesController { @ApiOperation({ summary: 'Kick a player' }) @UseGuards(AuthGuard) kick( - @Param('id') id: string, @Session() session: SessionData, + @Param('id') id: string, @Param('id', GamePipe) game: Game, @Param('playerId') playerId: string, ) { - if (session.user.id != game.owner.id) { + if (session.user.id != game.ownerId) { throw new HttpException( 'You do do not own this game', HttpStatus.FORBIDDEN, @@ -102,6 +105,43 @@ export class GamesController { return this.lobbyService.kickPlayer(game, session.user, playerId); } + @Get('currentWord') + @ApiOperation({ summary: 'Read Current Word' }) + @UseGuards(AuthGuard) + getCurrentWord( + @Session() session: SessionData, + @Param('id') id: string, + @Param('id', GamePipe) game: Game, + ) { + if (session.user.id != game.currentPlayer.userId) { + throw new HttpException( + 'It is not your turn to play', + HttpStatus.FORBIDDEN, + ); + } + return new WordResponseDTO(game.currentWord); + } + + @Post('/message') + @ApiOperation({ summary: 'try to guess a word' }) + @ApiBody({ type: WordRecieveDTO }) + @UseGuards(AuthGuard) + async postMessage( + @Session() session: SessionData, + @Param('id') id: string, + @Param('id', GamePipe) game: Game, + @Body('value') value: string, + ) { + const player = game.players.find((p) => p.userId == session.user.id); + if (!game.started) { + throw new HttpException('Game not started', HttpStatus.BAD_REQUEST); + } + if (!player) { + throw new HttpException('You are not in this game', HttpStatus.FORBIDDEN); + } + await this.gameService.postMessage(game, player, value); + } + @Sse('/subscribe') subscribe(@Param('id') id: string): Observable<{ data: string }> { return fromEvent(this.eventEmitter, 'sse.game.' + id).pipe( diff --git a/src/games/controllers/lobby.controller.ts b/src/games/controllers/lobby.controller.ts index b1a4c24..a568d9e 100644 --- a/src/games/controllers/lobby.controller.ts +++ b/src/games/controllers/lobby.controller.ts @@ -89,7 +89,7 @@ export class LobbyController { @Param('id', GamePipe) game: Game, @GetUser() user: User, ) { - if (game.owner.id != user.id) { + if (game.ownerId != user.id) { throw new HttpException( 'You can only delete games you own', HttpStatus.FORBIDDEN, @@ -110,7 +110,7 @@ export class LobbyController { throw new HttpException('Game already started', HttpStatus.BAD_REQUEST); } - if (game.players.some((p) => p.user.id == user.id)) { + if (game.players.some((p) => p.userId == user.id)) { throw new HttpException( 'You are already in this game', HttpStatus.BAD_REQUEST, diff --git a/src/games/dto/messageRecieve.dto.ts b/src/games/dto/messageRecieve.dto.ts new file mode 100644 index 0000000..d304de6 --- /dev/null +++ b/src/games/dto/messageRecieve.dto.ts @@ -0,0 +1,7 @@ +export class MessageRecieveDTO { + constructor(partial: Partial) { + this.value = partial.value; + } + + value: string; +} diff --git a/src/games/entities/game.entity.ts b/src/games/entities/game.entity.ts index f4f8684..1477fdc 100644 --- a/src/games/entities/game.entity.ts +++ b/src/games/entities/game.entity.ts @@ -1,6 +1,7 @@ import { Column, Entity, + JoinColumn, JoinTable, ManyToOne, OneToMany, @@ -10,6 +11,8 @@ import { import { Player } from './player.entity'; import { ConceptInTurn } from './concept_in_turn.entity'; import { User } from 'src/users/entities/user.entity'; +import { Word } from 'src/word/entities/word.entity'; +import { Message } from './message.entity'; @Entity() export class Game { @@ -19,8 +22,12 @@ export class Game { @Column() seed: string; - @ManyToOne(() => User, { eager: true }) - owner: User; + @JoinColumn({ name: 'ownerId' }) + @ManyToOne(() => User) + owner: Promise; + + @Column({ name: 'ownerId' }) + ownerId: string; @OneToMany(() => Player, (player) => player.game, { eager: true, @@ -28,15 +35,32 @@ export class Game { @JoinTable() players: Player[]; - @OneToOne(() => Player) + @OneToOne(() => Player, { nullable: true, eager: true }) + @JoinColumn() currentPlayer: Player; - @Column({ nullable: true }) - currentWord: string; + @ManyToOne(() => Word, { nullable: true, eager: true }) + currentWord: Word; @Column({ default: false }) started: boolean; - @OneToMany(() => ConceptInTurn, (cit) => cit.game, { onDelete: 'CASCADE' }) + @Column({ default: 0 }) + turn: number = 0; + + @OneToMany(() => ConceptInTurn, (cit) => cit.game, { + onDelete: 'CASCADE', + eager: true, + }) currentConcepts: ConceptInTurn[]; + + @OneToMany(() => Message, (msg) => msg.game, { + onDelete: 'CASCADE', + eager: true, + }) + _messages: Message[]; + + get messages() { + return this._messages.sort((a, b) => a.id - b.id); + } } diff --git a/src/games/entities/message.entity.ts b/src/games/entities/message.entity.ts new file mode 100644 index 0000000..99ba85b --- /dev/null +++ b/src/games/entities/message.entity.ts @@ -0,0 +1,32 @@ +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, + Unique, +} from 'typeorm'; +import { Player } from './player.entity'; + +import { Game } from './game.entity'; + +@Entity() +export class Message { + @PrimaryGeneratedColumn() + id: number; + + @Column() + value: string; + + @JoinColumn({ name: 'authorId' }) + @ManyToOne(() => Player, { eager: true }) + author: Player; + + get authorId() { + return this.author.userId; + } + + @ManyToOne(() => Game) + game: Promise; +} diff --git a/src/games/entities/player.entity.ts b/src/games/entities/player.entity.ts index 939456f..daa6180 100644 --- a/src/games/entities/player.entity.ts +++ b/src/games/entities/player.entity.ts @@ -2,6 +2,7 @@ import { User } from '../../users/entities/user.entity'; import { Column, Entity, + JoinColumn, JoinTable, ManyToMany, ManyToOne, @@ -16,8 +17,12 @@ export class Player { @PrimaryGeneratedColumn() id: number; - @ManyToOne(() => User, { eager: true, onDelete: 'CASCADE' }) - user: User; + @JoinColumn({ name: 'userId' }) + @ManyToOne(() => User, { onDelete: 'CASCADE' }) + user: Promise; + + @Column({ name: 'userId' }) + userId: string; @ManyToOne(() => Game, { onDelete: 'CASCADE' }) game: Game; diff --git a/src/games/games.module.ts b/src/games/games.module.ts index 2f94a47..7c6537d 100644 --- a/src/games/games.module.ts +++ b/src/games/games.module.ts @@ -8,13 +8,16 @@ import { GameService } from './services/games.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Player } from './entities/player.entity'; import { User } from 'src/users/entities/user.entity'; +import { WordModule } from 'src/word/words.module'; +import { Message } from './entities/message.entity'; @Module({ imports: [ UsersModule, + WordModule, TypeOrmModule.forFeature([Game]), TypeOrmModule.forFeature([Player]), - TypeOrmModule.forFeature([User]), + TypeOrmModule.forFeature([Message]), ], controllers: [LobbyController, GamesController], providers: [LobbyService, GameService], diff --git a/src/games/services/games.service.ts b/src/games/services/games.service.ts index d416d61..4647666 100644 --- a/src/games/services/games.service.ts +++ b/src/games/services/games.service.ts @@ -12,15 +12,58 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { Player } from '../entities/player.entity'; import { User } from 'src/users/entities/user.entity'; +import { WordService } from 'src/word/services/word.service'; +import { + GameMessageDeleteEvent, + GameMessageEvent, + GameStartEvent, +} from 'events/game.events'; +import random from 'random'; +import { platform } from 'os'; +import { Message } from '../entities/message.entity'; + @Injectable() export class GameService { constructor( @InjectRepository(Game) private gameRepository: Repository, + @InjectRepository(Message) private messageResporitory: Repository, + private readonly wordService: WordService, private eventEmitter: EventEmitter2, ) {} async start(game: Game) { - throw Error('Not Implemented'); + const rng = random.clone(game.seed); + game.started = true; + game.currentPlayer = rng.choice(game.players); + const words = await this.wordService.listActive(); + game.currentWord = rng.choice(words); + + await this.gameRepository.save(game); + this.eventEmitter.emit( + 'sse.game.' + game.id, + new GameStartEvent(game.currentPlayer), + ); + } + + async postMessage(game: Game, author: Player, value: string) { + const message = this.messageResporitory.create(); + message.author = author; + message.game = Promise.resolve(game); + message.value = value; + await this.messageResporitory.save(message); + this.eventEmitter.emit( + 'sse.game.' + game.id, + new GameMessageEvent(message), + ); + const oldMessage = game.messages[0]; + const oldMessageId = oldMessage.id; + if (game.messages.length > 3) { + await this.messageResporitory.remove(oldMessage); + } + this.eventEmitter.emit( + 'sse.game.' + game.id, + new GameMessageDeleteEvent(oldMessageId), + ); } async end(game: Game, winner: Player) { diff --git a/src/games/services/lobby.service.ts b/src/games/services/lobby.service.ts index 36e83d3..7e4c8df 100644 --- a/src/games/services/lobby.service.ts +++ b/src/games/services/lobby.service.ts @@ -26,7 +26,7 @@ export class LobbyService { async create(owner: User) { const createdGame = this.gameRepository.create(); - createdGame.owner = owner; + createdGame.owner = Promise.resolve(owner); createdGame.seed = randomUUID(); await this.gameRepository.save(createdGame); @@ -40,11 +40,11 @@ export class LobbyService { async joinGame(game: Game, user: User) { const player = this.playerRepository.create(); - player.user = user; + player.user = Promise.resolve(user); player.game = game; + await this.playerRepository.save(player); this.eventEmitter.emit('sse.lobby', new LobbyJoinEvent(player, game)); this.eventEmitter.emit('sse.game.' + game.id, new GameJoinEvent(player)); - await this.playerRepository.save(player); } findByOwner(user: User) { diff --git a/src/word/entities/topic.entity.ts b/src/word/entities/topic.entity.ts index 34f203c..625b67f 100644 --- a/src/word/entities/topic.entity.ts +++ b/src/word/entities/topic.entity.ts @@ -19,5 +19,5 @@ export class Topic { name: string; @ManyToMany((type) => Word, (word) => word.topics) - words: Word[]; + words: Promise; } diff --git a/src/word/entities/word.entity.ts b/src/word/entities/word.entity.ts index 918c41f..fc47efb 100644 --- a/src/word/entities/word.entity.ts +++ b/src/word/entities/word.entity.ts @@ -20,7 +20,7 @@ export class Word { @JoinColumn({ name: 'ownerId' }) @ManyToOne(() => User) - owner: User; + owner: Promise; @Column({ name: 'ownerId' }) ownerId: string; diff --git a/src/word/services/word.service.ts b/src/word/services/word.service.ts index d5c57e6..bc822fe 100644 --- a/src/word/services/word.service.ts +++ b/src/word/services/word.service.ts @@ -37,7 +37,7 @@ export class WordService { async create(value: string, owner: User) { const word = this.wordRepository.create(); - word.owner = owner; + word.owner = Promise.resolve(owner); word.value = value; try { await this.wordRepository.save(word); diff --git a/src/word/words.module.ts b/src/word/words.module.ts index 08a69d7..2deb75d 100644 --- a/src/word/words.module.ts +++ b/src/word/words.module.ts @@ -10,5 +10,6 @@ import { UsersModule } from 'src/users/users.module'; imports: [UsersModule, TypeOrmModule.forFeature([Word])], controllers: [WordController], providers: [WordService], + exports: [WordService], }) export class WordModule {} diff --git a/types.d.ts/express-session.d.ts b/types.d.ts/express-session.d.ts index d98b218..b83c764 100644 --- a/types.d.ts/express-session.d.ts +++ b/types.d.ts/express-session.d.ts @@ -1,4 +1,4 @@ -import { User } from '../users/entities/user.entity'; +import { type User } from 'src/users/entities/user.entity'; declare module 'express-session' { interface SessionData { user?: User;