feat: working on messages

This commit is contained in:
2024-12-30 18:16:25 +01:00
parent cfafcfaddf
commit bafe228911
18 changed files with 201 additions and 33 deletions
+1 -1
Submodule events updated: cdddb9a073...22656ba608
+10 -7
View File
@@ -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",
+1 -1
View File
@@ -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",
+11 -1
View File
@@ -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,
+44 -4
View File
@@ -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(
+2 -2
View File
@@ -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,
+7
View File
@@ -0,0 +1,7 @@
export class MessageRecieveDTO {
constructor(partial: Partial<MessageRecieveDTO>) {
this.value = partial.value;
}
value: string;
}
+30 -6
View File
@@ -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<User>;
@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);
}
}
+32
View File
@@ -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<Game>;
}
+7 -2
View File
@@ -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<User>;
@Column({ name: 'userId' })
userId: string;
@ManyToOne(() => Game, { onDelete: 'CASCADE' })
game: Game;
+4 -1
View File
@@ -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],
+44 -1
View File
@@ -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<Game>,
@InjectRepository(Message) private messageResporitory: Repository<Message>,
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) {
+3 -3
View File
@@ -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) {
+1 -1
View File
@@ -19,5 +19,5 @@ export class Topic {
name: string;
@ManyToMany((type) => Word, (word) => word.topics)
words: Word[];
words: Promise<Word[]>;
}
+1 -1
View File
@@ -20,7 +20,7 @@ export class Word {
@JoinColumn({ name: 'ownerId' })
@ManyToOne(() => User)
owner: User;
owner: Promise<User>;
@Column({ name: 'ownerId' })
ownerId: string;
+1 -1
View File
@@ -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);
+1
View File
@@ -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 {}
+1 -1
View File
@@ -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;