Compare commits

..

15 commits
TGD-54 ... main

21 changed files with 1825 additions and 42 deletions

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

1314
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": "Запустить самолетик так, чтобы он попал в цель"
}
]

View file

@ -2,4 +2,5 @@ export class FeatureFlagsConsts {
static EnableEndgamePoints = 'EnableEndgamePoints'; static EnableEndgamePoints = 'EnableEndgamePoints';
static DontMarkQuestionsAsCompleted = 'DontMarkQuestionsAsCompleted'; static DontMarkQuestionsAsCompleted = 'DontMarkQuestionsAsCompleted';
static DisableVoice = 'DisableVoice'; static DisableVoice = 'DisableVoice';
static StartVersusIfPlayersAnsweredInSameTime = 'StartVersusIfPlayersAnsweredInSameTime';
} }

View file

@ -27,7 +27,7 @@ export class GameProceedGameQueueCommandHandler
return this.cmdBus.execute(new NextQuestionCommand()); return this.cmdBus.execute(new NextQuestionCommand());
} }
this.sharedService.notifyAllClients<IGameQueueSocketEvent>(ClientNotificationType.GameQueueItem, { this.sharedService.notifyAllClients<IGameQueueSocketEvent>(ClientNotificationType.GameQueueItem, {
_id: item.id, _id: item._id,
completed: item.completed, completed: item.completed,
target: item.target, target: item.target,
type: item.type, type: item.type,

View file

@ -250,7 +250,7 @@ export class BanPlayer extends GameCard {
async handle() { async handle() {
await this.commandBus.execute( await this.commandBus.execute(
new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, 2,false) new SelectTargetPlayerCommand(this.telegramId, DebuffsConsts.bannedFor, getRandomInt(2,3), false)
) )
await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null)); await this.queryBus.execute(new FilterGuestsWithPropertyQuery(null,null,null));
this.eventBus.subscribe((data) =>{ this.eventBus.subscribe((data) =>{
@ -262,7 +262,7 @@ export class BanPlayer extends GameCard {
export const gameCards: typeof GameCard[] = [ export const gameCards: typeof GameCard[] = [
DoubleTreasureCard, DoubleTreasureCard,
StolePrizeCard, StolePrizeCard,
ShitCard, // ShitCard,
LuckyCard, LuckyCard,
AvoidPenaltyCard, AvoidPenaltyCard,
BanPlayer, BanPlayer,

View file

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

View file

@ -45,4 +45,10 @@ export class GameController {
this.logger.warn(`[clearQueue] enter`); this.logger.warn(`[clearQueue] enter`);
await this.gameService.clearGameQueue(); await this.gameService.clearGameQueue();
} }
@Post('simulate-valid-answer')
async simulateValidAnswer() {
this.logger.verbose(`[simulateValidAnswer] enter`);
return await this.gameService.simulateValidAnswer();
}
} }

View file

@ -10,6 +10,8 @@ import {ConfigService} from "@nestjs/config";
import {gameCards} from "./entities/cards.entities"; import {gameCards} from "./entities/cards.entities";
import {IEmptyNotification} from "../Consts/types"; import {IEmptyNotification} from "../Consts/types";
import {ClientNotificationType} from "../socket/socket.gateway"; import {ClientNotificationType} from "../socket/socket.gateway";
import {GetGuestQuery} from "../guests/queries/getguest.query";
import {ValidAnswerReceivedEvent} from "./events/valid-answer.recieved";
@Injectable() @Injectable()
export class GameService implements OnApplicationBootstrap{ export class GameService implements OnApplicationBootstrap{
@ -50,7 +52,26 @@ export class GameService implements OnApplicationBootstrap{
} }
async getGameQueueItem() { 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) { async markQueueAsCompleted(id: string| null) {
@ -110,4 +131,8 @@ export class GameService implements OnApplicationBootstrap{
await this.gameQueueModel.deleteMany({}).exec(); await this.gameQueueModel.deleteMany({}).exec();
return { result: true }; return { result: true };
} }
async simulateValidAnswer() {
this.eventBus.publish(new ValidAnswerReceivedEvent(11178819, 'test', ''));
}
} }

View file

@ -13,6 +13,8 @@ import {GuestPropertyNamesConsts} from "../../Consts/guest-property-names.consts
import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command"; import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.command";
import {IVersusBeginSocketEvent, IVersusEndSocketEvent} from "../../Consts/types"; import {IVersusBeginSocketEvent, IVersusEndSocketEvent} from "../../Consts/types";
import {ClientNotificationType} from "../../socket/socket.gateway"; import {ClientNotificationType} from "../../socket/socket.gateway";
import {CreateNewQueueItemCommand} from "../commands/create-new-queue-item.command";
import {GameQueueTypes} from "../../schemas/game-queue.schema";
@Injectable() @Injectable()
export class VersusService { export class VersusService {
@ -42,6 +44,7 @@ export class VersusService {
async beginVersus(player1: number, player2: number) { async beginVersus(player1: number, player2: number) {
const [p1data,p2data] = await Promise.all([this.guestService.findById(player1), this.guestService.findById(player2)]); 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({ await this.sharedService.setConfig(VersusService.configKeyCurrentAction, JSON.stringify({
action:'versus', action:'versus',
data: { data: {

View file

@ -76,6 +76,9 @@ export class GuestsService {
async findById(id: number) { async findById(id: number) {
const result = await this.guestModel.findOne({ telegramId: id }).exec(); const result = await this.guestModel.findOne({ telegramId: id }).exec();
if(!result) {
return null;
}
delete result.photo; delete result.photo;
return result; return result;
} }

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

@ -26,6 +26,11 @@ export class QuizController {
return this.quizService.proceedWithGame(); return this.quizService.proceedWithGame();
} }
@Post('timeout')
async Timeout() {
return await this.quizService.questionTimeout();
}
@Post('questions') @Post('questions')
async postQuestion(@Body() questionDto: QuestionDto[]) { async postQuestion(@Body() questionDto: QuestionDto[]) {
return await this.quizService.populateQuestions(questionDto); return await this.quizService.populateQuestions(questionDto);

View file

@ -12,12 +12,17 @@ import { MarkQuestionsAsUnansweredCommandHandler } from './command-handlers/mark
import { PenaltyModule } from '../penalty/penalty.module'; import { PenaltyModule } from '../penalty/penalty.module';
import {ConfigModule, ConfigService} from "@nestjs/config"; import {ConfigModule, ConfigService} from "@nestjs/config";
import {Config, ConfigSchema} from "../schemas/config.schema"; import {Config, ConfigSchema} from "../schemas/config.schema";
import {StateChangedEventHandler} from "./event-handlers/state-changed-event.handler";
const cmdHandlers = [ const cmdHandlers = [
GameNextQuestionCommandHandler, GameNextQuestionCommandHandler,
MarkQuestionsAsUnansweredCommandHandler, MarkQuestionsAsUnansweredCommandHandler,
]; ];
const eventHandlers = [
StateChangedEventHandler
]
@Global() @Global()
@Module({ @Module({
imports: [ imports: [
@ -33,6 +38,6 @@ const cmdHandlers = [
], ],
controllers: [QuizController], controllers: [QuizController],
exports: [QuizService], exports: [QuizService],
providers: [QuizService,ConfigService, ...cmdHandlers], providers: [QuizService,ConfigService, ...cmdHandlers, ...eventHandlers],
}) })
export class QuizModule {} export class QuizModule {}

View file

@ -8,16 +8,19 @@ import {GuestsService} from "../guests/guests.service";
import {GuestsServiceMock} from "../mocks/guests-service.mock"; import {GuestsServiceMock} from "../mocks/guests-service.mock";
import {SharedService} from "../shared/shared.service"; import {SharedService} from "../shared/shared.service";
import {SharedServiceMock} from "../mocks/shared-service.mock"; import {SharedServiceMock} from "../mocks/shared-service.mock";
import {CommandBus, EventBus} from "@nestjs/cqrs"; import {CommandBus, EventBus, ICommand} from "@nestjs/cqrs";
import {EventbusMock} from "../mocks/eventbus.mock"; import {EventbusMock} from "../mocks/eventbus.mock";
import {CommandbusMock} from "../mocks/commandbus.mock"; import {CommandbusMock} from "../mocks/commandbus.mock";
import {FeatureflagService} from "../featureflag/featureflag.service"; import {FeatureflagService, IFeatureFlagStatus} from "../featureflag/featureflag.service";
import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock";
import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command"; import {IncreasePlayerWinningRateCommand} from "../game/commands/increase-player-winning-rate.command";
import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command"; import {IncreasePlayerScoreCommand} from "../guests/command/increase-player-score.command";
import {getRandomInt} from "../helpers/rand-number"; import {getRandomInt} from "../helpers/rand-number";
import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command"; import {CreateNewQueueItemCommand} from "../game/commands/create-new-queue-item.command";
import {GameQueueTypes} from "../schemas/game-queue.schema"; 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'); jest.mock('../../src/helpers/rand-number');
@ -25,6 +28,7 @@ describe('QuizService', () => {
let service: QuizService; let service: QuizService;
let cmdBus: CommandBus; let cmdBus: CommandBus;
let guestService: GuestsService; let guestService: GuestsService;
let featureFlagService: FeatureflagService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
@ -43,6 +47,7 @@ describe('QuizService', () => {
service = await module.resolve<QuizService>(QuizService); service = await module.resolve<QuizService>(QuizService);
cmdBus = await module.resolve<CommandBus>(CommandBus); cmdBus = await module.resolve<CommandBus>(CommandBus);
guestService = await module.resolve<GuestsService>(GuestsService); guestService = await module.resolve<GuestsService>(GuestsService);
featureFlagService = await module.resolve<FeatureflagService>(FeatureflagService);
}); });
it('should be defined', () => { it('should be defined', () => {
@ -50,6 +55,8 @@ describe('QuizService', () => {
}); });
describe('calculateScore()', () => { describe('calculateScore()', () => {
let cmdBusExecSpy: jest.SpyInstance<Promise<unknown>, [command: ICommand], any>;
let getSpy;
const questionDocumentMock = { const questionDocumentMock = {
text: 'test question', text: 'test question',
answered: false, answered: false,
@ -70,16 +77,22 @@ describe('QuizService', () => {
user: 3, user: 3,
time: new Date(), time: new Date(),
valid: true, valid: true,
}, {
user: 4,
time: new Date(),
valid: false,
}], }],
scoreCalculated: false, scoreCalculated: false,
save: jest.fn(), 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 () => { it('should not calculate score if it is already calculated', async () => {
// setup // setup
questionDocumentMock.scoreCalculated = true; questionDocumentMock.scoreCalculated = true;
const getSpy = jest.spyOn(service,'get').mockResolvedValue(questionDocumentMock as any);
const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute').mockResolvedValue(null);
// act // act
await service.calculateScore(); await service.calculateScore();
@ -92,8 +105,6 @@ describe('QuizService', () => {
it('should assign points to winner', async () => { it('should assign points to winner', async () => {
//setup //setup
questionDocumentMock.scoreCalculated = false; questionDocumentMock.scoreCalculated = false;
const getSpy = jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any);
const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute');
// act // act
await service.calculateScore(); await service.calculateScore();
@ -110,8 +121,6 @@ describe('QuizService', () => {
// setup // setup
(getRandomInt as jest.Mock).mockReturnValue(65); (getRandomInt as jest.Mock).mockReturnValue(65);
questionDocumentMock.scoreCalculated = false; questionDocumentMock.scoreCalculated = false;
const getSpy = jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any);
const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute');
const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2); const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2);
//act //act
@ -127,8 +136,6 @@ describe('QuizService', () => {
jest.clearAllMocks(); jest.clearAllMocks();
(getRandomInt as jest.Mock).mockReturnValue(10); (getRandomInt as jest.Mock).mockReturnValue(10);
questionDocumentMock.scoreCalculated = false; questionDocumentMock.scoreCalculated = false;
jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any);
const cmdBusExecSpy = jest.spyOn(cmdBus, 'execute');
const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2); const whoShouldGetPenalty = questionDocumentMock.userAnswers.find(x => x.user == 2);
//act //act
@ -141,16 +148,15 @@ describe('QuizService', () => {
}) })
it('should set score calculated after calculation', async () => { it('should set score calculated after calculation', async () => {
// setup
questionDocumentMock.scoreCalculated = false; questionDocumentMock.scoreCalculated = false;
const saveSpy = jest.spyOn(questionDocumentMock,'save').mockResolvedValue(true); const saveSpy = jest.spyOn(questionDocumentMock,'save').mockResolvedValue(true);
jest.spyOn(service, 'get').mockResolvedValue(questionDocumentMock as any);
// act // act
await service.calculateScore(); await service.calculateScore();
//validate //validate
expect(saveSpy).toHaveBeenCalled(); expect(saveSpy).toHaveBeenCalled();
}) });
it('should add show results in queue', async () => { it('should add show results in queue', async () => {
// setup // setup
@ -165,6 +171,90 @@ describe('QuizService', () => {
// validate // validate
expect(cmdBusExecSpy).toHaveBeenCalledWith(new CreateNewQueueItemCommand(expect.anything(), GameQueueTypes.showresults)); 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(),
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(),
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() - 5.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

@ -20,12 +20,11 @@ import {FeatureflagService} from "../featureflag/featureflag.service";
import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts";
import {QuizEndGameResults} from "./quiz.types"; import {QuizEndGameResults} from "./quiz.types";
import {ClientNotificationType} from "../socket/socket.gateway"; import {ClientNotificationType} from "../socket/socket.gateway";
import {BeginVersusCommand} from "../game/commands/begin-versus.command";
@Injectable({ scope: Scope.TRANSIENT }) @Injectable({ scope: Scope.TRANSIENT })
export class QuizService { export class QuizService {
private readonly answerNumbers = Messages.answerNumbers;
private readonly logger = new Logger(QuizService.name); private readonly logger = new Logger(QuizService.name);
private AcceptAnswersFromAllMembers: boolean = true; // TODO: move this to configurable state
constructor( constructor(
@InjectModel(Question.name) private questionModel: Model<QuestionDocument>, @InjectModel(Question.name) private questionModel: Model<QuestionDocument>,
@InjectModel(QuestionStorage.name) @InjectModel(QuestionStorage.name)
@ -61,6 +60,9 @@ export class QuizService {
); );
// check that answer exist // check that answer exist
const shortAnswers = question.answers.map((answer) => answer.substring(0,50)); const shortAnswers = question.answers.map((answer) => answer.substring(0,50));
if(question.countdownFinished) {
return;
}
const shortValidAnswer = question.valid.substring(0,50); const shortValidAnswer = question.valid.substring(0,50);
if(shortAnswers.indexOf(answer) === -1) { if(shortAnswers.indexOf(answer) === -1) {
this.logger.warn(`[validateAnswer] this question is not on game now`); this.logger.warn(`[validateAnswer] this question is not on game now`);
@ -120,6 +122,14 @@ export class QuizService {
return Promise.resolve(true); 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 <= 0.5;
}
async calculateScore() { async calculateScore() {
const question = await this.get(); const question = await this.get();
if(question.scoreCalculated) { if(question.scoreCalculated) {
@ -141,14 +151,24 @@ export class QuizService {
const winner = sortedAnswers.find((answer) => answer.valid); const winner = sortedAnswers.find((answer) => answer.valid);
let targetUser = 0; let targetUser = 0;
if(winner) { if(winner) {
const totalWinningScore = 80; const totalWinningScore = getRandomInt(40,60);
sortedAnswers.filter(x => x.valid).forEach((answer) => { sortedAnswers.filter(x => x.valid).forEach((answer) => {
this.logger.debug(`Giving 1 point to all who answered right`); this.logger.debug(`Giving 1 point to all who answered right`);
this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user, this.commandBus.execute(new IncreasePlayerWinningRateCommand(answer.user,
totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length)); totalWinningScore / sortedAnswers.filter((answer) => answer.valid).length));
this.commandBus.execute(new IncreasePlayerScoreCommand(answer.user,1)); this.commandBus.execute(new IncreasePlayerScoreCommand(answer.user,1));
}); });
await this.commandBus.execute(new IncreasePlayerWinningRateCommand(sortedAnswers[0].user, 15)); 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, getRandomInt(4,9)));
this.logger.debug(`Giving 1 point to first`); this.logger.debug(`Giving 1 point to first`);
await this.commandBus.execute(new IncreasePlayerScoreCommand(winner.user,1)); await this.commandBus.execute(new IncreasePlayerScoreCommand(winner.user,1));
targetUser = winner.user; targetUser = winner.user;
@ -197,6 +217,9 @@ export class QuizService {
} }
} }
await this.sharedService.setConfig('endgame-points', JSON.stringify(result)); 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; return result;
} }
@ -323,4 +346,11 @@ export class QuizService {
const res = await this.sharedService.getConfig('endgame-points'); const res = await this.sharedService.getConfig('endgame-points');
return JSON.parse(res.value); return JSON.parse(res.value);
} }
async questionTimeout() {
const question = await this.get();
question.countdownFinished = true;
await question.save();
return question;
}
} }

View file

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

View file

@ -31,5 +31,7 @@ export class Question {
userAnswers: QuestionAnswer[]; userAnswers: QuestionAnswer[];
@Prop({ default: false }) @Prop({ default: false })
scoreCalculated: boolean; scoreCalculated: boolean;
@Prop({ default: false})
countdownFinished: boolean;
} }
export const QuestionSchema = SchemaFactory.createForClass(Question); export const QuestionSchema = SchemaFactory.createForClass(Question);

View file

@ -38,16 +38,16 @@ export class StateController {
if (setStateDto.value === 'quiz') { if (setStateDto.value === 'quiz') {
this.eventBus.publish(new GameStartedEvent()); this.eventBus.publish(new GameStartedEvent());
} else if(setStateDto.value === 'onboarding') { } else if(setStateDto.value === 'onboarding') {
this.telegramService.send<MqtMessageModel,any>( // this.telegramService.send<MqtMessageModel,any>(
{ cmd: CommandsConsts.SetCommands }, // { cmd: CommandsConsts.SetCommands },
[ // [
{ command: 'start', description: 'главное меню'}, // { command: 'start', description: 'главное меню'},
{ command: 'cards', description: 'сыграть карту'}, // { command: 'cards', description: 'сыграть карту'},
{ command: 'question', description: 'вернутся к вопросу'} // { command: 'question', description: 'вернутся к вопросу'}
] // ]
).subscribe(() => { // ).subscribe(() => {
this.logger.verbose('Bot commands updated'); // this.logger.verbose('Bot commands updated');
}); // });
} else { } else {
this.logger.verbose('reset commands'); this.logger.verbose('reset commands');
this.telegramService.emit({ cmd: CommandsConsts.ResetCommands }, {}); this.telegramService.emit({ cmd: CommandsConsts.ResetCommands }, {});

View file

@ -5,6 +5,7 @@ import { Model } from 'mongoose';
import { EventBus } from '@nestjs/cqrs'; import { EventBus } from '@nestjs/cqrs';
import { PrepareGameEvent } from '../game/events/prepare-game.event'; import { PrepareGameEvent } from '../game/events/prepare-game.event';
import {IStateInfo} from "../Consts/types"; import {IStateInfo} from "../Consts/types";
import {StateChangedEvent} from "../game/events/state-changed.event";
interface StateDTO { interface StateDTO {
name: string; name: string;
@ -35,6 +36,8 @@ export class StateService {
if (newValue === 'onboarding') { if (newValue === 'onboarding') {
this.eventBus.publish(new PrepareGameEvent()); this.eventBus.publish(new PrepareGameEvent());
} }
this.eventBus.publish(new StateChangedEvent(newValue));
const stateEntity = await this.getState(name); const stateEntity = await this.getState(name);
stateEntity.value = newValue; stateEntity.value = newValue;
await stateEntity.save(); await stateEntity.save();

View file

@ -1,7 +1,5 @@
export const validPrefixDict = [ export const validPrefixDict = [
'Да, %answer%, был правильным ответом, молодец %user%', 'Все так, %user%',
'Ура, %answer%, это верно. Забирай очко %user%',
'Все так, %user%, и правильный ответ это действительно %answer%',
'Выиграл балл - к призу ближе стал', 'Выиграл балл - к призу ближе стал',
'Ответил верно - за это можно и выпить', 'Ответил верно - за это можно и выпить',
'Уф, какой ты умненький, %user%', 'Уф, какой ты умненький, %user%',
@ -33,4 +31,14 @@ export const validPrefixDict = [
'%user%, откуда ты все знаешь? Ты случаем не эшник, а?', '%user%, откуда ты все знаешь? Ты случаем не эшник, а?',
'молодец, %user%, можешь погладить пёселя', 'молодец, %user%, можешь погладить пёселя',
'%user%, у тебя такой склад ума, что хоть сторожа нанимай!', '%user%, у тебя такой склад ума, что хоть сторожа нанимай!',
'Отличный ответ, %user%!',
'Ты попал в самую точку, %user%!',
'Вот это да, %user%, ты знаешь толк!',
'Так держать, %user%!',
'Твои знания на высоте, %user%!',
'Умница, %user%! Всё верно.',
'Блестяще, %user%! Продолжай в том же духе.',
'Ты меня впечатлил, %user%!',
'Не перестаёшь удивлять, %user%!',
'Великолепно, %user%! Твой ответ правильный!',
]; ];