Merge pull request 'Y2024 release' (#1) from 2024edition into main

Reviewed-on: #1
This commit is contained in:
webster 2024-11-28 01:33:24 +04:00
commit 682126e043
119 changed files with 3223 additions and 342 deletions

1
.husky/pre-commit Normal file
View file

@ -0,0 +1 @@
npm test

156
data/gifts.json Normal file
View file

@ -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
}
]

1034
data/questions.json Normal file

File diff suppressed because it is too large Load diff

110
data/versus.json Normal file
View file

@ -0,0 +1,110 @@
[
{
"text":"угадайка",
"description":"Угадай кто я - по стикеру на лбу"
},
{
"text":"тест на устойчивость к юмору",
"description": "Кто первый засмеется с водой во рту"
},
{
"text":"лучший китаец",
"description": "Кто быстрее съест палочками для суши зеленый горошек или консервированную кукурузу"
},
{
"text": "Прыжки в длину",
"description": "тут надо самим угадать"
},
{
"text": "грузинские буквы",
"description": "Кто отгадает больше грузинских букв и быстрее"
},
{
"text": "лучший котик на тусовке",
"description": "кто лучше изобразит квадробера"
},
{
"text": "Гонки на ложках",
"description": "перенести шарик на ложке, зажатой в зубах, до финиша"
},
{
"text": "Сванская башня",
"description": "за 1 минуту построить башню из пластиковых стаканов"
},
{
"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": "Запустить самолетик так, чтобы он попал в цель"
}
]

34
package-lock.json generated
View file

@ -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",
@ -41,7 +42,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 +1906,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 +1933,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",
@ -5544,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",

View file

@ -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",
@ -41,6 +42,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",
@ -53,7 +55,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",

View file

@ -0,0 +1,6 @@
export class FeatureFlagsConsts {
static EnableEndgamePoints = 'EnableEndgamePoints';
static DontMarkQuestionsAsCompleted = 'DontMarkQuestionsAsCompleted';
static DisableVoice = 'DisableVoice';
static StartVersusIfPlayersAnsweredInSameTime = 'StartVersusIfPlayersAnsweredInSameTime';
}

View file

@ -9,4 +9,6 @@ export class CommandsConsts {
static GetCards = 'GetCards';
static ApplyDebuff = 'ApplyDebuff';
static CompleteQueue = 'CompleteQueue';
}
static GetQuestion = 'GetQuestion';
static QuestionAnswer = "QuestionAnswer";
}

View file

@ -0,0 +1,5 @@
export class GameStateConsts {
static Main = 'main';
static EndgamePoints = 'endgamepoints';
static Finish = 'finish';
}

View file

@ -0,0 +1,4 @@
export class GuestPropertyNamesConsts {
static VersusWonCount = 'versusWonCount';
static VersusLoseCount = 'versusLoseCount';
}

65
src/Consts/types.d.ts vendored Normal file
View file

@ -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;
}

View file

@ -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],
exports: [AppService, SocketGateway],
controllers: [AppController, FeatureflagController],
providers: [AppService, SocketGateway, SchedulerService, FeatureflagService],
exports: [AppService, SocketGateway, FeatureflagService],
})
export class AppModule {}

View file

@ -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>(CardsController);

View file

@ -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>(CardsService);

View file

@ -0,0 +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>(FeatureflagController);
featureflagService = module.get<FeatureflagService>(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);
});
});

View file

@ -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()
async setFeatureFlag(@Body() ffState: { name: string, state: boolean }) {
return await this.featureflagService.setFeatureFlag(ffState.name, ffState.state );
}
}

View file

