- 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>
397 lines
No EOL
15 KiB
JavaScript
397 lines
No EOL
15 KiB
JavaScript
// plugins/hilo.js - Hi-Lo Casino game plugin
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
module.exports = {
|
|
init(bot) {
|
|
console.log('Hi-Lo Casino plugin initialized');
|
|
this.bot = bot;
|
|
|
|
// Persistent player statistics (saved to file)
|
|
this.playerStats = new Map(); // nick -> { totalWins, totalLosses, totalGames, biggestWin, winStreak, bestStreak, name }
|
|
|
|
// Active games storage - nick -> { firstCard, bet, challenger }
|
|
this.activeGames = new Map();
|
|
|
|
// Set up score file path
|
|
this.scoresFile = path.join(__dirname, 'hilo_scores.json');
|
|
|
|
// Load existing scores
|
|
this.loadScores();
|
|
},
|
|
|
|
cleanup(bot) {
|
|
console.log('Hi-Lo Casino plugin cleaned up');
|
|
// Save scores before cleanup
|
|
this.saveScores();
|
|
// Clear active games
|
|
this.activeGames.clear();
|
|
},
|
|
|
|
// 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} Hi-Lo 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 Hi-Lo players:', topPlayers.map(p => `${p.name}(${p.totalWins})`).join(', '));
|
|
}
|
|
} else {
|
|
console.log(`🃏 No existing Hi-Lo scores file found, starting fresh`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ Error loading Hi-Lo 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} Hi-Lo player stats to ${this.scoresFile}`);
|
|
} catch (error) {
|
|
console.error(`❌ Error saving Hi-Lo 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,
|
|
totalGames: 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();
|
|
},
|
|
|
|
// Generate random card (1-100)
|
|
generateCard() {
|
|
return Math.floor(Math.random() * 100) + 1;
|
|
},
|
|
|
|
// Get card display with emoji
|
|
getCardDisplay(card) {
|
|
let emoji = '🃏';
|
|
if (card <= 25) emoji = '🟢'; // Low (1-25)
|
|
else if (card <= 50) emoji = '🟡'; // Medium-Low (26-50)
|
|
else if (card <= 75) emoji = '🟠'; // Medium-High (51-75)
|
|
else emoji = '🔴'; // High (76-100)
|
|
|
|
return `${emoji}${card}`;
|
|
},
|
|
|
|
commands: [
|
|
{
|
|
name: 'hilo',
|
|
description: 'Start a Hi-Lo game or guess higher/lower',
|
|
execute: function(context, bot) {
|
|
const plugin = module.exports;
|
|
const target = context.replyTo;
|
|
const from = context.nick;
|
|
const args = context.args;
|
|
|
|
// Check if replying to an existing game
|
|
if (plugin.activeGames.has(from)) {
|
|
bot.say(target, `${from}: You already have an active Hi-Lo game! Use !higuess or !loguess`);
|
|
return;
|
|
}
|
|
|
|
// Check if someone challenged this player
|
|
let challengerGame = null;
|
|
for (const [challenger, game] of plugin.activeGames.entries()) {
|
|
if (game.challenger === from) {
|
|
challengerGame = { challenger, game };
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (challengerGame) {
|
|
bot.say(target, `${from}: ${challengerGame.challenger} is waiting for your !higuess or !loguess response!`);
|
|
return;
|
|
}
|
|
|
|
// Parse command: !hilo <opponent> <bet>
|
|
if (args.length < 2) {
|
|
bot.say(target, `Usage: !hilo <opponent> <bet> - Example: !hilo alice 10`);
|
|
return;
|
|
}
|
|
|
|
const opponent = args[0];
|
|
const bet = parseInt(args[1]);
|
|
|
|
if (isNaN(bet) || bet < 1 || bet > 100) {
|
|
bot.say(target, `${from}: Bet must be between 1 and 100`);
|
|
return;
|
|
}
|
|
|
|
if (opponent === from) {
|
|
bot.say(target, `${from}: You can't challenge yourself!`);
|
|
return;
|
|
}
|
|
|
|
// Initialize players if new
|
|
if (!plugin.playerStats.has(from)) {
|
|
plugin.updatePlayerStats(from, {
|
|
totalWins: 0,
|
|
totalLosses: 0,
|
|
totalGames: 0,
|
|
biggestWin: 0,
|
|
winStreak: 0,
|
|
bestStreak: 0,
|
|
name: from
|
|
});
|
|
}
|
|
|
|
// Generate first card
|
|
const firstCard = plugin.generateCard();
|
|
|
|
// Store the game
|
|
plugin.activeGames.set(from, {
|
|
firstCard: firstCard,
|
|
bet: bet,
|
|
challenger: opponent
|
|
});
|
|
|
|
const cardDisplay = plugin.getCardDisplay(firstCard);
|
|
bot.say(target, `🃏 ${from} challenges ${opponent} to Hi-Lo! First card: ${cardDisplay} | Bet: ${bet} pts | ${opponent}: !higuess or !loguess?`);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'higuess',
|
|
description: 'Guess the next card will be higher',
|
|
execute: function(context, bot) {
|
|
const plugin = module.exports;
|
|
const target = context.replyTo;
|
|
const from = context.nick;
|
|
|
|
plugin.processGuess(target, from, 'higher', bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'loguess',
|
|
description: 'Guess the next card will be lower',
|
|
execute: function(context, bot) {
|
|
const plugin = module.exports;
|
|
const target = context.replyTo;
|
|
const from = context.nick;
|
|
|
|
plugin.processGuess(target, from, 'lower', bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'hilostats',
|
|
description: 'Show your Hi-Lo 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 played Hi-Lo yet! Use !hilo to start.`);
|
|
return;
|
|
}
|
|
|
|
const player = plugin.playerStats.get(from);
|
|
const winRate = player.totalGames > 0 ? ((player.totalWins / player.totalGames) * 100).toFixed(1) : 0;
|
|
|
|
bot.say(target, `🃏 ${from}: ${player.totalWins}W/${player.totalLosses}L (${winRate}%) | Best: ${player.biggestWin} | Streak: ${player.winStreak} (best: ${player.bestStreak}) | Games: ${player.totalGames}`);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'tophilo',
|
|
description: 'Show top Hi-Lo players',
|
|
execute: function(context, bot) {
|
|
const plugin = module.exports;
|
|
const target = context.replyTo;
|
|
|
|
if (plugin.playerStats.size === 0) {
|
|
bot.say(target, 'No Hi-Lo scores recorded yet! Use !hilo 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 Hi-Lo: ';
|
|
byWins.forEach((player, i) => {
|
|
const trophy = i === 0 ? '🥇' : i === 1 ? '🥈' : '🥉';
|
|
const winRate = player.totalGames > 0 ? ((player.totalWins / player.totalGames) * 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: 'resethilo',
|
|
description: 'Reset all Hi-Lo 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.activeGames.clear();
|
|
plugin.saveScores();
|
|
|
|
bot.say(target, `🗑️ ${from}: Reset ${playerCount} Hi-Lo player stats`);
|
|
}
|
|
}
|
|
],
|
|
|
|
// Process the guess (shared logic)
|
|
processGuess(target, from, guess, bot) {
|
|
// Find the game where this player is the challenger
|
|
let gameData = null;
|
|
let challenger = null;
|
|
|
|
for (const [chalName, game] of this.activeGames.entries()) {
|
|
if (game.challenger === from) {
|
|
gameData = game;
|
|
challenger = chalName;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gameData) {
|
|
bot.say(target, `${from}: No Hi-Lo challenge found for you!`);
|
|
return;
|
|
}
|
|
|
|
// Initialize challenger if new
|
|
if (!this.playerStats.has(from)) {
|
|
this.updatePlayerStats(from, {
|
|
totalWins: 0,
|
|
totalLosses: 0,
|
|
totalGames: 0,
|
|
biggestWin: 0,
|
|
winStreak: 0,
|
|
bestStreak: 0,
|
|
name: from
|
|
});
|
|
}
|
|
|
|
// Generate second card
|
|
const secondCard = this.generateCard();
|
|
const firstCard = gameData.firstCard;
|
|
const bet = gameData.bet;
|
|
|
|
// Determine if guess was correct
|
|
let isCorrect = false;
|
|
if (guess === 'higher' && secondCard > firstCard) isCorrect = true;
|
|
if (guess === 'lower' && secondCard < firstCard) isCorrect = true;
|
|
if (secondCard === firstCard) {
|
|
// Tie - push/draw
|
|
bot.say(target, `🃏 ${from}: ${this.getCardDisplay(firstCard)}→${this.getCardDisplay(secondCard)} | 🤝 TIE! No winner`);
|
|
this.activeGames.delete(challenger);
|
|
return;
|
|
}
|
|
|
|
// Update stats for both players
|
|
const challengerPlayer = this.playerStats.get(challenger);
|
|
const guesserPlayer = this.playerStats.get(from);
|
|
|
|
let challengerUpdates = { totalGames: challengerPlayer.totalGames + 1 };
|
|
let guesserUpdates = { totalGames: guesserPlayer.totalGames + 1 };
|
|
|
|
const firstDisplay = this.getCardDisplay(firstCard);
|
|
const secondDisplay = this.getCardDisplay(secondCard);
|
|
const direction = guess === 'higher' ? 'HI' : 'LO';
|
|
|
|
if (isCorrect) {
|
|
// Guesser wins
|
|
guesserUpdates.totalWins = guesserPlayer.totalWins + 1;
|
|
guesserUpdates.winStreak = guesserPlayer.winStreak + 1;
|
|
challengerUpdates.totalLosses = challengerPlayer.totalLosses + 1;
|
|
challengerUpdates.winStreak = 0;
|
|
|
|
if (guesserUpdates.winStreak > guesserPlayer.bestStreak) {
|
|
guesserUpdates.bestStreak = guesserUpdates.winStreak;
|
|
}
|
|
|
|
if (bet > guesserPlayer.biggestWin) {
|
|
guesserUpdates.biggestWin = bet;
|
|
}
|
|
|
|
bot.say(target, `🃏 ${from}: ${firstDisplay}→${secondDisplay} ${direction} | ✅ CORRECT! +${bet} pts | Streak: ${guesserUpdates.winStreak}`);
|
|
} else {
|
|
// Challenger wins
|
|
challengerUpdates.totalWins = challengerPlayer.totalWins + 1;
|
|
challengerUpdates.winStreak = challengerPlayer.winStreak + 1;
|
|
guesserUpdates.totalLosses = guesserPlayer.totalLosses + 1;
|
|
guesserUpdates.winStreak = 0;
|
|
|
|
if (challengerUpdates.winStreak > challengerPlayer.bestStreak) {
|
|
challengerUpdates.bestStreak = challengerUpdates.winStreak;
|
|
}
|
|
|
|
if (bet > challengerPlayer.biggestWin) {
|
|
challengerUpdates.biggestWin = bet;
|
|
}
|
|
|
|
bot.say(target, `🃏 ${from}: ${firstDisplay}→${secondDisplay} ${direction} | ❌ WRONG! ${challenger} wins ${bet} pts`);
|
|
}
|
|
|
|
// Update both players
|
|
this.updatePlayerStats(challenger, challengerUpdates);
|
|
this.updatePlayerStats(from, guesserUpdates);
|
|
|
|
// Remove the game
|
|
this.activeGames.delete(challenger);
|
|
}
|
|
}; |