megabot/plugins/duck_hunt_plugin.js
megaproxy a3ed25f8dd Initial commit: Enhanced IRC bot with duck hunt game
- 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>
2025-07-17 19:03:45 +00:00

597 lines
No EOL
24 KiB
JavaScript

// plugins/duck-hunt.js - Duck Hunt Game Plugin
const fs = require('fs');
const path = require('path');
module.exports = {
gameState: {
activeDucks: new Map(), // channel -> duck data
lastActivity: new Map(), // channel -> timestamp
duckTimers: new Map(), // channel -> timer
scores: new Map(), // nick -> score
channelActivity: new Map(), // channel -> activity count
enabledChannels: new Map() // channel -> enabled status (default true)
},
duckTypes: [
{ emoji: '🦆', name: 'Mallard Duck', points: 1, rarity: 'common', sound: 'Quack!' },
{ emoji: '🦢', name: 'Swan', points: 2, rarity: 'uncommon', sound: 'Honk!' },
{ emoji: '🐧', name: 'Penguin', points: 3, rarity: 'rare', sound: 'Squawk!' },
{ emoji: '🦅', name: 'Eagle', points: 5, rarity: 'epic', sound: 'SCREECH!' },
{ emoji: '🦉', name: 'Owl', points: 4, rarity: 'rare', sound: 'Hoot!' },
{ emoji: '🪿', name: 'Goose', points: 2, rarity: 'uncommon', sound: 'HONK HONK!' },
{ emoji: '🦜', name: 'Parrot', points: 3, rarity: 'rare', sound: 'Polly wants a cracker!' }
],
rareDucks: [
{ emoji: '🏆', name: 'Golden Duck', points: 10, rarity: 'legendary', sound: '*shimmers*' },
{ emoji: '💎', name: 'Diamond Duck', points: 15, rarity: 'mythical', sound: '*sparkles*' },
{ emoji: '🌟', name: 'Cosmic Duck', points: 20, rarity: 'cosmic', sound: '*otherworldly quack*' }
],
init(bot) {
console.log('Duck Hunt plugin initialized - Get ready to hunt! 🦆🔫');
// Set up score file path
this.scoresFile = path.join(__dirname, 'duck_hunt_scores.json');
// Load existing scores
this.loadScores();
this.startDuckSpawning(bot);
},
cleanup(bot) {
console.log('Duck Hunt plugin cleaned up - Ducks are safe... for now');
// Save scores before cleanup
this.saveScores();
// Clear all timers
this.gameState.duckTimers.forEach(timer => clearTimeout(timer));
this.gameState.duckTimers.clear();
},
commands: [
{
name: 'shoot',
description: 'Shoot a duck if one is present',
execute(context, bot) {
module.exports.shootDuck(context, bot);
}
},
{
name: 'bang',
description: 'Alternative shoot command',
execute(context, bot) {
module.exports.shootDuck(context, bot);
}
},
{
name: 'duckscore',
description: 'Check your duck hunting score',
execute(context, bot) {
const score = module.exports.gameState.scores.get(context.nick) || 0;
bot.say(context.replyTo, `🎯 ${context.nick} has shot ${score} ducks!`);
}
},
{
name: 'topducks',
description: 'Show top duck hunters',
execute(context, bot) {
module.exports.showLeaderboard(context, bot);
}
},
{
name: 'duckstats',
description: 'Show duck hunting statistics',
execute(context, bot) {
module.exports.showStats(context, bot);
}
},
{
name: 'stopducks',
description: 'Stop duck spawning in this channel (bakedbeans only)',
execute(context, bot) {
module.exports.stopDucks(context, bot);
}
},
{
name: 'startducks',
description: 'Resume duck spawning in this channel (bakedbeans only)',
execute(context, bot) {
module.exports.startDucks(context, bot);
}
},
{
name: 'feed',
description: 'Feed a duck if one is present',
execute(context, bot) {
module.exports.feedDuck(context, bot);
}
}
],
// Handle all channel messages to track activity
onMessage(data, bot) {
if (data.isChannel) {
this.trackActivity(data.target);
this.scheduleNextDuck(data.target, bot);
}
},
trackActivity(channel) {
const now = Date.now();
this.gameState.lastActivity.set(channel, now);
// Increment activity counter
const currentActivity = this.gameState.channelActivity.get(channel) || 0;
this.gameState.channelActivity.set(channel, currentActivity + 1);
},
scheduleNextDuck(channel, bot) {
// Don't schedule if there's already a timer or active duck
if (this.gameState.duckTimers.has(channel) || this.gameState.activeDucks.has(channel)) {
return;
}
// Check if ducks are enabled for this channel
if (!this.isDuckHuntEnabled(channel)) {
return;
}
// Random delay between 6-20 minutes, but weighted towards longer times
const minDelay = 6 * 60 * 1000; // 6 minutes
const maxDelay = 20 * 60 * 1000; // 20 minutes
// Use exponential distribution to favor longer waits
const randomFactor = Math.random() * Math.random(); // 0-1, skewed towards 0
const delay = minDelay + (randomFactor * (maxDelay - minDelay));
console.log(`🦆 Scheduling duck for ${channel} in ${Math.round(delay/1000)} seconds`);
const timer = setTimeout(() => {
this.spawnDuck(channel, bot);
}, delay);
this.gameState.duckTimers.set(channel, timer);
},
spawnDuck(channel, bot) {
// Clear the timer
this.gameState.duckTimers.delete(channel);
// Check if channel has been active recently (within last 30 minutes)
const lastActivity = this.gameState.lastActivity.get(channel) || 0;
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
if (lastActivity < thirtyMinutesAgo) {
console.log(`🦆 Channel ${channel} inactive, duck going back to sleep`);
return; // Channel is quiet, don't spawn duck
}
// Don't spawn if there's already an active duck
if (this.gameState.activeDucks.has(channel)) {
return;
}
const duck = this.getRandomDuck();
const spawnTime = Date.now();
// Randomly determine if this is a hunt duck (80%) or feed duck (20%)
const isHuntDuck = Math.random() < 0.8;
// Duck flies away after 45-90 seconds if not shot/fed
const flyAwayTime = 45000 + (Math.random() * 45000);
this.gameState.activeDucks.set(channel, {
duck: duck,
spawnTime: spawnTime,
isHuntDuck: isHuntDuck,
timer: setTimeout(() => {
this.duckFliesAway(channel, bot, duck);
}, flyAwayTime)
});
// Duck spawn messages based on type
let spawnMessages;
if (isHuntDuck) {
spawnMessages = [
`${duck.emoji} *${duck.sound}* A ${duck.name} flies into the channel! Quick, !shoot it!`,
`${duck.emoji} *WHOOSH* A ${duck.name} appears! Someone !shoot it before it escapes!`,
`${duck.emoji} *${duck.sound}* Look! A wild ${duck.name}! Type !shoot to hunt it!`,
`${duck.emoji} *flutters* A ${duck.name} lands nearby! !shoot to bag it!`,
`${duck.emoji} *${duck.sound}* A ${duck.name} swoops in! !bang or !shoot to take it down!`
];
} else {
spawnMessages = [
`${duck.emoji} *${duck.sound}* A hungry ${duck.name} waddles into the channel! !feed it some bread!`,
`${duck.emoji} *chirp chirp* A ${duck.name} looks around for food! Quick, !feed it!`,
`${duck.emoji} *${duck.sound}* A friendly ${duck.name} approaches peacefully! !feed it something tasty!`,
`${duck.emoji} *peep peep* A ${duck.name} seems hungry! !feed it before it leaves!`,
`${duck.emoji} *${duck.sound}* A gentle ${duck.name} lands nearby looking for food! !feed it!`
];
}
const message = this.randomChoice(spawnMessages);
bot.say(channel, message);
console.log(`🦆 Spawned ${duck.name} in ${channel}`);
},
duckFliesAway(channel, bot, duck) {
this.gameState.activeDucks.delete(channel);
const escapeMessages = [
`${duck.emoji} *${duck.sound}* The ${duck.name} got away! Better luck next time!`,
`${duck.emoji} *whoosh* The ${duck.name} flies away safely! Too slow, hunters!`,
`${duck.emoji} *${duck.sound}* Nobody shot the ${duck.name}! It escapes into the sunset!`,
`${duck.emoji} The ${duck.name} laughs at your slow reflexes and flies away!`
];
bot.say(channel, this.randomChoice(escapeMessages));
console.log(`🦆 Duck escaped from ${channel}`);
// Schedule next duck
this.scheduleNextDuck(channel, bot);
},
shootDuck(context, bot) {
const channel = context.channel;
if (!channel) {
bot.say(context.replyTo, '🦆 You can only hunt ducks in channels!');
return;
}
const activeDuck = this.gameState.activeDucks.get(channel);
if (!activeDuck) {
const failMessages = [
'🎯 *BANG!* You shoot at empty air! No ducks here!',
'🔫 *click* Nothing to shoot! Wait for a duck to appear!',
'🦆 The only thing you hit was your own ego! No ducks present!',
'💨 *WHOOSH* Your bullet flies into the void! No targets!',
'🎪 *BANG!* You\'ve shot a circus balloon! Still not a duck!'
];
bot.say(context.replyTo, this.randomChoice(failMessages));
return;
}
// Check if this is a feed duck (wrong action)
if (!activeDuck.isHuntDuck) {
const duck = activeDuck.duck;
const wrongActionMessages = [
`😱 *BANG!* You shot at the friendly ${duck.name}! It flies away terrified! You monster!`,
`🚫 *BOOM!* The peaceful ${duck.name} was just hungry! Now it's gone because of you!`,
`💔 *BLAM!* You scared away the gentle ${duck.name}! It just wanted food!`,
`😰 *BANG!* The ${duck.name} flees in terror! Wrong action, hunter!`,
`🦆💨 *BOOM!* You frightened the hungry ${duck.name}! It escapes without eating!`
];
// Clear the duck
clearTimeout(activeDuck.timer);
this.gameState.activeDucks.delete(channel);
bot.say(context.replyTo, this.randomChoice(wrongActionMessages));
console.log(`🦆 ${context.nick} tried to shoot a feed duck in ${channel}`);
// Schedule next duck
this.scheduleNextDuck(channel, bot);
return;
}
// Clear the fly-away timer
clearTimeout(activeDuck.timer);
// Calculate reaction time
const reactionTime = Date.now() - activeDuck.spawnTime;
const reactionSeconds = (reactionTime / 1000).toFixed(2);
// Remove duck from active ducks
this.gameState.activeDucks.delete(channel);
// Award points
const duck = activeDuck.duck;
const currentScore = this.gameState.scores.get(context.nick) || 0;
this.gameState.scores.set(context.nick, currentScore + duck.points);
// Save scores to file after each duck shot
this.saveScores();
// Success messages with variety
const successMessages = [
`🎯 *BANG!* ${context.nick} shot the ${duck.name}! +${duck.points} points! (${reactionSeconds}s reaction time)`,
`🔫 *BOOM!* ${context.nick} bagged a ${duck.name}! +${duck.points} points in ${reactionSeconds} seconds!`,
`🏆 Nice shot, ${context.nick}! You got the ${duck.name} worth ${duck.points} points! (${reactionSeconds}s)`,
`💥 *BLAM!* ${context.nick} is a crack shot! ${duck.name} down for ${duck.points} points! Reaction: ${reactionSeconds}s`,
`🎪 ${context.nick} with the quick draw! ${duck.name} eliminated! +${duck.points} points (${reactionSeconds}s)`
];
// Add special messages for rare ducks
if (duck.rarity === 'legendary' || duck.rarity === 'mythical' || duck.rarity === 'cosmic') {
bot.say(channel, `🌟 ✨ RARE DUCK ALERT! ✨ 🌟`);
}
bot.say(channel, this.randomChoice(successMessages));
// Extra flair for special achievements
if (reactionTime < 1000) {
bot.say(channel, '⚡ LIGHTNING FAST! That was under 1 second!');
} else if (duck.points >= 10) {
bot.say(channel, `💎 LEGENDARY SHOT! You got a ${duck.rarity} duck!`);
}
console.log(`🎯 ${context.nick} shot ${duck.name} in ${channel} (${reactionSeconds}s)`);
// Schedule next duck
this.scheduleNextDuck(channel, bot);
},
feedDuck(context, bot) {
const channel = context.channel;
if (!channel) {
bot.say(context.replyTo, '🦆 You can only feed ducks in channels!');
return;
}
const activeDuck = this.gameState.activeDucks.get(channel);
if (!activeDuck) {
const failMessages = [
'🍞 You scatter breadcrumbs in the empty air! No ducks here!',
'🌾 You hold out seeds but there\'s nothing to feed!',
'🦆 You make feeding noises but no ducks are around!',
'🥖 Your bread goes stale waiting for a duck to appear!',
'🍀 You offer lettuce to the void! Where are the ducks?'
];
bot.say(context.replyTo, this.randomChoice(failMessages));
return;
}
// Check if this is a hunt duck (wrong action)
if (activeDuck.isHuntDuck) {
const duck = activeDuck.duck;
const wrongActionMessages = [
`🔫 *WHOOSH* The wild ${duck.name} ignores your food and flies away! You should have shot it!`,
`🏹 *FLAP FLAP* The ${duck.name} doesn't want food, it's a wild hunter! Wrong approach!`,
`🎯 *SWOOSH* The aggressive ${duck.name} flies off! It needed to be hunted, not fed!`,
`💨 *FLUTTER* The ${duck.name} wasn't hungry for food! You missed your shot!`,
`🦆 *ESCAPE* The wild ${duck.name} laughs at your bread and flies away! Should have used !shoot!`
];
// Clear the duck
clearTimeout(activeDuck.timer);
this.gameState.activeDucks.delete(channel);
bot.say(context.replyTo, this.randomChoice(wrongActionMessages));
console.log(`🦆 ${context.nick} tried to feed a hunt duck in ${channel}`);
// Schedule next duck
this.scheduleNextDuck(channel, bot);
return;
}
// Clear the fly-away timer
clearTimeout(activeDuck.timer);
// Calculate reaction time
const reactionTime = Date.now() - activeDuck.spawnTime;
const reactionSeconds = (reactionTime / 1000).toFixed(2);
// Remove duck from active ducks
this.gameState.activeDucks.delete(channel);
// Award points (same as hunting)
const duck = activeDuck.duck;
const currentScore = this.gameState.scores.get(context.nick) || 0;
this.gameState.scores.set(context.nick, currentScore + duck.points);
// Save scores to file after each duck fed
this.saveScores();
// Success messages for feeding
const successMessages = [
`🍞 *nom nom* ${context.nick} fed the ${duck.name}! +${duck.points} points! It waddles away happily! (${reactionSeconds}s reaction time)`,
`🌾 *chirp* ${context.nick} gave seeds to the ${duck.name}! +${duck.points} points in ${reactionSeconds} seconds! So wholesome!`,
`🥖 *happy quack* ${context.nick} shared bread with the ${duck.name}! +${duck.points} points! (${reactionSeconds}s) What a kind soul!`,
`🍀 *munch munch* ${context.nick} fed the ${duck.name} some greens! +${duck.points} points! Reaction: ${reactionSeconds}s`,
`🎪 ${context.nick} the duck whisperer! Fed the ${duck.name} perfectly! +${duck.points} points (${reactionSeconds}s)`
];
// Add special messages for rare ducks
if (duck.rarity === 'legendary' || duck.rarity === 'mythical' || duck.rarity === 'cosmic') {
bot.say(channel, `🌟 ✨ RARE DUCK FEEDING! ✨ 🌟`);
}
bot.say(channel, this.randomChoice(successMessages));
// Extra flair for special achievements
if (reactionTime < 1000) {
bot.say(channel, '⚡ LIGHTNING FAST FEEDING! That was under 1 second!');
} else if (duck.points >= 10) {
bot.say(channel, `💎 LEGENDARY FEEDING! You fed a ${duck.rarity} duck!`);
}
console.log(`🦆 ${context.nick} fed ${duck.name} in ${channel} (${reactionSeconds}s)`);
// Schedule next duck
this.scheduleNextDuck(channel, bot);
},
getRandomDuck() {
const rand = Math.random();
// Rare duck chances (very low)
if (rand < 0.005) { // 0.5% chance for cosmic
return this.rareDucks[2]; // Cosmic Duck
} else if (rand < 0.015) { // 1% more for mythical
return this.rareDucks[1]; // Diamond Duck
} else if (rand < 0.04) { // 2.5% more for legendary
return this.rareDucks[0]; // Golden Duck
}
// Regular ducks with weighted probabilities
const weights = [40, 25, 15, 8, 10, 25, 15]; // Common to rare
const totalWeight = weights.reduce((a, b) => a + b, 0);
const randomWeight = Math.random() * totalWeight;
let currentWeight = 0;
for (let i = 0; i < this.duckTypes.length; i++) {
currentWeight += weights[i];
if (randomWeight <= currentWeight) {
return this.duckTypes[i];
}
}
return this.duckTypes[0]; // Fallback to mallard
},
showLeaderboard(context, bot) {
const scores = Array.from(this.gameState.scores.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
if (scores.length === 0) {
bot.say(context.replyTo, '🦆 No one has shot any ducks yet! Get hunting!');
return;
}
bot.say(context.replyTo, '🏆 TOP DUCK HUNTERS 🏆');
scores.forEach((score, index) => {
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${score[1]} ducks`);
});
},
showStats(context, bot) {
const totalDucks = Array.from(this.gameState.scores.values()).reduce((a, b) => a + b, 0);
const totalHunters = this.gameState.scores.size;
const activeDucks = this.gameState.activeDucks.size;
bot.say(context.replyTo, `🦆 DUCK HUNT STATS 🦆`);
bot.say(context.replyTo, `🎯 Total ducks shot: ${totalDucks}`);
bot.say(context.replyTo, `🏹 Active hunters: ${totalHunters}`);
bot.say(context.replyTo, `🦆 Ducks currently in channels: ${activeDucks}`);
},
startDuckSpawning(bot) {
// Check for inactive channels every 5 minutes and clean up
setInterval(() => {
const now = Date.now();
const thirtyMinutesAgo = now - (30 * 60 * 1000);
this.gameState.lastActivity.forEach((lastActivity, channel) => {
if (lastActivity < thirtyMinutesAgo) {
// Channel is inactive, clear any timers
const timer = this.gameState.duckTimers.get(channel);
if (timer) {
clearTimeout(timer);
this.gameState.duckTimers.delete(channel);
console.log(`🦆 Cleared inactive duck timer for ${channel}`);
}
}
});
}, 5 * 60 * 1000); // Every 5 minutes
},
randomChoice(array) {
return array[Math.floor(Math.random() * array.length)];
},
isDuckHuntEnabled(channel) {
// Default to enabled if not explicitly set
return this.gameState.enabledChannels.get(channel) !== false;
},
stopDucks(context, bot) {
// Only allow this command from #bakedbeans channel
if (context.channel !== '#bakedbeans') {
bot.say(context.replyTo, '🦆 This command can only be used in #bakedbeans!');
return;
}
const channel = context.channel;
// Set channel as disabled
this.gameState.enabledChannels.set(channel, false);
// Clear any existing timer
const timer = this.gameState.duckTimers.get(channel);
if (timer) {
clearTimeout(timer);
this.gameState.duckTimers.delete(channel);
}
// Remove any active duck
const activeDuck = this.gameState.activeDucks.get(channel);
if (activeDuck) {
clearTimeout(activeDuck.timer);
this.gameState.activeDucks.delete(channel);
}
bot.say(context.replyTo, '🦆 Duck hunt disabled in this channel! Use !startducks to resume.');
console.log(`🦆 Duck hunt disabled in ${channel} by ${context.nick}`);
},
startDucks(context, bot) {
// Only allow this command from #bakedbeans channel
if (context.channel !== '#bakedbeans') {
bot.say(context.replyTo, '🦆 This command can only be used in #bakedbeans!');
return;
}
const channel = context.channel;
// Set channel as enabled
this.gameState.enabledChannels.set(channel, true);
bot.say(context.replyTo, '🦆 Duck hunt resumed in this channel! Ducks will start spawning again.');
console.log(`🦆 Duck hunt enabled in ${channel} by ${context.nick}`);
// Schedule the next duck
this.scheduleNextDuck(channel, bot);
},
// Load scores from file
loadScores() {
try {
if (fs.existsSync(this.scoresFile)) {
const data = fs.readFileSync(this.scoresFile, 'utf8');
const scoresObject = JSON.parse(data);
// Convert back to Map
this.gameState.scores = new Map(Object.entries(scoresObject));
console.log(`🦆 Loaded ${this.gameState.scores.size} duck hunter scores from ${this.scoresFile}`);
// Log top 3 hunters if any exist
if (this.gameState.scores.size > 0) {
const topHunters = Array.from(this.gameState.scores.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
console.log('🏆 Top duck hunters:', topHunters.map(([name, score]) => `${name}(${score})`).join(', '));
}
} else {
console.log(`🦆 No existing duck hunt scores file found, starting fresh`);
}
} catch (error) {
console.error(`❌ Error loading duck hunt scores:`, error);
this.gameState.scores = new Map(); // Reset to empty if load fails
}
},
// Save scores to file
saveScores() {
try {
// Convert Map to plain object for JSON serialization
const scoresObject = Object.fromEntries(this.gameState.scores);
const data = JSON.stringify(scoresObject, null, 2);
fs.writeFileSync(this.scoresFile, data, 'utf8');
console.log(`💾 Saved ${this.gameState.scores.size} duck hunter scores to ${this.scoresFile}`);
} catch (error) {
console.error(`❌ Error saving duck hunt scores:`, error);
}
}
};