@ -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>(FeatureflagService);
sharedService = module.get<SharedService>(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}`);
});
});

View file

@ -0,0 +1,43 @@
import {Injectable, Logger} from '@nestjs/common';
import {SharedService} from "../shared/shared.service";
import {ClientNotificationType} from "../socket/socket.gateway";
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<IFeatureFlagStatus> {
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: ffState
}
}
async setFeatureFlag(id: string, status: boolean) : Promise<IFeatureFlagStatus> {
this.logger.verbose(`Setting feature flag status for ${id} to ${status} `);
const result = await this.sharedService.setConfig(`featureflag/${id}`, status.toString());
const ffStatus: IFeatureFlagStatus = {
name: id,
state: result.value !== 'false',
}
this.sharedService.notifyAllClients<IFeatureFlagStatus>(ClientNotificationType.FeatureFlagChanged, ffStatus);
return ffStatus;
}
}

View file

@ -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<BeginVersusCommand> {
constructor(private versusService:VersusService) {
}
execute(command: BeginVersusCommand): Promise<any> {
return this.versusService.beginVersus(command.sourceId,command.destinationId);
}
}

View file

@ -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<CreateNewQueueItemCommand> {
private logger = new Logger(CreateNewQueueItemCommandHandler.name);
constructor(
private gameService: GameService,
) {
}
async execute(command: CreateNewQueueItemCommand): Promise<any> {
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);
}

View file

@ -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<any> {
this.logger.verbose(`Player winning a prize ${command.telegramId}`);
await this.guestService.incrementPrizeCount(command.telegramId);
return this.gameService.addTaskToGameQueue(
command.telegramId,
GameQueueTypes.giveOutAPrize,

View file

@ -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<IGameQueueSocketEvent>(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(

View file

@ -18,18 +18,21 @@ export class SelectTargetPlayerHandler implements ICommandHandler<SelectTargetPl
}
async execute(command: SelectTargetPlayerCommand): Promise<any> {
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<MqtMessageModel,ChatMessageRequestModel>(
{ cmd: CommandsConsts.SendMessage},

View file

@ -0,0 +1,6 @@
export class BeginVersusCommand {
constructor(public sourceId: number, public destinationId: number) {
}
}

View file

@ -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) {
}
}

View file

@ -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,43 @@ 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));
});
}
async handle() {
await this.commandBus.execute(
new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.versus, 0, false)
)
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;
@ -197,7 +235,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) => {
@ -213,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, getRandomInt(2,3), false)
)
await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null));
this.eventBus.subscribe((data) =>{
@ -225,8 +262,9 @@ export class BanPlayer extends GameCard {
export const gameCards: typeof GameCard[] = [
DoubleTreasureCard,
StolePrizeCard,
ShitCard,
// ShitCard,
LuckyCard,
AvoidPenaltyCard,
BanPlayer
BanPlayer,
VersusCard,
];

View file

@ -1,3 +1,4 @@
export class DebuffsConsts {
static bannedFor = 'bannedFor';
static versus = 'versus';
}

View file

@ -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}`));
}
}

View file

@ -10,9 +10,6 @@ export class GameWrongAnswerReceivedEventHandler
}
async handle(event: WrongAnswerReceivedEvent) {
await this.gameService.addTaskToGameQueue(
event.tId,
GameQueueTypes.penalty,
);
//
}
}

View file

@ -0,0 +1,4 @@
export class StateChangedEvent {
constructor(state: string) {
}
}

View file

@ -1,5 +1,9 @@
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;
@ -7,6 +11,10 @@ describe('GameController', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GameController],
providers: [
{ provide: GameService, useValue: GameServiceMock },
{ provide: VersusService, useValue: VersusServiceMock },
]
}).compile();
controller = module.get<GameController>(GameController);

View file

