Template
feat: working on messages
This commit is contained in:
+1
-1
Submodule events updated: cdddb9a073...22656ba608
Generated
+10
-7
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export class MessageRecieveDTO {
|
||||
constructor(partial: Partial<MessageRecieveDTO>) {
|
||||
this.value = partial.value;
|
||||
}
|
||||
|
||||
value: string;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -19,5 +19,5 @@ export class Topic {
|
||||
name: string;
|
||||
|
||||
@ManyToMany((type) => Word, (word) => word.topics)
|
||||
words: Word[];
|
||||
words: Promise<Word[]>;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class Word {
|
||||
|
||||
@JoinColumn({ name: 'ownerId' })
|
||||
@ManyToOne(() => User)
|
||||
owner: User;
|
||||
owner: Promise<User>;
|
||||
|
||||
@Column({ name: 'ownerId' })
|
||||
ownerId: string;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Vendored
+1
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user