Compare commits

...

87 Commits

Author SHA1 Message Date
3nd3r
0b5b42a507 Fix duck tracking across channel case 2026-01-01 13:32:49 -06:00
3nd3r
626eb7cb2a Fix ignore/unignore global behavior 2026-01-01 11:16:47 -06:00
3nd3r
b6d2fe2a35 Update README; fix duck_types config 2026-01-01 11:10:14 -06:00
3nd3r
77ed3f95ad Update bot 2026-01-01 10:45:59 -06:00
3nd3r
68a0a1fc83 Update DuckHunt bot 2026-01-01 10:42:12 -06:00
3nd3r
292db0c95e PM-only admin reload restarts bot; raise clover price 2025-12-30 23:21:48 -06:00
3nd3r
b944e234d6 Fix file mode (shop.json non-executable) 2025-12-30 23:19:45 -06:00
3nd3r
735c46b8c2 Add clover luck item and admin restart command 2025-12-30 23:19:37 -06:00
3nd3r
38d9159f50 Label legacy bucket in globaltop 2025-12-30 22:56:15 -06:00
3nd3r
a1afd25053 Simplify globaltop output 2025-12-30 22:54:07 -06:00
3nd3r
1e4e1e31ba Format globaltop channel labels 2025-12-30 22:53:12 -06:00
3nd3r
9bff554a07 Fix file mode (non-executable) 2025-12-30 22:49:02 -06:00
3nd3r
2372075195 Add globaltop leaderboard command 2025-12-30 22:49:02 -06:00
3nd3r
2ef81cdd26 Fix file modes (non-executable) 2025-12-30 22:43:30 -06:00
3nd3r
67bf6957a7 Separate player stats per channel 2025-12-30 22:43:23 -06:00
3nd3r
214d1ed263 Fix file modes 2025-12-29 23:36:18 -06:00
3nd3r
f47778e608 Load messages.json from repo root 2025-12-29 23:35:38 -06:00
3nd3r
7147f5f30c Add more duck spawn quacks 2025-12-29 23:32:04 -06:00
3nd3r
51c212e8cd Fix JOIN confirmation tracking for auto-rejoin 2025-12-28 23:11:21 -06:00
3nd3r
5efe1e70bf Fix rejoin loop crash when pending_joins missing 2025-12-28 23:08:04 -06:00
3nd3r
a8b4196cf2 Fix auto-rejoin after KICK to rejoin immediately 2025-12-28 23:04:37 -06:00
3nd3r
5e4bbb4309 Update README to reflect current features
- Removed references to removed duck types (concrete, diamond, explosive, etc.)
- Removed references to removed items (sniper rifle, duck radar, etc.)
- Updated to show only 3 duck types: normal, golden, fast
- Documented current commands and features
- Added Recent Updates section
- Cleaned up and organized for clarity
2025-12-28 17:58:01 -06:00
3nd3r
53a66f5202 Update duckhelp to send PM with detailed commands 2025-12-28 17:52:02 -06:00
3nd3r
b71f8f4ec6 Add join and part admin commands for channel management 2025-12-28 17:49:31 -06:00
3nd3r
5db4ce0ab3 Revert to original working code - clean slate
- Start from original code that was known to work
- Will add features back incrementally
- This ensures we know exactly what breaks/works
2025-12-28 17:43:11 -06:00
3nd3r
90b604ba72 Add force_spawn_duck for admin ducklaunch - fixes missing method error 2025-12-28 16:51:26 -06:00
3nd3r
dd06c9377f Revert to simple version: Remove new ducks and items
- Removed new duck types (concrete, diamond, holy_grail, explosive, poisonous, etc.)
- Removed new shop items (sniper rifle, duck radar, bread, splash water, etc.)
- Removed status effects system (eliminated, poisoned, wet, etc.)
- Removed item drops and temporary effects
- Kept only original 3 duck types: normal, golden, fast
- Kept original simple shop
- KEPT BUG FIX: Golden duck XP now awarded on each hit
- KEPT BUG FIX: Message sanitization preserves IRC codes
- Simpler, more stable bot with core improvements
2025-12-28 16:48:56 -06:00
3nd3r
eb907e1e2c Add original/backup code for reference
- Original working code before recent modifications
- Kept for comparison and debugging purposes
- Helps identify what changed and what broke
2025-12-28 16:29:47 -06:00
3nd3r
f6a9f4592a Fix critical bug: Messages being stripped by over-aggressive sanitization
- Bot messages containing IRC color codes were being completely stripped
- sanitize_user_input() without allowed_chars was removing all formatting
- Changed to only remove CR/LF from messages while preserving formatting codes
- This was causing silent failures where no messages were sent to channel
2025-12-28 16:03:42 -06:00
3nd3r
6069240553 Fix critical bug: Award XP on multi-HP duck hits
- Fixed issue where players weren't getting XP when hitting (but not killing) multi-HP ducks
- XP was calculated but never added to player's total
- This bug could cause bot failures with new multi-HP duck types
- Now properly awards XP on each hit and saves to database
2025-12-28 16:00:40 -06:00
3nd3r
f3f251a391 Add explicit error replies for core commands 2025-12-28 15:49:07 -06:00
3nd3r
3e7436840e Fix: remove blocking sleep, unreachable code, and unused admin helper 2025-12-28 15:40:59 -06:00
3nd3r
9bd51a24cc Accept leading-whitespace commands; log spawn send failures 2025-12-28 15:33:17 -06:00
3nd3r
d5654e9783 Normalize channel names for join tracking and commands 2025-12-28 15:30:17 -06:00
3nd3r
ba9beae82f Harden temporary effect parsing to prevent silent command failures 2025-12-28 15:24:46 -06:00
3nd3r
7d85f83faa Fix JOIN parsing for trailing channel and rejoin success handling 2025-12-28 15:19:49 -06:00
3nd3r
02c055d7e3 Log IRC join failures and confirm joins 2025-12-28 15:12:30 -06:00
3nd3r
617d9560e6 Restrict PM spawns to normal/fast/golden 2025-12-28 14:34:14 -06:00
3nd3r
3b72a853ae Allow ducklaunch for new duck types 2025-12-28 14:31:45 -06:00
3nd3r
ffe8bdfaf2 Update README for new duck types and items 2025-12-28 13:50:08 -06:00
3nd3r
b256b9a9f6 Add new duck types and items 2025-12-28 13:36:41 -06:00
3nd3r
4d17ae8f04 Prepare for GitHub release 2025-12-28 13:16:55 -06:00
f8c46980de Update auto-rejoin settings for more persistent reconnection
- Reduced retry interval from 30 to 20 seconds for faster rejoins
- Increased max rejoin attempts from 10 to 100 for greater persistence
- Bot will now be more aggressive about staying in channels
2025-10-05 19:39:00 +01:00
b5613f20dd Add automatic channel rejoin functionality
- Added auto_rejoin configuration to connection settings
- Handles KICK events and automatically schedules rejoin attempts
- Configurable retry interval and max attempts
- Tracks rejoin attempts per channel with exponential backoff
- Handles JOIN confirmations to stop rejoin loops
- Proper cleanup of rejoin tasks on shutdown
- Respects shutdown and connection state before rejoining
- Logs all rejoin attempts and results for debugging
2025-10-05 19:34:49 +01:00
0176284012 Add comprehensive .gitignore
- Ignore Python __pycache__ directories and .pyc files
- Ignore logs/ directory and all .log files
- Ignore temporary and backup files
- Ignore IDE and OS generated files
- Remove tracked __pycache__ files from repository
2025-10-05 19:23:53 +01:00
857a15b666 Delete duckhunt.log 2025-10-05 18:22:54 +00:00
489989001c Delete logs/duckhunt.log 2025-10-05 18:22:40 +00:00
b39c82c84b Delete __pycache__/duckhunt.cpython-312.pyc 2025-10-05 18:22:24 +00:00
0a27f7272e hmm 2025-10-05 19:19:18 +01:00
85fa8a9170 Fix database corruption handling and auto-creation
- Added datetime import to fix NameError
- Simplified database handling to create new file if missing or corrupted
- Removed backup functionality per user request
- Fixed duplicate method definitions
- Enhanced error handling throughout database operations
- Auto-creates duckhunt.json with proper structure on startup
2025-10-05 19:18:46 +01:00
00e129d2f3 Fix database corruption and enhance duck messages
- Fix missing field errors that caused 'ducks_shot' message format errors
- Enhanced _sanitize_player_data to ensure all required fields exist
- Added comprehensive field validation and type conversion
- Added multiple variations for duck_flies_away messages (normal, fast, golden)
- Improved error handling for corrupted/incomplete player data
2025-10-03 20:14:38 +01:00
470edb4401 Add give command and rearm all functionality 2025-10-02 01:01:00 +01:00
a17bba215d Update player data with retroactive XP adjustments
- Adjusted XP values for players with recorded misses
- Applied -1 XP per miss to maintain consistency with new mechanic
- Players with 0 XP remain at 0 (no negative XP allowed)
2025-10-01 20:32:33 +01:00
687a57f018 Add XP loss for missing ducks
- Players now lose 1 XP when missing a duck
- Updated miss message to show XP loss
- XP cannot go below 0
2025-10-01 20:27:12 +01:00
7aded2ed83 Implement duck item drop system and remove all emojis
Duck Item Drop System:
- Added configurable drop rates per duck type (15%/25%/50%)
- Created weighted drop tables for different items
- Normal ducks: basic items (bullets, magazines, gun brush, sand)
- Fast ducks: useful items including bucket of water
- Golden ducks: rare items (bread, insurance, gun buyback, dry clothes)
- Items automatically added to player inventory
- Added drop notification messages for each duck type
- Integrated seamlessly with existing combat mechanics

Emoji Removal:
- Removed all emojis from source code files
- Updated logging system to use clean text prefixes
- Replaced trophy/medal emojis with #1/#2/#3 rankings
- Updated README.md to remove all emojis
- Professional clean appearance throughout codebase
2025-09-26 19:59:34 +01:00
5ed2f0fce6 Fix duckstats and rearm targeting issues
- Added target support to duckstats command (duckstats username)
- Disabled problematic channel membership validation for admin commands
- Activity validation is sufficient - if player exists and has game activity, allow targeting
- Fixes persistent not currently in channel errors for rearm command
2025-09-26 19:27:29 +01:00
b1b1d4d65f Fix channel validation by saving activity tracking immediately
- Activity tracking now saves to database immediately when commands are processed
- This ensures validate_target_player can see recent activity for channel membership checks
2025-09-26 19:24:45 +01:00
bf3cd48639 Update database and logs from bot runtime 2025-09-26 19:21:01 +01:00
25226a460b Fix magazine and bullet usage limits
- Fixed bug where players could use magazines beyond their level's maximum limit
- Added validation to prevent using bullets when magazine is already full
- Magazine items now respect level-based limits (e.g., 3 magazines at level 1)
- Items are not consumed from inventory if they can't be used due to limits
- Added proper error messages for when limits are reached
- Updated ShopManager to work with LevelManager for limit validation
2025-09-26 19:13:52 +01:00
f3a9c5b611 Security fixes, UI improvements, and game balance updates
- Fixed critical security vulnerabilities in shop targeting system
- Fixed admin authentication bypass issues
- Fixed auto-rearm feature config path (duck_spawning.rearm_on_duck_shot)
- Updated duck spawn timing to 20-60 minutes for better game balance
- Enhanced inventory display formatting with proper spacing
- Added comprehensive admin security documentation
2025-09-26 19:06:26 +01:00
5484548c30 yeah 2025-09-25 19:47:44 +01:00
eb041477dc Add comprehensive documentation and fix authentication config paths
- Add detailed README.md with installation, usage, and development guide
- Add CONFIG.md with complete configuration documentation
- Update connection and SASL authentication to use nested config paths
- Fix server password and SASL username/password config access
- Add config validation test script (test_config.py)
- Clean up config.json (removed invalid JSON comments)
- Improve error handling for config arrays and null values
2025-09-24 20:33:23 +01:00
74f3afdf4b Restructure config.json with nested hierarchy and dot notation access
- Reorganized config.json into logical sections: connection, duck_spawning, duck_types, player_defaults, gameplay, features, limits
- Enhanced get_config() method to support dot notation (e.g., 'duck_types.normal.xp')
- Added comprehensive configurable parameters for all game mechanics
- Updated player creation to use configurable starting values
- Added individual timeout settings per duck type
- Made XP rewards, accuracy mechanics, and game limits fully configurable
- Fixed syntax errors in duck_spawn_loop function
2025-09-24 20:26:49 +01:00
6ca624bd2f Add hidden duck types: Normal, Golden, and Fast ducks
Features:
- Three duck types spawn randomly with configurable chances
- All duck types use same spawn message - type is hidden until shot/timeout
- Golden ducks: 3-5 HP, 15 XP base, reveal type when hit but still alive
- Fast ducks: 1 HP, 12 XP, fly away in 30s instead of 60s
- Normal ducks: 1 HP, 10 XP, standard 60s timeout

Configuration options:
- golden_duck_chance: 0.15 (15% spawn rate)
- fast_duck_chance: 0.25 (25% spawn rate)
- golden_duck_xp: 15, golden_duck_min/max_hp: 3-5
- fast_duck_xp: 12, fast_duck_timeout: 30s

Duck type is revealed when:
- Shot (different messages for each type)
- Flies away (type-specific fly away messages)
- Golden ducks reveal immediately when hit (before death)

Maintains backward compatibility with existing game mechanics.
2025-09-24 16:35:45 +01:00
f9883758f3 Add configurable auto-rearm feature when duck is shot/befriended
- Add 'rearm_on_duck_shot' config option (defaults to true)
- Automatically rearm all disarmed players when any duck is successfully shot
- Also applies when ducks are successfully befriended
- Includes error handling and logging for the rearm process
- Improves game flow by reducing downtime for players who made wild shots
2025-09-24 16:03:03 +01:00
688aca759f Fix indentation and duplicate function errors in duckhuntbot.py
- Remove duplicate handle_rearm function definition
- Fix indentation in setup_signal_handlers function
- Add missing handle_disarm method
- Remove duplicate send_message in handle_ducklaunch
- All syntax errors resolved, bot should now start properly
2025-09-24 01:56:18 +01:00
78caccd8b4 Fix ASCII encoding issues and add robust error handling
- Add comprehensive UTF-8 decoding error handling for IRC messages
- Implement robust error handling for all command processing
- Add network connection error resilience
- Add database operation error handling
- Ensure bot doesn't crash on any input or network issues
- Maintain original duck hunt functionality without feature additions
2025-09-24 01:51:24 +01:00
73582f7a44 more fixes 2025-09-23 20:20:53 +01:00
d6e64d5eab more fixes 2025-09-23 20:20:06 +01:00
0c8b4f9543 Implement magazine system and inventory management
- Add level-based magazine system (3 mags at L1, 2 at L3-5, 1 at L6-8)
- Replace ammo/chargers with current_ammo/magazines/bullets_per_magazine
- Add inventory system for storing and using shop items
- Add Magazine item to shop (15 XP, adds 1 magazine)
- Auto-migrate existing players from old ammo system
- Auto-update magazines when players level up
- Fix method name bugs (get_player_level -> calculate_player_level)
2025-09-23 20:13:01 +01:00
3aaf0d0bb4 feat: Add shop system, befriend command, and level system
- Added configurable shop system with shop.json
- Created ShopManager class for modular shop handling
- Implemented level system with levels.json for difficulty scaling
- Added multiple duck spawn messages with random selection
- Enhanced message system with color placeholders
- Added ducks_befriended tracking separate from ducks_shot
- Updated help system and admin commands
- All systems tested and working correctly
2025-09-23 18:05:28 +01:00
de64756b6d Simplified DuckHunt bot with customizable messages and colors 2025-09-23 02:57:28 +01:00
9285b1b29d Clean up codebase and add documentation
- Remove all comments from Python source files for cleaner code
- Add comprehensive README.md with installation and usage instructions
- Add .gitignore to exclude runtime files and sensitive configuration
- Preserve all functionality while improving code readability
2025-09-22 18:10:21 +01:00
ba7f082d5c Implement competitive item snatching system
- Add dropped items tracking with timestamps per channel
- Items drop to ground (10% chance on duck kills) for any player to grab
- Add 60-second timeout for unclaimed items
- Background cleanup task removes expired items automatically
- First-come-first-served basis for item collection
- Eggdrop-style messaging for drops and successful snatches
2025-09-19 21:43:25 +01:00
1f5af7ed83 mhm 2025-09-13 16:19:05 +01:00
1f64b6fc82 test 2025-09-13 16:14:46 +01:00
339f32b6a0 Make bread cheaper and add channel limit system
- Reduced bread cost from 50 to 10 in all shop definitions
- Added channel_bread tracking system to track deployed bread per channel
- Added check in buy function to limit maximum 3 bread items per channel
- Initialized bread tracking in bot constructor
- Next step: implement bread deployment in use command
2025-09-13 15:51:46 +01:00
7c0974cfbf Change delignore command to unignore to match help text
- Updated private message handler from delignore to unignore
- Fixed string length calculation for command parsing (10 chars for unignore vs 11 for delignore)
- Updated help text in private message handler to show unignore
2025-09-13 15:44:29 +01:00
5ebe8a8e21 Change stats command to duckstats to avoid conflicts with other bots 2025-09-13 15:41:55 +01:00
746957bc17 Fix help command organization and stats message output
- Added player_stats message type to force_public config
- Stats messages now always appear in public channel instead of as notices
- Non-admin users no longer see ignore/unignore commands in help
2025-09-13 15:39:55 +01:00
b198ef2b9e Remove unknown command handler to avoid interfering with other bots
- Removed the 'Unknown command! Use !duckhelp' message for unrecognized commands
- Bot now only responds to valid duckhunt commands and stays silent for others
- This allows other bots in the channel to handle their own commands without interference
2025-09-13 15:35:50 +01:00
34daa04238 Fix command handling and improve help system 2025-09-13 15:34:05 +01:00
6854e88037 Fix messages being sent as notices instead of public by default
- Changed new player default 'notices' setting from True to False
- Added specific message types for duck_miss and wild_shot events
- Added duck_miss and wild_shot to force_public config settings
- All miss/wild shot messages now go to public channel by default
- Legacy users with notices=True will still get notices, but new users get public messages
2025-09-13 15:10:33 +01:00
2bbc202f8f Fix KeyError: 'fastest_shot' by ensuring complete channel records initialization
- Fixed incomplete channel_records initialization in shoot command
- Fixed incomplete channel_records initialization in wild shot tracking
- All channel records now properly include: fastest_shot, last_duck, total_ducks, total_shots
- Resolves runtime error when update_channel_records tries to access fastest_shot field
2025-09-13 15:07:32 +01:00
62da9d1c28 Major improvements: Remove rate limiting and improve message readability
 Completely removed all rate limiting functionality
 Made messages less compact and more readable
 Removed duck detector and auto shotgun from shop

Changes:
- Removed rate limiting: check_rate_limit(), is_rate_limited(), command_cooldowns
- Improved message formats with natural language
- Hit messages: 'Duck shot in X.Xs!' instead of 'X.Xs'
- Miss messages: 'You missed the duck!' instead of 'MISS'
- Reload messages: 'You reloaded your gun!' instead of 'RELOADED'
- Stats display: Full words instead of abbreviations
- Shop cleanup: Removed items #14 (auto shotgun) and #22 (duck detector)
- Better spacing and punctuation throughout
- Maintained efficiency while improving readability
2025-09-13 15:02:38 +01:00
408a840e94 Fix shop command errors
 Fixed indentation issue in handle_shop function
 Added missing 'magenta' color code for IRC
 Shop command now works without errors

Fixes:
- Removed extra indentation causing syntax error
- Added magenta color (\x0313) to colors dictionary
- Shop display now shows all categories properly
2025-09-13 14:47:50 +01:00
009a851696 Major code cleanup and compact message improvements
 Fixed all syntax errors and indentation issues
 Made all command outputs more compact and readable
 Fixed shop display with full item names
 Improved message formats for better IRC experience

Changes:
- Fixed broken try-except blocks and indentation
- Compacted hit/miss/reload messages
- Fixed color code issues in logging
- All syntax errors resolved - bot fully functional
2025-09-13 14:36:57 +01:00
45 changed files with 5927 additions and 6164 deletions

158
.gitignore vendored Normal file
View File

@@ -0,0 +1,158 @@
# Python
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# DuckHunt Bot specific
logs/
*.log
duckhunt.log*
*.corrupted.*
*.tmp
*.backup
# Runtime files (do not commit)
duckhunt.json
config.json
duckhunt.json.bak
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
*.temp
*.temporary
temp/
tmp/

45
MULTI_CHANNEL_PLAN.md Normal file
View File

@@ -0,0 +1,45 @@
# Multi-Channel Support Implementation
## What Multi-Channel Means
- Players have **separate stats in each channel**
- Nick "Bob" in #channel1 has different XP than "Bob" in #channel2
- Database structure: `channels -> #channel1 -> players -> bob`
## Changes Needed
### 1. Database Structure (db.py)
```python
{
"channels": {
"#channel1": {
"players": {
"bob": { "xp": 100, ... },
"alice": { "xp": 50, ... }
}
},
"#channel2": {
"players": {
"bob": { "xp": 20, ... } # Different stats!
}
}
}
}
```
### 2. Database Methods
- `get_player(nick, channel)` - Get player in specific channel
- `get_players_for_channel(channel)` - Get all players in a channel
- `iter_all_players()` - Iterate over all channels and players
### 3. Command Changes (duckhuntbot.py)
- Pass `channel` parameter when calling `db.get_player(nick, channel)`
- Channel normalization (case-insensitive)
### 4. Stats Commands
- `!duckstats` shows stats for current channel
- `!globalducks` shows combined stats across all channels
## Benefits
- Fair: Can't bring channel1 XP into channel2
- Better: Each channel has own leaderboard
- Clean: Stats don't mix between channels

171
README.md
View File

@@ -0,0 +1,171 @@
# DuckHunt IRC Bot
DuckHunt is an asyncio-based IRC bot that runs a classic "duck hunting" mini-game in IRC channels.
## Credits
- Originally written by **Computertech**
- New features, fixes, and maintenance added by **End3r**
## Features
- **Multi-channel support** - Bot can be in multiple channels
- **Per-channel player stats** - Stats are tracked separately per channel
- **Global leaderboard** - View the global top 5 across all channels
- **Three duck types** - Normal, Golden (multi-HP), and Fast ducks
- **Shop system** - Buy items to improve your hunting
- **Leveling system** - Gain XP and increase your level
- **Admin commands** - Join/leave channels, spawn ducks, manage players
- **JSON persistence** - All stats saved to disk
- **Auto-save** - Progress saved automatically after each action
## Quick Start
### Requirements
- Python 3.8+
### Run
From the repo root:
```bash
python3 duckhunt.py
```
## Configuration
Copy the example config and edit it:
```bash
cp config.json.example config.json
```
Then edit `config.json`:
- `connection.server`, `connection.port`, `connection.nick`
- `connection.channels` (list of channels to join on connect)
- `connection.ssl` and optional password/SASL settings
- `admins` (list of admin nicks or nick+hostmask patterns)
**Security note:** `config.json` is ignored by git - don't commit real IRC passwords/tokens.
### Duck Types
Three duck types with different behaviors:
- **Normal** - Standard duck, 1 HP, base XP
- **Golden** - Multi-HP duck (3-5 HP), high XP, awards XP per hit
- **Fast** - Quick duck, 1 HP, flies away faster
Duck spawn behavior is configured in `config.json` under `duck_types`:
- `duck_types.golden.chance` - Probability of a golden duck (default: 0.15)
- `duck_types.fast.chance` - Probability of a fast duck (default: 0.25)
- `duck_types.golden.min_hp` / `duck_types.golden.max_hp` - Golden duck HP range
## Persistence
Player stats are saved to `duckhunt.json`:
- **Per-channel stats** - Players have separate stats per channel (stored under `channels`)
- **Global top 5** - `!globaltop` aggregates XP across all channels
- **Auto-save** - Database saved after each action (shoot, reload, shop, etc.)
- **Atomic writes** - Safe file handling prevents database corruption
- **Retry logic** - Automatic retry on save failures
## Commands
### Player Commands
- `!bang` - Shoot at a duck
- `!bef` or `!befriend` - Try to befriend a duck
- `!reload` - Reload your gun
- `!shop` - View available items
- `!shop buy <item_id>` - Purchase an item from the shop
- `!duckstats [player]` - View hunting statistics for the current channel
- `!topduck` - View leaderboard (top hunters)
- `!globaltop` - View global leaderboard (top 5 across all channels)
- `!duckhelp` - Get detailed command list via PM
### Admin Commands
- `!rearm <player|all>` - Give player a gun
- `!disarm <player>` - Confiscate player's gun
- `!ignore <player>` / `!unignore <player>` - Ignore/unignore commands
- `!ducklaunch [duck_type]` - Force spawn a duck (normal, golden, fast)
- `!join <#channel>` - Make bot join a channel
- `!part <#channel>` - Make bot leave a channel
Admin commands work in PM or in-channel.
## Shop Items
Basic shop items available (use `!shop` to see current inventory):
- **Bullets** - Ammunition refills
- **Magazines** - Extra ammo capacity
- **Gun Improvements** - Better accuracy, less jamming
- **Gun License** - Buy back your confiscated gun
- **Insurance** - Protection from penalties
Use `!shop buy <id>` to purchase.
## Gameplay
### How to Play
1. Wait for a duck to spawn (appears randomly in channel)
2. Type `!bang` to shoot it
3. Earn XP for successful hits
4. Level up to improve your stats
5. Buy items from `!shop` to enhance your hunting
### Duck Behavior
- **Normal ducks** - Standard targets, 1 shot to kill
- **Golden ducks** - Tougher! Multiple HP, gives XP per hit
- **Fast ducks** - Quick! They fly away faster than normal
### Stats Tracked
- XP (experience points)
- Ducks shot
- Ducks befriended
- Shots fired
- Accuracy percentage
- Current level
Note: stats are tracked per-channel; use `!globaltop` for an across-channels view.
## Repo Layout
```
duckhunt/
├── duckhunt.py # Entry point
├── config.json # Bot configuration (ignored by git)
├── config.json.example # Safe template to copy
├── duckhunt.json # Player database (auto-generated)
├── levels.json # Level definitions
├── shop.json # Shop item catalog
├── messages.json # Bot messages
└── src/
├── duckhuntbot.py # IRC bot + command routing
├── game.py # Duck game logic
├── db.py # Database persistence
├── shop.py # Shop/inventory system
├── levels.py # Leveling system
├── sasl.py # SASL authentication
├── error_handling.py # Error recovery
└── utils.py # Utility functions
```
## Recent Updates
- ✅ Fixed golden duck XP bug (now awards XP on each hit)
- ✅ Added `!join` and `!part` admin commands
- ✅ Improved `!duckhelp` with detailed PM
- ✅ Simplified to 3 core duck types for stability
- ✅ Enhanced database save reliability
**Happy Duck Hunting!** 🦆

171
config.json.example Normal file
View File

@@ -0,0 +1,171 @@
{
"connection": {
"server": "irc.example.net",
"port": 6697,
"nick": "Quackbot",
"channels": [
"#duckhunt"
],
"ssl": true,
"password": "",
"max_retries": 3,
"retry_delay": 5,
"timeout": 30,
"auto_rejoin": {
"enabled": true,
"retry_interval": 20,
"max_rejoin_attempts": 100
}
},
"sasl": {
"enabled": false,
"username": "",
"password": ""
},
"admins": [
"yourNickHere"
],
"duck_spawning": {
"spawn_min": 1200,
"spawn_max": 3600,
"timeout": 60,
"rearm_on_duck_shot": true
},
"duck_types": {
"normal": {
"xp": 10,
"timeout": 60,
"drop_chance": 0.15
},
"golden": {
"chance": 0.15,
"min_hp": 3,
"max_hp": 5,
"xp": 15,
"timeout": 60,
"drop_chance": 0.5
},
"fast": {
"chance": 0.25,
"timeout": 20,
"xp": 12,
"drop_chance": 0.25
},
"concrete": {
"chance": 0.08,
"hp": 3,
"xp": 3,
"timeout": 90,
"drop_chance": 0.15
},
"holy_grail": {
"chance": 0.03,
"hp": 8,
"xp": 10,
"timeout": 120,
"drop_chance": 0.35
},
"diamond": {
"chance": 0.01,
"hp": 10,
"xp": 15,
"timeout": 150,
"drop_chance": 0.5
},
"explosive": {
"chance": 0.02,
"hp": 1,
"xp": 20,
"timeout": 60,
"drop_chance": 0.25
},
"poisonous": {
"chance": 0.02,
"hp": 1,
"xp": 8,
"timeout": 60,
"drop_chance": 0.25
},
"radioactive": {
"chance": 0.005,
"hp": 1,
"xp": 15,
"timeout": 60,
"drop_chance": 0.35
},
"couple": {
"chance": 0.03,
"timeout": 60
},
"family": {
"chance": 0.015,
"timeout": 60
}
},
"item_drops": {
"normal_duck_drops": [
{"item_id": 1, "weight": 40},
{"item_id": 2, "weight": 25},
{"item_id": 4, "weight": 20},
{"item_id": 3, "weight": 15}
],
"fast_duck_drops": [
{"item_id": 1, "weight": 30},
{"item_id": 2, "weight": 25},
{"item_id": 4, "weight": 20},
{"item_id": 8, "weight": 15},
{"item_id": 3, "weight": 10}
],
"golden_duck_drops": [
{"item_id": 5, "weight": 25},
{"item_id": 6, "weight": 20},
{"item_id": 7, "weight": 20},
{"item_id": 2, "weight": 15},
{"item_id": 9, "weight": 10},
{"item_id": 1, "weight": 10}
]
},
"player_defaults": {
"accuracy": 75,
"magazines": 3,
"bullets_per_magazine": 6,
"jam_chance": 15,
"xp": 0
},
"gameplay": {
"befriend_success_rate": 75,
"befriend_xp": 5,
"accuracy_gain_on_hit": 1,
"accuracy_loss_on_miss": 2,
"min_accuracy": 10,
"max_accuracy": 100,
"min_befriend_success_rate": 5,
"max_befriend_success_rate": 95,
"wet_clothes_duration": 300
},
"features": {
"shop_enabled": true,
"inventory_enabled": true,
"auto_rearm_enabled": true
},
"limits": {
"max_inventory_items": 20,
"max_temp_effects": 20
},
"debug": {
"_comment_enabled": "Whether debug logging is enabled at all (true=debug mode, false=minimal logging)",
"enabled": true,
"_comment_log_level": "Overall logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
"log_level": "DEBUG",
"_comment_console_level": "Console output level - what shows in terminal (DEBUG, INFO, WARNING, ERROR)",
"console_log_level": "INFO",
"_comment_file_level": "File logging level - what gets written to log files (DEBUG, INFO, WARNING, ERROR)",
"file_log_level": "DEBUG",
"_comment_log_everything": "If true, logs ALL events. If false, logs only important events",
"log_everything": true,
"_comment_log_performance": "Whether to enable performance/metrics logging to performance.log",
"log_performance": true,
"_comment_unified_format": "If true, console and file logs use same format. If false, console has colors, file is plain",
"unified_format": true
}
}

46
duckhunt.py Normal file
View File

@@ -0,0 +1,46 @@
"""
DuckHunt IRC Bot - Simplified Entry Point
Commands: !bang, !reload, !shop, !rearm, !disarm
"""
import asyncio
import json
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from src.duckhuntbot import DuckHuntBot
def main():
"""Main entry point for DuckHunt Bot"""
try:
config_file = 'config.json'
if not os.path.exists(config_file):
print("❌ config.json not found!")
sys.exit(1)
with open(config_file) as f:
config = json.load(f)
bot = DuckHuntBot(config)
bot.logger.info("Starting DuckHunt Bot...")
# Run the bot
asyncio.run(bot.run())
except KeyboardInterrupt:
print("\n🛑 Shutdown interrupted by user")
except FileNotFoundError:
print("❌ config.json not found!")
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
else:
print("👋 DuckHunt Bot stopped gracefully")
if __name__ == '__main__':
main()

View File