@ -1,9 +1,11 @@
import { Controller, Get, Param, Post } from '@nestjs/common';
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 {
constructor(private gameService: GameService) {
private readonly logger = new Logger(GameController.name);
constructor(private gameService: GameService, private versusService: VersusService) {
}
@Post(':id/complete')
@ -30,4 +32,23 @@ export class GameController {
async playExtraCards() {
return this.gameService.playExtraCards();
}
@Get('state-details')
async getStateDetails() {
return this.gameService.getStateDetails();
}
@Post('clear-queue')
async clearQueue() {
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();
}
}

View file

@ -17,6 +17,11 @@ 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";
import {BeginVersusCommandHandler} from "./comand-handlers/begin-versus-command.handler";
import {CheckIfAnotherVersusInProgressHandler} from "./queries/handlers/check-if-another-versus-in-progress.handler";
const eventHandlers = [
@ -34,19 +39,23 @@ const commandHandlers = [
GamePrizeChanceIncreasedEventHandler,
GameProceedGameQueueCommandHandler,
SelectTargetPlayerHandler,
SendBetweenRoundsActionsHandler
SendBetweenRoundsActionsHandler,
BeginVersusCommandHandler,
];
const queryHandlers = [CheckIfAnotherVersusInProgressHandler];
@Global()
@Module({
imports: [
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,...queryHandlers, VersusService],
exports: [GameService],
controllers: [GameController],
controllers: [GameController, VersusController],
})
export class GameModule {}

View file

@ -1,12 +1,35 @@
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";
import {Guest} from "../schemas/guest.schema";
import {GuestsService} from "../guests/guests.service";
import {GuestsServiceMock} from "../mocks/guests-service.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 },
{ provide: GuestsService, useValue: GuestsServiceMock }
],
}).compile();
service = module.get<GameService>(GameService);

View file

@ -1,18 +1,17 @@
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 {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{
@ -53,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) {
@ -63,35 +81,27 @@ 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();
this.sharedService.sendSocketNotificationToAllClients(
SocketEvents.QUEUE_COMPLETED,
{},
);
this.sharedService.notifyAllClients<IEmptyNotification>(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<IEmptyNotification>(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<IEmptyNotification>(ClientNotificationType.GameResumed,{});
return Promise.resolve({ result: true });
}
@ -112,4 +122,17 @@ export class GameService implements OnApplicationBootstrap{
cardInstance.setupHandlers(this.eventBus, this.commandBus, this.queryBus);
})
}
async getStateDetails() {
return await this.sharedService.getConfig('current_action') || null;
}
async clearGameQueue() {
await this.gameQueueModel.deleteMany({}).exec();
return { result: true };
}
async simulateValidAnswer() {
this.eventBus.publish(new ValidAnswerReceivedEvent(11178819, 'test', ''));
}
}

View file

@ -0,0 +1,3 @@
export class CheckIfAnotherVersusInProgressQuery {
}

View file

@ -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<CheckIfAnotherVersusInProgressHandler> {
constructor(private versusService: VersusService) {
}
async execute(query: CheckIfAnotherVersusInProgressHandler): Promise<any> {
return await this.versusService.checkIfAnotherVersusInProgress();
}
}

View file

@ -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>(VersusController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View file

@ -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, loser: number }) {
return await this.versusService.complete(payload.winner, payload.loser);
}
@Post('reset-all')
async markAllUncompleted() {
return await this.versusService.markAllAsUncompleted();
}
}

View file

@ -0,0 +1,61 @@
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, 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<Versus>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
VersusService,
{ provide: GuestsService, useValue: GuestsServiceMock },
{ provide: SharedService, useValue: SharedService },
{ provide: getModelToken(Versus.name), useValue: mockVersusModel },
{ provide: CommandBus, useValue: CommandBus },
{ provide: QueryBus, useValue: QueryBusMock },
],
}).compile();
service = module.get<VersusService>(VersusService);
versusModel = module.get<Model<VersusDocument>>(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();
});
})
});

View file

