- Added hunt/feed duck mechanics (80% hunt, 20% feed) - Implemented persistent scoring system - Added channel control commands (\!stopducks/\!startducks) - Enhanced duck hunt with wrong action penalties - Organized bot structure with botmain.js as main file - Added comprehensive documentation (README.md) - Included 17 plugins with various games and utilities 🦆 Duck Hunt Features: - Hunt ducks with \!shoot/\!bang (80% of spawns) - Feed ducks with \!feed (20% of spawns) - Persistent scores saved to JSON - Channel-specific controls for #bakedbeans - Reaction time tracking and special achievements 🎮 Other Games: - Casino games (slots, coinflip, hi-lo, scratch cards) - Multiplayer games (pigs, zombie dice, quiplash) - Text generation (babble, conspiracy, drunk historian) - Interactive features (story writing, emojify, combos) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
409 lines
No EOL
17 KiB
JavaScript
409 lines
No EOL
17 KiB
JavaScript
// plugins/pigs.js - Pass the Pigs game plugin
|
|
module.exports = {
|
|
init(bot) {
|
|
console.log('Pass the Pigs plugin initialized');
|
|
this.bot = bot;
|
|
|
|
// Game state and scores storage
|
|
this.pigScores = new Map(); // nick -> { totalScore, bestRoll, rollCount, name, eliminated }
|
|
this.gameState = {
|
|
active: false,
|
|
players: [],
|
|
currentPlayer: 0,
|
|
turnScore: 0,
|
|
channel: null
|
|
};
|
|
},
|
|
|
|
cleanup(bot) {
|
|
console.log('Pass the Pigs plugin cleaned up');
|
|
// Clear any active games on cleanup
|
|
this.gameState = {
|
|
active: false,
|
|
players: [],
|
|
currentPlayer: 0,
|
|
turnScore: 0,
|
|
channel: null
|
|
};
|
|
},
|
|
|
|
// Pass the Pigs game simulation with increased difficulty
|
|
rollPigs() {
|
|
// Actual probabilities from 11,954 sample study
|
|
const positions = [
|
|
{ name: 'Side (no dot)', points: 0, weight: 34.9 },
|
|
{ name: 'Side (dot)', points: 0, weight: 30.2 },
|
|
{ name: 'Razorback', points: 5, weight: 22.4 },
|
|
{ name: 'Trotter', points: 5, weight: 8.8 },
|
|
{ name: 'Snouter', points: 10, weight: 3.0 },
|
|
{ name: 'Leaning Jowler', points: 15, weight: 0.61 }
|
|
];
|
|
|
|
const rollPig = () => {
|
|
const totalWeight = positions.reduce((sum, pos) => sum + pos.weight, 0);
|
|
let random = Math.random() * totalWeight;
|
|
|
|
for (const position of positions) {
|
|
random -= position.weight;
|
|
if (random <= 0) {
|
|
return position;
|
|
}
|
|
}
|
|
return positions[0]; // fallback
|
|
};
|
|
|
|
const pig1 = rollPig();
|
|
const pig2 = rollPig();
|
|
|
|
// Check for touching conditions first (increased probability for more difficulty)
|
|
if (Math.random() < 0.035) { // 3.5% chance of touching (was 1.5%)
|
|
if (Math.random() < 0.25) { // 25% of touches are Piggyback
|
|
return { result: 'Piggyback', points: -9999, description: '🐷📚 Piggyback! ELIMINATED!' };
|
|
} else { // 75% are Makin' Bacon/Oinker
|
|
return { result: 'Makin\' Bacon', points: -999, description: '🥓 Makin\' Bacon! Lose ALL points!' };
|
|
}
|
|
}
|
|
|
|
// Handle side positions (Sider vs Pig Out)
|
|
const pig1IsSide = pig1.name.startsWith('Side');
|
|
const pig2IsSide = pig2.name.startsWith('Side');
|
|
|
|
if (pig1IsSide && pig2IsSide) {
|
|
// Both on sides - increased chance of opposite sides (Pig Out)
|
|
const pig1Dot = pig1.name.includes('dot');
|
|
const pig2Dot = pig2.name.includes('dot');
|
|
|
|
// Bias toward opposite sides for more busts
|
|
const oppositeRoll = Math.random();
|
|
if (oppositeRoll < 0.6) { // 60% chance of opposite sides when both are siders
|
|
return { result: 'Pig Out', points: 0, description: '💥 Pig Out! Turn ends!' };
|
|
} else {
|
|
return { result: 'Sider', points: 1, description: '🐷 Sider' };
|
|
}
|
|
}
|
|
|
|
// If only one pig is on its side, that pig scores nothing
|
|
if (pig1IsSide && !pig2IsSide) {
|
|
return {
|
|
result: pig2.name,
|
|
points: pig2.points,
|
|
description: `🐷 ${pig2.name} (${pig2.points})`
|
|
};
|
|
}
|
|
|
|
if (pig2IsSide && !pig1IsSide) {
|
|
return {
|
|
result: pig1.name,
|
|
points: pig1.points,
|
|
description: `🐷 ${pig1.name} (${pig1.points})`
|
|
};
|
|
}
|
|
|
|
// Neither pig is on its side - normal scoring
|
|
if (pig1.name === pig2.name) {
|
|
// Double combinations - sum doubled (quadruple individual)
|
|
const doublePoints = (pig1.points + pig2.points) * 2;
|
|
switch (pig1.name) {
|
|
case 'Razorback':
|
|
return { result: 'Double Razorback', points: doublePoints, description: '🐷🐷 Double Razorback (20)' };
|
|
case 'Trotter':
|
|
return { result: 'Double Trotter', points: doublePoints, description: '🐷🐷 Double Trotter (20)' };
|
|
case 'Snouter':
|
|
return { result: 'Double Snouter', points: doublePoints, description: '🐷🐷 Double Snouter (40)' };
|
|
case 'Leaning Jowler':
|
|
return { result: 'Double Leaning Jowler', points: doublePoints, description: '🐷🐷 Double Leaning Jowler (60)' };
|
|
}
|
|
}
|
|
|
|
// Mixed combination - sum of individual scores
|
|
const totalPoints = pig1.points + pig2.points;
|
|
return {
|
|
result: 'Mixed Combo',
|
|
points: totalPoints,
|
|
description: `🐷 ${pig1.name} + ${pig2.name} (${totalPoints})`
|
|
};
|
|
},
|
|
|
|
// Turn management for pig game
|
|
nextTurn(target) {
|
|
this.gameState.currentPlayer = (this.gameState.currentPlayer + 1) % 2;
|
|
const nextPlayer = this.gameState.players[this.gameState.currentPlayer];
|
|
|
|
// Check if next player is eliminated
|
|
const nextPlayerStats = this.pigScores.get(nextPlayer);
|
|
if (nextPlayerStats.eliminated) {
|
|
const winner = this.gameState.players[(this.gameState.currentPlayer + 1) % 2];
|
|
this.bot.say(target, `${nextPlayer} is eliminated! ${winner} wins!`);
|
|
this.endGame(target);
|
|
return;
|
|
}
|
|
|
|
this.bot.say(target, `🎲 ${nextPlayer}'s turn!`);
|
|
},
|
|
|
|
endGame(target) {
|
|
this.bot.say(target, '🏁 Game ended! Use !pigs to start new game.');
|
|
this.gameState = {
|
|
active: false,
|
|
players: [],
|
|
currentPlayer: 0,
|
|
turnScore: 0,
|
|
channel: null
|
|
};
|
|
},
|
|
|
|
commands: [
|
|
{
|
|
name: 'pigs',
|
|
description: 'Start or join a Pass the Pigs game',
|
|
execute(context, bot) {
|
|
const target = context.replyTo;
|
|
const from = context.nick;
|
|
const to = context.channel || context.replyTo;
|
|
|
|
// Only allow channel play for turn-based games
|
|
if (!context.channel) {
|
|
bot.say(target, 'Turn-based pig games can only be played in channels!');
|
|
return;
|
|
}
|
|
|
|
// Game setup phase
|
|
if (!this.gameState.active) {
|
|
// First player joins
|
|
if (this.gameState.players.length === 0) {
|
|
this.gameState.players.push(from);
|
|
this.gameState.channel = to;
|
|
|
|
// Initialize player
|
|
if (!this.pigScores.has(from)) {
|
|
this.pigScores.set(from, {
|
|
totalScore: 0,
|
|
bestRoll: 0,
|
|
rollCount: 0,
|
|
name: from,
|
|
eliminated: false
|
|
});
|
|
}
|
|
|
|
bot.say(target, `🐷 ${from} wants to start a pig game! Second player use !pigs to join`);
|
|
return;
|
|
}
|
|
|
|
// Second player joins - start game
|
|
if (this.gameState.players.length === 1 && !this.gameState.players.includes(from)) {
|
|
this.gameState.players.push(from);
|
|
this.gameState.active = true;
|
|
this.gameState.currentPlayer = 0;
|
|
this.gameState.turnScore = 0;
|
|
|
|
// Initialize second player
|
|
if (!this.pigScores.has(from)) {
|
|
this.pigScores.set(from, {
|
|
totalScore: 0,
|
|
bestRoll: 0,
|
|
rollCount: 0,
|
|
name: from,
|
|
eliminated: false
|
|
});
|
|
}
|
|
|
|
bot.say(target, `🎮 ${this.gameState.players[0]} vs ${this.gameState.players[1]} - ${this.gameState.players[0]} goes first!`);
|
|
return;
|
|
}
|
|
|
|
// Handle various edge cases
|
|
if (this.gameState.players.includes(from)) {
|
|
bot.say(target, `${from}: You're already in the game!`);
|
|
return;
|
|
} else {
|
|
bot.say(target, `${from}: Game is full! Wait for current game to finish.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Game is active - check if it's player's turn
|
|
if (!this.gameState.players.includes(from)) {
|
|
bot.say(target, `${from}: You're not in the current game!`);
|
|
return;
|
|
}
|
|
|
|
const currentPlayerName = this.gameState.players[this.gameState.currentPlayer];
|
|
if (from !== currentPlayerName) {
|
|
bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`);
|
|
return;
|
|
}
|
|
|
|
// Check if current player is eliminated
|
|
const playerStats = this.pigScores.get(from);
|
|
if (playerStats.eliminated) {
|
|
bot.say(target, `${from}: You are eliminated!`);
|
|
this.endGame(target);
|
|
return;
|
|
}
|
|
|
|
// Roll the pigs!
|
|
const roll = this.rollPigs();
|
|
playerStats.rollCount++;
|
|
|
|
let message = '';
|
|
let endTurn = false;
|
|
let endGame = false;
|
|
|
|
if (roll.points === -9999) {
|
|
// Piggyback - eliminate player and end game
|
|
playerStats.eliminated = true;
|
|
message = `${from}: ${roll.description}`;
|
|
endGame = true;
|
|
} else if (roll.points === -999) {
|
|
// Makin' Bacon - lose all points and end turn
|
|
const lostPoints = playerStats.totalScore;
|
|
playerStats.totalScore = 0;
|
|
this.gameState.turnScore = 0;
|
|
message = `${from}: ${roll.description} Lost ${lostPoints} total!`;
|
|
endTurn = true;
|
|
} else if (roll.points === 0) {
|
|
// Pig Out - lose turn score and end turn
|
|
message = `${from}: ${roll.description} Lost ${this.gameState.turnScore} turn points!`;
|
|
this.gameState.turnScore = 0;
|
|
endTurn = true;
|
|
} else {
|
|
// Normal scoring - add to turn score
|
|
this.gameState.turnScore += roll.points;
|
|
|
|
// Update best roll if this is better
|
|
if (roll.points > playerStats.bestRoll) {
|
|
playerStats.bestRoll = roll.points;
|
|
}
|
|
|
|
const potentialTotal = playerStats.totalScore + this.gameState.turnScore;
|
|
message = `${from}: ${roll.description} | Turn: ${this.gameState.turnScore} | Total: ${playerStats.totalScore}`;
|
|
|
|
// Check potential win
|
|
if (potentialTotal >= 100) {
|
|
message += ` | Can win!`;
|
|
}
|
|
}
|
|
|
|
bot.say(target, message);
|
|
|
|
if (endGame) {
|
|
this.endGame(target);
|
|
} else if (endTurn) {
|
|
this.nextTurn(target);
|
|
}
|
|
}.bind(this)
|
|
},
|
|
|
|
{
|
|
name: 'bank',
|
|
description: 'Bank your turn points in Pass the Pigs',
|
|
execute(context, bot) {
|
|
const target = context.replyTo;
|
|
const from = context.nick;
|
|
|
|
if (!this.gameState.active || !this.gameState.players.includes(from)) {
|
|
bot.say(target, `${from}: You're not in an active pig game!`);
|
|
return;
|
|
}
|
|
|
|
const currentPlayerName = this.gameState.players[this.gameState.currentPlayer];
|
|
if (from !== currentPlayerName) {
|
|
bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`);
|
|
return;
|
|
}
|
|
|
|
if (this.gameState.turnScore === 0) {
|
|
bot.say(target, `${from}: No points to bank!`);
|
|
return;
|
|
}
|
|
|
|
// Bank the points
|
|
const playerStats = this.pigScores.get(from);
|
|
playerStats.totalScore += this.gameState.turnScore;
|
|
|
|
bot.say(target, `${from}: Banked ${this.gameState.turnScore} points! Total: ${playerStats.totalScore}`);
|
|
|
|
// Check for win
|
|
if (playerStats.totalScore >= 100) {
|
|
bot.say(target, `🎉 ${from} WINS with ${playerStats.totalScore} points! 🎉`);
|
|
this.endGame(target);
|
|
return;
|
|
}
|
|
|
|
this.gameState.turnScore = 0;
|
|
this.nextTurn(target);
|
|
}.bind(this)
|
|
},
|
|
|
|
{
|
|
name: 'quitpigs',
|
|
description: 'Quit the current pig game',
|
|
execute(context, bot) {
|
|
const target = context.replyTo;
|
|
const from = context.nick;
|
|
|
|
if (!this.gameState.active && this.gameState.players.length === 0) {
|
|
bot.say(target, `${from}: No pig game to quit!`);
|
|
return;
|
|
}
|
|
|
|
if (this.gameState.players.includes(from)) {
|
|
bot.say(target, `${from} quit the pig game!`);
|
|
this.endGame(target);
|
|
} else {
|
|
bot.say(target, `${from}: You're not in the current game!`);
|
|
}
|
|
}.bind(this)
|
|
},
|
|
|
|
{
|
|
name: 'toppigs',
|
|
description: 'Show top pig players',
|
|
execute(context, bot) {
|
|
const target = context.replyTo;
|
|
|
|
if (this.pigScores.size === 0) {
|
|
bot.say(target, 'No pig scores recorded yet! Use !pigs to start playing.');
|
|
return;
|
|
}
|
|
|
|
// Convert to array and sort by total score
|
|
const sortedScores = Array.from(this.pigScores.values())
|
|
.sort((a, b) => b.totalScore - a.totalScore)
|
|
.slice(0, 5); // Top 5
|
|
|
|
bot.say(target, '🏆 Top Pig Players:');
|
|
|
|
sortedScores.forEach((player, index) => {
|
|
const rank = index + 1;
|
|
const trophy = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `${rank}.`;
|
|
bot.say(target, `${trophy} ${player.name}: ${player.totalScore} points (best roll: ${player.bestRoll}, ${player.rollCount} rolls)`);
|
|
});
|
|
}.bind(this)
|
|
},
|
|
|
|
{
|
|
name: 'pigstatus',
|
|
description: 'Show current pig game status',
|
|
execute(context, bot) {
|
|
const target = context.replyTo;
|
|
|
|
if (!this.gameState.active) {
|
|
if (this.gameState.players.length === 1) {
|
|
bot.say(target, `🐷 ${this.gameState.players[0]} is waiting for a second player! Use !pigs to join.`);
|
|
} else {
|
|
bot.say(target, 'No active pig game. Use !pigs to start one!');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const currentPlayer = this.gameState.players[this.gameState.currentPlayer];
|
|
const player1Stats = this.pigScores.get(this.gameState.players[0]);
|
|
const player2Stats = this.pigScores.get(this.gameState.players[1]);
|
|
|
|
bot.say(target, `🎮 Current Game: ${this.gameState.players[0]} (${player1Stats.totalScore}) vs ${this.gameState.players[1]} (${player2Stats.totalScore})`);
|
|
bot.say(target, `🎲 ${currentPlayer}'s turn | Turn score: ${this.gameState.turnScore}`);
|
|
}.bind(this)
|
|
}
|
|
]
|
|
}; |