Add new IRC games and enhance bot functionality

- Add Rock Paper Scissors game with PvP and bot modes
  - Fixed syntax errors and improved game mechanics
  - PvP moves now require PM for secrecy

- Add Word Scramble game with difficulty levels
  - Multiple word categories and persistent scoring

- Enhance duck hunt with better statistics tracking
  - Separate points vs duck count tracking
  - Fixed migration logic issues

- Add core rate limiting system (5 commands/30s)
  - Admin whitelist for megasconed
  - Automatic cleanup and unblocking

- Improve reload functionality for hot-reloading plugins
- Add channel-specific commands (\!stopducks/\!startducks)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-17 20:06:32 +00:00
parent a3ed25f8dd
commit 8552887b6c
9 changed files with 1536 additions and 30 deletions

View file

@ -70,22 +70,43 @@ module.exports = {
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!`);
const playerData = module.exports.gameState.scores.get(context.nick);
if (!playerData) {
bot.say(context.replyTo, `🎯 ${context.nick} hasn't shot any ducks yet!`);
return;
}
const points = module.exports.getTotalPoints(playerData);
const ducks = module.exports.getTotalDucks(playerData);
const isMigrated = playerData.migrated;
if (isMigrated && ducks === 0) {
bot.say(context.replyTo, `🎯 ${context.nick}: ${points} points (duck count tracked from next shot)`);
} else {
bot.say(context.replyTo, `🎯 ${context.nick}: ${points} points from ${ducks} ducks!`);
}
}
},
{
name: 'topducks',
description: 'Show top duck hunters',
description: 'Show top duck hunters by points',
execute(context, bot) {
module.exports.showLeaderboard(context, bot);
}
},
{
name: 'duckcount',
description: 'Show top duck hunters by quantity',
execute(context, bot) {
module.exports.showDuckCount(context, bot);
}
},
{
name: 'duckstats',
description: 'Show duck hunting statistics',
description: 'Show duck hunting statistics and most shot birds',
execute(context, bot) {
module.exports.showStats(context, bot);
}
@ -293,10 +314,9 @@ module.exports = {
// Remove duck from active ducks
this.gameState.activeDucks.delete(channel);
// Award points
// Award points and track duck type
const duck = activeDuck.duck;
const currentScore = this.gameState.scores.get(context.nick) || 0;
this.gameState.scores.set(context.nick, currentScore + duck.points);
this.updatePlayerScore(context.nick, duck.name, duck.points);
// Save scores to file after each duck shot
this.saveScores();
@ -383,10 +403,9 @@ module.exports = {
// Remove duck from active ducks
this.gameState.activeDucks.delete(channel);
// Award points (same as hunting)
// Award points and track duck type (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);
this.updatePlayerScore(context.nick, duck.name, duck.points);
// Save scores to file after each duck fed
this.saveScores();
@ -450,7 +469,7 @@ module.exports = {
showLeaderboard(context, bot) {
const scores = Array.from(this.gameState.scores.entries())
.sort((a, b) => b[1] - a[1])
.sort((a, b) => this.getTotalPoints(b[1]) - this.getTotalPoints(a[1]))
.slice(0, 10);
if (scores.length === 0) {
@ -458,22 +477,70 @@ module.exports = {
return;
}
bot.say(context.replyTo, '🏆 TOP DUCK HUNTERS 🏆');
bot.say(context.replyTo, '🏆 TOP DUCK HUNTERS (by points) 🏆');
scores.forEach((score, index) => {
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${score[1]} ducks`);
const points = this.getTotalPoints(score[1]);
bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${points} points`);
});
},
showDuckCount(context, bot) {
// Filter out migrated users with 0 duck count for this leaderboard
const scores = Array.from(this.gameState.scores.entries())
.filter(([nick, data]) => {
const ducks = this.getTotalDucks(data);
return ducks > 0 || !data.migrated; // Include non-migrated users even with 0 ducks
})
.sort((a, b) => this.getTotalDucks(b[1]) - this.getTotalDucks(a[1]))
.slice(0, 10);
if (scores.length === 0) {
bot.say(context.replyTo, '🦆 No one has shot any tracked ducks yet! Get hunting!');
bot.say(context.replyTo, '📝 Note: Duck counts are tracked from new shots only (not migrated data)');
return;
}
bot.say(context.replyTo, '🦆 TOP DUCK COUNTERS (by quantity) 🦆');
scores.forEach((score, index) => {
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
const ducks = this.getTotalDucks(score[1]);
bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${ducks} ducks`);
});
},
showStats(context, bot) {
const totalDucks = Array.from(this.gameState.scores.values()).reduce((a, b) => a + b, 0);
const totalDucks = Array.from(this.gameState.scores.values()).reduce((a, b) => a + this.getTotalDucks(b), 0);
const totalPoints = Array.from(this.gameState.scores.values()).reduce((a, b) => a + this.getTotalPoints(b), 0);
const totalHunters = this.gameState.scores.size;
const activeDucks = this.gameState.activeDucks.size;
bot.say(context.replyTo, `🦆 DUCK HUNT STATS 🦆`);
// Calculate most shot duck types
const duckTypeStats = {};
this.gameState.scores.forEach(playerData => {
if (typeof playerData === 'object' && playerData.duckTypes) {
Object.entries(playerData.duckTypes).forEach(([duckType, count]) => {
duckTypeStats[duckType] = (duckTypeStats[duckType] || 0) + count;
});
}
});
const sortedDuckTypes = Object.entries(duckTypeStats)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
bot.say(context.replyTo, `🦆 DUCK HUNT STATISTICS 🦆`);
bot.say(context.replyTo, `🎯 Total ducks shot: ${totalDucks}`);
bot.say(context.replyTo, `🏆 Total points earned: ${totalPoints}`);
bot.say(context.replyTo, `🏹 Active hunters: ${totalHunters}`);
bot.say(context.replyTo, `🦆 Ducks currently in channels: ${activeDucks}`);
if (sortedDuckTypes.length > 0) {
bot.say(context.replyTo, `📊 Most hunted: ${sortedDuckTypes[0][0]} (${sortedDuckTypes[0][1]} times)`);
if (sortedDuckTypes.length > 1) {
bot.say(context.replyTo, `🥈 Second most: ${sortedDuckTypes[1][0]} (${sortedDuckTypes[1][1]} times)`);
}
}
},
startDuckSpawning(bot) {
@ -561,16 +628,40 @@ module.exports = {
const data = fs.readFileSync(this.scoresFile, 'utf8');
const scoresObject = JSON.parse(data);
// Convert back to Map
this.gameState.scores = new Map(Object.entries(scoresObject));
// Convert back to Map and migrate old format if needed
this.gameState.scores = new Map();
let migrationNeeded = false;
for (const [nick, scoreData] of Object.entries(scoresObject)) {
// Check if this is old format (just a number) or new format (object)
if (typeof scoreData === 'number') {
// Old format: migrate to new structure
this.gameState.scores.set(nick, {
totalPoints: scoreData,
totalDucks: 0, // We don't know the real duck count, start fresh
duckTypes: {},
migrated: true // Flag to indicate this is migrated data
});
migrationNeeded = true;
} else {
// New format: use as-is
this.gameState.scores.set(nick, scoreData);
}
}
console.log(`🦆 Loaded ${this.gameState.scores.size} duck hunter scores from ${this.scoresFile}`);
if (migrationNeeded) {
console.log('📈 Migrated old score format to new detailed tracking');
this.saveScores(); // Save migrated data
}
// 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])
.sort((a, b) => this.getTotalPoints(b[1]) - this.getTotalPoints(a[1]))
.slice(0, 3);
console.log('🏆 Top duck hunters:', topHunters.map(([name, score]) => `${name}(${score})`).join(', '));
console.log('🏆 Top duck hunters:', topHunters.map(([name, data]) => `${name}(${this.getTotalPoints(data)}pts)`).join(', '));
}
} else {
console.log(`🦆 No existing duck hunt scores file found, starting fresh`);
@ -593,5 +684,48 @@ module.exports = {
} catch (error) {
console.error(`❌ Error saving duck hunt scores:`, error);
}
},
// Helper functions for new scoring system
getTotalPoints(playerData) {
if (typeof playerData === 'number') {
return playerData; // Old format compatibility
}
return playerData.totalPoints || 0;
},
getTotalDucks(playerData) {
if (typeof playerData === 'number') {
return playerData; // Old format estimate
}
return playerData.totalDucks || 0;
},
initializePlayerData(nick) {
if (!this.gameState.scores.has(nick)) {
this.gameState.scores.set(nick, {
totalPoints: 0,
totalDucks: 0,
duckTypes: {},
migrated: false // New players have accurate data from the start
});
}
},
updatePlayerScore(nick, duckName, points) {
this.initializePlayerData(nick);
const playerData = this.gameState.scores.get(nick);
// Update totals
playerData.totalPoints += points;
playerData.totalDucks += 1;
// Update duck type count
if (!playerData.duckTypes[duckName]) {
playerData.duckTypes[duckName] = 0;
}
playerData.duckTypes[duckName] += 1;
this.gameState.scores.set(nick, playerData);
}
};