rx-irc/bot-quiz/lib/quiz-game.js
// NPM Dependencies
const assert = require('assert');
const colors = require('irc-colors');
// Local Dependencies
const QuizPlayers = require('./quiz-players');
const QuizQuestion = require('./quiz-question');
const QuizQuestions = require('./quiz-questions');
const QuizHelp = require('./quiz-help');
/**
* @type {Object}
* @property {string} prefix='[Qz]'
* @property {number} startDelay=10000
* @property {number} questionDelay=5000
* @property {number} hintInterval=5000
* @property {boolean} moderated=true
* @property {boolean} hintsUseCache=false
* @property {string} logLevel='error'
*/
let defaults = {
prefix: '[Qz]',
startDelay: 10000,
questionDelay: 5000,
hintInterval: 5000,
moderated: true,
hintsUseCache: false,
logLevel: 'error',
};
module.exports = class QuizGame {
/**
* @param {Client} client
* @param {object} options
* @returns {void}
*/
constructor(client, options) {
assert.strictEqual(typeof options.channel, 'string');
/** @type {Client} */
this.client = client;
/** @type {object} */
this.settings = { ...defaults, ...options };
/** @type {QuizPlayers} */
this.players = new QuizPlayers();
/** @type {QuizQuestions} */
this.questions = new QuizQuestions();
/** @type {QuizHelp} */
this.help = new QuizHelp({ useCache: this.settings.hintsUseCache });
/** @type {boolean} */
this.gameActive = false;
/** @type {number} */
this.startTimeoutId = null;
/** @type {QuizQuestion} */
this.currentChallenge = null;
/** @type {number} */
this.challengeTimeoutId = null;
/** @type {number} */
this.hintIntervalId = null;
}
/**
* @param {string} nick
* @returns {void}
*/
addPlayer(nick) {
if (!this.players.isPlayer(nick)) {
if (this.gameActive) {
this.notify(nick, `Game is already running.`);
} else {
if (this.settings.moderated) {
this.sendModes(this.settings.channel, '+v', nick);
}
this.tellToChannel(`Player ${nick} has joined the game.`);
this.players.addPlayer(nick);
if (this.players.nickList.length === 2) {
this.tellToChannel('Enough players found for new game.');
this.beginGame();
} else if (this.players.nickList.length > 2) {
this.tellToChannel('Additional player joined the game.');
this.beginGame();
}
}
}
}
/**
* @param {string} nick
* @returns {void}
*/
removePlayer(nick) {
if (this.players.isPlayer(nick)) {
if (this.settings.moderated) {
this.sendModes(this.settings.channel, '-v', nick);
}
this.tellToChannel(`Player ${nick} has quit the game.`);
this.players.removePlayer(nick);
if (this.players.nickList.length === 1) {
clearTimeout(this.startTimeoutId);
if (this.gameActive) {
this.finishChallenge();
this.endGame(`Last man standing.`);
}
}
}
}
/**
* @param {string} oldnick
* @param {string} newnick
* @returns {void}
*/
updatePlayer(oldnick, newnick) {
if (this.players.isPlayer(oldnick)) {
this.players.updatePlayer(oldnick, newnick);
}
}
/**
* @returns {void}
*/
beginGame() {
this.tellToChannel(`Game starting in ${this.settings.startDelay / 1000} seconds.`);
clearTimeout(this.startTimeoutId);
this.startTimeoutId = setTimeout(() => {
let title = colors.inverse(` GAME STARTS `);
this.gameActive = true;
this.questions.load('de-quake', 10);
if (this.settings.moderated) {
this.sendModes(this.settings.channel, '+m');
}
this.tellToChannel(title);
this.tellToChannel(
'Total questions: ' + this.questions.total + '; ' +
'Using: ' + this.questions.current.length + '; ' +
'Remaining: ' + this.questions.remaining.length
);
this.tellToChannel('Players: ' + this.players.nickList.join(', '));
this.nextChallenge();
}, this.settings.startDelay);
}
/**
* @param {string} reason
* @returns {void}
*/
endGame(reason) {
let title = colors.inverse(` GAME OVER `);
clearTimeout(this.startTimeoutId);
clearInterval(this.hintIntervalId);
if (this.gameActive) {
this.tellToChannel(`${title} ${reason}`);
this.tellScore();
this.gameActive = false;
this.tellToChannel(`Thank you for playing.`);
if (this.settings.moderated) {
this.sendModes(this.settings.channel, '-m');
this.players.nickList.forEach(nick => {
this.sendModes(this.settings.channel, '-v', nick);
});
}
this.players.removeAllPlayers();
}
}
// ____ _ _ _
// / ___| |__ __ _| | | ___ _ __ __ _ ___
// | | | '_ \ / _` | | |/ _ \ '_ \ / _` |/ _ \
// | |___| | | | (_| | | | __/ | | | (_| | __/
// \____|_| |_|\__,_|_|_|\___|_| |_|\__, |\___|
// |___/
//
/**
* @returns {void}
*/
nextChallenge() {
if (this.gameActive) {
if (this.currentChallenge !== null) {
// Finish current challenge first.
} else if (this.challengeTimeoutId !== null) {
// Next challenge already initialized.
} else if (this.questions.current.length === 0) {
this.endGame(`No more questions.`);
} else {
this.challengeTimeoutId = setTimeout(() => {
let title = colors.inverse(` NEW QUESTION `);
this.challengeTimeoutId = null;
this.currentChallenge = new QuizQuestion(this.questions.pop());
this.tellToChannel(`${title} ${this.questions.currentLimit - this.questions.current.length} / ${this.questions.currentLimit}`);
this.tellToChannel(this.currentChallenge.question);
this.tellToChannel(this.currentChallenge.hintPlaceholder);
clearInterval(this.hintIntervalId);
this.hintIntervalId = setInterval(
() => this.giveHint(),
this.settings.hintInterval
);
}, this.settings.questionDelay);
}
}
}
/**
* @param {string} winner
* @returns {void}
*/
finishChallenge(winner) {
let title = colors.inverse(` QUESTION END `);
clearInterval(this.hintIntervalId);
this.players.resetRevoltees();
let message;
if (winner) {
this.players.increaseScore(winner);
message = `${winner} had the correct answer!`;
} else {
message = `The question went unanswered.`;
}
this.tellToChannel(`${title} ${message}`);
this.tellToChannel(`Question: ${this.currentChallenge.question}`);
this.tellToChannel(`Answer: ${this.currentChallenge.hintString}`);
this.tellScore();
this.currentChallenge = null;
}
/**
* @param {string} nick
* @returns {void}
*/
handleRevolt(nick) {
if (this.currentChallenge !== null) {
if (this.players.isRevolting(nick)) {
this.notify(nick, `You are already revolting.`);
} else if (this.currentChallenge.hintsGiven < 1) {
this.tellToChannel(`Revolting is only possible after the first hint.`);
} else {
this.players.setRevoltee(nick);
this.tellToChannel(`${nick} is revolting!`);
if (this.players.getRevoltees().length > this.players.nickList.length / 2) {
this.tellToChannel(`Revolt successful. The quiz master gives in.`);
this.finishChallenge();
this.nextChallenge();
} else {
this.tellToChannel(`The mob is negligible. The quiz master is not impressed.`)
}
}
}
}
/**
* @param {string} nick
* @param {string} guess
* @returns {void}
*/
handleGuess(nick, guess) {
if (this.currentChallenge !== null) {
if (this.currentChallenge.checkGuess(guess)) {
this.finishChallenge(nick);
this.nextChallenge();
}
}
}
/**
* @returns {void}
*/
giveHint() {
if (this.currentChallenge === null) {
} else if (this.currentChallenge.hintArray.length < 2) {
this.finishChallenge();
this.nextChallenge();
} else {
this.currentChallenge.addHint();
this.tellToChannel(this.currentChallenge.question);
this.tellToChannel(this.currentChallenge.hintPlaceholder);
}
}
// _____ _ _
// |_ _|_ _| | | __
// | |/ _` | | |/ /
// | | (_| | | <
// |_|\__,_|_|_|\_\
//
/**
* @returns {void}
*/
sendModes() {
let args = Array.from(arguments);
args.unshift('MODE');
this.client.lib.send.apply(this.client.lib, args);
}
/**
* @param {string} message
* @returns {void}
*/
tellToChannel(message) {
this.client.tell(this.settings.channel, message, this.settings.prefix);
}
/**
* @param {string} target
* @param {string} message
* @returns {void}
*/
notify(target, message) {
this.client.notify(target, message, this.settings.prefix);
}
/**
* @param {string[]} multiline
* @returns {void}
*/
tellScore(multiline) {
let title = colors.inverse(` SCORE `);
let output = [];
if (this.gameActive) {
let ranking = this.players.getRanking().map(player => {
return `${player.position}. ${player.nick} (${player.score})`;
});
if (multiline) {
output.push(`${title}`);
output.concat(ranking);
} else {
output.push(`${title} ${ranking.join(', ')}`);
}
} else {
output.push(`${title} No active game.`);
}
this.tellToChannel(output);
}
/**
* @param {string} target
* @param {string} category
* @returns {void}
*/
tellHelp(target, category) {
this.notify(target, this.help.getArray(category));
}
};