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:
parent
a3ed25f8dd
commit
8552887b6c
9 changed files with 1536 additions and 30 deletions
178
botmain.js
178
botmain.js
|
|
@ -32,8 +32,20 @@ class IRCBot {
|
|||
this.commands = new Map();
|
||||
this.connected = false;
|
||||
|
||||
// Rate limiting system
|
||||
this.rateLimits = new Map(); // nick -> { commands: [], blocked: false, blockExpiry: null }
|
||||
this.rateLimitConfig = {
|
||||
maxCommands: 5, // Max commands per window
|
||||
windowMs: 30000, // 30 second window
|
||||
blockDurationMs: 60000, // 1 minute block
|
||||
cleanupInterval: 300000, // 5 minutes cleanup
|
||||
adminWhitelist: ['megasconed'] // Users exempt from rate limiting
|
||||
};
|
||||
|
||||
console.log('🔧 Bot configuration:', this.config);
|
||||
console.log('🚦 Rate limiting enabled:', this.rateLimitConfig);
|
||||
this.loadPlugins();
|
||||
this.startRateLimitCleanup();
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
|
@ -129,12 +141,17 @@ class IRCBot {
|
|||
const commandLine = msg.message.substring(this.config.commandPrefix.length);
|
||||
const [commandName, ...args] = commandLine.split(' ');
|
||||
|
||||
// Handle built-in admin commands first
|
||||
// Handle built-in admin commands first (keeping legacy support)
|
||||
if (commandName === 'reloadplugins' && msg.nick === 'megasconed') {
|
||||
this.say(replyTo, '🔄 Reloading all plugins...');
|
||||
this.say(replyTo, '🔄 Reloading all plugins... (use !reload command next time)');
|
||||
const pluginCount = this.plugins.size;
|
||||
this.reloadAllPlugins();
|
||||
this.say(replyTo, `✅ Reloaded ${pluginCount} plugins. New commands: ${Array.from(this.commands.keys()).join(', ')}`);
|
||||
try {
|
||||
this.reloadAllPlugins();
|
||||
this.say(replyTo, `✅ Reloaded ${pluginCount} plugins. New commands: ${Array.from(this.commands.keys()).join(', ')}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error during plugin reload:', error);
|
||||
this.say(replyTo, '❌ Error during plugin reload - check console for details.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +180,14 @@ class IRCBot {
|
|||
}
|
||||
|
||||
executeCommand(commandName, context) {
|
||||
// Check rate limit BEFORE executing command
|
||||
if (this.isRateLimited(context.nick, context.replyTo)) {
|
||||
return; // User is rate limited, ignore command
|
||||
}
|
||||
|
||||
// Track this command
|
||||
this.trackCommand(context.nick);
|
||||
|
||||
const command = this.commands.get(commandName);
|
||||
if (command) {
|
||||
try {
|
||||
|
|
@ -335,32 +360,64 @@ module.exports = {
|
|||
}
|
||||
|
||||
reloadPlugin(filename) {
|
||||
console.log(`🔄 Reloading plugin: ${filename}`);
|
||||
|
||||
const plugin = this.plugins.get(filename);
|
||||
if (plugin) {
|
||||
// Unregister old commands
|
||||
if (plugin.commands) {
|
||||
plugin.commands.forEach(command => {
|
||||
this.commands.delete(command.name);
|
||||
console.log(`🗑️ Unregistered command: ${command.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Call cleanup if available
|
||||
if (typeof plugin.cleanup === 'function') {
|
||||
plugin.cleanup(this);
|
||||
try {
|
||||
plugin.cleanup(this);
|
||||
console.log(`🧹 Cleaned up plugin: ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error cleaning up plugin ${filename}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.plugins.delete(filename);
|
||||
}
|
||||
|
||||
// Clear require cache for this specific plugin
|
||||
const pluginPath = path.resolve(this.config.pluginsDir, filename);
|
||||
if (require.cache[pluginPath]) {
|
||||
delete require.cache[pluginPath];
|
||||
console.log(`🗑️ Cleared cache for: ${filename}`);
|
||||
}
|
||||
|
||||
// Load the plugin again
|
||||
this.loadPlugin(filename);
|
||||
}
|
||||
|
||||
reloadAllPlugins() {
|
||||
console.log('🔄 Starting plugin reload process...');
|
||||
|
||||
// Clear all plugins and commands
|
||||
this.plugins.forEach((plugin, filename) => {
|
||||
if (typeof plugin.cleanup === 'function') {
|
||||
plugin.cleanup(this);
|
||||
try {
|
||||
plugin.cleanup(this);
|
||||
console.log(`🧹 Cleaned up plugin: ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error cleaning up plugin ${filename}:`, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Clear require cache for all plugin files
|
||||
console.log('🗑️ Clearing require cache...');
|
||||
const pluginDir = path.resolve(this.config.pluginsDir);
|
||||
Object.keys(require.cache).forEach(filepath => {
|
||||
if (filepath.startsWith(pluginDir)) {
|
||||
delete require.cache[filepath];
|
||||
console.log(`🗑️ Cleared cache for: ${path.basename(filepath)}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -368,7 +425,9 @@ module.exports = {
|
|||
this.commands.clear();
|
||||
|
||||
// Reload all plugins
|
||||
console.log('🔄 Reloading all plugins...');
|
||||
this.loadPlugins();
|
||||
console.log('✅ Plugin reload complete');
|
||||
}
|
||||
|
||||
// Helper methods for plugins
|
||||
|
|
@ -404,6 +463,113 @@ module.exports = {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Rate limiting methods
|
||||
isRateLimited(nick, replyTo) {
|
||||
// Check if user is whitelisted (admin)
|
||||
if (this.rateLimitConfig.adminWhitelist.includes(nick)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const userLimits = this.rateLimits.get(nick);
|
||||
|
||||
if (!userLimits) {
|
||||
// First time user, no limits
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user is currently blocked
|
||||
if (userLimits.blocked && now < userLimits.blockExpiry) {
|
||||
const remainingMs = userLimits.blockExpiry - now;
|
||||
const remainingSeconds = Math.ceil(remainingMs / 1000);
|
||||
console.log(`🚫 Rate limited user ${nick} tried command, ${remainingSeconds}s remaining`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If block expired, unblock user
|
||||
if (userLimits.blocked && now >= userLimits.blockExpiry) {
|
||||
userLimits.blocked = false;
|
||||
userLimits.blockExpiry = null;
|
||||
userLimits.commands = [];
|
||||
console.log(`✅ Rate limit expired for ${nick}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Count recent commands in sliding window
|
||||
const windowStart = now - this.rateLimitConfig.windowMs;
|
||||
const recentCommands = userLimits.commands.filter(timestamp => timestamp > windowStart);
|
||||
|
||||
// If user exceeds limit, block them
|
||||
if (recentCommands.length >= this.rateLimitConfig.maxCommands) {
|
||||
userLimits.blocked = true;
|
||||
userLimits.blockExpiry = now + this.rateLimitConfig.blockDurationMs;
|
||||
|
||||
const blockMinutes = Math.ceil(this.rateLimitConfig.blockDurationMs / 60000);
|
||||
this.say(replyTo, `⚠️ ${nick}: Rate limit exceeded! Please wait ${blockMinutes} minute(s) before using commands again.`);
|
||||
console.log(`🚫 Rate limited ${nick} for ${blockMinutes} minute(s)`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
trackCommand(nick) {
|
||||
// Don't track admin commands
|
||||
if (this.rateLimitConfig.adminWhitelist.includes(nick)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (!this.rateLimits.has(nick)) {
|
||||
this.rateLimits.set(nick, {
|
||||
commands: [],
|
||||
blocked: false,
|
||||
blockExpiry: null
|
||||
});
|
||||
}
|
||||
|
||||
const userLimits = this.rateLimits.get(nick);
|
||||
userLimits.commands.push(now);
|
||||
|
||||
// Clean up old command timestamps (sliding window)
|
||||
const windowStart = now - this.rateLimitConfig.windowMs;
|
||||
userLimits.commands = userLimits.commands.filter(timestamp => timestamp > windowStart);
|
||||
}
|
||||
|
||||
startRateLimitCleanup() {
|
||||
// Clean up old rate limit entries every 5 minutes
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
const cutoffTime = now - (this.rateLimitConfig.windowMs * 2); // Keep data for 2x window time
|
||||
|
||||
let cleanedUsers = 0;
|
||||
for (const [nick, limits] of this.rateLimits.entries()) {
|
||||
// Remove users who haven't used commands recently and aren't blocked
|
||||
if (!limits.blocked && limits.commands.length === 0) {
|
||||
this.rateLimits.delete(nick);
|
||||
cleanedUsers++;
|
||||
} else if (!limits.blocked) {
|
||||
// Clean up old command timestamps
|
||||
const oldLength = limits.commands.length;
|
||||
limits.commands = limits.commands.filter(timestamp => timestamp > cutoffTime);
|
||||
|
||||
// If no recent commands, remove user
|
||||
if (limits.commands.length === 0) {
|
||||
this.rateLimits.delete(nick);
|
||||
cleanedUsers++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedUsers > 0) {
|
||||
console.log(`🧹 Cleaned up ${cleanedUsers} old rate limit entries`);
|
||||
}
|
||||
}, this.rateLimitConfig.cleanupInterval);
|
||||
|
||||
console.log(`🧹 Rate limit cleanup scheduled every ${this.rateLimitConfig.cleanupInterval / 1000}s`);
|
||||
}
|
||||
}
|
||||
|
||||
// Create and start bot
|
||||
|
|
|
|||
|
|
@ -33,6 +33,76 @@ module.exports = {
|
|||
const now = new Date().toLocaleString();
|
||||
bot.say(context.replyTo, `Current time: ${now}`);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'ratelimit',
|
||||
description: 'Check your rate limit status',
|
||||
execute(context, bot) {
|
||||
// Only allow in #bakedbeans or for admins
|
||||
if (context.channel !== '#bakedbeans' && !bot.rateLimitConfig.adminWhitelist.includes(context.nick)) {
|
||||
bot.say(context.replyTo, '🚫 This command can only be used in #bakedbeans!');
|
||||
return;
|
||||
}
|
||||
|
||||
const userLimits = bot.rateLimits.get(context.nick);
|
||||
const config = bot.rateLimitConfig;
|
||||
|
||||
if (config.adminWhitelist.includes(context.nick)) {
|
||||
bot.say(context.replyTo, `✅ ${context.nick}: You are whitelisted (no rate limits)`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userLimits) {
|
||||
bot.say(context.replyTo, `✅ ${context.nick}: No rate limit data (you're clean!)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (userLimits.blocked && now < userLimits.blockExpiry) {
|
||||
const remainingMs = userLimits.blockExpiry - now;
|
||||
const remainingSeconds = Math.ceil(remainingMs / 1000);
|
||||
bot.say(context.replyTo, `🚫 ${context.nick}: Rate limited for ${remainingSeconds} more seconds`);
|
||||
return;
|
||||
}
|
||||
|
||||
const windowStart = now - config.windowMs;
|
||||
const recentCommands = userLimits.commands.filter(timestamp => timestamp > windowStart);
|
||||
const remaining = config.maxCommands - recentCommands.length;
|
||||
|
||||
bot.say(context.replyTo, `📊 ${context.nick}: ${recentCommands.length}/${config.maxCommands} commands used in last ${config.windowMs/1000}s (${remaining} remaining)`);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'reload',
|
||||
description: 'Reload all plugins (admin only)',
|
||||
execute(context, bot) {
|
||||
// Only allow for admin
|
||||
if (context.nick !== 'megasconed') {
|
||||
bot.say(context.replyTo, '🚫 Only admins can reload plugins!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow in #bakedbeans
|
||||
if (context.channel !== '#bakedbeans') {
|
||||
bot.say(context.replyTo, '🚫 Plugin reload can only be done in #bakedbeans!');
|
||||
return;
|
||||
}
|
||||
|
||||
bot.say(context.replyTo, '🔄 Reloading all plugins...');
|
||||
const pluginCount = bot.plugins.size;
|
||||
|
||||
try {
|
||||
bot.reloadAllPlugins();
|
||||
const newCommandCount = bot.commands.size;
|
||||
bot.say(context.replyTo, `✅ Reloaded ${pluginCount} plugins successfully! Now have ${newCommandCount} commands available.`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error during plugin reload:', error);
|
||||
bot.say(context.replyTo, '❌ Error during plugin reload - check console for details.');
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,3 +1 @@
|
|||
{
|
||||
"Monqui": 5
|
||||
}
|
||||
{}
|
||||
594
plugins/rock_paper_scissors_plugin.js
Normal file
594
plugins/rock_paper_scissors_plugin.js
Normal file
|
|
@ -0,0 +1,594 @@
|
|||
// plugins/rock_paper_scissors_plugin.js - Rock Paper Scissors Game Plugin
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
gameState: {
|
||||
activeGames: new Map(), // channel -> game data
|
||||
challenges: new Map(), // channel -> challenge data
|
||||
scores: new Map() // nick -> score data
|
||||
},
|
||||
|
||||
// Game choices and their relationships
|
||||
choices: {
|
||||
'rock': {
|
||||
emoji: '🪨',
|
||||
beats: 'scissors',
|
||||
losesTo: 'paper'
|
||||
},
|
||||
'paper': {
|
||||
emoji: '📄',
|
||||
beats: 'rock',
|
||||
losesTo: 'scissors'
|
||||
},
|
||||
'scissors': {
|
||||
emoji: '✂️',
|
||||
beats: 'paper',
|
||||
losesTo: 'rock'
|
||||
}
|
||||
},
|
||||
|
||||
// Aliases for user input
|
||||
aliases: {
|
||||
'r': 'rock',
|
||||
'p': 'paper',
|
||||
's': 'scissors',
|
||||
'stone': 'rock',
|
||||
'scissor': 'scissors'
|
||||
},
|
||||
|
||||
init(bot) {
|
||||
console.log('Rock Paper Scissors plugin initialized - Ready to rumble! 🪨📄✂️');
|
||||
|
||||
// Set up score file path
|
||||
this.scoresFile = path.join(__dirname, 'rps_scores.json');
|
||||
|
||||
// Load existing scores
|
||||
this.loadScores();
|
||||
},
|
||||
|
||||
cleanup(bot) {
|
||||
console.log('Rock Paper Scissors plugin cleaned up');
|
||||
|
||||
// Save scores before cleanup
|
||||
this.saveScores();
|
||||
|
||||
// Clear all active games and timers
|
||||
this.gameState.activeGames.forEach(game => {
|
||||
if (game.timer) {
|
||||
clearTimeout(game.timer);
|
||||
}
|
||||
});
|
||||
this.gameState.activeGames.clear();
|
||||
|
||||
// Clear all challenges and timers
|
||||
this.gameState.challenges.forEach(challenge => {
|
||||
if (challenge.timer) {
|
||||
clearTimeout(challenge.timer);
|
||||
}
|
||||
});
|
||||
this.gameState.challenges.clear();
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: 'rps',
|
||||
description: 'Play Rock Paper Scissors (!rps vs bot OR !rps @player)',
|
||||
execute(context, bot) {
|
||||
module.exports.startGame(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'rock',
|
||||
description: 'Play rock in active RPS game',
|
||||
execute(context, bot) {
|
||||
module.exports.playMove(context, bot, 'rock');
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'paper',
|
||||
description: 'Play paper in active RPS game',
|
||||
execute(context, bot) {
|
||||
module.exports.playMove(context, bot, 'paper');
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'scissors',
|
||||
description: 'Play scissors in active RPS game',
|
||||
execute(context, bot) {
|
||||
module.exports.playMove(context, bot, 'scissors');
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'accept',
|
||||
description: 'Accept a Rock Paper Scissors challenge',
|
||||
execute(context, bot) {
|
||||
module.exports.acceptChallenge(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'rpsstats',
|
||||
description: 'Show your Rock Paper Scissors statistics',
|
||||
execute(context, bot) {
|
||||
module.exports.showPlayerStats(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'toprps',
|
||||
description: 'Show top Rock Paper Scissors players',
|
||||
execute(context, bot) {
|
||||
module.exports.showLeaderboard(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'rpsscore',
|
||||
description: 'Show your current RPS score',
|
||||
execute(context, bot) {
|
||||
module.exports.showScore(context, bot);
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
startGame(context, bot) {
|
||||
const channel = context.channel;
|
||||
if (!channel) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ Rock Paper Scissors can only be played in channels!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's already an active game
|
||||
if (this.gameState.activeGames.has(channel)) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ There\'s already an active RPS game in this channel!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's already a challenge
|
||||
if (this.gameState.challenges.has(channel)) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ There\'s already a pending challenge in this channel!');
|
||||
return;
|
||||
}
|
||||
|
||||
const args = context.args;
|
||||
|
||||
// Check if challenging another player
|
||||
if (args.length > 0) {
|
||||
const target = args[0].replace('@', '');
|
||||
|
||||
if (target === context.nick) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ You can\'t challenge yourself! Use !rps to play vs the bot.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (target === bot.config.nick) {
|
||||
// Play against bot
|
||||
this.startBotGame(context, bot, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Challenge another player
|
||||
this.createChallenge(context, bot, channel, target);
|
||||
return;
|
||||
}
|
||||
|
||||
// Default: play against bot
|
||||
this.startBotGame(context, bot, channel);
|
||||
},
|
||||
|
||||
startBotGame(context, bot, channel) {
|
||||
const gameData = {
|
||||
type: 'bot',
|
||||
player: context.nick,
|
||||
startTime: Date.now(),
|
||||
playerMove: null,
|
||||
botMove: null
|
||||
};
|
||||
|
||||
// Set 30 second timer
|
||||
gameData.timer = setTimeout(() => {
|
||||
this.timeoutGame(channel, bot, gameData);
|
||||
}, 30000);
|
||||
|
||||
this.gameState.activeGames.set(channel, gameData);
|
||||
|
||||
bot.say(channel, `🪨📄✂️ ${context.nick} vs ${bot.config.nick}! Use !rock, !paper, or !scissors (30s limit)`);
|
||||
},
|
||||
|
||||
createChallenge(context, bot, channel, target) {
|
||||
const challengeData = {
|
||||
challenger: context.nick,
|
||||
target: target,
|
||||
startTime: Date.now()
|
||||
};
|
||||
|
||||
// Set 60 second timer for challenge acceptance
|
||||
challengeData.timer = setTimeout(() => {
|
||||
this.timeoutChallenge(channel, bot, challengeData);
|
||||
}, 60000);
|
||||
|
||||
this.gameState.challenges.set(channel, challengeData);
|
||||
|
||||
bot.say(channel, `🪨📄✂️ ${context.nick} challenges ${target} to Rock Paper Scissors! Use !accept to accept (60s limit)`);
|
||||
},
|
||||
|
||||
acceptChallenge(context, bot) {
|
||||
const channel = context.channel;
|
||||
if (!channel) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ Challenges can only be accepted in channels!');
|
||||
return;
|
||||
}
|
||||
|
||||
const challenge = this.gameState.challenges.get(channel);
|
||||
if (!challenge) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ No active challenge to accept!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.nick !== challenge.target) {
|
||||
bot.say(context.replyTo, `🪨📄✂️ This challenge is for ${challenge.target}, not you!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear challenge timer
|
||||
clearTimeout(challenge.timer);
|
||||
this.gameState.challenges.delete(channel);
|
||||
|
||||
// Start player vs player game
|
||||
this.startPlayerGame(bot, channel, challenge.challenger, challenge.target);
|
||||
},
|
||||
|
||||
startPlayerGame(bot, channel, player1, player2) {
|
||||
const gameData = {
|
||||
type: 'player',
|
||||
player1: player1,
|
||||
player2: player2,
|
||||
startTime: Date.now(),
|
||||
moves: new Map(), // nick -> move
|
||||
revealed: false
|
||||
};
|
||||
|
||||
// Set 45 second timer
|
||||
gameData.timer = setTimeout(() => {
|
||||
this.timeoutGame(channel, bot, gameData);
|
||||
}, 45000);
|
||||
|
||||
this.gameState.activeGames.set(channel, gameData);
|
||||
|
||||
bot.say(channel, `🪨📄✂️ ${player1} vs ${player2}! Both players PM the bot your moves: /msg ${bot.config.nick} !rock, !paper, or !scissors (45s limit)`);
|
||||
},
|
||||
|
||||
playMove(context, bot, move) {
|
||||
const channel = context.channel;
|
||||
|
||||
// If in a channel (bot vs player games)
|
||||
if (channel) {
|
||||
const game = this.gameState.activeGames.get(channel);
|
||||
if (!game) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ No active RPS game to play in!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.type === 'bot') {
|
||||
this.handleBotMove(context, bot, channel, game, move);
|
||||
} else {
|
||||
this.handlePlayerMove(context, bot, channel, game, move);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If in PM (player vs player games)
|
||||
// Find the game this player is in
|
||||
let foundGame = null;
|
||||
let foundChannel = null;
|
||||
|
||||
for (const [channelName, game] of this.gameState.activeGames) {
|
||||
if (game.type === 'player' && (game.player1 === context.nick || game.player2 === context.nick)) {
|
||||
foundGame = game;
|
||||
foundChannel = channelName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundGame) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ You\'re not in an active player vs player game!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.handlePlayerMove(context, bot, foundChannel, foundGame, move);
|
||||
},
|
||||
|
||||
handleBotMove(context, bot, channel, game, move) {
|
||||
if (context.nick !== game.player) {
|
||||
bot.say(context.replyTo, `🪨📄✂️ This game is for ${game.player}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.playerMove) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ You already made your move!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Player made their move
|
||||
game.playerMove = move;
|
||||
|
||||
// Bot makes random move
|
||||
const botChoices = ['rock', 'paper', 'scissors'];
|
||||
game.botMove = botChoices[Math.floor(Math.random() * botChoices.length)];
|
||||
|
||||
// Clear timer
|
||||
clearTimeout(game.timer);
|
||||
|
||||
// Determine winner and show result
|
||||
this.resolveGame(channel, bot, game);
|
||||
},
|
||||
|
||||
handlePlayerMove(context, bot, channel, game, move) {
|
||||
if (context.nick !== game.player1 && context.nick !== game.player2) {
|
||||
bot.say(context.replyTo, `🪨📄✂️ This game is for ${game.player1} and ${game.player2}!`);
|
||||
return;
|
||||
}
|
||||
|
||||
// For player vs player games, moves must be made via PM
|
||||
if (context.channel) {
|
||||
bot.say(context.replyTo, `🪨📄✂️ ${context.nick}: Send your move via PM to keep it secret! Use /msg ${bot.config.nick} !rock, !paper, or !scissors`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.moves.has(context.nick)) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ You already made your move!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Record move
|
||||
game.moves.set(context.nick, move);
|
||||
|
||||
// Send confirmation via private message
|
||||
bot.say(context.nick, `🪨📄✂️ You played ${this.choices[move].emoji} ${move}!`);
|
||||
|
||||
// Check if both players have moved
|
||||
if (game.moves.size === 2) {
|
||||
// Clear timer
|
||||
clearTimeout(game.timer);
|
||||
|
||||
// Resolve game
|
||||
this.resolveGame(channel, bot, game);
|
||||
} else {
|
||||
// Wait for other player
|
||||
const waitingFor = game.player1 === context.nick ? game.player2 : game.player1;
|
||||
bot.say(channel, `🪨📄✂️ Waiting for ${waitingFor} to make their move via PM...`);
|
||||
}
|
||||
},
|
||||
|
||||
resolveGame(channel, bot, game) {
|
||||
this.gameState.activeGames.delete(channel);
|
||||
|
||||
if (game.type === 'bot') {
|
||||
this.resolveBotGame(channel, bot, game);
|
||||
} else {
|
||||
this.resolvePlayerGame(channel, bot, game);
|
||||
}
|
||||
},
|
||||
|
||||
resolveBotGame(channel, bot, game) {
|
||||
const playerMove = game.playerMove;
|
||||
const botMove = game.botMove;
|
||||
|
||||
const playerChoice = this.choices[playerMove];
|
||||
const botChoice = this.choices[botMove];
|
||||
|
||||
// Show moves
|
||||
bot.say(channel, `🪨📄✂️ ${game.player}: ${playerChoice.emoji} | ${bot.config.nick}: ${botChoice.emoji}`);
|
||||
|
||||
// Determine result
|
||||
let result;
|
||||
if (playerMove === botMove) {
|
||||
result = 'draw';
|
||||
bot.say(channel, `🤝 It's a draw! Both played ${playerMove}!`);
|
||||
} else if (playerChoice.beats === botMove) {
|
||||
result = 'win';
|
||||
bot.say(channel, `🎉 ${game.player} wins! ${playerMove} beats ${botMove}!`);
|
||||
} else {
|
||||
result = 'loss';
|
||||
bot.say(channel, `🤖 ${bot.config.nick} wins! ${botMove} beats ${playerMove}!`);
|
||||
}
|
||||
|
||||
// Update scores
|
||||
this.updatePlayerScore(game.player, result, 'bot');
|
||||
this.saveScores();
|
||||
},
|
||||
|
||||
resolvePlayerGame(channel, bot, game) {
|
||||
const player1Move = game.moves.get(game.player1);
|
||||
const player2Move = game.moves.get(game.player2);
|
||||
|
||||
const player1Choice = this.choices[player1Move];
|
||||
const player2Choice = this.choices[player2Move];
|
||||
|
||||
// Show moves
|
||||
bot.say(channel, `🪨📄✂️ ${game.player1}: ${player1Choice.emoji} | ${game.player2}: ${player2Choice.emoji}`);
|
||||
|
||||
// Determine result
|
||||
if (player1Move === player2Move) {
|
||||
bot.say(channel, `🤝 It's a draw! Both played ${player1Move}!`);
|
||||
this.updatePlayerScore(game.player1, 'draw', 'player');
|
||||
this.updatePlayerScore(game.player2, 'draw', 'player');
|
||||
} else if (player1Choice.beats === player2Move) {
|
||||
bot.say(channel, `🎉 ${game.player1} wins! ${player1Move} beats ${player2Move}!`);
|
||||
this.updatePlayerScore(game.player1, 'win', 'player');
|
||||
this.updatePlayerScore(game.player2, 'loss', 'player');
|
||||
} else {
|
||||
bot.say(channel, `🎉 ${game.player2} wins! ${player2Move} beats ${player1Move}!`);
|
||||
this.updatePlayerScore(game.player2, 'win', 'player');
|
||||
this.updatePlayerScore(game.player1, 'loss', 'player');
|
||||
}
|
||||
|
||||
this.saveScores();
|
||||
},
|
||||
|
||||
timeoutGame(channel, bot, game) {
|
||||
this.gameState.activeGames.delete(channel);
|
||||
|
||||
if (game.type === 'bot') {
|
||||
bot.say(channel, `⏰ ${game.player} took too long to make a move! Game cancelled.`);
|
||||
} else {
|
||||
const missingPlayers = [];
|
||||
if (!game.moves.has(game.player1)) missingPlayers.push(game.player1);
|
||||
if (!game.moves.has(game.player2)) missingPlayers.push(game.player2);
|
||||
|
||||
bot.say(channel, `⏰ Game timed out! ${missingPlayers.join(' and ')} didn't make their moves.`);
|
||||
}
|
||||
},
|
||||
|
||||
timeoutChallenge(channel, bot, challenge) {
|
||||
this.gameState.challenges.delete(channel);
|
||||
bot.say(channel, `⏰ Challenge timed out! ${challenge.target} didn't accept the challenge.`);
|
||||
},
|
||||
|
||||
// Player score management
|
||||
initializePlayerData(nick) {
|
||||
if (!this.gameState.scores.has(nick)) {
|
||||
this.gameState.scores.set(nick, {
|
||||
totalPoints: 0,
|
||||
totalGames: 0,
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
draws: 0,
|
||||
winStreak: 0,
|
||||
longestWinStreak: 0,
|
||||
vsBot: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
draws: 0
|
||||
},
|
||||
vsPlayer: {
|
||||
wins: 0,
|
||||
losses: 0,
|
||||
draws: 0
|
||||
},
|
||||
favoriteMove: null,
|
||||
moveStats: {
|
||||
rock: 0,
|
||||
paper: 0,
|
||||
scissors: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updatePlayerScore(nick, result, gameType) {
|
||||
this.initializePlayerData(nick);
|
||||
const playerData = this.gameState.scores.get(nick);
|
||||
|
||||
// Update totals
|
||||
playerData.totalGames += 1;
|
||||
playerData[result + 's'] += 1;
|
||||
|
||||
// Update game type stats
|
||||
playerData[gameType === 'bot' ? 'vsBot' : 'vsPlayer'][result + 's'] += 1;
|
||||
|
||||
// Update points and streaks
|
||||
if (result === 'win') {
|
||||
playerData.totalPoints += 3;
|
||||
playerData.winStreak += 1;
|
||||
if (playerData.winStreak > playerData.longestWinStreak) {
|
||||
playerData.longestWinStreak = playerData.winStreak;
|
||||
}
|
||||
} else if (result === 'draw') {
|
||||
playerData.totalPoints += 1;
|
||||
playerData.winStreak = 0;
|
||||
} else {
|
||||
playerData.winStreak = 0;
|
||||
}
|
||||
|
||||
this.gameState.scores.set(nick, playerData);
|
||||
},
|
||||
|
||||
showScore(context, bot) {
|
||||
const playerData = this.gameState.scores.get(context.nick);
|
||||
if (!playerData) {
|
||||
bot.say(context.replyTo, `🪨📄✂️ ${context.nick} hasn't played any RPS games yet!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const winRate = playerData.totalGames > 0 ? (playerData.wins / playerData.totalGames * 100).toFixed(1) : 0;
|
||||
bot.say(context.replyTo, `🎯 ${context.nick}: ${playerData.totalPoints} points | ${playerData.wins}W-${playerData.losses}L-${playerData.draws}D (${winRate}% win rate)`);
|
||||
},
|
||||
|
||||
showPlayerStats(context, bot) {
|
||||
const playerData = this.gameState.scores.get(context.nick);
|
||||
if (!playerData) {
|
||||
bot.say(context.replyTo, `🪨📄✂️ ${context.nick} hasn't played any RPS games yet!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const winRate = playerData.totalGames > 0 ? (playerData.wins / playerData.totalGames * 100).toFixed(1) : 0;
|
||||
|
||||
bot.say(context.replyTo, `📊 ${context.nick}'s RPS Stats:`);
|
||||
bot.say(context.replyTo, `🎯 Points: ${playerData.totalPoints} | Games: ${playerData.totalGames} | Win Rate: ${winRate}%`);
|
||||
bot.say(context.replyTo, `🏆 Record: ${playerData.wins}W-${playerData.losses}L-${playerData.draws}D | Streak: ${playerData.winStreak} (best: ${playerData.longestWinStreak})`);
|
||||
bot.say(context.replyTo, `🤖 vs Bot: ${playerData.vsBot.wins}W-${playerData.vsBot.losses}L-${playerData.vsBot.draws}D | 👥 vs Players: ${playerData.vsPlayer.wins}W-${playerData.vsPlayer.losses}L-${playerData.vsPlayer.draws}D`);
|
||||
},
|
||||
|
||||
showLeaderboard(context, bot) {
|
||||
const scores = Array.from(this.gameState.scores.entries())
|
||||
.sort((a, b) => b[1].totalPoints - a[1].totalPoints)
|
||||
.slice(0, 10);
|
||||
|
||||
if (scores.length === 0) {
|
||||
bot.say(context.replyTo, '🪨📄✂️ No one has played RPS yet!');
|
||||
return;
|
||||
}
|
||||
|
||||
bot.say(context.replyTo, '🏆 TOP RPS PLAYERS 🏆');
|
||||
scores.forEach(([nick, data], index) => {
|
||||
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
|
||||
const winRate = data.totalGames > 0 ? (data.wins / data.totalGames * 100).toFixed(1) : 0;
|
||||
bot.say(context.replyTo, `${medal} ${index + 1}. ${nick}: ${data.totalPoints}pts (${data.wins}W-${data.losses}L-${data.draws}D, ${winRate}%)`);
|
||||
});
|
||||
},
|
||||
|
||||
// Score persistence
|
||||
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} RPS scores from ${this.scoresFile}`);
|
||||
|
||||
// Log top 3 players if any exist
|
||||
if (this.gameState.scores.size > 0) {
|
||||
const topPlayers = Array.from(this.gameState.scores.entries())
|
||||
.sort((a, b) => b[1].totalPoints - a[1].totalPoints)
|
||||
.slice(0, 3);
|
||||
console.log('🏆 Top RPS players:', topPlayers.map(([name, data]) => `${name}(${data.totalPoints}pts)`).join(', '));
|
||||
}
|
||||
} else {
|
||||
console.log(`🪨📄✂️ No existing RPS scores file found, starting fresh`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading RPS scores:`, error);
|
||||
this.gameState.scores = new Map();
|
||||
}
|
||||
},
|
||||
|
||||
saveScores() {
|
||||
try {
|
||||
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} RPS scores to ${this.scoresFile}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error saving RPS scores:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
54
plugins/rps_scores.json
Normal file
54
plugins/rps_scores.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"megasconed": {
|
||||
"totalPoints": 1,
|
||||
"totalGames": 3,
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 1,
|
||||
"winStreak": 0,
|
||||
"longestWinStreak": 0,
|
||||
"vsBot": {
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 1
|
||||
},
|
||||
"vsPlayer": {
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0,
|
||||
"losss": 1
|
||||
},
|
||||
"favoriteMove": null,
|
||||
"moveStats": {
|
||||
"rock": 0,
|
||||
"paper": 0,
|
||||
"scissors": 0
|
||||
},
|
||||
"losss": 1
|
||||
},
|
||||
"cr0sis": {
|
||||
"totalPoints": 6,
|
||||
"totalGames": 2,
|
||||
"wins": 2,
|
||||
"losses": 0,
|
||||
"draws": 0,
|
||||
"winStreak": 2,
|
||||
"longestWinStreak": 2,
|
||||
"vsBot": {
|
||||
"wins": 0,
|
||||
"losses": 0,
|
||||
"draws": 0
|
||||
},
|
||||
"vsPlayer": {
|
||||
"wins": 2,
|
||||
"losses": 0,
|
||||
"draws": 0
|
||||
},
|
||||
"favoriteMove": null,
|
||||
"moveStats": {
|
||||
"rock": 0,
|
||||
"paper": 0,
|
||||
"scissors": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
28
plugins/scramble_scores.json
Normal file
28
plugins/scramble_scores.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"megasconed": {
|
||||
"totalPoints": 6,
|
||||
"totalSolved": 2,
|
||||
"avgTime": 3.5725000000000002,
|
||||
"bestTime": 2.657,
|
||||
"currentStreak": 2,
|
||||
"longestStreak": 2,
|
||||
"difficultyStats": {
|
||||
"easy": 1,
|
||||
"medium": 0,
|
||||
"hard": 1
|
||||
}
|
||||
},
|
||||
"cr0sis": {
|
||||
"totalPoints": 2,
|
||||
"totalSolved": 1,
|
||||
"avgTime": 19.268,
|
||||
"bestTime": 19.268,
|
||||
"currentStreak": 1,
|
||||
"longestStreak": 1,
|
||||
"difficultyStats": {
|
||||
"easy": 0,
|
||||
"medium": 1,
|
||||
"hard": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"megasconed": {
|
||||
"totalWins": 57,
|
||||
"totalWins": 62,
|
||||
"biggestWin": 33,
|
||||
"totalSpins": 47,
|
||||
"totalSpins": 60,
|
||||
"jackpots": 0,
|
||||
"name": "megasconed"
|
||||
},
|
||||
|
|
|
|||
462
plugins/word_scramble_plugin.js
Normal file
462
plugins/word_scramble_plugin.js
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
// plugins/word_scramble_plugin.js - Word Scramble Game Plugin
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
gameState: {
|
||||
activeScrambles: new Map(), // channel -> scramble data
|
||||
scores: new Map() // nick -> score data
|
||||
},
|
||||
|
||||
// Word lists organized by difficulty - easy to expand!
|
||||
wordLists: {
|
||||
easy: [
|
||||
'cat', 'dog', 'run', 'sun', 'fun', 'big', 'red', 'car', 'hat', 'map',
|
||||
'pen', 'box', 'key', 'cup', 'bag', 'egg', 'ice', 'zoo', 'owl', 'fox',
|
||||
'bus', 'toy', 'fly', 'sky', 'day', 'way', 'boy', 'joy', 'may', 'say',
|
||||
'boat', 'coat', 'goat', 'road', 'toad', 'load', 'cold', 'gold', 'hold',
|
||||
'book', 'look', 'took', 'cook', 'hook', 'good', 'wood', 'food', 'mood',
|
||||
'hand', 'land', 'band', 'sand', 'wind', 'kind', 'find', 'mind', 'wild'
|
||||
],
|
||||
|
||||
medium: [
|
||||
'house', 'mouse', 'horse', 'nurse', 'purse', 'curse', 'force', 'voice',
|
||||
'table', 'cable', 'fable', 'stable', 'noble', 'double', 'trouble', 'bubble',
|
||||
'water', 'later', 'paper', 'tiger', 'tower', 'power', 'flower', 'shower',
|
||||
'school', 'smooth', 'choose', 'cheese', 'freeze', 'breeze', 'please', 'sleeve',
|
||||
'market', 'basket', 'rocket', 'socket', 'pocket', 'ticket', 'jacket', 'bucket',
|
||||
'garden', 'golden', 'wooden', 'broken', 'spoken', 'chosen', 'frozen', 'proven',
|
||||
'friend', 'prince', 'bridge', 'orange', 'change', 'chance', 'france', 'dance',
|
||||
'winter', 'summer', 'spring', 'autumn', 'season', 'reason', 'person', 'lesson'
|
||||
],
|
||||
|
||||
hard: [
|
||||
'computer', 'elephant', 'treasure', 'building', 'mountain', 'champion', 'surprise',
|
||||
'together', 'birthday', 'question', 'language', 'sandwich', 'princess', 'keyboard',
|
||||
'umbrella', 'hospital', 'festival', 'material', 'original', 'terminal', 'personal',
|
||||
'elephant', 'telephone', 'envelope', 'magazine', 'medicine', 'adventure', 'passenger',
|
||||
'butterfly', 'wonderful', 'beautiful', 'dangerous', 'important', 'different', 'excellent',
|
||||
'knowledge', 'challenge', 'advantage', 'encourage', 'education', 'operation', 'direction',
|
||||
'beginning', 'happening', 'lightning', 'frightening', 'strengthening', 'interesting',
|
||||
'playground', 'background', 'understand', 'recommend', 'restaurant', 'government'
|
||||
]
|
||||
},
|
||||
|
||||
init(bot) {
|
||||
console.log('Word Scramble plugin initialized - Get ready to unscramble! 🔤');
|
||||
|
||||
// Set up score file path
|
||||
this.scoresFile = path.join(__dirname, 'scramble_scores.json');
|
||||
|
||||
// Load existing scores
|
||||
this.loadScores();
|
||||
},
|
||||
|
||||
cleanup(bot) {
|
||||
console.log('Word Scramble plugin cleaned up');
|
||||
|
||||
// Save scores before cleanup
|
||||
this.saveScores();
|
||||
|
||||
// Clear all active scrambles and timers
|
||||
this.gameState.activeScrambles.forEach(scramble => {
|
||||
if (scramble.timer) {
|
||||
clearTimeout(scramble.timer);
|
||||
}
|
||||
});
|
||||
this.gameState.activeScrambles.clear();
|
||||
},
|
||||
|
||||
commands: [
|
||||
{
|
||||
name: 'scramble',
|
||||
description: 'Start a word scramble game (easy/medium/hard)',
|
||||
execute(context, bot) {
|
||||
module.exports.startScramble(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'hint',
|
||||
description: 'Get a hint for the current scramble',
|
||||
execute(context, bot) {
|
||||
module.exports.giveHint(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'skip',
|
||||
description: 'Skip the current scramble',
|
||||
execute(context, bot) {
|
||||
module.exports.skipScramble(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'scramblestats',
|
||||
description: 'Show your word scramble statistics',
|
||||
execute(context, bot) {
|
||||
module.exports.showPlayerStats(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'topscramble',
|
||||
description: 'Show top word scramble players',
|
||||
execute(context, bot) {
|
||||
module.exports.showLeaderboard(context, bot);
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'scramblescore',
|
||||
description: 'Show your current scramble score',
|
||||
execute(context, bot) {
|
||||
module.exports.showScore(context, bot);
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Handle channel messages for answer checking
|
||||
onMessage(data, bot) {
|
||||
if (data.isChannel) {
|
||||
this.checkAnswer(data, bot);
|
||||
}
|
||||
},
|
||||
|
||||
startScramble(context, bot) {
|
||||
const channel = context.channel;
|
||||
if (!channel) {
|
||||
bot.say(context.replyTo, '🔤 Word scramble can only be played in channels!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's already an active scramble
|
||||
if (this.gameState.activeScrambles.has(channel)) {
|
||||
const scramble = this.gameState.activeScrambles.get(channel);
|
||||
const timeLeft = Math.ceil((scramble.endTime - Date.now()) / 1000);
|
||||
bot.say(context.replyTo, `🔤 Scramble already active! "${scramble.scrambledWord}" (${timeLeft}s left)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine difficulty
|
||||
const difficulty = this.parseDifficulty(context.args[0]);
|
||||
|
||||
// Select random word
|
||||
const word = this.getRandomWord(difficulty);
|
||||
const scrambledWord = this.scrambleWord(word);
|
||||
|
||||
// Create scramble data
|
||||
const scrambleData = {
|
||||
word: word,
|
||||
scrambledWord: scrambledWord,
|
||||
difficulty: difficulty,
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now() + 60000, // 60 seconds
|
||||
hintUsed: false,
|
||||
starter: context.nick
|
||||
};
|
||||
|
||||
// Set timer for timeout
|
||||
scrambleData.timer = setTimeout(() => {
|
||||
this.timeoutScramble(channel, bot, scrambleData);
|
||||
}, 60000);
|
||||
|
||||
this.gameState.activeScrambles.set(channel, scrambleData);
|
||||
|
||||
// Announce the scramble
|
||||
bot.say(channel, `🔤 WORD SCRAMBLE: "${scrambledWord}" (${word.length} letters, ${difficulty}) - 60s to solve!`);
|
||||
bot.say(channel, `💡 Use !hint for a clue or !skip to skip (started by ${context.nick})`);
|
||||
|
||||
console.log(`🔤 Started scramble in ${channel}: ${word} → ${scrambledWord} (${difficulty})`);
|
||||
},
|
||||
|
||||
checkAnswer(data, bot) {
|
||||
const channel = data.target;
|
||||
const scramble = this.gameState.activeScrambles.get(channel);
|
||||
|
||||
if (!scramble) {
|
||||
return; // No active scramble
|
||||
}
|
||||
|
||||
const answer = data.message.toLowerCase().trim();
|
||||
const correctAnswer = scramble.word.toLowerCase();
|
||||
|
||||
if (answer === correctAnswer) {
|
||||
// Correct answer!
|
||||
this.handleCorrectAnswer(channel, data.nick, scramble, bot);
|
||||
}
|
||||
},
|
||||
|
||||
handleCorrectAnswer(channel, nick, scramble, bot) {
|
||||
// Clear timer
|
||||
clearTimeout(scramble.timer);
|
||||
|
||||
// Calculate solve time
|
||||
const solveTime = (Date.now() - scramble.startTime) / 1000;
|
||||
|
||||
// Calculate points
|
||||
let points = this.getDifficultyPoints(scramble.difficulty);
|
||||
|
||||
// Speed bonus
|
||||
if (solveTime < 10) {
|
||||
points += 1;
|
||||
}
|
||||
|
||||
// Hint penalty
|
||||
if (scramble.hintUsed) {
|
||||
points = Math.max(1, points - 1);
|
||||
}
|
||||
|
||||
// Update player score
|
||||
this.updatePlayerScore(nick, scramble.difficulty, points, solveTime);
|
||||
|
||||
// Save scores
|
||||
this.saveScores();
|
||||
|
||||
// Remove scramble
|
||||
this.gameState.activeScrambles.delete(channel);
|
||||
|
||||
// Announce success
|
||||
const speedBonus = solveTime < 10 ? ' ⚡ SPEED BONUS!' : '';
|
||||
const hintPenalty = scramble.hintUsed ? ' (hint used)' : '';
|
||||
bot.say(channel, `✅ Correct! ${nick} solved "${scramble.word}" in ${solveTime.toFixed(1)}s (+${points} points)${speedBonus}${hintPenalty}`);
|
||||
|
||||
console.log(`🎯 ${nick} solved scramble in ${channel}: ${scramble.word} (${solveTime.toFixed(1)}s, ${points}pts)`);
|
||||
},
|
||||
|
||||
giveHint(context, bot) {
|
||||
const channel = context.channel;
|
||||
if (!channel) {
|
||||
bot.say(context.replyTo, '🔤 Hints only work in channels!');
|
||||
return;
|
||||
}
|
||||
|
||||
const scramble = this.gameState.activeScrambles.get(channel);
|
||||
if (!scramble) {
|
||||
bot.say(context.replyTo, '🔤 No active scramble to give hints for!');
|
||||
return;
|
||||
}
|
||||
|
||||
if (scramble.hintUsed) {
|
||||
bot.say(context.replyTo, '💡 Hint already used for this scramble!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate hint (first and last letter)
|
||||
const word = scramble.word;
|
||||
const hint = word.length > 2 ?
|
||||
`${word[0]}${'_'.repeat(word.length - 2)}${word[word.length - 1]}` :
|
||||
`${word[0]}${'_'.repeat(word.length - 1)}`;
|
||||
|
||||
scramble.hintUsed = true;
|
||||
bot.say(channel, `💡 Hint: ${hint} (points reduced by 1)`);
|
||||
},
|
||||
|
||||
skipScramble(context, bot) {
|
||||
const channel = context.channel;
|
||||
if (!channel) {
|
||||
bot.say(context.replyTo, '🔤 Skip only works in channels!');
|
||||
return;
|
||||
}
|
||||
|
||||
const scramble = this.gameState.activeScrambles.get(channel);
|
||||
if (!scramble) {
|
||||
bot.say(context.replyTo, '🔤 No active scramble to skip!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only starter or admin can skip
|
||||
if (context.nick !== scramble.starter && context.nick !== 'megasconed') {
|
||||
bot.say(context.replyTo, '🔤 Only the scramble starter or admin can skip!');
|
||||
return;
|
||||
}
|
||||
|
||||
this.timeoutScramble(channel, bot, scramble);
|
||||
},
|
||||
|
||||
timeoutScramble(channel, bot, scramble) {
|
||||
clearTimeout(scramble.timer);
|
||||
this.gameState.activeScrambles.delete(channel);
|
||||
|
||||
bot.say(channel, `⏰ Time's up! The word was "${scramble.word}" - better luck next time!`);
|
||||
console.log(`⏰ Scramble timed out in ${channel}: ${scramble.word}`);
|
||||
},
|
||||
|
||||
// Utility functions
|
||||
parseDifficulty(input) {
|
||||
if (!input) return 'medium';
|
||||
|
||||
const difficulty = input.toLowerCase();
|
||||
if (['easy', 'medium', 'hard'].includes(difficulty)) {
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
if (difficulty === 'random') {
|
||||
const difficulties = ['easy', 'medium', 'hard'];
|
||||
return difficulties[Math.floor(Math.random() * difficulties.length)];
|
||||
}
|
||||
|
||||
return 'medium';
|
||||
},
|
||||
|
||||
getRandomWord(difficulty) {
|
||||
const words = this.wordLists[difficulty];
|
||||
return words[Math.floor(Math.random() * words.length)];
|
||||
},
|
||||
|
||||
scrambleWord(word) {
|
||||
const letters = word.split('');
|
||||
|
||||
// Fisher-Yates shuffle
|
||||
for (let i = letters.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[letters[i], letters[j]] = [letters[j], letters[i]];
|
||||
}
|
||||
|
||||
const scrambled = letters.join('');
|
||||
|
||||
// If scrambled word is the same as original, try again
|
||||
if (scrambled === word && word.length > 2) {
|
||||
return this.scrambleWord(word);
|
||||
}
|
||||
|
||||
return scrambled;
|
||||
},
|
||||
|
||||
getDifficultyPoints(difficulty) {
|
||||
const points = {
|
||||
easy: 1,
|
||||
medium: 2,
|
||||
hard: 3
|
||||
};
|
||||
return points[difficulty] || 2;
|
||||
},
|
||||
|
||||
// Player score management
|
||||
initializePlayerData(nick) {
|
||||
if (!this.gameState.scores.has(nick)) {
|
||||
this.gameState.scores.set(nick, {
|
||||
totalPoints: 0,
|
||||
totalSolved: 0,
|
||||
avgTime: 0,
|
||||
bestTime: null,
|
||||
currentStreak: 0,
|
||||
longestStreak: 0,
|
||||
difficultyStats: {
|
||||
easy: 0,
|
||||
medium: 0,
|
||||
hard: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updatePlayerScore(nick, difficulty, points, solveTime) {
|
||||
this.initializePlayerData(nick);
|
||||
const playerData = this.gameState.scores.get(nick);
|
||||
|
||||
// Update totals
|
||||
playerData.totalPoints += points;
|
||||
playerData.totalSolved += 1;
|
||||
playerData.currentStreak += 1;
|
||||
|
||||
// Update best time
|
||||
if (playerData.bestTime === null || solveTime < playerData.bestTime) {
|
||||
playerData.bestTime = solveTime;
|
||||
}
|
||||
|
||||
// Update longest streak
|
||||
if (playerData.currentStreak > playerData.longestStreak) {
|
||||
playerData.longestStreak = playerData.currentStreak;
|
||||
}
|
||||
|
||||
// Update average time
|
||||
playerData.avgTime = ((playerData.avgTime * (playerData.totalSolved - 1)) + solveTime) / playerData.totalSolved;
|
||||
|
||||
// Update difficulty stats
|
||||
playerData.difficultyStats[difficulty] += 1;
|
||||
|
||||
this.gameState.scores.set(nick, playerData);
|
||||
},
|
||||
|
||||
showScore(context, bot) {
|
||||
const playerData = this.gameState.scores.get(context.nick);
|
||||
if (!playerData) {
|
||||
bot.say(context.replyTo, `🔤 ${context.nick} hasn't solved any scrambles yet!`);
|
||||
return;
|
||||
}
|
||||
|
||||
bot.say(context.replyTo, `🎯 ${context.nick}: ${playerData.totalPoints} points from ${playerData.totalSolved} solved words!`);
|
||||
},
|
||||
|
||||
showPlayerStats(context, bot) {
|
||||
const playerData = this.gameState.scores.get(context.nick);
|
||||
if (!playerData) {
|
||||
bot.say(context.replyTo, `🔤 ${context.nick} hasn't solved any scrambles yet!`);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = playerData.difficultyStats;
|
||||
bot.say(context.replyTo, `📊 ${context.nick}'s Stats:`);
|
||||
bot.say(context.replyTo, `🎯 Points: ${playerData.totalPoints} | Words: ${playerData.totalSolved} | Streak: ${playerData.currentStreak} (best: ${playerData.longestStreak})`);
|
||||
bot.say(context.replyTo, `⏱️ Avg time: ${playerData.avgTime.toFixed(1)}s | Best: ${playerData.bestTime ? playerData.bestTime.toFixed(1) + 's' : 'N/A'}`);
|
||||
bot.say(context.replyTo, `🔤 Easy: ${stats.easy} | Medium: ${stats.medium} | Hard: ${stats.hard}`);
|
||||
},
|
||||
|
||||
showLeaderboard(context, bot) {
|
||||
const scores = Array.from(this.gameState.scores.entries())
|
||||
.sort((a, b) => b[1].totalPoints - a[1].totalPoints)
|
||||
.slice(0, 10);
|
||||
|
||||
if (scores.length === 0) {
|
||||
bot.say(context.replyTo, '🔤 No one has solved any scrambles yet!');
|
||||
return;
|
||||
}
|
||||
|
||||
bot.say(context.replyTo, '🏆 TOP SCRAMBLERS 🏆');
|
||||
scores.forEach(([nick, data], index) => {
|
||||
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
|
||||
bot.say(context.replyTo, `${medal} ${index + 1}. ${nick}: ${data.totalPoints} points (${data.totalSolved} words)`);
|
||||
});
|
||||
},
|
||||
|
||||
// Score persistence
|
||||
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} scramble scores from ${this.scoresFile}`);
|
||||
|
||||
// Log top 3 players if any exist
|
||||
if (this.gameState.scores.size > 0) {
|
||||
const topPlayers = Array.from(this.gameState.scores.entries())
|
||||
.sort((a, b) => b[1].totalPoints - a[1].totalPoints)
|
||||
.slice(0, 3);
|
||||
console.log('🏆 Top scramblers:', topPlayers.map(([name, data]) => `${name}(${data.totalPoints}pts)`).join(', '));
|
||||
}
|
||||
} else {
|
||||
console.log(`🔤 No existing scramble scores file found, starting fresh`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading scramble scores:`, error);
|
||||
this.gameState.scores = new Map();
|
||||
}
|
||||
},
|
||||
|
||||
saveScores() {
|
||||
try {
|
||||
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} scramble scores to ${this.scoresFile}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error saving scramble scores:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue