- Added hunt/feed duck mechanics (80% hunt, 20% feed) - Implemented persistent scoring system - Added channel control commands (\!stopducks/\!startducks) - Enhanced duck hunt with wrong action penalties - Organized bot structure with botmain.js as main file - Added comprehensive documentation (README.md) - Included 17 plugins with various games and utilities 🦆 Duck Hunt Features: - Hunt ducks with \!shoot/\!bang (80% of spawns) - Feed ducks with \!feed (20% of spawns) - Persistent scores saved to JSON - Channel-specific controls for #bakedbeans - Reaction time tracking and special achievements 🎮 Other Games: - Casino games (slots, coinflip, hi-lo, scratch cards) - Multiplayer games (pigs, zombie dice, quiplash) - Text generation (babble, conspiracy, drunk historian) - Interactive features (story writing, emojify, combos) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
517 lines
No EOL
20 KiB
JavaScript
517 lines
No EOL
20 KiB
JavaScript
// 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 <your funny response>`);
|
|
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 <number> (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 <number>`);
|
|
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 <number>`);
|
|
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 <your response>`);
|
|
return;
|
|
}
|
|
|
|
if (args.length === 0) {
|
|
bot.say(target, 'Usage: !answer <your funny response>');
|
|
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(', ')}`);
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}; |