diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..72c4429 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/data/gifts.json b/data/gifts.json new file mode 100644 index 0000000..b05f045 --- /dev/null +++ b/data/gifts.json @@ -0,0 +1,156 @@ +[{ +"prizeID": 1, +"name": "Тесто для лепки невкусное", +"isGifted": false +}, +{ +"prizeID": 2, +"name": "Палочки с ароматом лучших публичных домов Бангкока", +"isGifted": false +}, +{ +"prizeID": 3, +"name": "2 метра хюгге", +"isGifted": false +}, +{ +"prizeID": 4, +"name": "Палку светящуюся бесполезную", +"isGifted": false +}, +{ +"prizeID": 5, +"name": "Тёрку для лилипутов", +"isGifted": false +}, +{ +"prizeID": 6, +"name": "Мёртвую белочку", +"isGifted": false +}, +{ +"prizeID": 7, +"name": "Лучшего друга Спанч боба засушенного", +"isGifted": false +}, +{ +"prizeID": 8, +"name": "Подарок для любителей помесить глину", +"isGifted": false +}, +{ +"prizeID": 9, +"name": "Палку чесательную полезную", +"isGifted": false +}, +{ +"prizeID": 10, +"name": "Красного петуха - своё тотемное животное", +"isGifted": false +}, +{ +"prizeID": 11, +"name": "Набор свечей романтишный", +"isGifted": false +}, +{ +"prizeID": 12, +"name": "Хранилище для денег патриотическое", +"isGifted": false +}, +{ +"prizeID": 13, +"name": "Мерч от Каца", +"isGifted": false +}, +{ +"prizeID": 14, +"name": "Чупа-чупс со вкусом патриотизма", +"isGifted": false +}, +{ +"prizeID": 15, +"name": "Тренажеры для легких разноцветные", +"isGifted": false +}, +{ +"prizeID": 16, +"name": "Паззл предсказуемый", +"isGifted": false +}, +{ +"prizeID": 17, +"name": "Жопный блокнот", +"isGifted": false +}, +{ +"prizeID": 18, +"name": "Носки от батьки праздничные", +"isGifted": false +}, +{ +"prizeID": 19, +"name": "Носки женские миленькие", +"isGifted": false +}, +{ +"prizeID": 20, +"name": "Набор художника-нумеролога", +"isGifted": false +}, +{ +"prizeID": 21, +"name": "Карандаш вечный как Путин", +"isGifted": false +}, +{ +"prizeID": 22, +"name": "Массажёр для жопы", +"isGifted": false +}, +{ +"prizeID": 23, +"name": "Сладкий подарок рот в рот", +"isGifted": false +}, +{ +"prizeID": 24, +"name": "Мотоцикл (ненастоящий)", +"isGifted": false +}, +{ +"prizeID": 25, +"name": "Вышивку для эскортниц (алмазную)", +"isGifted": false +}, +{ +"prizeID": 26, +"name": "Звенящие бубенцы", +"isGifted": false +}, +{ +"prizeID": 27, +"name": "Спонж для умывания твоих кислых щей", +"isGifted": false +}, +{ +"prizeID": 28, +"name": "Мочалку с портретом дракона", +"isGifted": false +}, +{ +"prizeID": 29, +"name": "Тетрадь для чётких квадроберов", +"isGifted": false +}, +{ +"prizeID": 30, +"name": "Костюм для руки незнакомки эротишный", +"isGifted": false +}, +{ +"prizeID": 31, +"name": "Плакат с кумиром детства нарядный", +"isGifted": false +} +] \ No newline at end of file diff --git a/data/questions.json b/data/questions.json new file mode 100644 index 0000000..d097f8b --- /dev/null +++ b/data/questions.json @@ -0,0 +1,1034 @@ +[ + { + "question": "Как называется самый большой океан на Земле?", + "a1": "Атлантический океан", + "a2": "Тихий океан", + "a3": "Индийский океан", + "a4": "Северный Ледовитый океан", + "valid": "Тихий океан" + }, + { + "question": "Кто написал роман 'Преступление и наказание'?", + "a1": "Лев Толстой", + "a2": "Федор Достоевский", + "a3": "Антон Чехов", + "a4": "Николай Гоголь", + "valid": "Федор Достоевский" + }, + { + "question": "Кто является режиссером фильма 'Гражданин Кейн', считающегося одним из величайших фильмов всех времен?", + "a1": "Орсон Уэллс", + "a2": "Альфред Хичкок", + "a3": "Стэнли Кубрик", + "a4": "Фрэнк Капра", + "valid": "Орсон Уэллс" + }, + { + "question": "Кто сыграл главную роль в фильме 'Форрест Гамп'?", + "a1": "Том Хэнкс", + "a2": "Брэд Питт", + "a3": "Джон Траволта", + "a4": "Леонардо ДиКаприо", + "valid": "Том Хэнкс" + }, + { + "question": "Какой режиссер снял трилогию 'Властелин колец'?", + "a1": "Питер Джексон", + "a2": "Кристофер Нолан", + "a3": "Джеймс Кэмерон", + "a4": "Гильермо дель Торо", + "valid": "Питер Джексон" + }, + { + "question": "Какой режиссер снял фильмы 'Малхолланд Драйв' и 'Голова-ластик'?", + "a1": "Дэвид Линч", + "a2": "Дэвид Финчер", + "a3": "Стэнли Кубрик", + "a4": "Тим Бёртон", + "valid": "Дэвид Линч" + }, + { + "question": "Какой актер сыграл главную роль в фильме 'Матрица'?", + "a1": "Киану Ривз", + "a2": "Уилл Смит", + "a3": "Том Круз", + "a4": "Джонни Депп", + "valid": "Киану Ривз" + }, + { + "question": "Какой фильм японского режиссера Хаяо Миядзаки получил премию 'Оскар' за лучший анимационный фильм?", + "a1": "Унесенные призраками", + "a2": "Ходячий замок", + "a3": "Принцесса Мононоке", + "a4": "Мой сосед Тоторо", + "valid": "Унесенные призраками" + }, + { + "question": "Какой фильм Кристофера Нолана рассказывает о попытке кражи идей через сновидения?", + "a1": "Начало", + "a2": "Престиж", + "a3": "Интерстеллар", + "a4": "Мементо", + "valid": "Начало" + }, + { + "question": "Какой газ составляет около 78% атмосферы Земли?", + "a1": "Кислород", + "a2": "Углекислый газ", + "a3": "Азот", + "a4": "Аргон", + "valid": "Азот" + }, + { + "question": "Как зовут обезьяну Росса в сериале 'Друзья'?", + "a1": "Марсель", + "a2": "Морис", + "a3": "Макс", + "a4": "Мэри", + "valid": "Марсель" + }, + { + "question": "Кто является национальным поэтом Грузии, автором поэмы 'Витязь в тигровой шкуре'?", + "a1": "Шота Руставели", + "a2": "Илья Чавчавадзе", + "a3": "Галактион Табидзе", + "a4": "Акакий Церетели", + "valid": "Шота Руставели" + }, + { + "question": "Какой город является столицей Австралии?", + "a1": "Сидней", + "a2": "Мельбурн", + "a3": "Канберра", + "a4": "Брисбен", + "valid": "Канберра" + }, + { + "question": "Какой элемент периодической таблицы имеет символ 'Fe'?", + "a1": "Калий", + "a2": "Фосфор", + "a3": "Железо", + "a4": "Фтор", + "valid": "Железо" + }, + { + "question": "В каком году человек впервые ступил на Луну?", + "a1": "1965", + "a2": "1969", + "a3": "1972", + "a4": "1959", + "valid": "1969" + }, + { + "question": "Кто написал симфонию №9 'Из Нового Света'?", + "a1": "Антонин Дворжак", + "a2": "Людвиг ван Бетховен", + "a3": "Иоганн Себастьян Бах", + "a4": "Вольфганг Амадей Моцарт", + "valid": "Антонин Дворжак" + }, + { + "question": "Какое среднее имя у Чендлера в сериале 'Друзья'?", + "a1": "Мэри", + "a2": "Мюриэл", + "a3": "Мэтью", + "a4": "Майкл", + "valid": "Мюриэл" + }, + { + "question": "Как называлось древнее грузинское царство, существовавшее в VI-IV веках до н.э.?", + "a1": "Колхида", + "a2": "Иверия", + "a3": "Лазика", + "a4": "Месхетия", + "valid": "Колхида" + }, + { + "question": "Какой город является столицей Бразилии?", + "a1": "Рио-де-Жанейро", + "a2": "Сан-Паулу", + "a3": "Бразилиа", + "a4": "Сальвадор", + "valid": "Бразилиа" + }, + { + "question": "Кто написал 'Маленький принц'?", + "a1": "Антуан де Сент-Экзюпери", + "a2": "Жюль Верн", + "a3": "Александр Дюма", + "a4": "Гюстав Флобер", + "valid": "Антуан де Сент-Экзюпери" + }, + { + "question": "Как называется научная единица измерения силы?", + "a1": "Джоуль", + "a2": "Ньютон", + "a3": "Паскаль", + "a4": "Ватт", + "valid": "Ньютон" + }, + { + "question": "Как зовут сестру-близнеца Фиби в сериале 'Друзья'?", + "a1": "Урсула", + "a2": "Рэйчел", + "a3": "Моника", + "a4": "Эмили", + "valid": "Урсула" + }, + { + "question": "Кто был первым президентом независимой Грузии после распада СССР?", + "a1": "Эдуард Шеварднадзе", + "a2": "Звиад Гамсахурдия", + "a3": "Михаил Саакашвили", + "a4": "Гиорги Маргвелашвили", + "valid": "Звиад Гамсахурдия" + }, + { + "question": "Какой город является столицей Новой Зеландии?", + "a1": "Окленд", + "a2": "Веллингтон", + "a3": "Крайстчерч", + "a4": "Данидин", + "valid": "Веллингтон" + }, + { + "question": "Какой самый большой орган в человеческом теле?", + "a1": "Сердце", + "a2": "Печень", + "a3": "Кожа", + "a4": "Легкие", + "valid": "Кожа" + }, + { + "question": "Кто написал музыку к балету 'Лебединое озеро'?", + "a1": "Петр Чайковский", + "a2": "Игорь Стравинский", + "a3": "Сергей Прокофьев", + "a4": "Дмитрий Шостакович", + "valid": "Петр Чайковский" + }, + { + "question": "Какое животное является символом Всемирного фонда дикой природы (WWF)?", + "a1": "Тигр", + "a2": "Панда", + "a3": "Слон", + "a4": "Кит", + "valid": "Панда" + }, + { + "question": "Чем на самом деле занимается Чендлер Бинг по профессии в сериале 'Друзья'?", + "a1": "Рекламный агент", + "a2": "Аналитик данных", + "a3": "Юрист", + "a4": "Доктор", + "valid": "Аналитик данных" + }, + { + "question": "Как называется древний пещерный город в Грузии, основанный в XII веке?", + "a1": "Уплисцихе", + "a2": "Вардзия", + "a3": "Мцхета", + "a4": "Сигнахи", + "valid": "Вардзия" + }, + { + "question": "Какой город является столицей Норвегии?", + "a1": "Стокгольм", + "a2": "Осло", + "a3": "Копенгаген", + "a4": "Хельсинки", + "valid": "Осло" + }, + { + "question": "Какой континент не имеет пустынь?", + "a1": "Европа", + "a2": "Антарктида", + "a3": "Африка", + "a4": "Южная Америка", + "valid": "Европа" + }, + { + "question": "Какой планете в Солнечной системе соответствует прозвище 'Красная планета'?", + "a1": "Марс", + "a2": "Венера", + "a3": "Юпитер", + "a4": "Меркурий", + "valid": "Марс" + }, + { + "question": "Какая столица Брунея?", + "a1": "Бандар-Сери-Бегаван", + "a2": "Куала-Лумпур", + "a3": "Джакарта", + "a4": "Бангкок", + "valid": "Бандар-Сери-Бегаван" + }, + { + "question": "Как называется столица Мьянмы (Бирмы) с 2005 года?", + "a1": "Янгон", + "a2": "Нейпьидо", + "a3": "Мандалай", + "a4": "Баган", + "valid": "Нейпьидо" + }, + { + "question": "Какая столица Палау?", + "a1": "Нгерулмуд", + "a2": "Корор", + "a3": "Маджуро", + "a4": "Фунафути", + "valid": "Нгерулмуд" + } + { + "question": "Кто из друзей работает массажисткой?", + "a1": "Моника", + "a2": "Фиби", + "a3": "Рэйчел", + "a4": "Дженис", + "valid": "Фиби" + }, + { + "question": "Как называется традиционное грузинское застолье с тамадой?", + "a1": "Хинкали", + "a2": "Супра", + "a3": "Чача", + "a4": "Киндзмараули", + "valid": "Супра" + }, + { + "question": "Какой город является столицей Швейцарии?", + "a1": "Цюрих", + "a2": "Женева", + "a3": "Берн", + "a4": "Лозанна", + "valid": "Берн" + }, + { + "question": "Кто был первым человеком в космосе?", + "a1": "Нил Армстронг", + "a2": "Юрий Гагарин", + "a3": "Алан Шепард", + "a4": "Герман Титов", + "valid": "Юрий Гагарин" + }, + { + "question": "Как называется крупнейшая коралловая система в мире?", + "a1": "Большой Барьерный риф", + "a2": "Коралловый треугольник", + "a3": "Красное море", + "a4": "Мальдивский риф", + "valid": "Большой Барьерный риф" + }, + { + "question": "Какая компания разработала первый персональный компьютер с графическим интерфейсом?", + "a1": "Microsoft", + "a2": "Apple", + "a3": "IBM", + "a4": "Hewlett-Packard", + "valid": "Apple" + }, + { + "question": "Кто изобрел систему переменного тока (AC)?", + "a1": "Томас Эдисон", + "a2": "Никола Тесла", + "a3": "Джордж Вестингауз", + "a4": "Майкл Фарадей", + "valid": "Никола Тесла" + }, + { + "question": "Кто разработал язык программирования Java?", + "a1": "Джеймс Гослинг", + "a2": "Деннис Ритчи", + "a3": "Бьёрн Страуструп", + "a4": "Гвидо ван Россум", + "valid": "Джеймс Гослинг" + }, + { + "question": "Как называется процесс деления клетки в биологии?", + "a1": "Мейоз", + "a2": "Митоз", + "a3": "Фотосинтез", + "a4": "Апоптоз", + "valid": "Митоз" + }, + { + "question": "Как называется самая продаваемая видеоигра всех времён?", + "a1": "Minecraft", + "a2": "Tetris", + "a3": "Grand Theft Auto V", + "a4": "Wii Sports", + "valid": "Minecraft" + }, + { + "question": "Кто является создателем серии игр 'Супер Марио'?", + "a1": "Сигэру Миямото", + "a2": "Хидэо Кодзима", + "a3": "Сид Мейер", + "a4": "Тодд Говард", + "valid": "Сигэру Миямото" + }, + { + "question": "Какой регион Грузии известен своими пещерными городами, такими как Вардзия?", + "a1": "Самцхе-Джавахети", + "a2": "Имерети", + "a3": "Мцхета-Мтианети", + "a4": "Кахети", + "valid": "Самцхе-Джавахети" + }, + { + "question": "В каком регионе Грузии находится гора Казбек?", + "a1": "Мцхета-Мтианети", + "a2": "Сванети", + "a3": "Кахети", + "a4": "Имерети", + "valid": "Мцхета-Мтианети" + }, + { + "question": "Какой регион Грузии граничит с Арменией и известен своими армянскими общинами?", + "a1": "Самцхе-Джавахети", + "a2": "Квемо Картли", + "a3": "Имерети", + "a4": "Гурия", + "valid": "Самцхе-Джавахети" + }, + { + "question": "Какая игра стала самой первой 3D игрой с открытым миром?", + "a1": "Grand Theft Auto III", + "a2": "The Elder Scrolls III: Morrowind", + "a3": "Super Mario 64", + "a4": "The Legend of Zelda: Ocarina of Time", + "valid": "Super Mario 64" + }, + { + "question": "Кто является разработчиком серии игр 'Metal Gear Solid'?", + "a1": "Ко Джунг Хван", + "a2": "Хидэо Кодзима", + "a3": "Кадзунори Ямаути", + "a4": "Нобуо Уэмацу", + "valid": "Хидэо Кодзима" + }, + { + "question": "Какая игра, выпущенная CD Projekt Red в 2015 году, получила множество наград 'Игра года'?", + "a1": "The Witcher 3: Wild Hunt", + "a2": "Cyberpunk 2077", + "a3": "Red Dead Redemption 2", + "a4": "Assassin's Creed Odyssey", + "valid": "The Witcher 3: Wild Hunt" + }, + { + "question": "Какой город называют 'Воротами в Азию' и он расположен на двух континентах?", + "a1": "Стамбул", + "a2": "Москва", + "a3": "Токио", + "a4": "Дубай", + "valid": "Стамбул" + }, + { + "question": "В какой стране находится древний город Петра, высеченный в скалах?", + "a1": "Египет", + "a2": "Иордания", + "a3": "Марокко", + "a4": "Турция", + "valid": "Иордания" + }, + { + "question": "В какой стране можно посетить исторический Великий Будда Камакура?", + "a1": "Китай", + "a2": "Япония", + "a3": "Таиланд", + "a4": "Южная Корея", + "valid": "Япония" + }, + { + "question": "Какой бренд сигарет использовал Джесси для хранения рицина в сериале во все тяжкие?", + "a1": "Marlboro Red", + "a2": "Camel Blue", + "a3": "Lucky Strike", + "a4": "Parliament", + "valid": "Lucky Strike" + }, + { + "question": "Как называется компания по борьбе с вредителями, которую использовали Уолт и Джесси в качестве прикрытия в сериале во все тяжкие?", + "a1": "Pest Gone", + "a2": "Vamonos Pest", + "a3": "Bug Busters", + "a4": "Insect Away", + "valid": "Vamonos Pest" + }, + { + "question": "Какой архипелаг известен своими уникальными видами животных и вдохновил Чарльза Дарвина на создание теории эволюции?", + "a1": "Гавайские острова", + "a2": "Мальдивы", + "a3": "Галапагосские острова", + "a4": "Канарские острова", + "valid": "Галапагосские острова" + }, + { + "question": "В какой стране находится знаменитый пляж Копакабана?", + "a1": "Испания", + "a2": "Мексика", + "a3": "Бразилия", + "a4": "Португалия", + "valid": "Бразилия" + }, + { + "question": "Какой британский музыкальный коллектив считается самым продаваемым в истории?", + "a1": "The Beatles", + "a2": "Queen", + "a3": "The Rolling Stones", + "a4": "Pink Floyd", + "valid": "The Beatles" + }, + { + "question": "Как называется направление электронной музыки, возникшее в Детройте в 1980-х годах?", + "a1": "Техно", + "a2": "Транс", + "a3": "Хаус", + "a4": "Дабстеп", + "valid": "Техно" + }, + { + "question": "Кто является лучшим бомбардиром в истории сборной Грузии по футболу?", + "a1": "Кахабер Каладзе", + "a2": "Шота Арвеладзе", + "a3": "Георгий Кинкладзе", + "a4": "Леван Кобиашвили", + "valid": "Шота Арвеладзе" + }, + { + "question": "Какой игрок сборной Грузии выступает за итальянский клуб 'Наполи' и известен под прозвищем 'Кварадонна'?", + "a1": "Георгий Чакветадзе", + "a2": "Хвича Кварацхелия", + "a3": "Гиорги Абурджания", + "a4": "Вако Казаишвили", + "valid": "Хвича Кварацхелия" + }, + { + "question": "Какой остров является самым большим в Средиземном море?", + "a1": "Сардиния", + "a2": "Сицилия", + "a3": "Крит", + "a4": "Кипр", + "valid": "Сицилия" + }, + { + "question": "Какой горный массив является самым длинным в мире?", + "a1": "Гималаи", + "a2": "Анды", + "a3": "Альпы", + "a4": "Скалистые горы", + "valid": "Анды" + }, + { + "question": "Какой водопад считается самым высоким в мире?", + "a1": "Ниагарский водопад", + "a2": "Водопад Виктория", + "a3": "Анхель", + "a4": "Игуасу", + "valid": "Анхель" + }, + { + "question": "Кто был первым человеком, совершившим одиночный беспосадочный перелет через Атлантический океан?", + "a1": "Амелия Эрхарт", + "a2": "Чарльз Линдберг", + "a3": "Говард Хьюз", + "a4": "Ричард Брансон", + "valid": "Чарльз Линдберг" + }, + { + "question": "Как называется прибор, используемый для измерения высоты полета самолета?", + "a1": "Спидометр", + "a2": "Альтиметр", + "a3": "Вариметр", + "a4": "Гироскоп", + "valid": "Альтиметр" + }, + { + "question": "Кто считается первым человеком, совершившим управляемый полет на самолете с двигателем?", + "a1": "Чарльз Линдберг", + "a2": "Братья Райт", + "a3": "Амелия Эрхарт", + "a4": "Луи Блерио", + "valid": "Братья Райт" + }, + { + "question": "Как называется международная организация, устанавливающая стандарты в гражданской авиации?", + "a1": "IATA", + "a2": "ICAO", + "a3": "FAA", + "a4": "EASA", + "valid": "ICAO" + }, + { + "question": "Как называется математическая константа, приблизительно равная 2,71828?", + "a1": "Число Пи", + "a2": "Число Эйлера", + "a3": "Золотое сечение", + "a4": "Число Фибоначчи", + "valid": "Число Эйлера" + }, + { + "question": "Как называется явление, при котором свет изменяет направление при переходе из одной среды в другую?", + "a1": "Дифракция", + "a2": "Рефракция", + "a3": "Интерференция", + "a4": "Поляризация", + "valid": "Рефракция" + }, + { + "question": "Какой математик считается основателем аналитической геометрии?", + "a1": "Рене Декарт", + "a2": "Исаак Ньютон", + "a3": "Блез Паскаль", + "a4": "Карл Гаусс", + "valid": "Рене Декарт" + }, + { + "question": "Кто первым доказал, что Земля вращается вокруг Солнца?", + "a1": "Николай Коперник", + "a2": "Иоганн Кеплер", + "a3": "Галилео Галилей", + "a4": "Тихо Браге", + "valid": "Николай Коперник" + }, + { + "question": "Какой элемент периодической таблицы имеет атомный номер 6?", + "a1": "Кислород", + "a2": "Азот", + "a3": "Углерод", + "a4": "Водород", + "valid": "Углерод" + }, + { + "question": "Как называется самый большой спутник Сатурна?", + "a1": "Титан", + "a2": "Европа", + "a3": "Ганимед", + "a4": "Каллисто", + "valid": "Титан" + }, + { + "question": "Как зовут младшую сестру Рэйчел Грин?", + "a1": "Эми", + "a2": "Джилл", + "a3": "Эмили", + "a4": "Джуди", + "valid": "Джилл" + }, + { + "question": "Какой город является столицей Марокко?", + "a1": "Касабланка", + "a2": "Рабат", + "a3": "Марракеш", + "a4": "Фес", + "valid": "Рабат" + }, + { + "question": "Как называется научная единица измерения электрического сопротивления?", + "a1": "Вольт", + "a2": "Ампер", + "a3": "Ом", + "a4": "Фарад", + "valid": "Ом" + }, + { + "question": "Как называется самый большой остров в Средиземном море?", + "a1": "Сардиния", + "a2": "Крит", + "a3": "Сицилия", + "a4": "Кипр", + "valid": "Сицилия" + }, + { + "question": "Кто из героев 'Друзей' был первым женатым на Монике?", + "a1": "Чендлер", + "a2": "Ричард", + "a3": "Росс", + "a4": "Джо", + "valid": "Чендлер" + }, + { + "question": "Как называется грузинская письменность, используемая для написания грузинского языка?", + "a1": "Глаголица", + "a2": "Асомтаврули", + "a3": "Мхедрули", + "a4": "Кириллица", + "valid": "Мхедрули" + }, + { + "question": "Какой город является столицей Кирибати?", + "a1": "Южная Тарава", + "a2": "Паликир", + "a3": "Хониара", + "a4": "Фунафути", + "valid": "Южная Тарава" + }, + { + "question": "Какой город является столицей Финляндии?", + "a1": "Хельсинки", + "a2": "Стокгольм", + "a3": "Осло", + "a4": "Копенгаген", + "valid": "Хельсинки" + }, + { + "question": "Как называется самый большой вид акул?", + "a1": "Белая акула", + "a2": "Тигровая акула", + "a3": "Китовая акула", + "a4": "Рифовая акула", + "valid": "Китовая акула" + }, + { + "question": "Кто написал пьесу 'Ромео и Джульетта'?", + "a1": "Уильям Шекспир", + "a2": "Джон Мильтон", + "a3": "Джордж Байрон", + "a4": "Чарльз Диккенс", + "valid": "Уильям Шекспир" + }, + { + "question": "Какой шрифт был разработан в 1957 году Максом Мидингером и Эдуардом Хоффманном и стал одним из самых популярных в мире?", + "a1": "Arial", + "a2": "Times New Roman", + "a3": "Helvetica", + "a4": "Courier New", + "valid": "Helvetica" + }, + { + "question": "Какой шрифт был создан Винсентом Коннаром в 1994 году и часто критикуется дизайнерами за его неуместное использование?", + "a1": "Comic Sans MS", + "a2": "Papyrus", + "a3": "Impact", + "a4": "Brush Script", + "valid": "Comic Sans MS" + }, + { + "question": "Как зовут главного героя мультфильма «Тачки»?", + "a1": "Молния Маккуин", + "a2": "Док Хадсон", + "a3": "Мэтр", + "a4": "Салли", + "valid": "Молния Маккуин" + }, + { + "question": "Как называется старая гоночная машина, которая становится наставником Маккуина?", + "a1": "Док Хадсон", + "a2": "Чик Хикс", + "a3": "Кинг", + "a4": "Сержант", + "valid": "Док Хадсон" + }, + { + "question": "Как называется топливо, которое продает хиппи-мобиль Филмор?", + "a1": "Органическое топливо", + "a2": "Супероктан", + "a3": "Электрозаряд", + "a4": "БиоДизель", + "valid": "Органическое топливо" + }, + { + "question": "Какой регион Грузии известен своими средневековыми башнями и уникальной культурой?", + "a1": "Кахети", + "a2": "Сванети", + "a3": "Самегрело", + "a4": "Имерети", + "valid": "Сванети" + }, + { + "question": "Какое грузинское вино стало первым защищённым наименованием по месту происхождения в Грузии?", + "a1": "Саперави", + "a2": "Киндзмараули", + "a3": "Хванчкара", + "a4": "Цинандали", + "valid": "Хванчкара" + }, + { + "question": "Какой грузинский город является вторым по величине и расположен на реке Риони?", + "a1": "Кутаиси", + "a2": "Батуми", + "a3": "Гори", + "a4": "Рустави", + "valid": "Кутаиси" + }, + { + "question": "Как называется грузинское национальное блюдо из курицы в ореховом соусе?", + "a1": "Сациви", + "a2": "Чахохбили", + "a3": "Оджахури", + "a4": "Чакапули", + "valid": "Сациви" + }, + { + "question": "В какой стране находится Мачу-Пикчу, древний город инков?", + "a1": "Боливия", + "a2": "Перу", + "a3": "Чили", + "a4": "Колумбия", + "valid": "Перу" + }, + { + "question": "Какой остров известен своими гигантскими каменными статуями моаи?", + "a1": "Остров Пасхи", + "a2": "Гавайи", + "a3": "Мадагаскар", + "a4": "Сицилия", + "valid": "Остров Пасхи" + }, + { + "question": "В какой стране находится древний храмовый комплекс Боробудур?", + "a1": "Индонезия", + "a2": "Малайзия", + "a3": "Таиланд", + "a4": "Вьетнам", + "valid": "Индонезия" + }, + { + "question": "Как называется грузинский музыкальный инструмент, похожий на гитару?", + "a1": "Пандури", + "a2": "Дудук", + "a3": "Чонгури", + "a4": "Саз", + "valid": "Пандури" + }, + { + "question": "Как называется игровая консоль от Sega, выпущенная в 1999 году и ставшая последней консолью компании?", + "a1": "Sega Saturn", + "a2": "Sega Genesis", + "a3": "Sega Dreamcast", + "a4": "Sega CD", + "valid": "Sega Dreamcast" + }, + { + "question": "Как называется грузинская народная песня, отправленная в космос на борту 'Вояджера'?", + "a1": "Чакруло", + "a2": "Сулико", + "a3": "Мравалжамиер", + "a4": "Цин цин царо", + "valid": "Чакруло" + }, + { + "question": "Как называется грузинский поэт и публицист XIX века, известный как 'отец нации'?", + "a1": "Илья Чавчавадзе", + "a2": "Акакий Церетели", + "a3": "Важа-Пшавела", + "a4": "Галактион Табидзе", + "valid": "Илья Чавчавадзе" + }, + { + "question": "Какое название носит организация, созданная Михаилом Ходорковским для поддержки гражданского общества в России?", + "a1": "Открытая Россия", + "a2": "Новая Газета", + "a3": "Голос", + "a4": "Мемориал", + "valid": "Открытая Россия" + }, + { + "question": "Кто из перечисленных политиков был мэром Екатеринбурга и известен своей независимой позицией?", + "a1": "Евгений Ройзман", + "a2": "Анатолий Локоть", + "a3": "Сергей Собянин", + "a4": "Александр Беглов", + "valid": "Евгений Ройзман" + }, + { + "question": "Какой вид бизнеса наиболее часто открывают русские эмигранты в Грузии?", + "a1": "Рестораны и кафе", + "a2": "ИТ-компании", + "a3": "Туристические агентства", + "a4": "Магазины одежды", + "valid": "ИТ-компании" + }, + { + "question": "Сколько палат в парламенте Грузии согласно Конституции?", + "a1": "Однопалатный", + "a2": "Двухпалатный", + "a3": "Трехпалатный", + "a4": "Четырехпалатный", + "valid": "Однопалатный" + }, + { + "question": "Какой максимальный срок лишения свободы предусмотрен Уголовным кодексом Грузии?", + "a1": "20 лет", + "a2": "25 лет", + "a3": "Пожизненное заключение", + "a4": "30 лет", + "valid": "Пожизненное заключение" + }, + { + "question": "Какое минимальное количество депутатов в Парламенте Грузии?", + "a1": "77", + "a2": "100", + "a3": "150", + "a4": "200", + "valid": "150" + }, + { + "question": "Как называется любимая фраза Джои в сериале 'Друзья'?", + "a1": "How you doin'?", + "a2": "What's up?", + "a3": "Hey there!", + "a4": "Long time no see!", + "valid": "How you doin'?" + }, + { + "question": "Какой город является столицей Исландии?", + "a1": "Рейкьявик", + "a2": "Хельсинки", + "a3": "Осло", + "a4": "Копенгаген", + "valid": "Рейкьявик" + }, + { + "question": "Кто изобретатель первого практичного телефона?", + "a1": "Томас Эдисон", + "a2": "Александр Белл", + "a3": "Никола Тесла", + "a4": "Гульельмо Маркони", + "valid": "Александр Белл" + }, + { + "question": "Как называется книга, написанная Никколо Макиавелли о политике?", + "a1": "Республика", + "a2": "Государь", + "a3": "Левиафан", + "a4": "Об общественном договоре", + "valid": "Государь" + }, + { + "question": "Как называется мыльная опера, в которой снимается Джои Триббиани?", + "a1": "Дни нашей жизни", + "a2": "Молодые и дерзкие", + "a3": "Всё мои дети", + "a4": "Главный госпиталь", + "valid": "Дни нашей жизни" + }, + { + "question": "Какой монарх объединил Восточную и Западную Грузию в начале XI века?", + "a1": "Баграт III", + "a2": "Давид IV", + "a3": "Георгий I", + "a4": "Тамара", + "valid": "Баграт III" + }, + { + "question": "Какой город является столицей Монголии?", + "a1": "Алма-Ата", + "a2": "Улан-Батор", + "a3": "Бишкек", + "a4": "Душанбе", + "valid": "Улан-Батор" + }, + { + "question": "Какой химический элемент является основным компонентом алмазов?", + "a1": "Углерод", + "a2": "Кремний", + "a3": "Сера", + "a4": "Бор", + "valid": "Углерод" + }, + { + "question": "Какой художник отрезал себе часть уха?", + "a1": "Пабло Пикассо", + "a2": "Клод Моне", + "a3": "Винсент Ван Гог", + "a4": "Сальвадор Дали", + "valid": "Винсент Ван Гог" + }, + { + "question": "Как называется наука о поведении и психологии животных?", + "a1": "Этология", + "a2": "Зоология", + "a3": "Биология", + "a4": "Антропология", + "valid": "Этология" + }, + { + "question": "Кто изобрел первый механический калькулятор?", + "a1": "Блез Паскаль", + "a2": "Готфрид Лейбниц", + "a3": "Чарльз Бэббидж", + "a4": "Алан Тьюринг", + "valid": "Блез Паскаль" + }, + { + "question": "Как зовут любимых питомцев Джои и Чендлера в сериале 'Друзья'?", + "a1": "Чик и Дак", + "a2": "Бёрд и Дак", + "a3": "Чикки и Дакки", + "a4": "Птичка и Утенок", + "valid": "Чик и Дак" + }, + { + "question": "Как называется газета, которую часто читает волшебный мир в Гарри Поттере?", + "a1": "Ежедневный Пророк", + "a2": "Волшебный Вестник", + "a3": "Магический Курьер", + "a4": "Вечерний Волшебник", + "valid": "Ежедневный Пророк" + }, + { + "question": "Как зовут эльфа-домовика семьи Чёрных?", + "a1": "Добби", + "a2": "Кикимер", + "a3": "Винки", + "a4": "Хокки", + "valid": "Кикимер" + }, + { + "question": "Какое полное имя Долорес Амбридж?", + "a1": "Джейн", + "a2": "Энн", + "a3": "Мэри", + "a4": "Лили", + "valid": "Джейн" + }, + { + "question": "Какой предмет преподавал Квиринус Квиррелл до того, как стал преподавателем защиты от темных искусств?", + "a1": "Трансфигурация", + "a2": "Маггловедение", + "a3": "Зельеварение", + "a4": "История магии", + "valid": "Маггловедение" + }, + { + "question": "Как зовут мать Нимфадоры Тонкс?", + "a1": "Андромеда Тонкс", + "a2": "Нарцисса Малфой", + "a3": "Беллатрисса Лестрейндж", + "a4": "Лили Поттер", + "valid": "Андромеда Тонкс" + }, + { + "question": "Кто сформулировал принцип неопределённости в квантовой механике?", + "a1": "Альберт Эйнштейн", + "a2": "Вернер Гейзенберг", + "a3": "Нильс Бор", + "a4": "Макс Планк", + "valid": "Вернер Гейзенберг" + }, + { + "question": "Какая планета Солнечной системы имеет наибольшее количество спутников?", + "a1": "Юпитер", + "a2": "Сатурн", + "a3": "Уран", + "a4": "Нептун", + "valid": "Сатурн" + }, + { + "question": "Какое число является следующим в ряде Фибоначчи: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34?", + "a1": "44", + "a2": "55", + "a3": "60", + "a4": "65", + "valid": "55" + }, + { + "question": "Что станет больше, если его перевернуть вверх ногами?", + "a1": "Число 6", + "a2": "Число 9", + "a3": "Число 0", + "a4": "Число 8", + "valid": "Число 6" + } +] \ No newline at end of file diff --git a/data/versus.json b/data/versus.json new file mode 100644 index 0000000..4c2f0b0 --- /dev/null +++ b/data/versus.json @@ -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": "Запустить самолетик так, чтобы он попал в цель" + } +] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 01f1711..76127da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "class-transformer": "^0.5.1", "cyrillic-to-translit-js": "^3.2.1", "dotenv": "^16.3.1", + "husky": "^9.1.6", "latin-to-cyrillic": "^1.0.1", "mongodb": "^6.2.0", "mongoose": "^8.0.0", @@ -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", diff --git a/package.json b/package.json index 21a7ae2..6a36d3b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "prepare": "husky" }, "dependencies": { "@nestjs/axios": "3.0.1", @@ -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", diff --git a/src/Consts/FeatureFlags.consts.ts b/src/Consts/FeatureFlags.consts.ts new file mode 100644 index 0000000..848ffbe --- /dev/null +++ b/src/Consts/FeatureFlags.consts.ts @@ -0,0 +1,6 @@ +export class FeatureFlagsConsts { + static EnableEndgamePoints = 'EnableEndgamePoints'; + static DontMarkQuestionsAsCompleted = 'DontMarkQuestionsAsCompleted'; + static DisableVoice = 'DisableVoice'; + static StartVersusIfPlayersAnsweredInSameTime = 'StartVersusIfPlayersAnsweredInSameTime'; +} \ No newline at end of file diff --git a/src/Consts/commands.consts.ts b/src/Consts/commands.consts.ts index d6c9c6b..4486485 100644 --- a/src/Consts/commands.consts.ts +++ b/src/Consts/commands.consts.ts @@ -9,4 +9,6 @@ export class CommandsConsts { static GetCards = 'GetCards'; static ApplyDebuff = 'ApplyDebuff'; static CompleteQueue = 'CompleteQueue'; -} \ No newline at end of file + static GetQuestion = 'GetQuestion'; + static QuestionAnswer = "QuestionAnswer"; +} diff --git a/src/Consts/game-state.consts.ts b/src/Consts/game-state.consts.ts new file mode 100644 index 0000000..02384c0 --- /dev/null +++ b/src/Consts/game-state.consts.ts @@ -0,0 +1,5 @@ +export class GameStateConsts { + static Main = 'main'; + static EndgamePoints = 'endgamepoints'; + static Finish = 'finish'; +} \ No newline at end of file diff --git a/src/Consts/guest-property-names.consts.ts b/src/Consts/guest-property-names.consts.ts new file mode 100644 index 0000000..33d1b60 --- /dev/null +++ b/src/Consts/guest-property-names.consts.ts @@ -0,0 +1,4 @@ +export class GuestPropertyNamesConsts { + static VersusWonCount = 'versusWonCount'; + static VersusLoseCount = 'versusLoseCount'; +} \ No newline at end of file diff --git a/src/Consts/types.d.ts b/src/Consts/types.d.ts new file mode 100644 index 0000000..76ba488 --- /dev/null +++ b/src/Consts/types.d.ts @@ -0,0 +1,65 @@ +import {GameQueueTypes} from "../schemas/game-queue.schema"; + +export interface IStateInfo { + state:string; + value:string; +} + +export interface IValidAnswerReceivedSocketEvent { + telegramId: number; + validAnswer: string; + note: string; +} + +export interface IUserInfoMinimal { + telegramId: number; +} + +export interface IUserBasicInfo extends IUserInfoMinimal { + name: string; +} + +export interface IVersusBeginSocketEvent { + player1: number; + player2: number; + player1name: string; + player2name: string; +} + +export interface IVersusEndSocketEvent { + winner: number; +} + +export interface IScoreChangedSocketEvent extends IUserInfoMinimal { + newScore: number; +} + +export interface IUserCardChangedEvent extends IUserInfoMinimal { + cards: string[]; +} + +export interface IEmptyNotification {} + +export interface ISocketNotificationEvent { + text: string; + timeout: number; +} + +export interface IUserPropertyChangedEvent { + user: number; + property: string; + value: string; +} + +export interface ICardPlayedSocketEvent extends IUserInfoMinimal{ + card: string; + name: string; + timeout: number; +} +export interface IGameQueueSocketEvent { + _id: any; + completed: boolean; + target: number; + type: GameQueueTypes; + text: string; +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 107f690..6422730 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -20,6 +20,8 @@ import {ConfigModule} from "@nestjs/config"; import {MessagingModule} from "./messaging/messaging.module"; import * as process from "process"; import {OpenaiModule} from "./openai/openai.module"; +import { FeatureflagController } from './featureflag/featureflag.controller'; +import { FeatureflagService } from './featureflag/featureflag.service'; @Module({ imports: [ @@ -41,8 +43,8 @@ import {OpenaiModule} from "./openai/openai.module"; GiftsModule, OpenaiModule ], - controllers: [AppController], - providers: [AppService, SocketGateway, SchedulerService], - exports: [AppService, SocketGateway], + controllers: [AppController, FeatureflagController], + providers: [AppService, SocketGateway, SchedulerService, FeatureflagService], + exports: [AppService, SocketGateway, FeatureflagService], }) export class AppModule {} diff --git a/src/cards/cards.controller.spec.ts b/src/cards/cards.controller.spec.ts index d8c7700..187a263 100644 --- a/src/cards/cards.controller.spec.ts +++ b/src/cards/cards.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CardsController } from './cards.controller'; +import {CardsService} from "./cards.service"; +import {CardsServiceMock} from "../mocks/cards-service.mock"; describe('CardsController', () => { let controller: CardsController; @@ -7,6 +9,9 @@ describe('CardsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [CardsController], + providers: [ + { provide: CardsService, useValue: CardsServiceMock }, + ] }).compile(); controller = module.get(CardsController); diff --git a/src/cards/cards.service.spec.ts b/src/cards/cards.service.spec.ts index 2e26f28..a183748 100644 --- a/src/cards/cards.service.spec.ts +++ b/src/cards/cards.service.spec.ts @@ -1,12 +1,24 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CardsService } from './cards.service'; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {getModelToken} from "@nestjs/mongoose"; +import {Card} from "../schemas/cards.schema"; +import {Model} from "mongoose"; +import {EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; describe('CardsService', () => { let service: CardsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [CardsService], + providers: [ + CardsService, + { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: getModelToken(Card.name), useValue: Model }, + { provide: EventBus, useValue: EventbusMock }, + ], }).compile(); service = module.get(CardsService); diff --git a/src/featureflag/featureflag.controller.spec.ts b/src/featureflag/featureflag.controller.spec.ts new file mode 100644 index 0000000..e2642f2 --- /dev/null +++ b/src/featureflag/featureflag.controller.spec.ts @@ -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); + featureflagService = module.get(FeatureflagService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call feature flag service to get state', async() => { + const ffNameToTest = "TestFeature"; + const getFFMock = jest.spyOn(featureflagService, 'getFeatureFlag') + .mockImplementation( + (name) => Promise.resolve({ name: name, state: true}) + ); + + await controller.getFeatureFlag({ ffname: ffNameToTest }); + + expect(getFFMock).toHaveBeenCalled(); + expect(getFFMock).toHaveBeenCalledWith(ffNameToTest); + }); + + it('should call feature flag service to set state', async () => { + const ffNameToTest = "TestFeature"; + const setFFMock = jest.spyOn(featureflagService, 'setFeatureFlag') + .mockImplementation((id, status) => Promise.resolve({ name: id, state: false})); + + await controller.setFeatureFlag({ name: ffNameToTest, state: true }); + + expect(setFFMock).toHaveBeenCalled(); + expect(setFFMock).toHaveBeenCalledWith(ffNameToTest, true); + }); +}); diff --git a/src/featureflag/featureflag.controller.ts b/src/featureflag/featureflag.controller.ts new file mode 100644 index 0000000..16c53e8 --- /dev/null +++ b/src/featureflag/featureflag.controller.ts @@ -0,0 +1,18 @@ +import {Body, Controller, Get, Param, Post} from '@nestjs/common'; +import {FeatureflagService} from "./featureflag.service"; + +@Controller('featureflag') +export class FeatureflagController { + constructor(private featureflagService: FeatureflagService) { + } + + @Get(':ffname') + async getFeatureFlag(@Param() params: { ffname: string}) { + return await this.featureflagService.getFeatureFlag(params.ffname); + } + + @Post() + async setFeatureFlag(@Body() ffState: { name: string, state: boolean }) { + return await this.featureflagService.setFeatureFlag(ffState.name, ffState.state ); + } +} diff --git a/src/featureflag/featureflag.service.spec.ts b/src/featureflag/featureflag.service.spec.ts new file mode 100644 index 0000000..d114fae --- /dev/null +++ b/src/featureflag/featureflag.service.spec.ts @@ -0,0 +1,51 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FeatureflagService } from './featureflag.service'; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; + + +describe('FeatureflagService', () => { + let service: FeatureflagService; + let sharedService: SharedService; + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + FeatureflagService, + { provide: SharedService,useValue: SharedServiceMock }, + ], + }).compile(); + + service = module.get(FeatureflagService); + sharedService = module.get(SharedService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should set feature flag state', async () => { + const testFeatureName = 'TestFeatureFlag'; + const testFeatureState = true; + const setConfigMock = jest + .spyOn(sharedService,'setConfig') + .mockImplementation((name: string, value: string) => Promise.resolve({ key: name, value: value})); + await service.setFeatureFlag(testFeatureName, testFeatureState); + expect(setConfigMock).toHaveBeenCalledWith(`featureflag/${testFeatureName}`, testFeatureState.toString()); + expect(setConfigMock).toHaveBeenCalled(); + }); + + it('should return state of the feature flag', async () => { + const testFeatureName = 'TestFeatureFlag'; + const testFeatureState = true; + const getConfigMock = jest + .spyOn(sharedService, 'getConfig') + .mockImplementation((key: string) => Promise.resolve({ key:key, value: testFeatureState.toString() })); + await service.getFeatureFlag(testFeatureName); + + expect(getConfigMock).toHaveBeenCalledTimes(1); + expect(getConfigMock).toHaveBeenCalledWith(`featureflag/${testFeatureName}`); + + }); +}); diff --git a/src/featureflag/featureflag.service.ts b/src/featureflag/featureflag.service.ts new file mode 100644 index 0000000..f3360b0 --- /dev/null +++ b/src/featureflag/featureflag.service.ts @@ -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 { + 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 { + 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(ClientNotificationType.FeatureFlagChanged, ffStatus); + return ffStatus; + } + +} diff --git a/src/game/comand-handlers/begin-versus-command.handler.ts b/src/game/comand-handlers/begin-versus-command.handler.ts new file mode 100644 index 0000000..be114ef --- /dev/null +++ b/src/game/comand-handlers/begin-versus-command.handler.ts @@ -0,0 +1,13 @@ +import {CommandHandler, ICommandHandler} from "@nestjs/cqrs"; +import {BeginVersusCommand} from "../commands/begin-versus.command"; +import {VersusService} from "../versus/versus.service"; + +@CommandHandler(BeginVersusCommand) +export class BeginVersusCommandHandler implements ICommandHandler { + constructor(private versusService:VersusService) { + } + execute(command: BeginVersusCommand): Promise { + return this.versusService.beginVersus(command.sourceId,command.destinationId); + } + +} \ No newline at end of file diff --git a/src/game/comand-handlers/create-new-queue-item-command.handler.ts b/src/game/comand-handlers/create-new-queue-item-command.handler.ts index 88448f6..588c0fa 100644 --- a/src/game/comand-handlers/create-new-queue-item-command.handler.ts +++ b/src/game/comand-handlers/create-new-queue-item-command.handler.ts @@ -1,15 +1,19 @@ import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CreateNewQueueItemCommand } from '../commands/create-new-queue-item.command'; import { GameService } from '../game.service'; +import {Logger} from "@nestjs/common"; @CommandHandler(CreateNewQueueItemCommand) export class CreateNewQueueItemCommandHandler implements ICommandHandler { + private logger = new Logger(CreateNewQueueItemCommandHandler.name); constructor( private gameService: GameService, + ) { } async execute(command: CreateNewQueueItemCommand): Promise { + this.logger.verbose(`Adding new queue item ${command.type} for ${command.target}`); await this.gameService.addTaskToGameQueue(command.target, command.type, command.text); return Promise.resolve(undefined); } diff --git a/src/game/comand-handlers/give-out-a-prize-command.handler.ts b/src/game/comand-handlers/give-out-a-prize-command.handler.ts index 2ebd100..77ddcb8 100644 --- a/src/game/comand-handlers/give-out-a-prize-command.handler.ts +++ b/src/game/comand-handlers/give-out-a-prize-command.handler.ts @@ -3,6 +3,7 @@ import { GiveOutAPrizeCommand } from '../commands/give-out-a-prize.command'; import { GameService } from '../game.service'; import { Logger } from '@nestjs/common'; import { GameQueueTypes } from '../../schemas/game-queue.schema'; +import {GuestsService} from "../../guests/guests.service"; @CommandHandler(GiveOutAPrizeCommand) export class GameGiveOutAPrizeCommandHandler @@ -10,11 +11,12 @@ export class GameGiveOutAPrizeCommandHandler private readonly logger = new Logger(GameGiveOutAPrizeCommandHandler.name); - constructor(private gameService: GameService) { + constructor(private gameService: GameService, private guestService: GuestsService) { } async execute(command: GiveOutAPrizeCommand): Promise { this.logger.verbose(`Player winning a prize ${command.telegramId}`); + await this.guestService.incrementPrizeCount(command.telegramId); return this.gameService.addTaskToGameQueue( command.telegramId, GameQueueTypes.giveOutAPrize, diff --git a/src/game/comand-handlers/proceed-game-queue-command.handler.ts b/src/game/comand-handlers/proceed-game-queue-command.handler.ts index 1732821..1cd5497 100644 --- a/src/game/comand-handlers/proceed-game-queue-command.handler.ts +++ b/src/game/comand-handlers/proceed-game-queue-command.handler.ts @@ -1,13 +1,14 @@ -import { CommandBus, CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; -import { ProceedGameQueueCommand } from '../commands/proceed-game-queue.command'; -import { GameService } from '../game.service'; -import { NextQuestionCommand } from '../commands/next-question.command'; -import { SharedService } from '../../shared/shared.service'; -import { SocketEvents } from '../../shared/events.consts'; -import { Logger } from '@nestjs/common'; -import { GameQueueTypes } from '../../schemas/game-queue.schema'; -import { QuizAnswerStateChangedEvent } from '../events/quiz-answer-state-changed.event'; -import { QuizAnswerStateEnum } from '../entities/quiz-answer-state.enum'; +import {CommandBus, CommandHandler, EventBus, ICommandHandler} from '@nestjs/cqrs'; +import {ProceedGameQueueCommand} from '../commands/proceed-game-queue.command'; +import {GameService} from '../game.service'; +import {NextQuestionCommand} from '../commands/next-question.command'; +import {SharedService} from '../../shared/shared.service'; +import {Logger} from '@nestjs/common'; +import {GameQueueTypes} from '../../schemas/game-queue.schema'; +import {QuizAnswerStateChangedEvent} from '../events/quiz-answer-state-changed.event'; +import {QuizAnswerStateEnum} from '../entities/quiz-answer-state.enum'; +import {IGameQueueSocketEvent} from "../../Consts/types"; +import {ClientNotificationType} from "../../socket/socket.gateway"; @CommandHandler(ProceedGameQueueCommand) export class GameProceedGameQueueCommandHandler @@ -25,16 +26,13 @@ export class GameProceedGameQueueCommandHandler if (!item) { return this.cmdBus.execute(new NextQuestionCommand()); } - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.GameQueueItem, - { - _id: item.id, - completed: item.completed, - target: item.target, - type: item.type, - text: item.text - }, - ); + this.sharedService.notifyAllClients(ClientNotificationType.GameQueueItem, { + _id: item._id, + completed: item.completed, + target: item.target, + type: item.type, + text: item.text + }); switch (item.type) { case GameQueueTypes.giveOutAPrize: this.eventBus.publish( diff --git a/src/game/comand-handlers/select-target-player.handler.ts b/src/game/comand-handlers/select-target-player.handler.ts index 6e92112..cc03a48 100644 --- a/src/game/comand-handlers/select-target-player.handler.ts +++ b/src/game/comand-handlers/select-target-player.handler.ts @@ -18,18 +18,21 @@ export class SelectTargetPlayerHandler implements ICommandHandler { this.logger.verbose('enter'); - //const user = await this.guestService.findById(command.player); - const allUsers = await this.guestService.findAll(); + let allUsers = await this.guestService.findAll(); const user = allUsers.find(x => x.telegramId === command.player); if(!user) { throw new Error(`Cant find current user ${command.player}`); } + if(!command.allowSelf) { + allUsers = allUsers.filter((x) => x.telegramId !== command.player); + } const buttons = allUsers.map((x) => { return [{ text: `${Messages.EMOJI_PLAYER} ${x.name}`, callback_data: `{ "card": "${command.debuffName}", "value": "${command.value}", "user": "${x.telegramId}" }` }] }); + console.log(buttons); this.telegramService.send( { cmd: CommandsConsts.SendMessage}, diff --git a/src/game/commands/begin-versus.command.ts b/src/game/commands/begin-versus.command.ts new file mode 100644 index 0000000..6019beb --- /dev/null +++ b/src/game/commands/begin-versus.command.ts @@ -0,0 +1,6 @@ + +export class BeginVersusCommand { + constructor(public sourceId: number, public destinationId: number) { + + } +} \ No newline at end of file diff --git a/src/game/commands/select-target-player.command.ts b/src/game/commands/select-target-player.command.ts index 3ef7950..6e6f76d 100644 --- a/src/game/commands/select-target-player.command.ts +++ b/src/game/commands/select-target-player.command.ts @@ -1,4 +1,4 @@ export class SelectTargetPlayerCommand { - constructor(public player,public debuffName: string, public value: string|number) { + constructor(public player,public debuffName: string, public value: string|number, public allowSelf = true) { } } \ No newline at end of file diff --git a/src/game/entities/cards.entities.ts b/src/game/entities/cards.entities.ts index 6361606..2366170 100644 --- a/src/game/entities/cards.entities.ts +++ b/src/game/entities/cards.entities.ts @@ -19,8 +19,9 @@ import {SetGuestPropertyCommand} from "../../guests/command/set-guest-property.c import {StringHelper} from "../../helpers/stringhelper"; import {GetGuestQuery} from "../../guests/queries/getguest.query"; import {CardsSetChangedEvent} from "../events/cards-events/cards-set-changed.event"; -import {GetGuestPropertyQuery} from "../../guests/command/get-guest-property.handler"; import {GuestPropertiesConsts} from "../../schemas/properties.consts"; +import {BeginVersusCommand} from "../commands/begin-versus.command"; +import {CheckIfAnotherVersusInProgressQuery} from "../queries/check-if-another-versus-in-progress.query"; export interface IGameCard { setupHandlers(eventBus: EventBus, commandBus: CommandBus, queryBus: QueryBus): void; @@ -74,7 +75,7 @@ export class DoubleTreasureCard extends GameCard { await this.commandBus.execute( new GiveOutAPrizeCommand(this.telegramId), ); - const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId));; + const userSrc = await this.queryBus.execute(new GetGuestQuery(this.telegramId)); const subjcaseFrom = userSrc.get(StringHelper.getPropertyName(GuestPropertiesConsts.NameSubjectiveCase)); const message = `${subjcaseFrom} решает удвоить приз!`; await this.commandBus.execute(new SendToastCommand(message, 8000)); @@ -174,6 +175,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, ]; diff --git a/src/game/entities/debuffs.consts.ts b/src/game/entities/debuffs.consts.ts index 50162fd..ffd14fd 100644 --- a/src/game/entities/debuffs.consts.ts +++ b/src/game/entities/debuffs.consts.ts @@ -1,3 +1,4 @@ export class DebuffsConsts { static bannedFor = 'bannedFor'; + static versus = 'versus'; } \ No newline at end of file diff --git a/src/game/event-handlers/quiz-answered-event.handler.ts b/src/game/event-handlers/quiz-answered-event.handler.ts index b6337f8..3602780 100644 --- a/src/game/event-handlers/quiz-answered-event.handler.ts +++ b/src/game/event-handlers/quiz-answered-event.handler.ts @@ -10,6 +10,6 @@ export class QuizAnsweredEventHandler } async handle(event: QuizAnsweredEvent) { - await this.commandBus.execute(new HideKeyboardCommand(`На вопрос ответил: ${event.name}`)); + // await this.commandBus.execute(new HideKeyboardCommand(`На вопрос ответил: ${event.name}`)); } } diff --git a/src/game/event-handlers/wrong-answer-received-event.handler.ts b/src/game/event-handlers/wrong-answer-received-event.handler.ts index 1f2ae9c..c96d313 100644 --- a/src/game/event-handlers/wrong-answer-received-event.handler.ts +++ b/src/game/event-handlers/wrong-answer-received-event.handler.ts @@ -10,9 +10,6 @@ export class GameWrongAnswerReceivedEventHandler } async handle(event: WrongAnswerReceivedEvent) { - await this.gameService.addTaskToGameQueue( - event.tId, - GameQueueTypes.penalty, - ); + // } } diff --git a/src/game/events/state-changed.event.ts b/src/game/events/state-changed.event.ts new file mode 100644 index 0000000..76acc40 --- /dev/null +++ b/src/game/events/state-changed.event.ts @@ -0,0 +1,4 @@ +export class StateChangedEvent { + constructor(state: string) { + } +} \ No newline at end of file diff --git a/src/game/game.controller.spec.ts b/src/game/game.controller.spec.ts index f70c47c..cc45b54 100644 --- a/src/game/game.controller.spec.ts +++ b/src/game/game.controller.spec.ts @@ -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); diff --git a/src/game/game.controller.ts b/src/game/game.controller.ts index ec4ed4c..30f6562 100644 --- a/src/game/game.controller.ts +++ b/src/game/game.controller.ts @@ -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(); + } } diff --git a/src/game/game.module.ts b/src/game/game.module.ts index c49ae13..3e55e32 100644 --- a/src/game/game.module.ts +++ b/src/game/game.module.ts @@ -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 {} diff --git a/src/game/game.service.spec.ts b/src/game/game.service.spec.ts index f4a1db7..bb06bf1 100644 --- a/src/game/game.service.spec.ts +++ b/src/game/game.service.spec.ts @@ -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); diff --git a/src/game/game.service.ts b/src/game/game.service.ts index 4f8e43b..abd9bba 100644 --- a/src/game/game.service.ts +++ b/src/game/game.service.ts @@ -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(ClientNotificationType.QueueCompleted, {}); await this.cmdBus.execute(new ProceedGameQueueCommand()); return qItem; } async pauseGame() { await this.sharedService.setConfig('game_state', 'paused'); - await this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.GAME_PAUSED, - {}, - ); + this.sharedService.notifyAllClients(ClientNotificationType.GamePaused, {}); return Promise.resolve({ result: true }); } async resumeGame() { await this.sharedService.setConfig('game_state', 'running'); - await this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.GAME_RESUMED, - {}, - ); + this.sharedService.notifyAllClients(ClientNotificationType.GameResumed,{}); return Promise.resolve({ result: true }); } @@ -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', '')); + } } diff --git a/src/game/queries/check-if-another-versus-in-progress.query.ts b/src/game/queries/check-if-another-versus-in-progress.query.ts new file mode 100644 index 0000000..91b6288 --- /dev/null +++ b/src/game/queries/check-if-another-versus-in-progress.query.ts @@ -0,0 +1,3 @@ +export class CheckIfAnotherVersusInProgressQuery { + +} \ No newline at end of file diff --git a/src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts b/src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts new file mode 100644 index 0000000..6089b25 --- /dev/null +++ b/src/game/queries/handlers/check-if-another-versus-in-progress.handler.ts @@ -0,0 +1,13 @@ +import {IQueryHandler, QueryHandler} from "@nestjs/cqrs"; +import {CheckIfAnotherVersusInProgressQuery} from "../check-if-another-versus-in-progress.query"; +import {VersusService} from "../../versus/versus.service"; + +@QueryHandler(CheckIfAnotherVersusInProgressQuery) +export class CheckIfAnotherVersusInProgressHandler implements IQueryHandler { + constructor(private versusService: VersusService) { + } + async execute(query: CheckIfAnotherVersusInProgressHandler): Promise { + return await this.versusService.checkIfAnotherVersusInProgress(); + } + +} \ No newline at end of file diff --git a/src/game/versus/versus.controller.spec.ts b/src/game/versus/versus.controller.spec.ts new file mode 100644 index 0000000..5fbe666 --- /dev/null +++ b/src/game/versus/versus.controller.spec.ts @@ -0,0 +1,23 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { VersusController } from './versus.controller'; +import {VersusService} from "./versus.service"; +import {VersusServiceMock} from "../../mocks/versus-service.mock"; + +describe('VersusController', () => { + let controller: VersusController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [VersusController], + providers: [ + { provide: VersusService, useValue: VersusServiceMock }, + ] + }).compile(); + + controller = module.get(VersusController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/game/versus/versus.controller.ts b/src/game/versus/versus.controller.ts new file mode 100644 index 0000000..e5c3ada --- /dev/null +++ b/src/game/versus/versus.controller.ts @@ -0,0 +1,37 @@ +import {Body, Controller, Get, Logger, Post} from '@nestjs/common'; +import {VersusService} from "./versus.service"; +import {VersusDto} from "./versus.types"; + +@Controller('versus') +export class VersusController { + private logger = new Logger(VersusController.name); + constructor(private versusService: VersusService) { + + } + + @Post('simulate-versus') + async SimulateVersus() { + this.logger.verbose('[SimulateVersus] enter'); + return this.versusService.simulateVersus(); + } + + @Post('import') + async Import(@Body() data: VersusDto[]) { + return await this.versusService.importVersus(data); + } + + @Get() + async GetVersusTask() { + return await this.versusService.getVersusTask(); + } + + @Post('complete') + async Completed(@Body() payload: { winner: number, loser: number }) { + return await this.versusService.complete(payload.winner, payload.loser); + } + + @Post('reset-all') + async markAllUncompleted() { + return await this.versusService.markAllAsUncompleted(); + } +} diff --git a/src/game/versus/versus.service.spec.ts b/src/game/versus/versus.service.spec.ts new file mode 100644 index 0000000..ca254b9 --- /dev/null +++ b/src/game/versus/versus.service.spec.ts @@ -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; + + 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); + versusModel = module.get>(getModelToken(Versus.name)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('validateVersusTasksAndResetIfNecessary', () => { + it('should reset all tasks if no remaining', async () => { + // setup + mockVersusModel.exec.mockResolvedValue([]); + const markCompletedSpy = jest.spyOn(service, 'markAllAsUncompleted').mockResolvedValue(null); + + // act + await service.validateVersusTasksAndResetIfNecessary(); + + // validate + expect(markCompletedSpy).toHaveBeenCalled(); + }); + + it('should not reset tasks if it is presented', async () => { + mockVersusModel.exec.mockReturnValue(['item1', 'item2']); + const markCompletedSpy = jest.spyOn(service,'markAllAsUncompleted').mockResolvedValue(null); + await service.validateVersusTasksAndResetIfNecessary(); + expect(markCompletedSpy).not.toHaveBeenCalled(); + }); + }) +}); diff --git a/src/game/versus/versus.service.ts b/src/game/versus/versus.service.ts new file mode 100644 index 0000000..782e0db --- /dev/null +++ b/src/game/versus/versus.service.ts @@ -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, + 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(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(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 !== ''; + + } +} diff --git a/src/game/versus/versus.types.d.ts b/src/game/versus/versus.types.d.ts new file mode 100644 index 0000000..e81bec8 --- /dev/null +++ b/src/game/versus/versus.types.d.ts @@ -0,0 +1,6 @@ +export interface VersusDto { + id: string; + text: string; + completed: boolean; + description: string; +} \ No newline at end of file diff --git a/src/gifts/gifts.controller.spec.ts b/src/gifts/gifts.controller.spec.ts index 3bdf5c2..d5cef58 100644 --- a/src/gifts/gifts.controller.spec.ts +++ b/src/gifts/gifts.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GiftsController } from './gifts.controller'; +import {GiftsService} from "./gifts.service"; +import {GiftServiceMock} from "../mocks/gift-service.mock"; describe('GiftsController', () => { let controller: GiftsController; @@ -7,6 +9,9 @@ describe('GiftsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [GiftsController], + providers: [ + { provide: GiftsService, useValue: GiftServiceMock }, + ] }).compile(); controller = module.get(GiftsController); diff --git a/src/gifts/gifts.service.spec.ts b/src/gifts/gifts.service.spec.ts index 6fabaa2..ab2b5fa 100644 --- a/src/gifts/gifts.service.spec.ts +++ b/src/gifts/gifts.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GiftsService } from './gifts.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Prize} from "../schemas/prize.schema"; +import {Model} from "mongoose"; describe('GiftsService', () => { let service: GiftsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [GiftsService], + providers: [ + GiftsService, + { provide: getModelToken(Prize.name), useValue: Model }, + ], }).compile(); service = module.get(GiftsService); diff --git a/src/guests/command/get-guest-property.handler.ts b/src/guests/command/get-guest-property.handler.ts index 075d87f..19fdcac 100644 --- a/src/guests/command/get-guest-property.handler.ts +++ b/src/guests/command/get-guest-property.handler.ts @@ -15,9 +15,8 @@ export class GetGuestPropertyHandler implements ICommandHandler { this.logger.verbose(`entering`); const guest = await this.guestService.findById(command.user); - console.log(command); + this.logger.verbose(command); if(!command.property.startsWith('properties.')) { - command.property = `properties.${command.property}`; this.logger.warn(`update prop ${command.property}`); } diff --git a/src/guests/command/guests.post-cards-to-user-command.ts b/src/guests/command/guests.post-cards-to-user-command.ts index ac0ac03..3eafee4 100644 --- a/src/guests/command/guests.post-cards-to-user-command.ts +++ b/src/guests/command/guests.post-cards-to-user-command.ts @@ -28,13 +28,16 @@ export class TgPostCardsToUserCommandHandler implements ICommandHandler { extra.reply_markup.keyboard.push([ {text: Messages.EMOJI_CARD + ' ' + card}, ]); - extra_Inline.reply_markup.inline_keyboard.push([{ text: Messages.EMOJI_CARD + ' ' + card}]) + extra_Inline.reply_markup.inline_keyboard.push([{ text: Messages.EMOJI_CARD + ' ' + card, callback_data: `card/${card}`}]) }); await this.sharedService.setConfig(`buttons_${command.chatId}`, JSON.stringify(extra), diff --git a/src/guests/command/handlers/increase-player-score-command.handler.ts b/src/guests/command/handlers/increase-player-score-command.handler.ts new file mode 100644 index 0000000..bce1dde --- /dev/null +++ b/src/guests/command/handlers/increase-player-score-command.handler.ts @@ -0,0 +1,19 @@ +import {CommandHandler, ICommandHandler} from "@nestjs/cqrs"; +import {IncreasePlayerScoreCommand} from "../increase-player-score.command"; +import { Logger } from "@nestjs/common"; +import {GuestsService} from "../../guests.service"; + +@CommandHandler(IncreasePlayerScoreCommand) +export class IncreasePlayerScoreCommandHandler implements ICommandHandler { + private logger = new Logger(IncreasePlayerScoreCommandHandler.name); + constructor(private guestService: GuestsService) { + } + + + async execute(command: IncreasePlayerScoreCommand): Promise { + this.logger.verbose(`IncreasePlayerScoreCommandHandler: entering, arguments: player: ${command.user}, amount: ${command.score}`); + await this.guestService.updatePlayerScore(command.user, command.score); + return true; + } + +} \ No newline at end of file diff --git a/src/guests/command/increase-player-score.command.ts b/src/guests/command/increase-player-score.command.ts new file mode 100644 index 0000000..7e87c36 --- /dev/null +++ b/src/guests/command/increase-player-score.command.ts @@ -0,0 +1,4 @@ +export class IncreasePlayerScoreCommand { + constructor(public user: number, public score: number) { + } +} \ No newline at end of file diff --git a/src/guests/command/remove-card-from-user.handler.ts b/src/guests/command/remove-card-from-user.handler.ts index 5505a3d..b1f07cf 100644 --- a/src/guests/command/remove-card-from-user.handler.ts +++ b/src/guests/command/remove-card-from-user.handler.ts @@ -20,36 +20,36 @@ export class RemoveCardFromUserCommandHandler implements ICommandHandler { - const guest = await this.guestService.findById(command.telegramId); - const data = await this.sharedService.getConfig(`buttons_${command.telegramId}`); - const extra = { - reply_markup: { - remove_keyboard: false, - keyboard: [], - }, - }; - const buttons = JSON.parse(data.value); - let found = false; - buttons.reply_markup.keyboard.forEach((item) => { - if (item[0].text.includes(command.card.description) && !found) { - found = true; - } else { - extra.reply_markup.keyboard.push( - [ { ...item[0] } ] - ) - } - }); - if (extra.reply_markup.keyboard.length === 0) { - extra.reply_markup.remove_keyboard = true; - } - await this.sharedService.setConfig(`buttons_${command.telegramId}`, JSON.stringify(extra)); - this.telegramService.emit({ - cmd: CommandsConsts.SendMessage, - }, { - chatId: guest.chatId, - message: Messages.SELECT_CARD, - extra: extra, - }) + // const guest = await this.guestService.findById(command.telegramId); + // const data = await this.sharedService.getConfig(`buttons_${command.telegramId}`); + // const extra = { + // reply_markup: { + // remove_keyboard: false, + // keyboard: [], + // }, + // }; + // const buttons = JSON.parse(data.value); + // let found = false; + // buttons.reply_markup.keyboard.forEach((item) => { + // if (item[0].text.includes(command.card.description) && !found) { + // found = true; + // } else { + // extra.reply_markup.keyboard.push( + // [ { ...item[0] } ] + // ) + // } + // }); + // if (extra.reply_markup.keyboard.length === 0) { + // extra.reply_markup.remove_keyboard = true; + // } + // await this.sharedService.setConfig(`buttons_${command.telegramId}`, JSON.stringify(extra)); + // this.telegramService.emit({ + // cmd: CommandsConsts.SendMessage, + // }, { + // chatId: guest.chatId, + // message: Messages.SELECT_CARD, + // extra: extra, + // }) } } \ No newline at end of file diff --git a/src/guests/command/send-between-rounds-actions.command.ts b/src/guests/command/send-between-rounds-actions.command.ts index 5ef3973..292e312 100644 --- a/src/guests/command/send-between-rounds-actions.command.ts +++ b/src/guests/command/send-between-rounds-actions.command.ts @@ -3,7 +3,7 @@ import {Promise} from "mongoose"; import {GuestsService} from "../guests.service"; export class SendBetweenRoundsActionsCommand { - constructor(public user: number, public inline: boolean = false) { + constructor(public user: number, public inline: boolean = true) { } } diff --git a/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts b/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts index 802ce1e..0c864fa 100644 --- a/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts +++ b/src/guests/event-handlers/guest-valid-answer-received-event.handler.ts @@ -21,14 +21,14 @@ export class GuestValidAnswerReceivedEventHandler async handle(event: ValidAnswerReceivedEvent) { await this.guestService.notifyAboutValidAnswer(event.tId); await this.guestService.sendValidAnswerActions(event.tId); - await this.guestService.updatePlayerScore(event.tId, 1); + // await this.guestService.updatePlayerScore(event.tId, 1); if (event.extraDetails) { await this.commandBus.execute( new SendToastCommand(event.extraDetails, 4000), ); } const coef = +(await this.guestService.getCoefficient()); - await this.guestService.changeWinningChance(event.tId, 29 * coef); + // await this.guestService.changeWinningChance(event.tId, 29 * coef); await this.guestService.resetInvalidAnswersInTheRow(event.tId); } } diff --git a/src/guests/guest.types.d.ts b/src/guests/guest.types.d.ts new file mode 100644 index 0000000..894e19e --- /dev/null +++ b/src/guests/guest.types.d.ts @@ -0,0 +1,5 @@ +export interface GuestNamesInCases { + SubjectiveCase: string; + AccusativeCase: string; + GenitiveCase: string; +} \ No newline at end of file diff --git a/src/guests/guests.controller.spec.ts b/src/guests/guests.controller.spec.ts index 157ce15..c59e9e7 100644 --- a/src/guests/guests.controller.spec.ts +++ b/src/guests/guests.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GuestsController } from './guests.controller'; +import {GuestsService} from "./guests.service"; +import {GuestsServiceMock} from "../mocks/guests-service.mock"; describe('GuestsController', () => { let controller: GuestsController; @@ -7,6 +9,9 @@ describe('GuestsController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [GuestsController], + providers: [ + { provide: GuestsService, useValue: GuestsServiceMock }, + ] }).compile(); controller = module.get(GuestsController); diff --git a/src/guests/guests.controller.ts b/src/guests/guests.controller.ts index 0fc9cea..31dd713 100644 --- a/src/guests/guests.controller.ts +++ b/src/guests/guests.controller.ts @@ -1,6 +1,6 @@ import { Controller, - Get, Param, Res + Get, Param, Post, Res } from "@nestjs/common"; import { GuestsService } from './guests.service'; import { Response } from 'express'; @@ -42,4 +42,9 @@ export class GuestsController { // this.echoService.enterQuiz(u.chatId); }); } + @Post('reset-score') + async resetAllPlayersScore() { + await this.guestService.resetPlayersScore(); + return { result: true }; + } } diff --git a/src/guests/guests.module.ts b/src/guests/guests.module.ts index 95e776f..8964c88 100644 --- a/src/guests/guests.module.ts +++ b/src/guests/guests.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import {forwardRef, Module} from '@nestjs/common'; import { GuestsService } from './guests.service'; import { MongooseModule } from '@nestjs/mongoose'; import { Guest, GuestSchema } from '../schemas/guest.schema'; @@ -23,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 {} diff --git a/src/guests/guests.service.spec.ts b/src/guests/guests.service.spec.ts index 7919215..214a84f 100644 --- a/src/guests/guests.service.spec.ts +++ b/src/guests/guests.service.spec.ts @@ -1,12 +1,39 @@ import { Test, TestingModule } from '@nestjs/testing'; import { GuestsService } from './guests.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Guest} from "../schemas/guest.schema"; +import {Model} from "mongoose"; +import {Config} from "../schemas/config.schema"; +import {ClientProxy} from "@nestjs/microservices"; +import {ClientProxyMock} from "../mocks/client-proxy.mock"; +import {CommandBus, EventBus, QueryBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; +import {CommandbusMock} from "../mocks/commandbus.mock"; +import {CardsServiceMock} from "../mocks/cards-service.mock"; +import {CardsService} from "../cards/cards.service"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {QueryBusMock} from "../mocks/querybus.mock"; +import {VoiceService} from "../voice/voice.service"; +import {VoiceServiceMock} from "../mocks/voice-service.mock"; describe('GuestsService', () => { let service: GuestsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [GuestsService], + providers: [ + GuestsService, + { provide: getModelToken(Guest.name), useValue: Model }, + { provide: getModelToken(Config.name), useValue: Model }, + { provide: 'Telegram', useValue: ClientProxyMock }, + { provide: EventBus, useValue: EventbusMock }, + { provide: CommandBus, useValue: CommandbusMock }, + { provide: CardsService, useValue: CardsServiceMock }, + { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: QueryBus, useValue: QueryBusMock }, + { provide: VoiceService, useValue: VoiceServiceMock }, + ], }).compile(); service = module.get(GuestsService); diff --git a/src/guests/guests.service.ts b/src/guests/guests.service.ts index 4910caa..959f713 100644 --- a/src/guests/guests.service.ts +++ b/src/guests/guests.service.ts @@ -1,7 +1,7 @@ import {Inject, Injectable, Logger} from '@nestjs/common'; import {InjectModel} from '@nestjs/mongoose'; import {Guest, GuestDocument} from '../schemas/guest.schema'; -import {Model} from 'mongoose'; +import {Document, Model} from 'mongoose'; import {CreateGuestDto} from './dto/create-guest.dto'; import {QuestionDto} from '../quiz/dto/question.dto'; import {Messages} from '../messaging/tg.text'; @@ -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 { + 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(); + } } diff --git a/src/guests/queries/handlers/get-guest-query.handler.ts b/src/guests/queries/handlers/get-guest-query.handler.ts index 247aaf6..d9c1883 100644 --- a/src/guests/queries/handlers/get-guest-query.handler.ts +++ b/src/guests/queries/handlers/get-guest-query.handler.ts @@ -1,6 +1,5 @@ import {IQueryHandler, QueryHandler} from "@nestjs/cqrs"; import {GetGuestQuery} from "../getguest.query"; -import {Promise} from "mongoose"; import {GuestsService} from "../../guests.service"; @QueryHandler(GetGuestQuery) diff --git a/src/messaging/guests.controller.ts b/src/messaging/guests.controller.ts index 49adfd6..bd4794f 100644 --- a/src/messaging/guests.controller.ts +++ b/src/messaging/guests.controller.ts @@ -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); + } + } \ No newline at end of file diff --git a/src/messaging/models/validate-answer.model.ts b/src/messaging/models/validate-answer.model.ts index e3c9249..229dc5c 100644 --- a/src/messaging/models/validate-answer.model.ts +++ b/src/messaging/models/validate-answer.model.ts @@ -2,4 +2,10 @@ export interface ValidateAnswerModel { answer: string; user: number; name: string; +} + +export interface ValidateAnswerInline { + answer: string; + user: number; + name: string; } \ No newline at end of file diff --git a/src/messaging/quiz-messaging.controller.ts b/src/messaging/quiz-messaging.controller.ts index 71b9b8c..5417a4f 100644 --- a/src/messaging/quiz-messaging.controller.ts +++ b/src/messaging/quiz-messaging.controller.ts @@ -1,7 +1,7 @@ import {Controller, Logger} from "@nestjs/common"; import {MessagePattern, Payload} from "@nestjs/microservices"; import {CommandsConsts} from "../Consts/commands.consts"; -import {ValidateAnswerModel} from "./models/validate-answer.model"; +import {ValidateAnswerInline, ValidateAnswerModel} from "./models/validate-answer.model"; import {QuizAnsweredEvent} from "../game/events/quiz.answered"; import {QuizService} from "../quiz/quiz.service"; import {CommandBus, EventBus} from "@nestjs/cqrs"; @@ -15,11 +15,12 @@ export class QuizMessagingController { constructor(private quizService: QuizService, private eventBus: EventBus, private cmdBus: CommandBus, private gameService: GameService) { } - @MessagePattern({ cmd: CommandsConsts.ValidateAnswer}) - async validateAnswer(@Payload() data: ValidateAnswerModel) { + + @MessagePattern({ cmd: CommandsConsts.QuestionAnswer}) + async getQuestionAnswer(@Payload() data: ValidateAnswerInline) { this.logger.verbose(`Validate answer ${data}`); this.eventBus.publish(new QuizAnsweredEvent(data.name)); - const result = await this.quizService.validateAnswer( + const result = await this.quizService.validateAnswerInline( data.answer, data.user, ); @@ -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}) diff --git a/src/mocks/cards-service.mock.ts b/src/mocks/cards-service.mock.ts new file mode 100644 index 0000000..00e00b0 --- /dev/null +++ b/src/mocks/cards-service.mock.ts @@ -0,0 +1,3 @@ +export const CardsServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/client-proxy.mock.ts b/src/mocks/client-proxy.mock.ts new file mode 100644 index 0000000..3672ec4 --- /dev/null +++ b/src/mocks/client-proxy.mock.ts @@ -0,0 +1,3 @@ +export const ClientProxyMock = { + +} \ No newline at end of file diff --git a/src/mocks/commandbus.mock.ts b/src/mocks/commandbus.mock.ts new file mode 100644 index 0000000..b3dbd30 --- /dev/null +++ b/src/mocks/commandbus.mock.ts @@ -0,0 +1,3 @@ +export const CommandbusMock = { + execute: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/config-service.mock.ts b/src/mocks/config-service.mock.ts new file mode 100644 index 0000000..4c85c87 --- /dev/null +++ b/src/mocks/config-service.mock.ts @@ -0,0 +1,3 @@ +export const ConfigServiceMock = { + get: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/eventbus.mock.ts b/src/mocks/eventbus.mock.ts new file mode 100644 index 0000000..162baf2 --- /dev/null +++ b/src/mocks/eventbus.mock.ts @@ -0,0 +1,3 @@ +export const EventbusMock = { + +} \ No newline at end of file diff --git a/src/mocks/featureflag-service.mock.ts b/src/mocks/featureflag-service.mock.ts new file mode 100644 index 0000000..2bb8a11 --- /dev/null +++ b/src/mocks/featureflag-service.mock.ts @@ -0,0 +1,4 @@ +export const FeatureflagServiceMock = { + getFeatureFlag: jest.fn(() => Promise.resolve(false)), + setFeatureFlag: jest.fn(() => Promise.resolve(false)) +} \ No newline at end of file diff --git a/src/mocks/game-service.mock.ts b/src/mocks/game-service.mock.ts new file mode 100644 index 0000000..ecf04ef --- /dev/null +++ b/src/mocks/game-service.mock.ts @@ -0,0 +1,3 @@ +export const GameServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/gift-service.mock.ts b/src/mocks/gift-service.mock.ts new file mode 100644 index 0000000..1e5b4af --- /dev/null +++ b/src/mocks/gift-service.mock.ts @@ -0,0 +1,3 @@ +export const GiftServiceMock = { + getRemainingPrizeCount: () => jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/guests-service.mock.ts b/src/mocks/guests-service.mock.ts new file mode 100644 index 0000000..cc01ec0 --- /dev/null +++ b/src/mocks/guests-service.mock.ts @@ -0,0 +1,3 @@ +export const GuestsServiceMock = { + updatePenaltiesCount: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/httpservice.mock.ts b/src/mocks/httpservice.mock.ts new file mode 100644 index 0000000..5798e0d --- /dev/null +++ b/src/mocks/httpservice.mock.ts @@ -0,0 +1,3 @@ +export const HttpServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/penalty-service.mock.ts b/src/mocks/penalty-service.mock.ts new file mode 100644 index 0000000..52a71d8 --- /dev/null +++ b/src/mocks/penalty-service.mock.ts @@ -0,0 +1,3 @@ +export const PenaltyServiceMock = { + +} \ No newline at end of file diff --git a/src/mocks/querybus.mock.ts b/src/mocks/querybus.mock.ts new file mode 100644 index 0000000..e4df30c --- /dev/null +++ b/src/mocks/querybus.mock.ts @@ -0,0 +1,3 @@ +export const QueryBusMock = { + +} \ No newline at end of file diff --git a/src/mocks/quiz-service.mock.ts b/src/mocks/quiz-service.mock.ts new file mode 100644 index 0000000..4cef2c0 --- /dev/null +++ b/src/mocks/quiz-service.mock.ts @@ -0,0 +1,3 @@ +export const QuizServiceMock = { + getRemainQuestionCount: () =>jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/shared-service.mock.ts b/src/mocks/shared-service.mock.ts new file mode 100644 index 0000000..1d54e71 --- /dev/null +++ b/src/mocks/shared-service.mock.ts @@ -0,0 +1,5 @@ +export const SharedServiceMock = { + setConfig: jest.fn(), + getConfig: jest.fn(), + notifyAllClients: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/socket-gateway.mock.ts b/src/mocks/socket-gateway.mock.ts new file mode 100644 index 0000000..ea7d28d --- /dev/null +++ b/src/mocks/socket-gateway.mock.ts @@ -0,0 +1,3 @@ +export const SocketGatewayMock = { + +} \ No newline at end of file diff --git a/src/mocks/state-service.mock.ts b/src/mocks/state-service.mock.ts new file mode 100644 index 0000000..797245e --- /dev/null +++ b/src/mocks/state-service.mock.ts @@ -0,0 +1,3 @@ +export const StateServiceMock = { + setState: jest.fn(), +} \ No newline at end of file diff --git a/src/mocks/versus-service.mock.ts b/src/mocks/versus-service.mock.ts new file mode 100644 index 0000000..b3555ad --- /dev/null +++ b/src/mocks/versus-service.mock.ts @@ -0,0 +1,3 @@ +export class VersusServiceMock { + +} \ No newline at end of file diff --git a/src/mocks/voice-service.mock.ts b/src/mocks/voice-service.mock.ts new file mode 100644 index 0000000..63e0e21 --- /dev/null +++ b/src/mocks/voice-service.mock.ts @@ -0,0 +1,3 @@ +export const VoiceServiceMock = { + +} \ No newline at end of file diff --git a/src/penalty/penalty.controller.spec.ts b/src/penalty/penalty.controller.spec.ts index 24f00d8..9e00e9a 100644 --- a/src/penalty/penalty.controller.spec.ts +++ b/src/penalty/penalty.controller.spec.ts @@ -1,5 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PenaltyController } from './penalty.controller'; +import {PenaltyService} from "./penalty.service"; +import {PenaltyServiceMock} from "../mocks/penalty-service.mock"; describe('PenaltyController', () => { let controller: PenaltyController; @@ -7,6 +9,9 @@ describe('PenaltyController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [PenaltyController], + providers: [ + { provide: PenaltyService, useValue: PenaltyServiceMock }, + ] }).compile(); controller = module.get(PenaltyController); diff --git a/src/penalty/penalty.service.spec.ts b/src/penalty/penalty.service.spec.ts index c37fee8..dd3b657 100644 --- a/src/penalty/penalty.service.spec.ts +++ b/src/penalty/penalty.service.spec.ts @@ -1,12 +1,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PenaltyService } from './penalty.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {Penalty} from "../schemas/penalty.schema"; +import {Model} from "mongoose"; describe('PenaltyService', () => { let service: PenaltyService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [PenaltyService], + providers: [ + PenaltyService, + { provide: getModelToken(Penalty.name), useValue: Model }, + ], }).compile(); service = module.get(PenaltyService); diff --git a/src/quiz/dto/question.dto.ts b/src/quiz/dto/question.dto.ts index 2c1e413..017f163 100644 --- a/src/quiz/dto/question.dto.ts +++ b/src/quiz/dto/question.dto.ts @@ -1,8 +1,10 @@ export interface QuestionDto { + id: string; text: string; answers: string[]; valid: string; note: string | null; + qId: string; } diff --git a/src/quiz/event-handlers/state-changed-event.handler.ts b/src/quiz/event-handlers/state-changed-event.handler.ts new file mode 100644 index 0000000..e8cb84c --- /dev/null +++ b/src/quiz/event-handlers/state-changed-event.handler.ts @@ -0,0 +1,17 @@ +import {EventsHandler, IEventHandler} from "@nestjs/cqrs"; +import {StateChangedEvent} from "../../game/events/state-changed.event"; +import {QuizService} from "../quiz.service"; +import {Logger} from "@nestjs/common"; + +@EventsHandler(StateChangedEvent) +export class StateChangedEventHandler implements IEventHandler { + logger = new Logger(StateChangedEventHandler.name); + constructor(private quizService: QuizService) { + } + + async handle(event: StateChangedEvent) { + this.logger.verbose(`[StateChangedEventHandler] enter, event: ${event}}`) + await this.quizService.calculateEndgamePoints(); + } + +} \ No newline at end of file diff --git a/src/quiz/quiz.controller.spec.ts b/src/quiz/quiz.controller.spec.ts index 9d9adb2..fcf5076 100644 --- a/src/quiz/quiz.controller.spec.ts +++ b/src/quiz/quiz.controller.spec.ts @@ -1,15 +1,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { QuizController } from './quiz.controller'; +import {QuizService} from "./quiz.service"; +import {QuizServiceMock} from "../mocks/quiz-service.mock"; describe('QuizController', () => { let controller: QuizController; + let quizService: QuizService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [QuizController], + providers: [ + { provide: QuizService, useValue: QuizServiceMock }, + ] }).compile(); controller = module.get(QuizController); + quizService = module.get(QuizService); }); it('should be defined', () => { diff --git a/src/quiz/quiz.controller.ts b/src/quiz/quiz.controller.ts index babe221..554b603 100644 --- a/src/quiz/quiz.controller.ts +++ b/src/quiz/quiz.controller.ts @@ -1,7 +1,7 @@ -import { Body, Controller, Get, Post } from "@nestjs/common"; -import { QuestionDto, QuestionDtoExcel } from './dto/question.dto'; -import { QuizService } from "./quiz.service"; -import { ExtraQuestionDto } from './dto/extra-question.dto'; +import {Body, Controller, Get, Post} from "@nestjs/common"; +import {QuestionDto, QuestionDtoExcel} from './dto/question.dto'; +import {QuizService} from "./quiz.service"; +import {ExtraQuestionDto} from './dto/extra-question.dto'; @Controller('quiz') export class QuizController { @@ -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(); + } } diff --git a/src/quiz/quiz.module.ts b/src/quiz/quiz.module.ts index 7551cbd..e3ed269 100644 --- a/src/quiz/quiz.module.ts +++ b/src/quiz/quiz.module.ts @@ -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 {} diff --git a/src/quiz/quiz.service.spec.ts b/src/quiz/quiz.service.spec.ts index 03b305f..f60b3b3 100644 --- a/src/quiz/quiz.service.spec.ts +++ b/src/quiz/quiz.service.spec.ts @@ -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); + service = await module.resolve(QuizService); + cmdBus = await module.resolve(CommandBus); + guestService = await module.resolve(GuestsService); + featureFlagService = await module.resolve(FeatureflagService); }); it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('calculateScore()', () => { + let cmdBusExecSpy: jest.SpyInstance, [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', () => { + + }) + }); }); diff --git a/src/quiz/quiz.service.ts b/src/quiz/quiz.service.ts index 76a89b2..21be7bb 100644 --- a/src/quiz/quiz.service.ts +++ b/src/quiz/quiz.service.ts @@ -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, @@ -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 { 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(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 { + 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 { + async getRemainQuestionWithoutValidAnswer(): Promise { 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; + } } diff --git a/src/quiz/quiz.types.d.ts b/src/quiz/quiz.types.d.ts new file mode 100644 index 0000000..69f34b4 --- /dev/null +++ b/src/quiz/quiz.types.d.ts @@ -0,0 +1,11 @@ +export interface QuizEndGameResultsDetails { + id: number; + count: number; + name: string; +} + +export interface QuizEndGameResults { + maxInvalidAnswers: QuizEndGameResultsDetails; + maxRewards: QuizEndGameResultsDetails; + maxPenalties: QuizEndGameResultsDetails; +} \ No newline at end of file diff --git a/src/scheduler/scheduler.service.spec.ts b/src/scheduler/scheduler.service.spec.ts index 1327eb5..135f68c 100644 --- a/src/scheduler/scheduler.service.spec.ts +++ b/src/scheduler/scheduler.service.spec.ts @@ -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); + giftService = module.get(GiftsService); + sharedService = module.get(SharedService); + stateService = module.get(StateService); + featureFlagService = module.get(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(); + }); }); diff --git a/src/scheduler/scheduler.service.ts b/src/scheduler/scheduler.service.ts index 1b2974a..c6316a1 100644 --- a/src/scheduler/scheduler.service.ts +++ b/src/scheduler/scheduler.service.ts @@ -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(ClientNotificationType.StateChanged, state); + } else { + const state = await this.stateService.setState('main', 'finish'); + this.sharedService.notifyAllClients(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( diff --git a/src/schemas/config.schema.ts b/src/schemas/config.schema.ts index 39a4049..a213192 100644 --- a/src/schemas/config.schema.ts +++ b/src/schemas/config.schema.ts @@ -1,8 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -export type ConfigDocument = Config & Document; - @Schema() export class Config { @Prop() @@ -12,3 +10,4 @@ export class Config { } export const ConfigSchema = SchemaFactory.createForClass(Config); +export type ConfigDocument = Config & Document; \ No newline at end of file diff --git a/src/schemas/game-queue.schema.ts b/src/schemas/game-queue.schema.ts index aca1299..80107c2 100644 --- a/src/schemas/game-queue.schema.ts +++ b/src/schemas/game-queue.schema.ts @@ -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; diff --git a/src/schemas/guest.schema.ts b/src/schemas/guest.schema.ts index 9ad02c2..e5ba7b2 100644 --- a/src/schemas/guest.schema.ts +++ b/src/schemas/guest.schema.ts @@ -25,8 +25,6 @@ export class Guest { @Prop({ default: 10 }) prizeChance: number; @Prop({ default: 0 }) - prizesCount: number; - @Prop({ default: 0 }) validAnswers: number; @Prop({ default: 0 }) invalidAnswers: number; @@ -34,6 +32,8 @@ export class Guest { invalidAnswersInRow: number; @Prop({ default:0 }) rewardsReceived: number; + @Prop({ default: 0}) + penaltiesReceived: number; @Prop({ type: Map }) properties: Record; } diff --git a/src/schemas/question.schema.ts b/src/schemas/question.schema.ts index f3bc4b0..3cb1762 100644 --- a/src/schemas/question.schema.ts +++ b/src/schemas/question.schema.ts @@ -1,8 +1,16 @@ import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; import { Document } from 'mongoose'; + +export class QuestionAnswer { + user: number; + time: Date; + valid: boolean; +} + export type QuestionDocument = Question & Document; + @Schema() export class Question { @Prop() @@ -15,8 +23,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); diff --git a/src/schemas/versus.schema.ts b/src/schemas/versus.schema.ts new file mode 100644 index 0000000..49a8295 --- /dev/null +++ b/src/schemas/versus.schema.ts @@ -0,0 +1,15 @@ +import {Prop, Schema, SchemaFactory} from "@nestjs/mongoose"; +import {Document} from "mongoose"; + +@Schema() +export class Versus { + @Prop() + text: string; + @Prop({ default: false}) + completed: boolean; + @Prop() + description: string; +} + +export type VersusDocument = Versus & Document; +export const VersusSchema = SchemaFactory.createForClass(Versus); \ No newline at end of file diff --git a/src/shared/events.consts.ts b/src/shared/events.consts.ts deleted file mode 100644 index b07930e..0000000 --- a/src/shared/events.consts.ts +++ /dev/null @@ -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', -} diff --git a/src/shared/shared.module.ts b/src/shared/shared.module.ts index 4367356..7149884 100644 --- a/src/shared/shared.module.ts +++ b/src/shared/shared.module.ts @@ -8,6 +8,7 @@ import { ClientProxyFactory, Transport } from '@nestjs/microservices'; import * as process from "process"; import {ConfigModule} from "@nestjs/config"; import {CqrsModule} from "@nestjs/cqrs"; +import {FeatureflagService} from "../featureflag/featureflag.service"; @Global() @Module({ imports: [ @@ -17,7 +18,7 @@ import {CqrsModule} from "@nestjs/cqrs"; GameModule, MongooseModule.forFeature([{ name: Config.name, schema: ConfigSchema }]), ], - providers: [SharedService, { + providers: [SharedService,FeatureflagService, { provide: 'Telegram', useFactory: () => ClientProxyFactory.create({ @@ -31,7 +32,7 @@ import {CqrsModule} from "@nestjs/cqrs"; }, }), }], - exports: [SharedService, 'Telegram'], + exports: [SharedService, 'Telegram',FeatureflagService], }) export class SharedModule { constructor() { diff --git a/src/shared/shared.service.spec.ts b/src/shared/shared.service.spec.ts index 204dcb9..d87f25d 100644 --- a/src/shared/shared.service.spec.ts +++ b/src/shared/shared.service.spec.ts @@ -1,12 +1,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SharedService } from './shared.service'; +import {SocketGateway} from "../socket/socket.gateway"; +import {SocketGatewayMock} from "../mocks/socket-gateway.mock"; +import {getModelToken} from "@nestjs/mongoose"; +import {Config} from "../schemas/config.schema"; +import {Model} from "mongoose"; describe('SharedService', () => { let service: SharedService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [SharedService], + providers: [ + SharedService, + { provide: SocketGateway, useValue: SocketGatewayMock }, + { provide: getModelToken(Config.name), useValue: Model }, + ], }).compile(); service = module.get(SharedService); diff --git a/src/shared/shared.service.ts b/src/shared/shared.service.ts index 85955d6..8c044c8 100644 --- a/src/shared/shared.service.ts +++ b/src/shared/shared.service.ts @@ -1,4 +1,4 @@ -import { SocketGateway } from '../socket/socket.gateway'; +import {ClientNotificationType, SocketGateway} from '../socket/socket.gateway'; import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Config, ConfigDocument } from '../schemas/config.schema'; @@ -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, ) { } 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(event: ClientNotificationType, payload: T): void { this.logger.verbose(`Sending notification to client: ${event}, ${JSON.stringify(payload)}`); this.socketGateway.notifyAllClients(event, payload); } + } diff --git a/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts b/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts index 2650579..1eb26ad 100644 --- a/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts +++ b/src/socket/socket-handlers/commands-handlers/notify-card-played-command.handler.ts @@ -1,8 +1,9 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { NotifyCardOnScreenCommand } from '../../../game/commands/notify-card-on-screen-command'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; -import { Logger } from '@nestjs/common'; +import {CommandHandler, ICommandHandler} from '@nestjs/cqrs'; +import {NotifyCardOnScreenCommand} from '../../../game/commands/notify-card-on-screen-command'; +import {SharedService} from '../../../shared/shared.service'; +import {Logger} from '@nestjs/common'; +import {ICardPlayedSocketEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @CommandHandler(NotifyCardOnScreenCommand) export class NotifyCardPlayedCommandHandler @@ -12,8 +13,7 @@ export class NotifyCardPlayedCommandHandler } async execute(command: NotifyCardOnScreenCommand): Promise { this.logger.log(`Notify about card`); - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.CARD_PLAYED, { + this.sharedService.notifyAllClients(ClientNotificationType.CardPlayed, { telegramId: command.telegramId, card: command.card.description, name: command.card.name, diff --git a/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts b/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts index e0b3bc3..38668fd 100644 --- a/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts +++ b/src/socket/socket-handlers/commands-handlers/send-toast-command-handler.ts @@ -1,8 +1,9 @@ -import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; -import { SendToastCommand } from '../../../game/commands/send-toast.command'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {CommandHandler, ICommandHandler} from '@nestjs/cqrs'; +import {SendToastCommand} from '../../../game/commands/send-toast.command'; +import {SharedService} from '../../../shared/shared.service'; import {Logger} from "@nestjs/common"; +import {ISocketNotificationEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @CommandHandler(SendToastCommand) export class SendToastCommandHandler implements ICommandHandler { @@ -11,12 +12,9 @@ export class SendToastCommandHandler implements ICommandHandler(ClientNotificationType.Notification, { + text: command.text, + timeout: command.timeout, + }); } } diff --git a/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts b/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts index 84d8df3..d164717 100644 --- a/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/cards-set-changed-event.handler.ts @@ -1,17 +1,16 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { CardsSetChangedEvent } from '../../../game/events/cards-events/cards-set-changed.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {CardsSetChangedEvent} from '../../../game/events/cards-events/cards-set-changed.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IUserInfoMinimal} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(CardsSetChangedEvent) export class CardsSetChangedEventHandler implements IEventHandler { constructor(private sharedService: SharedService) {} - handle(event: CardsSetChangedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.CARDS_CHANGED_EVENT, - { telegramId: event.telegramId }, - ); + handle(event: CardsSetChangedEvent): void { + this.sharedService + .notifyAllClients(ClientNotificationType.CardsChanged, { telegramId: event.telegramId}) } } diff --git a/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts b/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts index f4ebbc5..44a2550 100644 --- a/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/cards-updated.event.handler.ts @@ -1,18 +1,17 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { CardsDealedEvent } from '../../../game/events/cards-dealed.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {CardsDealedEvent} from '../../../game/events/cards-dealed.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IUserCardChangedEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(CardsDealedEvent) export class CardsUpdatedEventHandler implements IEventHandler { constructor(private sharedService: SharedService) { } - handle(event: CardsDealedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.CARDS_CHANGED_EVENT, - { - telegramId: event.telegramId, - cards: event.cards, + handle(event: CardsDealedEvent): void { + this.sharedService.notifyAllClients(ClientNotificationType.CardsChanged, { + telegramId: event.telegramId, + cards: event.cards, }); } } diff --git a/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts b/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts index 0238771..4987d41 100644 --- a/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/score-changed-event.handler.ts @@ -1,7 +1,8 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { ScoreChangedEvent } from '../../../game/events/score-changed.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {ScoreChangedEvent} from '../../../game/events/score-changed.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IScoreChangedSocketEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(ScoreChangedEvent) export class SocketScoreChangedEventHandler @@ -9,12 +10,9 @@ export class SocketScoreChangedEventHandler constructor(private sharedService: SharedService) { } async handle(event: ScoreChangedEvent) { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.SCORE_CHANGED, - { - telegramId: event.telegramId, - newScore: event.newScore, - }, - ); + this.sharedService.notifyAllClients(ClientNotificationType.ScoreChanged, { + telegramId: event.telegramId, + newScore: event.newScore, + }); } } diff --git a/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts b/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts index ff2937b..e384714 100644 --- a/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/user-property-changed-event.handler.ts @@ -1,17 +1,19 @@ import {EventsHandler, IEventHandler} from "@nestjs/cqrs"; import {UserPropertyChangedEvent} from "../../../guests/event-handlers/user-property-changed.event"; import {SharedService} from "../../../shared/shared.service"; -import {SocketEvents} from "../../../shared/events.consts"; +import {IUserPropertyChangedEvent} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; @EventsHandler(UserPropertyChangedEvent) export class UserPropertyChangedEventHandler implements IEventHandler { constructor(private sharedService: SharedService) { } - handle(event: UserPropertyChangedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.USER_PROPERTY_CHANGED, - { user: event.user, property: event.property, value: event.propertyValue } - ); + handle(event: UserPropertyChangedEvent): void { + this.sharedService.notifyAllClients(ClientNotificationType.UserPropertyChanged, { + user: event.user, + property: event.property, + value: event.propertyValue + }); } } \ No newline at end of file diff --git a/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts b/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts index 9fc0eb8..ce88775 100644 --- a/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/user-registered.event.handler.ts @@ -1,17 +1,17 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { UserRegisteredEvent } from '../../../game/events/user-registered.event'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {UserRegisteredEvent} from '../../../game/events/user-registered.event'; +import {SharedService} from '../../../shared/shared.service'; +import {IUserBasicInfo} from "../../../Consts/types"; +import {ClientNotificationType} from "../../socket.gateway"; + @EventsHandler(UserRegisteredEvent) export class UserRegisteredEventHandler implements IEventHandler { constructor(private sharedService: SharedService) { } - handle(event: UserRegisteredEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.USER_ADDED, - { - telegramId: event.telegramId, - name: event.name, + handle(event: UserRegisteredEvent): void { + this.sharedService.notifyAllClients(ClientNotificationType.UserAdded, { + telegramId: event.telegramId, + name: event.name, }); } } diff --git a/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts b/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts index 1977df3..9a5dadc 100644 --- a/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/valid-answer-received-event.handler.ts @@ -1,7 +1,8 @@ -import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; -import { ValidAnswerReceivedEvent } from '../../../game/events/valid-answer.recieved'; -import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; +import {EventsHandler, IEventHandler} from '@nestjs/cqrs'; +import {ValidAnswerReceivedEvent} from '../../../game/events/valid-answer.recieved'; +import {SharedService} from '../../../shared/shared.service'; +import {ClientNotificationType} from "../../socket.gateway"; +import {IValidAnswerReceivedSocketEvent} from "../../../Consts/types"; @EventsHandler(ValidAnswerReceivedEvent) export class SocketValidAnswerReceivedEventHandler @@ -11,9 +12,12 @@ export class SocketValidAnswerReceivedEventHandler } handle(event: ValidAnswerReceivedEvent): any { - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.VALID_ANSWER_RECEIVED, - { telegramId: event.tId, validAnswer: event.validAnswer, note: event.extraDetails }, - ); + const notification : IValidAnswerReceivedSocketEvent = { + telegramId: event.tId, + validAnswer: event.validAnswer, + note: event.extraDetails + }; + this.sharedService + .notifyAllClients(ClientNotificationType.ValidAnswerReceived, notification); } } diff --git a/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts b/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts index f8dcc9b..034ce82 100644 --- a/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts +++ b/src/socket/socket-handlers/event-handlers/wrong-answer-received-event.handler.ts @@ -1,7 +1,6 @@ import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; import { WrongAnswerReceivedEvent } from '../../../game/events/wrong-answer-received.event'; import { SharedService } from '../../../shared/shared.service'; -import { SocketEvents } from '../../../shared/events.consts'; import { Logger } from '@nestjs/common'; @EventsHandler(WrongAnswerReceivedEvent) @@ -14,13 +13,5 @@ export class SocketWrongAnswerReceivedEventHandler constructor(private sharedService: SharedService) {} handle(event: WrongAnswerReceivedEvent): any { - - this.sharedService.sendSocketNotificationToAllClients( - SocketEvents.WRONG_ANSWER_RECEIVED, - { - telegramId: event.tId, - validAnswer: event.validAnswer, - }, - ); } } diff --git a/src/socket/socket.gateway.ts b/src/socket/socket.gateway.ts index d0562b2..0c3dc48 100644 --- a/src/socket/socket.gateway.ts +++ b/src/socket/socket.gateway.ts @@ -10,6 +10,28 @@ import { Server, Socket } from 'socket.io'; import { Injectable, Logger } from "@nestjs/common"; import { from, map, Observable } from 'rxjs'; + +export const enum ClientNotificationType { + StateChanged = 'state_changed', + PhotosUpdated = 'photos_updated', + ValidAnswerReceived = 'answer_received', + WrongAnswerReceived = 'wrong_answer_received', + UserAdded = 'user_added', + UserPropertyChanged = 'user_property_changed', + CardsChanged = 'cards_changed', + CardPlayed = 'card_played', + ScoreChanged = 'score_changed', + GameQueueItem = 'game_queue', + QueueCompleted = 'queue_completed', + GamePaused = 'game_paused', + GameResumed = 'game_resumed', + Notification = 'notification', + FeatureFlagChanged = 'feature_flag_changed', + BeginVersus = 'begin_versus', + EndVersus = 'end_versus', + QuestionChanged = 'question_changed' +} + @WebSocketGateway({ cors: true, transports: ['websocket']}) @Injectable() export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect{ @@ -47,7 +69,7 @@ export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect{ } - notifyAllClients(event: string, payload: any) { + notifyAllClients(event: ClientNotificationType, payload: any) { this.server.emit("events", { event, data: payload}); // this.logger.warn(`send notification to all clients ${event}`); // this.clients.forEach((c) => { diff --git a/src/state/state.controller.spec.ts b/src/state/state.controller.spec.ts index e108a36..eb944e9 100644 --- a/src/state/state.controller.spec.ts +++ b/src/state/state.controller.spec.ts @@ -1,5 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StateController } from './state.controller'; +import {State} from "../schemas/state.schema"; +import {StateService} from "./state.service"; +import {StateServiceMock} from "../mocks/state-service.mock"; +import {SharedService} from "../shared/shared.service"; +import {SharedServiceMock} from "../mocks/shared-service.mock"; +import {EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; +import {ClientProxyMock} from "../mocks/client-proxy.mock"; describe('StateController', () => { let controller: StateController; @@ -7,6 +15,12 @@ describe('StateController', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [StateController], + providers: [ + { provide: StateService, useValue: StateServiceMock }, + { provide: SharedService, useValue: SharedServiceMock }, + { provide: EventBus, useValue: EventbusMock }, + { provide: 'Telegram', useValue: ClientProxyMock }, + ] }).compile(); controller = module.get(StateController); diff --git a/src/state/state.controller.ts b/src/state/state.controller.ts index 7cf564c..dcd2e75 100644 --- a/src/state/state.controller.ts +++ b/src/state/state.controller.ts @@ -1,11 +1,13 @@ import {Body, Controller, Get, Inject, Logger, Param, Post} from '@nestjs/common'; -import { StateService } from './state.service'; -import { SharedService } from '../shared/shared.service'; -import { EventBus } from '@nestjs/cqrs'; -import { GameStartedEvent } from '../game/events/game-started.event'; +import {StateService} from './state.service'; +import {SharedService} from '../shared/shared.service'; +import {EventBus} from '@nestjs/cqrs'; +import {GameStartedEvent} from '../game/events/game-started.event'; import {ClientProxy} from "@nestjs/microservices"; import {CommandsConsts} from "../Consts/commands.consts"; import {MqtMessageModel} from "../messaging/models/mqt-message.model"; +import {ClientNotificationType} from "../socket/socket.gateway"; +import {IStateInfo} from "../Consts/types"; interface SetStateDTO { state: string; @@ -36,20 +38,21 @@ export class StateController { if (setStateDto.value === 'quiz') { this.eventBus.publish(new GameStartedEvent()); } else if(setStateDto.value === 'onboarding') { - this.telegramService.send( - { cmd: CommandsConsts.SetCommands }, - [ - { command: 'start', description: 'главное меню'}, - { command: 'cards', description: 'сыграть карту'} - ] - ).subscribe(() => { - this.logger.verbose('Bot commands updated'); - }); + // this.telegramService.send( + // { cmd: CommandsConsts.SetCommands }, + // [ + // { command: 'start', description: 'главное меню'}, + // { command: 'cards', description: 'сыграть карту'}, + // { command: 'question', description: 'вернутся к вопросу'} + // ] + // ).subscribe(() => { + // this.logger.verbose('Bot commands updated'); + // }); } else { this.logger.verbose('reset commands'); this.telegramService.emit({ cmd: CommandsConsts.ResetCommands }, {}); } - this.sharedService.sendSocketNotificationToAllClients('state_changed', res); + this.sharedService.notifyAllClients(ClientNotificationType.StateChanged, res); return res; } } diff --git a/src/state/state.service.spec.ts b/src/state/state.service.spec.ts index f2a1fd2..0bd4ac8 100644 --- a/src/state/state.service.spec.ts +++ b/src/state/state.service.spec.ts @@ -1,12 +1,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { StateService } from './state.service'; +import {getModelToken} from "@nestjs/mongoose"; +import {State} from "../schemas/state.schema"; +import {Model} from "mongoose"; +import {EventBus} from "@nestjs/cqrs"; +import {EventbusMock} from "../mocks/eventbus.mock"; describe('StateService', () => { let service: StateService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [StateService], + providers: [ + StateService, + { provide: getModelToken(State.name), useValue: Model }, + { provide: EventBus, useValue: EventbusMock }, + ], }).compile(); service = module.get(StateService); diff --git a/src/state/state.service.ts b/src/state/state.service.ts index 47dc65b..c6937c2 100644 --- a/src/state/state.service.ts +++ b/src/state/state.service.ts @@ -4,6 +4,8 @@ import { State, StateDocument } from '../schemas/state.schema'; import { Model } from 'mongoose'; import { EventBus } from '@nestjs/cqrs'; import { PrepareGameEvent } from '../game/events/prepare-game.event'; +import {IStateInfo} from "../Consts/types"; +import {StateChangedEvent} from "../game/events/state-changed.event"; interface StateDTO { name: string; @@ -30,13 +32,18 @@ export class StateService { return state; } - async setState(name: string, newValue: string) { + async setState(name: string, newValue: string): Promise { if (newValue === 'onboarding') { this.eventBus.publish(new PrepareGameEvent()); } + this.eventBus.publish(new StateChangedEvent(newValue)); + const stateEntity = await this.getState(name); stateEntity.value = newValue; await stateEntity.save(); - return stateEntity; + return { + state: stateEntity.state, + value: stateEntity.value, + } as IStateInfo } } diff --git a/src/voice/dicts/endgame.dict.ts b/src/voice/dicts/endgame.dict.ts new file mode 100644 index 0000000..d0cc39d --- /dev/null +++ b/src/voice/dicts/endgame.dict.ts @@ -0,0 +1,6 @@ + +export class EndgameDict { + static maxAmountOfInvalidQuestions = "За самое большое количество неправильных ответов, 2 балла получает %GenitiveCase%"; + static maxAmountOfRewards ="За самое большое количество полученных призов %GenitiveCase% получает минус два балла"; + static maxPenalties = "За самое большое количество наказаний %GenitiveCase% получает 3 балла"; +} \ No newline at end of file diff --git a/src/voice/voice.controller.spec.ts b/src/voice/voice.controller.spec.ts index 948bc66..ba35ee9 100644 --- a/src/voice/voice.controller.spec.ts +++ b/src/voice/voice.controller.spec.ts @@ -1,15 +1,36 @@ import { Test, TestingModule } from '@nestjs/testing'; import { VoiceController } from './voice.controller'; +import {VoiceService} from "./voice.service"; +import {VoiceServiceMock} from "../mocks/voice-service.mock"; +import {Config} from "../schemas/config.schema"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureflagServiceMock} from "../mocks/featureflag-service.mock"; describe('VoiceController', () => { let controller: VoiceController; + let voiceService: VoiceService; + let configService: ConfigService; + let featureflagService: FeatureflagService; beforeEach(async () => { + jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - controllers: [VoiceController], + controllers: [ + VoiceController, + ], + providers: [ + { provide: VoiceService, useValue: VoiceServiceMock }, + { provide: ConfigService, useValue: ConfigServiceMock }, + { provide: FeatureflagService, useValue: FeatureflagServiceMock }, + ] }).compile(); controller = module.get(VoiceController); + voiceService = module.get(VoiceService); + configService = module.get(ConfigService); + }); it('should be defined', () => { diff --git a/src/voice/voice.controller.ts b/src/voice/voice.controller.ts index 2c3cd3d..ce69d53 100644 --- a/src/voice/voice.controller.ts +++ b/src/voice/voice.controller.ts @@ -1,20 +1,31 @@ -import { Controller, Get, Header, NotFoundException, Query, StreamableFile } from '@nestjs/common'; +import {Controller, Get, Header, Logger, NotFoundException, Query, StreamableFile} from '@nestjs/common'; import { VoiceService } from './voice.service'; import { TtsRequestDto, TtsRequestWithVars } from './models/TtsRequestDto'; import { invalidPrefixDict } from './dicts/invalid-prefix.dict'; import { validPrefixDict } from './dicts/valid-prefix.dict'; import * as translit from 'latin-to-cyrillic'; -import {ConfigService} from "@nestjs/config"; +import {FeatureflagService} from "../featureflag/featureflag.service"; +import {FeatureFlagsConsts} from "../Consts/FeatureFlags.consts"; @Controller('voice') export class VoiceController { - constructor(private voiceService: VoiceService, private configService: ConfigService) {} + private voiceEnabled = false; + private logger = new Logger(VoiceController.name) + constructor(private voiceService: VoiceService,private featureFlagService: FeatureflagService) { + setInterval(() => { + this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => this.voiceEnabled = !r.state); + }, 10000); + this.featureFlagService.getFeatureFlag(FeatureFlagsConsts.DisableVoice).then(r => { + this.voiceEnabled = !r.state; + }); + } @Get('ssml') @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') async textToSpeechSSML(@Query() dto: TtsRequestDto) { - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { + this.logger.verbose(`[textToSpeechSSML] enter, FF state is: ${this.voiceEnabled}`); + if (this.voiceEnabled) { return new StreamableFile(await this.voiceService.textToFile(dto, true)); } else { return new NotFoundException('Voice disabled'); @@ -26,7 +37,7 @@ export class VoiceController { @Header('content-disposition', 'inline') async getText(@Query() dto: TtsRequestDto) { dto.text = translit(dto.text); - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { + if (this.voiceEnabled) { return new StreamableFile(await this.voiceService.textToFile(dto)); } else { return new NotFoundException('Voice disabled'); @@ -36,8 +47,7 @@ export class VoiceController { @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') async announceValid(@Query() dto: TtsRequestWithVars) { - console.log(this.configService.get('ENABLE_VOICE')); - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { + if (this.voiceEnabled) { const vars = JSON.parse(dto.vars); dto.text = this.voiceService.buildTemplate(dto, vars, validPrefixDict); return new StreamableFile(await this.voiceService.textToFile(dto)); @@ -50,7 +60,7 @@ export class VoiceController { @Header('content-type', 'audio/opus') @Header('content-disposition', 'inline') async announceInvalid(@Query() dto: TtsRequestWithVars) { - if (Boolean(this.configService.get('ENABLE_VOICE')) === true) { + if (this.voiceEnabled) { const vars = JSON.parse(dto.vars); dto.text = this.voiceService.buildTemplate(dto, vars, invalidPrefixDict); return new StreamableFile(await this.voiceService.textToFile(dto)); diff --git a/src/voice/voice.service.spec.ts b/src/voice/voice.service.spec.ts index 76b1a6b..c9d5f1b 100644 --- a/src/voice/voice.service.spec.ts +++ b/src/voice/voice.service.spec.ts @@ -1,12 +1,20 @@ import { Test, TestingModule } from '@nestjs/testing'; import { VoiceService } from './voice.service'; +import {HttpService} from "@nestjs/axios"; +import {HttpServiceMock} from "../mocks/httpservice.mock"; +import {ConfigService} from "@nestjs/config"; +import {ConfigServiceMock} from "../mocks/config-service.mock"; describe('VoiceService', () => { let service: VoiceService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [VoiceService], + providers: [ + VoiceService, + { provide: HttpService, useValue: HttpServiceMock }, + { provide: ConfigService, useValue: ConfigServiceMock } + ], }).compile(); service = module.get(VoiceService); diff --git a/src/voice/voice.service.ts b/src/voice/voice.service.ts index 78cc2dd..0a662c5 100644 --- a/src/voice/voice.service.ts +++ b/src/voice/voice.service.ts @@ -7,6 +7,7 @@ import { AxiosRequestConfig } from 'axios'; import * as translit from 'latin-to-cyrillic'; import { TGD_Config } from 'app.config'; import {ConfigService} from "@nestjs/config"; +import {FeatureflagService} from "../featureflag/featureflag.service"; @Injectable() export class VoiceService { private apiUrl = 'https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize'; @@ -29,7 +30,6 @@ export class VoiceService { } this.logger.verbose(`Result is: ${template}`); // eslint-disable-next-line @typescript-eslint/no-var-requires - template = translit(template); return template; }