@@ -1,272 +0,0 @@
# DuckHunt Bot Configuration Guide
This document explains all the configuration options available in `config.json` to customize your DuckHunt bot experience.
## Basic IRC Settings
```json
{
"server": "irc.rizon.net", // IRC server hostname
"port": 6697, // IRC server port (6667 for non-SSL, 6697 for SSL)
"nick": "DuckHunt", // Bot's nickname
"channels": ["#channel"], // List of channels to join
"ssl": true, // Enable SSL/TLS connection
"password": "", // Server password (if required)
"admins": ["nick1", "nick2"] // List of admin nicknames
}
```
## SASL Authentication
```json
"sasl": {
"enabled": true, // Enable SASL authentication
"username": "botaccount", // NickServ account username
"password": "botpassword" // NickServ account password
}
```
## Duck Spawning Configuration
```json
"duck_spawn_min": 1800, // Minimum time between duck spawns (seconds)
"duck_spawn_max": 5400, // Maximum time between duck spawns (seconds)
"duck_timeout_min": 45, // Minimum time duck stays alive (seconds)
"duck_timeout_max": 75, // Maximum time duck stays alive (seconds)
"sleep_hours": [], // Hours when no ducks spawn [start_hour, end_hour]
"max_ducks_per_channel": 3, // Maximum ducks that can exist per channel
```
### Duck Types
Configure different duck types with spawn rates and rewards:
```json
"duck_types": {
"normal": {
"enabled": true, // Enable this duck type
"spawn_rate": 70, // Percentage chance to spawn (out of 100)
"xp_reward": 10, // XP gained when caught
"health": 1 // How many hits to kill
},
"golden": {
"enabled": true,
"spawn_rate": 8,
"xp_reward": 50,
"health": 1
}
// ... more duck types
}
```
## Duck Befriending System
```json
"befriending": {
"enabled": true, // Enable !bef command
"base_success_rate": 65, // Base chance of successful befriend (%)
"max_success_rate": 90, // Maximum possible success rate (%)
"level_bonus_per_level": 2, // Success bonus per player level (%)
"level_bonus_cap": 20, // Maximum level bonus (%)
"luck_bonus_per_point": 3, // Success bonus per luck point (%)
"xp_reward": 8, // XP gained on successful befriend
"xp_reward_min": 1, // Minimum XP from befriending
"xp_reward_max": 3, // Maximum XP from befriending
"failure_xp_penalty": 1, // XP lost on failed befriend
"scared_away_chance": 10, // Chance duck flies away on failure (%)
"lucky_item_chance": 5 // Base chance for lucky item drops (%)
}
```
## Shooting Mechanics
```json
"shooting": {
"enabled": true, // Enable !bang command
"base_accuracy": 85, // Starting player accuracy (%)
"base_reliability": 90, // Starting gun reliability (%)
"jam_chance_base": 10, // Base gun jam chance (%)
"friendly_fire_enabled": true, // Allow shooting other players
"friendly_fire_chance": 5, // Chance of friendly fire (%)
"reflex_shot_bonus": 5, // Bonus for quick shots (%)
"miss_xp_penalty": 5, // XP lost on missed shot
"wild_shot_xp_penalty": 10, // XP lost on wild shot
"teamkill_xp_penalty": 20 // XP lost on team kill
}
```
## Weapon System
```json
"weapons": {
"enabled": true, // Enable weapon mechanics
"starting_weapon": "pistol", // Default weapon for new players
"starting_ammo": 6, // Starting ammo count
"max_ammo_base": 6, // Base maximum ammo capacity
"starting_chargers": 2, // Starting reload items
"max_chargers_base": 2, // Base maximum reload items
"durability_enabled": true, // Enable weapon wear/breaking
"confiscation_enabled": true // Allow admin gun confiscation
}
```
## Economy System
```json
"economy": {
"enabled": true, // Enable coin/shop system
"starting_coins": 100, // Coins for new players
"shop_enabled": true, // Enable !shop command
"trading_enabled": true, // Enable !trade command
"theft_enabled": true, // Enable !steal command
"theft_success_rate": 30, // Chance theft succeeds (%)
"theft_penalty": 50, // Coins lost if theft fails
"banking_enabled": true, // Enable banking system
"interest_rate": 5, // Bank interest rate (%)
"loan_enabled": true // Enable loan system
}
```
## Player Progression
```json
"progression": {
"enabled": true, // Enable XP/leveling system
"max_level": 40, // Maximum player level
"xp_multiplier": 1.0, // Global XP multiplier
"level_benefits_enabled": true, // Level bonuses (accuracy, etc.)
"titles_enabled": true, // Show player titles
"prestige_enabled": false // Enable prestige system
}
```
## Karma System
```json
"karma": {
"enabled": true, // Enable karma tracking
"hit_bonus": 2, // Karma for successful shots
"golden_hit_bonus": 5, // Karma for golden duck hits
"teamkill_penalty": 10, // Karma lost for team kills
"wild_shot_penalty": 3, // Karma lost for wild shots
"miss_penalty": 1, // Karma lost for misses
"befriend_success_bonus": 2, // Karma for successful befriends
"befriend_fail_penalty": 1 // Karma lost for failed befriends
}
```
## Items and Powerups
```json
"items": {
"enabled": true, // Enable item system
"lucky_items_enabled": true, // Enable lucky item drops
"lucky_item_base_chance": 5, // Base lucky item chance (%)
"detector_enabled": true, // Enable duck detector item
"silencer_enabled": true, // Enable silencer item
"sunglasses_enabled": true, // Enable sunglasses item
"explosive_ammo_enabled": true, // Enable explosive ammo
"sabotage_enabled": true, // Enable sabotage mechanics
"insurance_enabled": true, // Enable insurance system
"decoy_enabled": true // Enable decoy ducks
}
```
## Social Features
```json
"social": {
"leaderboards_enabled": true, // Enable !top command
"duck_alerts_enabled": true, // Enable duck spawn notifications
"private_messages_enabled": true, // Allow PM commands
"statistics_sharing_enabled": true, // Enable !stats sharing
"achievements_enabled": false // Enable achievement system
}
```
## Moderation Features
```json
"moderation": {
"ignore_system_enabled": true, // Enable !ignore command
"rate_limiting_enabled": true, // Prevent command spam
"rate_limit_cooldown": 2.0, // Seconds between commands
"admin_commands_enabled": true, // Enable admin commands
"ban_system_enabled": true, // Enable player banning
"database_reset_enabled": true, // Allow database resets
"admin_rearm_gives_full_ammo": true, // Admin !rearm gives full ammo
"admin_rearm_gives_full_chargers": true // Admin !rearm gives full chargers
}
```
## Advanced Features
```json
"advanced": {
"gun_jamming_enabled": true, // Enable gun jam mechanics
"weather_effects_enabled": false, // Weather affecting gameplay
"seasonal_events_enabled": false, // Special holiday events
"daily_challenges_enabled": false, // Daily quest system
"guild_system_enabled": false, // Player guilds/teams
"pvp_enabled": false // Player vs player combat
}
```
## Message Customization
```json
"messages": {
"custom_duck_messages_enabled": true, // Varied duck spawn messages
"color_enabled": true, // IRC color codes in messages
"emoji_enabled": true, // Unicode emojis in messages
"verbose_messages": true, // Detailed action messages
"success_sound_effects": true // Text sound effects
}
```
## Database Settings
```json
"database": {
"auto_save_enabled": true, // Automatic database saving
"auto_save_interval": 300, // Auto-save every N seconds
"backup_enabled": true, // Create database backups
"backup_interval": 3600, // Backup every N seconds
"compression_enabled": false // Compress database files
}
```
## Debug Options
```json
"debug": {
"debug_mode": false, // Enable debug features
"verbose_logging": false, // Extra detailed logs
"command_logging": false, // Log all commands
"performance_monitoring": false // Track performance metrics
}
```
## Configuration Tips
1. **Duck Spawn Timing**: Adjust `duck_spawn_min/max` based on channel activity
2. **Difficulty**: Lower `befriending.base_success_rate` for harder gameplay
3. **Economy**: Adjust XP rewards to balance progression
4. **Features**: Disable unwanted features by setting `enabled: false`
5. **Performance**: Enable rate limiting and disable verbose logging for busy channels
6. **Testing**: Use debug mode and shorter spawn times for testing
## Example Configurations
### Casual Server (Easy)
```json
"befriending": {
"base_success_rate": 80,
"max_success_rate": 95
},
"economy": {
"starting_coins": 200
}
```
### Competitive Server (Hard)
```json
"befriending": {
"base_success_rate": 45,
"max_success_rate": 75
},
"shooting": {
"base_accuracy": 70,
"friendly_fire_chance": 10
}
```
### Minimal Features
```json
"befriending": { "enabled": false },
"items": { "enabled": false },
"karma": { "enabled": false },
"social": { "leaderboards_enabled": false }
```

View File

@@ -1,172 +0,0 @@
# 🦆 DuckHunt IRC Bot
A feature-rich IRC game bot where players hunt ducks, upgrade weapons, trade items, and compete on leaderboards!
## 🚀 Features
### 🎯 Core Game Mechanics
- **Different Duck Types**: Common, Rare, Golden, and Armored ducks with varying rewards
- **Weapon System**: Multiple weapon types (Basic Gun, Shotgun, Rifle) with durability
- **Ammunition Types**: Standard, Rubber Bullets, Explosive Rounds
- **Weapon Attachments**: Laser Sight, Extended Magazine, Bipod
- **Accuracy & Reliability**: Skill-based hit/miss and reload failure mechanics
### 🏦 Economy System
- **Shop**: Buy/sell weapons, attachments, and upgrades
- **Banking**: Deposit coins for interest, take loans
- **Trading**: Trade coins and items with other players
- **Insurance**: Protect your equipment from damage
- **Hunting Licenses**: Unlock premium features and bonuses
### 👤 Player Progression
- **Hunter Levels**: Gain XP and level up for better abilities
- **Account System**: Register accounts with password authentication
- **Multiple Auth Methods**: Nick-based, hostmask, or registered account
- **Persistent Stats**: All progress saved to SQLite database
### 🏆 Social Features
- **Leaderboards**: Compete for top rankings
- **Duck Alerts**: Get notified when rare ducks spawn
- **Sabotage**: Interfere with other players (for a cost!)
- **Comprehensive Help**: Detailed command reference
## 📋 Requirements
- Python 3.7+
- asyncio support
- SQLite3 (included with Python)
## 🛠️ Installation
1. Clone or download the bot files
2. Edit `config.json` with your IRC server details:
```json
{
"server": "irc.libera.chat",
"port": 6697,
"nick": "DuckHuntBot",
"channels": ["#yourchannel"],
"ssl": true,
"sasl": false,
"password": "",
"duck_spawn_min": 60,
"duck_spawn_max": 300
}
```
3. Test the bot:
```bash
python test_bot.py
```
4. Run the bot:
```bash
python duckhunt.py
```
## 🎮 Commands
### 🎯 Hunting
- `!bang` - Shoot at a duck (accuracy-based hit/miss)
- `!reload` - Reload weapon (can fail based on reliability)
- `!catch` - Catch a duck with your hands
- `!bef` - Befriend a duck instead of shooting
### 🛒 Economy
- `!shop` - View available items
- `!buy <number>` - Purchase items
- `!sell <number>` - Sell equipment
- `!bank` - Banking services
- `!trade <player> <item> <amount>` - Trade with others
### 📊 Stats & Info
- `!stats` - Detailed combat statistics
- `!duckstats` - Personal hunting record
- `!leaderboard` - Top players ranking
- `!license` - Hunting license management
### ⚙️ Settings
- `!alerts` - Toggle duck spawn notifications
- `!help` - Complete command reference
### 🔐 Account System
- `/msg BotNick register <username> <password>` - Register account
- `/msg BotNick identify <username> <password>` - Login to account
### 🎮 Advanced
- `!sabotage <player>` - Sabotage another hunter's weapon
## 🗂️ File Structure
```
duckhunt/
├── src/
│ ├── duckhuntbot.py # Main IRC bot logic
│ ├── game.py # Game mechanics and commands
│ ├── db.py # SQLite database handling
│ ├── auth.py # Authentication system
│ ├── items.py # Duck types, weapons, attachments
│ ├── logging_utils.py # Colored logging setup
│ └── utils.py # IRC message parsing
├── config.json # Bot configuration
├── duckhunt.py # Main entry point
├── test_bot.py # Test script
└── README.md # This file
```
## 🎯 Game Balance
### Duck Types & Rewards
- **Common Duck** 🦆: 1 coin, 10 XP (70% spawn rate)
- **Rare Duck** 🦆✨: 3 coins, 25 XP (20% spawn rate)
- **Golden Duck** 🥇🦆: 10 coins, 50 XP (8% spawn rate)
- **Armored Duck** 🛡️🦆: 15 coins, 75 XP (2% spawn rate, 3 health)
### Weapon Stats
- **Basic Gun**: 0% accuracy bonus, 100 durability, 1 attachment slot
- **Shotgun**: -10% accuracy, 80 durability, 2 slots, spread shot
- **Rifle**: +20% accuracy, 120 durability, 3 slots
### Progression
- Players start with 100 coins and basic stats
- Level up by gaining XP from successful hunts
- Unlock better equipment and abilities as you progress
## 🔧 Configuration
Edit `config.json` to customize:
- IRC server and channels
- Duck spawn timing (min/max seconds)
- SSL and SASL authentication
- Bot nickname
## 🛡️ Security
- Passwords are hashed with PBKDF2
- Account data stored separately from temporary nick data
- Multiple authentication methods supported
- Database uses prepared statements to prevent injection
## 🐛 Troubleshooting
1. **Bot won't connect**: Check server/port in config.json
2. **Database errors**: Ensure write permissions in bot directory
3. **Commands not working**: Verify bot has joined the channel
4. **Test failures**: Run `python test_bot.py` to diagnose issues
## 🎖️ Contributing
Feel free to add new features:
- More duck types and weapons
- Additional mini-games
- Seasonal events
- Guild/team systems
- Advanced trading mechanics
## 📄 License
This bot is provided as-is for educational and entertainment purposes.
---
🦆 **Happy Hunting!** 🦆

View File

@@ -1,205 +0,0 @@
{
"server": "irc.rizon.net",
"port": 6697,
"nick": "DuckHunt",
"channels": ["#computertech"],
"ssl": true,
"sasl": {
"enabled": true,
"username": "duckhunt",
"password": "duckhunt//789//"
},
"password": "",
"admins": ["peorth", "computertech", "colby"],
"_comment_duck_spawning": "Duck spawning configuration",
"duck_spawn_min": 1800,
"duck_spawn_max": 5400,
"duck_timeout_min": 45,
"duck_timeout_max": 75,
"duck_types": {
"normal": {
"spawn_chance": 0.6,
"xp_reward": 10,
"difficulty": 1.0,
"flee_time": 15,
"messages": ["・゜゜・。。・゜゜\\\\_o< QUACK!"]
},
"fast": {
"spawn_chance": 0.25,
"xp_reward": 15,
"difficulty": 1.5,
"flee_time": 8,
"messages": ["・゜゜・。。・゜゜\\\\_o< QUACK! (Fast duck!)"]
},
"rare": {
"spawn_chance": 0.1,
"xp_reward": 30,
"difficulty": 2.0,
"flee_time": 12,
"messages": ["・゜゜・。。・゜゜\\\\_o< QUACK! (Rare duck!)"]
},
"golden": {
"spawn_chance": 0.05,
"xp_reward": 75,
"difficulty": 3.0,
"flee_time": 10,
"messages": ["・゜゜・。。・゜゜\\\\_✪< ★ GOLDEN DUCK ★"]
}
},
"sleep_hours": [],
"max_ducks_per_channel": 3,
"_comment_befriending": "Duck befriending configuration",
"befriending": {
"enabled": true,
"success_chance": 0.7,
"failure_messages": [
"The duck looked at you suspiciously and flew away!",
"The duck didn't trust you and escaped!",
"The duck was too scared and ran off!"
],
"scared_away_chance": 0.1,
"scared_away_messages": [
"You scared the duck away with your approach!",
"The duck was terrified and fled immediately!"
],
"xp_reward_min": 1,
"xp_reward_max": 3
},
"_comment_shooting": "Shooting mechanics configuration",
"shooting": {
"enabled": true,
"base_accuracy": 85,
"base_reliability": 90,
"jam_chance_base": 10,
"friendly_fire_enabled": true,
"friendly_fire_chance": 5,
"reflex_shot_bonus": 5,
"miss_xp_penalty": 5,
"wild_shot_xp_penalty": 10,
"teamkill_xp_penalty": 20
},
"_comment_weapons": "Weapon system configuration",
"weapons": {
"enabled": true,
"starting_weapon": "pistol",
"starting_ammo": 6,
"max_ammo_base": 6,
"starting_chargers": 2,
"max_chargers_base": 2,
"durability_enabled": true,
"confiscation_enabled": true
},
"_comment_economy": "Economy and shop configuration",
"economy": {
"enabled": true,
"starting_coins": 100,
"shop_enabled": true,
"trading_enabled": true,
"theft_enabled": true,
"theft_success_rate": 30,
"theft_penalty": 50,
"banking_enabled": true,
"interest_rate": 5,
"loan_enabled": true,
"inventory_system_enabled": true,
"max_inventory_slots": 20
},
"_comment_progression": "Player progression configuration",
"progression": {
"enabled": true,
"max_level": 40,
"xp_multiplier": 1.0,
"level_benefits_enabled": true,
"titles_enabled": true,
"prestige_enabled": false
},
"_comment_karma": "Karma system configuration",
"karma": {
"enabled": true,
"hit_bonus": 2,
"golden_hit_bonus": 5,
"teamkill_penalty": 10,
"wild_shot_penalty": 3,
"miss_penalty": 1,
"befriend_success_bonus": 2,
"befriend_fail_penalty": 1
},
"_comment_items": "Items and powerups configuration",
"items": {
"enabled": true,
"lucky_items_enabled": true,
"lucky_item_base_chance": 5,
"detector_enabled": true,
"silencer_enabled": true,
"sunglasses_enabled": true,
"explosive_ammo_enabled": true,
"sabotage_enabled": true,
"insurance_enabled": true,
"decoy_enabled": true
},
"_comment_social": "Social features configuration",
"social": {
"leaderboards_enabled": true,
"duck_alerts_enabled": true,
"private_messages_enabled": true,
"statistics_sharing_enabled": true,
"achievements_enabled": false
},
"_comment_moderation": "Moderation and admin features",
"moderation": {
"ignore_system_enabled": true,
"rate_limiting_enabled": true,
"rate_limit_cooldown": 2.0,
"admin_commands_enabled": true,
"ban_system_enabled": true,
"database_reset_enabled": true,
"admin_rearm_gives_full_ammo": false,
"admin_rearm_gives_full_chargers":false
},
"_comment_advanced": "Advanced game mechanics",
"advanced": {
"gun_jamming_enabled": true,
"weather_effects_enabled": false,
"seasonal_events_enabled": false,
"daily_challenges_enabled": false,
"guild_system_enabled": false,
"pvp_enabled": false
},
"_comment_messages": "Message customization",
"messages": {
"custom_duck_messages_enabled": true,
"color_enabled": true,
"emoji_enabled": true,
"verbose_messages": true,
"success_sound_effects": true
},
"_comment_database": "Database and persistence",
"database": {
"auto_save_enabled": true,
"auto_save_interval": 300,
"backup_enabled": true,
"backup_interval": 3600,
"compression_enabled": false
},
"_comment_debug": "Debug and logging options",
"debug": {
"debug_mode": false,
"verbose_logging": false,
"command_logging": false,
"performance_monitoring": false
}
}

View File

@@ -1,216 +0,0 @@
{
"server": "irc.rizon.net",
"port": 6697,
"nick": "DuckHunt",
"channels": ["#computertech"],
"ssl": true,
"sasl": {
"enabled": true,
"username": "duckhunt",
"password": "duckhunt//789//"
},
"password": "",
"admins": ["peorth", "computertech"],
"_comment_duck_spawning": "Duck spawning configuration",
"duck_spawn_min": 1800,
"duck_spawn_max": 5400,
"duck_timeout_min": 45,
"duck_timeout_max": 75,
"duck_types": {
"normal": {
"enabled": true,
"spawn_rate": 70,
"xp_reward": 10,
"coin_reward": 1,
"health": 1
},
"golden": {
"enabled": true,
"spawn_rate": 8,
"xp_reward": 50,
"coin_reward": 10,
"health": 1
},
"armored": {
"enabled": true,
"spawn_rate": 2,
"xp_reward": 75,
"coin_reward": 15,
"health": 3
},
"rare": {
"enabled": true,
"spawn_rate": 20,
"xp_reward": 25,
"coin_reward": 3,
"health": 1
}
},
"sleep_hours": [],
"max_ducks_per_channel": 3,
"_comment_befriending": "Duck befriending configuration",
"befriending": {
"enabled": true,
"base_success_rate": 65,
"max_success_rate": 90,
"level_bonus_per_level": 2,
"level_bonus_cap": 20,
"luck_bonus_per_point": 3,
"xp_reward": 8,
"coin_reward_min": 1,
"coin_reward_max": 2,
"failure_xp_penalty": 1,
"scared_away_chance": 10,
"lucky_item_chance": 5
},
"_comment_shooting": "Shooting mechanics configuration",
"shooting": {
"enabled": true,
"base_accuracy": 85,
"base_reliability": 90,
"jam_chance_base": 10,
"friendly_fire_enabled": true,
"friendly_fire_chance": 5,
"reflex_shot_bonus": 5,
"miss_xp_penalty": 5,
"wild_shot_xp_penalty": 10,
"teamkill_xp_penalty": 20
},
"_comment_weapons": "Weapon system configuration",
"weapons": {
"enabled": true,
"starting_weapon": "pistol",
"starting_ammo": 6,
"max_ammo_base": 6,
"starting_chargers": 2,
"max_chargers_base": 2,
"durability_enabled": true,
"confiscation_enabled": true
},
"_comment_economy": "Economy and shop configuration",
"economy": {
"enabled": true,
"starting_coins": 100,
"shop_enabled": true,
"trading_enabled": true,
"theft_enabled": true,
"theft_success_rate": 30,
"theft_penalty": 50,
"banking_enabled": true,
"interest_rate": 5,
"loan_enabled": true
},
"_comment_progression": "Player progression configuration",
"progression": {
"enabled": true,
"max_level": 40,
"xp_multiplier": 1.0,
"level_benefits_enabled": true,
"titles_enabled": true,
"prestige_enabled": false
},
"_comment_karma": "Karma system configuration",
"karma": {
"enabled": true,
"hit_bonus": 2,
"golden_hit_bonus": 5,
"teamkill_penalty": 10,
"wild_shot_penalty": 3,
"miss_penalty": 1,
"befriend_success_bonus": 2,
"befriend_fail_penalty": 1
},
"_comment_items": "Items and powerups configuration",
"items": {
"enabled": true,
"lucky_items_enabled": true,
"lucky_item_base_chance": 5,
"detector_enabled": true,
"silencer_enabled": true,
"sunglasses_enabled": true,
"explosive_ammo_enabled": true,
"sabotage_enabled": true,
"insurance_enabled": true,
"decoy_enabled": true
},
"_comment_social": "Social features configuration",
"social": {
"leaderboards_enabled": true,
"duck_alerts_enabled": true,
"private_messages_enabled": true,
"statistics_sharing_enabled": true,
"achievements_enabled": false
},
"_comment_moderation": "Moderation and admin features",
"moderation": {
"ignore_system_enabled": true,
"rate_limiting_enabled": true,
"rate_limit_cooldown": 2.0,
"admin_commands_enabled": true,
"ban_system_enabled": true,
"database_reset_enabled": true
},
"_comment_advanced": "Advanced game mechanics",
"advanced": {
"gun_jamming_enabled": true,
"weather_effects_enabled": false,
"seasonal_events_enabled": false,
"daily_challenges_enabled": false,
"guild_system_enabled": false,
"pvp_enabled": false
},
"_comment_messages": "Message customization",
"messages": {
"custom_duck_messages_enabled": true,
"color_enabled": true,
"emoji_enabled": true,
"verbose_messages": true,
"success_sound_effects": true
},
"_comment_database": "Database and persistence",
"database": {
"auto_save_enabled": true,
"auto_save_interval": 300,
"backup_enabled": true,
"backup_interval": 3600,
"compression_enabled": false
},
"_comment_debug": "Debug and logging options",
"debug": {
"debug_mode": false,
"verbose_logging": false,
"command_logging": false,
"performance_monitoring": false
}
}c.rizon.net",
"port": 6697,
"nick": "DuckHunt",
"channels": ["#computertech"],
"ssl": true,
"sasl": {
"enabled": true,
"username": "duckhunt",
"password": "duckhunt//789//"
},
"password": "",
"admins": ["colby", "computertech"],
"duck_spawn_min": 1800,
"duck_spawn_max": 5400,
"duck_timeout_min": 45,
"duck_timeout_max": 75,
"_comment": "Run with: python3 simple_duckhunt.py | Admins config-only | Private admin: /msg DuckHuntBot restart|quit|launch | Duck timeout: random between min-max seconds"
}

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env python3
"""
SASL Integration Demo for DuckHunt Bot
This script demonstrates how the modular SASL authentication works
"""
import asyncio
import json
import sys
import os
# Add src directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from src.sasl import SASLHandler
from src.logging_utils import setup_logger
class MockBot:
"""Mock bot for testing SASL without IRC connection"""
def __init__(self, config):
self.config = config
self.logger = setup_logger("MockBot")
self.messages_sent = []
def send_raw(self, message):
"""Mock send_raw that just logs the message"""
self.messages_sent.append(message)
self.logger.info(f"SEND: {message}")
async def register_user(self):
"""Mock registration"""
self.logger.info("Mock user registration completed")
async def demo_sasl_flow():
"""Demonstrate the SASL authentication flow"""
print("🔐 SASL Authentication Flow Demo")
print("=" * 50)
# Load config
with open('config.json') as f:
config = json.load(f)
# Override with test credentials for demo
config['sasl'] = {
'enabled': True,
'username': 'testuser',
'password': 'testpass123'
}
# Create mock bot and SASL handler
bot = MockBot(config)
sasl_handler = SASLHandler(bot, config)
print("\n1⃣ Starting SASL negotiation...")
if await sasl_handler.start_negotiation():
print("✅ SASL negotiation started successfully")
else:
print("❌ SASL negotiation failed to start")
return
print("\n2⃣ Simulating server CAP response...")
# Simulate server listing SASL capability
params = ['*', 'LS', '*']
trailing = 'sasl multi-prefix extended-join'
await sasl_handler.handle_cap_response(params, trailing)
print("\n3⃣ Simulating server acknowledging SASL capability...")
# Simulate server acknowledging SASL
params = ['*', 'ACK']
trailing = 'sasl'
await sasl_handler.handle_cap_response(params, trailing)
print("\n4⃣ Simulating server ready for authentication...")
# Simulate server ready for auth
params = ['+']
await sasl_handler.handle_authenticate_response(params)
print("\n5⃣ Simulating successful authentication...")
# Simulate successful authentication
params = ['DuckHunt']
trailing = 'You are now logged in as duckhunt'
await sasl_handler.handle_sasl_result('903', params, trailing)
print(f"\n📤 Messages sent to server:")
for i, msg in enumerate(bot.messages_sent, 1):
print(f" {i}. {msg}")
print(f"\n🔍 Authentication status: {'✅ Authenticated' if sasl_handler.is_authenticated() else '❌ Not authenticated'}")
print("\n" + "=" * 50)
print("✨ SASL flow demonstration complete!")
async def demo_sasl_failure():
"""Demonstrate SASL failure handling"""
print("\n\n🚫 SASL Failure Handling Demo")
print("=" * 50)
# Create mock bot with wrong credentials
config = {
'sasl': {
'enabled': True,
'username': 'testuser',
'password': 'wrong_password'
}
}
bot = MockBot(config)
sasl_handler = SASLHandler(bot, config)
print("\n1⃣ Starting SASL with wrong credentials...")
await sasl_handler.start_negotiation()
# Simulate failed authentication
params = ['DuckHunt']
trailing = 'Invalid credentials'
await sasl_handler.handle_sasl_result('904', params, trailing)
print(f"\n🔍 Authentication status: {'✅ Authenticated' if sasl_handler.is_authenticated() else '❌ Not authenticated'}")
print("✅ Failure handled gracefully - bot will fallback to NickServ")
if __name__ == '__main__':
asyncio.run(demo_sasl_flow())
asyncio.run(demo_sasl_failure())

Binary file not shown.

View File

