// plugins/quiplash.js - Quiplash game for IRC module.exports = { init(bot) { console.log('🎮 Quiplash plugin initialized'); this.bot = bot; // ================================ // EASY CONFIG - EDIT THIS SECTION // ================================ this.config = { gameChannel: '#quiplash', // ONLY channel where game works minPlayers: 3, // Minimum players to start maxPlayers: 8, // Maximum players allowed promptTimeout: 120000, // 2 minutes to submit answers (in ms) votingTimeout: 60000, // 1 minute to vote (in ms) joinTimeout: 30000 // 30 seconds to join game (in ms) }; // Game state this.gameState = { phase: 'idle', // 'idle', 'joining', 'prompts', 'voting', 'results' players: [], // Array of player nicknames currentPrompt: null, // Current prompt being voted on answers: new Map(), // nick -> answer text votes: new Map(), // nick -> voted_for_nick scores: new Map(), // nick -> total_score promptIndex: 0, // Which prompt we're on timers: { join: null, prompt: null, voting: null } }; // Sample prompts for basic version this.prompts = [ "The worst superhero power: _____", "A rejected Netflix series: _____", "What aliens probably think about humans: _____", "The most useless app idea: _____", "A terrible name for a restaurant: _____", "The worst thing to find in your pocket: _____", "A bad slogan for a dating site: _____", "The weirdest thing to collect: _____", "A terrible excuse for being late: _____", "The worst fortune cookie message: _____", "A rejected crayon color: _____", "The most awkward place to run into your ex: _____", "A bad name for a pet: _____", "The worst thing to hear from a pilot: _____", "A terrible superhero catchphrase: _____" ]; // Track who we've sent prompts to this.promptsSent = new Set(); this.answersReceived = new Set(); }, cleanup(bot) { console.log('🎮 Quiplash plugin cleaned up'); this.clearAllTimers(); this.resetGame(); }, // Clear all active timers clearAllTimers() { Object.values(this.gameState.timers).forEach(timer => { if (timer) clearTimeout(timer); }); this.gameState.timers = { join: null, prompt: null, voting: null }; }, // Reset game to idle state resetGame() { this.clearAllTimers(); this.gameState = { phase: 'idle', players: [], currentPrompt: null, answers: new Map(), votes: new Map(), scores: new Map(), promptIndex: 0, timers: { join: null, prompt: null, voting: null } }; this.promptsSent.clear(); this.answersReceived.clear(); }, // Check if command is in the correct channel isValidChannel(context) { return context.channel === this.config.gameChannel; }, // Start the join phase startJoinPhase(context) { this.resetGame(); this.gameState.phase = 'joining'; this.gameState.players = [context.nick]; this.bot.say(this.config.gameChannel, `🎮 ${context.nick} started a Quiplash game!`); this.bot.say(this.config.gameChannel, `💬 Type !join to play! Need ${this.config.minPlayers}-${this.config.maxPlayers} players.`); this.bot.say(this.config.gameChannel, `⏰ 30 seconds to join...`); // Set join timer this.gameState.timers.join = setTimeout(() => { this.startGameIfReady(); }, this.config.joinTimeout); }, // Add player to game addPlayer(nick) { if (this.gameState.players.includes(nick)) { return { success: false, message: `${nick}: You're already in the game!` }; } if (this.gameState.players.length >= this.config.maxPlayers) { return { success: false, message: `${nick}: Game is full! (${this.config.maxPlayers} players max)` }; } this.gameState.players.push(nick); this.gameState.scores.set(nick, 0); return { success: true, message: `🎮 ${nick} joined! (${this.gameState.players.length}/${this.config.maxPlayers} players)` }; }, // Remove player from game removePlayer(nick) { const index = this.gameState.players.indexOf(nick); if (index === -1) { return { success: false, message: `${nick}: You're not in the game!` }; } this.gameState.players.splice(index, 1); this.gameState.scores.delete(nick); this.gameState.answers.delete(nick); this.gameState.votes.delete(nick); return { success: true, message: `👋 ${nick} left the game. (${this.gameState.players.length} players remaining)` }; }, // Check if we can start the game startGameIfReady() { if (this.gameState.players.length < this.config.minPlayers) { this.bot.say(this.config.gameChannel, `😞 Not enough players (${this.gameState.players.length}/${this.config.minPlayers}). Game cancelled.`); this.resetGame(); return; } this.startPromptPhase(); }, // Start the prompt phase startPromptPhase() { this.gameState.phase = 'prompts'; this.gameState.answers.clear(); this.promptsSent.clear(); this.answersReceived.clear(); // Pick a random prompt const promptText = this.prompts[Math.floor(Math.random() * this.prompts.length)]; this.gameState.currentPrompt = promptText; this.bot.say(this.config.gameChannel, `🎯 Round starting! Check your PMs for the prompt.`); this.bot.say(this.config.gameChannel, `⏰ You have 2 minutes to submit your answer!`); // Send prompt to all players via PM this.gameState.players.forEach(player => { this.bot.say(player, `🎯 Quiplash Prompt: "${promptText}"`); this.bot.say(player, `💬 Reply with: !answer `); this.promptsSent.add(player); }); // Set prompt timer this.gameState.timers.prompt = setTimeout(() => { this.startVotingPhase(); }, this.config.promptTimeout); }, // Handle answer submission submitAnswer(nick, answerText) { if (this.gameState.phase !== 'prompts') { return { success: false, message: 'Not accepting answers right now!' }; } if (!this.gameState.players.includes(nick)) { return { success: false, message: 'You\'re not in the current game!' }; } if (this.gameState.answers.has(nick)) { return { success: false, message: 'You already submitted an answer!' }; } if (!answerText || answerText.trim().length === 0) { return { success: false, message: 'Answer cannot be empty!' }; } if (answerText.length > 100) { return { success: false, message: 'Answer too long! Keep it under 100 characters.' }; } this.gameState.answers.set(nick, answerText.trim()); this.answersReceived.add(nick); // Check if all players have answered if (this.answersReceived.size === this.gameState.players.length) { clearTimeout(this.gameState.timers.prompt); this.startVotingPhase(); } return { success: true, message: `✅ Answer submitted! (${this.answersReceived.size}/${this.gameState.players.length} received)` }; }, // Start voting phase startVotingPhase() { if (this.gameState.answers.size === 0) { this.bot.say(this.config.gameChannel, `😞 No one submitted answers! Game ended.`); this.resetGame(); return; } this.gameState.phase = 'voting'; this.gameState.votes.clear(); this.bot.say(this.config.gameChannel, `🗳️ VOTING TIME!`); this.bot.say(this.config.gameChannel, `📝 Prompt: "${this.gameState.currentPrompt}"`); this.bot.say(this.config.gameChannel, `📋 Answers:`); // Display all answers with numbers const answers = Array.from(this.gameState.answers.entries()); answers.forEach(([author, answer], index) => { this.bot.say(this.config.gameChannel, `${index + 1}) ${answer}`); }); this.bot.say(this.config.gameChannel, `🗳️ Vote with: !vote (You have 1 minute!)`); this.bot.say(this.config.gameChannel, `⚠️ You cannot vote for your own answer!`); // Set voting timer this.gameState.timers.voting = setTimeout(() => { this.showResults(); }, this.config.votingTimeout); }, // Handle vote submission submitVote(nick, voteNumber) { if (this.gameState.phase !== 'voting') { return { success: false, message: 'Not accepting votes right now!' }; } if (!this.gameState.players.includes(nick)) { return { success: false, message: 'You\'re not in the current game!' }; } if (this.gameState.votes.has(nick)) { return { success: false, message: 'You already voted!' }; } const answers = Array.from(this.gameState.answers.entries()); if (voteNumber < 1 || voteNumber > answers.length) { return { success: false, message: `Invalid vote! Choose 1-${answers.length}` }; } const [votedForNick, votedForAnswer] = answers[voteNumber - 1]; // Check if voting for themselves if (votedForNick === nick) { return { success: false, message: 'You cannot vote for your own answer!' }; } this.gameState.votes.set(nick, votedForNick); // Check if all players have voted const eligibleVoters = this.gameState.players.filter(p => this.gameState.answers.has(p)); if (this.gameState.votes.size === eligibleVoters.length) { clearTimeout(this.gameState.timers.voting); this.showResults(); } return { success: true, message: `✅ Vote recorded! (${this.gameState.votes.size}/${eligibleVoters.length} votes)` }; }, // Show results and end game showResults() { this.gameState.phase = 'results'; // Count votes const voteCount = new Map(); for (const votedFor of this.gameState.votes.values()) { voteCount.set(votedFor, (voteCount.get(votedFor) || 0) + 1); } // Update scores for (const [nick, votes] of voteCount.entries()) { const currentScore = this.gameState.scores.get(nick) || 0; this.gameState.scores.set(nick, currentScore + votes); } this.bot.say(this.config.gameChannel, `🏆 RESULTS!`); this.bot.say(this.config.gameChannel, `📝 Prompt: "${this.gameState.currentPrompt}"`); // Show answers with vote counts and authors const answers = Array.from(this.gameState.answers.entries()); answers.forEach(([author, answer]) => { const votes = voteCount.get(author) || 0; const voteText = votes === 1 ? '1 vote' : `${votes} votes`; this.bot.say(this.config.gameChannel, `• "${answer}" - ${author} (${voteText})`); }); // Show current scores this.bot.say(this.config.gameChannel, `📊 Scores:`); const sortedScores = Array.from(this.gameState.scores.entries()) .sort((a, b) => b[1] - a[1]); sortedScores.forEach(([nick, score], index) => { const position = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}.`; this.bot.say(this.config.gameChannel, `${position} ${nick}: ${score} points`); }); this.bot.say(this.config.gameChannel, `🎮 Game complete! Use !quiplash to play again.`); // Reset game after showing results setTimeout(() => { this.resetGame(); }, 5000); }, commands: [ { name: 'quiplash', description: 'Start a new Quiplash game or show current status', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; // Check if in correct channel if (!plugin.isValidChannel(context)) { bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); return; } switch (plugin.gameState.phase) { case 'idle': plugin.startJoinPhase(context); break; case 'joining': const timeLeft = Math.ceil((plugin.config.joinTimeout - (Date.now() - Date.now())) / 1000); bot.say(target, `🎮 Game starting soon! Players: ${plugin.gameState.players.join(', ')}`); bot.say(target, `💬 Type !join to play!`); break; case 'prompts': bot.say(target, `🎯 Game in progress! Waiting for answers to: "${plugin.gameState.currentPrompt}"`); bot.say(target, `📊 Received: ${plugin.answersReceived.size}/${plugin.gameState.players.length}`); break; case 'voting': bot.say(target, `🗳️ Voting in progress! Use !vote `); const eligibleVoters = plugin.gameState.players.filter(p => plugin.gameState.answers.has(p)); bot.say(target, `📊 Votes: ${plugin.gameState.votes.size}/${eligibleVoters.length}`); break; case 'results': bot.say(target, `🏆 Game finishing up! New game starting soon...`); break; } } }, { name: 'join', description: 'Join the current Quiplash game', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; if (!plugin.isValidChannel(context)) { bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); return; } if (plugin.gameState.phase !== 'joining') { bot.say(target, `${from}: No game to join! Use !quiplash to start one.`); return; } const result = plugin.addPlayer(from); bot.say(target, result.message); } }, { name: 'leave', description: 'Leave the current Quiplash game', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; if (!plugin.isValidChannel(context)) { bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); return; } if (plugin.gameState.phase === 'idle') { bot.say(target, `${from}: No active game to leave!`); return; } const result = plugin.removePlayer(from); bot.say(target, result.message); // Check if too few players remain if (plugin.gameState.players.length < plugin.config.minPlayers && plugin.gameState.phase !== 'idle') { bot.say(target, `😞 Too few players remaining. Game cancelled.`); plugin.resetGame(); } } }, { name: 'vote', description: 'Vote for an answer during voting phase', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const args = context.args; if (!plugin.isValidChannel(context)) { return; // Silently ignore votes in wrong channel } if (args.length === 0) { bot.say(target, `${from}: Usage: !vote `); return; } const voteNumber = parseInt(args[0]); if (isNaN(voteNumber)) { bot.say(target, `${from}: Vote must be a number!`); return; } const result = plugin.submitVote(from, voteNumber); if (!result.success) { bot.say(target, `${from}: ${result.message}`); } else { // Send confirmation via PM to avoid spam bot.say(from, result.message); } } }, { name: 'answer', description: 'Submit your answer (use in PM)', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const args = context.args; // This command should only work in PM if (context.channel) { bot.say(target, `${from}: Send your answer via PM! Type /msg ${bot.config.nick} !answer `); return; } if (args.length === 0) { bot.say(target, 'Usage: !answer '); return; } const answer = args.join(' '); const result = plugin.submitAnswer(from, answer); bot.say(target, result.message); } }, { name: 'players', description: 'Show current players in the game', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; if (!plugin.isValidChannel(context)) { bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); return; } if (plugin.gameState.players.length === 0) { bot.say(target, 'No active game. Use !quiplash to start one!'); } else { bot.say(target, `🎮 Players (${plugin.gameState.players.length}): ${plugin.gameState.players.join(', ')}`); } } } ] };