Initial commit: Complete PetBot IRC Game

🎮 Features implemented:
- Pokemon-style pet collection and battles
- Multi-location exploration system
- Dynamic weather with background updates
- Achievement system with location unlocks
- Web dashboard for player stats
- Modular command system
- Async database with SQLite
- PM flood prevention
- Persistent player data

🌤️ Weather System:
- 6 weather types with spawn modifiers
- 30min-3hour dynamic durations
- Background task for automatic updates
- Location-specific weather patterns

🐛 Recent Bug Fixes:
- Database persistence on restart
- Player page SQLite row conversion
- Achievement count calculations
- Travel requirement messages
- Battle move color coding
- Locations page display

🔧 Generated with Claude Code
🤖 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-13 23:57:39 +01:00
commit 47f160a295
31 changed files with 6235 additions and 0 deletions

View file

@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(python3:*)",
"Bash(timeout:*)",
"Bash(ls:*)",
"Bash(kill:*)",
"Bash(cat:*)",
"Bash(pip3 install:*)",
"Bash(apt list:*)",
"Bash(curl:*)"
],
"deny": []
}
}

78
.gitignore vendored Normal file
View file

@ -0,0 +1,78 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Database files
*.db
*.sqlite
*.sqlite3
data/petbot.db
# Logs
*.log
logs/
webserver.log
# Configuration (might contain sensitive data)
config/secrets.json
config/private.json
config/settings.json
# Test files
test_*.py
*_test.py
test_output/
# Temporary files
tmp/
temp/
*.tmp
*.bak
*.backup
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IRC bot specific
*.pid
*.lock

184
README.md Normal file
View file