@ -0,0 +1,145 @@
import {Injectable, Logger} from '@nestjs/common';
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, QueryBus} from "@nestjs/cqrs";
import {IncreasePlayerScoreCommand} from "../../guests/command/increase-player-score.command";
import {IncreasePlayerWinningRateCommand} from "../commands/increase-player-winning-rate.command";
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";
import {CreateNewQueueItemCommand} from "../commands/create-new-queue-item.command";
import {GameQueueTypes} from "../../schemas/game-queue.schema";
@Injectable()
export class VersusService {
static configKeyCurrentAction = 'current_action';
static configKeyActiveVersus = 'active_versus';
private logger = new Logger(VersusService.name);
constructor(
private guestService: GuestsService,
private sharedService: SharedService,
private queryBus: QueryBus,
@InjectModel(Versus.name) private versusModel: Model<VersusDocument>,
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.cmdBus.execute(new CreateNewQueueItemCommand(player1, GameQueueTypes.versus));
await this.sharedService.setConfig(VersusService.configKeyCurrentAction, JSON.stringify({
action:'versus',
data: {
player1: player1,
player2: player2,
player1name: p1data.name,
player2name: p2data.name,
}
}));
this.sharedService.notifyAllClients<IVersusBeginSocketEvent>(ClientNotificationType.BeginVersus, {
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 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();
// @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, 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();
const tasks = [];
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 = 1;
} else {
wonCount = +wonCount++;
}
if(!loseCount) {
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<IVersusEndSocketEvent>(ClientNotificationType.EndVersus, {
winner: winner
}
);
return item;
}
async checkIfAnotherVersusInProgress() {
this.logger.debug(`checkIfAnotherVersusInProgress enter`)
const currentAction = await this.sharedService.getConfig(VersusService.configKeyCurrentAction);
if(!currentAction) {
return false;
}
return currentAction.value !== '';
}
}

6
src/game/versus/versus.types.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
export interface VersusDto {
id: string;
text: string;
completed: boolean;
description: string;
}

View file

@ -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>(GiftsController);

View file

@ -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>(GiftsService);

View file

@ -15,9 +15,8 @@ export class GetGuestPropertyHandler implements ICommandHandler<GetGuestProperty
async execute(command: GetGuestPropertyQuery): Promise<string> {
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}`);
}

View file

@ -28,13 +28,16 @@ export class TgPostCardsToUserCommandHandler implements ICommandHandler<PostCard
},
}
if (command.cards.length === 0) {
this.telegramService.emit({ cmd: CommandsConsts.SendMessage }, { chatId: command.chatId, message: "У вас нет карт которые можно сейчас использовать"});
return;
}
command.cards.forEach((card) => {
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),

View file

@ -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<IncreasePlayerScoreCommand> {
private logger = new Logger(IncreasePlayerScoreCommandHandler.name);
constructor(private guestService: GuestsService) {
}
async execute(command: IncreasePlayerScoreCommand): Promise<any> {
this.logger.verbose(`IncreasePlayerScoreCommandHandler: entering, arguments: player: ${command.user}, amount: ${command.score}`);
await this.guestService.updatePlayerScore(command.user, command.score);
return true;
}
}

View file

@ -0,0 +1,4 @@
export class IncreasePlayerScoreCommand {
constructor(public user: number, public score: number) {
}
}

View file

@ -20,36 +20,36 @@ export class RemoveCardFromUserCommandHandler implements ICommandHandler<RemoveC
}
async execute(command: RemoveCardFromUserCommand): Promise<any> {
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<MqtMessageModel, ChatMessageRequestModel>({
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<MqtMessageModel, ChatMessageRequestModel>({
// cmd: CommandsConsts.SendMessage,
// }, {
// chatId: guest.chatId,
// message: Messages.SELECT_CARD,
// extra: extra,
// })
}
}

View file

@ -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) {
}
}

View file

@ -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);
}
}

5
src/guests/guest.types.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
export interface GuestNamesInCases {
SubjectiveCase: string;
AccusativeCase: string;
GenitiveCase: string;
}

View file

@ -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>(GuestsController);

View file

@ -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 };
}
}

View file

@ -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,8 +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,
@ -32,6 +36,7 @@ const commandHandlers = [
IncreasePlayerWinningRateCommandHandler,
GetGuestPropertyHandler,
SetGuestPropertyCommandHandler,
IncreasePlayerScoreCommandHandler,
];
@ -60,7 +65,7 @@ const eventHandlers = [
CardsModule,
],
providers: [GuestsService, ConfigService, ...commandHandlers,...QueryHandlers, ...eventHandlers, ],
exports: [GuestsService],
exports: [GuestsService,],
controllers: [GuestsController],
})
export class GuestsModule {}

View file

@ -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>(GuestsService);

View file

@ -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';
@ -24,11 +24,11 @@ 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";
import {GuestNamesInCases} from "./guest.types";
import {QuizService} from "../quiz/quiz.service";
@Injectable()
export class GuestsService {
@ -66,12 +66,21 @@ export class GuestsService {
return this.guestModel.find().exec();
}
getModel() {
return this.guestModel;
}
async filter(properties: object) {
return this.guestModel.find(properties).exec();
}
async findById(id: number) {
return this.guestModel.findOne({ telegramId: id }).exec();
const result = await this.guestModel.findOne({ telegramId: id }).exec();
if(!result) {
return null;
}
delete result.photo;
return result;
}
async hideKeyboard(text: string) {
@ -84,17 +93,26 @@ export class GuestsService {
this.logger.verbose(`Keyboard hidden`);
}
async postQuestion(questionDto: QuestionDto, targetId = null) {
const guests = await this.findAll();
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) => {
@ -310,6 +328,15 @@ export class GuestsService {
await this.guestModel.updateMany({}, { prizeChance: 0 });
}
async getGuestNameInCases(telegramId: number): Promise<GuestNamesInCases> {
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);
@ -317,7 +344,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}`);
@ -327,8 +353,8 @@ 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.penalty));
//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;
}
@ -341,4 +367,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();
}
}

