From ed28fe6090a4117fe25cfadcc0a666652bddbce4 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 30 Oct 2024 18:56:48 +0400 Subject: [PATCH 01/32] fix voice & params --- .../quiz-answered-event.handler.ts | 2 +- .../wrong-answer-received-event.handler.ts | 5 +-- src/guests/guests.service.ts | 3 +- src/quiz/dto/question.dto.ts | 2 ++ src/quiz/quiz.module.ts | 1 + src/quiz/quiz.service.ts | 35 ++++++++++++++----- src/schemas/config.schema.ts | 3 +- src/schemas/question.schema.ts | 13 ++++++- .../wrong-answer-received-event.handler.ts | 9 ----- 9 files changed, 45 insertions(+), 28 deletions(-) diff --git a/src/game/event-handlers/quiz-answered-event.handler.ts b/src/game/event-handlers/quiz-answered-event.handler.ts index b6337f8..3602780 100644 --- a/src/game/event-handlers/quiz-answered-event.handler.ts +++ b/src/game/event-handlers/quiz-answered-event.handler.ts @@ -10,6 +10,6 @@ export class QuizAnsweredEventHandler } async handle(event: QuizAnsweredEvent) { - await this.commandBus.execute(new HideKeyboardCommand(`На вопрос ответил: ${event.name}`)); + // await this.commandBus.execute(new HideKeyboardCommand(`На вопрос ответил: ${event.name}`)); } } diff --git a/src/game/event-handlers/wrong-answer-received-event.handler.ts b/src/game/event-handlers/wrong-answer-received-event.handler.ts index 1f2ae9c..c96d313 100644 --- a/src/game/event-handlers/wrong-answer-received-event.handler.ts +++ b/src/game/event-handlers/wrong-answer-received-event.handler.ts @@ -10,9 +10,6 @@ export class GameWrongAnswerReceivedEventHandler } async handle(event: WrongAnswerReceivedEvent) { - await this.gameService.addTaskToGameQueue( - event.tId, - GameQueueTypes.penalty, - ); + // } } diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index 4910caa..0e5f0cb 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -317,7 +317,6 @@ export class GuestsService { this.logger.error(`Can't find user ${tId} for incrementing invalid answers count`); return; } - guest.invalidAnswers = +guest.invalidAnswers + 1; guest.invalidAnswersInRow = +guest.invalidAnswersInRow + 1; this.logger.verbose(`Invalid answers: ${guest.invalidAnswers}, inRow: ${guest.invalidAnswersInRow}`); @@ -328,7 +327,7 @@ export class GuestsService { GenitiveCase: guest.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameGenitiveCase)) }, [...screpaDictManyInvalidAnswersDict]) await this.commandBus.execute(new CreateNewQueueItemCommand(guest.telegramId, GameQueueTypes.screpaAnounce, text)); - await this.commandBus.execute(new CreateNewQueueItemCommand(guest.telegramId, GameQueueTypes.penalty)); + // await this.commandBus.execute(new CreateNewQueueItemCommand(guest.telegramId, GameQueueTypes.penalty)); this.logger.verbose(`Reset invalidAnswerInRow, since user received penalty`); guest.invalidAnswersInRow = 0; } diff --git a/src/quiz/dto/question.dto.ts b/src/quiz/dto/question.dto.ts index 2c1e413..017f163 100644 --- a/src/quiz/dto/question.dto.ts +++ b/src/quiz/dto/question.dto.ts @@ -1,8 +1,10 @@ export interface QuestionDto { + id: string; text: string; answers: string[]; valid: string; note: string | null; + qId: string; } diff --git a/src/quiz/quiz.module.ts b/src/quiz/quiz.module.ts index 7551cbd..3975216 100644 --- a/src/quiz/quiz.module.ts +++ b/src/quiz/quiz.module.ts @@ -11,6 +11,7 @@ import { GameNextQuestionCommandHandler } from './command-handlers/next-question import { MarkQuestionsAsUnansweredCommandHandler } from './command-handlers/mark-questions-as-unanswred-command.handler'; import { PenaltyModule } from '../penalty/penalty.module'; import {ConfigModule, ConfigService} from "@nestjs/config"; +import {Config, ConfigSchema} from "../schemas/config.schema"; const cmdHandlers = [ GameNextQuestionCommandHandler, diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 76a89b2..da122a0 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -15,11 +15,13 @@ import {Messages} from "../messaging/tg.text"; import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; import {GameQueueTypes} from "../schemas/game-queue.schema"; import {ConfigService} from "@nestjs/config"; +import {Config, ConfigDocument, ConfigSchema} from "../schemas/config.schema"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { private readonly answerNumbers = Messages.answerNumbers; private readonly logger = new Logger(QuizService.name); + private AcceptAnswersFromAllMembers: boolean = true; // TODO: move this to configurable state constructor( @InjectModel(Question.name) private questionModel: Model, @InjectModel(QuestionStorage.name) @@ -27,15 +29,17 @@ export class QuizService { private guestService: GuestsService, private sharedService: SharedService, private eventBus: EventBus, - private configService: ConfigService, private commandBus: CommandBus, - ) {} + ) { + + } async get(): Promise { return this.questionModel.find().sort({ _id: -1 }).findOne(); } async setQuestion(questionDto: QuestionDto, target: number = null) { + await this.sharedService.setConfig('currentQuestion', questionDto.id) const item = new this.questionModel(questionDto); await item.save(); this.logger.verbose(`Question updated`); @@ -50,10 +54,7 @@ export class QuizService { async validateAnswer(answer: string, id: number) { this.logger.verbose(`enter validate answer ${answer} ${id}`); const question = await this.get(); - if (question.answered) { - this.logger.verbose(`Question already answered`); - return false; - } + question.answered = true; await question.save(); const regexp = new RegExp( @@ -69,6 +70,20 @@ export class QuizService { `Validating answer for question: ${JSON.stringify(question.text)}`, ); const filtered = answer.replace(regexp, '').trim(); + const isAnswerValid = question.valid === filtered; + if(question.userAnswers.find(answer => answer.user === id)) { + this.logger.verbose("question->useranswer is already containing record"); + return; + } + question.userAnswers.push({ + user: id, + valid: isAnswerValid, + time: new Date() + }) + console.log(question); + await question.save(); + console.log(question); + this.logger.verbose("question saved with user details") if (question.valid === filtered) { question.answered = true; question.answeredBy = id; @@ -116,7 +131,7 @@ export class QuizService { .findOne({ isAnswered: false }) .exec(); if (!question) { - const unanswered = await this.getRemainQuestionWithouValidAnswer(); + const unanswered = await this.getRemainQuestionWithoutValidAnswer(); const skipRand = getRandomInt(0, unanswered); question = await this.questionStorageModel .findOne({ isAnsweredCorrectly: false }) @@ -131,6 +146,8 @@ export class QuizService { const question = await this.getNextQuestion(); question.isAnswered = true; await this.setQuestion({ + qId: question.id, + id: question.id, text: question.text, answers: question.answers, valid: question.valid, @@ -143,7 +160,7 @@ export class QuizService { const question = await this.getNextQuestion(); this.logger.verbose(`playExtraQuestion: ${question.text}`); await this.setQuestion( - { text: question.text, answers: question.answers, valid: question.valid, note: question.note }, + { qId: question.id, id: question.id, text: question.text, answers: question.answers, valid: question.valid, note: question.note }, telegramId, ); question.isAnswered = true; @@ -166,7 +183,7 @@ export class QuizService { return questions.length; } - async getRemainQuestionWithouValidAnswer(): Promise { + async getRemainQuestionWithoutValidAnswer(): Promise { const questions = await this.questionStorageModel.find({ isAnsweredCorrectly: false, }); diff --git a/src/schemas/config.schema.ts b/src/schemas/config.schema.ts index 39a4049..a213192 100644 --- a/src/schemas/config.schema.ts +++ b/src/schemas/config.schema.ts @@ -1,8 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -export type ConfigDocument = Config & Document; - @Schema() export class Config { @Prop() @@ -12,3 +10,4 @@ export class Config { } export const ConfigSchema = SchemaFactory.createForClass(Config); +export type ConfigDocument = Config & Document; \ No newline at end of file diff --git a/src/schemas/question.schema.ts b/src/schemas/question.schema.ts index f3bc4b0..471956e 100644 --- a/src/schemas/question.schema.ts +++ b/src/schemas/question.schema.ts @@ -1,8 +1,16 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { Document } from 'mongoose'; + +export class QuestionAnswer { + user: number; + time: Date; + valid: boolean; +} + export type QuestionDocument = Question & Document; + @Schema() export class Question { @Prop() @@ -15,8 +23,11 @@ export class Question { answered: boolean; @Prop() answeredBy: number; - @Prop() note: string | null; + @Prop() + qId: string; + @Prop([ { user: { type: Number }, time: { type: Date }, valid: { type: Boolean}}]) + userAnswers: QuestionAnswer[]; } export const QuestionSchema = SchemaFactory.createForClass(Question); diff --git a/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts b/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts index f8dcc9b..034ce82 100644 --- a/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts @@ -1,7 +1,6 @@ import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; import { WrongAnswerReceivedEvent } from '../../../game/events/wrong-answer-received.event'; import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; import { Logger } from '@nestjs/common'; @EventsHandler(WrongAnswerReceivedEvent) @@ -14,13 +13,5 @@ export class SocketWrongAnswerReceivedEventHandler constructor(private sharedService: SharedService) {} handle(event: WrongAnswerReceivedEvent): any { - - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.WRONG_ANSWER_RECEIVED, - { - telegramId: event.tId, - validAnswer: event.validAnswer, - }, - ); } } -- 2.45.2 From 34eea8ac2e22404834228c1c1e4270f682fe83d1 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 30 Oct 2024 23:26:06 +0400 Subject: [PATCH 02/32] new game logic --- .../create-new-queue-item-command.handler.ts | 4 ++ src/guests/guests.service.ts | 2 +- src/quiz/quiz.controller.ts | 1 + src/quiz/quiz.service.ts | 57 ++++++++++++++++--- src/schemas/game-queue.schema.ts | 1 + src/voice/voice.controller.ts | 4 +- 6 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/game/comand-handlers/create-new-queue-item-command.handler.ts b/src/game/comand-handlers/create-new-queue-item-command.handler.ts index 88448f6..588c0fa 100644 --- a/src/game/comand-handlers/create-new-queue-item-command.handler.ts +++ b/src/game/comand-handlers/create-new-queue-item-command.handler.ts @@ -1,15 +1,19 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CreateNewQueueItemCommand } from '../commands/create-new-queue-item.command'; import { GameService } from '../game.service'; +import {Logger} from "@nestjs/common"; @CommandHandler(CreateNewQueueItemCommand) export class CreateNewQueueItemCommandHandler implements ICommandHandler { + private logger = new Logger(CreateNewQueueItemCommandHandler.name); constructor( private gameService: GameService, + ) { } async execute(command: CreateNewQueueItemCommand): Promise { + this.logger.verbose(`Adding new queue item ${command.type} for ${command.target}`); await this.gameService.addTaskToGameQueue(command.target, command.type, command.text); return Promise.resolve(undefined); } diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index 0e5f0cb..c39980c 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -326,7 +326,7 @@ export class GuestsService { AccusativeCase: guest.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameAccusativeCase)), GenitiveCase: guest.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameGenitiveCase)) }, [...screpaDictManyInvalidAnswersDict]) - await this.commandBus.execute(new CreateNewQueueItemCommand(guest.telegramId, GameQueueTypes.screpaAnounce, text)); + //await this.commandBus.execute(new CreateNewQueueItemCommand(guest.telegramId, GameQueueTypes.screpaAnounce, text)); // await this.commandBus.execute(new CreateNewQueueItemCommand(guest.telegramId, GameQueueTypes.penalty)); this.logger.verbose(`Reset invalidAnswerInRow, since user received penalty`); guest.invalidAnswersInRow = 0; diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index babe221..eb16946 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -18,6 +18,7 @@ export class QuizController { @Post('proceed') async Get() { + console.log('proceed with game') return this.quizService.proceedWithGame(); } diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index da122a0..83ef0be 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -14,8 +14,8 @@ import {getRandomInt} from 'src/helpers/rand-number'; import {Messages} from "../messaging/tg.text"; import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; import {GameQueueTypes} from "../schemas/game-queue.schema"; -import {ConfigService} from "@nestjs/config"; -import {Config, ConfigDocument, ConfigSchema} from "../schemas/config.schema"; +import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; +import {SocketEvents} from "../shared/events.consts"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { @@ -54,8 +54,7 @@ export class QuizService { async validateAnswer(answer: string, id: number) { this.logger.verbose(`enter validate answer ${answer} ${id}`); const question = await this.get(); - - question.answered = true; + //question.answered = true; await question.save(); const regexp = new RegExp( Object.keys(this.answerNumbers) @@ -72,7 +71,7 @@ export class QuizService { const filtered = answer.replace(regexp, '').trim(); const isAnswerValid = question.valid === filtered; if(question.userAnswers.find(answer => answer.user === id)) { - this.logger.verbose("question->useranswer is already containing record"); + this.logger.verbose("question->user answer is already containing record"); return; } question.userAnswers.push({ @@ -85,12 +84,12 @@ export class QuizService { console.log(question); this.logger.verbose("question saved with user details") if (question.valid === filtered) { - question.answered = true; + //question.answered = true; question.answeredBy = id; this.logger.verbose(`extra ${question.note}`); - this.eventBus.publish( - new ValidAnswerReceivedEvent(id, filtered, question.note), - ); + // this.eventBus.publish( + // new ValidAnswerReceivedEvent(id, filtered, question.note), + // ); await question.save(); await this.markQuestionStorageAsAnsweredCorrectly(question.text); return true; @@ -122,10 +121,50 @@ export class QuizService { async proceedWithGame() { this.logger.verbose(`[proceedWithGame] Executing proceed with game`); + await this.calculateScore(); + //this.sharedService.sendSocketNotificationToAllClients(SocketEvents.GameQueueItem, {}); await this.commandBus.execute(new ProceedGameQueueCommand()); return Promise.resolve(true); } + private async calculateScore() { + const question = await this.get(); + this.logger.verbose(`[calculateScore] enter `); + const playerAnswers = question.userAnswers.map((answer) => { + return { + user: answer.user, + valid: answer.valid, + time: answer.time.getTime() + } + }); + console.log(playerAnswers); + const sortedAnswers = playerAnswers.sort((a, b) => a.time - b.time); + const winner = sortedAnswers.find((answer) => answer.valid); + if(winner) { + const totalWinningScore = 100; + sortedAnswers.forEach((answer) => { + this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user, + totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)) + }); + await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 20)); + await this.commandBus.execute(new CreateNewQueueItemCommand(winner.user, GameQueueTypes.showresults)); + + } + + const invalidAnswers = sortedAnswers.filter((answer) => !answer.valid) + if(invalidAnswers.length > 0) { + const lastInvalidAnswer = invalidAnswers[invalidAnswers.length - 1]; + if(!lastInvalidAnswer) { + return; + } + await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.showresults)); + await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty, "лох")); + + + } + + } + private async getNextQuestion() { let question = await this.questionStorageModel .findOne({ isAnswered: false }) diff --git a/src/schemas/game-queue.schema.ts b/src/schemas/game-queue.schema.ts index aca1299..fc32b04 100644 --- a/src/schemas/game-queue.schema.ts +++ b/src/schemas/game-queue.schema.ts @@ -7,6 +7,7 @@ export enum GameQueueTypes { penalty = 'penalty', playExtraCard = 'play_extra_card', screpaAnounce = 'screpa', + showresults = 'show_results', } export type GameQueueDocument = GameQueue & Document; diff --git a/src/voice/voice.controller.ts b/src/voice/voice.controller.ts index 2c3cd3d..c28648a 100644 --- a/src/voice/voice.controller.ts +++ b/src/voice/voice.controller.ts @@ -15,7 +15,7 @@ export class VoiceController { @Header('content-disposition', 'inline') async textToSpeechSSML(@Query() dto: TtsRequestDto) { if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { - return new StreamableFile(await this.voiceService.textToFile(dto, true)); + //return new StreamableFile(await this.voiceService.textToFile(dto, true)); } else { return new NotFoundException('Voice disabled'); } @@ -27,7 +27,7 @@ export class VoiceController { async getText(@Query() dto: TtsRequestDto) { dto.text = translit(dto.text); if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { - return new StreamableFile(await this.voiceService.textToFile(dto)); + //return new StreamableFile(await this.voiceService.textToFile(dto)); } else { return new NotFoundException('Voice disabled'); } -- 2.45.2 From bcc2913d0f4afbf874c1aec38a9e38524244326e Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 30 Oct 2024 23:38:37 +0400 Subject: [PATCH 03/32] fix logic --- src/quiz/quiz.service.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 83ef0be..573b232 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -87,9 +87,9 @@ export class QuizService { //question.answered = true; question.answeredBy = id; this.logger.verbose(`extra ${question.note}`); - // this.eventBus.publish( - // new ValidAnswerReceivedEvent(id, filtered, question.note), - // ); + this.eventBus.publish( + new ValidAnswerReceivedEvent(id, filtered, question.note), + ); await question.save(); await this.markQuestionStorageAsAnsweredCorrectly(question.text); return true; @@ -140,15 +140,15 @@ export class QuizService { console.log(playerAnswers); const sortedAnswers = playerAnswers.sort((a, b) => a.time - b.time); const winner = sortedAnswers.find((answer) => answer.valid); + let targetUser = 0; if(winner) { - const totalWinningScore = 100; - sortedAnswers.forEach((answer) => { + const totalWinningScore = 80; + sortedAnswers.filter(x => x.valid).forEach((answer) => { this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user, totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)) }); - await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 20)); - await this.commandBus.execute(new CreateNewQueueItemCommand(winner.user, GameQueueTypes.showresults)); - + await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 15)); + targetUser = winner.user; } const invalidAnswers = sortedAnswers.filter((answer) => !answer.valid) @@ -157,11 +157,10 @@ export class QuizService { if(!lastInvalidAnswer) { return; } - await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.showresults)); - await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty, "лох")); - - + targetUser = lastInvalidAnswer.user; + await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty)); } + await this.commandBus.execute(new CreateNewQueueItemCommand(targetUser, GameQueueTypes.showresults)); } -- 2.45.2 From 75080c29bb5072c444d559478f636e744c36e016 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Thu, 31 Oct 2024 01:32:02 +0400 Subject: [PATCH 04/32] results page --- src/game/game.service.ts | 3 ++- src/messaging/quiz-messaging.controller.ts | 1 + src/quiz/quiz.controller.ts | 5 +++++ src/quiz/quiz.service.ts | 6 +++++- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 4f8e43b..75c49ca 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -63,9 +63,10 @@ export class GameService implements OnApplicationBootstrap{ } else { qItem = await this.gameQueueModel.findById(id).exec(); } - this.logger.verbose(`Set ${id} in queue as completed`); + this.logger.verbose(`Set ${qItem.id} in queue as completed`); if (!qItem) { throw new InternalServerErrorException('no such item'); + } qItem.completed = true; await qItem.save(); diff --git a/src/messaging/quiz-messaging.controller.ts b/src/messaging/quiz-messaging.controller.ts index 71b9b8c..964adc2 100644 --- a/src/messaging/quiz-messaging.controller.ts +++ b/src/messaging/quiz-messaging.controller.ts @@ -30,6 +30,7 @@ export class QuizMessagingController { async completeQueueItem(@Payload() data: any) { this.logger.verbose(`complete item`) await this.gameService.markQueueAsCompleted(null); + //await this.quizService.proceedWithGame(); } @MessagePattern({ cmd: CommandsConsts.GetCards}) diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index eb16946..17980e6 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -16,6 +16,11 @@ export class QuizController { return await this.quizService.setQuestion(qustionDto); } + @Get('question-results') + async GetQuestionResults() { + return await this.quizService.getQuestionResults(); + } + @Post('proceed') async Get() { console.log('proceed with game') diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 573b232..0178dea 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -122,7 +122,6 @@ export class QuizService { async proceedWithGame() { this.logger.verbose(`[proceedWithGame] Executing proceed with game`); await this.calculateScore(); - //this.sharedService.sendSocketNotificationToAllClients(SocketEvents.GameQueueItem, {}); await this.commandBus.execute(new ProceedGameQueueCommand()); return Promise.resolve(true); } @@ -264,4 +263,9 @@ export class QuizService { await newQuestion.save(); } } + + async getQuestionResults() { + const question = await this.get(); + return question.userAnswers; + } } -- 2.45.2 From 08fa0563e7c1ceb887a688676c688cf5d859a9a5 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Thu, 31 Oct 2024 14:08:31 +0400 Subject: [PATCH 05/32] TGD-23 --- .../increase-player-score-command.handler.ts | 19 +++++++++++++++++++ .../command/increase-player-score.command.ts | 4 ++++ src/guests/guests.module.ts | 2 ++ src/quiz/quiz.controller.ts | 1 - src/quiz/quiz.service.ts | 7 +++---- 5 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 src/guests/command/handlers/increase-player-score-command.handler.ts create mode 100644 src/guests/command/increase-player-score.command.ts diff --git a/src/guests/command/handlers/increase-player-score-command.handler.ts b/src/guests/command/handlers/increase-player-score-command.handler.ts new file mode 100644 index 0000000..bce1dde --- /dev/null +++ b/src/guests/command/handlers/increase-player-score-command.handler.ts @@ -0,0 +1,19 @@ +import {CommandHandler, ICommandHandler} from "@nestjs/cqrs"; +import {IncreasePlayerScoreCommand} from "../increase-player-score.command"; +import { Logger } from "@nestjs/common"; +import {GuestsService} from "../../guests.service"; + +@CommandHandler(IncreasePlayerScoreCommand) +export class IncreasePlayerScoreCommandHandler implements ICommandHandler { + private logger = new Logger(IncreasePlayerScoreCommandHandler.name); + constructor(private guestService: GuestsService) { + } + + + async execute(command: IncreasePlayerScoreCommand): Promise { + this.logger.verbose(`IncreasePlayerScoreCommandHandler: entering, arguments: player: ${command.user}, amount: ${command.score}`); + await this.guestService.updatePlayerScore(command.user, command.score); + return true; + } + +} \ No newline at end of file diff --git a/src/guests/command/increase-player-score.command.ts b/src/guests/command/increase-player-score.command.ts new file mode 100644 index 0000000..7e87c36 --- /dev/null +++ b/src/guests/command/increase-player-score.command.ts @@ -0,0 +1,4 @@ +export class IncreasePlayerScoreCommand { + constructor(public user: number, public score: number) { + } +} \ No newline at end of file diff --git a/src/guests/guests.module.ts b/src/guests/guests.module.ts index 95e776f..5b5de11 100644 --- a/src/guests/guests.module.ts +++ b/src/guests/guests.module.ts @@ -25,6 +25,7 @@ import {SetGuestPropertyCommandHandler} from "./command/handlers/set-guest-prope import {WrongAnswerReceivedGuestEventHandler} from "./event-handlers/wrong-answer-received-guest-event.handler"; import {VoiceService} from "../voice/voice.service"; import {VoiceModule} from "../voice/voice.module"; +import {IncreasePlayerScoreCommandHandler} from "./command/handlers/increase-player-score-command.handler"; const commandHandlers = [ GuestsRemoveKeyboardHandler, @@ -32,6 +33,7 @@ const commandHandlers = [ IncreasePlayerWinningRateCommandHandler, GetGuestPropertyHandler, SetGuestPropertyCommandHandler, + IncreasePlayerScoreCommandHandler, ]; diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index 17980e6..a0d850c 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -23,7 +23,6 @@ export class QuizController { @Post('proceed') async Get() { - console.log('proceed with game') return this.quizService.proceedWithGame(); } diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 0178dea..821c573 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -16,6 +16,7 @@ import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item. import {GameQueueTypes} from "../schemas/game-queue.schema"; import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; import {SocketEvents} from "../shared/events.consts"; +import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { @@ -79,9 +80,7 @@ export class QuizService { valid: isAnswerValid, time: new Date() }) - console.log(question); await question.save(); - console.log(question); this.logger.verbose("question saved with user details") if (question.valid === filtered) { //question.answered = true; @@ -136,7 +135,6 @@ export class QuizService { time: answer.time.getTime() } }); - console.log(playerAnswers); const sortedAnswers = playerAnswers.sort((a, b) => a.time - b.time); const winner = sortedAnswers.find((answer) => answer.valid); let targetUser = 0; @@ -144,7 +142,8 @@ export class QuizService { const totalWinningScore = 80; sortedAnswers.filter(x => x.valid).forEach((answer) => { this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user, - totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)) + totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)); + this.commandBus.execute(new IncreasePlayerScoreCommand(answer.user,1)); }); await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 15)); targetUser = winner.user; -- 2.45.2 From ced62ddfbae6608f0a5c980a1228de674e6d3973 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 12:32:04 +0400 Subject: [PATCH 06/32] tests & endgame logic (start) --- package-lock.json | 18 +++++--- package.json | 2 +- .../give-out-a-prize-command.handler.ts | 4 +- src/guests/guests.service.ts | 18 +++++++- src/mocks/gift-service.mock.ts | 3 ++ src/mocks/quiz-service.mock.ts | 3 ++ src/mocks/shared-service.mock.ts | 3 ++ src/mocks/state-service.mock.ts | 3 ++ src/quiz/quiz.controller.ts | 14 +++++-- src/quiz/quiz.service.ts | 29 ++++++++++++- src/scheduler/scheduler.service.spec.ts | 41 ++++++++++++++++++- src/scheduler/scheduler.service.ts | 15 +++---- src/schemas/guest.schema.ts | 4 +- src/state/state.service.ts | 5 ++- 14 files changed, 132 insertions(+), 30 deletions(-) create mode 100644 src/mocks/gift-service.mock.ts create mode 100644 src/mocks/quiz-service.mock.ts create mode 100644 src/mocks/shared-service.mock.ts create mode 100644 src/mocks/state-service.mock.ts diff --git a/package-lock.json b/package-lock.json index 01f1711..b03798a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ }, "devDependencies": { "@nestjs/schematics": "^10.0.3", - "@nestjs/testing": "^10.2.8", + "@nestjs/testing": "^10.4.7", "@types/cron": "^2.0.1", "@types/express": "^4.17.21", "@types/jest": "^29.5.8", @@ -1905,12 +1905,13 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.8.tgz", - "integrity": "sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==", + "version": "10.4.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.7.tgz", + "integrity": "sha512-aS3sQ0v4g8cyHDzW3xJv1+8MiFAkxUNXmnau588IFFI/nBIo/kevLNHNPr85keYekkJ/lwNDW72h8UGg8BYd9w==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "2.6.2" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -1931,6 +1932,13 @@ } } }, + "node_modules/@nestjs/testing/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" + }, "node_modules/@nestjs/websockets": { "version": "10.2.8", "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.2.8.tgz", diff --git a/package.json b/package.json index 21a7ae2..2644a62 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@nestjs/schematics": "^10.0.3", - "@nestjs/testing": "^10.2.8", + "@nestjs/testing": "^10.4.7", "@types/cron": "^2.0.1", "@types/express": "^4.17.21", "@types/jest": "^29.5.8", diff --git a/src/game/comand-handlers/give-out-a-prize-command.handler.ts b/src/game/comand-handlers/give-out-a-prize-command.handler.ts index 2ebd100..77ddcb8 100644 --- a/src/game/comand-handlers/give-out-a-prize-command.handler.ts +++ b/src/game/comand-handlers/give-out-a-prize-command.handler.ts @@ -3,6 +3,7 @@ import { GiveOutAPrizeCommand } from '../commands/give-out-a-prize.command'; import { GameService } from '../game.service'; import { Logger } from '@nestjs/common'; import { GameQueueTypes } from '../../schemas/game-queue.schema'; +import {GuestsService} from "../../guests/guests.service"; @CommandHandler(GiveOutAPrizeCommand) export class GameGiveOutAPrizeCommandHandler @@ -10,11 +11,12 @@ export class GameGiveOutAPrizeCommandHandler private readonly logger = new Logger(GameGiveOutAPrizeCommandHandler.name); - constructor(private gameService: GameService) { + constructor(private gameService: GameService, private guestService: GuestsService) { } async execute(command: GiveOutAPrizeCommand): Promise { this.logger.verbose(`Player winning a prize ${command.telegramId}`); + await this.guestService.incrementPrizeCount(command.telegramId); return this.gameService.addTaskToGameQueue( command.telegramId, GameQueueTypes.giveOutAPrize, diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index c39980c..650f83a 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -24,8 +24,6 @@ import {MqtMessageModel} from "../messaging/models/mqt-message.model"; import {ConfigService} from "@nestjs/config"; import {StringHelper} from "../helpers/stringhelper"; import {DebuffsConsts} from "../game/entities/debuffs.consts"; -import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; -import {GameQueueTypes} from "../schemas/game-queue.schema"; import {VoiceService} from "../voice/voice.service"; import {screpaDictManyInvalidAnswersDict} from "../voice/dicts/screpa-dict-many-invalid-answers.dict"; import {GuestPropertiesConsts} from "../schemas/properties.consts"; @@ -66,6 +64,10 @@ export class GuestsService { return this.guestModel.find().exec(); } + getModel() { + return this.guestModel; + } + async filter(properties: object) { return this.guestModel.find(properties).exec(); } @@ -340,4 +342,16 @@ export class GuestsService { guest.invalidAnswersInRow = 0; await guest.save(); } + + async incrementPrizeCount(telegramId: number) { + const guest = await this.findById(telegramId); + guest.rewardsReceived += 1; + await guest.save(); + } + + async updatePenaltiesCount(user: number) { + const guest = await this.findById(user); + guest.penaltiesReceived += 1; + await guest.save(); + } } diff --git a/src/mocks/gift-service.mock.ts b/src/mocks/gift-service.mock.ts new file mode 100644 index 0000000..1e5b4af --- /dev/null +++ b/src/mocks/gift-service.mock.ts @@ -0,0 +1,3 @@ +export const GiftServiceMock = { + getRemainingPrizeCount: () => jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/quiz-service.mock.ts b/src/mocks/quiz-service.mock.ts new file mode 100644 index 0000000..4cef2c0 --- /dev/null +++ b/src/mocks/quiz-service.mock.ts @@ -0,0 +1,3 @@ +export const QuizServiceMock = { + getRemainQuestionCount: () =>jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/shared-service.mock.ts b/src/mocks/shared-service.mock.ts new file mode 100644 index 0000000..e1696ae --- /dev/null +++ b/src/mocks/shared-service.mock.ts @@ -0,0 +1,3 @@ +export const SharedServiceMock = { + sendSocketNotificationToAllClients: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/state-service.mock.ts b/src/mocks/state-service.mock.ts new file mode 100644 index 0000000..797245e --- /dev/null +++ b/src/mocks/state-service.mock.ts @@ -0,0 +1,3 @@ +export const StateServiceMock = { + setState: jest.fn(), +} \ No newline at end of file diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index a0d850c..652de1f 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Post } from "@nestjs/common"; -import { QuestionDto, QuestionDtoExcel } from './dto/question.dto'; -import { QuizService } from "./quiz.service"; -import { ExtraQuestionDto } from './dto/extra-question.dto'; +import {Body, Controller, Get, Post} from "@nestjs/common"; +import {QuestionDto, QuestionDtoExcel} from './dto/question.dto'; +import {QuizService} from "./quiz.service"; +import {ExtraQuestionDto} from './dto/extra-question.dto'; @Controller('quiz') export class QuizController { @@ -50,4 +50,10 @@ export class QuizController { async dealPrize() { return this.quizService.dealPrize(); } + + @Get('endgame-extrapoints') + async endgameExtrapoints() + { + return await this.quizService.addEndgamePoints(); + } } diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 821c573..42fb91a 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -10,12 +10,11 @@ import {ValidAnswerReceivedEvent} from '../game/events/valid-answer.recieved'; import {QuestionStorage, QuestionStorageDocument,} from '../schemas/question-storage.schema'; import {WrongAnswerReceivedEvent} from '../game/events/wrong-answer-received.event'; import {ProceedGameQueueCommand} from '../game/commands/proceed-game-queue.command'; -import {getRandomInt} from 'src/helpers/rand-number'; +import {getRandomInt} from '../helpers/rand-number'; import {Messages} from "../messaging/tg.text"; import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; import {GameQueueTypes} from "../schemas/game-queue.schema"; import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; -import {SocketEvents} from "../shared/events.consts"; import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; @Injectable({ scope: Scope.TRANSIENT }) @@ -156,10 +155,36 @@ export class QuizService { return; } targetUser = lastInvalidAnswer.user; + await this.guestService.updatePenaltiesCount(lastInvalidAnswer.user); await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty)); } await this.commandBus.execute(new CreateNewQueueItemCommand(targetUser, GameQueueTypes.showresults)); + } + public async addEndgamePoints() { + const maxInvalidAnswersPromise = this.guestService.getModel().find({}).sort({ ['invalidAnswers']: 'desc'}).exec(); + const maxRewardsPromise = this.guestService.getModel().find({}).sort({['rewardsReceived']: "desc"}).exec(); + const maxPenaltiesPromise = this.guestService.getModel().find({}).sort({['penaltiesReceived']: 'desc'}).exec(); + + //const { maxRewards, maxInvalidAnswers } = Promise.all([maxRewardsPromise, maxInvalidAnswersPromise]); + const [maxRewards, maxInvalidAnswers, maxPenaltiesReceived] = await Promise.all([maxRewardsPromise, maxInvalidAnswersPromise, maxPenaltiesPromise]); + return { + maxInvalidAnswers: { + id: maxInvalidAnswers[0].id, + count: maxInvalidAnswers[0].invalidAnswers, + name: maxInvalidAnswers[0].name, + }, + maxRewards: { + id: maxRewards[0].id, + count: maxRewards[0].rewardsReceived, + name: maxRewards[0].name, + }, + maxPenalties: { + id: maxPenaltiesReceived[0].id, + count: maxPenaltiesReceived[0].penaltiesReceived, + name: maxPenaltiesReceived[0].name, + } + } } private async getNextQuestion() { diff --git a/src/scheduler/scheduler.service.spec.ts b/src/scheduler/scheduler.service.spec.ts index 1327eb5..e089e01 100644 --- a/src/scheduler/scheduler.service.spec.ts +++ b/src/scheduler/scheduler.service.spec.ts @@ -1,18 +1,55 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchedulerService } from './scheduler.service'; +import {GiftsService} from "../gifts/gifts.service"; +import {GiftServiceMock} from "../mocks/gift-service.mock"; +import {StateService} from "../state/state.service"; +import {StateServiceMock} from "../mocks/state-service.mock"; +import {QuizService} from "../quiz/quiz.service"; +import {QuizServiceMock} from "../mocks/quiz-service.mock"; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; + describe('SchedulerService', () => { let service: SchedulerService; - + let giftService: GiftsService; + let sharedService: SharedService; + let stateService: StateService; beforeEach(async () => { + jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - providers: [SchedulerService], + providers: [SchedulerService, + { provide: GiftsService, useValue: GiftServiceMock }, + { provide: StateService, useValue: StateServiceMock }, + { provide: QuizService, useValue: QuizServiceMock }, + { provide: SharedService, useValue: SharedServiceMock }, + ], }).compile(); service = module.get(SchedulerService); + giftService = module.get(GiftsService); + sharedService = module.get(SharedService); + stateService = module.get(StateService); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should finish game if prizes count is 0', async () => { + const getRemainingPrizeCountFn = jest.spyOn(giftService, 'getRemainingPrizeCount').mockImplementation(() => Promise.resolve(0)); + const notificationFn = jest.spyOn(sharedService,'sendSocketNotificationToAllClients').mockImplementation(); + const setStateFn = jest.spyOn(stateService,'setState').mockImplementation((name,newstate) => Promise.resolve({ state: name, value: newstate})); + await service.gameStatus(); + expect(getRemainingPrizeCountFn).toHaveBeenCalled(); + expect(notificationFn).toHaveBeenCalled(); + expect(notificationFn).toHaveBeenCalledWith('state_changed', expect.objectContaining({ state: 'main', value: 'finish'})); + }); + + it('should not finish game if prizes count above 0', async () => { + const getRemainingPrizeCountFn = jest.spyOn(giftService, 'getRemainingPrizeCount').mockImplementation(() => Promise.resolve(5)); + const notificationFn = jest.spyOn(sharedService,'sendSocketNotificationToAllClients').mockImplementation() + await service.gameStatus(); + expect(notificationFn).not.toHaveBeenCalled(); + }); }); diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts index 1b2974a..6898424 100644 --- a/src/scheduler/scheduler.service.ts +++ b/src/scheduler/scheduler.service.ts @@ -1,14 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { Cron } from '@nestjs/schedule'; import { StateService } from '../state/state.service'; -import {CommandBus, QueryBus} from '@nestjs/cqrs'; -import { GiftsService } from 'src/gifts/gifts.service'; -import { QuizService } from 'src/quiz/quiz.service'; -import { SharedService } from 'src/shared/shared.service'; -import {GetGuestPropertyQuery} from "../guests/command/get-guest-property.handler"; -import {GuestPropertiesConsts} from "../schemas/properties.consts"; -import {GetGuestQuery} from "../guests/queries/getguest.query"; -import {StringHelper} from "../helpers/stringhelper"; +import { QuizService } from '../quiz/quiz.service'; +import { GiftsService } from '../gifts/gifts.service'; +import { SharedService } from '../shared/shared.service'; + @Injectable() export class SchedulerService { private readonly logger = new Logger(SchedulerService.name); @@ -17,8 +13,6 @@ export class SchedulerService { constructor( private stateService: StateService, - private cmdBus: CommandBus, - private queryBus: QueryBus, private giftsService: GiftsService, private quizService: QuizService, private sharedService: SharedService, @@ -38,6 +32,7 @@ export class SchedulerService { const giftsLeft = await this.giftsService.getRemainingPrizeCount(); if (giftsLeft === 0) { const state = await this.stateService.setState('main', 'finish'); + console.log(this.state); this.sharedService.sendSocketNotificationToAllClients( 'state_changed', state, diff --git a/src/schemas/guest.schema.ts b/src/schemas/guest.schema.ts index 9ad02c2..e5ba7b2 100644 --- a/src/schemas/guest.schema.ts +++ b/src/schemas/guest.schema.ts @@ -25,8 +25,6 @@ export class Guest { @Prop({ default: 10 }) prizeChance: number; @Prop({ default: 0 }) - prizesCount: number; - @Prop({ default: 0 }) validAnswers: number; @Prop({ default: 0 }) invalidAnswers: number; @@ -34,6 +32,8 @@ export class Guest { invalidAnswersInRow: number; @Prop({ default:0 }) rewardsReceived: number; + @Prop({ default: 0}) + penaltiesReceived: number; @Prop({ type: Map }) properties: Record; } diff --git a/src/state/state.service.ts b/src/state/state.service.ts index 47dc65b..feaaaac 100644 --- a/src/state/state.service.ts +++ b/src/state/state.service.ts @@ -37,6 +37,9 @@ export class StateService { const stateEntity = await this.getState(name); stateEntity.value = newValue; await stateEntity.save(); - return stateEntity; + return { + state: stateEntity.state, + value: stateEntity.value, + } } } -- 2.45.2 From dfd149103e68ce8faab77172dc740339ed919644 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 13:55:55 +0400 Subject: [PATCH 07/32] TGD-30: FeatureFlags --- src/app.module.ts | 6 ++++-- src/mocks/config-service.mock.ts | 3 +++ src/mocks/featureflag-service.mock.ts | 4 ++++ src/mocks/shared-service.mock.ts | 2 ++ src/mocks/voice-service.mock.ts | 3 +++ src/scheduler/scheduler.service.ts | 1 - src/shared/shared.service.ts | 16 +++++++++++++--- src/voice/voice.controller.spec.ts | 18 +++++++++++++++++- 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 src/mocks/config-service.mock.ts create mode 100644 src/mocks/featureflag-service.mock.ts create mode 100644 src/mocks/voice-service.mock.ts diff --git a/src/app.module.ts b/src/app.module.ts index 107f690..5b0d4e9 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -20,6 +20,8 @@ import {ConfigModule} from "@nestjs/config"; import {MessagingModule} from "./messaging/messaging.module"; import * as process from "process"; import {OpenaiModule} from "./openai/openai.module"; +import { FeatureflagController } from './featureflag/featureflag.controller'; +import { FeatureflagService } from './featureflag/featureflag.service'; @Module({ imports: [ @@ -41,8 +43,8 @@ import {OpenaiModule} from "./openai/openai.module"; GiftsModule, OpenaiModule ], - controllers: [AppController], - providers: [AppService, SocketGateway, SchedulerService], + controllers: [AppController, FeatureflagController], + providers: [AppService, SocketGateway, SchedulerService, FeatureflagService], exports: [AppService, SocketGateway], }) export class AppModule {} diff --git a/src/mocks/config-service.mock.ts b/src/mocks/config-service.mock.ts new file mode 100644 index 0000000..d8019f2 --- /dev/null +++ b/src/mocks/config-service.mock.ts @@ -0,0 +1,3 @@ +export const ConfigServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/featureflag-service.mock.ts b/src/mocks/featureflag-service.mock.ts new file mode 100644 index 0000000..f905e58 --- /dev/null +++ b/src/mocks/featureflag-service.mock.ts @@ -0,0 +1,4 @@ +export const FeatureflagServiceMock = { + getFeatureFlag: jest.fn(), + setFeatureFlag: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/shared-service.mock.ts b/src/mocks/shared-service.mock.ts index e1696ae..ab35373 100644 --- a/src/mocks/shared-service.mock.ts +++ b/src/mocks/shared-service.mock.ts @@ -1,3 +1,5 @@ export const SharedServiceMock = { + setConfig: jest.fn(), + getConfig: jest.fn(), sendSocketNotificationToAllClients: jest.fn(), } \ No newline at end of file diff --git a/src/mocks/voice-service.mock.ts b/src/mocks/voice-service.mock.ts new file mode 100644 index 0000000..63e0e21 --- /dev/null +++ b/src/mocks/voice-service.mock.ts @@ -0,0 +1,3 @@ +export const VoiceServiceMock = { + +} \ No newline at end of file diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts index 6898424..62a7508 100644 --- a/src/scheduler/scheduler.service.ts +++ b/src/scheduler/scheduler.service.ts @@ -32,7 +32,6 @@ export class SchedulerService { const giftsLeft = await this.giftsService.getRemainingPrizeCount(); if (giftsLeft === 0) { const state = await this.stateService.setState('main', 'finish'); - console.log(this.state); this.sharedService.sendSocketNotificationToAllClients( 'state_changed', state, diff --git a/src/shared/shared.service.ts b/src/shared/shared.service.ts index 85955d6..6ba9c3b 100644 --- a/src/shared/shared.service.ts +++ b/src/shared/shared.service.ts @@ -16,11 +16,15 @@ export class SharedService { } async getConfig(key: string) { - return this.configModel + const res = await this.configModel .findOne({ key, }) .exec(); + return { + key: res.key, + value: res.value, + } } async setConfig(key: string, value: string) { @@ -35,11 +39,17 @@ export class SharedService { value, }); await record.save(); - return record; + return { + key: record.key, + value: record.value, + } } cfgItem.value = value; await cfgItem.save(); - return cfgItem; + return { + key: cfgItem.key, + value: cfgItem.value, + } } sendSocketNotificationToAllClients(event: string, payload?: any) { diff --git a/src/voice/voice.controller.spec.ts b/src/voice/voice.controller.spec.ts index 948bc66..735777b 100644 --- a/src/voice/voice.controller.spec.ts +++ b/src/voice/voice.controller.spec.ts @@ -1,15 +1,31 @@ import { Test, TestingModule } from '@nestjs/testing'; import { VoiceController } from './voice.controller'; +import {VoiceService} from "./voice.service"; +import {VoiceServiceMock} from "../mocks/voice-service.mock"; +import {Config} from "../schemas/config.schema"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; describe('VoiceController', () => { let controller: VoiceController; + let voiceService: VoiceService; + let configService: ConfigService; beforeEach(async () => { + jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - controllers: [VoiceController], + controllers: [ + VoiceController, + ], + providers: [ + { provide: VoiceService, useValue: VoiceServiceMock }, + { provide: ConfigService, useValue: ConfigServiceMock }, + ] }).compile(); controller = module.get(VoiceController); + voiceService = module.get(VoiceService); + configService = module.get(ConfigService); }); it('should be defined', () => { -- 2.45.2 From 0d261019480e70f9576ec703dc6bb3ca78dfc4fe Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 13:56:04 +0400 Subject: [PATCH 08/32] TGD-30: FeatureFlags --- .../featureflag.controller.spec.ts | 18 +++++++ src/featureflag/featureflag.controller.ts | 18 +++++++ src/featureflag/featureflag.service.spec.ts | 51 +++++++++++++++++++ src/featureflag/featureflag.service.ts | 35 +++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 src/featureflag/featureflag.controller.spec.ts create mode 100644 src/featureflag/featureflag.controller.ts create mode 100644 src/featureflag/featureflag.service.spec.ts create mode 100644 src/featureflag/featureflag.service.ts diff --git a/src/featureflag/featureflag.controller.spec.ts b/src/featureflag/featureflag.controller.spec.ts new file mode 100644 index 0000000..9dbbf1f --- /dev/null +++ b/src/featureflag/featureflag.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FeatureflagController } from './featureflag.controller'; + +describe('FeatureflagController', () => { + let controller: FeatureflagController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [FeatureflagController], + }).compile(); + + controller = module.get(FeatureflagController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/featureflag/featureflag.controller.ts b/src/featureflag/featureflag.controller.ts new file mode 100644 index 0000000..fca0071 --- /dev/null +++ b/src/featureflag/featureflag.controller.ts @@ -0,0 +1,18 @@ +import {Body, Controller, Get, Param, Post} from '@nestjs/common'; +import {FeatureflagService} from "./featureflag.service"; + +@Controller('featureflag') +export class FeatureflagController { + constructor(private featureflagService: FeatureflagService) { + } + + @Get(':ffname') + async getFeatureFlag(@Param() params: { ffname: string}) { + return await this.featureflagService.getFeatureFlag(params.ffname); + } + + @Post(':ffname') + async setFeatureFlag(@Param() params: { ffname: string}, @Body() ffState: { name: string, state: boolean }) { + return await this.featureflagService.setFeatureFlag(params.ffname, ffState.state ); + } +} diff --git a/src/featureflag/featureflag.service.spec.ts b/src/featureflag/featureflag.service.spec.ts new file mode 100644 index 0000000..d114fae --- /dev/null +++ b/src/featureflag/featureflag.service.spec.ts @@ -0,0 +1,51 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FeatureflagService } from './featureflag.service'; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; + + +describe('FeatureflagService', () => { + let service: FeatureflagService; + let sharedService: SharedService; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FeatureflagService, + { provide: SharedService,useValue: SharedServiceMock }, + ], + }).compile(); + + service = module.get(FeatureflagService); + sharedService = module.get(SharedService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should set feature flag state', async () => { + const testFeatureName = 'TestFeatureFlag'; + const testFeatureState = true; + const setConfigMock = jest + .spyOn(sharedService,'setConfig') + .mockImplementation((name: string, value: string) => Promise.resolve({ key: name, value: value})); + await service.setFeatureFlag(testFeatureName, testFeatureState); + expect(setConfigMock).toHaveBeenCalledWith(`featureflag/${testFeatureName}`, testFeatureState.toString()); + expect(setConfigMock).toHaveBeenCalled(); + }); + + it('should return state of the feature flag', async () => { + const testFeatureName = 'TestFeatureFlag'; + const testFeatureState = true; + const getConfigMock = jest + .spyOn(sharedService, 'getConfig') + .mockImplementation((key: string) => Promise.resolve({ key:key, value: testFeatureState.toString() })); + await service.getFeatureFlag(testFeatureName); + + expect(getConfigMock).toHaveBeenCalledTimes(1); + expect(getConfigMock).toHaveBeenCalledWith(`featureflag/${testFeatureName}`); + + }); +}); diff --git a/src/featureflag/featureflag.service.ts b/src/featureflag/featureflag.service.ts new file mode 100644 index 0000000..bda57f9 --- /dev/null +++ b/src/featureflag/featureflag.service.ts @@ -0,0 +1,35 @@ +import {Injectable, Logger} from '@nestjs/common'; +import {SharedService} from "../shared/shared.service"; + +export interface IFeatureFlagStatus { + name: string; + state: boolean; +} + +@Injectable() +export class FeatureflagService { + private logger = new Logger(FeatureflagService.name); + constructor(private sharedService: SharedService ) { + } + + async getFeatureFlag(id: string): Promise { + this.logger.verbose(`Getting feature flag status for ${id}`); + const state = await this.sharedService.getConfig(`featureflag/${id}`); + return { + name: id, + state: state.value !== 'false', + } + } + + async setFeatureFlag(id: string, status: boolean) : Promise { + this.logger.verbose(`Setting feature flag status for ${id} to ${status} `); + const result = await this.sharedService.setConfig(`featureflag/${id}`, status.toString()); + console.log(result.value); + console.log(!!result.value); + return { + name: id, + state: result.value !== 'false', + } + } + +} -- 2.45.2 From 0a4176aa93d24151be0204d7f78701adb5fb9602 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 14:24:33 +0400 Subject: [PATCH 09/32] TGD-30: FeatureFlags add tests --- src/Consts/FeatureFlags.consts.ts | 3 ++ src/Consts/commands.consts.ts | 2 +- .../featureflag.controller.spec.ts | 33 ++++++++++++++++++- src/featureflag/featureflag.controller.ts | 6 ++-- src/featureflag/featureflag.service.ts | 2 -- src/scheduler/scheduler.service.ts | 6 ++++ 6 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 src/Consts/FeatureFlags.consts.ts diff --git a/src/Consts/FeatureFlags.consts.ts b/src/Consts/FeatureFlags.consts.ts new file mode 100644 index 0000000..495e440 --- /dev/null +++ b/src/Consts/FeatureFlags.consts.ts @@ -0,0 +1,3 @@ +export class FeatureFlagsConsts { + static EnableEndgamePoints = 'EnableEndgamePoints'; +} \ No newline at end of file diff --git a/src/Consts/commands.consts.ts b/src/Consts/commands.consts.ts index d6c9c6b..feb6b6d 100644 --- a/src/Consts/commands.consts.ts +++ b/src/Consts/commands.consts.ts @@ -9,4 +9,4 @@ export class CommandsConsts { static GetCards = 'GetCards'; static ApplyDebuff = 'ApplyDebuff'; static CompleteQueue = 'CompleteQueue'; -} \ No newline at end of file +} diff --git a/src/featureflag/featureflag.controller.spec.ts b/src/featureflag/featureflag.controller.spec.ts index 9dbbf1f..e2642f2 100644 --- a/src/featureflag/featureflag.controller.spec.ts +++ b/src/featureflag/featureflag.controller.spec.ts @@ -1,18 +1,49 @@ import { Test, TestingModule } from '@nestjs/testing'; import { FeatureflagController } from './featureflag.controller'; +import {FeatureflagService} from "./featureflag.service"; +import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; describe('FeatureflagController', () => { let controller: FeatureflagController; + let featureflagService: FeatureflagService; beforeEach(async () => { + jest.resetAllMocks(); const module: TestingModule = await Test.createTestingModule({ controllers: [FeatureflagController], + providers: [ + { provide: FeatureflagService, useValue: FeatureflagServiceMock }, + ] }).compile(); - controller = module.get(FeatureflagController); + featureflagService = module.get(FeatureflagService); }); it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('should call feature flag service to get state', async() => { + const ffNameToTest = "TestFeature"; + const getFFMock = jest.spyOn(featureflagService, 'getFeatureFlag') + .mockImplementation( + (name) => Promise.resolve({ name: name, state: true}) + ); + + await controller.getFeatureFlag({ ffname: ffNameToTest }); + + expect(getFFMock).toHaveBeenCalled(); + expect(getFFMock).toHaveBeenCalledWith(ffNameToTest); + }); + + it('should call feature flag service to set state', async () => { + const ffNameToTest = "TestFeature"; + const setFFMock = jest.spyOn(featureflagService, 'setFeatureFlag') + .mockImplementation((id, status) => Promise.resolve({ name: id, state: false})); + + await controller.setFeatureFlag({ name: ffNameToTest, state: true }); + + expect(setFFMock).toHaveBeenCalled(); + expect(setFFMock).toHaveBeenCalledWith(ffNameToTest, true); + }); }); diff --git a/src/featureflag/featureflag.controller.ts b/src/featureflag/featureflag.controller.ts index fca0071..16c53e8 100644 --- a/src/featureflag/featureflag.controller.ts +++ b/src/featureflag/featureflag.controller.ts @@ -11,8 +11,8 @@ export class FeatureflagController { return await this.featureflagService.getFeatureFlag(params.ffname); } - @Post(':ffname') - async setFeatureFlag(@Param() params: { ffname: string}, @Body() ffState: { name: string, state: boolean }) { - return await this.featureflagService.setFeatureFlag(params.ffname, ffState.state ); + @Post() + async setFeatureFlag(@Body() ffState: { name: string, state: boolean }) { + return await this.featureflagService.setFeatureFlag(ffState.name, ffState.state ); } } diff --git a/src/featureflag/featureflag.service.ts b/src/featureflag/featureflag.service.ts index bda57f9..3ab3e5b 100644 --- a/src/featureflag/featureflag.service.ts +++ b/src/featureflag/featureflag.service.ts @@ -24,8 +24,6 @@ export class FeatureflagService { async setFeatureFlag(id: string, status: boolean) : Promise { this.logger.verbose(`Setting feature flag status for ${id} to ${status} `); const result = await this.sharedService.setConfig(`featureflag/${id}`, status.toString()); - console.log(result.value); - console.log(!!result.value); return { name: id, state: result.value !== 'false', diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts index 62a7508..fcea24e 100644 --- a/src/scheduler/scheduler.service.ts +++ b/src/scheduler/scheduler.service.ts @@ -4,6 +4,8 @@ import { StateService } from '../state/state.service'; import { QuizService } from '../quiz/quiz.service'; import { GiftsService } from '../gifts/gifts.service'; import { SharedService } from '../shared/shared.service'; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; @Injectable() export class SchedulerService { @@ -16,6 +18,7 @@ export class SchedulerService { private giftsService: GiftsService, private quizService: QuizService, private sharedService: SharedService, + private featureFlagService: FeatureflagService, ) {} @Cron('* * * * *') @@ -31,6 +34,9 @@ export class SchedulerService { async gameStatus() { const giftsLeft = await this.giftsService.getRemainingPrizeCount(); if (giftsLeft === 0) { + if(await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.EnableEndgamePoints)) { + this.logger.verbose(`Feature flag ${FeatureFlagsConsts.EnableEndgamePoints} is enabled`); + } const state = await this.stateService.setState('main', 'finish'); this.sharedService.sendSocketNotificationToAllClients( 'state_changed', -- 2.45.2 From 9e4f5e13c05bf4eae937e6dbb550df48c803df1c Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 15:18:43 +0400 Subject: [PATCH 10/32] TGD-31: fix all tests suites --- src/cards/cards.controller.spec.ts | 5 +++++ src/cards/cards.service.spec.ts | 14 +++++++++++- src/game/game.controller.spec.ts | 5 +++++ src/game/game.service.spec.ts | 21 +++++++++++++++++- src/gifts/gifts.controller.spec.ts | 5 +++++ src/gifts/gifts.service.spec.ts | 8 ++++++- src/guests/guests.controller.spec.ts | 5 +++++ src/guests/guests.service.spec.ts | 29 ++++++++++++++++++++++++- src/mocks/cards-service.mock.ts | 3 +++ src/mocks/client-proxy.mock.ts | 3 +++ src/mocks/commandbus.mock.ts | 3 +++ src/mocks/config-service.mock.ts | 2 +- src/mocks/eventbus.mock.ts | 3 +++ src/mocks/game-service.mock.ts | 3 +++ src/mocks/guests-service.mock.ts | 3 +++ src/mocks/httpservice.mock.ts | 3 +++ src/mocks/penalty-service.mock.ts | 3 +++ src/mocks/querybus.mock.ts | 3 +++ src/mocks/socket-gateway.mock.ts | 3 +++ src/penalty/penalty.controller.spec.ts | 5 +++++ src/penalty/penalty.service.spec.ts | 8 ++++++- src/quiz/quiz.controller.spec.ts | 7 ++++++ src/quiz/quiz.service.spec.ts | 23 ++++++++++++++++++-- src/scheduler/scheduler.service.spec.ts | 5 +++++ src/shared/shared.service.spec.ts | 11 +++++++++- src/shared/shared.service.ts | 1 - src/state/state.controller.spec.ts | 14 ++++++++++++ src/state/state.service.spec.ts | 11 +++++++++- src/voice/voice.service.spec.ts | 10 ++++++++- 29 files changed, 207 insertions(+), 12 deletions(-) create mode 100644 src/mocks/cards-service.mock.ts create mode 100644 src/mocks/client-proxy.mock.ts create mode 100644 src/mocks/commandbus.mock.ts create mode 100644 src/mocks/eventbus.mock.ts create mode 100644 src/mocks/game-service.mock.ts create mode 100644 src/mocks/guests-service.mock.ts create mode 100644 src/mocks/httpservice.mock.ts create mode 100644 src/mocks/penalty-service.mock.ts create mode 100644 src/mocks/querybus.mock.ts create mode 100644 src/mocks/socket-gateway.mock.ts diff --git a/src/cards/cards.controller.spec.ts b/src/cards/cards.controller.spec.ts index d8c7700..187a263 100644 --- a/src/cards/cards.controller.spec.ts +++ b/src/cards/cards.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CardsController } from './cards.controller'; +import {CardsService} from "./cards.service"; +import {CardsServiceMock} from "../mocks/cards-service.mock"; describe('CardsController', () => { let controller: CardsController; @@ -7,6 +9,9 @@ describe('CardsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [CardsController], + providers: [ + { provide: CardsService, useValue: CardsServiceMock }, + ] }).compile(); controller = module.get(CardsController); diff --git a/src/cards/cards.service.spec.ts b/src/cards/cards.service.spec.ts index 2e26f28..a183748 100644 --- a/src/cards/cards.service.spec.ts +++ b/src/cards/cards.service.spec.ts @@ -1,12 +1,24 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CardsService } from './cards.service'; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {getModelToken} from "@nestjs/mongoose"; +import {Card} from "../schemas/cards.schema"; +import {Model} from "mongoose"; +import {EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; describe('CardsService', () => { let service: CardsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [CardsService], + providers: [ + CardsService, + { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: getModelToken(Card.name), useValue: Model }, + { provide: EventBus, useValue: EventbusMock }, + ], }).compile(); service = module.get(CardsService); diff --git a/src/game/game.controller.spec.ts b/src/game/game.controller.spec.ts index f70c47c..e9b857e 100644 --- a/src/game/game.controller.spec.ts +++ b/src/game/game.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GameController } from './game.controller'; +import {GameService} from "./game.service"; +import {GameServiceMock} from "../mocks/game-service.mock"; describe('GameController', () => { let controller: GameController; @@ -7,6 +9,9 @@ describe('GameController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [GameController], + providers: [ + { provide: GameService, useValue: GameServiceMock }, + ] }).compile(); controller = module.get(GameController); diff --git a/src/game/game.service.spec.ts b/src/game/game.service.spec.ts index f4a1db7..9509a9f 100644 --- a/src/game/game.service.spec.ts +++ b/src/game/game.service.spec.ts @@ -1,12 +1,31 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GameService } from './game.service'; +import {CommandBus, EventBus, QueryBus} from "@nestjs/cqrs"; +import {CommandbusMock} from "../mocks/commandbus.mock"; +import {EventbusMock} from "../mocks/eventbus.mock"; +import {getModelToken} from "@nestjs/mongoose"; +import {GameQueue} from "../schemas/game-queue.schema"; +import {Model} from "mongoose"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; +import {QueryBusMock} from "../mocks/querybus.mock"; describe('GameService', () => { let service: GameService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [GameService], + providers: [ + GameService, + { provide: CommandBus, useValue: CommandbusMock }, + { provide: EventBus, useValue: EventbusMock }, + { provide: getModelToken(GameQueue.name), useValue: Model }, + { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: SharedService, useValue: SharedServiceMock }, + { provide: QueryBus, useValue: QueryBusMock }, + ], }).compile(); service = module.get(GameService); diff --git a/src/gifts/gifts.controller.spec.ts b/src/gifts/gifts.controller.spec.ts index 3bdf5c2..d5cef58 100644 --- a/src/gifts/gifts.controller.spec.ts +++ b/src/gifts/gifts.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GiftsController } from './gifts.controller'; +import {GiftsService} from "./gifts.service"; +import {GiftServiceMock} from "../mocks/gift-service.mock"; describe('GiftsController', () => { let controller: GiftsController; @@ -7,6 +9,9 @@ describe('GiftsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [GiftsController], + providers: [ + { provide: GiftsService, useValue: GiftServiceMock }, + ] }).compile(); controller = module.get(GiftsController); diff --git a/src/gifts/gifts.service.spec.ts b/src/gifts/gifts.service.spec.ts index 6fabaa2..ab2b5fa 100644 --- a/src/gifts/gifts.service.spec.ts +++ b/src/gifts/gifts.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GiftsService } from './gifts.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Prize} from "../schemas/prize.schema"; +import {Model} from "mongoose"; describe('GiftsService', () => { let service: GiftsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [GiftsService], + providers: [ + GiftsService, + { provide: getModelToken(Prize.name), useValue: Model }, + ], }).compile(); service = module.get(GiftsService); diff --git a/src/guests/guests.controller.spec.ts b/src/guests/guests.controller.spec.ts index 157ce15..c59e9e7 100644 --- a/src/guests/guests.controller.spec.ts +++ b/src/guests/guests.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GuestsController } from './guests.controller'; +import {GuestsService} from "./guests.service"; +import {GuestsServiceMock} from "../mocks/guests-service.mock"; describe('GuestsController', () => { let controller: GuestsController; @@ -7,6 +9,9 @@ describe('GuestsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [GuestsController], + providers: [ + { provide: GuestsService, useValue: GuestsServiceMock }, + ] }).compile(); controller = module.get(GuestsController); diff --git a/src/guests/guests.service.spec.ts b/src/guests/guests.service.spec.ts index 7919215..214a84f 100644 --- a/src/guests/guests.service.spec.ts +++ b/src/guests/guests.service.spec.ts @@ -1,12 +1,39 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GuestsService } from './guests.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Guest} from "../schemas/guest.schema"; +import {Model} from "mongoose"; +import {Config} from "../schemas/config.schema"; +import {ClientProxy} from "@nestjs/microservices"; +import {ClientProxyMock} from "../mocks/client-proxy.mock"; +import {CommandBus, EventBus, QueryBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; +import {CommandbusMock} from "../mocks/commandbus.mock"; +import {CardsServiceMock} from "../mocks/cards-service.mock"; +import {CardsService} from "../cards/cards.service"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {QueryBusMock} from "../mocks/querybus.mock"; +import {VoiceService} from "../voice/voice.service"; +import {VoiceServiceMock} from "../mocks/voice-service.mock"; describe('GuestsService', () => { let service: GuestsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [GuestsService], + providers: [ + GuestsService, + { provide: getModelToken(Guest.name), useValue: Model }, + { provide: getModelToken(Config.name), useValue: Model }, + { provide: 'Telegram', useValue: ClientProxyMock }, + { provide: EventBus, useValue: EventbusMock }, + { provide: CommandBus, useValue: CommandbusMock }, + { provide: CardsService, useValue: CardsServiceMock }, + { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: QueryBus, useValue: QueryBusMock }, + { provide: VoiceService, useValue: VoiceServiceMock }, + ], }).compile(); service = module.get(GuestsService); diff --git a/src/mocks/cards-service.mock.ts b/src/mocks/cards-service.mock.ts new file mode 100644 index 0000000..00e00b0 --- /dev/null +++ b/src/mocks/cards-service.mock.ts @@ -0,0 +1,3 @@ +export const CardsServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/client-proxy.mock.ts b/src/mocks/client-proxy.mock.ts new file mode 100644 index 0000000..3672ec4 --- /dev/null +++ b/src/mocks/client-proxy.mock.ts @@ -0,0 +1,3 @@ +export const ClientProxyMock = { + +} \ No newline at end of file diff --git a/src/mocks/commandbus.mock.ts b/src/mocks/commandbus.mock.ts new file mode 100644 index 0000000..60ad82e --- /dev/null +++ b/src/mocks/commandbus.mock.ts @@ -0,0 +1,3 @@ +export const CommandbusMock = { + +} \ No newline at end of file diff --git a/src/mocks/config-service.mock.ts b/src/mocks/config-service.mock.ts index d8019f2..4c85c87 100644 --- a/src/mocks/config-service.mock.ts +++ b/src/mocks/config-service.mock.ts @@ -1,3 +1,3 @@ export const ConfigServiceMock = { - + get: jest.fn(), } \ No newline at end of file diff --git a/src/mocks/eventbus.mock.ts b/src/mocks/eventbus.mock.ts new file mode 100644 index 0000000..162baf2 --- /dev/null +++ b/src/mocks/eventbus.mock.ts @@ -0,0 +1,3 @@ +export const EventbusMock = { + +} \ No newline at end of file diff --git a/src/mocks/game-service.mock.ts b/src/mocks/game-service.mock.ts new file mode 100644 index 0000000..ecf04ef --- /dev/null +++ b/src/mocks/game-service.mock.ts @@ -0,0 +1,3 @@ +export const GameServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/guests-service.mock.ts b/src/mocks/guests-service.mock.ts new file mode 100644 index 0000000..f70f872 --- /dev/null +++ b/src/mocks/guests-service.mock.ts @@ -0,0 +1,3 @@ +export const GuestsServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/httpservice.mock.ts b/src/mocks/httpservice.mock.ts new file mode 100644 index 0000000..5798e0d --- /dev/null +++ b/src/mocks/httpservice.mock.ts @@ -0,0 +1,3 @@ +export const HttpServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/penalty-service.mock.ts b/src/mocks/penalty-service.mock.ts new file mode 100644 index 0000000..52a71d8 --- /dev/null +++ b/src/mocks/penalty-service.mock.ts @@ -0,0 +1,3 @@ +export const PenaltyServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/querybus.mock.ts b/src/mocks/querybus.mock.ts new file mode 100644 index 0000000..e4df30c --- /dev/null +++ b/src/mocks/querybus.mock.ts @@ -0,0 +1,3 @@ +export const QueryBusMock = { + +} \ No newline at end of file diff --git a/src/mocks/socket-gateway.mock.ts b/src/mocks/socket-gateway.mock.ts new file mode 100644 index 0000000..ea7d28d --- /dev/null +++ b/src/mocks/socket-gateway.mock.ts @@ -0,0 +1,3 @@ +export const SocketGatewayMock = { + +} \ No newline at end of file diff --git a/src/penalty/penalty.controller.spec.ts b/src/penalty/penalty.controller.spec.ts index 24f00d8..9e00e9a 100644 --- a/src/penalty/penalty.controller.spec.ts +++ b/src/penalty/penalty.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PenaltyController } from './penalty.controller'; +import {PenaltyService} from "./penalty.service"; +import {PenaltyServiceMock} from "../mocks/penalty-service.mock"; describe('PenaltyController', () => { let controller: PenaltyController; @@ -7,6 +9,9 @@ describe('PenaltyController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [PenaltyController], + providers: [ + { provide: PenaltyService, useValue: PenaltyServiceMock }, + ] }).compile(); controller = module.get(PenaltyController); diff --git a/src/penalty/penalty.service.spec.ts b/src/penalty/penalty.service.spec.ts index c37fee8..dd3b657 100644 --- a/src/penalty/penalty.service.spec.ts +++ b/src/penalty/penalty.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PenaltyService } from './penalty.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Penalty} from "../schemas/penalty.schema"; +import {Model} from "mongoose"; describe('PenaltyService', () => { let service: PenaltyService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [PenaltyService], + providers: [ + PenaltyService, + { provide: getModelToken(Penalty.name), useValue: Model }, + ], }).compile(); service = module.get(PenaltyService); diff --git a/src/quiz/quiz.controller.spec.ts b/src/quiz/quiz.controller.spec.ts index 9d9adb2..fcf5076 100644 --- a/src/quiz/quiz.controller.spec.ts +++ b/src/quiz/quiz.controller.spec.ts @@ -1,15 +1,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { QuizController } from './quiz.controller'; +import {QuizService} from "./quiz.service"; +import {QuizServiceMock} from "../mocks/quiz-service.mock"; describe('QuizController', () => { let controller: QuizController; + let quizService: QuizService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [QuizController], + providers: [ + { provide: QuizService, useValue: QuizServiceMock }, + ] }).compile(); controller = module.get(QuizController); + quizService = module.get(QuizService); }); it('should be defined', () => { diff --git a/src/quiz/quiz.service.spec.ts b/src/quiz/quiz.service.spec.ts index 03b305f..85928ac 100644 --- a/src/quiz/quiz.service.spec.ts +++ b/src/quiz/quiz.service.spec.ts @@ -1,15 +1,34 @@ import { Test, TestingModule } from '@nestjs/testing'; import { QuizService } from './quiz.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Question} from "../schemas/question.schema"; +import {Model} from "mongoose"; +import {QuestionStorage} from "../schemas/question-storage.schema"; +import {GuestsService} from "../guests/guests.service"; +import {GuestsServiceMock} from "../mocks/guests-service.mock"; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; +import {CommandBus, EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; +import {CommandbusMock} from "../mocks/commandbus.mock"; describe('QuizService', () => { let service: QuizService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [QuizService], + providers: [ + QuizService, + { provide: getModelToken(Question.name), useValue: Model }, + { provide: getModelToken(QuestionStorage.name), useValue: Model}, + { provide: GuestsService, useValue: GuestsServiceMock }, + { provide: SharedService, useValue: SharedServiceMock }, + { provide: EventBus, useValue: EventbusMock }, + { provide: CommandBus, useValue: CommandbusMock } + ], }).compile(); - service = module.get(QuizService); + service = await module.resolve(QuizService); }); it('should be defined', () => { diff --git a/src/scheduler/scheduler.service.spec.ts b/src/scheduler/scheduler.service.spec.ts index e089e01..3a38d6e 100644 --- a/src/scheduler/scheduler.service.spec.ts +++ b/src/scheduler/scheduler.service.spec.ts @@ -8,6 +8,8 @@ import {QuizService} from "../quiz/quiz.service"; import {QuizServiceMock} from "../mocks/quiz-service.mock"; import {SharedService} from "../shared/shared.service"; import {SharedServiceMock} from "../mocks/shared-service.mock"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; describe('SchedulerService', () => { @@ -15,6 +17,7 @@ describe('SchedulerService', () => { let giftService: GiftsService; let sharedService: SharedService; let stateService: StateService; + let featureFlagService: FeatureflagService; beforeEach(async () => { jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ @@ -23,6 +26,7 @@ describe('SchedulerService', () => { { provide: StateService, useValue: StateServiceMock }, { provide: QuizService, useValue: QuizServiceMock }, { provide: SharedService, useValue: SharedServiceMock }, + { provide: FeatureflagService, useValue: FeatureflagServiceMock } ], }).compile(); @@ -30,6 +34,7 @@ describe('SchedulerService', () => { giftService = module.get(GiftsService); sharedService = module.get(SharedService); stateService = module.get(StateService); + featureFlagService = module.get(FeatureflagService); }); it('should be defined', () => { diff --git a/src/shared/shared.service.spec.ts b/src/shared/shared.service.spec.ts index 204dcb9..d87f25d 100644 --- a/src/shared/shared.service.spec.ts +++ b/src/shared/shared.service.spec.ts @@ -1,12 +1,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SharedService } from './shared.service'; +import {SocketGateway} from "../socket/socket.gateway"; +import {SocketGatewayMock} from "../mocks/socket-gateway.mock"; +import {getModelToken} from "@nestjs/mongoose"; +import {Config} from "../schemas/config.schema"; +import {Model} from "mongoose"; describe('SharedService', () => { let service: SharedService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [SharedService], + providers: [ + SharedService, + { provide: SocketGateway, useValue: SocketGatewayMock }, + { provide: getModelToken(Config.name), useValue: Model }, + ], }).compile(); service = module.get(SharedService); diff --git a/src/shared/shared.service.ts b/src/shared/shared.service.ts index 6ba9c3b..7cc7793 100644 --- a/src/shared/shared.service.ts +++ b/src/shared/shared.service.ts @@ -9,7 +9,6 @@ export class SharedService { private logger = new Logger(SharedService.name); constructor( private socketGateway: SocketGateway, - private eventBus: EventBus, @InjectModel(Config.name) private configModel: Model, ) { diff --git a/src/state/state.controller.spec.ts b/src/state/state.controller.spec.ts index e108a36..eb944e9 100644 --- a/src/state/state.controller.spec.ts +++ b/src/state/state.controller.spec.ts @@ -1,5 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StateController } from './state.controller'; +import {State} from "../schemas/state.schema"; +import {StateService} from "./state.service"; +import {StateServiceMock} from "../mocks/state-service.mock"; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; +import {EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; +import {ClientProxyMock} from "../mocks/client-proxy.mock"; describe('StateController', () => { let controller: StateController; @@ -7,6 +15,12 @@ describe('StateController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [StateController], + providers: [ + { provide: StateService, useValue: StateServiceMock }, + { provide: SharedService, useValue: SharedServiceMock }, + { provide: EventBus, useValue: EventbusMock }, + { provide: 'Telegram', useValue: ClientProxyMock }, + ] }).compile(); controller = module.get(StateController); diff --git a/src/state/state.service.spec.ts b/src/state/state.service.spec.ts index f2a1fd2..0bd4ac8 100644 --- a/src/state/state.service.spec.ts +++ b/src/state/state.service.spec.ts @@ -1,12 +1,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StateService } from './state.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {State} from "../schemas/state.schema"; +import {Model} from "mongoose"; +import {EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; describe('StateService', () => { let service: StateService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [StateService], + providers: [ + StateService, + { provide: getModelToken(State.name), useValue: Model }, + { provide: EventBus, useValue: EventbusMock }, + ], }).compile(); service = module.get(StateService); diff --git a/src/voice/voice.service.spec.ts b/src/voice/voice.service.spec.ts index 76b1a6b..c9d5f1b 100644 --- a/src/voice/voice.service.spec.ts +++ b/src/voice/voice.service.spec.ts @@ -1,12 +1,20 @@ import { Test, TestingModule } from '@nestjs/testing'; import { VoiceService } from './voice.service'; +import {HttpService} from "@nestjs/axios"; +import {HttpServiceMock} from "../mocks/httpservice.mock"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; describe('VoiceService', () => { let service: VoiceService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [VoiceService], + providers: [ + VoiceService, + { provide: HttpService, useValue: HttpServiceMock }, + { provide: ConfigService, useValue: ConfigServiceMock } + ], }).compile(); service = module.get(VoiceService); -- 2.45.2 From 59f32b94d9f7918c4f41841cfdbc5d2c76114027 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 18:16:33 +0400 Subject: [PATCH 11/32] featureflag for DisableVoice & DontMarkQuestsion --- src/Consts/FeatureFlags.consts.ts | 2 ++ src/app.module.ts | 2 +- src/featureflag/featureflag.service.ts | 15 +++++++++--- src/quiz/quiz.service.ts | 10 ++++++-- src/shared/events.consts.ts | 1 + src/shared/shared.module.ts | 5 ++-- src/shared/shared.service.ts | 3 +++ src/voice/voice.controller.ts | 32 ++++++++++++++++++-------- src/voice/voice.service.ts | 3 ++- 9 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/Consts/FeatureFlags.consts.ts b/src/Consts/FeatureFlags.consts.ts index 495e440..31b1dda 100644 --- a/src/Consts/FeatureFlags.consts.ts +++ b/src/Consts/FeatureFlags.consts.ts @@ -1,3 +1,5 @@ export class FeatureFlagsConsts { static EnableEndgamePoints = 'EnableEndgamePoints'; + static DontMarkQuestionsAsCompleted = 'DontMarkQuestionsAsCompleted'; + static DisableVoice = 'DisableVoice'; } \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 5b0d4e9..6422730 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -45,6 +45,6 @@ import { FeatureflagService } from './featureflag/featureflag.service'; ], controllers: [AppController, FeatureflagController], providers: [AppService, SocketGateway, SchedulerService, FeatureflagService], - exports: [AppService, SocketGateway], + exports: [AppService, SocketGateway, FeatureflagService], }) export class AppModule {} diff --git a/src/featureflag/featureflag.service.ts b/src/featureflag/featureflag.service.ts index 3ab3e5b..a968439 100644 --- a/src/featureflag/featureflag.service.ts +++ b/src/featureflag/featureflag.service.ts @@ -1,5 +1,6 @@ import {Injectable, Logger} from '@nestjs/common'; import {SharedService} from "../shared/shared.service"; +import {SocketEvents} from "../shared/events.consts"; export interface IFeatureFlagStatus { name: string; @@ -13,17 +14,25 @@ export class FeatureflagService { } async getFeatureFlag(id: string): Promise { - this.logger.verbose(`Getting feature flag status for ${id}`); - const state = await this.sharedService.getConfig(`featureflag/${id}`); + this.logger.verbose(`[getFeatureFlag] Getting feature flag status for ${id}`); + const configRecord = await this.sharedService.getConfig(`featureflag/${id}`); + let ffState; + if(!configRecord) { + ffState = false; + } else { + ffState = configRecord.value !== 'false' + } + this.logger.verbose(`[getFeatureFlag] Feature flag status for ${id} is ${ffState}`); return { name: id, - state: state.value !== 'false', + state: ffState } } async setFeatureFlag(id: string, status: boolean) : Promise { this.logger.verbose(`Setting feature flag status for ${id} to ${status} `); const result = await this.sharedService.setConfig(`featureflag/${id}`, status.toString()); + this.sharedService.sendSocketNotificationToAllClients(SocketEvents.FEATURE_FLAG_CHANGED, {}); return { name: id, state: result.value !== 'false', diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 42fb91a..208221f 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -16,6 +16,8 @@ import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item. import {GameQueueTypes} from "../schemas/game-queue.schema"; import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { @@ -30,6 +32,7 @@ export class QuizService { private sharedService: SharedService, private eventBus: EventBus, private commandBus: CommandBus, + private featureFlagService: FeatureflagService, ) { } @@ -54,7 +57,6 @@ export class QuizService { async validateAnswer(answer: string, id: number) { this.logger.verbose(`enter validate answer ${answer} ${id}`); const question = await this.get(); - //question.answered = true; await question.save(); const regexp = new RegExp( Object.keys(this.answerNumbers) @@ -82,7 +84,6 @@ export class QuizService { await question.save(); this.logger.verbose("question saved with user details") if (question.valid === filtered) { - //question.answered = true; question.answeredBy = id; this.logger.verbose(`extra ${question.note}`); this.eventBus.publish( @@ -126,6 +127,11 @@ export class QuizService { private async calculateScore() { const question = await this.get(); + if(!await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DontMarkQuestionsAsCompleted)) { + this.logger.verbose(`[proceedWithGame]: DontMarkQuestionsAsCompleted disabled, marking as complete`); + question.answered = true; + await question.save(); + } this.logger.verbose(`[calculateScore] enter `); const playerAnswers = question.userAnswers.map((answer) => { return { diff --git a/src/shared/events.consts.ts b/src/shared/events.consts.ts index b07930e..4806418 100644 --- a/src/shared/events.consts.ts +++ b/src/shared/events.consts.ts @@ -12,4 +12,5 @@ export enum SocketEvents { GAME_PAUSED = 'game_paused', GAME_RESUMED = 'game_resumed', NOTIFICATION = 'notification', + FEATURE_FLAG_CHANGED = 'feature_flag_changed', } diff --git a/src/shared/shared.module.ts b/src/shared/shared.module.ts index 4367356..7149884 100644 --- a/src/shared/shared.module.ts +++ b/src/shared/shared.module.ts @@ -8,6 +8,7 @@ import { ClientProxyFactory, Transport } from '@nestjs/microservices'; import * as process from "process"; import {ConfigModule} from "@nestjs/config"; import {CqrsModule} from "@nestjs/cqrs"; +import {FeatureflagService} from "../featureflag/featureflag.service"; @Global() @Module({ imports: [ @@ -17,7 +18,7 @@ import {CqrsModule} from "@nestjs/cqrs"; GameModule, MongooseModule.forFeature([{ name: Config.name, schema: ConfigSchema }]), ], - providers: [SharedService, { + providers: [SharedService,FeatureflagService, { provide: 'Telegram', useFactory: () => ClientProxyFactory.create({ @@ -31,7 +32,7 @@ import {CqrsModule} from "@nestjs/cqrs"; }, }), }], - exports: [SharedService, 'Telegram'], + exports: [SharedService, 'Telegram',FeatureflagService], }) export class SharedModule { constructor() { diff --git a/src/shared/shared.service.ts b/src/shared/shared.service.ts index 7cc7793..aec82bf 100644 --- a/src/shared/shared.service.ts +++ b/src/shared/shared.service.ts @@ -20,6 +20,9 @@ export class SharedService { key, }) .exec(); + if(!res) { + return null; + } return { key: res.key, value: res.value, diff --git a/src/voice/voice.controller.ts b/src/voice/voice.controller.ts index c28648a..c1fe6c7 100644 --- a/src/voice/voice.controller.ts +++ b/src/voice/voice.controller.ts @@ -1,24 +1,39 @@ -import { Controller, Get, Header, NotFoundException, Query, StreamableFile } from '@nestjs/common'; +import {Controller, Get, Header, Logger, NotFoundException, Query, StreamableFile} from '@nestjs/common'; import { VoiceService } from './voice.service'; import { TtsRequestDto, TtsRequestWithVars } from './models/TtsRequestDto'; import { invalidPrefixDict } from './dicts/invalid-prefix.dict'; import { validPrefixDict } from './dicts/valid-prefix.dict'; import * as translit from 'latin-to-cyrillic'; import {ConfigService} from "@nestjs/config"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; @Controller('voice') export class VoiceController { - constructor(private voiceService: VoiceService, private configService: ConfigService) {} + private voiceEnabled = false; + private logger = new Logger(VoiceController.name) + constructor(private voiceService: VoiceService, private configService: ConfigService,private featureFlagService: FeatureflagService) { + setInterval(() => { + this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => this.voiceEnabled = !r.state); + }, 30000); + this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => { + this.voiceEnabled = !r.state; + console.log(`Voice enabled: ${this.voiceEnabled}`); + }); + + } @Get('ssml') @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') async textToSpeechSSML(@Query() dto: TtsRequestDto) { - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { - //return new StreamableFile(await this.voiceService.textToFile(dto, true)); + this.logger.verbose(`[textToSpeechSSML] enter, FF state is: ${this.voiceEnabled}`); + if (this.voiceEnabled) { + return new StreamableFile(await this.voiceService.textToFile(dto, true)); } else { return new NotFoundException('Voice disabled'); } + } @Get('tts') @@ -26,8 +41,8 @@ export class VoiceController { @Header('content-disposition', 'inline') async getText(@Query() dto: TtsRequestDto) { dto.text = translit(dto.text); - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { - //return new StreamableFile(await this.voiceService.textToFile(dto)); + if (this.voiceEnabled) { + return new StreamableFile(await this.voiceService.textToFile(dto)); } else { return new NotFoundException('Voice disabled'); } @@ -36,8 +51,7 @@ export class VoiceController { @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') async announceValid(@Query() dto: TtsRequestWithVars) { - console.log(this.configService.get('ENABLE_VOICE')); - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { + if (this.voiceEnabled) { const vars = JSON.parse(dto.vars); dto.text = this.voiceService.buildTemplate(dto, vars, validPrefixDict); return new StreamableFile(await this.voiceService.textToFile(dto)); @@ -50,7 +64,7 @@ export class VoiceController { @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') async announceInvalid(@Query() dto: TtsRequestWithVars) { - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { + if (this.voiceEnabled) { const vars = JSON.parse(dto.vars); dto.text = this.voiceService.buildTemplate(dto, vars, invalidPrefixDict); return new StreamableFile(await this.voiceService.textToFile(dto)); diff --git a/src/voice/voice.service.ts b/src/voice/voice.service.ts index 78cc2dd..b8d05cb 100644 --- a/src/voice/voice.service.ts +++ b/src/voice/voice.service.ts @@ -7,12 +7,13 @@ import { AxiosRequestConfig } from 'axios'; import * as translit from 'latin-to-cyrillic'; import { TGD_Config } from 'app.config'; import {ConfigService} from "@nestjs/config"; +import {FeatureflagService} from "../featureflag/featureflag.service"; @Injectable() export class VoiceService { private apiUrl = 'https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize'; private apiKey: string; private readonly logger = new Logger(VoiceService.name); - constructor(private httpService: HttpService, private configService: ConfigService) { + constructor(private httpService: HttpService, private configService: ConfigService, private featureFlagService: FeatureflagService) { this.apiKey = this.configService.get("VOICE_APIKEY"); } -- 2.45.2 From 41388c035b3c0d0b3bc49199681ea0b6fc28c7e4 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 18:24:57 +0400 Subject: [PATCH 12/32] fix tests --- package-lock.json | 16 ++++++++++++++++ package.json | 4 ++++ src/mocks/featureflag-service.mock.ts | 4 ++-- src/quiz/quiz.service.spec.ts | 5 ++++- src/voice/voice.controller.spec.ts | 5 +++++ src/voice/voice.service.ts | 2 +- 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b03798a..76127da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "class-transformer": "^0.5.1", "cyrillic-to-translit-js": "^3.2.1", "dotenv": "^16.3.1", + "husky": "^9.1.6", "latin-to-cyrillic": "^1.0.1", "mongodb": "^6.2.0", "mongoose": "^8.0.0", @@ -5552,6 +5553,21 @@ "ms": "^2.0.0" } }, + "node_modules/husky": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 2644a62..586e003 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "class-transformer": "^0.5.1", "cyrillic-to-translit-js": "^3.2.1", "dotenv": "^16.3.1", + "husky": "^9.1.6", "latin-to-cyrillic": "^1.0.1", "mongodb": "^6.2.0", "mongoose": "^8.0.0", @@ -89,5 +90,8 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" + }, + "hooks": { + "pre-commit": "npm run test" } } diff --git a/src/mocks/featureflag-service.mock.ts b/src/mocks/featureflag-service.mock.ts index f905e58..2bb8a11 100644 --- a/src/mocks/featureflag-service.mock.ts +++ b/src/mocks/featureflag-service.mock.ts @@ -1,4 +1,4 @@ export const FeatureflagServiceMock = { - getFeatureFlag: jest.fn(), - setFeatureFlag: jest.fn(), + getFeatureFlag: jest.fn(() => Promise.resolve(false)), + setFeatureFlag: jest.fn(() => Promise.resolve(false)) } \ No newline at end of file diff --git a/src/quiz/quiz.service.spec.ts b/src/quiz/quiz.service.spec.ts index 85928ac..bc3e406 100644 --- a/src/quiz/quiz.service.spec.ts +++ b/src/quiz/quiz.service.spec.ts @@ -11,6 +11,8 @@ import {SharedServiceMock} from "../mocks/shared-service.mock"; import {CommandBus, EventBus} from "@nestjs/cqrs"; import {EventbusMock} from "../mocks/eventbus.mock"; import {CommandbusMock} from "../mocks/commandbus.mock"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; describe('QuizService', () => { let service: QuizService; @@ -24,7 +26,8 @@ describe('QuizService', () => { { provide: GuestsService, useValue: GuestsServiceMock }, { provide: SharedService, useValue: SharedServiceMock }, { provide: EventBus, useValue: EventbusMock }, - { provide: CommandBus, useValue: CommandbusMock } + { provide: CommandBus, useValue: CommandbusMock }, + { provide: FeatureflagService, useValue: FeatureflagServiceMock } ], }).compile(); diff --git a/src/voice/voice.controller.spec.ts b/src/voice/voice.controller.spec.ts index 735777b..ba35ee9 100644 --- a/src/voice/voice.controller.spec.ts +++ b/src/voice/voice.controller.spec.ts @@ -5,11 +5,14 @@ import {VoiceServiceMock} from "../mocks/voice-service.mock"; import {Config} from "../schemas/config.schema"; import {ConfigService} from "@nestjs/config"; import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; describe('VoiceController', () => { let controller: VoiceController; let voiceService: VoiceService; let configService: ConfigService; + let featureflagService: FeatureflagService; beforeEach(async () => { jest.clearAllMocks(); @@ -20,12 +23,14 @@ describe('VoiceController', () => { providers: [ { provide: VoiceService, useValue: VoiceServiceMock }, { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: FeatureflagService, useValue: FeatureflagServiceMock }, ] }).compile(); controller = module.get(VoiceController); voiceService = module.get(VoiceService); configService = module.get(ConfigService); + }); it('should be defined', () => { diff --git a/src/voice/voice.service.ts b/src/voice/voice.service.ts index b8d05cb..45098db 100644 --- a/src/voice/voice.service.ts +++ b/src/voice/voice.service.ts @@ -13,7 +13,7 @@ export class VoiceService { private apiUrl = 'https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize'; private apiKey: string; private readonly logger = new Logger(VoiceService.name); - constructor(private httpService: HttpService, private configService: ConfigService, private featureFlagService: FeatureflagService) { + constructor(private httpService: HttpService, private configService: ConfigService) { this.apiKey = this.configService.get("VOICE_APIKEY"); } -- 2.45.2 From 5b3bc5d3b8758ba1ee0620058514b42f252077ab Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 18:25:59 +0400 Subject: [PATCH 13/32] add husky --- .husky/pre-commit | 1 + package.json | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..72c4429 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/package.json b/package.json index 586e003..6a36d3b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "prepare": "husky" }, "dependencies": { "@nestjs/axios": "3.0.1", @@ -90,8 +91,5 @@ ], "coverageDirectory": "../coverage", "testEnvironment": "node" - }, - "hooks": { - "pre-commit": "npm run test" } } -- 2.45.2 From ede897c8e2f992115c76c3f03b5e820b7ccbebe5 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 11 Nov 2024 18:31:42 +0400 Subject: [PATCH 14/32] voice ff check interval --- src/voice/voice.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/voice/voice.controller.ts b/src/voice/voice.controller.ts index c1fe6c7..7382ab5 100644 --- a/src/voice/voice.controller.ts +++ b/src/voice/voice.controller.ts @@ -11,10 +11,10 @@ import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; export class VoiceController { private voiceEnabled = false; private logger = new Logger(VoiceController.name) - constructor(private voiceService: VoiceService, private configService: ConfigService,private featureFlagService: FeatureflagService) { + constructor(private voiceService: VoiceService,private featureFlagService: FeatureflagService) { setInterval(() => { this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => this.voiceEnabled = !r.state); - }, 30000); + }, 10000); this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => { this.voiceEnabled = !r.state; console.log(`Voice enabled: ${this.voiceEnabled}`); -- 2.45.2 From 457554e95240af17cd54c2af20b8dee4cfb3b618 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Tue, 12 Nov 2024 02:23:12 +0400 Subject: [PATCH 15/32] versus implementation --- src/game/game.controller.ts | 9 +++- src/game/game.service.spec.ts | 4 ++ src/game/game.service.ts | 22 ++++++++++ src/guests/guest.types.d.ts | 5 +++ src/guests/guests.service.ts | 12 +++++- src/quiz/quiz.controller.ts | 2 +- src/quiz/quiz.service.ts | 3 +- src/quiz/quiz.types.d.ts | 11 +++++ src/scheduler/scheduler.service.spec.ts | 8 +++- src/scheduler/scheduler.service.ts | 55 ++++++++++++++++++------- src/schemas/game-queue.schema.ts | 1 + src/shared/events.consts.ts | 1 + src/voice/dicts/endgame.dict.ts | 6 +++ src/voice/voice.controller.ts | 3 -- src/voice/voice.service.ts | 1 - 15 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 src/guests/guest.types.d.ts create mode 100644 src/quiz/quiz.types.d.ts create mode 100644 src/voice/dicts/endgame.dict.ts diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index ec4ed4c..5726cf9 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -1,8 +1,9 @@ -import { Controller, Get, Param, Post } from '@nestjs/common'; +import { Controller, Get, Logger, Param, Post } from '@nestjs/common'; import { GameService } from './game.service'; @Controller('game') export class GameController { + private readonly logger = new Logger(GameController.name); constructor(private gameService: GameService) { } @@ -30,4 +31,10 @@ export class GameController { async playExtraCards() { return this.gameService.playExtraCards(); } + + @Post('simulate-versus') + async SimulateVersus() { + this.logger.verbose('[SimulateVersus] enter'); + return this.gameService.simulateVersus(); + } } diff --git a/src/game/game.service.spec.ts b/src/game/game.service.spec.ts index 9509a9f..bb06bf1 100644 --- a/src/game/game.service.spec.ts +++ b/src/game/game.service.spec.ts @@ -11,6 +11,9 @@ import {ConfigServiceMock} from "../mocks/config-service.mock"; import {SharedService} from "../shared/shared.service"; import {SharedServiceMock} from "../mocks/shared-service.mock"; import {QueryBusMock} from "../mocks/querybus.mock"; +import {Guest} from "../schemas/guest.schema"; +import {GuestsService} from "../guests/guests.service"; +import {GuestsServiceMock} from "../mocks/guests-service.mock"; describe('GameService', () => { let service: GameService; @@ -25,6 +28,7 @@ describe('GameService', () => { { provide: ConfigService, useValue: ConfigServiceMock }, { provide: SharedService, useValue: SharedServiceMock }, { provide: QueryBus, useValue: QueryBusMock }, + { provide: GuestsService, useValue: GuestsServiceMock } ], }).compile(); diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 75c49ca..3a367bd 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -13,6 +13,7 @@ import { SharedService } from '../shared/shared.service'; import { SocketEvents } from '../shared/events.consts'; import {ConfigService} from "@nestjs/config"; import {gameCards} from "./entities/cards.entities"; +import {GuestsService} from "../guests/guests.service"; @Injectable() export class GameService implements OnApplicationBootstrap{ @@ -26,6 +27,7 @@ export class GameService implements OnApplicationBootstrap{ private sharedService: SharedService, private commandBus: CommandBus, private queryBus: QueryBus, + private guestService: GuestsService, ) { } @@ -113,4 +115,24 @@ export class GameService implements OnApplicationBootstrap{ cardInstance.setupHandlers(this.eventBus, this.commandBus, this.queryBus); }) } + + async simulateVersus() { + const guests = (await this.guestService.findAll()).slice(0,2).map((guest) => { + return { + id: guest.telegramId, + name: guest.name, + } + }); + if(guests.length < 2) { + throw new Error("Can't simulate, in db less than 2 players") + } + await this.beginVersus(guests[0].id, guests[1].id); + } + + async beginVersus(player1: number, player2: number) { + this.sharedService.sendSocketNotificationToAllClients( + SocketEvents.BEGIN_VERSUS, + { player1, player2 } + ) + } } diff --git a/src/guests/guest.types.d.ts b/src/guests/guest.types.d.ts new file mode 100644 index 0000000..894e19e --- /dev/null +++ b/src/guests/guest.types.d.ts @@ -0,0 +1,5 @@ +export interface GuestNamesInCases { + SubjectiveCase: string; + AccusativeCase: string; + GenitiveCase: string; +} \ No newline at end of file diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index 650f83a..dfe0cc0 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -1,7 +1,7 @@ import {Inject, Injectable, Logger} from '@nestjs/common'; import {InjectModel} from '@nestjs/mongoose'; import {Guest, GuestDocument} from '../schemas/guest.schema'; -import {Model} from 'mongoose'; +import {Document, Model} from 'mongoose'; import {CreateGuestDto} from './dto/create-guest.dto'; import {QuestionDto} from '../quiz/dto/question.dto'; import {Messages} from '../messaging/tg.text'; @@ -27,6 +27,7 @@ import {DebuffsConsts} from "../game/entities/debuffs.consts"; import {VoiceService} from "../voice/voice.service"; import {screpaDictManyInvalidAnswersDict} from "../voice/dicts/screpa-dict-many-invalid-answers.dict"; import {GuestPropertiesConsts} from "../schemas/properties.consts"; +import {GuestNamesInCases} from "./guest.types"; @Injectable() export class GuestsService { @@ -312,6 +313,15 @@ export class GuestsService { await this.guestModel.updateMany({}, { prizeChance: 0 }); } + async getGuestNameInCases(telegramId: number): Promise { + const guest = await this.findById(telegramId); + return { + SubjectiveCase: guest.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase)), + AccusativeCase: guest.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameAccusativeCase)), + GenitiveCase: guest.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameGenitiveCase)) + } + } + async incrementInvalidAnswersCount(tId: number) { this.logger.verbose(`Increment invalid answers in the row for ${tId}`); const guest = await this.findById(tId); diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index 652de1f..5c251be 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -54,6 +54,6 @@ export class QuizController { @Get('endgame-extrapoints') async endgameExtrapoints() { - return await this.quizService.addEndgamePoints(); + return await this.quizService.calculateEndgamePoints(); } } diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 208221f..d4f93eb 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -18,6 +18,7 @@ import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; +import {QuizEndGameResults} from "./quiz.types"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { @@ -167,7 +168,7 @@ export class QuizService { await this.commandBus.execute(new CreateNewQueueItemCommand(targetUser, GameQueueTypes.showresults)); } - public async addEndgamePoints() { + public async calculateEndgamePoints(): Promise { const maxInvalidAnswersPromise = this.guestService.getModel().find({}).sort({ ['invalidAnswers']: 'desc'}).exec(); const maxRewardsPromise = this.guestService.getModel().find({}).sort({['rewardsReceived']: "desc"}).exec(); const maxPenaltiesPromise = this.guestService.getModel().find({}).sort({['penaltiesReceived']: 'desc'}).exec(); diff --git a/src/quiz/quiz.types.d.ts b/src/quiz/quiz.types.d.ts new file mode 100644 index 0000000..69f34b4 --- /dev/null +++ b/src/quiz/quiz.types.d.ts @@ -0,0 +1,11 @@ +export interface QuizEndGameResultsDetails { + id: number; + count: number; + name: string; +} + +export interface QuizEndGameResults { + maxInvalidAnswers: QuizEndGameResultsDetails; + maxRewards: QuizEndGameResultsDetails; + maxPenalties: QuizEndGameResultsDetails; +} \ No newline at end of file diff --git a/src/scheduler/scheduler.service.spec.ts b/src/scheduler/scheduler.service.spec.ts index 3a38d6e..5329357 100644 --- a/src/scheduler/scheduler.service.spec.ts +++ b/src/scheduler/scheduler.service.spec.ts @@ -10,6 +10,10 @@ import {SharedService} from "../shared/shared.service"; import {SharedServiceMock} from "../mocks/shared-service.mock"; import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; +import {GuestsService} from "../guests/guests.service"; +import {GuestsServiceMock} from "../mocks/guests-service.mock"; +import {CommandBus} from "@nestjs/cqrs"; +import {CommandbusMock} from "../mocks/commandbus.mock"; describe('SchedulerService', () => { @@ -26,7 +30,9 @@ describe('SchedulerService', () => { { provide: StateService, useValue: StateServiceMock }, { provide: QuizService, useValue: QuizServiceMock }, { provide: SharedService, useValue: SharedServiceMock }, - { provide: FeatureflagService, useValue: FeatureflagServiceMock } + { provide: FeatureflagService, useValue: FeatureflagServiceMock }, + { provide: CommandBus, useValue: CommandbusMock }, + { provide: GuestsService, useValue: GuestsServiceMock }, ], }).compile(); diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts index fcea24e..175a4a5 100644 --- a/src/scheduler/scheduler.service.ts +++ b/src/scheduler/scheduler.service.ts @@ -1,11 +1,15 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { StateService } from '../state/state.service'; -import { QuizService } from '../quiz/quiz.service'; -import { GiftsService } from '../gifts/gifts.service'; -import { SharedService } from '../shared/shared.service'; +import {Injectable, Logger} from '@nestjs/common'; +import {Cron} from '@nestjs/schedule'; +import {StateService} from '../state/state.service'; +import {QuizService} from '../quiz/quiz.service'; +import {GiftsService} from '../gifts/gifts.service'; +import {SharedService} from '../shared/shared.service'; import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; +import {CommandBus} from "@nestjs/cqrs"; +import {EndgameDict} from "../voice/dicts/endgame.dict"; +import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; +import {GameQueueTypes} from "../schemas/game-queue.schema"; @Injectable() export class SchedulerService { @@ -19,6 +23,7 @@ export class SchedulerService { private quizService: QuizService, private sharedService: SharedService, private featureFlagService: FeatureflagService, + private commandBus: CommandBus, ) {} @Cron('* * * * *') @@ -26,6 +31,34 @@ export class SchedulerService { await this.updateState(); } + + + async finishGame() { + if(await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.EnableEndgamePoints)) { + this.logger.verbose(`Feature flag ${FeatureFlagsConsts.EnableEndgamePoints} is enabled`); + const endgamePoints = await this.quizService.calculateEndgamePoints(); + await Promise.all([ + this.commandBus.execute( + new CreateNewQueueItemCommand(endgamePoints.maxInvalidAnswers.id, + GameQueueTypes.extra_points, + EndgameDict.maxAmountOfInvalidQuestions)), + new CreateNewQueueItemCommand(endgamePoints.maxPenalties.id, + GameQueueTypes.extra_points, + EndgameDict.maxPenalties), + new CreateNewQueueItemCommand(endgamePoints.maxRewards.id, + GameQueueTypes.extra_points, + EndgameDict.maxAmountOfRewards) + ]); + } else { + const state = await this.stateService.setState('main', 'finish'); + this.sharedService.sendSocketNotificationToAllClients( + 'state_changed', + state, + ); + this.logger.warn(`Gifts is ended, finishing game`); + } + } + private async updateState() { this.state = (await this.stateService.getState('main')).value; this.logger.verbose(`Game state is: ${this.state}`); @@ -34,15 +67,7 @@ export class SchedulerService { async gameStatus() { const giftsLeft = await this.giftsService.getRemainingPrizeCount(); if (giftsLeft === 0) { - if(await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.EnableEndgamePoints)) { - this.logger.verbose(`Feature flag ${FeatureFlagsConsts.EnableEndgamePoints} is enabled`); - } - const state = await this.stateService.setState('main', 'finish'); - this.sharedService.sendSocketNotificationToAllClients( - 'state_changed', - state, - ); - this.logger.warn(`Gifts is ended, finishing game`); + await this.finishGame(); } const questionsLeft = await this.quizService.getRemainQuestionCount(); this.logger.verbose( diff --git a/src/schemas/game-queue.schema.ts b/src/schemas/game-queue.schema.ts index fc32b04..15ed27c 100644 --- a/src/schemas/game-queue.schema.ts +++ b/src/schemas/game-queue.schema.ts @@ -8,6 +8,7 @@ export enum GameQueueTypes { playExtraCard = 'play_extra_card', screpaAnounce = 'screpa', showresults = 'show_results', + extra_points = 'extra_points', } export type GameQueueDocument = GameQueue & Document; diff --git a/src/shared/events.consts.ts b/src/shared/events.consts.ts index 4806418..1823fe5 100644 --- a/src/shared/events.consts.ts +++ b/src/shared/events.consts.ts @@ -13,4 +13,5 @@ export enum SocketEvents { GAME_RESUMED = 'game_resumed', NOTIFICATION = 'notification', FEATURE_FLAG_CHANGED = 'feature_flag_changed', + BEGIN_VERSUS = 'begin_versus', } diff --git a/src/voice/dicts/endgame.dict.ts b/src/voice/dicts/endgame.dict.ts new file mode 100644 index 0000000..d0cc39d --- /dev/null +++ b/src/voice/dicts/endgame.dict.ts @@ -0,0 +1,6 @@ + +export class EndgameDict { + static maxAmountOfInvalidQuestions = "За самое большое количество неправильных ответов, 2 балла получает %GenitiveCase%"; + static maxAmountOfRewards ="За самое большое количество полученных призов %GenitiveCase% получает минус два балла"; + static maxPenalties = "За самое большое количество наказаний %GenitiveCase% получает 3 балла"; +} \ No newline at end of file diff --git a/src/voice/voice.controller.ts b/src/voice/voice.controller.ts index 7382ab5..32452a3 100644 --- a/src/voice/voice.controller.ts +++ b/src/voice/voice.controller.ts @@ -4,7 +4,6 @@ import { TtsRequestDto, TtsRequestWithVars } from './models/TtsRequestDto'; import { invalidPrefixDict } from './dicts/invalid-prefix.dict'; import { validPrefixDict } from './dicts/valid-prefix.dict'; import * as translit from 'latin-to-cyrillic'; -import {ConfigService} from "@nestjs/config"; import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; @Controller('voice') @@ -22,7 +21,6 @@ export class VoiceController { } - @Get('ssml') @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') @@ -33,7 +31,6 @@ export class VoiceController { } else { return new NotFoundException('Voice disabled'); } - } @Get('tts') diff --git a/src/voice/voice.service.ts b/src/voice/voice.service.ts index 45098db..0a662c5 100644 --- a/src/voice/voice.service.ts +++ b/src/voice/voice.service.ts @@ -30,7 +30,6 @@ export class VoiceService { } this.logger.verbose(`Result is: ${template}`); // eslint-disable-next-line @typescript-eslint/no-var-requires - template = translit(template); return template; } -- 2.45.2 From 9177d1fc16262859b5770e630efbe43be7877068 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Tue, 12 Nov 2024 15:51:45 +0400 Subject: [PATCH 16/32] add question command & handler --- src/Consts/commands.consts.ts | 1 + src/game/game.controller.ts | 5 +++++ src/game/game.service.ts | 11 +++++++++++ src/guests/guests.module.ts | 9 ++++++--- src/guests/guests.service.ts | 4 ++++ src/messaging/guests.controller.ts | 8 +++++++- src/quiz/quiz.service.ts | 14 ++++++++++++++ src/state/state.controller.ts | 3 ++- 8 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Consts/commands.consts.ts b/src/Consts/commands.consts.ts index feb6b6d..fc65d8e 100644 --- a/src/Consts/commands.consts.ts +++ b/src/Consts/commands.consts.ts @@ -9,4 +9,5 @@ export class CommandsConsts { static GetCards = 'GetCards'; static ApplyDebuff = 'ApplyDebuff'; static CompleteQueue = 'CompleteQueue'; + static GetQuestion = 'GetQuestion'; } diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index 5726cf9..a451301 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -37,4 +37,9 @@ export class GameController { this.logger.verbose('[SimulateVersus] enter'); return this.gameService.simulateVersus(); } + + @Get('state-details') + async getStateDetails() { + return this.gameService.getStateDetails(); + } } diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 3a367bd..759382b 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -130,9 +130,20 @@ export class GameService implements OnApplicationBootstrap{ } async beginVersus(player1: number, player2: number) { + await this.sharedService.setConfig('current_action', JSON.stringify({ + action:'versus', + data: { + player1: player1, + player2: player2, + } + })); this.sharedService.sendSocketNotificationToAllClients( SocketEvents.BEGIN_VERSUS, { player1, player2 } ) } + + async getStateDetails() { + return await this.sharedService.getConfig('current_action') || null; + } } diff --git a/src/guests/guests.module.ts b/src/guests/guests.module.ts index 5b5de11..8964c88 100644 --- a/src/guests/guests.module.ts +++ b/src/guests/guests.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import {forwardRef, Module} from '@nestjs/common'; import { GuestsService } from './guests.service'; import { MongooseModule } from '@nestjs/mongoose'; import { Guest, GuestSchema } from '../schemas/guest.schema'; @@ -23,9 +23,12 @@ import {GetGuestPropertyHandler} from "./command/get-guest-property.handler"; import {QueryHandlers } from "./queries"; import {SetGuestPropertyCommandHandler} from "./command/handlers/set-guest-property.handler"; import {WrongAnswerReceivedGuestEventHandler} from "./event-handlers/wrong-answer-received-guest-event.handler"; -import {VoiceService} from "../voice/voice.service"; + import {VoiceModule} from "../voice/voice.module"; import {IncreasePlayerScoreCommandHandler} from "./command/handlers/increase-player-score-command.handler"; +import {QuizService} from "../quiz/quiz.service"; +import {QuizModule} from "../quiz/quiz.module"; + const commandHandlers = [ GuestsRemoveKeyboardHandler, @@ -62,7 +65,7 @@ const eventHandlers = [ CardsModule, ], providers: [GuestsService, ConfigService, ...commandHandlers,...QueryHandlers, ...eventHandlers, ], - exports: [GuestsService], + exports: [GuestsService,], controllers: [GuestsController], }) export class GuestsModule {} diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index dfe0cc0..cf3078e 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -28,6 +28,7 @@ import {VoiceService} from "../voice/voice.service"; import {screpaDictManyInvalidAnswersDict} from "../voice/dicts/screpa-dict-many-invalid-answers.dict"; import {GuestPropertiesConsts} from "../schemas/properties.consts"; import {GuestNamesInCases} from "./guest.types"; +import {QuizService} from "../quiz/quiz.service"; @Injectable() export class GuestsService { @@ -87,6 +88,9 @@ export class GuestsService { this.logger.verbose(`Keyboard hidden`); } + + + async postQuestion(questionDto: QuestionDto, targetId = null) { const guests = await this.findAll(); const extra = { diff --git a/src/messaging/guests.controller.ts b/src/messaging/guests.controller.ts index 49adfd6..049160a 100644 --- a/src/messaging/guests.controller.ts +++ b/src/messaging/guests.controller.ts @@ -9,12 +9,13 @@ import {CommandsConsts} from "../Consts/commands.consts"; import {EventBus} from "@nestjs/cqrs"; import {PlayerCardSelectedEvent} from "../game/events/player-card-selected.event"; import {getCard} from "../helpers/card-parser"; +import {QuizService} from "../quiz/quiz.service"; @Controller() export class GuestsMessageController { private readonly logger = new Logger(GuestsMessageController.name); - constructor(private guestService: GuestsService, private sharedService: SharedService, private eventBus: EventBus) { + constructor(private guestService: GuestsService, private sharedService: SharedService, private eventBus: EventBus, private quizService: QuizService) { } @MessagePattern({ cmd: 'GuestInfo'} ) async getGuestInformation(@Payload() data: GetGuestInfoModel, @Ctx() context: RmqContext) { @@ -58,4 +59,9 @@ export class GuestsMessageController { ); return; } + + @MessagePattern({ cmd: CommandsConsts.GetQuestion}) + async getQuestion(@Payload() data: { user: number, inline: false}) { + await this.quizService.displayQuestionForUser(data.user); + } } \ No newline at end of file diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index d4f93eb..269cc4b 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -55,6 +55,7 @@ export class QuizService { return item.save(); } + async validateAnswer(answer: string, id: number) { this.logger.verbose(`enter validate answer ${answer} ${id}`); const question = await this.get(); @@ -299,4 +300,17 @@ export class QuizService { const question = await this.get(); return question.userAnswers; } + + async displayQuestionForUser(telegramId: number) { + const question = await this.get(); + const dto: QuestionDto = { + id: question.id, + text: question.text, + answers: question.answers, + valid: question.valid, + note: question.note, + qId: question.qId, + } + await this.guestService.postQuestion(dto, telegramId); + } } diff --git a/src/state/state.controller.ts b/src/state/state.controller.ts index 7cf564c..b06fa96 100644 --- a/src/state/state.controller.ts +++ b/src/state/state.controller.ts @@ -40,7 +40,8 @@ export class StateController { { cmd: CommandsConsts.SetCommands }, [ { command: 'start', description: 'главное меню'}, - { command: 'cards', description: 'сыграть карту'} + { command: 'cards', description: 'сыграть карту'}, + { command: 'question', description: 'вернутся к вопросу'} ] ).subscribe(() => { this.logger.verbose('Bot commands updated'); -- 2.45.2 From 456cdcb4aaad2ba7d8dc553b20b6f634f3504c55 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Tue, 12 Nov 2024 21:40:40 +0400 Subject: [PATCH 17/32] TGD-35 --- src/Consts/commands.consts.ts | 1 + src/game/entities/cards.entities.ts | 1 - .../guests.post-cards-to-user-command.ts | 5 +++- src/guests/guests.service.ts | 12 ++++++-- src/messaging/guests.controller.ts | 1 + src/messaging/models/validate-answer.model.ts | 6 ++++ src/messaging/quiz-messaging.controller.ts | 11 +++---- src/quiz/quiz.service.ts | 29 ++++++++----------- 8 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/Consts/commands.consts.ts b/src/Consts/commands.consts.ts index fc65d8e..4486485 100644 --- a/src/Consts/commands.consts.ts +++ b/src/Consts/commands.consts.ts @@ -10,4 +10,5 @@ export class CommandsConsts { static ApplyDebuff = 'ApplyDebuff'; static CompleteQueue = 'CompleteQueue'; static GetQuestion = 'GetQuestion'; + static QuestionAnswer = "QuestionAnswer"; } diff --git a/src/game/entities/cards.entities.ts b/src/game/entities/cards.entities.ts index 6361606..a6751d2 100644 --- a/src/game/entities/cards.entities.ts +++ b/src/game/entities/cards.entities.ts @@ -197,7 +197,6 @@ export class BanPlayer extends GameCard { eventBus.publish(new CardsSetChangedEvent(sourceUser.telegramId)); }) eventBus.pipe(ofType(NextQuestionEvent)).subscribe(async (r)=> { - this.logger.verbose(`next event`); const players = await queryBus.execute(new FilterGuestsWithPropertyQuery(DebuffsConsts.bannedFor, '$gt', 0)) this.logger.verbose(`enter: ban card handler, banned players count ${players.length}`); players.map(async (player) => { diff --git a/src/guests/command/guests.post-cards-to-user-command.ts b/src/guests/command/guests.post-cards-to-user-command.ts index ac0ac03..3eafee4 100644 --- a/src/guests/command/guests.post-cards-to-user-command.ts +++ b/src/guests/command/guests.post-cards-to-user-command.ts @@ -28,13 +28,16 @@ export class TgPostCardsToUserCommandHandler implements ICommandHandler { extra.reply_markup.keyboard.push([ {text: Messages.EMOJI_CARD + ' ' + card}, ]); - extra_Inline.reply_markup.inline_keyboard.push([{ text: Messages.EMOJI_CARD + ' ' + card}]) + extra_Inline.reply_markup.inline_keyboard.push([{ text: Messages.EMOJI_CARD + ' ' + card, callback_data: `card/${card}`}]) }); await this.sharedService.setConfig(`buttons_${command.chatId}`, JSON.stringify(extra), diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index cf3078e..ed5f18b 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -96,12 +96,18 @@ export class GuestsService { const extra = { reply_markup: { keyboard: [], + inline_keyboard: [], }, }; questionDto.answers.forEach((item, index) => { - extra.reply_markup.keyboard.push([ - { text: this.nums[index] + ' ' + item }, - ]); + // extra.reply_markup.keyboard.push([ + // { text: this.nums[index] + ' ' + item }, + // ]); + if(item !== null){ + extra.reply_markup.inline_keyboard.push( + [ { text: item, callback_data: `${item.substring(0,50)}` }, + ]); + } }); if (!targetId) { guests.forEach((guest) => { diff --git a/src/messaging/guests.controller.ts b/src/messaging/guests.controller.ts index 049160a..38cb32f 100644 --- a/src/messaging/guests.controller.ts +++ b/src/messaging/guests.controller.ts @@ -64,4 +64,5 @@ export class GuestsMessageController { async getQuestion(@Payload() data: { user: number, inline: false}) { await this.quizService.displayQuestionForUser(data.user); } + } \ No newline at end of file diff --git a/src/messaging/models/validate-answer.model.ts b/src/messaging/models/validate-answer.model.ts index e3c9249..229dc5c 100644 --- a/src/messaging/models/validate-answer.model.ts +++ b/src/messaging/models/validate-answer.model.ts @@ -2,4 +2,10 @@ export interface ValidateAnswerModel { answer: string; user: number; name: string; +} + +export interface ValidateAnswerInline { + answer: string; + user: number; + name: string; } \ No newline at end of file diff --git a/src/messaging/quiz-messaging.controller.ts b/src/messaging/quiz-messaging.controller.ts index 964adc2..5417a4f 100644 --- a/src/messaging/quiz-messaging.controller.ts +++ b/src/messaging/quiz-messaging.controller.ts @@ -1,7 +1,7 @@ import {Controller, Logger} from "@nestjs/common"; import {MessagePattern, Payload} from "@nestjs/microservices"; import {CommandsConsts} from "../Consts/commands.consts"; -import {ValidateAnswerModel} from "./models/validate-answer.model"; +import {ValidateAnswerInline, ValidateAnswerModel} from "./models/validate-answer.model"; import {QuizAnsweredEvent} from "../game/events/quiz.answered"; import {QuizService} from "../quiz/quiz.service"; import {CommandBus, EventBus} from "@nestjs/cqrs"; @@ -15,11 +15,12 @@ export class QuizMessagingController { constructor(private quizService: QuizService, private eventBus: EventBus, private cmdBus: CommandBus, private gameService: GameService) { } - @MessagePattern({ cmd: CommandsConsts.ValidateAnswer}) - async validateAnswer(@Payload() data: ValidateAnswerModel) { + + @MessagePattern({ cmd: CommandsConsts.QuestionAnswer}) + async getQuestionAnswer(@Payload() data: ValidateAnswerInline) { this.logger.verbose(`Validate answer ${data}`); this.eventBus.publish(new QuizAnsweredEvent(data.name)); - const result = await this.quizService.validateAnswer( + const result = await this.quizService.validateAnswerInline( data.answer, data.user, ); @@ -36,7 +37,7 @@ export class QuizMessagingController { @MessagePattern({ cmd: CommandsConsts.GetCards}) async getCardsForUser(@Payload() data: { user: number, inline: boolean}) { this.logger.verbose(`getCardsForUser ${data}`); - await this.cmdBus.execute(new SendBetweenRoundsActionsCommand(data.user, data.inline)) + await this.cmdBus.execute(new SendBetweenRoundsActionsCommand(data.user, true)) } @MessagePattern({ cmd: CommandsConsts.ApplyDebuff}) diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 269cc4b..2c29058 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -55,25 +55,20 @@ export class QuizService { return item.save(); } - - async validateAnswer(answer: string, id: number) { - this.logger.verbose(`enter validate answer ${answer} ${id}`); + async validateAnswerInline(answer:string, id: number) { + this.logger.verbose(`[validateAnswer] enter ${answer} ${id}`); const question = await this.get(); - await question.save(); - const regexp = new RegExp( - Object.keys(this.answerNumbers) - .map((x) => { - x = this.answerNumbers[x].replace('.', '.').replace(' ', ' '); - return x; - }) - .join('|'), - 'gi', - ); this.logger.verbose( `Validating answer for question: ${JSON.stringify(question.text)}`, ); - const filtered = answer.replace(regexp, '').trim(); - const isAnswerValid = question.valid === filtered; + // check that answer exist + const shortAnswers = question.answers.map((answer) => answer.substring(0,50)); + const shortValidAnswer = question.valid.substring(0,50); + if(shortAnswers.indexOf(answer) === -1) { + this.logger.warn(`[validateAnswer] this question is not on game now`); + return; + } + const isAnswerValid = shortValidAnswer === answer; if(question.userAnswers.find(answer => answer.user === id)) { this.logger.verbose("question->user answer is already containing record"); return; @@ -85,11 +80,11 @@ export class QuizService { }) await question.save(); this.logger.verbose("question saved with user details") - if (question.valid === filtered) { + if (shortValidAnswer=== answer) { question.answeredBy = id; this.logger.verbose(`extra ${question.note}`); this.eventBus.publish( - new ValidAnswerReceivedEvent(id, filtered, question.note), + new ValidAnswerReceivedEvent(id, answer, question.note), ); await question.save(); await this.markQuestionStorageAsAnsweredCorrectly(question.text); -- 2.45.2 From ac9116e1383d9e3c67ea8e6a28878635ef51fb3c Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 13 Nov 2024 02:08:39 +0400 Subject: [PATCH 18/32] Versus --- src/game/game.controller.spec.ts | 3 + src/game/game.controller.ts | 15 ++-- src/game/game.module.ts | 8 +- src/game/game.service.ts | 32 ++------ src/game/versus/versus.controller.spec.ts | 23 ++++++ src/game/versus/versus.controller.ts | 37 +++++++++ src/game/versus/versus.service.spec.ts | 31 +++++++ src/game/versus/versus.service.ts | 98 +++++++++++++++++++++++ src/game/versus/versus.types.d.ts | 6 ++ src/guests/guests.controller.ts | 7 +- src/mocks/versus-service.mock.ts | 3 + src/schemas/versus.schema.ts | 15 ++++ src/shared/events.consts.ts | 1 + src/shared/shared.service.ts | 6 ++ 14 files changed, 249 insertions(+), 36 deletions(-) create mode 100644 src/game/versus/versus.controller.spec.ts create mode 100644 src/game/versus/versus.controller.ts create mode 100644 src/game/versus/versus.service.spec.ts create mode 100644 src/game/versus/versus.service.ts create mode 100644 src/game/versus/versus.types.d.ts create mode 100644 src/mocks/versus-service.mock.ts create mode 100644 src/schemas/versus.schema.ts diff --git a/src/game/game.controller.spec.ts b/src/game/game.controller.spec.ts index e9b857e..cc45b54 100644 --- a/src/game/game.controller.spec.ts +++ b/src/game/game.controller.spec.ts @@ -2,6 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GameController } from './game.controller'; import {GameService} from "./game.service"; import {GameServiceMock} from "../mocks/game-service.mock"; +import {VersusService} from "./versus/versus.service"; +import {VersusServiceMock} from "../mocks/versus-service.mock"; describe('GameController', () => { let controller: GameController; @@ -11,6 +13,7 @@ describe('GameController', () => { controllers: [GameController], providers: [ { provide: GameService, useValue: GameServiceMock }, + { provide: VersusService, useValue: VersusServiceMock }, ] }).compile(); diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index a451301..6c28a04 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -1,10 +1,11 @@ import { Controller, Get, Logger, Param, Post } from '@nestjs/common'; import { GameService } from './game.service'; +import {VersusService} from "./versus/versus.service"; @Controller('game') export class GameController { private readonly logger = new Logger(GameController.name); - constructor(private gameService: GameService) { + constructor(private gameService: GameService, private versusService: VersusService) { } @Post(':id/complete') @@ -32,14 +33,16 @@ export class GameController { return this.gameService.playExtraCards(); } - @Post('simulate-versus') - async SimulateVersus() { - this.logger.verbose('[SimulateVersus] enter'); - return this.gameService.simulateVersus(); - } + @Get('state-details') async getStateDetails() { return this.gameService.getStateDetails(); } + + @Post('clear-queue') + async clearQueue() { + this.logger.warn(`[clearQueue] enter`); + await this.gameService.clearGameQueue(); + } } diff --git a/src/game/game.module.ts b/src/game/game.module.ts index c49ae13..bbb4d25 100644 --- a/src/game/game.module.ts +++ b/src/game/game.module.ts @@ -17,6 +17,9 @@ import {ConfigService} from "@nestjs/config"; import {SelectTargetPlayerHandler} from "./comand-handlers/select-target-player.handler"; import {SendBetweenRoundsActionsHandler} from "../guests/command/send-between-rounds-actions.command"; import {GuestsModule} from "../guests/guests.module"; +import { VersusService } from './versus/versus.service'; +import { VersusController } from './versus/versus.controller'; +import {Versus, VersusSchema} from "../schemas/versus.schema"; const eventHandlers = [ @@ -42,11 +45,12 @@ const commandHandlers = [ CqrsModule, MongooseModule.forFeature([ { name: GameQueue.name, schema: GameQueueSchema }, + { name: Versus.name, schema: VersusSchema } ]), forwardRef(() => GuestsModule) ], - providers: [GameService, ConfigService, ...eventHandlers, ...commandHandlers], + providers: [GameService, ConfigService, ...eventHandlers, ...commandHandlers, VersusService], exports: [GameService], - controllers: [GameController], + controllers: [GameController, VersusController], }) export class GameModule {} diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 759382b..5c85159 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -116,34 +116,12 @@ export class GameService implements OnApplicationBootstrap{ }) } - async simulateVersus() { - const guests = (await this.guestService.findAll()).slice(0,2).map((guest) => { - return { - id: guest.telegramId, - name: guest.name, - } - }); - if(guests.length < 2) { - throw new Error("Can't simulate, in db less than 2 players") - } - await this.beginVersus(guests[0].id, guests[1].id); - } - - async beginVersus(player1: number, player2: number) { - await this.sharedService.setConfig('current_action', JSON.stringify({ - action:'versus', - data: { - player1: player1, - player2: player2, - } - })); - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.BEGIN_VERSUS, - { player1, player2 } - ) - } - async getStateDetails() { return await this.sharedService.getConfig('current_action') || null; } + + async clearGameQueue() { + await this.gameQueueModel.deleteMany({}).exec(); + return { result: true }; + } } diff --git a/src/game/versus/versus.controller.spec.ts b/src/game/versus/versus.controller.spec.ts new file mode 100644 index 0000000..5fbe666 --- /dev/null +++ b/src/game/versus/versus.controller.spec.ts @@ -0,0 +1,23 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { VersusController } from './versus.controller'; +import {VersusService} from "./versus.service"; +import {VersusServiceMock} from "../../mocks/versus-service.mock"; + +describe('VersusController', () => { + let controller: VersusController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [VersusController], + providers: [ + { provide: VersusService, useValue: VersusServiceMock }, + ] + }).compile(); + + controller = module.get(VersusController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/game/versus/versus.controller.ts b/src/game/versus/versus.controller.ts new file mode 100644 index 0000000..7d6cb7c --- /dev/null +++ b/src/game/versus/versus.controller.ts @@ -0,0 +1,37 @@ +import {Body, Controller, Get, Logger, Post} from '@nestjs/common'; +import {VersusService} from "./versus.service"; +import {VersusDto} from "./versus.types"; + +@Controller('versus') +export class VersusController { + private logger = new Logger(VersusController.name); + constructor(private versusService: VersusService) { + + } + + @Post('simulate-versus') + async SimulateVersus() { + this.logger.verbose('[SimulateVersus] enter'); + return this.versusService.simulateVersus(); + } + + @Post('import') + async Import(@Body() data: VersusDto[]) { + return await this.versusService.importVersus(data); + } + + @Get() + async GetVersusTask() { + return await this.versusService.getVersusTask(); + } + + @Post('complete') + async Completed(@Body() payload: { winner: number }) { + return await this.versusService.complete(payload.winner); + } + + @Post('reset-all') + async markAllUncompleted() { + return await this.versusService.markAllAsUncompleted(); + } +} diff --git a/src/game/versus/versus.service.spec.ts b/src/game/versus/versus.service.spec.ts new file mode 100644 index 0000000..f5f06a8 --- /dev/null +++ b/src/game/versus/versus.service.spec.ts @@ -0,0 +1,31 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { VersusService } from './versus.service'; +import {GuestsService} from "../../guests/guests.service"; +import {GuestsServiceMock} from "../../mocks/guests-service.mock"; +import {SharedService} from "../../shared/shared.service"; +import {getModelToken} from "@nestjs/mongoose"; +import {Versus} from "../../schemas/versus.schema"; +import {Model} from "mongoose"; +import {CommandBus} from "@nestjs/cqrs"; + +describe('VersusService', () => { + let service: VersusService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + VersusService, + { provide: GuestsService, useValue: GuestsServiceMock }, + { provide: SharedService, useValue: SharedService }, + { provide: getModelToken(Versus.name), useValue: Model }, + { provide: CommandBus, useValue: CommandBus }, + ], + }).compile(); + + service = module.get(VersusService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts new file mode 100644 index 0000000..a607acc --- /dev/null +++ b/src/game/versus/versus.service.ts @@ -0,0 +1,98 @@ +import { Injectable } from '@nestjs/common'; +import {SocketEvents} from "../../shared/events.consts"; +import {GuestsService} from "../../guests/guests.service"; +import {SharedService} from "../../shared/shared.service"; +import {InjectModel} from "@nestjs/mongoose"; +import {Versus, VersusDocument} from "../../schemas/versus.schema"; +import {Model} from "mongoose"; +import {VersusDto} from "./versus.types"; +import {CommandBus} from "@nestjs/cqrs"; +import {IncreasePlayerScoreCommand} from "../../guests/command/increase-player-score.command"; +import {IncreasePlayerWinningRateCommand} from "../commands/increase-player-winning-rate.command"; + +@Injectable() +export class VersusService { + static configKeyCurrentAction = 'current_action'; + static configKeyActiveVersus = 'active_versus'; + constructor( + private guestService: GuestsService, + private sharedService: SharedService, + @InjectModel(Versus.name) private versusModel: Model, + private cmdBus: CommandBus, + ) { + } + async simulateVersus() { + const guests = (await this.guestService.findAll()).slice(0,2).map((guest) => { + return { + id: guest.telegramId, + name: guest.name, + } + }); + if(guests.length < 2) { + throw new Error("Can't simulate, in db less than 2 players") + } + await this.beginVersus(guests[0].id, guests[1].id); + } + + async beginVersus(player1: number, player2: number) { + const [p1data,p2data] = await Promise.all([this.guestService.findById(player1), this.guestService.findById(player2)]); + await this.sharedService.setConfig(VersusService.configKeyCurrentAction, JSON.stringify({ + action:'versus', + data: { + player1: player1, + player2: player2, + player1name: p1data.name, + player2name: p2data.name, + } + })); + this.sharedService.sendSocketNotificationToAllClients( + SocketEvents.BEGIN_VERSUS, + { player1, player2, player1name: p1data.name, player2name: p2data.name } + ) + } + + async importVersus(data: VersusDto[]) { + data.map(async (record) => { + const item = new this.versusModel({ + ...record + }); + await item.save(); + }); + return { result: true }; + } + + async getVersusTask() { + const rand = await this.versusModel + .aggregate([{ $match: { completed: false } }, { $sample: { size: 1 } }]) + .exec(); + // @TODO check who win with telegram + + const item = await this.versusModel.findOne({ _id: rand[0]._id }).exec(); + await this.sharedService.setConfig(VersusService.configKeyActiveVersus, item.id); + return item; + } + async markAllAsUncompleted() { + const versuses = await this.versusModel.find().exec(); + versuses.map(async (versus) => { + versus.completed = false; + await versus.save(); + }); + return { result: true }; + } + + async complete(winner: number) { + const activeVersus = await this.sharedService.getConfig(VersusService.configKeyActiveVersus); + const item = await this.versusModel.findOne({ _id: activeVersus.value }).exec(); + item.completed = true; + await item.save(); + await this.cmdBus.execute(new IncreasePlayerScoreCommand(winner, 2)); + await this.cmdBus.execute(new IncreasePlayerWinningRateCommand(winner, 30)); + await this.sharedService.setConfig(VersusService.configKeyCurrentAction, ''); + this.sharedService.sendSocketNotificationToAllClients( + SocketEvents.END_VERSUS, + { winner: winner } + ) + + return item; + } +} diff --git a/src/game/versus/versus.types.d.ts b/src/game/versus/versus.types.d.ts new file mode 100644 index 0000000..e81bec8 --- /dev/null +++ b/src/game/versus/versus.types.d.ts @@ -0,0 +1,6 @@ +export interface VersusDto { + id: string; + text: string; + completed: boolean; + description: string; +} \ No newline at end of file diff --git a/src/guests/guests.controller.ts b/src/guests/guests.controller.ts index 0fc9cea..31dd713 100644 --- a/src/guests/guests.controller.ts +++ b/src/guests/guests.controller.ts @@ -1,6 +1,6 @@ import { Controller, - Get, Param, Res + Get, Param, Post, Res } from "@nestjs/common"; import { GuestsService } from './guests.service'; import { Response } from 'express'; @@ -42,4 +42,9 @@ export class GuestsController { // this.echoService.enterQuiz(u.chatId); }); } + @Post('reset-score') + async resetAllPlayersScore() { + await this.guestService.resetPlayersScore(); + return { result: true }; + } } diff --git a/src/mocks/versus-service.mock.ts b/src/mocks/versus-service.mock.ts new file mode 100644 index 0000000..b3555ad --- /dev/null +++ b/src/mocks/versus-service.mock.ts @@ -0,0 +1,3 @@ +export class VersusServiceMock { + +} \ No newline at end of file diff --git a/src/schemas/versus.schema.ts b/src/schemas/versus.schema.ts new file mode 100644 index 0000000..49a8295 --- /dev/null +++ b/src/schemas/versus.schema.ts @@ -0,0 +1,15 @@ +import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose"; +import {Document} from "mongoose"; + +@Schema() +export class Versus { + @Prop() + text: string; + @Prop({ default: false}) + completed: boolean; + @Prop() + description: string; +} + +export type VersusDocument = Versus & Document; +export const VersusSchema = SchemaFactory.createForClass(Versus); \ No newline at end of file diff --git a/src/shared/events.consts.ts b/src/shared/events.consts.ts index 1823fe5..37781fe 100644 --- a/src/shared/events.consts.ts +++ b/src/shared/events.consts.ts @@ -14,4 +14,5 @@ export enum SocketEvents { NOTIFICATION = 'notification', FEATURE_FLAG_CHANGED = 'feature_flag_changed', BEGIN_VERSUS = 'begin_versus', + END_VERSUS = 'end_versus', } diff --git a/src/shared/shared.service.ts b/src/shared/shared.service.ts index aec82bf..99fd64d 100644 --- a/src/shared/shared.service.ts +++ b/src/shared/shared.service.ts @@ -54,6 +54,12 @@ export class SharedService { } } + /** + * Notifies all connected socket about changes + * @deprecated Use specific handlers in this class + * @param event + * @param payload + */ sendSocketNotificationToAllClients(event: string, payload?: any) { this.logger.verbose(`Sending notification to client: ${event}, ${JSON.stringify(payload)}`); this.socketGateway.notifyAllClients(event, payload); -- 2.45.2 From 666240dfb84ef94407306b9608ec61839bc5f8bb Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 13 Nov 2024 03:33:03 +0400 Subject: [PATCH 19/32] Versus fixes --- .../begin-versus-command.handler.ts | 13 +++++ src/game/commands/begin-versus.command.ts | 6 +++ src/game/entities/cards.entities.ts | 47 +++++++++++++++++-- src/game/entities/debuffs.consts.ts | 1 + src/game/game.module.ts | 9 +++- ...eck-if-another-versus-in-progress.query.ts | 3 ++ ...k-if-another-versus-in-progress.handler.ts | 13 +++++ src/game/versus/versus.service.ts | 10 +++- .../send-between-rounds-actions.command.ts | 2 +- .../handlers/get-guest-query.handler.ts | 1 - 10 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 src/game/comand-handlers/begin-versus-command.handler.ts create mode 100644 src/game/commands/begin-versus.command.ts create mode 100644 src/game/queries/check-if-another-versus-in-progress.query.ts create mode 100644 src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts diff --git a/src/game/comand-handlers/begin-versus-command.handler.ts b/src/game/comand-handlers/begin-versus-command.handler.ts new file mode 100644 index 0000000..be114ef --- /dev/null +++ b/src/game/comand-handlers/begin-versus-command.handler.ts @@ -0,0 +1,13 @@ +import {CommandHandler, ICommandHandler} from "@nestjs/cqrs"; +import {BeginVersusCommand} from "../commands/begin-versus.command"; +import {VersusService} from "../versus/versus.service"; + +@CommandHandler(BeginVersusCommand) +export class BeginVersusCommandHandler implements ICommandHandler { + constructor(private versusService:VersusService) { + } + execute(command: BeginVersusCommand): Promise { + return this.versusService.beginVersus(command.sourceId,command.destinationId); + } + +} \ No newline at end of file diff --git a/src/game/commands/begin-versus.command.ts b/src/game/commands/begin-versus.command.ts new file mode 100644 index 0000000..6019beb --- /dev/null +++ b/src/game/commands/begin-versus.command.ts @@ -0,0 +1,6 @@ + +export class BeginVersusCommand { + constructor(public sourceId: number, public destinationId: number) { + + } +} \ No newline at end of file diff --git a/src/game/entities/cards.entities.ts b/src/game/entities/cards.entities.ts index a6751d2..27f2b44 100644 --- a/src/game/entities/cards.entities.ts +++ b/src/game/entities/cards.entities.ts @@ -19,8 +19,9 @@ import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.c import {StringHelper} from "../../helpers/stringhelper"; import {GetGuestQuery} from "../../guests/queries/getguest.query"; import {CardsSetChangedEvent} from "../events/cards-events/cards-set-changed.event"; -import {GetGuestPropertyQuery} from "../../guests/command/get-guest-property.handler"; import {GuestPropertiesConsts} from "../../schemas/properties.consts"; +import {BeginVersusCommand} from "../commands/begin-versus.command"; +import {CheckIfAnotherVersusInProgressQuery} from "../queries/check-if-another-versus-in-progress.query"; export interface IGameCard { setupHandlers(eventBus: EventBus, commandBus: CommandBus, queryBus: QueryBus): void; @@ -74,7 +75,7 @@ export class DoubleTreasureCard extends GameCard { await this.commandBus.execute( new GiveOutAPrizeCommand(this.telegramId), ); - const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId));; + const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId)); const subjcaseFrom = userSrc.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase)); const message = `${subjcaseFrom} решает удвоить приз!`; await this.commandBus.execute(new SendToastCommand(message, 8000)); @@ -174,6 +175,45 @@ export class AvoidPenaltyCard extends GameCard { } } + +@Injectable() +export class VersusCard extends GameCard { + dealOnStart = true; + name = VersusCard.name; + chance = 10; + emoji = '🆚'; + description = 'Поединок'; + mightBePlayed = QuizAnswerStateEnum.betweenRounds; + async setupHandlers(eventBus: EventBus, commandBus: CommandBus, queryBus: QueryBus) { + super.setupHandlers(eventBus, commandBus, queryBus); + eventBus.pipe( + ofType(DebuffCardPlayedEvent), + filter(x => x.debufName == DebuffsConsts.versus)) + .subscribe(async (r) =>{ + const versusInProgress = await queryBus.execute(new CheckIfAnotherVersusInProgressQuery()); + this.logger.verbose(`versusInProgress ${versusInProgress}`); + if(versusInProgress) { + this.logger.warn(`another versus in progress`); + + return; + } + const destUser = await queryBus.execute(new GetGuestQuery(r.dest)) + const sourceUser = await queryBus.execute(new GetGuestQuery(r.from)); + await commandBus.execute(new BeginVersusCommand(sourceUser.telegramId, destUser.telegramId)); + }); + //eventBus.pipe(ofType(DebuffCardPlayedEvent)).subscribe(r => console.log(r)); + } + async handle() { + await this.commandBus.execute( + new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.versus, 0) + ) + await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null)); + this.eventBus.subscribe((data) =>{ + this.logger.verbose(`Response from cmdBus: ${data}`); + }); + } +} + @Injectable() export class BanPlayer extends GameCard { dealOnStart = true; @@ -227,5 +267,6 @@ export const gameCards: typeof GameCard[] = [ ShitCard, LuckyCard, AvoidPenaltyCard, - BanPlayer + BanPlayer, + VersusCard, ]; diff --git a/src/game/entities/debuffs.consts.ts b/src/game/entities/debuffs.consts.ts index 50162fd..ffd14fd 100644 --- a/src/game/entities/debuffs.consts.ts +++ b/src/game/entities/debuffs.consts.ts @@ -1,3 +1,4 @@ export class DebuffsConsts { static bannedFor = 'bannedFor'; + static versus = 'versus'; } \ No newline at end of file diff --git a/src/game/game.module.ts b/src/game/game.module.ts index bbb4d25..3e55e32 100644 --- a/src/game/game.module.ts +++ b/src/game/game.module.ts @@ -20,6 +20,8 @@ import {GuestsModule} from "../guests/guests.module"; import { VersusService } from './versus/versus.service'; import { VersusController } from './versus/versus.controller'; import {Versus, VersusSchema} from "../schemas/versus.schema"; +import {BeginVersusCommandHandler} from "./comand-handlers/begin-versus-command.handler"; +import {CheckIfAnotherVersusInProgressHandler} from "./queries/handlers/check-if-another-versus-in-progress.handler"; const eventHandlers = [ @@ -37,8 +39,11 @@ const commandHandlers = [ GamePrizeChanceIncreasedEventHandler, GameProceedGameQueueCommandHandler, SelectTargetPlayerHandler, - SendBetweenRoundsActionsHandler + SendBetweenRoundsActionsHandler, + BeginVersusCommandHandler, ]; + +const queryHandlers = [CheckIfAnotherVersusInProgressHandler]; @Global() @Module({ imports: [ @@ -49,7 +54,7 @@ const commandHandlers = [ ]), forwardRef(() => GuestsModule) ], - providers: [GameService, ConfigService, ...eventHandlers, ...commandHandlers, VersusService], + providers: [GameService, ConfigService, ...eventHandlers, ...commandHandlers,...queryHandlers, VersusService], exports: [GameService], controllers: [GameController, VersusController], }) diff --git a/src/game/queries/check-if-another-versus-in-progress.query.ts b/src/game/queries/check-if-another-versus-in-progress.query.ts new file mode 100644 index 0000000..91b6288 --- /dev/null +++ b/src/game/queries/check-if-another-versus-in-progress.query.ts @@ -0,0 +1,3 @@ +export class CheckIfAnotherVersusInProgressQuery { + +} \ No newline at end of file diff --git a/src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts b/src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts new file mode 100644 index 0000000..6089b25 --- /dev/null +++ b/src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts @@ -0,0 +1,13 @@ +import {IQueryHandler, QueryHandler} from "@nestjs/cqrs"; +import {CheckIfAnotherVersusInProgressQuery} from "../check-if-another-versus-in-progress.query"; +import {VersusService} from "../../versus/versus.service"; + +@QueryHandler(CheckIfAnotherVersusInProgressQuery) +export class CheckIfAnotherVersusInProgressHandler implements IQueryHandler { + constructor(private versusService: VersusService) { + } + async execute(query: CheckIfAnotherVersusInProgressHandler): Promise { + return await this.versusService.checkIfAnotherVersusInProgress(); + } + +} \ No newline at end of file diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts index a607acc..b554d5f 100644 --- a/src/game/versus/versus.service.ts +++ b/src/game/versus/versus.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import {Injectable, Logger} from '@nestjs/common'; import {SocketEvents} from "../../shared/events.consts"; import {GuestsService} from "../../guests/guests.service"; import {SharedService} from "../../shared/shared.service"; @@ -14,6 +14,7 @@ import {IncreasePlayerWinningRateCommand} from "../commands/increase-player-winn export class VersusService { static configKeyCurrentAction = 'current_action'; static configKeyActiveVersus = 'active_versus'; + private logger = new Logger(VersusService.name); constructor( private guestService: GuestsService, private sharedService: SharedService, @@ -95,4 +96,11 @@ export class VersusService { return item; } + + async checkIfAnotherVersusInProgress() { + this.logger.debug(`checkIfAnotherVersusInProgress enter`) + const currentAction = await this.sharedService.getConfig(VersusService.configKeyCurrentAction); + return currentAction.value !== ''; + + } } diff --git a/src/guests/command/send-between-rounds-actions.command.ts b/src/guests/command/send-between-rounds-actions.command.ts index 5ef3973..292e312 100644 --- a/src/guests/command/send-between-rounds-actions.command.ts +++ b/src/guests/command/send-between-rounds-actions.command.ts @@ -3,7 +3,7 @@ import {Promise} from "mongoose"; import {GuestsService} from "../guests.service"; export class SendBetweenRoundsActionsCommand { - constructor(public user: number, public inline: boolean = false) { + constructor(public user: number, public inline: boolean = true) { } } diff --git a/src/guests/queries/handlers/get-guest-query.handler.ts b/src/guests/queries/handlers/get-guest-query.handler.ts index 247aaf6..d9c1883 100644 --- a/src/guests/queries/handlers/get-guest-query.handler.ts +++ b/src/guests/queries/handlers/get-guest-query.handler.ts @@ -1,6 +1,5 @@ import {IQueryHandler, QueryHandler} from "@nestjs/cqrs"; import {GetGuestQuery} from "../getguest.query"; -import {Promise} from "mongoose"; import {GuestsService} from "../../guests.service"; @QueryHandler(GetGuestQuery) -- 2.45.2 From e2cf582b9fd15d0ec09e1455de96b0dd843b170a Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 13 Nov 2024 19:03:59 +0400 Subject: [PATCH 20/32] don't show keyboard after card selection --- .../command/remove-card-from-user.handler.ts | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/guests/command/remove-card-from-user.handler.ts b/src/guests/command/remove-card-from-user.handler.ts index 5505a3d..b1f07cf 100644 --- a/src/guests/command/remove-card-from-user.handler.ts +++ b/src/guests/command/remove-card-from-user.handler.ts @@ -20,36 +20,36 @@ export class RemoveCardFromUserCommandHandler implements ICommandHandler { - const guest = await this.guestService.findById(command.telegramId); - const data = await this.sharedService.getConfig(`buttons_${command.telegramId}`); - const extra = { - reply_markup: { - remove_keyboard: false, - keyboard: [], - }, - }; - const buttons = JSON.parse(data.value); - let found = false; - buttons.reply_markup.keyboard.forEach((item) => { - if (item[0].text.includes(command.card.description) && !found) { - found = true; - } else { - extra.reply_markup.keyboard.push( - [ { ...item[0] } ] - ) - } - }); - if (extra.reply_markup.keyboard.length === 0) { - extra.reply_markup.remove_keyboard = true; - } - await this.sharedService.setConfig(`buttons_${command.telegramId}`, JSON.stringify(extra)); - this.telegramService.emit({ - cmd: CommandsConsts.SendMessage, - }, { - chatId: guest.chatId, - message: Messages.SELECT_CARD, - extra: extra, - }) + // const guest = await this.guestService.findById(command.telegramId); + // const data = await this.sharedService.getConfig(`buttons_${command.telegramId}`); + // const extra = { + // reply_markup: { + // remove_keyboard: false, + // keyboard: [], + // }, + // }; + // const buttons = JSON.parse(data.value); + // let found = false; + // buttons.reply_markup.keyboard.forEach((item) => { + // if (item[0].text.includes(command.card.description) && !found) { + // found = true; + // } else { + // extra.reply_markup.keyboard.push( + // [ { ...item[0] } ] + // ) + // } + // }); + // if (extra.reply_markup.keyboard.length === 0) { + // extra.reply_markup.remove_keyboard = true; + // } + // await this.sharedService.setConfig(`buttons_${command.telegramId}`, JSON.stringify(extra)); + // this.telegramService.emit({ + // cmd: CommandsConsts.SendMessage, + // }, { + // chatId: guest.chatId, + // message: Messages.SELECT_CARD, + // extra: extra, + // }) } } \ No newline at end of file -- 2.45.2 From 0b28aff726404320d434e5fade31e0bcfc5557f0 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Fri, 15 Nov 2024 11:46:07 +0400 Subject: [PATCH 21/32] bugfixes --- src/game/comand-handlers/select-target-player.handler.ts | 7 +++++-- src/game/commands/select-target-player.command.ts | 2 +- src/game/entities/cards.entities.ts | 6 ++---- src/voice/voice.controller.ts | 1 - 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/game/comand-handlers/select-target-player.handler.ts b/src/game/comand-handlers/select-target-player.handler.ts index 6e92112..cc03a48 100644 --- a/src/game/comand-handlers/select-target-player.handler.ts +++ b/src/game/comand-handlers/select-target-player.handler.ts @@ -18,18 +18,21 @@ export class SelectTargetPlayerHandler implements ICommandHandler { this.logger.verbose('enter'); - //const user = await this.guestService.findById(command.player); - const allUsers = await this.guestService.findAll(); + let allUsers = await this.guestService.findAll(); const user = allUsers.find(x => x.telegramId === command.player); if(!user) { throw new Error(`Cant find current user ${command.player}`); } + if(!command.allowSelf) { + allUsers = allUsers.filter((x) => x.telegramId !== command.player); + } const buttons = allUsers.map((x) => { return [{ text: `${Messages.EMOJI_PLAYER} ${x.name}`, callback_data: `{ "card": "${command.debuffName}", "value": "${command.value}", "user": "${x.telegramId}" }` }] }); + console.log(buttons); this.telegramService.send( { cmd: CommandsConsts.SendMessage}, diff --git a/src/game/commands/select-target-player.command.ts b/src/game/commands/select-target-player.command.ts index 3ef7950..6e6f76d 100644 --- a/src/game/commands/select-target-player.command.ts +++ b/src/game/commands/select-target-player.command.ts @@ -1,4 +1,4 @@ export class SelectTargetPlayerCommand { - constructor(public player,public debuffName: string, public value: string|number) { + constructor(public player,public debuffName: string, public value: string|number, public allowSelf = true) { } } \ No newline at end of file diff --git a/src/game/entities/cards.entities.ts b/src/game/entities/cards.entities.ts index 27f2b44..7bafa9f 100644 --- a/src/game/entities/cards.entities.ts +++ b/src/game/entities/cards.entities.ts @@ -194,18 +194,16 @@ export class VersusCard extends GameCard { this.logger.verbose(`versusInProgress ${versusInProgress}`); if(versusInProgress) { this.logger.warn(`another versus in progress`); - return; } const destUser = await queryBus.execute(new GetGuestQuery(r.dest)) const sourceUser = await queryBus.execute(new GetGuestQuery(r.from)); await commandBus.execute(new BeginVersusCommand(sourceUser.telegramId, destUser.telegramId)); }); - //eventBus.pipe(ofType(DebuffCardPlayedEvent)).subscribe(r => console.log(r)); } async handle() { await this.commandBus.execute( - new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.versus, 0) + new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.versus, 0, false) ) await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null)); this.eventBus.subscribe((data) =>{ @@ -252,7 +250,7 @@ export class BanPlayer extends GameCard { async handle() { await this.commandBus.execute( - new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, 2) + new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, 2,false) ) await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null)); this.eventBus.subscribe((data) =>{ diff --git a/src/voice/voice.controller.ts b/src/voice/voice.controller.ts index 32452a3..ce69d53 100644 --- a/src/voice/voice.controller.ts +++ b/src/voice/voice.controller.ts @@ -16,7 +16,6 @@ export class VoiceController { }, 10000); this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => { this.voiceEnabled = !r.state; - console.log(`Voice enabled: ${this.voiceEnabled}`); }); } -- 2.45.2 From 7030a19b0367458ac6876463e5518ad33fccffbd Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Mon, 18 Nov 2024 12:09:24 +0400 Subject: [PATCH 22/32] add loser to stat versus --- src/Consts/guest-property-names.consts.ts | 4 +++ src/game/versus/versus.controller.ts | 4 +-- src/game/versus/versus.service.spec.ts | 4 ++- src/game/versus/versus.service.ts | 34 +++++++++++++++---- .../command/get-guest-property.handler.ts | 3 +- 5 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 src/Consts/guest-property-names.consts.ts diff --git a/src/Consts/guest-property-names.consts.ts b/src/Consts/guest-property-names.consts.ts new file mode 100644 index 0000000..33d1b60 --- /dev/null +++ b/src/Consts/guest-property-names.consts.ts @@ -0,0 +1,4 @@ +export class GuestPropertyNamesConsts { + static VersusWonCount = 'versusWonCount'; + static VersusLoseCount = 'versusLoseCount'; +} \ No newline at end of file diff --git a/src/game/versus/versus.controller.ts b/src/game/versus/versus.controller.ts index 7d6cb7c..e5c3ada 100644 --- a/src/game/versus/versus.controller.ts +++ b/src/game/versus/versus.controller.ts @@ -26,8 +26,8 @@ export class VersusController { } @Post('complete') - async Completed(@Body() payload: { winner: number }) { - return await this.versusService.complete(payload.winner); + async Completed(@Body() payload: { winner: number, loser: number }) { + return await this.versusService.complete(payload.winner, payload.loser); } @Post('reset-all') diff --git a/src/game/versus/versus.service.spec.ts b/src/game/versus/versus.service.spec.ts index f5f06a8..b3725db 100644 --- a/src/game/versus/versus.service.spec.ts +++ b/src/game/versus/versus.service.spec.ts @@ -6,7 +6,8 @@ import {SharedService} from "../../shared/shared.service"; import {getModelToken} from "@nestjs/mongoose"; import {Versus} from "../../schemas/versus.schema"; import {Model} from "mongoose"; -import {CommandBus} from "@nestjs/cqrs"; +import {CommandBus, QueryBus} from "@nestjs/cqrs"; +import {QueryBusMock} from "../../mocks/querybus.mock"; describe('VersusService', () => { let service: VersusService; @@ -19,6 +20,7 @@ describe('VersusService', () => { { provide: SharedService, useValue: SharedService }, { provide: getModelToken(Versus.name), useValue: Model }, { provide: CommandBus, useValue: CommandBus }, + { provide: QueryBus, useValue: QueryBusMock }, ], }).compile(); diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts index b554d5f..715e9ef 100644 --- a/src/game/versus/versus.service.ts +++ b/src/game/versus/versus.service.ts @@ -4,11 +4,15 @@ import {GuestsService} from "../../guests/guests.service"; import {SharedService} from "../../shared/shared.service"; import {InjectModel} from "@nestjs/mongoose"; import {Versus, VersusDocument} from "../../schemas/versus.schema"; -import {Model} from "mongoose"; +import {Model, Query} from "mongoose"; import {VersusDto} from "./versus.types"; -import {CommandBus} from "@nestjs/cqrs"; +import {CommandBus, QueryBus} from "@nestjs/cqrs"; import {IncreasePlayerScoreCommand} from "../../guests/command/increase-player-score.command"; import {IncreasePlayerWinningRateCommand} from "../commands/increase-player-winning-rate.command"; +import {GetGuestQuery} from "../../guests/queries/getguest.query"; +import {GetGuestPropertyQuery} from "../../guests/command/get-guest-property.handler"; +import {GuestPropertyNamesConsts} from "../../Consts/guest-property-names.consts"; +import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command"; @Injectable() export class VersusService { @@ -18,6 +22,7 @@ export class VersusService { constructor( private guestService: GuestsService, private sharedService: SharedService, + private queryBus: QueryBus, @InjectModel(Versus.name) private versusModel: Model, private cmdBus: CommandBus, ) { @@ -81,14 +86,31 @@ export class VersusService { return { result: true }; } - async complete(winner: number) { + async complete(winner: number, loser: number) { const activeVersus = await this.sharedService.getConfig(VersusService.configKeyActiveVersus); const item = await this.versusModel.findOne({ _id: activeVersus.value }).exec(); item.completed = true; await item.save(); - await this.cmdBus.execute(new IncreasePlayerScoreCommand(winner, 2)); - await this.cmdBus.execute(new IncreasePlayerWinningRateCommand(winner, 30)); - await this.sharedService.setConfig(VersusService.configKeyCurrentAction, ''); + const tasks = []; + tasks.push(this.cmdBus.execute(new IncreasePlayerScoreCommand(winner, 2))); + tasks.push(this.cmdBus.execute(new IncreasePlayerWinningRateCommand(winner, 30))); + tasks.push(this.sharedService.setConfig(VersusService.configKeyCurrentAction, '')); + let wonCount = await this.queryBus.execute(new GetGuestPropertyQuery(winner, GuestPropertyNamesConsts.VersusWonCount)); + let loseCount = await this.queryBus.execute(new GetGuestPropertyQuery(loser, GuestPropertyNamesConsts.VersusLoseCount)); + if(!wonCount) { + wonCount = 0; + } else { + wonCount = +wonCount++; + } + if(!loseCount) { + loseCount = 0; + } else { + loseCount = +loseCount++; + } + this.logger.verbose(`Set loseCount for ${loser} to ${loseCount}`); + this.logger.verbose(`Set win count for ${winner} to ${wonCount}`); + tasks.push(await this.cmdBus.execute(new SetGuestPropertyCommand(winner, GuestPropertyNamesConsts.VersusWonCount, wonCount.toString))); + await Promise.all(tasks); this.sharedService.sendSocketNotificationToAllClients( SocketEvents.END_VERSUS, { winner: winner } diff --git a/src/guests/command/get-guest-property.handler.ts b/src/guests/command/get-guest-property.handler.ts index 075d87f..19fdcac 100644 --- a/src/guests/command/get-guest-property.handler.ts +++ b/src/guests/command/get-guest-property.handler.ts @@ -15,9 +15,8 @@ export class GetGuestPropertyHandler implements ICommandHandler { this.logger.verbose(`entering`); const guest = await this.guestService.findById(command.user); - console.log(command); + this.logger.verbose(command); if(!command.property.startsWith('properties.')) { - command.property = `properties.${command.property}`; this.logger.warn(`update prop ${command.property}`); } -- 2.45.2 From d18d12a458e0a7b0ae852e4768bd97394a2921e1 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 20 Nov 2024 14:24:46 +0400 Subject: [PATCH 23/32] refact --- src/Consts/game-state.consts.ts | 5 ++ src/Consts/types.d.ts | 65 +++++++++++++++++++ src/featureflag/featureflag.service.ts | 7 +- .../proceed-game-queue-command.handler.ts | 38 +++++------ src/game/game.service.ts | 34 +++------- src/game/versus/versus.service.ts | 25 +++---- src/messaging/guests.controller.ts | 6 +- src/mocks/shared-service.mock.ts | 2 +- src/quiz/quiz.controller.ts | 2 +- src/quiz/quiz.service.ts | 10 +-- src/scheduler/scheduler.service.spec.ts | 4 +- src/scheduler/scheduler.service.ts | 25 ++----- src/shared/events.consts.ts | 18 ----- src/shared/shared.service.ts | 17 +++-- .../notify-card-played-command.handler.ts | 14 ++-- .../send-toast-command-handler.ts | 20 +++--- .../cards-set-changed-event.handler.ts | 17 +++-- .../cards-updated.event.handler.ts | 19 +++--- .../score-changed-event.handler.ts | 20 +++--- .../user-property-changed-event.handler.ts | 14 ++-- .../user-registered.event.handler.ts | 20 +++--- .../valid-answer-received-event.handler.ts | 20 +++--- src/socket/socket.gateway.ts | 24 ++++++- src/state/state.controller.ts | 12 ++-- src/state/state.service.ts | 5 +- 25 files changed, 249 insertions(+), 194 deletions(-) create mode 100644 src/Consts/game-state.consts.ts create mode 100644 src/Consts/types.d.ts delete mode 100644 src/shared/events.consts.ts diff --git a/src/Consts/game-state.consts.ts b/src/Consts/game-state.consts.ts new file mode 100644 index 0000000..02384c0 --- /dev/null +++ b/src/Consts/game-state.consts.ts @@ -0,0 +1,5 @@ +export class GameStateConsts { + static Main = 'main'; + static EndgamePoints = 'endgamepoints'; + static Finish = 'finish'; +} \ No newline at end of file diff --git a/src/Consts/types.d.ts b/src/Consts/types.d.ts new file mode 100644 index 0000000..76ba488 --- /dev/null +++ b/src/Consts/types.d.ts @@ -0,0 +1,65 @@ +import {GameQueueTypes} from "../schemas/game-queue.schema"; + +export interface IStateInfo { + state:string; + value:string; +} + +export interface IValidAnswerReceivedSocketEvent { + telegramId: number; + validAnswer: string; + note: string; +} + +export interface IUserInfoMinimal { + telegramId: number; +} + +export interface IUserBasicInfo extends IUserInfoMinimal { + name: string; +} + +export interface IVersusBeginSocketEvent { + player1: number; + player2: number; + player1name: string; + player2name: string; +} + +export interface IVersusEndSocketEvent { + winner: number; +} + +export interface IScoreChangedSocketEvent extends IUserInfoMinimal { + newScore: number; +} + +export interface IUserCardChangedEvent extends IUserInfoMinimal { + cards: string[]; +} + +export interface IEmptyNotification {} + +export interface ISocketNotificationEvent { + text: string; + timeout: number; +} + +export interface IUserPropertyChangedEvent { + user: number; + property: string; + value: string; +} + +export interface ICardPlayedSocketEvent extends IUserInfoMinimal{ + card: string; + name: string; + timeout: number; +} +export interface IGameQueueSocketEvent { + _id: any; + completed: boolean; + target: number; + type: GameQueueTypes; + text: string; +} \ No newline at end of file diff --git a/src/featureflag/featureflag.service.ts b/src/featureflag/featureflag.service.ts index a968439..f3360b0 100644 --- a/src/featureflag/featureflag.service.ts +++ b/src/featureflag/featureflag.service.ts @@ -1,6 +1,6 @@ import {Injectable, Logger} from '@nestjs/common'; import {SharedService} from "../shared/shared.service"; -import {SocketEvents} from "../shared/events.consts"; +import {ClientNotificationType} from "../socket/socket.gateway"; export interface IFeatureFlagStatus { name: string; @@ -32,11 +32,12 @@ export class FeatureflagService { async setFeatureFlag(id: string, status: boolean) : Promise { this.logger.verbose(`Setting feature flag status for ${id} to ${status} `); const result = await this.sharedService.setConfig(`featureflag/${id}`, status.toString()); - this.sharedService.sendSocketNotificationToAllClients(SocketEvents.FEATURE_FLAG_CHANGED, {}); - return { + const ffStatus: IFeatureFlagStatus = { name: id, state: result.value !== 'false', } + this.sharedService.notifyAllClients(ClientNotificationType.FeatureFlagChanged, ffStatus); + return ffStatus; } } diff --git a/src/game/comand-handlers/proceed-game-queue-command.handler.ts b/src/game/comand-handlers/proceed-game-queue-command.handler.ts index 1732821..28dcd2c 100644 --- a/src/game/comand-handlers/proceed-game-queue-command.handler.ts +++ b/src/game/comand-handlers/proceed-game-queue-command.handler.ts @@ -1,13 +1,14 @@ -import { CommandBus, CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; -import { ProceedGameQueueCommand } from '../commands/proceed-game-queue.command'; -import { GameService } from '../game.service'; -import { NextQuestionCommand } from '../commands/next-question.command'; -import { SharedService } from '../../shared/shared.service'; -import { SocketEvents } from '../../shared/events.consts'; -import { Logger } from '@nestjs/common'; -import { GameQueueTypes } from '../../schemas/game-queue.schema'; -import { QuizAnswerStateChangedEvent } from '../events/quiz-answer-state-changed.event'; -import { QuizAnswerStateEnum } from '../entities/quiz-answer-state.enum'; +import {CommandBus, CommandHandler, EventBus, ICommandHandler} from '@nestjs/cqrs'; +import {ProceedGameQueueCommand} from '../commands/proceed-game-queue.command'; +import {GameService} from '../game.service'; +import {NextQuestionCommand} from '../commands/next-question.command'; +import {SharedService} from '../../shared/shared.service'; +import {Logger} from '@nestjs/common'; +import {GameQueueTypes} from '../../schemas/game-queue.schema'; +import {QuizAnswerStateChangedEvent} from '../events/quiz-answer-state-changed.event'; +import {QuizAnswerStateEnum} from '../entities/quiz-answer-state.enum'; +import {IGameQueueSocketEvent} from "../../Consts/types"; +import {ClientNotificationType} from "../../socket/socket.gateway"; @CommandHandler(ProceedGameQueueCommand) export class GameProceedGameQueueCommandHandler @@ -25,16 +26,13 @@ export class GameProceedGameQueueCommandHandler if (!item) { return this.cmdBus.execute(new NextQuestionCommand()); } - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.GameQueueItem, - { - _id: item.id, - completed: item.completed, - target: item.target, - type: item.type, - text: item.text - }, - ); + this.sharedService.notifyAllClients(ClientNotificationType.GameQueueItem, { + _id: item.id, + completed: item.completed, + target: item.target, + type: item.type, + text: item.text + }); switch (item.type) { case GameQueueTypes.giveOutAPrize: this.eventBus.publish( diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 5c85159..ae416d3 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -1,19 +1,15 @@ import {Injectable, InternalServerErrorException, Logger, OnApplicationBootstrap} from '@nestjs/common'; import {CommandBus, EventBus, QueryBus} from '@nestjs/cqrs'; -import { CardSelectionTimeExceedCommand } from './commands/card-selection-time-exceed.command'; -import { InjectModel } from '@nestjs/mongoose'; -import { - GameQueue, - GameQueueDocument, - GameQueueTypes, -} from '../schemas/game-queue.schema'; +import {CardSelectionTimeExceedCommand} from './commands/card-selection-time-exceed.command'; +import {InjectModel} from '@nestjs/mongoose'; +import {GameQueue, GameQueueDocument, GameQueueTypes,} from '../schemas/game-queue.schema'; import {Model, Promise} from 'mongoose'; -import { ProceedGameQueueCommand } from './commands/proceed-game-queue.command'; -import { SharedService } from '../shared/shared.service'; -import { SocketEvents } from '../shared/events.consts'; +import {ProceedGameQueueCommand} from './commands/proceed-game-queue.command'; +import {SharedService} from '../shared/shared.service'; import {ConfigService} from "@nestjs/config"; import {gameCards} from "./entities/cards.entities"; -import {GuestsService} from "../guests/guests.service"; +import {IEmptyNotification} from "../Consts/types"; +import {ClientNotificationType} from "../socket/socket.gateway"; @Injectable() export class GameService implements OnApplicationBootstrap{ @@ -27,7 +23,6 @@ export class GameService implements OnApplicationBootstrap{ private sharedService: SharedService, private commandBus: CommandBus, private queryBus: QueryBus, - private guestService: GuestsService, ) { } @@ -72,29 +67,20 @@ export class GameService implements OnApplicationBootstrap{ } qItem.completed = true; await qItem.save(); - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.QUEUE_COMPLETED, - {}, - ); + this.sharedService.notifyAllClients(ClientNotificationType.QueueCompleted, {}); await this.cmdBus.execute(new ProceedGameQueueCommand()); return qItem; } async pauseGame() { await this.sharedService.setConfig('game_state', 'paused'); - await this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.GAME_PAUSED, - {}, - ); + this.sharedService.notifyAllClients(ClientNotificationType.GamePaused, {}); return Promise.resolve({ result: true }); } async resumeGame() { await this.sharedService.setConfig('game_state', 'running'); - await this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.GAME_RESUMED, - {}, - ); + this.sharedService.notifyAllClients(ClientNotificationType.GameResumed,{}); return Promise.resolve({ result: true }); } diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts index 715e9ef..cf8ede2 100644 --- a/src/game/versus/versus.service.ts +++ b/src/game/versus/versus.service.ts @@ -1,18 +1,18 @@ import {Injectable, Logger} from '@nestjs/common'; -import {SocketEvents} from "../../shared/events.consts"; import {GuestsService} from "../../guests/guests.service"; import {SharedService} from "../../shared/shared.service"; import {InjectModel} from "@nestjs/mongoose"; import {Versus, VersusDocument} from "../../schemas/versus.schema"; -import {Model, Query} from "mongoose"; +import {Model} from "mongoose"; import {VersusDto} from "./versus.types"; import {CommandBus, QueryBus} from "@nestjs/cqrs"; import {IncreasePlayerScoreCommand} from "../../guests/command/increase-player-score.command"; import {IncreasePlayerWinningRateCommand} from "../commands/increase-player-winning-rate.command"; -import {GetGuestQuery} from "../../guests/queries/getguest.query"; import {GetGuestPropertyQuery} from "../../guests/command/get-guest-property.handler"; import {GuestPropertyNamesConsts} from "../../Consts/guest-property-names.consts"; import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command"; +import {IVersusBeginSocketEvent, IVersusEndSocketEvent} from "../../Consts/types"; +import {ClientNotificationType} from "../../socket/socket.gateway"; @Injectable() export class VersusService { @@ -51,10 +51,12 @@ export class VersusService { player2name: p2data.name, } })); - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.BEGIN_VERSUS, - { player1, player2, player1name: p1data.name, player2name: p2data.name } - ) + this.sharedService.notifyAllClients(ClientNotificationType.BeginVersus, { + player1, + player2, + player1name: p1data.name, + player2name: p2data.name + }); } async importVersus(data: VersusDto[]) { @@ -111,11 +113,10 @@ export class VersusService { this.logger.verbose(`Set win count for ${winner} to ${wonCount}`); tasks.push(await this.cmdBus.execute(new SetGuestPropertyCommand(winner, GuestPropertyNamesConsts.VersusWonCount, wonCount.toString))); await Promise.all(tasks); - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.END_VERSUS, - { winner: winner } - ) - + this.sharedService.notifyAllClients(ClientNotificationType.EndVersus, { + winner: winner + } + ); return item; } diff --git a/src/messaging/guests.controller.ts b/src/messaging/guests.controller.ts index 38cb32f..bd4794f 100644 --- a/src/messaging/guests.controller.ts +++ b/src/messaging/guests.controller.ts @@ -1,15 +1,15 @@ import {Controller, Logger} from "@nestjs/common"; -import {Ctx, EventPattern, MessagePattern, Payload, RmqContext} from "@nestjs/microservices"; +import {Ctx, MessagePattern, Payload, RmqContext} from "@nestjs/microservices"; import {GetGuestInfoModel} from "./models/get-guest-info.model"; import {GuestsService} from "../guests/guests.service"; import {RegisterUserModel} from "./models/register-user.model"; import {SharedService} from "../shared/shared.service"; -import {SocketEvents} from "../shared/events.consts"; import {CommandsConsts} from "../Consts/commands.consts"; import {EventBus} from "@nestjs/cqrs"; import {PlayerCardSelectedEvent} from "../game/events/player-card-selected.event"; import {getCard} from "../helpers/card-parser"; import {QuizService} from "../quiz/quiz.service"; +import {ClientNotificationType} from "../socket/socket.gateway"; @Controller() export class GuestsMessageController { @@ -49,7 +49,7 @@ export class GuestsMessageController { @MessagePattern({ cmd: CommandsConsts.PhotoUpdated }) async photoUpdated(@Payload() data: { id: number}) { this.logger.verbose(`Photo updated event, send notification`); - this.sharedService.sendSocketNotificationToAllClients(SocketEvents.PHOTOS_UPDATED_EVENT, data); + this.sharedService.notifyAllClients<{id: number}>(ClientNotificationType.PhotosUpdated, data) } @MessagePattern({ cmd: CommandsConsts.CardPlayed }) diff --git a/src/mocks/shared-service.mock.ts b/src/mocks/shared-service.mock.ts index ab35373..1d54e71 100644 --- a/src/mocks/shared-service.mock.ts +++ b/src/mocks/shared-service.mock.ts @@ -1,5 +1,5 @@ export const SharedServiceMock = { setConfig: jest.fn(), getConfig: jest.fn(), - sendSocketNotificationToAllClients: jest.fn(), + notifyAllClients: jest.fn(), } \ No newline at end of file diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index 5c251be..c5b1d53 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -51,7 +51,7 @@ export class QuizController { return this.quizService.dealPrize(); } - @Get('endgame-extrapoints') + @Post('calculate-endgame-extrapoints') async endgameExtrapoints() { return await this.quizService.calculateEndgamePoints(); diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 2c29058..f9e6a8d 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -19,6 +19,7 @@ import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-scor import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; import {QuizEndGameResults} from "./quiz.types"; +import {ClientNotificationType} from "../socket/socket.gateway"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { @@ -48,10 +49,7 @@ export class QuizService { await item.save(); this.logger.verbose(`Question updated`); await this.guestService.postQuestion(questionDto, target); - this.sharedService.sendSocketNotificationToAllClients( - 'question_changed', - questionDto, - ); + this.sharedService.notifyAllClients(ClientNotificationType.QuestionChanged, questionDto); return item.save(); } @@ -171,7 +169,7 @@ export class QuizService { //const { maxRewards, maxInvalidAnswers } = Promise.all([maxRewardsPromise, maxInvalidAnswersPromise]); const [maxRewards, maxInvalidAnswers, maxPenaltiesReceived] = await Promise.all([maxRewardsPromise, maxInvalidAnswersPromise, maxPenaltiesPromise]); - return { + const result = { maxInvalidAnswers: { id: maxInvalidAnswers[0].id, count: maxInvalidAnswers[0].invalidAnswers, @@ -188,6 +186,8 @@ export class QuizService { name: maxPenaltiesReceived[0].name, } } + await this.sharedService.setConfig('endgame-points', JSON.stringify(result)); + return result; } private async getNextQuestion() { diff --git a/src/scheduler/scheduler.service.spec.ts b/src/scheduler/scheduler.service.spec.ts index 5329357..135f68c 100644 --- a/src/scheduler/scheduler.service.spec.ts +++ b/src/scheduler/scheduler.service.spec.ts @@ -49,7 +49,7 @@ describe('SchedulerService', () => { it('should finish game if prizes count is 0', async () => { const getRemainingPrizeCountFn = jest.spyOn(giftService, 'getRemainingPrizeCount').mockImplementation(() => Promise.resolve(0)); - const notificationFn = jest.spyOn(sharedService,'sendSocketNotificationToAllClients').mockImplementation(); + const notificationFn = jest.spyOn(sharedService,'notifyAllClients').mockImplementation(); const setStateFn = jest.spyOn(stateService,'setState').mockImplementation((name,newstate) => Promise.resolve({ state: name, value: newstate})); await service.gameStatus(); expect(getRemainingPrizeCountFn).toHaveBeenCalled(); @@ -59,7 +59,7 @@ describe('SchedulerService', () => { it('should not finish game if prizes count above 0', async () => { const getRemainingPrizeCountFn = jest.spyOn(giftService, 'getRemainingPrizeCount').mockImplementation(() => Promise.resolve(5)); - const notificationFn = jest.spyOn(sharedService,'sendSocketNotificationToAllClients').mockImplementation() + const notificationFn = jest.spyOn(sharedService,'notifyAllClients').mockImplementation() await service.gameStatus(); expect(notificationFn).not.toHaveBeenCalled(); }); diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts index 175a4a5..c6316a1 100644 --- a/src/scheduler/scheduler.service.ts +++ b/src/scheduler/scheduler.service.ts @@ -7,9 +7,9 @@ import {SharedService} from '../shared/shared.service'; import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; import {CommandBus} from "@nestjs/cqrs"; -import {EndgameDict} from "../voice/dicts/endgame.dict"; -import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; -import {GameQueueTypes} from "../schemas/game-queue.schema"; +import {GameStateConsts} from "../Consts/game-state.consts"; +import {IStateInfo} from "../Consts/types"; +import {ClientNotificationType} from "../socket/socket.gateway"; @Injectable() export class SchedulerService { @@ -37,24 +37,11 @@ export class SchedulerService { if(await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.EnableEndgamePoints)) { this.logger.verbose(`Feature flag ${FeatureFlagsConsts.EnableEndgamePoints} is enabled`); const endgamePoints = await this.quizService.calculateEndgamePoints(); - await Promise.all([ - this.commandBus.execute( - new CreateNewQueueItemCommand(endgamePoints.maxInvalidAnswers.id, - GameQueueTypes.extra_points, - EndgameDict.maxAmountOfInvalidQuestions)), - new CreateNewQueueItemCommand(endgamePoints.maxPenalties.id, - GameQueueTypes.extra_points, - EndgameDict.maxPenalties), - new CreateNewQueueItemCommand(endgamePoints.maxRewards.id, - GameQueueTypes.extra_points, - EndgameDict.maxAmountOfRewards) - ]); + const state = await this.stateService.setState(GameStateConsts.Main, GameStateConsts.EndgamePoints); + this.sharedService.notifyAllClients(ClientNotificationType.StateChanged, state); } else { const state = await this.stateService.setState('main', 'finish'); - this.sharedService.sendSocketNotificationToAllClients( - 'state_changed', - state, - ); + this.sharedService.notifyAllClients(ClientNotificationType.StateChanged, state); this.logger.warn(`Gifts is ended, finishing game`); } } diff --git a/src/shared/events.consts.ts b/src/shared/events.consts.ts deleted file mode 100644 index 37781fe..0000000 --- a/src/shared/events.consts.ts +++ /dev/null @@ -1,18 +0,0 @@ -export enum SocketEvents { - PHOTOS_UPDATED_EVENT = 'photos_updated', - VALID_ANSWER_RECEIVED = 'answer_received', - WRONG_ANSWER_RECEIVED = 'wrong_answer_received', - USER_ADDED = 'user_added', - USER_PROPERTY_CHANGED = 'user_property_changed', - CARDS_CHANGED_EVENT = 'cards_changed', - CARD_PLAYED = 'card_played', - SCORE_CHANGED = 'score_changed', - GameQueueItem = 'game_queue', - QUEUE_COMPLETED = 'queue_completed', - GAME_PAUSED = 'game_paused', - GAME_RESUMED = 'game_resumed', - NOTIFICATION = 'notification', - FEATURE_FLAG_CHANGED = 'feature_flag_changed', - BEGIN_VERSUS = 'begin_versus', - END_VERSUS = 'end_versus', -} diff --git a/src/shared/shared.service.ts b/src/shared/shared.service.ts index 99fd64d..8c044c8 100644 --- a/src/shared/shared.service.ts +++ b/src/shared/shared.service.ts @@ -1,4 +1,4 @@ -import { SocketGateway } from '../socket/socket.gateway'; +import {ClientNotificationType, SocketGateway} from '../socket/socket.gateway'; import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Config, ConfigDocument } from '../schemas/config.schema'; @@ -55,14 +55,19 @@ export class SharedService { } /** - * Notifies all connected socket about changes - * @deprecated Use specific handlers in this class - * @param event - * @param payload + * Notifies all connected clients via the socket gateway with the given event and payload. + * + * @template T - The type of the payload. + * + * @param event - The event name to be sent to the clients. + * @param payload - The data to be sent along with the event. + * + * @returns {void} - This function does not return any value. */ - sendSocketNotificationToAllClients(event: string, payload?: any) { + notifyAllClients(event: ClientNotificationType, payload: T): void { this.logger.verbose(`Sending notification to client: ${event}, ${JSON.stringify(payload)}`); this.socketGateway.notifyAllClients(event, payload); } + } diff --git a/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts b/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts index 2650579..1eb26ad 100644 --- a/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts +++ b/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts @@ -1,8 +1,9 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { NotifyCardOnScreenCommand } from '../../../game/commands/notify-card-on-screen-command'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; -import { Logger } from '@nestjs/common'; +import {CommandHandler, ICommandHandler} from '@nestjs/cqrs'; +import {NotifyCardOnScreenCommand} from '../../../game/commands/notify-card-on-screen-command'; +import {SharedService} from '../../../shared/shared.service'; +import {Logger} from '@nestjs/common'; +import {ICardPlayedSocketEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @CommandHandler(NotifyCardOnScreenCommand) export class NotifyCardPlayedCommandHandler @@ -12,8 +13,7 @@ export class NotifyCardPlayedCommandHandler } async execute(command: NotifyCardOnScreenCommand): Promise { this.logger.log(`Notify about card`); - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.CARD_PLAYED, { + this.sharedService.notifyAllClients(ClientNotificationType.CardPlayed, { telegramId: command.telegramId, card: command.card.description, name: command.card.name, diff --git a/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts b/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts index e0b3bc3..38668fd 100644 --- a/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts +++ b/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts @@ -1,8 +1,9 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { SendToastCommand } from '../../../game/commands/send-toast.command'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {CommandHandler, ICommandHandler} from '@nestjs/cqrs'; +import {SendToastCommand} from '../../../game/commands/send-toast.command'; +import {SharedService} from '../../../shared/shared.service'; import {Logger} from "@nestjs/common"; +import {ISocketNotificationEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @CommandHandler(SendToastCommand) export class SendToastCommandHandler implements ICommandHandler { @@ -11,12 +12,9 @@ export class SendToastCommandHandler implements ICommandHandler(ClientNotificationType.Notification, { + text: command.text, + timeout: command.timeout, + }); } } diff --git a/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts b/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts index 84d8df3..d164717 100644 --- a/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts @@ -1,17 +1,16 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { CardsSetChangedEvent } from '../../../game/events/cards-events/cards-set-changed.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {CardsSetChangedEvent} from '../../../game/events/cards-events/cards-set-changed.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IUserInfoMinimal} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(CardsSetChangedEvent) export class CardsSetChangedEventHandler implements IEventHandler { constructor(private sharedService: SharedService) {} - handle(event: CardsSetChangedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.CARDS_CHANGED_EVENT, - { telegramId: event.telegramId }, - ); + handle(event: CardsSetChangedEvent): void { + this.sharedService + .notifyAllClients(ClientNotificationType.CardsChanged, { telegramId: event.telegramId}) } } diff --git a/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts b/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts index f4ebbc5..44a2550 100644 --- a/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts @@ -1,18 +1,17 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { CardsDealedEvent } from '../../../game/events/cards-dealed.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {CardsDealedEvent} from '../../../game/events/cards-dealed.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IUserCardChangedEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(CardsDealedEvent) export class CardsUpdatedEventHandler implements IEventHandler { constructor(private sharedService: SharedService) { } - handle(event: CardsDealedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.CARDS_CHANGED_EVENT, - { - telegramId: event.telegramId, - cards: event.cards, + handle(event: CardsDealedEvent): void { + this.sharedService.notifyAllClients(ClientNotificationType.CardsChanged, { + telegramId: event.telegramId, + cards: event.cards, }); } } diff --git a/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts b/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts index 0238771..4987d41 100644 --- a/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts @@ -1,7 +1,8 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { ScoreChangedEvent } from '../../../game/events/score-changed.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {ScoreChangedEvent} from '../../../game/events/score-changed.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IScoreChangedSocketEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(ScoreChangedEvent) export class SocketScoreChangedEventHandler @@ -9,12 +10,9 @@ export class SocketScoreChangedEventHandler constructor(private sharedService: SharedService) { } async handle(event: ScoreChangedEvent) { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.SCORE_CHANGED, - { - telegramId: event.telegramId, - newScore: event.newScore, - }, - ); + this.sharedService.notifyAllClients(ClientNotificationType.ScoreChanged, { + telegramId: event.telegramId, + newScore: event.newScore, + }); } } diff --git a/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts b/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts index ff2937b..e384714 100644 --- a/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts @@ -1,17 +1,19 @@ import {EventsHandler, IEventHandler} from "@nestjs/cqrs"; import {UserPropertyChangedEvent} from "../../../guests/event-handlers/user-property-changed.event"; import {SharedService} from "../../../shared/shared.service"; -import {SocketEvents} from "../../../shared/events.consts"; +import {IUserPropertyChangedEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(UserPropertyChangedEvent) export class UserPropertyChangedEventHandler implements IEventHandler { constructor(private sharedService: SharedService) { } - handle(event: UserPropertyChangedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.USER_PROPERTY_CHANGED, - { user: event.user, property: event.property, value: event.propertyValue } - ); + handle(event: UserPropertyChangedEvent): void { + this.sharedService.notifyAllClients(ClientNotificationType.UserPropertyChanged, { + user: event.user, + property: event.property, + value: event.propertyValue + }); } } \ No newline at end of file diff --git a/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts b/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts index 9fc0eb8..ce88775 100644 --- a/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts @@ -1,17 +1,17 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { UserRegisteredEvent } from '../../../game/events/user-registered.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {UserRegisteredEvent} from '../../../game/events/user-registered.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IUserBasicInfo} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; + @EventsHandler(UserRegisteredEvent) export class UserRegisteredEventHandler implements IEventHandler { constructor(private sharedService: SharedService) { } - handle(event: UserRegisteredEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.USER_ADDED, - { - telegramId: event.telegramId, - name: event.name, + handle(event: UserRegisteredEvent): void { + this.sharedService.notifyAllClients(ClientNotificationType.UserAdded, { + telegramId: event.telegramId, + name: event.name, }); } } diff --git a/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts b/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts index 1977df3..9a5dadc 100644 --- a/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts @@ -1,7 +1,8 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { ValidAnswerReceivedEvent } from '../../../game/events/valid-answer.recieved'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {ValidAnswerReceivedEvent} from '../../../game/events/valid-answer.recieved'; +import {SharedService} from '../../../shared/shared.service'; +import {ClientNotificationType} from "../../socket.gateway"; +import {IValidAnswerReceivedSocketEvent} from "../../../Consts/types"; @EventsHandler(ValidAnswerReceivedEvent) export class SocketValidAnswerReceivedEventHandler @@ -11,9 +12,12 @@ export class SocketValidAnswerReceivedEventHandler } handle(event: ValidAnswerReceivedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.VALID_ANSWER_RECEIVED, - { telegramId: event.tId, validAnswer: event.validAnswer, note: event.extraDetails }, - ); + const notification : IValidAnswerReceivedSocketEvent = { + telegramId: event.tId, + validAnswer: event.validAnswer, + note: event.extraDetails + }; + this.sharedService + .notifyAllClients(ClientNotificationType.ValidAnswerReceived, notification); } } diff --git a/src/socket/socket.gateway.ts b/src/socket/socket.gateway.ts index d0562b2..0c3dc48 100644 --- a/src/socket/socket.gateway.ts +++ b/src/socket/socket.gateway.ts @@ -10,6 +10,28 @@ import { Server, Socket } from 'socket.io'; import { Injectable, Logger } from "@nestjs/common"; import { from, map, Observable } from 'rxjs'; + +export const enum ClientNotificationType { + StateChanged = 'state_changed', + PhotosUpdated = 'photos_updated', + ValidAnswerReceived = 'answer_received', + WrongAnswerReceived = 'wrong_answer_received', + UserAdded = 'user_added', + UserPropertyChanged = 'user_property_changed', + CardsChanged = 'cards_changed', + CardPlayed = 'card_played', + ScoreChanged = 'score_changed', + GameQueueItem = 'game_queue', + QueueCompleted = 'queue_completed', + GamePaused = 'game_paused', + GameResumed = 'game_resumed', + Notification = 'notification', + FeatureFlagChanged = 'feature_flag_changed', + BeginVersus = 'begin_versus', + EndVersus = 'end_versus', + QuestionChanged = 'question_changed' +} + @WebSocketGateway({ cors: true, transports: ['websocket']}) @Injectable() export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect{ @@ -47,7 +69,7 @@ export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect{ } - notifyAllClients(event: string, payload: any) { + notifyAllClients(event: ClientNotificationType, payload: any) { this.server.emit("events", { event, data: payload}); // this.logger.warn(`send notification to all clients ${event}`); // this.clients.forEach((c) => { diff --git a/src/state/state.controller.ts b/src/state/state.controller.ts index b06fa96..1ad04e7 100644 --- a/src/state/state.controller.ts +++ b/src/state/state.controller.ts @@ -1,11 +1,13 @@ import {Body, Controller, Get, Inject, Logger, Param, Post} from '@nestjs/common'; -import { StateService } from './state.service'; -import { SharedService } from '../shared/shared.service'; -import { EventBus } from '@nestjs/cqrs'; -import { GameStartedEvent } from '../game/events/game-started.event'; +import {StateService} from './state.service'; +import {SharedService} from '../shared/shared.service'; +import {EventBus} from '@nestjs/cqrs'; +import {GameStartedEvent} from '../game/events/game-started.event'; import {ClientProxy} from "@nestjs/microservices"; import {CommandsConsts} from "../Consts/commands.consts"; import {MqtMessageModel} from "../messaging/models/mqt-message.model"; +import {ClientNotificationType} from "../socket/socket.gateway"; +import {IStateInfo} from "../Consts/types"; interface SetStateDTO { state: string; @@ -50,7 +52,7 @@ export class StateController { this.logger.verbose('reset commands'); this.telegramService.emit({ cmd: CommandsConsts.ResetCommands }, {}); } - this.sharedService.sendSocketNotificationToAllClients('state_changed', res); + this.sharedService.notifyAllClients(ClientNotificationType.StateChanged, res); return res; } } diff --git a/src/state/state.service.ts b/src/state/state.service.ts index feaaaac..783e24d 100644 --- a/src/state/state.service.ts +++ b/src/state/state.service.ts @@ -4,6 +4,7 @@ import { State, StateDocument } from '../schemas/state.schema'; import { Model } from 'mongoose'; import { EventBus } from '@nestjs/cqrs'; import { PrepareGameEvent } from '../game/events/prepare-game.event'; +import {IStateInfo} from "../Consts/types"; interface StateDTO { name: string; @@ -30,7 +31,7 @@ export class StateService { return state; } - async setState(name: string, newValue: string) { + async setState(name: string, newValue: string): Promise { if (newValue === 'onboarding') { this.eventBus.publish(new PrepareGameEvent()); } @@ -40,6 +41,6 @@ export class StateService { return { state: stateEntity.state, value: stateEntity.value, - } + } as IStateInfo } } -- 2.45.2 From 97d6b13541c94392bdc13a60e0ae412093039f8b Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Thu, 21 Nov 2024 00:17:59 +0400 Subject: [PATCH 24/32] refact --- src/game/versus/versus.service.ts | 13 +++++++---- ...est-valid-answer-received-event.handler.ts | 4 ++-- src/guests/guests.service.ts | 4 +++- src/quiz/quiz.controller.ts | 5 +++++ src/quiz/quiz.service.ts | 22 ++++++++++++++----- src/schemas/question.schema.ts | 2 ++ 6 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts index cf8ede2..b4abfaf 100644 --- a/src/game/versus/versus.service.ts +++ b/src/game/versus/versus.service.ts @@ -94,24 +94,26 @@ export class VersusService { item.completed = true; await item.save(); const tasks = []; - tasks.push(this.cmdBus.execute(new IncreasePlayerScoreCommand(winner, 2))); - tasks.push(this.cmdBus.execute(new IncreasePlayerWinningRateCommand(winner, 30))); + tasks.push(this.cmdBus.execute(new IncreasePlayerScoreCommand(winner, 1))); + tasks.push(this.cmdBus.execute(new IncreasePlayerWinningRateCommand(winner, 20))); tasks.push(this.sharedService.setConfig(VersusService.configKeyCurrentAction, '')); let wonCount = await this.queryBus.execute(new GetGuestPropertyQuery(winner, GuestPropertyNamesConsts.VersusWonCount)); let loseCount = await this.queryBus.execute(new GetGuestPropertyQuery(loser, GuestPropertyNamesConsts.VersusLoseCount)); if(!wonCount) { - wonCount = 0; + wonCount = 1; } else { wonCount = +wonCount++; } if(!loseCount) { - loseCount = 0; + loseCount = 1; } else { loseCount = +loseCount++; } this.logger.verbose(`Set loseCount for ${loser} to ${loseCount}`); this.logger.verbose(`Set win count for ${winner} to ${wonCount}`); tasks.push(await this.cmdBus.execute(new SetGuestPropertyCommand(winner, GuestPropertyNamesConsts.VersusWonCount, wonCount.toString))); + tasks.push(await this.cmdBus.execute(new SetGuestPropertyCommand(loser, GuestPropertyNamesConsts.VersusWonCount, loseCount.toString))); + await Promise.all(tasks); this.sharedService.notifyAllClients(ClientNotificationType.EndVersus, { winner: winner @@ -123,6 +125,9 @@ export class VersusService { async checkIfAnotherVersusInProgress() { this.logger.debug(`checkIfAnotherVersusInProgress enter`) const currentAction = await this.sharedService.getConfig(VersusService.configKeyCurrentAction); + if(!currentAction) { + return false; + } return currentAction.value !== ''; } diff --git a/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts b/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts index 802ce1e..0c864fa 100644 --- a/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts +++ b/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts @@ -21,14 +21,14 @@ export class GuestValidAnswerReceivedEventHandler async handle(event: ValidAnswerReceivedEvent) { await this.guestService.notifyAboutValidAnswer(event.tId); await this.guestService.sendValidAnswerActions(event.tId); - await this.guestService.updatePlayerScore(event.tId, 1); + // await this.guestService.updatePlayerScore(event.tId, 1); if (event.extraDetails) { await this.commandBus.execute( new SendToastCommand(event.extraDetails, 4000), ); } const coef = +(await this.guestService.getCoefficient()); - await this.guestService.changeWinningChance(event.tId, 29 * coef); + // await this.guestService.changeWinningChance(event.tId, 29 * coef); await this.guestService.resetInvalidAnswersInTheRow(event.tId); } } diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index ed5f18b..97fa427 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -75,7 +75,9 @@ export class GuestsService { } async findById(id: number) { - return this.guestModel.findOne({ telegramId: id }).exec(); + const result = await this.guestModel.findOne({ telegramId: id }).exec(); + delete result.photo; + return result; } async hideKeyboard(text: string) { diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index c5b1d53..10818be 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -56,4 +56,9 @@ export class QuizController { { return await this.quizService.calculateEndgamePoints(); } + + @Get('endgame-results') + async endgameResults() { + return await this.quizService.getEndgameResults(); + } } diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index f9e6a8d..b744bba 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -122,10 +122,13 @@ export class QuizService { private async calculateScore() { const question = await this.get(); + if(question.scoreCalculated) { + return; + } if(!await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DontMarkQuestionsAsCompleted)) { this.logger.verbose(`[proceedWithGame]: DontMarkQuestionsAsCompleted disabled, marking as complete`); question.answered = true; - await question.save(); + } this.logger.verbose(`[calculateScore] enter `); const playerAnswers = question.userAnswers.map((answer) => { @@ -141,11 +144,14 @@ export class QuizService { if(winner) { const totalWinningScore = 80; sortedAnswers.filter(x => x.valid).forEach((answer) => { + this.logger.debug(`Giving 1 point to all who answered right`); this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user, totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)); this.commandBus.execute(new IncreasePlayerScoreCommand(answer.user,1)); }); await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 15)); + this.logger.debug(`Giving 1 point to first`); + await this.commandBus.execute(new IncreasePlayerScoreCommand(winner.user,1)); targetUser = winner.user; } @@ -160,6 +166,8 @@ export class QuizService { await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty)); } await this.commandBus.execute(new CreateNewQueueItemCommand(targetUser, GameQueueTypes.showresults)); + question.scoreCalculated = true; + await question.save(); } public async calculateEndgamePoints(): Promise { @@ -167,21 +175,20 @@ export class QuizService { const maxRewardsPromise = this.guestService.getModel().find({}).sort({['rewardsReceived']: "desc"}).exec(); const maxPenaltiesPromise = this.guestService.getModel().find({}).sort({['penaltiesReceived']: 'desc'}).exec(); - //const { maxRewards, maxInvalidAnswers } = Promise.all([maxRewardsPromise, maxInvalidAnswersPromise]); const [maxRewards, maxInvalidAnswers, maxPenaltiesReceived] = await Promise.all([maxRewardsPromise, maxInvalidAnswersPromise, maxPenaltiesPromise]); const result = { maxInvalidAnswers: { - id: maxInvalidAnswers[0].id, + id: maxInvalidAnswers[0].telegramId, count: maxInvalidAnswers[0].invalidAnswers, name: maxInvalidAnswers[0].name, }, maxRewards: { - id: maxRewards[0].id, + id: maxRewards[0].telegramId, count: maxRewards[0].rewardsReceived, name: maxRewards[0].name, }, maxPenalties: { - id: maxPenaltiesReceived[0].id, + id: maxPenaltiesReceived[0].telegramId, count: maxPenaltiesReceived[0].penaltiesReceived, name: maxPenaltiesReceived[0].name, } @@ -308,4 +315,9 @@ export class QuizService { } await this.guestService.postQuestion(dto, telegramId); } + + async getEndgameResults() { + const res = await this.sharedService.getConfig('endgame-points'); + return JSON.parse(res.value); + } } diff --git a/src/schemas/question.schema.ts b/src/schemas/question.schema.ts index 471956e..2f23742 100644 --- a/src/schemas/question.schema.ts +++ b/src/schemas/question.schema.ts @@ -29,5 +29,7 @@ export class Question { qId: string; @Prop([ { user: { type: Number }, time: { type: Date }, valid: { type: Boolean}}]) userAnswers: QuestionAnswer[]; + @Prop({ default: false }) + scoreCalculated: boolean; } export const QuestionSchema = SchemaFactory.createForClass(Question); -- 2.45.2 From 5fd65e815c746caf3a6d3a4ff9af9f83abb27168 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Sun, 24 Nov 2024 18:16:49 +0400 Subject: [PATCH 25/32] TGD-55 --- src/game/versus/versus.service.spec.ts | 32 ++++++++++++++++++++++++-- src/game/versus/versus.service.ts | 8 +++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/game/versus/versus.service.spec.ts b/src/game/versus/versus.service.spec.ts index b3725db..ca254b9 100644 --- a/src/game/versus/versus.service.spec.ts +++ b/src/game/versus/versus.service.spec.ts @@ -4,13 +4,19 @@ import {GuestsService} from "../../guests/guests.service"; import {GuestsServiceMock} from "../../mocks/guests-service.mock"; import {SharedService} from "../../shared/shared.service"; import {getModelToken} from "@nestjs/mongoose"; -import {Versus} from "../../schemas/versus.schema"; +import {Versus, VersusDocument} from "../../schemas/versus.schema"; import {Model} from "mongoose"; import {CommandBus, QueryBus} from "@nestjs/cqrs"; import {QueryBusMock} from "../../mocks/querybus.mock"; +const mockVersusModel = { + aggregate: jest.fn().mockReturnThis(), + exec: jest.fn(), +} + describe('VersusService', () => { let service: VersusService; + let versusModel: Model; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -18,16 +24,38 @@ describe('VersusService', () => { VersusService, { provide: GuestsService, useValue: GuestsServiceMock }, { provide: SharedService, useValue: SharedService }, - { provide: getModelToken(Versus.name), useValue: Model }, + { provide: getModelToken(Versus.name), useValue: mockVersusModel }, { provide: CommandBus, useValue: CommandBus }, { provide: QueryBus, useValue: QueryBusMock }, ], }).compile(); service = module.get(VersusService); + versusModel = module.get>(getModelToken(Versus.name)); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('validateVersusTasksAndResetIfNecessary', () => { + it('should reset all tasks if no remaining', async () => { + // setup + mockVersusModel.exec.mockResolvedValue([]); + const markCompletedSpy = jest.spyOn(service, 'markAllAsUncompleted').mockResolvedValue(null); + + // act + await service.validateVersusTasksAndResetIfNecessary(); + + // validate + expect(markCompletedSpy).toHaveBeenCalled(); + }); + + it('should not reset tasks if it is presented', async () => { + mockVersusModel.exec.mockReturnValue(['item1', 'item2']); + const markCompletedSpy = jest.spyOn(service,'markAllAsUncompleted').mockResolvedValue(null); + await service.validateVersusTasksAndResetIfNecessary(); + expect(markCompletedSpy).not.toHaveBeenCalled(); + }); + }) }); diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts index b4abfaf..a4dbcb4 100644 --- a/src/game/versus/versus.service.ts +++ b/src/game/versus/versus.service.ts @@ -69,7 +69,15 @@ export class VersusService { return { result: true }; } + async validateVersusTasksAndResetIfNecessary() { + const versus = await this.versusModel.aggregate([{ $match: { completed: false } }, { $sample: { size: 1 } }]).exec(); + if(versus.length == 0 ) { + await this.markAllAsUncompleted(); + } + } + async getVersusTask() { + await this.validateVersusTasksAndResetIfNecessary(); const rand = await this.versusModel .aggregate([{ $match: { completed: false } }, { $sample: { size: 1 } }]) .exec(); -- 2.45.2 From 26fa7c701d76ea686c000a45099b9db28f9d6ac0 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Sun, 24 Nov 2024 20:36:25 +0400 Subject: [PATCH 26/32] TGD-55 --- src/mocks/commandbus.mock.ts | 2 +- src/mocks/guests-service.mock.ts | 2 +- src/quiz/quiz.service.spec.ts | 134 ++++++++++++++++++++++++++++++- src/quiz/quiz.service.ts | 15 ++-- 4 files changed, 143 insertions(+), 10 deletions(-) diff --git a/src/mocks/commandbus.mock.ts b/src/mocks/commandbus.mock.ts index 60ad82e..b3dbd30 100644 --- a/src/mocks/commandbus.mock.ts +++ b/src/mocks/commandbus.mock.ts @@ -1,3 +1,3 @@ export const CommandbusMock = { - + execute: jest.fn(), } \ No newline at end of file diff --git a/src/mocks/guests-service.mock.ts b/src/mocks/guests-service.mock.ts index f70f872..cc01ec0 100644 --- a/src/mocks/guests-service.mock.ts +++ b/src/mocks/guests-service.mock.ts @@ -1,3 +1,3 @@ export const GuestsServiceMock = { - + updatePenaltiesCount: jest.fn(), } \ No newline at end of file diff --git a/src/quiz/quiz.service.spec.ts b/src/quiz/quiz.service.spec.ts index bc3e406..bf168b7 100644 --- a/src/quiz/quiz.service.spec.ts +++ b/src/quiz/quiz.service.spec.ts @@ -1,5 +1,5 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { QuizService } from './quiz.service'; +import {Test, TestingModule} from '@nestjs/testing'; +import {QuizService} from './quiz.service'; import {getModelToken} from "@nestjs/mongoose"; import {Question} from "../schemas/question.schema"; import {Model} from "mongoose"; @@ -13,9 +13,18 @@ import {EventbusMock} from "../mocks/eventbus.mock"; import {CommandbusMock} from "../mocks/commandbus.mock"; import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; +import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; +import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; +import {getRandomInt} from "../helpers/rand-number"; +import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; +import {GameQueueTypes} from "../schemas/game-queue.schema"; + +jest.mock('../../src/helpers/rand-number'); describe('QuizService', () => { let service: QuizService; + let cmdBus: CommandBus; + let guestService: GuestsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -32,9 +41,130 @@ describe('QuizService', () => { }).compile(); service = await module.resolve(QuizService); + cmdBus = await module.resolve(CommandBus); + guestService = await module.resolve(GuestsService); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('calculateScore()', () => { + const questionDocumentMock = { + text: 'test question', + answered: false, + valid: 'option1', + answers: ['option1', 'option2', 'option3', 'option4'], + answeredBy: 1, + note: '', + qId: 'xx-xxx-xxx', + userAnswers: [{ + user: 1, + time: new Date(), + valid: false, + }, { + user: 2, + time: new Date(new Date().setSeconds((new Date).getSeconds() - 5)), + valid: false, + }, { + user: 3, + time: new Date(), + valid: true, + }], + scoreCalculated: false, + save: jest.fn(), + }; + + it('should not calculate score if it is already calculated', async () => { + // setup + questionDocumentMock.scoreCalculated = true; + const getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any); + const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute').mockResolvedValue(null); + + // act + await service.calculateScore(); + + // validate + expect(getSpy).toHaveBeenCalled(); + expect(cmdBusExecSpy).not.toHaveBeenCalled(); + }) + + it('should assign points to winner', async () => { + //setup + questionDocumentMock.scoreCalculated = false; + const getSpy = jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); + const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); + + // act + await service.calculateScore(); + + // validate + const validUser = questionDocumentMock.userAnswers.find(user => user.valid) + expect(cmdBusExecSpy).toHaveBeenNthCalledWith(1,new IncreasePlayerWinningRateCommand(validUser.user, expect.anything())); + expect(cmdBusExecSpy).toHaveBeenNthCalledWith(2, new IncreasePlayerScoreCommand(validUser.user, 1)); + expect(cmdBusExecSpy).toHaveBeenNthCalledWith(4, new IncreasePlayerScoreCommand(validUser.user, 1)); + }) + + + it('should randomly add penalty to last answer if rnd > 50', async () => { + // setup + (getRandomInt as jest.Mock).mockReturnValue(65); + questionDocumentMock.scoreCalculated = false; + const getSpy = jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); + const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); + const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2); + + //act + await service.calculateScore(); + + //validate + expect(getRandomInt).toHaveBeenCalledWith(0,100); + expect(cmdBusExecSpy).toHaveBeenCalledWith(new CreateNewQueueItemCommand(whoShouldGetPenalty.user, GameQueueTypes.penalty)); + }); + + it('should not add penalty to last answer if rnd < 50', async () => { + // setup + jest.clearAllMocks(); + (getRandomInt as jest.Mock).mockReturnValue(10); + questionDocumentMock.scoreCalculated = false; + jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); + const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); + const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2); + + //act + await service.calculateScore(); + + //validate + expect(getRandomInt).toHaveBeenCalledWith(0,100); + expect(cmdBusExecSpy).not.toHaveBeenCalledWith(new CreateNewQueueItemCommand(whoShouldGetPenalty.user, GameQueueTypes.penalty)); + + }) + + it('should set score calculated after calculation', async () => { + questionDocumentMock.scoreCalculated = false; + const saveSpy = jest.spyOn(questionDocumentMock,'save').mockResolvedValue(true); + jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); + + // act + await service.calculateScore(); + + //validate + expect(saveSpy).toHaveBeenCalled(); + }) + + it('should add show results in queue', async () => { + // setup + questionDocumentMock.scoreCalculated = false; + const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); + const validUser = questionDocumentMock.userAnswers.find(user => user.valid) + jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); + + + // act + await service.calculateScore(); + + // validate + expect(cmdBusExecSpy).toHaveBeenCalledWith(new CreateNewQueueItemCommand(expect.anything(), GameQueueTypes.showresults)); + }) + }); }); diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index b744bba..3a06f55 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -120,7 +120,7 @@ export class QuizService { return Promise.resolve(true); } - private async calculateScore() { + async calculateScore() { const question = await this.get(); if(question.scoreCalculated) { return; @@ -128,7 +128,6 @@ export class QuizService { if(!await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DontMarkQuestionsAsCompleted)) { this.logger.verbose(`[proceedWithGame]: DontMarkQuestionsAsCompleted disabled, marking as complete`); question.answered = true; - } this.logger.verbose(`[calculateScore] enter `); const playerAnswers = question.userAnswers.map((answer) => { @@ -157,13 +156,17 @@ export class QuizService { const invalidAnswers = sortedAnswers.filter((answer) => !answer.valid) if(invalidAnswers.length > 0) { - const lastInvalidAnswer = invalidAnswers[invalidAnswers.length - 1]; + //const lastInvalidAnswer = invalidAnswers[invalidAnswers.length - 1]; + const lastInvalidAnswer = invalidAnswers.sort((a,b) => a.time - b.time)[0]; if(!lastInvalidAnswer) { return; } - targetUser = lastInvalidAnswer.user; - await this.guestService.updatePenaltiesCount(lastInvalidAnswer.user); - await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty)); + const random = getRandomInt(0,100); + if(random > 50) { + await this.guestService.updatePenaltiesCount(lastInvalidAnswer.user); + await this.commandBus.execute(new CreateNewQueueItemCommand(lastInvalidAnswer.user, GameQueueTypes.penalty)); + targetUser = lastInvalidAnswer.user; + } } await this.commandBus.execute(new CreateNewQueueItemCommand(targetUser, GameQueueTypes.showresults)); question.scoreCalculated = true; -- 2.45.2 From 13cf1a9c2fb72a8f155b0cc9271bf939f0698091 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Sun, 24 Nov 2024 22:45:59 +0400 Subject: [PATCH 27/32] TGD-58 --- src/Consts/FeatureFlags.consts.ts | 1 + src/quiz/quiz.service.spec.ts | 130 +++++++++++++++++++++++++----- src/quiz/quiz.service.ts | 19 +++++ 3 files changed, 130 insertions(+), 20 deletions(-) diff --git a/src/Consts/FeatureFlags.consts.ts b/src/Consts/FeatureFlags.consts.ts index 31b1dda..848ffbe 100644 --- a/src/Consts/FeatureFlags.consts.ts +++ b/src/Consts/FeatureFlags.consts.ts @@ -2,4 +2,5 @@ export class FeatureFlagsConsts { static EnableEndgamePoints = 'EnableEndgamePoints'; static DontMarkQuestionsAsCompleted = 'DontMarkQuestionsAsCompleted'; static DisableVoice = 'DisableVoice'; + static StartVersusIfPlayersAnsweredInSameTime = 'StartVersusIfPlayersAnsweredInSameTime'; } \ No newline at end of file diff --git a/src/quiz/quiz.service.spec.ts b/src/quiz/quiz.service.spec.ts index bf168b7..f60b3b3 100644 --- a/src/quiz/quiz.service.spec.ts +++ b/src/quiz/quiz.service.spec.ts @@ -8,16 +8,19 @@ import {GuestsService} from "../guests/guests.service"; import {GuestsServiceMock} from "../mocks/guests-service.mock"; import {SharedService} from "../shared/shared.service"; import {SharedServiceMock} from "../mocks/shared-service.mock"; -import {CommandBus, EventBus} from "@nestjs/cqrs"; +import {CommandBus, EventBus, ICommand} from "@nestjs/cqrs"; import {EventbusMock} from "../mocks/eventbus.mock"; import {CommandbusMock} from "../mocks/commandbus.mock"; -import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureflagService, IFeatureFlagStatus} from "../featureflag/featureflag.service"; import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; import {getRandomInt} from "../helpers/rand-number"; import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; import {GameQueueTypes} from "../schemas/game-queue.schema"; +import {BeginVersusCommand} from "../game/commands/begin-versus.command" +import spyOn = jest.spyOn; +import clearAllMocks = jest.clearAllMocks; jest.mock('../../src/helpers/rand-number'); @@ -25,24 +28,26 @@ describe('QuizService', () => { let service: QuizService; let cmdBus: CommandBus; let guestService: GuestsService; + let featureFlagService: FeatureflagService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ QuizService, - { provide: getModelToken(Question.name), useValue: Model }, - { provide: getModelToken(QuestionStorage.name), useValue: Model}, - { provide: GuestsService, useValue: GuestsServiceMock }, - { provide: SharedService, useValue: SharedServiceMock }, - { provide: EventBus, useValue: EventbusMock }, - { provide: CommandBus, useValue: CommandbusMock }, - { provide: FeatureflagService, useValue: FeatureflagServiceMock } + {provide: getModelToken(Question.name), useValue: Model}, + {provide: getModelToken(QuestionStorage.name), useValue: Model}, + {provide: GuestsService, useValue: GuestsServiceMock}, + {provide: SharedService, useValue: SharedServiceMock}, + {provide: EventBus, useValue: EventbusMock}, + {provide: CommandBus, useValue: CommandbusMock}, + {provide: FeatureflagService, useValue: FeatureflagServiceMock} ], }).compile(); service = await module.resolve(QuizService); cmdBus = await module.resolve(CommandBus); guestService = await module.resolve(GuestsService); + featureFlagService = await module.resolve(FeatureflagService); }); it('should be defined', () => { @@ -50,6 +55,8 @@ describe('QuizService', () => { }); describe('calculateScore()', () => { + let cmdBusExecSpy: jest.SpyInstance, [command: ICommand], any>; + let getSpy; const questionDocumentMock = { text: 'test question', answered: false, @@ -70,16 +77,22 @@ describe('QuizService', () => { user: 3, time: new Date(), valid: true, + }, { + user: 4, + time: new Date(), + valid: false, }], scoreCalculated: false, save: jest.fn(), }; + beforeEach(() => { + cmdBusExecSpy = jest.spyOn(cmdBus, 'execute').mockResolvedValue(null); + getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any); + }); it('should not calculate score if it is already calculated', async () => { // setup questionDocumentMock.scoreCalculated = true; - const getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any); - const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute').mockResolvedValue(null); // act await service.calculateScore(); @@ -92,8 +105,6 @@ describe('QuizService', () => { it('should assign points to winner', async () => { //setup questionDocumentMock.scoreCalculated = false; - const getSpy = jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); - const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); // act await service.calculateScore(); @@ -110,8 +121,6 @@ describe('QuizService', () => { // setup (getRandomInt as jest.Mock).mockReturnValue(65); questionDocumentMock.scoreCalculated = false; - const getSpy = jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); - const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2); //act @@ -127,8 +136,6 @@ describe('QuizService', () => { jest.clearAllMocks(); (getRandomInt as jest.Mock).mockReturnValue(10); questionDocumentMock.scoreCalculated = false; - jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); - const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute'); const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2); //act @@ -141,16 +148,15 @@ describe('QuizService', () => { }) it('should set score calculated after calculation', async () => { + // setup questionDocumentMock.scoreCalculated = false; const saveSpy = jest.spyOn(questionDocumentMock,'save').mockResolvedValue(true); - jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any); - // act await service.calculateScore(); //validate expect(saveSpy).toHaveBeenCalled(); - }) + }); it('should add show results in queue', async () => { // setup @@ -165,6 +171,90 @@ describe('QuizService', () => { // validate expect(cmdBusExecSpy).toHaveBeenCalledWith(new CreateNewQueueItemCommand(expect.anything(), GameQueueTypes.showresults)); + }); + + + it('should start versus if user replied in less than 5 seconds if ff enabled', async () => { + // setup + questionDocumentMock.scoreCalculated = false; + const ffstate: IFeatureFlagStatus = { + name: '', + state: true, + } + spyOn(featureFlagService,'getFeatureFlag').mockResolvedValue(ffstate); + questionDocumentMock.userAnswers = [{ + user: 1, + time: new Date(new Date().setSeconds(new Date().getSeconds() - 5)), + valid: true, + }, + { + user: 2, + time: new Date(), + valid: true, + } + ] + getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any); + + // act + await service.calculateScore(); + + // validate + expect(cmdBusExecSpy).toHaveBeenCalledWith(new BeginVersusCommand(expect.anything(), expect.anything())); + }); + + it('should not start versus if FF is off and gap less than 5', async () => { + // setup + jest.clearAllMocks(); + questionDocumentMock.scoreCalculated = false; + const ffstate: IFeatureFlagStatus = { + name: '', + state: false, + } + spyOn(featureFlagService,'getFeatureFlag').mockResolvedValue(ffstate); + questionDocumentMock.userAnswers = [{ + user: 1, + time: new Date(new Date().setSeconds(new Date().getSeconds() - 3)), + valid: true, + }, + { + user: 2, + time: new Date(), + valid: true, + } + ] + getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any); + + // act + await service.calculateScore(); + + // validate + expect(cmdBusExecSpy).not.toHaveBeenCalledWith(new BeginVersusCommand(expect.anything(), expect.anything())); + + }); + it('should not start versus if gap more than 5 seconds', async () => { + // setup + questionDocumentMock.scoreCalculated = false; + questionDocumentMock.userAnswers = [{ + user: 1, + time: new Date(new Date().setSeconds(new Date().getSeconds() - 7)), + valid: true, + }, + { + user: 2, + time: new Date(), + valid: true, + } + ] + getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any); + + // act + await service.calculateScore(); + // validate + expect(cmdBusExecSpy).not.toHaveBeenCalledWith(new BeginVersusCommand(expect.anything(), expect.anything())); + }); + + it('should not start versus if only one player answered correctly', () => { + }) }); }); diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 3a06f55..0fca176 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -20,6 +20,7 @@ import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; import {QuizEndGameResults} from "./quiz.types"; import {ClientNotificationType} from "../socket/socket.gateway"; +import {BeginVersusCommand} from "../game/commands/begin-versus.command"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { @@ -120,6 +121,14 @@ export class QuizService { return Promise.resolve(true); } + private checkIfWeShouldStartVersus(answers: { valid: boolean; time: number; user: number }[]) { + if(answers.length === 0 && answers.length <= 2) { + return false; + } + const diff = Math.abs(new Date(answers[0].time).getTime() - new Date(answers[1].time).getTime()) / 1000; + return diff <= 5; + } + async calculateScore() { const question = await this.get(); if(question.scoreCalculated) { @@ -148,6 +157,16 @@ export class QuizService { totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)); this.commandBus.execute(new IncreasePlayerScoreCommand(answer.user,1)); }); + const ffState = await this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.StartVersusIfPlayersAnsweredInSameTime) + if(ffState.state) { + if(this.checkIfWeShouldStartVersus(sortedAnswers.filter(x => x.valid))) { + await this.commandBus.execute( + new BeginVersusCommand( + sortedAnswers.filter(x => x.valid)[0].user, + sortedAnswers.filter(x => x.valid)[1].user, + )); + } + } await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 15)); this.logger.debug(`Giving 1 point to first`); await this.commandBus.execute(new IncreasePlayerScoreCommand(winner.user,1)); -- 2.45.2 From fddaf5e5f68f9e876911b298f42f0eb0199ab294 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Tue, 26 Nov 2024 17:52:22 +0400 Subject: [PATCH 28/32] TGD-9 --- data/gifts.json | 156 +++++++ data/questions.json | 1034 +++++++++++++++++++++++++++++++++++++++++++ data/versus.json | 114 +++++ 3 files changed, 1304 insertions(+) create mode 100644 data/gifts.json create mode 100644 data/questions.json create mode 100644 data/versus.json diff --git a/data/gifts.json b/data/gifts.json new file mode 100644 index 0000000..b05f045 --- /dev/null +++ b/data/gifts.json @@ -0,0 +1,156 @@ +[{ +"prizeID": 1, +"name": "Тесто для лепки невкусное", +"isGifted": false +}, +{ +"prizeID": 2, +"name": "Палочки с ароматом лучших публичных домов Бангкока", +"isGifted": false +}, +{ +"prizeID": 3, +"name": "2 метра хюгге", +"isGifted": false +}, +{ +"prizeID": 4, +"name": "Палку светящуюся бесполезную", +"isGifted": false +}, +{ +"prizeID": 5, +"name": "Тёрку для лилипутов", +"isGifted": false +}, +{ +"prizeID": 6, +"name": "Мёртвую белочку", +"isGifted": false +}, +{ +"prizeID": 7, +"name": "Лучшего друга Спанч боба засушенного", +"isGifted": false +}, +{ +"prizeID": 8, +"name": "Подарок для любителей помесить глину", +"isGifted": false +}, +{ +"prizeID": 9, +"name": "Палку чесательную полезную", +"isGifted": false +}, +{ +"prizeID": 10, +"name": "Красного петуха - своё тотемное животное", +"isGifted": false +}, +{ +"prizeID": 11, +"name": "Набор свечей романтишный", +"isGifted": false +}, +{ +"prizeID": 12, +"name": "Хранилище для денег патриотическое", +"isGifted": false +}, +{ +"prizeID": 13, +"name": "Мерч от Каца", +"isGifted": false +}, +{ +"prizeID": 14, +"name": "Чупа-чупс со вкусом патриотизма", +"isGifted": false +}, +{ +"prizeID": 15, +"name": "Тренажеры для легких разноцветные", +"isGifted": false +}, +{ +"prizeID": 16, +"name": "Паззл предсказуемый", +"isGifted": false +}, +{ +"prizeID": 17, +"name": "Жопный блокнот", +"isGifted": false +}, +{ +"prizeID": 18, +"name": "Носки от батьки праздничные", +"isGifted": false +}, +{ +"prizeID": 19, +"name": "Носки женские миленькие", +"isGifted": false +}, +{ +"prizeID": 20, +"name": "Набор художника-нумеролога", +"isGifted": false +}, +{ +"prizeID": 21, +"name": "Карандаш вечный как Путин", +"isGifted": false +}, +{ +"prizeID": 22, +"name": "Массажёр для жопы", +"isGifted": false +}, +{ +"prizeID": 23, +"name": "Сладкий подарок рот в рот", +"isGifted": false +}, +{ +"prizeID": 24, +"name": "Мотоцикл (ненастоящий)", +"isGifted": false +}, +{ +"prizeID": 25, +"name": "Вышивку для эскортниц (алмазную)", +"isGifted": false +}, +{ +"prizeID": 26, +"name": "Звенящие бубенцы", +"isGifted": false +}, +{ +"prizeID": 27, +"name": "Спонж для умывания твоих кислых щей", +"isGifted": false +}, +{ +"prizeID": 28, +"name": "Мочалку с портретом дракона", +"isGifted": false +}, +{ +"prizeID": 29, +"name": "Тетрадь для чётких квадроберов", +"isGifted": false +}, +{ +"prizeID": 30, +"name": "Костюм для руки незнакомки эротишный", +"isGifted": false +}, +{ +"prizeID": 31, +"name": "Плакат с кумиром детства нарядный", +"isGifted": false +} +] \ No newline at end of file diff --git a/data/questions.json b/data/questions.json new file mode 100644 index 0000000..d097f8b --- /dev/null +++ b/data/questions.json @@ -0,0 +1,1034 @@ +[ + { + "question": "Как называется самый большой океан на Земле?", + "a1": "Атлантический океан", + "a2": "Тихий океан", + "a3": "Индийский океан", + "a4": "Северный Ледовитый океан", + "valid": "Тихий океан" + }, + { + "question": "Кто написал роман 'Преступление и наказание'?", + "a1": "Лев Толстой", + "a2": "Федор Достоевский", + "a3": "Антон Чехов", + "a4": "Николай Гоголь", + "valid": "Федор Достоевский" + }, + { + "question": "Кто является режиссером фильма 'Гражданин Кейн', считающегося одним из величайших фильмов всех времен?", + "a1": "Орсон Уэллс", + "a2": "Альфред Хичкок", + "a3": "Стэнли Кубрик", + "a4": "Фрэнк Капра", + "valid": "Орсон Уэллс" + }, + { + "question": "Кто сыграл главную роль в фильме 'Форрест Гамп'?", + "a1": "Том Хэнкс", + "a2": "Брэд Питт", + "a3": "Джон Траволта", + "a4": "Леонардо ДиКаприо", + "valid": "Том Хэнкс" + }, + { + "question": "Какой режиссер снял трилогию 'Властелин колец'?", + "a1": "Питер Джексон", + "a2": "Кристофер Нолан", + "a3": "Джеймс Кэмерон", + "a4": "Гильермо дель Торо", + "valid": "Питер Джексон" + }, + { + "question": "Какой режиссер снял фильмы 'Малхолланд Драйв' и 'Голова-ластик'?", + "a1": "Дэвид Линч", + "a2": "Дэвид Финчер", + "a3": "Стэнли Кубрик", + "a4": "Тим Бёртон", + "valid": "Дэвид Линч" + }, + { + "question": "Какой актер сыграл главную роль в фильме 'Матрица'?", + "a1": "Киану Ривз", + "a2": "Уилл Смит", + "a3": "Том Круз", + "a4": "Джонни Депп", + "valid": "Киану Ривз" + }, + { + "question": "Какой фильм японского режиссера Хаяо Миядзаки получил премию 'Оскар' за лучший анимационный фильм?", + "a1": "Унесенные призраками", + "a2": "Ходячий замок", + "a3": "Принцесса Мононоке", + "a4": "Мой сосед Тоторо", + "valid": "Унесенные призраками" + }, + { + "question": "Какой фильм Кристофера Нолана рассказывает о попытке кражи идей через сновидения?", + "a1": "Начало", + "a2": "Престиж", + "a3": "Интерстеллар", + "a4": "Мементо", + "valid": "Начало" + }, + { + "question": "Какой газ составляет около 78% атмосферы Земли?", + "a1": "Кислород", + "a2": "Углекислый газ", + "a3": "Азот", + "a4": "Аргон", + "valid": "Азот" + }, + { + "question": "Как зовут обезьяну Росса в сериале 'Друзья'?", + "a1": "Марсель", + "a2": "Морис", + "a3": "Макс", + "a4": "Мэри", + "valid": "Марсель" + }, + { + "question": "Кто является национальным поэтом Грузии, автором поэмы 'Витязь в тигровой шкуре'?", + "a1": "Шота Руставели", + "a2": "Илья Чавчавадзе", + "a3": "Галактион Табидзе", + "a4": "Акакий Церетели", + "valid": "Шота Руставели" + }, + { + "question": "Какой город является столицей Австралии?", + "a1": "Сидней", + "a2": "Мельбурн", + "a3": "Канберра", + "a4": "Брисбен", + "valid": "Канберра" + }, + { + "question": "Какой элемент периодической таблицы имеет символ 'Fe'?", + "a1": "Калий", + "a2": "Фосфор", + "a3": "Железо", + "a4": "Фтор", + "valid": "Железо" + }, + { + "question": "В каком году человек впервые ступил на Луну?", + "a1": "1965", + "a2": "1969", + "a3": "1972", + "a4": "1959", + "valid": "1969" + }, + { + "question": "Кто написал симфонию №9 'Из Нового Света'?", + "a1": "Антонин Дворжак", + "a2": "Людвиг ван Бетховен", + "a3": "Иоганн Себастьян Бах", + "a4": "Вольфганг Амадей Моцарт", + "valid": "Антонин Дворжак" + }, + { + "question": "Какое среднее имя у Чендлера в сериале 'Друзья'?", + "a1": "Мэри", + "a2": "Мюриэл", + "a3": "Мэтью", + "a4": "Майкл", + "valid": "Мюриэл" + }, + { + "question": "Как называлось древнее грузинское царство, существовавшее в VI-IV веках до н.э.?", + "a1": "Колхида", + "a2": "Иверия", + "a3": "Лазика", + "a4": "Месхетия", + "valid": "Колхида" + }, + { + "question": "Какой город является столицей Бразилии?", + "a1": "Рио-де-Жанейро", + "a2": "Сан-Паулу", + "a3": "Бразилиа", + "a4": "Сальвадор", + "valid": "Бразилиа" + }, + { + "question": "Кто написал 'Маленький принц'?", + "a1": "Антуан де Сент-Экзюпери", + "a2": "Жюль Верн", + "a3": "Александр Дюма", + "a4": "Гюстав Флобер", + "valid": "Антуан де Сент-Экзюпери" + }, + { + "question": "Как называется научная единица измерения силы?", + "a1": "Джоуль", + "a2": "Ньютон", + "a3": "Паскаль", + "a4": "Ватт", + "valid": "Ньютон" + }, + { + "question": "Как зовут сестру-близнеца Фиби в сериале 'Друзья'?", + "a1": "Урсула", + "a2": "Рэйчел", + "a3": "Моника", + "a4": "Эмили", + "valid": "Урсула" + }, + { + "question": "Кто был первым президентом независимой Грузии после распада СССР?", + "a1": "Эдуард Шеварднадзе", + "a2": "Звиад Гамсахурдия", + "a3": "Михаил Саакашвили", + "a4": "Гиорги Маргвелашвили", + "valid": "Звиад Гамсахурдия" + }, + { + "question": "Какой город является столицей Новой Зеландии?", + "a1": "Окленд", + "a2": "Веллингтон", + "a3": "Крайстчерч", + "a4": "Данидин", + "valid": "Веллингтон" + }, + { + "question": "Какой самый большой орган в человеческом теле?", + "a1": "Сердце", + "a2": "Печень", + "a3": "Кожа", + "a4": "Легкие", + "valid": "Кожа" + }, + { + "question": "Кто написал музыку к балету 'Лебединое озеро'?", + "a1": "Петр Чайковский", + "a2": "Игорь Стравинский", + "a3": "Сергей Прокофьев", + "a4": "Дмитрий Шостакович", + "valid": "Петр Чайковский" + }, + { + "question": "Какое животное является символом Всемирного фонда дикой природы (WWF)?", + "a1": "Тигр", + "a2": "Панда", + "a3": "Слон", + "a4": "Кит", + "valid": "Панда" + }, + { + "question": "Чем на самом деле занимается Чендлер Бинг по профессии в сериале 'Друзья'?", + "a1": "Рекламный агент", + "a2": "Аналитик данных", + "a3": "Юрист", + "a4": "Доктор", + "valid": "Аналитик данных" + }, + { + "question": "Как называется древний пещерный город в Грузии, основанный в XII веке?", + "a1": "Уплисцихе", + "a2": "Вардзия", + "a3": "Мцхета", + "a4": "Сигнахи", + "valid": "Вардзия" + }, + { + "question": "Какой город является столицей Норвегии?", + "a1": "Стокгольм", + "a2": "Осло", + "a3": "Копенгаген", + "a4": "Хельсинки", + "valid": "Осло" + }, + { + "question": "Какой континент не имеет пустынь?", + "a1": "Европа", + "a2": "Антарктида", + "a3": "Африка", + "a4": "Южная Америка", + "valid": "Европа" + }, + { + "question": "Какой планете в Солнечной системе соответствует прозвище 'Красная планета'?", + "a1": "Марс", + "a2": "Венера", + "a3": "Юпитер", + "a4": "Меркурий", + "valid": "Марс" + }, + { + "question": "Какая столица Брунея?", + "a1": "Бандар-Сери-Бегаван", + "a2": "Куала-Лумпур", + "a3": "Джакарта", + "a4": "Бангкок", + "valid": "Бандар-Сери-Бегаван" + }, + { + "question": "Как называется столица Мьянмы (Бирмы) с 2005 года?", + "a1": "Янгон", + "a2": "Нейпьидо", + "a3": "Мандалай", + "a4": "Баган", + "valid": "Нейпьидо" + }, + { + "question": "Какая столица Палау?", + "a1": "Нгерулмуд", + "a2": "Корор", + "a3": "Маджуро", + "a4": "Фунафути", + "valid": "Нгерулмуд" + } + { + "question": "Кто из друзей работает массажисткой?", + "a1": "Моника", + "a2": "Фиби", + "a3": "Рэйчел", + "a4": "Дженис", + "valid": "Фиби" + }, + { + "question": "Как называется традиционное грузинское застолье с тамадой?", + "a1": "Хинкали", + "a2": "Супра", + "a3": "Чача", + "a4": "Киндзмараули", + "valid": "Супра" + }, + { + "question": "Какой город является столицей Швейцарии?", + "a1": "Цюрих", + "a2": "Женева", + "a3": "Берн", + "a4": "Лозанна", + "valid": "Берн" + }, + { + "question": "Кто был первым человеком в космосе?", + "a1": "Нил Армстронг", + "a2": "Юрий Гагарин", + "a3": "Алан Шепард", + "a4": "Герман Титов", + "valid": "Юрий Гагарин" + }, + { + "question": "Как называется крупнейшая коралловая система в мире?", + "a1": "Большой Барьерный риф", + "a2": "Коралловый треугольник", + "a3": "Красное море", + "a4": "Мальдивский риф", + "valid": "Большой Барьерный риф" + }, + { + "question": "Какая компания разработала первый персональный компьютер с графическим интерфейсом?", + "a1": "Microsoft", + "a2": "Apple", + "a3": "IBM", + "a4": "Hewlett-Packard", + "valid": "Apple" + }, + { + "question": "Кто изобрел систему переменного тока (AC)?", + "a1": "Томас Эдисон", + "a2": "Никола Тесла", + "a3": "Джордж Вестингауз", + "a4": "Майкл Фарадей", + "valid": "Никола Тесла" + }, + { + "question": "Кто разработал язык программирования Java?", + "a1": "Джеймс Гослинг", + "a2": "Деннис Ритчи", + "a3": "Бьёрн Страуструп", + "a4": "Гвидо ван Россум", + "valid": "Джеймс Гослинг" + }, + { + "question": "Как называется процесс деления клетки в биологии?", + "a1": "Мейоз", + "a2": "Митоз", + "a3": "Фотосинтез", + "a4": "Апоптоз", + "valid": "Митоз" + }, + { + "question": "Как называется самая продаваемая видеоигра всех времён?", + "a1": "Minecraft", + "a2": "Tetris", + "a3": "Grand Theft Auto V", + "a4": "Wii Sports", + "valid": "Minecraft" + }, + { + "question": "Кто является создателем серии игр 'Супер Марио'?", + "a1": "Сигэру Миямото", + "a2": "Хидэо Кодзима", + "a3": "Сид Мейер", + "a4": "Тодд Говард", + "valid": "Сигэру Миямото" + }, + { + "question": "Какой регион Грузии известен своими пещерными городами, такими как Вардзия?", + "a1": "Самцхе-Джавахети", + "a2": "Имерети", + "a3": "Мцхета-Мтианети", + "a4": "Кахети", + "valid": "Самцхе-Джавахети" + }, + { + "question": "В каком регионе Грузии находится гора Казбек?", + "a1": "Мцхета-Мтианети", + "a2": "Сванети", + "a3": "Кахети", + "a4": "Имерети", + "valid": "Мцхета-Мтианети" + }, + { + "question": "Какой регион Грузии граничит с Арменией и известен своими армянскими общинами?", + "a1": "Самцхе-Джавахети", + "a2": "Квемо Картли", + "a3": "Имерети", + "a4": "Гурия", + "valid": "Самцхе-Джавахети" + }, + { + "question": "Какая игра стала самой первой 3D игрой с открытым миром?", + "a1": "Grand Theft Auto III", + "a2": "The Elder Scrolls III: Morrowind", + "a3": "Super Mario 64", + "a4": "The Legend of Zelda: Ocarina of Time", + "valid": "Super Mario 64" + }, + { + "question": "Кто является разработчиком серии игр 'Metal Gear Solid'?", + "a1": "Ко Джунг Хван", + "a2": "Хидэо Кодзима", + "a3": "Кадзунори Ямаути", + "a4": "Нобуо Уэмацу", + "valid": "Хидэо Кодзима" + }, + { + "question": "Какая игра, выпущенная CD Projekt Red в 2015 году, получила множество наград 'Игра года'?", + "a1": "The Witcher 3: Wild Hunt", + "a2": "Cyberpunk 2077", + "a3": "Red Dead Redemption 2", + "a4": "Assassin's Creed Odyssey", + "valid": "The Witcher 3: Wild Hunt" + }, + { + "question": "Какой город называют 'Воротами в Азию' и он расположен на двух континентах?", + "a1": "Стамбул", + "a2": "Москва", + "a3": "Токио", + "a4": "Дубай", + "valid": "Стамбул" + }, + { + "question": "В какой стране находится древний город Петра, высеченный в скалах?", + "a1": "Египет", + "a2": "Иордания", + "a3": "Марокко", + "a4": "Турция", + "valid": "Иордания" + }, + { + "question": "В какой стране можно посетить исторический Великий Будда Камакура?", + "a1": "Китай", + "a2": "Япония", + "a3": "Таиланд", + "a4": "Южная Корея", + "valid": "Япония" + }, + { + "question": "Какой бренд сигарет использовал Джесси для хранения рицина в сериале во все тяжкие?", + "a1": "Marlboro Red", + "a2": "Camel Blue", + "a3": "Lucky Strike", + "a4": "Parliament", + "valid": "Lucky Strike" + }, + { + "question": "Как называется компания по борьбе с вредителями, которую использовали Уолт и Джесси в качестве прикрытия в сериале во все тяжкие?", + "a1": "Pest Gone", + "a2": "Vamonos Pest", + "a3": "Bug Busters", + "a4": "Insect Away", + "valid": "Vamonos Pest" + }, + { + "question": "Какой архипелаг известен своими уникальными видами животных и вдохновил Чарльза Дарвина на создание теории эволюции?", + "a1": "Гавайские острова", + "a2": "Мальдивы", + "a3": "Галапагосские острова", + "a4": "Канарские острова", + "valid": "Галапагосские острова" + }, + { + "question": "В какой стране находится знаменитый пляж Копакабана?", + "a1": "Испания", + "a2": "Мексика", + "a3": "Бразилия", + "a4": "Португалия", + "valid": "Бразилия" + }, + { + "question": "Какой британский музыкальный коллектив считается самым продаваемым в истории?", + "a1": "The Beatles", + "a2": "Queen", + "a3": "The Rolling Stones", + "a4": "Pink Floyd", + "valid": "The Beatles" + }, + { + "question": "Как называется направление электронной музыки, возникшее в Детройте в 1980-х годах?", + "a1": "Техно", + "a2": "Транс", + "a3": "Хаус", + "a4": "Дабстеп", + "valid": "Техно" + }, + { + "question": "Кто является лучшим бомбардиром в истории сборной Грузии по футболу?", + "a1": "Кахабер Каладзе", + "a2": "Шота Арвеладзе", + "a3": "Георгий Кинкладзе", + "a4": "Леван Кобиашвили", + "valid": "Шота Арвеладзе" + }, + { + "question": "Какой игрок сборной Грузии выступает за итальянский клуб 'Наполи' и известен под прозвищем 'Кварадонна'?", + "a1": "Георгий Чакветадзе", + "a2": "Хвича Кварацхелия", + "a3": "Гиорги Абурджания", + "a4": "Вако Казаишвили", + "valid": "Хвича Кварацхелия" + }, + { + "question": "Какой остров является самым большим в Средиземном море?", + "a1": "Сардиния", + "a2": "Сицилия", + "a3": "Крит", + "a4": "Кипр", + "valid": "Сицилия" + }, + { + "question": "Какой горный массив является самым длинным в мире?", + "a1": "Гималаи", + "a2": "Анды", + "a3": "Альпы", + "a4": "Скалистые горы", + "valid": "Анды" + }, + { + "question": "Какой водопад считается самым высоким в мире?", + "a1": "Ниагарский водопад", + "a2": "Водопад Виктория", + "a3": "Анхель", + "a4": "Игуасу", + "valid": "Анхель" + }, + { + "question": "Кто был первым человеком, совершившим одиночный беспосадочный перелет через Атлантический океан?", + "a1": "Амелия Эрхарт", + "a2": "Чарльз Линдберг", + "a3": "Говард Хьюз", + "a4": "Ричард Брансон", + "valid": "Чарльз Линдберг" + }, + { + "question": "Как называется прибор, используемый для измерения высоты полета самолета?", + "a1": "Спидометр", + "a2": "Альтиметр", + "a3": "Вариметр", + "a4": "Гироскоп", + "valid": "Альтиметр" + }, + { + "question": "Кто считается первым человеком, совершившим управляемый полет на самолете с двигателем?", + "a1": "Чарльз Линдберг", + "a2": "Братья Райт", + "a3": "Амелия Эрхарт", + "a4": "Луи Блерио", + "valid": "Братья Райт" + }, + { + "question": "Как называется международная организация, устанавливающая стандарты в гражданской авиации?", + "a1": "IATA", + "a2": "ICAO", + "a3": "FAA", + "a4": "EASA", + "valid": "ICAO" + }, + { + "question": "Как называется математическая константа, приблизительно равная 2,71828?", + "a1": "Число Пи", + "a2": "Число Эйлера", + "a3": "Золотое сечение", + "a4": "Число Фибоначчи", + "valid": "Число Эйлера" + }, + { + "question": "Как называется явление, при котором свет изменяет направление при переходе из одной среды в другую?", + "a1": "Дифракция", + "a2": "Рефракция", + "a3": "Интерференция", + "a4": "Поляризация", + "valid": "Рефракция" + }, + { + "question": "Какой математик считается основателем аналитической геометрии?", + "a1": "Рене Декарт", + "a2": "Исаак Ньютон", + "a3": "Блез Паскаль", + "a4": "Карл Гаусс", + "valid": "Рене Декарт" + }, + { + "question": "Кто первым доказал, что Земля вращается вокруг Солнца?", + "a1": "Николай Коперник", + "a2": "Иоганн Кеплер", + "a3": "Галилео Галилей", + "a4": "Тихо Браге", + "valid": "Николай Коперник" + }, + { + "question": "Какой элемент периодической таблицы имеет атомный номер 6?", + "a1": "Кислород", + "a2": "Азот", + "a3": "Углерод", + "a4": "Водород", + "valid": "Углерод" + }, + { + "question": "Как называется самый большой спутник Сатурна?", + "a1": "Титан", + "a2": "Европа", + "a3": "Ганимед", + "a4": "Каллисто", + "valid": "Титан" + }, + { + "question": "Как зовут младшую сестру Рэйчел Грин?", + "a1": "Эми", + "a2": "Джилл", + "a3": "Эмили", + "a4": "Джуди", + "valid": "Джилл" + }, + { + "question": "Какой город является столицей Марокко?", + "a1": "Касабланка", + "a2": "Рабат", + "a3": "Марракеш", + "a4": "Фес", + "valid": "Рабат" + }, + { + "question": "Как называется научная единица измерения электрического сопротивления?", + "a1": "Вольт", + "a2": "Ампер", + "a3": "Ом", + "a4": "Фарад", + "valid": "Ом" + }, + { + "question": "Как называется самый большой остров в Средиземном море?", + "a1": "Сардиния", + "a2": "Крит", + "a3": "Сицилия", + "a4": "Кипр", + "valid": "Сицилия" + }, + { + "question": "Кто из героев 'Друзей' был первым женатым на Монике?", + "a1": "Чендлер", + "a2": "Ричард", + "a3": "Росс", + "a4": "Джо", + "valid": "Чендлер" + }, + { + "question": "Как называется грузинская письменность, используемая для написания грузинского языка?", + "a1": "Глаголица", + "a2": "Асомтаврули", + "a3": "Мхедрули", + "a4": "Кириллица", + "valid": "Мхедрули" + }, + { + "question": "Какой город является столицей Кирибати?", + "a1": "Южная Тарава", + "a2": "Паликир", + "a3": "Хониара", + "a4": "Фунафути", + "valid": "Южная Тарава" + }, + { + "question": "Какой город является столицей Финляндии?", + "a1": "Хельсинки", + "a2": "Стокгольм", + "a3": "Осло", + "a4": "Копенгаген", + "valid": "Хельсинки" + }, + { + "question": "Как называется самый большой вид акул?", + "a1": "Белая акула", + "a2": "Тигровая акула", + "a3": "Китовая акула", + "a4": "Рифовая акула", + "valid": "Китовая акула" + }, + { + "question": "Кто написал пьесу 'Ромео и Джульетта'?", + "a1": "Уильям Шекспир", + "a2": "Джон Мильтон", + "a3": "Джордж Байрон", + "a4": "Чарльз Диккенс", + "valid": "Уильям Шекспир" + }, + { + "question": "Какой шрифт был разработан в 1957 году Максом Мидингером и Эдуардом Хоффманном и стал одним из самых популярных в мире?", + "a1": "Arial", + "a2": "Times New Roman", + "a3": "Helvetica", + "a4": "Courier New", + "valid": "Helvetica" + }, + { + "question": "Какой шрифт был создан Винсентом Коннаром в 1994 году и часто критикуется дизайнерами за его неуместное использование?", + "a1": "Comic Sans MS", + "a2": "Papyrus", + "a3": "Impact", + "a4": "Brush Script", + "valid": "Comic Sans MS" + }, + { + "question": "Как зовут главного героя мультфильма «Тачки»?", + "a1": "Молния Маккуин", + "a2": "Док Хадсон", + "a3": "Мэтр", + "a4": "Салли", + "valid": "Молния Маккуин" + }, + { + "question": "Как называется старая гоночная машина, которая становится наставником Маккуина?", + "a1": "Док Хадсон", + "a2": "Чик Хикс", + "a3": "Кинг", + "a4": "Сержант", + "valid": "Док Хадсон" + }, + { + "question": "Как называется топливо, которое продает хиппи-мобиль Филмор?", + "a1": "Органическое топливо", + "a2": "Супероктан", + "a3": "Электрозаряд", + "a4": "БиоДизель", + "valid": "Органическое топливо" + }, + { + "question": "Какой регион Грузии известен своими средневековыми башнями и уникальной культурой?", + "a1": "Кахети", + "a2": "Сванети", + "a3": "Самегрело", + "a4": "Имерети", + "valid": "Сванети" + }, + { + "question": "Какое грузинское вино стало первым защищённым наименованием по месту происхождения в Грузии?", + "a1": "Саперави", + "a2": "Киндзмараули", + "a3": "Хванчкара", + "a4": "Цинандали", + "valid": "Хванчкара" + }, + { + "question": "Какой грузинский город является вторым по величине и расположен на реке Риони?", + "a1": "Кутаиси", + "a2": "Батуми", + "a3": "Гори", + "a4": "Рустави", + "valid": "Кутаиси" + }, + { + "question": "Как называется грузинское национальное блюдо из курицы в ореховом соусе?", + "a1": "Сациви", + "a2": "Чахохбили", + "a3": "Оджахури", + "a4": "Чакапули", + "valid": "Сациви" + }, + { + "question": "В какой стране находится Мачу-Пикчу, древний город инков?", + "a1": "Боливия", + "a2": "Перу", + "a3": "Чили", + "a4": "Колумбия", + "valid": "Перу" + }, + { + "question": "Какой остров известен своими гигантскими каменными статуями моаи?", + "a1": "Остров Пасхи", + "a2": "Гавайи", + "a3": "Мадагаскар", + "a4": "Сицилия", + "valid": "Остров Пасхи" + }, + { + "question": "В какой стране находится древний храмовый комплекс Боробудур?", + "a1": "Индонезия", + "a2": "Малайзия", + "a3": "Таиланд", + "a4": "Вьетнам", + "valid": "Индонезия" + }, + { + "question": "Как называется грузинский музыкальный инструмент, похожий на гитару?", + "a1": "Пандури", + "a2": "Дудук", + "a3": "Чонгури", + "a4": "Саз", + "valid": "Пандури" + }, + { + "question": "Как называется игровая консоль от Sega, выпущенная в 1999 году и ставшая последней консолью компании?", + "a1": "Sega Saturn", + "a2": "Sega Genesis", + "a3": "Sega Dreamcast", + "a4": "Sega CD", + "valid": "Sega Dreamcast" + }, + { + "question": "Как называется грузинская народная песня, отправленная в космос на борту 'Вояджера'?", + "a1": "Чакруло", + "a2": "Сулико", + "a3": "Мравалжамиер", + "a4": "Цин цин царо", + "valid": "Чакруло" + }, + { + "question": "Как называется грузинский поэт и публицист XIX века, известный как 'отец нации'?", + "a1": "Илья Чавчавадзе", + "a2": "Акакий Церетели", + "a3": "Важа-Пшавела", + "a4": "Галактион Табидзе", + "valid": "Илья Чавчавадзе" + }, + { + "question": "Какое название носит организация, созданная Михаилом Ходорковским для поддержки гражданского общества в России?", + "a1": "Открытая Россия", + "a2": "Новая Газета", + "a3": "Голос", + "a4": "Мемориал", + "valid": "Открытая Россия" + }, + { + "question": "Кто из перечисленных политиков был мэром Екатеринбурга и известен своей независимой позицией?", + "a1": "Евгений Ройзман", + "a2": "Анатолий Локоть", + "a3": "Сергей Собянин", + "a4": "Александр Беглов", + "valid": "Евгений Ройзман" + }, + { + "question": "Какой вид бизнеса наиболее часто открывают русские эмигранты в Грузии?", + "a1": "Рестораны и кафе", + "a2": "ИТ-компании", + "a3": "Туристические агентства", + "a4": "Магазины одежды", + "valid": "ИТ-компании" + }, + { + "question": "Сколько палат в парламенте Грузии согласно Конституции?", + "a1": "Однопалатный", + "a2": "Двухпалатный", + "a3": "Трехпалатный", + "a4": "Четырехпалатный", + "valid": "Однопалатный" + }, + { + "question": "Какой максимальный срок лишения свободы предусмотрен Уголовным кодексом Грузии?", + "a1": "20 лет", + "a2": "25 лет", + "a3": "Пожизненное заключение", + "a4": "30 лет", + "valid": "Пожизненное заключение" + }, + { + "question": "Какое минимальное количество депутатов в Парламенте Грузии?", + "a1": "77", + "a2": "100", + "a3": "150", + "a4": "200", + "valid": "150" + }, + { + "question": "Как называется любимая фраза Джои в сериале 'Друзья'?", + "a1": "How you doin'?", + "a2": "What's up?", + "a3": "Hey there!", + "a4": "Long time no see!", + "valid": "How you doin'?" + }, + { + "question": "Какой город является столицей Исландии?", + "a1": "Рейкьявик", + "a2": "Хельсинки", + "a3": "Осло", + "a4": "Копенгаген", + "valid": "Рейкьявик" + }, + { + "question": "Кто изобретатель первого практичного телефона?", + "a1": "Томас Эдисон", + "a2": "Александр Белл", + "a3": "Никола Тесла", + "a4": "Гульельмо Маркони", + "valid": "Александр Белл" + }, + { + "question": "Как называется книга, написанная Никколо Макиавелли о политике?", + "a1": "Республика", + "a2": "Государь", + "a3": "Левиафан", + "a4": "Об общественном договоре", + "valid": "Государь" + }, + { + "question": "Как называется мыльная опера, в которой снимается Джои Триббиани?", + "a1": "Дни нашей жизни", + "a2": "Молодые и дерзкие", + "a3": "Всё мои дети", + "a4": "Главный госпиталь", + "valid": "Дни нашей жизни" + }, + { + "question": "Какой монарх объединил Восточную и Западную Грузию в начале XI века?", + "a1": "Баграт III", + "a2": "Давид IV", + "a3": "Георгий I", + "a4": "Тамара", + "valid": "Баграт III" + }, + { + "question": "Какой город является столицей Монголии?", + "a1": "Алма-Ата", + "a2": "Улан-Батор", + "a3": "Бишкек", + "a4": "Душанбе", + "valid": "Улан-Батор" + }, + { + "question": "Какой химический элемент является основным компонентом алмазов?", + "a1": "Углерод", + "a2": "Кремний", + "a3": "Сера", + "a4": "Бор", + "valid": "Углерод" + }, + { + "question": "Какой художник отрезал себе часть уха?", + "a1": "Пабло Пикассо", + "a2": "Клод Моне", + "a3": "Винсент Ван Гог", + "a4": "Сальвадор Дали", + "valid": "Винсент Ван Гог" + }, + { + "question": "Как называется наука о поведении и психологии животных?", + "a1": "Этология", + "a2": "Зоология", + "a3": "Биология", + "a4": "Антропология", + "valid": "Этология" + }, + { + "question": "Кто изобрел первый механический калькулятор?", + "a1": "Блез Паскаль", + "a2": "Готфрид Лейбниц", + "a3": "Чарльз Бэббидж", + "a4": "Алан Тьюринг", + "valid": "Блез Паскаль" + }, + { + "question": "Как зовут любимых питомцев Джои и Чендлера в сериале 'Друзья'?", + "a1": "Чик и Дак", + "a2": "Бёрд и Дак", + "a3": "Чикки и Дакки", + "a4": "Птичка и Утенок", + "valid": "Чик и Дак" + }, + { + "question": "Как называется газета, которую часто читает волшебный мир в Гарри Поттере?", + "a1": "Ежедневный Пророк", + "a2": "Волшебный Вестник", + "a3": "Магический Курьер", + "a4": "Вечерний Волшебник", + "valid": "Ежедневный Пророк" + }, + { + "question": "Как зовут эльфа-домовика семьи Чёрных?", + "a1": "Добби", + "a2": "Кикимер", + "a3": "Винки", + "a4": "Хокки", + "valid": "Кикимер" + }, + { + "question": "Какое полное имя Долорес Амбридж?", + "a1": "Джейн", + "a2": "Энн", + "a3": "Мэри", + "a4": "Лили", + "valid": "Джейн" + }, + { + "question": "Какой предмет преподавал Квиринус Квиррелл до того, как стал преподавателем защиты от темных искусств?", + "a1": "Трансфигурация", + "a2": "Маггловедение", + "a3": "Зельеварение", + "a4": "История магии", + "valid": "Маггловедение" + }, + { + "question": "Как зовут мать Нимфадоры Тонкс?", + "a1": "Андромеда Тонкс", + "a2": "Нарцисса Малфой", + "a3": "Беллатрисса Лестрейндж", + "a4": "Лили Поттер", + "valid": "Андромеда Тонкс" + }, + { + "question": "Кто сформулировал принцип неопределённости в квантовой механике?", + "a1": "Альберт Эйнштейн", + "a2": "Вернер Гейзенберг", + "a3": "Нильс Бор", + "a4": "Макс Планк", + "valid": "Вернер Гейзенберг" + }, + { + "question": "Какая планета Солнечной системы имеет наибольшее количество спутников?", + "a1": "Юпитер", + "a2": "Сатурн", + "a3": "Уран", + "a4": "Нептун", + "valid": "Сатурн" + }, + { + "question": "Какое число является следующим в ряде Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34?", + "a1": "44", + "a2": "55", + "a3": "60", + "a4": "65", + "valid": "55" + }, + { + "question": "Что станет больше, если его перевернуть вверх ногами?", + "a1": "Число 6", + "a2": "Число 9", + "a3": "Число 0", + "a4": "Число 8", + "valid": "Число 6" + } +] \ No newline at end of file diff --git a/data/versus.json b/data/versus.json new file mode 100644 index 0000000..1caa537 --- /dev/null +++ b/data/versus.json @@ -0,0 +1,114 @@ +[ + { + "text":"угадайка", + "description":"Угадай кто я - по стикеру на лбу" + }, + { + "text":"тест на устойчивость к юмору", + "description": "Кто первый засмеется с водой во рту" + }, + { + "text":"лучший китаец", + "description": "Кто быстрее съест палочками для суши зеленый горошек или консервированную кукурузу" + }, + { + "text": "Прыжки в длину", + "description": "тут надо самим угадать" + }, + { + "text": "грузинские буквы", + "description": "Кто отгадает больше грузинских букв и быстрее" + }, + { + "text": "лучший котик на тусовке", + "description": "кто лучше изобразит квадробера" + }, + { + "text": "Гонки на ложках", + "description": "перенести шарик на ложке, зажатой в зубах, до финиша" + }, + { + "text": "Сванская башня", + "description": "за 1 минуту построить башню из пластиковых стаканов" + }, + { + "text": "Перевороты монет", + "description": "Найди предмет" + }, + { + "text": "Скоростное рисование", + "description": "нарисовать лошадь за минуту" + }, + { + "text": "нарисуй хуйло", + "description": "нарисовать путина за минуту" + }, + { + "text": "сотрудник GWP", + "description": "кто точнее наполнит стакан до края" + }, + { + "text": "Скоростная чистка овоща", + "description": "кто быстрее очистит картофелину" + }, + { + "text": "Стрельба из рогатки", + "description": "попасть в цель мячиками" + }, + { + "text": "Найди отличия", + "description": "кто быстрее найдёт отличия на двух картинках" + }, + { + "text": "Переводка предмета без рук", + "description": "перенести мелкий предмет, держа его между коленями" + }, + { + "text": "менеджер GWP", + "description": "перенести воду в ложке, не пролив" + }, + { + "text": "Бой подушками", + "description": "пока кто-то не выронит подушку." + }, + { + "text": "шарик", + "description": "кто быстрее надует воздушный шарик" + }, + { + "text": "Камень, ножницы, бумага", + "description": "Сыграть три раунда и определить победителя" + }, + { + "text": "Сложи бумажный самолетик и запусти", + "description": "Чей самолетик пролетит дальше" + }, + { + "text": "Лимбо", + "description": "Пройти под планкой, не задев её, при каждом раунде ниже" + }, + { + "text": "Пой без слов", + "description": "Напеть мелодию песни, чтобы другой отгадал" + }, + { + "text": "Нарисуй вслепую", + "description": "Нарисовать предмет с закрытыми глазами" + }, + { + "text": "Балансировка книги на голове", + "description": "Кто дольше продержится с книгой на голове, выполняя задания" + }, + { + "text": "Быстрый переводчик", + "description": "Перевести фразы на другой язык быстрее соперника" + }, + { + "text": "Словесный бой", + "description": "Назвать слова на заданную букву, пока не закончится время" + }, + { + "text": "Бумажный самолетик на точность", + "description": "Запустить самолетик так, чтобы он попал в цель" + } +] \ No newline at end of file -- 2.45.2 From d314197d2f6898897e8dee872741588781e53311 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 27 Nov 2024 11:46:29 +0400 Subject: [PATCH 29/32] fix review point --- data/versus.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/data/versus.json b/data/versus.json index 1caa537..4c2f0b0 100644 --- a/data/versus.json +++ b/data/versus.json @@ -31,10 +31,6 @@ "text": "Сванская башня", "description": "за 1 минуту построить башню из пластиковых стаканов" }, - { - "text": "Перевороты монет", - "description": "Найди предмет" - }, { "text": "Скоростное рисование", "description": "нарисовать лошадь за минуту" -- 2.45.2 From a8a3c6d48a0b86ad7935e4eb9e2954d8c85e8a7f Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 27 Nov 2024 13:36:37 +0400 Subject: [PATCH 30/32] TGD-52 Fix --- src/game/game.controller.ts | 6 ++++++ src/game/game.service.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index 6c28a04..30f6562 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -45,4 +45,10 @@ export class GameController { this.logger.warn(`[clearQueue] enter`); await this.gameService.clearGameQueue(); } + + @Post('simulate-valid-answer') + async simulateValidAnswer() { + this.logger.verbose(`[simulateValidAnswer] enter`); + return await this.gameService.simulateValidAnswer(); + } } diff --git a/src/game/game.service.ts b/src/game/game.service.ts index ae416d3..0092070 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -10,6 +10,8 @@ import {ConfigService} from "@nestjs/config"; import {gameCards} from "./entities/cards.entities"; import {IEmptyNotification} from "../Consts/types"; import {ClientNotificationType} from "../socket/socket.gateway"; +import {GetGuestQuery} from "../guests/queries/getguest.query"; +import {ValidAnswerReceivedEvent} from "./events/valid-answer.recieved"; @Injectable() export class GameService implements OnApplicationBootstrap{ @@ -110,4 +112,8 @@ export class GameService implements OnApplicationBootstrap{ await this.gameQueueModel.deleteMany({}).exec(); return { result: true }; } + + async simulateValidAnswer() { + this.eventBus.publish(new ValidAnswerReceivedEvent(11178819, 'test', '')); + } } -- 2.45.2 From 2b59393d98d399983713702c471e4f0e0f8d34d1 Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Wed, 27 Nov 2024 21:20:34 +0400 Subject: [PATCH 31/32] remove shitcard --- src/game/entities/cards.entities.ts | 2 +- src/quiz/quiz.service.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/game/entities/cards.entities.ts b/src/game/entities/cards.entities.ts index 7bafa9f..5ee0af4 100644 --- a/src/game/entities/cards.entities.ts +++ b/src/game/entities/cards.entities.ts @@ -262,7 +262,7 @@ export class BanPlayer extends GameCard { export const gameCards: typeof GameCard[] = [ DoubleTreasureCard, StolePrizeCard, - ShitCard, + // ShitCard, LuckyCard, AvoidPenaltyCard, BanPlayer, diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 0fca176..96c27e2 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -24,9 +24,7 @@ import {BeginVersusCommand} from "../game/commands/begin-versus.command"; @Injectable({ scope: Scope.TRANSIENT }) export class QuizService { - private readonly answerNumbers = Messages.answerNumbers; private readonly logger = new Logger(QuizService.name); - private AcceptAnswersFromAllMembers: boolean = true; // TODO: move this to configurable state constructor( @InjectModel(Question.name) private questionModel: Model, @InjectModel(QuestionStorage.name) -- 2.45.2 From 2073dcff93dc53ecc52ba62d3fb8b708aea963ef Mon Sep 17 00:00:00 2001 From: Kirill Ivlev Date: Thu, 28 Nov 2024 01:30:41 +0400 Subject: [PATCH 32/32] bugfixes --- .../proceed-game-queue-command.handler.ts | 2 +- src/game/entities/cards.entities.ts | 2 +- src/game/events/state-changed.event.ts | 4 ++++ src/game/game.service.ts | 21 ++++++++++++++++++- src/game/versus/versus.service.ts | 3 +++ src/guests/guests.service.ts | 3 +++ .../state-changed-event.handler.ts | 17 +++++++++++++++ src/quiz/quiz.controller.ts | 5 +++++ src/quiz/quiz.module.ts | 7 ++++++- src/quiz/quiz.service.ts | 19 ++++++++++++++--- src/schemas/game-queue.schema.ts | 1 + src/schemas/question.schema.ts | 2 ++ src/state/state.controller.ts | 20 +++++++++--------- src/state/state.service.ts | 3 +++ 14 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 src/game/events/state-changed.event.ts create mode 100644 src/quiz/event-handlers/state-changed-event.handler.ts diff --git a/src/game/comand-handlers/proceed-game-queue-command.handler.ts b/src/game/comand-handlers/proceed-game-queue-command.handler.ts index 28dcd2c..1cd5497 100644 --- a/src/game/comand-handlers/proceed-game-queue-command.handler.ts +++ b/src/game/comand-handlers/proceed-game-queue-command.handler.ts @@ -27,7 +27,7 @@ export class GameProceedGameQueueCommandHandler return this.cmdBus.execute(new NextQuestionCommand()); } this.sharedService.notifyAllClients(ClientNotificationType.GameQueueItem, { - _id: item.id, + _id: item._id, completed: item.completed, target: item.target, type: item.type, diff --git a/src/game/entities/cards.entities.ts b/src/game/entities/cards.entities.ts index 5ee0af4..2366170 100644 --- a/src/game/entities/cards.entities.ts +++ b/src/game/entities/cards.entities.ts @@ -250,7 +250,7 @@ export class BanPlayer extends GameCard { async handle() { await this.commandBus.execute( - new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, 2,false) + new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, getRandomInt(2,3), false) ) await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null)); this.eventBus.subscribe((data) =>{ diff --git a/src/game/events/state-changed.event.ts b/src/game/events/state-changed.event.ts new file mode 100644 index 0000000..76acc40 --- /dev/null +++ b/src/game/events/state-changed.event.ts @@ -0,0 +1,4 @@ +export class StateChangedEvent { + constructor(state: string) { + } +} \ No newline at end of file diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 0092070..abd9bba 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -52,7 +52,26 @@ export class GameService implements OnApplicationBootstrap{ } async getGameQueueItem() { - return this.gameQueueModel.findOne({ completed: false }).exec(); + const item = await this.gameQueueModel.aggregate([ + { + $match: { completed: false } + }, + { + $addFields: { + priority: { + $cond: [{ $eq: ["$type", "versus"] }, 1, 0] + } + } + }, + { + $sort: { priority: -1 } + }, + { + $limit: 1 + } + ]).exec(); + console.log(item[0]); + return item[0]; } async markQueueAsCompleted(id: string| null) { diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts index a4dbcb4..782e0db 100644 --- a/src/game/versus/versus.service.ts +++ b/src/game/versus/versus.service.ts @@ -13,6 +13,8 @@ import {GuestPropertyNamesConsts} from "../../Consts/guest-property-names.consts import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command"; import {IVersusBeginSocketEvent, IVersusEndSocketEvent} from "../../Consts/types"; import {ClientNotificationType} from "../../socket/socket.gateway"; +import {CreateNewQueueItemCommand} from "../commands/create-new-queue-item.command"; +import {GameQueueTypes} from "../../schemas/game-queue.schema"; @Injectable() export class VersusService { @@ -42,6 +44,7 @@ export class VersusService { async beginVersus(player1: number, player2: number) { const [p1data,p2data] = await Promise.all([this.guestService.findById(player1), this.guestService.findById(player2)]); + await this.cmdBus.execute(new CreateNewQueueItemCommand(player1, GameQueueTypes.versus)); await this.sharedService.setConfig(VersusService.configKeyCurrentAction, JSON.stringify({ action:'versus', data: { diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index 97fa427..959f713 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -76,6 +76,9 @@ export class GuestsService { async findById(id: number) { const result = await this.guestModel.findOne({ telegramId: id }).exec(); + if(!result) { + return null; + } delete result.photo; return result; } diff --git a/src/quiz/event-handlers/state-changed-event.handler.ts b/src/quiz/event-handlers/state-changed-event.handler.ts new file mode 100644 index 0000000..e8cb84c --- /dev/null +++ b/src/quiz/event-handlers/state-changed-event.handler.ts @@ -0,0 +1,17 @@ +import {EventsHandler, IEventHandler} from "@nestjs/cqrs"; +import {StateChangedEvent} from "../../game/events/state-changed.event"; +import {QuizService} from "../quiz.service"; +import {Logger} from "@nestjs/common"; + +@EventsHandler(StateChangedEvent) +export class StateChangedEventHandler implements IEventHandler { + logger = new Logger(StateChangedEventHandler.name); + constructor(private quizService: QuizService) { + } + + async handle(event: StateChangedEvent) { + this.logger.verbose(`[StateChangedEventHandler] enter, event: ${event}}`) + await this.quizService.calculateEndgamePoints(); + } + +} \ No newline at end of file diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index 10818be..554b603 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -26,6 +26,11 @@ export class QuizController { return this.quizService.proceedWithGame(); } + @Post('timeout') + async Timeout() { + return await this.quizService.questionTimeout(); + } + @Post('questions') async postQuestion(@Body() questionDto: QuestionDto[]) { return await this.quizService.populateQuestions(questionDto); diff --git a/src/quiz/quiz.module.ts b/src/quiz/quiz.module.ts index 3975216..e3ed269 100644 --- a/src/quiz/quiz.module.ts +++ b/src/quiz/quiz.module.ts @@ -12,12 +12,17 @@ import { MarkQuestionsAsUnansweredCommandHandler } from './command-handlers/mark import { PenaltyModule } from '../penalty/penalty.module'; import {ConfigModule, ConfigService} from "@nestjs/config"; import {Config, ConfigSchema} from "../schemas/config.schema"; +import {StateChangedEventHandler} from "./event-handlers/state-changed-event.handler"; const cmdHandlers = [ GameNextQuestionCommandHandler, MarkQuestionsAsUnansweredCommandHandler, ]; +const eventHandlers = [ + StateChangedEventHandler +] + @Global() @Module({ imports: [ @@ -33,6 +38,6 @@ const cmdHandlers = [ ], controllers: [QuizController], exports: [QuizService], - providers: [QuizService,ConfigService, ...cmdHandlers], + providers: [QuizService,ConfigService, ...cmdHandlers, ...eventHandlers], }) export class QuizModule {} diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 96c27e2..21be7bb 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -60,6 +60,9 @@ export class QuizService { ); // check that answer exist const shortAnswers = question.answers.map((answer) => answer.substring(0,50)); + if(question.countdownFinished) { + return; + } const shortValidAnswer = question.valid.substring(0,50); if(shortAnswers.indexOf(answer) === -1) { this.logger.warn(`[validateAnswer] this question is not on game now`); @@ -124,7 +127,7 @@ export class QuizService { return false; } const diff = Math.abs(new Date(answers[0].time).getTime() - new Date(answers[1].time).getTime()) / 1000; - return diff <= 5; + return diff <= 1; } async calculateScore() { @@ -148,7 +151,7 @@ export class QuizService { const winner = sortedAnswers.find((answer) => answer.valid); let targetUser = 0; if(winner) { - const totalWinningScore = 80; + const totalWinningScore = 50; sortedAnswers.filter(x => x.valid).forEach((answer) => { this.logger.debug(`Giving 1 point to all who answered right`); this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user, @@ -165,7 +168,7 @@ export class QuizService { )); } } - await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 15)); + await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 5)); this.logger.debug(`Giving 1 point to first`); await this.commandBus.execute(new IncreasePlayerScoreCommand(winner.user,1)); targetUser = winner.user; @@ -214,6 +217,9 @@ export class QuizService { } } await this.sharedService.setConfig('endgame-points', JSON.stringify(result)); + await this.commandBus.execute(new IncreasePlayerScoreCommand(result.maxInvalidAnswers.id, 2)); + await this.commandBus.execute(new IncreasePlayerScoreCommand(result.maxPenalties.id, 2)); + await this.commandBus.execute(new IncreasePlayerScoreCommand(result.maxRewards.id, -2)); return result; } @@ -340,4 +346,11 @@ export class QuizService { const res = await this.sharedService.getConfig('endgame-points'); return JSON.parse(res.value); } + + async questionTimeout() { + const question = await this.get(); + question.countdownFinished = true; + await question.save(); + return question; + } } diff --git a/src/schemas/game-queue.schema.ts b/src/schemas/game-queue.schema.ts index 15ed27c..80107c2 100644 --- a/src/schemas/game-queue.schema.ts +++ b/src/schemas/game-queue.schema.ts @@ -9,6 +9,7 @@ export enum GameQueueTypes { screpaAnounce = 'screpa', showresults = 'show_results', extra_points = 'extra_points', + versus = 'versus', } export type GameQueueDocument = GameQueue & Document; diff --git a/src/schemas/question.schema.ts b/src/schemas/question.schema.ts index 2f23742..3cb1762 100644 --- a/src/schemas/question.schema.ts +++ b/src/schemas/question.schema.ts @@ -31,5 +31,7 @@ export class Question { userAnswers: QuestionAnswer[]; @Prop({ default: false }) scoreCalculated: boolean; + @Prop({ default: false}) + countdownFinished: boolean; } export const QuestionSchema = SchemaFactory.createForClass(Question); diff --git a/src/state/state.controller.ts b/src/state/state.controller.ts index 1ad04e7..dcd2e75 100644 --- a/src/state/state.controller.ts +++ b/src/state/state.controller.ts @@ -38,16 +38,16 @@ export class StateController { if (setStateDto.value === 'quiz') { this.eventBus.publish(new GameStartedEvent()); } else if(setStateDto.value === 'onboarding') { - this.telegramService.send( - { cmd: CommandsConsts.SetCommands }, - [ - { command: 'start', description: 'главное меню'}, - { command: 'cards', description: 'сыграть карту'}, - { command: 'question', description: 'вернутся к вопросу'} - ] - ).subscribe(() => { - this.logger.verbose('Bot commands updated'); - }); + // this.telegramService.send( + // { cmd: CommandsConsts.SetCommands }, + // [ + // { command: 'start', description: 'главное меню'}, + // { command: 'cards', description: 'сыграть карту'}, + // { command: 'question', description: 'вернутся к вопросу'} + // ] + // ).subscribe(() => { + // this.logger.verbose('Bot commands updated'); + // }); } else { this.logger.verbose('reset commands'); this.telegramService.emit({ cmd: CommandsConsts.ResetCommands }, {}); diff --git a/src/state/state.service.ts b/src/state/state.service.ts index 783e24d..c6937c2 100644 --- a/src/state/state.service.ts +++ b/src/state/state.service.ts @@ -5,6 +5,7 @@ import { Model } from 'mongoose'; import { EventBus } from '@nestjs/cqrs'; import { PrepareGameEvent } from '../game/events/prepare-game.event'; import {IStateInfo} from "../Consts/types"; +import {StateChangedEvent} from "../game/events/state-changed.event"; interface StateDTO { name: string; @@ -35,6 +36,8 @@ export class StateService { if (newValue === 'onboarding') { this.eventBus.publish(new PrepareGameEvent()); } + this.eventBus.publish(new StateChangedEvent(newValue)); + const stateEntity = await this.getState(name); stateEntity.value = newValue; await stateEntity.save(); -- 2.45.2