@ -0,0 +1,184 @@
# PetBot - IRC Pokemon-Style Pet Game Bot
A feature-rich IRC bot that brings Pokemon-style pet collecting and battling to your IRC channel! Players can catch pets, explore locations, battle wild creatures, earn achievements, and more.
## 🎮 Features
### Core Gameplay
- **Pet Collection**: Catch and collect different species of pets
- **Exploration**: Travel between various themed locations
- **Battle System**: Engage in turn-based battles with wild pets
- **Team Management**: Activate/deactivate pets, swap team members
- **Achievement System**: Unlock new areas by completing challenges
### Advanced Systems
- **Dynamic Weather**: Real-time weather system affecting spawn rates
- **Web Interface**: Modern web dashboard for player stats and pet collections
- **Location-Based Spawns**: Different pets spawn in different locations
- **Level Progression**: Pets gain experience and level up
- **Type Effectiveness**: Strategic battle system with type advantages
### Technical Features
- **Modular Architecture**: Clean, extensible codebase
- **Async Database**: SQLite with async operations
- **Background Tasks**: Automated weather updates
- **PM Flood Prevention**: Web-based responses for large data sets
- **Persistent Data**: Player progress survives bot restarts
## 🚀 Quick Start
### Prerequisites
- Python 3.8+
- SQLite3
- Network access for IRC and web server
### Installation
1. Clone the repository:
```bash
git clone https://github.com/yourusername/petbot.git
cd petbot
```
2. Install dependencies:
```bash
pip install aiosqlite
```
3. Configure the bot:
```bash
cp config/settings.example.json config/settings.json
# Edit config/settings.json with your IRC details
```
4. Run the bot:
```bash
python3 run_bot_debug.py
```
## 📋 Commands
### Player Commands
- `!start` - Begin your pet collecting journey
- `!explore` - Search for wild pets in your current location
- `!catch <pet>` - Attempt to catch a wild pet
- `!team` - View your active team
- `!pets` - View your complete collection (web link)
- `!travel <location>` - Move to a different location
- `!weather` - Check current weather effects
- `!achievements` - View your progress and unlocked achievements
### Battle Commands
- `!battle` - Start a battle with a wild pet
- `!attack <move>` - Use a move in battle
- `!flee` - Attempt to escape from battle
- `!moves` - View your active pet's moves
### Pet Management
- `!activate <pet>` - Activate a pet for battle
- `!deactivate <pet>` - Move a pet to storage
- `!swap <pet1> <pet2>` - Swap two pets' active status
## 🌍 Locations
### Available Areas
- **Starter Town**: Peaceful starting area (Fire/Water/Grass pets)
- **Whispering Woods**: Ancient forest (Grass/Nature pets)
- **Electric Canyon**: Charged valley (Electric/Rock pets)
- **Crystal Caves**: Underground caverns (Rock/Crystal pets)
- **Frozen Tundra**: Icy wasteland (Ice/Water pets)
- **Dragon's Peak**: Ultimate challenge (Fire/Rock/Ice pets)
### Unlocking Locations
Locations are unlocked by completing achievements:
- **Nature Explorer**: Catch 3 different Grass-type pets → Whispering Woods
- **Spark Collector**: Catch 2 different Electric-type pets → Electric Canyon
- **Rock Hound**: Catch 3 different Rock-type pets → Crystal Caves
- **Ice Breaker**: Catch 5 different Water/Ice-type pets → Frozen Tundra
- **Dragon Tamer**: Catch 15 pets total + 3 Fire-types → Dragon's Peak
## 🌤️ Weather System
### Weather Types & Effects
- **Sunny**: 1.5x Fire/Grass spawns (1-2 hours)
- **Rainy**: 2.0x Water spawns (45-90 minutes)
- **Thunderstorm**: 2.0x Electric spawns (30-60 minutes)
- **Blizzard**: 1.7x Ice/Water spawns (1-2 hours)
- **Earthquake**: 1.8x Rock spawns (30-90 minutes)
- **Calm**: Normal spawns (1.5-3 hours)
### Background System
- Weather updates automatically every 5 minutes
- Dynamic durations from 30 minutes to 3 hours
- Location-specific weather patterns
- Real-time spawn rate modifications
## 🌐 Web Interface
Access the web dashboard at `http://localhost:8080/`:
- **Player Profiles**: Complete stats and pet collections
- **Leaderboard**: Top players by level and achievements
- **Locations Guide**: All areas with spawn information
- **Help System**: Complete command reference
## 🔧 Architecture
### Core Components
- **Database Layer**: Async SQLite with comprehensive schema
- **Game Engine**: Core logic for pets, battles, weather
- **Module System**: Pluggable command handlers
- **Web Server**: Built-in HTTP server for dashboard
- **Battle Engine**: Turn-based combat system
### Key Files
- `src/database.py` - Database operations and schema
- `src/game_engine.py` - Core game logic and weather system
- `src/battle_engine.py` - Combat mechanics
- `modules/` - Command handler modules
- `webserver.py` - Web interface server
- `config/` - Configuration files
## 🐛 Recent Updates
### Weather System Enhancement
- ✅ Added background task for automatic weather updates
- ✅ Changed weather durations from 2-6 hours to 30min-3hours
- ✅ Implemented continuous weather coverage
- ✅ Added graceful shutdown handling
### Bug Fixes
- ✅ Fixed database persistence on bot restart
- ✅ Resolved individual player pages showing 'not found'
- ✅ Corrected achievement count displays
- ✅ Added specific travel requirement messages
- ✅ Fixed locations page display
- ✅ Implemented colored battle moves by type
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature-name`
3. Make your changes
4. Add tests if applicable
5. Commit with descriptive messages
6. Push and create a pull request
## 📝 License
This project is open source. Feel free to use, modify, and distribute.
## 🎯 Roadmap
- [ ] PvP battles between players
- [ ] Pet evolution system
- [ ] Trading between players
- [ ] Seasonal events
- [ ] More pet types and locations
- [ ] Mobile-responsive web interface
## 🐾 Support
For questions, bug reports, or feature requests, please open an issue on GitHub.
---
*Built with ❤️ for IRC communities who love Pokemon-style games!*

51
config/achievements.json Normal file
View file

@ -0,0 +1,51 @@
[
{
"name": "Nature Explorer",
"description": "Catch 3 different Grass-type pets",
"requirement_type": "catch_type",
"requirement_data": "3:Grass",
"unlock_location": "Whispering Woods"
},
{
"name": "Spark Collector",
"description": "Catch 2 different Electric-type pets",
"requirement_type": "catch_type",
"requirement_data": "2:Electric",
"unlock_location": "Electric Canyon"
},
{
"name": "Rock Hound",
"description": "Catch 3 different Rock-type pets",
"requirement_type": "catch_type",
"requirement_data": "3:Rock",
"unlock_location": "Crystal Caves"
},
{
"name": "Ice Breaker",
"description": "Catch 5 different Water or Ice-type pets",
"requirement_type": "catch_type",
"requirement_data": "5:Water",
"unlock_location": "Frozen Tundra"
},
{
"name": "Dragon Tamer",
"description": "Catch 15 pets total and have 3 Fire-type pets",
"requirement_type": "catch_total",
"requirement_data": "15",
"unlock_location": "Dragon's Peak"
},
{
"name": "Pet Collector",
"description": "Catch your first 5 pets",
"requirement_type": "catch_total",
"requirement_data": "5",
"unlock_location": null
},
{
"name": "Advanced Trainer",
"description": "Catch 10 pets total",
"requirement_type": "catch_total",
"requirement_data": "10",
"unlock_location": null
}
]

66
config/locations.json Normal file
View file

@ -0,0 +1,66 @@
[
{
"name": "Starter Town",
"description": "A peaceful town where all trainers begin their journey",
"level_min": 1,
"level_max": 3,
"spawns": [
{"species": "Leafy", "spawn_rate": 0.35, "min_level": 1, "max_level": 2},
{"species": "Flamey", "spawn_rate": 0.35, "min_level": 1, "max_level": 2},
{"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 2}
]
},
{
"name": "Whispering Woods",
"description": "Ancient woods filled with grass and nature pets",
"level_min": 2,
"level_max": 6,
"spawns": [
{"species": "Leafy", "spawn_rate": 0.5, "min_level": 2, "max_level": 5},
{"species": "Flamey", "spawn_rate": 0.2, "min_level": 3, "max_level": 4},
{"species": "Aqua", "spawn_rate": 0.3, "min_level": 2, "max_level": 4}
]
},
{
"name": "Electric Canyon",
"description": "A charged valley crackling with energy",
"level_min": 4,
"level_max": 9,
"spawns": [
{"species": "Sparky", "spawn_rate": 0.6, "min_level": 4, "max_level": 7},
{"species": "Rocky", "spawn_rate": 0.4, "min_level": 5, "max_level": 8}
]
},
{
"name": "Crystal Caves",
"description": "Deep underground caverns with rock and crystal pets",
"level_min": 6,
"level_max": 12,
"spawns": [
{"species": "Rocky", "spawn_rate": 0.7, "min_level": 6, "max_level": 10},
{"species": "Sparky", "spawn_rate": 0.3, "min_level": 7, "max_level": 9}
]
},
{
"name": "Frozen Tundra",
"description": "An icy wasteland where only the strongest survive",
"level_min": 10,
"level_max": 16,
"spawns": [
{"species": "Hydrox", "spawn_rate": 0.4, "min_level": 10, "max_level": 14},
{"species": "Rocky", "spawn_rate": 0.3, "min_level": 11, "max_level": 15},
{"species": "Sparky", "spawn_rate": 0.3, "min_level": 12, "max_level": 14}
]
},
{
"name": "Dragon's Peak",
"description": "The ultimate challenge - legendary pets roam these heights",
"level_min": 15,
"level_max": 25,
"spawns": [
{"species": "Blazeon", "spawn_rate": 0.5, "min_level": 15, "max_level": 20},
{"species": "Hydrox", "spawn_rate": 0.3, "min_level": 16, "max_level": 22},
{"species": "Rocky", "spawn_rate": 0.2, "min_level": 18, "max_level": 25}
]
}
]

56
config/moves.json Normal file
View file

@ -0,0 +1,56 @@
[
{
"name": "Tackle",
"type": "Normal",
"category": "Physical",
"power": 40,
"accuracy": 100,
"pp": 35,
"description": "A physical attack in which the user charges and slams into the target."
},
{
"name": "Ember",
"type": "Fire",
"category": "Special",
"power": 40,
"accuracy": 100,
"pp": 25,
"description": "The target is attacked with small flames."
},
{
"name": "Water Gun",
"type": "Water",
"category": "Special",
"power": 40,
"accuracy": 100,
"pp": 25,
"description": "The target is blasted with a forceful shot of water."
},
{
"name": "Vine Whip",
"type": "Grass",
"category": "Physical",
"power": 45,
"accuracy": 100,
"pp": 25,
"description": "The target is struck with slender, whiplike vines."
},
{
"name": "Thunder Shock",
"type": "Electric",
"category": "Special",
"power": 40,
"accuracy": 100,
"pp": 30,
"description": "A jolt of electricity crashes down on the target."
},
{
"name": "Rock Throw",
"type": "Rock",
"category": "Physical",
"power": 50,
"accuracy": 90,
"pp": 15,
"description": "The user picks up and hurls a rock at the target."
}
]

79
config/pets.json Normal file
View file

@ -0,0 +1,79 @@
[
{
"name": "Flamey",
"type1": "Fire",
"type2": null,
"base_hp": 45,
"base_attack": 52,
"base_defense": 43,
"base_speed": 65,
"evolution_level": null,
"rarity": 1
},
{
"name": "Aqua",
"type1": "Water",
"type2": null,
"base_hp": 44,
"base_attack": 48,
"base_defense": 65,
"base_speed": 43,
"evolution_level": null,
"rarity": 1
},
{
"name": "Leafy",
"type1": "Grass",
"type2": null,
"base_hp": 45,
"base_attack": 49,
"base_defense": 49,
"base_speed": 45,
"evolution_level": null,
"rarity": 1
},
{
"name": "Sparky",
"type1": "Electric",
"type2": null,
"base_hp": 35,
"base_attack": 55,
"base_defense": 40,
"base_speed": 90,
"evolution_level": null,
"rarity": 2
},
{
"name": "Rocky",
"type1": "Rock",
"type2": null,
"base_hp": 40,
"base_attack": 80,
"base_defense": 100,
"base_speed": 25,
"evolution_level": null,
"rarity": 2
},
{
"name": "Blazeon",
"type1": "Fire",
"type2": null,
"base_hp": 65,
"base_attack": 80,
"base_defense": 60,
"base_speed": 95,
"evolution_level": null,
"rarity": 3
},
{
"name": "Hydrox",
"type1": "Water",
"type2": "Ice",
"base_hp": 70,
"base_attack": 65,
"base_defense": 90,
"base_speed": 60,
"evolution_level": null,
"rarity": 3
}
]

View file

@ -0,0 +1,10 @@
{
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!",
"admin_users": ["your_nickname_here"],
"web_port": 8080,
"database_path": "data/petbot.db"
}

46
config/types.json Normal file
View file

@ -0,0 +1,46 @@
{
"effectiveness": {
"Fire": {
"strong_against": ["Grass", "Ice"],
"weak_against": ["Water", "Rock"],
"immune_to": [],
"resists": ["Fire", "Grass"]
},
"Water": {
"strong_against": ["Fire", "Rock"],
"weak_against": ["Electric", "Grass"],
"immune_to": [],
"resists": ["Water", "Fire", "Ice"]
},
"Grass": {
"strong_against": ["Water", "Rock"],
"weak_against": ["Fire", "Ice"],
"immune_to": [],
"resists": ["Water", "Electric", "Grass"]
},
"Electric": {
"strong_against": ["Water"],
"weak_against": ["Rock"],
"immune_to": [],
"resists": ["Electric"]
},
"Rock": {
"strong_against": ["Fire", "Electric"],
"weak_against": ["Water", "Grass"],
"immune_to": [],
"resists": ["Fire", "Normal"]
},
"Ice": {
"strong_against": ["Grass"],
"weak_against": ["Fire", "Rock"],
"immune_to": [],
"resists": ["Ice"]
},
"Normal": {
"strong_against": [],
"weak_against": ["Rock"],
"immune_to": [],
"resists": []
}
}
}

View file

@ -0,0 +1,48 @@
{
"weather_types": {
"Sunny": {
"description": "Bright sunshine increases Fire and Grass-type spawns",
"spawn_modifier": 1.5,
"affected_types": ["Fire", "Grass"],
"duration_minutes": [60, 120]
},
"Rainy": {
"description": "Heavy rain boosts Water-type spawns significantly",
"spawn_modifier": 2.0,
"affected_types": ["Water"],
"duration_minutes": [45, 90]
},
"Thunderstorm": {
"description": "Electric storms double Electric-type spawn rates",
"spawn_modifier": 2.0,
"affected_types": ["Electric"],
"duration_minutes": [30, 60]
},
"Blizzard": {
"description": "Harsh snowstorm increases Ice and Water-type spawns",
"spawn_modifier": 1.7,
"affected_types": ["Ice", "Water"],
"duration_minutes": [60, 120]
},
"Earthquake": {
"description": "Ground tremors bring Rock-type pets to the surface",
"spawn_modifier": 1.8,
"affected_types": ["Rock"],
"duration_minutes": [30, 90]
},
"Calm": {
"description": "Perfect weather with normal spawn rates",
"spawn_modifier": 1.0,
"affected_types": [],
"duration_minutes": [90, 180]
}
},
"location_weather_chances": {
"Starter Town": ["Sunny", "Calm", "Rainy"],
"Whispering Woods": ["Sunny", "Rainy", "Calm"],
"Electric Canyon": ["Thunderstorm", "Sunny", "Calm"],
"Crystal Caves": ["Earthquake", "Calm"],
"Frozen Tundra": ["Blizzard", "Calm"],
"Dragon's Peak": ["Thunderstorm", "Sunny", "Calm"]
}
}

525
help.html Normal file
View file

@ -0,0 +1,525 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PetBot IRC Commands Reference</title>
<style>
:root {
--bg-primary: #0f0f23;
--bg-secondary: #1e1e3f;
--bg-tertiary: #2a2a4a;
--text-primary: #cccccc;
--text-secondary: #999999;
--text-accent: #66ff66;
--border-color: #333366;
--hover-color: #3a3a5a;
--gradient-primary: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--gradient-secondary: linear-gradient(135deg, #ff6b6b 0%, #feca57 100%);
--gradient-tertiary: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
--shadow-dark: 0 4px 20px rgba(0,0,0,0.3);
--shadow-glow: 0 0 20px rgba(102, 255, 102, 0.2);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', 'Roboto', 'Arial', sans-serif;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
background-image:
radial-gradient(circle at 20% 50%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(120, 198, 119, 0.1) 0%, transparent 50%);
min-height: 100vh;
}
.header {
text-align: center;
background: var(--gradient-primary);
color: white;
padding: 40px;
border-radius: 20px;
margin-bottom: 40px;
box-shadow: var(--shadow-dark);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Ccircle cx='30' cy='30' r='4'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
z-index: 0;
}
.header > * {
position: relative;
z-index: 1;
}
.header h1 {
margin: 0;
font-size: 3em;
font-weight: 700;
background: linear-gradient(45deg, #fff, #66ff66);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header p {
margin: 15px 0 0 0;
opacity: 0.9;
font-size: 1.3em;
font-weight: 300;
}
.section {
background: var(--bg-secondary);
margin-bottom: 30px;
border-radius: 15px;
overflow: hidden;
box-shadow: var(--shadow-dark);
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.section:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-glow);
}
.section-header {
background: var(--gradient-tertiary);
color: white;
padding: 20px 25px;
font-size: 1.4em;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.command-grid {
padding: 25px;
display: grid;
gap: 20px;
}
.command {
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
transition: all 0.3s ease;
background: var(--bg-tertiary);
}
.command:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(102, 255, 102, 0.15);
border-color: var(--text-accent);
}
.command-name {
background: var(--bg-primary);
padding: 15px 20px;
font-family: 'Fira Code', 'Courier New', monospace;
font-weight: bold;
color: var(--text-accent);
border-bottom: 1px solid var(--border-color);
font-size: 1.2em;
text-shadow: 0 0 10px rgba(102, 255, 102, 0.3);
}
.command-desc {
padding: 20px;
line-height: 1.7;
color: var(--text-primary);
}
.command-example {
background: var(--bg-primary);
padding: 12px 20px;
font-family: 'Fira Code', 'Courier New', monospace;
color: var(--text-secondary);
border-top: 1px solid var(--border-color);
font-size: 0.95em;
}
.locations-list, .status-list {
background: var(--bg-tertiary);
padding: 20px;
border-radius: 12px;
margin: 20px 0;
border: 1px solid var(--border-color);
}
.locations-list h4, .status-list h4 {
margin: 0 0 15px 0;
color: var(--text-accent);
font-size: 1.1em;
font-weight: 600;
}
.locations-list ul, .status-list ul {
margin: 0;
padding-left: 25px;
}
.locations-list li, .status-list li {
margin: 8px 0;
color: var(--text-primary);
}
.locations-list strong, .status-list strong {
color: var(--text-accent);
}
.footer {
text-align: center;
margin-top: 50px;
padding: 30px;
background: var(--bg-secondary);
border-radius: 15px;
color: var(--text-secondary);
box-shadow: var(--shadow-dark);
border: 1px solid var(--border-color);
}
.tip {
background: var(--bg-tertiary);
border: 1px solid var(--text-accent);
padding: 20px;
border-radius: 12px;
margin: 20px 0;
box-shadow: 0 0 15px rgba(102, 255, 102, 0.1);
}
.tip strong {
color: var(--text-accent);
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: var(--bg-primary);
color: var(--text-accent);
font-weight: 600;
}
tr:hover {
background: var(--hover-color);
}
code {
background: var(--bg-primary);
color: var(--text-accent);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Fira Code', 'Courier New', monospace;
font-size: 0.9em;
}
.new-badge {
background: var(--gradient-secondary);
color: white;
font-size: 0.7em;
padding: 3px 8px;
border-radius: 12px;
margin-left: 10px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.pm-indicator {
background: rgba(102, 255, 102, 0.2);
color: var(--text-accent);
font-size: 0.8em;
padding: 2px 8px;
border-radius: 8px;
margin-left: 10px;
border: 1px solid var(--text-accent);
}
</style>
</head>
<body>
<div class="header">
<h1>🐾 PetBot Command Reference</h1>
<p>Complete guide to IRC pet collection and battle commands</p>
<p><em>Connect to irc.libera.chat #petz to play!</em></p>
</div>
<div class="section">
<div class="section-header">🎮 Getting Started</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!start</div>
<div class="command-desc">Begin your pet journey! Creates your trainer account and gives you a starter pet in Starter Town.</div>
<div class="command-example">Example: !start</div>
</div>
<div class="command">
<div class="command-name">!help</div>
<div class="command-desc">Display a quick list of available commands in the IRC channel.</div>
<div class="command-example">Example: !help</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🔍 Exploration & Travel</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!explore</div>
<div class="command-desc">Explore your current location to find wild pets. Weather affects what types of pets you'll encounter!</div>
<div class="command-example">Example: !explore</div>
</div>
<div class="command">
<div class="command-name">!location (or !where)</div>
<div class="command-desc">See where you currently are, including the location description.</div>
<div class="command-example">Example: !location</div>
</div>
<div class="command">
<div class="command-name">!travel &lt;location&gt;</div>
<div class="command-desc">Travel to a different location. Some locations require achievements to unlock!</div>
<div class="command-example">Example: !travel Whispering Woods</div>
</div>
<div class="command">
<div class="command-name">!wild &lt;location&gt;</div>
<div class="command-desc">Check what types of pets can be found in a specific location.</div>
<div class="command-example">Example: !wild Electric Canyon</div>
</div>
</div>
<div class="locations-list">
<h4>Available Locations:</h4>
<ul>
<li><strong>Starter Town</strong> - Where all trainers begin (always accessible)</li>
<li><strong>Whispering Woods</strong> - Unlocked by catching 3 different Grass-type pets</li>
<li><strong>Electric Canyon</strong> - Unlocked by catching 2 different Electric-type pets</li>
<li><strong>Crystal Caves</strong> - Unlocked by catching 3 different Rock-type pets</li>
<li><strong>Frozen Tundra</strong> - Unlocked by catching 5 different Water/Ice-type pets</li>
<li><strong>Dragon's Peak</strong> - Unlocked by catching 15 pets total and having 3 Fire-type pets</li>
</ul>
</div>
</div>
<div class="section">
<div class="section-header">⚔️ Battle System</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!battle</div>
<div class="command-desc">Start a battle with a wild pet you encountered during exploration. Strategic combat with type advantages!</div>
<div class="command-example">Example: !battle</div>
</div>
<div class="command">
<div class="command-name">!attack &lt;move&gt;</div>
<div class="command-desc">Use a specific move during battle. Each pet has different moves based on their type.</div>
<div class="command-example">Example: !attack Ember</div>
</div>
<div class="command">
<div class="command-name">!flee</div>
<div class="command-desc">Attempt to escape from battle. Success depends on your pet's speed vs the wild pet's speed.</div>
<div class="command-example">Example: !flee</div>
</div>
<div class="command">
<div class="command-name">!moves</div>
<div class="command-desc">View your active pet's available moves (up to 4 moves). Shows move type and power for battle planning.</div>
<div class="command-example">Example: !moves</div>
</div>
<div class="command">
<div class="command-name">!catch (or !capture)</div>
<div class="command-desc">Try to catch a pet during exploration OR during battle. During battle, weaker pets (lower HP) have significantly higher catch rates - up to 90% for nearly defeated pets! Both !catch and !capture work identically.</div>
<div class="command-example">Example: !catch or !capture</div>
</div>
</div>
<div class="tip">
<strong>Battle Strategy:</strong> Use type advantages! Water beats Fire, Fire beats Grass, Grass beats Water, Electric beats Water, Rock beats Fire and Electric. Weaken wild pets in battle to increase catch rate!
</div>
</div>
<div class="section">
<div class="section-header">🐾 Pet Management</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!team</div>
<div class="command-desc">View all your pets with active pets marked by ⭐. Shows levels, HP, and storage status.</div>
<div class="command-example">Example: !team</div>
</div>
<div class="command">
<div class="command-name">!stats</div>
<div class="command-desc">View your player statistics including level, experience, and money.</div>
<div class="command-example">Example: !stats</div>
</div>
<div class="command">
<div class="command-name">!activate &lt;pet&gt; <span class="pm-indicator">PM ONLY</span></div>
<div class="command-desc">Activate a pet for battle by nickname or species name. Only inactive pets can be activated. This command only works in private messages to prevent channel spam.</div>
<div class="command-example">Example: /msg PetBot !activate Sparky<br>Example: /msg PetBot !activate Pikachu</div>
</div>
<div class="command">
<div class="command-name">!deactivate &lt;pet&gt; <span class="pm-indicator">PM ONLY</span></div>
<div class="command-desc">Deactivate an active pet, removing it from battle readiness. This command only works in private messages to prevent channel spam.</div>
<div class="command-example">Example: /msg PetBot !deactivate Sparky<br>Example: /msg PetBot !deactivate Pikachu</div>
</div>
<div class="command">
<div class="command-name">!swap &lt;pet1&gt; &lt;pet2&gt; <span class="pm-indicator">PM ONLY</span></div>
<div class="command-desc">Swap activation status between two pets. The first pet becomes inactive, the second becomes active. This command only works in private messages to prevent channel spam.</div>
<div class="command-example">Example: /msg PetBot !swap Sparky Flame<br>Example: /msg PetBot !swap Pikachu Charmander</div>
</div>
</div>
<div class="tip">
<strong>Pet Management Tips:</strong> You can only have a limited number of active pets at once. Use !team to see which pets are active (⭐). Pet management commands (!activate, !deactivate, !swap) must be sent as private messages to the bot to prevent channel spam. Use <code>/msg PetBot &lt;command&gt;</code> format.
</div>
</div>
<div class="section">
<div class="section-header">🏆 Achievements & Progress</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!achievements</div>
<div class="command-desc">View your earned achievements and progress. Achievements unlock new locations!</div>
<div class="command-example">Example: !achievements</div>
</div>
</div>
<div class="locations-list">
<h4>Key Achievements:</h4>
<ul>
<li><strong>Pet Collector</strong> - Catch your first 5 pets</li>
<li><strong>Advanced Trainer</strong> - Catch 10 pets total</li>
<li><strong>Nature Explorer</strong> - Catch 3 different Grass-type pets (unlocks Whispering Woods)</li>
<li><strong>Spark Collector</strong> - Catch 2 different Electric-type pets (unlocks Electric Canyon)</li>
<li><strong>Rock Hound</strong> - Catch 3 different Rock-type pets (unlocks Crystal Caves)</li>
<li><strong>Ice Breaker</strong> - Catch 5 different Water/Ice-type pets (unlocks Frozen Tundra)</li>
<li><strong>Dragon Tamer</strong> - Catch 15 pets total + 3 Fire types (unlocks Dragon's Peak)</li>
</ul>
</div>
</div>
<div class="section">
<div class="section-header">🌤️ Weather System</div>
<div class="command-grid">
<div class="command">
<div class="command-name">!weather</div>
<div class="command-desc">Check the current weather in your location and its effects on pet spawns.</div>
<div class="command-example">Example: !weather</div>
</div>
</div>
<div class="status-list">
<h4>Weather Effects:</h4>
<ul>
<li><strong>Sunny</strong> - 1.5x Fire and Grass-type spawns</li>
<li><strong>Rainy</strong> - 2.0x Water-type spawns</li>
<li><strong>Thunderstorm</strong> - 2.0x Electric-type spawns</li>
<li><strong>Blizzard</strong> - 1.7x Ice and Water-type spawns</li>
<li><strong>Earthquake</strong> - 1.8x Rock-type spawns</li>
<li><strong>Calm</strong> - Normal spawn rates for all types</li>
</ul>
</div>
</div>
<div class="section">
<div class="section-header">📚 Game Mechanics</div>
<div class="command-grid">
<div style="grid-column: 1 / -1;">
<div class="tip">
<strong>How to Play:</strong>
<ol style="margin: 10px 0 0 20px; padding: 0;">
<li>Use <code>!start</code> to create your trainer and get a starter pet</li>
<li>Use <code>!explore</code> to find wild pets in your current location</li>
<li>Choose to <code>!battle</code> the wild pet (recommended) or <code>!catch</code> directly</li>
<li>In battle, use <code>!attack &lt;move&gt;</code> to weaken the wild pet</li>
<li>Use <code>!catch</code> (or <code>!capture</code>) during battle for much higher success rates on damaged pets</li>
<li>Battle-catch rates: 30% base + up to 50% bonus for low HP (90% max for nearly defeated pets)</li>
<li>Collect different types of pets to unlock achievements and new locations</li>
<li>Use <code>!travel</code> to explore new areas as you unlock them</li>
<li>Check <code>!weather</code> for optimal catching conditions</li>
</ol>
</div>
</div>
</div>
</div>
<div class="section">
<div class="section-header">🔧 Type Effectiveness Chart</div>
<div class="command-grid">
<div style="grid-column: 1 / -1;">
<table>
<thead>
<tr>
<th>Attacking Type</th>
<th>Strong Against (2x)</th>
<th>Weak Against (0.5x)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Fire</strong></td>
<td>Grass, Ice</td>
<td>Water, Rock</td>
</tr>
<tr>
<td><strong>Water</strong></td>
<td>Fire, Rock</td>
<td>Electric, Grass</td>
</tr>
<tr>
<td><strong>Grass</strong></td>
<td>Water, Rock</td>
<td>Fire, Ice</td>
</tr>
<tr>
<td><strong>Electric</strong></td>
<td>Water</td>
<td>Rock</td>
</tr>
<tr>
<td><strong>Rock</strong></td>
<td>Fire, Electric</td>
<td>Water, Grass</td>
</tr>
<tr>
<td><strong>Normal</strong></td>
<td>None</td>
<td>Rock</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="footer">
<p><strong>PetBot v1.1</strong> - A complete IRC pet collection and battle game</p>
<p>Created for Libera Chat #petz | Last updated: July 13, 2025</p>
<p><em>Join the adventure and become the ultimate pet trainer!</em></p>
</div>
</body>
</html>

18
modules/__init__.py Normal file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env python3
"""PetBot modules package"""
from .core_commands import CoreCommands
from .exploration import Exploration
from .battle_system import BattleSystem
from .pet_management import PetManagement
from .achievements import Achievements
from .admin import Admin
__all__ = [
'CoreCommands',
'Exploration',
'BattleSystem',
'PetManagement',
'Achievements',
'Admin'
]

32
modules/achievements.py Normal file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Achievements commands module for PetBot"""
from .base_module import BaseModule
class Achievements(BaseModule):
"""Handles achievements display and tracking"""
def get_commands(self):
return ["achievements"]
async def handle_command(self, channel, nickname, command, args):
if command == "achievements":
await self.cmd_achievements(channel, nickname)
async def cmd_achievements(self, channel, nickname):
"""Show player achievements"""
player = await self.require_player(channel, nickname)
if not player:
return
achievements = await self.database.get_player_achievements(player["id"])
if achievements:
self.send_message(channel, f"🏆 {nickname}'s Achievements:")
for achievement in achievements[:5]: # Show last 5 achievements
self.send_message(channel, f"{achievement['name']}: {achievement['description']}")
if len(achievements) > 5:
self.send_message(channel, f"... and {len(achievements) - 5} more!")
else:
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")

30
modules/admin.py Normal file
View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Admin commands module for PetBot"""
from .base_module import BaseModule
class Admin(BaseModule):
"""Handles admin-only commands like reload"""
def get_commands(self):
return ["reload"]
async def handle_command(self, channel, nickname, command, args):
if command == "reload":
await self.cmd_reload(channel, nickname)
async def cmd_reload(self, channel, nickname):
"""Reload bot modules (megasconed only)"""
if nickname.lower() != "megasconed":
self.send_message(channel, f"{nickname}: Access denied. Admin command.")
return
try:
# Trigger module reload in main bot
success = await self.bot.reload_modules()
if success:
self.send_message(channel, f"{nickname}: ✅ Modules reloaded successfully!")
else:
self.send_message(channel, f"{nickname}: ❌ Module reload failed!")
except Exception as e:
self.send_message(channel, f"{nickname}: ❌ Reload error: {str(e)}")

43
modules/base_module.py Normal file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Base module class for PetBot command modules"""
import asyncio
from abc import ABC, abstractmethod
class BaseModule(ABC):
"""Base class for all PetBot modules"""
def __init__(self, bot, database, game_engine):
self.bot = bot
self.database = database
self.game_engine = game_engine
@abstractmethod
def get_commands(self):
"""Return list of commands this module handles"""
pass
@abstractmethod
async def handle_command(self, channel, nickname, command, args):
"""Handle a command for this module"""
pass
def send_message(self, target, message):
"""Send message through the bot"""
self.bot.send_message(target, message)
def send_pm(self, nickname, message):
"""Send private message to user"""
self.bot.send_message(nickname, message)
async def get_player(self, nickname):
"""Get player from database"""
return await self.database.get_player(nickname)
async def require_player(self, channel, nickname):
"""Get player or send start message if not found"""
player = await self.get_player(nickname)
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return None
return player

192
modules/battle_system.py Normal file
View file

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Battle system commands module for PetBot"""
from .base_module import BaseModule
class BattleSystem(BaseModule):
"""Handles battle, attack, flee, and moves commands"""
# IRC color codes for different move types
MOVE_TYPE_COLORS = {
'Fire': '\x0304', # Red
'Water': '\x0312', # Light Blue
'Grass': '\x0303', # Green
'Electric': '\x0308', # Yellow
'Rock': '\x0307', # Orange/Brown
'Normal': '\x0314', # Gray
'Physical': '\x0313', # Pink/Magenta (fallback for category)
'Special': '\x0310' # Cyan (fallback for category)
}
def get_commands(self):
return ["battle", "attack", "flee", "moves"]
def get_move_color(self, move_type):
"""Get IRC color code for a move type"""
return self.MOVE_TYPE_COLORS.get(move_type, '\x0312') # Default to light blue
async def handle_command(self, channel, nickname, command, args):
if command == "battle":
await self.cmd_battle(channel, nickname)
elif command == "attack":
await self.cmd_attack(channel, nickname, args)
elif command == "flee":
await self.cmd_flee(channel, nickname)
elif command == "moves":
await self.cmd_moves(channel, nickname)
async def cmd_battle(self, channel, nickname):
"""Start a battle with encountered wild pet"""
player = await self.require_player(channel, nickname)
if not player:
return
# Check if player has an active encounter
if player["id"] not in self.bot.active_encounters:
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet to battle!")
return
# Check if already in battle
active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"])
if active_battle:
self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.")
return
# Get player's active pet
pets = await self.database.get_player_pets(player["id"], active_only=True)
if not pets:
self.send_message(channel, f"{nickname}: You need an active pet to battle! Use !team to check your pets.")
return
player_pet = pets[0] # Use first active pet
wild_pet = self.bot.active_encounters[player["id"]]
# Start battle
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, wild_pet)
# Condensed battle start message with all info
moves_colored = " | ".join([
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
for move in battle["available_moves"]
])
battle_start_msg = (f"⚔️ {nickname}: Battle! Your {player_pet['species_name']} (Lv.{player_pet['level']}, "
f"{battle['player_hp']}/{player_pet['max_hp']} HP) vs Wild {wild_pet['species_name']} "
f"(Lv.{wild_pet['level']}, {battle['wild_hp']}/{wild_pet['max_hp']} HP)")
self.send_message(channel, battle_start_msg)
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move>")
async def cmd_attack(self, channel, nickname, args):
"""Use a move in battle"""
if not args:
self.send_message(channel, f"{nickname}: Specify a move to use! Example: !attack Tackle")
return
player = await self.require_player(channel, nickname)
if not player:
return
move_name = " ".join(args).title() # Normalize to Title Case
result = await self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name)
if "error" in result:
self.send_message(channel, f"{nickname}: {result['error']}")
return
# Display battle results - condensed with player names for clarity
for action in result["results"]:
# Determine attacker and target with player context
if "Wild" in action['attacker']:
attacker = f"Wild {action['attacker']}"
target_context = f"{nickname}"
else:
attacker = f"{nickname}'s {action['attacker']}"
target_context = "wild pet"
# Build condensed message with all info on one line
if action["damage"] > 0:
effectiveness_msgs = {
"super_effective": "⚡ Super effective!",
"not_very_effective": "💫 Not very effective...",
"no_effect": "❌ No effect!",
"super_effective_critical": "💥 CRIT! Super effective!",
"normal_critical": "💥 Critical hit!",
"not_very_effective_critical": "💥 Crit, not very effective..."
}
effectiveness = effectiveness_msgs.get(action["effectiveness"], "")
battle_msg = f"⚔️ {attacker} used {action['move']}{action['damage']} damage to {target_context}! {effectiveness} (HP: {action['target_hp']})"
else:
# Status move or no damage
battle_msg = f"⚔️ {attacker} used {action['move']} on {target_context}! (HP: {action['target_hp']})"
self.send_message(channel, battle_msg)
if result["battle_over"]:
if result["winner"] == "player":
self.send_message(channel, f"🎉 {nickname}: You won the battle!")
# Remove encounter since battle is over
if player["id"] in self.bot.active_encounters:
del self.bot.active_encounters[player["id"]]
else:
self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...")
# Remove encounter
if player["id"] in self.bot.active_encounters:
del self.bot.active_encounters[player["id"]]
else:
# Battle continues - show available moves with type-based colors
moves_colored = " | ".join([
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
for move in result["available_moves"]
])
self.send_message(channel, f"🎯 {nickname}'s turn! Moves: {moves_colored}")
async def cmd_flee(self, channel, nickname):
"""Attempt to flee from battle"""
player = await self.require_player(channel, nickname)
if not player:
return
success = await self.game_engine.battle_engine.flee_battle(player["id"])
if success:
self.send_message(channel, f"💨 {nickname} successfully fled from battle!")
# Remove encounter
if player["id"] in self.bot.active_encounters:
del self.bot.active_encounters[player["id"]]
else:
self.send_message(channel, f"{nickname} couldn't escape! Battle continues!")
async def cmd_moves(self, channel, nickname):
"""Show active pet's available moves"""
player = await self.require_player(channel, nickname)
if not player:
return
# Get player's active pets
pets = await self.database.get_player_pets(player["id"], active_only=True)
if not pets:
self.send_message(channel, f"{nickname}: You don't have any active pets! Use !team to check your pets.")
return
active_pet = pets[0] # Use first active pet
available_moves = self.game_engine.battle_engine.get_available_moves(active_pet)
if not available_moves:
self.send_message(channel, f"{nickname}: Your {active_pet['species_name']} has no available moves!")
return
# Limit to 4 moves max and format compactly
moves_to_show = available_moves[:4]
move_info = []
for move in moves_to_show:
power = move.get('power', 'Status')
power_str = str(power) if power != 'Status' else 'Stat'
move_colored = f"{self.get_move_color(move['type'])}{move['name']}\x0F"
move_info.append(f"{move_colored} ({move['type']}, {power_str})")
pet_name = active_pet["nickname"] or active_pet["species_name"]
moves_line = " | ".join(move_info)
self.send_message(channel, f"🎯 {nickname}'s {pet_name}: {moves_line}")

44
modules/core_commands.py Normal file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Core commands module for PetBot"""
from .base_module import BaseModule
class CoreCommands(BaseModule):
"""Handles basic bot commands like start, help, stats"""
def get_commands(self):
return ["help", "start", "stats"]
async def handle_command(self, channel, nickname, command, args):
if command == "help":
await self.cmd_help(channel, nickname)
elif command == "start":
await self.cmd_start(channel, nickname)
elif command == "stats":
await self.cmd_stats(channel, nickname, args)
async def cmd_help(self, channel, nickname):
"""Send help URL to prevent rate limiting"""
self.send_message(channel, f"{nickname}: Complete command reference available at: http://localhost:8080/help")
async def cmd_start(self, channel, nickname):
"""Start a new player"""
player = await self.get_player(nickname)
if player:
self.send_message(channel, f"{nickname}: You already have an account! Use !team to see your pets.")
return
player_id = await self.database.create_player(nickname)
starter_pet = await self.game_engine.give_starter_pet(player_id)
self.send_message(channel,
f"🎉 {nickname}: Welcome to the world of pets! You received a Level {starter_pet['level']} {starter_pet['species_name']}! You are now in Starter Town.")
async def cmd_stats(self, channel, nickname, args):
"""Show player statistics"""
player = await self.require_player(channel, nickname)
if not player:
return
self.send_message(channel,
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")

225
modules/exploration.py Normal file
View file

@ -0,0 +1,225 @@
#!/usr/bin/env python3
"""Exploration commands module for PetBot"""
from .base_module import BaseModule
class Exploration(BaseModule):
"""Handles exploration, travel, location, weather, and wild commands"""
def get_commands(self):
return ["explore", "travel", "location", "where", "weather", "wild", "catch", "capture"]
async def handle_command(self, channel, nickname, command, args):
if command == "explore":
await self.cmd_explore(channel, nickname)
elif command == "travel":
await self.cmd_travel(channel, nickname, args)
elif command in ["location", "where"]:
await self.cmd_location(channel, nickname)
elif command == "weather":
await self.cmd_weather(channel, nickname)
elif command == "wild":
await self.cmd_wild(channel, nickname, args)
elif command in ["catch", "capture"]:
await self.cmd_catch(channel, nickname)
async def cmd_explore(self, channel, nickname):
"""Explore current location"""
player = await self.require_player(channel, nickname)
if not player:
return
encounter = await self.game_engine.explore_location(player["id"])
if encounter["type"] == "error":
self.send_message(channel, f"{nickname}: {encounter['message']}")
elif encounter["type"] == "empty":
self.send_message(channel, f"🔍 {nickname}: {encounter['message']}")
elif encounter["type"] == "encounter":
# Store the encounter for potential catching
self.bot.active_encounters[player["id"]] = encounter["pet"]
pet = encounter["pet"]
type_str = pet["type1"]
if pet["type2"]:
type_str += f"/{pet['type2']}"
self.send_message(channel,
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!")
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
async def cmd_travel(self, channel, nickname, args):
"""Travel to a different location"""
if not args:
self.send_message(channel, f"{nickname}: Specify where to travel! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
return
player = await self.require_player(channel, nickname)
if not player:
return
destination = " ".join(args).title() # Normalize to Title Case
location = await self.database.get_location_by_name(destination)
if not location:
self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!")
return
# Check if player can access this location
missing_requirements = await self.database.get_missing_location_requirements(player["id"], location["id"])
if missing_requirements:
# Build specific message about required achievements
if len(missing_requirements) == 1:
achievement = missing_requirements[0]
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Required achievement: '{achievement['name']}' - {achievement['description']}")
else:
achievement_names = [f"'{req['name']}'" for req in missing_requirements]
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Required achievements: {', '.join(achievement_names)}. Use !achievements to see progress.")
return
# Clear any active encounters when traveling
if player["id"] in self.bot.active_encounters:
del self.bot.active_encounters[player["id"]]
await self.database.update_player_location(player["id"], location["id"])
# Show weather info
weather = await self.database.get_location_weather(location["id"])
weather_msg = ""
if weather:
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
weather_msg = f" Weather: {weather['weather_type']} - {weather_desc}"
self.send_message(channel, f"🗺️ {nickname}: You traveled to {destination}. {location['description']}{weather_msg}")
async def cmd_location(self, channel, nickname):
"""Show current location"""
player = await self.require_player(channel, nickname)
if not player:
return
location = await self.database.get_player_location(player["id"])
if location:
self.send_message(channel, f"📍 {nickname}: You are currently in {location['name']}. {location['description']}")
else:
self.send_message(channel, f"{nickname}: You seem to be lost! Contact an admin.")
async def cmd_weather(self, channel, nickname):
"""Show current weather"""
player = await self.require_player(channel, nickname)
if not player:
return
location = await self.database.get_player_location(player["id"])
if not location:
self.send_message(channel, f"{nickname}: You seem to be lost!")
return
weather = await self.database.get_location_weather(location["id"])
if weather:
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
self.send_message(channel, f"🌤️ {nickname}: Current weather in {location['name']}: {weather['weather_type']}")
self.send_message(channel, f"Effect: {weather_desc}")
else:
self.send_message(channel, f"🌤️ {nickname}: The weather in {location['name']} is calm with no special effects.")
async def cmd_wild(self, channel, nickname, args):
"""Show wild pets in location (defaults to current)"""
player = await self.require_player(channel, nickname)
if not player:
return
if args:
# Specific location requested
location_name = " ".join(args).title()
else:
# Default to current location
current_location = await self.database.get_player_location(player["id"])
if not current_location:
self.send_message(channel, f"{nickname}: You seem to be lost!")
return
location_name = current_location["name"]
wild_pets = await self.game_engine.get_location_spawns(location_name)
if wild_pets:
pet_list = ", ".join([pet["name"] for pet in wild_pets])
self.send_message(channel, f"🌿 Wild pets in {location_name}: {pet_list}")
else:
self.send_message(channel, f"{nickname}: No location found called '{location_name}'")
async def cmd_catch(self, channel, nickname):
"""Catch a pet during exploration or battle"""
player = await self.require_player(channel, nickname)
if not player:
return
# Check if player is in an active battle
active_battle = await self.game_engine.battle_engine.get_active_battle(player["id"])
if active_battle:
# Catching during battle
wild_pet = active_battle["wild_pet"]
current_hp = active_battle["wild_hp"]
max_hp = wild_pet["max_hp"]
# Calculate catch rate based on remaining HP (lower HP = higher catch rate)
hp_percentage = current_hp / max_hp
base_catch_rate = 0.3 # Lower base rate than direct catch
hp_bonus = (1.0 - hp_percentage) * 0.5 # Up to 50% bonus for low HP
final_catch_rate = min(0.9, base_catch_rate + hp_bonus) # Cap at 90%
import random
if random.random() < final_catch_rate:
# Successful catch during battle
result = await self.game_engine.attempt_catch_current_location(player["id"], wild_pet)
# End the battle
await_result = await self.game_engine.battle_engine.end_battle(player["id"], "caught")
# Check for achievements
type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "")
total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "")
all_achievements = type_achievements + total_achievements
if all_achievements:
for achievement in all_achievements:
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
# Remove encounter
if player["id"] in self.bot.active_encounters:
del self.bot.active_encounters[player["id"]]
self.send_message(channel, f"🎯 {nickname}: Caught the {wild_pet['species_name']} during battle! (HP: {current_hp}/{max_hp})")
else:
# Failed catch - battle continues
catch_percentage = int(final_catch_rate * 100)
self.send_message(channel, f"🎯 {nickname}: The {wild_pet['species_name']} broke free! ({catch_percentage}% catch rate) Battle continues!")
return
# Regular exploration catch (not in battle)
if player["id"] not in self.bot.active_encounters:
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet, or be in battle to catch!")
return
target_pet = self.bot.active_encounters[player["id"]]
result = await self.game_engine.attempt_catch_current_location(player["id"], target_pet)
# Check for achievements after successful catch
if "Success!" in result:
type_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_type", "")
total_achievements = await self.game_engine.check_and_award_achievements(player["id"], "catch_total", "")
all_achievements = type_achievements + total_achievements
if all_achievements:
for achievement in all_achievements:
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
# Remove the encounter regardless of success
del self.bot.active_encounters[player["id"]]
self.send_message(channel, f"🎯 {nickname}: {result}")

149
modules/pet_management.py Normal file
View file

@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Pet management commands module for PetBot"""
from .base_module import BaseModule
class PetManagement(BaseModule):
"""Handles team, pets, and future pet management commands"""
def get_commands(self):
return ["team", "pets", "activate", "deactivate", "swap"]
async def handle_command(self, channel, nickname, command, args):
if command == "team":
await self.cmd_team(channel, nickname)
elif command == "pets":
await self.cmd_pets(channel, nickname)
elif command == "activate":
await self.cmd_activate(channel, nickname, args)
elif command == "deactivate":
await self.cmd_deactivate(channel, nickname, args)
elif command == "swap":
await self.cmd_swap(channel, nickname, args)
async def cmd_team(self, channel, nickname):
"""Show active pets (channel display)"""
player = await self.require_player(channel, nickname)
if not player:
return
pets = await self.database.get_player_pets(player["id"], active_only=False)
if not pets:
self.send_message(channel, f"{nickname}: You don't have any pets! Use !catch to find some.")
return
# Show active pets first, then others
active_pets = [pet for pet in pets if pet.get("is_active")]
inactive_pets = [pet for pet in pets if not pet.get("is_active")]
team_info = []
# Active pets with star
for pet in active_pets:
name = pet["nickname"] or pet["species_name"]
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
# Inactive pets
for pet in inactive_pets[:5]: # Show max 5 inactive
name = pet["nickname"] or pet["species_name"]
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
if len(inactive_pets) > 5:
team_info.append(f"... and {len(inactive_pets) - 5} more in storage")
self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info))
async def cmd_pets(self, channel, nickname):
"""Show link to pet collection web page"""
player = await self.require_player(channel, nickname)
if not player:
return
# Send URL to player's profile page instead of PM spam
self.send_message(channel, f"{nickname}: View your complete pet collection at: http://localhost:8080/player/{nickname}")
async def cmd_activate(self, channel, nickname, args):
"""Activate a pet for battle (PM only)"""
# Redirect to PM for privacy
if not args:
self.send_pm(nickname, "Usage: !activate <pet_name>")
self.send_message(channel, f"{nickname}: Pet activation instructions sent via PM!")
return
player = await self.require_player(channel, nickname)
if not player:
return
pet_name = " ".join(args)
result = await self.database.activate_pet(player["id"], pet_name)
if result["success"]:
pet = result["pet"]
display_name = pet["nickname"] or pet["species_name"]
self.send_pm(nickname, f"{display_name} is now active for battle!")
self.send_message(channel, f"{nickname}: Pet activated successfully!")
else:
self.send_pm(nickname, f"{result['error']}")
self.send_message(channel, f"{nickname}: Pet activation failed - check PM for details!")
async def cmd_deactivate(self, channel, nickname, args):
"""Deactivate a pet to storage (PM only)"""
# Redirect to PM for privacy
if not args:
self.send_pm(nickname, "Usage: !deactivate <pet_name>")
self.send_message(channel, f"{nickname}: Pet deactivation instructions sent via PM!")
return
player = await self.require_player(channel, nickname)
if not player:
return
pet_name = " ".join(args)
result = await self.database.deactivate_pet(player["id"], pet_name)
if result["success"]:
pet = result["pet"]
display_name = pet["nickname"] or pet["species_name"]
self.send_pm(nickname, f"📦 {display_name} moved to storage!")
self.send_message(channel, f"{nickname}: Pet deactivated successfully!")
else:
self.send_pm(nickname, f"{result['error']}")
self.send_message(channel, f"{nickname}: Pet deactivation failed - check PM for details!")
async def cmd_swap(self, channel, nickname, args):
"""Swap active/storage status of two pets (PM only)"""
# Redirect to PM for privacy
if len(args) < 2:
self.send_pm(nickname, "Usage: !swap <pet1> <pet2>")
self.send_pm(nickname, "Example: !swap Flamey Aqua")
self.send_message(channel, f"{nickname}: Pet swap instructions sent via PM!")
return
player = await self.require_player(channel, nickname)
if not player:
return
# Handle multi-word pet names by splitting on first space vs last space
if len(args) == 2:
pet1_name, pet2_name = args
else:
# For more complex parsing, assume equal split
mid_point = len(args) // 2
pet1_name = " ".join(args[:mid_point])
pet2_name = " ".join(args[mid_point:])
result = await self.database.swap_pets(player["id"], pet1_name, pet2_name)
if result["success"]:
pet1 = result["pet1"]
pet2 = result["pet2"]
pet1_display = pet1["nickname"] or pet1["species_name"]
pet2_display = pet2["nickname"] or pet2["species_name"]
self.send_pm(nickname, f"🔄 Swap complete!")
self.send_pm(nickname, f"{pet1_display}{result['pet1_now']}")
self.send_pm(nickname, f"{pet2_display}{result['pet2_now']}")
self.send_message(channel, f"{nickname}: Pet swap completed!")
else:
self.send_pm(nickname, f"{result['error']}")
self.send_message(channel, f"{nickname}: Pet swap failed - check PM for details!")

0
plugins/__init__.py Normal file
View file

4
requirements.txt Normal file
View file

@ -0,0 +1,4 @@
irc>=20.3.0
aiosqlite>=0.19.0
python-dotenv>=1.0.0
asyncio

85
reset_players.py Executable file
View file

@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""
Reset Player Database Script
Clears all player data for a fresh start while preserving game data (species, locations, etc.)
"""
import os
import sys
import asyncio
# Add the project directory to the path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
async def reset_player_data(force=False):
"""Reset all player-related data in the database"""
print("🔄 Initializing database connection...")
database = Database()
await database.init_database()
print("⚠️ WARNING: This will delete ALL player data!")
print(" - All player accounts")
print(" - All caught pets")
print(" - All player achievements")
print(" - All player progress")
print(" - All battle history")
print("")
print("Game data (species, locations, moves, etc.) will be preserved.")
print("")
if not force:
confirm = input("Are you sure you want to reset all player data? (type 'yes' to confirm): ")
if confirm.lower() != 'yes':
print("❌ Reset cancelled.")
return
else:
print("🚀 Force mode enabled - proceeding with reset...")
print("")
print("🗑️ Resetting player data...")
try:
# Get database connection using aiosqlite directly
import aiosqlite
async with aiosqlite.connect(database.db_path) as db:
# Clear player-related tables in correct order (foreign key constraints)
tables_to_clear = [
'active_battles', # Battle data
'player_achievements', # Player achievements
'pets', # Player pets
'players' # Player accounts
]
for table in tables_to_clear:
print(f" Clearing {table}...")
await db.execute(f"DELETE FROM {table}")
await db.commit()
# Reset auto-increment counters
print(" Resetting ID counters...")
for table in tables_to_clear:
await db.execute(f"DELETE FROM sqlite_sequence WHERE name='{table}'")
await db.commit()
print("✅ Player data reset complete!")
print("")
print("🎮 Players can now use !start to begin fresh journeys.")
except Exception as e:
print(f"❌ Error resetting player data: {e}")
import traceback
traceback.print_exc()
async def main():
"""Main function"""
print("🐾 PetBot Player Data Reset Tool")
print("=" * 40)
# Check for force flag
force = len(sys.argv) > 1 and sys.argv[1] == '--force'
await reset_player_data(force=force)
if __name__ == "__main__":
asyncio.run(main())

254
run_bot.py Normal file
View file

@ -0,0 +1,254 @@
#!/usr/bin/env python3
import socket
import time
import sys
import os
import asyncio
import importlib
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
from src.game_engine import GameEngine
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin
class PetBot:
def __init__(self):
self.database = Database()
self.game_engine = GameEngine(self.database)
self.config = {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
self.socket = None
self.connected = False
self.running = True
self.active_encounters = {} # player_id -> encounter_data
self.modules = {}
self.command_map = {}
def initialize_async_components(self):
"""Initialize async components"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
print("Initializing database...")
loop.run_until_complete(self.database.init_database())
print("Loading game data...")
loop.run_until_complete(self.game_engine.load_game_data())
print("Loading modules...")
self.load_modules()
print("✓ Bot initialized successfully!")
self.loop = loop
def load_modules(self):
"""Load all command modules"""
module_classes = [
CoreCommands,
Exploration,
BattleSystem,
PetManagement,
Achievements,
Admin
]
self.modules = {}
self.command_map = {}
for module_class in module_classes:
module_name = module_class.__name__
module_instance = module_class(self, self.database, self.game_engine)
self.modules[module_name] = module_instance
# Map commands to modules
for command in module_instance.get_commands():
self.command_map[command] = module_instance
print(f"✓ Loaded {len(self.modules)} modules with {len(self.command_map)} commands")
async def reload_modules(self):
"""Reload all modules (for admin use)"""
try:
# Reload module files
import modules
importlib.reload(modules.core_commands)
importlib.reload(modules.exploration)
importlib.reload(modules.battle_system)
importlib.reload(modules.pet_management)
importlib.reload(modules.achievements)
importlib.reload(modules.admin)
importlib.reload(modules)
# Reinitialize modules
self.load_modules()
return True
except Exception as e:
print(f"Module reload failed: {e}")
return False
def test_connection(self):
"""Test if we can connect to IRC"""
print("Testing IRC connection...")
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(5)
test_sock.connect((self.config["server"], self.config["port"]))
test_sock.close()
print("✓ IRC connection test successful")
return True
except Exception as e:
print(f"✗ IRC connection failed: {e}")
return False
def connect(self):
self.initialize_async_components()
if not self.test_connection():
print("\n=== IRC Connection Failed ===")
print("The bot's core functionality is working perfectly!")
print("Run these commands to test locally:")
print(" python3 test/test_commands.py - Test all functionality")
print(" python3 test/test_db.py - Test database operations")
print("\nTo connect to IRC:")
print("1. Ensure network access to irc.libera.chat:6667")
print("2. Or modify config in run_bot.py to use a different server")
return
print(f"Connecting to {self.config['server']}:{self.config['port']}")
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10)
try:
self.socket.connect((self.config["server"], self.config["port"]))
print("✓ Connected to IRC server!")
except Exception as e:
print(f"✗ Failed to connect: {e}")
return
# Send IRC handshake
self.send(f"NICK {self.config['nickname']}")
self.send(f"USER {self.config['nickname']} 0 * :{self.config['nickname']}")
print("✓ Sent IRC handshake")
# Start message processing
self.socket.settimeout(1)
self.main_loop()
def main_loop(self):
print("✓ Bot is now running! Use Ctrl+C to stop.")
print(f"✓ Bot will join {self.config['channel']} when connected")
print(f"✓ Loaded modules: {', '.join(self.modules.keys())}")
while self.running:
try:
data = self.socket.recv(4096).decode('utf-8', errors='ignore')
if not data:
print("Connection closed by server")
break
lines = data.strip().split('\\n')
for line in lines:
if line.strip():
self.handle_line(line.strip())
except socket.timeout:
continue
except KeyboardInterrupt:
print("\\n✓ Shutting down bot...")
self.running = False
break
except Exception as e:
print(f"Error in main loop: {e}")
time.sleep(1)
if self.socket:
self.socket.close()
self.loop.close()
def send(self, message):
if self.socket:
full_message = f"{message}\\r\\n"
print(f">> {message}")
self.socket.send(full_message.encode('utf-8'))
def handle_line(self, line):
print(f"<< {line}")
if line.startswith("PING"):
pong_response = line.replace("PING", "PONG")
self.send(pong_response)
return
# Handle connection messages
if "376" in line or "422" in line: # End of MOTD
if not self.connected:
self.send(f"JOIN {self.config['channel']}")
self.connected = True
print(f"✓ Joined {self.config['channel']} - Bot is ready!")
return
parts = line.split()
if len(parts) < 4:
return
if parts[1] == "PRIVMSG":
channel = parts[2]
message = " ".join(parts[3:])[1:]
hostmask = parts[0][1:]
nickname = hostmask.split('!')[0]
if message.startswith(self.config["command_prefix"]):
print(f"Processing command from {nickname}: {message}")
self.handle_command(channel, nickname, message)
def handle_command(self, channel, nickname, message):
command_parts = message[1:].split()
if not command_parts:
return
command = command_parts[0].lower()
args = command_parts[1:]
try:
if command in self.command_map:
module = self.command_map[command]
# Run async command handler
self.loop.run_until_complete(
module.handle_command(channel, nickname, command, args)
)
else:
self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
except Exception as e:
self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
print(f"Command error: {e}")
def send_message(self, target, message):
self.send(f"PRIVMSG {target} :{message}")
time.sleep(0.5)
def run_async_command(self, coro):
return self.loop.run_until_complete(coro)
if __name__ == "__main__":
print("🐾 Starting Pet Bot for IRC (Modular Version)...")
bot = PetBot()
try:
bot.connect()
except KeyboardInterrupt:
print("\\n🔄 Bot stopping...")
# Gracefully shutdown the game engine
try:
bot.loop.run_until_complete(bot.game_engine.shutdown())
except Exception as e:
print(f"Error during shutdown: {e}")
print("✓ Bot stopped by user")
except Exception as e:
print(f"✗ Bot crashed: {e}")
import traceback
traceback.print_exc()

283
run_bot_debug.py Normal file
View file

@ -0,0 +1,283 @@
#!/usr/bin/env python3
import socket
import time
import sys
import os
import asyncio
import importlib
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
from src.game_engine import GameEngine
from modules import CoreCommands, Exploration, BattleSystem, PetManagement, Achievements, Admin
from webserver import PetBotWebServer
class PetBotDebug:
def __init__(self):
print("🤖 PetBot Debug Mode - Initializing...")
self.database = Database()
self.game_engine = GameEngine(self.database)
self.config = {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
self.socket = None
self.connected = False
self.running = True
self.active_encounters = {}
self.modules = {}
self.command_map = {}
self.web_server = None
print("✅ Basic initialization complete")
def initialize_async_components(self):
"""Initialize async components"""
print("🔄 Creating event loop...")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
print("✅ Event loop created")
print("🔄 Initializing database...")
loop.run_until_complete(self.database.init_database())
print("✅ Database initialized")
print("🔄 Loading game data...")
loop.run_until_complete(self.game_engine.load_game_data())
print("✅ Game data loaded")
print("🔄 Loading modules...")
self.load_modules()
print("✅ Modules loaded")
print("🔄 Starting web server...")
self.web_server = PetBotWebServer(self.database, port=8080)
self.web_server.start_in_thread()
print("✅ Web server started")
self.loop = loop
print("✅ Async components ready")
def load_modules(self):
"""Load all command modules"""
module_classes = [
CoreCommands,
Exploration,
BattleSystem,
PetManagement,
Achievements,
Admin
]
self.modules = {}
self.command_map = {}
for module_class in module_classes:
module_name = module_class.__name__
print(f" Loading {module_name}...")
module_instance = module_class(self, self.database, self.game_engine)
self.modules[module_name] = module_instance
# Map commands to modules
commands = module_instance.get_commands()
for command in commands:
self.command_map[command] = module_instance
print(f"{module_name}: {len(commands)} commands")
print(f"✅ Loaded {len(self.modules)} modules with {len(self.command_map)} commands")
async def reload_modules(self):
"""Reload all modules (for admin use)"""
try:
# Reload module files
import modules
importlib.reload(modules.core_commands)
importlib.reload(modules.exploration)
importlib.reload(modules.battle_system)
importlib.reload(modules.pet_management)
importlib.reload(modules.achievements)
importlib.reload(modules.admin)
importlib.reload(modules)
# Reinitialize modules
print("🔄 Reloading modules...")
self.load_modules()
print("✅ Modules reloaded successfully")
return True
except Exception as e:
print(f"❌ Module reload failed: {e}")
return False
def test_connection(self):
"""Test if we can connect to IRC"""
print("🔄 Testing IRC connection...")
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(5)
test_sock.connect((self.config["server"], self.config["port"]))
test_sock.close()
print("✅ IRC connection test successful")
return True
except Exception as e:
print(f"❌ IRC connection failed: {e}")
return False
def connect(self):
print("🚀 Starting bot connection process...")
print("🔄 Step 1: Initialize async components...")
self.initialize_async_components()
print("✅ Step 1 complete")
print("🔄 Step 2: Test IRC connectivity...")
if not self.test_connection():
print("❌ IRC connection test failed - would run offline mode")
print("✅ Bot core systems are working - IRC connection unavailable")
return
print("✅ Step 2 complete")
print("🔄 Step 3: Create IRC socket...")
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10)
print("✅ Socket created")
print("🔄 Step 4: Connect to IRC server...")
try:
self.socket.connect((self.config["server"], self.config["port"]))
print("✅ Connected to IRC server!")
except Exception as e:
print(f"❌ Failed to connect: {e}")
return
print("🔄 Step 5: Send IRC handshake...")
self.send(f"NICK {self.config['nickname']}")
self.send(f"USER {self.config['nickname']} 0 * :{self.config['nickname']}")
print("✅ Handshake sent")
print("🔄 Step 6: Start main loop...")
self.socket.settimeout(1)
print("✅ Bot is now running!")
print(f"✅ Will join {self.config['channel']} when connected")
print(f"✅ Loaded commands: {', '.join(sorted(self.command_map.keys()))}")
print("🎮 Ready for testing! Press Ctrl+C to stop.")
self.main_loop()
def main_loop(self):
message_count = 0
while self.running:
try:
data = self.socket.recv(4096).decode('utf-8', errors='ignore')
if not data:
print("💀 Connection closed by server")
break
lines = data.strip().split('\n')
for line in lines:
if line.strip():
message_count += 1
if message_count <= 10: # Show first 10 messages for debugging
print(f"📨 {message_count}: {line.strip()}")
self.handle_line(line.strip())
except socket.timeout:
continue
except KeyboardInterrupt:
print("\n🛑 Shutting down bot...")
self.running = False
break
except Exception as e:
print(f"❌ Error in main loop: {e}")
time.sleep(1)
if self.socket:
self.socket.close()
self.loop.close()
print("✅ Bot shutdown complete")
def send(self, message):
if self.socket:
full_message = f"{message}\r\n"
print(f"📤 >> {message}")
self.socket.send(full_message.encode('utf-8'))
def handle_line(self, line):
if line.startswith("PING"):
pong_response = line.replace("PING", "PONG")
self.send(pong_response)
return
# Handle connection messages
if "376" in line or "422" in line: # End of MOTD
if not self.connected:
self.send(f"JOIN {self.config['channel']}")
self.connected = True
print(f"🎉 Joined {self.config['channel']} - Bot is ready for commands!")
return
parts = line.split()
if len(parts) < 4:
return
if parts[1] == "PRIVMSG":
channel = parts[2]
message = " ".join(parts[3:])[1:]
hostmask = parts[0][1:]
nickname = hostmask.split('!')[0]
if message.startswith(self.config["command_prefix"]):
print(f"🎮 Command from {nickname}: {message}")
self.handle_command(channel, nickname, message)
def handle_command(self, channel, nickname, message):
command_parts = message[1:].split()
if not command_parts:
return
command = command_parts[0].lower()
args = command_parts[1:]
try:
if command in self.command_map:
module = self.command_map[command]
print(f"🔧 Executing {command} via {module.__class__.__name__}")
# Run async command handler
self.loop.run_until_complete(
module.handle_command(channel, nickname, command, args)
)
else:
self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
except Exception as e:
self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
print(f"❌ Command error: {e}")
import traceback
traceback.print_exc()
def send_message(self, target, message):
self.send(f"PRIVMSG {target} :{message}")
time.sleep(0.5)
def run_async_command(self, coro):
return self.loop.run_until_complete(coro)
if __name__ == "__main__":
print("🐾 Starting Pet Bot for IRC (Debug Mode)...")
bot = PetBotDebug()
try:
bot.connect()
except KeyboardInterrupt:
print("\n🔄 Bot stopping...")
# Gracefully shutdown the game engine
try:
bot.loop.run_until_complete(bot.game_engine.shutdown())
except Exception as e:
print(f"Error during shutdown: {e}")
print("✅ Bot stopped by user")
except Exception as e:
print(f"❌ Bot crashed: {e}")
import traceback
traceback.print_exc()

633
run_bot_original.py Normal file
View file

@ -0,0 +1,633 @@
#!/usr/bin/env python3
import socket
import threading
import time
import json
import sys
import os
import asyncio
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from src.database import Database
from src.game_engine import GameEngine
class PetBot:
def __init__(self):
self.database = Database()
self.game_engine = GameEngine(self.database)
self.config = {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
self.socket = None
self.connected = False
self.running = True
self.active_encounters = {} # player_id -> encounter_data
def initialize_async_components(self):
"""Initialize async components"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
print("Initializing database...")
loop.run_until_complete(self.database.init_database())
print("Loading game data...")
loop.run_until_complete(self.game_engine.load_game_data())
print("✓ Bot initialized successfully!")
self.loop = loop
def test_connection(self):
"""Test if we can connect to IRC"""
print("Testing IRC connection...")
try:
test_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_sock.settimeout(5)
test_sock.connect((self.config["server"], self.config["port"]))
test_sock.close()
print("✓ IRC connection test successful")
return True
except Exception as e:
print(f"✗ IRC connection failed: {e}")
return False
def connect(self):
self.initialize_async_components()
if not self.test_connection():
print("\n=== IRC Connection Failed ===")
print("The bot's core functionality is working perfectly!")
print("Run these commands to test locally:")
print(" python3 test_commands.py - Test all functionality")
print(" python3 test_db.py - Test database operations")
print("\nTo connect to IRC:")
print("1. Ensure network access to irc.libera.chat:6667")
print("2. Or modify config in run_bot.py to use a different server")
return
print(f"Connecting to {self.config['server']}:{self.config['port']}")
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.settimeout(10)
try:
self.socket.connect((self.config["server"], self.config["port"]))
print("✓ Connected to IRC server!")
except Exception as e:
print(f"✗ Failed to connect: {e}")
return
# Send IRC handshake
self.send(f"NICK {self.config['nickname']}")
self.send(f"USER {self.config['nickname']} 0 * :{self.config['nickname']}")
print("✓ Sent IRC handshake")
# Start message processing
self.socket.settimeout(1)
self.main_loop()
def main_loop(self):
print("✓ Bot is now running! Use Ctrl+C to stop.")
print(f"✓ Bot will join {self.config['channel']} when connected")
print("✓ Available commands: !help !start !catch !team !wild !stats")
while self.running:
try:
data = self.socket.recv(4096).decode('utf-8', errors='ignore')
if not data:
print("Connection closed by server")
break
lines = data.strip().split('\n')
for line in lines:
if line.strip():
self.handle_line(line.strip())
except socket.timeout:
continue
except KeyboardInterrupt:
print("\n✓ Shutting down bot...")
self.running = False
break
except Exception as e:
print(f"Error in main loop: {e}")
time.sleep(1)
if self.socket:
self.socket.close()
self.loop.close()
def send(self, message):
if self.socket:
full_message = f"{message}\r\n"
print(f">> {message}")
self.socket.send(full_message.encode('utf-8'))
def handle_line(self, line):
print(f"<< {line}")
if line.startswith("PING"):
pong_response = line.replace("PING", "PONG")
self.send(pong_response)
return
# Handle connection messages
if "376" in line or "422" in line: # End of MOTD
if not self.connected:
self.send(f"JOIN {self.config['channel']}")
self.connected = True
print(f"✓ Joined {self.config['channel']} - Bot is ready!")
return
parts = line.split()
if len(parts) < 4:
return
if parts[1] == "PRIVMSG":
channel = parts[2]
message = " ".join(parts[3:])[1:]
hostmask = parts[0][1:]
nickname = hostmask.split('!')[0]
if message.startswith(self.config["command_prefix"]):
print(f"Processing command from {nickname}: {message}")
self.handle_command(channel, nickname, message)
def handle_command(self, channel, nickname, message):
command_parts = message[1:].split()
if not command_parts:
return
command = command_parts[0].lower()
args = command_parts[1:]
try:
if command == "help":
self.cmd_help(channel, nickname)
elif command == "start":
self.cmd_start(channel, nickname)
elif command == "explore":
self.cmd_explore(channel, nickname)
elif command == "catch" or command == "capture":
self.cmd_catch(channel, nickname)
elif command == "battle":
self.cmd_battle(channel, nickname)
elif command == "attack":
self.cmd_attack(channel, nickname, args)
elif command == "flee":
self.cmd_flee(channel, nickname)
elif command == "moves":
self.cmd_moves(channel, nickname)
elif command == "travel":
self.cmd_travel(channel, nickname, args)
elif command == "location" or command == "where":
self.cmd_location(channel, nickname)
elif command == "achievements":
self.cmd_achievements(channel, nickname)
elif command == "weather":
self.cmd_weather(channel, nickname)
elif command == "team":
self.cmd_team(channel, nickname)
elif command == "wild":
self.cmd_wild(channel, nickname, args)
elif command == "stats":
self.cmd_stats(channel, nickname, args)
else:
self.send_message(channel, f"{nickname}: Unknown command. Use !help for available commands.")
except Exception as e:
self.send_message(channel, f"{nickname}: Error processing command: {str(e)}")
print(f"Command error: {e}")
def send_message(self, target, message):
self.send(f"PRIVMSG {target} :{message}")
time.sleep(0.5)
def run_async_command(self, coro):
return self.loop.run_until_complete(coro)
def cmd_help(self, channel, nickname):
help_text = [
"🐾 Pet Bot Commands:",
"!start - Begin your pet journey in Starter Town",
"!explore - Explore your current location for wild pets",
"!catch (or !capture) - Try to catch a pet during exploration or battle",
"!battle - Start a battle with an encountered wild pet",
"!attack <move> - Use a move in battle",
"!flee - Try to escape from battle",
"!moves - View your active pet's available moves",
"!travel <location> - Travel to a different location",
"!location - See where you currently are",
"!achievements - View your achievements and progress",
"!weather - Check current weather effects",
"!team - View your active pets",
"!wild <location> - See what pets spawn in a location",
"!stats - View your player stats",
"Locations: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak"
]
for line in help_text:
self.send_message(channel, line)
def cmd_start(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if player:
self.send_message(channel, f"{nickname}: You already have an account! Use !team to see your pets.")
return
player_id = self.run_async_command(self.database.create_player(nickname))
starter_pet = self.run_async_command(self.game_engine.give_starter_pet(player_id))
self.send_message(channel,
f"🎉 {nickname}: Welcome to the world of pets! You received a Level {starter_pet['level']} {starter_pet['species_name']}! You are now in Starter Town.")
def cmd_explore(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
encounter = self.run_async_command(self.game_engine.explore_location(player["id"]))
if encounter["type"] == "error":
self.send_message(channel, f"{nickname}: {encounter['message']}")
elif encounter["type"] == "empty":
self.send_message(channel, f"🔍 {nickname}: {encounter['message']}")
elif encounter["type"] == "encounter":
# Store the encounter for potential catching
self.active_encounters[player["id"]] = encounter["pet"]
pet = encounter["pet"]
type_str = pet["type1"]
if pet["type2"]:
type_str += f"/{pet['type2']}"
self.send_message(channel,
f"🐾 {nickname}: A wild Level {pet['level']} {pet['species_name']} ({type_str}) appeared in {encounter['location']}!")
self.send_message(channel, f"Choose your action: !battle to fight it, or !catch to try catching it directly!")
def cmd_battle(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
# Check if player has an active encounter
if player["id"] not in self.active_encounters:
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet to battle!")
return
# Check if already in battle
active_battle = self.run_async_command(self.game_engine.battle_engine.get_active_battle(player["id"]))
if active_battle:
self.send_message(channel, f"{nickname}: You're already in battle! Use !attack <move> or !flee.")
return
# Get player's active pet
pets = self.run_async_command(self.database.get_player_pets(player["id"], active_only=True))
if not pets:
self.send_message(channel, f"{nickname}: You need an active pet to battle! Use !team to check your pets.")
return
player_pet = pets[0] # Use first active pet
wild_pet = self.active_encounters[player["id"]]
# Start battle
battle = self.run_async_command(self.game_engine.battle_engine.start_battle(player["id"], player_pet, wild_pet))
self.send_message(channel, f"⚔️ {nickname}: Battle started!")
self.send_message(channel, f"Your {player_pet['species_name']} (Lv.{player_pet['level']}, {battle['player_hp']}/{player_pet['max_hp']} HP) vs Wild {wild_pet['species_name']} (Lv.{wild_pet['level']}, {battle['wild_hp']}/{wild_pet['max_hp']} HP)")
move_list = [move["name"] for move in battle["available_moves"]]
self.send_message(channel, f"Available moves: {', '.join(move_list)}")
self.send_message(channel, f"Use !attack <move_name> to attack!")
def cmd_attack(self, channel, nickname, args):
if not args:
self.send_message(channel, f"{nickname}: Specify a move to use! Example: !attack Tackle")
return
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
move_name = " ".join(args).title() # Normalize to Title Case
result = self.run_async_command(self.game_engine.battle_engine.execute_battle_turn(player["id"], move_name))
if "error" in result:
self.send_message(channel, f"{nickname}: {result['error']}")
return
# Display battle results
for action in result["results"]:
damage_msg = f"{action['attacker']} used {action['move']}!"
self.send_message(channel, damage_msg)
if action["damage"] > 0:
effectiveness_msgs = {
"super_effective": "It's super effective!",
"not_very_effective": "It's not very effective...",
"no_effect": "It had no effect!",
"super_effective_critical": "Critical hit! It's super effective!",
"normal_critical": "Critical hit!",
"not_very_effective_critical": "Critical hit! It's not very effective..."
}
damage_text = f"Dealt {action['damage']} damage!"
if action["effectiveness"] in effectiveness_msgs:
damage_text += f" {effectiveness_msgs[action['effectiveness']]}"
self.send_message(channel, damage_text)
self.send_message(channel, f"Target HP: {action['target_hp']}")
if result["battle_over"]:
if result["winner"] == "player":
self.send_message(channel, f"🎉 {nickname}: You won the battle!")
# Remove encounter since battle is over
if player["id"] in self.active_encounters:
del self.active_encounters[player["id"]]
else:
self.send_message(channel, f"💀 {nickname}: Your pet fainted! You lost the battle...")
# Remove encounter
if player["id"] in self.active_encounters:
del self.active_encounters[player["id"]]
else:
# Battle continues
move_list = [move["name"] for move in result["available_moves"]]
self.send_message(channel, f"Your turn! Available moves: {', '.join(move_list)}")
def cmd_flee(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
success = self.run_async_command(self.game_engine.battle_engine.flee_battle(player["id"]))
if success:
self.send_message(channel, f"💨 {nickname}: You successfully escaped from battle!")
# Remove encounter
if player["id"] in self.active_encounters:
del self.active_encounters[player["id"]]
else:
self.send_message(channel, f"{nickname}: Couldn't escape! The battle continues!")
def cmd_moves(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
# Get player's active pets
pets = self.run_async_command(self.database.get_player_pets(player["id"], active_only=True))
if not pets:
self.send_message(channel, f"{nickname}: You don't have any active pets! Use !team to check your pets.")
return
active_pet = pets[0] # Use first active pet
available_moves = self.game_engine.battle_engine.get_available_moves(active_pet)
if not available_moves:
self.send_message(channel, f"{nickname}: Your {active_pet['species_name']} has no available moves!")
return
# Limit to 4 moves max
moves_to_show = available_moves[:4]
move_info = []
for move in moves_to_show:
power_info = f"Power: {move.get('power', 'N/A')}" if move.get('power') else "Status move"
move_info.append(f"{move['name']} ({move['type']}) - {power_info}")
pet_name = active_pet["nickname"] or active_pet["species_name"]
self.send_message(channel, f"🎯 {pet_name}'s moves:")
for move_line in move_info:
self.send_message(channel, f"{move_line}")
def cmd_catch(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
# Check if player is in an active battle
active_battle = self.run_async_command(self.game_engine.battle_engine.get_active_battle(player["id"]))
if active_battle:
# Catching during battle
wild_pet = active_battle["wild_pet"]
current_hp = active_battle["wild_hp"]
max_hp = wild_pet["max_hp"]
# Calculate catch rate based on remaining HP (lower HP = higher catch rate)
hp_percentage = current_hp / max_hp
base_catch_rate = 0.3 # Lower base rate than direct catch
hp_bonus = (1.0 - hp_percentage) * 0.5 # Up to 50% bonus for low HP
final_catch_rate = min(0.9, base_catch_rate + hp_bonus) # Cap at 90%
import random
if random.random() < final_catch_rate:
# Successful catch during battle
result = self.run_async_command(self.game_engine.attempt_catch_current_location(player["id"], wild_pet))
# End the battle
await_result = self.run_async_command(self.game_engine.battle_engine.end_battle(player["id"], "caught"))
# Check for achievements
achievements = self.run_async_command(self.game_engine.check_and_award_achievements(player["id"], "catch_type", ""))
if achievements:
for achievement in achievements:
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
# Remove encounter
if player["id"] in self.active_encounters:
del self.active_encounters[player["id"]]
self.send_message(channel, f"🎯 {nickname}: Caught the {wild_pet['species_name']} during battle! (HP: {current_hp}/{max_hp})")
else:
# Failed catch - battle continues
catch_percentage = int(final_catch_rate * 100)
self.send_message(channel, f"🎯 {nickname}: The {wild_pet['species_name']} broke free! ({catch_percentage}% catch rate) Battle continues!")
return
# Regular exploration catch (not in battle)
if player["id"] not in self.active_encounters:
self.send_message(channel, f"{nickname}: You need to !explore first to find a pet, or be in battle to catch!")
return
target_pet = self.active_encounters[player["id"]]
result = self.run_async_command(self.game_engine.attempt_catch_current_location(player["id"], target_pet))
# Check for achievements after successful catch
if "Success!" in result:
achievements = self.run_async_command(self.game_engine.check_and_award_achievements(player["id"], "catch_type", ""))
if achievements:
for achievement in achievements:
self.send_message(channel, f"🏆 {nickname}: Achievement unlocked: {achievement['name']}! {achievement['description']}")
# Remove the encounter regardless of success
del self.active_encounters[player["id"]]
self.send_message(channel, f"🎯 {nickname}: {result}")
def cmd_travel(self, channel, nickname, args):
if not args:
self.send_message(channel, f"{nickname}: Specify where to travel! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
return
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
destination = " ".join(args).title() # Normalize to Title Case
location = self.run_async_command(self.database.get_location_by_name(destination))
if not location:
self.send_message(channel, f"{nickname}: '{destination}' is not a valid location!")
return
# Check if player can access this location
can_access = self.run_async_command(self.database.can_access_location(player["id"], location["id"]))
if not can_access:
self.send_message(channel, f"{nickname}: You cannot access {destination} yet! Complete achievements to unlock new areas. Use !achievements to see progress.")
return
# Clear any active encounters when traveling
if player["id"] in self.active_encounters:
del self.active_encounters[player["id"]]
self.run_async_command(self.database.update_player_location(player["id"], location["id"]))
# Show weather info
weather = self.run_async_command(self.database.get_location_weather(location["id"]))
weather_msg = ""
if weather:
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
weather_msg = f" Weather: {weather['weather_type']} - {weather_desc}"
self.send_message(channel, f"🗺️ {nickname}: You traveled to {destination}. {location['description']}{weather_msg}")
def cmd_location(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
location = self.run_async_command(self.database.get_player_location(player["id"]))
if location:
self.send_message(channel, f"📍 {nickname}: You are currently in {location['name']}. {location['description']}")
else:
self.send_message(channel, f"{nickname}: You seem to be lost! Contact an admin.")
def cmd_achievements(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
achievements = self.run_async_command(self.database.get_player_achievements(player["id"]))
if achievements:
self.send_message(channel, f"🏆 {nickname}'s Achievements:")
for achievement in achievements[:5]: # Show last 5 achievements
self.send_message(channel, f"{achievement['name']}: {achievement['description']}")
if len(achievements) > 5:
self.send_message(channel, f"... and {len(achievements) - 5} more!")
else:
self.send_message(channel, f"{nickname}: No achievements yet! Keep exploring and catching pets to unlock new areas!")
def cmd_weather(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
location = self.run_async_command(self.database.get_player_location(player["id"]))
if not location:
self.send_message(channel, f"{nickname}: You seem to be lost!")
return
weather = self.run_async_command(self.database.get_location_weather(location["id"]))
if weather:
weather_patterns = getattr(self.game_engine, 'weather_patterns', {})
weather_info = weather_patterns.get("weather_types", {}).get(weather["weather_type"], {})
weather_desc = weather_info.get("description", f"{weather['weather_type']} weather")
self.send_message(channel, f"🌤️ {nickname}: Current weather in {location['name']}: {weather['weather_type']}")
self.send_message(channel, f"Effect: {weather_desc}")
else:
self.send_message(channel, f"🌤️ {nickname}: The weather in {location['name']} is calm with no special effects.")
def cmd_team(self, channel, nickname):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
pets = self.run_async_command(self.database.get_player_pets(player["id"], active_only=False))
if not pets:
self.send_message(channel, f"{nickname}: You don't have any pets! Use !catch to find some.")
return
# Show active pets first, then others
active_pets = [pet for pet in pets if pet.get("is_active")]
inactive_pets = [pet for pet in pets if not pet.get("is_active")]
team_info = []
# Active pets with star
for pet in active_pets:
name = pet["nickname"] or pet["species_name"]
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
# Inactive pets
for pet in inactive_pets[:5]: # Show max 5 inactive
name = pet["nickname"] or pet["species_name"]
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
if len(inactive_pets) > 5:
team_info.append(f"... and {len(inactive_pets) - 5} more in storage")
self.send_message(channel, f"🐾 {nickname}'s team: " + " | ".join(team_info))
def cmd_wild(self, channel, nickname, args):
if not args:
self.send_message(channel, f"{nickname}: Specify a location! Available: Starter Town, Whispering Woods, Electric Canyon, Crystal Caves, Frozen Tundra, Dragon's Peak")
return
location_name = " ".join(args).title() # Normalize to Title Case
wild_pets = self.run_async_command(self.game_engine.get_location_spawns(location_name))
if wild_pets:
pet_list = ", ".join([pet["name"] for pet in wild_pets])
self.send_message(channel, f"🌿 Wild pets in {location_name}: {pet_list}")
else:
self.send_message(channel, f"{nickname}: No location found called '{location_name}'")
def cmd_stats(self, channel, nickname, args):
player = self.run_async_command(self.database.get_player(nickname))
if not player:
self.send_message(channel, f"{nickname}: Use !start to begin your journey first!")
return
self.send_message(channel,
f"📊 {nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
if __name__ == "__main__":
print("🐾 Starting Pet Bot for IRC...")
bot = PetBot()
try:
bot.connect()
except KeyboardInterrupt:
print("\n✓ Bot stopped by user")
except Exception as e:
print(f"✗ Bot crashed: {e}")
import traceback
traceback.print_exc()

0
src/__init__.py Normal file
View file

350
src/battle_engine.py Normal file
View file

@ -0,0 +1,350 @@
import json
import random
import aiosqlite
from typing import Dict, List, Optional, Tuple
from .database import Database
class BattleEngine:
def __init__(self, database: Database):
self.database = database
self.type_effectiveness = {}
self.moves_data = {}
async def load_battle_data(self):
"""Load moves and type effectiveness data"""
await self.load_moves()
await self.load_type_effectiveness()
async def load_moves(self):
"""Load moves from config"""
try:
with open("config/moves.json", "r") as f:
moves_data = json.load(f)
# Store moves by name for quick lookup
for move in moves_data:
self.moves_data[move["name"]] = move
# Load into database
async with aiosqlite.connect(self.database.db_path) as db:
for move in moves_data:
await db.execute("""
INSERT OR REPLACE INTO moves
(name, type, category, power, accuracy, pp, description)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
move["name"], move["type"], move["category"],
move.get("power"), move.get("accuracy", 100),
move.get("pp", 10), move.get("description", "")
))
await db.commit()
except FileNotFoundError:
print("No moves.json found, using basic moves")
self.moves_data = {
"Tackle": {"name": "Tackle", "type": "Normal", "category": "Physical", "power": 40, "accuracy": 100}
}
async def load_type_effectiveness(self):
"""Load type effectiveness chart"""
try:
with open("config/types.json", "r") as f:
types_data = json.load(f)
self.type_effectiveness = types_data.get("effectiveness", {})
except FileNotFoundError:
# Basic type chart
self.type_effectiveness = {
"Fire": {"strong_against": ["Grass"], "weak_against": ["Water", "Rock"]},
"Water": {"strong_against": ["Fire", "Rock"], "weak_against": ["Electric", "Grass"]},
"Grass": {"strong_against": ["Water", "Rock"], "weak_against": ["Fire"]},
"Electric": {"strong_against": ["Water"], "weak_against": ["Rock"]},
"Rock": {"strong_against": ["Fire", "Electric"], "weak_against": ["Water", "Grass"]},
"Normal": {"strong_against": [], "weak_against": ["Rock"]}
}
def get_type_multiplier(self, attack_type: str, defending_types: List[str]) -> float:
"""Calculate type effectiveness multiplier"""
multiplier = 1.0
for defending_type in defending_types:
if attack_type in self.type_effectiveness:
effectiveness = self.type_effectiveness[attack_type]
if defending_type in effectiveness.get("strong_against", []):
multiplier *= 2.0 # Super effective
elif defending_type in effectiveness.get("weak_against", []):
multiplier *= 0.5 # Not very effective
elif defending_type in effectiveness.get("immune_to", []):
multiplier *= 0.0 # No effect
return multiplier
def calculate_damage(self, attacker: Dict, defender: Dict, move: Dict) -> Tuple[int, float, str]:
"""Calculate damage from an attack"""
if move.get("category") == "Status":
return 0, 1.0, "status"
# Base damage calculation (simplified Pokémon formula)
level = attacker.get("level", 1)
attack_stat = attacker.get("attack", 50)
defense_stat = defender.get("defense", 50)
move_power = move.get("power", 40)
# Get defending types
defending_types = [defender.get("type1")]
if defender.get("type2"):
defending_types.append(defender["type2"])
# Type effectiveness
type_multiplier = self.get_type_multiplier(move["type"], defending_types)
# Critical hit chance (6.25%)
critical = random.random() < 0.0625
crit_multiplier = 1.5 if critical else 1.0
# Random factor (85-100%)
random_factor = random.uniform(0.85, 1.0)
# STAB (Same Type Attack Bonus)
stab = 1.5 if move["type"] in [attacker.get("type1"), attacker.get("type2")] else 1.0
# Calculate final damage
damage = ((((2 * level + 10) / 250) * (attack_stat / defense_stat) * move_power + 2)
* type_multiplier * stab * crit_multiplier * random_factor)
damage = max(1, int(damage)) # Minimum 1 damage
# Determine effectiveness message
effectiveness_msg = ""
if type_multiplier > 1.0:
effectiveness_msg = "super_effective"
elif type_multiplier < 1.0:
effectiveness_msg = "not_very_effective"
elif type_multiplier == 0.0:
effectiveness_msg = "no_effect"
else:
effectiveness_msg = "normal"
if critical:
effectiveness_msg += "_critical"
return damage, type_multiplier, effectiveness_msg
def get_available_moves(self, pet: Dict) -> List[Dict]:
"""Get available moves for a pet (simplified)"""
# For now, all pets have basic moves based on their type
pet_type = pet.get("type1", "Normal")
basic_moves = {
"Fire": ["Tackle", "Ember"],
"Water": ["Tackle", "Water Gun"],
"Grass": ["Tackle", "Vine Whip"],
"Electric": ["Tackle", "Thunder Shock"],
"Rock": ["Tackle", "Rock Throw"],
"Normal": ["Tackle"],
"Ice": ["Tackle", "Water Gun"] # Add Ice type support
}
move_names = basic_moves.get(pet_type, ["Tackle"])
available_moves = []
for name in move_names:
if name in self.moves_data:
available_moves.append(self.moves_data[name])
else:
# Fallback to Tackle if move not found
available_moves.append(self.moves_data.get("Tackle", {"name": "Tackle", "type": "Normal", "category": "Physical", "power": 40}))
return available_moves
async def start_battle(self, player_id: int, player_pet: Dict, wild_pet: Dict) -> Dict:
"""Start a new battle"""
wild_pet_json = json.dumps(wild_pet)
async with aiosqlite.connect(self.database.db_path) as db:
cursor = await db.execute("""
INSERT INTO active_battles
(player_id, wild_pet_data, player_pet_id, player_hp, wild_hp)
VALUES (?, ?, ?, ?, ?)
""", (
player_id, wild_pet_json, player_pet["id"],
player_pet["hp"], wild_pet["stats"]["hp"]
))
battle_id = cursor.lastrowid
await db.commit()
return {
"battle_id": battle_id,
"player_pet": player_pet,
"wild_pet": wild_pet,
"player_hp": player_pet["hp"],
"wild_hp": wild_pet["stats"]["hp"],
"turn": "player",
"available_moves": self.get_available_moves(player_pet)
}
async def get_active_battle(self, player_id: int) -> Optional[Dict]:
"""Get player's active battle"""
async with aiosqlite.connect(self.database.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM active_battles
WHERE player_id = ? AND battle_status = 'active'
ORDER BY id DESC LIMIT 1
""", (player_id,))
battle = await cursor.fetchone()
if not battle:
return None
# Get player pet data
cursor = await db.execute("""
SELECT p.*, ps.name as species_name, ps.type1, ps.type2
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.id = ?
""", (battle["player_pet_id"],))
player_pet = await cursor.fetchone()
if not player_pet:
return None
return {
"battle_id": battle["id"],
"player_pet": dict(player_pet),
"wild_pet": json.loads(battle["wild_pet_data"]),
"player_hp": battle["player_hp"],
"wild_hp": battle["wild_hp"],
"turn": battle["current_turn"],
"turn_count": battle["turn_count"],
"available_moves": self.get_available_moves(dict(player_pet))
}
async def execute_battle_turn(self, player_id: int, player_move: str) -> Dict:
"""Execute a battle turn"""
battle = await self.get_active_battle(player_id)
if not battle:
return {"error": "No active battle"}
if battle["turn"] != "player":
return {"error": "Not your turn"}
# Get move data
move = self.moves_data.get(player_move)
if not move:
return {"error": "Invalid move"}
results = []
battle_over = False
winner = None
# Player's turn
damage, multiplier, effectiveness = self.calculate_damage(
battle["player_pet"], battle["wild_pet"], move
)
new_wild_hp = max(0, battle["wild_hp"] - damage)
results.append({
"attacker": battle["player_pet"]["species_name"],
"move": move["name"],
"damage": damage,
"effectiveness": effectiveness,
"target_hp": new_wild_hp
})
# Check if wild pet fainted
if new_wild_hp <= 0:
battle_over = True
winner = "player"
else:
# Wild pet's turn (AI)
wild_moves = self.get_available_moves(battle["wild_pet"])
wild_move = random.choice(wild_moves)
wild_damage, wild_multiplier, wild_effectiveness = self.calculate_damage(
battle["wild_pet"], battle["player_pet"], wild_move
)
new_player_hp = max(0, battle["player_hp"] - wild_damage)
results.append({
"attacker": battle["wild_pet"]["species_name"],
"move": wild_move["name"],
"damage": wild_damage,
"effectiveness": wild_effectiveness,
"target_hp": new_player_hp
})
# Check if player pet fainted
if new_player_hp <= 0:
battle_over = True
winner = "wild"
battle["player_hp"] = new_player_hp
battle["wild_hp"] = new_wild_hp
battle["turn_count"] += 1
# Update battle in database
async with aiosqlite.connect(self.database.db_path) as db:
if battle_over:
await db.execute("""
UPDATE active_battles
SET player_hp = ?, wild_hp = ?, turn_count = ?, battle_status = 'finished'
WHERE id = ?
""", (battle["player_hp"], battle["wild_hp"], battle["turn_count"], battle["battle_id"]))
else:
await db.execute("""
UPDATE active_battles
SET player_hp = ?, wild_hp = ?, turn_count = ?
WHERE id = ?
""", (battle["player_hp"], battle["wild_hp"], battle["turn_count"], battle["battle_id"]))
await db.commit()
return {
"results": results,
"battle_over": battle_over,
"winner": winner,
"player_hp": battle["player_hp"],
"wild_hp": battle["wild_hp"],
"available_moves": battle["available_moves"] if not battle_over else []
}
async def flee_battle(self, player_id: int) -> bool:
"""Attempt to flee from battle"""
battle = await self.get_active_battle(player_id)
if not battle:
return False
# 50% base flee chance, higher if player pet is faster
player_speed = battle["player_pet"].get("speed", 50)
wild_speed = battle["wild_pet"]["stats"].get("speed", 50)
flee_chance = 0.5 + (player_speed - wild_speed) * 0.01
flee_chance = max(0.1, min(0.9, flee_chance)) # Clamp between 10% and 90%
if random.random() < flee_chance:
# Successful flee
async with aiosqlite.connect(self.database.db_path) as db:
await db.execute("""
UPDATE active_battles
SET battle_status = 'fled'
WHERE id = ?
""", (battle["battle_id"],))
await db.commit()
return True
return False
async def end_battle(self, player_id: int, reason: str = "manual"):
"""End an active battle"""
async with aiosqlite.connect(self.database.db_path) as db:
await db.execute("""
UPDATE active_battles
SET battle_status = ?
WHERE player_id = ? AND battle_status = 'active'
""", (reason, player_id))
await db.commit()
return True

191
src/bot.py Normal file
View file

@ -0,0 +1,191 @@
import asyncio
import irc.client_aio
import irc.connection
import threading
from typing import Dict, List, Optional
import json
import random
from datetime import datetime
from .database import Database
from .game_engine import GameEngine
class PetBot:
def __init__(self, config_path: str = "config/settings.json"):
self.config = self.load_config(config_path)
self.database = Database()
self.game_engine = GameEngine(self.database)
self.client = None
self.connection = None
def load_config(self, config_path: str) -> Dict:
try:
with open(config_path, 'r') as f:
return json.load(f)
except FileNotFoundError:
return {
"server": "irc.libera.chat",
"port": 6667,
"nickname": "PetBot",
"channel": "#petz",
"command_prefix": "!"
}
async def start(self):
await self.database.init_database()
await self.game_engine.load_game_data()
self.client = irc.client_aio.AioReactor()
try:
self.connection = await self.client.server().connect(
self.config["server"],
self.config["port"],
self.config["nickname"]
)
except irc.client.ServerConnectionError:
print(f"Could not connect to {self.config['server']}:{self.config['port']}")
return
self.connection.add_global_handler("welcome", self.on_connect)
self.connection.add_global_handler("pubmsg", self.on_message)
self.connection.add_global_handler("privmsg", self.on_private_message)
await self.client.process_forever(timeout=0.2)
def on_connect(self, connection, event):
connection.join(self.config["channel"])
print(f"Connected to {self.config['channel']}")
async def on_message(self, connection, event):
message = event.arguments[0]
nickname = event.source.nick
channel = event.target
if message.startswith(self.config["command_prefix"]):
await self.handle_command(connection, channel, nickname, message)
async def on_private_message(self, connection, event):
message = event.arguments[0]
nickname = event.source.nick
if message.startswith(self.config["command_prefix"]):
await self.handle_command(connection, nickname, nickname, message)
async def handle_command(self, connection, target, nickname, message):
command_parts = message[1:].split()
if not command_parts:
return
command = command_parts[0].lower()
args = command_parts[1:]
try:
if command == "help":
await self.cmd_help(connection, target, nickname)
elif command == "start":
await self.cmd_start(connection, target, nickname)
elif command == "catch":
await self.cmd_catch(connection, target, nickname, args)
elif command == "team":
await self.cmd_team(connection, target, nickname)
elif command == "wild":
await self.cmd_wild(connection, target, nickname, args)
elif command == "battle":
await self.cmd_battle(connection, target, nickname, args)
elif command == "stats":
await self.cmd_stats(connection, target, nickname, args)
else:
await self.send_message(connection, target, f"{nickname}: Unknown command. Use !help for available commands.")
except Exception as e:
await self.send_message(connection, target, f"{nickname}: Error processing command: {str(e)}")
async def send_message(self, connection, target, message):
connection.privmsg(target, message)
await asyncio.sleep(0.5)
async def cmd_help(self, connection, target, nickname):
help_text = [
"Available commands:",
"!start - Begin your pet journey",
"!catch <location> - Try to catch a pet in a location",
"!team - View your active pets",
"!wild <location> - See what pets are in an area",
"!battle <player> - Challenge another player",
"!stats [pet_name] - View pet or player stats"
]
for line in help_text:
await self.send_message(connection, target, line)
async def cmd_start(self, connection, target, nickname):
player = await self.database.get_player(nickname)
if player:
await self.send_message(connection, target, f"{nickname}: You already have an account! Use !team to see your pets.")
return
player_id = await self.database.create_player(nickname)
starter_pet = await self.game_engine.give_starter_pet(player_id)
await self.send_message(connection, target,
f"{nickname}: Welcome to the world of pets! You received a {starter_pet['species_name']}!")
async def cmd_catch(self, connection, target, nickname, args):
if not args:
await self.send_message(connection, target, f"{nickname}: Specify a location to catch pets in!")
return
location_name = " ".join(args)
player = await self.database.get_player(nickname)
if not player:
await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!")
return
result = await self.game_engine.attempt_catch(player["id"], location_name)
await self.send_message(connection, target, f"{nickname}: {result}")
async def cmd_team(self, connection, target, nickname):
player = await self.database.get_player(nickname)
if not player:
await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!")
return
pets = await self.database.get_player_pets(player["id"], active_only=True)
if not pets:
await self.send_message(connection, target, f"{nickname}: You don't have any active pets! Use !catch to find some.")
return
team_info = []
for pet in pets:
name = pet["nickname"] or pet["species_name"]
team_info.append(f"{name} (Lv.{pet['level']}) - {pet['hp']}/{pet['max_hp']} HP")
await self.send_message(connection, target, f"{nickname}'s team: " + " | ".join(team_info))
async def cmd_wild(self, connection, target, nickname, args):
if not args:
await self.send_message(connection, target, f"{nickname}: Specify a location to explore!")
return
location_name = " ".join(args)
wild_pets = await self.game_engine.get_location_spawns(location_name)
if wild_pets:
pet_list = ", ".join([pet["name"] for pet in wild_pets])
await self.send_message(connection, target, f"Wild pets in {location_name}: {pet_list}")
else:
await self.send_message(connection, target, f"{nickname}: No location found called '{location_name}'")
async def cmd_battle(self, connection, target, nickname, args):
await self.send_message(connection, target, f"{nickname}: Battle system coming soon!")
async def cmd_stats(self, connection, target, nickname, args):
player = await self.database.get_player(nickname)
if not player:
await self.send_message(connection, target, f"{nickname}: Use !start to begin your journey first!")
return
await self.send_message(connection, target,
f"{nickname}: Level {player['level']} | {player['experience']} XP | ${player['money']}")
if __name__ == "__main__":
bot = PetBot()
asyncio.run(bot.start())

522
src/database.py Normal file
View file

@ -0,0 +1,522 @@
import aiosqlite
import json
from typing import Dict, List, Optional, Tuple
from datetime import datetime
class Database:
def __init__(self, db_path: str = "data/petbot.db"):
self.db_path = db_path
async def init_database(self):
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nickname TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_active TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
level INTEGER DEFAULT 1,
experience INTEGER DEFAULT 0,
money INTEGER DEFAULT 100,
current_location_id INTEGER DEFAULT NULL,
FOREIGN KEY (current_location_id) REFERENCES locations (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS pet_species (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
type1 TEXT NOT NULL,
type2 TEXT,
base_hp INTEGER NOT NULL,
base_attack INTEGER NOT NULL,
base_defense INTEGER NOT NULL,
base_speed INTEGER NOT NULL,
evolution_level INTEGER,
evolution_species_id INTEGER,
rarity INTEGER DEFAULT 1,
FOREIGN KEY (evolution_species_id) REFERENCES pet_species (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS pets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
species_id INTEGER NOT NULL,
nickname TEXT,
level INTEGER DEFAULT 1,
experience INTEGER DEFAULT 0,
hp INTEGER NOT NULL,
max_hp INTEGER NOT NULL,
attack INTEGER NOT NULL,
defense INTEGER NOT NULL,
speed INTEGER NOT NULL,
happiness INTEGER DEFAULT 50,
caught_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT FALSE,
FOREIGN KEY (player_id) REFERENCES players (id),
FOREIGN KEY (species_id) REFERENCES pet_species (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS moves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
type TEXT NOT NULL,
category TEXT NOT NULL,
power INTEGER,
accuracy INTEGER DEFAULT 100,
pp INTEGER DEFAULT 10,
description TEXT
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS pet_moves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pet_id INTEGER NOT NULL,
move_id INTEGER NOT NULL,
current_pp INTEGER,
FOREIGN KEY (pet_id) REFERENCES pets (id),
FOREIGN KEY (move_id) REFERENCES moves (id),
UNIQUE(pet_id, move_id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS battles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player1_id INTEGER NOT NULL,
player2_id INTEGER,
battle_type TEXT NOT NULL,
status TEXT DEFAULT 'active',
winner_id INTEGER,
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ended_at TIMESTAMP,
FOREIGN KEY (player1_id) REFERENCES players (id),
FOREIGN KEY (player2_id) REFERENCES players (id),
FOREIGN KEY (winner_id) REFERENCES players (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
level_min INTEGER DEFAULT 1,
level_max INTEGER DEFAULT 5
)
""")
# Add current_location_id column if it doesn't exist
try:
await db.execute("ALTER TABLE players ADD COLUMN current_location_id INTEGER DEFAULT 1")
await db.commit()
print("Added current_location_id column to players table")
except:
pass # Column already exists
await db.execute("""
CREATE TABLE IF NOT EXISTS location_spawns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
species_id INTEGER NOT NULL,
spawn_rate REAL DEFAULT 0.1,
min_level INTEGER DEFAULT 1,
max_level INTEGER DEFAULT 5,
FOREIGN KEY (location_id) REFERENCES locations (id),
FOREIGN KEY (species_id) REFERENCES pet_species (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
description TEXT,
requirement_type TEXT NOT NULL,
requirement_data TEXT,
unlock_location_id INTEGER,
FOREIGN KEY (unlock_location_id) REFERENCES locations (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS player_achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
achievement_id INTEGER NOT NULL,
completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (player_id) REFERENCES players (id),
FOREIGN KEY (achievement_id) REFERENCES achievements (id),
UNIQUE(player_id, achievement_id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS location_weather (
id INTEGER PRIMARY KEY AUTOINCREMENT,
location_id INTEGER NOT NULL,
weather_type TEXT NOT NULL,
active_until TIMESTAMP,
spawn_modifier REAL DEFAULT 1.0,
affected_types TEXT,
FOREIGN KEY (location_id) REFERENCES locations (id)
)
""")
await db.execute("""
CREATE TABLE IF NOT EXISTS active_battles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_id INTEGER NOT NULL,
wild_pet_data TEXT NOT NULL,
player_pet_id INTEGER NOT NULL,
player_hp INTEGER NOT NULL,
wild_hp INTEGER NOT NULL,
turn_count INTEGER DEFAULT 1,
current_turn TEXT DEFAULT 'player',
battle_status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (player_id) REFERENCES players (id),
FOREIGN KEY (player_pet_id) REFERENCES pets (id)
)
""")
await db.commit()
async def get_player(self, nickname: str) -> Optional[Dict]:
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"SELECT * FROM players WHERE nickname = ?", (nickname,)
)
row = await cursor.fetchone()
return dict(row) if row else None
async def create_player(self, nickname: str) -> int:
async with aiosqlite.connect(self.db_path) as db:
# Get Starter Town ID
cursor = await db.execute("SELECT id FROM locations WHERE name = 'Starter Town'")
starter_town = await cursor.fetchone()
if not starter_town:
raise Exception("Starter Town location not found in database - ensure game data is loaded first")
starter_town_id = starter_town[0]
cursor = await db.execute(
"INSERT INTO players (nickname, current_location_id) VALUES (?, ?)",
(nickname, starter_town_id)
)
await db.commit()
return cursor.lastrowid
async def get_player_pets(self, player_id: int, active_only: bool = False) -> List[Dict]:
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
query = """
SELECT p.*, ps.name as species_name, ps.type1, ps.type2
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ?
"""
params = [player_id]
if active_only:
query += " AND p.is_active = TRUE"
cursor = await db.execute(query, params)
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def get_player_location(self, player_id: int) -> Optional[Dict]:
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT l.* FROM locations l
JOIN players p ON p.current_location_id = l.id
WHERE p.id = ?
""", (player_id,))
row = await cursor.fetchone()
return dict(row) if row else None
async def update_player_location(self, player_id: int, location_id: int):
async with aiosqlite.connect(self.db_path) as db:
await db.execute(
"UPDATE players SET current_location_id = ? WHERE id = ?",
(location_id, player_id)
)
await db.commit()
async def get_location_by_name(self, location_name: str) -> Optional[Dict]:
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"SELECT * FROM locations WHERE name = ?", (location_name,)
)
row = await cursor.fetchone()
return dict(row) if row else None
async def check_player_achievements(self, player_id: int, achievement_type: str, data: str):
"""Check and award achievements based on player actions"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Get relevant achievements not yet earned
cursor = await db.execute("""
SELECT a.* FROM achievements a
WHERE a.requirement_type = ?
AND a.id NOT IN (
SELECT pa.achievement_id FROM player_achievements pa
WHERE pa.player_id = ?
)
""", (achievement_type, player_id))
achievements = await cursor.fetchall()
newly_earned = []
for achievement in achievements:
if await self._check_achievement_requirement(player_id, achievement, data):
# Award achievement
await db.execute("""
INSERT INTO player_achievements (player_id, achievement_id)
VALUES (?, ?)
""", (player_id, achievement["id"]))
newly_earned.append(dict(achievement))
await db.commit()
return newly_earned
async def _check_achievement_requirement(self, player_id: int, achievement: Dict, data: str) -> bool:
"""Check if player meets achievement requirement"""
req_type = achievement["requirement_type"]
req_data = achievement["requirement_data"]
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
if req_type == "catch_type":
# Count unique species of a specific type caught
required_count, pet_type = req_data.split(":")
cursor = await db.execute("""
SELECT COUNT(DISTINCT ps.id) as count
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ? AND (ps.type1 = ? OR ps.type2 = ?)
""", (player_id, pet_type, pet_type))
result = await cursor.fetchone()
return result["count"] >= int(required_count)
elif req_type == "catch_total":
# Count total pets caught
required_count = int(req_data)
cursor = await db.execute("""
SELECT COUNT(*) as count FROM pets WHERE player_id = ?
""", (player_id,))
result = await cursor.fetchone()
return result["count"] >= required_count
elif req_type == "explore_count":
# This would need exploration tracking - placeholder for now
return False
return False
async def get_player_achievements(self, player_id: int) -> List[Dict]:
"""Get all achievements earned by player"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT a.*, pa.completed_at
FROM achievements a
JOIN player_achievements pa ON a.id = pa.achievement_id
WHERE pa.player_id = ?
ORDER BY pa.completed_at DESC
""", (player_id,))
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def can_access_location(self, player_id: int, location_id: int) -> bool:
"""Check if player can access a location based on achievements"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Check if location requires any achievements
cursor = await db.execute("""
SELECT a.* FROM achievements a
WHERE a.unlock_location_id = ?
""", (location_id,))
required_achievements = await cursor.fetchall()
if not required_achievements:
return True # No requirements
# Check if player has ALL required achievements
for achievement in required_achievements:
cursor = await db.execute("""
SELECT 1 FROM player_achievements
WHERE player_id = ? AND achievement_id = ?
""", (player_id, achievement["id"]))
if not await cursor.fetchone():
return False # Missing required achievement
return True
async def get_missing_location_requirements(self, player_id: int, location_id: int) -> List[Dict]:
"""Get list of achievements required to access a location that the player doesn't have"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Get all achievements required for this location
cursor = await db.execute("""
SELECT a.* FROM achievements a
WHERE a.unlock_location_id = ?
""", (location_id,))
required_achievements = await cursor.fetchall()
missing_achievements = []
# Check which ones the player doesn't have
for achievement in required_achievements:
cursor = await db.execute("""
SELECT 1 FROM player_achievements
WHERE player_id = ? AND achievement_id = ?
""", (player_id, achievement["id"]))
if not await cursor.fetchone():
# Player doesn't have this achievement - convert to dict manually
achievement_dict = {
'id': achievement[0],
'name': achievement[1],
'description': achievement[2],
'requirement_type': achievement[3],
'requirement_data': achievement[4],
'unlock_location_id': achievement[5]
}
missing_achievements.append(achievement_dict)
return missing_achievements
async def get_location_weather(self, location_id: int) -> Optional[Dict]:
"""Get current weather for a location"""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM location_weather
WHERE location_id = ? AND active_until > datetime('now')
ORDER BY id DESC LIMIT 1
""", (location_id,))
row = await cursor.fetchone()
return dict(row) if row else None
async def activate_pet(self, player_id: int, pet_identifier: str) -> Dict:
"""Activate a pet by name or species name. Returns result dict."""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Find pet by nickname or species name
cursor = await db.execute("""
SELECT p.*, ps.name as species_name
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ? AND p.is_active = FALSE
AND (p.nickname = ? OR ps.name = ?)
LIMIT 1
""", (player_id, pet_identifier, pet_identifier))
pet = await cursor.fetchone()
if not pet:
return {"success": False, "error": f"No inactive pet found named '{pet_identifier}'"}
# Activate the pet
await db.execute("UPDATE pets SET is_active = TRUE WHERE id = ?", (pet["id"],))
await db.commit()
return {"success": True, "pet": dict(pet)}
async def deactivate_pet(self, player_id: int, pet_identifier: str) -> Dict:
"""Deactivate a pet by name or species name. Returns result dict."""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Find pet by nickname or species name
cursor = await db.execute("""
SELECT p.*, ps.name as species_name
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ? AND p.is_active = TRUE
AND (p.nickname = ? OR ps.name = ?)
LIMIT 1
""", (player_id, pet_identifier, pet_identifier))
pet = await cursor.fetchone()
if not pet:
return {"success": False, "error": f"No active pet found named '{pet_identifier}'"}
# Check if this is the only active pet
cursor = await db.execute("SELECT COUNT(*) as count FROM pets WHERE player_id = ? AND is_active = TRUE", (player_id,))
active_count = await cursor.fetchone()
if active_count["count"] <= 1:
return {"success": False, "error": "You must have at least one active pet!"}
# Deactivate the pet
await db.execute("UPDATE pets SET is_active = FALSE WHERE id = ?", (pet["id"],))
await db.commit()
return {"success": True, "pet": dict(pet)}
async def swap_pets(self, player_id: int, pet1_identifier: str, pet2_identifier: str) -> Dict:
"""Swap the active status of two pets. Returns result dict."""
async with aiosqlite.connect(self.db_path) as db:
db.row_factory = aiosqlite.Row
# Find both pets
cursor = await db.execute("""
SELECT p.*, ps.name as species_name
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ?
AND (p.nickname = ? OR ps.name = ?)
LIMIT 1
""", (player_id, pet1_identifier, pet1_identifier))
pet1 = await cursor.fetchone()
cursor = await db.execute("""
SELECT p.*, ps.name as species_name
FROM pets p
JOIN pet_species ps ON p.species_id = ps.id
WHERE p.player_id = ?
AND (p.nickname = ? OR ps.name = ?)
LIMIT 1
""", (player_id, pet2_identifier, pet2_identifier))
pet2 = await cursor.fetchone()
if not pet1:
return {"success": False, "error": f"Pet '{pet1_identifier}' not found"}
if not pet2:
return {"success": False, "error": f"Pet '{pet2_identifier}' not found"}
if pet1["id"] == pet2["id"]:
return {"success": False, "error": "Cannot swap a pet with itself"}
# Swap their active status
await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet1["is_active"], pet1["id"]))
await db.execute("UPDATE pets SET is_active = ? WHERE id = ?", (not pet2["is_active"], pet2["id"]))
await db.commit()
return {
"success": True,
"pet1": dict(pet1),
"pet2": dict(pet2),
"pet1_now": "active" if not pet1["is_active"] else "storage",
"pet2_now": "active" if not pet2["is_active"] else "storage"
}

596
src/game_engine.py Normal file
View file

@ -0,0 +1,596 @@
import json
import random
import aiosqlite
import asyncio
from typing import Dict, List, Optional
from .database import Database
from .battle_engine import BattleEngine
class GameEngine:
def __init__(self, database: Database):
self.database = database
self.battle_engine = BattleEngine(database)
self.pet_species = {}
self.locations = {}
self.moves = {}
self.type_chart = {}
self.weather_patterns = {}
self.weather_task = None
self.shutdown_event = asyncio.Event()
async def load_game_data(self):
await self.load_pet_species()
await self.load_locations()
await self.load_moves()
await self.load_type_chart()
await self.load_achievements()
await self.init_weather_system()
await self.battle_engine.load_battle_data()
async def load_pet_species(self):
try:
with open("config/pets.json", "r") as f:
species_data = json.load(f)
async with aiosqlite.connect(self.database.db_path) as db:
for species in species_data:
await db.execute("""
INSERT OR IGNORE INTO pet_species
(name, type1, type2, base_hp, base_attack, base_defense,
base_speed, evolution_level, rarity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
species["name"], species["type1"], species.get("type2"),
species["base_hp"], species["base_attack"], species["base_defense"],
species["base_speed"], species.get("evolution_level"),
species.get("rarity", 1)
))
await db.commit()
except FileNotFoundError:
await self.create_default_species()
async def create_default_species(self):
default_species = [
{
"name": "Flamey",
"type1": "Fire",
"base_hp": 45,
"base_attack": 52,
"base_defense": 43,
"base_speed": 65,
"rarity": 1
},
{
"name": "Aqua",
"type1": "Water",
"base_hp": 44,
"base_attack": 48,
"base_defense": 65,
"base_speed": 43,
"rarity": 1
},
{
"name": "Leafy",
"type1": "Grass",
"base_hp": 45,
"base_attack": 49,
"base_defense": 49,
"base_speed": 45,
"rarity": 1
},
{
"name": "Sparky",
"type1": "Electric",
"base_hp": 35,
"base_attack": 55,
"base_defense": 40,
"base_speed": 90,
"rarity": 2
},
{
"name": "Rocky",
"type1": "Rock",
"base_hp": 40,
"base_attack": 80,
"base_defense": 100,
"base_speed": 25,
"rarity": 2
}
]
async with aiosqlite.connect(self.database.db_path) as db:
for species in default_species:
await db.execute("""
INSERT OR IGNORE INTO pet_species
(name, type1, type2, base_hp, base_attack, base_defense, base_speed, rarity)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
species["name"], species["type1"], species.get("type2"),
species["base_hp"], species["base_attack"], species["base_defense"],
species["base_speed"], species["rarity"]
))
await db.commit()
async def load_locations(self):
try:
with open("config/locations.json", "r") as f:
locations_data = json.load(f)
except FileNotFoundError:
locations_data = self.get_default_locations()
async with aiosqlite.connect(self.database.db_path) as db:
for location in locations_data:
await db.execute("""
INSERT OR IGNORE INTO locations (name, description, level_min, level_max)
VALUES (?, ?, ?, ?)
""", (location["name"], location["description"],
location["level_min"], location["level_max"]))
cursor = await db.execute(
"SELECT id FROM locations WHERE name = ?", (location["name"],)
)
location_row = await cursor.fetchone()
location_id = location_row[0]
for spawn in location["spawns"]:
species_cursor = await db.execute(
"SELECT id FROM pet_species WHERE name = ?", (spawn["species"],)
)
species_id = await species_cursor.fetchone()
if species_id:
await db.execute("""
INSERT OR IGNORE INTO location_spawns
(location_id, species_id, spawn_rate, min_level, max_level)
VALUES (?, ?, ?, ?, ?)
""", (location_id, species_id[0], spawn["spawn_rate"],
spawn["min_level"], spawn["max_level"]))
await db.commit()
def get_default_locations(self):
return [
{
"name": "Starter Woods",
"description": "A peaceful forest perfect for beginners",
"level_min": 1,
"level_max": 5,
"spawns": [
{"species": "Leafy", "spawn_rate": 0.4, "min_level": 1, "max_level": 3},
{"species": "Flamey", "spawn_rate": 0.3, "min_level": 1, "max_level": 3},
{"species": "Aqua", "spawn_rate": 0.3, "min_level": 1, "max_level": 3}
]
},
{
"name": "Electric Canyon",
"description": "A charged valley crackling with energy",
"level_min": 3,
"level_max": 8,
"spawns": [
{"species": "Sparky", "spawn_rate": 0.6, "min_level": 3, "max_level": 6},
{"species": "Rocky", "spawn_rate": 0.4, "min_level": 4, "max_level": 7}
]
}
]
async def load_moves(self):
pass
async def load_type_chart(self):
pass
async def give_starter_pet(self, player_id: int) -> Dict:
starters = ["Flamey", "Aqua", "Leafy"]
chosen_starter = random.choice(starters)
async with aiosqlite.connect(self.database.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"SELECT * FROM pet_species WHERE name = ?", (chosen_starter,)
)
species = await cursor.fetchone()
pet_data = self.generate_pet_stats(dict(species), level=5)
cursor = await db.execute("""
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
attack, defense, speed, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (player_id, species["id"], pet_data["level"], 0,
pet_data["hp"], pet_data["hp"], pet_data["attack"],
pet_data["defense"], pet_data["speed"], True))
await db.commit()
return {"species_name": chosen_starter, **pet_data}
def generate_pet_stats(self, species: Dict, level: int = 1) -> Dict:
iv_bonus = random.randint(0, 31)
hp = int((2 * species["base_hp"] + iv_bonus) * level / 100) + level + 10
attack = int((2 * species["base_attack"] + iv_bonus) * level / 100) + 5
defense = int((2 * species["base_defense"] + iv_bonus) * level / 100) + 5
speed = int((2 * species["base_speed"] + iv_bonus) * level / 100) + 5
return {
"level": level,
"hp": hp,
"attack": attack,
"defense": defense,
"speed": speed
}
async def attempt_catch(self, player_id: int, location_name: str) -> str:
async with aiosqlite.connect(self.database.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"SELECT * FROM locations WHERE name = ?", (location_name,)
)
location = await cursor.fetchone()
if not location:
return f"Location '{location_name}' not found!"
cursor = await db.execute("""
SELECT ls.*, ps.name as species_name, ps.*
FROM location_spawns ls
JOIN pet_species ps ON ls.species_id = ps.id
WHERE ls.location_id = ?
""", (location["id"],))
spawns = await cursor.fetchall()
if not spawns:
return f"No pets found in {location_name}!"
if random.random() > 0.7:
return f"You searched {location_name} but found nothing..."
chosen_spawn = random.choices(
spawns,
weights=[spawn["spawn_rate"] for spawn in spawns]
)[0]
pet_level = random.randint(chosen_spawn["min_level"], chosen_spawn["max_level"])
pet_stats = self.generate_pet_stats(dict(chosen_spawn), pet_level)
catch_rate = 0.5 + (0.3 / chosen_spawn["rarity"])
if random.random() < catch_rate:
cursor = await db.execute("""
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
attack, defense, speed, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (player_id, chosen_spawn["species_id"], pet_level, 0,
pet_stats["hp"], pet_stats["hp"], pet_stats["attack"],
pet_stats["defense"], pet_stats["speed"], False))
await db.commit()
return f"Caught a level {pet_level} {chosen_spawn['species_name']}!"
else:
return f"A wild {chosen_spawn['species_name']} appeared but escaped!"
async def get_location_spawns(self, location_name: str) -> List[Dict]:
async with aiosqlite.connect(self.database.db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(
"SELECT * FROM locations WHERE name = ?", (location_name,)
)
location = await cursor.fetchone()
if not location:
return []
cursor = await db.execute("""
SELECT ps.name, ps.type1, ps.type2, ls.spawn_rate
FROM location_spawns ls
JOIN pet_species ps ON ls.species_id = ps.id
WHERE ls.location_id = ?
""", (location["id"],))
spawns = await cursor.fetchall()
return [dict(spawn) for spawn in spawns]
async def explore_location(self, player_id: int) -> Dict:
"""Explore the player's current location and return encounter results"""
async with aiosqlite.connect(self.database.db_path) as db:
db.row_factory = aiosqlite.Row
# Get player's current location
location = await self.database.get_player_location(player_id)
if not location:
return {"type": "error", "message": "You are not in a valid location!"}
# Get spawns for current location
cursor = await db.execute("""
SELECT ls.*, ps.name as species_name, ps.*
FROM location_spawns ls
JOIN pet_species ps ON ls.species_id = ps.id
WHERE ls.location_id = ?
""", (location["id"],))
spawns = await cursor.fetchall()
if not spawns:
return {"type": "empty", "message": f"You explore {location['name']} but find nothing interesting..."}
# Apply weather modifiers to spawns
modified_spawns = await self.get_weather_modified_spawns(location["id"], spawns)
# Random encounter chance (70% chance of finding something)
if random.random() > 0.7:
return {"type": "empty", "message": f"You explore {location['name']} but find nothing this time..."}
# Choose random spawn with weather-modified rates
chosen_spawn = random.choices(
modified_spawns,
weights=[spawn["spawn_rate"] for spawn in modified_spawns]
)[0]
pet_level = random.randint(chosen_spawn["min_level"], chosen_spawn["max_level"])
pet_stats = self.generate_pet_stats(dict(chosen_spawn), pet_level)
return {
"type": "encounter",
"location": location["name"],
"pet": {
"species_id": chosen_spawn["species_id"],
"species_name": chosen_spawn["species_name"],
"level": pet_level,
"type1": chosen_spawn["type1"],
"type2": chosen_spawn["type2"],
"stats": pet_stats,
# Additional battle-ready stats
"hp": pet_stats["hp"],
"max_hp": pet_stats["hp"],
"attack": pet_stats["attack"],
"defense": pet_stats["defense"],
"speed": pet_stats["speed"]
}
}
async def attempt_catch_current_location(self, player_id: int, target_pet: Dict) -> str:
"""Attempt to catch a pet during exploration"""
catch_rate = 0.5 + (0.3 / target_pet.get("rarity", 1))
if random.random() < catch_rate:
# Successfully caught the pet
async with aiosqlite.connect(self.database.db_path) as db:
cursor = await db.execute("""
INSERT INTO pets (player_id, species_id, level, experience, hp, max_hp,
attack, defense, speed, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (player_id, target_pet["species_id"], target_pet["level"], 0,
target_pet["stats"]["hp"], target_pet["stats"]["hp"],
target_pet["stats"]["attack"], target_pet["stats"]["defense"],
target_pet["stats"]["speed"], False))
await db.commit()
return f"Success! You caught the Level {target_pet['level']} {target_pet['species_name']}!"
else:
return f"The wild {target_pet['species_name']} escaped!"
async def load_achievements(self):
"""Load achievements from config and populate database"""
try:
with open("config/achievements.json", "r") as f:
achievements_data = json.load(f)
async with aiosqlite.connect(self.database.db_path) as db:
for achievement in achievements_data:
# Get location ID if specified
location_id = None
if achievement.get("unlock_location"):
cursor = await db.execute(
"SELECT id FROM locations WHERE name = ?",
(achievement["unlock_location"],)
)
location_row = await cursor.fetchone()
if location_row:
location_id = location_row[0]
# Insert or update achievement
await db.execute("""
INSERT OR IGNORE INTO achievements
(name, description, requirement_type, requirement_data, unlock_location_id)
VALUES (?, ?, ?, ?, ?)
""", (
achievement["name"], achievement["description"],
achievement["requirement_type"], achievement["requirement_data"],
location_id
))
await db.commit()
except FileNotFoundError:
print("No achievements.json found, skipping achievement loading")
async def init_weather_system(self):
"""Initialize random weather for all locations"""
try:
with open("config/weather_patterns.json", "r") as f:
self.weather_patterns = json.load(f)
# Set initial weather for all locations
await self.update_all_weather()
# Start background weather update task
await self.start_weather_system()
except FileNotFoundError:
print("No weather_patterns.json found, skipping weather system")
self.weather_patterns = {"weather_types": {}, "location_weather_chances": {}}
async def update_all_weather(self):
"""Update weather for all locations"""
import datetime
async with aiosqlite.connect(self.database.db_path) as db:
# Get all locations
cursor = await db.execute("SELECT * FROM locations")
locations = await cursor.fetchall()
for location in locations:
location_name = location[1] # name is second column
# Get possible weather for this location
possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["Calm"])
# Choose random weather
weather_type = random.choice(possible_weather)
weather_config = self.weather_patterns.get("weather_types", {}).get(weather_type, {
"spawn_modifier": 1.0,
"affected_types": [],
"duration_minutes": [90, 180]
})
# Calculate end time with random duration
duration_range = weather_config.get("duration_minutes", [90, 180])
duration_minutes = random.randint(duration_range[0], duration_range[1])
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration_minutes)
# Clear old weather and set new
await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location[0],))
await db.execute("""
INSERT INTO location_weather
(location_id, weather_type, active_until, spawn_modifier, affected_types)
VALUES (?, ?, ?, ?, ?)
""", (
location[0], weather_type, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
))
await db.commit()
async def check_and_award_achievements(self, player_id: int, action_type: str, data: str = ""):
"""Check for new achievements after player actions"""
return await self.database.check_player_achievements(player_id, action_type, data)
async def get_weather_modified_spawns(self, location_id: int, spawns: list) -> list:
"""Apply weather modifiers to spawn rates"""
weather = await self.database.get_location_weather(location_id)
if not weather or not weather.get("affected_types"):
return spawns # No weather effects
affected_types = weather["affected_types"].split(",") if weather["affected_types"] else []
modifier = weather.get("spawn_modifier", 1.0)
# Apply weather modifier to matching pet types
modified_spawns = []
for spawn in spawns:
spawn_dict = dict(spawn)
pet_types = [spawn_dict.get("type1")]
if spawn_dict.get("type2"):
pet_types.append(spawn_dict["type2"])
# Check if this pet type is affected by weather
if any(pet_type in affected_types for pet_type in pet_types):
spawn_dict["spawn_rate"] *= modifier
modified_spawns.append(spawn_dict)
return modified_spawns
async def start_weather_system(self):
"""Start the background weather update task"""
if self.weather_task is None or self.weather_task.done():
print("🌤️ Starting weather update background task...")
self.weather_task = asyncio.create_task(self._weather_update_loop())
async def stop_weather_system(self):
"""Stop the background weather update task"""
print("🌤️ Stopping weather update background task...")
self.shutdown_event.set()
if self.weather_task and not self.weather_task.done():
self.weather_task.cancel()
try:
await self.weather_task
except asyncio.CancelledError:
pass
async def _weather_update_loop(self):
"""Background task that checks and updates expired weather"""
try:
while not self.shutdown_event.is_set():
try:
# Check every 5 minutes for expired weather
await asyncio.sleep(300) # 5 minutes
if self.shutdown_event.is_set():
break
# Check for locations with expired weather
await self._check_and_update_expired_weather()
except asyncio.CancelledError:
break
except Exception as e:
print(f"Error in weather update loop: {e}")
# Continue the loop even if there's an error
await asyncio.sleep(60) # Wait a minute before retrying
except asyncio.CancelledError:
print("Weather update task cancelled")
async def _check_and_update_expired_weather(self):
"""Check for expired weather and update it"""
try:
async with aiosqlite.connect(self.database.db_path) as db:
# Find locations with expired weather
cursor = await db.execute("""
SELECT l.id, l.name
FROM locations l
WHERE l.id NOT IN (
SELECT location_id FROM location_weather
WHERE active_until > datetime('now')
)
""")
expired_locations = await cursor.fetchall()
if expired_locations:
print(f"🌤️ Updating weather for {len(expired_locations)} locations with expired weather")
for location in expired_locations:
location_id = location[0]
location_name = location[1]
# Get possible weather for this location
possible_weather = self.weather_patterns.get("location_weather_chances", {}).get(location_name, ["Calm"])
# Choose random weather
weather_type = random.choice(possible_weather)
weather_config = self.weather_patterns.get("weather_types", {}).get(weather_type, {
"spawn_modifier": 1.0,
"affected_types": [],
"duration_minutes": [90, 180]
})
# Calculate end time with random duration
duration_range = weather_config.get("duration_minutes", [90, 180])
duration_minutes = random.randint(duration_range[0], duration_range[1])
import datetime
end_time = datetime.datetime.now() + datetime.timedelta(minutes=duration_minutes)
# Clear old weather and set new
await db.execute("DELETE FROM location_weather WHERE location_id = ?", (location_id,))
await db.execute("""
INSERT INTO location_weather
(location_id, weather_type, active_until, spawn_modifier, affected_types)
VALUES (?, ?, ?, ?, ?)
""", (
location_id, weather_type, end_time.isoformat(),
weather_config.get("spawn_modifier", 1.0),
",".join(weather_config.get("affected_types", []))
))
print(f" 🌤️ {location_name}: New {weather_type} weather for {duration_minutes} minutes")
await db.commit()
except Exception as e:
print(f"Error checking expired weather: {e}")
async def shutdown(self):
"""Gracefully shutdown the game engine"""
print("🔄 Shutting down game engine...")
await self.stop_weather_system()

1425
webserver.py Normal file

File diff suppressed because it is too large Load diff