View file

@ -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)

View file

@ -1,20 +1,21 @@
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 {
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) {
@ -48,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 })
@ -58,4 +59,10 @@ export class GuestsMessageController {
);
return;
}
@MessagePattern({ cmd: CommandsConsts.GetQuestion})
async getQuestion(@Payload() data: { user: number, inline: false}) {
await this.quizService.displayQuestionForUser(data.user);
}
}

View file

@ -2,4 +2,10 @@ export interface ValidateAnswerModel {
answer: string;
user: number;
name: string;
}
export interface ValidateAnswerInline {
answer: string;
user: number;
name: string;
}

View file

@ -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,
);
@ -30,12 +31,13 @@ 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})
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})

View file

@ -0,0 +1,3 @@
export const CardsServiceMock = {
}

View file

@ -0,0 +1,3 @@
export const ClientProxyMock = {
}

View file

@ -0,0 +1,3 @@
export const CommandbusMock = {
execute: jest.fn(),
}

View file

@ -0,0 +1,3 @@
export const ConfigServiceMock = {
get: jest.fn(),
}

View file

@ -0,0 +1,3 @@
export const EventbusMock = {
}

View file

@ -0,0 +1,4 @@
export const FeatureflagServiceMock = {
getFeatureFlag: jest.fn(() => Promise.resolve(false)),
setFeatureFlag: jest.fn(() => Promise.resolve(false))
}

View file

@ -0,0 +1,3 @@
export const GameServiceMock = {
}

View file

@ -0,0 +1,3 @@
export const GiftServiceMock = {
getRemainingPrizeCount: () => jest.fn(),
}

View file

@ -0,0 +1,3 @@
export const GuestsServiceMock = {
updatePenaltiesCount: jest.fn(),
}

View file

@ -0,0 +1,3 @@
export const HttpServiceMock = {
}

View file

@ -0,0 +1,3 @@
export const PenaltyServiceMock = {
}

View file

@ -0,0 +1,3 @@
export const QueryBusMock = {
}

View file

@ -0,0 +1,3 @@
export const QuizServiceMock = {
getRemainQuestionCount: () =>jest.fn(),
}

View file

@ -0,0 +1,5 @@
export const SharedServiceMock = {
setConfig: jest.fn(),
getConfig: jest.fn(),
notifyAllClients: jest.fn(),
}

View file

@ -0,0 +1,3 @@
export const SocketGatewayMock = {
}

View file

@ -0,0 +1,3 @@
export const StateServiceMock = {
setState: jest.fn(),
}

View file

