Simplified DuckHunt bot with customizable messages and colors
This commit is contained in:
156
README.md
156
README.md
@@ -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
243
config.json
243
config.json
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
1101
duckhunt.json
1101
duckhunt.json
File diff suppressed because it is too large
Load Diff
146
duckhunt.log
146
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,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
|
||||||
|
|||||||
15
duckhunt.py
15
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
|
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
54
messages.json
Normal 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
180
src/db.py
180
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 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
|
|
||||||
1219
src/duckhuntbot.py
1219
src/duckhuntbot.py
File diff suppressed because it is too large
Load Diff
333
src/game.py
333
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 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
|
|
||||||
@@ -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)
|
||||||
|
|||||||
85
src/utils.py
85
src/utils.py
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user