// 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); } } };