From de64756b6d1c051ac33a120e9bd8a9ebaa117cb3 Mon Sep 17 00:00:00 2001 From: ComputerTech312 Date: Tue, 23 Sep 2025 02:57:28 +0100 Subject: [PATCH] Simplified DuckHunt bot with customizable messages and colors --- README.md | 156 - __pycache__/duckhunt.cpython-312.pyc | Bin 1756 -> 0 bytes backup/simple_duckhunt.py | 2839 ----------------- config.json | 245 +- duckhunt.json | 1101 +------ duckhunt.log | 146 + duckhunt.py | 17 +- messages.json | 54 + src/__pycache__/db.cpython-312.pyc | Bin 9704 -> 4444 bytes src/__pycache__/duckhuntbot.cpython-312.pyc | Bin 62464 -> 26762 bytes src/__pycache__/game.cpython-312.pyc | Bin 15808 -> 5341 bytes src/__pycache__/logging_utils.cpython-312.pyc | Bin 3271 -> 3279 bytes src/__pycache__/sasl.cpython-312.pyc | Bin 11033 -> 11033 bytes src/__pycache__/utils.cpython-312.pyc | Bin 3105 -> 3281 bytes src/db.py | 196 +- src/duckhuntbot.py | 1301 +++----- src/game.py | 351 +- src/logging_utils.py | 16 +- src/utils.py | 87 +- 19 files changed, 797 insertions(+), 5712 deletions(-) delete mode 100644 README.md delete mode 100644 __pycache__/duckhunt.cpython-312.pyc delete mode 100644 backup/simple_duckhunt.py create mode 100644 messages.json diff --git a/README.md b/README.md deleted file mode 100644 index 5e2c7ca..0000000 --- a/README.md +++ /dev/null @@ -1,156 +0,0 @@ -# DuckHunt IRC Bot - -A competitive IRC game bot implementing the classic DuckHunt mechanics with modern features. Players compete to shoot ducks that spawn in channels, managing ammunition, accuracy, and collecting items in a persistent progression system. - -## Features - -### Core Gameplay -- **Duck Spawning**: Ducks appear randomly in configured channels with ASCII art -- **Shooting Mechanics**: Players use `!bang` to shoot ducks with limited ammunition -- **Accuracy System**: Hit chances based on player skill that improves with successful shots -- **Gun Jamming**: Weapons can jam and require reloading based on reliability stats -- **Wild Shots**: Shooting without targets results in gun confiscation - -### Progression System -- **Experience Points**: Earned from successful duck kills and befriending -- **Level System**: 40 levels with titles and increasing XP requirements -- **Statistics Tracking**: Comprehensive stats including accuracy, best times, and shot counts -- **Leaderboards**: Top player rankings and personal statistics - -### Item System -- **Shop**: 8 different items available for purchase with earned money -- **Inventory**: Persistent item storage with quantity tracking -- **Item Effects**: Consumable and permanent items affecting gameplay -- **Competitive Drops**: Items drop to the ground for any player to grab with `!snatch` - -### Gun Mechanics -- **Ammunition Management**: Limited shots per magazine with reloading required -- **Charger System**: Multiple magazines with reload mechanics -- **Gun Confiscation**: Administrative punishment system for wild shooting -- **Reliability**: Weapon condition affecting jam probability - -## Installation - -### Requirements -- Python 3.7 or higher -- asyncio support -- SSL/TLS support for secure IRC connections - -### Setup -1. Clone the repository -2. Install Python dependencies (none required beyond standard library) -3. Copy and configure `config.json` -4. Run the bot - -```bash -python3 duckhunt.py -``` - -## Configuration - -The bot uses `config.json` for all configuration. Key sections include: - -### IRC Connection -```json -{ - "server": "irc.example.net", - "port": 6697, - "nick": "DuckHunt", - "channels": ["#games"], - "ssl": true -} -``` - -### SASL Authentication -```json -{ - "sasl": { - "enabled": true, - "username": "bot_username", - "password": "bot_password" - } -} -``` - -### Game Settings -- Duck spawn intervals and timing -- Sleep hours when ducks don't spawn -- Duck type probabilities and rewards -- Shop item prices and effects - -## Commands - -### Player Commands -- `!bang` - Shoot at a duck -- `!reload` - Reload your weapon -- `!bef` / `!befriend` - Attempt to befriend a duck instead of shooting -- `!shop` - View available items for purchase -- `!duckstats` - View your personal statistics -- `!topduck` - View the leaderboard -- `!snatch` - Grab items dropped by other players -- `!use [target]` - Use an item from inventory -- `!sell ` - Sell an item for half price - -### Admin Commands -- `!rearm [player]` - Restore confiscated guns -- `!disarm ` - Confiscate a player's gun -- `!ducklaunch` - Force spawn a duck -- `!reset [confirm]` - Reset player statistics - -## Architecture - -### Modular Design -- `duckhuntbot.py` - Main bot class and IRC handling -- `game.py` - Duck spawning and game mechanics -- `db.py` - Player data persistence and management -- `utils.py` - Input validation and IRC message parsing -- `sasl.py` - SASL authentication implementation -- `logging_utils.py` - Enhanced logging with rotation - -### Database -Player data is stored in JSON format with automatic backups. The system handles: -- Player statistics and progression -- Inventory and item management -- Configuration and preferences -- Historical data and records - -### Concurrency -Built on Python's asyncio framework for handling: -- IRC message processing -- Duck spawning timers -- Background cleanup tasks -- Multiple simultaneous players - -## Duck Types - -- **Normal Duck**: Standard rewards and difficulty -- **Fast Duck**: Higher XP but harder to hit -- **Rare Duck**: Bonus rewards and special drops -- **Boss Duck**: Challenging encounters with significant rewards - -## Item Types - -1. **Extra Shots** - Temporary ammunition boost -2. **Faster Reload** - Reduced reload time -3. **Accuracy Charm** - Permanent accuracy improvement -4. **Lucky Charm** - Increased rare duck encounters -5. **Friendship Bracelet** - Better befriending success rates -6. **Duck Caller** - Faster duck spawning -7. **Camouflage** - Temporary stealth mode -8. **Energy Drink** - Energy restoration - -## Development - -The codebase follows clean architecture principles with: -- Separation of concerns between IRC, game logic, and data persistence -- Comprehensive error handling and logging -- Input validation and sanitization -- Graceful shutdown handling -- Signal-based process management - -### Adding Features -New features can be added by: -1. Extending the command processing in `duckhuntbot.py` -2. Adding game mechanics to `game.py` -3. Updating data structures in `db.py` -4. Configuring behavior in `config.json` \ No newline at end of file diff --git a/__pycache__/duckhunt.cpython-312.pyc b/__pycache__/duckhunt.cpython-312.pyc deleted file mode 100644 index 0008df4906fd560e1f3a88943a1ce678b166f164..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1756 zcmb7EO-vg{6n?Y2w%1;4$HHj>YTBhVX=sZnttc%*kqm_tRYg^E!m6|~-Zj`UyY6@` zFoL$J+6JUF80BE3C`zJ8jVdlV_Eaf}da5d+LaTP*)Sp9cE+9oBm%dqhF(~xVS!v#T zGv9pg&3kY5$6zo3IDWYI)8thV;7@M2L#``3+o)7PhXm+E7jh&)94RCOM~Vs2ky1i3 z#H_>TQq*KNc-ZEaP7LWtZ1`#He;^}AApNVPwyKM(LQQie;n7REz?70uQ1^TRi6$ec zdoP{^pt1l)(`ucNKuIVG9lqJqfp?0ji|-aqTOIjuP#v&rwNrgRnK9KN)21c$11n?N zfk*tz2~pMwwL(7D zAkzYYF(CL|xiBM+!8Bl4-4|A7}4}2)40+WoHjLrN*Gbtl)XDqWrVkFJHwMdybZrO|&1@2hIv8Q%TT2n?> z%F2zGx^#S$llUn2l~}&Sq$zxtl;?y07Yg6w^chs8W>565V5Ho+9#pG9{4d;!Zyj%` zj4h9?e0=Srh2h5{uI;)4@saehc-KKt7TyTymy9@LqvX*?yGh9&iBnr@{ujW!`C<$nXpv%~}oE0Isy zlgyhjv14p3J2ocs=^1J!rwmS##sX>@3*+xC=4Kwt<1~enZw}&MP5TyhkQFF@etUBN zl%*GQ#yNThP2Ot-9jB^D2>C}6h_oXE32%b334u-USG_=_%km}pa`;lXOv&@{E82B4r+V&nN(dug;H7(5kEVleE$zKjvMUYzQ HX>{~296frA diff --git a/backup/simple_duckhunt.py b/backup/simple_duckhunt.py deleted file mode 100644 index 3e1359d..0000000 --- a/backup/simple_duckhunt.py +++ /dev/null @@ -1,2839 +0,0 @@ -#!/usr/bin/env python3 -""" -Standalone DuckHunt IRC Bot with JSON Database Storage -""" - -import asyncio -import ssl -import json -import random -import logging -import logging.handlers -import sys -import os -import base64 -import subprocess -import time -import uuid -import signal -import traceback -import re -from functools import partial -from typing import Optional, Dict, Any - -# Import SASL handler -from src.sasl import SASLHandler - -# Enhanced logger with detailed formatting -class DetailedColorFormatter(logging.Formatter): - COLORS = { - 'DEBUG': '\033[94m', # Blue - 'INFO': '\033[92m', # Green - 'WARNING': '\033[93m', # Yellow - 'ERROR': '\033[91m', # Red - 'CRITICAL': '\033[95m', # Magenta - 'ENDC': '\033[0m' # End color - } - - def format(self, record): - color = self.COLORS.get(record.levelname, '') - endc = self.COLORS['ENDC'] - msg = super().format(record) - return f"{color}{msg}{endc}" - -class DetailedFileFormatter(logging.Formatter): - """File formatter with extra context but no colors""" - def format(self, record): - return super().format(record) - -def setup_logger(): - logger = logging.getLogger('DuckHuntBot') - logger.setLevel(logging.DEBUG) - - # Clear any existing handlers - logger.handlers.clear() - - # Console handler with colors - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_formatter = DetailedColorFormatter( - '%(asctime)s [%(levelname)s] %(name)s: %(message)s' - ) - console_handler.setFormatter(console_formatter) - logger.addHandler(console_handler) - - # File handler with rotation for detailed logs - try: - file_handler = logging.handlers.RotatingFileHandler( - 'duckhunt.log', - maxBytes=10*1024*1024, # 10MB per file - backupCount=5 # Keep 5 backup files - ) - file_handler.setLevel(logging.DEBUG) - file_formatter = DetailedFileFormatter( - '%(asctime)s [%(levelname)-8s] %(name)s - %(funcName)s:%(lineno)d: %(message)s' - ) - file_handler.setFormatter(file_formatter) - logger.addHandler(file_handler) - - logger.info("Enhanced logging system initialized with file rotation") - except Exception as e: - logger.error(f"Failed to setup file logging: {e}") - - return logger - -# Input validation functions -class InputValidator: - @staticmethod - def validate_nickname(nick: str) -> bool: - """Validate IRC nickname format""" - if not nick or len(nick) > 30: - return False - # RFC 2812 nickname pattern - pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$' - return bool(re.match(pattern, nick)) - - @staticmethod - def validate_channel(channel: str) -> bool: - """Validate IRC channel format""" - if not channel or len(channel) > 50: - return False - return channel.startswith('#') and ' ' not in channel - - @staticmethod - def validate_numeric_input(value: str, min_val: Optional[int] = None, max_val: Optional[int] = None) -> Optional[int]: - """Safely parse and validate numeric input""" - try: - num = int(value) - if min_val is not None and num < min_val: - return None - if max_val is not None and num > max_val: - return None - return num - except (ValueError, TypeError): - return None - - @staticmethod - def sanitize_message(message: str) -> str: - """Sanitize user input message""" - if not message: - return "" - # Remove control characters and limit length - sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n') - return sanitized[:500] # Limit message length - -# Simple IRC message parser -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 - -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.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 - - # Duck intelligence and records tracking - self.channel_records = {} # Channel-specific records {channel: {'fastest_shot': {}, 'last_duck': {}, 'total_ducks': 0}} - self.duck_difficulty = {} # Per-channel duck difficulty {channel: multiplier} - self.next_duck_spawn = {} # Track next spawn time per channel - self.channel_bread = {} # Track deployed bread per channel {channel: [{'time': timestamp, 'owner': nick}]} - - # 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', - 'magenta': '\x0313', - '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']}") - # Player data will be saved by the calling function - - 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, message_type='default'): - """Send message to user respecting their output mode preferences and config overrides""" - player = self.get_player(f"{nick}!*@*") - - # Check if this message type should be forced to public - force_public_key = f'message_output.force_public.{message_type}' - if self.get_config(force_public_key, False): - self.send_message(channel, message) - return - - # Default to config setting if player not found or no settings - default_mode = self.get_config('message_output.default_user_mode', 'PUBLIC') - output_mode = default_mode - if player and 'settings' in player: - output_mode = player['settings'].get('output_mode', default_mode) - # Handle legacy 'notices' setting for backwards compatibility - if 'output_mode' not in player['settings'] and 'notices' in player['settings']: - output_mode = 'NOTICE' if player['settings']['notices'] else 'PRIVMSG' - - if output_mode == 'PUBLIC': - # Send as regular channel message - self.send_message(channel, message) - elif output_mode == 'NOTICE': - # Send as notice to user - notice_msg = message.replace(f"{nick} > ", "") # Remove nick prefix for notice - self.send_raw(f'NOTICE {nick} :{notice_msg}') - else: # PRIVMSG - # 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 - - def _get_starting_accuracy(self): - """Get starting accuracy for new player - either fixed or random""" - if self.get_config('new_players.random_stats.enabled', False): - accuracy_range = self.get_config('new_players.random_stats.accuracy_range', [60, 80]) - if accuracy_range and len(accuracy_range) >= 2: - return random.randint(accuracy_range[0], accuracy_range[1]) - return self.get_config('new_players.starting_accuracy', 65) - - def _get_starting_reliability(self): - """Get starting reliability for new player - either fixed or random""" - if self.get_config('new_players.random_stats.enabled', False): - reliability_range = self.get_config('new_players.random_stats.reliability_range', [65, 85]) - if reliability_range and len(reliability_range) >= 2: - return random.randint(reliability_range[0], reliability_range[1]) - return self.get_config('new_players.starting_reliability', 70) - - async def auto_rearm_confiscated_guns(self, channel, shooter_nick): - """Auto-rearm all players with confiscated guns when someone shoots a duck""" - if not self.get_config('weapons.auto_rearm_on_duck_shot', False): - return - - rearmed_players = [] - for user_host, player_data in self.players.items(): - if player_data.get('gun_confiscated', False): - player_data['gun_confiscated'] = False - player_data['ammo'] = player_data.get('ammo', 0) + 1 # Give them 1 ammo - - # Get just the nickname from user!host format - nick = user_host.split('!')[0] if '!' in user_host else user_host - rearmed_players.append(nick) - - if rearmed_players: - self.save_database() - # Send notification to channel - rearmed_list = ', '.join(rearmed_players) - self.send_message(channel, f"🔫 {self.colors['green']}Auto-rearm:{self.colors['reset']} {rearmed_list} got their guns back! (Thanks to {shooter_nick}'s duck shot)") - self.logger.info(f"Auto-rearmed {len(rearmed_players)} players after {shooter_nick} shot duck in {channel}") - - async def update_channel_records(self, channel, hunter, shot_time, duck_type): - """Update channel records and duck difficulty after a successful shot""" - if not self.get_config('records_tracking.enabled', True): - return - - # Initialize channel records if needed - if channel not in self.channel_records: - self.channel_records[channel] = { - 'fastest_shot': None, - 'last_duck': None, - 'total_ducks': 0, - 'total_shots': 0 - } - - records = self.channel_records[channel] - - # Update totals - records['total_ducks'] += 1 - - # Update fastest shot record - if not records['fastest_shot'] or shot_time < records['fastest_shot']['time']: - records['fastest_shot'] = { - 'time': shot_time, - 'hunter': hunter, - 'duck_type': duck_type, - 'timestamp': time.time() - } - # Announce new record - self.send_message(channel, f"🏆 {self.colors['yellow']}NEW RECORD!{self.colors['reset']} {hunter} set fastest shot: {shot_time:.3f}s!") - - # Update last duck info - records['last_duck'] = { - 'hunter': hunter, - 'type': duck_type, - 'shot_time': shot_time, - 'timestamp': time.time() - } - - # Increase duck difficulty (smartness) - if self.get_config('duck_smartness.enabled', True): - if channel not in self.duck_difficulty: - self.duck_difficulty[channel] = 1.0 - - learning_rate = self.get_config('duck_smartness.learning_rate', 0.1) - max_difficulty = self.get_config('duck_smartness.max_difficulty_multiplier', 2.0) - - # Ensure max_difficulty has a valid value - if max_difficulty is None: - max_difficulty = 2.0 - - # Increase difficulty slightly with each duck shot - self.duck_difficulty[channel] = min( - max_difficulty, - self.duck_difficulty[channel] + learning_rate - ) - - # Save records to database periodically - self.save_database() - - async def connect(self): - try: - 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 - except Exception as e: - self.logger.error(f"Connection failed: {e}") - return False - - async def register_user(self): - """Register the user with the IRC server""" - # Send password FIRST if configured (for I-line exemption) - if self.config.get('password'): - self.send_raw(f'PASS {self.config["password"]}') - - 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') - - 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': self.get_config('new_players.starting_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_starting_accuracy(), - 'reliability': self._get_starting_reliability(), - 'weapon': self.get_config('weapons.starting_weapon', 'pistol'), - 'gun_confiscated': False, - 'explosive_ammo': False, - 'settings': { - 'output_mode': self.get_config('message_output.default_user_mode', 'PUBLIC'), - 'notices': False, # Legacy setting for backwards compatibility - False means public - 'private_messages': False - }, - # Inventory system - 'inventory': {}, - # New advanced stats - 'golden_ducks': 0, - 'karma': self.get_config('new_players.starting_karma', 0), - 'deflection': self.get_config('new_players.starting_deflection', 0), - 'defense': self.get_config('new_players.starting_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 - try: - self.save_database() - self.logger.debug("Database batch save completed") - except Exception as e: - self.logger.error(f"Database batch save failed: {e}") - finally: - 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)) - - async def handle_command(self, user, channel, message): - """Enhanced command handler with logging, validation, and graceful degradation""" - if not user: - self.logger.warning("Received command with no user information") - return - - try: - nick = user.split('!')[0] - nick_lower = nick.lower() - - # Input validation - if not InputValidator.validate_nickname(nick): - self.logger.warning(f"Invalid nickname format: {nick}") - return - - if not InputValidator.validate_channel(channel) and not channel == self.config['nick']: - self.logger.warning(f"Invalid channel format: {channel}") - return - - # Sanitize message input - message = InputValidator.sanitize_message(message) - if not message: - return - - # Enhanced logging with context - self.logger.debug(f"Processing command from {nick} in {channel}: {message[:100]}") - - # Check if user is ignored - if nick_lower in self.ignored_nicks: - self.logger.debug(f"Ignoring command from ignored user: {nick}") - 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() - self.logger.info(f"Private command from {nick}: {cmd}") - - # 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('unignore '): - target_nick = cmd[9:].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 , unignore ") - return - else: - # Non-admin private message - self.send_message(response_target, f"{nick} > Private commands are admin-only. Use !duckhelp 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': - 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']}Gun confiscated! Buy 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']}Gun jammed! Use !reload{self.colors['reset']}") - return - - # Check ammo - if player['ammo'] <= 0: - self.send_message(channel, f"{nick} > Empty! | {player['ammo']}/{player['max_ammo']} | {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 - - # Apply duck smartness penalty - duck_difficulty = self.duck_difficulty.get(channel, 1.0) - if duck_difficulty > 1.0: - # Smarter ducks are harder to hit - difficulty_penalty = (duck_difficulty - 1.0) * 20 # Up to 20% penalty at max difficulty - base_accuracy = max(base_accuracy - difficulty_penalty, 10) # Never go below 10% - - 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 - - # Track total shots for channel statistics - if channel not in self.channel_records: - self.channel_records[channel] = { - 'fastest_shot': None, - 'last_duck': None, - 'total_ducks': 0, - 'total_shots': 0 - } - self.channel_records[channel]['total_shots'] += 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} ★ GOLDEN DUCK ★{self.colors['reset']} shot in {shot_time:.3f}s! | Ducks: {player['caught']} ({self.colors['yellow']}{golden_count} golden{self.colors['reset']}) | Level {level} | +{xp_earned} xp{explosive_text}{lucky_text}" - else: - hit_msg = f"{nick} > {self.colors['green']}{shot_sound}{self.colors['reset']} Duck shot in {shot_time:.3f}s! | Ducks: {player['caught']} | Level {level} | +{xp_earned} xp{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) - - # Auto-rearm confiscated guns if enabled - await self.auto_rearm_confiscated_guns(channel, nick) - - # Track records and increase duck difficulty - await self.update_channel_records(channel, nick, shot_time, target_duck['type']) - - 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" [HIT:{ricochet_target}:{ricochet_dmg}xp]" - - # 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_penalty} xp{ricochet_msg}", 'duck_miss') - - else: - # No duck present - wild fire! - player['wild_shots'] = player.get('wild_shots', 0) + 1 - self.update_karma(player, 'wild_shot') - - # Track wild shots in channel statistics - if channel not in self.channel_records: - self.channel_records[channel] = { - 'fastest_shot': None, - 'last_duck': None, - 'total_ducks': 0, - 'total_shots': 0 - } - self.channel_records[channel]['total_shots'] += 1 - - # 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" [HIT:{ff_target}:{ff_dmg}xp]" - - wild_sound = "•BOUM•" if player.get('explosive_ammo', False) else "*BANG*" - if player.get('silencer', 0) > 0: - wild_sound = "•" + wild_sound[1:-1] + "•" - - await self.send_user_message(nick, channel, f"{nick} > {wild_sound} You shot at nothing! What were you aiming at? | {miss_penalty+wild_penalty} xp | GUN CONFISCATED{friendly_fire_msg}", 'wild_shot') - - # 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} > \\_o< You befriended the duck in {bef_time:.3f}s! | Friends: {player['befriended']} ducks | +{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} > Already loaded | {player['ammo']}/{player['max_ammo']} | {player['chargers']}/{player['max_chargers']}") - return - - if player['chargers'] <= 0: - self.send_message(channel, f"{nick} > No chargers! | {player['ammo']}/{player['max_ammo']} | 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 == '!duckstats': - await self.handle_stats(nick, channel, user) - elif cmd == '!duckhelp': - 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('!give '): - parts = full_cmd[6:].split() - if len(parts) >= 2: - target_nick, item_id = parts[0], parts[1] - await self.handle_give(nick, channel, user, target_nick, item_id) - else: - self.send_message(channel, f"{nick} > Usage: !give ") - 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' or cmd == '!topduck': - # 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 cmd == '!ducktime': - # Show time until next duck spawn - await self.handle_ducktime(nick, channel) - - elif cmd == '!lastduck': - # Show information about the last duck shot - await self.handle_lastduck(nick, channel) - - elif cmd == '!records': - # Show channel records - await self.handle_records(nick, channel) - 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('!unignore ') and self.is_admin(user): # Admin only - target_nick = full_cmd[10:].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 ") - # No else clause - ignore unknown commands to avoid interfering with other bots - - except Exception as e: - # Graceful degradation - log error but don't crash - self.logger.error(f"Command handling error for {user} in {channel}: {e}") - import traceback - self.logger.error(f"Full traceback: {traceback.format_exc()}") - - # Send a gentle error message to user - try: - nick = user.split('!')[0] if user and '!' in user else "Unknown" - error_msg = f"{nick} > Sorry, there was an error processing your command. Please try again." - if channel == self.config['nick']: # Private message - self.send_message(nick, error_msg) - else: # Channel message - self.send_message(channel, error_msg) - except Exception as send_error: - self.logger.error(f"Failed to send error message: {send_error}") - - 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']}" - - # Compact stats display - combine into fewer lines - duck_display = f"D:{player.get('caught', 0)}" - if player.get('befriended', 0) > 0: - duck_display += f"/B:{player['befriended']}" - if player.get('golden_ducks', 0) > 0: - duck_display += f"/{self.colors['yellow']}G:{player['golden_ducks']}{self.colors['reset']}" - - # Main stats line - karma_color = self.colors['green'] if player.get('karma', 0) >= 0 else self.colors['red'] - stats_line1 = f"{nick} > {duck_display} | L{level} | XP:{player['xp']}" - if xp_for_next > 0: - stats_line1 += f"(+{xp_for_next})" - stats_line1 += f" | {karma_color}K:{player.get('karma', 0)}{self.colors['reset']}" - - # Equipment line with compact gun status - weapon_name = player.get('weapon', 'pistol')[:6] # Shorten weapon names - compact_gun_status = "" - if player.get('gun_confiscated', False): - compact_gun_status += "[CONF]" - if player.get('jammed', False): - compact_gun_status += "[JAM]" - if player.get('explosive_ammo', False): - compact_gun_status += "[EXP]" - - stats_line2 = f"{nick} > {weapon_name.title()}{compact_gun_status} | Ammo: {player['ammo']}/{player['max_ammo']} | Chargers: {player['chargers']}/{player['max_chargers']} | Accuracy: {player['accuracy']}% (effective: {effective_accuracy:.0f}%) | Reliability: {self.calculate_gun_reliability(player)}%" - - # Optional advanced stats line (if requested) - best_time = player.get('best_time', 999.9) - best_display = f"{best_time:.3f}s" if best_time < 999 else "none" - - stats_line3 = f"{nick} > Best:{best_display} | Avg:{average_time:.3f}s | Jams:{player.get('jammed_count', 0)} | Accidents:{player.get('accidents', 0)} | Lucky:{player.get('lucky_shots', 0)}" - - # Send compact stats (just 2-3 lines instead of 4+) - await self.send_user_message(nick, channel, stats_line1, 'player_stats') - await self.send_user_message(nick, channel, stats_line2, 'player_stats') - - # Display inventory compactly if player has items - if player.get('inventory'): - inventory_items = [] - shop_items = { - '1': 'Bullet', '2': 'Clip', '3': 'AP', '4': 'Explosive', - '5': 'Gun', '6': 'Grease', '7': 'Sight', '8': 'Detector', '9': 'Silencer', - '10': 'Clover', '11': 'Shotgun', '12': 'Rifle', '13': 'Sniper', '14': 'Auto', - '15': 'Sand', '16': 'Water', '17': 'Sabotage', '18': 'Life Ins', - '19': 'Liability', '20': 'Decoy', '21': 'Bread', '22': 'Detector', '23': 'Mech Duck' - } - - for item_id, count in player['inventory'].items(): - item_name = shop_items.get(item_id, f"#{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[:8])}" - if len(inventory_items) > 8: - inventory_display += f" ... +{len(inventory_items) - 8} more" - await self.send_user_message(nick, channel, inventory_display, 'player_stats') - # Only show advanced stats if player has significant activity - if player.get('reflex_shots', 0) > 5: - await self.send_user_message(nick, channel, stats_line3, 'player_stats') - - # 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(): - inventory_items.append(f"{item_id}({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']}Items({total_items}/{max_slots}):{self.colors['reset']} {' '.join(inventory_items[:15])}" - if len(inventory_items) > 15: - inventory_display += f" +{len(inventory_items) - 15}" - await self.send_user_message(nick, channel, inventory_display, 'player_stats') - - 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 COMMANDS 🦆{self.colors['reset']}", - f"{nick} > {self.colors['green']}Game:{self.colors['reset']} !bang !bef !reload !duckstats !topduck !shop !buy !use !give ", - f"{nick} > {self.colors['blue']}Settings:{self.colors['reset']} !output " - ] - 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 !ignore !unignore | /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 (PUBLIC, NOTICE, or PRIVMSG)""" - 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: - default_mode = self.get_config('message_output.default_user_mode', 'PUBLIC') - player['settings'] = { - 'output_mode': default_mode - } - - output_type = output_type.upper() - - if output_type == 'PUBLIC': - player['settings']['output_mode'] = 'PUBLIC' - self.save_database() - self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PUBLIC{self.colors['reset']} (channel messages)") - - elif output_type == 'NOTICE': - player['settings']['output_mode'] = 'NOTICE' - self.save_database() - self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}NOTICE{self.colors['reset']} (channel notices)") - - elif output_type == 'PRIVMSG': - player['settings']['output_mode'] = 'PRIVMSG' - self.save_database() - self.send_message(channel, f"{nick} > Output mode set to {self.colors['cyan']}PRIVMSG{self.colors['reset']} (private messages)") - - else: - current_mode = player['settings'].get('output_mode', 'NOTICE') - self.send_message(channel, f"{nick} > Current output mode: {self.colors['cyan']}{current_mode}{self.colors['reset']} | Usage: !output PUBLIC, !output NOTICE, or !output PRIVMSG") - - async def handle_ducktime(self, nick, channel): - """Show time until next duck spawn""" - current_time = time.time() - - # Check if there are active ducks - channel_ducks = self.ducks.get(channel, []) - alive_ducks = [duck for duck in channel_ducks if duck.get('alive')] - - if alive_ducks: - self.send_message(channel, f"{nick} > {len(alive_ducks)} duck(s) currently active! Go hunt them!") - return - - # Show next spawn time if we have it - if channel in self.next_duck_spawn: - next_spawn = self.next_duck_spawn[channel] - time_until = max(0, next_spawn - current_time) - - if time_until > 0: - minutes = int(time_until // 60) - seconds = int(time_until % 60) - difficulty = self.duck_difficulty.get(channel, 1.0) - difficulty_text = f" (Difficulty: {difficulty:.1f}x)" if difficulty > 1.0 else "" - self.send_message(channel, f"{nick} > Next duck spawn in {self.colors['cyan']}{minutes}m {seconds}s{self.colors['reset']}{difficulty_text}") - else: - self.send_message(channel, f"{nick} > Duck should spawn any moment now...") - else: - # Estimate based on spawn range - min_min = self.duck_spawn_min // 60 - max_min = self.duck_spawn_max // 60 - self.send_message(channel, f"{nick} > Ducks spawn every {min_min}-{max_min} minutes (spawn time varies)") - - async def handle_lastduck(self, nick, channel): - """Show information about the last duck shot in this channel""" - if channel not in self.channel_records: - self.send_message(channel, f"{nick} > No duck records found for {channel}") - return - - last_duck = self.channel_records[channel].get('last_duck') - if not last_duck: - self.send_message(channel, f"{nick} > No ducks have been shot in {channel} yet") - return - - # Format the last duck info - hunter = last_duck['hunter'] - duck_type = last_duck['type'] - shot_time = last_duck['shot_time'] - time_ago = time.time() - last_duck['timestamp'] - - # Format time ago - if time_ago < 60: - time_ago_str = f"{int(time_ago)}s ago" - elif time_ago < 3600: - time_ago_str = f"{int(time_ago // 60)}m ago" - else: - time_ago_str = f"{int(time_ago // 3600)}h ago" - - duck_emoji = "🥇" if duck_type == "golden" else "🦆" - self.send_message(channel, f"{nick} > Last duck: {duck_emoji} {duck_type} duck shot by {self.colors['cyan']}{hunter}{self.colors['reset']} in {shot_time:.3f}s ({time_ago_str})") - - async def handle_records(self, nick, channel): - """Show channel records and statistics""" - if channel not in self.channel_records: - self.send_message(channel, f"{nick} > No records found for {channel}") - return - - records = self.channel_records[channel] - - # Header - self.send_message(channel, f"{nick} > {self.colors['yellow']}📊 {channel.upper()} RECORDS 📊{self.colors['reset']}") - - # Fastest shot - fastest = records.get('fastest_shot') - if fastest: - self.send_message(channel, f"🏆 Fastest shot: {self.colors['green']}{fastest['time']:.3f}s{self.colors['reset']} by {self.colors['cyan']}{fastest['hunter']}{self.colors['reset']} ({fastest['duck_type']} duck)") - - # Total stats - total_ducks = records.get('total_ducks', 0) - total_shots = records.get('total_shots', 0) - accuracy = (total_ducks / total_shots * 100) if total_shots > 0 else 0 - - self.send_message(channel, f"📈 Total: {total_ducks} ducks shot, {total_shots} shots fired ({accuracy:.1f}% accuracy)") - - # Current difficulty - difficulty = self.duck_difficulty.get(channel, 1.0) - if difficulty > 1.0: - self.send_message(channel, f"🧠 Duck intelligence: {self.colors['red']}{difficulty:.2f}x harder{self.colors['reset']} (they're learning!)") - else: - self.send_message(channel, f"🧠 Duck intelligence: Normal (fresh ducks)") - - 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 - - # Create organized shop display - shop_items = { - 'ammo': [ - {'id': '1', 'name': 'Extra bullet', 'cost': 7}, - {'id': '2', 'name': 'Extra clip', 'cost': 20}, - {'id': '3', 'name': 'AP ammo', 'cost': 15}, - {'id': '4', 'name': 'Explosive ammo', 'cost': 25} - ], - 'weapons': [ - {'id': '11', 'name': 'Shotgun', 'cost': 100}, - {'id': '12', 'name': 'Assault rifle', 'cost': 200}, - {'id': '13', 'name': 'Sniper rifle', 'cost': 350} - ], - 'upgrades': [ - {'id': '6', 'name': 'Grease', 'cost': 8}, - {'id': '7', 'name': 'Sight', 'cost': 6}, - {'id': '8', 'name': 'Infrared detector', 'cost': 15}, - {'id': '9', 'name': 'Silencer', 'cost': 5}, - {'id': '10', 'name': 'Four-leaf clover', 'cost': 13} - ], - 'special': [ - {'id': '5', 'name': 'Repurchase gun', 'cost': 40}, - {'id': '15', 'name': 'Sand', 'cost': 7}, - {'id': '16', 'name': 'Water bucket', 'cost': 10}, - {'id': '17', 'name': 'Sabotage', 'cost': 14}, - {'id': '20', 'name': 'Decoy', 'cost': 80}, - {'id': '21', 'name': 'Bread', 'cost': 10}, - {'id': '23', 'name': 'Mechanical duck', 'cost': 50} - ], - 'insurance': [ - {'id': '18', 'name': 'Life insurance', 'cost': 10}, - {'id': '19', 'name': 'Liability insurance', 'cost': 5} - ] - } - - # Format each category - def format_items(items, color): - formatted = [] - for item in items: - formatted.append(f"{item['id']}.{item['name']}({item['cost']})") - return ' '.join(formatted) - - # Send compact shop header - self.send_message(channel, f"{nick} > {self.colors['yellow']}SHOP{self.colors['reset']} XP:{self.colors['green']}{player['xp']}{self.colors['reset']}") - - # Send categorized items in compact format - self.send_message(channel, f"{self.colors['cyan']}Ammo:{self.colors['reset']} {format_items(shop_items['ammo'], self.colors['cyan'])}") - self.send_message(channel, f"{self.colors['red']}Weapons:{self.colors['reset']} {format_items(shop_items['weapons'], self.colors['red'])}") - self.send_message(channel, f"{self.colors['green']}Upgrades:{self.colors['reset']} {format_items(shop_items['upgrades'], self.colors['green'])}") - self.send_message(channel, f"{self.colors['yellow']}Special:{self.colors['reset']} {format_items(shop_items['special'], self.colors['yellow'])}") - self.send_message(channel, f"{self.colors['magenta']}Insurance:{self.colors['reset']} {format_items(shop_items['insurance'], self.colors['magenta'])}") - - # Compact footer - self.send_message(channel, f"Use !shop to buy") - - 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': 10}, - '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] - - # Check for bread channel limit - if item == '21': # Bread - # Initialize and clean up old bread - if channel not in self.channel_bread: - self.channel_bread[channel] = [] - - # Clean up expired bread (30 minutes) - import time - current_time = time.time() - self.channel_bread[channel] = [b for b in self.channel_bread[channel] if current_time - b['time'] < 1800] - - # Check limit after cleanup - channel_bread_count = len(self.channel_bread[channel]) - if channel_bread_count >= 3: - self.send_message(channel, f"{nick} > Maximum 3 bread items allowed in channel! Current: {channel_bread_count}") - return - 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': 10}, - '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!") - elif effect == 'bread': - # Bread - deploy in channel to attract ducks faster - if using_on_other: - self.send_message(channel, f"{nick} > You can't use bread on other players! Deploy it in the channel.") - return - - # Initialize channel bread if needed - if channel not in self.channel_bread: - self.channel_bread[channel] = [] - - # Check limit (should have been checked in buy, but double-check) - if len(self.channel_bread[channel]) >= 3: - self.send_message(channel, f"{nick} > Maximum 3 bread items already deployed in this channel!") - return - - # Deploy bread - import time - bread_info = { - 'time': time.time(), - 'owner': nick - } - self.channel_bread[channel].append(bread_info) - - self.send_message(channel, f"{nick} > *CRUMBLE CRUMBLE* You scattered bread crumbs around the channel! Ducks will be attracted faster. ({len(self.channel_bread[channel])}/3 bread deployed)") - # 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_give(self, nick, channel, user, target_nick, item_id): - """Give an item from inventory to another player""" - # Get giver's player data - player = self.get_player(user) - if not player: - self.send_message(channel, f"{nick} > Player data not found!") - return - - # Check if giver has the item - 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 !duckstats to see your inventory.") - return - - # Find target player - 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 - - # Can't give to yourself - if target_nick_lower == nick.lower(): - self.send_message(channel, f"{nick} > You can't give items to yourself!") - return - - target_player = self.players[target_nick_lower] - - # 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! Use shop numbers 1-23.") - return - - shop_item = shop_items[item_id] - - # Remove item from giver - player['inventory'][item_id] -= 1 - if player['inventory'][item_id] <= 0: - del player['inventory'][item_id] - - # Add item to target player - if 'inventory' not in target_player: - target_player['inventory'] = {} - if item_id not in target_player['inventory']: - target_player['inventory'][item_id] = 0 - target_player['inventory'][item_id] += 1 - - # Announce the gift - self.send_message(channel, f"{nick} > Gave {shop_item['name']} to {target_nick}!") - - # Save both players - self.save_player(user) - 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) - - # Consume bread when duck spawns (bread gets eaten!) - if channel in self.channel_bread and self.channel_bread[channel]: - consumed_bread = self.channel_bread[channel].pop(0) # Remove oldest bread - self.logger.info(f"Duck consumed bread from {consumed_bread['owner']} in {channel}") - - # 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) - - # Apply bread effects - reduce spawn time - for channel in self.channels_joined: - if channel in self.channel_bread and self.channel_bread[channel]: - # Clean up old bread (expires after 30 minutes) - current_time = time.time() - self.channel_bread[channel] = [b for b in self.channel_bread[channel] if current_time - b['time'] < 1800] - - if self.channel_bread[channel]: # If any bread remains after cleanup - # Each bread reduces spawn time by 20%, max 60% reduction - bread_count = len(self.channel_bread[channel]) - reduction = min(0.6, bread_count * 0.2) # Max 60% reduction - wait_time = int(wait_time * (1 - reduction)) - self.logger.info(f"Bread effect: {bread_count} bread in {channel}, {reduction*100:.0f}% spawn time reduction") - break # Apply effect based on first channel with bread - - self.logger.info(f"Waiting {wait_time//60}m {wait_time%60}s for next duck") - - # Set next spawn time for all channels - next_spawn_time = time.time() + wait_time - for channel in self.channels_joined: - self.next_duck_spawn[channel] = next_spawn_time - - # 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) - - # Check each channel for possible duck spawning - 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 consider spawning if no ducks are alive - if not alive_ducks: - # Calculate spawn chance based on bread - base_chance = 0.3 # 30% base chance per spawn cycle - bread_count = 0 - - # Count and clean up bread - if channel in self.channel_bread and self.channel_bread[channel]: - current_time = time.time() - self.channel_bread[channel] = [b for b in self.channel_bread[channel] if current_time - b['time'] < 1800] - bread_count = len(self.channel_bread[channel]) - - # Each bread adds 25% spawn chance - spawn_chance = base_chance + (bread_count * 0.25) - spawn_chance = min(0.95, spawn_chance) # Cap at 95% - - # Roll for spawn - if random.random() < spawn_chance: - await self.spawn_duck_now(channel) - bread_msg = f" (bread boosted: {bread_count} bread = {spawn_chance*100:.0f}% chance)" if bread_count > 0 else "" - self.logger.info(f"Duck spawned in {channel}{bread_msg}") - break # Only spawn in one channel per cycle - - 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.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/config.json b/config.json index 0c74b5f..d5620de 100644 --- a/config.json +++ b/config.json @@ -10,246 +10,9 @@ "password": "duckhunt//789//" }, "password": "your_iline_password_here", - "_comment_password": "Server password for I-line exemption (PASS command)", "admins": ["peorth", "computertech", "colby"], - "_comment_message_output": "Default output modes for different message types", - "message_output": { - "default_user_mode": "PUBLIC", - "_comment_default_user_mode": "Default output mode for new users: PUBLIC, NOTICE, or PRIVMSG", - "force_public": { - "duck_spawn": true, - "duck_shot": true, - "duck_befriend": true, - "duck_miss": true, - "wild_shot": true, - "player_stats": true, - "leaderboard": true, - "admin_commands": true - }, - "_comment_force_public": "Message types that always go to channel regardless of user preference" - }, - - "_comment_duck_spawning": "Duck spawning configuration", - "duck_spawn_min": 1800, - "duck_spawn_max": 5400, - "duck_timeout_min": 45, - "duck_timeout_max": 75, - "duck_smartness": { - "enabled": true, - "learning_rate": 0.1, - "max_difficulty_multiplier": 2.0, - "_comment": "Ducks become harder to hit as more are shot in channel" - }, - "records_tracking": { - "enabled": true, - "track_fastest_shots": true, - "track_channel_records": true, - "max_records_stored": 10 - }, - "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, - "auto_rearm_on_duck_shot": true, - "_comment_auto_rearm": "Automatically restore confiscated guns when anyone shoots a duck" - }, - - "_comment_new_players": "Starting stats for new hunters", - "new_players": { - "starting_xp": 0, - "starting_accuracy": 65, - "starting_reliability": 70, - "starting_karma": 0, - "starting_deflection": 0, - "starting_defense": 0, - "luck_chance": 5, - "_comment_luck_chance": "Base percentage chance for lucky events", - "random_stats": { - "enabled": false, - "accuracy_range": [60, 80], - "reliability_range": [65, 85], - "_comment_ranges": "If enabled, new players get random stats within these ranges" - } - }, - - "_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 - } -} + "duck_spawn_min": 10, + "duck_spawn_max": 30, + "duck_timeout": 60 +} \ No newline at end of file diff --git a/duckhunt.json b/duckhunt.json index b34f2ee..3f9b65e 100644 --- a/duckhunt.json +++ b/duckhunt.json @@ -1,1105 +1,16 @@ { "players": { - "guest44288": { - "current_nick": "Guest44288", - "hostmask": "~Colby@Rizon-FFE0901B.ipcom.comunitel.net", - "coins": 102, - "caught": 1, - "ammo": 9, - "max_ammo": 10, - "chargers": 2, - "max_chargers": 2, - "xp": 15, - "accuracy": 85, - "reliability": 90, - "duck_start_time": null, - "gun_level": 1, - "luck": 0, - "gun_type": "pistol" - }, - "colby": { - "coins": 119, - "caught": 12, - "ammo": 5, - "max_ammo": 10, - "chargers": 2, - "max_chargers": 2, - "xp": 100, - "accuracy": 65, - "reliability": 90, - "duck_start_time": null, - "gun_level": 1, - "luck": 0, - "gun_type": "pistol", - "gun_confiscated": true, - "jammed": false, - "jammed_count": 1, - "total_ammo_used": 18, - "shot_at": 9, - "reflex_shots": 6, - "total_reflex_time": 47.87793278694153, - "best_time": 2.6269078254699707, - "karma": 1, - "wild_shots": 10, - "befriended": 6, - "missed": 1, - "inventory": { - "15": 1 - }, - "sand": 1, - "shots": 9, - "max_shots": 10, - "reload_time": 5.0, - "ducks_shot": 12, - "ducks_befriended": 6, - "accuracy_bonus": 0, - "xp_bonus": 0, - "charm_bonus": 0, - "exp": 100, - "money": 119, - "last_hunt": 0, - "last_reload": 0, - "level": 1, - "ignored_users": [], - "confiscated_count": 2 - }, - "colby_": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "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, - "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 - }, "computertech": { - "xp": 4, - "caught": 1, - "befriended": 0, - "missed": 2, - "ammo": 3, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 0, - "deflection": 0, - "defense": 0, - "jammed": true, - "jammed_count": 4, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 1.458634614944458, - "total_reflex_time": 1.458634614944458, - "reflex_shots": 1, - "wild_shots": 0, - "accidents": 0, - "total_ammo_used": 3, - "shot_at": 3, - "lucky_shots": 0, - "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, - "inventory": {} - }, - "loulan": { - "xp": -9, - "caught": 0, - "befriended": 0, - "missed": 2, + "nick": "ComputerTech", + "xp": 10, + "ducks_shot": 1, "ammo": 5, "max_ammo": 6, "chargers": 2, "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": -5, - "deflection": 0, - "defense": 0, - "jammed": true, - "jammed_count": 1, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 999.9, - "total_reflex_time": 0.0, - "reflex_shots": 0, - "wild_shots": 1, - "accidents": 0, - "total_ammo_used": 3, - "shot_at": 2, - "lucky_shots": 0, - "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 - }, - "madafaka": { - "xp": 42, - "caught": 3, - "befriended": 0, - "missed": 0, - "ammo": 3, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 6, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 0, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 2.8583714962005615, - "total_reflex_time": 12.643521547317505, - "reflex_shots": 3, - "wild_shots": 0, - "accidents": 0, - "total_ammo_used": 3, - "shot_at": 4, - "lucky_shots": 0, - "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 - }, - "peorth": { - "xp": 4, - "caught": 1, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 2, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 1, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 18.33902668952942, - "total_reflex_time": 18.33902668952942, - "reflex_shots": 1, - "wild_shots": 0, - "accidents": 0, - "total_ammo_used": 1, - "shot_at": 2, - "lucky_shots": 0, - "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 - }, - "dgw": { - "xp": 30, - "caught": 2, - "befriended": 0, - "missed": 0, - "ammo": 4, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 4, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 0, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 9.253741025924683, - "total_reflex_time": 20.60851550102234, - "reflex_shots": 2, - "wild_shots": 0, - "accidents": 0, - "total_ammo_used": 2, - "shot_at": 2, - "lucky_shots": 0, - "luck": 0, - "detector": 0, - "silencer": 0, - "sunglasses": 0, - "clothes": 0, - "grease": 0, - "brush": 0, - "mirror": 0, - "sand": 0, - "water": 0, - "sabotage": 0, - "life_insurance": 1, - "liability": 0, - "decoy": 0, - "bread": 0, - "duck_detector": 0, - "mechanical": 0 - }, - "admiral_hubris": { - "xp": 0, - "caught": 1, - "befriended": 0, - "missed": 0, - "ammo": 4, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 45, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": true, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": -17, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 0, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 6.574016809463501, - "total_reflex_time": 6.574016809463501, - "reflex_shots": 1, - "wild_shots": 3, - "accidents": 1, - "total_ammo_used": 4, - "shot_at": 1, - "lucky_shots": 0, - "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, - "inventory": {} - }, - "xysha": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 5, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": true, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": -14, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 1, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 999.9, - "total_reflex_time": 0.0, - "reflex_shots": 0, - "wild_shots": 1, - "accidents": 1, - "total_ammo_used": 1, - "shot_at": 1, - "lucky_shots": 0, - "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, - "inventory": {} - }, - "boliver": { - "xp": 125, - "caught": 5, - "befriended": 4, - "missed": 2, - "ammo": 5, - "max_ammo": 6, - "chargers": 1, - "max_chargers": 2, - "accuracy": 45, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": true, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 1, - "karma": -3, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 7, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 2.484381914138794, - "total_reflex_time": 19.068661212921143, - "reflex_shots": 5, - "wild_shots": 2, - "accidents": 1, - "total_ammo_used": 9, - "shot_at": 7, - "lucky_shots": 1, - "luck": 1, - "detector": 0, - "silencer": 0, - "sunglasses": 0, - "clothes": 0, - "grease": 0, - "brush": 0, - "mirror": 0, - "sand": 0, - "water": 1, - "sabotage": 0, - "life_insurance": 0, - "liability": 0, - "decoy": 0, - "bread": 0, - "duck_detector": 0, - "mechanical": 0, - "inventory": {} - }, - "kaitphone": { - "xp": 13, - "caught": 1, - "befriended": 0, - "missed": 1, - "ammo": 4, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 1, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 3, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 28.610472440719604, - "total_reflex_time": 28.610472440719604, - "reflex_shots": 1, - "wild_shots": 0, - "accidents": 0, - "total_ammo_used": 2, - "shot_at": 2, - "lucky_shots": 0, - "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 - }, - "milambar": { - "xp": 12, - "caught": 2, - "befriended": 3, - "missed": 0, - "ammo": 3, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": true, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 7, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 1, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 2.784888982772827, - "total_reflex_time": 8.606451749801636, - "reflex_shots": 2, - "wild_shots": 1, - "accidents": 0, - "total_ammo_used": 3, - "shot_at": 2, - "lucky_shots": 0, - "luck": 1, - "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, - "inventory": {} - }, - "wobotkoala": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "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, - "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 - }, - "general_kornwallace": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 0, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 1, - "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, - "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 - }, - "helderheid": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "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, - "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 - }, - "coolkevin": { - "xp": 12, - "caught": 1, - "befriended": 0, - "missed": 0, - "ammo": 5, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": 2, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 0, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 4.75800085067749, - "total_reflex_time": 4.75800085067749, - "reflex_shots": 1, - "wild_shots": 0, - "accidents": 0, - "total_ammo_used": 1, - "shot_at": 2, - "lucky_shots": 0, - "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 - }, - "magicalpig": { - "xp": 23, - "caught": 2, - "befriended": 1, - "missed": 1, - "ammo": 3, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 85, - "reliability": 90, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "golden_ducks": 0, - "karma": -11, - "deflection": 0, - "defense": 0, - "jammed": false, - "jammed_count": 0, - "deaths": 0, - "neutralized": 0, - "deflected": 0, - "best_time": 9.407448291778564, - "total_reflex_time": 21.80314612388611, - "reflex_shots": 2, - "wild_shots": 2, - "accidents": 1, - "total_ammo_used": 5, - "shot_at": 3, - "lucky_shots": 0, - "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, - "inventory": {} - }, - "tabb": { - "xp": -5, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 5, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 85, - "reliability": 90, - "weapon": "pistol", - "gun_confiscated": true, - "explosive_ammo": false, - "settings": { - "notices": true, - "private_messages": false - }, - "inventory": {}, - "golden_ducks": 0, - "karma": -3, - "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": 1, - "accidents": 0, - "total_ammo_used": 1, - "shot_at": 0, - "lucky_shots": 0, - "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 - }, - "testuser!test@test.com": { - "xp": 100, - "caught": 0, - "befriended": 0 - }, - "testuser": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "output_mode": "PUBLIC", - "notices": true, - "private_messages": false - }, - "inventory": {}, - "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, - "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 - }, - "py-ctcp": { - "xp": 0, - "caught": 0, - "befriended": 0, - "missed": 0, - "ammo": 6, - "max_ammo": 6, - "chargers": 2, - "max_chargers": 2, - "accuracy": 65, - "reliability": 70, - "weapon": "pistol", - "gun_confiscated": false, - "explosive_ammo": false, - "settings": { - "output_mode": "PUBLIC", - "notices": false, - "private_messages": false - }, - "inventory": {}, - "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, - "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, - "shots": 6, - "max_shots": 6, - "reload_time": 5.0, - "ducks_shot": 0, - "ducks_befriended": 0, - "accuracy_bonus": 0, - "xp_bonus": 0, - "charm_bonus": 0, - "exp": 0, - "money": 100, - "last_hunt": 0, - "last_reload": 0, - "level": 1, - "ignored_users": [], - "confiscated_count": 0 + "accuracy": 64, + "gun_confiscated": false } }, - "last_save": "1758314578.1957033" + "last_save": "1758592642.337953" } \ No newline at end of file diff --git a/duckhunt.log b/duckhunt.log index 8037e9f..06b41fe 100644 --- a/duckhunt.log +++ b/duckhunt.log @@ -196,3 +196,149 @@ 2025-09-19 21:42:58,198 [INFO ] DuckHuntBot - run:1154: Database saved 2025-09-19 21:42:58,261 [ERROR ] DuckHuntBot - run:1166: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) 2025-09-19 21:42:58,262 [INFO ] DuckHuntBot - run:1168: Bot shutdown complete +2025-09-22 20:48:31,249 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-22 20:48:31,249 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-22 20:48:31,250 [INFO ] DuckHuntBot - main:22: 🦆 Starting DuckHunt Bot... +2025-09-22 20:48:31,252 [INFO ] DuckHuntBot.DB - load_database:38: Loaded 22 players from duckhunt.json +2025-09-22 20:48:31,588 [INFO ] DuckHuntBot - connect:86: Connected to irc.rizon.net:6697 +2025-09-22 20:48:32,079 [INFO ] DuckHuntBot - handle_message:190: Successfully registered with IRC server +2025-09-22 20:48:33,627 [INFO ] DuckHuntBot - signal_handler:174: Received signal 2, shutting down... +2025-09-22 20:48:33,699 [INFO ] DuckHuntBot - run:1063: Main loop cancelled +2025-09-22 20:48:33,699 [INFO ] DuckHuntBot - run:1072: Shutting down bot... +2025-09-22 20:48:33,699 [INFO ] DuckHuntBot - message_loop:1118: Message loop cancelled +2025-09-22 20:48:33,700 [INFO ] DuckHuntBot - message_loop:1124: Message loop ended +2025-09-22 20:48:33,700 [INFO ] DuckHuntBot.Game - duck_timeout_checker:293: Duck timeout checker cancelled +2025-09-22 20:48:33,700 [INFO ] DuckHuntBot.Game - spawn_ducks:248: Duck spawning loop cancelled +2025-09-22 20:48:33,703 [INFO ] DuckHuntBot - run:1086: Database saved +2025-09-22 20:48:33,772 [ERROR ] DuckHuntBot - run:1097: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-22 20:48:33,772 [INFO ] DuckHuntBot - run:1099: Bot shutdown complete +2025-09-22 20:48:37,968 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-22 20:48:37,969 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-22 20:48:37,969 [INFO ] DuckHuntBot - main:22: 🦆 Starting DuckHunt Bot... +2025-09-22 20:48:37,971 [INFO ] DuckHuntBot.DB - load_database:38: Loaded 22 players from duckhunt.json +2025-09-22 20:48:38,200 [INFO ] DuckHuntBot - connect:86: Connected to irc.rizon.net:6697 +2025-09-22 20:48:38,340 [INFO ] DuckHuntBot - handle_message:190: Successfully registered with IRC server +2025-09-22 20:50:02,412 [INFO ] DuckHuntBot - signal_handler:174: Received signal 2, shutting down... +2025-09-22 20:50:02,471 [INFO ] DuckHuntBot.Game - duck_timeout_checker:293: Duck timeout checker cancelled +2025-09-22 20:50:02,471 [INFO ] DuckHuntBot - message_loop:1118: Message loop cancelled +2025-09-22 20:50:02,471 [INFO ] DuckHuntBot - message_loop:1124: Message loop ended +2025-09-22 20:50:02,471 [INFO ] DuckHuntBot.Game - spawn_ducks:248: Duck spawning loop cancelled +2025-09-22 20:50:02,471 [INFO ] DuckHuntBot - run:1063: Main loop cancelled +2025-09-22 20:50:02,472 [INFO ] DuckHuntBot - run:1072: Shutting down bot... +2025-09-22 20:50:02,474 [INFO ] DuckHuntBot - run:1086: Database saved +2025-09-22 20:50:02,522 [ERROR ] DuckHuntBot - run:1097: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-22 20:50:02,522 [INFO ] DuckHuntBot - run:1099: Bot shutdown complete +2025-09-23 00:16:59,762 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 00:16:59,763 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 00:16:59,763 [INFO ] DuckHuntBot - load_shop_config:81: Loaded shop configuration with 15 items +2025-09-23 00:22:10,719 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 00:22:10,720 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 00:22:10,722 [INFO ] DuckHuntBot - load_shop_config:81: Loaded shop configuration with 15 items +2025-09-23 00:23:53,961 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 00:23:53,963 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 00:23:53,964 [INFO ] DuckHuntBot - load_shop_config:81: Loaded shop configuration with 15 items +2025-09-23 01:45:21,315 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 01:45:21,316 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 01:45:21,317 [INFO ] DuckHuntBot - load_shop_config:90: Loaded shop configuration with 15 items +2025-09-23 01:45:21,318 [INFO ] DuckHuntBot - load_messages_config:107: Loaded messages configuration +2025-09-23 01:45:21,319 [INFO ] DuckHuntBot - main:22: 🦆 Starting DuckHunt Bot... +2025-09-23 01:45:21,322 [INFO ] DuckHuntBot.DB - load_database:38: Loaded 22 players from duckhunt.json +2025-09-23 01:45:21,670 [INFO ] DuckHuntBot - connect:272: Connected to irc.rizon.net:6697 +2025-09-23 01:45:22,224 [INFO ] DuckHuntBot - handle_message:381: Successfully registered with IRC server +2025-09-23 01:48:04,868 [INFO ] DuckHuntBot - signal_handler:365: Received signal 1, shutting down... +2025-09-23 01:48:04,922 [INFO ] DuckHuntBot - run:1473: Main loop cancelled +2025-09-23 01:48:04,924 [INFO ] DuckHuntBot - run:1482: Shutting down bot... +2025-09-23 01:48:04,926 [INFO ] DuckHuntBot - message_loop:1530: Message loop cancelled +2025-09-23 01:48:04,926 [INFO ] DuckHuntBot - message_loop:1536: Message loop ended +2025-09-23 01:48:04,927 [INFO ] DuckHuntBot.Game - duck_timeout_checker:311: Duck timeout checker cancelled +2025-09-23 01:48:04,928 [INFO ] DuckHuntBot.Game - spawn_ducks:260: Duck spawning loop cancelled +2025-09-23 01:48:04,938 [INFO ] DuckHuntBot - run:1496: Database saved +2025-09-23 01:48:04,987 [ERROR ] DuckHuntBot - run:1509: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-23 01:48:04,988 [INFO ] DuckHuntBot - run:1511: Bot shutdown complete +2025-09-23 02:23:43,434 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 02:23:43,436 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 0 players from duckhunt_simple.json +2025-09-23 02:23:43,437 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 02:23:43,438 [INFO ] DuckHuntBot - main:29: 🦆 Starting Simplified DuckHunt Bot... +2025-09-23 02:23:43,773 [INFO ] DuckHuntBot - connect:86: Connected to irc.rizon.net:6697 +2025-09-23 02:23:43,950 [INFO ] DuckHuntBot - handle_message:112: Successfully registered with IRC server +2025-09-23 02:25:35,240 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:26:37,051 [INFO ] DuckHuntBot - message_loop:283: Message loop ended +2025-09-23 02:26:37,051 [INFO ] DuckHuntBot.Game - duck_spawn_loop:51: Duck spawning loop cancelled +2025-09-23 02:26:37,051 [INFO ] DuckHuntBot.Game - duck_timeout_loop:81: Duck timeout loop cancelled +2025-09-23 02:26:37,052 [INFO ] DuckHuntBot.Game - start_game_loops:31: Game loops cancelled +2025-09-23 02:26:37,052 [INFO ] DuckHuntBot - run:316: Shutting down bot... +2025-09-23 02:26:37,053 [INFO ] DuckHuntBot - run:321: Database saved +2025-09-23 02:26:37,113 [ERROR ] DuckHuntBot - run:331: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-23 02:26:37,113 [INFO ] DuckHuntBot - run:335: Bot shutdown complete +2025-09-23 02:38:10,738 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 02:38:10,739 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json +2025-09-23 02:38:10,739 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 02:38:10,739 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot... +2025-09-23 02:38:10,980 [INFO ] DuckHuntBot - connect:86: Connected to irc.rizon.net:6697 +2025-09-23 02:38:11,465 [INFO ] DuckHuntBot - handle_message:112: Successfully registered with IRC server +2025-09-23 02:39:58,322 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:03,825 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:04,024 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:04,205 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:04,381 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:04,554 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:04,743 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,003 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,500 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,572 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,596 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,648 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,653 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,683 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,739 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,744 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,807 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,809 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,883 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,898 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,948 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:05,960 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:06,029 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:06,031 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:06,046 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:06,102 [INFO ] DuckHuntBot - signal_handler:71: Received signal 2, shutting down... +2025-09-23 02:40:21,162 [INFO ] DuckHuntBot - message_loop:350: Message loop ended +2025-09-23 02:40:21,162 [INFO ] DuckHuntBot.Game - duck_spawn_loop:51: Duck spawning loop cancelled +2025-09-23 02:40:21,163 [INFO ] DuckHuntBot.Game - duck_timeout_loop:81: Duck timeout loop cancelled +2025-09-23 02:40:21,163 [INFO ] DuckHuntBot.Game - start_game_loops:31: Game loops cancelled +2025-09-23 02:40:21,163 [INFO ] DuckHuntBot - run:383: Shutting down bot... +2025-09-23 02:40:21,165 [INFO ] DuckHuntBot - run:388: Database saved +2025-09-23 02:40:21,214 [ERROR ] DuckHuntBot - run:398: Error closing connection: [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2685) +2025-09-23 02:40:21,218 [INFO ] DuckHuntBot - run:402: Bot shutdown complete +2025-09-23 02:42:31,255 [INFO ] DuckHuntBot - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 02:42:31,255 [INFO ] DuckHuntBot.DB - load_database:28: Loaded 1 players from duckhunt.json +2025-09-23 02:42:31,256 [INFO ] SASL - setup_logger:61: Enhanced logging system initialized with file rotation +2025-09-23 02:42:31,256 [INFO ] DuckHuntBot - main:28: 🦆 Starting DuckHunt Bot... +2025-09-23 02:42:31,501 [INFO ] DuckHuntBot - connect:95: Connected to irc.rizon.net:6697 +2025-09-23 02:42:31,502 [INFO ] DuckHuntBot - run:377: 🦆 Bot is now running! Press Ctrl+C to stop. +2025-09-23 02:42:31,642 [INFO ] DuckHuntBot - handle_message:121: Successfully registered with IRC server +2025-09-23 02:42:47,509 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:43:02,511 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:43:14,515 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:44:28,542 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:45:34,560 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:46:46,591 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:47:57,623 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:49:22,643 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:51:01,687 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:52:03,709 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:53:32,732 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:54:45,747 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:56:12,772 [INFO ] DuckHuntBot.Game - spawn_duck:103: Duck spawned in #ct +2025-09-23 02:57:17,679 [INFO ] DuckHuntBot - signal_handler:73: 🛑 Received SIGINT (Ctrl+C), initiating graceful shutdown... +2025-09-23 02:57:21,807 [INFO ] DuckHuntBot - run:386: 🛑 Shutdown signal received, cleaning up... +2025-09-23 02:57:21,811 [INFO ] DuckHuntBot - _graceful_shutdown:433: 📤 Sending QUIT message to IRC... +2025-09-23 02:57:21,880 [INFO ] DuckHuntBot - message_loop:359: Message loop ended +2025-09-23 02:57:22,314 [INFO ] DuckHuntBot - _graceful_shutdown:441: 💾 Saving database... +2025-09-23 02:57:22,333 [INFO ] DuckHuntBot.Game - duck_spawn_loop:51: Duck spawning loop cancelled +2025-09-23 02:57:22,334 [INFO ] DuckHuntBot.Game - duck_timeout_loop:81: Duck timeout loop cancelled +2025-09-23 02:57:22,336 [INFO ] DuckHuntBot.Game - start_game_loops:31: Game loops cancelled +2025-09-23 02:57:22,337 [INFO ] DuckHuntBot - run:405: 🔄 Final cleanup... +2025-09-23 02:57:22,339 [INFO ] DuckHuntBot - run:419: 💾 Database saved +2025-09-23 02:57:22,340 [INFO ] DuckHuntBot - _close_connection:454: 🔌 IRC connection closed +2025-09-23 02:57:22,341 [INFO ] DuckHuntBot - run:426: ✅ Bot shutdown complete diff --git a/duckhunt.py b/duckhunt.py index 5340e9b..28eaf81 100644 --- a/duckhunt.py +++ b/duckhunt.py @@ -1,5 +1,6 @@ """ -DuckHunt IRC Bot - Main Entry Point +DuckHunt IRC Bot - Simplified Entry Point +Commands: !bang, !reload, !shop, !rearm, !disarm """ import asyncio @@ -15,23 +16,31 @@ from src.duckhuntbot import DuckHuntBot def main(): """Main entry point for DuckHunt Bot""" try: - with open('config.json') as f: + config_file = 'config.json' + if not os.path.exists(config_file): + print("❌ config.json not found!") + sys.exit(1) + + with open(config_file) as f: config = json.load(f) bot = DuckHuntBot(config) bot.logger.info("🦆 Starting DuckHunt Bot...") + # Run the bot asyncio.run(bot.run()) except KeyboardInterrupt: - print("\n🛑 Bot stopped by user") + print("\n🛑 Shutdown interrupted by user") except FileNotFoundError: print("❌ config.json not found!") sys.exit(1) except Exception as e: print(f"❌ Error: {e}") sys.exit(1) + else: + print("👋 DuckHunt Bot stopped gracefully") if __name__ == '__main__': - main() + main() \ No newline at end of file diff --git a/messages.json b/messages.json new file mode 100644 index 0000000..fdf5ae6 --- /dev/null +++ b/messages.json @@ -0,0 +1,54 @@ +{{ + "duck_spawn": "・゜゜・。。・゜゜\\_o< \u000303QUACK!\u000F A \u000308duck\u000F has appeared! Type \u000302!bang\u000F to shoot it!", + "duck_flies_away": "The \u000308duck\u000F flies away. ·°'`'°-.,¸¸.·°'`", + "bang_hit": "{nick} > \u000304*BANG*\u000F You shot the \u000308duck\u000F! [\u000303+{xp_gained} xp\u000F] [Total ducks: \u000302{ducks_shot}\u000F]", + "bang_miss": "{nick} > \u000304*BANG*\u000F You missed the \u000308duck\u000F!", + "bang_no_duck": "{nick} > \u000304*BANG*\u000F What did you shoot at? There is \u000304no duck\u000F in the area... [\u000304GUN CONFISCATED\u000F]", + "bang_no_ammo": "{nick} > \u000307*click*\u000F You're out of ammo! Use \u000302!reload\u000F", + "bang_not_armed": "{nick} > You are \u000304not armed\u000F.", + "reload_success": "{nick} > \u000307*click*\u000F Reloaded! [Ammo: \u000303{ammo}\u000F/\u000303{max_ammo}\u000F] [Chargers: \u000302{chargers}\u000F]", + "reload_already_loaded": "{nick} > Your gun is \u000303already loaded\u000F!", + "reload_no_chargers": "{nick} > You're out of \u000304chargers\u000F!", + "reload_not_armed": "{nick} > You are \u000304not armed\u000F.", + "shop_display": "DuckHunt Shop: {items} | You have \u000303{xp} XP\u000F", + "shop_item_format": "(\u000302{id}\u000F) \u000310{name}\u000F - \u000303{price} XP\u000F", + "help_header": "\u000302DuckHunt Commands:\u000F", + "help_user_commands": "\u000302!bang\u000F - Shoot at ducks | \u000302!reload\u000F - Reload your gun | \u000302!shop\u000F - View the shop", + "help_help_command": "\u000302!duckhelp\u000F - Show this help", + "help_admin_commands": "\u000304Admin:\u000F \u000302!rearm \u000F | \u000302!disarm \u000F | \u000302!ignore \u000F | \u000302!unignore \u000F | \u000302!ducklaunch\u000F", + "admin_rearm_player": "[\u000304ADMIN\u000F] \u000310{target}\u000F has been rearmed by \u000302{admin}\u000F", + "admin_rearm_all": "[\u000304ADMIN\u000F] All players have been rearmed by \u000302{admin}\u000F", + "admin_disarm": "[\u000304ADMIN\u000F] \u000310{target}\u000F has been disarmed by \u000302{admin}\u000F", + "admin_ignore": "[\u000304ADMIN\u000F] \u000310{target}\u000F is now ignored by \u000302{admin}\u000F", + "admin_unignore": "[\u000304ADMIN\u000F] \u000310{target}\u000F is no longer ignored by \u000302{admin}\u000F", + "admin_ducklaunch": "[\u000304ADMIN\u000F] A \u000308duck\u000F has been launched by \u000302{admin}\u000F", + "admin_ducklaunch_not_enabled": "[\u000304ADMIN\u000F] This channel is \u000304not enabled\u000F for duckhunt", + "usage_rearm": "Usage: \u000302!rearm \u000F", + "usage_disarm": "Usage: \u000302!disarm \u000F", + "usage_ignore": "Usage: \u000302!ignore \u000F", + "usage_unignore": "Usage: \u000302!unignore \u000F", + + "colours": { + "white": "\u00030", + "black": "\u00031", + "blue": "\u00032", + "green": "\u00033", + "red": "\u00034", + "brown": "\u00035", + "purple": "\u00036", + "orange": "\u00037", + "yellow": "\u00038", + "light_green": "\u00039", + "cyan": "\u000310", + "light_cyan": "\u000311", + "light_blue": "\u000312", + "pink": "\u000313", + "grey": "\u000314", + "light_grey": "\u000315", + "bold": "\u0002", + "underline": "\u001f", + "italic": "\u001d", + "strikethrough": "\u001e", + "reset": "\u000f" + } +} \ No newline at end of file diff --git a/src/__pycache__/db.cpython-312.pyc b/src/__pycache__/db.cpython-312.pyc index 68ae34304f2beef2534394a7e65bfb79b8180524..c76a996ee8e20ee13e17314fa94a905c88d76b55 100644 GIT binary patch literal 4444 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 z
    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 literal 62464 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 W169EXs^Z`2wph3o5fz1O4*nbE?1`QL diff --git a/src/__pycache__/game.cpython-312.pyc b/src/__pycache__/game.cpython-312.pyc index 0ab22ef2ded9bd3cf09285a979e1d8ddda6bbb73..863a05f54cc1099412b5524c8a15128952b152b2 100644 GIT binary patch literal 5341 zcmb_gT~Hg>6~3!oNh=|Yz!(I`c(Dms*a$+5jWLdgAIFZ}p_AC@G&Idt)-GaY{m|Vd zSc=GGY)@;?&@p+b&DaU}$z;+_=~LXvLlfLQb*3XlcBE{0@J#%Wyb;Ju<4hiU&Ppo@ zA}4w19qHb4&)s|Oxo7YB&bj;V%1S$d!aPn*T;4{=7uYchlP5L?fVfTsGDZYSFomcw zN^ebLCcUM{XuV~|7!x@`1o|=&nD;CqvS>BnsRKUs&YAqJP2{ zk^Blb9+kO~DgXJCQxTOLimLV@pCb7=0a%6?y7kpSn52%IW~#psmEMI)h@{VS9| z>d&o`tEqfv!wbZ9A`(!Pppu}V=`tB(P_;3$z(8#g%urhe3)F1P>R~k&bwJhP-V$8{ zn|1Bvf>VmRSvNmjq6E_T-*`jUAweYwUl>l9pl4`FBm%v>nFvv>#$!R&po*_dK-5C}L<_#^q&PRl#j@9QPg9H4zPqUH)k3T&xS1n1C1C zrO5s+G^Telrm;LPMI@Ey zSi~rg$;{UcOgl5gK6j~ZJtjh(5+&Ltt^ z9ZGqJ(%zAD<8eHstL{A+cUQ{YwajF?pG$Q=m+n59c0YfGxz9GNF|@6BwWi^^_bu;Y z?VVk>cP+POdWKUy!|9rljAP^z$H?Q=s=7?oQ>m(_)(K_n%{rXdPG3Epakx_scXH=5 zpE?e#(ZK&+LCn55SUq%<`lz;Z$VOHuYRJm0m>8rj6!2H96w)^DP(QtL$b_836w>`P z(B=K-n}Yw?hz(B!tg^KP3qDaC!T4aD!Wn?XpFNK*Mv@tmTJVmtXP-(G*Pzvczm@Zd z1Z|v9@Cox~pHT6{=;aA|mhlin+6JR2j7tH{B17blhBYg;Qbd)}UYc16 ziQ=Tjgd|1PsxXm=h#`d!Mx}@-XcqrORPu{P@Ivim7fjRLUB<0@EV{RAEU3#*`y>@8 z--R4y!#8leVHb2s>z$|I`-lAbkp%UHd~&(>fXIDIzPJ9xBTn0``J0$qq?2f!*7KbpHJ6$ zGfr>P>HWq++}t8_hrP{axV{wEmu@;d$9}fGX|=g^Av_;mI+ ze6)`mCd^0s2e(0IrJou$F)K$bK;JV_STo+?Uiw~-2`1b-Kq1{r1HB1jFon1T|BZJb z9_L~-V$6@n)nZx%o?_bK$3?U-GFcFB6eIS|PysRnX<0c$2^KKKm{rUHM#yjfM93?k z>kK`^fDChj0UjhSGZsktECP+^EDq5aWS5D_!2-$@Webd8Hc;~X2nM5BPr@9;V6>$q zN_=(p2tyKPSi4JKn+aBLc^JB-&p$z*ayUc-Cgigtip)L@>o77kJCiV#)v17~@a#2s z)&x3v(LM>fynSE89oCc|OX&84)pkE={3x02X2={!05p8ODTn!gprLgW8k#ntAscs< zqM>mwL683m-}R^gJvxdwbWq4Y^c4o?sr^u$h5QFcxbcuADx7cH7xQu}@2@Pk|E_&y zxzoFUz%(Ee<59d3H-s6V7a!8$7Or-Bp4AKE$_T`eJZLuB1^_f6@7G~(C|%c{akeL& z?T>ctS%}TYZoWR}$hNj6oh=XBI_?B-2QzI4Q*8&+ZGG#cqPqXB(YX^hUdp!Zo%>~$ zgRIH4dn#Eomb--6;!S7p%L?bqL68*DuVZ=d9?g9j;5CYuz68TdRqc>6 z(V9sT#eg_*Mj(dF(lSp)vjI#`Sk$#bMQhmHr9h z08(i2T!KgI@RdWSKHwT(CvsWaQ^1LDFF;W48i_!Jn^NvVrDM;-ikcg(3mx+vsfyO@ z?$#?K*G^qMH8-7d>{_!BxO(+p?Vk&LQqi1s)Bs~g2TuRBm2ujyFpzxNDzDkE+UE|X*rp7- zJH_r^JaC`g3sPZ#y~<|Tohf$bjX<(}=05wzL)LMv;%Y^Pb){HWvT5JaiKWBIhTaG4 z!N&?3=U$=yGBL^01Cjsy-)` WpOcEu$+rJmenU}*)(O_Sl>Y%B<#z@E literal 15808 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 PpR`_vF5je(lIs2+F#(e& diff --git a/src/__pycache__/logging_utils.cpython-312.pyc b/src/__pycache__/logging_utils.cpython-312.pyc index 1ea0c6c9ecf231620201040efa1713feff627fa1..e1d49cbc8d4427e0e1a06ee65e56278c2fc81c2d 100644 GIT binary patch delta 280 zcmX>ud0vwDG%qg~0}wdAyO>eAkyn7xOxz{4Br!85HN`nUC%?4FEx#x?v7{ums7lp2 zKd(4HCsiR0CZtfFS(2fU3{tl_kWqz^josPb$3Jv(6_XJg8(4VqMy6msITRBo3o?rZ z`2nr`(ZFy+P;|P?M49V?iWdbH7pPrVGP&;S4c delta 340 zcmX>vd0dkBG%qg~0}$+PyqHn2kyn7xM9d|%Br!85HN`nUC%?!ozbH4cq$IVdO2s)p zuQ)#^RUr)~q)?t&lA(|cRJJ*oQH7C}&Dr0_e{wC85i2W5aPn5B;Cfl?rs+Wp5dqp< z>;oizG%(x{5Sh+Bk$XD-ME(VA*A)ydDi~}~x-4MY!Fq#VxWBrydWP9Wex(kUJAA^O z>798q!Y=cvbTHo#5}zPGLwE-3jFcH+7x+~!aHxC+`A3ue7Ds%1-sCRkY!$r+g3>nx zq;3ey-T*R0 zZ8qXAX4F!h5q@1(@1m^U7Y2St)h{Z{jA}F7ugmCOl+pdd0OWk(QD77*k_1`{091%> AJ^%m! diff --git a/src/__pycache__/sasl.cpython-312.pyc b/src/__pycache__/sasl.cpython-312.pyc index 9736ca21c40303292e259c2eb6c37cd0b9581259..8961ac100d2d0d8941c2423a6daa2c7b264f1315 100644 GIT binary patch delta 19 ZcmbOkHZzRtG%qg~0}vD}*~rDO4FES~1mOSx delta 19 ZcmbOkHZzRtG%qg~0}$M7+{neR4FEYT1t$Oi diff --git a/src/__pycache__/utils.cpython-312.pyc b/src/__pycache__/utils.cpython-312.pyc index 372fe943e857b3b11e38e683819f3fff693cda3a..2d841cfba81240f043429b2d67e1d543af8e0155 100644 GIT binary patch delta 823 zcmX|;&r4KM6vyvy@3T@g zH!if*$XhhE$rgdtCUGP97et$eHufNbS_Kg!27!y{oU!RV-shZq&i!%j_r3@hA`9;W z0Y3rj`&pQ%)(QEBgR;T6WV^S)Jt2&kgi$7?s7b-gDJd0>@iW16K{8homyMWA45V+#5)g&F#yV(K$e+*lcD56hl@CF|IenPkdSZ%k)X zmZuEa!dZXznNoe@p>&Bx55PjtFPtQI!~@%zh*Mq#DM0~^Jm`Lvvoz#hSAHC+gAA`n zGyps~Go2O+&I04XjXUZVr9n5MJr2jPZir;YcI;V;3G5!{xLev?*sN8**cpHnKa8kF z$cQMS1#uGaitrG|L4;O<`%>RLQA*<2Q&M)u;s%7H*lpRcC){thin@=C_L^f%o&z13 zT1)N|9{Cr$2mRGeOlC@1+uP9PJe?;Qtvq|4GWU}aql){-xEWCHC7=;?4wf+09r1T` zw!$HMw1~1!@4QUHDNuzC)c-X@p0&RpMN<~yS?I}=AYut9^yIwMKG@H^`Oe$-YqzphzWb?Au!*ua zPj(m1$y3(l$@U!!TWNfsZRmfI^KIsRJ5mWUjM$3Wi5Ifx9IuqBo2|_Y{YM)s>nkts zy=r_ddCx}H%)TzaIDQxBa7~W!d_*BL)1@Sg=MQ%=92oQQ8N_gCEO}s^?JTlx6rAHN lb;ZU7uKPCyfPGB^eP9u=_ek}xc^anoiNfFpIN%L)`~%vghHL-; diff --git a/src/db.py b/src/db.py index a8d6379..dbf2b72 100644 --- a/src/db.py +++ b/src/db.py @@ -1,181 +1,81 @@ """ -Database functionality for DuckHunt Bot +Simplified Database management for DuckHunt Bot +Only essential player fields """ import json -import os -import time -import asyncio import logging +import time +import os class DuckDB: - """Database management for DuckHunt Bot""" + """Simplified database management""" def __init__(self, db_file="duckhunt.json"): self.db_file = db_file self.players = {} - self._save_pending = False self.logger = logging.getLogger('DuckHuntBot.DB') - - def get_config(self, path, default=None): - """Helper method to get config values (needs to be set by bot)""" - if hasattr(self, '_config_getter'): - return self._config_getter(path, default) - return default - - def set_config_getter(self, config_getter): - """Set the config getter function from the main bot""" - self._config_getter = config_getter - + self.load_database() + def load_database(self): """Load player data from JSON file""" - if os.path.exists(self.db_file): - try: + try: + if os.path.exists(self.db_file): 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.logger.info(f"Loaded {len(self.players)} players from {self.db_file}") + else: self.players = {} - else: + self.logger.info(f"No existing database found, starting fresh") + except Exception as e: + self.logger.error(f"Error loading database: {e}") 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""" + """Save all player data to JSON file""" try: - temp_file = f"{self.db_file}.tmp" data = { 'players': self.players, 'last_save': str(time.time()) } - with open(temp_file, 'w') as f: + + # Create backup + if os.path.exists(self.db_file): + backup_file = f"{self.db_file}.backup" + try: + with open(self.db_file, 'r') as src, open(backup_file, 'w') as dst: + dst.write(src.read()) + except Exception as e: + self.logger.warning(f"Failed to create backup: {e}") + + # Save main file + with open(self.db_file, 'w') as f: json.dump(data, f, indent=2) - - 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 _get_starting_accuracy(self): - """Get starting accuracy with optional randomization""" - base_accuracy = self.get_config('new_players.starting_accuracy', 65) or 65 - if self.get_config('new_players.random_stats.enabled', False): - import random - variance = self.get_config('new_players.random_stats.accuracy_variance', 10) or 10 - return max(10, min(95, base_accuracy + random.randint(-variance, variance))) - return base_accuracy - - def _get_starting_reliability(self): - """Get starting reliability with optional randomization""" - base_reliability = self.get_config('new_players.starting_reliability', 70) or 70 - if self.get_config('new_players.random_stats.enabled', False): - import random - variance = self.get_config('new_players.random_stats.reliability_variance', 10) or 10 - return max(10, min(95, base_reliability + random.randint(-variance, variance))) - return base_reliability - - def get_player(self, user): + self.logger.error(f"Error saving database: {e}") + + def get_player(self, nick): """Get player data, creating if doesn't exist""" - if '!' not in user: - nick = user.lower() - else: - nick = user.split('!')[0].lower() - - if nick in self.players: - player = self.players[nick] - self._ensure_player_fields(player) - return player - else: - return self.create_player(nick) + nick_lower = nick.lower() + + if nick_lower not in self.players: + self.players[nick_lower] = self.create_player(nick) + + return self.players[nick_lower] def create_player(self, nick): - """Create a new player with default stats""" - player = { - 'shots': 6, - 'max_shots': 6, + """Create a new player with basic stats""" + return { + 'nick': nick, + 'xp': 0, + 'ducks_shot': 0, + 'ammo': 6, + 'max_ammo': 6, 'chargers': 2, 'max_chargers': 2, - 'reload_time': 5.0, - 'ducks_shot': 0, - 'ducks_befriended': 0, - 'accuracy_bonus': 0, - 'xp_bonus': 0, - 'charm_bonus': 0, - 'exp': 0, - 'money': 100, - 'last_hunt': 0, - 'last_reload': 0, - 'level': 1, - 'inventory': {}, - 'ignored_users': [], - 'jammed': False, - 'jammed_count': 0, - 'total_ammo_used': 0, - 'shot_at': 0, - 'wild_shots': 0, - 'accuracy': self._get_starting_accuracy(), - 'reliability': self._get_starting_reliability(), - 'gun_confiscated': False, - 'confiscated_count': 0 - } - - self.players[nick] = player - self.logger.info(f"Created new player: {nick}") - return player - - def _ensure_player_fields(self, player): - """Ensure player has all required fields for backward compatibility""" - required_fields = { - 'shots': player.get('ammo', 6), - 'max_shots': player.get('max_ammo', 6), - 'chargers': player.get('chargers', 2), - 'max_chargers': player.get('max_chargers', 2), - 'reload_time': 5.0, - 'ducks_shot': player.get('caught', 0), - 'ducks_befriended': player.get('befriended', 0), - 'accuracy_bonus': 0, - 'xp_bonus': 0, - 'charm_bonus': 0, - 'exp': player.get('xp', 0), - 'money': player.get('coins', 100), - 'last_hunt': 0, - 'last_reload': 0, - 'level': 1, - 'inventory': {}, - 'ignored_users': [], - 'jammed': False, - 'jammed_count': player.get('jammed_count', 0), - 'total_ammo_used': player.get('total_ammo_used', 0), - 'shot_at': player.get('shot_at', 0), - 'wild_shots': player.get('wild_shots', 0), - 'accuracy': player.get('accuracy', 65), - 'reliability': player.get('reliability', 70), - 'gun_confiscated': player.get('gun_confiscated', False), - 'confiscated_count': player.get('confiscated_count', 0) - } - - for field, default_value in required_fields.items(): - if field not in player: - player[field] = default_value - - def save_player(self, user): - """Save player data - batch saves for performance""" - if not self._save_pending: - self._save_pending = True - asyncio.create_task(self._delayed_save()) - - async def _delayed_save(self): - """Batch save to reduce disk I/O""" - await asyncio.sleep(0.5) - try: - self.save_database() - self.logger.debug("Database batch save completed") - except Exception as e: - self.logger.error(f"Database batch save failed: {e}") - finally: - self._save_pending = False \ No newline at end of file + 'accuracy': 65, + 'gun_confiscated': False + } \ No newline at end of file diff --git a/src/duckhuntbot.py b/src/duckhuntbot.py index e2c139c..edfb569 100644 --- a/src/duckhuntbot.py +++ b/src/duckhuntbot.py @@ -1,5 +1,6 @@ """ -Main DuckHunt IRC Bot +Simplified DuckHunt IRC Bot - Core Features Only +Commands: !bang, !reload, !shop, !duckhelp, !rearm, !disarm, !ignore, !unignore, !ducklaunch """ import asyncio @@ -14,14 +15,14 @@ import signal from typing import Optional from .logging_utils import setup_logger -from .utils import parse_message, InputValidator +from .utils import parse_irc_message, InputValidator, MessageManager from .db import DuckDB from .game import DuckGame from .sasl import SASLHandler class DuckHuntBot: - """Main IRC Bot for DuckHunt game""" + """Simplified IRC Bot for DuckHunt game""" def __init__(self, config): self.config = config @@ -33,30 +34,18 @@ class DuckHuntBot: self.shutdown_requested = False self.db = DuckDB() - self.db.set_config_getter(self.get_config) self.game = DuckGame(self, self.db) + self.messages = MessageManager() self.sasl_handler = SASLHandler(self, config) self.admins = [admin.lower() for admin in self.config.get('admins', ['colby'])] - self.ignored_nicks = set() - self.duck_spawn_times = {} - self.channel_records = {} - - self.dropped_items = {} - - self.colors = { - 'red': '\x0304', - 'green': '\x0303', - 'yellow': '\x0308', - 'blue': '\x0302', - 'cyan': '\x0311', - 'magenta': '\x0306', - 'white': '\x0300', - 'bold': '\x02', - 'reset': '\x03', - 'underline': '\x1f' + # Simple shop items - hardcoded + self.shop_items = { + 1: {"name": "Extra Shots", "price": 10, "description": "5 extra shots"}, + 2: {"name": "Accuracy Boost", "price": 20, "description": "+10% accuracy"}, + 3: {"name": "Lucky Charm", "price": 30, "description": "+5% duck spawn chance"} } def get_config(self, path, default=None): @@ -69,994 +58,426 @@ class DuckHuntBot: else: return default return value - - async def connect(self): - """Connect to IRC server""" - try: - ssl_context = None - if self.config.get('ssl', False): - ssl_context = ssl.create_default_context() - - self.reader, self.writer = await asyncio.open_connection( - self.config['server'], - self.config['port'], - ssl=ssl_context - ) - - self.logger.info(f"Connected to {self.config['server']}:{self.config['port']}") - - if self.config.get('password'): - self.send_raw(f"PASS {self.config['password']}") - - await self.register_user() - - except Exception as e: - self.logger.error(f"Connection failed: {e}") - raise - - async def register_user(self): - """Register user with IRC server""" - nick = self.config['nick'] - self.send_raw(f'NICK {nick}') - self.send_raw(f'USER {nick} 0 * :DuckHunt Bot') - - 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()) - except Exception as e: - self.logger.error(f"Error sending message: {e}") - - def send_message(self, target, msg): - """Send message to target (channel or user)""" - self.send_raw(f'PRIVMSG {target} :{msg}') - + 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 - - def get_random_player_for_friendly_fire(self, shooter_nick): - """Get random player for friendly fire accident""" - other_players = [nick for nick in self.db.players.keys() - if nick.lower() != shooter_nick.lower()] - if other_players: - return random.choice(other_players) - return None - - async def send_user_message(self, nick, channel, message, message_type='default'): - """Send message to user respecting their output mode preferences""" - player = self.db.get_player(f"{nick}!user@host") - if not player: - self.send_message(channel, f"{nick} > {message}") - return - - force_public_types = self.get_config('message_output.force_public', {}) or {} - if force_public_types.get(message_type, False): - self.send_message(channel, f"{nick} > {message}") - return - - output_mode = player.get('settings', {}).get('output_mode', 'PUBLIC') - - if output_mode == 'NOTICE': - self.send_raw(f'NOTICE {nick} :{message}') - elif output_mode == 'PRIVMSG': - self.send_message(nick, message) - else: - self.send_message(channel, f"{nick} > {message}") - - async def auto_rearm_confiscated_guns(self, channel, shooter_nick): - """Auto-rearm confiscated guns when someone shoots a duck""" - if not self.get_config('weapons.auto_rearm_on_duck_shot', True): - return - - rearmed_players = [] - for nick, player in self.db.players.items(): - if player.get('gun_confiscated', False): - player['gun_confiscated'] = False - player['ammo'] = player.get('max_ammo', 6) - player['chargers'] = player.get('max_chargers', 2) - rearmed_players.append(nick) - - if rearmed_players: - self.logger.info(f"Auto-rearmed guns for: {', '.join(rearmed_players)}") - self.send_message(channel, - f"{self.colors['green']}Guns have been returned to all hunters! " - f"({len(rearmed_players)} players rearmed){self.colors['reset']}") - self.db.save_database() - + def setup_signal_handlers(self): """Setup signal handlers for graceful shutdown""" def signal_handler(signum, frame): - self.logger.info(f"Received signal {signum}, shutting down...") + signal_name = "SIGINT" if signum == signal.SIGINT else "SIGTERM" + self.logger.info(f"🛑 Received {signal_name} (Ctrl+C), initiating graceful shutdown...") self.shutdown_requested = True - for task in asyncio.all_tasks(): - if not task.done(): - task.cancel() - + signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGTERM, signal_handler) - if hasattr(signal, 'SIGHUP'): - signal.signal(signal.SIGHUP, signal_handler) - + + async def connect(self): + """Connect to IRC server""" + try: + ssl_context = ssl.create_default_context() if self.config.get('ssl', False) else None + self.reader, self.writer = await asyncio.open_connection( + self.config['server'], + self.config['port'], + ssl=ssl_context + ) + self.logger.info(f"Connected to {self.config['server']}:{self.config['port']}") + except Exception as e: + self.logger.error(f"Failed to connect: {e}") + raise + + def send_raw(self, msg): + """Send raw IRC message""" + if self.writer: + self.writer.write(f"{msg}\r\n".encode('utf-8')) + + def send_message(self, target, msg): + """Send message to target (channel or user)""" + self.send_raw(f"PRIVMSG {target} :{msg}") + + async def register_user(self): + """Register user with IRC server""" + if self.config.get('password'): + self.send_raw(f"PASS {self.config['password']}") + + self.send_raw(f"NICK {self.config['nick']}") + self.send_raw(f"USER {self.config['nick']} 0 * :{self.config['nick']}") + async def handle_message(self, prefix, command, params, trailing): """Handle incoming IRC messages""" - try: - if command == '001': - self.registered = True - self.logger.info("Successfully registered with IRC server") - - for channel in self.config['channels']: - self.send_raw(f'JOIN {channel}') - - elif command == 'JOIN': - if params and prefix.split('!')[0] == self.config['nick']: - channel = params[0] - self.channels_joined.add(channel) - self.logger.info(f"Joined channel: {channel}") - - elif command == 'PRIVMSG': - if len(params) >= 1: - target = params[0] - message = trailing - - if message.startswith('!') or target == self.config['nick']: - await self.handle_command(prefix, target, message) - - elif command == 'PING': - self.send_raw(f'PONG :{trailing}') - - elif 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']: - await self.sasl_handler.handle_sasl_result(command, params, trailing) - - except Exception as e: - self.logger.error(f"Error handling message: {e}") + if command == "001": # Welcome message + self.registered = True + self.logger.info("Successfully registered with IRC server") + # Join channels + for channel in self.config.get('channels', []): + self.send_raw(f"JOIN {channel}") + self.channels_joined.add(channel) + + elif command == "PRIVMSG": + if len(params) >= 1: + target = params[0] + message = trailing or "" + await self.handle_command(prefix, target, message) + + elif command == "PING": + self.send_raw(f"PONG :{trailing}") + async def handle_command(self, user, channel, message): """Handle bot commands""" - if not user: - return - - try: - nick = user.split('!')[0] - nick_lower = nick.lower() - - if not InputValidator.validate_nickname(nick): - return - - if nick_lower in self.ignored_nicks: - return - - message = InputValidator.sanitize_message(message) - if not message: - return - - is_private = channel == self.config['nick'] - response_target = nick if is_private else channel - - if message.startswith('!'): - cmd_parts = message[1:].split() - else: - cmd_parts = message.split() - - if not cmd_parts: - return - - cmd = cmd_parts[0].lower() - args = cmd_parts[1:] if len(cmd_parts) > 1 else [] - - player = self.db.get_player(user) - if not player: - return - - await self.process_command(nick, response_target, cmd, args, player, user) - - except Exception as e: - self.logger.error(f"Error in command handler: {e}") - - async def process_command(self, nick, target, cmd, args, player, user): - """Process individual commands""" - if cmd == 'bang': - await self.handle_bang(nick, target, player) - elif cmd == 'reload': - await self.handle_reload(nick, target, player) - elif cmd == 'bef' or cmd == 'befriend': - await self.handle_befriend(nick, target, player) - elif cmd == 'duckstats': - await self.handle_duckstats(nick, target, player) - elif cmd == 'shop': - await self.handle_shop(nick, target, player) - elif cmd == 'sell': - if args: - await self.handle_sell(nick, target, args[0], player) - else: - await self.send_user_message(nick, target, "Usage: !sell ") - elif cmd == 'use': - if args: - target_nick = args[1] if len(args) > 1 else None - await self.handle_use(nick, target, args[0], player, target_nick) - else: - await self.send_user_message(nick, target, "Usage: !use [target_player]") - elif cmd == 'duckhelp': - await self.handle_duckhelp(nick, target) - elif cmd == 'ignore': - if args: - await self.handle_ignore(nick, target, args[0]) - else: - await self.send_user_message(nick, target, "Usage: !ignore ") - elif cmd == 'delignore': - if args: - await self.handle_delignore(nick, target, args[0]) - else: - await self.send_user_message(nick, target, "Usage: !delignore ") - elif cmd == 'topduck': - await self.handle_topduck(nick, target) - elif cmd == 'snatch': - await self.handle_snatch(nick, target, player) - elif cmd == 'rearm' and self.is_admin(user): - target_nick = args[0] if args else None - await self.handle_rearm(nick, target, player, target_nick) - elif cmd == 'disarm' and self.is_admin(user): - target_nick = args[0] if args else None - await self.handle_disarm(nick, target, target_nick) - elif cmd == 'ducklaunch' and self.is_admin(user): - await self.handle_ducklaunch(nick, target) - elif cmd == 'reset' and self.is_admin(user): - if len(args) >= 2 and args[1] == 'confirm': - await self.handle_reset_confirm(nick, target, args[0]) - elif args: - await self.handle_reset(nick, target, args[0]) - else: - await self.send_user_message(nick, target, "Usage: !reset [confirm]") - else: - pass - - async def handle_bang(self, nick, channel, player): - """Handle !bang command - shoot at duck (eggdrop style)""" - if player.get('gun_confiscated', False): - await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot shoot.") - return - - if player.get('jammed', False): - message = f"{nick} > Gun jammed! Use !reload" - await self.send_user_message(nick, channel, message) - return - - if player['shots'] <= 0: - message = f"{nick} > *click* You're out of ammo! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" - await self.send_user_message(nick, channel, message) + if not message.startswith('!'): return + parts = message[1:].split() + if not parts: + return + + cmd = parts[0].lower() + args = parts[1:] if len(parts) > 1 else [] + nick = user.split('!')[0] if '!' in user else user + + player = self.db.get_player(nick) + + # Check if player is ignored (unless it's an admin) + if player.get('ignored', False) and not self.is_admin(user): + return + + if cmd == "bang": + await self.handle_bang(nick, channel, player) + elif cmd == "reload": + await self.handle_reload(nick, channel, player) + elif cmd == "shop": + await self.handle_shop(nick, channel, player) + elif cmd == "duckhelp": + await self.handle_duckhelp(nick, channel, player) + elif cmd == "rearm" and self.is_admin(user): + await self.handle_rearm(nick, channel, args) + elif cmd == "disarm" and self.is_admin(user): + await self.handle_disarm(nick, channel, args) + elif cmd == "ignore" and self.is_admin(user): + await self.handle_ignore(nick, channel, args) + elif cmd == "unignore" and self.is_admin(user): + await self.handle_unignore(nick, channel, args) + elif cmd == "ducklaunch" and self.is_admin(user): + await self.handle_ducklaunch(nick, channel, args) + + async def handle_bang(self, nick, channel, player): + """Handle !bang command""" + # Check if gun is confiscated + if player.get('gun_confiscated', False): + message = self.messages.get('bang_not_armed', nick=nick) + self.send_message(channel, message) + return + + # Check ammo + if player['ammo'] <= 0: + message = self.messages.get('bang_no_ammo', nick=nick) + self.send_message(channel, message) + return + + # Check for duck if channel not in self.game.ducks or not self.game.ducks[channel]: - player['shots'] -= 1 - player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 - player['wild_shots'] = player.get('wild_shots', 0) + 1 - - if self.game.gun_jams(player): - player['jammed'] = True - player['jammed_count'] = player.get('jammed_count', 0) + 1 - message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? *click* Gun jammed! | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}" - else: - message = f"{nick} > *BANG* You shot at nothing! What were you aiming at? | 0 xp | {self.colors['red']}GUN CONFISCATED{self.colors['reset']}" - + # Wild shot - gun confiscated + player['ammo'] -= 1 player['gun_confiscated'] = True - player['confiscated_count'] = player.get('confiscated_count', 0) + 1 - + message = self.messages.get('bang_no_duck', nick=nick) self.send_message(channel, message) self.db.save_database() return - duck = self.game.ducks[channel][0] - player['shots'] -= 1 - player['total_ammo_used'] = player.get('total_ammo_used', 0) + 1 - player['shot_at'] = player.get('shot_at', 0) + 1 + # Shoot at duck + player['ammo'] -= 1 - if self.game.gun_jams(player): - player['jammed'] = True - player['jammed_count'] = player.get('jammed_count', 0) + 1 - message = f"{nick} > *BANG* *click* Gun jammed while shooting! | Ammo: {player['shots']}/{player['max_shots']}" - self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") + # Calculate hit chance + hit_chance = player.get('accuracy', 65) / 100.0 + if random.random() < hit_chance: + # Hit! Remove the duck + duck = self.game.ducks[channel].pop(0) + xp_gained = 10 + player['xp'] = player.get('xp', 0) + xp_gained + player['ducks_shot'] = player.get('ducks_shot', 0) + 1 + player['accuracy'] = min(player.get('accuracy', 65) + 1, 100) + + message = self.messages.get('bang_hit', + nick=nick, + xp_gained=xp_gained, + ducks_shot=player['ducks_shot']) + self.send_message(channel, message) else: - hit_chance = min(0.7 + (player.get('accuracy', 0) * 0.001), 0.95) - if random.random() < hit_chance: - await self.handle_duck_hit(nick, channel, player, duck) - else: - await self.handle_duck_miss(nick, channel, player) + # Miss! Duck stays in the channel + player['accuracy'] = max(player.get('accuracy', 65) - 2, 10) + message = self.messages.get('bang_miss', nick=nick) + self.send_message(channel, message) self.db.save_database() - async def handle_duck_hit(self, nick, channel, player, duck): - """Handle successful duck hit (eggdrop style)""" - self.game.ducks[channel].remove(duck) - - shot_time = time.time() - reaction_time = shot_time - duck.get('spawn_time', shot_time) - - points_earned = duck['points'] - xp_earned = duck['xp'] - - if reaction_time < 2.0: - quick_bonus = int(points_earned * 0.5) - points_earned += quick_bonus - quick_shot_msg = f" [Quick shot bonus: +{quick_bonus}]" - else: - quick_shot_msg = "" - - xp_earned = int(xp_earned * (1 + player.get('xp_bonus', 0) * 0.001)) - - player['ducks_shot'] += 1 - player['exp'] += xp_earned - player['money'] += points_earned - player['last_hunt'] = time.time() - - current_accuracy = player.get('accuracy', 65) - player['accuracy'] = min(current_accuracy + 1, 95) - - if 'best_time' not in player or reaction_time < player['best_time']: - player['best_time'] = reaction_time - - player['total_reflex_time'] = player.get('total_reflex_time', 0) + reaction_time - player['reflex_shots'] = player.get('reflex_shots', 0) + 1 - - await self.check_level_up(nick, channel, player) - - message = f"{nick} > *BANG* you shot down the duck in {reaction_time:.2f} seconds. \\_X< *KWAK* [+{xp_earned} xp] [TOTAL DUCKS: {player['ducks_shot']}]" - self.send_message(channel, f"{self.colors['green']}{message}{self.colors['reset']}") - - if random.random() < 0.1: - await self.drop_random_item(nick, channel) - - async def handle_duck_miss(self, nick, channel, player): - """Handle duck miss (eggdrop style)""" - current_accuracy = player.get('accuracy', 65) - player['accuracy'] = max(current_accuracy - 2, 10) - - player['missed'] = player.get('missed', 0) + 1 - - message = f"{nick} > *BANG* You missed the duck! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" - self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") - - if channel in self.game.ducks and len(self.game.ducks[channel]) > 1: - for other_duck in self.game.ducks[channel][:]: - if random.random() < 0.2: - self.game.ducks[channel].remove(other_duck) - self.send_message(channel, f"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The other ducks fly away, scared by the noise!") - async def handle_reload(self, nick, channel, player): - """Handle reload command (eggdrop style) - reload ammo and clear jams""" - current_time = time.time() - + """Handle !reload command""" if player.get('gun_confiscated', False): - await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! You cannot reload.") + message = self.messages.get('reload_not_armed', nick=nick) + self.send_message(channel, message) return - if player.get('jammed', False): - player['jammed'] = False - player['last_reload'] = current_time - - message = f"{nick} > *click click* You unjammed your gun! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" - self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") - self.db.save_database() - return - - if player['shots'] >= player['max_shots']: - message = f"{nick} > Gun is already fully loaded! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" - await self.send_user_message(nick, channel, message) + if player['ammo'] >= player.get('max_ammo', 6): + message = self.messages.get('reload_already_loaded', nick=nick) + self.send_message(channel, message) return - if player.get('chargers', 0) <= 0: - message = f"{nick} > No chargers left to reload with! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: 0/{player.get('max_chargers', 2)}" - await self.send_user_message(nick, channel, message) + if player.get('chargers', 2) <= 0: + message = self.messages.get('reload_no_chargers', nick=nick) + self.send_message(channel, message) return - if current_time - player.get('last_reload', 0) < player['reload_time']: - remaining = int(player['reload_time'] - (current_time - player.get('last_reload', 0))) - message = f"{nick} > Reload cooldown: {remaining} seconds remaining | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player.get('chargers', 0)}/{player.get('max_chargers', 2)}" - await self.send_user_message(nick, channel, message) - return - - old_shots = player['shots'] - player['shots'] = player['max_shots'] - player['chargers'] = max(0, player.get('chargers', 2) - 1) - player['last_reload'] = current_time - shots_added = player['shots'] - old_shots - - message = f"{nick} > *click clack* Reloaded! +{shots_added} shots | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player['chargers']}/{player.get('max_chargers', 2)}" - - self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") - - self.db.save_database() - - async def handle_befriend(self, nick, channel, player): - """Handle !bef command - befriend a duck""" - if channel not in self.game.ducks or not self.game.ducks[channel]: - await self.send_user_message(nick, channel, "There are no ducks to befriend!") - return - - duck = self.game.ducks[channel][0] - - befriend_chance = 0.5 + (player.get('charm_bonus', 0) * 0.001) - - if random.random() < befriend_chance: - self.game.ducks[channel].remove(duck) - - xp_earned = duck['xp'] - friendship_bonus = duck['points'] // 2 - - player['exp'] += xp_earned - player['money'] += friendship_bonus - player['ducks_befriended'] += 1 - - await self.check_level_up(nick, channel, player) - - effects = [ - ("luck", 10, "You feel lucky!"), - ("charm_bonus", 5, "The duck teaches you about friendship!"), - ("accuracy_bonus", 3, "The duck gives you aiming tips!") - ] - - if random.random() < 0.3: - effect, amount, message = random.choice(effects) - player[effect] = player.get(effect, 0) + amount - bonus_msg = f" {message}" - else: - bonus_msg = "" - - message = (f"{nick} befriended a {duck['type']} duck! " - f"+{friendship_bonus} coins, +{xp_earned} XP.{bonus_msg}") - self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") - - if random.random() < 0.15: - await self.award_random_item(nick, channel, player) - else: - miss_messages = [ - f"The {duck['type']} duck doesn't trust you yet!", - f"The {duck['type']} duck flies away from you!", - f"You need to be more patient with the {duck['type']} duck!", - f"The {duck['type']} duck looks at you suspiciously!" - ] - - message = f"{nick} {random.choice(miss_messages)}" - self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}") - - player['charm_bonus'] = max(player.get('charm_bonus', 0) - 1, -50) + player['ammo'] = player.get('max_ammo', 6) + player['chargers'] = player.get('chargers', 2) - 1 + message = self.messages.get('reload_success', + nick=nick, + ammo=player['ammo'], + max_ammo=player.get('max_ammo', 6), + chargers=player['chargers']) + self.send_message(channel, message) self.db.save_database() async def handle_shop(self, nick, channel, player): - """Handle shop command""" - shop_items = [ - "=== DUCK HUNT SHOP ===", - "1. Extra Shots (3) - $50", - "2. Faster Reload - $100", - "3. Accuracy Charm - $75", - "4. Lucky Charm - $125", - "5. Friendship Bracelet - $80", - "6. Duck Caller - $200", - "7. Camouflage - $150", - "8. Energy Drink - $60", - "==================", - f"Your money: ${player['money']}", - "Use !use to purchase/use items" - ] - for line in shop_items: - await self.send_user_message(nick, channel, line) + """Handle !shop command""" + items = [] + for item_id, item in self.shop_items.items(): + item_text = self.messages.get('shop_item_format', + id=item_id, + name=item['name'], + price=item['price']) + items.append(item_text) + + shop_text = self.messages.get('shop_display', + items=" | ".join(items), + xp=player.get('xp', 0)) + + self.send_message(channel, shop_text) - async def handle_sell(self, nick, channel, item_id, player): - """Handle sell command""" - try: - item_id = int(item_id) - except ValueError: - await self.send_user_message(nick, channel, "Invalid item ID!") - return - - if 'inventory' not in player: - player['inventory'] = {} - - item_key = str(item_id) - if item_key not in player['inventory'] or player['inventory'][item_key] <= 0: - await self.send_user_message(nick, channel, "You don't have that item!") - return - - shop_items = { - 1: {'name': 'Extra Shots', 'price': 50}, - 2: {'name': 'Faster Reload', 'price': 100}, - 3: {'name': 'Accuracy Charm', 'price': 75}, - 4: {'name': 'Lucky Charm', 'price': 125}, - 5: {'name': 'Friendship Bracelet', 'price': 80}, - 6: {'name': 'Duck Caller', 'price': 200}, - 7: {'name': 'Camouflage', 'price': 150}, - 8: {'name': 'Energy Drink', 'price': 60} - } - item_info = shop_items.get(item_id) - if not item_info: - await self.send_user_message(nick, channel, "Invalid item!") - return - - player['inventory'][item_key] -= 1 - if player['inventory'][item_key] <= 0: - del player['inventory'][item_key] - - sell_price = item_info['price'] // 2 - player['money'] += sell_price - - message = f"Sold {item_info['name']} for ${sell_price}!" - await self.send_user_message(nick, channel, message) - - self.db.save_database() - - async def handle_use(self, nick, channel, item_id, player, target_nick=None): - """Handle use command""" - try: - item_id = int(item_id) - except ValueError: - await self.send_user_message(nick, channel, "Invalid item ID!") - return - - if 'inventory' not in player: - player['inventory'] = {} - - shop_items = { - 1: {'name': 'Extra Shots', 'price': 50, 'consumable': True}, - 2: {'name': 'Faster Reload', 'price': 100, 'consumable': True}, - 3: {'name': 'Accuracy Charm', 'price': 75, 'consumable': False}, - 4: {'name': 'Lucky Charm', 'price': 125, 'consumable': False}, - 5: {'name': 'Friendship Bracelet', 'price': 80, 'consumable': False}, - 6: {'name': 'Duck Caller', 'price': 200, 'consumable': True}, - 7: {'name': 'Camouflage', 'price': 150, 'consumable': True}, - 8: {'name': 'Energy Drink', 'price': 60, 'consumable': True} - } - - item_key = str(item_id) - item_info = shop_items.get(item_id) - - if not item_info: - await self.send_user_message(nick, channel, "Invalid item ID!") - return - - if item_key in player['inventory'] and player['inventory'][item_key] > 0: - await self.use_item_effect(player, item_id, nick, channel, target_nick) - player['inventory'][item_key] -= 1 - if player['inventory'][item_key] <= 0: - del player['inventory'][item_key] - else: - if player['money'] >= item_info['price']: - if item_info.get('consumable', True): - player['money'] -= item_info['price'] - await self.use_item_effect(player, item_id, nick, channel, target_nick) - else: - player['money'] -= item_info['price'] - player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 - await self.send_user_message(nick, channel, f"Purchased {item_info['name']}!") - else: - await self.send_user_message(nick, channel, - f"Not enough money! Need ${item_info['price']}, you have ${player['money']}") - return - - self.db.save_database() - - async def handle_topduck(self, nick, channel): - """Handle topduck command - show leaderboard""" - sorted_players = sorted( - [(name, data) for name, data in self.db.players.items()], - key=lambda x: x[1]['ducks_shot'], - reverse=True - ) - - if not sorted_players: - await self.send_user_message(nick, channel, "No players found!") - return - - await self.send_user_message(nick, channel, "=== TOP DUCK HUNTERS ===") - for i, (name, data) in enumerate(sorted_players[:5], 1): - stats = f"{i}. {name}: {data['ducks_shot']} ducks (Level {data['level']})" - await self.send_user_message(nick, channel, stats) - - async def handle_snatch(self, nick, channel, player): - """Handle snatch command - grab dropped items competitively""" - import time - - if channel not in self.dropped_items or not self.dropped_items[channel]: - await self.send_user_message(nick, channel, f"{nick} > There are no items to snatch!") - return - - item = self.dropped_items[channel].pop(0) - - current_time = time.time() - if current_time - item['timestamp'] > 60: - await self.send_user_message(nick, channel, f"{nick} > The item has disappeared!") - self.dropped_items[channel] = [ - i for i in self.dropped_items[channel] - if current_time - i['timestamp'] <= 60 - ] - return - - if 'inventory' not in player: - player['inventory'] = {} - - item_key = item['item_id'] - player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 - - message = f"{nick} snatched a {item['item_name']}! ⚡" - self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") - - async def handle_rearm(self, nick, channel, player, target_nick=None): - """Handle rearm command - restore confiscated guns""" - if target_nick: - target_player = self.db.get_player(target_nick.lower()) - if target_player: - target_player['gun_confiscated'] = False - target_player['shots'] = target_player['max_shots'] - target_player['chargers'] = target_player.get('max_chargers', 2) - target_player['jammed'] = False - target_player['last_reload'] = 0 - message = f"{nick} returned {target_nick}'s confiscated gun! | Ammo: {target_player['shots']}/{target_player['max_shots']} | Chargers: {target_player['chargers']}/{target_player.get('max_chargers', 2)}" - self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") - else: - await self.send_user_message(nick, channel, "Player not found!") - else: - if not player.get('gun_confiscated', False): - await self.send_user_message(nick, channel, f"{nick} > Your gun is not confiscated!") - return - - if self.is_admin(nick): - player['gun_confiscated'] = False - player['shots'] = player['max_shots'] - player['chargers'] = player.get('max_chargers', 2) - player['jammed'] = False - player['last_reload'] = 0 - message = f"{nick} > Gun returned by admin! | Ammo: {player['shots']}/{player['max_shots']} | Chargers: {player['chargers']}/{player.get('max_chargers', 2)}" - self.send_message(channel, f"{self.colors['cyan']}{message}{self.colors['reset']}") - else: - await self.send_user_message(nick, channel, f"{nick} > Your gun has been confiscated! Wait for an admin or automatic return.") - - self.db.save_database() - - async def handle_disarm(self, nick, channel, target_nick): - """Handle disarm command (admin only)""" - target_player = self.db.get_player(target_nick.lower()) - if target_player: - target_player['shots'] = 0 - message = f"Admin {nick} disarmed {target_nick}!" - self.send_message(channel, f"{self.colors['red']}{message}{self.colors['reset']}") - - self.db.save_database() - else: - await self.send_user_message(nick, channel, "Player not found!") - - async def handle_ducklaunch(self, nick, channel): - """Handle !ducklaunch admin command""" - duck = await self.game.spawn_duck_now(channel) - if duck: - self.send_message(channel, - f"{self.colors['green']}Admin {nick} launched a duck!{self.colors['reset']}") - else: - await self.send_user_message(nick, channel, "Failed to spawn duck (channel may be full)!") - - async def handle_duckstats(self, nick, channel, player): - """Handle duckstats command""" - stats_msg = ( - f"{nick}'s duck hunting stats: " - f"Level {player['level']} | " - f"Ducks shot: {player['ducks_shot']} | " - f"Befriended: {player['ducks_befriended']} | " - f"Money: ${player['money']} | " - f"XP: {player['exp']}/{self.get_xp_for_level(player['level'] + 1)}" - ) - await self.send_user_message(nick, channel, stats_msg) - - if 'inventory' in player and player['inventory']: - shop_items = { - 1: 'Extra Shots', 2: 'Faster Reload', 3: 'Accuracy Charm', 4: 'Lucky Charm', - 5: 'Friendship Bracelet', 6: 'Duck Caller', 7: 'Camouflage', 8: 'Energy Drink', - 9: 'Armor Vest', 10: 'Gunpowder', 11: 'Sight', 12: 'Silencer', - 13: 'Explosive Ammo', 14: 'Mirror', 15: 'Sunglasses', 16: 'Clothes', - 17: 'Grease', 18: 'Brush', 19: 'Sand', 20: 'Water', - 21: 'Sabotage Kit', 22: 'Life Insurance', 23: 'Decoy' - } - - inventory_items = [] - for item_id, quantity in player['inventory'].items(): - item_name = shop_items.get(int(item_id), f"Item {item_id}") - inventory_items.append(f"{item_name} x{quantity}") - - if inventory_items: - inventory_msg = f"Inventory: {', '.join(inventory_items)}" - await self.send_user_message(nick, channel, inventory_msg) - - async def handle_duckhelp(self, nick, channel): - """Handle duckhelp command""" + async def handle_duckhelp(self, nick, channel, player): + """Handle !duckhelp command""" help_lines = [ - "=== DUCK HUNT COMMANDS ===", - "!bang - Shoot at ducks", - "!reload - Reload your gun", - "!bef - Befriend a duck", - "!duckstats - View your statistics", - "!shop - View the shop", - "!inventory - View your items", - "!use - Use/buy shop items", - "!sell - Sell inventory items", - "!topduck - View leaderboard", - "!rearm - Quick reload (costs money)", - "!ducklaunch - Spawn duck (admin)", - "!disarm - Disarm player (admin)", - "!reset - Reset player (admin)", - "========================" + self.messages.get('help_header'), + self.messages.get('help_user_commands'), + self.messages.get('help_help_command') ] - for line in help_lines: - await self.send_user_message(nick, channel, line) - - async def handle_ignore(self, nick, channel, target_nick): - """Handle ignore command""" - if 'ignored_users' not in self.db.players[nick.lower()]: - self.db.players[nick.lower()]['ignored_users'] = [] - ignored_list = self.db.players[nick.lower()]['ignored_users'] - if target_nick.lower() not in ignored_list: - ignored_list.append(target_nick.lower()) - await self.send_user_message(nick, channel, f"Now ignoring {target_nick}") - self.db.save_database() - else: - await self.send_user_message(nick, channel, f"{target_nick} is already ignored") + # Add admin commands if user is admin + if self.is_admin(f"{nick}!user@host"): + help_lines.append(self.messages.get('help_admin_commands')) + + for line in help_lines: + self.send_message(channel, line) - async def handle_delignore(self, nick, channel, target_nick): - """Handle delignore command""" - if 'ignored_users' not in self.db.players[nick.lower()]: - await self.send_user_message(nick, channel, f"{target_nick} is not ignored") + async def handle_rearm(self, nick, channel, args): + """Handle !rearm command (admin only)""" + if args: + target = args[0].lower() + player = self.db.get_player(target) + player['gun_confiscated'] = False + player['ammo'] = player.get('max_ammo', 6) + player['chargers'] = 2 + message = self.messages.get('admin_rearm_player', target=target, admin=nick) + self.send_message(channel, message) + else: + # Rearm everyone + for player_data in self.db.players.values(): + player_data['gun_confiscated'] = False + player_data['ammo'] = player_data.get('max_ammo', 6) + player_data['chargers'] = 2 + message = self.messages.get('admin_rearm_all', admin=nick) + self.send_message(channel, message) + + self.db.save_database() + + async def handle_disarm(self, nick, channel, args): + """Handle !disarm command (admin only)""" + if not args: + message = self.messages.get('usage_disarm') + self.send_message(channel, message) return - ignored_list = self.db.players[nick.lower()]['ignored_users'] - if target_nick.lower() in ignored_list: - ignored_list.remove(target_nick.lower()) - await self.send_user_message(nick, channel, f"No longer ignoring {target_nick}") - self.db.save_database() - else: - await self.send_user_message(nick, channel, f"{target_nick} is not ignored") - - async def handle_reset(self, nick, channel, target_nick): - """Handle !reset admin command (requires confirmation)""" - await self.send_user_message(nick, channel, - f"⚠️ WARNING: This will completely reset {target_nick}'s progress! " - f"Use `!reset {target_nick} confirm` to proceed.") - - async def handle_reset_confirm(self, nick, channel, target_nick): - """Handle !reset confirm admin command""" - if target_nick.lower() in self.db.players: - del self.db.players[target_nick.lower()] - self.send_message(channel, - f"{self.colors['red']}Admin {nick} has reset {target_nick}'s progress!{self.colors['reset']}") - self.db.save_database() - else: - await self.send_user_message(nick, channel, "Player not found!") + target = args[0].lower() + player = self.db.get_player(target) + player['gun_confiscated'] = True + + message = self.messages.get('admin_disarm', target=target, admin=nick) + self.send_message(channel, message) + self.db.save_database() - async def check_level_up(self, nick, channel, player): - """Check if player leveled up""" - current_level = player['level'] - new_level = self.calculate_level(player['exp']) + async def handle_ignore(self, nick, channel, args): + """Handle !ignore command (admin only)""" + if not args: + message = self.messages.get('usage_ignore') + self.send_message(channel, message) + return - if new_level > current_level: - player['level'] = new_level - - player['max_shots'] = min(player['max_shots'] + 1, 10) - player['reload_time'] = max(player['reload_time'] - 0.5, 2.0) - - message = (f"🎉 {nick} leveled up to level {new_level}! " - f"Max shots: {player['max_shots']}, " - f"Reload time: {player['reload_time']}s") - self.send_message(channel, f"{self.colors['yellow']}{message}{self.colors['reset']}") + target = args[0].lower() + player = self.db.get_player(target) + player['ignored'] = True + + message = self.messages.get('admin_ignore', target=target, admin=nick) + self.send_message(channel, message) + self.db.save_database() - def calculate_level(self, exp): - """Calculate level from experience points""" - import math - return int(math.sqrt(exp / 100)) + 1 + async def handle_unignore(self, nick, channel, args): + """Handle !unignore command (admin only)""" + if not args: + message = self.messages.get('usage_unignore') + self.send_message(channel, message) + return + + target = args[0].lower() + player = self.db.get_player(target) + player['ignored'] = False + + message = self.messages.get('admin_unignore', target=target, admin=nick) + self.send_message(channel, message) + self.db.save_database() - def get_xp_for_level(self, level): - """Get XP required for a specific level""" - return (level - 1) ** 2 * 100 + async def handle_ducklaunch(self, nick, channel, args): + """Handle !ducklaunch command (admin only)""" + if channel not in self.channels_joined: + message = self.messages.get('admin_ducklaunch_not_enabled') + self.send_message(channel, message) + return + + # Force spawn a duck + if channel not in self.game.ducks: + self.game.ducks[channel] = [] + self.game.ducks[channel].append({"spawn_time": time.time()}) + admin_message = self.messages.get('admin_ducklaunch', admin=nick) + duck_message = self.messages.get('duck_spawn') + + self.send_message(channel, admin_message) + self.send_message(channel, duck_message) - async def drop_random_item(self, nick, channel): - """Drop a random item to the ground for competitive snatching""" - import time - - item_ids = [1, 2, 3, 4, 5, 6, 7, 8] - item_id = random.choice(item_ids) - item_key = str(item_id) - - item_names = { - '1': 'Extra Shots', '2': 'Faster Reload', '3': 'Accuracy Charm', - '4': 'Lucky Charm', '5': 'Friendship Bracelet', '6': 'Duck Caller', - '7': 'Camouflage', '8': 'Energy Drink' - } - - item_name = item_names.get(item_key, f'Item {item_id}') - - if channel not in self.dropped_items: - self.dropped_items[channel] = [] - - dropped_item = { - 'item_id': item_key, - 'item_name': item_name, - 'timestamp': time.time(), - 'dropper': nick - } - self.dropped_items[channel].append(dropped_item) - - message = f"🎁 A {item_name} has been dropped! Type !snatch to grab it!" - self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") - - async def award_random_item(self, nick, channel, player): - """Award a random item to player""" - if 'inventory' not in player: - player['inventory'] = {} - - item_ids = [1, 2, 3, 4, 5, 6, 7, 8] - item_id = random.choice(item_ids) - item_key = str(item_id) - - player['inventory'][item_key] = player['inventory'].get(item_key, 0) + 1 - - item_names = { - '1': 'Extra Shots', '2': 'Faster Reload', '3': 'Accuracy Charm', - '4': 'Lucky Charm', '5': 'Friendship Bracelet', '6': 'Duck Caller', - '7': 'Camouflage', '8': 'Energy Drink' - } - - item_name = item_names.get(item_key, f'Item {item_id}') - message = f"🎁 {nick} found a {item_name}!" - self.send_message(channel, f"{self.colors['magenta']}{message}{self.colors['reset']}") - async def use_item_effect(self, player, item_id, nick, channel, target_nick=None): - """Apply item effects""" - effects = { - 1: "Extra Shots! +3 shots", - 2: "Faster Reload! -1s reload time", - 3: "Accuracy Charm! +5 accuracy", - 4: "Lucky Charm! +10 luck", - 5: "Friendship Bracelet! +5 charm", - 6: "Duck Caller! Next duck spawns faster", - 7: "Camouflage! Ducks can't see you for 60s", - 8: "Energy Drink! +50 energy" - } - - if item_id == 1: - player['shots'] = min(player['shots'] + 3, player['max_shots']) - elif item_id == 2: - player['reload_time'] = max(player['reload_time'] - 1, 1) - elif item_id == 3: - player['accuracy_bonus'] = player.get('accuracy_bonus', 0) + 5 - elif item_id == 4: - player['luck'] = player.get('luck', 0) + 10 - elif item_id == 5: - player['charm_bonus'] = player.get('charm_bonus', 0) + 5 - elif item_id == 6: - pass - elif item_id == 7: - player['camouflaged_until'] = time.time() + 60 - elif item_id == 8: - player['energy'] = player.get('energy', 100) + 50 - - effect_msg = effects.get(item_id, "Unknown effect") - await self.send_user_message(nick, channel, f"Used item: {effect_msg}") - - async def cleanup_expired_items(self): - """Background task to clean up expired dropped items""" - import time - - while not self.shutdown_requested: - try: - current_time = time.time() - - for channel in list(self.dropped_items.keys()): - if channel in self.dropped_items: - original_count = len(self.dropped_items[channel]) - - self.dropped_items[channel] = [ - item for item in self.dropped_items[channel] - if current_time - item['timestamp'] <= 60 - ] - - removed_count = original_count - len(self.dropped_items[channel]) - if removed_count > 0: - self.logger.debug(f"Cleaned up {removed_count} expired items from {channel}") - - await asyncio.sleep(30) - - except Exception as e: - self.logger.error(f"Error in cleanup_expired_items: {e}") - await asyncio.sleep(30) - - async def run(self): - """Main bot run loop""" - tasks = [] + async def message_loop(self): + """Main message processing loop""" try: - self.setup_signal_handlers() - self.db.load_database() - await self.connect() - - tasks = [ - asyncio.create_task(self.message_loop()), - asyncio.create_task(self.game.spawn_ducks()), - asyncio.create_task(self.game.duck_timeout_checker()), - asyncio.create_task(self.cleanup_expired_items()), - ] - - try: - while not self.shutdown_requested: - for task in tasks: - if task.done() and task.exception(): - self.logger.error(f"Task failed: {task.exception()}") - self.shutdown_requested = True - break - - await asyncio.sleep(0.1) - - except asyncio.CancelledError: - self.logger.info("Main loop cancelled") - except KeyboardInterrupt: - self.logger.info("Keyboard interrupt received") - self.shutdown_requested = True + while not self.shutdown_requested and self.reader: + line = await self.reader.readline() + if not line: + break + line = line.decode('utf-8').strip() + if not line: + continue + + try: + prefix, command, params, trailing = parse_irc_message(line) + await self.handle_message(prefix, command, params, trailing) + except Exception as e: + self.logger.error(f"Error processing message '{line}': {e}") + + except asyncio.CancelledError: + self.logger.info("Message loop cancelled") except Exception as e: - self.logger.error(f"Bot error: {e}") - raise + self.logger.error(f"Message loop error: {e}") finally: - self.logger.info("Shutting down bot...") + self.logger.info("Message loop ended") + + async def run(self): + """Main bot loop with improved shutdown handling""" + self.setup_signal_handlers() + + game_task = None + message_task = None + + try: + await self.connect() + await self.register_user() - for task in tasks: + # Start game loops + game_task = asyncio.create_task(self.game.start_game_loops()) + message_task = asyncio.create_task(self.message_loop()) + shutdown_task = asyncio.create_task(self.shutdown_event.wait()) + + self.logger.info("🦆 Bot is now running! Press Ctrl+C to stop.") + + # Wait for shutdown signal or task completion + done, pending = await asyncio.wait( + [game_task, message_task, shutdown_task], + return_when=asyncio.FIRST_COMPLETED + ) + + if shutdown_task in done: + self.logger.info("🛑 Shutdown signal received, cleaning up...") + await self._graceful_shutdown() + + # Cancel remaining tasks + for task in pending: if not task.done(): task.cancel() try: await task except asyncio.CancelledError: - pass - except Exception as e: - self.logger.error(f"Error cancelling task: {e}") + self.logger.debug(f"Task cancelled: {task}") + except asyncio.TimeoutError: + self.logger.debug(f"Task timed out: {task}") + + except asyncio.CancelledError: + self.logger.info("🛑 Main loop cancelled") + except Exception as e: + self.logger.error(f"❌ Bot error: {e}") + finally: + self.logger.info("🔄 Final cleanup...") + # Ensure tasks are cancelled + for task in [game_task, message_task]: + if task and not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + # Final database save try: self.db.save_database() - self.logger.info("Database saved") + self.logger.info("💾 Database saved") except Exception as e: - self.logger.error(f"Error saving database: {e}") + self.logger.error(f"❌ Error saving database: {e}") + # Close IRC connection + await self._close_connection() + + self.logger.info("✅ Bot shutdown complete") + + async def _graceful_shutdown(self): + """Perform graceful shutdown steps""" + try: + # Send quit message to IRC if self.writer and not self.writer.is_closing(): - try: - self.send_raw("QUIT :Bot shutting down") - self.writer.close() - await self.writer.wait_closed() - self.logger.info("IRC connection closed") - except Exception as e: - self.logger.error(f"Error closing connection: {e}") - - self.logger.info("Bot shutdown complete") - - async def message_loop(self): - """Handle incoming IRC messages""" - while not self.shutdown_requested and self.reader: - try: - line = await asyncio.wait_for(self.reader.readline(), timeout=1.0) - if not line: - self.logger.warning("Empty line received, connection may be closed") - break - - line = line.decode().strip() - if line: - prefix, command, params, trailing = parse_message(line) - await self.handle_message(prefix, command, params, trailing) - - except asyncio.TimeoutError: - continue - except asyncio.CancelledError: - self.logger.info("Message loop cancelled") - break - except Exception as e: - self.logger.error(f"Message loop error: {e}") - break + self.logger.info("📤 Sending QUIT message to IRC...") + quit_message = self.config.get('quit_message', 'DuckHunt Bot shutting down') + self.send_raw(f"QUIT :{quit_message}") - self.logger.info("Message loop ended") \ No newline at end of file + # Give IRC server time to process quit + await asyncio.sleep(0.5) + + # Save database + self.logger.info("💾 Saving database...") + self.db.save_database() + + except Exception as e: + self.logger.error(f"❌ Error during graceful shutdown: {e}") + + async def _close_connection(self): + """Close IRC connection safely""" + if self.writer: + try: + if not self.writer.is_closing(): + self.writer.close() + await asyncio.wait_for(self.writer.wait_closed(), timeout=3.0) + self.logger.info("🔌 IRC connection closed") + except asyncio.TimeoutError: + self.logger.warning("⚠️ Connection close timed out") + except Exception as e: + self.logger.error(f"❌ Error closing connection: {e}") \ No newline at end of file diff --git a/src/game.py b/src/game.py index 6adea68..ae4942b 100644 --- a/src/game.py +++ b/src/game.py @@ -1,310 +1,105 @@ """ -Game mechanics for DuckHunt Bot +Simplified Game mechanics for DuckHunt Bot +Basic duck spawning and timeout only """ import asyncio import random import time -import uuid import logging -from typing import Dict, Any, Optional, List class DuckGame: - """Game mechanics and duck management""" + """Simplified game mechanics - just duck spawning""" def __init__(self, bot, db): self.bot = bot self.db = db - self.ducks = {} + self.ducks = {} # {channel: [duck1, duck2, ...]} self.logger = logging.getLogger('DuckHuntBot.Game') - - self.colors = { - 'red': '\x0304', - 'green': '\x0303', - 'yellow': '\x0308', - 'blue': '\x0302', - 'cyan': '\x0311', - 'magenta': '\x0306', - 'white': '\x0300', - 'bold': '\x02', - 'reset': '\x03', - 'underline': '\x1f' - } - - def get_config(self, path, default=None): - """Helper method to get config values""" - return self.bot.get_config(path, default) - - def get_player_level(self, xp): - """Calculate player level from XP""" - if xp < 0: - return 0 - return int((xp ** 0.5) / 2) + 1 + self.spawn_task = None + self.timeout_task = None - def get_xp_for_next_level(self, xp): - """Calculate XP needed for next level""" - level = self.get_player_level(xp) - return ((level * 2) ** 2) - xp - - def calculate_penalty_by_level(self, base_penalty, xp): - """Reduce penalties for higher level players""" - level = self.get_player_level(xp) - return max(1, base_penalty - (level - 1)) - - def update_karma(self, player, event): - """Update player karma based on events""" - if not self.get_config('karma.enabled', True): - return - - karma_changes = { - 'hit': self.get_config('karma.hit_bonus', 2), - 'golden_hit': self.get_config('karma.golden_hit_bonus', 5), - 'teamkill': -self.get_config('karma.teamkill_penalty', 10), - 'wild_shot': -self.get_config('karma.wild_shot_penalty', 3), - 'miss': -self.get_config('karma.miss_penalty', 1), - 'befriend_success': self.get_config('karma.befriend_success_bonus', 2), - 'befriend_fail': -self.get_config('karma.befriend_fail_penalty', 1) - } + async def start_game_loops(self): + """Start the game loops""" + self.spawn_task = asyncio.create_task(self.duck_spawn_loop()) + self.timeout_task = asyncio.create_task(self.duck_timeout_loop()) - if event in karma_changes: - player['karma'] = player.get('karma', 0) + karma_changes[event] + try: + await asyncio.gather(self.spawn_task, self.timeout_task) + except asyncio.CancelledError: + self.logger.info("Game loops cancelled") - def is_sleep_time(self): - """Check if ducks should not spawn due to sleep hours""" - sleep_hours = self.get_config('sleep_hours', []) - if not sleep_hours or len(sleep_hours) != 2: - return False - - import datetime - current_hour = datetime.datetime.now().hour - start_hour, end_hour = sleep_hours - - if start_hour <= end_hour: - return start_hour <= current_hour <= end_hour - else: - return current_hour >= start_hour or current_hour <= end_hour - - def calculate_gun_reliability(self, player): - """Calculate gun reliability with modifiers""" - base_reliability = player.get('reliability', 70) - return min(100, max(0, base_reliability)) - - def gun_jams(self, player): - """Check if gun jams (eggdrop style)""" - reliability = player.get('reliability', 70) - jam_chance = max(1, 101 - reliability) - - if player.get('total_ammo_used', 0) > 100: - jam_chance += 2 - - if player.get('jammed_count', 0) > 5: - jam_chance += 1 - - return random.randint(1, 100) <= jam_chance + async def duck_spawn_loop(self): + """Simple duck spawning loop""" + try: + while True: + # Wait random time between spawns + min_wait = self.bot.get_config('duck_spawn_min', 300) # 5 minutes + max_wait = self.bot.get_config('duck_spawn_max', 900) # 15 minutes + wait_time = random.randint(min_wait, max_wait) - async def scare_other_ducks(self, channel, shot_duck_id): - """Scare other ducks when one is shot""" - if channel not in self.ducks: - return - - for duck in self.ducks[channel][:]: - if duck['id'] != shot_duck_id and duck['alive']: - if random.random() < 0.3: - duck['alive'] = False - self.ducks[channel].remove(duck) + await asyncio.sleep(wait_time) + + # Spawn duck in random channel + channels = list(self.bot.channels_joined) + if channels: + channel = random.choice(channels) + await self.spawn_duck(channel) + + except asyncio.CancelledError: + self.logger.info("Duck spawning loop cancelled") + + async def duck_timeout_loop(self): + """Simple duck timeout loop""" + try: + while True: + await asyncio.sleep(10) # Check every 10 seconds + + current_time = time.time() + channels_to_clear = [] + + for channel, ducks in self.ducks.items(): + ducks_to_remove = [] + for duck in ducks: + if current_time - duck['spawn_time'] > self.bot.get_config('duck_timeout', 60): + ducks_to_remove.append(duck) - async def scare_duck_on_miss(self, channel, target_duck): - """Scare duck when someone misses""" - if target_duck and random.random() < 0.15: - target_duck['alive'] = False - if channel in self.ducks and target_duck in self.ducks[channel]: - self.ducks[channel].remove(target_duck) + for duck in ducks_to_remove: + ducks.remove(duck) + message = self.bot.messages.get('duck_flies_away') + self.bot.send_message(channel, message) + + if not ducks: + channels_to_clear.append(channel) - async def find_bushes_items(self, nick, channel, player): - """Find random items in bushes""" - if not self.get_config('items.enabled', True): - return - - if random.random() < 0.1: - items = [ - ("a mirror", "mirror", "You can now deflect shots!"), - ("some sand", "sand", "Throw this to blind opponents!"), - ("a rusty bullet", None, "It's too rusty to use..."), - ("some bread crumbs", "bread", "Feed ducks to make them friendly!"), - ] - - found_item, item_key, message = random.choice(items) - - if item_key and item_key in player: - player[item_key] = player.get(item_key, 0) + 1 - elif item_key in player: - player[item_key] = player.get(item_key, 0) + 1 - - await self.bot.send_user_message(nick, channel, - f"You found {found_item} in the bushes! {message}") + # Clean up empty channels + for channel in channels_to_clear: + if channel in self.ducks and not self.ducks[channel]: + del self.ducks[channel] + + except asyncio.CancelledError: + self.logger.info("Duck timeout loop cancelled") - def get_duck_spawn_message(self): - """Get random duck spawn message (eggdrop style)""" - messages = [ - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUACK", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o< QUACK!", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUAAACK!", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_ö< Quack?", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< *QUACK*" - ] - return random.choice(messages) - - async def spawn_duck_now(self, channel, force_golden=False): - """Spawn a duck immediately in the specified channel""" + async def spawn_duck(self, channel): + """Spawn a duck in the channel""" if channel not in self.ducks: self.ducks[channel] = [] - - max_ducks = self.get_config('max_ducks_per_channel', 3) - if len([d for d in self.ducks[channel] if d['alive']]) >= max_ducks: - self.logger.debug(f"Max ducks already in {channel}") + + # Don't spawn if there's already a duck + if self.ducks[channel]: return - - if force_golden: - duck_type = "golden" - else: - rand = random.random() - if rand < 0.02: - duck_type = "armored" - elif rand < 0.10: - duck_type = "golden" - elif rand < 0.30: - duck_type = "rare" - elif rand < 0.40: - duck_type = "fast" - else: - duck_type = "normal" - - duck_config = self.get_config(f'duck_types.{duck_type}', {}) - if not duck_config.get('enabled', True): - duck_type = "normal" - duck_config = self.get_config('duck_types.normal', {}) - + duck = { - 'id': str(uuid.uuid4())[:8], - 'type': duck_type, - 'alive': True, + 'id': f"duck_{int(time.time())}_{random.randint(1000, 9999)}", 'spawn_time': time.time(), - 'health': duck_config.get('health', 1), - 'max_health': duck_config.get('health', 1) + 'channel': channel } self.ducks[channel].append(duck) - messages = duck_config.get('messages', [self.get_duck_spawn_message()]) - spawn_message = random.choice(messages) + # Send spawn message + message = self.bot.messages.get('duck_spawn') + self.bot.send_message(channel, message) - self.bot.send_message(channel, spawn_message) - self.logger.info(f"Spawned {duck_type} duck in {channel}") - - await self.send_duck_alerts(channel, duck_type) - - return duck - - async def send_duck_alerts(self, channel, duck_type): - """Send alerts to users who have them enabled""" - if not self.get_config('social.duck_alerts_enabled', True): - return - - self.logger.debug(f"Duck alerts for {duck_type} duck in {channel}") - - async def spawn_ducks(self): - """Main duck spawning loop""" - while not self.bot.shutdown_requested: - try: - if self.is_sleep_time(): - await asyncio.sleep(300) - continue - - for channel in self.bot.channels_joined: - if self.bot.shutdown_requested: - break - - if channel not in self.ducks: - self.ducks[channel] = [] - - self.ducks[channel] = [d for d in self.ducks[channel] if d['alive']] - - max_ducks = self.get_config('max_ducks_per_channel', 3) - alive_ducks = len([d for d in self.ducks[channel] if d['alive']]) - - if alive_ducks < max_ducks: - min_spawn_time = self.get_config('duck_spawn_min', 1800) - max_spawn_time = self.get_config('duck_spawn_max', 5400) - - if random.random() < 0.1: - await self.spawn_duck_now(channel) - - await asyncio.sleep(random.randint(60, 300)) - - except asyncio.CancelledError: - self.logger.info("Duck spawning loop cancelled") - break - except Exception as e: - self.logger.error(f"Error in duck spawning: {e}") - await asyncio.sleep(60) - - async def duck_timeout_checker(self): - """Check for ducks that should timeout""" - while not self.bot.shutdown_requested: - try: - current_time = time.time() - - for channel in list(self.ducks.keys()): - if self.bot.shutdown_requested: - break - - if channel not in self.ducks: - continue - - for duck in self.ducks[channel][:]: - if not duck['alive']: - continue - - age = current_time - duck['spawn_time'] - min_timeout = self.get_config('duck_timeout_min', 45) - max_timeout = self.get_config('duck_timeout_max', 75) - - timeout = random.randint(min_timeout, max_timeout) - - if age > timeout: - duck['alive'] = False - self.ducks[channel].remove(duck) - - timeout_messages = [ - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck flew away!", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *FLAP FLAP FLAP*", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck got tired of waiting and left!", - "-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *KWAK* The duck escaped!" - ] - self.bot.send_message(channel, random.choice(timeout_messages)) - self.logger.debug(f"Duck timed out in {channel}") - - await asyncio.sleep(10) - - except asyncio.CancelledError: - self.logger.info("Duck timeout checker cancelled") - break - except Exception as e: - self.logger.error(f"Error in duck timeout checker: {e}") - await asyncio.sleep(30) - - def get_alive_ducks(self, channel): - """Get list of alive ducks in channel""" - if channel not in self.ducks: - return [] - return [d for d in self.ducks[channel] if d['alive']] - - def get_duck_by_id(self, channel, duck_id): - """Get duck by ID""" - if channel not in self.ducks: - return None - for duck in self.ducks[channel]: - if duck['id'] == duck_id and duck['alive']: - return duck - return None \ No newline at end of file + self.logger.info(f"Duck spawned in {channel}") \ No newline at end of file diff --git a/src/logging_utils.py b/src/logging_utils.py index 77f22fc..9d2962b 100644 --- a/src/logging_utils.py +++ b/src/logging_utils.py @@ -6,9 +6,9 @@ import logging import logging.handlers -class DetailedColorFormatter(logging.Formatter): - """Console formatter with color support""" - COLORS = { +class DetailedColourFormatter(logging.Formatter): + """Console formatter with colour support""" + COLOURS = { 'DEBUG': '\033[94m', 'INFO': '\033[92m', 'WARNING': '\033[93m', @@ -18,14 +18,14 @@ class DetailedColorFormatter(logging.Formatter): } def format(self, record): - color = self.COLORS.get(record.levelname, '') - endc = self.COLORS['ENDC'] + colour = self.COLOURS.get(record.levelname, '') + endc = self.COLOURS['ENDC'] msg = super().format(record) - return f"{color}{msg}{endc}" + return f"{colour}{msg}{endc}" class DetailedFileFormatter(logging.Formatter): - """File formatter with extra context but no colors""" + """File formatter with extra context but no colours""" def format(self, record): return super().format(record) @@ -39,7 +39,7 @@ def setup_logger(name="DuckHuntBot"): console_handler = logging.StreamHandler() console_handler.setLevel(logging.INFO) - console_formatter = DetailedColorFormatter( + console_formatter = DetailedColourFormatter( '%(asctime)s [%(levelname)s] %(name)s: %(message)s' ) console_handler.setFormatter(console_formatter) diff --git a/src/utils.py b/src/utils.py index 6d7b592..7050445 100644 --- a/src/utils.py +++ b/src/utils.py @@ -3,7 +3,83 @@ Utility functions for DuckHunt Bot """ import re -from typing import Optional +import json +import os +from typing import Optional, Tuple, List, Dict, Any + + +class MessageManager: + """Manages customizable IRC messages with color support""" + + def __init__(self, messages_file: str = "messages.json"): + self.messages_file = messages_file + self.messages = {} + self.load_messages() + + def load_messages(self): + """Load messages from JSON file""" + try: + if os.path.exists(self.messages_file): + with open(self.messages_file, 'r', encoding='utf-8') as f: + self.messages = json.load(f) + else: + # Fallback messages if file doesn't exist + self.messages = self._get_default_messages() + except Exception as e: + print(f"Error loading messages: {e}, using defaults") + self.messages = self._get_default_messages() + + def _get_default_messages(self) -> Dict[str, str]: + """Default fallback messages without colors""" + return { + "duck_spawn": "・゜゜・。。・゜゜\\_o< QUACK! A duck has appeared! Type !bang to shoot it!", + "duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`", + "bang_hit": "{nick} > *BANG* You shot the duck! [+{xp_gained} xp] [Total ducks: {ducks_shot}]", + "bang_miss": "{nick} > *BANG* You missed the duck!", + "bang_no_duck": "{nick} > *BANG* What did you shoot at? There is no duck in the area... [GUN CONFISCATED]", + "bang_no_ammo": "{nick} > *click* You're out of ammo! Use !reload", + "bang_not_armed": "{nick} > You are not armed.", + "reload_success": "{nick} > *click* Reloaded! [Ammo: {ammo}/{max_ammo}] [Chargers: {chargers}]", + "reload_already_loaded": "{nick} > Your gun is already loaded!", + "reload_no_chargers": "{nick} > You're out of chargers!", + "reload_not_armed": "{nick} > You are not armed.", + "shop_display": "DuckHunt Shop: {items} | You have {xp} XP", + "shop_item_format": "({id}) {name} - {price} XP", + "help_header": "DuckHunt Commands:", + "help_user_commands": "!bang - Shoot at ducks | !reload - Reload your gun | !shop - View the shop", + "help_help_command": "!duckhelp - Show this help", + "help_admin_commands": "Admin: !rearm | !disarm | !ignore | !unignore | !ducklaunch", + "admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}", + "admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}", + "admin_disarm": "[ADMIN] {target} has been disarmed by {admin}", + "admin_ignore": "[ADMIN] {target} is now ignored by {admin}", + "admin_unignore": "[ADMIN] {target} is no longer ignored by {admin}", + "admin_ducklaunch": "[ADMIN] A duck has been launched by {admin}", + "admin_ducklaunch_not_enabled": "[ADMIN] This channel is not enabled for duckhunt", + "usage_rearm": "Usage: !rearm ", + "usage_disarm": "Usage: !disarm ", + "usage_ignore": "Usage: !ignore ", + "usage_unignore": "Usage: !unignore " + } + + def get(self, key: str, **kwargs) -> str: + """Get a formatted message by key""" + if key not in self.messages: + return f"[Missing message: {key}]" + + message = self.messages[key] + + # Format with provided variables + try: + return message.format(**kwargs) + except KeyError as e: + return f"[Message format error: {e}]" + except Exception as e: + return f"[Message error: {e}]" + + def reload(self): + """Reload messages from file""" + self.load_messages() class InputValidator: @@ -46,12 +122,17 @@ class InputValidator: return sanitized[:500] -def parse_message(line): +def parse_irc_message(line: str) -> Tuple[str, str, List[str], str]: """Parse IRC message format""" prefix = '' trailing = '' if line.startswith(':'): - prefix, line = line[1:].split(' ', 1) + if ' ' in line[1:]: + prefix, line = line[1:].split(' ', 1) + else: + # Handle malformed IRC line with no space after prefix + prefix = line[1:] + line = '' if ' :' in line: line, trailing = line.split(' :', 1) parts = line.split()