@ -0,0 +1,3 @@
export class VersusServiceMock {
}

View file

@ -0,0 +1,3 @@
export const VoiceServiceMock = {
}

View file

@ -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>(PenaltyController);

View file

@ -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>(PenaltyService);

View file

@ -1,8 +1,10 @@
export interface QuestionDto {
id: string;
text: string;
answers: string[];
valid: string;
note: string | null;
qId: string;
}

View file

@ -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<StateChangedEvent> {
logger = new Logger(StateChangedEventHandler.name);
constructor(private quizService: QuizService) {
}
async handle(event: StateChangedEvent) {
this.logger.verbose(`[StateChangedEventHandler] enter, event: ${event}}`)
await this.quizService.calculateEndgamePoints();
}
}

View file

@ -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>(QuizController);
quizService = module.get<QuizService>(QuizService);
});
it('should be defined', () => {

View file

@ -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 {
@ -16,11 +16,21 @@ export class QuizController {
return await this.quizService.setQuestion(qustionDto);
}
@Get('question-results')
async GetQuestionResults() {
return await this.quizService.getQuestionResults();
}
@Post('proceed')
async Get() {
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);
@ -45,4 +55,15 @@ export class QuizController {
async dealPrize() {
return this.quizService.dealPrize();
}
@Post('calculate-endgame-extrapoints')
async endgameExtrapoints()
{
return await this.quizService.calculateEndgamePoints();
}
@Get('endgame-results')
async endgameResults() {
return await this.quizService.getEndgameResults();
}
}

View file

@ -11,12 +11,18 @@ 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";
import {StateChangedEventHandler} from "./event-handlers/state-changed-event.handler";
const cmdHandlers = [
GameNextQuestionCommandHandler,
MarkQuestionsAsUnansweredCommandHandler,
];
const eventHandlers = [
StateChangedEventHandler
]
@Global()
@Module({
imports: [
@ -32,6 +38,6 @@ const cmdHandlers = [
],
controllers: [QuizController],
exports: [QuizService],
providers: [QuizService,ConfigService, ...cmdHandlers],
providers: [QuizService,ConfigService, ...cmdHandlers, ...eventHandlers],
})
export class QuizModule {}

View file

@ -1,18 +1,260 @@
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";
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, ICommand} from "@nestjs/cqrs";
import {EventbusMock} from "../mocks/eventbus.mock";
import {CommandbusMock} from "../mocks/commandbus.mock";
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');
describe('QuizService', () => {
let service: QuizService;
let cmdBus: CommandBus;
let guestService: GuestsService;
let featureFlagService: FeatureflagService;
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},
{provide: FeatureflagService, useValue: FeatureflagServiceMock}
],
}).compile();
service = module.get<QuizService>(QuizService);
service = await module.resolve<QuizService>(QuizService);
cmdBus = await module.resolve<CommandBus>(CommandBus);
guestService = await module.resolve<GuestsService>(GuestsService);
featureFlagService = await module.resolve<FeatureflagService>(FeatureflagService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('calculateScore()', () => {
let cmdBusExecSpy: jest.SpyInstance<Promise<unknown>, [command: ICommand], any>;
let getSpy;
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,
}, {
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;
// act
await service.calculateScore();
// validate
expect(getSpy).toHaveBeenCalled();
expect(cmdBusExecSpy).not.toHaveBeenCalled();
})
it('should assign points to winner', async () => {
//setup
questionDocumentMock.scoreCalculated = false;
// 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 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;
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 () => {
// setup
questionDocumentMock.scoreCalculated = false;
const saveSpy = jest.spyOn(questionDocumentMock,'save').mockResolvedValue(true);
// 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));
});
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', () => {
})
});
});

View file

