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

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()