2021-11-06 00:33:52 +00:00
'use strict' ;
const config = require ( 'config' ) ;
const timers = require ( 'timers/promises' ) ;
2021-11-15 19:29:13 +00:00
const { decode } = require ( 'html-entities' ) ;
2021-11-06 00:33:52 +00:00
const questions = require ( '../../assets/jeopardy.json' ) ;
2021-11-15 14:51:10 +00:00
const shuffle = require ( '../utils/shuffle' ) ;
2022-10-17 02:06:55 +00:00
const style = require ( '../utils/style' ) ;
2021-11-06 00:33:52 +00:00
2021-11-08 01:48:26 +00:00
const settings = { ... config . trivia } ;
2022-01-07 21:52:44 +00:00
const help = {
mode : '\'first\' or \'timeout\'' ,
rounds : 'rounds per game as a number' ,
timeout : 'seconds as a number' ,
} ;
2021-11-06 00:33:52 +00:00
2022-10-17 02:06:55 +00:00
// let game = null;
const games = new Map ( ) ;
2021-11-06 00:33:52 +00:00
function scoreRound ( context , round ) {
2022-10-17 02:06:55 +00:00
const game = games . get ( context . room . id ) ;
2022-01-10 22:42:17 +00:00
if ( game . answers . size === 0 ) {
2021-11-06 00:33:52 +00:00
return ` No one scored in round ${ round + 1 } , better luck next time! ` ;
}
2022-01-10 22:42:17 +00:00
return Array . from ( game . answers . values ( ) ) . map ( ( { user } ) => {
2021-11-09 13:51:37 +00:00
if ( user ) {
context . setPoints ( user , 1 ) ;
game . points [ user . username ] = ( game . points [ user . username ] || 0 ) + 1 ;
2021-11-06 00:33:52 +00:00
2022-10-17 02:06:55 +00:00
return ` ${ style . bold ( style . cyan ( ` ${ config . usernamePrefix } ${ user . username } ` ) ) } gets a point ` ;
2021-11-09 13:51:37 +00:00
}
return null ;
} ) . filter ( Boolean ) . join ( ', ' ) ;
2021-11-06 00:33:52 +00:00
}
2022-10-17 02:06:55 +00:00
function getLeaders ( context ) {
const game = games . get ( context . room . id ) ;
2022-07-15 15:46:12 +00:00
return Object . entries ( game . points ) . sort ( ( [ , scoreA ] , [ , scoreB ] ) => scoreB - scoreA ) . map ( ( [ username , score ] , index ) => {
if ( index === 0 ) {
2022-10-17 02:06:55 +00:00
return ` ${ style . bold ( ` ${ config . usernamePrefix } ${ username } ` ) } with ${ style . bold ( ` ${ score } ` ) } points ` ;
2022-07-15 15:46:12 +00:00
}
2022-10-17 02:06:55 +00:00
return ` ${ style . bold ( style . cyan ( ` ${ config . usernamePrefix } ${ username } ` ) ) } with ${ style . bold ( ` ${ score } ` ) } points ` ;
2022-07-15 15:46:12 +00:00
} ) . join ( ', ' ) ;
}
2021-11-06 00:33:52 +00:00
async function playRound ( context , round = 0 ) {
2022-10-17 02:06:55 +00:00
const game = games . get ( context . room . id ) ;
2021-11-06 00:33:52 +00:00
const ac = new AbortController ( ) ; // eslint-disable-line no-undef
const now = new Date ( ) ;
2022-01-10 22:42:17 +00:00
game . answers = new Map ( ) ;
2021-11-06 00:33:52 +00:00
game . round = round ;
game . ac = ac ;
const question = game . questions [ round ] ;
2022-10-18 23:24:13 +00:00
context . sendMessage ( ` ${ style . bold ( style . purple ( ` Question ${ round + 1 } / ${ game . questions . length } ` ) ) } ${ style . silver ( ` ( ${ question . category } ) ` ) } : ${ question . question } ` , context . room . id ) ;
2021-11-06 00:33:52 +00:00
context . logger . info ( ` Trivia asked " ${ question . question } " with answer: ${ question . answer } ` ) ;
try {
2021-11-14 16:56:07 +00:00
await timers . setTimeout ( ( game . timeout / 3 ) * 1000 , null , {
signal : ac . signal ,
} ) ;
// replace space with U+2003 Em Space to separate words, since a single space separates the placeholders, and double spaces are removed during Markdown render
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( style . green ( ` ${ Math . floor ( game . timeout / 3 ) * 2 } seconds ` ) ) } left, first hint for ${ style . bold ( style . purple ( ` question ${ round + 1 } / ${ game . questions . length } ` ) ) } : ${ style . bold ( ` ${ question . answer . slice ( 0 , 1 ) } ${ question . answer . slice ( 1 ) . replace ( /\s/g , ' ' ) . replace ( /[^\s]/g , '_ ' ) . trim ( ) } ` ) } ` , context . room . id ) ;
2021-11-14 16:56:07 +00:00
await timers . setTimeout ( ( game . timeout / 3 ) * 1000 , null , {
signal : ac . signal ,
} ) ;
if ( question . answer . length > 3 ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( style . green ( ` ${ Math . floor ( game . timeout / 3 ) } seconds ` ) ) } left, second hint for ${ style . bold ( style . purple ( ` question ${ round + 1 } / ${ game . questions . length } ` ) ) } : ${ style . bold ( ` ${ question . answer . slice ( 0 , 1 ) } ${ question . answer . slice ( 1 , - 1 ) . replace ( /\s/g , ' ' ) . replace ( /[^\s]/g , '_ ' ) } ${ question . answer . slice ( - 1 ) } ` ) } ` , context . room . id ) ;
2021-11-14 16:56:07 +00:00
}
await timers . setTimeout ( ( game . timeout / 3 ) * 1000 , null , {
2021-11-06 00:33:52 +00:00
signal : ac . signal ,
} ) ;
} catch ( error ) {
2021-11-14 16:56:07 +00:00
// abort expected, probably not an error
2021-11-06 00:33:52 +00:00
}
if ( ! ac . signal . aborted ) {
ac . abort ( ) ;
}
if ( game . stopped ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` The game was stopped by ${ style . cyan ( ` ${ config . usernamePrefix } ${ game . stopped . username } ` ) } . The answer to the last question was: ${ style . bold ( question . answer ) } . Best players: ${ getLeaders ( context ) } ` , context . room . id ) ;
games . delete ( context . room . id ) ;
2021-11-06 00:33:52 +00:00
return ;
}
2022-01-10 22:42:17 +00:00
if ( game . answers . size === 0 ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( style . red ( 'TIME\'S UP!' ) ) } No one guessed the answer: ${ style . bold ( question . answer ) } ` , context . room . id ) ;
2021-11-06 00:33:52 +00:00
} else {
const scores = scoreRound ( context , round ) ;
if ( game . mode === 'first' ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( style . yellow ( question . fullAnswer || question . answer ) ) } is the right answer, played in ${ style . bold ( style . green ( ` ${ ( ( new Date ( ) - now ) / 1000 ) . toFixed ( 3 ) } s ` ) ) } ! ${ scores } ` , context . room . id ) ;
2021-11-06 00:33:52 +00:00
}
if ( game . mode === 'timeout' ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( style . red ( 'STOP!' ) ) } The correct answer is ${ style . bold ( style . green ( question . fullAnswer || question . answer ) ) } . ${ scores } ` , context . room . id ) ;
2021-11-06 00:33:52 +00:00
}
}
if ( round < game . questions . length - 1 ) {
await timers . setTimeout ( 5000 ) ;
if ( game . stopped ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` The game was stopped by ${ config . usernamePrefix } ${ game . stopped . username } . The answer to the last question was: ${ style . bold ( question . answer ) } . Best players: ${ getLeaders ( context ) } ` , context . room . id ) ;
games . delete ( context . room . id ) ;
2021-11-06 00:33:52 +00:00
return ;
}
playRound ( context , round + 1 ) ;
return ;
}
2021-11-08 01:48:26 +00:00
await timers . setTimeout ( 2000 ) ;
2022-10-17 22:08:17 +00:00
context . sendMessage ( ` That's the end of the game! Best players: ${ getLeaders ( context ) } ` , context . room . id ) ;
2021-11-06 00:33:52 +00:00
2022-10-17 02:06:55 +00:00
games . delete ( context . room . id ) ;
2021-11-06 00:33:52 +00:00
}
async function start ( context ) {
const roundQuestions = shuffle ( questions , settings . rounds ) ;
2022-10-17 02:06:55 +00:00
games . set ( context . room . id , {
2021-11-06 00:33:52 +00:00
round : 0 ,
questions : roundQuestions ,
answers : [ ] ,
points : { } ,
... settings ,
2022-10-17 02:06:55 +00:00
} ) ;
2021-11-06 00:33:52 +00:00
playRound ( context , 0 ) ;
}
async function stop ( context ) {
2022-10-17 02:06:55 +00:00
const game = games . get ( context . room . id ) ;
2021-11-06 00:33:52 +00:00
game . stopped = context . user ;
game . ac . abort ( ) ;
}
function onCommand ( args , context ) {
2022-10-17 02:06:55 +00:00
const game = games . get ( context . room . id ) ;
2021-11-06 00:33:52 +00:00
if ( ! context . subcommand && ! game ) {
start ( context ) ;
return ;
}
if ( ! context . subcommand && game ) {
2021-11-14 16:56:07 +00:00
context . sendMessage ( ` There is already a game going on! Use ${ config . prefix } trivia:stop to reset. The current question for round ${ game . round + 1 } is: ${ game . questions [ game . round ] . question } ` , context . room . id ) ;
2021-11-06 00:33:52 +00:00
return ;
}
if ( context . subcommand === 'stop' && game ) {
stop ( context ) ;
}
if ( context . subcommand === 'stop' && ! game ) {
context . sendMessage ( ` There is no game going on at the moment. Start one with ${ config . prefix } trivia! ` , context . room . id ) ;
}
2021-11-09 12:48:21 +00:00
2022-01-07 21:52:44 +00:00
if ( [ 'help' , 'commands' ] . includes ( context . subcommand ) ) {
context . sendMessage ( ` Available subcommands for ${ config . prefix } trivia are :stop, :mode, :rounds and :timeout ` , context . room . id ) ;
}
2021-11-09 12:48:21 +00:00
const subcommand = context . subcommand ? . toLowerCase ( ) ;
2021-11-14 16:56:07 +00:00
if ( subcommand && settings [ subcommand ] ) {
2022-01-07 21:52:44 +00:00
if ( args [ 0 ] ) {
2022-10-20 00:45:15 +00:00
const bounds = config . trivia . bounds [ subcommand ] ;
const curatedSetting = typeof settings [ subcommand ] === 'number' ? Number ( args [ 0 ] ) : args [ 0 ] ;
if ( Number . isNaN ( curatedSetting ) ) {
context . sendMessage ( ` ${ subcommand } must be a valid number ` , context . room . id ) ;
}
if ( Array . isArray ( bounds ) && typeof settings [ subcommand ] === 'number' && ( curatedSetting < bounds [ 0 ] || curatedSetting > bounds [ 1 ] ) ) {
context . sendMessage ( ` ${ subcommand } must be between ${ bounds [ 0 ] } and ${ bounds [ 1 ] } ` , context . room . id ) ;
return ;
}
settings [ subcommand ] = curatedSetting ;
2021-11-09 12:48:21 +00:00
2022-01-07 21:52:44 +00:00
context . sendMessage ( ` ${ subcommand } set to ${ settings [ subcommand ] } ` , context . room . id ) ;
} else if ( help [ subcommand ] ) {
context . sendMessage ( ` Please give ${ help [ subcommand ] } ` , context . room . id ) ;
}
2021-11-09 12:48:21 +00:00
}
2021-11-06 00:33:52 +00:00
}
async function onMessage ( message , context ) {
2022-10-18 23:24:13 +00:00
const game = games . get ( context . room ? . id ) ;
2022-10-17 02:06:55 +00:00
2021-11-29 17:44:44 +00:00
if ( ! game || context . user ? . id === config . user ? . id ) {
2021-11-06 00:33:52 +00:00
return ;
}
2022-01-07 21:52:44 +00:00
const { answer , fullAnswer } = game . questions [ game . round ] ;
2021-11-06 00:33:52 +00:00
2022-01-10 22:42:17 +00:00
if ( new RegExp ( answer , 'i' ) . test ( decode ( message . originalBody || message . body ) ) && ! game . answers . has ( context . user . id ) ) { // resolve HTML entities in case original body is not available, such as & to &
game . answers . set ( context . user . id , {
2021-11-06 00:33:52 +00:00
user : context . user ,
answer : message . body ,
} ) ;
if ( settings . mode === 'first' ) {
game . ac . abort ( ) ;
}
2022-01-07 21:52:44 +00:00
2022-01-07 22:39:46 +00:00
if ( settings . mode === 'timeout' && ! game . ac . signal . aborted ) {
2022-01-07 21:52:44 +00:00
if ( message . type === 'message' ) {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( fullAnswer || answer ) } is the correct answer! You might want to ${ style . bold ( '/whisper' ) } the answer to me instead, so others can't leech off your impeccable knowledge. You will receive a point at the end of the round. ` , context . room . id , null , context . user . username ) ;
2022-01-07 21:52:44 +00:00
} else {
2022-10-17 02:06:55 +00:00
context . sendMessage ( ` ${ style . bold ( fullAnswer || answer ) } is the correct answer! You will receive a point at the end of the round. ` , context . room . id , null , context . user . username ) ;
2022-01-07 21:52:44 +00:00
}
}
2021-11-06 00:33:52 +00:00
}
}
module . exports = {
name : 'Trivia' ,
onCommand ,
onMessage ,
} ;