@ -10,15 +10,20 @@ 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 {ConfigService} from "@nestjs/config";
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";
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 {
private readonly answerNumbers = Messages.answerNumbers;
private readonly logger = new Logger(QuizService.name);
constructor(
@InjectModel(Question.name) private questionModel: Model<QuestionDocument>,
@ -27,54 +32,59 @@ export class QuizService {
private guestService: GuestsService,
private sharedService: SharedService,
private eventBus: EventBus,
private configService: ConfigService,
private commandBus: CommandBus,
) {}
private featureFlagService: FeatureflagService,
) {
}
async get(): Promise<QuestionDocument> {
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`);
await this.guestService.postQuestion(questionDto, target);
this.sharedService.sendSocketNotificationToAllClients(
'question_changed',
questionDto,
);
this.sharedService.notifyAllClients<QuestionDto>(ClientNotificationType.QuestionChanged, questionDto);
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();
if (question.answered) {
this.logger.verbose(`Question already answered`);
return false;
}
question.answered = true;
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();
if (question.valid === filtered) {
question.answered = true;
// 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`);
return;
}
const isAnswerValid = shortValidAnswer === answer;
if(question.userAnswers.find(answer => answer.user === id)) {
this.logger.verbose("question->user answer is already containing record");
return;
}
question.userAnswers.push({
user: id,
valid: isAnswerValid,
time: new Date()
})
await question.save();
this.logger.verbose("question saved with user details")
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);
@ -107,16 +117,118 @@ export class QuizService {
async proceedWithGame() {
this.logger.verbose(`[proceedWithGame] Executing proceed with game`);
await this.calculateScore();
await this.commandBus.execute(new ProceedGameQueueCommand());
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 <= 1;
}
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;
}
this.logger.verbose(`[calculateScore] enter `);
const playerAnswers = question.userAnswers.map((answer) => {
return {
user: answer.user,
valid: answer.valid,
time: answer.time.getTime()
}
});
const sortedAnswers = playerAnswers.sort((a, b) => a.time - b.time);
const winner = sortedAnswers.find((answer) => answer.valid);
let targetUser = 0;
if(winner) {
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,
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, 5));
this.logger.debug(`Giving 1 point to first`);
await this.commandBus.execute(new IncreasePlayerScoreCommand(winner.user,1));
targetUser = winner.user;
}
const invalidAnswers = sortedAnswers.filter((answer) => !answer.valid)
if(invalidAnswers.length > 0) {
//const lastInvalidAnswer = invalidAnswers[invalidAnswers.length - 1];
const lastInvalidAnswer = invalidAnswers.sort((a,b) => a.time - b.time)[0];
if(!lastInvalidAnswer) {
return;
}
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;
await question.save();
}
public async calculateEndgamePoints(): Promise<QuizEndGameResults> {
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, maxPenaltiesReceived] = await Promise.all([maxRewardsPromise, maxInvalidAnswersPromise, maxPenaltiesPromise]);
const result = {
maxInvalidAnswers: {
id: maxInvalidAnswers[0].telegramId,
count: maxInvalidAnswers[0].invalidAnswers,
name: maxInvalidAnswers[0].name,
},
maxRewards: {
id: maxRewards[0].telegramId,
count: maxRewards[0].rewardsReceived,
name: maxRewards[0].name,
},
maxPenalties: {
id: maxPenaltiesReceived[0].telegramId,
count: maxPenaltiesReceived[0].penaltiesReceived,
name: maxPenaltiesReceived[0].name,
}
}
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;
}
private async getNextQuestion() {
let question = await this.questionStorageModel
.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 +243,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 +257,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 +280,7 @@ export class QuizService {
return questions.length;
}
async getRemainQuestionWithouValidAnswer(): Promise<number> {
async getRemainQuestionWithoutValidAnswer(): Promise<number> {
const questions = await this.questionStorageModel.find({
isAnsweredCorrectly: false,
});
@ -209,4 +323,34 @@ export class QuizService {
await newQuestion.save();
}
}
async getQuestionResults() {
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);
}
async getEndgameResults() {
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;
}
}

11
src/quiz/quiz.types.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
export interface QuizEndGameResultsDetails {
id: number;
count: number;
name: string;
}
export interface QuizEndGameResults {
maxInvalidAnswers: QuizEndGameResultsDetails;
maxRewards: QuizEndGameResultsDetails;
maxPenalties: QuizEndGameResultsDetails;
}

