Simplified DuckHunt bot with customizable messages and colors

This commit is contained in:
2025-09-23 02:57:28 +01:00
parent 9285b1b29d
commit de64756b6d
19 changed files with 797 additions and 5712 deletions

156
README.md
View File

@@ -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 <item_number> [target]` - Use an item from inventory
- `!sell <item_number>` - Sell an item for half price
### Admin Commands
- `!rearm [player]` - Restore confiscated guns
- `!disarm <player>` - Confiscate a player's gun
- `!ducklaunch` - Force spawn a duck
- `!reset <player> [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`

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -10,246 +10,9 @@
"password": "duckhunt//789//" "password": "duckhunt//789//"
}, },
"password": "your_iline_password_here", "password": "your_iline_password_here",
"_comment_password": "Server password for I-line exemption (PASS command)",
"admins": ["peorth", "computertech", "colby"], "admins": ["peorth", "computertech", "colby"],
"_comment_message_output": "Default output modes for different message types", "duck_spawn_min": 10,
"message_output": { "duck_spawn_max": 30,
"default_user_mode": "PUBLIC", "duck_timeout": 60
"_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
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -196,3 +196,149 @@
2025-09-19 21:42:58,198 [INFO ] DuckHuntBot - run:1154: Database saved 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,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-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

View File

@@ -1,5 +1,6 @@
""" """
DuckHunt IRC Bot - Main Entry Point DuckHunt IRC Bot - Simplified Entry Point
Commands: !bang, !reload, !shop, !rearm, !disarm
""" """
import asyncio import asyncio
@@ -15,22 +16,30 @@ from src.duckhuntbot import DuckHuntBot
def main(): def main():
"""Main entry point for DuckHunt Bot""" """Main entry point for DuckHunt Bot"""
try: 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) config = json.load(f)
bot = DuckHuntBot(config) bot = DuckHuntBot(config)
bot.logger.info("🦆 Starting DuckHunt Bot...") bot.logger.info("🦆 Starting DuckHunt Bot...")
# Run the bot
asyncio.run(bot.run()) asyncio.run(bot.run())
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n🛑 Bot stopped by user") print("\n🛑 Shutdown interrupted by user")
except FileNotFoundError: except FileNotFoundError:
print("❌ config.json not found!") print("❌ config.json not found!")
sys.exit(1) sys.exit(1)
except Exception as e: except Exception as e:
print(f"❌ Error: {e}") print(f"❌ Error: {e}")
sys.exit(1) sys.exit(1)
else:
print("👋 DuckHunt Bot stopped gracefully")
if __name__ == '__main__': if __name__ == '__main__':

54
messages.json Normal file
View File

@@ -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 <player>\u000F | \u000302!disarm <player>\u000F | \u000302!ignore <player>\u000F | \u000302!unignore <player>\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 <player>\u000F",
"usage_disarm": "Usage: \u000302!disarm <player>\u000F",
"usage_ignore": "Usage: \u000302!ignore <player>\u000F",
"usage_unignore": "Usage: \u000302!unignore <player>\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"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

180
src/db.py
View File

@@ -1,181 +1,81 @@
""" """
Database functionality for DuckHunt Bot Simplified Database management for DuckHunt Bot
Only essential player fields
""" """
import json import json
import os
import time
import asyncio
import logging import logging
import time
import os
class DuckDB: class DuckDB:
"""Database management for DuckHunt Bot""" """Simplified database management"""
def __init__(self, db_file="duckhunt.json"): def __init__(self, db_file="duckhunt.json"):
self.db_file = db_file self.db_file = db_file
self.players = {} self.players = {}
self._save_pending = False
self.logger = logging.getLogger('DuckHuntBot.DB') self.logger = logging.getLogger('DuckHuntBot.DB')
self.load_database()
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
def load_database(self): def load_database(self):
"""Load player data from JSON file""" """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: with open(self.db_file, 'r') as f:
data = json.load(f) data = json.load(f)
self.players = data.get('players', {}) self.players = data.get('players', {})
self.logger.info(f"Loaded {len(self.players)} players from {self.db_file}") 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: else:
self.players = {} self.players = {}
self.logger.info(f"Created new database: {self.db_file}") self.logger.info(f"No existing database found, starting fresh")
except Exception as e:
self.logger.error(f"Error loading database: {e}")
self.players = {}
def save_database(self): def save_database(self):
"""Save all player data to JSON file with error handling""" """Save all player data to JSON file"""
try: try:
temp_file = f"{self.db_file}.tmp"
data = { data = {
'players': self.players, 'players': self.players,
'last_save': str(time.time()) '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) 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: except Exception as e:
self.logger.error(f"Unexpected database save error: {e}") self.logger.error(f"Error saving database: {e}")
def _get_starting_accuracy(self): def get_player(self, nick):
"""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):
"""Get player data, creating if doesn't exist""" """Get player data, creating if doesn't exist"""
if '!' not in user: nick_lower = nick.lower()
nick = user.lower()
else:
nick = user.split('!')[0].lower()
if nick in self.players: if nick_lower not in self.players:
player = self.players[nick] self.players[nick_lower] = self.create_player(nick)
self._ensure_player_fields(player)
return player return self.players[nick_lower]
else:
return self.create_player(nick)
def create_player(self, nick): def create_player(self, nick):
"""Create a new player with default stats""" """Create a new player with basic stats"""
player = { return {
'shots': 6, 'nick': nick,
'max_shots': 6, 'xp': 0,
'ducks_shot': 0,
'ammo': 6,
'max_ammo': 6,
'chargers': 2, 'chargers': 2,
'max_chargers': 2, 'max_chargers': 2,
'reload_time': 5.0, 'accuracy': 65,
'ducks_shot': 0, 'gun_confiscated': False
'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

File diff suppressed because it is too large Load Diff

View File

@@ -1,310 +1,105 @@
""" """
Game mechanics for DuckHunt Bot Simplified Game mechanics for DuckHunt Bot
Basic duck spawning and timeout only
""" """
import asyncio import asyncio
import random import random
import time import time
import uuid
import logging import logging
from typing import Dict, Any, Optional, List
class DuckGame: class DuckGame:
"""Game mechanics and duck management""" """Simplified game mechanics - just duck spawning"""
def __init__(self, bot, db): def __init__(self, bot, db):
self.bot = bot self.bot = bot
self.db = db self.db = db
self.ducks = {} self.ducks = {} # {channel: [duck1, duck2, ...]}
self.logger = logging.getLogger('DuckHuntBot.Game') self.logger = logging.getLogger('DuckHuntBot.Game')
self.spawn_task = None
self.timeout_task = None
self.colors = { async def start_game_loops(self):
'red': '\x0304', """Start the game loops"""
'green': '\x0303', self.spawn_task = asyncio.create_task(self.duck_spawn_loop())
'yellow': '\x0308', self.timeout_task = asyncio.create_task(self.duck_timeout_loop())
'blue': '\x0302',
'cyan': '\x0311',
'magenta': '\x0306',
'white': '\x0300',
'bold': '\x02',
'reset': '\x03',
'underline': '\x1f'
}
def get_config(self, path, default=None): try:
"""Helper method to get config values""" await asyncio.gather(self.spawn_task, self.timeout_task)
return self.bot.get_config(path, default) except asyncio.CancelledError:
self.logger.info("Game loops cancelled")
def get_player_level(self, xp): async def duck_spawn_loop(self):
"""Calculate player level from XP""" """Simple duck spawning loop"""
if xp < 0: try:
return 0 while True:
return int((xp ** 0.5) / 2) + 1 # 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)
def get_xp_for_next_level(self, xp): await asyncio.sleep(wait_time)
"""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): # Spawn duck in random channel
"""Reduce penalties for higher level players""" channels = list(self.bot.channels_joined)
level = self.get_player_level(xp) if channels:
return max(1, base_penalty - (level - 1)) channel = random.choice(channels)
await self.spawn_duck(channel)
def update_karma(self, player, event): except asyncio.CancelledError:
"""Update player karma based on events""" self.logger.info("Duck spawning loop cancelled")
if not self.get_config('karma.enabled', True):
return
karma_changes = { async def duck_timeout_loop(self):
'hit': self.get_config('karma.hit_bonus', 2), """Simple duck timeout loop"""
'golden_hit': self.get_config('karma.golden_hit_bonus', 5), try:
'teamkill': -self.get_config('karma.teamkill_penalty', 10), while True:
'wild_shot': -self.get_config('karma.wild_shot_penalty', 3), await asyncio.sleep(10) # Check every 10 seconds
'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)
}
if event in karma_changes: current_time = time.time()
player['karma'] = player.get('karma', 0) + karma_changes[event] channels_to_clear = []
def is_sleep_time(self): for channel, ducks in self.ducks.items():
"""Check if ducks should not spawn due to sleep hours""" ducks_to_remove = []
sleep_hours = self.get_config('sleep_hours', []) for duck in ducks:
if not sleep_hours or len(sleep_hours) != 2: if current_time - duck['spawn_time'] > self.bot.get_config('duck_timeout', 60):
return False ducks_to_remove.append(duck)
import datetime for duck in ducks_to_remove:
current_hour = datetime.datetime.now().hour ducks.remove(duck)
start_hour, end_hour = sleep_hours message = self.bot.messages.get('duck_flies_away')
self.bot.send_message(channel, message)
if start_hour <= end_hour: if not ducks:
return start_hour <= current_hour <= end_hour channels_to_clear.append(channel)
else:
return current_hour >= start_hour or current_hour <= end_hour
def calculate_gun_reliability(self, player): # Clean up empty channels
"""Calculate gun reliability with modifiers""" for channel in channels_to_clear:
base_reliability = player.get('reliability', 70) if channel in self.ducks and not self.ducks[channel]:
return min(100, max(0, base_reliability)) del self.ducks[channel]
def gun_jams(self, player): except asyncio.CancelledError:
"""Check if gun jams (eggdrop style)""" self.logger.info("Duck timeout loop cancelled")
reliability = player.get('reliability', 70)
jam_chance = max(1, 101 - reliability)
if player.get('total_ammo_used', 0) > 100: async def spawn_duck(self, channel):
jam_chance += 2 """Spawn a duck in the channel"""
if player.get('jammed_count', 0) > 5:
jam_chance += 1
return random.randint(1, 100) <= jam_chance
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)
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)
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}")
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"""
if channel not in self.ducks: if channel not in self.ducks:
self.ducks[channel] = [] self.ducks[channel] = []
max_ducks = self.get_config('max_ducks_per_channel', 3) # Don't spawn if there's already a duck
if len([d for d in self.ducks[channel] if d['alive']]) >= max_ducks: if self.ducks[channel]:
self.logger.debug(f"Max ducks already in {channel}")
return 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 = { duck = {
'id': str(uuid.uuid4())[:8], 'id': f"duck_{int(time.time())}_{random.randint(1000, 9999)}",
'type': duck_type,
'alive': True,
'spawn_time': time.time(), 'spawn_time': time.time(),
'health': duck_config.get('health', 1), 'channel': channel
'max_health': duck_config.get('health', 1)
} }
self.ducks[channel].append(duck) self.ducks[channel].append(duck)
messages = duck_config.get('messages', [self.get_duck_spawn_message()]) # Send spawn message
spawn_message = random.choice(messages) message = self.bot.messages.get('duck_spawn')
self.bot.send_message(channel, message)
self.bot.send_message(channel, spawn_message) self.logger.info(f"Duck spawned in {channel}")
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

View File

@@ -6,9 +6,9 @@ import logging
import logging.handlers import logging.handlers
class DetailedColorFormatter(logging.Formatter): class DetailedColourFormatter(logging.Formatter):
"""Console formatter with color support""" """Console formatter with colour support"""
COLORS = { COLOURS = {
'DEBUG': '\033[94m', 'DEBUG': '\033[94m',
'INFO': '\033[92m', 'INFO': '\033[92m',
'WARNING': '\033[93m', 'WARNING': '\033[93m',
@@ -18,14 +18,14 @@ class DetailedColorFormatter(logging.Formatter):
} }
def format(self, record): def format(self, record):
color = self.COLORS.get(record.levelname, '') colour = self.COLOURS.get(record.levelname, '')
endc = self.COLORS['ENDC'] endc = self.COLOURS['ENDC']
msg = super().format(record) msg = super().format(record)
return f"{color}{msg}{endc}" return f"{colour}{msg}{endc}"
class DetailedFileFormatter(logging.Formatter): class DetailedFileFormatter(logging.Formatter):
"""File formatter with extra context but no colors""" """File formatter with extra context but no colours"""
def format(self, record): def format(self, record):
return super().format(record) return super().format(record)
@@ -39,7 +39,7 @@ def setup_logger(name="DuckHuntBot"):
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
console_formatter = DetailedColorFormatter( console_formatter = DetailedColourFormatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s' '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
) )
console_handler.setFormatter(console_formatter) console_handler.setFormatter(console_formatter)

View File

@@ -3,7 +3,83 @@ Utility functions for DuckHunt Bot
""" """
import re 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 <player> | !disarm <player> | !ignore <player> | !unignore <player> | !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 <player>",
"usage_disarm": "Usage: !disarm <player>",
"usage_ignore": "Usage: !ignore <player>",
"usage_unignore": "Usage: !unignore <player>"
}
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: class InputValidator:
@@ -46,12 +122,17 @@ class InputValidator:
return sanitized[:500] return sanitized[:500]
def parse_message(line): def parse_irc_message(line: str) -> Tuple[str, str, List[str], str]:
"""Parse IRC message format""" """Parse IRC message format"""
prefix = '' prefix = ''
trailing = '' trailing = ''
if line.startswith(':'): if line.startswith(':'):
if ' ' in line[1:]:
prefix, line = line[1:].split(' ', 1) prefix, line = line[1:].split(' ', 1)
else:
# Handle malformed IRC line with no space after prefix
prefix = line[1:]
line = ''
if ' :' in line: if ' :' in line:
line, trailing = line.split(' :', 1) line, trailing = line.split(' :', 1)
parts = line.split() parts = line.split()