"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var trivia_exports = {}; __export(trivia_exports, { FirstModeTrivia: () => FirstModeTrivia, Mastermind: () => Mastermind, MastermindFinals: () => MastermindFinals, MastermindRound: () => MastermindRound, NumberModeTrivia: () => NumberModeTrivia, TimerModeTrivia: () => TimerModeTrivia, TriumvirateModeTrivia: () => TriumvirateModeTrivia, Trivia: () => Trivia, cachedLadder: () => cachedLadder, commands: () => commands, database: () => database, mergeAlts: () => mergeAlts, pendingAltMerges: () => pendingAltMerges, requestAltMerge: () => requestAltMerge }); module.exports = __toCommonJS(trivia_exports); var import_lib = require("../../../lib"); var import_database = require("./database"); const MAIN_CATEGORIES = { ae: "Arts and Entertainment", pokemon: "Pok\xE9mon", sg: "Science and Geography", sh: "Society and Humanities" }; const SPECIAL_CATEGORIES = { misc: "Miscellaneous", event: "Event", eventused: "Event (used)", subcat: "Sub-Category 1", subcat2: "Sub-Category 2", subcat3: "Sub-Category 3", subcat4: "Sub-Category 4", subcat5: "Sub-Category 5", oldae: "Old Arts and Entertainment", oldsg: "Old Science and Geography", oldsh: "Old Society and Humanities", oldpoke: "Old Pok\xE9mon" }; const ALL_CATEGORIES = { ...SPECIAL_CATEGORIES, ...MAIN_CATEGORIES }; const CATEGORY_ALIASES = { poke: "pokemon", subcat1: "subcat" }; const MODES = { first: "First", number: "Number", timer: "Timer", triumvirate: "Triumvirate" }; const LENGTHS = { short: { cap: 20, prizes: [3, 2, 1] }, medium: { cap: 35, prizes: [4, 2, 1] }, long: { cap: 50, prizes: [5, 3, 1] }, infinite: { cap: false, prizes: [5, 3, 1] } }; Object.setPrototypeOf(MAIN_CATEGORIES, null); Object.setPrototypeOf(SPECIAL_CATEGORIES, null); Object.setPrototypeOf(ALL_CATEGORIES, null); Object.setPrototypeOf(MODES, null); Object.setPrototypeOf(LENGTHS, null); const SIGNUP_PHASE = "signups"; const QUESTION_PHASE = "question"; const INTERMISSION_PHASE = "intermission"; const MASTERMIND_ROUNDS_PHASE = "rounds"; const MASTERMIND_FINALS_PHASE = "finals"; const MOVE_QUESTIONS_AFTER_USE_FROM_CATEGORY = "event"; const MOVE_QUESTIONS_AFTER_USE_TO_CATEGORY = "eventused"; const START_TIMEOUT = 30 * 1e3; const MASTERMIND_FINALS_START_TIMEOUT = 30 * 1e3; const INTERMISSION_INTERVAL = 20 * 1e3; const MASTERMIND_INTERMISSION_INTERVAL = 500; const PAUSE_INTERMISSION = 5 * 1e3; const MAX_QUESTION_LENGTH = 252; const MAX_ANSWER_LENGTH = 32; const database = new import_database.TriviaSQLiteDatabase("config/chat-plugins/triviadata.json"); const pendingAltMerges = /* @__PURE__ */ new Map(); function getTriviaGame(room) { if (!room) { throw new Chat.ErrorMessage(`This command can only be used in the Trivia room.`); } const game = room.game; if (!game) { throw new Chat.ErrorMessage(room.tr`There is no game in progress.`); } if (game.gameid !== "trivia") { throw new Chat.ErrorMessage(room.tr`The currently running game is not Trivia, it's ${game.title}.`); } return game; } function getMastermindGame(room) { if (!room) { throw new Chat.ErrorMessage(`This command can only be used in the Trivia room.`); } const game = room.game; if (!game) { throw new Chat.ErrorMessage(room.tr`There is no game in progress.`); } if (game.gameid !== "mastermind") { throw new Chat.ErrorMessage(room.tr`The currently running game is not Mastermind, it's ${game.title}.`); } return game; } function getTriviaOrMastermindGame(room) { try { return getMastermindGame(room); } catch { return getTriviaGame(room); } } function broadcast(room, title, message) { let buffer = `
/trivia join
to sign up manually.)`
);
}
getDescription() {
return this.room.tr`Mode: ${this.game.mode} | Category: ${this.game.category} | Cap: ${this.getDisplayableCap()}`;
}
/**
* Formats the player list for display when using /trivia players.
*/
formatPlayerList(settings) {
return this.getTopPlayers(settings).map((player) => {
const buf = import_lib.Utils.html`${player.name} (${player.player.points || "0"})`;
return player.player.isAbsent ? `${buf}` : buf;
}).join(", ");
}
/**
* Kicks a player from the game, preventing them from joining it again
* until the next game begins.
*/
kick(user) {
if (!this.playerTable[user.id]) {
if (this.kickedUsers.has(user.id)) {
throw new Chat.ErrorMessage(this.room.tr`User ${user.name} has already been kicked from the game.`);
}
for (const id of user.previousIDs) {
if (this.kickedUsers.has(id)) {
throw new Chat.ErrorMessage(this.room.tr`User ${user.name} has already been kicked from the game.`);
}
}
for (const kickedUserid of this.kickedUsers) {
const kickedUser = Users.get(kickedUserid);
if (kickedUser) {
const isSameUser = kickedUser.previousIDs.includes(user.id) || kickedUser.previousIDs.some((id) => user.previousIDs.includes(id)) || !Config.noipchecks && kickedUser.ips.some((ip) => user.ips.includes(ip));
if (isSameUser)
throw new Chat.ErrorMessage(this.room.tr`User ${user.name} has already been kicked from the game.`);
}
}
throw new Chat.ErrorMessage(this.room.tr`User ${user.name} is not a player in the game.`);
}
this.kickedUsers.add(user.id);
for (const id of user.previousIDs) {
this.kickedUsers.add(id);
}
this.removePlayer(this.playerTable[user.id]);
}
leave(user) {
if (!this.playerTable[user.id]) {
throw new Chat.ErrorMessage(this.room.tr`You are not a player in the current game.`);
}
this.removePlayer(this.playerTable[user.id]);
}
/**
* Starts the question loop for a trivia game in its signup phase.
*/
start() {
if (this.phase !== SIGNUP_PHASE)
throw new Chat.ErrorMessage(this.room.tr`The game has already been started.`);
broadcast(this.room, this.room.tr`The game will begin in ${START_TIMEOUT / 1e3} seconds...`);
this.phase = INTERMISSION_PHASE;
this.setPhaseTimeout(() => void this.askQuestion(), START_TIMEOUT);
}
pause() {
if (this.isPaused)
throw new Chat.ErrorMessage(this.room.tr`The trivia game is already paused.`);
if (this.phase === QUESTION_PHASE) {
throw new Chat.ErrorMessage(this.room.tr`You cannot pause the trivia game during a question.`);
}
this.isPaused = true;
broadcast(this.room, this.room.tr`The Trivia game has been paused.`);
}
resume() {
if (!this.isPaused)
throw new Chat.ErrorMessage(this.room.tr`The trivia game is not paused.`);
this.isPaused = false;
broadcast(this.room, this.room.tr`The Trivia game has been resumed.`);
if (this.phase === INTERMISSION_PHASE)
this.setPhaseTimeout(() => void this.askQuestion(), PAUSE_INTERMISSION);
}
/**
* Broadcasts the next question on the questions list to the room and sets
* a timeout to tally the answers received.
*/
async askQuestion() {
if (this.isPaused)
return;
if (!this.questions.length) {
const cap = this.getCap();
if (!cap.questions && !cap.points) {
if (this.game.length === "infinite") {
this.questions = await database.getQuestions(this.categories, 1e3, {
order: this.game.mode.startsWith("Random") ? "random" : "oldestfirst"
});
}
void this.win(`The game of Trivia has ended because there are no more questions!`);
return;
}
if (this.phaseTimeout)
clearTimeout(this.phaseTimeout);
this.phaseTimeout = null;
broadcast(
this.room,
this.room.tr`No questions are left!`,
this.room.tr`The game has reached a stalemate`
);
if (this.room)
this.destroy();
return;
}
this.phase = QUESTION_PHASE;
this.askedAt = process.hrtime();
const question = this.questions.shift();
this.questionNumber++;
this.curQuestion = question.question;
this.curAnswers = question.answers;
this.sendQuestion(question);
this.setTallyTimeout();
if (question.category === MOVE_QUESTIONS_AFTER_USE_FROM_CATEGORY && await database.shouldMoveEventQuestions()) {
await database.moveQuestionToCategory(question.question, MOVE_QUESTIONS_AFTER_USE_TO_CATEGORY);
}
}
setTallyTimeout() {
this.setPhaseTimeout(() => void this.tallyAnswers(), this.getRoundLength());
}
/**
* Broadcasts to the room what the next question is.
*/
sendQuestion(question) {
broadcast(
this.room,
this.room.tr`Question ${this.questionNumber}: ${question.question}`,
this.room.tr`Category: ${ALL_CATEGORIES[question.category]}`
);
}
/**
* This is a noop here since it'd defined properly by subclasses later on.
* All this is obligated to do is take a user and their answer as args;
* the behaviour of this method can be entirely arbitrary otherwise.
*/
answerQuestion(answer, user) {
}
/**
* Verifies whether or not an answer is correct. In longer answers, small
* typos can be made and still have the answer be considered correct.
*/
verifyAnswer(targetAnswer) {
return this.curAnswers.some((answer) => {
const mla = this.maxLevenshteinAllowed(answer.length);
return answer === targetAnswer || import_lib.Utils.levenshtein(targetAnswer, answer, mla) <= mla;
});
}
/**
* Return the maximum Levenshtein distance that is allowable for answers of the given length.
*/
maxLevenshteinAllowed(answerLength) {
if (answerLength > 5) {
return 2;
}
if (answerLength > 4) {
return 1;
}
return 0;
}
/**
* This is a noop here since it'd defined properly by mode subclasses later
* on. This calculates the points a correct responder earns, which is
* typically between 1-5.
*/
calculatePoints(diff, totalDiff) {
}
/**
* This is a noop here since it's defined properly by mode subclasses later
* on. This is obligated to update the game phase, but it can be entirely
* arbitrary otherwise.
*/
tallyAnswers() {
}
/**
* Ends the game after a player's score has exceeded the score cap.
*/
async win(buffer) {
if (this.phaseTimeout)
clearTimeout(this.phaseTimeout);
this.phaseTimeout = null;
const winners = this.getTopPlayers({ max: 3, requirePoints: true });
buffer += `Points gained | ${this.room.tr`Correct`} |
---|---|
${pointValue} | ` + import_lib.Utils.html`${playerNames.join(", ")} | ` + "
— | ${this.room.tr`No one answered correctly...`} |
/mastermind join
to sign up for the game.`
);
}
addTriviaPlayer(user) {
if (user.previousIDs.concat(user.id).some((id) => id in this.playerTable)) {
throw new Chat.ErrorMessage(this.room.tr`You have already signed up for this game.`);
}
for (const targetUser of Object.keys(this.playerTable).map((id) => Users.get(id))) {
if (!targetUser)
continue;
const isSameUser = targetUser.previousIDs.includes(user.id) || targetUser.previousIDs.some((tarId) => user.previousIDs.includes(tarId)) || !Config.noipchecks && targetUser.ips.some((ip) => user.ips.includes(ip));
if (isSameUser)
throw new Chat.ErrorMessage(this.room.tr`You have already signed up for this game.`);
}
this.addPlayer(user);
}
formatPlayerList() {
return import_lib.Utils.sortBy(
Object.values(this.playerTable),
(player) => -(this.leaderboard.get(player.id)?.score || 0)
).map((player) => {
const isFinalist = this.currentRound instanceof MastermindFinals && player.id in this.currentRound.playerTable;
const name = isFinalist ? import_lib.Utils.html`${player.name}` : import_lib.Utils.escapeHTML(player.name);
return `${name} (${this.leaderboard.get(player.id)?.score || "0"})`;
}).join(", ");
}
/**
* Starts a new round for a particular player.
* @param playerID the user ID of the player
* @param category the category to ask questions in (e.g. Pokémon)
* @param questions an array of TriviaQuestions to be asked
* @param timeout the period of time to end the round after (in seconds)
*/
startRound(playerID, category, questions, timeout) {
if (this.currentRound) {
throw new Chat.ErrorMessage(this.room.tr`There is already a round of Mastermind in progress.`);
}
if (!(playerID in this.playerTable)) {
throw new Chat.ErrorMessage(this.room.tr`That user is not signed up for Mastermind!`);
}
if (this.leaderboard.has(playerID)) {
throw new Chat.ErrorMessage(this.room.tr`The user "${playerID}" has already played their round of Mastermind.`);
}
if (this.playerCount <= this.numFinalists) {
throw new Chat.ErrorMessage(this.room.tr`You cannot start the game of Mastermind until there are more players than finals slots.`);
}
this.phase = MASTERMIND_ROUNDS_PHASE;
this.currentRound = new MastermindRound(this.room, category, questions, playerID);
setTimeout(() => {
if (!this.currentRound)
return;
const points = this.currentRound.playerTable[playerID]?.points;
const player = this.playerTable[playerID].name;
broadcast(
this.room,
this.room.tr`The round of Mastermind has ended!`,
points ? this.room.tr`${player} earned ${points} points!` : void 0
);
this.leaderboard.set(playerID, { score: points || 0 });
this.currentRound.destroy();
this.currentRound = null;
}, timeout * 1e3);
}
/**
* Starts the Mastermind finals.
* According the specification given by Trivia auth,
* Mastermind finals are always in the 'all' category.
* @param timeout timeout in seconds
*/
async startFinals(timeout) {
if (this.currentRound) {
throw new Chat.ErrorMessage(this.room.tr`There is already a round of Mastermind in progress.`);
}
for (const player in this.playerTable) {
if (!this.leaderboard.has(toID(player))) {
throw new Chat.ErrorMessage(this.room.tr`You cannot start finals until the user '${player}' has played a round.`);
}
}
const questions = await getQuestions(["all"], "random");
if (!questions.length)
throw new Chat.ErrorMessage(this.room.tr`There are no questions in the Trivia database.`);
this.currentRound = new MastermindFinals(this.room, "all", questions, this.getTopPlayers(this.numFinalists));
this.phase = MASTERMIND_FINALS_PHASE;
setTimeout(() => {
void (async () => {
if (this.currentRound) {
await this.currentRound.win();
const [winner, second, third] = this.currentRound.getTopPlayers();
this.currentRound.destroy();
this.currentRound = null;
let buf = this.room.tr`No one scored any points, so it's a tie!`;
if (winner) {
const winnerName = import_lib.Utils.escapeHTML(winner.name);
buf = this.room.tr`${winnerName} won the game of Mastermind with ${winner.player.points} points!`;
}
let smallBuf;
if (second && third) {
const secondPlace = import_lib.Utils.escapeHTML(second.name);
const thirdPlace = import_lib.Utils.escapeHTML(third.name);
smallBuf = `${Chat.count(submissions.length, " questions")} awaiting review: | |||||
# | ${this.tr`Category`} | ${this.tr`Question`} | ${this.tr`Answer(s)`} | ${this.tr`Submitted By`} |
---|
Category | ${this.tr`Question Count`} |
---|---|
${name} | ${tally} (${(tally * 100 / counts.total).toFixed(2)}%) |
${this.tr`Total`} | ${counts.total} |
# | ${this.tr`Category`} | ${this.tr`Question`} |
---|---|---|
${this.tr`There are ${results.length} matches for your query:`} | ||
${i + 1} | ${q.category} | ${q.question} |
${name} | Cycle Ladder | All-time Score Ladder | All-time Wins Ladder |
---|---|---|---|
Leaderboard score | ` + display(cycleScore, cycleRanks, "score") + display(score, ranks, "score") + display(allTimeScore, allTimeRanks, "score") + `|||
Total game points | ` + display(cycleScore, cycleRanks, "totalPoints") + display(score, ranks, "totalPoints") + display(allTimeScore, allTimeRanks, "totalPoints") + `|||
Total correct answers | ` + display(cycleScore, cycleRanks, "totalCorrectAnswers") + display(score, ranks, "totalCorrectAnswers") + display(allTimeScore, allTimeRanks, "totalCorrectAnswers") + `
${this.tr`Rank`} | ${this.tr`User`} | ${this.tr`Leaderboard score`} | ${this.tr`Total game points`} | ${this.tr`Total correct answers`} |
---|---|---|---|---|
${i + 1} | ${leaderid} | ${rank.score} | ${rank.totalPoints} | ${rank.totalCorrectAnswers} |
Arts & Entertainment
, Pokémon
, Science & Geography
, Society & Humanities
, Random
, and All
./trivia new [mode], [categories], [length]
- Begin signups for a new Trivia game. [categories]
can be either one category, or a +
-separated list of categories. Requires: + % @ # ~/trivia unrankednew [mode], [category], [length]
- Begin a new Trivia game that does not award leaderboard points. Requires: + % @ # ~/trivia sortednew [mode], [category], [length]
\u2014 Begin a new Trivia game in which the question order is not randomized. Requires: + % @ # ~/trivia join
- Join a game of Trivia or Mastermind during signups./trivia start
- Begin the game once enough users have signed up. Requires: + % @ # ~/ta [answer]
- Answer the current question./trivia kick [username]
- Disqualify a participant from the current trivia game. Requires: % @ # ~/trivia leave
- Makes the player leave the game./trivia end
- End a trivia game. Requires: + % @ # ~/trivia win
- End a trivia game and tally the points to find winners. Requires: + % @ # ~ in Infinite length, else # ~/trivia pause
- Pauses a trivia game. Requires: + % @ # ~/trivia resume
- Resumes a paused trivia game. Requires: + % @ # ~/trivia submit [category] | [question] | [answer1], [answer2] ... [answern]
- Adds question(s) to the submission database for staff to review. Requires: + % @ # ~/trivia review
- View the list of submitted questions. Requires: @ # ~/trivia accept [index1], [index2], ... [indexn] OR all
- Add questions from the submission database to the question database using their index numbers or ranges of them. Requires: @ # ~/trivia reject [index1], [index2], ... [indexn] OR all
- Remove questions from the submission database using their index numbers or ranges of them. Requires: @ # ~/trivia add [category] | [question] | [answer1], [answer2], ... [answern]
- Adds question(s) to the question database. Requires: % @ # ~/trivia delete [question]
- Delete a question from the trivia database. Requires: % @ # ~/trivia move [category] | [question]
- Change the category of question in the trivia database. Requires: % @ # ~/trivia migrate [source category], [destination category]
\u2014 Moves all questions in a category to another category. Requires: # ~${"/trivia edit | | "}
: Edit a question in the trivia database, replacing answers if specified. Requires: % @ # ~` + import_lib.Utils.html`${"/trivia edit answers | | "}
: Replaces the answers of a question. Requires: % @ # ~${"/trivia edit question | | "}
: Edits only the text of a question. Requires: % @ # ~/trivia qs
- View the distribution of questions in the question database./trivia qs [category]
- View the questions in the specified category. Requires: % @ # ~/trivia clearqs [category]
- Clear all questions in the given category. Requires: # ~/trivia moveusedevent
- Tells you whether or not moving used event questions to a different category is enabled./trivia moveusedevent [on or off]
- Toggles moving used event questions to a different category. Requires: # ~/trivia search [type], [query]
- Searches for questions based on their type and their query. Valid types: submissions
, subs
, questions
, qs
. Requires: + % @ # ~/trivia casesensitivesearch [type], [query]
- Like /trivia search
, but is case sensitive (i.e., capitalization matters). Requires: + % @ * ~/trivia status [player]
- lists the player's standings (your own if no player is specified) and the list of players in the current trivia game./trivia rank [username]
- View the rank of the specified user. If none is given, view your own./trivia history
- View a list of the 10 most recently played trivia games./trivia lastofficialscore
- View the scores from the last Trivia game. Intended for bots./trivia ladder [n]
- Displays the top [n]
users on the cycle-specific Trivia leaderboard. If [n]
isn't specified, shows 100 users./trivia alltimewinsladder
- Like /trivia ladder
, but displays the all-time wins Trivia leaderboard (formerly all-time)./trivia alltimescoreladder
- Like /trivia ladder
, but displays the all-time score Trivia leaderboard (formerly non\u2014all-time)/trivia resetcycleleaderboard
- Resets the cycle-specific Trivia leaderboard. Requires: # ~ /trivia mergescore [user]
\u2014 Merge another user's Trivia leaderboard score with yours./trivia addpoints [user], [points]
- Add points to a given user's score on the Trivia leaderboard. Requires: # ~/trivia removepoints [user], [points]
- Remove points from a given user's score on the Trivia leaderboard. Requires: # ~/trivia removeleaderboardentry [user]
\u2014 Remove all Trivia leaderboard entries for a user. Requires: # ~/mastermind new [number of finalists]
: starts a new game of Mastermind with the specified number of finalists. Requires: + % @ # ~`,
`/mastermind start [category], [length in seconds], [player]
: starts a round of Mastermind for a player. Requires: + % @ # ~`,
`/mastermind finals [length in seconds]
: starts the Mastermind finals. Requires: + % @ # ~`,
`/mastermind kick [user]
: kicks a user from the current game of Mastermind. Requires: % @ # ~`,
`/mastermind join
: joins the current game of Mastermind.`,
`/mastermind answer OR /mma [answer]
: answers a question in a round of Mastermind.`,
`/mastermind pass OR /mmp
: passes on the current question. Must be the player of the current round of Mastermind.`
];
return this.sendReplyBox(
`Mastermind is a game in which each player tries to score as many points as possible in a timed round where only they can answer, and the top X players advance to the finals, which is a timed game of Trivia in which only the first player to answer a question recieves points.