472 lines
10 KiB
JavaScript
472 lines
10 KiB
JavaScript
/*
|
|
* Copyright (C) 2021 Thomas Van Acker
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License as published by the Free Software
|
|
* Foundation, either version 3 of the License, or (at your option) any later
|
|
* version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*
|
|
* This project is hosted on https://git.bitscuit.be/bitscuit/ReverseQuiz
|
|
*
|
|
*/
|
|
|
|
// Imports
|
|
const http = require("http");
|
|
const url = require("url");
|
|
const fs = require("fs");
|
|
const util = require("util");
|
|
const fetch = require("node-fetch");
|
|
|
|
// Vars
|
|
const hostname = "127.0.0.1"; // Enter your local IP address here
|
|
const port = 5000; // Enter a port number here
|
|
|
|
const STATE_LOBBY = 0;
|
|
const STATE_MANUAL = 10;
|
|
const STATE_WRITE = 20;
|
|
const STATE_PICK = 30;
|
|
const STATE_ANSWER = 40;
|
|
const STATE_END = 50;
|
|
var state = STATE_LOBBY;
|
|
|
|
var userSockets = [];
|
|
var userNames = [];
|
|
var userOK = [];
|
|
|
|
var questionNr = 0; // Current question
|
|
var questionsTotal = 10; // Total nr of questions
|
|
var questionData = {};
|
|
var questionCorrectAnswerIndex;
|
|
var answers;
|
|
const QUESTION_CATEGORIES = [9,15,16,17,18,19,20,22,23,27,28,30]; // Default enabled categories
|
|
|
|
var userWrite = [];
|
|
var userPick = [];
|
|
var userPoints = [];
|
|
|
|
var config = {
|
|
categories: [], // Filled in later
|
|
answerPreview: true,
|
|
timer: 60,
|
|
};
|
|
|
|
// Get question categories
|
|
function isCategoryEnabledByDefault(id){
|
|
return QUESTION_CATEGORIES.includes(id);
|
|
}
|
|
fetch("https://opentdb.com/api_category.php")
|
|
.then(res => res.json()) // Convert to JSON
|
|
.then(json => {
|
|
// Question loaded
|
|
console.log("Fetched question categories from OpenTDB");
|
|
console.log(json);
|
|
|
|
// TODO: check response code
|
|
|
|
// Save data
|
|
var cats = [];
|
|
for(var i=0; i<json.trivia_categories.length; i++){
|
|
cats.push({
|
|
id: json.trivia_categories[i].id,
|
|
name: json.trivia_categories[i].name,
|
|
enabled: isCategoryEnabledByDefault(json.trivia_categories[i].id),
|
|
});
|
|
}
|
|
config.categories = cats;
|
|
|
|
console.log(config);
|
|
});
|
|
|
|
// Create HTTP server
|
|
const server = http.createServer((req, res) => {
|
|
// Parse url
|
|
const requrl = url.parse(req.url);
|
|
var reqpath = requrl.pathname;
|
|
if(reqpath == "/") reqpath = "/index.html";
|
|
|
|
// Respond to requests
|
|
try{
|
|
// Return requested page
|
|
res.statusCode = 200;
|
|
res.write(fs.readFileSync("html"+reqpath));
|
|
res.end();
|
|
|
|
}catch(error){
|
|
// 404 if page not found
|
|
res.statusCode = 404;
|
|
res.setHeader("Content-Type", "text/plain");
|
|
res.end("404 - Page Not Found");
|
|
}
|
|
});
|
|
|
|
|
|
// Create socket.io server
|
|
const io = require("socket.io")(server); // Can only be done when server is created
|
|
|
|
io.on("connection", (socket) => {
|
|
console.log("New user connected!");
|
|
|
|
// Init socket listeners
|
|
|
|
// Disconnect
|
|
socket.on("disconnect", () => {
|
|
console.log("User disconnected!");
|
|
|
|
// Remove from user lists
|
|
if(userSockets.indexOf(socket) != -1){
|
|
// When in users list
|
|
name = userNames[userSockets.indexOf(socket)];
|
|
|
|
userNames.splice(userSockets.indexOf(socket), 1);
|
|
userSockets.splice(userSockets.indexOf(socket), 1);
|
|
|
|
if(state == STATE_LOBBY){
|
|
// Send new lobby
|
|
userSockets.forEach(function(s){s.emit("LOBBY", userNames);});
|
|
}else{
|
|
// Send disconnect message
|
|
userSockets.forEach(function(s){s.emit("DISCONNECT", name);});
|
|
}
|
|
}
|
|
});
|
|
|
|
// JOIN
|
|
socket.on("JOIN", (username) => {
|
|
console.log("JOIN request for "+username);
|
|
|
|
// Check if can enter in lobby
|
|
if(state == STATE_LOBBY){
|
|
// Add user to lists
|
|
userSockets.push(socket);
|
|
userNames.push(username);
|
|
console.log(userNames);
|
|
|
|
// Send lobby data
|
|
userSockets.forEach(function(s){s.emit("LOBBY", userNames);});
|
|
|
|
// Send config to new user
|
|
socket.emit("CONFIG", {config: config});
|
|
|
|
}else{
|
|
// When can't enter
|
|
socket.emit("LOBBY_CLOSED");
|
|
}
|
|
});
|
|
|
|
// CONFIG
|
|
socket.on("CONFIG", (data) => {
|
|
console.log("CONFIG request");
|
|
|
|
// Update server config
|
|
config = data.config;
|
|
|
|
// Send new config to all users
|
|
userSockets.forEach(function(s){s.emit("CONFIG", {config: config});});
|
|
});
|
|
|
|
// START
|
|
socket.on("START", () => {
|
|
console.log("START request");
|
|
|
|
// Send START to all users
|
|
userSockets.forEach(function(s){s.emit("START");});
|
|
|
|
// Set state
|
|
state = STATE_MANUAL;
|
|
|
|
// Reset vars
|
|
questionNr = 0;
|
|
userPoints = [];
|
|
for(i=0; i<userSockets.length; i++){
|
|
userPoints.push(0);
|
|
}
|
|
|
|
// Send game data
|
|
var dataUser = {usernames:userNames, questionsTotal:questionsTotal};
|
|
for(i=0; i<userSockets.length; i++){
|
|
userSockets[i].emit("START", dataUser);
|
|
}
|
|
|
|
console.log("Game data sent!");
|
|
|
|
// Reset OK
|
|
userOK = [];
|
|
for(i=0; i<userSockets.length; i++){
|
|
userOK[i] = false;
|
|
}
|
|
});
|
|
|
|
// OK
|
|
socket.on("OK", () => {
|
|
ok(socket);
|
|
});
|
|
|
|
|
|
// WRITE
|
|
socket.on("WRITE", (data) => {
|
|
// When received write from player
|
|
|
|
// Check if empty
|
|
if(data.write == ""){
|
|
socket.emit("WRITE_REPLY", {error:"Please enter a false answer!"});
|
|
return;
|
|
}
|
|
|
|
// Check if valid
|
|
var valid = true;
|
|
valid = valid && data.write.toUpperCase() != questionData.correct_answer.toUpperCase();
|
|
for(i=0; i<questionData.incorrect_answers.length; i++){
|
|
valid = valid && data.write.toUpperCase() != questionData.incorrect_answers[i].toUpperCase();
|
|
}
|
|
for(i=0; i<userWrite.length; i++){
|
|
valid = valid && data.write.toUpperCase() != userWrite[i].toUpperCase();
|
|
}
|
|
|
|
// Send error message if invalid
|
|
if(!valid){
|
|
socket.emit("WRITE_REPLY", {error:"This answer already exists!"});
|
|
return;
|
|
}
|
|
|
|
// Add to list
|
|
userWrite[userSockets.indexOf(socket)] = data.write;
|
|
|
|
// Perform ok
|
|
ok(socket);
|
|
});
|
|
|
|
|
|
// PICK
|
|
socket.on("PICK", (id) => {
|
|
// When received pick form player
|
|
userPick[userSockets.indexOf(socket)] = id;
|
|
|
|
// Perform ok
|
|
ok(socket);
|
|
});
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
// Start HTTP server
|
|
server.listen(port, hostname, () => {
|
|
console.log("Server running at http://"+hostname+":"+port);
|
|
});
|
|
|
|
|
|
|
|
// Game methods
|
|
function next(){
|
|
// Go to next step of game
|
|
|
|
// Send LOADING
|
|
userSockets.forEach(function(s){s.emit("LOADING");});
|
|
|
|
|
|
if(state == STATE_MANUAL || state == STATE_ANSWER){
|
|
// Write
|
|
state = STATE_WRITE;
|
|
|
|
// Update vars
|
|
questionNr++;
|
|
userWrite = [];
|
|
for(i=0; i<userSockets.length; i++){
|
|
userWrite.push("");
|
|
}
|
|
userPick = [];
|
|
for(i=0; i<userSockets.length; i++){
|
|
userPick.push("");
|
|
}
|
|
|
|
// Stop if got all questions
|
|
if(questionNr > questionsTotal){
|
|
// End
|
|
state = STATE_END;
|
|
|
|
// Gather data
|
|
var sorted = [];
|
|
for(i=0; i<userNames.length; i++){
|
|
sorted.push({name:userNames[i], score:userPoints[i]});
|
|
}
|
|
sorted.sort((a,b) => b.score-a.score);
|
|
console.log(sorted);
|
|
|
|
var ranks = [];
|
|
var r = 1;
|
|
var ls = sorted[0].score;
|
|
for(i=0; i<sorted.length; i++){
|
|
if(sorted[i].score < ls){
|
|
r = i+1;
|
|
ls = sorted[i].score;
|
|
}
|
|
ranks.push({name:sorted[i].name, score:sorted[i].score, rank:r});
|
|
}
|
|
console.log(ranks);
|
|
|
|
// Send data
|
|
var data = {ranks:ranks};
|
|
userSockets.forEach(function(s){s.emit("STOP", data);});
|
|
|
|
// Reset vars
|
|
state = STATE_LOBBY;
|
|
userSockets = [];
|
|
userNames = [];
|
|
|
|
|
|
}else{
|
|
// Load new question
|
|
console.log("Loading new question...");
|
|
var category = QUESTION_CATEGORIES[rand(0, QUESTION_CATEGORIES.length)];
|
|
fetch("https://opentdb.com/api.php?amount=1&type=multiple&category="+category)
|
|
.then(res => res.json()) // Convert to JSON
|
|
.then(json => {
|
|
// Question loaded
|
|
console.log("Question loaded!");
|
|
console.log(json);
|
|
|
|
// TODO: check response code
|
|
|
|
// Save data
|
|
questionData = json.results[0];
|
|
|
|
// Send question to players
|
|
userSockets.forEach(function(s){s.emit("WRITE", {questionNr:questionNr, questionsTotal:questionsTotal, question:questionData.question});});
|
|
});
|
|
}
|
|
|
|
|
|
}else if(state == STATE_WRITE){
|
|
// Pick
|
|
state = STATE_PICK;
|
|
console.log("Preparing PICK");
|
|
|
|
// Send question and answers to players
|
|
answers = [];
|
|
for(i=0; i<questionData.incorrect_answers.length; i++){
|
|
answers.splice(rand(0, answers.length+1), 0, questionData.incorrect_answers[i]);
|
|
}
|
|
for(i=0; i<userWrite.length; i++){
|
|
answers.splice(rand(0, answers.length+1), 0, userWrite[i]);
|
|
}
|
|
questionCorrectAnswerIndex = rand(0, answers.length+1);
|
|
answers.splice(questionCorrectAnswerIndex, 0, questionData.correct_answer);
|
|
console.log(answers);
|
|
|
|
// Send
|
|
var data = {questionNr:questionNr, questionsTotal:questionsTotal, question:questionData.question, answers:answers};
|
|
userSockets.forEach(function(s){s.emit("PICK", data);});
|
|
console.log(data);
|
|
|
|
|
|
|
|
}else if(state == STATE_PICK){
|
|
// Answer
|
|
state = STATE_ANSWER;
|
|
console.log("Preparing ANSWER");
|
|
|
|
// Update points
|
|
var pointsDiff = [];
|
|
for(i=0; i<userSockets.length; i++){
|
|
pointsDiff.push(0);
|
|
}
|
|
for(i=0; i<userSockets.length; i++){
|
|
// Check if correct
|
|
if(userPick[i] == questionCorrectAnswerIndex){
|
|
userPoints[i] += 3;
|
|
pointsDiff[i] += 3;
|
|
}
|
|
|
|
// Check writes
|
|
for(j=0; j<userWrite.length; j++){
|
|
if(answers[userPick[i]].toUpperCase() == userWrite[j].toUpperCase() && i != j){
|
|
userPoints[j] += 1;
|
|
pointsDiff[j] += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send data to players
|
|
var data = {questionNr:questionNr, questionsTotal:questionsTotal, question:questionData.question, answers:answers, correctAnswer:questionCorrectAnswerIndex, userPick:userPick, userWrite:userWrite, userPoints:userPoints, pointsDiff:pointsDiff, usernames:userNames};
|
|
|
|
userSockets.forEach(function(s){s.emit("ANSWER", data);});
|
|
console.log(data);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
function ok(socket){
|
|
// Set OK for this player
|
|
userOK[userSockets.indexOf(socket)] = true;
|
|
|
|
// Check if everybody OK
|
|
var OK = true;
|
|
for(i=0; i<userOK.length; i++){
|
|
OK = OK && userOK[i];
|
|
}
|
|
|
|
// Send OK data
|
|
var waitingFor = [];
|
|
for(i=0; i<userOK.length; i++){
|
|
if(!userOK[i]) waitingFor.push(userNames[i]);
|
|
}
|
|
var dataOK = {users:waitingFor};
|
|
userSockets.forEach(function(s){s.emit("OK_REPLY", dataOK)});
|
|
|
|
// Perform next step if OK
|
|
if(OK){
|
|
|
|
// Reset OK
|
|
userOK = [];
|
|
for(i=0; i<userSockets.length; i++){
|
|
userOK[i] = false;
|
|
}
|
|
|
|
// Next step
|
|
next();
|
|
}
|
|
}
|
|
|
|
|
|
// Helper methods
|
|
function rand(min, max){
|
|
return Math.floor(Math.random()*(max-min) + min);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|