@@ -1,960 +0,0 @@
{
"players": {
"guest44288": {
"current_nick": "Guest44288",
"hostmask": "~Colby@Rizon-FFE0901B.ipcom.comunitel.net",
"coins": 102,
"caught": 1,
"ammo": 9,
"max_ammo": 10,
"chargers": 2,
"max_chargers": 2,
"xp": 15,
"accuracy": 85,
"reliability": 90,
"duck_start_time": null,
"gun_level": 1,
"luck": 0,
"gun_type": "pistol"
},
"colby": {
"coins": 119,
"caught": 12,
"ammo": 5,
"max_ammo": 10,
"chargers": 0,
"max_chargers": 2,
"xp": 100,
"accuracy": 65,
"reliability": 90,
"duck_start_time": null,
"gun_level": 1,
"luck": 0,
"gun_type": "pistol",
"gun_confiscated": false,
"jammed": false,
"jammed_count": 1,
"total_ammo_used": 12,
"shot_at": 9,
"reflex_shots": 6,
"total_reflex_time": 47.87793278694153,
"best_time": 2.6269078254699707,
"karma": 1,
"wild_shots": 4,
"befriended": 6,
"missed": 1,
"inventory": {
"15": 1
},
"sand": 1
},
"colby_": {
"xp": 0,
"caught": 0,
"befriended": 0,
"missed": 0,
"ammo": 6,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 0,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 0,
"shot_at": 0,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"computertech": {
"xp": 4,
"caught": 1,
"befriended": 0,
"missed": 2,
"ammo": 3,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 0,
"deflection": 0,
"defense": 0,
"jammed": true,
"jammed_count": 4,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 1.458634614944458,
"total_reflex_time": 1.458634614944458,
"reflex_shots": 1,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 3,
"shot_at": 3,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0,
"inventory": {}
},
"loulan": {
"xp": -9,
"caught": 0,
"befriended": 0,
"missed": 2,
"ammo": 5,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": -5,
"deflection": 0,
"defense": 0,
"jammed": true,
"jammed_count": 1,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 1,
"accidents": 0,
"total_ammo_used": 3,
"shot_at": 2,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"madafaka": {
"xp": 42,
"caught": 3,
"befriended": 0,
"missed": 0,
"ammo": 3,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 6,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 2.8583714962005615,
"total_reflex_time": 12.643521547317505,
"reflex_shots": 3,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 3,
"shot_at": 4,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"peorth": {
"xp": 4,
"caught": 1,
"befriended": 0,
"missed": 0,
"ammo": 6,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 2,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 1,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 18.33902668952942,
"total_reflex_time": 18.33902668952942,
"reflex_shots": 1,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 1,
"shot_at": 2,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"dgw": {
"xp": 30,
"caught": 2,
"befriended": 0,
"missed": 0,
"ammo": 4,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 4,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 9.253741025924683,
"total_reflex_time": 20.60851550102234,
"reflex_shots": 2,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 2,
"shot_at": 2,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 1,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"admiral_hubris": {
"xp": 0,
"caught": 1,
"befriended": 0,
"missed": 0,
"ammo": 4,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 45,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": true,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": -17,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 6.574016809463501,
"total_reflex_time": 6.574016809463501,
"reflex_shots": 1,
"wild_shots": 3,
"accidents": 1,
"total_ammo_used": 4,
"shot_at": 1,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0,
"inventory": {}
},
"xysha": {
"xp": 0,
"caught": 0,
"befriended": 0,
"missed": 0,
"ammo": 5,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": true,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": -14,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 1,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 1,
"accidents": 1,
"total_ammo_used": 1,
"shot_at": 1,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0,
"inventory": {}
},
"boliver": {
"xp": 125,
"caught": 5,
"befriended": 4,
"missed": 2,
"ammo": 5,
"max_ammo": 6,
"chargers": 1,
"max_chargers": 2,
"accuracy": 45,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": true,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 1,
"karma": -3,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 7,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 2.484381914138794,
"total_reflex_time": 19.068661212921143,
"reflex_shots": 5,
"wild_shots": 2,
"accidents": 1,
"total_ammo_used": 9,
"shot_at": 7,
"lucky_shots": 1,
"luck": 1,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 1,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0,
"inventory": {}
},
"kaitphone": {
"xp": 13,
"caught": 1,
"befriended": 0,
"missed": 1,
"ammo": 4,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 1,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 3,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 28.610472440719604,
"total_reflex_time": 28.610472440719604,
"reflex_shots": 1,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 2,
"shot_at": 2,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"milambar": {
"xp": 12,
"caught": 2,
"befriended": 3,
"missed": 0,
"ammo": 3,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": true,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 7,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 1,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 2.784888982772827,
"total_reflex_time": 8.606451749801636,
"reflex_shots": 2,
"wild_shots": 1,
"accidents": 0,
"total_ammo_used": 3,
"shot_at": 2,
"lucky_shots": 0,
"luck": 1,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0,
"inventory": {}
},
"wobotkoala": {
"xp": 0,
"caught": 0,
"befriended": 0,
"missed": 0,
"ammo": 6,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 0,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 0,
"shot_at": 0,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"general_kornwallace": {
"xp": 0,
"caught": 0,
"befriended": 0,
"missed": 0,
"ammo": 6,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 0,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 1,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 0,
"shot_at": 0,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"helderheid": {
"xp": 0,
"caught": 0,
"befriended": 0,
"missed": 0,
"ammo": 6,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 0,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 0,
"shot_at": 0,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"coolkevin": {
"xp": 12,
"caught": 1,
"befriended": 0,
"missed": 0,
"ammo": 5,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 65,
"reliability": 70,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": 2,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 4.75800085067749,
"total_reflex_time": 4.75800085067749,
"reflex_shots": 1,
"wild_shots": 0,
"accidents": 0,
"total_ammo_used": 1,
"shot_at": 2,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
},
"magicalpig": {
"xp": 23,
"caught": 2,
"befriended": 1,
"missed": 1,
"ammo": 3,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 85,
"reliability": 90,
"weapon": "pistol",
"gun_confiscated": false,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"golden_ducks": 0,
"karma": -11,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 9.407448291778564,
"total_reflex_time": 21.80314612388611,
"reflex_shots": 2,
"wild_shots": 2,
"accidents": 1,
"total_ammo_used": 5,
"shot_at": 3,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0,
"inventory": {}
},
"tabb": {
"xp": -5,
"caught": 0,
"befriended": 0,
"missed": 0,
"ammo": 5,
"max_ammo": 6,
"chargers": 2,
"max_chargers": 2,
"accuracy": 85,
"reliability": 90,
"weapon": "pistol",
"gun_confiscated": true,
"explosive_ammo": false,
"settings": {
"notices": true,
"private_messages": false
},
"inventory": {},
"golden_ducks": 0,
"karma": -3,
"deflection": 0,
"defense": 0,
"jammed": false,
"jammed_count": 0,
"deaths": 0,
"neutralized": 0,
"deflected": 0,
"best_time": 999.9,
"total_reflex_time": 0.0,
"reflex_shots": 0,
"wild_shots": 1,
"accidents": 0,
"total_ammo_used": 1,
"shot_at": 0,
"lucky_shots": 0,
"luck": 0,
"detector": 0,
"silencer": 0,
"sunglasses": 0,
"clothes": 0,
"grease": 0,
"brush": 0,
"mirror": 0,
"sand": 0,
"water": 0,
"sabotage": 0,
"life_insurance": 0,
"liability": 0,
"decoy": 0,
"bread": 0,
"duck_detector": 0,
"mechanical": 0
}
},
"last_save": "1757707147.438802"
}

View File

@@ -1,543 +0,0 @@
[2025-09-11 18:30:40,346] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 18:30:40,346] INFO: DuckHunt Bot initializing...
[2025-09-11 18:30:40,347] INFO: Starting DuckHunt Bot...
[2025-09-11 18:30:40,347] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 18:30:40,420] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-11 18:30:40,579] INFO: Connected successfully!
[2025-09-11 18:30:40,579] INFO: Registering as DuckHuntBot
[2025-09-11 18:30:41,067] INFO: Successfully registered!
[2025-09-11 18:30:41,067] INFO: Joining #colby
[2025-09-11 18:30:41,118] INFO: Successfully joined #colby
[2025-09-11 18:30:41,582] INFO: Starting duck spawning...
[2025-09-11 18:30:46,583] INFO: Admin spawned normal duck 965d7945 in #colby
[2025-09-11 18:30:46,583] INFO: Waiting 56m 37s for next duck
[2025-09-11 18:31:46,591] INFO: Duck 965d7945 timed out in #colby
[2025-09-11 18:38:33,894] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-11 18:38:34,097] INFO: Shutdown requested, stopping all tasks...
[2025-09-11 18:38:34,097] INFO: Starting cleanup process...
[2025-09-11 18:38:35,211] INFO: IRC connection closed
[2025-09-11 18:38:35,225] INFO: Final database save completed - 3 players saved
[2025-09-11 18:38:35,226] INFO: Cleanup completed successfully
[2025-09-11 18:38:35,234] INFO: DuckHunt Bot shutdown complete
[2025-09-11 18:38:53,536] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 18:38:53,536] INFO: DuckHunt Bot initializing...
[2025-09-11 18:38:53,537] INFO: Starting DuckHunt Bot...
[2025-09-11 18:38:53,537] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 18:38:53,607] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-11 18:38:53,785] INFO: Connected successfully!
[2025-09-11 18:38:53,785] INFO: SASL authentication enabled
[2025-09-11 18:38:54,162] INFO: SASL capability available
[2025-09-11 18:38:54,221] INFO: SASL capability acknowledged
[2025-09-11 18:38:54,221] INFO: Authenticating via SASL as duckhunt
[2025-09-11 18:38:54,645] INFO: Server ready for SASL authentication
[2025-09-11 18:38:54,645] ERROR: SASL authentication failed!
[2025-09-11 18:38:54,645] INFO: Registering as DuckHunt
[2025-09-11 18:39:27,102] WARNING: Connection closed by server
[2025-09-11 18:39:27,103] WARNING: A main task completed unexpectedly
[2025-09-11 18:39:27,103] INFO: Starting cleanup process...
[2025-09-11 18:39:27,105] INFO: Final database save completed - 3 players saved
[2025-09-11 18:39:27,105] INFO: Cleanup completed successfully
[2025-09-11 18:39:27,106] INFO: DuckHunt Bot shutdown complete
[2025-09-11 18:41:03,279] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 18:41:03,279] INFO: DuckHunt Bot initializing...
[2025-09-11 18:41:03,280] INFO: Starting DuckHunt Bot...
[2025-09-11 18:41:03,280] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 18:41:03,354] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-11 18:41:03,516] INFO: Connected successfully!
[2025-09-11 18:41:03,517] INFO: SASL authentication enabled
[2025-09-11 18:41:03,611] INFO: SASL capability available
[2025-09-11 18:41:03,660] INFO: SASL capability acknowledged
[2025-09-11 18:41:03,660] INFO: Authenticating via SASL as duckhunt
[2025-09-11 18:41:04,075] INFO: Server ready for SASL authentication
[2025-09-11 18:41:04,076] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-11 18:41:04,076] ERROR: Attempted username: duckhunt
[2025-09-11 18:41:04,076] INFO: Registering as DuckHunt
[2025-09-11 18:41:36,030] WARNING: Connection closed by server
[2025-09-11 18:41:36,031] WARNING: A main task completed unexpectedly
[2025-09-11 18:41:36,031] INFO: Starting cleanup process...
[2025-09-11 18:41:36,032] INFO: Final database save completed - 3 players saved
[2025-09-11 18:41:36,032] INFO: Cleanup completed successfully
[2025-09-11 18:41:36,033] INFO: DuckHunt Bot shutdown complete
[2025-09-11 17:52:42,777] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 17:52:42,778] INFO: DuckHunt Bot initializing...
[2025-09-11 17:52:42,778] INFO: Starting DuckHunt Bot...
[2025-09-11 17:52:42,778] INFO: Loaded 3 players from duckhunt.json
[2025-09-11 17:52:42,800] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-11 17:52:42,914] INFO: Connected successfully!
[2025-09-11 17:52:42,914] INFO: SASL authentication enabled
[2025-09-11 17:52:42,926] INFO: SASL capability available
[2025-09-11 17:52:42,932] INFO: SASL capability acknowledged
[2025-09-11 17:52:42,932] INFO: Authenticating via SASL as duckhunt
[2025-09-11 17:52:43,305] INFO: Server ready for SASL authentication
[2025-09-11 17:52:43,305] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-11 17:52:43,305] INFO: Falling back to NickServ identification...
[2025-09-11 17:52:43,306] ERROR: Attempted username: duckhunt
[2025-09-11 17:52:43,306] INFO: Registering as DuckHunt
[2025-09-11 17:52:43,357] ERROR: SASL authentication aborted! (906)
[2025-09-11 17:52:43,357] INFO: Falling back to NickServ identification...
[2025-09-11 17:52:43,358] INFO: Registering as DuckHunt
[2025-09-11 17:52:43,358] INFO: Successfully registered!
[2025-09-11 17:52:43,358] INFO: Attempting NickServ identification for duckhunt
[2025-09-11 17:52:44,359] INFO: NickServ identification commands sent
[2025-09-11 17:52:44,360] INFO: Joining #computertech
[2025-09-11 17:52:44,366] INFO: Successfully joined #computertech
[2025-09-11 17:52:44,917] INFO: Starting duck spawning...
[2025-09-11 17:52:49,920] INFO: Admin spawned normal duck 2afda3aa in #computertech
[2025-09-11 17:52:49,920] INFO: Waiting 47m 46s for next duck
[2025-09-11 17:53:11,783] INFO: Admin spawned normal duck abbfac62 in #computertech
[2025-09-11 17:54:10,655] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-11 17:54:11,044] INFO: Duck spawning stopped due to shutdown request
[2025-09-11 17:54:11,045] INFO: Shutdown requested, stopping all tasks...
[2025-09-11 17:54:11,045] INFO: Starting cleanup process...
[2025-09-11 17:54:12,149] INFO: IRC connection closed
[2025-09-11 17:54:12,154] INFO: Final database save completed - 6 players saved
[2025-09-11 17:54:12,154] INFO: Cleanup completed successfully
[2025-09-11 17:54:12,156] INFO: DuckHunt Bot shutdown complete
[2025-09-11 17:54:28,757] INFO: Loaded 6 players from duckhunt.json
[2025-09-11 17:54:28,757] INFO: DuckHunt Bot initializing...
[2025-09-11 17:54:28,757] INFO: Starting DuckHunt Bot...
[2025-09-11 17:54:28,757] INFO: Loaded 6 players from duckhunt.json
[2025-09-11 17:54:28,780] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-11 17:54:28,894] INFO: Connected successfully!
[2025-09-11 17:54:28,894] INFO: SASL authentication enabled
[2025-09-11 17:54:28,906] INFO: SASL capability available
[2025-09-11 17:54:28,913] INFO: SASL capability acknowledged
[2025-09-11 17:54:28,913] INFO: Authenticating via SASL as duckhunt
[2025-09-11 17:54:29,288] INFO: Server ready for SASL authentication
[2025-09-11 17:54:29,289] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-11 17:54:29,289] INFO: Falling back to NickServ identification...
[2025-09-11 17:54:29,289] ERROR: Attempted username: duckhunt
[2025-09-11 17:54:29,289] INFO: Registering as DuckHunt
[2025-09-11 17:54:29,302] ERROR: SASL authentication aborted! (906)
[2025-09-11 17:54:29,303] INFO: Falling back to NickServ identification...
[2025-09-11 17:54:29,303] INFO: Registering as DuckHunt
[2025-09-11 17:54:29,303] INFO: Successfully registered!
[2025-09-11 17:54:29,303] INFO: Attempting NickServ identification for duckhunt
[2025-09-11 17:54:30,304] INFO: NickServ identification commands sent
[2025-09-11 17:54:30,305] INFO: Joining #computertech
[2025-09-11 17:54:30,311] INFO: Successfully joined #computertech
[2025-09-11 17:54:30,898] INFO: Starting duck spawning...
[2025-09-11 17:54:35,900] INFO: Admin spawned normal duck ff2612cf in #computertech
[2025-09-11 17:54:35,901] INFO: Waiting 41m 7s for next duck
[2025-09-11 17:55:55,911] INFO: Duck ff2612cf timed out in #computertech
[2025-09-11 18:10:31,079] INFO: Admin spawned normal duck b7398aed in #computertech
[2025-09-11 18:35:46,651] INFO: Admin spawned normal duck ae21e5f4 in #computertech
[2025-09-11 18:35:46,651] INFO: Waiting 30m 29s for next duck
[2025-09-11 19:06:18,504] INFO: Admin spawned normal duck 79032e28 in #computertech
[2025-09-11 19:06:18,505] INFO: Waiting 37m 53s for next duck
[2025-09-11 19:44:15,288] INFO: Admin spawned normal duck 02fe65b6 in #computertech
[2025-09-11 19:44:15,288] INFO: Waiting 39m 22s for next duck
[2025-09-11 20:23:41,185] INFO: Admin spawned normal duck 829273ae in #computertech
[2025-09-11 20:23:41,186] INFO: Waiting 47m 31s for next duck
[2025-09-11 20:24:57,093] INFO: Duck 829273ae timed out in #computertech
[2025-09-11 21:11:16,779] INFO: Admin spawned normal duck 8298e88d in #computertech
[2025-09-11 21:11:16,779] INFO: Waiting 75m 48s for next duck
[2025-09-11 22:27:12,035] INFO: Admin spawned normal duck ef5755f3 in #computertech
[2025-09-11 22:27:12,035] INFO: Waiting 55m 57s for next duck
[2025-09-11 23:23:14,339] INFO: Admin spawned normal duck 9725ad5b in #computertech
[2025-09-11 23:23:14,339] INFO: Waiting 57m 47s for next duck
[2025-09-12 00:21:06,885] INFO: Admin spawned normal duck 62b88d87 in #computertech
[2025-09-12 00:21:06,885] INFO: Waiting 73m 47s for next duck
[2025-09-12 01:35:00,951] INFO: Admin spawned normal duck 3f7dc294 in #computertech
[2025-09-12 01:35:00,951] INFO: Waiting 84m 10s for next duck
[2025-09-12 01:36:09,586] INFO: Duck 3f7dc294 timed out in #computertech
[2025-09-12 02:59:19,153] INFO: Admin spawned normal duck 45e3ab57 in #computertech
[2025-09-12 02:59:19,154] INFO: Waiting 60m 35s for next duck
[2025-09-12 02:59:25,289] WARNING: A main task completed unexpectedly
[2025-09-12 02:59:25,289] INFO: Starting cleanup process...
[2025-09-12 02:59:26,394] INFO: IRC connection closed
[2025-09-12 02:59:26,400] INFO: Final database save completed - 16 players saved
[2025-09-12 02:59:26,400] INFO: Cleanup completed successfully
[2025-09-12 02:59:26,401] INFO: DuckHunt Bot shutdown complete
[2025-09-12 13:45:28,730] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 13:45:28,730] INFO: DuckHunt Bot initializing...
[2025-09-12 13:45:28,730] INFO: Starting DuckHunt Bot...
[2025-09-12 13:45:28,731] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 13:45:28,753] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 13:45:28,865] INFO: Connected successfully!
[2025-09-12 13:45:28,865] INFO: SASL authentication enabled
[2025-09-12 13:45:30,015] INFO: SASL capability available
[2025-09-12 13:45:30,022] INFO: SASL capability acknowledged
[2025-09-12 13:45:30,022] INFO: Authenticating via SASL as duckhunt
[2025-09-12 13:45:30,389] INFO: Server ready for SASL authentication
[2025-09-12 13:45:30,389] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-12 13:45:30,389] INFO: Falling back to NickServ identification...
[2025-09-12 13:45:30,389] ERROR: Attempted username: duckhunt
[2025-09-12 13:45:30,389] INFO: Registering as DuckHunt
[2025-09-12 13:45:30,402] ERROR: SASL authentication aborted! (906)
[2025-09-12 13:45:30,402] INFO: Falling back to NickServ identification...
[2025-09-12 13:45:30,403] INFO: Registering as DuckHunt
[2025-09-12 13:45:30,403] INFO: Successfully registered!
[2025-09-12 13:45:30,403] INFO: Attempting NickServ identification for duckhunt
[2025-09-12 13:45:31,406] INFO: NickServ identification commands sent
[2025-09-12 13:45:31,406] INFO: Joining #computertech
[2025-09-12 13:45:31,413] INFO: Successfully joined #computertech
[2025-09-12 13:45:31,870] INFO: Starting duck spawning...
[2025-09-12 13:45:36,872] INFO: Admin spawned normal duck 8e703ce0 in #computertech
[2025-09-12 13:45:36,872] INFO: Waiting 64m 21s for next duck
[2025-09-12 13:45:46,370] WARNING: A main task completed unexpectedly
[2025-09-12 13:45:46,370] INFO: Starting cleanup process...
[2025-09-12 13:45:47,474] INFO: IRC connection closed
[2025-09-12 13:45:47,479] INFO: Final database save completed - 16 players saved
[2025-09-12 13:45:47,479] INFO: Cleanup completed successfully
[2025-09-12 13:45:47,480] INFO: DuckHunt Bot shutdown complete
[2025-09-12 15:02:57,578] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 15:02:57,578] INFO: DuckHunt Bot initializing...
[2025-09-12 15:02:57,578] INFO: Starting DuckHunt Bot...
[2025-09-12 15:02:57,578] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 15:02:57,601] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 15:02:57,725] INFO: Connected successfully!
[2025-09-12 15:02:57,726] INFO: SASL authentication enabled
[2025-09-12 15:02:57,738] INFO: SASL capability available
[2025-09-12 15:02:57,745] INFO: SASL capability acknowledged
[2025-09-12 15:02:57,745] INFO: Authenticating via SASL as duckhunt
[2025-09-12 15:02:58,113] INFO: Server ready for SASL authentication
[2025-09-12 15:02:58,113] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-12 15:02:58,113] INFO: Falling back to NickServ identification...
[2025-09-12 15:02:58,113] ERROR: Attempted username: duckhunt
[2025-09-12 15:02:58,114] INFO: Registering as DuckHunt
[2025-09-12 15:02:58,172] ERROR: SASL authentication aborted! (906)
[2025-09-12 15:02:58,173] INFO: Falling back to NickServ identification...
[2025-09-12 15:02:58,173] INFO: Registering as DuckHunt
[2025-09-12 15:02:58,173] INFO: Successfully registered!
[2025-09-12 15:02:58,174] INFO: Attempting NickServ identification for duckhunt
[2025-09-12 15:02:59,176] INFO: NickServ identification commands sent
[2025-09-12 15:02:59,176] INFO: Joining #computertech
[2025-09-12 15:02:59,183] INFO: Successfully joined #computertech
[2025-09-12 15:02:59,728] INFO: Starting duck spawning...
[2025-09-12 15:03:04,730] INFO: Admin spawned normal duck 3e922056 in #computertech
[2025-09-12 15:03:04,731] INFO: Waiting 51m 22s for next duck
[2025-09-12 15:03:12,954] WARNING: A main task completed unexpectedly
[2025-09-12 15:03:12,954] INFO: Starting cleanup process...
[2025-09-12 15:03:14,059] INFO: IRC connection closed
[2025-09-12 15:03:14,062] INFO: Final database save completed - 16 players saved
[2025-09-12 15:03:14,063] INFO: Cleanup completed successfully
[2025-09-12 15:03:14,063] INFO: DuckHunt Bot shutdown complete
[2025-09-12 18:34:46,067] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 18:34:46,067] INFO: DuckHunt Bot initializing...
[2025-09-12 18:34:46,067] INFO: Starting DuckHunt Bot...
[2025-09-12 18:34:46,067] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 18:34:46,089] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 18:34:46,191] INFO: Connected successfully!
[2025-09-12 18:34:46,191] INFO: SASL authentication enabled
[2025-09-12 18:34:47,098] INFO: SASL capability available
[2025-09-12 18:34:47,105] INFO: SASL capability acknowledged
[2025-09-12 18:34:47,105] INFO: Authenticating via SASL as duckhunt
[2025-09-12 18:34:47,472] INFO: Server ready for SASL authentication
[2025-09-12 18:34:47,472] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-12 18:34:47,473] INFO: Falling back to NickServ identification...
[2025-09-12 18:34:47,473] ERROR: Attempted username: duckhunt
[2025-09-12 18:34:47,474] INFO: Registering as DuckHunt
[2025-09-12 18:35:13,492] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 18:35:13,528] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 18:35:13,528] INFO: Starting cleanup process...
[2025-09-12 18:35:14,531] INFO: IRC connection closed
[2025-09-12 18:35:14,532] INFO: Final database save completed - 16 players saved
[2025-09-12 18:35:14,532] INFO: Cleanup completed successfully
[2025-09-12 18:35:14,532] INFO: DuckHunt Bot shutdown complete
[2025-09-12 18:35:15,124] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 18:35:15,124] INFO: DuckHunt Bot initializing...
[2025-09-12 18:35:15,124] INFO: Starting DuckHunt Bot...
[2025-09-12 18:35:15,125] INFO: Loaded 16 players from duckhunt.json
[2025-09-12 18:35:15,147] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 18:35:15,277] INFO: Connected successfully!
[2025-09-12 18:35:15,277] INFO: SASL authentication enabled
[2025-09-12 18:35:15,289] INFO: SASL capability available
[2025-09-12 18:35:15,295] INFO: SASL capability acknowledged
[2025-09-12 18:35:15,295] INFO: Authenticating via SASL as duckhunt
[2025-09-12 18:35:15,662] INFO: Server ready for SASL authentication
[2025-09-12 18:35:15,663] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-12 18:35:15,663] INFO: Falling back to NickServ identification...
[2025-09-12 18:35:15,663] ERROR: Attempted username: duckhunt
[2025-09-12 18:35:15,663] INFO: Registering as DuckHunt
[2025-09-12 18:35:15,676] ERROR: SASL authentication aborted! (906)
[2025-09-12 18:35:15,677] INFO: Falling back to NickServ identification...
[2025-09-12 18:35:15,677] INFO: Registering as DuckHunt
[2025-09-12 18:35:15,677] INFO: Successfully registered!
[2025-09-12 18:35:15,677] INFO: Attempting NickServ identification for duckhunt
[2025-09-12 18:35:16,679] INFO: NickServ identification commands sent
[2025-09-12 18:35:16,679] INFO: Joining #computertech
[2025-09-12 18:35:16,686] INFO: Successfully joined #computertech
[2025-09-12 18:35:17,281] INFO: Starting duck spawning...
[2025-09-12 18:35:22,283] INFO: Admin spawned normal duck 76766050 in #computertech
[2025-09-12 18:35:22,283] INFO: Waiting 41m 31s for next duck
[2025-09-12 18:35:37,207] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 18:35:37,308] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 18:35:37,308] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 18:35:37,309] INFO: Starting cleanup process...
[2025-09-12 18:35:38,414] INFO: IRC connection closed
[2025-09-12 18:35:38,420] INFO: Final database save completed - 17 players saved
[2025-09-12 18:35:38,420] INFO: Cleanup completed successfully
[2025-09-12 18:35:38,420] INFO: DuckHunt Bot shutdown complete
[2025-09-12 18:37:36,458] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 18:37:36,458] INFO: DuckHunt Bot initializing...
[2025-09-12 18:37:36,458] INFO: Starting DuckHunt Bot...
[2025-09-12 18:37:36,458] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 18:37:36,481] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 18:37:36,589] INFO: Connected successfully!
[2025-09-12 18:37:36,589] INFO: SASL authentication enabled
[2025-09-12 18:37:36,601] INFO: SASL capability available
[2025-09-12 18:37:36,608] INFO: SASL capability acknowledged
[2025-09-12 18:37:36,608] INFO: Authenticating via SASL as duckhunt
[2025-09-12 18:37:36,975] INFO: Server ready for SASL authentication
[2025-09-12 18:37:36,976] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
[2025-09-12 18:37:36,976] INFO: Falling back to NickServ identification...
[2025-09-12 18:37:36,976] ERROR: Attempted username: duckhunt
[2025-09-12 18:37:36,976] INFO: Registering as DuckHunt
[2025-09-12 18:37:36,990] ERROR: SASL authentication aborted! (906)
[2025-09-12 18:37:36,990] INFO: Falling back to NickServ identification...
[2025-09-12 18:37:36,990] INFO: Registering as DuckHunt
[2025-09-12 18:37:36,991] INFO: Successfully registered!
[2025-09-12 18:37:36,991] INFO: Attempting NickServ identification for duckhunt
[2025-09-12 18:37:37,770] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 18:37:37,993] INFO: NickServ identification commands sent
[2025-09-12 18:37:37,994] INFO: Joining #computertech
[2025-09-12 18:37:37,994] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 18:37:37,994] INFO: Starting cleanup process...
[2025-09-12 18:37:38,999] INFO: IRC connection closed
[2025-09-12 18:37:39,004] INFO: Final database save completed - 17 players saved
[2025-09-12 18:37:39,004] INFO: Cleanup completed successfully
[2025-09-12 18:37:39,005] INFO: DuckHunt Bot shutdown complete
[2025-09-12 19:48:20,215] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 19:50:01,514] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 19:50:01,514] INFO: DuckHunt Bot initializing...
[2025-09-12 19:50:01,515] INFO: Starting DuckHunt Bot...
[2025-09-12 19:50:01,516] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 19:50:01,587] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 19:50:01,776] INFO: Connected successfully!
[2025-09-12 19:50:02,946] INFO: Registering as DuckHunt
[2025-09-12 19:50:03,077] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 19:50:03,078] INFO: Joining #colby
[2025-09-12 19:50:03,146] INFO: Successfully joined #colby
[2025-09-12 19:50:03,780] INFO: Starting duck spawning...
[2025-09-12 19:50:08,782] INFO: Admin spawned normal duck 89470db7 in #colby
[2025-09-12 19:50:08,783] INFO: Waiting 72m 24s for next duck
[2025-09-12 19:50:35,497] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 19:50:35,810] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 19:50:35,810] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 19:50:35,811] INFO: Starting cleanup process...
[2025-09-12 19:50:36,916] INFO: IRC connection closed
[2025-09-12 19:50:36,922] INFO: Final database save completed - 17 players saved
[2025-09-12 19:50:36,922] INFO: Cleanup completed successfully
[2025-09-12 19:50:36,925] INFO: DuckHunt Bot shutdown complete
[2025-09-12 19:50:38,060] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 19:50:38,060] INFO: DuckHunt Bot initializing...
[2025-09-12 19:50:38,061] INFO: Starting DuckHunt Bot...
[2025-09-12 19:50:38,062] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 19:50:38,126] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 19:50:38,317] INFO: Connected successfully!
[2025-09-12 19:50:39,335] INFO: Registering as DuckHunt
[2025-09-12 19:50:39,456] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 19:50:39,456] INFO: Joining #colby
[2025-09-12 19:50:39,519] INFO: Successfully joined #colby
[2025-09-12 19:50:40,323] INFO: Starting duck spawning...
[2025-09-12 19:50:45,326] INFO: Admin spawned normal duck 1399472e in #colby
[2025-09-12 19:50:45,326] INFO: Waiting 89m 7s for next duck
[2025-09-12 19:50:58,428] INFO: Admin spawned normal duck 7278ccc5 in #colby
[2025-09-12 19:51:03,718] INFO: Admin spawned normal duck b79d4c60 in #colby
[2025-09-12 19:51:04,973] INFO: Admin spawned normal duck f16535b2 in #colby
[2025-09-12 19:51:06,296] INFO: Admin spawned normal duck 287c11e5 in #colby
[2025-09-12 19:51:07,607] INFO: Admin spawned normal duck 87d9f58d in #colby
[2025-09-12 19:55:27,299] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 19:55:27,760] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 19:55:27,764] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 19:55:27,766] INFO: Starting cleanup process...
[2025-09-12 19:55:28,894] INFO: IRC connection closed
[2025-09-12 19:55:28,907] INFO: Final database save completed - 17 players saved
[2025-09-12 19:55:28,908] INFO: Cleanup completed successfully
[2025-09-12 19:55:28,925] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:09:21,565] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:09:21,568] INFO: DuckHunt Bot initializing...
[2025-09-12 20:09:21,568] INFO: Starting DuckHunt Bot...
[2025-09-12 20:09:21,569] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:09:21,648] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:09:21,817] INFO: Connected successfully!
[2025-09-12 20:09:22,784] INFO: Registering as DuckHunt
[2025-09-12 20:09:22,880] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:09:22,881] INFO: Joining #computertech
[2025-09-12 20:09:22,937] INFO: Successfully joined #computertech
[2025-09-12 20:09:23,822] INFO: Starting duck spawning...
[2025-09-12 20:09:28,824] INFO: Admin spawned normal duck 3d18761a in #computertech
[2025-09-12 20:09:28,825] INFO: Waiting 43m 4s for next duck
[2025-09-12 20:10:31,986] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:10:32,701] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:10:32,702] INFO: Starting cleanup process...
[2025-09-12 20:10:33,810] INFO: IRC connection closed
[2025-09-12 20:10:33,819] INFO: Final database save completed - 17 players saved
[2025-09-12 20:10:33,819] INFO: Cleanup completed successfully
[2025-09-12 20:10:33,823] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:10:34,241] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:10:34,242] INFO: DuckHunt Bot initializing...
[2025-09-12 20:10:34,243] INFO: Starting DuckHunt Bot...
[2025-09-12 20:10:34,244] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:10:34,365] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:10:34,541] INFO: Connected successfully!
[2025-09-12 20:10:36,475] INFO: Registering as DuckHunt
[2025-09-12 20:10:36,594] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:10:36,597] INFO: Joining #computertech
[2025-09-12 20:10:36,665] INFO: Successfully joined #computertech
[2025-09-12 20:10:37,545] INFO: Starting duck spawning...
[2025-09-12 20:10:42,548] INFO: Admin spawned normal duck 11290de8 in #computertech
[2025-09-12 20:10:42,549] INFO: Waiting 74m 3s for next duck
[2025-09-12 20:10:53,126] WARNING: A main task completed unexpectedly
[2025-09-12 20:10:53,127] INFO: Starting cleanup process...
[2025-09-12 20:10:54,233] INFO: IRC connection closed
[2025-09-12 20:10:54,244] INFO: Final database save completed - 17 players saved
[2025-09-12 20:10:54,245] INFO: Cleanup completed successfully
[2025-09-12 20:10:54,248] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:11:53,511] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:11:53,512] INFO: DuckHunt Bot initializing...
[2025-09-12 20:11:53,512] INFO: Starting DuckHunt Bot...
[2025-09-12 20:11:53,513] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:11:53,599] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:11:53,797] INFO: Connected successfully!
[2025-09-12 20:11:54,764] INFO: Registering as DuckHunt
[2025-09-12 20:11:54,865] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:11:54,865] INFO: Joining #computertech
[2025-09-12 20:11:54,914] INFO: Successfully joined #computertech
[2025-09-12 20:11:55,800] INFO: Starting duck spawning...
[2025-09-12 20:12:00,802] INFO: Admin spawned normal duck cefbb956 in #computertech
[2025-09-12 20:12:00,803] INFO: Waiting 32m 48s for next duck
[2025-09-12 20:12:01,603] INFO: Admin spawned normal duck e19381c2 in #computertech
[2025-09-12 20:12:08,171] INFO: Admin spawned normal duck 642daf60 in #computertech
[2025-09-12 20:12:08,705] INFO: Admin spawned normal duck deb6cc88 in #computertech
[2025-09-12 20:12:32,287] INFO: Admin spawned golden duck f5c388c1 in #computertech
[2025-09-12 20:17:29,215] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:17:29,253] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 20:17:29,254] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:17:29,254] INFO: Starting cleanup process...
[2025-09-12 20:17:30,361] INFO: IRC connection closed
[2025-09-12 20:17:30,368] INFO: Final database save completed - 17 players saved
[2025-09-12 20:17:30,369] INFO: Cleanup completed successfully
[2025-09-12 20:17:30,372] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:17:31,507] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:17:31,508] INFO: DuckHunt Bot initializing...
[2025-09-12 20:17:31,509] INFO: Starting DuckHunt Bot...
[2025-09-12 20:17:31,511] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:17:31,600] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:17:31,788] INFO: Connected successfully!
[2025-09-12 20:17:32,919] INFO: Registering as DuckHunt
[2025-09-12 20:17:33,047] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:17:33,048] INFO: Joining #computertech
[2025-09-12 20:17:33,116] INFO: Successfully joined #computertech
[2025-09-12 20:17:33,792] INFO: Starting duck spawning...
[2025-09-12 20:17:38,793] INFO: Admin spawned normal duck fdc26682 in #computertech
[2025-09-12 20:17:38,794] INFO: Waiting 77m 31s for next duck
[2025-09-12 20:18:12,540] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:18:12,858] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 20:18:12,859] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:18:12,859] INFO: Starting cleanup process...
[2025-09-12 20:18:13,964] INFO: IRC connection closed
[2025-09-12 20:18:13,970] INFO: Final database save completed - 17 players saved
[2025-09-12 20:18:13,971] INFO: Cleanup completed successfully
[2025-09-12 20:18:13,975] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:18:14,525] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:18:14,527] INFO: DuckHunt Bot initializing...
[2025-09-12 20:18:14,528] INFO: Starting DuckHunt Bot...
[2025-09-12 20:18:14,531] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:18:14,636] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:18:14,805] INFO: Connected successfully!
[2025-09-12 20:18:15,768] INFO: Registering as DuckHunt
[2025-09-12 20:18:15,871] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:18:15,873] INFO: Joining #computertech
[2025-09-12 20:18:15,929] INFO: Successfully joined #computertech
[2025-09-12 20:18:16,123] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:18:16,129] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:18:16,147] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:18:16,148] INFO: Starting cleanup process...
[2025-09-12 20:18:17,115] INFO: Received SIGTERM, initiating graceful shutdown...
[2025-09-12 20:18:17,268] INFO: IRC connection closed
[2025-09-12 20:18:17,276] INFO: Final database save completed - 17 players saved
[2025-09-12 20:18:17,276] INFO: Cleanup completed successfully
[2025-09-12 20:18:17,290] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:18:23,251] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:18:23,252] INFO: DuckHunt Bot initializing...
[2025-09-12 20:18:23,253] INFO: Starting DuckHunt Bot...
[2025-09-12 20:18:23,255] INFO: Loaded 17 players from duckhunt.json
[2025-09-12 20:18:23,349] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:18:23,549] INFO: Connected successfully!
[2025-09-12 20:18:24,557] INFO: Registering as DuckHunt
[2025-09-12 20:18:24,676] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:18:24,677] INFO: Joining #computertech
[2025-09-12 20:18:24,736] INFO: Successfully joined #computertech
[2025-09-12 20:18:25,554] INFO: Starting duck spawning...
[2025-09-12 20:18:30,556] INFO: Admin spawned normal duck 083638bd in #computertech
[2025-09-12 20:18:30,557] INFO: Waiting 38m 8s for next duck
[2025-09-12 20:18:43,399] INFO: Admin spawned normal duck 777c5080 in #computertech
[2025-09-12 20:18:43,910] INFO: Admin spawned normal duck 0e36368a in #computertech
[2025-09-12 20:18:44,307] INFO: Admin spawned normal duck 0dbf0209 in #computertech
[2025-09-12 20:18:44,693] INFO: Admin spawned normal duck 224f9870 in #computertech
[2025-09-12 20:18:45,055] INFO: Admin spawned normal duck 96c7a18d in #computertech
[2025-09-12 20:18:45,659] INFO: Admin spawned normal duck f881fa3a in #computertech
[2025-09-12 20:29:51,955] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:29:52,036] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:29:52,036] INFO: Starting cleanup process...
[2025-09-12 20:29:53,159] INFO: IRC connection closed
[2025-09-12 20:29:53,169] INFO: Final database save completed - 18 players saved
[2025-09-12 20:29:53,170] INFO: Cleanup completed successfully
[2025-09-12 20:29:53,181] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:29:54,600] INFO: Loaded 18 players from duckhunt.json
[2025-09-12 20:29:54,601] INFO: DuckHunt Bot initializing...
[2025-09-12 20:29:54,601] INFO: Starting DuckHunt Bot...
[2025-09-12 20:29:54,604] INFO: Loaded 18 players from duckhunt.json
[2025-09-12 20:29:54,699] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:29:54,874] INFO: Connected successfully!
[2025-09-12 20:29:55,842] INFO: Registering as DuckHunt
[2025-09-12 20:29:55,941] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:29:55,941] INFO: Joining #computertech
[2025-09-12 20:29:55,991] INFO: Successfully joined #computertech
[2025-09-12 20:29:56,877] INFO: Starting duck spawning...
[2025-09-12 20:30:01,878] INFO: Admin spawned golden duck f7373d01 in #computertech
[2025-09-12 20:30:01,878] INFO: Waiting 49m 50s for next duck
[2025-09-12 20:31:39,076] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:31:39,717] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:31:39,718] INFO: Starting cleanup process...
[2025-09-12 20:31:40,827] INFO: IRC connection closed
[2025-09-12 20:31:40,834] INFO: Final database save completed - 19 players saved
[2025-09-12 20:31:40,835] INFO: Cleanup completed successfully
[2025-09-12 20:31:40,838] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:31:42,386] INFO: Loaded 19 players from duckhunt.json
[2025-09-12 20:31:42,387] INFO: DuckHunt Bot initializing...
[2025-09-12 20:31:42,388] INFO: Starting DuckHunt Bot...
[2025-09-12 20:31:42,389] INFO: Loaded 19 players from duckhunt.json
[2025-09-12 20:31:42,504] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:31:42,737] INFO: Connected successfully!
[2025-09-12 20:31:43,914] INFO: Registering as DuckHunt
[2025-09-12 20:31:44,034] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:31:44,034] INFO: Joining #computertech
[2025-09-12 20:31:44,097] INFO: Successfully joined #computertech
[2025-09-12 20:31:44,744] INFO: Starting duck spawning...
[2025-09-12 20:31:49,747] INFO: Admin spawned normal duck 30bc917d in #computertech
[2025-09-12 20:31:49,748] INFO: Waiting 44m 50s for next duck
[2025-09-12 20:45:42,441] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:45:42,988] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 20:45:42,993] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:45:43,000] INFO: Starting cleanup process...
[2025-09-12 20:45:44,197] INFO: IRC connection closed
[2025-09-12 20:45:44,234] INFO: Final database save completed - 19 players saved
[2025-09-12 20:45:44,235] INFO: Cleanup completed successfully
[2025-09-12 20:45:44,316] INFO: DuckHunt Bot shutdown complete
[2025-09-12 20:57:05,952] INFO: Loaded 19 players from duckhunt.json
[2025-09-12 20:57:05,954] INFO: DuckHunt Bot initializing...
[2025-09-12 20:57:05,955] INFO: Starting DuckHunt Bot...
[2025-09-12 20:57:05,955] INFO: Loaded 19 players from duckhunt.json
[2025-09-12 20:57:06,019] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
[2025-09-12 20:57:06,197] INFO: Connected successfully!
[2025-09-12 20:57:07,157] INFO: Registering as DuckHunt
[2025-09-12 20:57:07,256] INFO: Successfully registered! (SASL authenticated)
[2025-09-12 20:57:07,256] INFO: Joining #computertech
[2025-09-12 20:57:07,307] INFO: Successfully joined #computertech
[2025-09-12 20:57:08,199] INFO: Starting duck spawning...
[2025-09-12 20:57:13,200] INFO: Admin spawned normal duck 77291a83 in #computertech
[2025-09-12 20:57:13,200] INFO: Waiting 49m 42s for next duck
[2025-09-12 20:57:25,869] INFO: Admin spawned normal duck 4a2ff363 in #computertech
[2025-09-12 20:57:26,569] INFO: Admin spawned normal duck f2ae2d52 in #computertech
[2025-09-12 20:57:39,277] INFO: Admin spawned golden duck 3ef1281f in #computertech
[2025-09-12 20:59:05,695] INFO: Received SIGINT, initiating graceful shutdown...
[2025-09-12 20:59:06,333] INFO: Duck spawning stopped due to shutdown request
[2025-09-12 20:59:06,334] INFO: Shutdown requested, stopping all tasks...
[2025-09-12 20:59:06,334] INFO: Starting cleanup process...
[2025-09-12 20:59:07,437] INFO: IRC connection closed
[2025-09-12 20:59:07,442] INFO: Final database save completed - 19 players saved
[2025-09-12 20:59:07,443] INFO: Cleanup completed successfully
[2025-09-12 20:59:07,445] INFO: DuckHunt Bot shutdown complete