View file

@ -1,18 +1,66 @@
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";
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', () => {
let service: SchedulerService;
let giftService: GiftsService;
let sharedService: SharedService;
let stateService: StateService;
let featureFlagService: FeatureflagService;
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 },
{ provide: FeatureflagService, useValue: FeatureflagServiceMock },
{ provide: CommandBus, useValue: CommandbusMock },
{ provide: GuestsService, useValue: GuestsServiceMock },
],
}).compile();
service = module.get<SchedulerService>(SchedulerService);
giftService = module.get<GiftsService>(GiftsService);
sharedService = module.get<SharedService>(SharedService);
stateService = module.get<StateService>(StateService);
featureFlagService = module.get<FeatureflagService>(FeatureflagService);
});
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,'notifyAllClients').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,'notifyAllClients').mockImplementation()
await service.gameStatus();
expect(notificationFn).not.toHaveBeenCalled();
});
});

View file

@ -1,14 +1,16 @@
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 {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 {GameStateConsts} from "../Consts/game-state.consts";
import {IStateInfo} from "../Consts/types";
import {ClientNotificationType} from "../socket/socket.gateway";
@Injectable()
export class SchedulerService {
private readonly logger = new Logger(SchedulerService.name);
@ -17,11 +19,11 @@ export class SchedulerService {
constructor(
private stateService: StateService,
private cmdBus: CommandBus,
private queryBus: QueryBus,
private giftsService: GiftsService,
private quizService: QuizService,
private sharedService: SharedService,
private featureFlagService: FeatureflagService,
private commandBus: CommandBus,
) {}
@Cron('* * * * *')
@ -29,6 +31,21 @@ 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();
const state = await this.stateService.setState(GameStateConsts.Main, GameStateConsts.EndgamePoints);
this.sharedService.notifyAllClients<IStateInfo>(ClientNotificationType.StateChanged, state);
} else {
const state = await this.stateService.setState('main', 'finish');
this.sharedService.notifyAllClients<IStateInfo>(ClientNotificationType.StateChanged, 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}`);
@ -37,12 +54,7 @@ export class SchedulerService {
async gameStatus() {
const giftsLeft = await this.giftsService.getRemainingPrizeCount();
if (giftsLeft === 0) {
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(

View file

@ -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;

View file

@ -7,6 +7,9 @@ export enum GameQueueTypes {
penalty = 'penalty',
playExtraCard = 'play_extra_card',
screpaAnounce = 'screpa',
showresults = 'show_results',
extra_points = 'extra_points',
versus = 'versus',
}
export type GameQueueDocument = GameQueue & Document;

View file

@ -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<string, string>;
}

View file

@ -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,15 @@ 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[];
@Prop({ default: false })
scoreCalculated: boolean;
@Prop({ default: false})
countdownFinished: boolean;
}
export const QuestionSchema = SchemaFactory.createForClass(Question);

View file

@ -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);

View file

@ -1,15 +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',
}

View file

@ -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() {

View file

@ -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>(SharedService);

View file

@ -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';
@ -9,18 +9,24 @@ export class SharedService {
private logger = new Logger(SharedService.name);
constructor(
private socketGateway: SocketGateway,
private eventBus: EventBus,
@InjectModel(Config.name)
private configModel: Model<ConfigDocument>,
) {
}
async getConfig(key: string) {
return this.configModel
const res = await this.configModel
.findOne({
key,
})
.exec();
if(!res) {
return null;
}
return {
key: res.key,
value: res.value,
}
}
async setConfig(key: string, value: string) {
@ -35,16 +41,33 @@ 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) {
/**
* 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.
*/
notifyAllClients<T>(event: ClientNotificationType, payload: T): void {
this.logger.verbose(`Sending notification to client: ${event}, ${JSON.stringify(payload)}`);
this.socketGateway.notifyAllClients(event, payload);
}
}

Some files were not shown because too many files have changed in this diff Show more