- 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>
462 lines
No EOL
17 KiB
JavaScript
462 lines
No EOL
17 KiB
JavaScript
// 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);
|
|
}
|
|
}
|
|
}; |