Initial commit: Своя Игра - multiplayer quiz game
18
server/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3001
|
||||
|
||||
# Start server
|
||||
CMD ["npm", "start"]
|
||||
1731
server/package-lock.json
generated
Normal file
25
server/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "svoya-igra-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend server for Svoya Igra",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "nodemon src/index.js",
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"socket.io": "^4.7.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"mongoose": "^8.0.3",
|
||||
"nanoid": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
BIN
server/public/audio/aeropotri_cut.mp3
Normal file
BIN
server/public/audio/aeropotri_reverse.mp3
Normal file
BIN
server/public/audio/dasha_playlist.mp3
Normal file
BIN
server/public/audio/derji_cut.mp3
Normal file
BIN
server/public/audio/derji_reverse.mp3
Normal file
BIN
server/public/audio/grim_answer.mp3
Normal file
BIN
server/public/audio/grim_cut.mp3
Normal file
BIN
server/public/audio/kolambia_answer.mp3
Normal file
BIN
server/public/audio/kolambia_cut.mp3
Normal file
BIN
server/public/audio/korni_cut.mp3
Normal file
BIN
server/public/audio/korni_reverse.mp3
Normal file
BIN
server/public/audio/ksusha_playlist.mp3
Normal file
BIN
server/public/audio/kvartali_cut.mp3
Normal file
BIN
server/public/audio/kvartali_reverse.mp3
Normal file
BIN
server/public/audio/narkotik_answer.mp3
Normal file
BIN
server/public/audio/narkotik_cut.mp3
Normal file
BIN
server/public/audio/prichin_answer.mp3
Normal file
BIN
server/public/audio/prichin_cut.mp3
Normal file
BIN
server/public/audio/shubina_playlist.mp3
Normal file
BIN
server/public/audio/sveta_playlist.mp3
Normal file
BIN
server/public/audio/vera_answer.mp3
Normal file
BIN
server/public/audio/vera_cut.mp3
Normal file
BIN
server/public/audio/znaesh_cut.mp3
Normal file
BIN
server/public/audio/znaesh_reverse.mp3
Normal file
BIN
server/public/images/danya_4.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
server/public/images/danya_5.jpg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
server/public/images/kto_2.jpeg
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
server/public/images/kto_4.jpg
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
server/public/images/kto_5.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
server/public/images/poezdki_5.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
server/public/images/professii_2.jpg
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
server/public/images/professii_3.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
server/public/images/professii_5.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
server/public/images/sumerki_1.jpg
Normal file
|
After Width: | Height: | Size: 347 KiB |
BIN
server/public/images/sumerki_2.jpg
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
server/public/images/sumerki_3.jpg
Normal file
|
After Width: | Height: | Size: 506 KiB |
BIN
server/public/images/sumerki_4.jpg
Normal file
|
After Width: | Height: | Size: 883 KiB |
BIN
server/public/images/sumerki_5.jpg
Normal file
|
After Width: | Height: | Size: 446 KiB |
BIN
server/public/images/univer_5.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
server/public/images/vstrechi_3.jpg
Normal file
|
After Width: | Height: | Size: 398 KiB |
BIN
server/public/video/univer_4.mp4
Normal file
BIN
server/public/video/vstrechi_4.mp4
Normal file
396
server/src/data/Untitled-1.groovy
Normal file
@@ -0,0 +1,396 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"id": "reverse",
|
||||
"name": "Песни наоборот",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "audio",
|
||||
"question": "/audio/aeropotri_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Агутин - Аэропорты",
|
||||
"answerMedia": "/audio/aeropotri_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "audio",
|
||||
"question": "/audio/derji_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Дима Билан - Держи",
|
||||
"answerMedia": "/audio/derji_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/kvartali_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Звери - Районы кварталы",
|
||||
"answerMedia": "/audio/kvartali_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "audio",
|
||||
"question": "/audio/korni_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Корни - Ты узнаешь ее",
|
||||
"answerMedia": "/audio/korni_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "audio",
|
||||
"question": "/audio/znaesh_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Максим - Знаешь ли ты",
|
||||
"answerMedia": "/audio/znaesh_cut.mp3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "prodolji",
|
||||
"name": "Продолжи песню",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "audio",
|
||||
"question": "/audio/kolambia_cut.mp3",
|
||||
"questionText": "Что поется дальше?",
|
||||
"answer": "Мы увидим сзади",
|
||||
"answerMedia": "/audio/kolambia_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "audio",
|
||||
"question": "/audio/vera_cut.mp3",
|
||||
"questionText": "Слова из чего? (продолжи)",
|
||||
"answer": "Из ярких лампочек",
|
||||
"answerMedia": "/audio/vera_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/prichin_cut.mp3",
|
||||
"questionText": "Третья причина - это?",
|
||||
"answer": "Все твои слова",
|
||||
"answerMedia": "/audio/prichin_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "audio",
|
||||
"question": "/audio/grim_cut.mp3",
|
||||
"questionText": "Что дальше?",
|
||||
"answer": "Силы не жалей",
|
||||
"answerMedia": "/audio/grim_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "audio",
|
||||
"question": "/audio/narkotik_cut.mp3",
|
||||
"questionText": "Что дальше?",
|
||||
"answer": "Что любят гламур",
|
||||
"answerMedia": "/audio/narkotik_answer.mp3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "danya",
|
||||
"name": "Вопросы от Дани",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "Из-за чего Даня обиделся на Юлю Шубину на даче летом?",
|
||||
"answer": "Она забыла его имя"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "Баскетбол, танцы, легкая атлетика, настольный теннис. Чем из этого не занимался Даня?",
|
||||
"answer": "Настольный теннис"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "У какого героя мультфильма в голове были опилки?",
|
||||
"answer": "Винни-пух"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "image",
|
||||
"question": "/images/danya_4.jpg",
|
||||
"questionText": "Какой патронус у МакГонагалл?",
|
||||
"answer": "Кошка"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/danya_5.jpg",
|
||||
"questionText": "Сколько это будет в десятичной системе?",
|
||||
"answer": "25"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sumerki",
|
||||
"name": "Сумерки",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_1.jpg",
|
||||
"questionText": "Как зовут главную героиню?",
|
||||
"answer": "Белла"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_2.jpg",
|
||||
"questionText": "Как зовут вампира, в которого влюбляется главная героиня?",
|
||||
"answer": "Эдвард Каллен"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_3.jpg",
|
||||
"questionText": "В какую игру играет семья Калленов во время грозы?",
|
||||
"answer": "Бейсбол"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_4.jpg",
|
||||
"questionText": "Куда Джеймс заманивает Беллу для финальной схватки?",
|
||||
"answer": "В балетную студию в Фениксе"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_5.jpg",
|
||||
"questionText": "Какую ложь придумывают, чтобы объяснить травмы Беллы после нападения Джеймса?",
|
||||
"answer": "Что она упала с лестницы и вылетела через окно"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "playlist",
|
||||
"name": "Чей плейлист",
|
||||
"questions": [
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/ksusha_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Ксюша"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/dasha_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Даша"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/sveta_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Света"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/shubina_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Юля Шубина"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/mosina_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Юля Мосина (Слесарчук)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "poezdki",
|
||||
"name": "Наши поездки",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "Сколько шагов мы прошли в Пскове? (Диапазон в 2 тысячи шагов)",
|
||||
"answer": "25 тысяч шагов"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "facti",
|
||||
"name": "Факты о нас",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "Сколько лет Ксюша носила брекеты? (Диапазон в 2 месяца)",
|
||||
"answer": "1 год и 7 месяцев (19 месяцев)"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "univer",
|
||||
"name": "Универ",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "В каком году основан наш любимый универ?",
|
||||
"answer": "1832"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stroyka",
|
||||
"name": "Вопросы о стройке",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "Как расшифровывается КГИОП?",
|
||||
"answer": "Комитет по государственному контролю, использованию и охране памятников истории и культуры Санкт-Петербурга"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "vstrechi",
|
||||
"name": "Наши встречи",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "Когда мы толпой ходили на БухайТанцуй? Месяц и год",
|
||||
"answer": "Август 2023 года"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
407
server/src/data/questions.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"id": "reverse",
|
||||
"name": "Песни наоборот",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "audio",
|
||||
"question": "/audio/aeropotri_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Агутин - Аэропорты",
|
||||
"answerMedia": "/audio/aeropotri_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "audio",
|
||||
"question": "/audio/derji_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Дима Билан - Держи",
|
||||
"answerMedia": "/audio/derji_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/kvartali_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Звери - Районы кварталы",
|
||||
"answerMedia": "/audio/kvartali_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "audio",
|
||||
"question": "/audio/korni_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Корни - Ты узнаешь ее",
|
||||
"answerMedia": "/audio/korni_cut.mp3"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "audio",
|
||||
"question": "/audio/znaesh_reverse.mp3",
|
||||
"questionText": "Что это за песня?",
|
||||
"answer": "Максим - Знаешь ли ты",
|
||||
"answerMedia": "/audio/znaesh_cut.mp3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "prodolji",
|
||||
"name": "Продолжи песню",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "audio",
|
||||
"question": "/audio/kolambia_cut.mp3",
|
||||
"questionText": "Что поется дальше?",
|
||||
"answer": "Мы увидим сзади",
|
||||
"answerMedia": "/audio/kolambia_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "audio",
|
||||
"question": "/audio/vera_cut.mp3",
|
||||
"questionText": "Слова из чего? (продолжи)",
|
||||
"answer": "Из ярких лампочек",
|
||||
"answerMedia": "/audio/vera_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/prichin_cut.mp3",
|
||||
"questionText": "Третья причина - это?",
|
||||
"answer": "Все твои слова",
|
||||
"answerMedia": "/audio/prichin_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "audio",
|
||||
"question": "/audio/grim_cut.mp3",
|
||||
"questionText": "Что дальше?",
|
||||
"answer": "Силы не жалей",
|
||||
"answerMedia": "/audio/grim_answer.mp3"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "audio",
|
||||
"question": "/audio/narkotik_cut.mp3",
|
||||
"questionText": "Что дальше?",
|
||||
"answer": "Что любят гламур",
|
||||
"answerMedia": "/audio/narkotik_answer.mp3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "danya",
|
||||
"name": "Вопросы от Дани",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "Из-за чего Даня обиделся на Юлю Шубину на даче летом?",
|
||||
"answer": "Она забыла его имя"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "Баскетбол, танцы, легкая атлетика, настольный теннис. Чем из этого не занимался Даня?",
|
||||
"answer": "Настольный теннис"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "У какого героя мультфильма в голове были опилки?",
|
||||
"answer": "Винни-пух"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "image",
|
||||
"question": "/images/danya_4.jpg",
|
||||
"questionText": "Какой патронус у МакГонагалл?",
|
||||
"answer": "Кошка"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/danya_5.jpg",
|
||||
"questionText": "Сколько это будет в десятичной системе?",
|
||||
"answer": "25"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "sumerki",
|
||||
"name": "Сумерки",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_1.jpg",
|
||||
"questionText": "Как зовут главную героиню?",
|
||||
"answer": "Белла"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_2.jpg",
|
||||
"questionText": "Как зовут вампира, в которого влюбляется главная героиня?",
|
||||
"answer": "Эдвард Каллен"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_3.jpg",
|
||||
"questionText": "В какую игру играет семья Калленов во время грозы?",
|
||||
"answer": "Бейсбол"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_4.jpg",
|
||||
"questionText": "Куда Джеймс заманивает Беллу для финальной схватки?",
|
||||
"answer": "В балетную студию в Фениксе"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/sumerki_5.jpg",
|
||||
"questionText": "Какую ложь придумывают, чтобы объяснить травмы Беллы после нападения Джеймса?",
|
||||
"answer": "Что она упала с лестницы и вылетела через окно"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "playlist",
|
||||
"name": "Чей плейлист",
|
||||
"questions": [
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/ksusha_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Ксюша (Гарик Сукачёв - Ботик)"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/dasha_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Даша (Skinny Kaleo)"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/sveta_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Света (30 Seconds to Mars - The Kill)"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/shubina_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Юля Шубина (Второй Ка - Тени от пальм)"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "audio",
|
||||
"question": "/audio/mosina_playlist.mp3",
|
||||
"questionText": "Кто выбрал эту песню?",
|
||||
"answer": "Юля Мосина (Слесарчук)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "poezdki",
|
||||
"name": "Наши поездки",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "Сколько шагов мы прошли в Пскове? (Диапазон в 2 тысячи шагов)",
|
||||
"answer": "25 тысяч шагов"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "Как называются керамические плитки, которые украшают храмы Ярославля?",
|
||||
"answer": "Изразцы"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "Как звали проводника в поезде Санкт-Петербург -> Ярославль?",
|
||||
"answer": "Альберт"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "Эта знаменитая княгиня, предположительно родом из Пскова, первая на Руси приняла христианство",
|
||||
"answer": "Княгиня Ольга"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/poezdki_5.jpg",
|
||||
"questionText": "Сколько лет исполнилось бы самой первой Церкви Благовещения на Городище в Великом Новгороде?",
|
||||
"answer": "922"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "facti",
|
||||
"name": "Факты о нас",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "Сколько времени жила Юля Мосина у Даши Неберовой на карантине?",
|
||||
"answer": "2 месяца"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "image",
|
||||
"question": "/images/kto_2.jpeg",
|
||||
"questionText": "Кто на фото?",
|
||||
"answer": "Даша Неберова"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "Сколько лет Ксюша носила брекеты? (Диапазон в 2 месяца)",
|
||||
"answer": "1 год и 7 месяцев (19 месяцев)"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "image",
|
||||
"question": "/images/kto_4.jpg",
|
||||
"questionText": "С чем Света больше всего любила играть в детстве?",
|
||||
"answer": "Животные игрушки"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/kto_5.jpg",
|
||||
"questionText": "Что с ней случилось?",
|
||||
"answer": "Аллергия на текилу"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "univer",
|
||||
"name": "Универ",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "В каком году основан наш любимый универ?",
|
||||
"answer": "1832"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "По мнению Барышниковой Тамары Николаевны (препод по экологии), какой самый экологически грязный город России?",
|
||||
"answer": "Норильск"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "text",
|
||||
"question": "Назовите полное название предмета, на котором мы изучали AutoCAD",
|
||||
"answer": "Информационные технологии графического проектирования"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "image",
|
||||
"question": "/images/univer_5.jpg",
|
||||
"questionText": "Кто это (какой предмет вел)?",
|
||||
"answer": "Препод по философии (Гурьев Евгений Павлович)"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "video",
|
||||
"question": "/video/univer_4.mp4",
|
||||
"questionText": "Чей это голос (аудиозапись)?",
|
||||
"answer": "Разумнова Елена Альбертовна"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "stroyka",
|
||||
"name": "Профессия",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "",
|
||||
"answer": ""
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "image",
|
||||
"question": "/images/professii_3.jpg",
|
||||
"questionText": "Какой кондиционер изображен на картинке?",
|
||||
"answer": "Канальный"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "image",
|
||||
"question": "/images/professii_2.jpg",
|
||||
"questionText": "Кто из глав нашей страны по своей основной профессии был строителем?",
|
||||
"answer": "Б.Н. Ельцин"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "text",
|
||||
"question": "Как расшифровывается КГИОП?",
|
||||
"answer": "Комитет по государственному контролю, использованию и охране памятников истории и культуры Санкт-Петербурга"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "image",
|
||||
"question": "/images/professii_5.jpg",
|
||||
"questionText": "Выберите верный вариант армирования конструкции",
|
||||
"answer": "4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "vstrechi",
|
||||
"name": "Наши встречи",
|
||||
"questions": [
|
||||
{
|
||||
"points": 100,
|
||||
"type": "text",
|
||||
"question": "Какой предмет личной гигиены только одна из нас взяла на девичник Светы?",
|
||||
"answer": "Зубная паста"
|
||||
},
|
||||
{
|
||||
"points": 200,
|
||||
"type": "text",
|
||||
"question": "Когда мы толпой ходили на Бухай-Танцуй? Месяц и год",
|
||||
"answer": "Август 2023 года"
|
||||
},
|
||||
{
|
||||
"points": 300,
|
||||
"type": "image",
|
||||
"question": "/images/vstrechi_3.jpg",
|
||||
"questionText": "Что скрыто за черным квадратом и находится на столе перед Ксюшей?",
|
||||
"answer": "Учебник по металлическим конструкциям"
|
||||
},
|
||||
{
|
||||
"points": 400,
|
||||
"type": "video",
|
||||
"question": "/video/vstrechi_4.mp4",
|
||||
"questionText": "Продолжите крылатую фразу, которую сказала в тот вечер Даша. Для чего велась съемка?",
|
||||
"answer": "Для смысла"
|
||||
},
|
||||
{
|
||||
"points": 500,
|
||||
"type": "text",
|
||||
"question": "Как остановить время при встрече?",
|
||||
"answer": "Вынуть батарейки из Дашиных часов в гостиной"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
30
server/src/data/Сумерки.txt
Normal file
@@ -0,0 +1,30 @@
|
||||
Песни наоборот +
|
||||
Продолжи песню +
|
||||
Вопросы от Дани +
|
||||
Сумерки +
|
||||
|
||||
Угадай из чьего плейлиста
|
||||
1 +
|
||||
2 +
|
||||
3 +
|
||||
4 +
|
||||
5
|
||||
|
||||
Поездки +
|
||||
Факты о нас +
|
||||
Универ +
|
||||
|
||||
Профессии
|
||||
1
|
||||
2 +
|
||||
3 +
|
||||
4 +
|
||||
5 +
|
||||
|
||||
Наши встречи +
|
||||
|
||||
Юля М
|
||||
- Плейлист
|
||||
|
||||
Юля Ш
|
||||
- Профессии
|
||||
68
server/src/index.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import mongoose from 'mongoose';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: process.env.CLIENT_URL || 'http://localhost:5173',
|
||||
methods: ['GET', 'POST']
|
||||
}
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Статичные файлы для медиа
|
||||
app.use('/images', express.static(join(__dirname, '../public/images')));
|
||||
app.use('/audio', express.static(join(__dirname, '../public/audio')));
|
||||
app.use('/video', express.static(join(__dirname, '../public/video')));
|
||||
|
||||
// MongoDB connection (optional - можно использовать JSON файлы)
|
||||
const connectDB = async () => {
|
||||
if (process.env.MONGODB_URI) {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGODB_URI);
|
||||
console.log('✅ MongoDB connected');
|
||||
} catch (error) {
|
||||
console.log('⚠️ MongoDB not connected, using JSON storage');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ No MongoDB URI, using JSON storage');
|
||||
}
|
||||
};
|
||||
|
||||
// Хранилище активных игр в памяти
|
||||
const activeGames = new Map();
|
||||
|
||||
// Импорт роутов и сокет обработчиков
|
||||
import questionsRouter from './routes/questions.js';
|
||||
import gamesRouter from './routes/games.js';
|
||||
import { setupSocketHandlers } from './socket/handlers.js';
|
||||
|
||||
app.use('/api/questions', questionsRouter);
|
||||
app.use('/api/games', gamesRouter);
|
||||
|
||||
// WebSocket обработчики
|
||||
setupSocketHandlers(io, activeGames);
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
connectDB().then(() => {
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on port ${PORT}`);
|
||||
});
|
||||
});
|
||||
|
||||
export { io, activeGames };
|
||||
54
server/src/models/Game.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const gameSchema = new mongoose.Schema({
|
||||
gameCode: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
hostId: String,
|
||||
hostName: String,
|
||||
teams: [{
|
||||
id: String,
|
||||
name: String,
|
||||
score: { type: Number, default: 0 },
|
||||
players: [{
|
||||
id: String
|
||||
}]
|
||||
}],
|
||||
currentRound: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
selectedCategories: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: []
|
||||
},
|
||||
usedCategoryIds: {
|
||||
type: [String],
|
||||
default: []
|
||||
},
|
||||
usedQuestions: [{
|
||||
category: String,
|
||||
points: Number
|
||||
}],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['waiting', 'playing', 'finished'],
|
||||
default: 'waiting'
|
||||
},
|
||||
currentQuestion: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: null
|
||||
},
|
||||
answeringTeam: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
const Game = mongoose.model('Game', gameSchema);
|
||||
|
||||
export default Game;
|
||||
33
server/src/models/Question.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const questionSchema = new mongoose.Schema({
|
||||
category: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
points: {
|
||||
type: Number,
|
||||
required: true,
|
||||
enum: [100, 200, 300, 400, 500, 200, 400, 600, 800, 1000]
|
||||
},
|
||||
question: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
answer: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
round: {
|
||||
type: Number,
|
||||
required: true,
|
||||
enum: [1, 2],
|
||||
default: 1
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
const Question = mongoose.model('Question', questionSchema);
|
||||
|
||||
export default Question;
|
||||
46
server/src/routes/games.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import express from 'express';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Создать новую игру
|
||||
router.post('/create', (req, res) => {
|
||||
try {
|
||||
const gameCode = nanoid(6).toUpperCase();
|
||||
const { teamName } = req.body;
|
||||
|
||||
const game = {
|
||||
gameCode,
|
||||
teams: [{
|
||||
name: teamName,
|
||||
score: 0,
|
||||
players: []
|
||||
}],
|
||||
currentRound: 1,
|
||||
usedQuestions: [],
|
||||
status: 'waiting',
|
||||
createdAt: new Date()
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
gameCode,
|
||||
game
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Проверить существование игры
|
||||
router.get('/:gameCode', (req, res) => {
|
||||
try {
|
||||
const { gameCode } = req.params;
|
||||
// Проверка будет в Socket.io, т.к. игры хранятся в памяти
|
||||
res.json({ exists: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
31
server/src/routes/questions.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import express from 'express';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const router = express.Router();
|
||||
const questionsPath = join(__dirname, '../data/questions.json');
|
||||
|
||||
// Helper функция для чтения вопросов
|
||||
const getQuestions = () => {
|
||||
if (!existsSync(questionsPath)) {
|
||||
return { categories: [] };
|
||||
}
|
||||
const data = readFileSync(questionsPath, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
};
|
||||
|
||||
// GET все категории и вопросы
|
||||
router.get('/', (req, res) => {
|
||||
try {
|
||||
const questions = getQuestions();
|
||||
res.json(questions);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
762
server/src/socket/handlers.js
Normal file
@@ -0,0 +1,762 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import Game from '../models/Game.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Функция для выбора случайных 5 категорий (из всех доступных)
|
||||
const selectRandomCategories = (allCategories, usedCategoryIds) => {
|
||||
const availableCategories = allCategories.filter(
|
||||
cat => !usedCategoryIds.includes(cat.id)
|
||||
);
|
||||
|
||||
// Перемешиваем и берем первые 5
|
||||
const shuffled = availableCategories.sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, 5);
|
||||
};
|
||||
|
||||
// Функция проверки, закончилась ли игра
|
||||
const checkGameFinished = (game, allCategories) => {
|
||||
// Проверяем, остались ли доступные категории
|
||||
const availableCategories = allCategories.filter(
|
||||
cat => !game.usedCategoryIds.includes(cat.id)
|
||||
);
|
||||
|
||||
// Проверяем, все ли вопросы текущего раунда использованы
|
||||
const totalQuestionsInRound = game.selectedCategories.reduce(
|
||||
(sum, cat) => sum + cat.questions.length,
|
||||
0
|
||||
);
|
||||
const allQuestionsUsed = game.usedQuestions.length >= totalQuestionsInRound;
|
||||
|
||||
// Игра заканчивается если:
|
||||
// 1. Все вопросы текущего раунда использованы
|
||||
// 2. И нет больше доступных категорий для следующего раунда
|
||||
return allQuestionsUsed && availableCategories.length === 0;
|
||||
};
|
||||
|
||||
// Функция для отправки обновления игры (без timerInterval)
|
||||
const sendGameUpdate = (io, gameCode, game) => {
|
||||
const { timerInterval, ...gameToSend } = game;
|
||||
io.to(gameCode).emit('game-updated', gameToSend);
|
||||
};
|
||||
|
||||
export const setupSocketHandlers = (io, activeGames) => {
|
||||
io.on('connection', (socket) => {
|
||||
console.log('👤 Client connected:', socket.id);
|
||||
|
||||
// Создание игры
|
||||
socket.on('create-game', async ({ hostName }) => {
|
||||
const gameCode = nanoid(6).toUpperCase();
|
||||
|
||||
// Загружаем все категории
|
||||
const questionsPath = join(__dirname, '../data/questions.json');
|
||||
const questionsData = JSON.parse(readFileSync(questionsPath, 'utf-8'));
|
||||
|
||||
console.log(`📚 Total categories available: ${questionsData.categories.length}`);
|
||||
|
||||
// Выбираем 5 случайных категорий
|
||||
const selectedCategories = selectRandomCategories(questionsData.categories, []);
|
||||
|
||||
console.log(`🎲 Selected ${selectedCategories.length} categories for game ${gameCode}`);
|
||||
|
||||
const gameData = {
|
||||
gameCode,
|
||||
hostId: socket.id,
|
||||
hostName: hostName,
|
||||
teams: [],
|
||||
currentRound: 1,
|
||||
selectedCategories: selectedCategories,
|
||||
usedCategoryIds: selectedCategories.map(cat => cat.id),
|
||||
usedQuestions: [],
|
||||
status: 'waiting',
|
||||
currentQuestion: null,
|
||||
answeringTeam: null,
|
||||
timer: null
|
||||
};
|
||||
|
||||
// Сохраняем в MongoDB
|
||||
try {
|
||||
await Game.create(gameData);
|
||||
console.log(`💾 Game ${gameCode} saved to DB`);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error saving to DB:', error.message);
|
||||
}
|
||||
|
||||
activeGames.set(gameCode, gameData);
|
||||
socket.join(gameCode);
|
||||
|
||||
socket.emit('game-created', { gameCode, game: gameData });
|
||||
console.log(`🎮 Game created: ${gameCode} by host ${hostName}`);
|
||||
});
|
||||
|
||||
// Ведущий присоединяется к игре
|
||||
socket.on('host-join-game', async ({ gameCode }) => {
|
||||
let game = activeGames.get(gameCode);
|
||||
|
||||
// Если игры нет в памяти, пытаемся загрузить из БД
|
||||
if (!game) {
|
||||
try {
|
||||
const dbGame = await Game.findOne({ gameCode }).lean();
|
||||
if (dbGame) {
|
||||
game = { ...dbGame, timer: null, timerInterval: null };
|
||||
activeGames.set(gameCode, game);
|
||||
console.log(`📥 Game ${gameCode} loaded from DB`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error loading from DB:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!game) {
|
||||
socket.emit('error', { message: 'Игра не найдена' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем hostId на текущий socket
|
||||
game.hostId = socket.id;
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate({ gameCode }, { hostId: socket.id });
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating DB:', error.message);
|
||||
}
|
||||
|
||||
socket.join(gameCode);
|
||||
socket.emit('host-game-loaded', game);
|
||||
|
||||
console.log(`🎤 Host joined game ${gameCode}`);
|
||||
});
|
||||
|
||||
// Присоединение к игре
|
||||
socket.on('join-game', async ({ gameCode, teamName }) => {
|
||||
let game = activeGames.get(gameCode);
|
||||
|
||||
// Если игры нет в памяти, пытаемся загрузить из БД
|
||||
if (!game) {
|
||||
try {
|
||||
const dbGame = await Game.findOne({ gameCode }).lean();
|
||||
if (dbGame) {
|
||||
game = { ...dbGame, timer: null, timerInterval: null };
|
||||
activeGames.set(gameCode, game);
|
||||
console.log(`📥 Game ${gameCode} loaded from DB for join`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error loading from DB:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!game) {
|
||||
socket.emit('error', { message: 'Игра не найдена' });
|
||||
return;
|
||||
}
|
||||
|
||||
let team = game.teams.find(t => t.name === teamName);
|
||||
|
||||
if (!team) {
|
||||
// Создаем новую команду
|
||||
team = {
|
||||
id: nanoid(),
|
||||
name: teamName,
|
||||
score: 0,
|
||||
players: []
|
||||
};
|
||||
game.teams.push(team);
|
||||
}
|
||||
|
||||
// Добавляем socket id в команду (без имени игрока)
|
||||
team.players.push({ id: socket.id });
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate({ gameCode }, { teams: game.teams });
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating teams in DB:', error.message);
|
||||
}
|
||||
|
||||
socket.join(gameCode);
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
|
||||
console.log(`👥 Team ${teamName} joined game ${gameCode}`);
|
||||
});
|
||||
|
||||
// Начать игру
|
||||
socket.on('start-game', async ({ gameCode }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
if (game) {
|
||||
game.status = 'playing';
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate({ gameCode }, { status: 'playing' });
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating status in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('game-started', game);
|
||||
}
|
||||
});
|
||||
|
||||
// Выбор вопроса
|
||||
socket.on('select-question', ({ gameCode, category, points }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
// Находим категорию
|
||||
const categoryData = game.selectedCategories.find(cat => cat.name === category);
|
||||
|
||||
// Подсчитываем, сколько вопросов с такими баллами уже использовано в этой категории
|
||||
const usedCount = game.usedQuestions.filter(
|
||||
q => q.category === category && q.points === points
|
||||
).length;
|
||||
|
||||
// Находим следующий неиспользованный вопрос с такими баллами (по порядку)
|
||||
const questionsWithPoints = categoryData?.questions
|
||||
.map((q, idx) => ({ ...q, originalIndex: idx }))
|
||||
.filter(q => q.points === points);
|
||||
|
||||
const questionIndex = questionsWithPoints[usedCount]?.originalIndex;
|
||||
|
||||
console.log(`📊 Select question: category="${category}", points=${points}, usedCount=${usedCount}, questionIndex=${questionIndex}`);
|
||||
console.log(`📊 Questions with these points:`, questionsWithPoints.map(q => q.originalIndex));
|
||||
|
||||
if (questionIndex === undefined) {
|
||||
console.log('❌ No available question found');
|
||||
socket.emit('error', { message: 'Этот вопрос уже был использован' });
|
||||
return;
|
||||
}
|
||||
|
||||
game.currentQuestion = { category, points, questionIndex };
|
||||
console.log(`✅ Selected question:`, game.currentQuestion);
|
||||
game.answeringTeam = null;
|
||||
game.answeredTeams = []; // Список команд, которые уже отвечали на этот вопрос
|
||||
|
||||
// Получаем тип вопроса
|
||||
const questionData = categoryData?.questions[questionIndex];
|
||||
const questionType = questionData?.type || 'text';
|
||||
|
||||
game.timer = 60; // 60 секунд на ответ
|
||||
game.timerStarted = false; // Флаг, запущен ли таймер
|
||||
|
||||
io.to(gameCode).emit('question-selected', {
|
||||
category,
|
||||
points,
|
||||
questionIndex,
|
||||
questionType,
|
||||
timer: game.timer
|
||||
});
|
||||
|
||||
// Для текстовых и картинок запускаем таймер сразу
|
||||
// Для аудио и видео - ждем события 'media-ended' от клиента
|
||||
if (questionType === 'text' || questionType === 'image') {
|
||||
console.log(`⏱️ Starting timer immediately for ${questionType} question`);
|
||||
game.timerStarted = true;
|
||||
|
||||
const timerInterval = setInterval(() => {
|
||||
game.timer--;
|
||||
io.to(gameCode).emit('timer-update', { timer: game.timer });
|
||||
|
||||
if (game.timer <= 0) {
|
||||
clearInterval(timerInterval);
|
||||
io.to(gameCode).emit('time-up');
|
||||
game.currentQuestion = null;
|
||||
game.answeringTeam = null;
|
||||
game.answeredTeams = [];
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
game.timerInterval = timerInterval;
|
||||
} else {
|
||||
console.log(`⏸️ Waiting for media to end before starting timer (${questionType} question)`);
|
||||
}
|
||||
});
|
||||
|
||||
// Событие окончания проигрывания медиа (аудио/видео)
|
||||
socket.on('media-ended', ({ gameCode }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
if (!game || !game.currentQuestion || game.timerStarted) return;
|
||||
|
||||
console.log(`🎬 Media ended, starting timer now`);
|
||||
game.timerStarted = true;
|
||||
|
||||
// Запускаем таймер
|
||||
const timerInterval = setInterval(() => {
|
||||
game.timer--;
|
||||
io.to(gameCode).emit('timer-update', { timer: game.timer });
|
||||
|
||||
if (game.timer <= 0) {
|
||||
clearInterval(timerInterval);
|
||||
io.to(gameCode).emit('time-up');
|
||||
game.currentQuestion = null;
|
||||
game.answeringTeam = null;
|
||||
game.answeredTeams = [];
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
game.timerInterval = timerInterval;
|
||||
});
|
||||
|
||||
// Нажатие кнопки "Ответить"
|
||||
socket.on('buzz-in', ({ gameCode, teamId }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
if (!game || !game.currentQuestion || game.answeringTeam) return;
|
||||
|
||||
// Проверяем, не отвечала ли уже эта команда на этот вопрос
|
||||
if (game.answeredTeams && game.answeredTeams.includes(teamId)) {
|
||||
return; // Команда уже отвечала, игнорируем
|
||||
}
|
||||
|
||||
game.answeringTeam = teamId;
|
||||
|
||||
// Останавливаем таймер
|
||||
if (game.timerInterval) {
|
||||
clearInterval(game.timerInterval);
|
||||
}
|
||||
|
||||
const team = game.teams.find(t => t.id === teamId);
|
||||
io.to(gameCode).emit('team-buzzing', {
|
||||
teamId,
|
||||
teamName: team.name
|
||||
});
|
||||
|
||||
// Отправляем обновление игры чтобы у ведущего появились кнопки
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
});
|
||||
|
||||
// Проверка ответа
|
||||
socket.on('submit-answer', async ({ gameCode, teamId, isCorrect }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
console.log(`📝 Submit answer: gameCode=${gameCode}, teamId=${teamId}, isCorrect=${isCorrect}`);
|
||||
|
||||
if (!game || !game.currentQuestion) {
|
||||
console.log('⚠️ No game or no current question');
|
||||
return;
|
||||
}
|
||||
|
||||
const team = game.teams.find(t => t.id === teamId);
|
||||
const basePoints = game.currentQuestion.points;
|
||||
// Удваиваем баллы во втором раунде
|
||||
const points = game.currentRound === 2 ? basePoints * 2 : basePoints;
|
||||
|
||||
console.log(`Team: ${team?.name}, Base Points: ${basePoints}, Actual Points (Round ${game.currentRound}): ${points}`);
|
||||
|
||||
// Найдем answerMedia из вопроса
|
||||
const category = game.selectedCategories.find(cat => cat.name === game.currentQuestion.category);
|
||||
const questionData = category?.questions.find(q => q.points === game.currentQuestion.points);
|
||||
const answerMedia = questionData?.answerMedia;
|
||||
|
||||
if (isCorrect) {
|
||||
console.log('✅ Correct answer!');
|
||||
team.score += points;
|
||||
game.usedQuestions.push(game.currentQuestion);
|
||||
game.currentQuestion = null;
|
||||
game.answeringTeam = null;
|
||||
|
||||
// Проверяем, закончилась ли игра
|
||||
const questionsPath = join(__dirname, '../data/questions.json');
|
||||
const questionsData = JSON.parse(readFileSync(questionsPath, 'utf-8'));
|
||||
const isGameFinished = checkGameFinished(game, questionsData.categories);
|
||||
|
||||
if (isGameFinished) {
|
||||
console.log(`🏁 All questions answered and no more categories.`);
|
||||
|
||||
// Если есть answerMedia, отправляем флаг shouldFinish вместо немедленного завершения
|
||||
if (answerMedia) {
|
||||
console.log('Answer media exists, delaying game finish');
|
||||
// Обновляем в БД (но не меняем статус на finished)
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
teams: game.teams,
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeringTeam: null
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('answer-result', {
|
||||
teamId,
|
||||
isCorrect: true,
|
||||
newScore: team.score,
|
||||
questionClosed: true,
|
||||
answerMedia: answerMedia,
|
||||
shouldFinish: true
|
||||
});
|
||||
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если нет answerMedia, завершаем игру сразу
|
||||
game.status = 'finished';
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
teams: game.teams,
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeringTeam: null,
|
||||
status: 'finished'
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error finishing game in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('answer-result', {
|
||||
teamId,
|
||||
isCorrect: true,
|
||||
newScore: team.score,
|
||||
questionClosed: true,
|
||||
answerMedia: answerMedia
|
||||
});
|
||||
|
||||
io.to(gameCode).emit('game-finished', game);
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
teams: game.teams,
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeringTeam: null
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating answer in DB:', error.message);
|
||||
}
|
||||
|
||||
console.log('Sending answer-result (correct) and game-updated');
|
||||
io.to(gameCode).emit('answer-result', {
|
||||
teamId,
|
||||
isCorrect: true,
|
||||
newScore: team.score,
|
||||
questionClosed: true,
|
||||
answerMedia: answerMedia
|
||||
});
|
||||
} else {
|
||||
console.log('❌ Incorrect answer!');
|
||||
team.score -= points;
|
||||
|
||||
// Добавляем команду в список ответивших
|
||||
if (!game.answeredTeams) {
|
||||
game.answeredTeams = [];
|
||||
}
|
||||
game.answeredTeams.push(teamId);
|
||||
|
||||
game.answeringTeam = null;
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{ teams: game.teams, answeringTeam: null }
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating answer in DB:', error.message);
|
||||
}
|
||||
|
||||
console.log('Sending answer-result (incorrect)');
|
||||
io.to(gameCode).emit('answer-result', {
|
||||
teamId,
|
||||
isCorrect: false,
|
||||
newScore: team.score,
|
||||
questionClosed: false
|
||||
});
|
||||
|
||||
// Проверяем, остались ли команды которые могут ответить
|
||||
const remainingTeams = game.teams.filter(t => !game.answeredTeams.includes(t.id));
|
||||
console.log(`Remaining teams: ${remainingTeams.length}, Timer: ${game.timer}`);
|
||||
|
||||
if (remainingTeams.length === 0 || game.timer <= 0) {
|
||||
console.log('No more teams or time up - closing question');
|
||||
// Все команды ответили неправильно или время вышло
|
||||
game.usedQuestions.push(game.currentQuestion);
|
||||
game.currentQuestion = null;
|
||||
game.answeredTeams = [];
|
||||
|
||||
// Проверяем, закончилась ли игра
|
||||
const questionsPath = join(__dirname, '../data/questions.json');
|
||||
const questionsData = JSON.parse(readFileSync(questionsPath, 'utf-8'));
|
||||
const isGameFinished = checkGameFinished(game, questionsData.categories);
|
||||
|
||||
if (isGameFinished) {
|
||||
console.log(`🏁 All questions answered and no more categories. Finishing game ${gameCode}`);
|
||||
game.status = 'finished';
|
||||
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeredTeams: [],
|
||||
status: 'finished'
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error finishing game in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('time-up');
|
||||
io.to(gameCode).emit('game-finished', game);
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
return;
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('time-up');
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
} else {
|
||||
console.log('Resuming timer for other teams');
|
||||
// Возобновляем таймер для других команд
|
||||
const timerInterval = setInterval(() => {
|
||||
game.timer--;
|
||||
io.to(gameCode).emit('timer-update', { timer: game.timer });
|
||||
|
||||
if (game.timer <= 0) {
|
||||
clearInterval(timerInterval);
|
||||
io.to(gameCode).emit('time-up');
|
||||
game.currentQuestion = null;
|
||||
game.answeredTeams = [];
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
game.timerInterval = timerInterval;
|
||||
}
|
||||
}
|
||||
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
});
|
||||
|
||||
// Досрочное закрытие вопроса ведущим
|
||||
socket.on('close-question', async ({ gameCode }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
if (!game || !game.currentQuestion) return;
|
||||
|
||||
// Найдем answerMedia из вопроса перед закрытием
|
||||
const category = game.selectedCategories.find(cat => cat.name === game.currentQuestion.category);
|
||||
const questionData = category?.questions.find(q => q.points === game.currentQuestion.points);
|
||||
const answerMedia = questionData?.answerMedia;
|
||||
|
||||
// Останавливаем таймер
|
||||
if (game.timerInterval) {
|
||||
clearInterval(game.timerInterval);
|
||||
}
|
||||
|
||||
// Добавляем вопрос в использованные (чтобы он пропал с доски)
|
||||
game.usedQuestions.push(game.currentQuestion);
|
||||
|
||||
// Закрываем вопрос
|
||||
game.currentQuestion = null;
|
||||
game.answeringTeam = null;
|
||||
game.timer = null;
|
||||
|
||||
// Проверяем, закончилась ли игра
|
||||
const questionsPath = join(__dirname, '../data/questions.json');
|
||||
const questionsData = JSON.parse(readFileSync(questionsPath, 'utf-8'));
|
||||
const isGameFinished = checkGameFinished(game, questionsData.categories);
|
||||
|
||||
if (isGameFinished) {
|
||||
console.log(`🏁 All questions answered and no more categories.`);
|
||||
|
||||
// Если есть answerMedia, отправляем флаг shouldFinish вместо немедленного завершения
|
||||
if (answerMedia) {
|
||||
console.log('Answer media exists, delaying game finish');
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeringTeam: null
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('time-up', { answerMedia: answerMedia, shouldFinish: true });
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если нет answerMedia, завершаем игру сразу
|
||||
game.status = 'finished';
|
||||
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeringTeam: null,
|
||||
status: 'finished'
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error finishing game in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('time-up', { answerMedia: answerMedia });
|
||||
io.to(gameCode).emit('game-finished', game);
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
usedQuestions: game.usedQuestions,
|
||||
currentQuestion: null,
|
||||
answeringTeam: null
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating close-question in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('time-up', { answerMedia: answerMedia });
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
});
|
||||
|
||||
// Завершение игры
|
||||
socket.on('finish-game', async ({ gameCode }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
game.status = 'finished';
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate({ gameCode }, { status: 'finished' });
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating game status in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('game-finished', game);
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
|
||||
console.log(`🏁 Game ${gameCode} finished`);
|
||||
});
|
||||
|
||||
// Переход к следующему раунду
|
||||
socket.on('next-round', async ({ gameCode }) => {
|
||||
const game = activeGames.get(gameCode);
|
||||
|
||||
if (!game) return;
|
||||
|
||||
// Загружаем все категории
|
||||
const questionsPath = join(__dirname, '../data/questions.json');
|
||||
const questionsData = JSON.parse(readFileSync(questionsPath, 'utf-8'));
|
||||
|
||||
// Выбираем 5 новых случайных категорий (исключая использованные)
|
||||
const selectedCategories = selectRandomCategories(
|
||||
questionsData.categories,
|
||||
game.usedCategoryIds
|
||||
);
|
||||
|
||||
// Проверяем, есть ли еще доступные категории
|
||||
if (selectedCategories.length === 0) {
|
||||
// Категории закончились - завершаем игру автоматически
|
||||
console.log(`🏁 No more categories available. Finishing game ${gameCode}`);
|
||||
game.status = 'finished';
|
||||
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{ status: 'finished' }
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error finishing game in DB:', error.message);
|
||||
}
|
||||
|
||||
io.to(gameCode).emit('game-finished', game);
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
return;
|
||||
}
|
||||
|
||||
// Увеличиваем номер раунда
|
||||
const nextRound = game.currentRound + 1;
|
||||
|
||||
game.currentRound = nextRound;
|
||||
game.selectedCategories = selectedCategories;
|
||||
game.usedCategoryIds = [...game.usedCategoryIds, ...selectedCategories.map(cat => cat.id)];
|
||||
game.usedQuestions = [];
|
||||
game.currentQuestion = null;
|
||||
|
||||
// Обновляем в БД
|
||||
try {
|
||||
await Game.findOneAndUpdate(
|
||||
{ gameCode },
|
||||
{
|
||||
currentRound: nextRound,
|
||||
selectedCategories: selectedCategories,
|
||||
usedCategoryIds: game.usedCategoryIds,
|
||||
usedQuestions: [],
|
||||
currentQuestion: null
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('⚠️ Error updating round in DB:', error.message);
|
||||
}
|
||||
|
||||
console.log(`🎮 Game ${gameCode}: Starting round ${nextRound} with ${selectedCategories.length} categories`);
|
||||
io.to(gameCode).emit('round-changed', { round: nextRound });
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
});
|
||||
|
||||
// Отключение
|
||||
socket.on('disconnect', () => {
|
||||
console.log('👤 Client disconnected:', socket.id);
|
||||
|
||||
// Удаляем игрока из всех игр
|
||||
activeGames.forEach((game, gameCode) => {
|
||||
// Проверяем, является ли отключившийся хостом
|
||||
const isHost = game.hostId === socket.id;
|
||||
|
||||
if (isHost) {
|
||||
// Если отключился хост, игра завершается
|
||||
activeGames.delete(gameCode);
|
||||
io.to(gameCode).emit('host-disconnected');
|
||||
console.log(`🗑️ Game ${gameCode} deleted (host disconnected)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Удаляем игрока из команд
|
||||
game.teams.forEach(team => {
|
||||
team.players = team.players.filter(p => p.id !== socket.id);
|
||||
});
|
||||
|
||||
// Удаляем пустые команды
|
||||
game.teams = game.teams.filter(team => team.players.length > 0);
|
||||
|
||||
// Обновляем игру для оставшихся игроков
|
||||
sendGameUpdate(io, gameCode, game);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||