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>
This commit is contained in:
commit
a3ed25f8dd
39 changed files with 12360 additions and 0 deletions
337
plugins/coinflip_plugin.js
Normal file
337
plugins/coinflip_plugin.js
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
// plugins/coinflip.js - Coin Flip Casino game plugin
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
init(bot) {
|
||||
console.log('Coin Flip Casino plugin initialized');
|
||||
this.bot = bot;
|
||||
|
||||
// Persistent player statistics (saved to file)
|
||||
this.playerStats = new Map(); // nick -> { totalWins, totalLosses, totalFlips, biggestWin, winStreak, bestStreak, name }
|
||||
|
||||
// Set up score file path
|
||||
this.scoresFile = path.join(__dirname, 'coinflip_scores.json');
|
||||
|
||||
// Load existing scores
|
||||
this.loadScores();
|
||||
},
|
||||
|
||||
cleanup(bot) {
|
||||
console.log('Coin Flip Casino plugin cleaned up');
|
||||
// Save scores before cleanup
|
||||
this.saveScores();
|
||||
},
|
||||
|
||||
// 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.playerStats = new Map(Object.entries(scoresObject));
|
||||
console.log(`🪙 Loaded ${this.playerStats.size} coin flip player stats from ${this.scoresFile}`);
|
||||
|
||||
// Log top 3 players if any exist
|
||||
if (this.playerStats.size > 0) {
|
||||
const topPlayers = Array.from(this.playerStats.values())
|
||||
.sort((a, b) => b.totalWins - a.totalWins)
|
||||
.slice(0, 3);
|
||||
console.log('🏆 Top coin flippers:', topPlayers.map(p => `${p.name}(${p.totalWins}W)`).join(', '));
|
||||
}
|
||||
} else {
|
||||
console.log(`🪙 No existing coin flip scores file found, starting fresh`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading coin flip scores:`, error);
|
||||
this.playerStats = 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.playerStats);
|
||||
const data = JSON.stringify(scoresObject, null, 2);
|
||||
|
||||
fs.writeFileSync(this.scoresFile, data, 'utf8');
|
||||
console.log(`💾 Saved ${this.playerStats.size} coin flip player stats to ${this.scoresFile}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error saving coin flip scores:`, error);
|
||||
}
|
||||
},
|
||||
|
||||
// Update a player's persistent stats and save to file
|
||||
updatePlayerStats(nick, updates) {
|
||||
// Ensure playerStats Map exists
|
||||
if (!this.playerStats) {
|
||||
this.playerStats = new Map();
|
||||
}
|
||||
|
||||
if (!this.playerStats.has(nick)) {
|
||||
this.playerStats.set(nick, {
|
||||
totalWins: 0,
|
||||
totalLosses: 0,
|
||||
totalFlips: 0,
|
||||
biggestWin: 0,
|
||||
winStreak: 0,
|
||||
bestStreak: 0,
|
||||
name: nick
|
||||
});
|
||||
}
|
||||
|
||||
const player = this.playerStats.get(nick);
|
||||
Object.assign(player, updates);
|
||||
|
||||
// Save to file after each update
|
||||
this.saveScores();
|
||||
},
|
||||
|
||||
// Flip the coin and return result
|
||||
flipCoin() {
|
||||
return Math.random() < 0.5 ? 'heads' : 'tails';
|
||||
},
|
||||
|
||||
// Calculate win amount based on bet
|
||||
calculateWinnings(bet, isWin) {
|
||||
if (!isWin) return 0;
|
||||
|
||||
// Simple 1:1 payout for coin flip
|
||||
return bet;
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: 'flip',
|
||||
description: 'Flip a coin and bet on the outcome',
|
||||
execute: function(context, bot) {
|
||||
const plugin = module.exports;
|
||||
const target = context.replyTo;
|
||||
const from = context.nick;
|
||||
const args = context.args;
|
||||
|
||||
// Parse command: !flip <heads/tails> [bet]
|
||||
if (args.length === 0) {
|
||||
bot.say(target, `Usage: !flip <heads/tails> [bet] - Example: !flip heads 5`);
|
||||
return;
|
||||
}
|
||||
|
||||
const guess = args[0].toLowerCase();
|
||||
if (guess !== 'heads' && guess !== 'tails') {
|
||||
bot.say(target, `${from}: Choose 'heads' or 'tails'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse bet amount (default to 1)
|
||||
let bet = 1;
|
||||
if (args.length > 1) {
|
||||
bet = parseInt(args[1]);
|
||||
if (isNaN(bet) || bet < 1 || bet > 100) {
|
||||
bot.say(target, `${from}: Bet must be between 1 and 100`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize player if new
|
||||
if (!plugin.playerStats.has(from)) {
|
||||
plugin.updatePlayerStats(from, {
|
||||
totalWins: 0,
|
||||
totalLosses: 0,
|
||||
totalFlips: 0,
|
||||
biggestWin: 0,
|
||||
winStreak: 0,
|
||||
bestStreak: 0,
|
||||
name: from
|
||||
});
|
||||
}
|
||||
|
||||
// Flip the coin
|
||||
const result = plugin.flipCoin();
|
||||
const isWin = guess === result;
|
||||
const winnings = plugin.calculateWinnings(bet, isWin);
|
||||
|
||||
// Update player stats
|
||||
const player = plugin.playerStats.get(from);
|
||||
const updates = {
|
||||
totalFlips: player.totalFlips + 1
|
||||
};
|
||||
|
||||
if (isWin) {
|
||||
updates.totalWins = player.totalWins + 1;
|
||||
updates.winStreak = player.winStreak + 1;
|
||||
|
||||
// Update best streak
|
||||
if (updates.winStreak > player.bestStreak) {
|
||||
updates.bestStreak = updates.winStreak;
|
||||
}
|
||||
|
||||
// Update biggest win
|
||||
if (winnings > player.biggestWin) {
|
||||
updates.biggestWin = winnings;
|
||||
}
|
||||
} else {
|
||||
updates.totalLosses = player.totalLosses + 1;
|
||||
updates.winStreak = 0; // Reset streak on loss
|
||||
}
|
||||
|
||||
// Choose coin flip animation
|
||||
const coinEmojis = ['🪙', '🥇', '💰'];
|
||||
const coinEmoji = coinEmojis[Math.floor(Math.random() * coinEmojis.length)];
|
||||
|
||||
// Result message with color coding
|
||||
let resultEmoji, resultText;
|
||||
if (result === 'heads') {
|
||||
resultEmoji = '👑';
|
||||
resultText = 'HEADS';
|
||||
} else {
|
||||
resultEmoji = '🦅';
|
||||
resultText = 'TAILS';
|
||||
}
|
||||
|
||||
// Create result message
|
||||
let output = `${coinEmoji} ${from}: ${resultEmoji} ${resultText}! `;
|
||||
|
||||
if (isWin) {
|
||||
output += `✅ WIN! +${winnings} pts`;
|
||||
if (updates.winStreak > 1) {
|
||||
output += ` | 🔥 ${updates.winStreak} streak!`;
|
||||
}
|
||||
} else {
|
||||
output += `❌ LOSE! -${bet} pts`;
|
||||
}
|
||||
|
||||
// Add totals
|
||||
output += ` | W:${updates.totalWins} L:${updates.totalLosses}`;
|
||||
|
||||
bot.say(target, output);
|
||||
plugin.updatePlayerStats(from, updates);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'flipstats',
|
||||
description: 'Show your coin flip statistics',
|
||||
execute: function(context, bot) {
|
||||
const plugin = module.exports;
|
||||
const target = context.replyTo;
|
||||
const from = context.nick;
|
||||
|
||||
if (!plugin.playerStats.has(from)) {
|
||||
bot.say(target, `${from}: You haven't flipped any coins yet! Use !flip to start.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const player = plugin.playerStats.get(from);
|
||||
const winRate = player.totalFlips > 0 ? ((player.totalWins / player.totalFlips) * 100).toFixed(1) : 0;
|
||||
|
||||
bot.say(target, `🪙 ${from}: ${player.totalWins}W/${player.totalLosses}L (${winRate}%) | Best: ${player.biggestWin} | Streak: ${player.winStreak} (best: ${player.bestStreak}) | Flips: ${player.totalFlips}`);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'topflip',
|
||||
description: 'Show top coin flip players',
|
||||
execute: function(context, bot) {
|
||||
const plugin = module.exports;
|
||||
const target = context.replyTo;
|
||||
|
||||
if (plugin.playerStats.size === 0) {
|
||||
bot.say(target, 'No coin flip scores recorded yet! Use !flip to start playing.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get top players by different metrics
|
||||
const byWins = Array.from(plugin.playerStats.values())
|
||||
.sort((a, b) => b.totalWins - a.totalWins)
|
||||
.slice(0, 3);
|
||||
|
||||
const byStreak = Array.from(plugin.playerStats.values())
|
||||
.sort((a, b) => b.bestStreak - a.bestStreak)
|
||||
.slice(0, 1);
|
||||
|
||||
let output = '🏆 Top Flippers: ';
|
||||
byWins.forEach((player, i) => {
|
||||
const trophy = i === 0 ? '🥇' : i === 1 ? '🥈' : '🥉';
|
||||
const winRate = player.totalFlips > 0 ? ((player.totalWins / player.totalFlips) * 100).toFixed(0) : 0;
|
||||
output += `${trophy}${player.name}(${player.totalWins}W/${winRate}%) `;
|
||||
});
|
||||
|
||||
if (byStreak[0] && byStreak[0].bestStreak > 0) {
|
||||
output += `| 🔥 Best Streak: ${byStreak[0].name}(${byStreak[0].bestStreak})`;
|
||||
}
|
||||
|
||||
bot.say(target, output);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'fliptest',
|
||||
description: 'Test coin fairness with multiple flips',
|
||||
execute: function(context, bot) {
|
||||
const plugin = module.exports;
|
||||
const target = context.replyTo;
|
||||
const from = context.nick;
|
||||
const args = context.args;
|
||||
|
||||
// Parse number of flips (default 10, max 100)
|
||||
let numFlips = 10;
|
||||
if (args.length > 0) {
|
||||
numFlips = parseInt(args[0]);
|
||||
if (isNaN(numFlips) || numFlips < 1 || numFlips > 100) {
|
||||
bot.say(target, `${from}: Number of flips must be between 1 and 100`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform multiple flips
|
||||
let heads = 0;
|
||||
let tails = 0;
|
||||
let sequence = '';
|
||||
|
||||
for (let i = 0; i < numFlips; i++) {
|
||||
const result = plugin.flipCoin();
|
||||
if (result === 'heads') {
|
||||
heads++;
|
||||
sequence += '👑';
|
||||
} else {
|
||||
tails++;
|
||||
sequence += '🦅';
|
||||
}
|
||||
}
|
||||
|
||||
const headsPercent = ((heads / numFlips) * 100).toFixed(1);
|
||||
const tailsPercent = ((tails / numFlips) * 100).toFixed(1);
|
||||
|
||||
bot.say(target, `🪙 ${from}: ${numFlips} flips | 👑 ${heads} (${headsPercent}%) | 🦅 ${tails} (${tailsPercent}%)`);
|
||||
bot.say(target, `Sequence: ${sequence}`);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'resetflip',
|
||||
description: 'Reset all coin flip scores (admin command)',
|
||||
execute: function(context, bot) {
|
||||
const plugin = module.exports;
|
||||
const target = context.replyTo;
|
||||
const from = context.nick;
|
||||
|
||||
// Simple admin check
|
||||
const adminNicks = ['admin', 'owner', 'cancerbot', 'megasconed'];
|
||||
|
||||
if (!adminNicks.includes(from)) {
|
||||
bot.say(target, `${from}: Access denied - admin only command`);
|
||||
return;
|
||||
}
|
||||
|
||||
const playerCount = plugin.playerStats.size;
|
||||
plugin.playerStats.clear();
|
||||
plugin.saveScores();
|
||||
|
||||
bot.say(target, `🗑️ ${from}: Reset ${playerCount} coin flip player stats`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue