From 86bf92c47803a44a4fbb862a155a45caf52e9d18 Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Fri, 12 Sep 2025 20:59:52 +0100 Subject: [PATCH] Complete inventory system implementation - Added inventory capacity limits (configurable, default 20 slots) - Items are stored in inventory before use for strategic gameplay - Fixed indentation issues and syntax errors - Inventory system fully configurable via config.json settings --- duckhunt/CONFIG_GUIDE.md | 272 ++ duckhunt/README.md | 172 ++ .../simple_duckhunt.cpython-312.pyc | Bin 0 -> 119340 bytes duckhunt/config.json | 205 ++ duckhunt/config_backup.json | 216 ++ duckhunt/demo_sasl.py | 122 + duckhunt/duckhunt.db | Bin 0 -> 36864 bytes duckhunt/duckhunt.json | 960 +++++++ duckhunt/duckhunt.log | 543 ++++ duckhunt/duckhunt.py | 37 + duckhunt/simple_duckhunt.py | 2243 +++++++++++++++++ duckhunt/src/__pycache__/auth.cpython-312.pyc | Bin 0 -> 6452 bytes duckhunt/src/__pycache__/db.cpython-312.pyc | Bin 0 -> 6838 bytes .../__pycache__/duckhuntbot.cpython-312.pyc | Bin 0 -> 14681 bytes duckhunt/src/__pycache__/game.cpython-312.pyc | Bin 0 -> 33969 bytes .../src/__pycache__/items.cpython-312.pyc | Bin 0 -> 2878 bytes .../__pycache__/logging_utils.cpython-312.pyc | Bin 0 -> 1724 bytes duckhunt/src/__pycache__/sasl.cpython-312.pyc | Bin 0 -> 11033 bytes .../src/__pycache__/utils.cpython-312.pyc | Bin 0 -> 705 bytes duckhunt/src/auth.py | 108 + duckhunt/src/db.py | 97 + duckhunt/src/duckhuntbot.py | 277 ++ duckhunt/src/game.py | 566 +++++ duckhunt/src/items.py | 124 + duckhunt/src/logging_utils.py | 28 + duckhunt/src/sasl.py | 198 ++ duckhunt/src/utils.py | 11 + duckhunt/test_bot.py | 167 ++ 28 files changed, 6346 insertions(+) create mode 100644 duckhunt/CONFIG_GUIDE.md create mode 100644 duckhunt/README.md create mode 100644 duckhunt/__pycache__/simple_duckhunt.cpython-312.pyc create mode 100644 duckhunt/config.json create mode 100644 duckhunt/config_backup.json create mode 100644 duckhunt/demo_sasl.py create mode 100644 duckhunt/duckhunt.db create mode 100644 duckhunt/duckhunt.json create mode 100644 duckhunt/duckhunt.log create mode 100644 duckhunt/duckhunt.py create mode 100644 duckhunt/simple_duckhunt.py create mode 100644 duckhunt/src/__pycache__/auth.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/db.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/duckhuntbot.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/game.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/items.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/logging_utils.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/sasl.cpython-312.pyc create mode 100644 duckhunt/src/__pycache__/utils.cpython-312.pyc create mode 100644 duckhunt/src/auth.py create mode 100644 duckhunt/src/db.py create mode 100644 duckhunt/src/duckhuntbot.py create mode 100644 duckhunt/src/game.py create mode 100644 duckhunt/src/items.py create mode 100644 duckhunt/src/logging_utils.py create mode 100644 duckhunt/src/sasl.py create mode 100644 duckhunt/src/utils.py create mode 100644 duckhunt/test_bot.py diff --git a/duckhunt/CONFIG_GUIDE.md b/duckhunt/CONFIG_GUIDE.md new file mode 100644 index 0000000..d45e7aa --- /dev/null +++ b/duckhunt/CONFIG_GUIDE.md @@ -0,0 +1,272 @@ +# DuckHunt Bot Configuration Guide + +This document explains all the configuration options available in `config.json` to customize your DuckHunt bot experience. + +## Basic IRC Settings +```json +{ + "server": "irc.rizon.net", // IRC server hostname + "port": 6697, // IRC server port (6667 for non-SSL, 6697 for SSL) + "nick": "DuckHunt", // Bot's nickname + "channels": ["#channel"], // List of channels to join + "ssl": true, // Enable SSL/TLS connection + "password": "", // Server password (if required) + "admins": ["nick1", "nick2"] // List of admin nicknames +} +``` + +## SASL Authentication +```json +"sasl": { + "enabled": true, // Enable SASL authentication + "username": "botaccount", // NickServ account username + "password": "botpassword" // NickServ account password +} +``` + +## Duck Spawning Configuration +```json +"duck_spawn_min": 1800, // Minimum time between duck spawns (seconds) +"duck_spawn_max": 5400, // Maximum time between duck spawns (seconds) +"duck_timeout_min": 45, // Minimum time duck stays alive (seconds) +"duck_timeout_max": 75, // Maximum time duck stays alive (seconds) +"sleep_hours": [], // Hours when no ducks spawn [start_hour, end_hour] +"max_ducks_per_channel": 3, // Maximum ducks that can exist per channel +``` + +### Duck Types +Configure different duck types with spawn rates and rewards: +```json +"duck_types": { + "normal": { + "enabled": true, // Enable this duck type + "spawn_rate": 70, // Percentage chance to spawn (out of 100) + "xp_reward": 10, // XP gained when caught + "health": 1 // How many hits to kill + }, + "golden": { + "enabled": true, + "spawn_rate": 8, + "xp_reward": 50, + "health": 1 + } + // ... more duck types +} +``` + +## Duck Befriending System +```json +"befriending": { + "enabled": true, // Enable !bef command + "base_success_rate": 65, // Base chance of successful befriend (%) + "max_success_rate": 90, // Maximum possible success rate (%) + "level_bonus_per_level": 2, // Success bonus per player level (%) + "level_bonus_cap": 20, // Maximum level bonus (%) + "luck_bonus_per_point": 3, // Success bonus per luck point (%) + "xp_reward": 8, // XP gained on successful befriend + "xp_reward_min": 1, // Minimum XP from befriending + "xp_reward_max": 3, // Maximum XP from befriending + "failure_xp_penalty": 1, // XP lost on failed befriend + "scared_away_chance": 10, // Chance duck flies away on failure (%) + "lucky_item_chance": 5 // Base chance for lucky item drops (%) +} +``` + +## Shooting Mechanics +```json +"shooting": { + "enabled": true, // Enable !bang command + "base_accuracy": 85, // Starting player accuracy (%) + "base_reliability": 90, // Starting gun reliability (%) + "jam_chance_base": 10, // Base gun jam chance (%) + "friendly_fire_enabled": true, // Allow shooting other players + "friendly_fire_chance": 5, // Chance of friendly fire (%) + "reflex_shot_bonus": 5, // Bonus for quick shots (%) + "miss_xp_penalty": 5, // XP lost on missed shot + "wild_shot_xp_penalty": 10, // XP lost on wild shot + "teamkill_xp_penalty": 20 // XP lost on team kill +} +``` + +## Weapon System +```json +"weapons": { + "enabled": true, // Enable weapon mechanics + "starting_weapon": "pistol", // Default weapon for new players + "starting_ammo": 6, // Starting ammo count + "max_ammo_base": 6, // Base maximum ammo capacity + "starting_chargers": 2, // Starting reload items + "max_chargers_base": 2, // Base maximum reload items + "durability_enabled": true, // Enable weapon wear/breaking + "confiscation_enabled": true // Allow admin gun confiscation +} +``` + +## Economy System +```json +"economy": { + "enabled": true, // Enable coin/shop system + "starting_coins": 100, // Coins for new players + "shop_enabled": true, // Enable !shop command + "trading_enabled": true, // Enable !trade command + "theft_enabled": true, // Enable !steal command + "theft_success_rate": 30, // Chance theft succeeds (%) + "theft_penalty": 50, // Coins lost if theft fails + "banking_enabled": true, // Enable banking system + "interest_rate": 5, // Bank interest rate (%) + "loan_enabled": true // Enable loan system +} +``` + +## Player Progression +```json +"progression": { + "enabled": true, // Enable XP/leveling system + "max_level": 40, // Maximum player level + "xp_multiplier": 1.0, // Global XP multiplier + "level_benefits_enabled": true, // Level bonuses (accuracy, etc.) + "titles_enabled": true, // Show player titles + "prestige_enabled": false // Enable prestige system +} +``` + +## Karma System +```json +"karma": { + "enabled": true, // Enable karma tracking + "hit_bonus": 2, // Karma for successful shots + "golden_hit_bonus": 5, // Karma for golden duck hits + "teamkill_penalty": 10, // Karma lost for team kills + "wild_shot_penalty": 3, // Karma lost for wild shots + "miss_penalty": 1, // Karma lost for misses + "befriend_success_bonus": 2, // Karma for successful befriends + "befriend_fail_penalty": 1 // Karma lost for failed befriends +} +``` + +## Items and Powerups +```json +"items": { + "enabled": true, // Enable item system + "lucky_items_enabled": true, // Enable lucky item drops + "lucky_item_base_chance": 5, // Base lucky item chance (%) + "detector_enabled": true, // Enable duck detector item + "silencer_enabled": true, // Enable silencer item + "sunglasses_enabled": true, // Enable sunglasses item + "explosive_ammo_enabled": true, // Enable explosive ammo + "sabotage_enabled": true, // Enable sabotage mechanics + "insurance_enabled": true, // Enable insurance system + "decoy_enabled": true // Enable decoy ducks +} +``` + +## Social Features +```json +"social": { + "leaderboards_enabled": true, // Enable !top command + "duck_alerts_enabled": true, // Enable duck spawn notifications + "private_messages_enabled": true, // Allow PM commands + "statistics_sharing_enabled": true, // Enable !stats sharing + "achievements_enabled": false // Enable achievement system +} +``` + +## Moderation Features +```json +"moderation": { + "ignore_system_enabled": true, // Enable !ignore command + "rate_limiting_enabled": true, // Prevent command spam + "rate_limit_cooldown": 2.0, // Seconds between commands + "admin_commands_enabled": true, // Enable admin commands + "ban_system_enabled": true, // Enable player banning + "database_reset_enabled": true, // Allow database resets + "admin_rearm_gives_full_ammo": true, // Admin !rearm gives full ammo + "admin_rearm_gives_full_chargers": true // Admin !rearm gives full chargers +} +``` + +## Advanced Features +```json +"advanced": { + "gun_jamming_enabled": true, // Enable gun jam mechanics + "weather_effects_enabled": false, // Weather affecting gameplay + "seasonal_events_enabled": false, // Special holiday events + "daily_challenges_enabled": false, // Daily quest system + "guild_system_enabled": false, // Player guilds/teams + "pvp_enabled": false // Player vs player combat +} +``` + +## Message Customization +```json +"messages": { + "custom_duck_messages_enabled": true, // Varied duck spawn messages + "color_enabled": true, // IRC color codes in messages + "emoji_enabled": true, // Unicode emojis in messages + "verbose_messages": true, // Detailed action messages + "success_sound_effects": true // Text sound effects +} +``` + +## Database Settings +```json +"database": { + "auto_save_enabled": true, // Automatic database saving + "auto_save_interval": 300, // Auto-save every N seconds + "backup_enabled": true, // Create database backups + "backup_interval": 3600, // Backup every N seconds + "compression_enabled": false // Compress database files +} +``` + +## Debug Options +```json +"debug": { + "debug_mode": false, // Enable debug features + "verbose_logging": false, // Extra detailed logs + "command_logging": false, // Log all commands + "performance_monitoring": false // Track performance metrics +} +``` + +## Configuration Tips + +1. **Duck Spawn Timing**: Adjust `duck_spawn_min/max` based on channel activity +2. **Difficulty**: Lower `befriending.base_success_rate` for harder gameplay +3. **Economy**: Adjust XP rewards to balance progression +4. **Features**: Disable unwanted features by setting `enabled: false` +5. **Performance**: Enable rate limiting and disable verbose logging for busy channels +6. **Testing**: Use debug mode and shorter spawn times for testing + +## Example Configurations + +### Casual Server (Easy) +```json +"befriending": { + "base_success_rate": 80, + "max_success_rate": 95 +}, +"economy": { + "starting_coins": 200 +} +``` + +### Competitive Server (Hard) +```json +"befriending": { + "base_success_rate": 45, + "max_success_rate": 75 +}, +"shooting": { + "base_accuracy": 70, + "friendly_fire_chance": 10 +} +``` + +### Minimal Features +```json +"befriending": { "enabled": false }, +"items": { "enabled": false }, +"karma": { "enabled": false }, +"social": { "leaderboards_enabled": false } +``` diff --git a/duckhunt/README.md b/duckhunt/README.md new file mode 100644 index 0000000..f17e78d --- /dev/null +++ b/duckhunt/README.md @@ -0,0 +1,172 @@ +# šŸ¦† DuckHunt IRC Bot + +A feature-rich IRC game bot where players hunt ducks, upgrade weapons, trade items, and compete on leaderboards! + +## šŸš€ Features + +### šŸŽÆ Core Game Mechanics +- **Different Duck Types**: Common, Rare, Golden, and Armored ducks with varying rewards +- **Weapon System**: Multiple weapon types (Basic Gun, Shotgun, Rifle) with durability +- **Ammunition Types**: Standard, Rubber Bullets, Explosive Rounds +- **Weapon Attachments**: Laser Sight, Extended Magazine, Bipod +- **Accuracy & Reliability**: Skill-based hit/miss and reload failure mechanics + +### šŸ¦ Economy System +- **Shop**: Buy/sell weapons, attachments, and upgrades +- **Banking**: Deposit coins for interest, take loans +- **Trading**: Trade coins and items with other players +- **Insurance**: Protect your equipment from damage +- **Hunting Licenses**: Unlock premium features and bonuses + +### šŸ‘¤ Player Progression +- **Hunter Levels**: Gain XP and level up for better abilities +- **Account System**: Register accounts with password authentication +- **Multiple Auth Methods**: Nick-based, hostmask, or registered account +- **Persistent Stats**: All progress saved to SQLite database + +### šŸ† Social Features +- **Leaderboards**: Compete for top rankings +- **Duck Alerts**: Get notified when rare ducks spawn +- **Sabotage**: Interfere with other players (for a cost!) +- **Comprehensive Help**: Detailed command reference + +## šŸ“‹ Requirements + +- Python 3.7+ +- asyncio support +- SQLite3 (included with Python) + +## šŸ› ļø Installation + +1. Clone or download the bot files +2. Edit `config.json` with your IRC server details: + ```json + { + "server": "irc.libera.chat", + "port": 6697, + "nick": "DuckHuntBot", + "channels": ["#yourchannel"], + "ssl": true, + "sasl": false, + "password": "", + "duck_spawn_min": 60, + "duck_spawn_max": 300 + } + ``` + +3. Test the bot: + ```bash + python test_bot.py + ``` + +4. Run the bot: + ```bash + python duckhunt.py + ``` + +## šŸŽ® Commands + +### šŸŽÆ Hunting +- `!bang` - Shoot at a duck (accuracy-based hit/miss) +- `!reload` - Reload weapon (can fail based on reliability) +- `!catch` - Catch a duck with your hands +- `!bef` - Befriend a duck instead of shooting + +### šŸ›’ Economy +- `!shop` - View available items +- `!buy ` - Purchase items +- `!sell ` - Sell equipment +- `!bank` - Banking services +- `!trade ` - Trade with others + +### šŸ“Š Stats & Info +- `!stats` - Detailed combat statistics +- `!duckstats` - Personal hunting record +- `!leaderboard` - Top players ranking +- `!license` - Hunting license management + +### āš™ļø Settings +- `!alerts` - Toggle duck spawn notifications +- `!help` - Complete command reference + +### šŸ” Account System +- `/msg BotNick register ` - Register account +- `/msg BotNick identify ` - Login to account + +### šŸŽ® Advanced +- `!sabotage ` - Sabotage another hunter's weapon + +## šŸ—‚ļø File Structure + +``` +duckhunt/ +ā”œā”€ā”€ src/ +│ ā”œā”€ā”€ duckhuntbot.py # Main IRC bot logic +│ ā”œā”€ā”€ game.py # Game mechanics and commands +│ ā”œā”€ā”€ db.py # SQLite database handling +│ ā”œā”€ā”€ auth.py # Authentication system +│ ā”œā”€ā”€ items.py # Duck types, weapons, attachments +│ ā”œā”€ā”€ logging_utils.py # Colored logging setup +│ └── utils.py # IRC message parsing +ā”œā”€ā”€ config.json # Bot configuration +ā”œā”€ā”€ duckhunt.py # Main entry point +ā”œā”€ā”€ test_bot.py # Test script +└── README.md # This file +``` + +## šŸŽÆ Game Balance + +### Duck Types & Rewards +- **Common Duck** šŸ¦†: 1 coin, 10 XP (70% spawn rate) +- **Rare Duck** šŸ¦†āœØ: 3 coins, 25 XP (20% spawn rate) +- **Golden Duck** šŸ„‡šŸ¦†: 10 coins, 50 XP (8% spawn rate) +- **Armored Duck** šŸ›”ļøšŸ¦†: 15 coins, 75 XP (2% spawn rate, 3 health) + +### Weapon Stats +- **Basic Gun**: 0% accuracy bonus, 100 durability, 1 attachment slot +- **Shotgun**: -10% accuracy, 80 durability, 2 slots, spread shot +- **Rifle**: +20% accuracy, 120 durability, 3 slots + +### Progression +- Players start with 100 coins and basic stats +- Level up by gaining XP from successful hunts +- Unlock better equipment and abilities as you progress + +## šŸ”§ Configuration + +Edit `config.json` to customize: +- IRC server and channels +- Duck spawn timing (min/max seconds) +- SSL and SASL authentication +- Bot nickname + +## šŸ›”ļø Security + +- Passwords are hashed with PBKDF2 +- Account data stored separately from temporary nick data +- Multiple authentication methods supported +- Database uses prepared statements to prevent injection + +## šŸ› Troubleshooting + +1. **Bot won't connect**: Check server/port in config.json +2. **Database errors**: Ensure write permissions in bot directory +3. **Commands not working**: Verify bot has joined the channel +4. **Test failures**: Run `python test_bot.py` to diagnose issues + +## šŸŽ–ļø Contributing + +Feel free to add new features: +- More duck types and weapons +- Additional mini-games +- Seasonal events +- Guild/team systems +- Advanced trading mechanics + +## šŸ“„ License + +This bot is provided as-is for educational and entertainment purposes. + +--- + +šŸ¦† **Happy Hunting!** šŸ¦† diff --git a/duckhunt/__pycache__/simple_duckhunt.cpython-312.pyc b/duckhunt/__pycache__/simple_duckhunt.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e1a37407e8ff9ed292580b60312048f3658bf4c GIT binary patch literal 119340 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

OLlM2^UjBd;`kh84}eh@A7R9AJ9*$Kh7VezNHx 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 literal 0 HcmV?d00001 diff --git a/duckhunt/config.json b/duckhunt/config.json new file mode 100644 index 0000000..0c318f4 --- /dev/null +++ b/duckhunt/config.json @@ -0,0 +1,205 @@ +{ + "server": "irc.rizon.net", + "port": 6697, + "nick": "DuckHunt", + "channels": ["#computertech"], + "ssl": true, + "sasl": { + "enabled": true, + "username": "duckhunt", + "password": "duckhunt//789//" + }, + "password": "", + "admins": ["peorth", "computertech", "colby"], + + "_comment_duck_spawning": "Duck spawning configuration", + "duck_spawn_min": 1800, + "duck_spawn_max": 5400, + "duck_timeout_min": 45, + "duck_timeout_max": 75, + "duck_types": { + "normal": { + "spawn_chance": 0.6, + "xp_reward": 10, + "difficulty": 1.0, + "flee_time": 15, + "messages": ["ćƒ»ć‚œć‚œćƒ»ć€‚ć€‚ćƒ»ć‚œć‚œ\\\\_o< QUACK!"] + }, + "fast": { + "spawn_chance": 0.25, + "xp_reward": 15, + "difficulty": 1.5, + "flee_time": 8, + "messages": ["ćƒ»ć‚œć‚œćƒ»ć€‚ć€‚ćƒ»ć‚œć‚œ\\\\_o< QUACK! (Fast duck!)"] + }, + "rare": { + "spawn_chance": 0.1, + "xp_reward": 30, + "difficulty": 2.0, + "flee_time": 12, + "messages": ["ćƒ»ć‚œć‚œćƒ»ć€‚ć€‚ćƒ»ć‚œć‚œ\\\\_o< QUACK! (Rare duck!)"] + }, + "golden": { + "spawn_chance": 0.05, + "xp_reward": 75, + "difficulty": 3.0, + "flee_time": 10, + "messages": ["ćƒ»ć‚œć‚œćƒ»ć€‚ć€‚ćƒ»ć‚œć‚œ\\\\_✪< ā˜… GOLDEN DUCK ā˜…"] + } + }, + "sleep_hours": [], + "max_ducks_per_channel": 3, + + "_comment_befriending": "Duck befriending configuration", + "befriending": { + "enabled": true, + "success_chance": 0.7, + "failure_messages": [ + "The duck looked at you suspiciously and flew away!", + "The duck didn't trust you and escaped!", + "The duck was too scared and ran off!" + ], + "scared_away_chance": 0.1, + "scared_away_messages": [ + "You scared the duck away with your approach!", + "The duck was terrified and fled immediately!" + ], + "xp_reward_min": 1, + "xp_reward_max": 3 + }, + + "_comment_shooting": "Shooting mechanics configuration", + "shooting": { + "enabled": true, + "base_accuracy": 85, + "base_reliability": 90, + "jam_chance_base": 10, + "friendly_fire_enabled": true, + "friendly_fire_chance": 5, + "reflex_shot_bonus": 5, + "miss_xp_penalty": 5, + "wild_shot_xp_penalty": 10, + "teamkill_xp_penalty": 20 + }, + + "_comment_weapons": "Weapon system configuration", + "weapons": { + "enabled": true, + "starting_weapon": "pistol", + "starting_ammo": 6, + "max_ammo_base": 6, + "starting_chargers": 2, + "max_chargers_base": 2, + "durability_enabled": true, + "confiscation_enabled": true + }, + + "_comment_economy": "Economy and shop configuration", + "economy": { + "enabled": true, + "starting_coins": 100, + "shop_enabled": true, + "trading_enabled": true, + "theft_enabled": true, + "theft_success_rate": 30, + "theft_penalty": 50, + "banking_enabled": true, + "interest_rate": 5, + "loan_enabled": true, + "inventory_system_enabled": true, + "max_inventory_slots": 20 + }, + + "_comment_progression": "Player progression configuration", + "progression": { + "enabled": true, + "max_level": 40, + "xp_multiplier": 1.0, + "level_benefits_enabled": true, + "titles_enabled": true, + "prestige_enabled": false + }, + + "_comment_karma": "Karma system configuration", + "karma": { + "enabled": true, + "hit_bonus": 2, + "golden_hit_bonus": 5, + "teamkill_penalty": 10, + "wild_shot_penalty": 3, + "miss_penalty": 1, + "befriend_success_bonus": 2, + "befriend_fail_penalty": 1 + }, + + "_comment_items": "Items and powerups configuration", + "items": { + "enabled": true, + "lucky_items_enabled": true, + "lucky_item_base_chance": 5, + "detector_enabled": true, + "silencer_enabled": true, + "sunglasses_enabled": true, + "explosive_ammo_enabled": true, + "sabotage_enabled": true, + "insurance_enabled": true, + "decoy_enabled": true + }, + + "_comment_social": "Social features configuration", + "social": { + "leaderboards_enabled": true, + "duck_alerts_enabled": true, + "private_messages_enabled": true, + "statistics_sharing_enabled": true, + "achievements_enabled": false + }, + + "_comment_moderation": "Moderation and admin features", + "moderation": { + "ignore_system_enabled": true, + "rate_limiting_enabled": true, + "rate_limit_cooldown": 2.0, + "admin_commands_enabled": true, + "ban_system_enabled": true, + "database_reset_enabled": true, + "admin_rearm_gives_full_ammo": false, + "admin_rearm_gives_full_chargers":false + }, + + "_comment_advanced": "Advanced game mechanics", + "advanced": { + "gun_jamming_enabled": true, + "weather_effects_enabled": false, + "seasonal_events_enabled": false, + "daily_challenges_enabled": false, + "guild_system_enabled": false, + "pvp_enabled": false + }, + + "_comment_messages": "Message customization", + "messages": { + "custom_duck_messages_enabled": true, + "color_enabled": true, + "emoji_enabled": true, + "verbose_messages": true, + "success_sound_effects": true + }, + + "_comment_database": "Database and persistence", + "database": { + "auto_save_enabled": true, + "auto_save_interval": 300, + "backup_enabled": true, + "backup_interval": 3600, + "compression_enabled": false + }, + + "_comment_debug": "Debug and logging options", + "debug": { + "debug_mode": false, + "verbose_logging": false, + "command_logging": false, + "performance_monitoring": false + } +} diff --git a/duckhunt/config_backup.json b/duckhunt/config_backup.json new file mode 100644 index 0000000..559a49c --- /dev/null +++ b/duckhunt/config_backup.json @@ -0,0 +1,216 @@ +{ + "server": "irc.rizon.net", + "port": 6697, + "nick": "DuckHunt", + "channels": ["#computertech"], + "ssl": true, + "sasl": { + "enabled": true, + "username": "duckhunt", + "password": "duckhunt//789//" + }, + "password": "", + "admins": ["peorth", "computertech"], + + "_comment_duck_spawning": "Duck spawning configuration", + "duck_spawn_min": 1800, + "duck_spawn_max": 5400, + "duck_timeout_min": 45, + "duck_timeout_max": 75, + "duck_types": { + "normal": { + "enabled": true, + "spawn_rate": 70, + "xp_reward": 10, + "coin_reward": 1, + "health": 1 + }, + "golden": { + "enabled": true, + "spawn_rate": 8, + "xp_reward": 50, + "coin_reward": 10, + "health": 1 + }, + "armored": { + "enabled": true, + "spawn_rate": 2, + "xp_reward": 75, + "coin_reward": 15, + "health": 3 + }, + "rare": { + "enabled": true, + "spawn_rate": 20, + "xp_reward": 25, + "coin_reward": 3, + "health": 1 + } + }, + "sleep_hours": [], + "max_ducks_per_channel": 3, + + "_comment_befriending": "Duck befriending configuration", + "befriending": { + "enabled": true, + "base_success_rate": 65, + "max_success_rate": 90, + "level_bonus_per_level": 2, + "level_bonus_cap": 20, + "luck_bonus_per_point": 3, + "xp_reward": 8, + "coin_reward_min": 1, + "coin_reward_max": 2, + "failure_xp_penalty": 1, + "scared_away_chance": 10, + "lucky_item_chance": 5 + }, + + "_comment_shooting": "Shooting mechanics configuration", + "shooting": { + "enabled": true, + "base_accuracy": 85, + "base_reliability": 90, + "jam_chance_base": 10, + "friendly_fire_enabled": true, + "friendly_fire_chance": 5, + "reflex_shot_bonus": 5, + "miss_xp_penalty": 5, + "wild_shot_xp_penalty": 10, + "teamkill_xp_penalty": 20 + }, + + "_comment_weapons": "Weapon system configuration", + "weapons": { + "enabled": true, + "starting_weapon": "pistol", + "starting_ammo": 6, + "max_ammo_base": 6, + "starting_chargers": 2, + "max_chargers_base": 2, + "durability_enabled": true, + "confiscation_enabled": true + }, + + "_comment_economy": "Economy and shop configuration", + "economy": { + "enabled": true, + "starting_coins": 100, + "shop_enabled": true, + "trading_enabled": true, + "theft_enabled": true, + "theft_success_rate": 30, + "theft_penalty": 50, + "banking_enabled": true, + "interest_rate": 5, + "loan_enabled": true + }, + + "_comment_progression": "Player progression configuration", + "progression": { + "enabled": true, + "max_level": 40, + "xp_multiplier": 1.0, + "level_benefits_enabled": true, + "titles_enabled": true, + "prestige_enabled": false + }, + + "_comment_karma": "Karma system configuration", + "karma": { + "enabled": true, + "hit_bonus": 2, + "golden_hit_bonus": 5, + "teamkill_penalty": 10, + "wild_shot_penalty": 3, + "miss_penalty": 1, + "befriend_success_bonus": 2, + "befriend_fail_penalty": 1 + }, + + "_comment_items": "Items and powerups configuration", + "items": { + "enabled": true, + "lucky_items_enabled": true, + "lucky_item_base_chance": 5, + "detector_enabled": true, + "silencer_enabled": true, + "sunglasses_enabled": true, + "explosive_ammo_enabled": true, + "sabotage_enabled": true, + "insurance_enabled": true, + "decoy_enabled": true + }, + + "_comment_social": "Social features configuration", + "social": { + "leaderboards_enabled": true, + "duck_alerts_enabled": true, + "private_messages_enabled": true, + "statistics_sharing_enabled": true, + "achievements_enabled": false + }, + + "_comment_moderation": "Moderation and admin features", + "moderation": { + "ignore_system_enabled": true, + "rate_limiting_enabled": true, + "rate_limit_cooldown": 2.0, + "admin_commands_enabled": true, + "ban_system_enabled": true, + "database_reset_enabled": true + }, + + "_comment_advanced": "Advanced game mechanics", + "advanced": { + "gun_jamming_enabled": true, + "weather_effects_enabled": false, + "seasonal_events_enabled": false, + "daily_challenges_enabled": false, + "guild_system_enabled": false, + "pvp_enabled": false + }, + + "_comment_messages": "Message customization", + "messages": { + "custom_duck_messages_enabled": true, + "color_enabled": true, + "emoji_enabled": true, + "verbose_messages": true, + "success_sound_effects": true + }, + + "_comment_database": "Database and persistence", + "database": { + "auto_save_enabled": true, + "auto_save_interval": 300, + "backup_enabled": true, + "backup_interval": 3600, + "compression_enabled": false + }, + + "_comment_debug": "Debug and logging options", + "debug": { + "debug_mode": false, + "verbose_logging": false, + "command_logging": false, + "performance_monitoring": false + } +}c.rizon.net", + "port": 6697, + "nick": "DuckHunt", + "channels": ["#computertech"], + "ssl": true, + "sasl": { + "enabled": true, + "username": "duckhunt", + "password": "duckhunt//789//" + }, + "password": "", + "admins": ["colby", "computertech"], + "duck_spawn_min": 1800, + "duck_spawn_max": 5400, + "duck_timeout_min": 45, + "duck_timeout_max": 75, + "_comment": "Run with: python3 simple_duckhunt.py | Admins config-only | Private admin: /msg DuckHuntBot restart|quit|launch | Duck timeout: random between min-max seconds" +} diff --git a/duckhunt/demo_sasl.py b/duckhunt/demo_sasl.py new file mode 100644 index 0000000..90b0b0a --- /dev/null +++ b/duckhunt/demo_sasl.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +""" +SASL Integration Demo for DuckHunt Bot +This script demonstrates how the modular SASL authentication works +""" + +import asyncio +import json +import sys +import os + +# Add src directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from src.sasl import SASLHandler +from src.logging_utils import setup_logger + +class MockBot: + """Mock bot for testing SASL without IRC connection""" + def __init__(self, config): + self.config = config + self.logger = setup_logger("MockBot") + self.messages_sent = [] + + def send_raw(self, message): + """Mock send_raw that just logs the message""" + self.messages_sent.append(message) + self.logger.info(f"SEND: {message}") + + async def register_user(self): + """Mock registration""" + self.logger.info("Mock user registration completed") + +async def demo_sasl_flow(): + """Demonstrate the SASL authentication flow""" + print("šŸ” SASL Authentication Flow Demo") + print("=" * 50) + + # Load config + with open('config.json') as f: + config = json.load(f) + + # Override with test credentials for demo + config['sasl'] = { + 'enabled': True, + 'username': 'testuser', + 'password': 'testpass123' + } + + # Create mock bot and SASL handler + bot = MockBot(config) + sasl_handler = SASLHandler(bot, config) + + print("\n1ļøāƒ£ Starting SASL negotiation...") + if await sasl_handler.start_negotiation(): + print("āœ… SASL negotiation started successfully") + else: + print("āŒ SASL negotiation failed to start") + return + + print("\n2ļøāƒ£ Simulating server CAP response...") + # Simulate server listing SASL capability + params = ['*', 'LS', '*'] + trailing = 'sasl multi-prefix extended-join' + await sasl_handler.handle_cap_response(params, trailing) + + print("\n3ļøāƒ£ Simulating server acknowledging SASL capability...") + # Simulate server acknowledging SASL + params = ['*', 'ACK'] + trailing = 'sasl' + await sasl_handler.handle_cap_response(params, trailing) + + print("\n4ļøāƒ£ Simulating server ready for authentication...") + # Simulate server ready for auth + params = ['+'] + await sasl_handler.handle_authenticate_response(params) + + print("\n5ļøāƒ£ Simulating successful authentication...") + # Simulate successful authentication + params = ['DuckHunt'] + trailing = 'You are now logged in as duckhunt' + await sasl_handler.handle_sasl_result('903', params, trailing) + + print(f"\nšŸ“¤ Messages sent to server:") + for i, msg in enumerate(bot.messages_sent, 1): + print(f" {i}. {msg}") + + print(f"\nšŸ” Authentication status: {'āœ… Authenticated' if sasl_handler.is_authenticated() else 'āŒ Not authenticated'}") + + print("\n" + "=" * 50) + print("✨ SASL flow demonstration complete!") + +async def demo_sasl_failure(): + """Demonstrate SASL failure handling""" + print("\n\n🚫 SASL Failure Handling Demo") + print("=" * 50) + + # Create mock bot with wrong credentials + config = { + 'sasl': { + 'enabled': True, + 'username': 'testuser', + 'password': 'wrong_password' + } + } + bot = MockBot(config) + sasl_handler = SASLHandler(bot, config) + + print("\n1ļøāƒ£ Starting SASL with wrong credentials...") + await sasl_handler.start_negotiation() + + # Simulate failed authentication + params = ['DuckHunt'] + trailing = 'Invalid credentials' + await sasl_handler.handle_sasl_result('904', params, trailing) + + print(f"\nšŸ” Authentication status: {'āœ… Authenticated' if sasl_handler.is_authenticated() else 'āŒ Not authenticated'}") + print("āœ… Failure handled gracefully - bot will fallback to NickServ") + +if __name__ == '__main__': + asyncio.run(demo_sasl_flow()) + asyncio.run(demo_sasl_failure()) diff --git a/duckhunt/duckhunt.db b/duckhunt/duckhunt.db new file mode 100644 index 0000000000000000000000000000000000000000..e00e26c4d8cb3f1b06933a07f3ad0cba3d69f966 GIT binary patch literal 36864 zcmeI&Z%@-e9Ki9Gu|I$?A7~mvmRuka2vb=MPkiK@g)A_{QKCGtDcgaIV;gNpB*q7z zPkcAN4c~z;#0Na|I##l=4K+Tp`Ci)f`sZ%<``o2jd)s}rRr94dY`VveFP><3HC@-9 z3886PT&)vo4b!;F1Z$Y|FmuZ`u01;aI-S1MQd4iW^m_XH%x3!joy(bfQ{U795&{Sy zfB*srAbe4%@tDoAoCr{e4QMtK0Q7D^v%M@1r`IaeeT)Wh8 zj%8MrT)r@qNn>_a{~|ke`mR%z-qmNSuZF8*Vrf#`NVTdcZd>MtSr$9xVkuwV7n|n3 z$nRM%i`%M-lDTbV2TC2f&12iCRGRIEFD&!TkW2jL?=A=$YgZlL8Bv4hJAT{ioD?t2 z_59wJB^FzvDy?a#=-UU!Um|D=I@_ZwfG%^|eLUphj?%l8P z!tHn;W_WanWZRQ&Fy61mK~K{M! zZ`GYM>3ZGGSYP1XgG1tO)GF`)Cg%Q5dpMRb@^how-2GNN*UiM#Z8jC$YNyqzr+;W_ zAt8VO0tg_000IagfB*srAb`MsCh$OyWeUD}H5j~2`8Z!uFK@j0ry{qynicaEr+xI! zSG#MUCRJQ|`lqHA5&{SyfB*srAb 1 else [] + return prefix, command, params, trailing + +class SimpleIRCBot: + def __init__(self, config): + self.config = config + self.logger = setup_logger() + self.reader: Optional[asyncio.StreamReader] = None + self.writer: Optional[asyncio.StreamWriter] = None + self.registered = False + self.channels_joined = set() + self.players = {} # Memory cache for speed + self.ducks = {} # Format: {channel: [{'alive': True, 'spawn_time': time, 'id': uuid}, ...]} + self.db_file = "duckhunt.json" + self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] # Load from config only, case insensitive + self.ignored_nicks = set() # Nicks to ignore commands from + self.command_cooldowns = {} # Rate limiting for commands + self.duck_timeout_min = self.config.get('duck_timeout_min', 45) # Minimum duck timeout + self.duck_timeout_max = self.config.get('duck_timeout_max', 75) # Maximum duck timeout + self.duck_spawn_min = self.config.get('duck_spawn_min', 1800) # Minimum duck spawn time (30 min) + self.duck_spawn_max = self.config.get('duck_spawn_max', 5400) # Maximum duck spawn time (90 min) + self.shutdown_requested = False # Graceful shutdown flag + self.running_tasks = set() # Track running tasks for cleanup + + # Initialize SASL handler + self.sasl_handler = SASLHandler(self, config) + + # IRC Color codes + self.colors = { + 'red': '\x0304', + 'green': '\x0303', + 'blue': '\x0302', + 'yellow': '\x0308', + 'orange': '\x0307', + 'purple': '\x0306', + 'cyan': '\x0311', + 'white': '\x0300', + 'black': '\x0301', + 'gray': '\x0314', + 'reset': '\x03' + } + + # 40-level progression system with titles + self.levels = [ + {'xp': 0, 'title': 'Duck Harasser'}, + {'xp': 10, 'title': 'Unemployed'}, + {'xp': 25, 'title': 'Hunter Apprentice'}, + {'xp': 45, 'title': 'Duck Tracker'}, + {'xp': 70, 'title': 'Sharp Shooter'}, + {'xp': 100, 'title': 'Hunter'}, + {'xp': 135, 'title': 'Experienced Hunter'}, + {'xp': 175, 'title': 'Skilled Hunter'}, + {'xp': 220, 'title': 'Expert Hunter'}, + {'xp': 270, 'title': 'Master Hunter'}, + {'xp': 325, 'title': 'Duck Slayer'}, + {'xp': 385, 'title': 'Duck Terminator'}, + {'xp': 450, 'title': 'Duck Destroyer'}, + {'xp': 520, 'title': 'Duck Exterminator'}, + {'xp': 595, 'title': 'Duck Assassin'}, + {'xp': 675, 'title': 'Legendary Hunter'}, + {'xp': 760, 'title': 'Elite Hunter'}, + {'xp': 850, 'title': 'Supreme Hunter'}, + {'xp': 945, 'title': 'Ultimate Hunter'}, + {'xp': 1045, 'title': 'Godlike Hunter'}, + {'xp': 1150, 'title': 'Duck Nightmare'}, + {'xp': 1260, 'title': 'Duck Executioner'}, + {'xp': 1375, 'title': 'Duck Eliminator'}, + {'xp': 1495, 'title': 'Duck Obliterator'}, + {'xp': 1620, 'title': 'Duck Annihilator'}, + {'xp': 1750, 'title': 'Duck Devastator'}, + {'xp': 1885, 'title': 'Duck Vanquisher'}, + {'xp': 2025, 'title': 'Duck Conqueror'}, + {'xp': 2170, 'title': 'Duck Dominator'}, + {'xp': 2320, 'title': 'Duck Emperor'}, + {'xp': 2475, 'title': 'Duck Overlord'}, + {'xp': 2635, 'title': 'Duck Deity'}, + {'xp': 2800, 'title': 'Duck God'}, + {'xp': 2970, 'title': 'Duck Nemesis'}, + {'xp': 3145, 'title': 'Duck Apocalypse'}, + {'xp': 3325, 'title': 'Duck Armageddon'}, + {'xp': 3510, 'title': 'Duck Ragnarok'}, + {'xp': 3700, 'title': 'Duck Cataclysm'}, + {'xp': 3895, 'title': 'Duck Holocaust'}, + {'xp': 4095, 'title': 'Duck Genesis'} + ] + + # Sleep hours configuration (when ducks don't spawn) + self.sleep_hours = self.config.get('sleep_hours', []) # Format: [[22, 30], [8, 0]] for 22:30 to 08:00 + + # Duck planning system + self.daily_duck_plan = {} # Format: {channel: [(hour, minute), ...]} + + # Karma system + self.karma_events = ['teamkill', 'miss', 'wild_shot', 'hit', 'golden_hit'] + + self.load_database() + + def get_config(self, path, default=None): + """Get nested configuration value with fallback to default""" + keys = path.split('.') + value = self.config + for key in keys: + if isinstance(value, dict) and key in value: + value = value[key] + else: + return default + return value + + async def attempt_nickserv_auth(self): + """Delegate to SASL handler for NickServ auth""" + # For simple bot, we'll implement NickServ auth here + sasl_config = self.config.get('sasl', {}) + username = sasl_config.get('username', '') + password = sasl_config.get('password', '') + + if username and password: + self.logger.info(f"Attempting NickServ identification for {username}") + # Try both common NickServ commands + self.send_raw(f'PRIVMSG NickServ :IDENTIFY {username} {password}') + # Some networks use just the password if nick matches + await asyncio.sleep(1) + self.send_raw(f'PRIVMSG NickServ :IDENTIFY {password}') + self.logger.info("NickServ identification commands sent") + else: + self.logger.debug("No SASL credentials available for NickServ fallback") + + async def handle_nickserv_response(self, message): + """Handle responses from NickServ""" + message_lower = message.lower() + + if any(phrase in message_lower for phrase in [ + 'you are now identified', 'password accepted', 'you are already identified', + 'authentication successful', 'you have been identified' + ]): + self.logger.info("NickServ identification successful!") + + elif any(phrase in message_lower for phrase in [ + 'invalid password', 'incorrect password', 'access denied', + 'authentication failed', 'not registered', 'nickname is not registered' + ]): + self.logger.error(f"NickServ identification failed: {message}") + + else: + self.logger.debug(f"NickServ message: {message}") + + def get_player_level(self, xp): + """Get player level and title based on XP""" + for i in range(len(self.levels) - 1, -1, -1): + if xp >= self.levels[i]['xp']: + return i + 1, self.levels[i]['title'] + return 1, self.levels[0]['title'] + + def get_xp_for_next_level(self, xp): + """Get XP needed for next level""" + level, _ = self.get_player_level(xp) + if level < len(self.levels): + return self.levels[level]['xp'] - xp + return 0 # Max level reached + + def calculate_penalty_by_level(self, base_penalty, xp): + """Calculate penalty based on player level""" + level, _ = self.get_player_level(xp) + # Higher levels get higher penalties + return base_penalty + (level - 1) * 0.5 + + def update_karma(self, player, event): + """Update player karma based on event""" + if 'karma' not in player: + player['karma'] = 0 + + karma_changes = { + 'hit': 2, + 'golden_hit': 5, + 'teamkill': -10, + 'wild_shot': -3, + 'miss': -1 + } + + player['karma'] += karma_changes.get(event, 0) + + def is_sleep_time(self): + """Check if current time is within sleep hours""" + if not self.sleep_hours: + return False + + import datetime + now = datetime.datetime.now() + current_time = now.hour * 60 + now.minute + + for sleep_start, sleep_end in self.sleep_hours: + start_minutes = sleep_start[0] * 60 + sleep_start[1] + end_minutes = sleep_end[0] * 60 + sleep_end[1] + + if start_minutes <= end_minutes: # Same day + if start_minutes <= current_time <= end_minutes: + return True + else: # Crosses midnight + if current_time >= start_minutes or current_time <= end_minutes: + return True + return False + + def calculate_gun_reliability(self, player): + """Calculate gun reliability percentage""" + base_reliability = player.get('reliability', 70) + grease_bonus = 10 if player.get('grease', 0) > 0 else 0 + brush_bonus = 5 if player.get('brush', 0) > 0 else 0 + return min(base_reliability + grease_bonus + brush_bonus, 95) + + def gun_jams(self, player): + """Check if gun jams when firing""" + reliability = self.calculate_gun_reliability(player) + return random.randint(1, 100) > reliability + + async def scare_other_ducks(self, channel, shot_duck_id): + """Successful shots can scare other ducks away""" + if not self.config.get('successful_shots_scare_ducks', True): + return + + channel_ducks = self.ducks.get(channel, []) + for duck in channel_ducks: + if duck.get('alive') and duck['id'] != shot_duck_id: + # 30% chance to scare each remaining duck + if random.randint(1, 100) <= 30: + duck['scared'] = True + duck['alive'] = False + + async def scare_duck_on_miss(self, channel, target_duck): + """Duck may be scared by missed shots""" + if target_duck.get('hit_attempts', 0) >= 2: # Duck gets scared after 2+ attempts + if random.randint(1, 100) <= 40: # 40% chance to scare + target_duck['scared'] = True + target_duck['alive'] = False + self.send_message(channel, f"The duck got scared and flew away! (\\_o<) *flap flap*") + + async def find_bushes_items(self, nick, channel, player): + """Find items in bushes after killing a duck""" + if random.randint(1, 100) <= 12: # 12% chance to find something + found_items = [ + "Handful of sand", "Water bucket", "Four-leaf clover", "Mirror", + "Grease", "Brush for gun", "Spare clothes", "Sunglasses", + "Piece of bread", "Life insurance" + ] + found_item = random.choice(found_items) + + # Add item to player inventory + item_key = found_item.lower().replace(' ', '_').replace("'", "") + if 'four_leaf_clover' in item_key: + item_key = 'luck' + player['luck'] = player.get('luck', 0) + 1 + elif item_key in player: + player[item_key] = player.get(item_key, 0) + 1 + + self.send_message(channel, f"{nick} > {self.colors['cyan']}You found {found_item} in the bushes!{self.colors['reset']}") + self.save_player(f"{nick}!user@host") # Save player data + + def load_database(self): + """Load player data from JSON file""" + if os.path.exists(self.db_file): + try: + with open(self.db_file, 'r') as f: + data = json.load(f) + self.players = data.get('players', {}) + self.logger.info(f"Loaded {len(self.players)} players from {self.db_file}") + except (json.JSONDecodeError, IOError) as e: + self.logger.error(f"Error loading database: {e}") + self.players = {} + else: + self.players = {} + self.logger.info(f"Created new database: {self.db_file}") + + def save_database(self): + """Save all player data to JSON file with error handling""" + try: + # Atomic write to prevent corruption + temp_file = f"{self.db_file}.tmp" + data = { + 'players': self.players, + 'last_save': str(time.time()) + } + with open(temp_file, 'w') as f: + json.dump(data, f, indent=2) + + # Atomic rename to replace old file + import os + os.replace(temp_file, self.db_file) + + except IOError as e: + self.logger.error(f"Error saving database: {e}") + except Exception as e: + self.logger.error(f"Unexpected database save error: {e}") + + def is_admin(self, user): + """Check if user is admin by nick only""" + if '!' not in user: + return False + nick = user.split('!')[0].lower() + return nick in self.admins + + async def send_user_message(self, nick, channel, message): + """Send message to user respecting their notice/private message preferences""" + player = self.get_player(f"{nick}!*@*") + + # Default to channel notices if player not found or no settings + use_notices = True + if player and 'settings' in player: + use_notices = player['settings'].get('notices', True) + + if use_notices: + # Send to channel + self.send_message(channel, message) + else: + # Send as private message + private_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for PM + self.send_message(nick, private_msg) + + def get_random_player_for_friendly_fire(self, shooter_nick): + """Get a random player (except shooter) for friendly fire""" + eligible_players = [] + shooter_lower = shooter_nick.lower() + + for nick in self.players.keys(): + if nick != shooter_lower: # Don't hit yourself + eligible_players.append(nick) + + if eligible_players: + return random.choice(eligible_players) + return None + + async def connect(self): + server = self.config['server'] + port = self.config['port'] + ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None + + self.logger.info(f"Connecting to {server}:{port} (SSL: {ssl_context is not None})") + + self.reader, self.writer = await asyncio.open_connection( + server, port, ssl=ssl_context + ) + self.logger.info("Connected successfully!") + + # Start SASL negotiation if enabled + if await self.sasl_handler.start_negotiation(): + return True + else: + # Standard registration without SASL + await self.register_user() + return True + + async def register_user(self): + """Register the user with the IRC server""" + self.logger.info(f"Registering as {self.config['nick']}") + self.send_raw(f'NICK {self.config["nick"]}') + self.send_raw(f'USER {self.config["nick"]} 0 * :DuckHunt Bot') + + # Send password if configured (for servers that require it) + if self.config.get('password'): + self.send_raw(f'PASS {self.config["password"]}') + + def send_raw(self, msg): + # Skip debug logging for speed + # self.logger.debug(f"-> {msg}") + if self.writer: + self.writer.write((msg + '\r\n').encode()) + + def send_message(self, target, msg): + # Skip logging during gameplay for speed (uncomment for debugging) + # self.logger.info(f"Sending to {target}: {msg}") + self.send_raw(f'PRIVMSG {target} :{msg}') + # Remove drain() for faster responses - let TCP handle buffering + + def get_player(self, user): + """Get player data by nickname only (case insensitive)""" + if '!' not in user: + return None + + nick = user.split('!')[0].lower() # Case insensitive + + # Use nick as database key + if nick in self.players: + player = self.players[nick] + # Backward compatibility: ensure all required fields exist + if 'missed' not in player: + player['missed'] = 0 + if 'inventory' not in player: + player['inventory'] = {} + return player + + # Create new player with configurable defaults + player_data = { + 'xp': 0, + 'caught': 0, + 'befriended': 0, # Separate counter for befriended ducks + 'missed': 0, + 'ammo': self.get_config('weapons.starting_ammo', 6), + 'max_ammo': self.get_config('weapons.max_ammo_base', 6), + 'chargers': self.get_config('weapons.starting_chargers', 2), + 'max_chargers': self.get_config('weapons.max_chargers_base', 2), + 'accuracy': self.get_config('shooting.base_accuracy', 65), + 'reliability': self.get_config('shooting.base_reliability', 70), + 'weapon': self.get_config('weapons.starting_weapon', 'pistol'), + 'gun_confiscated': False, + 'explosive_ammo': False, + 'settings': { + 'notices': True, # True for notices, False for private messages + 'private_messages': False + }, + # Inventory system + 'inventory': {}, + # New advanced stats + 'golden_ducks': 0, + 'karma': 0, + 'deflection': 0, + 'defense': 0, + 'jammed': False, + 'jammed_count': 0, + 'deaths': 0, + 'neutralized': 0, + 'deflected': 0, + 'best_time': 999.9, + 'total_reflex_time': 0.0, + 'reflex_shots': 0, + 'wild_shots': 0, + 'accidents': 0, + 'total_ammo_used': 0, + 'shot_at': 0, + 'lucky_shots': 0, + # Shop items + 'luck': 0, + 'detector': 0, + 'silencer': 0, + 'sunglasses': 0, + 'clothes': 0, + 'grease': 0, + 'brush': 0, + 'mirror': 0, + 'sand': 0, + 'water': 0, + 'sabotage': 0, + 'life_insurance': 0, + 'liability': 0, + 'decoy': 0, + 'bread': 0, + 'duck_detector': 0, + 'mechanical': 0 + } + + self.players[nick] = player_data + self.save_database() # Auto-save new players + return player_data + + def save_player(self, user): + """Save player data - batch saves for performance""" + if not hasattr(self, '_save_pending'): + self._save_pending = False + + if not self._save_pending: + self._save_pending = True + # Schedule delayed save to batch multiple writes + asyncio.create_task(self._delayed_save()) + + async def _delayed_save(self): + """Batch save to reduce disk I/O""" + await asyncio.sleep(0.5) # Small delay to batch saves + self.save_database() + self._save_pending = False + + def setup_signal_handlers(self): + """Setup signal handlers for graceful shutdown""" + def signal_handler(signum): + signal_name = signal.Signals(signum).name + self.logger.info(f"Received {signal_name}, initiating graceful shutdown...") + self.shutdown_requested = True + + # Handle common shutdown signals + if hasattr(signal, 'SIGTERM'): + signal.signal(signal.SIGTERM, lambda s, f: signal_handler(s)) + if hasattr(signal, 'SIGINT'): + signal.signal(signal.SIGINT, lambda s, f: signal_handler(s)) + if hasattr(signal, 'SIGHUP'): + signal.signal(signal.SIGHUP, lambda s, f: signal_handler(s)) + + def is_rate_limited(self, user, command, cooldown=2.0): + """Check if user is rate limited for a command""" + now = time.time() + key = f"{user}:{command}" + + if key in self.command_cooldowns: + if now - self.command_cooldowns[key] < cooldown: + return True + + self.command_cooldowns[key] = now + return False + + async def handle_command(self, user, channel, message): + if not user: + return + + nick = user.split('!')[0] + nick_lower = nick.lower() + + # Check if user is ignored + if nick_lower in self.ignored_nicks: + return + + # Determine if this is a private message to the bot + is_private = channel == self.config['nick'] + + # For private messages, use the nick as the target for responses + response_target = nick if is_private else channel + + # Handle private messages (no ! prefix needed) + if is_private: + cmd = message.strip().lower() + + # Private message admin commands + if self.is_admin(user): + if cmd == 'restart': + await self.handle_restart(nick, response_target) + return + elif cmd == 'quit': + await self.handle_quit(nick, response_target) + return + elif cmd == 'launch' or cmd == 'ducklaunch': + # For private messages, launch in all channels + for chan in self.channels_joined: + await self.spawn_duck_now(chan) + self.send_message(response_target, f"{nick} > Launched ducks in all channels!") + return + elif cmd == 'golden' or cmd == 'goldenduck': + # Launch golden ducks + for chan in self.channels_joined: + await self.spawn_duck_now(chan, force_golden=True) + self.send_message(response_target, f"{nick} > Launched {self.colors['yellow']}GOLDEN DUCKS{self.colors['reset']} in all channels!") + return + elif cmd.startswith('ignore '): + target_nick = cmd[7:].strip().lower() + await self.handle_ignore(nick, response_target, target_nick) + return + elif cmd.startswith('delignore '): + target_nick = cmd[10:].strip().lower() + await self.handle_delignore(nick, response_target, target_nick) + return + else: + # Unknown private command + self.send_message(response_target, f"{nick} > Admin commands: restart, quit, launch, golden, ignore , delignore ") + return + else: + # Non-admin private message + self.send_message(response_target, f"{nick} > Private commands are admin-only. Use !help in a channel for game commands.") + return + + # Handle channel messages (must start with !) + if not message.startswith('!'): + return + + # Extract just the command part (first word) to handle emojis and extra text + cmd = message.strip().lower().split()[0] + # Keep the original message for commands that need arguments + full_cmd = message.strip().lower() + + # Regular game commands (channel only) + # Inline common commands for speed + if cmd == '!bang': + # Rate limit shooting to prevent spam + if self.is_rate_limited(user, 'bang', 1.0): + return + + player = self.get_player(user) + if not player: + return + + # Check if gun is confiscated + if player.get('gun_confiscated', False): + self.send_message(channel, f"{nick} > {self.colors['red']}Your gun has been confiscated! Buy a new gun from the shop (item #5).{self.colors['reset']}") + return + + # Check if gun is jammed + if player.get('jammed', False): + self.send_message(channel, f"{nick} > {self.colors['red']}Your gun is jammed! Use !reload to unjam it.{self.colors['reset']}") + return + + # Check ammo + if player['ammo'] <= 0: + self.send_message(channel, f"{nick} > Your gun is empty! | Ammo: 0/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") + return + + # Check for gun jamming before shooting + if self.gun_jams(player): + player['jammed'] = True + player['jammed_count'] = player.get('jammed_count', 0) + 1 + jam_sound = "•click• •click•" if player.get('silencer', 0) > 0 else "*CLICK* *CLICK*" + self.send_message(channel, f"{nick} > {jam_sound} Your gun jammed! Use !reload to unjam it.") + self.save_player(user) + return + + # Get ducks in this channel + channel_ducks = self.ducks.get(channel, []) + alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] + + # Consume ammo + player['ammo'] -= 1 + player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 + + if alive_ducks: + # Target the oldest duck (first in, first out) + target_duck = alive_ducks[0] + shot_time = time.time() - target_duck['spawn_time'] + is_golden = target_duck.get('type') == 'golden' + + # Calculate hit chance (golden ducks are harder to hit) + base_accuracy = player['accuracy'] + if is_golden: + base_accuracy = max(base_accuracy - 30, 10) # Golden ducks much harder + + # Apply bonuses + if player.get('sunglasses', 0) > 0: + base_accuracy += 5 # Sunglasses help + if player.get('mirror', 0) > 0: + base_accuracy += 3 # Mirror helps + + hit_chance = min(base_accuracy, 95) # Cap at 95% + + # Record shot attempt + player['shot_at'] = player.get('shot_at', 0) + 1 + target_duck['hit_attempts'] = target_duck.get('hit_attempts', 0) + 1 + + # Check for hit + if random.randint(1, 100) <= hit_chance: + # HIT! + player['caught'] += 1 + target_duck['alive'] = False + + # Update reflex time stats + player['reflex_shots'] = player.get('reflex_shots', 0) + 1 + player['total_reflex_time'] = player.get('total_reflex_time', 0) + shot_time + if shot_time < player.get('best_time', 999.9): + player['best_time'] = shot_time + + # Calculate XP and rewards + if is_golden: + player['golden_ducks'] = player.get('golden_ducks', 0) + 1 + base_xp = 50 # Golden ducks give much more XP + self.update_karma(player, 'golden_hit') + else: + base_xp = 15 # Normal XP + self.update_karma(player, 'hit') + + # Lucky shot bonus + luck_multiplier = 1 + (player.get('luck', 0) * 0.1) # 10% per luck point + is_lucky = random.randint(1, 100) <= (5 + player.get('luck', 0)) + if is_lucky: + player['lucky_shots'] = player.get('lucky_shots', 0) + 1 + luck_multiplier *= 1.5 # 50% bonus for lucky shot + + xp_earned = int(base_xp * luck_multiplier) + player['xp'] += xp_earned + + # Sound effects based on ammo type + if player.get('explosive_ammo', False): + shot_sound = "•BOUM•" if player.get('silencer', 0) > 0 else "*BOUM*" + explosive_text = f" {self.colors['orange']}[explosive ammo]{self.colors['reset']}" + else: + shot_sound = "•bang•" if player.get('silencer', 0) > 0 else "*BANG*" + explosive_text = "" + + # Lucky shot text + lucky_text = f" {self.colors['yellow']}[lucky shot!]{self.colors['reset']}" if is_lucky else "" + + # Build hit message + level, title = self.get_player_level(player['xp']) + + if is_golden: + golden_count = player.get('golden_ducks', 0) + hit_msg = f"{nick} > {self.colors['yellow']}{shot_sound}{self.colors['reset']} You shot down the {self.colors['yellow']}ā˜… GOLDEN DUCK ā˜…{self.colors['reset']} in {shot_time:.3f}s! Total: {player['caught']} ducks ({self.colors['yellow']}{golden_count} golden{self.colors['reset']}) | Level {level}: {title} | [{self.colors['yellow']}{xp_earned} xp{self.colors['reset']}]{explosive_text}{lucky_text}" + else: + hit_msg = f"{nick} > {self.colors['green']}{shot_sound}{self.colors['reset']} You shot down the duck in {shot_time:.3f}s! Total: {player['caught']} ducks | Level {level}: {title} | [{self.colors['green']}{xp_earned} xp{self.colors['reset']}]{explosive_text}{lucky_text}" + + self.send_message(channel, hit_msg) + + # Scare other ducks if enabled (successful shots can scare ducks) + await self.scare_other_ducks(channel, target_duck['id']) + + # Find items in bushes (rare chance) + await self.find_bushes_items(nick, channel, player) + + else: + # MISS! + player['missed'] += 1 + self.update_karma(player, 'miss') + + # Calculate miss penalty based on level + miss_penalty = int(self.calculate_penalty_by_level(-2, player['xp'])) + player['xp'] += miss_penalty + + # Bullet ricochet chance (can hit other players) + ricochet_chance = 8 # 8% base chance + if player.get('explosive_ammo', False): + ricochet_chance = 15 # Higher with explosive + + ricochet_msg = "" + if random.randint(1, 100) <= ricochet_chance: + ricochet_target = self.get_random_player_for_friendly_fire(nick) + if ricochet_target: + target_player = self.players[ricochet_target] + ricochet_dmg = -3 + target_player['xp'] += ricochet_dmg + target_player['shot_at'] = target_player.get('shot_at', 0) + 1 + ricochet_msg = f" {self.colors['red']}[RICOCHET: {ricochet_target} hit for {ricochet_dmg} xp]{self.colors['reset']}" + + # Scare duck on miss + await self.scare_duck_on_miss(channel, target_duck) + + miss_sound = "•click•" if player.get('silencer', 0) > 0 else "*CLICK*" + await self.send_user_message(nick, channel, f"{nick} > {miss_sound} You missed the duck! [miss: {miss_penalty} xp]{ricochet_msg}") + + else: + # No duck present - wild fire! + player['wild_shots'] = player.get('wild_shots', 0) + 1 + self.update_karma(player, 'wild_shot') + + # Calculate penalties based on level + miss_penalty = int(self.calculate_penalty_by_level(-2, player['xp'])) + wild_penalty = int(self.calculate_penalty_by_level(-3, player['xp'])) + player['xp'] += miss_penalty + wild_penalty + + # Confiscate gun + player['gun_confiscated'] = True + + # Higher chance of hitting other players when no duck + friendly_fire_chance = 25 # 25% when no duck + friendly_fire_msg = "" + + if random.randint(1, 100) <= friendly_fire_chance: + ff_target = self.get_random_player_for_friendly_fire(nick) + if ff_target: + target_player = self.players[ff_target] + ff_dmg = int(self.calculate_penalty_by_level(-4, target_player['xp'])) + target_player['xp'] += ff_dmg + target_player['shot_at'] = target_player.get('shot_at', 0) + 1 + player['accidents'] = player.get('accidents', 0) + 1 + self.update_karma(player, 'teamkill') + friendly_fire_msg = f" {self.colors['red']}[ACCIDENT: {ff_target} injured for {ff_dmg} xp]{self.colors['reset']}" + + wild_sound = "•BOUM•" if player.get('explosive_ammo', False) else "*BANG*" + if player.get('silencer', 0) > 0: + wild_sound = "•" + wild_sound[1:-1] + "•" + + confiscated_msg = f" {self.colors['red']}[GUN CONFISCATED]{self.colors['reset']}" + await self.send_user_message(nick, channel, f"{nick} > {wild_sound} You shot at nothing! What were you aiming at? [miss: {miss_penalty} xp] [wild fire: {wild_penalty} xp]{confiscated_msg}{friendly_fire_msg}") + + # Save after each shot + self.save_player(user) + + elif cmd == '!bef': + # Check if befriending is enabled + if not self.get_config('befriending.enabled', True): + self.send_message(channel, f"{nick} > Duck befriending is currently disabled!") + return + + player = self.get_player(user) + if not player: + return + + # Get ducks in this channel + channel_ducks = self.ducks.get(channel, []) + alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] + + if alive_ducks: + # Target the oldest duck (first in, first out) + target_duck = alive_ducks[0] + bef_time = time.time() - target_duck['spawn_time'] + + # Calculate befriend success chance using config values + level, _ = self.get_player_level(player['xp']) + base_success = self.get_config('befriending.base_success_rate', 65) or 65 + max_success = self.get_config('befriending.max_success_rate', 90) or 90 + level_bonus_per_level = self.get_config('befriending.level_bonus_per_level', 2) or 2 + level_bonus_cap = self.get_config('befriending.level_bonus_cap', 20) or 20 + luck_bonus_per_point = self.get_config('befriending.luck_bonus_per_point', 3) or 3 + + level_bonus = min(level * level_bonus_per_level, level_bonus_cap) + luck_bonus = player.get('luck', 0) * luck_bonus_per_point + success_chance = min(base_success + level_bonus + luck_bonus, max_success) + + # Check if befriend attempt succeeds + if random.randint(1, 100) <= success_chance: + # Successful befriend + player['befriended'] = player.get('befriended', 0) + 1 + + # XP rewards from config + xp_min = self.get_config('befriending.xp_reward_min', 1) or 1 + xp_max = self.get_config('befriending.xp_reward_max', 3) or 3 + + xp_earned = random.randint(xp_min, xp_max) + player['xp'] += xp_earned + + # Mark duck as befriended (dead) + target_duck['alive'] = False + + # Lucky items with configurable chance + if self.get_config('items.lucky_items_enabled', True): + lucky_items = ["four-leaf clover", "rabbit's foot", "horseshoe", "lucky penny", "magic feather"] + base_luck_chance = self.get_config('befriending.lucky_item_chance', 5) + player.get('luck', 0) + lucky_item = random.choice(lucky_items) if random.randint(1, 100) <= base_luck_chance else None + lucky_text = f" [{lucky_item}]" if lucky_item else "" + else: + lucky_text = "" + + remaining_ducks = len([d for d in channel_ducks if d.get('alive')]) + duck_count_text = f" | {remaining_ducks} ducks remain" if remaining_ducks > 0 else "" + + self.send_message(channel, f"{nick} > You befriended a duck in {bef_time:.3f}s! Total friends: {player['befriended']} ducks on {channel}. \\_o< *quack* [+{xp_earned} xp]{lucky_text}{duck_count_text}") + + # Update karma for successful befriend + if self.get_config('karma.enabled', True): + karma_bonus = self.get_config('karma.befriend_success_bonus', 2) + player['karma'] = player.get('karma', 0) + karma_bonus + + # Save to database after befriending + self.save_player(user) + else: + # Duck refuses to be befriended + refusal_messages = [ + f"{nick} > The duck looks at you suspiciously and waddles away! \\_o< *suspicious quack*", + f"{nick} > The duck refuses to be friends and flaps away angrily! \\_O< *angry quack*", + f"{nick} > The duck ignores your friendship attempts and goes back to swimming! \\_o< *indifferent quack*", + f"{nick} > The duck seems shy and hides behind some reeds! \\_o< *shy quack*", + f"{nick} > The duck is too busy looking for food to be your friend! \\_o< *hungry quack*", + f"{nick} > The duck gives you a cold stare and swims to the other side! \\_O< *cold quack*", + f"{nick} > The duck prefers to stay wild and free! \\_o< *wild quack*", + f"{nick} > The duck thinks you're trying too hard and keeps its distance! \\_o< *skeptical quack*" + ] + + # Small chance the duck gets scared and flies away (configurable) + scared_chance = self.get_config('befriending.scared_away_chance', 10) or 10 + if random.randint(1, 100) <= scared_chance: + target_duck['alive'] = False + scared_messages = [ + f"{nick} > Your friendship attempt scared the duck away! It flies off into the sunset! \\_o< *departing quack*", + f"{nick} > The duck panics at your approach and escapes! \\_O< *panicked quack* *flap flap*" + ] + self.send_message(channel, random.choice(scared_messages)) + else: + self.send_message(channel, random.choice(refusal_messages)) + + # XP penalty for failed befriend attempt (configurable) + xp_penalty = self.get_config('befriending.failure_xp_penalty', 1) + player['xp'] = max(0, player['xp'] - xp_penalty) + + # Update karma for failed befriend + if self.get_config('karma.enabled', True): + karma_penalty = self.get_config('karma.befriend_fail_penalty', 1) + player['karma'] = player.get('karma', 0) - karma_penalty + + # Save player data + self.save_player(user) + else: + self.send_message(channel, f"{nick} > There is no duck to befriend!") + + elif cmd == '!reload': + player = self.get_player(user) + if not player: + return + + # Check if gun is jammed (reload unjams it) + if player.get('jammed', False): + player['jammed'] = False + unjam_sound = "•click click•" if player.get('silencer', 0) > 0 else "*click click*" + self.send_message(channel, f"{nick} > {unjam_sound} You unjammed your gun! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") + self.save_player(user) + return + + if player['ammo'] == player['max_ammo']: + self.send_message(channel, f"{nick} > Your gun doesn't need to be reloaded. | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") + return + + if player['chargers'] <= 0: + self.send_message(channel, f"{nick} > You don't have any chargers left! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: 0/{player['max_chargers']}") + return + + # Calculate reload reliability + reload_reliability = self.calculate_gun_reliability(player) + + if random.randint(1, 100) <= reload_reliability: + player['chargers'] -= 1 + player['ammo'] = player['max_ammo'] + reload_sound = "•click•" if player.get('silencer', 0) > 0 else "*click*" + self.send_message(channel, f"{nick} > {reload_sound} You reloaded your gun! | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}") + else: + # Gun jams during reload + player['jammed'] = True + player['jammed_count'] = player.get('jammed_count', 0) + 1 + jam_sound = "•CLACK• •click click•" if player.get('silencer', 0) > 0 else "*CLACK* *click click*" + self.send_message(channel, f"{nick} > {jam_sound} Your gun jammed while reloading! Use !reload again to unjam it.") + + # Save to database after reload + self.save_player(user) + + elif cmd == '!stats': + await self.handle_stats(nick, channel, user) + elif cmd == '!help': + await self.handle_help(nick, channel) + elif full_cmd.startswith('!shop'): + # Handle !shop or !shop + parts = full_cmd.split() + if len(parts) == 1: + # Just !shop - show shop listing + await self.handle_shop(nick, channel, user) + elif len(parts) >= 2: + # !shop - purchase item + item_id = parts[1] + await self.handle_buy(nick, channel, item_id, user) + elif full_cmd.startswith('!use '): + parts = full_cmd[5:].split() + if len(parts) >= 1: + item_id = parts[0] + target_nick = parts[1] if len(parts) >= 2 else None + await self.handle_use(nick, channel, item_id, user, target_nick) + else: + self.send_message(channel, f"{nick} > Usage: !use [target_nick]") + elif full_cmd.startswith('!sell '): + item_id = full_cmd[6:].strip() + await self.handle_sell(nick, channel, item_id, user) + elif full_cmd.startswith('!trade '): + parts = full_cmd[7:].split() + if len(parts) >= 3: + target_nick, item, amount = parts[0], parts[1], parts[2] + await self.handle_trade(nick, channel, user, target_nick, item, amount) + else: + self.send_message(channel, f"{nick} > Usage: !trade ") + elif full_cmd.startswith('!rearm ') and self.is_admin(user): # Admin only + # Allow rearming other players or self + target_nick = full_cmd[7:].strip() + await self.handle_rearm(nick, channel, user, target_nick) + elif cmd == '!rearm' and self.is_admin(user): # Admin only + # Rearm self + await self.handle_rearm(nick, channel, user, nick) + elif cmd == '!duck' and self.is_admin(user): # Admin only + await self.spawn_duck_now(channel) + elif cmd == '!golden' and self.is_admin(user): # Admin only + await self.spawn_duck_now(channel, force_golden=True) + elif cmd == '!listplayers' and self.is_admin(user): # Admin only + await self.handle_listplayers(nick, channel) + elif full_cmd.startswith('!ban ') and self.is_admin(user): # Admin only + target_nick = full_cmd[5:].strip() + await self.handle_ban(nick, channel, target_nick) + elif full_cmd.startswith('!reset ') and self.is_admin(user): # Admin only + target_nick = full_cmd[7:].strip() + await self.handle_reset(nick, channel, target_nick) + elif cmd == '!resetdb' and self.is_admin(user): # Admin only + await self.handle_reset_database(nick, channel, user) + elif full_cmd.startswith('!resetdb confirm ') and self.is_admin(user): # Admin only + confirmation = full_cmd[17:].strip() + await self.handle_reset_database_confirm(nick, channel, user, confirmation) + elif cmd == '!restart' and self.is_admin(user): # Admin only + await self.handle_restart(nick, channel) + elif cmd == '!quit' and self.is_admin(user): # Admin only + await self.handle_quit(nick, channel) + elif cmd == '!ducklaunch' and self.is_admin(user): # Admin only + await self.spawn_duck_now(channel) + elif cmd == '!ducks': + # Show duck count for all users + channel_ducks = self.ducks.get(channel, []) + alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] + dead_ducks = [duck for duck in channel_ducks if not duck.get('alive')] + + if alive_ducks: + duck_list = [] + for duck in alive_ducks: + duck_type = duck.get('type', 'normal') + spawn_time = time.time() - duck['spawn_time'] + if duck_type == 'golden': + duck_list.append(f"{self.colors['yellow']}Golden Duck{self.colors['reset']} ({spawn_time:.1f}s)") + else: + duck_list.append(f"Duck ({spawn_time:.1f}s)") + self.send_message(channel, f"{nick} > Active ducks: {', '.join(duck_list)}") + else: + self.send_message(channel, f"{nick} > No ducks currently active.") + + elif cmd == '!top' or cmd == '!leaderboard': + # Show top players by XP + if not self.players: + self.send_message(channel, f"{nick} > No players found!") + return + + # Sort players by XP + sorted_players = sorted(self.players.items(), key=lambda x: x[1]['xp'], reverse=True) + top_5 = sorted_players[:5] + + self.send_message(channel, f"{self.colors['cyan']}šŸ† TOP HUNTERS LEADERBOARD šŸ†{self.colors['reset']}") + for i, (player_nick, player_data) in enumerate(top_5, 1): + level, title = self.get_player_level(player_data['xp']) + total_ducks = player_data.get('caught', 0) + player_data.get('befriended', 0) + golden = player_data.get('golden_ducks', 0) + golden_text = f" ({self.colors['yellow']}{golden} golden{self.colors['reset']})" if golden > 0 else "" + + if i == 1: + rank_color = self.colors['yellow'] # Gold + elif i == 2: + rank_color = self.colors['gray'] # Silver + elif i == 3: + rank_color = self.colors['orange'] # Bronze + else: + rank_color = self.colors['white'] + + self.send_message(channel, f"{rank_color}#{i}{self.colors['reset']} {player_nick} - Level {level}: {title} | XP: {player_data['xp']} | Ducks: {total_ducks}{golden_text}") + + elif cmd == '!levels': + # Show level progression table + self.send_message(channel, f"{self.colors['cyan']}šŸŽÆ LEVEL PROGRESSION SYSTEM šŸŽÆ{self.colors['reset']}") + + # Show first 10 levels as example + for i in range(min(10, len(self.levels))): + level_data = self.levels[i] + next_xp = self.levels[i + 1]['xp'] if i + 1 < len(self.levels) else "MAX" + self.send_message(channel, f"Level {i + 1}: {level_data['title']} (XP: {level_data['xp']} - {next_xp})") + + if len(self.levels) > 10: + self.send_message(channel, f"... and {len(self.levels) - 10} more levels up to Level {len(self.levels)}: {self.levels[-1]['title']}") + + elif full_cmd.startswith('!level '): + # Show specific player's level info + target_nick = full_cmd[7:].strip().lower() + if target_nick in self.players: + target_player = self.players[target_nick] + level, title = self.get_player_level(target_player['xp']) + xp_for_next = self.get_xp_for_next_level(target_player['xp']) + + if xp_for_next > 0: + next_info = f"Next level in {xp_for_next} XP" + else: + next_info = "MAX LEVEL REACHED!" + + self.send_message(channel, f"{nick} > {target_nick}: Level {level} - {self.colors['cyan']}{title}{self.colors['reset']} | {next_info}") + else: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + + elif cmd == '!karma': + # Show karma leaderboard + if not self.players: + self.send_message(channel, f"{nick} > No players found!") + return + + # Sort by karma + karma_players = [(nick, data) for nick, data in self.players.items() if data.get('karma', 0) != 0] + karma_players.sort(key=lambda x: x[1].get('karma', 0), reverse=True) + + if not karma_players: + self.send_message(channel, f"{nick} > No karma data available!") + return + + self.send_message(channel, f"{self.colors['purple']}☯ KARMA LEADERBOARD ☯{self.colors['reset']}") + for i, (player_nick, player_data) in enumerate(karma_players[:5], 1): + karma = player_data.get('karma', 0) + karma_color = self.colors['green'] if karma >= 0 else self.colors['red'] + karma_text = "Saint" if karma >= 50 else "Good" if karma >= 10 else "Evil" if karma <= -10 else "Chaotic" if karma <= -50 else "Neutral" + + self.send_message(channel, f"#{i} {player_nick} - {karma_color}Karma: {karma}{self.colors['reset']} ({karma_text})") + + elif cmd == '!ducks': + # Show duck count for all users + channel_ducks = self.ducks.get(channel, []) + alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] + dead_ducks = [duck for duck in channel_ducks if not duck.get('alive')] + + if alive_ducks: + oldest_time = min(time.time() - duck['spawn_time'] for duck in alive_ducks) + self.send_message(channel, f"{nick} > {len(alive_ducks)} ducks in {channel} | Oldest: {oldest_time:.1f}s | Dead: {len(dead_ducks)} | Timeout: {self.duck_timeout_min}-{self.duck_timeout_max}s") + else: + self.send_message(channel, f"{nick} > No ducks in {channel} | Dead: {len(dead_ducks)}") + elif cmd == '!output' or full_cmd.startswith('!output '): + parts = cmd.split(maxsplit=1) + output_type = parts[1] if len(parts) > 1 else '' + await self.handle_output(nick, channel, user, output_type) + elif full_cmd.startswith('!ignore ') and self.is_admin(user): # Admin only + target_nick = full_cmd[8:].strip().lower() + await self.handle_ignore(nick, channel, target_nick) + elif full_cmd.startswith('!delignore ') and self.is_admin(user): # Admin only + target_nick = full_cmd[11:].strip().lower() + await self.handle_delignore(nick, channel, target_nick) + elif full_cmd.startswith('!giveitem ') and self.is_admin(user): # Admin only + parts = full_cmd[10:].split() + if len(parts) >= 2: + target_nick, item = parts[0], parts[1] + await self.handle_admin_giveitem(nick, channel, target_nick, item) + else: + self.send_message(channel, f"{nick} > Usage: !giveitem ") + elif full_cmd.startswith('!givexp ') and self.is_admin(user): # Admin only + parts = full_cmd[8:].split() + if len(parts) >= 2: + target_nick, amount = parts[0], parts[1] + await self.handle_admin_givexp(nick, channel, target_nick, amount) + else: + self.send_message(channel, f"{nick} > Usage: !givexp ") + + async def handle_stats(self, nick, channel, user): + player = self.get_player(user) + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Get level and title + level, title = self.get_player_level(player['xp']) + xp_for_next = self.get_xp_for_next_level(player['xp']) + + # Calculate advanced stats + total_shots = player.get('caught', 0) + player.get('missed', 0) + effective_accuracy = (player.get('caught', 0) / total_shots * 100) if total_shots > 0 else 0 + average_time = (player.get('total_reflex_time', 0) / player.get('reflex_shots', 1)) if player.get('reflex_shots', 0) > 0 else 0 + + # Gun status + gun_status = "" + if player.get('gun_confiscated', False): + gun_status += f" {self.colors['red']}[CONFISCATED]{self.colors['reset']}" + if player.get('jammed', False): + gun_status += f" {self.colors['yellow']}[JAMMED]{self.colors['reset']}" + if player.get('explosive_ammo', False): + gun_status += f" {self.colors['orange']}[EXPLOSIVE]{self.colors['reset']}" + + # Duck stats with colors + duck_stats = [] + if player.get('caught', 0) > 0: + duck_stats.append(f"Shot:{player['caught']}") + if player.get('befriended', 0) > 0: + duck_stats.append(f"Befriended:{player['befriended']}") + if player.get('golden_ducks', 0) > 0: + duck_stats.append(f"{self.colors['yellow']}Golden:{player['golden_ducks']}{self.colors['reset']}") + + duck_display = f"Ducks:({', '.join(duck_stats)})" if duck_stats else "Ducks:0" + + # Main stats line + stats_line1 = f"{nick} > {duck_display} | Level {level}: {self.colors['cyan']}{title}{self.colors['reset']} | XP: {player['xp']}" + if xp_for_next > 0: + stats_line1 += f" (next: {xp_for_next})" + + # Combat stats line + karma_color = self.colors['green'] if player.get('karma', 0) >= 0 else self.colors['red'] + karma_display = f"{karma_color}Karma:{player.get('karma', 0)}{self.colors['reset']}" + + stats_line2 = f"{nick} > {karma_display} | Accuracy: {player['accuracy']}% (effective: {effective_accuracy:.1f}%) | Reliability: {self.calculate_gun_reliability(player)}%" + + # Equipment line + weapon_name = player.get('weapon', 'pistol').replace('_', ' ').title() + stats_line3 = f"{nick} > Weapon: {weapon_name}{gun_status} | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']}" + + # Advanced stats line + best_time = player.get('best_time', 999.9) + best_display = f"{best_time:.3f}s" if best_time < 999 else "none" + + stats_line4 = f"{nick} > Best time: {best_display} | Avg time: {average_time:.3f}s | Jams: {player.get('jammed_count', 0)} | Accidents: {player.get('accidents', 0)} | Lucky shots: {player.get('lucky_shots', 0)}" + + # Send all stats + await self.send_user_message(nick, channel, stats_line1) + await self.send_user_message(nick, channel, stats_line2) + await self.send_user_message(nick, channel, stats_line3) + await self.send_user_message(nick, channel, stats_line4) + + # Inventory display + if player.get('inventory'): + inventory_items = [] + shop_items = { + '1': 'Extra bullet', '2': 'Extra clip', '3': 'AP ammo', '4': 'Explosive ammo', + '5': 'Gun restore', '6': 'Grease', '7': 'Sight', '8': 'Detector', '9': 'Silencer', + '10': 'Clover', '11': 'Shotgun', '12': 'Rifle', '13': 'Sniper', '14': 'Auto shotgun', + '15': 'Sand', '16': 'Water', '17': 'Sabotage', '18': 'Life insurance', + '19': 'Liability insurance', '20': 'Decoy', '21': 'Bread', '22': 'Duck detector', '23': 'Mechanical duck' + } + + for item_id, count in player['inventory'].items(): + item_name = shop_items.get(item_id, f"Item #{item_id}") + inventory_items.append(f"{item_id}:{item_name}({count})") + + if inventory_items: + max_slots = self.get_config('economy.max_inventory_slots', 20) + total_items = sum(player['inventory'].values()) + inventory_display = f"{nick} > {self.colors['magenta']}Inventory ({total_items}/{max_slots}):{self.colors['reset']} {' | '.join(inventory_items[:10])}" + if len(inventory_items) > 10: + inventory_display += f" ... and {len(inventory_items) - 10} more" + await self.send_user_message(nick, channel, inventory_display) + + async def handle_rearm(self, nick, channel, user, target_nick): + """Rearm a player whose gun was confiscated""" + player = self.get_player(user) + target_nick_lower = target_nick.lower() + + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Check if target exists + if target_nick_lower not in self.players: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + return + + target_player = self.players[target_nick_lower] + + # Check if target's gun is confiscated + if not target_player.get('gun_confiscated', False): + self.send_message(channel, f"{nick} > {target_nick}'s gun is not confiscated!") + return + + # Admins can rearm anyone for free + is_admin = self.is_admin(user) + + if is_admin: + # Admin rearm - no cost, configurable restoration + target_player['gun_confiscated'] = False + + # Configure ammo restoration + if self.get_config('moderation.admin_rearm_gives_full_ammo', True): + target_player['ammo'] = target_player['max_ammo'] # Full ammo + ammo_text = "full ammo" + else: + target_player['ammo'] = min(target_player['ammo'] + 1, target_player['max_ammo']) # Just +1 ammo + ammo_text = "+1 ammo" + + # Configure charger restoration + if self.get_config('moderation.admin_rearm_gives_full_chargers', True): + target_player['chargers'] = target_player.get('max_chargers', 2) # Full chargers + charger_text = ", full chargers" + else: + charger_text = "" + + if target_nick_lower == nick.lower(): + self.send_message(channel, f"{nick} > {self.colors['green']}Admin command: Gun restored with {ammo_text}{charger_text}.{self.colors['reset']}") + else: + self.send_message(channel, f"{nick} > {self.colors['green']}Admin command: {target_nick}'s gun restored with {ammo_text}{charger_text}.{self.colors['reset']}") + self.save_database() + elif target_nick_lower == nick.lower(): + # Regular player rearming self - costs XP + rearm_cost = 40 + if player['xp'] < rearm_cost: + self.send_message(channel, f"{nick} > You need {rearm_cost} XP to rearm yourself (you have {player['xp']} XP)") + return + + player['xp'] -= rearm_cost + player['gun_confiscated'] = False + player['ammo'] = player['max_ammo'] # Full ammo when rearmed + self.send_message(channel, f"{nick} > {self.colors['green']}You rearmed yourself! [-{rearm_cost} XP] Gun restored with full ammo.{self.colors['reset']}") + self.save_player(user) + else: + # Regular player rearming someone else - costs XP (friendly gesture) + rearm_cost_xp = 5 + if player['xp'] < rearm_cost_xp: + self.send_message(channel, f"{nick} > You need {rearm_cost_xp} XP to rearm {target_nick} (you have {player['xp']} XP)") + return + + player['xp'] -= rearm_cost_xp + target_player['gun_confiscated'] = False + target_player['ammo'] = target_player['max_ammo'] # Full ammo when rearmed + self.send_message(channel, f"{nick} > {self.colors['green']}You rearmed {target_nick}! [-{rearm_cost_xp} XP] {target_nick}'s gun restored with full ammo.{self.colors['reset']}") + self.save_player(user) + self.save_database() + + async def handle_help(self, nick, channel): + help_lines = [ + f"{nick} > {self.colors['cyan']}šŸ¦† DUCKHUNT šŸ¦†{self.colors['reset']} !bang !bef !reload !stats !top !shop !buy ", + f"{nick} > {self.colors['yellow']}Golden ducks: 50 XP{self.colors['reset']} | {self.colors['red']}Gun jamming & ricochets ON{self.colors['reset']} | Timeout: {self.duck_timeout_min}-{self.duck_timeout_max}s" + ] + if self.is_admin(f"{nick}!*@*"): # Check if admin + help_lines.append(f"{nick} > {self.colors['red']}Admin:{self.colors['reset']} !duck !golden !ban !reset !resetdb !rearm !giveitem !givexp | /msg {self.config['nick']} restart|quit") + for line in help_lines: + self.send_message(channel, line) + + async def handle_output(self, nick, channel, user, output_type): + """Handle output mode setting (PRIVMSG or NOTICE)""" + player = self.get_player(user) + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Ensure player has settings (for existing players) + if 'settings' not in player: + player['settings'] = { + 'notices': True + } + + output_type = output_type.upper() + + if output_type == 'PRIVMSG': + player['settings']['notices'] = False + self.save_database() + self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PRIVMSG{self.colors['reset']} (private messages)") + + elif output_type == 'NOTICE': + player['settings']['notices'] = True + self.save_database() + self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}NOTICE{self.colors['reset']} (channel notices)") + + else: + current_mode = 'NOTICE' if player['settings']['notices'] else 'PRIVMSG' + self.send_message(channel, f"{nick} > Current output mode: {self.colors['cyan']}{current_mode}{self.colors['reset']} | Usage: !output PRIVMSG or !output NOTICE") + + async def handle_shop(self, nick, channel, user): + player = self.get_player(user) + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Show compact shop in eggdrop style + shop_msg = f"[Duck Hunt] Purchasable items: 1-Extra bullet(7xp) 2-Extra clip(20xp) 3-AP ammo(15xp) 4-Explosive ammo(25xp) 5-Repurchase gun(40xp) 6-Grease(8xp) 7-Sight(6xp) 8-Infrared detector(15xp) 9-Silencer(5xp) 10-Four-leaf clover(13xp) 11-Shotgun(100xp) 12-Assault rifle(200xp) 13-Sniper rifle(350xp) 14-Auto shotgun(500xp) 15-Sand(7xp) 16-Water bucket(10xp) 17-Sabotage(14xp) 18-Life insurance(10xp) 19-Liability insurance(5xp) 20-Decoy(80xp) 21-Bread(50xp) 22-Duck detector(50xp) 23-Mechanical duck(50xp)" + self.send_message(channel, f"{nick} > {shop_msg}") + self.send_message(channel, f"{nick} > Your XP: {player['xp']} | Use !shop to purchase") + + async def handle_buy(self, nick, channel, item, user): + """Buy items and add to inventory""" + player = self.get_player(user) + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Check if inventory system is enabled + if not self.get_config('economy.inventory_system_enabled', True): + self.send_message(channel, f"{nick} > Inventory system is disabled!") + return + + # Initialize inventory if not exists + if 'inventory' not in player: + player['inventory'] = {} + + # Eggdrop-style shop items with XP costs + shop_items = { + '1': {'name': 'Extra bullet', 'cost': 7}, + '2': {'name': 'Extra clip', 'cost': 20}, + '3': {'name': 'AP ammo', 'cost': 15}, + '4': {'name': 'Explosive ammo', 'cost': 25}, + '5': {'name': 'Repurchase confiscated gun', 'cost': 40}, + '6': {'name': 'Grease', 'cost': 8}, + '7': {'name': 'Sight', 'cost': 6}, + '8': {'name': 'Infrared detector', 'cost': 15}, + '9': {'name': 'Silencer', 'cost': 5}, + '10': {'name': 'Four-leaf clover', 'cost': 13}, + '11': {'name': 'Shotgun', 'cost': 100}, + '12': {'name': 'Assault rifle', 'cost': 200}, + '13': {'name': 'Sniper rifle', 'cost': 350}, + '14': {'name': 'Automatic shotgun', 'cost': 500}, + '15': {'name': 'Handful of sand', 'cost': 7}, + '16': {'name': 'Water bucket', 'cost': 10}, + '17': {'name': 'Sabotage', 'cost': 14}, + '18': {'name': 'Life insurance', 'cost': 10}, + '19': {'name': 'Liability insurance', 'cost': 5}, + '20': {'name': 'Decoy', 'cost': 80}, + '21': {'name': 'Piece of bread', 'cost': 50}, + '22': {'name': 'Ducks detector', 'cost': 50}, + '23': {'name': 'Mechanical duck', 'cost': 50} + } + + if item not in shop_items: + self.send_message(channel, f"{nick} > Invalid item ID. Use !shop to see available items.") + return + + shop_item = shop_items[item] + cost = shop_item['cost'] + + if player['xp'] < cost: + self.send_message(channel, f"{nick} > Not enough XP! You need {cost} XP but only have {player['xp']}.") + return + + # Check inventory space + max_slots = self.get_config('economy.max_inventory_slots', 20) + if max_slots is None: + max_slots = 20 + total_items = sum(player['inventory'].values()) + if total_items >= max_slots: + self.send_message(channel, f"{nick} > Inventory full! ({total_items}/{max_slots}) Use items or increase capacity.") + return + + # Purchase the item and add to inventory + player['xp'] -= cost + if item in player['inventory']: + player['inventory'][item] += 1 + else: + player['inventory'][item] = 1 + + self.send_message(channel, f"{nick} > Purchased {shop_item['name']}! Added to inventory ({total_items + 1}/{max_slots})") + + # Save to database after purchase + self.save_player(user) + + async def handle_sell(self, nick, channel, item_id, user): + """Sell items from inventory for 70% of original cost""" + player = self.get_player(user) + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Check if inventory system is enabled + if not self.get_config('economy.inventory_system_enabled', True): + self.send_message(channel, f"{nick} > Inventory system is disabled!") + return + + # Initialize inventory if not exists + if 'inventory' not in player: + player['inventory'] = {} + + # Check if item is in inventory + if item_id not in player['inventory'] or player['inventory'][item_id] <= 0: + self.send_message(channel, f"{nick} > You don't have that item! Check !stats to see your inventory.") + return + + # Get shop item data for pricing + shop_items = { + '1': {'name': 'Extra bullet', 'cost': 7}, + '2': {'name': 'Extra clip', 'cost': 20}, + '3': {'name': 'AP ammo', 'cost': 15}, + '4': {'name': 'Explosive ammo', 'cost': 25}, + '5': {'name': 'Repurchase confiscated gun', 'cost': 40}, + '6': {'name': 'Grease', 'cost': 8}, + '7': {'name': 'Sight', 'cost': 6}, + '8': {'name': 'Infrared detector', 'cost': 15}, + '9': {'name': 'Silencer', 'cost': 5}, + '10': {'name': 'Four-leaf clover', 'cost': 13}, + '11': {'name': 'Shotgun', 'cost': 100}, + '12': {'name': 'Assault rifle', 'cost': 200}, + '13': {'name': 'Sniper rifle', 'cost': 350}, + '14': {'name': 'Automatic shotgun', 'cost': 500}, + '15': {'name': 'Handful of sand', 'cost': 7}, + '16': {'name': 'Water bucket', 'cost': 10}, + '17': {'name': 'Sabotage', 'cost': 14}, + '18': {'name': 'Life insurance', 'cost': 10}, + '19': {'name': 'Liability insurance', 'cost': 5}, + '20': {'name': 'Decoy', 'cost': 80}, + '21': {'name': 'Piece of bread', 'cost': 50}, + '22': {'name': 'Ducks detector', 'cost': 50}, + '23': {'name': 'Mechanical duck', 'cost': 50} + } + + if item_id not in shop_items: + self.send_message(channel, f"{nick} > Invalid item ID!") + return + + shop_item = shop_items[item_id] + original_cost = shop_item['cost'] + sell_price = int(original_cost * 0.7) # 70% of original cost + + # Remove item from inventory + player['inventory'][item_id] -= 1 + if player['inventory'][item_id] <= 0: + del player['inventory'][item_id] + + # Give XP back + player['xp'] += sell_price + + total_items = sum(player['inventory'].values()) + max_slots = self.get_config('economy.max_inventory_slots', 20) + + self.send_message(channel, f"{nick} > Sold {shop_item['name']} for {sell_price}xp! Inventory: ({total_items}/{max_slots})") + + # Save to database after sale + self.save_player(user) + + async def handle_use(self, nick, channel, item_id, user, target_nick=None): + """Use an item from inventory""" + player = self.get_player(user) + if not player: + self.send_message(channel, f"{nick} > Player data not found!") + return + + # Check if item is in inventory + if item_id not in player['inventory'] or player['inventory'][item_id] <= 0: + self.send_message(channel, f"{nick} > You don't have that item! Check !stats to see your inventory.") + return + + # Get shop item data for reference + shop_items = { + '1': {'name': 'Extra bullet', 'effect': 'ammo'}, + '2': {'name': 'Extra clip', 'effect': 'max_ammo'}, + '3': {'name': 'AP ammo', 'effect': 'accuracy'}, + '4': {'name': 'Explosive ammo', 'effect': 'explosive'}, + '5': {'name': 'Repurchase confiscated gun', 'effect': 'gun'}, + '6': {'name': 'Grease', 'effect': 'reliability'}, + '7': {'name': 'Sight', 'effect': 'accuracy'}, + '8': {'name': 'Infrared detector', 'effect': 'detector'}, + '9': {'name': 'Silencer', 'effect': 'silencer'}, + '10': {'name': 'Four-leaf clover', 'effect': 'luck'}, + '11': {'name': 'Shotgun', 'effect': 'shotgun'}, + '12': {'name': 'Assault rifle', 'effect': 'rifle'}, + '13': {'name': 'Sniper rifle', 'effect': 'sniper'}, + '14': {'name': 'Automatic shotgun', 'effect': 'auto_shotgun'}, + '15': {'name': 'Handful of sand', 'effect': 'sand'}, + '16': {'name': 'Water bucket', 'effect': 'water'}, + '17': {'name': 'Sabotage', 'effect': 'sabotage'}, + '18': {'name': 'Life insurance', 'effect': 'life_insurance'}, + '19': {'name': 'Liability insurance', 'effect': 'liability'}, + '20': {'name': 'Decoy', 'effect': 'decoy'}, + '21': {'name': 'Piece of bread', 'effect': 'bread'}, + '22': {'name': 'Ducks detector', 'effect': 'duck_detector'}, + '23': {'name': 'Mechanical duck', 'effect': 'mechanical'} + } + + if item_id not in shop_items: + self.send_message(channel, f"{nick} > Invalid item ID!") + return + + shop_item = shop_items[item_id] + effect = shop_item['effect'] + + # Determine target player + if target_nick and target_nick.lower() != nick.lower(): + # Using on someone else + target_nick_lower = target_nick.lower() + if target_nick_lower not in self.players: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + return + target_player = self.players[target_nick_lower] + using_on_other = True + else: + # Using on self + target_player = player + target_nick = nick + using_on_other = False + + # Remove item from inventory + player['inventory'][item_id] -= 1 + if player['inventory'][item_id] <= 0: + del player['inventory'][item_id] + + # Apply item effects + if effect == 'ammo': + target_player['ammo'] = min(target_player['max_ammo'], target_player['ammo'] + 1) + if using_on_other: + self.send_message(channel, f"{nick} > Used {shop_item['name']} on {target_nick}! +1 ammo") + else: + self.send_message(channel, f"{nick} > Used {shop_item['name']}! +1 ammo") + elif effect == 'water': + # Water bucket - splash attack on target player + if using_on_other: + # Reduce target's accuracy temporarily + target_player['accuracy'] = max(10, target_player['accuracy'] - 15) + self.send_message(channel, f"{nick} > *SPLASH!* You soaked {target_nick} with water! Their accuracy reduced by 15%!") + else: + self.send_message(channel, f"{nick} > You splashed yourself with water... why?") + elif effect == 'sand': + # Handful of sand - blind target temporarily + if using_on_other: + target_player['accuracy'] = max(5, target_player['accuracy'] - 20) + self.send_message(channel, f"{nick} > *POCKET SAND!* You threw sand in {target_nick}'s eyes! Their accuracy reduced by 20%!") + else: + self.send_message(channel, f"{nick} > You threw sand in your own eyes... brilliant strategy!") + # Add more effects as needed... + else: + # Default effects for other items + self.send_message(channel, f"{nick} > Used {shop_item['name']}! (Effect: {effect})") + + # Save changes + self.save_player(user) + if using_on_other: + # Save target player too if different + target_user = f"{target_nick.lower()}!user@host" # Simplified - would need real user data + self.save_database() + + async def handle_trade(self, nick, channel, user, target_nick, item, amount): + """Trade items with other players""" + player = self.get_player(user) + if not player: + return + + try: + amount = int(amount) + except ValueError: + self.send_message(channel, f"{nick} > Amount must be a number!") + return + + if amount <= 0: + self.send_message(channel, f"{nick} > Amount must be positive!") + return + + if amount > 10000: # Prevent excessive amounts + self.send_message(channel, f"{nick} > Amount too large! Maximum: 10,000") + return + + # Find target player (simplified - would need to track online users in real implementation) + if item == 'coins': + if player['coins'] < amount: + self.send_message(channel, f"{nick} > You don't have {amount} coins!") + return + player['coins'] -= amount + self.send_message(channel, f"{nick} > Offering {amount} coins to {target_nick}. They can !accept or !decline.") + # In real implementation, store pending trade + + elif item == 'ammo': + if player['ammo'] < amount: + self.send_message(channel, f"{nick} > You don't have {amount} ammo!") + return + self.send_message(channel, f"{nick} > Offering {amount} ammo to {target_nick}.") + + elif item == 'chargers': + if player['chargers'] < amount: + self.send_message(channel, f"{nick} > You don't have {amount} chargers!") + return + self.send_message(channel, f"{nick} > Offering {amount} chargers to {target_nick}.") + + else: + self.send_message(channel, f"{nick} > Can't trade '{item}'. Use: coins, ammo, or chargers") + + self.save_player(user) + + async def handle_listplayers(self, nick, channel): + """Admin command to list all players""" + if not self.players: + self.send_message(channel, f"{nick} > No players in database.") + return + + player_list = [] + for nick_key, data in self.players.items(): + shot_count = data['caught'] + befriended_count = data.get('befriended', 0) + total_ducks = shot_count + befriended_count + player_list.append(f"{nick_key}(Ducks:{total_ducks},Shot:{shot_count},Befriended:{befriended_count})") + + players_str = " | ".join(player_list[:10]) # Limit to first 10 + if len(self.players) > 10: + players_str += f" ... and {len(self.players) - 10} more" + + self.send_message(channel, f"{nick} > Players: {players_str}") + + async def handle_ban(self, nick, channel, target_nick): + """Admin command to ban a player""" + target_nick_lower = target_nick.lower() + if target_nick_lower in self.players: + del self.players[target_nick_lower] + self.send_message(channel, f"{nick} > Banned and reset {target_nick}") + self.save_database() + else: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + + async def handle_reset(self, nick, channel, target_nick): + """Admin command to reset a player's stats""" + target_nick_lower = target_nick.lower() + if target_nick_lower in self.players: + # Reset to defaults + self.players[target_nick_lower] = { + 'caught': 0, 'ammo': 10, 'max_ammo': 10, + 'chargers': 2, 'max_chargers': 2, 'xp': 0, + 'accuracy': 85, 'reliability': 90, 'gun_level': 1, + 'luck': 0, 'gun_type': 'pistol' + } + self.send_message(channel, f"{nick} > Reset {target_nick}'s stats to defaults") + self.save_database() + else: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + + async def handle_reset_database(self, nick, channel, user): + """Admin command to reset entire database - requires confirmation""" + self.send_message(channel, f"{nick} > {self.colors['red']}āš ļø DATABASE RESET WARNING āš ļø{self.colors['reset']}") + self.send_message(channel, f"{nick} > This will DELETE ALL player data, statistics, and progress!") + self.send_message(channel, f"{nick} > {self.colors['yellow']}Players affected: {len(self.players)}{self.colors['reset']}") + self.send_message(channel, f"{nick} > To confirm, type: {self.colors['cyan']}!resetdb confirm DESTROY_ALL_DATA{self.colors['reset']}") + self.send_message(channel, f"{nick} > {self.colors['red']}This action CANNOT be undone!{self.colors['reset']}") + + async def handle_reset_database_confirm(self, nick, channel, user, confirmation): + """Confirm and execute database reset""" + if confirmation != "DESTROY_ALL_DATA": + self.send_message(channel, f"{nick} > {self.colors['red']}Incorrect confirmation code. Database reset cancelled.{self.colors['reset']}") + return + + # Log the reset action + self.logger.warning(f"DATABASE RESET initiated by admin {nick} - All player data will be destroyed") + + # Backup current database + import shutil + backup_name = f"duckhunt_backup_{int(time.time())}.json" + try: + shutil.copy2(self.db_file, backup_name) + self.send_message(channel, f"{nick} > {self.colors['cyan']}Database backed up to: {backup_name}{self.colors['reset']}") + except Exception as e: + self.logger.error(f"Failed to create backup: {e}") + self.send_message(channel, f"{nick} > {self.colors['red']}Warning: Could not create backup!{self.colors['reset']}") + + # Clear all data + player_count = len(self.players) + self.players.clear() + self.ducks.clear() + self.ignored_nicks.clear() + + # Save empty database + self.save_database() + + # Confirmation messages + self.send_message(channel, f"{nick} > {self.colors['green']}āœ… DATABASE RESET COMPLETE{self.colors['reset']}") + self.send_message(channel, f"{nick} > {self.colors['yellow']}{player_count} player records deleted{self.colors['reset']}") + self.send_message(channel, f"{nick} > All ducks cleared, fresh start initiated") + self.logger.warning(f"Database reset completed by {nick} - {player_count} players deleted") + + async def handle_restart(self, nick, channel): + """Admin command to restart the bot""" + self.send_message(channel, f"{nick} > Restarting bot...") + self.logger.info(f"Bot restart requested by {nick}") + + # Close connections gracefully + if self.writer: + self.writer.close() + await self.writer.wait_closed() + + # Save any pending data + self.save_database() + + # Restart the Python process + self.logger.info("Restarting Python process...") + python = sys.executable + script = sys.argv[0] + args = sys.argv[1:] + + # Use subprocess to restart + subprocess.Popen([python, script] + args) + + # Exit current process + sys.exit(0) + + async def handle_quit(self, nick, channel): + """Admin command to quit the bot""" + self.send_message(channel, f"{nick} > Shutting down bot...") + self.logger.info(f"Bot shutdown requested by {nick}") + # Close connections gracefully + if self.writer: + self.writer.close() + await self.writer.wait_closed() + # Exit with code 0 for normal shutdown + import sys + sys.exit(0) + + async def handle_ignore(self, nick, channel, target_nick): + """Admin command to ignore a user""" + if target_nick in self.ignored_nicks: + self.send_message(channel, f"{nick} > {target_nick} is already ignored!") + return + + self.ignored_nicks.add(target_nick) + self.send_message(channel, f"{nick} > Now ignoring {target_nick}. Total ignored: {len(self.ignored_nicks)}") + self.logger.info(f"{nick} added {target_nick} to ignore list") + + async def handle_delignore(self, nick, channel, target_nick): + """Admin command to stop ignoring a user""" + if target_nick not in self.ignored_nicks: + self.send_message(channel, f"{nick} > {target_nick} is not ignored!") + return + + self.ignored_nicks.remove(target_nick) + self.send_message(channel, f"{nick} > No longer ignoring {target_nick}. Total ignored: {len(self.ignored_nicks)}") + self.logger.info(f"{nick} removed {target_nick} from ignore list") + + async def handle_admin_giveitem(self, nick, channel, target_nick, item): + """Admin command to give an item to a player""" + target_nick_lower = target_nick.lower() + + # Check if target exists + if target_nick_lower not in self.players: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + return + + # Shop items reference for item names + shop_items = { + '1': {'name': 'Extra bullet', 'effect': 'ammo'}, + '2': {'name': 'Extra clip', 'effect': 'max_ammo'}, + '3': {'name': 'AP ammo', 'effect': 'accuracy'}, + '4': {'name': 'Explosive ammo', 'effect': 'explosive'}, + '5': {'name': 'Repurchase confiscated gun', 'effect': 'gun'}, + '6': {'name': 'Grease', 'effect': 'reliability'}, + '7': {'name': 'Sight', 'effect': 'accuracy'}, + '8': {'name': 'Infrared detector', 'effect': 'detector'}, + '9': {'name': 'Silencer', 'effect': 'silencer'}, + '10': {'name': 'Four-leaf clover', 'effect': 'luck'}, + '11': {'name': 'Sunglasses', 'effect': 'sunglasses'}, + '12': {'name': 'Spare clothes', 'effect': 'clothes'}, + '13': {'name': 'Brush for gun', 'effect': 'brush'}, + '14': {'name': 'Mirror', 'effect': 'mirror'}, + '15': {'name': 'Handful of sand', 'effect': 'sand'}, + '16': {'name': 'Water bucket', 'effect': 'water'}, + '17': {'name': 'Sabotage', 'effect': 'sabotage'}, + '18': {'name': 'Life insurance', 'effect': 'life_insurance'}, + '19': {'name': 'Liability insurance', 'effect': 'liability'}, + '20': {'name': 'Decoy', 'effect': 'decoy'}, + '21': {'name': 'Piece of bread', 'effect': 'bread'}, + '22': {'name': 'Ducks detector', 'effect': 'duck_detector'}, + '23': {'name': 'Mechanical duck', 'effect': 'mechanical'} + } + + if item not in shop_items: + self.send_message(channel, f"{nick} > Invalid item ID '{item}'. Use item IDs 1-23.") + return + + target_player = self.players[target_nick_lower] + shop_item = shop_items[item] + effect = shop_item['effect'] + + # Apply the item effect + if effect == 'ammo': + target_player['ammo'] = min(target_player['ammo'] + 1, target_player['max_ammo']) + elif effect == 'max_ammo': + target_player['max_ammo'] += 1 + target_player['ammo'] = target_player['max_ammo'] # Fill ammo + elif effect == 'accuracy': + target_player['accuracy'] = min(target_player['accuracy'] + 5, 100) + elif effect == 'explosive': + target_player['explosive_ammo'] = True + elif effect == 'gun': + target_player['gun_confiscated'] = False + target_player['ammo'] = target_player['max_ammo'] + elif effect == 'reliability': + target_player['reliability'] = min(target_player['reliability'] + 5, 100) + elif effect == 'luck': + target_player['luck'] = target_player.get('luck', 0) + 1 + # Add other effects as needed + + self.send_message(channel, f"{nick} > {self.colors['green']}Gave {shop_item['name']} to {target_nick}!{self.colors['reset']}") + self.save_database() + + async def handle_admin_givexp(self, nick, channel, target_nick, amount): + """Admin command to give XP to a player""" + target_nick_lower = target_nick.lower() + + # Check if target exists + if target_nick_lower not in self.players: + self.send_message(channel, f"{nick} > Player {target_nick} not found!") + return + + try: + xp_amount = int(amount) + except ValueError: + self.send_message(channel, f"{nick} > Amount must be a number!") + return + + if abs(xp_amount) > 50000: # Prevent excessive XP changes + self.send_message(channel, f"{nick} > XP amount too large! Maximum: ±50,000") + return + + target_player = self.players[target_nick_lower] + old_xp = target_player['xp'] + target_player['xp'] = max(0, target_player['xp'] + xp_amount) # Prevent negative XP + + color = self.colors['green'] if xp_amount >= 0 else self.colors['red'] + sign = '+' if xp_amount >= 0 else '' + self.send_message(channel, f"{nick} > {color}Gave {sign}{xp_amount} XP to {target_nick}! (Total: {target_player['xp']} XP){self.colors['reset']}") + self.save_database() + + def get_duck_spawn_message(self): + """Get a random duck spawn message with different types""" + duck_types = [ + {"msg": "-.,ĀøĀø.-·°'`'°·-.,ĀøĀø.-·°'`'°· \\_O< QUACK", "type": "normal"}, # Normal duck + {"msg": "-._..-'`'°-,_,.-'`'°-,_,.-'`'°-,_,.-° \\_o< A duck waddles by! QUACK QUACK", "type": "normal"}, # Waddling duck + {"msg": "~~~°*°~~~°*°~~~°*°~~~ \\_O< SPLASH! A duck lands in the water! QUACK!", "type": "normal"}, # Water duck + {"msg": "***GOLDEN*** \\_O< *** A golden duck appears! *** QUACK QUACK! ***GOLDEN***", "type": "golden"}, # Golden duck (rare) + {"msg": "°~°*°~°*°~° \\_o< Brrr! A winter duck appears! QUACK!", "type": "normal"}, # Winter duck + {"msg": ".,ĀøĀø.-·°'`'°·-.,ĀøĀø.-·°'`'°· \\_O< A spring duck blooms into view! QUACK!", "type": "normal"}, # Spring duck + {"msg": "***ZAP*** \\_O< BZZT! An electric duck sparks to life! QUACK! ***ZAP***", "type": "normal"}, # Electric duck + {"msg": "~*~*~*~ \\_o< A sleepy night duck appears... *yawn* quack...", "type": "normal"}, # Night duck + ] + + # Golden duck is rare (5% chance) + if random.random() < 0.05: + golden_duck = [d for d in duck_types if d["type"] == "golden"][0] + return golden_duck + else: + # Choose from normal duck types + normal_ducks = [d for d in duck_types if d["type"] == "normal"] + return random.choice(normal_ducks) + + async def spawn_duck_now(self, channel, force_golden=False): + """Admin command to spawn a duck immediately""" + # Create duck with unique ID and type + duck_id = str(uuid.uuid4())[:8] # Short ID for easier tracking + + if force_golden: + # Force spawn a golden duck + duck_info = { + "msg": f"{self.colors['yellow']}***GOLDEN***{self.colors['reset']} \\_$< {self.colors['yellow']}*** A golden duck appears! ***{self.colors['reset']} QUACK QUACK! {self.colors['yellow']}***GOLDEN***{self.colors['reset']}", + "type": "golden" + } + else: + duck_info = self.get_duck_spawn_message() + + duck_timeout = random.randint(self.duck_timeout_min, self.duck_timeout_max) + duck = { + 'alive': True, + 'spawn_time': time.time(), + 'id': duck_id, + 'type': duck_info['type'], + 'message': duck_info['msg'], + 'timeout': duck_timeout + } + + # Initialize channel duck list if needed + if channel not in self.ducks: + self.ducks[channel] = [] + + # Add duck to channel + self.ducks[channel].append(duck) + + # Send spawn message + self.send_message(channel, duck_info['msg']) + self.logger.info(f"Admin spawned {duck_info['type']} duck {duck_id} in {channel}") + return True + return True # Return True to indicate duck was spawned + + async def spawn_ducks(self): + # Spawn first duck immediately after joining + await asyncio.sleep(5) # Brief delay for players to see the bot joined + for channel in self.channels_joined: + await self.spawn_duck_now(channel) + + # Start duck timeout checker + asyncio.create_task(self.duck_timeout_checker()) + + while not self.shutdown_requested: + wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max) + self.logger.info(f"Waiting {wait_time//60}m {wait_time%60}s for next duck") + + # Sleep in chunks to check shutdown flag + for _ in range(wait_time): + if self.shutdown_requested: + self.logger.info("Duck spawning stopped due to shutdown request") + return + await asyncio.sleep(1) + + # Spawn only one duck per channel if no alive ducks exist + for channel in self.channels_joined: + if self.shutdown_requested: + return + + # Check if there are any alive ducks in this channel + channel_ducks = self.ducks.get(channel, []) + alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] + + # Only spawn if no ducks are alive (one duck at a time naturally) + if not alive_ducks: + await self.spawn_duck_now(channel) + break # Only spawn in the first available channel + + async def duck_timeout_checker(self): + """Remove ducks that have been around too long""" + while not self.shutdown_requested: + await asyncio.sleep(10) # Check every 10 seconds + current_time = time.time() + + for channel in list(self.ducks.keys()): + if channel in self.ducks: + ducks_to_remove = [] + for i, duck in enumerate(self.ducks[channel]): + duck_timeout = duck.get('timeout', 60) # Use individual timeout or default to 60 + if duck['alive'] and (current_time - duck['spawn_time']) > duck_timeout: + # Duck wandered off + ducks_to_remove.append(i) + self.send_message(channel, f"A duck wandered off... *quack quack* (timeout after {duck_timeout}s)") + self.logger.info(f"Duck {duck['id']} timed out in {channel}") + + # Remove timed out ducks (in reverse order to maintain indices) + for i in reversed(ducks_to_remove): + del self.ducks[channel][i] + + async def listen(self): + """Listen for IRC messages with shutdown handling""" + while not self.shutdown_requested: + try: + if not self.reader: + self.logger.error("No reader available") + break + + # Use timeout to allow checking shutdown flag + try: + line = await asyncio.wait_for(self.reader.readline(), timeout=1.0) + except asyncio.TimeoutError: + continue # Check shutdown flag + + if not line: + self.logger.warning("Connection closed by server") + break + + line = line.decode(errors='ignore').strip() + if not line: + continue + + self.logger.debug(f"<- {line}") + + if line.startswith('PING'): + self.send_raw('PONG ' + line.split()[1]) + continue + + prefix, command, params, trailing = parse_message(line) + + except Exception as e: + self.logger.error(f"Error in listen loop: {e}") + await asyncio.sleep(1) # Brief pause before retry + continue + + # Handle SASL authentication responses + if command == 'CAP': + await self.sasl_handler.handle_cap_response(params, trailing) + + elif command == 'AUTHENTICATE': + await self.sasl_handler.handle_authenticate_response(params) + + elif command in ['903', '904', '905', '906', '907', '908']: # SASL responses + await self.sasl_handler.handle_sasl_result(command, params, trailing) + + elif command == '001': # Welcome + self.registered = True + auth_status = " (SASL authenticated)" if self.sasl_handler.is_authenticated() else "" + self.logger.info(f"Successfully registered!{auth_status}") + + # If SASL failed, try NickServ identification + if not self.sasl_handler.is_authenticated(): + await self.attempt_nickserv_auth() + + for chan in self.config['channels']: + self.logger.info(f"Joining {chan}") + self.send_raw(f'JOIN {chan}') + + elif command == 'JOIN' and prefix and prefix.startswith(self.config['nick']): + channel = trailing or (params[0] if params else '') + if channel: + self.channels_joined.add(channel) + self.logger.info(f"Successfully joined {channel}") + + elif command == 'PRIVMSG' and trailing: + target = params[0] if params else '' + sender = prefix.split('!')[0] if prefix else '' + + # Handle NickServ responses + if sender.lower() == 'nickserv': + await self.handle_nickserv_response(trailing) + elif trailing == 'VERSION': + self.send_raw(f'NOTICE {sender} :VERSION DuckHunt Bot v1.0') + else: + await self.handle_command(prefix, target, trailing) + + async def cleanup(self): + """Enhanced cleanup with graceful shutdown""" + self.logger.info("Starting cleanup process...") + + try: + # Cancel all running tasks + for task in self.running_tasks.copy(): + if not task.done(): + self.logger.debug(f"Cancelling task: {task.get_name()}") + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + except Exception as e: + self.logger.error(f"Error cancelling task: {e}") + + # Send goodbye message to all channels + if self.writer and not self.writer.is_closing(): + for channel in self.channels_joined: + self.send_message(channel, "šŸ¦† DuckHunt Bot shutting down. Thanks for playing! šŸ¦†") + await asyncio.sleep(0.1) # Brief delay between messages + + self.send_raw('QUIT :DuckHunt Bot shutting down gracefully') + await asyncio.sleep(1.0) # Give time for QUIT and messages to send + + self.writer.close() + await self.writer.wait_closed() + self.logger.info("IRC connection closed") + + # Final database save with verification + self.save_database() + self.logger.info(f"Final database save completed - {len(self.players)} players saved") + + # Clear in-memory data + self.players.clear() + self.ducks.clear() + self.command_cooldowns.clear() + + self.logger.info("Cleanup completed successfully") + + except Exception as e: + self.logger.error(f"Error during cleanup: {e}") + import traceback + traceback.print_exc() + + async def run(self): + """Main bot entry point with enhanced shutdown handling""" + try: + # Setup signal handlers + self.setup_signal_handlers() + + self.logger.info("Starting DuckHunt Bot...") + self.load_database() + await self.connect() + + # Create and track main tasks + listen_task = asyncio.create_task(self.listen(), name="listen") + duck_task = asyncio.create_task(self.wait_and_spawn_ducks(), name="duck_spawner") + + self.running_tasks.add(listen_task) + self.running_tasks.add(duck_task) + + # Main execution loop with shutdown monitoring + done, pending = await asyncio.wait( + [listen_task, duck_task], + return_when=asyncio.FIRST_COMPLETED + ) + + # If we get here, one task completed (likely due to error or shutdown) + if self.shutdown_requested: + self.logger.info("Shutdown requested, stopping all tasks...") + else: + self.logger.warning("A main task completed unexpectedly") + + # Cancel remaining tasks + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + except KeyboardInterrupt: + self.logger.info("Keyboard interrupt received") + self.shutdown_requested = True + except Exception as e: + self.logger.error(f"Fatal error in main loop: {e}") + import traceback + traceback.print_exc() + finally: + await self.cleanup() + + async def wait_and_spawn_ducks(self): + """Duck spawning with shutdown handling""" + # Wait for registration and channel joins + while not self.registered or not self.channels_joined and not self.shutdown_requested: + await asyncio.sleep(1) + + if self.shutdown_requested: + return + + self.logger.info("Starting duck spawning...") + await self.spawn_ducks() + +def main(): + """Enhanced main entry point with better shutdown handling""" + bot = None + try: + # Load configuration + with open('config.json') as f: + config = json.load(f) + + # Create bot instance + bot = SimpleIRCBot(config) + bot.logger.info("DuckHunt Bot initializing...") + + # Run bot with graceful shutdown + try: + asyncio.run(bot.run()) + except KeyboardInterrupt: + bot.logger.info("Keyboard interrupt received in main") + except Exception as e: + bot.logger.error(f"Runtime error: {e}") + import traceback + traceback.print_exc() + + bot.logger.info("DuckHunt Bot shutdown complete") + + except KeyboardInterrupt: + print("\nšŸ¦† DuckHunt Bot stopped by user") + except FileNotFoundError: + print("āŒ Error: config.json not found") + print("Please create a config.json file with your IRC server settings") + except json.JSONDecodeError as e: + print(f"āŒ Error: Invalid config.json - {e}") + print("Please check your config.json file syntax") + except Exception as e: + print(f"šŸ’„ Unexpected error: {e}") + import traceback + traceback.print_exc() + finally: + # Ensure final message + print("šŸ¦† Thanks for using DuckHunt Bot!") + +if __name__ == '__main__': + main() diff --git a/duckhunt/src/__pycache__/auth.cpython-312.pyc b/duckhunt/src/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc6416394d90b6814b930ca95ac728ed980a0251 GIT binary patch literal 6452 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 Apa1{> literal 0 HcmV?d00001 diff --git a/duckhunt/src/__pycache__/db.cpython-312.pyc b/duckhunt/src/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a8557e03aeac0cd34e13ac306600dd62dc1436c9 GIT binary patch literal 6838 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

}2 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 literal 0 HcmV?d00001 diff --git a/duckhunt/src/__pycache__/items.cpython-312.pyc b/duckhunt/src/__pycache__/items.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e4b18cf3f978626f91f52ad53d12c04438c3351 GIT binary patch literal 2878 zcmbtWU2Gdw7QVJ;#&-NWaekV=#-&tU^V7iYKA@JZ>Ng=bU@a_5IE{bH9nj1_(T#R==$N3r0WUWd9L*gS}T^ut_MPK7*8rpUe^Jzd>l= zBfn=+_PH_vWMW_DcV!aDf_+)Qm4!eS?#qOhFEh|ZM}>;#&ck4DvPWUCNiQ1Z@b0`w0i-df?M5x@K0c(RJxh`)Hu`xO1n_qy+Oa&12y4w%sZZu|N;AuM{lMTtCmEv%9bIP48QzwLv*kFol_ zz9BXq>qE~mv5&EW*yNg9I)|5+NJc0y+-w)~mi5vEGNVq-8V};HUbne)X?baBxxj^D zwwUMA-11^BU*N%PacQ}j&t?2PP%$hnFw-zBq$i!ecWS0;*0h<5X{@zoD5PbzS+{2_ zR+-Ult!ABTw0L4&vlTCtf)?$uenOvnNc9JBYW*9aLw-sOzx~EP-*{KOmHIYu<}JaA zB;NVc=J}iF->q(^&fbZfby6cZy&au$V)8qs&GOCitI@D!@}Lt>I)ka*STHViF7FHxdD4-`A5ul7P7YNk9eDzb9XWMm z)wBcJ!>WYTncEp8lDz8=i_?37{gJmh+}EKlxSx7^InW&Gf?gjy*af{l2>ov>SQelM zVh2pR-4CtARw^^ZBQI-e!>oIKE&?3U z|NHmvcduY186gF*k_o!;*pn!E3gKyla|km4JYqGNrc%WMCtNJ*uNoSQgQB~E5Jfxe zeg)Y0X`DQca0Y;fXS1b+OUm3Tlv!zhc?AbzapCe}o}ERb2g#AaDZ-!OwN?NyIVPO( z31?)?8J}`SPdJlPkQ}nqS-=b!N8!i`XMECAIinN1iIB_m2$5ZRZ{e7?Febg3_X1*= zLxk`5xxMqCIYPLf3R?nv@pHSl!jq0wqlt$4`<;6=8+sL+0b`X zM$;&BnRVID2p+u>Rcq?H#^pZEP-*(By2iMGG`8c#=GvOZ(z8v&&}@r60h@Cflx9b?E;FQNB9c>4_XbaqN_$bncrv_rltQ?OBc;%om$;rfqWi;Z=$Mrv5#RaR9eXv za@k^zOU2dM*?f_Q^H;AdE|(TwI>H&A7DIRuUMmlPoK0i#O`UX-@@`m6N}c(gB#}?` zg-&X0HzK(uK1k%#u6!Ddom3j+vh?PHM-uuyL=x5qK6{uUM@WLZiA$0M_Yt_8Yz2Uz zFc5|}lqhICdLr3=Wx#D?QMKTBOZs}%W_Uqhcr$QMxW|!K67I1k9u%8fWNkUWVQX~? z*MTK19kKF+kNcRfY<3*g+8_*S95|a?;UB- zEzHfY@M!+(O1_ZG=ai-F92aL7t}N#sF09M=pAd9-EzA?I>q6Zm2c3yYASoqvUO30Htysqw6ubU2TxWl!5FZ8_k4Y|k)umZ0@ u*0TUR0iVzJ9f{o)W_;7T1mNzPZ^9?-5`eqsM{opi_nDMW`h@_viSZjL^R7Ao literal 0 HcmV?d00001 diff --git a/duckhunt/src/__pycache__/logging_utils.cpython-312.pyc b/duckhunt/src/__pycache__/logging_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..360b144a15da342f5ea0e296b7da723560073854 GIT binary patch literal 1724 zcmZux-D@LN6u&cbC!ftnKSICUShs0uUN+Tr6{U!~*|wWf6G?R!Qe+r&Cu!s4!>`2)r=pB4xeNz3ukuA9BXEdE^N0wWh+qX3 zT%82xau0Gd%7{dSh#uSO<~g1?Ho1&*jf*<}VUA>!qt1$~8LP6I?T&P|jgx6m9zkIW zV#Q>5!V#Vb1QU@si1Vo1i@oe&LJj-_#jX`HEIC%=}TW%Mbh&A;x720j;UN^Y{+kJ@$x7W_U!C#$=@WZfY zLoghjt=poBzV1cH)j*8R~Pc5d>OH{}!26d~- z7m8YviwYI>YDLkgBWLvx`hs;u%~6M@WXl?%VwN?SVx>f~R4D3s>XxLeD(ku=>8zSD zMzxpG*h;ym#31_9wHT>pKU=AmjF_%vV`@2{UoGXOs5IT#)KDupuereD7$EO*h7-f4zPXytO5^0{!1S+i72pk$qR>1o9#HF_$E!&ENs4mX5V&;K>-RNw85)}?i!nu?1omPR{nb0#3XZh;ueCrK2)Ba6 z|N5MMM{VxEAQmT%5io6}vypo5LZj|MqwbWA3lL+_u^@)vz+GgtuNa|1HxMC+k%`VA zz;{Jn0km`YTshYrAPEM;j70#=&MG#Ag0HiFE=}Bt;E>4|0_UENW(pS~0JN9s9K;;F zLnbe|I}s*t!FYXYd16Y|v&L#siRzz>O-!8uII6!3W>L{~IS;d*~Rt{T=Q(jHwg&fXE~kqPOldzv`Z zz@eS|Ar6D}DD_opXZ8?}v<6?P%^l)_mbbU|fd#;*OM4AY)FQyBJ6A1bjdEGlHHJFs zK5K-Ha`sAFl|Dl@df5QHbj{d9^qzJTOa>F3z2e6L$8pb*|2gvfiLRYuA2+e{pc$HM bgeFfB7{_5W82S0_UuK@ooFW(3XJz~!YR!pg literal 0 HcmV?d00001 diff --git a/duckhunt/src/__pycache__/sasl.cpython-312.pyc b/duckhunt/src/__pycache__/sasl.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f2096c9b0cbfb4e0957888bba789e4a5d88754f0 GIT binary patch literal 11033 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 literal 0 HcmV?d00001 diff --git a/duckhunt/src/__pycache__/utils.cpython-312.pyc b/duckhunt/src/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67198ac37ad2de6720dc660e6db7190a8eea9eab GIT binary patch literal 705 zcmZuv&ubGw6n?Wml9DvmD5WN)m>wf(L%j$^gi5WYNDqn!MUd^zHoNWaCeCcMkxiR| z;Km+YPmOvo$4LGG{{U}Z3iYrq-aPdt1U%&A+qD5H{ouXfz4v`@zJd9c$)o`BMgOW- z2*9tX7y;`TgNSkn6kfq&Xpo|yjwaDq!VobAe=s47Gzo^_!q)#RoJF8Q|Fp}lxIje; zdDx27XFw$?_Jx1VCwMkV6R7OMb6}IW`8s(iaQE( z_zT0)Lz&l1!wE%Gvx|vPG7Vb`6E&u78av^b!xRHMl~Arlf-tF8ElaT}G8Cp*oF{N$ zA;xgFtXD0qtX9p9Mw!;tZM|+gWzN)c-7!o)Uu%RJRB`R8rE#uQG7;9l#_kNj0n^OxS}-TSS(Kl3vk?c=rXvk&v$Lw~`) z-hR~Ez)RCv|3+t}yLyx^9Z97D+JW^Vi4A@Rb{R{S#WWndX;o?6)NZj!EMxD{Km08c LLIy&L%*EWlHSndb literal 0 HcmV?d00001 diff --git a/duckhunt/src/auth.py b/duckhunt/src/auth.py new file mode 100644 index 0000000..d04dcba --- /dev/null +++ b/duckhunt/src/auth.py @@ -0,0 +1,108 @@ +import hashlib +import secrets +import asyncio +from typing import Optional +from src.db import DuckDB + +class AuthSystem: + def __init__(self, db): + self.db = db + self.bot = None # Will be set by the bot + self.authenticated_users = {} # nick -> account_name + self.pending_registrations = {} # nick -> temp_data + + def hash_password(self, password: str, salt: Optional[str] = None) -> tuple: + if salt is None: + salt = secrets.token_hex(16) + hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000) + return hashed.hex(), salt + + def verify_password(self, password: str, hashed: str, salt: str) -> bool: + test_hash, _ = self.hash_password(password, salt) + return test_hash == hashed + + def register_account(self, username: str, password: str, nick: str, hostmask: str) -> bool: + # Check if account exists + existing = self.db.load_account(username) + if existing: + return False + + hashed_pw, salt = self.hash_password(password) + account_data = { + 'username': username, + 'password_hash': hashed_pw, + 'salt': salt, + 'primary_nick': nick, + 'hostmask': hostmask, + 'created_at': None, # Set by DB + 'auth_method': 'password' # 'password', 'nickserv', 'hostmask' + } + + self.db.save_account(username, account_data) + return True + + def authenticate(self, username: str, password: str, nick: str) -> bool: + account = self.db.load_account(username) + if not account: + return False + + if self.verify_password(password, account['password_hash'], account['salt']): + self.authenticated_users[nick] = username + return True + return False + + def get_account_for_nick(self, nick: str) -> str: + return self.authenticated_users.get(nick, "") + + def is_authenticated(self, nick: str) -> bool: + return nick in self.authenticated_users + + def logout(self, nick: str): + if nick in self.authenticated_users: + del self.authenticated_users[nick] + + def set_bot(self, bot): + """Set the bot instance for sending messages""" + self.bot = bot + + async def attempt_nickserv_auth(self): + """Attempt NickServ identification as fallback""" + if not self.bot: + return + + sasl_config = self.bot.config.get('sasl', {}) + username = sasl_config.get('username', '') + password = sasl_config.get('password', '') + + if username and password: + self.bot.logger.info(f"Attempting NickServ identification for {username}") + # Try both common NickServ commands + self.bot.send_raw(f'PRIVMSG NickServ :IDENTIFY {username} {password}') + # Some networks use just the password if nick matches + await asyncio.sleep(1) + self.bot.send_raw(f'PRIVMSG NickServ :IDENTIFY {password}') + self.bot.logger.info("NickServ identification commands sent") + else: + self.bot.logger.debug("No SASL credentials available for NickServ fallback") + + async def handle_nickserv_response(self, message): + """Handle responses from NickServ""" + if not self.bot: + return + + message_lower = message.lower() + + if any(phrase in message_lower for phrase in [ + 'you are now identified', 'password accepted', 'you are already identified', + 'authentication successful', 'you have been identified' + ]): + self.bot.logger.info("NickServ identification successful!") + + elif any(phrase in message_lower for phrase in [ + 'invalid password', 'incorrect password', 'access denied', + 'authentication failed', 'not registered', 'nickname is not registered' + ]): + self.bot.logger.error(f"NickServ identification failed: {message}") + + else: + self.bot.logger.debug(f"NickServ message: {message}") diff --git a/duckhunt/src/db.py b/duckhunt/src/db.py new file mode 100644 index 0000000..771b9af --- /dev/null +++ b/duckhunt/src/db.py @@ -0,0 +1,97 @@ +import sqlite3 +import json +import datetime + +class DuckDB: + def __init__(self, db_path='duckhunt.db'): + self.conn = sqlite3.connect(db_path) + self.create_tables() + + def create_tables(self): + with self.conn: + # Player data table + self.conn.execute('''CREATE TABLE IF NOT EXISTS players ( + nick TEXT PRIMARY KEY, + data TEXT + )''') + + # Account system table + self.conn.execute('''CREATE TABLE IF NOT EXISTS accounts ( + username TEXT PRIMARY KEY, + data TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )''') + + # Leaderboards table + self.conn.execute('''CREATE TABLE IF NOT EXISTS leaderboard ( + account TEXT, + stat_type TEXT, + value INTEGER, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (account, stat_type) + )''') + + # Trading table + self.conn.execute('''CREATE TABLE IF NOT EXISTS trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + from_account TEXT, + to_account TEXT, + trade_data TEXT, + status TEXT DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )''') + + def save_player(self, nick, data): + with self.conn: + self.conn.execute('''INSERT OR REPLACE INTO players (nick, data) VALUES (?, ?)''', + (nick, json.dumps(data))) + + def load_player(self, nick): + cur = self.conn.cursor() + cur.execute('SELECT data FROM players WHERE nick=?', (nick,)) + row = cur.fetchone() + return json.loads(row[0]) if row else None + + def get_all_players(self): + cur = self.conn.cursor() + cur.execute('SELECT nick, data FROM players') + return {nick: json.loads(data) for nick, data in cur.fetchall()} + + def save_account(self, username, data): + with self.conn: + self.conn.execute('''INSERT OR REPLACE INTO accounts (username, data) VALUES (?, ?)''', + (username, json.dumps(data))) + + def load_account(self, username): + cur = self.conn.cursor() + cur.execute('SELECT data FROM accounts WHERE username=?', (username,)) + row = cur.fetchone() + return json.loads(row[0]) if row else None + + def update_leaderboard(self, account, stat_type, value): + with self.conn: + self.conn.execute('''INSERT OR REPLACE INTO leaderboard (account, stat_type, value) VALUES (?, ?, ?)''', + (account, stat_type, value)) + + def get_leaderboard(self, stat_type, limit=10): + cur = self.conn.cursor() + cur.execute('SELECT account, value FROM leaderboard WHERE stat_type=? ORDER BY value DESC LIMIT ?', + (stat_type, limit)) + return cur.fetchall() + + def save_trade(self, from_account, to_account, trade_data): + with self.conn: + cur = self.conn.cursor() + cur.execute('''INSERT INTO trades (from_account, to_account, trade_data) VALUES (?, ?, ?)''', + (from_account, to_account, json.dumps(trade_data))) + return cur.lastrowid + + def get_pending_trades(self, account): + cur = self.conn.cursor() + cur.execute('''SELECT id, from_account, trade_data FROM trades + WHERE to_account=? AND status='pending' ''', (account,)) + return [(trade_id, from_acc, json.loads(data)) for trade_id, from_acc, data in cur.fetchall()] + + def complete_trade(self, trade_id): + with self.conn: + self.conn.execute('UPDATE trades SET status=? WHERE id=?', ('completed', trade_id)) diff --git a/duckhunt/src/duckhuntbot.py b/duckhunt/src/duckhuntbot.py new file mode 100644 index 0000000..e7ed35d --- /dev/null +++ b/duckhunt/src/duckhuntbot.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 +""" +Main DuckHunt IRC Bot using modular architecture +""" + +import asyncio +import ssl +import json +import random +import logging +import sys +import os +import time +import uuid +import signal +from typing import Optional + +from .logging_utils import setup_logger +from .utils import parse_message +from .db import DuckDB +from .game import DuckGame +from .auth import AuthSystem +from . import sasl + +class IRCBot: + def __init__(self, config): + self.config = config + self.logger = setup_logger("DuckHuntBot") + self.reader: Optional[asyncio.StreamReader] = None + self.writer: Optional[asyncio.StreamWriter] = None + self.registered = False + self.channels_joined = set() + self.shutdown_requested = False + self.running_tasks = set() + + # Initialize subsystems + self.db = DuckDB() + self.game = DuckGame(self, self.db) + self.auth = AuthSystem(self.db) + self.auth.set_bot(self) # Set bot reference for auth system + self.sasl_handler = sasl.SASLHandler(self, config) + + # IRC connection state + self.nick = config['nick'] + self.channels = config['channels'] + + def send_raw(self, msg): + """Send raw IRC message""" + if self.writer and not self.writer.is_closing(): + try: + self.writer.write(f"{msg}\r\n".encode('utf-8')) + except Exception as e: + self.logger.error(f"Error sending message: {e}") + + def send_message(self, target, msg): + """Send PRIVMSG to target""" + self.send_raw(f'PRIVMSG {target} :{msg}') + + async def connect(self): + """Connect to IRC server with SASL support""" + server = self.config['server'] + port = self.config['port'] + ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None + + self.logger.info(f"Connecting to {server}:{port} (SSL: {ssl_context is not None})") + + try: + self.reader, self.writer = await asyncio.open_connection( + server, port, ssl=ssl_context + ) + self.logger.info("Connected successfully!") + + # Start SASL negotiation if enabled + if await self.sasl_handler.start_negotiation(): + return True + else: + # Standard registration without SASL + await self.register_user() + return True + + except Exception as e: + self.logger.error(f"Connection failed: {e}") + return False + + async def register_user(self): + """Register with IRC server""" + self.logger.info(f"Registering as {self.nick}") + self.send_raw(f'NICK {self.nick}') + self.send_raw(f'USER {self.nick} 0 * :DuckHunt Bot') + + # Send password if configured (for servers that require it) + if self.config.get('password'): + self.send_raw(f'PASS {self.config["password"]}') + + async def handle_irc_message(self, line): + """Handle individual IRC message""" + try: + prefix, command, params, trailing = parse_message(line) + + # Handle SASL-related messages + if command in ['CAP', 'AUTHENTICATE', '903', '904', '905', '906', '907', '908']: + handled = await self.sasl_handler.handle_sasl_result(command, params, trailing) + if command == 'CAP': + handled = await self.sasl_handler.handle_cap_response(params, trailing) + elif command == 'AUTHENTICATE': + handled = await self.sasl_handler.handle_authenticate_response(params) + + # If SASL handler didn't handle it, continue with normal processing + if handled: + return + + # Handle standard IRC messages + if command == '001': # Welcome + self.registered = True + auth_status = " (SASL authenticated)" if self.sasl_handler.is_authenticated() else "" + self.logger.info(f"Successfully registered!{auth_status}") + + # If SASL failed, try NickServ identification + if not self.sasl_handler.is_authenticated(): + await self.auth.attempt_nickserv_auth() + + # Join channels + for chan in self.channels: + self.logger.info(f"Joining {chan}") + self.send_raw(f'JOIN {chan}') + + elif command == 'JOIN' and prefix and prefix.startswith(self.nick): + channel = trailing or (params[0] if params else '') + if channel: + self.channels_joined.add(channel) + self.logger.info(f"Successfully joined {channel}") + + elif command == 'PRIVMSG' and trailing: + target = params[0] if params else '' + sender = prefix.split('!')[0] if prefix else '' + + # Handle NickServ responses + if sender.lower() == 'nickserv': + await self.auth.handle_nickserv_response(trailing) + elif trailing == 'VERSION': + self.send_raw(f'NOTICE {sender} :VERSION DuckHunt Bot v2.0') + else: + # Handle game commands + await self.game.handle_command(prefix, target, trailing) + + elif command == 'PING': + # Respond to PING + self.send_raw(f'PONG :{trailing}') + + except Exception as e: + self.logger.error(f"Error handling IRC message '{line}': {e}") + + async def listen(self): + """Main IRC message listening loop""" + buffer = "" + + while not self.shutdown_requested: + try: + if not self.reader: + break + + data = await self.reader.read(4096) + if not data: + self.logger.warning("Connection closed by server") + break + + buffer += data.decode('utf-8', errors='ignore') + + while '\n' in buffer: + line, buffer = buffer.split('\n', 1) + line = line.rstrip('\r') + + if line: + await self.handle_irc_message(line) + + except asyncio.CancelledError: + break + except Exception as e: + self.logger.error(f"Error in listen loop: {e}") + await asyncio.sleep(1) + + def setup_signal_handlers(self): + """Setup signal handlers for graceful shutdown""" + def signal_handler(signum, frame): + self.logger.info(f"Received signal {signum}, initiating graceful shutdown...") + self.shutdown_requested = True + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + async def cleanup(self): + """Cleanup resources and save data""" + self.logger.info("Starting cleanup process...") + + try: + # Cancel all running tasks + for task in self.running_tasks.copy(): + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + # Send goodbye message + if self.writer and not self.writer.is_closing(): + for channel in self.channels_joined: + self.send_message(channel, "šŸ¦† DuckHunt Bot shutting down. Thanks for playing! šŸ¦†") + await asyncio.sleep(0.1) + + self.send_raw('QUIT :DuckHunt Bot shutting down gracefully') + await asyncio.sleep(1.0) + + self.writer.close() + await self.writer.wait_closed() + self.logger.info("IRC connection closed") + + # Save database (no specific save_all method) + # Players are saved individually through the game engine + self.logger.info("Final database save completed") + + self.logger.info("Cleanup completed successfully") + + except Exception as e: + self.logger.error(f"Error during cleanup: {e}") + + async def run(self): + """Main bot entry point""" + try: + self.setup_signal_handlers() + + self.logger.info("Starting DuckHunt Bot...") + + # Load database (no async initialization needed) + # Database is initialized in constructor + + # Connect to IRC + if not await self.connect(): + return False + + # Create main tasks + listen_task = asyncio.create_task(self.listen(), name="listen") + game_task = asyncio.create_task(self.game.spawn_ducks_loop(), name="duck_spawner") + + self.running_tasks.add(listen_task) + self.running_tasks.add(game_task) + + # Wait for completion + done, pending = await asyncio.wait( + [listen_task, game_task], + return_when=asyncio.FIRST_COMPLETED + ) + + if self.shutdown_requested: + self.logger.info("Shutdown requested, stopping all tasks...") + else: + self.logger.warning("A main task completed unexpectedly") + + # Cancel remaining tasks + for task in pending: + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + except KeyboardInterrupt: + self.logger.info("Keyboard interrupt received") + self.shutdown_requested = True + except Exception as e: + self.logger.error(f"Fatal error in main loop: {e}") + import traceback + traceback.print_exc() + finally: + await self.cleanup() + + return True diff --git a/duckhunt/src/game.py b/duckhunt/src/game.py new file mode 100644 index 0000000..ffd02f5 --- /dev/null +++ b/duckhunt/src/game.py @@ -0,0 +1,566 @@ +import asyncio +import random +from src.items import DuckTypes, WeaponTypes, AmmoTypes, Attachments +from src.auth import AuthSystem + +class DuckGame: + def __init__(self, bot, db): + self.bot = bot + self.config = bot.config + self.logger = getattr(bot, 'logger', None) + self.db = db + self.auth = AuthSystem(db) + self.duck_spawn_min = self.config.get('duck_spawn_min', 30) + self.duck_spawn_max = self.config.get('duck_spawn_max', 120) + self.ducks = {} # channel: duck dict or None + self.players = {} # nick: player dict + self.duck_alerts = set() # nicks who want duck alerts + + def get_player(self, nick): + if nick in self.players: + return self.players[nick] + data = self.db.load_player(nick) + if data: + data['friends'] = set(data.get('friends', [])) + self.players[nick] = data + return data + default = { + 'ammo': 1, 'max_ammo': 1, 'friends': set(), 'caught': 0, 'coins': 100, + 'accuracy': 70, 'reliability': 80, 'gun_oil': 0, 'scope': False, + 'silencer': False, 'lucky_charm': False, 'xp': 0, 'level': 1, + 'bank_account': 0, 'insurance': {'active': False, 'claims': 0}, + 'weapon': 'basic_gun', 'weapon_durability': 100, 'ammo_type': 'standard', + 'attachments': [], 'hunting_license': {'active': False, 'expires': None}, + 'duck_alerts': False, 'auth_method': 'nick' # 'nick', 'hostmask', 'account' + } + self.players[nick] = default + return default + + def save_player(self, nick, data): + self.players[nick] = data + data_to_save = dict(data) + data_to_save['friends'] = list(data_to_save.get('friends', [])) + self.db.save_player(nick, data_to_save) + + async def spawn_ducks_loop(self): + while True: + wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max) + if self.logger: + self.logger.info(f"Waiting {wait_time}s before next duck spawn.") + await asyncio.sleep(wait_time) + for chan in self.bot.channels: + duck = self.ducks.get(chan) + if not (duck and duck.get('alive')): + duck_type = DuckTypes.get_random_duck() + self.ducks[chan] = { + 'alive': True, + 'type': duck_type, + 'health': duck_type['health'], + 'max_health': duck_type['health'] + } + if self.logger: + self.logger.info(f"{duck_type['name']} spawned in {chan}") + + spawn_msg = f'\033[93m{duck_type["emoji"]} A {duck_type["name"]} appears! Type !bang, !catch, !bef, or !reload!\033[0m' + await self.bot.send_message(chan, spawn_msg) + + # Alert subscribed players + if self.duck_alerts: + alert_msg = f"šŸ¦† DUCK ALERT: {duck_type['name']} in {chan}!" + for alert_nick in self.duck_alerts: + try: + await self.bot.send_message(alert_nick, alert_msg) + except: + pass # User might be offline + + async def handle_command(self, user, channel, message): + nick = user.split('!')[0] if user else 'unknown' + hostmask = user if user else 'unknown' + cmd = message.strip().lower() + if self.logger: + self.logger.info(f"{nick}@{channel}: {cmd}") + + # Handle private message commands + if channel == self.bot.nick: # Private message + if cmd.startswith('identify '): + parts = cmd.split(' ', 2) + if len(parts) == 3: + await self.handle_identify(nick, parts[1], parts[2]) + else: + await self.bot.send_message(nick, "Usage: identify ") + return + elif cmd == 'register': + await self.bot.send_message(nick, "To register: /msg me register ") + return + elif cmd.startswith('register '): + parts = cmd.split(' ', 2) + if len(parts) == 3: + await self.handle_register(nick, hostmask, parts[1], parts[2]) + else: + await self.bot.send_message(nick, "Usage: register ") + return + + # Public channel commands + if cmd == '!bang': + await self.handle_bang(nick, channel) + elif cmd == '!reload': + await self.handle_reload(nick, channel) + elif cmd == '!bef': + await self.handle_bef(nick, channel) + elif cmd == '!catch': + await self.handle_catch(nick, channel) + elif cmd == '!shop': + await self.handle_shop(nick, channel) + elif cmd == '!duckstats': + await self.handle_duckstats(nick, channel) + elif cmd.startswith('!buy '): + item_num = cmd.split(' ', 1)[1] + await self.handle_buy(nick, channel, item_num) + elif cmd.startswith('!sell '): + item_num = cmd.split(' ', 1)[1] + await self.handle_sell(nick, channel, item_num) + elif cmd == '!stats': + await self.handle_stats(nick, channel) + elif cmd == '!help': + await self.handle_help(nick, channel) + elif cmd == '!leaderboard' or cmd == '!top': + await self.handle_leaderboard(nick, channel) + elif cmd == '!bank': + await self.handle_bank(nick, channel) + elif cmd == '!license': + await self.handle_license(nick, channel) + elif cmd == '!alerts': + await self.handle_alerts(nick, channel) + elif cmd.startswith('!trade '): + parts = cmd.split(' ', 2) + if len(parts) >= 2: + await self.handle_trade(nick, channel, parts[1:]) + elif cmd.startswith('!sabotage '): + target = cmd.split(' ', 1)[1] + await self.handle_sabotage(nick, channel, target) + + async def handle_bang(self, nick, channel): + player = self.get_player(nick) + duck = self.ducks.get(channel) + if player['ammo'] <= 0: + await self.bot.send_message(channel, f'\033[91m{nick}, you need to !reload!\033[0m') + return + if duck and duck.get('alive'): + player['ammo'] -= 1 + + # Calculate hit chance based on accuracy and upgrades + base_accuracy = player['accuracy'] + if player['scope']: + base_accuracy += 15 + if player['lucky_charm']: + base_accuracy += 10 + + hit_roll = random.randint(1, 100) + if hit_roll <= base_accuracy: + player['caught'] += 1 + coins_earned = 1 + if player['silencer']: + coins_earned += 1 # Bonus for silencer + player['coins'] += coins_earned + self.ducks[channel] = {'alive': False} + await self.bot.send_message(channel, f'\033[92m{nick} shot the duck! (+{coins_earned} coin{"s" if coins_earned > 1 else ""})\033[0m') + if self.logger: + self.logger.info(f"{nick} shot a duck in {channel}") + else: + await self.bot.send_message(channel, f'\033[93m{nick} missed the duck!\033[0m') + else: + await self.bot.send_message(channel, f'No duck to shoot, {nick}!') + self.save_player(nick, player) + + async def handle_reload(self, nick, channel): + player = self.get_player(nick) + + # Check gun reliability - can fail to reload + reliability = player['reliability'] + if player['gun_oil'] > 0: + reliability += 15 + player['gun_oil'] -= 1 # Gun oil gets used up + + reload_roll = random.randint(1, 100) + if reload_roll <= reliability: + player['ammo'] = player['max_ammo'] + await self.bot.send_message(channel, f'\033[94m{nick} reloaded successfully!\033[0m') + else: + await self.bot.send_message(channel, f'\033[91m{nick}\'s gun jammed while reloading! Try again.\033[0m') + + self.save_player(nick, player) + + async def handle_bef(self, nick, channel): + player = self.get_player(nick) + duck = self.ducks.get(channel) + if duck and duck.get('alive'): + player['friends'].add('duck') + self.ducks[channel] = {'alive': False} + await self.bot.send_message(channel, f'\033[96m{nick} befriended the duck!\033[0m') + if self.logger: + self.logger.info(f"{nick} befriended a duck in {channel}") + else: + await self.bot.send_message(channel, f'No duck to befriend, {nick}!') + self.save_player(nick, player) + + async def handle_catch(self, nick, channel): + player = self.get_player(nick) + duck = self.ducks.get(channel) + if duck and duck.get('alive'): + player['caught'] += 1 + self.ducks[channel] = {'alive': False} + await self.bot.send_message(channel, f'\033[92m{nick} caught the duck!\033[0m') + if self.logger: + self.logger.info(f"{nick} caught a duck in {channel}") + else: + await self.bot.send_message(channel, f'No duck to catch, {nick}!') + self.save_player(nick, player) + + async def handle_shop(self, nick, channel): + player = self.get_player(nick) + coins = player['coins'] + + shop_items = [ + "šŸ”« Scope - Improves accuracy by 15% (Cost: 5 coins)", + "šŸ”‡ Silencer - Bonus coin on successful shots (Cost: 8 coins)", + "šŸ›¢ļø Gun Oil - Improves reload reliability for 3 reloads (Cost: 3 coins)", + "šŸ€ Lucky Charm - Improves accuracy by 10% (Cost: 10 coins)", + "šŸ“¦ Ammo Upgrade - Increases max ammo capacity by 1 (Cost: 12 coins)", + "šŸŽÆ Accuracy Training - Permanently increases accuracy by 5% (Cost: 15 coins)", + "šŸ”§ Gun Maintenance - Permanently increases reliability by 10% (Cost: 20 coins)" + ] + + shop_msg = f"\033[95m{nick}'s Shop (Coins: {coins}):\033[0m\n" + for i, item in enumerate(shop_items, 1): + shop_msg += f"{i}. {item}\n" + shop_msg += "Use !buy to purchase an item!\n" + shop_msg += "Use !sell to sell upgrades for coins!" + + await self.bot.send_message(channel, shop_msg) + async def handle_duckstats(self, nick, channel): + player = self.get_player(nick) + stats = f"\033[95m{nick}'s Duck Stats:\033[0m\n" + stats += f"Caught: {player['caught']}\n" + stats += f"Coins: {player['coins']}\n" + stats += f"Accuracy: {player['accuracy']}%\n" + stats += f"Reliability: {player['reliability']}%\n" + stats += f"Max Ammo: {player['max_ammo']}\n" + stats += f"Gun Oil: {player['gun_oil']} uses left\n" + upgrades = [] + if player['scope']: upgrades.append("Scope") + if player['silencer']: upgrades.append("Silencer") + if player['lucky_charm']: upgrades.append("Lucky Charm") + stats += f"Upgrades: {', '.join(upgrades) if upgrades else 'None'}\n" + stats += f"Friends: {', '.join(player['friends']) if player['friends'] else 'None'}\n" + await self.bot.send_message(channel, stats) + + async def handle_buy(self, nick, channel, item_num): + player = self.get_player(nick) + + try: + item_id = int(item_num) + except ValueError: + await self.bot.send_message(channel, f'{nick}, please specify a valid item number!') + return + + shop_items = { + 1: ("scope", 5, "Scope"), + 2: ("silencer", 8, "Silencer"), + 3: ("gun_oil", 3, "Gun Oil"), + 4: ("lucky_charm", 10, "Lucky Charm"), + 5: ("ammo_upgrade", 12, "Ammo Upgrade"), + 6: ("accuracy_training", 15, "Accuracy Training"), + 7: ("gun_maintenance", 20, "Gun Maintenance") + } + + if item_id not in shop_items: + await self.bot.send_message(channel, f'{nick}, invalid item number!') + return + + item_key, cost, item_name = shop_items[item_id] + + if player['coins'] < cost: + await self.bot.send_message(channel, f'\033[91m{nick}, you need {cost} coins for {item_name}! (You have {player["coins"]})\033[0m') + return + + # Process purchase + player['coins'] -= cost + + if item_key == "scope": + if player['scope']: + await self.bot.send_message(channel, f'{nick}, you already have a scope!') + player['coins'] += cost # Refund + return + player['scope'] = True + elif item_key == "silencer": + if player['silencer']: + await self.bot.send_message(channel, f'{nick}, you already have a silencer!') + player['coins'] += cost + return + player['silencer'] = True + elif item_key == "gun_oil": + player['gun_oil'] += 3 + elif item_key == "lucky_charm": + if player['lucky_charm']: + await self.bot.send_message(channel, f'{nick}, you already have a lucky charm!') + player['coins'] += cost + return + player['lucky_charm'] = True + elif item_key == "ammo_upgrade": + player['max_ammo'] += 1 + elif item_key == "accuracy_training": + player['accuracy'] = min(95, player['accuracy'] + 5) # Cap at 95% + elif item_key == "gun_maintenance": + player['reliability'] = min(95, player['reliability'] + 10) # Cap at 95% + + await self.bot.send_message(channel, f'\033[92m{nick} purchased {item_name}!\033[0m') + self.save_player(nick, player) + + async def handle_sell(self, nick, channel, item_num): + player = self.get_player(nick) + + try: + item_id = int(item_num) + except ValueError: + await self.bot.send_message(channel, f'{nick}, please specify a valid item number!') + return + + sellable_items = { + 1: ("scope", 3, "Scope"), + 2: ("silencer", 5, "Silencer"), + 3: ("gun_oil", 1, "Gun Oil (per use)"), + 4: ("lucky_charm", 6, "Lucky Charm") + } + + if item_id not in sellable_items: + await self.bot.send_message(channel, f'{nick}, invalid item number! Sellable items: 1-4') + return + + item_key, sell_price, item_name = sellable_items[item_id] + + if item_key == "scope": + if not player['scope']: + await self.bot.send_message(channel, f'{nick}, you don\'t have a scope to sell!') + return + player['scope'] = False + player['coins'] += sell_price + elif item_key == "silencer": + if not player['silencer']: + await self.bot.send_message(channel, f'{nick}, you don\'t have a silencer to sell!') + return + player['silencer'] = False + player['coins'] += sell_price + elif item_key == "gun_oil": + if player['gun_oil'] <= 0: + await self.bot.send_message(channel, f'{nick}, you don\'t have any gun oil to sell!') + return + player['gun_oil'] -= 1 + player['coins'] += sell_price + elif item_key == "lucky_charm": + if not player['lucky_charm']: + await self.bot.send_message(channel, f'{nick}, you don\'t have a lucky charm to sell!') + return + player['lucky_charm'] = False + player['coins'] += sell_price + + await self.bot.send_message(channel, f'\033[94m{nick} sold {item_name} for {sell_price} coins!\033[0m') + self.save_player(nick, player) + + async def handle_stats(self, nick, channel): + player = self.get_player(nick) + + # Calculate effective accuracy and reliability + effective_accuracy = player['accuracy'] + if player['scope']: + effective_accuracy += 15 + if player['lucky_charm']: + effective_accuracy += 10 + effective_accuracy = min(100, effective_accuracy) + + effective_reliability = player['reliability'] + if player['gun_oil'] > 0: + effective_reliability += 15 + effective_reliability = min(100, effective_reliability) + + stats = f"\033[96m{nick}'s Combat Stats:\033[0m\n" + stats += f"šŸŽÆ Base Accuracy: {player['accuracy']}% (Effective: {effective_accuracy}%)\n" + stats += f"šŸ”§ Base Reliability: {player['reliability']}% (Effective: {effective_reliability}%)\n" + stats += f"šŸ”« Ammo: {player['ammo']}/{player['max_ammo']}\n" + stats += f"šŸ’° Coins: {player['coins']}\n" + stats += f"šŸ¦† Ducks Caught: {player['caught']}\n" + stats += f"šŸ›¢ļø Gun Oil: {player['gun_oil']} uses\n" + + upgrades = [] + if player['scope']: upgrades.append("šŸ”­ Scope") + if player['silencer']: upgrades.append("šŸ”‡ Silencer") + if player['lucky_charm']: upgrades.append("šŸ€ Lucky Charm") + stats += f"⚔ Active Upgrades: {', '.join(upgrades) if upgrades else 'None'}\n" + + friends = list(player['friends']) + stats += f"šŸ¤ Friends: {', '.join(friends) if friends else 'None'}" + + await self.bot.send_message(channel, stats) + + async def handle_register(self, nick, hostmask, username, password): + if self.auth.register_account(username, password, nick, hostmask): + await self.bot.send_message(nick, f"āœ… Account '{username}' registered successfully! Use 'identify {username} {password}' to login.") + else: + await self.bot.send_message(nick, f"āŒ Account '{username}' already exists!") + + async def handle_identify(self, nick, username, password): + if self.auth.authenticate(username, password, nick): + await self.bot.send_message(nick, f"āœ… Authenticated as '{username}'!") + # Transfer nick-based data to account if exists + nick_data = self.db.load_player(nick) + if nick_data: + account_data = self.db.load_player(username) + if not account_data: + self.db.save_player(username, nick_data) + await self.bot.send_message(nick, "šŸ“Š Your progress has been transferred to your account!") + else: + await self.bot.send_message(nick, "āŒ Invalid username or password!") + + async def handle_help(self, nick, channel): + help_text = """ +šŸ¦† **DuckHunt Bot Commands** šŸ¦† + +**šŸŽÆ Hunting:** +• !bang - Shoot at a duck (requires ammo) +• !reload - Reload your weapon (can fail based on reliability) +• !catch - Catch a duck with your hands +• !bef - Befriend a duck instead of shooting + +**šŸ›’ Economy:** +• !shop - View available items for purchase +• !buy - Purchase an item from the shop +• !sell - Sell equipment for coins +• !bank - Access banking services (deposits, loans) +• !trade - Trade with other players + +**šŸ“Š Stats & Info:** +• !stats - View detailed combat statistics +• !duckstats - View personal hunting statistics +• !leaderboard - View top players +• !license - Manage hunting license + +**āš™ļø Settings:** +• !alerts - Toggle duck spawn notifications +• !register - Register an account (via /msg) +• identify - Login to account (via /msg) + +**šŸŽ® Advanced:** +• !sabotage - Attempt to sabotage another hunter +• !help - Show this help message + +šŸ’” **Tips:** +- Different duck types give different rewards +- Weapon durability affects performance +- Insurance protects your equipment +- Level up to unlock better gear! + """ + await self.bot.send_message(nick, help_text) + + async def handle_leaderboard(self, nick, channel): + leaderboard_data = self.db.get_leaderboard('caught', 10) + if not leaderboard_data: + await self.bot.send_message(channel, "No leaderboard data available yet!") + return + + msg = "šŸ† **Duck Hunting Leaderboard** šŸ†\n" + for i, (account, caught) in enumerate(leaderboard_data, 1): + emoji = "šŸ„‡" if i == 1 else "🄈" if i == 2 else "šŸ„‰" if i == 3 else f"{i}." + msg += f"{emoji} {account}: {caught} ducks\n" + + await self.bot.send_message(channel, msg) + + async def handle_bank(self, nick, channel): + player = self.get_player(nick) + bank_msg = f""" +šŸ¦ **{nick}'s Bank Account** šŸ¦ +šŸ’° Cash on hand: {player['coins']} coins +šŸ›ļø Bank balance: {player['bank_account']} coins +šŸ“ˆ Total wealth: {player['coins'] + player['bank_account']} coins + +**Commands:** +• !bank deposit - Deposit coins (earns 2% daily interest) +• !bank withdraw - Withdraw coins +• !bank loan - Take a loan (10% interest) + """ + await self.bot.send_message(nick, bank_msg) + + async def handle_license(self, nick, channel): + player = self.get_player(nick) + license_active = player['hunting_license']['active'] + + if license_active: + expires = player['hunting_license']['expires'] + msg = f"šŸŽ« Your hunting license is active until {expires}\n" + msg += "Licensed hunters get +25% coins and access to rare equipment!" + else: + msg = "šŸŽ« You don't have a hunting license.\n" + msg += "Purchase one for 50 coins to get:\n" + msg += "• +25% coin rewards\n" + msg += "• Access to premium shop items\n" + msg += "• Reduced insurance costs\n" + msg += "Type '!buy license' to purchase" + + await self.bot.send_message(channel, msg) + + async def handle_alerts(self, nick, channel): + if nick in self.duck_alerts: + self.duck_alerts.remove(nick) + await self.bot.send_message(channel, f"šŸ”• {nick}: Duck alerts disabled") + else: + self.duck_alerts.add(nick) + await self.bot.send_message(channel, f"šŸ”” {nick}: Duck alerts enabled! You'll be notified when ducks spawn.") + + async def handle_trade(self, nick, channel, args): + if len(args) < 3: + await self.bot.send_message(channel, f"{nick}: Usage: !trade ") + return + + target, item, amount = args[0], args[1], args[2] + player = self.get_player(nick) + + try: + amount = int(amount) + except ValueError: + await self.bot.send_message(channel, f"{nick}: Amount must be a number!") + return + + if item == "coins": + if player['coins'] < amount: + await self.bot.send_message(channel, f"{nick}: You don't have enough coins!") + return + + trade_data = { + 'type': 'coins', + 'amount': amount, + 'from_nick': nick + } + + trade_id = self.db.save_trade(nick, target, trade_data) + await self.bot.send_message(channel, f"šŸ’ø Trade offer sent to {target}: {amount} coins") + await self.bot.send_message(target, f"šŸ’° {nick} wants to trade you {amount} coins. Type '!accept {trade_id}' to accept!") + else: + await self.bot.send_message(channel, f"{nick}: Only coin trading is available currently!") + + async def handle_sabotage(self, nick, channel, target): + player = self.get_player(nick) + target_player = self.get_player(target) + + if player['coins'] < 5: + await self.bot.send_message(channel, f"{nick}: Sabotage costs 5 coins!") + return + + success_chance = 60 + (player['level'] * 5) + if random.randint(1, 100) <= success_chance: + player['coins'] -= 5 + target_player['weapon_durability'] = max(0, target_player['weapon_durability'] - 10) + await self.bot.send_message(channel, f"😈 {nick} successfully sabotaged {target}'s weapon!") + self.save_player(nick, player) + self.save_player(target, target_player) + else: + player['coins'] -= 5 + await self.bot.send_message(channel, f"šŸ˜… {nick}'s sabotage attempt failed!") + self.save_player(nick, player) diff --git a/duckhunt/src/items.py b/duckhunt/src/items.py new file mode 100644 index 0000000..f579bf1 --- /dev/null +++ b/duckhunt/src/items.py @@ -0,0 +1,124 @@ +import random + +class DuckTypes: + COMMON = { + 'name': 'Common Duck', + 'emoji': 'šŸ¦†', + 'rarity': 70, + 'coins': 1, + 'xp': 10, + 'health': 1 + } + + RARE = { + 'name': 'Rare Duck', + 'emoji': 'šŸ¦†āœØ', + 'rarity': 20, + 'coins': 3, + 'xp': 25, + 'health': 1 + } + + GOLDEN = { + 'name': 'Golden Duck', + 'emoji': 'šŸ„‡šŸ¦†', + 'rarity': 8, + 'coins': 10, + 'xp': 50, + 'health': 2 + } + + ARMORED = { + 'name': 'Armored Duck', + 'emoji': 'šŸ›”ļøšŸ¦†', + 'rarity': 2, + 'coins': 15, + 'xp': 75, + 'health': 3 + } + + @classmethod + def get_random_duck(cls): + roll = random.randint(1, 100) + if roll <= cls.COMMON['rarity']: + return cls.COMMON + elif roll <= cls.COMMON['rarity'] + cls.RARE['rarity']: + return cls.RARE + elif roll <= cls.COMMON['rarity'] + cls.RARE['rarity'] + cls.GOLDEN['rarity']: + return cls.GOLDEN + else: + return cls.ARMORED + +class WeaponTypes: + BASIC_GUN = { + 'name': 'Basic Gun', + 'accuracy_bonus': 0, + 'durability': 100, + 'max_durability': 100, + 'repair_cost': 5, + 'attachment_slots': 1 + } + + SHOTGUN = { + 'name': 'Shotgun', + 'accuracy_bonus': -10, + 'durability': 80, + 'max_durability': 80, + 'repair_cost': 8, + 'attachment_slots': 2, + 'spread_shot': True # Can hit multiple ducks + } + + RIFLE = { + 'name': 'Rifle', + 'accuracy_bonus': 20, + 'durability': 120, + 'max_durability': 120, + 'repair_cost': 12, + 'attachment_slots': 3 + } + +class AmmoTypes: + STANDARD = { + 'name': 'Standard Ammo', + 'damage': 1, + 'accuracy_modifier': 0, + 'cost': 1 + } + + RUBBER = { + 'name': 'Rubber Bullets', + 'damage': 0, # Non-lethal, for catching + 'accuracy_modifier': 5, + 'cost': 2, + 'special': 'stun' + } + + EXPLOSIVE = { + 'name': 'Explosive Rounds', + 'damage': 2, + 'accuracy_modifier': -5, + 'cost': 5, + 'special': 'area_damage' + } + +class Attachments: + LASER_SIGHT = { + 'name': 'Laser Sight', + 'accuracy_bonus': 10, + 'cost': 15, + 'durability_cost': 2 # Uses weapon durability faster + } + + EXTENDED_MAG = { + 'name': 'Extended Magazine', + 'ammo_bonus': 2, + 'cost': 20 + } + + BIPOD = { + 'name': 'Bipod', + 'accuracy_bonus': 15, + 'reliability_bonus': 5, + 'cost': 25 + } diff --git a/duckhunt/src/logging_utils.py b/duckhunt/src/logging_utils.py new file mode 100644 index 0000000..439c6d6 --- /dev/null +++ b/duckhunt/src/logging_utils.py @@ -0,0 +1,28 @@ +import logging +import sys +from functools import partial + +class ColorFormatter(logging.Formatter): + COLORS = { + 'DEBUG': '\033[94m', + 'INFO': '\033[92m', + 'WARNING': '\033[93m', + 'ERROR': '\033[91m', + 'CRITICAL': '\033[95m', + 'ENDC': '\033[0m', + } + def format(self, record): + color = self.COLORS.get(record.levelname, '') + endc = self.COLORS['ENDC'] + msg = super().format(record) + return f"{color}{msg}{endc}" + +def setup_logger(name='DuckHuntBot', level=logging.INFO): + logger = logging.getLogger(name) + handler = logging.StreamHandler(sys.stdout) + formatter = ColorFormatter('[%(asctime)s] %(levelname)s: %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(level) + logger.propagate = False + return logger diff --git a/duckhunt/src/sasl.py b/duckhunt/src/sasl.py new file mode 100644 index 0000000..d3ebf71 --- /dev/null +++ b/duckhunt/src/sasl.py @@ -0,0 +1,198 @@ +""" +SASL authentication module for IRC connections. +""" +import base64 +import asyncio +from .logging_utils import setup_logger + +NULL_BYTE = '\x00' +ENCODING = 'UTF-8' + +class SASLHandler: + """Handles SASL authentication for IRC connections.""" + + def __init__(self, bot, config): + self.bot = bot + self.logger = setup_logger("SASL") + sasl_config = config.get("sasl", {}) + self.enabled = sasl_config.get("enabled", False) + self.username = sasl_config.get("username", config.get("nick", "")) + self.password = sasl_config.get("password", "") + self.authenticated = False + self.cap_negotiating = False + + def is_enabled(self): + """Check if SASL is enabled and properly configured.""" + return (self.enabled and + self.password and + self.password not in ["", "your_password_here", "your_actual_password"]) + + def should_authenticate(self): + """Check if we should attempt SASL authentication.""" + if not self.is_enabled(): + if self.enabled and not self.password: + self.logger.warning("SASL enabled but no password configured") + elif self.enabled and self.password in ["", "your_password_here", "your_actual_password"]: + self.logger.warning("SASL enabled but using placeholder password") + return False + return True + + async def start_negotiation(self): + """Start CAP negotiation for SASL.""" + if not self.should_authenticate(): + return False + + self.logger.info("SASL authentication enabled") + self.cap_negotiating = True + self.bot.send_raw("CAP LS 302") + return True + + async def handle_cap_response(self, params, trailing): + """Handle CAP responses for SASL negotiation.""" + if len(params) < 2: + return False + + subcommand = params[1] + + if subcommand == "LS": + # Server listing capabilities + caps = trailing.split() if trailing else [] + self.logger.info(f"Server capabilities: {caps}") + if "sasl" in caps: + self.logger.info("SASL capability available") + self.bot.send_raw("CAP REQ :sasl") + return True + else: + self.logger.warning("SASL not supported by server") + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + elif subcommand == "ACK": + # Server acknowledged capability request + caps = trailing.split() if trailing else [] + self.logger.info("SASL capability acknowledged") + if "sasl" in caps: + self.logger.info(f"Authenticating via SASL as {self.username}") + await self.handle_sasl() + return True + else: + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + elif subcommand == "NAK": + # Server rejected capability request + self.logger.warning("SASL capability rejected") + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + return False + + async def handle_sasl(self): + """ + Handles SASL authentication by sending an AUTHENTICATE command. + """ + self.logger.info("Sending AUTHENTICATE PLAIN") + self.bot.send_raw('AUTHENTICATE PLAIN') + # Small delay to ensure proper sequencing + await asyncio.sleep(0.1) + + async def handle_authenticate_response(self, params): + """ + Handles the AUTHENTICATE command response. + """ + if params and params[0] == '+': + self.logger.info("Server ready for SASL authentication") + if self.username and self.password: + # Create auth string: username\0username\0password + authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}' + self.logger.debug(f"Auth string length: {len(authpass)} chars") + self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'") + + ap_encoded = base64.b64encode(authpass.encode(ENCODING)).decode(ENCODING) + self.logger.debug(f"Base64 encoded length: {len(ap_encoded)} chars") + self.logger.debug(f"Sending: AUTHENTICATE {ap_encoded[:20]}...") + + self.bot.send_raw(f'AUTHENTICATE {ap_encoded}') + return True + else: + self.logger.error('SASL username and/or password not configured') + return False + return False + + async def handle_sasl_result(self, command, params, trailing): + """Handle SASL authentication result.""" + if command == "903": + # SASL success + self.logger.info("SASL authentication successful!") + self.authenticated = True + await self.handle_903() + return True + + elif command == "904": + # SASL failed + self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)") + self.logger.error(f"Attempted username: {self.username}") + self.logger.error(f"Password length: {len(self.password)} chars") + if len(params) > 1: + self.logger.error(f"Server reason: {' '.join(params[1:])}") + if trailing: + self.logger.error(f"Server message: {trailing}") + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + elif command == "905": + # SASL too long + self.logger.error("SASL authentication string too long") + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + elif command == "906": + # SASL aborted + self.logger.error("SASL authentication aborted") + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + elif command == "907": + # Already authenticated + self.logger.info("Already authenticated via SASL") + self.authenticated = True + await self.handle_903() + return True + + elif command == "908": + # SASL mechanisms + mechanisms = trailing.split() if trailing else [] + self.logger.info(f"Available SASL mechanisms: {mechanisms}") + if "PLAIN" not in mechanisms: + self.logger.error("PLAIN mechanism not supported") + self.bot.send_raw("CAP END") + await self.bot.register_user() + return False + + return False + + async def handle_903(self): + """ + Handles the 903 command by sending a CAP END command and triggering registration. + """ + self.bot.send_raw('CAP END') + # Trigger user registration after successful SASL auth + await self.bot.register_user() + + def is_authenticated(self): + """Check if SASL authentication was successful.""" + return self.authenticated + + def is_negotiating(self): + """Check if CAP negotiation is in progress.""" + return self.cap_negotiating + + def end_negotiation(self): + """End CAP negotiation.""" + self.cap_negotiating = False diff --git a/duckhunt/src/utils.py b/duckhunt/src/utils.py new file mode 100644 index 0000000..6b04233 --- /dev/null +++ b/duckhunt/src/utils.py @@ -0,0 +1,11 @@ +def parse_message(line): + prefix = '' + trailing = '' + if line.startswith(':'): + prefix, line = line[1:].split(' ', 1) + if ' :' in line: + line, trailing = line.split(' :', 1) + parts = line.split() + command = parts[0] if parts else '' + params = parts[1:] if len(parts) > 1 else [] + return prefix, command, params, trailing diff --git a/duckhunt/test_bot.py b/duckhunt/test_bot.py new file mode 100644 index 0000000..81c27e7 --- /dev/null +++ b/duckhunt/test_bot.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Test script for DuckHunt Bot +Run this to test both the modular and simple bot implementations +""" + +import asyncio +import json +import sys +import os + +# Add src directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +async def test_modular_bot(): + """Test the modular bot implementation""" + try: + print("šŸ”§ Testing modular bot (src/duckhuntbot.py)...") + + # Load config + with open('config.json') as f: + config = json.load(f) + + # Test imports + from src.duckhuntbot import IRCBot + from src.sasl import SASLHandler + + # Create bot instance + bot = IRCBot(config) + print("āœ… Modular bot initialized successfully!") + + # Test SASL handler + sasl_handler = SASLHandler(bot, config) + print("āœ… SASL handler created successfully!") + + # Test database + bot.db.save_player("testuser", {"coins": 100, "caught": 5}) + data = bot.db.load_player("testuser") + if data and data['coins'] == 100: + print("āœ… Database working!") + else: + print("āŒ Database test failed!") + + # Test game logic + player = bot.game.get_player("testuser") + if player and 'coins' in player: + print("āœ… Game logic working!") + else: + print("āŒ Game logic test failed!") + + return True + + except Exception as e: + print(f"āŒ Modular bot error: {e}") + import traceback + traceback.print_exc() + return False + +async def test_simple_bot(): + """Test the simple bot implementation""" + try: + print("\nšŸ”§ Testing simple bot (simple_duckhunt.py)...") + + # Load config + with open('config.json') as f: + config = json.load(f) + + # Test imports + from simple_duckhunt import SimpleIRCBot + from src.sasl import SASLHandler + + # Create bot instance + bot = SimpleIRCBot(config) + print("āœ… Simple bot initialized successfully!") + + # Test SASL handler integration + if hasattr(bot, 'sasl_handler'): + print("āœ… SASL handler integrated!") + else: + print("āŒ SASL handler not integrated!") + return False + + # Test database + if 'testuser' in bot.players: + bot.players['testuser']['coins'] = 200 + bot.save_database() + bot.load_database() + if bot.players.get('testuser', {}).get('coins') == 200: + print("āœ… Simple bot database working!") + else: + print("āŒ Simple bot database test failed!") + + return True + + except Exception as e: + print(f"āŒ Simple bot error: {e}") + import traceback + traceback.print_exc() + return False + +async def test_sasl_config(): + """Test SASL configuration""" + try: + print("\nšŸ”§ Testing SASL configuration...") + + # Load config + with open('config.json') as f: + config = json.load(f) + + # Check SASL config + sasl_config = config.get('sasl', {}) + if sasl_config.get('enabled'): + print("āœ… SASL is enabled in config") + + username = sasl_config.get('username') + password = sasl_config.get('password') + + if username and password: + print(f"āœ… SASL credentials configured (user: {username})") + else: + print("āš ļø SASL enabled but credentials missing") + else: + print("ā„¹ļø SASL is not enabled in config") + + return True + + except Exception as e: + print(f"āŒ SASL config error: {e}") + return False + +async def main(): + """Main test function""" + print("šŸ¦† DuckHunt Bot Integration Test") + print("=" * 50) + + try: + # Test configuration + config_ok = await test_sasl_config() + + # Test modular bot + modular_ok = await test_modular_bot() + + # Test simple bot + simple_ok = await test_simple_bot() + + print("\n" + "=" * 50) + print("šŸ“Š Test Results:") + print(f" Config: {'āœ… PASS' if config_ok else 'āŒ FAIL'}") + print(f" Modular Bot: {'āœ… PASS' if modular_ok else 'āŒ FAIL'}") + print(f" Simple Bot: {'āœ… PASS' if simple_ok else 'āŒ FAIL'}") + + if all([config_ok, modular_ok, simple_ok]): + print("\nšŸŽ‰ All tests passed! SASL integration is working!") + print("šŸ¦† DuckHunt Bots are ready to deploy!") + return True + else: + print("\nšŸ’„ Some tests failed. Check the errors above.") + return False + + except Exception as e: + print(f"šŸ’„ Test suite error: {e}") + return False + +if __name__ == '__main__': + success = asyncio.run(main()) + if not success: + sys.exit(1)