Uploading all files.
This commit is contained in:
173
README.md
173
README.md
@@ -1,3 +1,172 @@
|
||||
# duckhunt
|
||||
# 🦆 DuckHunt IRC Bot
|
||||
|
||||
DuckHunt IRC game 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!** 🦆
|
||||
|
||||
BIN
__pycache__/simple_duckhunt.cpython-312.pyc
Normal file
BIN
__pycache__/simple_duckhunt.cpython-312.pyc
Normal file
Binary file not shown.
19
config.json
Normal file
19
config.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"server": "irc.rizon.net",
|
||||
"port": 6697,
|
||||
"nick": "DuckHunt",
|
||||
"channels": ["#computertech"],
|
||||
"ssl": true,
|
||||
"sasl": {
|
||||
"enabled": true,
|
||||
"username": "duckhunt",
|
||||
"password": "duckhunt//789//"
|
||||
},
|
||||
"password": "",
|
||||
"admins": ["peorth", "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"
|
||||
}
|
||||
9
config.json.example
Normal file
9
config.json.example
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"server": "irc.example.com",
|
||||
"port": 6697,
|
||||
"use_ssl": true,
|
||||
"nick": "DuckHunt",
|
||||
"channels": ["#duckhunt"],
|
||||
"sasl_username": "your_nick_here",
|
||||
"sasl_password": "your_password_here"
|
||||
}
|
||||
205
duckhunt.json
Normal file
205
duckhunt.json
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"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": 113,
|
||||
"caught": 9,
|
||||
"ammo": 10,
|
||||
"max_ammo": 10,
|
||||
"chargers": 1,
|
||||
"max_chargers": 2,
|
||||
"xp": 44,
|
||||
"accuracy": 85,
|
||||
"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": 4,
|
||||
"shot_at": 3,
|
||||
"reflex_shots": 3,
|
||||
"total_reflex_time": 34.926591634750366,
|
||||
"best_time": 7.363537788391113,
|
||||
"karma": 3,
|
||||
"wild_shots": 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": 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
|
||||
},
|
||||
"boliver": {
|
||||
"xp": 6,
|
||||
"caught": 0,
|
||||
"befriended": 1,
|
||||
"missed": 1,
|
||||
"ammo": 5,
|
||||
"max_ammo": 6,
|
||||
"chargers": 2,
|
||||
"max_chargers": 2,
|
||||
"accuracy": 65,
|
||||
"reliability": 70,
|
||||
"weapon": "pistol",
|
||||
"gun_confiscated": false,
|
||||
"explosive_ammo": false,
|
||||
"settings": {
|
||||
"notices": true,
|
||||
"private_messages": false
|
||||
},
|
||||
"golden_ducks": 0,
|
||||
"karma": -1,
|
||||
"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": 0,
|
||||
"accidents": 0,
|
||||
"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
|
||||
}
|
||||
},
|
||||
"last_save": "1757695515.2473938"
|
||||
}
|
||||
93
duckhunt.log
Normal file
93
duckhunt.log
Normal file
@@ -0,0 +1,93 @@
|
||||
[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-12 17:44:59,246] INFO: Loaded 3 players from duckhunt.json
|
||||
[2025-09-12 17:44:59,247] INFO: DuckHunt Bot initializing...
|
||||
[2025-09-12 17:44:59,248] INFO: Starting DuckHunt Bot...
|
||||
[2025-09-12 17:44:59,249] INFO: Loaded 3 players from duckhunt.json
|
||||
[2025-09-12 17:44:59,329] INFO: Connecting to irc.rizon.net:6697 (SSL: True)
|
||||
[2025-09-12 17:44:59,997] INFO: Connected successfully!
|
||||
[2025-09-12 17:44:59,997] INFO: SASL authentication enabled
|
||||
[2025-09-12 17:45:00,375] INFO: SASL capability available
|
||||
[2025-09-12 17:45:00,433] INFO: SASL capability acknowledged
|
||||
[2025-09-12 17:45:00,433] INFO: Authenticating via SASL as duckhunt
|
||||
[2025-09-12 17:45:00,851] INFO: Server ready for SASL authentication
|
||||
[2025-09-12 17:45:00,851] ERROR: SASL authentication failed! (904 - Invalid credentials or account not found)
|
||||
[2025-09-12 17:45:00,852] INFO: Falling back to NickServ identification...
|
||||
[2025-09-12 17:45:00,852] ERROR: Attempted username: duckhunt
|
||||
[2025-09-12 17:45:00,852] INFO: Registering as DuckHunt
|
||||
[2025-09-12 17:45:00,968] ERROR: SASL authentication aborted! (906)
|
||||
[2025-09-12 17:45:00,968] INFO: Falling back to NickServ identification...
|
||||
[2025-09-12 17:45:00,969] INFO: Registering as DuckHunt
|
||||
[2025-09-12 17:45:00,969] INFO: Successfully registered!
|
||||
[2025-09-12 17:45:00,969] INFO: Attempting NickServ identification for duckhunt
|
||||
[2025-09-12 17:45:01,971] INFO: NickServ identification commands sent
|
||||
[2025-09-12 17:45:01,971] INFO: Joining #computertech
|
||||
[2025-09-12 17:45:02,032] INFO: Successfully joined #computertech
|
||||
[2025-09-12 17:45:03,001] INFO: Starting duck spawning...
|
||||
[2025-09-12 17:45:08,003] INFO: Admin spawned normal duck c2950231 in #computertech
|
||||
[2025-09-12 17:45:08,003] INFO: Waiting 58m 28s for next duck
|
||||
[2025-09-12 17:45:09,063] INFO: Admin spawned normal duck d30a9198 in #computertech
|
||||
[2025-09-12 17:45:14,112] WARNING: A main task completed unexpectedly
|
||||
[2025-09-12 17:45:14,141] INFO: Starting cleanup process...
|
||||
[2025-09-12 17:45:15,246] INFO: IRC connection closed
|
||||
[2025-09-12 17:45:15,248] INFO: Final database save completed - 5 players saved
|
||||
[2025-09-12 17:45:15,248] INFO: Cleanup completed successfully
|
||||
[2025-09-12 17:45:15,249] INFO: DuckHunt Bot shutdown complete
|
||||
37
duckhunt.py
Normal file
37
duckhunt.py
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/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()
|
||||
0
run_bot.sh
Normal file
0
run_bot.sh
Normal file
2221
simple_duckhunt.py
Normal file
2221
simple_duckhunt.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/__pycache__/auth.cpython-312.pyc
Normal file
BIN
src/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/db.cpython-312.pyc
Normal file
BIN
src/__pycache__/db.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/duckhuntbot.cpython-312.pyc
Normal file
BIN
src/__pycache__/duckhuntbot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/game.cpython-312.pyc
Normal file
BIN
src/__pycache__/game.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/items.cpython-312.pyc
Normal file
BIN
src/__pycache__/items.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/logging_utils.cpython-312.pyc
Normal file
BIN
src/__pycache__/logging_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/utils.cpython-312.pyc
Normal file
BIN
src/__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
60
src/auth.py
Normal file
60
src/auth.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
from typing import Optional
|
||||
from src.db import DuckDB
|
||||
|
||||
class AuthSystem:
|
||||
def __init__(self, db: DuckDB):
|
||||
self.db = db
|
||||
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]
|
||||
97
src/db.py
Normal file
97
src/db.py
Normal file
@@ -0,0 +1,97 @@
|
||||
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))
|
||||
0
src/duckhuntbot.py
Normal file
0
src/duckhuntbot.py
Normal file
566
src/game.py
Normal file
566
src/game.py
Normal file
@@ -0,0 +1,566 @@
|
||||
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)
|
||||
124
src/items.py
Normal file
124
src/items.py
Normal file
@@ -0,0 +1,124 @@
|
||||
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
|
||||
}
|
||||
28
src/logging_utils.py
Normal file
28
src/logging_utils.py
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
11
src/utils.py
Normal file
11
src/utils.py
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
53
test_bot.py
Normal file
53
test_bot.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for DuckHunt Bot
|
||||
Run this to test the bot locally
|
||||
"""
|
||||
|
||||
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 duckhuntbot import IRCBot
|
||||
|
||||
async def test_bot():
|
||||
"""Test the bot initialization and basic functionality"""
|
||||
try:
|
||||
# Load config
|
||||
with open('config.json') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Create bot instance
|
||||
bot = IRCBot(config)
|
||||
print("✅ Bot initialized 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!")
|
||||
|
||||
print("🦆 DuckHunt Bot is ready to deploy!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == '__main__':
|
||||
success = asyncio.run(test_bot())
|
||||
if not success:
|
||||
sys.exit(1)
|
||||
0
test_connection.py
Normal file
0
test_connection.py
Normal file
Reference in New Issue
Block a user