View File

@@ -1,37 +0,0 @@
#!/usr/bin/env python3
"""
Main entry point for DuckHunt Bot
"""
import asyncio
import json
import sys
import os
# Add src directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from src.duckhuntbot import IRCBot
def main():
try:
with open('config.json') as f:
config = json.load(f)
bot = IRCBot(config)
bot.logger.info("🦆 Starting DuckHunt Bot...")
# Run the bot
asyncio.run(bot.run())
except KeyboardInterrupt:
print("\n🛑 Bot stopped by user")
except FileNotFoundError:
print("❌ config.json not found!")
sys.exit(1)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@@ -1,108 +0,0 @@
import hashlib
import secrets
import asyncio
from typing import Optional
from src.db import DuckDB
class AuthSystem:
def __init__(self, db):
self.db = db
self.bot = None # Will be set by the bot
self.authenticated_users = {} # nick -> account_name
self.pending_registrations = {} # nick -> temp_data
def hash_password(self, password: str, salt: Optional[str] = None) -> tuple:
if salt is None:
salt = secrets.token_hex(16)
hashed = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
return hashed.hex(), salt
def verify_password(self, password: str, hashed: str, salt: str) -> bool:
test_hash, _ = self.hash_password(password, salt)
return test_hash == hashed
def register_account(self, username: str, password: str, nick: str, hostmask: str) -> bool:
# Check if account exists
existing = self.db.load_account(username)
if existing:
return False
hashed_pw, salt = self.hash_password(password)
account_data = {
'username': username,
'password_hash': hashed_pw,
'salt': salt,
'primary_nick': nick,
'hostmask': hostmask,
'created_at': None, # Set by DB
'auth_method': 'password' # 'password', 'nickserv', 'hostmask'
}
self.db.save_account(username, account_data)
return True
def authenticate(self, username: str, password: str, nick: str) -> bool:
account = self.db.load_account(username)
if not account:
return False
if self.verify_password(password, account['password_hash'], account['salt']):
self.authenticated_users[nick] = username
return True
return False
def get_account_for_nick(self, nick: str) -> str:
return self.authenticated_users.get(nick, "")
def is_authenticated(self, nick: str) -> bool:
return nick in self.authenticated_users
def logout(self, nick: str):
if nick in self.authenticated_users:
del self.authenticated_users[nick]
def set_bot(self, bot):
"""Set the bot instance for sending messages"""
self.bot = bot
async def attempt_nickserv_auth(self):
"""Attempt NickServ identification as fallback"""
if not self.bot:
return
sasl_config = self.bot.config.get('sasl', {})
username = sasl_config.get('username', '')
password = sasl_config.get('password', '')
if username and password:
self.bot.logger.info(f"Attempting NickServ identification for {username}")
# Try both common NickServ commands
self.bot.send_raw(f'PRIVMSG NickServ :IDENTIFY {username} {password}')
# Some networks use just the password if nick matches
await asyncio.sleep(1)
self.bot.send_raw(f'PRIVMSG NickServ :IDENTIFY {password}')
self.bot.logger.info("NickServ identification commands sent")
else:
self.bot.logger.debug("No SASL credentials available for NickServ fallback")
async def handle_nickserv_response(self, message):
"""Handle responses from NickServ"""
if not self.bot:
return
message_lower = message.lower()
if any(phrase in message_lower for phrase in [
'you are now identified', 'password accepted', 'you are already identified',
'authentication successful', 'you have been identified'
]):
self.bot.logger.info("NickServ identification successful!")
elif any(phrase in message_lower for phrase in [
'invalid password', 'incorrect password', 'access denied',
'authentication failed', 'not registered', 'nickname is not registered'
]):
self.bot.logger.error(f"NickServ identification failed: {message}")
else:
self.bot.logger.debug(f"NickServ message: {message}")

View File

