Compare commits
87 Commits
86bf92c478
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b5b42a507 | ||
|
|
626eb7cb2a | ||
|
|
b6d2fe2a35 | ||
|
|
77ed3f95ad | ||
|
|
68a0a1fc83 | ||
|
|
292db0c95e | ||
|
|
b944e234d6 | ||
|
|
735c46b8c2 | ||
|
|
38d9159f50 | ||
|
|
a1afd25053 | ||
|
|
1e4e1e31ba | ||
|
|
9bff554a07 | ||
|
|
2372075195 | ||
|
|
2ef81cdd26 | ||
|
|
67bf6957a7 | ||
|
|
214d1ed263 | ||
|
|
f47778e608 | ||
|
|
7147f5f30c | ||
|
|
51c212e8cd | ||
|
|
5efe1e70bf | ||
|
|
a8b4196cf2 | ||
|
|
5e4bbb4309 | ||
|
|
53a66f5202 | ||
|
|
b71f8f4ec6 | ||
|
|
5db4ce0ab3 | ||
|
|
90b604ba72 | ||
|
|
dd06c9377f | ||
|
|
eb907e1e2c | ||
|
|
f6a9f4592a | ||
|
|
6069240553 | ||
|
|
f3f251a391 | ||
|
|
3e7436840e | ||
|
|
9bd51a24cc | ||
|
|
d5654e9783 | ||
|
|
ba9beae82f | ||
|
|
7d85f83faa | ||
|
|
02c055d7e3 | ||
|
|
617d9560e6 | ||
|
|
3b72a853ae | ||
|
|
ffe8bdfaf2 | ||
|
|
b256b9a9f6 | ||
|
|
4d17ae8f04 | ||
| f8c46980de | |||
| b5613f20dd | |||
| 0176284012 | |||
| 857a15b666 | |||
| 489989001c | |||
| b39c82c84b | |||
| 0a27f7272e | |||
| 85fa8a9170 | |||
| 00e129d2f3 | |||
| 470edb4401 | |||
| a17bba215d | |||
| 687a57f018 | |||
| 7aded2ed83 | |||
| 5ed2f0fce6 | |||
| b1b1d4d65f | |||
| bf3cd48639 | |||
| 25226a460b | |||
| f3a9c5b611 | |||
| 5484548c30 | |||
| eb041477dc | |||
| 74f3afdf4b | |||
| 6ca624bd2f | |||
| f9883758f3 | |||
| 688aca759f | |||
| 78caccd8b4 | |||
| 73582f7a44 | |||
| d6e64d5eab | |||
| 0c8b4f9543 | |||
| 3aaf0d0bb4 | |||
| de64756b6d | |||
| 9285b1b29d | |||
| ba7f082d5c | |||
| 1f5af7ed83 | |||
| 1f64b6fc82 | |||
| 339f32b6a0 | |||
| 7c0974cfbf | |||
| 5ebe8a8e21 | |||
| 746957bc17 | |||
| b198ef2b9e | |||
| 34daa04238 | |||
| 6854e88037 | |||
| 2bbc202f8f | |||
| 62da9d1c28 | |||
| 408a840e94 | |||
| 009a851696 |
158
.gitignore
vendored
Normal file
158
.gitignore
vendored
Normal 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
45
MULTI_CHANNEL_PLAN.md
Normal 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
171
README.md
@@ -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
171
config.json.example
Normal 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
46
duckhunt.py
Normal 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()
|
||||
@@ -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 }
|
||||
```
|
||||
@@ -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!** 🦆
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}")
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
104
levels.json
Normal 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
127
messages.json
Normal 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
1
original
Submodule
Submodule original added at f8c46980de
75
shop.json
Normal file
75
shop.json
Normal 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
605
src/db.py
Normal 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
1874
src/duckhuntbot.py
Normal file
File diff suppressed because it is too large
Load Diff
262
src/error_handling.py
Normal file
262
src/error_handling.py
Normal 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
703
src/game.py
Normal 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
231
src/levels.py
Normal 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
375
src/logging_utils.py
Normal 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)
|
||||
@@ -14,10 +14,10 @@ class SASLHandler:
|
||||
def __init__(self, bot, config):
|
||||
self.bot = bot
|
||||
self.logger = setup_logger("SASL")
|
||||
sasl_config = config.get("sasl", {})
|
||||
self.enabled = sasl_config.get("enabled", False)
|
||||
self.username = sasl_config.get("username", config.get("nick", ""))
|
||||
self.password = sasl_config.get("password", "")
|
||||
# Use bot's get_config method for nested config access
|
||||
self.enabled = bot.get_config("sasl.enabled", False)
|
||||
self.username = bot.get_config("sasl.username", bot.get_config("connection.nick", ""))
|
||||
self.password = bot.get_config("sasl.password", "")
|
||||
self.authenticated = False
|
||||
self.cap_negotiating = False
|
||||
|
||||
@@ -55,7 +55,6 @@ class SASLHandler:
|
||||
subcommand = params[1]
|
||||
|
||||
if subcommand == "LS":
|
||||
# Server listing capabilities
|
||||
caps = trailing.split() if trailing else []
|
||||
self.logger.info(f"Server capabilities: {caps}")
|
||||
if "sasl" in caps:
|
||||
@@ -69,7 +68,6 @@ class SASLHandler:
|
||||
return False
|
||||
|
||||
elif subcommand == "ACK":
|
||||
# Server acknowledged capability request
|
||||
caps = trailing.split() if trailing else []
|
||||
self.logger.info("SASL capability acknowledged")
|
||||
if "sasl" in caps:
|
||||
@@ -82,7 +80,6 @@ class SASLHandler:
|
||||
return False
|
||||
|
||||
elif subcommand == "NAK":
|
||||
# Server rejected capability request
|
||||
self.logger.warning("SASL capability rejected")
|
||||
self.bot.send_raw("CAP END")
|
||||
await self.bot.register_user()
|
||||
@@ -96,7 +93,6 @@ class SASLHandler:
|
||||
"""
|
||||
self.logger.info("Sending AUTHENTICATE PLAIN")
|
||||
self.bot.send_raw('AUTHENTICATE PLAIN')
|
||||
# Small delay to ensure proper sequencing
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def handle_authenticate_response(self, params):
|
||||
@@ -106,7 +102,6 @@ class SASLHandler:
|
||||
if params and params[0] == '+':
|
||||
self.logger.info("Server ready for SASL authentication")
|
||||
if self.username and self.password:
|
||||
# Create auth string: username\0username\0password
|
||||
authpass = f'{self.username}{NULL_BYTE}{self.username}{NULL_BYTE}{self.password}'
|
||||
self.logger.debug(f"Auth string length: {len(authpass)} chars")
|
||||
self.logger.debug(f"Auth components: user='{self.username}', pass='{self.password[:3]}...'")
|
||||
@@ -125,14 +120,12 @@ class SASLHandler:
|
||||
async def handle_sasl_result(self, command, params, trailing):
|
||||
"""Handle SASL authentication result."""
|
||||
if command == "903":
|
||||
# SASL success
|
||||
self.logger.info("SASL authentication successful!")
|
||||
self.authenticated = True
|
||||
await self.handle_903()
|
||||
return True
|
||||
|
||||
elif command == "904":
|
||||
# SASL failed
|
||||
self.logger.error("SASL authentication failed! (904 - Invalid credentials or account not found)")
|
||||
self.logger.error(f"Attempted username: {self.username}")
|
||||
self.logger.error(f"Password length: {len(self.password)} chars")
|
||||
@@ -145,28 +138,24 @@ class SASLHandler:
|
||||
return False
|
||||
|
||||
elif command == "905":
|
||||
# SASL too long
|
||||
self.logger.error("SASL authentication string too long")
|
||||
self.bot.send_raw("CAP END")
|
||||
await self.bot.register_user()
|
||||
return False
|
||||
|
||||
elif command == "906":
|
||||
# SASL aborted
|
||||
self.logger.error("SASL authentication aborted")
|
||||
self.bot.send_raw("CAP END")
|
||||
await self.bot.register_user()
|
||||
return False
|
||||
|
||||
elif command == "907":
|
||||
# Already authenticated
|
||||
self.logger.info("Already authenticated via SASL")
|
||||
self.authenticated = True
|
||||
await self.handle_903()
|
||||
return True
|
||||
|
||||
elif command == "908":
|
||||
# SASL mechanisms
|
||||
mechanisms = trailing.split() if trailing else []
|
||||
self.logger.info(f"Available SASL mechanisms: {mechanisms}")
|
||||
if "PLAIN" not in mechanisms:
|
||||
@@ -182,7 +171,6 @@ class SASLHandler:
|
||||
Handles the 903 command by sending a CAP END command and triggering registration.
|
||||
"""
|
||||
self.bot.send_raw('CAP END')
|
||||
# Trigger user registration after successful SASL auth
|
||||
await self.bot.register_user()
|
||||
|
||||
def is_authenticated(self):
|
||||
710
src/shop.py
Normal file
710
src/shop.py
Normal 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
265
src/utils.py
Normal 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', [], ''
|
||||
Reference in New Issue
Block a user