@@ -1,97 +0,0 @@
import sqlite3
import json
import datetime
class DuckDB:
def __init__(self, db_path='duckhunt.db'):
self.conn = sqlite3.connect(db_path)
self.create_tables()
def create_tables(self):
with self.conn:
# Player data table
self.conn.execute('''CREATE TABLE IF NOT EXISTS players (
nick TEXT PRIMARY KEY,
data TEXT
)''')
# Account system table
self.conn.execute('''CREATE TABLE IF NOT EXISTS accounts (
username TEXT PRIMARY KEY,
data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
# Leaderboards table
self.conn.execute('''CREATE TABLE IF NOT EXISTS leaderboard (
account TEXT,
stat_type TEXT,
value INTEGER,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (account, stat_type)
)''')
# Trading table
self.conn.execute('''CREATE TABLE IF NOT EXISTS trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_account TEXT,
to_account TEXT,
trade_data TEXT,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
def save_player(self, nick, data):
with self.conn:
self.conn.execute('''INSERT OR REPLACE INTO players (nick, data) VALUES (?, ?)''',
(nick, json.dumps(data)))
def load_player(self, nick):
cur = self.conn.cursor()
cur.execute('SELECT data FROM players WHERE nick=?', (nick,))
row = cur.fetchone()
return json.loads(row[0]) if row else None
def get_all_players(self):
cur = self.conn.cursor()
cur.execute('SELECT nick, data FROM players')
return {nick: json.loads(data) for nick, data in cur.fetchall()}
def save_account(self, username, data):
with self.conn:
self.conn.execute('''INSERT OR REPLACE INTO accounts (username, data) VALUES (?, ?)''',
(username, json.dumps(data)))
def load_account(self, username):
cur = self.conn.cursor()
cur.execute('SELECT data FROM accounts WHERE username=?', (username,))
row = cur.fetchone()
return json.loads(row[0]) if row else None
def update_leaderboard(self, account, stat_type, value):
with self.conn:
self.conn.execute('''INSERT OR REPLACE INTO leaderboard (account, stat_type, value) VALUES (?, ?, ?)''',
(account, stat_type, value))
def get_leaderboard(self, stat_type, limit=10):
cur = self.conn.cursor()
cur.execute('SELECT account, value FROM leaderboard WHERE stat_type=? ORDER BY value DESC LIMIT ?',
(stat_type, limit))
return cur.fetchall()
def save_trade(self, from_account, to_account, trade_data):
with self.conn:
cur = self.conn.cursor()
cur.execute('''INSERT INTO trades (from_account, to_account, trade_data) VALUES (?, ?, ?)''',
(from_account, to_account, json.dumps(trade_data)))
return cur.lastrowid
def get_pending_trades(self, account):
cur = self.conn.cursor()
cur.execute('''SELECT id, from_account, trade_data FROM trades
WHERE to_account=? AND status='pending' ''', (account,))
return [(trade_id, from_acc, json.loads(data)) for trade_id, from_acc, data in cur.fetchall()]
def complete_trade(self, trade_id):
with self.conn:
self.conn.execute('UPDATE trades SET status=? WHERE id=?', ('completed', trade_id))

View File

@@ -1,277 +0,0 @@
#!/usr/bin/env python3
"""
Main DuckHunt IRC Bot using modular architecture
"""
import asyncio
import ssl
import json
import random
import logging
import sys
import os
import time
import uuid
import signal
from typing import Optional
from .logging_utils import setup_logger
from .utils import parse_message
from .db import DuckDB
from .game import DuckGame
from .auth import AuthSystem
from . import sasl
class IRCBot:
def __init__(self, config):
self.config = config
self.logger = setup_logger("DuckHuntBot")
self.reader: Optional[asyncio.StreamReader] = None
self.writer: Optional[asyncio.StreamWriter] = None
self.registered = False
self.channels_joined = set()
self.shutdown_requested = False
self.running_tasks = set()
# Initialize subsystems
self.db = DuckDB()
self.game = DuckGame(self, self.db)
self.auth = AuthSystem(self.db)
self.auth.set_bot(self) # Set bot reference for auth system
self.sasl_handler = sasl.SASLHandler(self, config)
# IRC connection state
self.nick = config['nick']
self.channels = config['channels']
def send_raw(self, msg):
"""Send raw IRC message"""
if self.writer and not self.writer.is_closing():
try:
self.writer.write(f"{msg}\r\n".encode('utf-8'))
except Exception as e:
self.logger.error(f"Error sending message: {e}")
def send_message(self, target, msg):
"""Send PRIVMSG to target"""
self.send_raw(f'PRIVMSG {target} :{msg}')
async def connect(self):
"""Connect to IRC server with SASL support"""
server = self.config['server']
port = self.config['port']
ssl_context = ssl.create_default_context() if self.config.get('ssl', True) else None
self.logger.info(f"Connecting to {server}:{port} (SSL: {ssl_context is not None})")
try:
self.reader, self.writer = await asyncio.open_connection(
server, port, ssl=ssl_context
)
self.logger.info("Connected successfully!")
# Start SASL negotiation if enabled
if await self.sasl_handler.start_negotiation():
return True
else:
# Standard registration without SASL
await self.register_user()
return True
except Exception as e:
self.logger.error(f"Connection failed: {e}")
return False
async def register_user(self):
"""Register with IRC server"""
self.logger.info(f"Registering as {self.nick}")
self.send_raw(f'NICK {self.nick}')
self.send_raw(f'USER {self.nick} 0 * :DuckHunt Bot')
# Send password if configured (for servers that require it)
if self.config.get('password'):
self.send_raw(f'PASS {self.config["password"]}')
async def handle_irc_message(self, line):
"""Handle individual IRC message"""
try:
prefix, command, params, trailing = parse_message(line)
# Handle SASL-related messages
if command in ['CAP', 'AUTHENTICATE', '903', '904', '905', '906', '907', '908']:
handled = await self.sasl_handler.handle_sasl_result(command, params, trailing)
if command == 'CAP':
handled = await self.sasl_handler.handle_cap_response(params, trailing)
elif command == 'AUTHENTICATE':
handled = await self.sasl_handler.handle_authenticate_response(params)
# If SASL handler didn't handle it, continue with normal processing
if handled:
return
# Handle standard IRC messages
if command == '001': # Welcome
self.registered = True
auth_status = " (SASL authenticated)" if self.sasl_handler.is_authenticated() else ""
self.logger.info(f"Successfully registered!{auth_status}")
# If SASL failed, try NickServ identification
if not self.sasl_handler.is_authenticated():
await self.auth.attempt_nickserv_auth()
# Join channels
for chan in self.channels:
self.logger.info(f"Joining {chan}")
self.send_raw(f'JOIN {chan}')
elif command == 'JOIN' and prefix and prefix.startswith(self.nick):
channel = trailing or (params[0] if params else '')
if channel:
self.channels_joined.add(channel)
self.logger.info(f"Successfully joined {channel}")
elif command == 'PRIVMSG' and trailing:
target = params[0] if params else ''
sender = prefix.split('!')[0] if prefix else ''
# Handle NickServ responses
if sender.lower() == 'nickserv':
await self.auth.handle_nickserv_response(trailing)
elif trailing == 'VERSION':
self.send_raw(f'NOTICE {sender} :VERSION DuckHunt Bot v2.0')
else:
# Handle game commands
await self.game.handle_command(prefix, target, trailing)
elif command == 'PING':
# Respond to PING
self.send_raw(f'PONG :{trailing}')
except Exception as e:
self.logger.error(f"Error handling IRC message '{line}': {e}")
async def listen(self):
"""Main IRC message listening loop"""
buffer = ""
while not self.shutdown_requested:
try:
if not self.reader:
break
data = await self.reader.read(4096)
if not data:
self.logger.warning("Connection closed by server")
break
buffer += data.decode('utf-8', errors='ignore')
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
line = line.rstrip('\r')
if line:
await self.handle_irc_message(line)
except asyncio.CancelledError:
break
except Exception as e:
self.logger.error(f"Error in listen loop: {e}")
await asyncio.sleep(1)
def setup_signal_handlers(self):
"""Setup signal handlers for graceful shutdown"""
def signal_handler(signum, frame):
self.logger.info(f"Received signal {signum}, initiating graceful shutdown...")
self.shutdown_requested = True
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
async def cleanup(self):
"""Cleanup resources and save data"""
self.logger.info("Starting cleanup process...")
try:
# Cancel all running tasks
for task in self.running_tasks.copy():
if not task.done():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# Send goodbye message
if self.writer and not self.writer.is_closing():
for channel in self.channels_joined:
self.send_message(channel, "🦆 DuckHunt Bot shutting down. Thanks for playing! 🦆")
await asyncio.sleep(0.1)
self.send_raw('QUIT :DuckHunt Bot shutting down gracefully')
await asyncio.sleep(1.0)
self.writer.close()
await self.writer.wait_closed()
self.logger.info("IRC connection closed")
# Save database (no specific save_all method)
# Players are saved individually through the game engine
self.logger.info("Final database save completed")
self.logger.info("Cleanup completed successfully")
except Exception as e:
self.logger.error(f"Error during cleanup: {e}")
async def run(self):
"""Main bot entry point"""
try:
self.setup_signal_handlers()
self.logger.info("Starting DuckHunt Bot...")
# Load database (no async initialization needed)
# Database is initialized in constructor
# Connect to IRC
if not await self.connect():
return False
# Create main tasks
listen_task = asyncio.create_task(self.listen(), name="listen")
game_task = asyncio.create_task(self.game.spawn_ducks_loop(), name="duck_spawner")
self.running_tasks.add(listen_task)
self.running_tasks.add(game_task)
# Wait for completion
done, pending = await asyncio.wait(
[listen_task, game_task],
return_when=asyncio.FIRST_COMPLETED
)
if self.shutdown_requested:
self.logger.info("Shutdown requested, stopping all tasks...")
else:
self.logger.warning("A main task completed unexpectedly")
# Cancel remaining tasks
for task in pending:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
except KeyboardInterrupt:
self.logger.info("Keyboard interrupt received")
self.shutdown_requested = True
except Exception as e:
self.logger.error(f"Fatal error in main loop: {e}")
import traceback
traceback.print_exc()
finally:
await self.cleanup()
return True

View File

@@ -1,566 +0,0 @@
import asyncio
import random
from src.items import DuckTypes, WeaponTypes, AmmoTypes, Attachments
from src.auth import AuthSystem
class DuckGame:
def __init__(self, bot, db):
self.bot = bot
self.config = bot.config
self.logger = getattr(bot, 'logger', None)
self.db = db
self.auth = AuthSystem(db)
self.duck_spawn_min = self.config.get('duck_spawn_min', 30)
self.duck_spawn_max = self.config.get('duck_spawn_max', 120)
self.ducks = {} # channel: duck dict or None
self.players = {} # nick: player dict
self.duck_alerts = set() # nicks who want duck alerts
def get_player(self, nick):
if nick in self.players:
return self.players[nick]
data = self.db.load_player(nick)
if data:
data['friends'] = set(data.get('friends', []))
self.players[nick] = data
return data
default = {
'ammo': 1, 'max_ammo': 1, 'friends': set(), 'caught': 0, 'coins': 100,
'accuracy': 70, 'reliability': 80, 'gun_oil': 0, 'scope': False,
'silencer': False, 'lucky_charm': False, 'xp': 0, 'level': 1,
'bank_account': 0, 'insurance': {'active': False, 'claims': 0},
'weapon': 'basic_gun', 'weapon_durability': 100, 'ammo_type': 'standard',
'attachments': [], 'hunting_license': {'active': False, 'expires': None},
'duck_alerts': False, 'auth_method': 'nick' # 'nick', 'hostmask', 'account'
}
self.players[nick] = default
return default
def save_player(self, nick, data):
self.players[nick] = data
data_to_save = dict(data)
data_to_save['friends'] = list(data_to_save.get('friends', []))
self.db.save_player(nick, data_to_save)
async def spawn_ducks_loop(self):
while True:
wait_time = random.randint(self.duck_spawn_min, self.duck_spawn_max)
if self.logger:
self.logger.info(f"Waiting {wait_time}s before next duck spawn.")
await asyncio.sleep(wait_time)
for chan in self.bot.channels:
duck = self.ducks.get(chan)
if not (duck and duck.get('alive')):
duck_type = DuckTypes.get_random_duck()
self.ducks[chan] = {
'alive': True,
'type': duck_type,
'health': duck_type['health'],
'max_health': duck_type['health']
}
if self.logger:
self.logger.info(f"{duck_type['name']} spawned in {chan}")
spawn_msg = f'\033[93m{duck_type["emoji"]} A {duck_type["name"]} appears! Type !bang, !catch, !bef, or !reload!\033[0m'
await self.bot.send_message(chan, spawn_msg)
# Alert subscribed players
if self.duck_alerts:
alert_msg = f"🦆 DUCK ALERT: {duck_type['name']} in {chan}!"
for alert_nick in self.duck_alerts:
try:
await self.bot.send_message(alert_nick, alert_msg)
except:
pass # User might be offline
async def handle_command(self, user, channel, message):
nick = user.split('!')[0] if user else 'unknown'
hostmask = user if user else 'unknown'
cmd = message.strip().lower()
if self.logger:
self.logger.info(f"{nick}@{channel}: {cmd}")
# Handle private message commands
if channel == self.bot.nick: # Private message
if cmd.startswith('identify '):
parts = cmd.split(' ', 2)
if len(parts) == 3:
await self.handle_identify(nick, parts[1], parts[2])
else:
await self.bot.send_message(nick, "Usage: identify <username> <password>")
return
elif cmd == 'register':
await self.bot.send_message(nick, "To register: /msg me register <username> <password>")
return
elif cmd.startswith('register '):
parts = cmd.split(' ', 2)
if len(parts) == 3:
await self.handle_register(nick, hostmask, parts[1], parts[2])
else:
await self.bot.send_message(nick, "Usage: register <username> <password>")
return
# Public channel commands
if cmd == '!bang':
await self.handle_bang(nick, channel)
elif cmd == '!reload':
await self.handle_reload(nick, channel)
elif cmd == '!bef':
await self.handle_bef(nick, channel)
elif cmd == '!catch':
await self.handle_catch(nick, channel)
elif cmd == '!shop':
await self.handle_shop(nick, channel)
elif cmd == '!duckstats':
await self.handle_duckstats(nick, channel)
elif cmd.startswith('!buy '):
item_num = cmd.split(' ', 1)[1]
await self.handle_buy(nick, channel, item_num)
elif cmd.startswith('!sell '):
item_num = cmd.split(' ', 1)[1]
await self.handle_sell(nick, channel, item_num)
elif cmd == '!stats':
await self.handle_stats(nick, channel)
elif cmd == '!help':
await self.handle_help(nick, channel)
elif cmd == '!leaderboard' or cmd == '!top':
await self.handle_leaderboard(nick, channel)
elif cmd == '!bank':
await self.handle_bank(nick, channel)
elif cmd == '!license':
await self.handle_license(nick, channel)
elif cmd == '!alerts':
await self.handle_alerts(nick, channel)
elif cmd.startswith('!trade '):
parts = cmd.split(' ', 2)
if len(parts) >= 2:
await self.handle_trade(nick, channel, parts[1:])
elif cmd.startswith('!sabotage '):
target = cmd.split(' ', 1)[1]
await self.handle_sabotage(nick, channel, target)
async def handle_bang(self, nick, channel):
player = self.get_player(nick)
duck = self.ducks.get(channel)
if player['ammo'] <= 0:
await self.bot.send_message(channel, f'\033[91m{nick}, you need to !reload!\033[0m')
return
if duck and duck.get('alive'):
player['ammo'] -= 1
# Calculate hit chance based on accuracy and upgrades
base_accuracy = player['accuracy']
if player['scope']:
base_accuracy += 15
if player['lucky_charm']:
base_accuracy += 10
hit_roll = random.randint(1, 100)
if hit_roll <= base_accuracy:
player['caught'] += 1
coins_earned = 1
if player['silencer']:
coins_earned += 1 # Bonus for silencer
player['coins'] += coins_earned
self.ducks[channel] = {'alive': False}
await self.bot.send_message(channel, f'\033[92m{nick} shot the duck! (+{coins_earned} coin{"s" if coins_earned > 1 else ""})\033[0m')
if self.logger:
self.logger.info(f"{nick} shot a duck in {channel}")
else:
await self.bot.send_message(channel, f'\033[93m{nick} missed the duck!\033[0m')
else:
await self.bot.send_message(channel, f'No duck to shoot, {nick}!')
self.save_player(nick, player)
async def handle_reload(self, nick, channel):
player = self.get_player(nick)
# Check gun reliability - can fail to reload
reliability = player['reliability']
if player['gun_oil'] > 0:
reliability += 15
player['gun_oil'] -= 1 # Gun oil gets used up
reload_roll = random.randint(1, 100)
if reload_roll <= reliability:
player['ammo'] = player['max_ammo']
await self.bot.send_message(channel, f'\033[94m{nick} reloaded successfully!\033[0m')
else:
await self.bot.send_message(channel, f'\033[91m{nick}\'s gun jammed while reloading! Try again.\033[0m')
self.save_player(nick, player)
async def handle_bef(self, nick, channel):
player = self.get_player(nick)
duck = self.ducks.get(channel)
if duck and duck.get('alive'):
player['friends'].add('duck')
self.ducks[channel] = {'alive': False}
await self.bot.send_message(channel, f'\033[96m{nick} befriended the duck!\033[0m')
if self.logger:
self.logger.info(f"{nick} befriended a duck in {channel}")
else:
await self.bot.send_message(channel, f'No duck to befriend, {nick}!')
self.save_player(nick, player)
async def handle_catch(self, nick, channel):
player = self.get_player(nick)
duck = self.ducks.get(channel)
if duck and duck.get('alive'):
player['caught'] += 1
self.ducks[channel] = {'alive': False}
await self.bot.send_message(channel, f'\033[92m{nick} caught the duck!\033[0m')
if self.logger:
self.logger.info(f"{nick} caught a duck in {channel}")
else:
await self.bot.send_message(channel, f'No duck to catch, {nick}!')
self.save_player(nick, player)
async def handle_shop(self, nick, channel):
player = self.get_player(nick)
coins = player['coins']
shop_items = [
"🔫 Scope - Improves accuracy by 15% (Cost: 5 coins)",
"🔇 Silencer - Bonus coin on successful shots (Cost: 8 coins)",
"🛢️ Gun Oil - Improves reload reliability for 3 reloads (Cost: 3 coins)",
"🍀 Lucky Charm - Improves accuracy by 10% (Cost: 10 coins)",
"📦 Ammo Upgrade - Increases max ammo capacity by 1 (Cost: 12 coins)",
"🎯 Accuracy Training - Permanently increases accuracy by 5% (Cost: 15 coins)",
"🔧 Gun Maintenance - Permanently increases reliability by 10% (Cost: 20 coins)"
]
shop_msg = f"\033[95m{nick}'s Shop (Coins: {coins}):\033[0m\n"
for i, item in enumerate(shop_items, 1):
shop_msg += f"{i}. {item}\n"
shop_msg += "Use !buy <number> to purchase an item!\n"
shop_msg += "Use !sell <number> to sell upgrades for coins!"
await self.bot.send_message(channel, shop_msg)
async def handle_duckstats(self, nick, channel):
player = self.get_player(nick)
stats = f"\033[95m{nick}'s Duck Stats:\033[0m\n"
stats += f"Caught: {player['caught']}\n"
stats += f"Coins: {player['coins']}\n"
stats += f"Accuracy: {player['accuracy']}%\n"
stats += f"Reliability: {player['reliability']}%\n"
stats += f"Max Ammo: {player['max_ammo']}\n"
stats += f"Gun Oil: {player['gun_oil']} uses left\n"
upgrades = []
if player['scope']: upgrades.append("Scope")
if player['silencer']: upgrades.append("Silencer")
if player['lucky_charm']: upgrades.append("Lucky Charm")
stats += f"Upgrades: {', '.join(upgrades) if upgrades else 'None'}\n"
stats += f"Friends: {', '.join(player['friends']) if player['friends'] else 'None'}\n"
await self.bot.send_message(channel, stats)
async def handle_buy(self, nick, channel, item_num):
player = self.get_player(nick)
try:
item_id = int(item_num)
except ValueError:
await self.bot.send_message(channel, f'{nick}, please specify a valid item number!')
return
shop_items = {
1: ("scope", 5, "Scope"),
2: ("silencer", 8, "Silencer"),
3: ("gun_oil", 3, "Gun Oil"),
4: ("lucky_charm", 10, "Lucky Charm"),
5: ("ammo_upgrade", 12, "Ammo Upgrade"),
6: ("accuracy_training", 15, "Accuracy Training"),
7: ("gun_maintenance", 20, "Gun Maintenance")
}
if item_id not in shop_items:
await self.bot.send_message(channel, f'{nick}, invalid item number!')
return
item_key, cost, item_name = shop_items[item_id]
if player['coins'] < cost:
await self.bot.send_message(channel, f'\033[91m{nick}, you need {cost} coins for {item_name}! (You have {player["coins"]})\033[0m')
return
# Process purchase
player['coins'] -= cost
if item_key == "scope":
if player['scope']:
await self.bot.send_message(channel, f'{nick}, you already have a scope!')
player['coins'] += cost # Refund
return
player['scope'] = True
elif item_key == "silencer":
if player['silencer']:
await self.bot.send_message(channel, f'{nick}, you already have a silencer!')
player['coins'] += cost
return
player['silencer'] = True
elif item_key == "gun_oil":
player['gun_oil'] += 3
elif item_key == "lucky_charm":
if player['lucky_charm']:
await self.bot.send_message(channel, f'{nick}, you already have a lucky charm!')
player['coins'] += cost
return
player['lucky_charm'] = True
elif item_key == "ammo_upgrade":
player['max_ammo'] += 1
elif item_key == "accuracy_training":
player['accuracy'] = min(95, player['accuracy'] + 5) # Cap at 95%
elif item_key == "gun_maintenance":
player['reliability'] = min(95, player['reliability'] + 10) # Cap at 95%
await self.bot.send_message(channel, f'\033[92m{nick} purchased {item_name}!\033[0m')
self.save_player(nick, player)
async def handle_sell(self, nick, channel, item_num):
player = self.get_player(nick)
try:
item_id = int(item_num)
except ValueError:
await self.bot.send_message(channel, f'{nick}, please specify a valid item number!')
return
sellable_items = {
1: ("scope", 3, "Scope"),
2: ("silencer", 5, "Silencer"),
3: ("gun_oil", 1, "Gun Oil (per use)"),
4: ("lucky_charm", 6, "Lucky Charm")
}
if item_id not in sellable_items:
await self.bot.send_message(channel, f'{nick}, invalid item number! Sellable items: 1-4')
return
item_key, sell_price, item_name = sellable_items[item_id]
if item_key == "scope":
if not player['scope']:
await self.bot.send_message(channel, f'{nick}, you don\'t have a scope to sell!')
return
player['scope'] = False
player['coins'] += sell_price
elif item_key == "silencer":
if not player['silencer']:
await self.bot.send_message(channel, f'{nick}, you don\'t have a silencer to sell!')
return
player['silencer'] = False
player['coins'] += sell_price
elif item_key == "gun_oil":
if player['gun_oil'] <= 0:
await self.bot.send_message(channel, f'{nick}, you don\'t have any gun oil to sell!')
return
player['gun_oil'] -= 1
player['coins'] += sell_price
elif item_key == "lucky_charm":
if not player['lucky_charm']:
await self.bot.send_message(channel, f'{nick}, you don\'t have a lucky charm to sell!')
return
player['lucky_charm'] = False
player['coins'] += sell_price
await self.bot.send_message(channel, f'\033[94m{nick} sold {item_name} for {sell_price} coins!\033[0m')
self.save_player(nick, player)
async def handle_stats(self, nick, channel):
player = self.get_player(nick)
# Calculate effective accuracy and reliability
effective_accuracy = player['accuracy']
if player['scope']:
effective_accuracy += 15
if player['lucky_charm']:
effective_accuracy += 10
effective_accuracy = min(100, effective_accuracy)
effective_reliability = player['reliability']
if player['gun_oil'] > 0:
effective_reliability += 15
effective_reliability = min(100, effective_reliability)
stats = f"\033[96m{nick}'s Combat Stats:\033[0m\n"
stats += f"🎯 Base Accuracy: {player['accuracy']}% (Effective: {effective_accuracy}%)\n"
stats += f"🔧 Base Reliability: {player['reliability']}% (Effective: {effective_reliability}%)\n"
stats += f"🔫 Ammo: {player['ammo']}/{player['max_ammo']}\n"
stats += f"💰 Coins: {player['coins']}\n"
stats += f"🦆 Ducks Caught: {player['caught']}\n"
stats += f"🛢️ Gun Oil: {player['gun_oil']} uses\n"
upgrades = []
if player['scope']: upgrades.append("🔭 Scope")
if player['silencer']: upgrades.append("🔇 Silencer")
if player['lucky_charm']: upgrades.append("🍀 Lucky Charm")
stats += f"⚡ Active Upgrades: {', '.join(upgrades) if upgrades else 'None'}\n"
friends = list(player['friends'])
stats += f"🤝 Friends: {', '.join(friends) if friends else 'None'}"
await self.bot.send_message(channel, stats)
async def handle_register(self, nick, hostmask, username, password):
if self.auth.register_account(username, password, nick, hostmask):
await self.bot.send_message(nick, f"✅ Account '{username}' registered successfully! Use 'identify {username} {password}' to login.")
else:
await self.bot.send_message(nick, f"❌ Account '{username}' already exists!")
async def handle_identify(self, nick, username, password):
if self.auth.authenticate(username, password, nick):
await self.bot.send_message(nick, f"✅ Authenticated as '{username}'!")
# Transfer nick-based data to account if exists
nick_data = self.db.load_player(nick)
if nick_data:
account_data = self.db.load_player(username)
if not account_data:
self.db.save_player(username, nick_data)
await self.bot.send_message(nick, "📊 Your progress has been transferred to your account!")
else:
await self.bot.send_message(nick, "❌ Invalid username or password!")
async def handle_help(self, nick, channel):
help_text = """
🦆 **DuckHunt Bot Commands** 🦆
**🎯 Hunting:**
• !bang - Shoot at a duck (requires ammo)
• !reload - Reload your weapon (can fail based on reliability)
• !catch - Catch a duck with your hands
• !bef - Befriend a duck instead of shooting
**🛒 Economy:**
• !shop - View available items for purchase
• !buy <number> - Purchase an item from the shop
• !sell <number> - Sell equipment for coins
• !bank - Access banking services (deposits, loans)
• !trade <player> <item> <amount> - Trade with other players
**📊 Stats & Info:**
• !stats - View detailed combat statistics
• !duckstats - View personal hunting statistics
• !leaderboard - View top players
• !license - Manage hunting license
**⚙️ Settings:**
• !alerts - Toggle duck spawn notifications
• !register - Register an account (via /msg)
• identify <user> <pass> - Login to account (via /msg)
**🎮 Advanced:**
• !sabotage <player> - Attempt to sabotage another hunter
• !help - Show this help message
💡 **Tips:**
- Different duck types give different rewards
- Weapon durability affects performance
- Insurance protects your equipment
- Level up to unlock better gear!
"""
await self.bot.send_message(nick, help_text)
async def handle_leaderboard(self, nick, channel):
leaderboard_data = self.db.get_leaderboard('caught', 10)
if not leaderboard_data:
await self.bot.send_message(channel, "No leaderboard data available yet!")
return
msg = "🏆 **Duck Hunting Leaderboard** 🏆\n"
for i, (account, caught) in enumerate(leaderboard_data, 1):
emoji = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
msg += f"{emoji} {account}: {caught} ducks\n"
await self.bot.send_message(channel, msg)
async def handle_bank(self, nick, channel):
player = self.get_player(nick)
bank_msg = f"""
🏦 **{nick}'s Bank Account** 🏦
💰 Cash on hand: {player['coins']} coins
🏛️ Bank balance: {player['bank_account']} coins
📈 Total wealth: {player['coins'] + player['bank_account']} coins
**Commands:**
• !bank deposit <amount> - Deposit coins (earns 2% daily interest)
• !bank withdraw <amount> - Withdraw coins
• !bank loan <amount> - Take a loan (10% interest)
"""
await self.bot.send_message(nick, bank_msg)
async def handle_license(self, nick, channel):
player = self.get_player(nick)
license_active = player['hunting_license']['active']
if license_active:
expires = player['hunting_license']['expires']
msg = f"🎫 Your hunting license is active until {expires}\n"
msg += "Licensed hunters get +25% coins and access to rare equipment!"
else:
msg = "🎫 You don't have a hunting license.\n"
msg += "Purchase one for 50 coins to get:\n"
msg += "• +25% coin rewards\n"
msg += "• Access to premium shop items\n"
msg += "• Reduced insurance costs\n"
msg += "Type '!buy license' to purchase"
await self.bot.send_message(channel, msg)
async def handle_alerts(self, nick, channel):
if nick in self.duck_alerts:
self.duck_alerts.remove(nick)
await self.bot.send_message(channel, f"🔕 {nick}: Duck alerts disabled")
else:
self.duck_alerts.add(nick)
await self.bot.send_message(channel, f"🔔 {nick}: Duck alerts enabled! You'll be notified when ducks spawn.")
async def handle_trade(self, nick, channel, args):
if len(args) < 3:
await self.bot.send_message(channel, f"{nick}: Usage: !trade <player> <item> <amount>")
return
target, item, amount = args[0], args[1], args[2]
player = self.get_player(nick)
try:
amount = int(amount)
except ValueError:
await self.bot.send_message(channel, f"{nick}: Amount must be a number!")
return
if item == "coins":
if player['coins'] < amount:
await self.bot.send_message(channel, f"{nick}: You don't have enough coins!")
return
trade_data = {
'type': 'coins',
'amount': amount,
'from_nick': nick
}
trade_id = self.db.save_trade(nick, target, trade_data)
await self.bot.send_message(channel, f"💸 Trade offer sent to {target}: {amount} coins")
await self.bot.send_message(target, f"💰 {nick} wants to trade you {amount} coins. Type '!accept {trade_id}' to accept!")
else:
await self.bot.send_message(channel, f"{nick}: Only coin trading is available currently!")
async def handle_sabotage(self, nick, channel, target):
player = self.get_player(nick)
target_player = self.get_player(target)
if player['coins'] < 5:
await self.bot.send_message(channel, f"{nick}: Sabotage costs 5 coins!")
return
success_chance = 60 + (player['level'] * 5)
if random.randint(1, 100) <= success_chance:
player['coins'] -= 5
target_player['weapon_durability'] = max(0, target_player['weapon_durability'] - 10)
await self.bot.send_message(channel, f"😈 {nick} successfully sabotaged {target}'s weapon!")
self.save_player(nick, player)
self.save_player(target, target_player)
else:
player['coins'] -= 5
await self.bot.send_message(channel, f"😅 {nick}'s sabotage attempt failed!")
self.save_player(nick, player)

View File

@@ -1,124 +0,0 @@
import random
class DuckTypes:
COMMON = {
'name': 'Common Duck',
'emoji': '🦆',
'rarity': 70,
'coins': 1,
'xp': 10,
'health': 1
}
RARE = {
'name': 'Rare Duck',
'emoji': '🦆✨',
'rarity': 20,
'coins': 3,
'xp': 25,
'health': 1
}
GOLDEN = {
'name': 'Golden Duck',
'emoji': '🥇🦆',
'rarity': 8,
'coins': 10,
'xp': 50,
'health': 2
}
ARMORED = {
'name': 'Armored Duck',
'emoji': '🛡️🦆',
'rarity': 2,
'coins': 15,
'xp': 75,
'health': 3
}
@classmethod
def get_random_duck(cls):
roll = random.randint(1, 100)
if roll <= cls.COMMON['rarity']:
return cls.COMMON
elif roll <= cls.COMMON['rarity'] + cls.RARE['rarity']:
return cls.RARE
elif roll <= cls.COMMON['rarity'] + cls.RARE['rarity'] + cls.GOLDEN['rarity']:
return cls.GOLDEN
else:
return cls.ARMORED
class WeaponTypes:
BASIC_GUN = {
'name': 'Basic Gun',
'accuracy_bonus': 0,
'durability': 100,
'max_durability': 100,
'repair_cost': 5,
'attachment_slots': 1
}
SHOTGUN = {
'name': 'Shotgun',
'accuracy_bonus': -10,
'durability': 80,
'max_durability': 80,
'repair_cost': 8,
'attachment_slots': 2,
'spread_shot': True # Can hit multiple ducks
}
RIFLE = {
'name': 'Rifle',
'accuracy_bonus': 20,
'durability': 120,
'max_durability': 120,
'repair_cost': 12,
'attachment_slots': 3
}
class AmmoTypes:
STANDARD = {
'name': 'Standard Ammo',
'damage': 1,
'accuracy_modifier': 0,
'cost': 1
}
RUBBER = {
'name': 'Rubber Bullets',
'damage': 0, # Non-lethal, for catching
'accuracy_modifier': 5,
'cost': 2,
'special': 'stun'
}
EXPLOSIVE = {
'name': 'Explosive Rounds',
'damage': 2,
'accuracy_modifier': -5,
'cost': 5,
'special': 'area_damage'
}
class Attachments:
LASER_SIGHT = {
'name': 'Laser Sight',
'accuracy_bonus': 10,
'cost': 15,
'durability_cost': 2 # Uses weapon durability faster
}
EXTENDED_MAG = {
'name': 'Extended Magazine',
'ammo_bonus': 2,
'cost': 20
}
BIPOD = {
'name': 'Bipod',
'accuracy_bonus': 15,
'reliability_bonus': 5,
'cost': 25
}

View File

@@ -1,28 +0,0 @@
import logging
import sys
from functools import partial
class ColorFormatter(logging.Formatter):
COLORS = {
'DEBUG': '\033[94m',
'INFO': '\033[92m',
'WARNING': '\033[93m',
'ERROR': '\033[91m',
'CRITICAL': '\033[95m',
'ENDC': '\033[0m',
}
def format(self, record):
color = self.COLORS.get(record.levelname, '')
endc = self.COLORS['ENDC']
msg = super().format(record)
return f"{color}{msg}{endc}"
def setup_logger(name='DuckHuntBot', level=logging.INFO):
logger = logging.getLogger(name)
handler = logging.StreamHandler(sys.stdout)
formatter = ColorFormatter('[%(asctime)s] %(levelname)s: %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(level)
logger.propagate = False
return logger

View File

@@ -1,11 +0,0 @@
def parse_message(line):
prefix = ''
trailing = ''
if line.startswith(':'):
prefix, line = line[1:].split(' ', 1)
if ' :' in line:
line, trailing = line.split(' :', 1)
parts = line.split()
command = parts[0] if parts else ''
params = parts[1:] if len(parts) > 1 else []
return prefix, command, params, trailing

View File

@@ -1,167 +0,0 @@
#!/usr/bin/env python3
"""
Test script for DuckHunt Bot
Run this to test both the modular and simple bot implementations
"""
import asyncio
import json
import sys
import os
# Add src directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
async def test_modular_bot():
"""Test the modular bot implementation"""
try:
print("🔧 Testing modular bot (src/duckhuntbot.py)...")
# Load config
with open('config.json') as f:
config = json.load(f)
# Test imports
from src.duckhuntbot import IRCBot
from src.sasl import SASLHandler
# Create bot instance
bot = IRCBot(config)
print("✅ Modular bot initialized successfully!")
# Test SASL handler
sasl_handler = SASLHandler(bot, config)
print("✅ SASL handler created successfully!")
# Test database
bot.db.save_player("testuser", {"coins": 100, "caught": 5})
data = bot.db.load_player("testuser")
if data and data['coins'] == 100:
print("✅ Database working!")
else:
print("❌ Database test failed!")
# Test game logic
player = bot.game.get_player("testuser")
if player and 'coins' in player:
print("✅ Game logic working!")
else:
print("❌ Game logic test failed!")
return True
except Exception as e:
print(f"❌ Modular bot error: {e}")
import traceback
traceback.print_exc()
return False
async def test_simple_bot():
"""Test the simple bot implementation"""
try:
print("\n🔧 Testing simple bot (simple_duckhunt.py)...")
# Load config
with open('config.json') as f:
config = json.load(f)
# Test imports
from simple_duckhunt import SimpleIRCBot
from src.sasl import SASLHandler
# Create bot instance
bot = SimpleIRCBot(config)
print("✅ Simple bot initialized successfully!")
# Test SASL handler integration
if hasattr(bot, 'sasl_handler'):
print("✅ SASL handler integrated!")
else:
print("❌ SASL handler not integrated!")
return False
# Test database
if 'testuser' in bot.players:
bot.players['testuser']['coins'] = 200
bot.save_database()
bot.load_database()
if bot.players.get('testuser', {}).get('coins') == 200:
print("✅ Simple bot database working!")
else:
print("❌ Simple bot database test failed!")
return True
except Exception as e:
print(f"❌ Simple bot error: {e}")
import traceback
traceback.print_exc()
return False
async def test_sasl_config():
"""Test SASL configuration"""
try:
print("\n🔧 Testing SASL configuration...")
# Load config
with open('config.json') as f:
config = json.load(f)
# Check SASL config
sasl_config = config.get('sasl', {})
if sasl_config.get('enabled'):
print("✅ SASL is enabled in config")
username = sasl_config.get('username')
password = sasl_config.get('password')
if username and password:
print(f"✅ SASL credentials configured (user: {username})")
else:
print("⚠️ SASL enabled but credentials missing")
else:
print(" SASL is not enabled in config")
return True
except Exception as e:
print(f"❌ SASL config error: {e}")
return False
async def main():
"""Main test function"""
print("🦆 DuckHunt Bot Integration Test")
print("=" * 50)
try:
# Test configuration
config_ok = await test_sasl_config()
# Test modular bot
modular_ok = await test_modular_bot()
# Test simple bot
simple_ok = await test_simple_bot()
print("\n" + "=" * 50)
print("📊 Test Results:")
print(f" Config: {'✅ PASS' if config_ok else '❌ FAIL'}")
print(f" Modular Bot: {'✅ PASS' if modular_ok else '❌ FAIL'}")
print(f" Simple Bot: {'✅ PASS' if simple_ok else '❌ FAIL'}")
if all([config_ok, modular_ok, simple_ok]):
print("\n🎉 All tests passed! SASL integration is working!")
print("🦆 DuckHunt Bots are ready to deploy!")
return True
else:
print("\n💥 Some tests failed. Check the errors above.")
return False
except Exception as e:
print(f"💥 Test suite error: {e}")
return False
if __name__ == '__main__':
success = asyncio.run(main())
if not success:
sys.exit(1)

104
levels.json Normal file
View File

@@ -0,0 +1,104 @@
{
"level_calculation": {
"method": "xp",
"description": "Level based on XP earned from hunting and befriending ducks"
},
"levels": {
"1": {
"name": "Duck Novice",
"min_xp": 0,
"max_xp": 49,
"befriend_success_rate": 95,
"accuracy_modifier": 25,
"jam_chance": 0,
"duck_spawn_speed_modifier": 1.0,
"magazines": 3,
"bullets_per_magazine": 6,
"description": "Just starting out, ducks are trusting and easier to hit"
},
"2": {
"name": "Pond Visitor",
"min_xp": 50,
"max_xp": 149,
"befriend_success_rate": 85,
"accuracy_modifier": 15,
"jam_chance": 1,
"duck_spawn_speed_modifier": 1.0,
"magazines": 3,
"bullets_per_magazine": 6,
"description": "Ducks are getting wary of you"
},
"3": {
"name": "Duck Hunter",
"min_xp": 150,
"max_xp": 299,
"befriend_success_rate": 80,
"accuracy_modifier": 5,
"jam_chance": 2,
"duck_spawn_speed_modifier": 0.9,
"magazines": 3,
"bullets_per_magazine": 6,
"description": "Your reputation precedes you, ducks are more cautious"
},
"4": {
"name": "Wetland Stalker",
"min_xp": 300,
"max_xp": 599,
"befriend_success_rate": 75,
"accuracy_modifier": -5,
"jam_chance": 3,
"duck_spawn_speed_modifier": 0.8,
"magazines": 2,
"bullets_per_magazine": 6,
"description": "Ducks flee at your approach, spawns are less frequent"
},
"5": {
"name": "Apex Predator",
"min_xp": 600,
"max_xp": 999,
"befriend_success_rate": 70,
"accuracy_modifier": -15,
"jam_chance": 4,
"duck_spawn_speed_modifier": 0.7,
"magazines": 2,
"bullets_per_magazine": 6,
"description": "You're feared throughout the pond, ducks are very elusive"
},
"6": {
"name": "Duck Whisperer",
"min_xp": 1000,
"max_xp": 1999,
"befriend_success_rate": 65,
"accuracy_modifier": -20,
"jam_chance": 5,
"duck_spawn_speed_modifier": 0.6,
"magazines": 1,
"bullets_per_magazine": 6,
"description": "Only the bravest ducks dare show themselves"
},
"7": {
"name": "Legendary Hunter",
"min_xp": 2000,
"max_xp": 4999,
"befriend_success_rate": 60,
"accuracy_modifier": -25,
"jam_chance": 6,
"duck_spawn_speed_modifier": 0.5,
"magazines": 1,
"bullets_per_magazine": 6,
"description": "Duck folklore speaks of your prowess, they're extremely rare"
},
"8": {
"name": "Duck Deity",
"min_xp": 5000,
"max_xp": 999999,
"befriend_success_rate": 55,
"accuracy_modifier": -30,
"jam_chance": 8,
"duck_spawn_speed_modifier": 0.4,
"magazines": 1,
"bullets_per_magazine": 6,
"description": "You've transcended mortal hunting, ducks are mythically scarce"
}
}
}

127
messages.json Normal file
View File

@@ -0,0 +1,127 @@
{
"duck_spawn": [
"・゜゜・。。・゜゜\\_O< {light_grey}QUACK!{reset}",
"{light_grey}・゜゜・。。・゜゜{reset}\\_O< {light_grey}QUACK!{reset}",
"・゜゜・。。・゜゜{black}\\_O< QUACK!{reset}",
"・゜゜・。。・゜゜\\_o< quack~",
"・゜゜・。。・゜゜\\_O> *flap flap*",
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°·\u000f \u0002\\_O<\u0002 \u000314QUACK\u000f",
"{light_grey}·.¸¸.·´¯`·.¸¸.·´¯`·.{reset} {bold}\\_O<{reset} {light_grey}QUACK!{reset}",
"{cyan}≋≋≋{reset} {bold}\\_O<{reset} {cyan}≋≋≋{reset} {light_grey}QUACK!{reset}",
"{yellow}⟦{reset}{bold}\\_O<{reset}{yellow}⟧{reset} {light_grey}Q U A C K{reset}",
"{bold}\\_O<{reset} {light_grey}*q u a c k*{reset} {grey}(respawned){reset}",
"{light_grey}..:::{reset} {bold}\\_O<{reset} {light_grey}:::..{reset} {light_grey}QUACK!{reset}",
"{bold}\\_O<{reset} {light_grey}≫{reset} {green}Q U A C K{reset} {light_grey}≪{reset}",
"{light_grey}~{reset}{bold}\\_O<{reset}{light_grey}~{reset} {light_grey}QUACK!{reset} {light_grey}~{reset}"
],
"duck_flies_away": [
"The duck flies away. ·°'`'°-.,¸¸.·°'`",
"The duck escapes into the sky! ·°'`'°-.,¸¸.·°'`",
"\\o< *quack* The duck waddles away safely.",
"The duck flaps away, living another day. ·°'`'°-.,¸¸.·°'`",
"\\o< The duck disappears into the distance.",
"The duck takes flight and vanishes! ·°'`'°-.,¸¸.·°'`",
"\\o< *flap* *flap* The duck has escaped!"
],
"fast_duck_flies_away": [
"The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`",
"\\o< *ZOOM* The speedy duck vanishes in a flash!",
"The fast duck zips away at lightning speed! ·°'`'°-.,¸¸.·°'`",
"\\o< Too slow! The fast duck has already escaped!",
"The swift duck darts away before you can blink! ·°'`'°-.,¸¸.·°'`",
"\\o< *whoosh* The fast duck is gone!"
],
"golden_duck_flies_away": [
"The {gold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`",
"\\o< The {gold}golden duck{reset} glides away gracefully, its feathers shimmering.",
"The precious {gold}golden duck{reset} escapes to safety! ·°'`'°-.,¸¸.·°'`",
"\\o< The {gold}golden duck{reset} spreads its magnificent wings and soars away.",
"The valuable {gold}golden duck{reset} disappears into the sunset! ·°'`'°-.,¸¸.·°'`",
"\\o< *glimmer* The {gold}golden duck{reset} vanishes like a treasure in the wind."
],
"bang_hit": "{nick} > {red}*BANG*{reset} You shot the duck! \\_X< *KWAK* {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
"bang_hit_golden": "{nick} > {red}*BANG*{reset} You shot a {gold}GOLDEN DUCK!{reset} [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
"bang_hit_golden_killed": "{nick} > {red}*BANG*{reset} You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
"bang_hit_fast": "{nick} > {red}*BANG*{reset} You shot a FAST DUCK! {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
"bang_miss": "{nick} > {red}*BANG*{reset} You missed the duck! {red}[-1 XP]{reset}",
"bang_friendly_fire_penalty": "{nick} > {red}*BANG*{reset} You missed and hit {victim}! {red}[GUN CONFISCATED]{reset} [LOST {xp_lost} XP]",
"bang_friendly_fire_insured": "{nick} > *BANG* You missed and hit {victim}! {green}[INSURANCE PROTECTED - No penalties]{reset}",
"bang_no_duck": "{nick} > *BANG* What did you shoot at? There is no duck in the area... {red}[GUN CONFISCATED]{reset}",
"bang_no_ammo": "{nick} > *click* You're out of ammo! Use !reload",
"bang_gun_jammed": "{nick} > *click* Your gun jammed! [AMMO WASTED]",
"bang_not_armed": "{nick} > Your gun has been confiscated. Buy it back from the shop.",
"bef_success": "{nick} > *befriend* You befriended the duck! [+{xp_gained} xp] [Ducks befriended: {ducks_befriended}]",
"bef_failed": "{nick} > *gentle approach* The duck doesn't trust you and flies away...",
"bef_no_duck": "{nick} > *gentle approach* There is no duck to befriend in the area...",
"bef_duck_shot": "{nick} > *gentle approach* The duck is already dead! You can't befriend it now...",
"reload_success": "{nick} > *click* New magazine loaded! [Ammo: {ammo}/{max_ammo}] [Spare magazines: {chargers}]",
"reload_already_loaded": "{nick} > Your gun is already loaded!",
"reload_no_chargers": "{nick} > You're out of ammo!",
"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 | !bef - Befriend ducks | !reload - Reload your gun | !shop - View/buy from shop | !duckstats - View your stats and items | !topduck - View leaderboards | !use - Use inventory items | !give - Give items to other players",
"help_help_command": "!duckhelp - Show this help",
"help_admin_commands": "Admin: !rearm <player|all> | !disarm <player> | !ignore <player> | !unignore <player> | !ducklaunch [duck_type] (all support /msg)",
"admin_rearm_player": "[ADMIN] {target} has been rearmed by {admin}",
"admin_rearm_all": "[ADMIN] All players have been rearmed by {admin}",
"admin_rearm_self": "[ADMIN] {admin} has rearmed themselves",
"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|all>",
"usage_disarm": "Usage: !disarm <player>",
"usage_ignore": "Usage: !ignore <player>",
"usage_unignore": "Usage: !unignore <player>",
"usage_give": "Usage: !give <item_id> <player>",
"shop_buy_success": "{nick} > You bought {item_name}! [-{price} XP] [Remaining: {remaining_xp} XP]",
"shop_buy_insufficient_xp": "{nick} > You don't have enough XP to buy {item_name}. Need {price} XP, you have {current_xp} XP.",
"shop_buy_invalid_id": "{nick} > Invalid item ID. Use !shop to see available items.",
"shop_buy_usage": "Usage: !shop buy <item_id>",
"use_attract_ducks": "{nick} > You scattered bread around the pond! Ducks will spawn {spawn_multiplier}x faster for {duration} minutes.",
"use_insurance": "{nick} > You activated Hunter's Insurance! Protected from friendly fire penalties for {duration} hours.",
"use_buy_gun_back": "{nick} > Your gun has been returned with {ammo_restored} bullets and {magazines_restored} magazines.",
"use_buy_gun_back_not_needed": "{nick} > Your gun is not confiscated.",
"bang_wet_clothes": "{nick} > *SPLASH* Your clothes are soaked! You can't shoot until you dry off or buy new clothes.",
"use_splash_water": "{nick} > *SPLASH* You soaked {target_nick} with water! They can't shoot for {duration} minutes.",
"use_dry_clothes": "{nick} > You changed into dry clothes! Ready to hunt again.",
"use_dry_clothes_not_needed": "{nick} > You weren't wet - no need for new clothes.",
"gift_success_generic": "{nick} > Successfully gave {item_name} to {target_nick}!",
"gift_ammo": "{nick} > Gave {amount} bullet(s) to {target_nick}! What a generous hunter.",
"gift_magazine": "{nick} > Gave 1 magazine to {target_nick}! Sharing the ammo love.",
"gift_gun_brush": "{nick} > Gave a gun brush to {target_nick} - keeping their weapon clean!",
"gift_insurance": "{nick} > Gave Hunter's Insurance to {target_nick} - protecting them from friendly fire!",
"gift_dry_clothes": "{nick} > Gave dry clothes to {target_nick} - now they can stay dry!",
"gift_buy_gun_back": "{nick} > Gave a gun license to {target_nick} - helping them get their gun back!",
"duck_drop_normal": "{nick} > The duck dropped a {green}{item_name}{reset}! [Added to inventory]",
"duck_drop_fast": "{nick} > The {cyan}fast duck{reset} dropped a {green}{item_name}{reset}! [Added to inventory]",
"duck_drop_golden": "{nick} > The {gold}golden duck{reset} dropped a {green}{item_name}{reset}! [Added to inventory]",
"colours": {
"white": "\u00030",
"black": "\u00031",
"blue": "\u00032",
"green": "\u00033",
"red": "\u00034",
"brown": "\u00035",
"purple": "\u00036",
"orange": "\u00037",
"yellow": "\u00038",
"gold": "\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"
}
}

1
original Submodule

Submodule original added at f8c46980de

75
shop.json Normal file
View File

@@ -0,0 +1,75 @@
{
"items": {
"1": {
"name": "Single Bullet",
"price": 5,
"description": "1 extra bullet",
"type": "ammo",
"amount": 1
},
"2": {
"name": "Magazine",
"price": 15,
"description": "1 extra magazine",
"type": "magazine",
"amount": 1
},
"3": {
"name": "Sand",
"price": 10,
"description": "Throw sand in target's gun - increases jam chance by 15%",
"type": "sabotage_jam",
"amount": 15
},
"4": {
"name": "Gun Brush",
"price": 20,
"description": "Clean your gun - decreases jam chance by 10%",
"type": "clean_gun",
"amount": -10
},
"5": {
"name": "Bread",
"price": 50,
"description": "Attract ducks - increases duck spawn rate for 20 minutes",
"type": "attract_ducks",
"duration": 1200,
"spawn_multiplier": 2.0
},
"6": {
"name": "Hunter's Insurance",
"price": 75,
"description": "Protects against friendly fire penalties for 24 hours - no XP loss or gun confiscation",
"type": "insurance",
"duration": 86400,
"protection": "friendly_fire"
},
"7": {
"name": "Buy Gun Back",
"price": 40,
"description": "Get your confiscated gun back with the same ammo it had when taken",
"type": "buy_gun_back"
},
"8": {
"name": "Bucket of Water",
"price": 25,
"description": "Splash another hunter with water - soaks their clothes and prevents shooting until they dry or buy new clothes",
"type": "splash_water"
},
"9": {
"name": "Dry Clothes",
"price": 30,
"description": "Change into dry clothes - allows shooting again after being soaked",
"type": "dry_clothes"
},
"10": {
"name": "4-Leaf Clover",
"price": 250,
"description": "Crazy luck for 10 minutes - greatly boosts hit & befriend success",
"type": "clover_luck",
"duration": 600,
"min_hit_chance": 0.95,
"min_befriend_chance": 0.95
}
}
}

605
src/db.py Normal file
View File

@@ -0,0 +1,605 @@
"""
Enhanced Database management for DuckHunt Bot
Focus on fixing missing field errors with improved error handling
"""
import json
import logging
import time
import os
from datetime import datetime
from .error_handling import with_retry, RetryConfig, ErrorRecovery, sanitize_user_input
class DuckDB:
"""Simplified database management"""
def __init__(self, db_file="duckhunt.json", bot=None):
# Resolve relative paths against the project root (repo root), not the process CWD.
# This prevents "stats wiped" symptoms when the bot is launched from a different working dir.
if os.path.isabs(db_file):
self.db_file = db_file
else:
project_root = os.path.dirname(os.path.dirname(__file__))
self.db_file = os.path.join(project_root, db_file)
self.bot = bot
# Channel-scoped data: {"#channel": {"players": {"nick": player_dict}}}
self.channels = {}
self.logger = logging.getLogger('DuckHuntBot.DB')
# Error recovery configuration
self.error_recovery = ErrorRecovery()
self.save_retry_config = RetryConfig(max_attempts=3, base_delay=0.5, max_delay=5.0)
data = self.load_database()
# Hydrate in-memory state from disk.
if isinstance(data, dict) and isinstance(data.get('channels'), dict):
self.channels = data['channels']
else:
self.channels = {}
@staticmethod
def _normalize_channel(channel: str) -> str:
"""Normalize channel keys (case-insensitive). Non-channel contexts go to a reserved bucket."""
if not isinstance(channel, str):
return '__unknown__'
channel = channel.strip()
if not channel:
return '__unknown__'
# Preserve internal buckets used by the bot/database.
# This allows explicit references like '__global__' without being remapped to '__pm__'.
if channel.startswith('__') and channel.endswith('__'):
return channel
if channel.startswith('#') or channel.startswith('&'):
return channel.lower()
return '__pm__'
def is_ignored(self, nick: str, channel: str) -> bool:
"""Return True if nick is ignored for this channel or globally."""
try:
if not isinstance(nick, str) or not nick.strip():
return False
nick_clean = sanitize_user_input(
nick,
max_length=50,
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\'
)
nick_lower = nick_clean.lower().strip()
if not nick_lower:
return False
# Channel-scoped ignore
player = self.get_player_if_exists(nick_lower, channel)
if isinstance(player, dict) and bool(player.get('ignored', False)):
return True
# Global ignore bucket
global_player = self.get_player_if_exists(nick_lower, '__global__')
return bool(global_player and global_player.get('ignored', False))
except Exception:
return False
def set_global_ignored(self, nick: str, ignored: bool) -> bool:
"""Set global ignored flag for nick (persisted)."""
try:
player = self.get_player(nick, '__global__')
if not isinstance(player, dict):
return False
player['ignored'] = bool(ignored)
return True
except Exception:
return False
@property
def players(self):
"""Backward-compatible flattened view of all players across channels."""
flattened = {}
try:
for _channel_key, channel_data in (self.channels or {}).items():
players = channel_data.get('players', {}) if isinstance(channel_data, dict) else {}
if isinstance(players, dict):
for nick, player in players.items():
# Last-write-wins if the same nick exists in multiple channels.
flattened[nick] = player
except Exception:
return {}
return flattened
def load_database(self) -> dict:
"""Load the database, creating it if it doesn't exist"""
try:
if not os.path.exists(self.db_file):
self.logger.info(f"Database file {self.db_file} not found, creating new one")
return self._create_default_database()
with open(self.db_file, 'r') as f:
content = f.read().strip()
if not content:
self.logger.warning("Database file is empty, creating new database")
return self._create_default_database()
data = json.loads(content)
# Validate basic structure
if not isinstance(data, dict):
raise ValueError("Database root is not a dictionary")
# Initialize metadata if missing
if 'metadata' not in data:
data['metadata'] = {
'version': '1.0',
'created': datetime.now().isoformat(),
'last_modified': datetime.now().isoformat()
}
# Migrate legacy flat structure (players) -> channels
if 'channels' not in data or not isinstance(data.get('channels'), dict):
legacy_players = data.get('players') if isinstance(data.get('players'), dict) else {}
channels = {}
if isinstance(legacy_players, dict):
for legacy_nick, legacy_player in legacy_players.items():
try:
last_channel = legacy_player.get('last_activity_channel') if isinstance(legacy_player, dict) else None
channel_key = self._normalize_channel(last_channel) if last_channel else '__global__'
channels.setdefault(channel_key, {'players': {}})
if isinstance(channels[channel_key].get('players'), dict):
channels[channel_key]['players'][str(legacy_nick).lower()] = legacy_player
except Exception:
continue
data['channels'] = channels
data['metadata']['version'] = '2.0'
# Ensure channels structure exists
if 'channels' not in data:
data['channels'] = {}
# Update last_modified
data['metadata']['last_modified'] = datetime.now().isoformat()
total_players = 0
try:
for _c, cdata in data.get('channels', {}).items():
if isinstance(cdata, dict) and isinstance(cdata.get('players'), dict):
total_players += len(cdata['players'])
except Exception:
total_players = 0
self.logger.info(f"Successfully loaded database with {total_players} players across {len(data.get('channels', {}))} channels")
return data
except (json.JSONDecodeError, ValueError) as e:
self.logger.error(f"Database corruption detected: {e}. Creating new database.")
return self._create_default_database()
except Exception as e:
self.logger.error(f"Error loading database: {e}")
return self._create_default_database()
def _create_default_database(self) -> dict:
"""Create a new default database file with proper structure"""
try:
default_data = {
"channels": {},
"last_save": str(time.time()),
"version": "2.0",
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
"description": "DuckHunt Bot Player Database"
}
with open(self.db_file, 'w', encoding='utf-8') as f:
json.dump(default_data, f, indent=2, ensure_ascii=False, sort_keys=True)
self.logger.info(f"Created new database file: {self.db_file}")
return default_data
except Exception as e:
self.logger.error(f"Failed to create default database: {e}")
# Return a minimal valid structure even if file creation fails
return {
"channels": {},
"last_save": str(time.time()),
"version": "2.0",
"created": time.strftime("%Y-%m-%d %H:%M:%S"),
"description": "DuckHunt Bot Player Database"
}
def _sanitize_player_data(self, player_data):
"""Sanitize and validate player data, ensuring ALL required fields exist"""
try:
sanitized = {}
# Get default values from config or fallbacks
default_accuracy = self.bot.get_config('player_defaults.accuracy', 75) if self.bot else 75
max_accuracy = self.bot.get_config('gameplay.max_accuracy', 100) if self.bot else 100
default_magazines = self.bot.get_config('player_defaults.magazines', 3) if self.bot else 3
default_bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6) if self.bot else 6
default_jam_chance = self.bot.get_config('player_defaults.jam_chance', 15) if self.bot else 15
# Core required fields - these MUST exist for messages to work
sanitized['nick'] = str(player_data.get('nick', 'Unknown'))[:50]
sanitized['xp'] = max(0, int(float(player_data.get('xp', 0))))
sanitized['ducks_shot'] = max(0, int(float(player_data.get('ducks_shot', 0))))
sanitized['ducks_befriended'] = max(0, int(float(player_data.get('ducks_befriended', 0))))
sanitized['shots_fired'] = max(0, int(float(player_data.get('shots_fired', 0))))
sanitized['shots_missed'] = max(0, int(float(player_data.get('shots_missed', 0))))
# Equipment and stats
sanitized['accuracy'] = max(0, min(max_accuracy, int(float(player_data.get('accuracy', default_accuracy)))))
sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
# Activity / admin flags
sanitized['last_activity_channel'] = str(player_data.get('last_activity_channel', ''))[:100]
try:
sanitized['last_activity_time'] = float(player_data.get('last_activity_time', 0.0))
except (ValueError, TypeError):
sanitized['last_activity_time'] = 0.0
sanitized['ignored'] = bool(player_data.get('ignored', False))
# Ammo system with validation
sanitized['current_ammo'] = max(0, min(50, int(float(player_data.get('current_ammo', default_bullets_per_mag)))))
sanitized['magazines'] = max(0, min(20, int(float(player_data.get('magazines', default_magazines)))))
sanitized['bullets_per_magazine'] = max(1, min(50, int(float(player_data.get('bullets_per_magazine', default_bullets_per_mag)))))
sanitized['jam_chance'] = max(0, min(100, int(float(player_data.get('jam_chance', default_jam_chance)))))
# Confiscated ammo (optional fields but with safe defaults)
sanitized['confiscated_ammo'] = max(0, min(50, int(float(player_data.get('confiscated_ammo', 0)))))
sanitized['confiscated_magazines'] = max(0, min(20, int(float(player_data.get('confiscated_magazines', 0)))))
# Safe inventory handling
inventory = player_data.get('inventory', {})
if isinstance(inventory, dict):
clean_inventory = {}
for k, v in inventory.items():
try:
clean_key = str(k)[:20]
clean_value = max(0, int(float(v))) if isinstance(v, (int, float, str)) else 0
if clean_value > 0:
clean_inventory[clean_key] = clean_value
except (ValueError, TypeError):
continue
sanitized['inventory'] = clean_inventory
else:
sanitized['inventory'] = {}
# Safe temporary effects
temp_effects = player_data.get('temporary_effects', [])
if isinstance(temp_effects, list):
clean_effects = []
for effect in temp_effects[:20]:
if isinstance(effect, dict) and 'type' in effect:
clean_effects.append(effect)
sanitized['temporary_effects'] = clean_effects
else:
sanitized['temporary_effects'] = []
# Add any missing fields that messages might reference
additional_fields = {
'best_time': 0.0,
'worst_time': 0.0,
'total_time_hunting': 0.0,
'level': 1,
'xp_gained': 0, # For message templates
'hp_remaining': 0, # For golden duck messages
'victim': '', # For friendly fire messages
'xp_lost': 0, # For penalty messages
'ammo': 0, # Legacy field
'max_ammo': 0, # Legacy field
'chargers': 0 # Legacy field
}
for field, default_value in additional_fields.items():
if field not in sanitized:
if field in ['best_time', 'worst_time', 'total_time_hunting']:
sanitized[field] = max(0.0, float(player_data.get(field, default_value)))
else:
sanitized[field] = player_data.get(field, default_value)
return sanitized
except Exception as e:
self.logger.error(f"Error sanitizing player data: {e}")
return self.create_player(player_data.get('nick', 'Unknown') if isinstance(player_data, dict) else 'Unknown')
@with_retry(RetryConfig(max_attempts=3, base_delay=0.5, max_delay=5.0),
exceptions=(OSError, PermissionError, IOError))
def save_database(self):
"""Save all player data to JSON file with retry logic and comprehensive error handling"""
return self._save_database_impl()
def _save_database_impl(self):
"""Internal implementation of database save"""
temp_file = f"{self.db_file}.tmp"
try:
# Prepare data with validation
data = {
'channels': {},
'last_save': str(time.time()),
'version': '2.0'
}
# Validate and clean player data before saving
valid_count = 0
for channel_key, channel_data in (self.channels or {}).items():
if not isinstance(channel_key, str) or not isinstance(channel_data, dict):
continue
players = channel_data.get('players', {})
if not isinstance(players, dict):
continue
out_channel_key = str(channel_key)
data['channels'].setdefault(out_channel_key, {'players': {}})
for nick, player_data in players.items():
if isinstance(nick, str) and isinstance(player_data, dict):
try:
sanitized_nick = sanitize_user_input(nick, max_length=50)
data['channels'][out_channel_key]['players'][sanitized_nick] = self._sanitize_player_data(player_data)
valid_count += 1
except Exception as e:
self.logger.warning(f"Error processing player {nick} in {out_channel_key} during save: {e}")
# Saving an empty database is valid (e.g., first run or after admin wipes).
# Previously this raised and prevented the file from being written/updated.
# Write to temporary file first (atomic write)
with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False, sort_keys=True)
f.flush()
os.fsync(f.fileno())
# Verify temp file was written correctly
try:
with open(temp_file, 'r', encoding='utf-8') as f:
json.load(f) # Verify it's valid JSON
except json.JSONDecodeError:
raise IOError("Temporary file contains invalid JSON")
# Atomic replace
if os.name == 'nt': # Windows
if os.path.exists(self.db_file):
os.remove(self.db_file)
os.rename(temp_file, self.db_file)
else: # Unix-like systems
os.rename(temp_file, self.db_file)
self.logger.debug(f"Database saved successfully with {valid_count} players")
return True
except Exception as e:
self.logger.error(f"Error in database save implementation: {e}")
raise # Re-raise for retry mechanism
finally:
# Clean up temp file if it still exists
try:
if os.path.exists(temp_file):
os.remove(temp_file)
except Exception:
pass
def get_players_for_channel(self, channel: str) -> dict:
"""Get the players dict for a channel, creating the channel bucket if needed."""
channel_key = self._normalize_channel(channel)
bucket = self.channels.setdefault(channel_key, {'players': {}})
if not isinstance(bucket, dict):
bucket = {'players': {}}
self.channels[channel_key] = bucket
if 'players' not in bucket or not isinstance(bucket.get('players'), dict):
bucket['players'] = {}
return bucket['players']
def iter_all_players(self):
"""Yield (channel_key, nick, player_dict) for all players in all channels."""
for channel_key, channel_data in (self.channels or {}).items():
if not isinstance(channel_data, dict):
continue
players = channel_data.get('players', {})
if not isinstance(players, dict):
continue
for nick, player in players.items():
yield channel_key, nick, player
def get_player_if_exists(self, nick, channel: str):
"""Return player dict for nick+channel if present; does not create records."""
try:
if not isinstance(nick, str) or not nick.strip():
return None
nick_clean = sanitize_user_input(nick, max_length=50,
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\')
nick_lower = nick_clean.lower().strip()
if not nick_lower:
return None
channel_key = self._normalize_channel(channel)
channel_data = self.channels.get(channel_key)
if not isinstance(channel_data, dict):
return None
players = channel_data.get('players')
if not isinstance(players, dict):
return None
player = players.get(nick_lower)
return player if isinstance(player, dict) else None
except Exception:
return None
def get_player(self, nick, channel: str):
"""Get player data for a specific channel, creating if doesn't exist with comprehensive validation"""
try:
# Validate and sanitize nick
if not isinstance(nick, str) or not nick.strip():
self.logger.warning(f"Invalid nick provided: {nick}")
return self.error_recovery.safe_execute(
lambda: self.create_player('Unknown'),
fallback={'nick': 'Unknown', 'xp': 0, 'ducks_shot': 0},
logger=self.logger
)
# Sanitize nick input
nick_clean = sanitize_user_input(nick, max_length=50,
allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-[]{}^`|\\')
nick_lower = nick_clean.lower().strip()
if not nick_lower:
self.logger.warning(f"Empty nick after sanitization: {nick}")
return self.create_player('Unknown')
players = self.get_players_for_channel(channel)
if nick_lower not in players:
players[nick_lower] = self.create_player(nick_clean)
else:
# Ensure existing players have all required fields
player = players[nick_lower]
if not isinstance(player, dict):
self.logger.warning(f"Invalid player data for {nick_lower}, recreating")
players[nick_lower] = self.create_player(nick_clean)
else:
# Migrate and validate existing player data with error recovery
validated = self.error_recovery.safe_execute(
lambda: self._migrate_and_validate_player(player, nick_clean),
fallback=self.create_player(nick_clean),
logger=self.logger
)
players[nick_lower] = validated
return players[nick_lower]
except Exception as e:
self.logger.error(f"Critical error getting player {nick}: {e}")
return self.create_player(nick if isinstance(nick, str) else 'Unknown')
def _migrate_and_validate_player(self, player, nick):
"""Migrate old player data and validate all fields"""
try:
# Start with sanitized data
validated_player = self._sanitize_player_data(player)
# Migrate from old ammo/chargers system to magazine system if needed
if 'magazines' not in player and ('ammo' in player or 'chargers' in player):
self.logger.info(f"Migrating {nick} from old ammo system to magazine system")
old_ammo = player.get('ammo', 6)
old_chargers = player.get('chargers', 2)
validated_player['current_ammo'] = max(0, min(50, int(old_ammo)))
validated_player['magazines'] = max(1, min(20, int(old_chargers) + 1))
validated_player['bullets_per_magazine'] = 6
# Update nick in case it changed
validated_player['nick'] = str(nick)[:50]
return validated_player
except Exception as e:
self.logger.error(f"Error migrating player data for {nick}: {e}")
return self.create_player(nick)
def create_player(self, nick):
"""Create a new player with all required fields"""
try:
safe_nick = str(nick)[:50] if nick else 'Unknown'
# Get configurable defaults from bot config
if self.bot:
accuracy = self.bot.get_config('player_defaults.accuracy', 75)
magazines = self.bot.get_config('player_defaults.magazines', 3)
bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6)
jam_chance = self.bot.get_config('player_defaults.jam_chance', 15)
xp = self.bot.get_config('player_defaults.xp', 0)
else:
accuracy = 75
magazines = 3
bullets_per_mag = 6
jam_chance = 15
xp = 0
return {
'nick': safe_nick,
'xp': xp,
'ducks_shot': 0,
'ducks_befriended': 0,
'shots_fired': 0,
'shots_missed': 0,
'current_ammo': bullets_per_mag,
'magazines': magazines,
'bullets_per_magazine': bullets_per_mag,
'accuracy': accuracy,
'jam_chance': jam_chance,
'gun_confiscated': False,
'confiscated_ammo': 0,
'confiscated_magazines': 0,
'inventory': {},
'temporary_effects': [],
'last_activity_channel': '',
'last_activity_time': 0.0,
'ignored': False,
# Additional fields to prevent KeyErrors
'best_time': 0.0,
'worst_time': 0.0,
'total_time_hunting': 0.0,
'level': 1,
'xp_gained': 0,
'hp_remaining': 0,
'victim': '',
'xp_lost': 0,
'ammo': bullets_per_mag, # Legacy
'max_ammo': bullets_per_mag, # Legacy
'chargers': magazines - 1 # Legacy
}
except Exception as e:
self.logger.error(f"Error creating player for {nick}: {e}")
return {
'nick': 'Unknown',
'xp': 0,
'ducks_shot': 0,
'ducks_befriended': 0,
'shots_fired': 0,
'shots_missed': 0,
'current_ammo': 6,
'magazines': 3,
'bullets_per_magazine': 6,
'accuracy': 75,
'jam_chance': 15,
'gun_confiscated': False,
'confiscated_ammo': 0,
'confiscated_magazines': 0,
'inventory': {},
'temporary_effects': [],
'last_activity_channel': '',
'last_activity_time': 0.0,
'ignored': False,
'best_time': 0.0,
'worst_time': 0.0,
'total_time_hunting': 0.0,
'level': 1,
'xp_gained': 0,
'hp_remaining': 0,
'victim': '',
'xp_lost': 0,
'ammo': 6,
'max_ammo': 6,
'chargers': 2
}
def get_leaderboard(self, channel: str, category='xp', limit=3):
"""Get top players by specified category for a given channel"""
try:
leaderboard = []
players = self.get_players_for_channel(channel)
for nick, player_data in players.items():
sanitized_data = self._sanitize_player_data(player_data)
if category == 'xp':
value = sanitized_data.get('xp', 0)
elif category == 'ducks_shot':
value = sanitized_data.get('ducks_shot', 0)
else:
continue
leaderboard.append((nick, value))
leaderboard.sort(key=lambda x: x[1], reverse=True)
return leaderboard[:limit]
except Exception as e:
self.logger.error(f"Error getting leaderboard for {category}: {e}")
return []

1874
src/duckhuntbot.py Normal file

File diff suppressed because it is too large Load Diff

262
src/error_handling.py Normal file
View File

@@ -0,0 +1,262 @@
"""
Enhanced error handling utilities for DuckHunt Bot
Includes retry mechanisms, circuit breakers, and graceful degradation
"""
import asyncio
import logging
import time
from functools import wraps
from typing import Callable, Any, Optional, Union
class RetryConfig:
"""Configuration for retry mechanisms"""
def __init__(self, max_attempts: int = 3, base_delay: float = 1.0,
max_delay: float = 60.0, exponential: bool = True):
self.max_attempts = max_attempts
self.base_delay = base_delay
self.max_delay = max_delay
self.exponential = exponential
class CircuitBreaker:
"""Circuit breaker pattern for preventing cascading failures"""
def __init__(self, failure_threshold: int = 5, timeout: float = 60.0):
self.failure_threshold = failure_threshold
self.timeout = timeout
self.failure_count = 0
self.last_failure_time = None
self.state = 'closed' # closed, open, half-open
self.logger = logging.getLogger('DuckHuntBot.CircuitBreaker')
def __call__(self, func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, **kwargs):
if self.state == 'open':
if self.last_failure_time is not None and time.time() - self.last_failure_time > self.timeout:
self.state = 'half-open'
self.logger.info("Circuit breaker moving to half-open state")
else:
raise Exception("Circuit breaker is open - operation blocked")
try:
result = await func(*args, **kwargs)
if self.state == 'half-open':
self.state = 'closed'
self.failure_count = 0
self.logger.info("Circuit breaker closed - service recovered")
return result
except Exception as e:
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = 'open'
self.logger.warning(f"Circuit breaker opened after {self.failure_count} failures")
raise e
return wrapper
def with_retry(config: Optional[RetryConfig] = None,
exceptions: tuple = (Exception,)):
"""Decorator for adding retry logic to functions"""
if config is None:
config = RetryConfig()
def decorator(func: Callable) -> Callable:
@wraps(func)
async def async_wrapper(*args, **kwargs):
logger = logging.getLogger(f'DuckHuntBot.Retry.{func.__name__}')
for attempt in range(config.max_attempts):
try:
return await func(*args, **kwargs)
except exceptions as e:
if attempt == config.max_attempts - 1:
logger.error(f"Function {func.__name__} failed after {config.max_attempts} attempts: {e}")
raise
delay = config.base_delay
if config.exponential:
delay *= (2 ** attempt)
delay = min(delay, config.max_delay)
logger.warning(f"Attempt {attempt + 1}/{config.max_attempts} failed for {func.__name__}: {e}. Retrying in {delay}s")
await asyncio.sleep(delay)
return None
@wraps(func)
def sync_wrapper(*args, **kwargs):
logger = logging.getLogger(f'DuckHuntBot.Retry.{func.__name__}')
for attempt in range(config.max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == config.max_attempts - 1:
logger.error(f"Function {func.__name__} failed after {config.max_attempts} attempts: {e}")
raise
delay = config.base_delay
if config.exponential:
delay *= (2 ** attempt)
delay = min(delay, config.max_delay)
logger.warning(f"Attempt {attempt + 1}/{config.max_attempts} failed for {func.__name__}: {e}. Retrying in {delay}s")
time.sleep(delay)
return None
# Return appropriate wrapper based on whether function is async
if asyncio.iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper
return decorator
class ErrorRecovery:
"""Error recovery and graceful degradation utilities"""
@staticmethod
def safe_execute(func: Callable, fallback: Any = None,
log_errors: bool = True, logger: Optional[logging.Logger] = None) -> Any:
"""Safely execute a function with fallback value on error"""
if logger is None:
logger = logging.getLogger('DuckHuntBot.ErrorRecovery')
try:
return func()
except Exception as e:
if log_errors:
logger.error(f"Error executing {func.__name__}: {e}")
return fallback
@staticmethod
async def safe_execute_async(func: Callable, fallback: Any = None,
log_errors: bool = True, logger: Optional[logging.Logger] = None) -> Any:
"""Safely execute an async function with fallback value on error"""
if logger is None:
logger = logging.getLogger('DuckHuntBot.ErrorRecovery')
try:
return await func()
except Exception as e:
if log_errors:
logger.error(f"Error executing {func.__name__}: {e}")
return fallback
@staticmethod
def validate_input(value: Any, validator: Callable, default: Any = None,
field_name: str = "input") -> Any:
"""Validate input with fallback to default"""
try:
if validator(value):
return value
else:
raise ValueError(f"Validation failed for {field_name}")
except Exception:
return default
class HealthChecker:
"""Health monitoring and alerting"""
def __init__(self, check_interval: float = 30.0):
self.check_interval = check_interval
self.checks = {}
self.logger = logging.getLogger('DuckHuntBot.Health')
def add_check(self, name: str, check_func: Callable, critical: bool = False):
"""Add a health check function"""
self.checks[name] = {
'func': check_func,
'critical': critical,
'last_success': None,
'failure_count': 0
}
async def run_checks(self) -> dict:
"""Run all health checks and return results"""
results = {}
for name, check in self.checks.items():
try:
result = await check['func']() if asyncio.iscoroutinefunction(check['func']) else check['func']()
check['last_success'] = time.time()
check['failure_count'] = 0
results[name] = {'status': 'healthy', 'result': result}
except Exception as e:
check['failure_count'] += 1
results[name] = {
'status': 'unhealthy',
'error': str(e),
'failure_count': check['failure_count']
}
if check['critical'] and check['failure_count'] >= 3:
self.logger.error(f"Critical health check '{name}' failed {check['failure_count']} times: {e}")
return results
def safe_format_message(template: str, **kwargs) -> str:
"""Safely format message templates with error handling"""
try:
return template.format(**kwargs)
except KeyError as e:
logger = logging.getLogger('DuckHuntBot.MessageFormat')
logger.error(f"Missing template variable {e} in message: {template[:100]}...")
# Try to provide safe fallback
safe_kwargs = {}
for key, value in kwargs.items():
try:
safe_kwargs[key] = str(value) if value is not None else ''
except Exception:
safe_kwargs[key] = ''
# Replace missing variables with placeholders
import re
def replace_missing(match):
var_name = match.group(1)
if var_name not in safe_kwargs:
return f"[{var_name}]"
return f"{{{var_name}}}"
safe_template = re.sub(r'\{([^}]+)\}', replace_missing, template)
try:
return safe_template.format(**safe_kwargs)
except Exception:
return f"[Message format error in template: {template[:50]}...]"
except Exception as e:
logger = logging.getLogger('DuckHuntBot.MessageFormat')
logger.error(f"Unexpected error formatting message: {e}")
return f"[Message error: {template[:50]}...]"
def sanitize_user_input(value: str, max_length: int = 100,
allowed_chars: Optional[str] = None) -> str:
"""Sanitize user input to prevent injection and errors"""
if not isinstance(value, str):
value = str(value)
# Limit length
value = value[:max_length]
# Remove/replace dangerous characters
value = value.replace('\r', '').replace('\n', ' ')
# Filter to allowed characters if specified
if allowed_chars:
value = ''.join(c for c in value if c in allowed_chars)
return value.strip()

703
src/game.py Normal file
View File

@@ -0,0 +1,703 @@
"""
Game mechanics for DuckHunt Bot
Handles duck spawning, shooting, befriending, and other game actions
"""
import asyncio
import random
import time
import logging
class DuckGame:
"""Game mechanics for DuckHunt - shooting, befriending, reloading"""
def __init__(self, bot, db):
self.bot = bot
self.db = db
self.ducks = {} # {channel: [duck1, duck2, ...]}
self.logger = logging.getLogger('DuckHuntBot.Game')
self.spawn_task = None
self.timeout_task = None
@staticmethod
def _channel_key(channel: str) -> str:
"""Normalize channel keys for internal dict lookups (IRC channels are case-insensitive)."""
if not isinstance(channel, str):
return ""
channel = channel.strip()
if channel.startswith('#') or channel.startswith('&'):
return channel.lower()
return channel
async def start_game_loops(self):
"""Start the game loops"""
self.spawn_task = asyncio.create_task(self.duck_spawn_loop())
self.timeout_task = asyncio.create_task(self.duck_timeout_loop())
try:
await asyncio.gather(self.spawn_task, self.timeout_task)
except asyncio.CancelledError:
self.logger.info("Game loops cancelled")
async def duck_spawn_loop(self):
"""Duck spawning loop with responsive shutdown"""
try:
while True:
# Wait random time between spawns, but in small chunks for responsiveness
min_wait = self.bot.get_config('duck_spawning.spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawning.spawn_max', 900) # 15 minutes
# Check for active bread effects to modify spawn timing
spawn_multiplier = self._get_active_spawn_multiplier()
if spawn_multiplier > 1.0:
# Reduce wait time when bread is active
min_wait = int(min_wait / spawn_multiplier)
max_wait = int(max_wait / spawn_multiplier)
wait_time = random.randint(min_wait, max_wait)
# Sleep in 1-second intervals to allow for quick cancellation
for _ in range(wait_time):
await asyncio.sleep(1)
# Spawn duck in random channel
channels = list(self.bot.channels_joined)
if channels:
channel = random.choice(channels)
await self.spawn_duck(channel)
except asyncio.CancelledError:
self.logger.info("Duck spawning loop cancelled")
async def duck_timeout_loop(self):
"""Duck timeout loop with responsive shutdown"""
try:
while True:
# Check every 2 seconds instead of 10 for more responsiveness
await asyncio.sleep(2)
current_time = time.time()
channels_to_clear = []
for channel, ducks in self.ducks.items():
ducks_to_remove = []
for duck in ducks:
# Get timeout for each duck type from config
duck_type = duck.get('duck_type', 'normal')
timeout = self.bot.get_config(f'duck_types.{duck_type}.timeout', 60)
if current_time - duck['spawn_time'] > timeout:
ducks_to_remove.append(duck)
for duck in ducks_to_remove:
ducks.remove(duck)
# Use appropriate fly away message based on duck type - revealing the type!
duck_type = duck.get('duck_type', 'normal')
if duck_type == 'golden':
message = self.bot.messages.get('golden_duck_flies_away')
elif duck_type == 'fast':
message = self.bot.messages.get('fast_duck_flies_away')
else:
message = self.bot.messages.get('duck_flies_away')
self.bot.send_message(channel, message)
if not ducks:
channels_to_clear.append(channel)
# Clean up empty channels
for channel in channels_to_clear:
if channel in self.ducks and not self.ducks[channel]:
del self.ducks[channel]
# Clean expired effects every loop iteration
self._clean_expired_effects()
except asyncio.CancelledError:
self.logger.info("Duck timeout loop cancelled")
async def spawn_duck(self, channel):
"""Spawn a duck in the channel"""
channel_key = self._channel_key(channel)
if channel_key not in self.ducks:
self.ducks[channel_key] = []
# Don't spawn if there's already a duck
if self.ducks[channel_key]:
return
# Determine duck type randomly.
# Prefer the newer config structure (duck_types.*) but keep legacy keys for compatibility.
golden_chance = self.bot.get_config(
'duck_types.golden.chance',
self.bot.get_config('golden_duck_chance', 0.15)
)
fast_chance = self.bot.get_config(
'duck_types.fast.chance',
self.bot.get_config('fast_duck_chance', 0.25)
)
rand = random.random()
if rand < golden_chance:
# Golden duck - high HP, high XP
min_hp = self.bot.get_config(
'duck_types.golden.min_hp',
self.bot.get_config('golden_duck_min_hp', 3)
)
max_hp = self.bot.get_config(
'duck_types.golden.max_hp',
self.bot.get_config('golden_duck_max_hp', 5)
)
hp = random.randint(min_hp, max_hp)
duck_type = 'golden'
duck = {
'id': f"golden_duck_{int(time.time())}_{random.randint(1000, 9999)}",
'spawn_time': time.time(),
'channel': channel,
'duck_type': duck_type,
'max_hp': hp,
'current_hp': hp
}
self.logger.info(f"Golden duck (hidden) spawned in {channel_key} with {hp} HP")
elif rand < golden_chance + fast_chance:
# Fast duck - normal HP, flies away faster
duck_type = 'fast'
duck = {
'id': f"fast_duck_{int(time.time())}_{random.randint(1000, 9999)}",
'spawn_time': time.time(),
'channel': channel,
'duck_type': duck_type,
'max_hp': 1,
'current_hp': 1
}
self.logger.info(f"Fast duck (hidden) spawned in {channel_key}")
else:
# Normal duck
duck_type = 'normal'
duck = {
'id': f"duck_{int(time.time())}_{random.randint(1000, 9999)}",
'spawn_time': time.time(),
'channel': channel,
'duck_type': duck_type,
'max_hp': 1,
'current_hp': 1
}
self.logger.info(f"Normal duck spawned in {channel_key}")
# All duck types use the same spawn message - type is hidden!
message = self.bot.messages.get('duck_spawn')
self.ducks[channel_key].append(duck)
self.bot.send_message(channel, message)
def shoot_duck(self, nick, channel, player):
"""Handle shooting at a duck"""
channel_key = self._channel_key(channel)
# Check if gun is confiscated
if player.get('gun_confiscated', False):
return {
'success': False,
'message_key': 'bang_not_armed',
'message_args': {'nick': nick}
}
# Check if clothes are wet
if self._is_player_wet(player):
return {
'success': False,
'message_key': 'bang_wet_clothes',
'message_args': {'nick': nick}
}
# Check ammo
if player.get('current_ammo', 0) <= 0:
return {
'success': False,
'message_key': 'bang_no_ammo',
'message_args': {'nick': nick}
}
# Check for gun jamming using level-based jam chance
jam_chance = self.bot.levels.get_jam_chance(player) / 100.0 # Convert percentage to decimal
if random.random() < jam_chance:
# Gun jammed! Use ammo but don't shoot
player['current_ammo'] = player.get('current_ammo', 1) - 1
self.db.save_database()
return {
'success': False,
'message_key': 'bang_gun_jammed',
'message_args': {'nick': nick}
}
# Check for duck
if channel_key not in self.ducks or not self.ducks[channel_key]:
# Wild shot - gun confiscated for unsafe shooting
player['shots_fired'] = player.get('shots_fired', 0) + 1 # Track wild shots too
player['shots_missed'] = player.get('shots_missed', 0) + 1 # Wild shots count as misses
# Use ammo for the shot, then store remaining ammo before confiscation
remaining_ammo = player.get('current_ammo', 1) - 1
player['confiscated_ammo'] = remaining_ammo
player['confiscated_magazines'] = player.get('magazines', 0)
player['current_ammo'] = 0 # No ammo while confiscated
player['gun_confiscated'] = True
self.db.save_database()
return {
'success': False,
'message_key': 'bang_no_duck',
'message_args': {'nick': nick}
}
# Shoot at duck
player['current_ammo'] = player.get('current_ammo', 1) - 1
player['shots_fired'] = player.get('shots_fired', 0) + 1 # Track total shots fired
# Calculate hit chance using level-modified accuracy
modified_accuracy = self.bot.levels.get_modified_accuracy(player)
hit_chance = modified_accuracy / 100.0
# Apply clover luck effect (temporary boost to minimum hit chance)
clover = self._get_active_effect(player, 'clover_luck')
if clover:
try:
min_hit = float(clover.get('min_hit_chance', 0.0) or 0.0)
except (ValueError, TypeError):
min_hit = 0.0
hit_chance = max(hit_chance, max(0.0, min(min_hit, 1.0)))
if random.random() < hit_chance:
# Hit! Get the duck and reveal its type
duck = self.ducks[channel_key][0]
duck_type = duck.get('duck_type', 'normal')
if duck_type == 'golden':
# Golden duck - multi-hit with high XP
duck['current_hp'] -= 1
xp_gained = self.bot.get_config('golden_duck_xp', 15)
if duck['current_hp'] > 0:
# Still alive, reveal it's golden but don't remove
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
max_accuracy = self.bot.get_config('max_accuracy', 100)
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
self.db.save_database()
return {
'success': True,
'hit': True,
'message_key': 'bang_hit_golden',
'message_args': {
'nick': nick,
'hp_remaining': duck['current_hp'],
'xp_gained': xp_gained
}
}
else:
# Golden duck killed!
self.ducks[channel_key].pop(0)
xp_gained = xp_gained * duck['max_hp'] # Bonus XP for killing
message_key = 'bang_hit_golden_killed'
elif duck_type == 'fast':
# Fast duck - normal HP but higher XP
self.ducks[channel_key].pop(0)
xp_gained = self.bot.get_config('fast_duck_xp', 12)
message_key = 'bang_hit_fast'
else:
# Normal duck
self.ducks[channel_key].pop(0)
xp_gained = self.bot.get_config('normal_duck_xp', 10)
message_key = 'bang_hit'
# Apply XP and level changes
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
max_accuracy = self.bot.get_config('max_accuracy', 100)
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
# Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player)
if new_level != old_level:
self.bot.levels.update_player_magazines(player)
# If config option enabled, rearm all disarmed players when duck is shot
if self.bot.get_config('duck_spawning.rearm_on_duck_shot', False):
self._rearm_all_disarmed_players(channel)
# Check for item drops
dropped_item = self._check_item_drop(player, duck_type)
self.db.save_database()
# Include drop info in the return
result = {
'success': True,
'hit': True,
'message_key': message_key,
'message_args': {
'nick': nick,
'xp_gained': xp_gained,
'ducks_shot': player['ducks_shot']
}
}
# Add drop info if an item was dropped
if dropped_item:
result['dropped_item'] = dropped_item
return result
else:
# Miss! Duck stays in the channel
player['shots_missed'] = player.get('shots_missed', 0) + 1 # Track missed shots
# Lose 1 XP for missing
player['xp'] = max(0, player.get('xp', 0) - 1)
accuracy_loss = self.bot.get_config('gameplay.accuracy_loss_on_miss', 2)
min_accuracy = self.bot.get_config('gameplay.min_accuracy', 10)
player['accuracy'] = max(player.get('accuracy', self.bot.get_config('player_defaults.accuracy', 75)) - accuracy_loss, min_accuracy)
# Check for friendly fire (chance to hit another hunter)
friendly_fire_chance = 0.15 # 15% chance of hitting another hunter on miss
if random.random() < friendly_fire_chance:
# Get other armed players in the same channel
armed_players = []
for other_nick, other_player in self.db.get_players_for_channel(channel).items():
if (str(other_nick).lower() != nick.lower() and
not other_player.get('gun_confiscated', False) and
other_player.get('current_ammo', 0) > 0):
armed_players.append((other_nick, other_player))
if armed_players:
# Hit a random armed hunter
victim_nick, victim_player = random.choice(armed_players)
# Check if shooter has insurance protection
has_insurance = self._check_insurance_protection(player, 'friendly_fire')
if has_insurance:
# Protected by insurance - no penalties
self.db.save_database()
return {
'success': True,
'hit': False,
'friendly_fire': True,
'victim': victim_nick,
'message_key': 'bang_friendly_fire_insured',
'message_args': {
'nick': nick,
'victim': victim_nick
}
}
else:
# Apply friendly fire penalties - gun confiscated for unsafe shooting
xp_loss = min(player.get('xp', 0) // 4, 25) # Lose 25% XP or max 25 XP
player['xp'] = max(0, player.get('xp', 0) - xp_loss)
# Store current ammo state before confiscation (no shot fired yet in friendly fire)
player['confiscated_ammo'] = player.get('current_ammo', 0)
player['confiscated_magazines'] = player.get('magazines', 0)
player['current_ammo'] = 0 # No ammo while confiscated
player['gun_confiscated'] = True
self.db.save_database()
return {
'success': True,
'hit': False,
'friendly_fire': True,
'victim': victim_nick,
'message_key': 'bang_friendly_fire_penalty',
'message_args': {
'nick': nick,
'victim': victim_nick,
'xp_lost': xp_loss
}
}
self.db.save_database()
return {
'success': True,
'hit': False,
'message_key': 'bang_miss',
'message_args': {'nick': nick}
}
def befriend_duck(self, nick, channel, player):
"""Handle befriending a duck"""
channel_key = self._channel_key(channel)
# Check for duck
if channel_key not in self.ducks or not self.ducks[channel_key]:
return {
'success': False,
'message_key': 'bef_no_duck',
'message_args': {'nick': nick}
}
# Check befriend success rate from config and level modifiers
base_rate = self.bot.get_config('gameplay.befriend_success_rate', 75)
try:
if base_rate is not None:
base_rate = float(base_rate)
else:
base_rate = 75.0
except (ValueError, TypeError):
base_rate = 75.0
# Apply level-based modification to befriend rate
level_modified_rate = self.bot.levels.get_modified_befriend_rate(player, base_rate)
success_rate = level_modified_rate / 100.0
# Apply clover luck effect (temporary boost to minimum befriend chance)
clover = self._get_active_effect(player, 'clover_luck')
if clover:
try:
min_bef = float(clover.get('min_befriend_chance', 0.0) or 0.0)
except (ValueError, TypeError):
min_bef = 0.0
success_rate = max(success_rate, max(0.0, min(min_bef, 1.0)))
if random.random() < success_rate:
# Success - befriend the duck
duck = self.ducks[channel_key].pop(0)
# Lower XP gain than shooting
xp_gained = self.bot.get_config('gameplay.befriend_xp', 5)
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
# Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player)
if new_level != old_level:
self.bot.levels.update_player_magazines(player)
# If config option enabled, rearm all disarmed players when duck is befriended
if self.bot.get_config('rearm_on_duck_shot', False):
self._rearm_all_disarmed_players(channel)
self.db.save_database()
return {
'success': True,
'befriended': True,
'message_key': 'bef_success',
'message_args': {
'nick': nick,
'xp_gained': xp_gained,
'ducks_befriended': player['ducks_befriended']
}
}
else:
# Failure - duck flies away, remove from channel
duck = self.ducks[channel_key].pop(0)
self.db.save_database()
return {
'success': True,
'befriended': False,
'message_key': 'bef_failed',
'message_args': {'nick': nick}
}
def reload_gun(self, nick, channel, player):
"""Handle reloading a gun (switching to a new magazine)"""
if player.get('gun_confiscated', False):
return {
'success': False,
'message_key': 'reload_not_armed',
'message_args': {'nick': nick}
}
current_ammo = player.get('current_ammo', 0)
bullets_per_mag = player.get('bullets_per_magazine', 6)
# Check if current magazine is already full
if current_ammo >= bullets_per_mag:
return {
'success': False,
'message_key': 'reload_already_loaded',
'message_args': {'nick': nick}
}
# Check if they have spare magazines
total_magazines = player.get('magazines', 1)
if total_magazines <= 1: # Only the current magazine
return {
'success': False,
'message_key': 'reload_no_chargers',
'message_args': {'nick': nick}
}
# Reload: discard current magazine and load a new full one
player['current_ammo'] = bullets_per_mag
player['magazines'] = total_magazines - 1
self.db.save_database()
return {
'success': True,
'message_key': 'reload_success',
'message_args': {
'nick': nick,
'ammo': player['current_ammo'],
'max_ammo': bullets_per_mag,
'chargers': player['magazines'] - 1 # Spare magazines (excluding current)
}
}
def _rearm_all_disarmed_players(self, channel):
"""Rearm all players who have been disarmed (gun confiscated) in the given channel"""
try:
rearmed_count = 0
for _player_name, player_data in self.db.get_players_for_channel(channel).items():
if player_data.get('gun_confiscated', False):
player_data['gun_confiscated'] = False
# Update magazines based on player level
self.bot.levels.update_player_magazines(player_data)
player_data['current_ammo'] = player_data.get('bullets_per_magazine', 6)
rearmed_count += 1
if rearmed_count > 0:
self.logger.info(f"Auto-rearmed {rearmed_count} disarmed players after duck shot")
except Exception as e:
self.logger.error(f"Error in _rearm_all_disarmed_players: {e}")
def _get_active_spawn_multiplier(self):
"""Get the current spawn rate multiplier from active bread effects"""
import time
max_multiplier = 1.0
current_time = time.time()
try:
for _ch, _player_name, player_data in self.db.iter_all_players():
effects = player_data.get('temporary_effects', [])
for effect in effects:
if (effect.get('type') == 'attract_ducks' and
effect.get('expires_at', 0) > current_time):
multiplier = effect.get('spawn_multiplier', 1.0)
max_multiplier = max(max_multiplier, multiplier)
return max_multiplier
except Exception as e:
self.logger.error(f"Error getting spawn multiplier: {e}")
return 1.0
def _is_player_wet(self, player):
"""Check if player has wet clothes that prevent shooting"""
import time
current_time = time.time()
effects = player.get('temporary_effects', [])
for effect in effects:
if (effect.get('type') == 'wet_clothes' and
effect.get('expires_at', 0) > current_time):
return True
return False
def _check_insurance_protection(self, player, protection_type):
"""Check if player has active insurance protection"""
import time
current_time = time.time()
try:
effects = player.get('temporary_effects', [])
for effect in effects:
if (effect.get('type') == 'insurance' and
effect.get('protection') == protection_type and
effect.get('expires_at', 0) > current_time):
return True
return False
except Exception as e:
self.logger.error(f"Error checking insurance protection: {e}")
return False
def _clean_expired_effects(self):
"""Remove expired temporary effects from all players"""
import time
current_time = time.time()
try:
for _ch, player_name, player_data in self.db.iter_all_players():
effects = player_data.get('temporary_effects', [])
active_effects = []
for effect in effects:
if effect.get('expires_at', 0) > current_time:
active_effects.append(effect)
if len(active_effects) != len(effects):
player_data['temporary_effects'] = active_effects
self.logger.debug(f"Cleaned expired effects for {player_name}")
except Exception as e:
self.logger.error(f"Error cleaning expired effects: {e}")
def _get_active_effect(self, player, effect_type: str):
"""Return the first active temporary effect dict matching type, or None."""
try:
current_time = time.time()
effects = player.get('temporary_effects', [])
if not isinstance(effects, list):
return None
for effect in effects:
if (isinstance(effect, dict) and effect.get('type') == effect_type and
effect.get('expires_at', 0) > current_time):
return effect
return None
except Exception:
return None
def _check_item_drop(self, player, duck_type):
"""
Check if the duck drops an item and add it to player's inventory
Returns the dropped item info or None
"""
import random
try:
# Get drop chance for this duck type
drop_chance = self.bot.get_config(f'duck_types.{duck_type}.drop_chance', 0.0)
# Roll for drop
if random.random() > drop_chance:
return None # No drop
# Get drop table for this duck type
drop_table_key = f'{duck_type}_duck_drops'
drop_table = self.bot.get_config(f'item_drops.{drop_table_key}', [])
if not drop_table:
self.logger.warning(f"No drop table found for {duck_type} duck")
return None
# Weighted random selection
total_weight = sum(item.get('weight', 1) for item in drop_table)
if total_weight <= 0:
return None
random_weight = random.randint(1, total_weight)
current_weight = 0
for drop_item in drop_table:
current_weight += drop_item.get('weight', 1)
if random_weight <= current_weight:
item_id = drop_item.get('item_id')
if item_id:
# Add item to player's inventory
inventory = player.get('inventory', {})
item_key = str(item_id)
inventory[item_key] = inventory.get(item_key, 0) + 1
player['inventory'] = inventory
# Get item info from shop
item_info = self.bot.shop.get_item(item_id)
item_name = item_info.get('name', f'Item {item_id}') if item_info else f'Item {item_id}'
self.logger.info(f"Duck dropped {item_name} for player {player.get('nick', 'Unknown')}")
return {
'item_id': item_id,
'item_name': item_name,
'duck_type': duck_type
}
break
return None
except Exception as e:
self.logger.error(f"Error in _check_item_drop: {e}")
return None

231
src/levels.py Normal file
View File

@@ -0,0 +1,231 @@
"""
Level system for DuckHunt Bot
Manages player levels and difficulty scaling
"""
import json
import os
import logging
from typing import Dict, Any, Optional, Tuple
class LevelManager:
"""Manages the DuckHunt level system and difficulty scaling"""
def __init__(self, levels_file: str = "levels.json"):
self.levels_file = levels_file
self.levels_data = {}
self.logger = logging.getLogger('DuckHuntBot.Levels')
self.load_levels()
def load_levels(self):
"""Load level definitions from JSON file"""
try:
if os.path.exists(self.levels_file):
with open(self.levels_file, 'r', encoding='utf-8') as f:
self.levels_data = json.load(f)
level_count = len(self.levels_data.get('levels', {}))
self.logger.info(f"Loaded {level_count} levels from {self.levels_file}")
else:
# Fallback levels if file doesn't exist
self.levels_data = self._get_default_levels()
self.logger.warning(f"{self.levels_file} not found, using default levels")
except Exception as e:
self.logger.error(f"Error loading levels: {e}, using defaults")
self.levels_data = self._get_default_levels()
def _get_default_levels(self) -> Dict[str, Any]:
"""Default fallback level system"""
return {
"level_calculation": {
"method": "xp",
"description": "Level based on XP earned"
},
"levels": {
"1": {
"name": "Duck Novice",
"min_xp": 0,
"max_xp": 49,
"befriend_success_rate": 85,
"accuracy_modifier": 5,
"duck_spawn_speed_modifier": 1.0,
"description": "Just starting out"
},
"2": {
"name": "Duck Hunter",
"min_xp": 50,
"max_xp": 299,
"befriend_success_rate": 75,
"accuracy_modifier": 0,
"duck_spawn_speed_modifier": 0.8,
"description": "Getting experienced"
}
}
}
def calculate_player_level(self, player: Dict[str, Any]) -> int:
"""Calculate a player's current level based on their stats"""
method = self.levels_data.get('level_calculation', {}).get('method', 'xp')
if method == 'xp':
player_xp = player.get('xp', 0)
elif method == 'total_ducks':
# Fallback to duck-based calculation if specified
total_ducks = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
player_xp = total_ducks # Use duck count as if it were XP
else:
player_xp = player.get('xp', 0)
# Find the appropriate level
levels = self.levels_data.get('levels', {})
for level_num in sorted(levels.keys(), key=int, reverse=True):
level_data = levels[level_num]
# Check for XP-based thresholds first, fallback to duck-based
min_threshold = level_data.get('min_xp', level_data.get('min_ducks', 0))
if player_xp >= min_threshold:
return int(level_num)
return 1 # Default to level 1
def get_level_data(self, level: int) -> Optional[Dict[str, Any]]:
"""Get level data for a specific level"""
return self.levels_data.get('levels', {}).get(str(level))
def get_player_level_info(self, player: Dict[str, Any]) -> Dict[str, Any]:
"""Get complete level information for a player"""
level = self.calculate_player_level(player)
level_data = self.get_level_data(level)
if not level_data:
return {
"level": 1,
"name": "Duck Novice",
"description": "Default level",
"befriend_success_rate": 75,
"accuracy_modifier": 0,
"duck_spawn_speed_modifier": 1.0
}
method = self.levels_data.get('level_calculation', {}).get('method', 'xp')
if method == 'xp':
current_value = player.get('xp', 0)
value_type = "xp"
else:
current_value = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
value_type = "ducks"
# Calculate progress to next level
next_level_data = self.get_level_data(level + 1)
if next_level_data:
threshold_key = f'min_{value_type}' if value_type == 'xp' else 'min_ducks'
next_threshold = next_level_data.get(threshold_key, 0)
needed_for_next = next_threshold - current_value
next_level_name = next_level_data.get('name', f"Level {level + 1}")
else:
needed_for_next = 0
next_level_name = "Max Level"
return {
"level": level,
"name": level_data.get('name', f"Level {level}"),
"description": level_data.get('description', ''),
"befriend_success_rate": level_data.get('befriend_success_rate', 75),
"accuracy_modifier": level_data.get('accuracy_modifier', 0),
"duck_spawn_speed_modifier": level_data.get('duck_spawn_speed_modifier', 1.0),
"current_xp": player.get('xp', 0),
"total_ducks": player.get('ducks_shot', 0) + player.get('ducks_befriended', 0),
"needed_for_next": max(0, needed_for_next),
"next_level_name": next_level_name,
"value_type": value_type
}
def get_modified_accuracy(self, player: Dict[str, Any]) -> int:
"""Get player's accuracy modified by their level"""
base_accuracy = player.get('accuracy', 75) # This will be updated by bot config in create_player
level_info = self.get_player_level_info(player)
modifier = level_info.get('accuracy_modifier', 0)
# Apply modifier and clamp between 10-100
modified_accuracy = base_accuracy + modifier
return max(10, min(100, modified_accuracy))
def get_modified_befriend_rate(self, player: Dict[str, Any], base_rate: float = 75.0) -> float:
"""Get player's befriend success rate modified by their level"""
level_info = self.get_player_level_info(player)
level_rate = level_info.get('befriend_success_rate', base_rate)
# Return as percentage (0-100) - these will be configurable later if bot reference is available
return max(5.0, min(95.0, level_rate))
def get_jam_chance(self, player: Dict[str, Any]) -> float:
"""Get player's gun jam chance based on their level"""
level_info = self.get_player_level_info(player)
level_data = self.get_level_data(level_info['level'])
if level_data and 'jam_chance' in level_data:
return level_data['jam_chance']
# Fallback to old system if no level-specific jam chance
return player.get('jam_chance', 5)
def get_duck_spawn_modifier(self, player_levels: list) -> float:
"""Get duck spawn speed modifier based on highest level player in channel"""
if not player_levels:
return 1.0
# Use the modifier from the highest level player (makes it harder for everyone)
max_level = max(player_levels)
level_data = self.get_level_data(max_level)
if level_data:
return level_data.get('duck_spawn_speed_modifier', 1.0)
return 1.0
def reload_levels(self) -> int:
"""Reload levels from file and return count"""
old_count = len(self.levels_data.get('levels', {}))
self.load_levels()
new_count = len(self.levels_data.get('levels', {}))
self.logger.info(f"Levels reloaded: {old_count} -> {new_count} levels")
return new_count
def update_player_magazines(self, player: Dict[str, Any]) -> Dict[str, Any]:
"""Update player's magazine count based on their current level"""
level_info = self.get_player_level_info(player)
level_magazines = level_info.get('magazines', 3)
level_bullets_per_mag = level_info.get('bullets_per_magazine', 6)
# Get current magazine status
current_magazines = player.get('magazines', 3)
current_ammo = player.get('current_ammo', 6)
current_bullets_per_mag = player.get('bullets_per_magazine', 6)
# Calculate total bullets they currently have
total_current_bullets = current_ammo + (current_magazines - 1) * current_bullets_per_mag
# Update magazine system to level requirements
player['magazines'] = level_magazines
player['bullets_per_magazine'] = level_bullets_per_mag
# Redistribute bullets across new magazine system
max_total_bullets = level_magazines * level_bullets_per_mag
new_total_bullets = min(total_current_bullets, max_total_bullets)
# Calculate how to distribute bullets
if new_total_bullets <= 0:
player['current_ammo'] = 0
elif new_total_bullets <= level_bullets_per_mag:
# All bullets fit in current magazine
player['current_ammo'] = new_total_bullets
else:
# Fill current magazine, save rest for other magazines
player['current_ammo'] = level_bullets_per_mag
return {
'old_magazines': current_magazines,
'new_magazines': level_magazines,
'old_total_bullets': total_current_bullets,
'new_total_bullets': new_total_bullets,
'current_ammo': player['current_ammo']
}

375
src/logging_utils.py Normal file
View File

@@ -0,0 +1,375 @@
"""
Enhanced logging utilities for DuckHunt Bot
Features: Colors, emojis, file rotation, structured formatting, configurable debug levels
"""
import json
import logging
import logging.handlers
import os
import sys
from datetime import datetime
def load_config():
"""Load configuration from config.json"""
try:
with open('config.json', 'r') as f:
config = json.load(f)
return config
except Exception as e:
print(f"Warning: Could not load config.json: {e}")
return {
"debug": {
"enabled": True,
"log_level": "DEBUG",
"console_log_level": "INFO",
"file_log_level": "DEBUG",
"log_everything": True,
"log_performance": True,
"unified_format": True
}
}
class EnhancedColourFormatter(logging.Formatter):
"""Enhanced colour formatter for different log levels"""
# ANSI color codes
COLORS = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
'RESET': '\033[0m', # Reset
'BOLD': '\033[1m', # Bold
'DIM': '\033[2m', # Dim
'UNDERLINE': '\033[4m', # Underline
}
# Log level prefixes
EMOJIS = {
'DEBUG': 'DEBUG',
'INFO': 'INFO',
'WARNING': 'WARNING',
'ERROR': 'ERROR',
'CRITICAL': 'CRITICAL',
}
# Component colors
COMPONENT_COLORS = {
'DuckHuntBot': '\033[94m', # Light blue
'DuckHuntBot.IRC': '\033[96m', # Light cyan
'DuckHuntBot.Game': '\033[92m', # Light green
'DuckHuntBot.Shop': '\033[93m', # Light yellow
'DuckHuntBot.DB': '\033[95m', # Light magenta
'SASL': '\033[97m', # White
}
def format(self, record):
# Get colors
level_color = self.COLORS.get(record.levelname, '')
component_color = self.COMPONENT_COLORS.get(record.name, '\033[37m') # Default gray
reset = self.COLORS['RESET']
bold = self.COLORS['BOLD']
dim = self.COLORS['DIM']
# Get level prefix
emoji = self.EMOJIS.get(record.levelname, 'LOG')
# Format timestamp
timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3]
# Format level with padding
level = f"{record.levelname:<8}"
# Format component name with truncation
component = record.name
if len(component) > 20:
component = component[:17] + "..."
# Build the formatted message
formatted_msg = (
f"{dim}{timestamp}{reset} "
f"{emoji} "
f"{level_color}{bold}{level}{reset} "
f"{component_color}{component:<20}{reset} "
f"{record.getMessage()}"
)
# Add function/line info for DEBUG level
if record.levelno == logging.DEBUG:
func_info = f"{dim}[{record.funcName}:{record.lineno}]{reset}"
formatted_msg += f" {func_info}"
return formatted_msg
class EnhancedFileFormatter(logging.Formatter):
"""Enhanced file formatter matching console format (no colors)"""
# Log level prefixes (same as console)
EMOJIS = {
'DEBUG': 'DEBUG',
'INFO': 'INFO',
'WARNING': 'WARNING',
'ERROR': 'ERROR',
'CRITICAL': 'CRITICAL',
}
def format(self, record):
# Get level prefix (same as console)
emoji = self.EMOJIS.get(record.levelname, 'LOG')
# Format timestamp (same as console - just time, not date)
timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3]
# Format level with padding (same as console)
level = f"{record.levelname:<8}"
# Format component name with truncation (same as console)
component = record.name
if len(component) > 20:
component = component[:17] + "..."
# Build the formatted message (same style as console)
formatted_msg = (
f"{timestamp} "
f"{emoji} "
f"{level} "
f"{component:<20} "
f"{record.getMessage()}"
)
# Add function/line info for DEBUG level (same as console)
if record.levelno == logging.DEBUG:
func_info = f"[{record.funcName}:{record.lineno}]"
formatted_msg += f" {func_info}"
# Add exception info if present
if record.exc_info:
formatted_msg += f"\n{self.formatException(record.exc_info)}"
return formatted_msg
class UnifiedFormatter(logging.Formatter):
"""Unified formatter that works for both console and file output"""
# ANSI color codes (only used when use_colors=True)
COLORS = {
'DEBUG': '\033[36m', # Cyan
'INFO': '\033[32m', # Green
'WARNING': '\033[33m', # Yellow
'ERROR': '\033[31m', # Red
'CRITICAL': '\033[35m', # Magenta
'RESET': '\033[0m', # Reset
'BOLD': '\033[1m', # Bold
'DIM': '\033[2m', # Dim
}
# Log level prefixes
EMOJIS = {
'DEBUG': 'DEBUG',
'INFO': 'INFO',
'WARNING': 'WARNING',
'ERROR': 'ERROR',
'CRITICAL': 'CRITICAL',
}
# Component colors
COMPONENT_COLORS = {
'DuckHuntBot': '\033[94m', # Light blue
'DuckHuntBot.IRC': '\033[96m', # Light cyan
'DuckHuntBot.Game': '\033[92m', # Light green
'DuckHuntBot.Shop': '\033[93m', # Light yellow
'DuckHuntBot.DB': '\033[95m', # Light magenta
'SASL': '\033[97m', # White
}
def __init__(self, use_colors=False):
super().__init__()
self.use_colors = use_colors
def format(self, record):
# Get level prefix
emoji = self.EMOJIS.get(record.levelname, 'LOG')
# Format timestamp (same for both)
timestamp = datetime.fromtimestamp(record.created).strftime('%H:%M:%S.%f')[:-3]
# Format level with padding
level = f"{record.levelname:<8}"
# Format component name with truncation
component = record.name
if len(component) > 20:
component = component[:17] + "..."
if self.use_colors:
# Console version with colors
level_color = self.COLORS.get(record.levelname, '')
component_color = self.COMPONENT_COLORS.get(record.name, '\033[37m')
reset = self.COLORS['RESET']
bold = self.COLORS['BOLD']
dim = self.COLORS['DIM']
formatted_msg = (
f"{dim}{timestamp}{reset} "
f"{emoji} "
f"{level_color}{bold}{level}{reset} "
f"{component_color}{component:<20}{reset} "
f"{record.getMessage()}"
)
# Add function/line info for DEBUG level
if record.levelno == logging.DEBUG:
func_info = f"{dim}[{record.funcName}:{record.lineno}]{reset}"
formatted_msg += f" {func_info}"
else:
# File version without colors
formatted_msg = (
f"{timestamp} "
f"{emoji} "
f"{level} "
f"{component:<20} "
f"{record.getMessage()}"
)
# Add function/line info for DEBUG level
if record.levelno == logging.DEBUG:
func_info = f"[{record.funcName}:{record.lineno}]"
formatted_msg += f" {func_info}"
# Add exception info if present
if record.exc_info:
formatted_msg += f"\n{self.formatException(record.exc_info)}"
return formatted_msg
class PerformanceFileFormatter(logging.Formatter):
"""Separate formatter for performance/metrics logging"""
def format(self, record):
timestamp = datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S')
# Extract performance metrics if available
metrics = []
for attr in ['duration', 'memory_usage', 'cpu_usage', 'users_count', 'channels_count']:
if hasattr(record, attr):
metrics.append(f"{attr}={getattr(record, attr)}")
metrics_str = f" METRICS[{', '.join(metrics)}]" if metrics else ""
return f"{timestamp} PERF | {record.getMessage()}{metrics_str}"
def setup_logger(name="DuckHuntBot", console_level=None, file_level=None):
"""Setup enhanced logger with multiple handlers and beautiful formatting"""
# Load configuration
config = load_config()
debug_config = config.get("debug", {})
# Determine if debug is enabled
debug_enabled = debug_config.get("enabled", True)
log_everything = debug_config.get("log_everything", True) if debug_enabled else False
unified_format = debug_config.get("unified_format", True)
# Set logging levels based on config
if console_level is None:
if debug_enabled and log_everything:
console_level = getattr(logging, debug_config.get("console_log_level", "DEBUG"), logging.DEBUG)
else:
console_level = logging.WARNING # Minimal logging
if file_level is None:
if debug_enabled and log_everything:
file_level = getattr(logging, debug_config.get("file_log_level", "DEBUG"), logging.DEBUG)
else:
file_level = logging.ERROR # Only errors
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG if debug_enabled else logging.WARNING)
# Clear existing handlers to avoid duplicates
logger.handlers.clear()
# === CONSOLE HANDLER ===
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(console_level)
# Use unified format if configured, otherwise use colorful console format
if unified_format:
console_formatter = UnifiedFormatter(use_colors=True)
else:
console_formatter = EnhancedColourFormatter()
console_handler.setFormatter(console_formatter)
logger.addHandler(console_handler)
# Create logs directory if it doesn't exist
logs_dir = "logs"
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
try:
# === MAIN LOG FILE (Rotating) ===
main_log_handler = logging.handlers.RotatingFileHandler(
os.path.join(logs_dir, 'duckhunt.log'),
maxBytes=20*1024*1024, # 20MB
backupCount=10,
encoding='utf-8'
)
main_log_handler.setLevel(file_level)
if unified_format:
main_log_formatter = UnifiedFormatter(use_colors=False)
else:
main_log_formatter = EnhancedFileFormatter()
main_log_handler.setFormatter(main_log_formatter)
logger.addHandler(main_log_handler)
# Log initialization success with config info
logger.info("Unified logging system initialized: all logs in duckhunt.log")
logger.info(f"Debug mode: {'ON' if debug_enabled else 'OFF'}")
logger.info(f"Log everything: {'YES' if log_everything else 'NO'}")
logger.info(f"Unified format: {'YES' if unified_format else 'NO'}")
logger.info(f"Console level: {logging.getLevelName(console_level)}")
logger.info(f"File level: {logging.getLevelName(file_level)}")
logger.info(f"Main log: {main_log_handler.baseFilename}")
except Exception as e:
# Fallback to simple file logging
try:
simple_handler = logging.FileHandler('duckhunt_fallback.log', encoding='utf-8')
simple_handler.setLevel(logging.DEBUG)
simple_formatter = logging.Formatter(
'%(asctime)s [%(levelname)-8s] %(name)s: %(message)s'
)
simple_handler.setFormatter(simple_formatter)
logger.addHandler(simple_handler)
logger.error(f"❌ Failed to setup enhanced file logging: {e}")
logger.info("Using fallback file logging")
except Exception as fallback_error:
logger.error(f"💥 Complete logging setup failure: {fallback_error}")
return logger
def get_performance_logger():
"""Get a specialized logger for performance metrics"""
return setup_logger("DuckHuntBot.Performance", console_level=logging.WARNING)
def log_with_context(logger, level, message, **context):
"""Log a message with additional context information"""
record = logger.makeRecord(
logger.name, level, '', 0, message, (), None
)
# Add context attributes to the record
for key, value in context.items():
setattr(record, key, value)
logger.handle(record)

View File

@@ -14,10 +14,10 @@ class SASLHandler:
def __init__(self, bot, config): def __init__(self, bot, config):
self.bot = bot self.bot = bot
self.logger = setup_logger("SASL") self.logger = setup_logger("SASL")
sasl_config = config.get("sasl", {}) # Use bot's get_config method for nested config access
self.enabled = sasl_config.get("enabled", False) self.enabled = bot.get_config("sasl.enabled", False)
self.username = sasl_config.get("username", config.get("nick", "")) self.username = bot.get_config("sasl.username", bot.get_config("connection.nick", ""))
self.password = sasl_config.get("password", "") self.password = bot.get_config("sasl.password", "")
self.authenticated = False self.authenticated = False
self.cap_negotiating = False self.cap_negotiating = False
@@ -55,7 +55,6 @@ class SASLHandler:
subcommand = params[1] subcommand = params[1]
if subcommand == "LS": if subcommand == "LS":
# Server listing capabilities
caps = trailing.split() if trailing else [] caps = trailing.split() if trailing else []
self.logger.info(f"Server capabilities: {caps}") self.logger.info(f"Server capabilities: {caps}")
if "sasl" in caps: if "sasl" in caps:
@@ -69,7 +68,6 @@ class SASLHandler:
return False return False
elif subcommand == "ACK": elif subcommand == "ACK":
# Server acknowledged capability request
caps = trailing.split() if trailing else [] caps = trailing.split() if trailing else []
self.logger.info("SASL capability acknowledged") self.logger.info("SASL capability acknowledged")
if "sasl" in caps: if "sasl" in caps:
@@ -82,7 +80,6 @@ class SASLHandler:
return False return False
elif subcommand == "NAK": elif subcommand == "NAK":
# Server rejected capability request
self.logger.warning("SASL capability rejected") self.logger.warning("SASL capability rejected")
self.bot.send_raw("CAP END") self.bot.send_raw("CAP END")
await self.bot.register_user() await self.bot.register_user()
@@ -96,7 +93,6 @@ class SASLHandler:
""" """
self.logger.info("Sending AUTHENTICATE PLAIN") self.logger.info("Sending AUTHENTICATE PLAIN")
self.bot.send_raw('AUTHENTICATE PLAIN') self.bot.send_raw('AUTHENTICATE PLAIN')
# Small delay to ensure proper sequencing
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
async def handle_authenticate_response(self, params): async def handle_authenticate_response(self, params):
@@ -106,7 +102,6 @@ class SASLHandler:
if params and params[0] == '+': if params and params[0] == '+':
self.logger.info("Server ready for SASL authentication") self.logger.info("Server ready for SASL authentication")
if self.username and self.password: if self.username and self.password:
# Create auth string: username\0username\0password
authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}' authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}'
self.logger.debug(f"Auth string length: {len(authpass)} chars") self.logger.debug(f"Auth string length: {len(authpass)} chars")
self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'") self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'")
@@ -125,14 +120,12 @@ class SASLHandler:
async def handle_sasl_result(self, command, params, trailing): async def handle_sasl_result(self, command, params, trailing):
"""Handle SASL authentication result.""" """Handle SASL authentication result."""
if command == "903": if command == "903":
# SASL success
self.logger.info("SASL authentication successful!") self.logger.info("SASL authentication successful!")
self.authenticated = True self.authenticated = True
await self.handle_903() await self.handle_903()
return True return True
elif command == "904": elif command == "904":
# SASL failed
self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)") self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)")
self.logger.error(f"Attempted username: {self.username}") self.logger.error(f"Attempted username: {self.username}")
self.logger.error(f"Password length: {len(self.password)} chars") self.logger.error(f"Password length: {len(self.password)} chars")
@@ -145,28 +138,24 @@ class SASLHandler:
return False return False
elif command == "905": elif command == "905":
# SASL too long
self.logger.error("SASL authentication string too long") self.logger.error("SASL authentication string too long")
self.bot.send_raw("CAP END") self.bot.send_raw("CAP END")
await self.bot.register_user() await self.bot.register_user()
return False return False
elif command == "906": elif command == "906":
# SASL aborted
self.logger.error("SASL authentication aborted") self.logger.error("SASL authentication aborted")
self.bot.send_raw("CAP END") self.bot.send_raw("CAP END")
await self.bot.register_user() await self.bot.register_user()
return False return False
elif command == "907": elif command == "907":
# Already authenticated
self.logger.info("Already authenticated via SASL") self.logger.info("Already authenticated via SASL")
self.authenticated = True self.authenticated = True
await self.handle_903() await self.handle_903()
return True return True
elif command == "908": elif command == "908":
# SASL mechanisms
mechanisms = trailing.split() if trailing else [] mechanisms = trailing.split() if trailing else []
self.logger.info(f"Available SASL mechanisms: {mechanisms}") self.logger.info(f"Available SASL mechanisms: {mechanisms}")
if "PLAIN" not in mechanisms: if "PLAIN" not in mechanisms:
@@ -182,7 +171,6 @@ class SASLHandler:
Handles the 903 command by sending a CAP END command and triggering registration. Handles the 903 command by sending a CAP END command and triggering registration.
""" """
self.bot.send_raw('CAP END') self.bot.send_raw('CAP END')
# Trigger user registration after successful SASL auth
await self.bot.register_user() await self.bot.register_user()
def is_authenticated(self): def is_authenticated(self):

710
src/shop.py Normal file
View File

@@ -0,0 +1,710 @@
"""
Shop system for DuckHunt Bot
Handles loading items, purchasing, and item effects including player-vs-player actions
"""
import json
import os
import time
import logging
from typing import Dict, Any, Optional
class ShopManager:
"""Manages the DuckHunt shop system"""
def __init__(self, shop_file: str = "shop.json", levels_manager=None):
self.shop_file = shop_file
self.levels = levels_manager
self.items = {}
self.logger = logging.getLogger('DuckHuntBot.Shop')
self.load_items()
def load_items(self):
"""Load shop items from JSON file"""
try:
if os.path.exists(self.shop_file):
with open(self.shop_file, 'r', encoding='utf-8') as f:
shop_data = json.load(f)
# Convert string keys to integers for easier handling
self.items = {int(k): v for k, v in shop_data.get('items', {}).items()}
self.logger.info(f"Loaded {len(self.items)} shop items from {self.shop_file}")
else:
# Fallback items if file doesn't exist
self.items = self._get_default_items()
self.logger.warning(f"{self.shop_file} not found, using default items")
except Exception as e:
self.logger.error(f"Error loading shop items: {e}, using defaults")
self.items = self._get_default_items()
def _get_default_items(self) -> Dict[int, Dict[str, Any]]:
"""Default fallback shop items"""
return {
1: {"name": "Single Bullet", "price": 5, "description": "1 extra bullet", "type": "ammo", "amount": 1},
2: {"name": "Accuracy Boost", "price": 20, "description": "+10% accuracy", "type": "accuracy", "amount": 10},
3: {"name": "Lucky Charm", "price": 30, "description": "+5% duck spawn chance", "type": "luck", "amount": 5}
}
def get_items(self) -> Dict[int, Dict[str, Any]]:
"""Get all shop items"""
return self.items.copy()
def get_item(self, item_id: int) -> Optional[Dict[str, Any]]:
"""Get a specific shop item by ID"""
return self.items.get(item_id)
def is_valid_item(self, item_id: int) -> bool:
"""Check if item ID exists"""
return item_id in self.items
def can_afford(self, player_xp: int, item_id: int) -> bool:
"""Check if player can afford an item"""
item = self.get_item(item_id)
if not item:
return False
return player_xp >= item['price']
def purchase_item(self, player: Dict[str, Any], item_id: int, target_player: Optional[Dict[str, Any]] = None, store_in_inventory: bool = False) -> Dict[str, Any]:
"""
Purchase an item and either store in inventory or apply immediately
Returns a result dictionary with success status and details
"""
item = self.get_item(item_id)
if not item:
return {"success": False, "error": "invalid_id", "message": "Invalid item ID"}
# If storing in inventory and item requires a target, that's invalid
if store_in_inventory and item.get('target_required', False):
return {
"success": False,
"error": "invalid_storage",
"message": f"{item['name']} cannot be stored - it targets other players",
"item_name": item['name']
}
# Check if item requires a target (only when not storing)
if not store_in_inventory and item.get('target_required', False) and not target_player:
return {
"success": False,
"error": "target_required",
"message": f"{item['name']} requires a target player",
"item_name": item['name']
}
player_xp = player.get('xp', 0)
if player_xp < item['price']:
return {
"success": False,
"error": "insufficient_xp",
"message": f"Need {item['price']} XP, have {player_xp} XP",
"item_name": item['name'],
"price": item['price'],
"current_xp": player_xp
}
# Deduct XP
player['xp'] = player_xp - item['price']
if store_in_inventory:
# Add to inventory with bounds checking
inventory = player.get('inventory', {})
item_id_str = str(item_id)
current_count = inventory.get(item_id_str, 0)
# Load inventory limits from config
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config.json')
max_per_item = 99 # Default limit per item type
max_total_items = 20 # Default total items limit
try:
with open(config_path, 'r') as f:
config = json.load(f)
max_total_items = config.get('gameplay', {}).get('max_inventory_items', 20)
max_per_item = config.get('gameplay', {}).get('max_per_item_type', 99)
except:
pass # Use defaults
# Check individual item limit
if current_count >= max_per_item:
return {
"success": False,
"error": "item_limit_reached",
"message": f"Cannot hold more than {max_per_item} {item['name']}s",
"item_name": item['name']
}
# Check total inventory size limit
total_items = sum(inventory.values())
if total_items >= max_total_items:
return {
"success": False,
"error": "inventory_full",
"message": f"Inventory full! (max {max_total_items} items)",
"item_name": item['name']
}
inventory[item_id_str] = current_count + 1
player['inventory'] = inventory
return {
"success": True,
"item_name": item['name'],
"price": item['price'],
"remaining_xp": player['xp'],
"stored_in_inventory": True,
"inventory_count": inventory[item_id_str]
}
else:
# Apply effect immediately
if item.get('target_required', False) and target_player:
effect_result = self._apply_item_effect(target_player, item)
return {
"success": True,
"item_name": item['name'],
"price": item['price'],
"remaining_xp": player['xp'],
"effect": effect_result,
"target_affected": True
}
else:
# Apply effect to purchaser
effect_result = self._apply_item_effect(player, item)
return {
"success": True,
"item_name": item['name'],
"price": item['price'],
"remaining_xp": player['xp'],
"effect": effect_result,
"target_affected": False
}
def _apply_item_effect(self, player: Dict[str, Any], item: Dict[str, Any]) -> Dict[str, Any]:
"""Apply the effect of an item to a player"""
item_type = item.get('type', 'unknown')
amount = item.get('amount', 0)
if item_type == 'ammo':
# Add bullets to current magazine
current_ammo = player.get('current_ammo', 0)
bullets_per_mag = player.get('bullets_per_magazine', 6)
new_ammo = min(current_ammo + amount, bullets_per_mag)
added_bullets = new_ammo - current_ammo
player['current_ammo'] = new_ammo
return {
"type": "ammo",
"added": added_bullets,
"new_total": new_ammo,
"max": bullets_per_mag
}
elif item_type == 'magazine':
# Add magazines (limit checking is done before this function is called)
current_magazines = player.get('magazines', 1)
if self.levels:
level_info = self.levels.get_player_level_info(player)
max_magazines = level_info.get('magazines', 3)
# Don't exceed maximum magazines for level
magazines_to_add = min(amount, max_magazines - current_magazines)
else:
# Fallback if levels not available
magazines_to_add = amount
new_magazines = current_magazines + magazines_to_add
player['magazines'] = new_magazines
return {
"type": "magazine",
"added": magazines_to_add,
"new_total": new_magazines
}
elif item_type == 'accuracy':
# Increase accuracy up to 100%
current_accuracy = player.get('accuracy', 75)
new_accuracy = min(current_accuracy + amount, 100)
player['accuracy'] = new_accuracy
return {
"type": "accuracy",
"added": new_accuracy - current_accuracy,
"new_total": new_accuracy
}
elif item_type == 'luck':
# Store luck bonus (would be used in duck spawning logic)
current_luck = player.get('luck_bonus', 0)
new_luck = min(max(current_luck + amount, -50), 100) # Bounded between -50 and +100
player['luck_bonus'] = new_luck
return {
"type": "luck",
"added": new_luck - current_luck,
"new_total": new_luck
}
elif item_type == 'jam_resistance':
# Reduce gun jamming chance (lower is better)
current_jam = player.get('jam_chance', 5) # Default 5% jam chance
new_jam = max(current_jam - amount, 0) # Can't go below 0%
player['jam_chance'] = new_jam
return {
"type": "jam_resistance",
"reduced": current_jam - new_jam,
"new_total": new_jam
}
elif item_type == 'max_ammo':
# Increase maximum ammo capacity
current_max = player.get('max_ammo', 6)
new_max = current_max + amount
player['max_ammo'] = new_max
return {
"type": "max_ammo",
"added": amount,
"new_total": new_max
}
elif item_type == 'chargers':
# Add reload chargers
current_chargers = player.get('chargers', 2)
new_chargers = current_chargers + amount
player['chargers'] = new_chargers
return {
"type": "chargers",
"added": amount,
"new_total": new_chargers
}
elif item_type == 'duck_attraction':
# Increase chance of ducks appearing when this player is online
current_attraction = player.get('duck_attraction', 0)
new_attraction = current_attraction + amount
player['duck_attraction'] = new_attraction
return {
"type": "duck_attraction",
"added": amount,
"new_total": new_attraction
}
elif item_type == 'critical_hit':
# Chance for critical hits (double XP)
current_crit = player.get('critical_chance', 0)
new_crit = min(current_crit + amount, 25) # Max 25% crit chance
player['critical_chance'] = new_crit
return {
"type": "critical_hit",
"added": new_crit - current_crit,
"new_total": new_crit
}
elif item_type == 'sabotage_jam':
# Increase target's gun jamming chance temporarily
current_jam = player.get('jam_chance', 5)
new_jam = min(current_jam + amount, 50) # Max 50% jam chance
player['jam_chance'] = new_jam
# Add temporary effect tracking
if 'temporary_effects' not in player:
player['temporary_effects'] = []
effect = {
'type': 'jam_increase',
'amount': amount,
'expires_at': time.time() + item.get('duration', 5) * 60 # duration in minutes
}
player['temporary_effects'].append(effect)
return {
"type": "sabotage_jam",
"added": new_jam - current_jam,
"new_total": new_jam,
"duration": item.get('duration', 5)
}
elif item_type == 'sabotage_accuracy':
# Reduce target's accuracy temporarily
current_acc = player.get('accuracy', 75)
new_acc = max(current_acc + amount, 10) # Min 10% accuracy (amount is negative)
player['accuracy'] = new_acc
# Add temporary effect tracking
if 'temporary_effects' not in player:
player['temporary_effects'] = []
effect = {
'type': 'accuracy_reduction',
'amount': amount,
'expires_at': time.time() + item.get('duration', 3) * 60
}
player['temporary_effects'].append(effect)
return {
"type": "sabotage_accuracy",
"reduced": current_acc - new_acc,
"new_total": new_acc,
"duration": item.get('duration', 3)
}
elif item_type == 'steal_ammo':
# Steal ammo from target player
current_ammo = player.get('ammo', 0)
stolen = min(amount, current_ammo)
player['ammo'] = max(current_ammo - stolen, 0)
return {
"type": "steal_ammo",
"stolen": stolen,
"remaining": player['ammo']
}
elif item_type == 'clean_gun':
# Clean gun to reduce jamming chance (positive amount reduces jam chance)
current_jam = player.get('jam_chance', 5) # Default 5% jam chance
new_jam = min(max(current_jam + amount, 0), 100) # Bounded between 0% and 100%
player['jam_chance'] = new_jam
return {
"type": "clean_gun",
"reduced": current_jam - new_jam,
"new_total": new_jam
}
elif item_type == 'attract_ducks':
# Add bread effect to increase duck spawn rate
if 'temporary_effects' not in player:
player['temporary_effects'] = []
duration = item.get('duration', 600) # 10 minutes default
spawn_multiplier = item.get('spawn_multiplier', 2.0) # 2x spawn rate default
effect = {
'type': 'attract_ducks',
'spawn_multiplier': spawn_multiplier,
'expires_at': time.time() + duration
}
player['temporary_effects'].append(effect)
return {
"type": "attract_ducks",
"spawn_multiplier": spawn_multiplier,
"duration": duration // 60 # return duration in minutes
}
elif item_type == 'clover_luck':
# Temporarily boost hit + befriend success rates
if 'temporary_effects' not in player or not isinstance(player.get('temporary_effects'), list):
player['temporary_effects'] = []
duration = item.get('duration', 600) # seconds
try:
duration = int(duration)
except (ValueError, TypeError):
duration = 600
duration = max(30, min(duration, 86400))
try:
min_hit = float(item.get('min_hit_chance', 0.95))
except (ValueError, TypeError):
min_hit = 0.95
try:
min_bef = float(item.get('min_befriend_chance', 0.95))
except (ValueError, TypeError):
min_bef = 0.95
min_hit = max(0.0, min(min_hit, 1.0))
min_bef = max(0.0, min(min_bef, 1.0))
now = time.time()
expires_at = now + duration
# If an existing clover effect is active, extend it instead of stacking.
for effect in player['temporary_effects']:
if isinstance(effect, dict) and effect.get('type') == 'clover_luck' and effect.get('expires_at', 0) > now:
effect['expires_at'] = max(effect.get('expires_at', now), now) + duration
effect['min_hit_chance'] = max(float(effect.get('min_hit_chance', 0.0) or 0.0), min_hit)
effect['min_befriend_chance'] = max(float(effect.get('min_befriend_chance', 0.0) or 0.0), min_bef)
return {
"type": "clover_luck",
"duration": duration // 60,
"min_hit_chance": min_hit,
"min_befriend_chance": min_bef,
"extended": True
}
effect = {
'type': 'clover_luck',
'min_hit_chance': min_hit,
'min_befriend_chance': min_bef,
'expires_at': expires_at
}
player['temporary_effects'].append(effect)
return {
"type": "clover_luck",
"duration": duration // 60,
"min_hit_chance": min_hit,
"min_befriend_chance": min_bef,
"extended": False
}
elif item_type == 'insurance':
# Add insurance protection against friendly fire
if 'temporary_effects' not in player:
player['temporary_effects'] = []
duration = item.get('duration', 86400) # 24 hours default
protection_type = item.get('protection', 'friendly_fire')
effect = {
'type': 'insurance',
'protection': protection_type,
'expires_at': time.time() + duration,
'name': 'Hunter\'s Insurance'
}
player['temporary_effects'].append(effect)
return {
"type": "insurance",
"protection": protection_type,
"duration": duration // 3600 # return duration in hours
}
elif item_type == 'buy_gun_back':
# Restore confiscated gun with original ammo
was_confiscated = player.get('gun_confiscated', False)
if was_confiscated:
player['gun_confiscated'] = False
# Restore original ammo and magazines from when gun was confiscated
restored_ammo = player.get('confiscated_ammo', 0)
restored_magazines = player.get('confiscated_magazines', 1)
player['current_ammo'] = restored_ammo
player['magazines'] = restored_magazines
# Clean up the stored values
player.pop('confiscated_ammo', None)
player.pop('confiscated_magazines', None)
return {
"type": "buy_gun_back",
"restored": True,
"ammo_restored": restored_ammo
}
else:
return {
"type": "buy_gun_back",
"restored": False,
"message": "Your gun is not confiscated"
}
elif item_type == 'dry_clothes':
# Remove wet clothes effect
# Remove any wet clothes effects
if 'temporary_effects' in player:
original_count = len(player['temporary_effects'])
player['temporary_effects'] = [
effect for effect in player['temporary_effects']
if effect.get('type') != 'wet_clothes'
]
new_count = len(player['temporary_effects'])
was_wet = original_count > new_count
else:
was_wet = False
return {
"type": "dry_clothes",
"was_wet": was_wet,
"message": "You changed into dry clothes!" if was_wet else "You weren't wet!"
}
else:
self.logger.warning(f"Unknown item type: {item_type}")
return {"type": "unknown", "message": f"Unknown effect type: {item_type}"}
def _apply_splash_water_effect(self, target_player: Dict[str, Any], item: Dict[str, Any]) -> Dict[str, Any]:
"""Apply splash water effect to target player"""
# Load config directly without import issues
config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config.json')
try:
with open(config_path, 'r') as f:
config = json.load(f)
wet_duration = config.get('gameplay', {}).get('wet_clothes_duration', 300) # 5 minutes default
except:
wet_duration = 300 # Default 5 minutes
if 'temporary_effects' not in target_player:
target_player['temporary_effects'] = []
# Add wet clothes effect
wet_effect = {
'type': 'wet_clothes',
'expires_at': time.time() + wet_duration
}
target_player['temporary_effects'].append(wet_effect)
return {
"type": "splash_water",
"target_soaked": True,
"duration": wet_duration // 60 # return duration in minutes
}
def use_inventory_item(self, player: Dict[str, Any], item_id: int, target_player: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Use an item from player's inventory
Returns a result dictionary with success status and details
"""
item = self.get_item(item_id)
if not item:
return {"success": False, "error": "invalid_id", "message": "Invalid item ID"}
inventory = player.get('inventory', {})
item_id_str = str(item_id)
if item_id_str not in inventory or inventory[item_id_str] <= 0:
return {
"success": False,
"error": "not_in_inventory",
"message": f"You don't have any {item['name']} in your inventory",
"item_name": item['name']
}
# Special restrictions: Some items require targets, bread cannot have targets
if item['type'] == 'attract_ducks' and target_player:
return {
"success": False,
"error": "bread_no_target",
"message": "Bread affects everyone in the channel - you cannot target a specific player",
"item_name": item['name']
}
# Items that must have targets when used (but can be stored in inventory)
target_required_items = ['sabotage_jam', 'splash_water']
if item['type'] in target_required_items and not target_player:
return {
"success": False,
"error": "target_required",
"message": f"{item['name']} requires a target player to use",
"item_name": item['name']
}
# Special checks for ammo/magazine limits
if item['type'] == 'magazine' and self.levels:
affected_player = target_player if target_player else player
current_magazines = affected_player.get('magazines', 1)
level_info = self.levels.get_player_level_info(affected_player)
max_magazines = level_info.get('magazines', 3)
if current_magazines >= max_magazines:
return {
"success": False,
"error": "max_magazines_reached",
"message": f"Already at maximum magazines ({max_magazines}) for current level!",
"item_name": item['name']
}
elif item['type'] == 'ammo':
affected_player = target_player if target_player else player
current_ammo = affected_player.get('current_ammo', 0)
bullets_per_mag = affected_player.get('bullets_per_magazine', 6)
if current_ammo >= bullets_per_mag:
return {
"success": False,
"error": "magazine_full",
"message": f"Current magazine is already full ({bullets_per_mag}/{bullets_per_mag})!",
"item_name": item['name']
}
# Remove item from inventory
inventory[item_id_str] -= 1
if inventory[item_id_str] <= 0:
del inventory[item_id_str]
player['inventory'] = inventory
# Determine who gets the effect
if target_player:
# Special handling for harmful effects
if item['type'] == 'splash_water':
effect_result = self._apply_splash_water_effect(target_player, item)
target_affected = True
elif item['type'] == 'sabotage_jam':
effect_result = self._apply_item_effect(target_player, item)
target_affected = True
else:
# Beneficial items - give to target (gifting)
effect_result = self._apply_item_effect(target_player, item)
target_affected = True
# Mark as gift in the result
effect_result['is_gift'] = True
return {
"success": True,
"item_name": item['name'],
"effect": effect_result,
"target_affected": target_affected,
"remaining_in_inventory": inventory.get(item_id_str, 0)
}
else:
# Apply effect to user (no target specified)
effect_result = self._apply_item_effect(player, item)
return {
"success": True,
"item_name": item['name'],
"effect": effect_result,
"target_affected": False,
"remaining_in_inventory": inventory.get(item_id_str, 0)
}
def get_inventory_display(self, player: Dict[str, Any]) -> Dict[str, Any]:
"""
Get formatted inventory display for a player
Returns dict with inventory info
"""
inventory = player.get('inventory', {})
if not inventory:
return {
"empty": True,
"message": "Your inventory is empty"
}
items = []
for item_id_str, quantity in inventory.items():
item_id = int(item_id_str)
item = self.get_item(item_id)
if item:
items.append({
"id": item_id,
"name": item['name'],
"quantity": quantity,
"description": item.get('description', 'No description')
})
return {
"empty": False,
"items": items,
"total_items": len(items)
}
def reload_items(self) -> int:
"""Reload items from file and return count"""
old_count = len(self.items)
self.load_items()
new_count = len(self.items)
self.logger.info(f"Shop reloaded: {old_count} -> {new_count} items")
return new_count
def get_shop_display(self, player, message_manager):
"""Get formatted shop display"""
items = []
for item_id, item in self.get_items().items():
item_text = message_manager.get('shop_item_format',
id=item_id,
name=item['name'],
price=item['price'])
items.append(item_text)
shop_text = message_manager.get('shop_display',
items=" | ".join(items),
xp=player.get('xp', 0))
return shop_text

265
src/utils.py Normal file
View File

@@ -0,0 +1,265 @@
"""
Utility functions for DuckHunt Bot
"""
import re
import json
import os
import random
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, Any]:
"""Default fallback messages without colors"""
return {
"duck_spawn": [
"・゜゜・。。・゜゜\\_o< QUACK! A duck has appeared! Type !bang to shoot it!",
"・゜゜・。。・゜゜\\_o< *flap flap* A wild duck landed! Use !bang to hunt it!",
"A duck swoops into view! Quick, type !bang before it escapes!",
"・゜゜・。。・゜゜\\_o< Quack quack! Fresh duck spotted! !bang to bag it!",
"*rustling* A duck waddles out from the bushes! Fire with !bang!",
"・゜゜・。。・゜゜\\_o< Splash! A duck surfaces! Shoot it with !bang!"
],
"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 with enhanced error handling"""
try:
if key not in self.messages:
return f"[Missing message: {key}]"
message = self.messages[key]
# If message is an array, randomly select one
if isinstance(message, list):
if not message:
return f"[Empty message array: {key}]"
message = random.choice(message)
# Ensure message is a string
if not isinstance(message, str):
return f"[Invalid message type: {key}]"
# Replace color placeholders with IRC codes
if "colours" in self.messages and isinstance(self.messages["colours"], dict):
for color_name, color_code in self.messages["colours"].items():
placeholder = "{" + color_name + "}"
message = message.replace(placeholder, color_code)
# Sanitize kwargs to prevent injection and ensure all values are safe
safe_kwargs = {}
for k, v in kwargs.items():
try:
# Sanitize key and value
safe_key = str(k)[:50] if k is not None else 'unknown'
if isinstance(v, (int, float)):
safe_kwargs[safe_key] = v
elif v is None:
safe_kwargs[safe_key] = ''
else:
# Sanitize string values
safe_value = str(v)[:200] # Limit length
safe_value = safe_value.replace('\r', '').replace('\n', ' ') # Remove newlines
safe_kwargs[safe_key] = safe_value
except Exception:
safe_kwargs[str(k)] = '[error]'
# Format with provided variables using safe formatting
try:
return message.format(**safe_kwargs)
except KeyError as e:
# Try to identify missing keys and provide defaults
missing_key = str(e).strip("'\"")
# Common defaults for missing keys
defaults = {
'nick': 'Player',
'xp_gained': 0,
'ducks_shot': 0,
'ducks_befriended': 0,
'hp_remaining': 0,
'ammo': 0,
'max_ammo': 0,
'magazines': 0,
'target': 'Unknown',
'victim': 'someone',
'xp_lost': 0,
'xp': 0
}
# Add default for missing key
if missing_key in defaults:
safe_kwargs[missing_key] = defaults[missing_key]
else:
safe_kwargs[missing_key] = f'[{missing_key}]'
try:
return message.format(**safe_kwargs)
except Exception:
return f"[Format error in {key}: missing {missing_key}]"
except ValueError as e:
return f"[Format error in {key}: {e}]"
except Exception as e:
return f"[Message error in {key}: {e}]"
except Exception as e:
return f"[Critical message error: {e}]"
def reload(self):
"""Reload messages from file"""
self.load_messages()
class InputValidator:
"""Input validation utilities"""
@staticmethod
def validate_nickname(nick: str) -> bool:
"""Validate IRC nickname format"""
if not nick or len(nick) > 30:
return False
pattern = r'^[a-zA-Z\[\]\\`_^{|}][a-zA-Z0-9\[\]\\`_^{|}\-]*$'
return bool(re.match(pattern, nick))
@staticmethod
def validate_channel(channel: str) -> bool:
"""Validate IRC channel format"""
if not channel or len(channel) > 50:
return False
return channel.startswith('#') and ' ' not in channel
@staticmethod
def validate_numeric_input(value: str, min_val: Optional[int] = None, max_val: Optional[int] = None) -> Optional[int]:
"""Safely parse and validate numeric input"""
try:
num = int(value)
if min_val is not None and num < min_val:
return None
if max_val is not None and num > max_val:
return None
return num
except (ValueError, TypeError):
return None
@staticmethod
def sanitize_message(message: str) -> str:
"""Sanitize user input message"""
if not message:
return ""
sanitized = ''.join(char for char in message if ord(char) >= 32 or char in '\t\n')
return sanitized[:500]
def parse_irc_message(line: str) -> Tuple[str, str, List[str], str]:
"""Parse IRC message format with comprehensive error handling"""
try:
# Validate input
if not isinstance(line, str):
raise ValueError(f"Expected string, got {type(line)}")
# Handle empty or whitespace-only lines
if not line or not line.strip():
return '', '', [], ''
line = line.strip()
# Initialize return values
prefix = ''
trailing = ''
command = ''
params = []
# Handle prefix (starts with :)
if line.startswith(':'):
try:
if ' ' in line[1:]:
prefix, line = line[1:].split(' ', 1)
else:
# Handle malformed IRC line with no space after prefix
prefix = line[1:]
line = ''
except ValueError:
# If split fails, treat entire line as prefix
prefix = line[1:]
line = ''
# Handle trailing parameter (starts with ' :')
if line and ' :' in line:
try:
line, trailing = line.split(' :', 1)
except ValueError:
# If split fails, keep line as is
pass
# Parse command and parameters
if line:
try:
parts = line.split()
command = parts[0] if parts else ''
params = parts[1:] if len(parts) > 1 else []
except Exception:
# If parsing fails, try to extract at least the command
command = line.split()[0] if line.split() else ''
params = []
# Validate that we have at least a command
if not command and not prefix:
raise ValueError(f"No valid command or prefix found in line: {line[:50]}...")
return prefix, command, params, trailing
except Exception as e:
# Log the error but return safe defaults to prevent crashes
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Error parsing IRC message '{line[:50]}...': {e}")
return '', 'UNKNOWN', [], ''