Petbot/modules/battle_system.py
megaproxy 6053161b6e Fix gym battle completion error handling
- Fixed tuple index out of range error in end_gym_battle()
- Added proper row factory and named column access in database queries
- Added exception handling and bounds checking in gym battle completion
- Added debug logging to track gym battle state issues
- Improved error messages for gym battle failures

Fixes: "tuple index out of range" error when gym battles complete

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 13:11:10 +01:00

294 lines
No EOL
14 KiB
Python

#!/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"]:
# Check if this is a gym battle
gym_battle = await self.database.get_active_gym_battle(player["id"])
if gym_battle:
print(f"DEBUG: Gym battle completion - player: {player['id']}, result: {result.get('winner')}")
await self.handle_gym_battle_completion(channel, nickname, player, result, gym_battle)
else:
# Regular wild battle
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
# Check if this is a gym battle
gym_battle = await self.database.get_active_gym_battle(player["id"])
if gym_battle:
# Can't flee from gym battles
self.send_message(channel, f"{nickname}: You can't flee from a gym battle! Fight or forfeit with your honor intact!")
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}")
async def handle_gym_battle_completion(self, channel, nickname, player, battle_result, gym_battle):
"""Handle completion of a gym battle turn"""
try:
if battle_result["winner"] == "player":
# Player won this individual battle
current_pet_index = gym_battle["current_pet_index"]
gym_team = gym_battle["gym_team"]
# Safety check for index bounds
if current_pet_index >= len(gym_team):
self.send_message(channel, f"{nickname}: Gym battle state error - please !forfeit and try again")
return
defeated_pet = gym_team[current_pet_index]
self.send_message(channel, f"🎉 {nickname}: You defeated {defeated_pet['species_name']}!")
# Check if there are more pets
if await self.database.advance_gym_battle(player["id"]):
# More pets to fight
next_index = current_pet_index + 1
next_pet = gym_team[next_index]
self.send_message(channel,
f"🥊 {gym_battle['leader_name']} sends out {next_pet['species_name']} (Lv.{next_pet['level']})!")
# Start battle with next gym pet
active_pets = await self.database.get_active_pets(player["id"])
player_pet = active_pets[0] # Use first active pet
# Create gym pet data for battle engine
next_gym_pet_data = {
"species_name": next_pet["species_name"],
"level": next_pet["level"],
"type1": next_pet["type1"],
"type2": next_pet["type2"],
"stats": {
"hp": next_pet["hp"],
"attack": next_pet["attack"],
"defense": next_pet["defense"],
"speed": next_pet["speed"]
}
}
# Start next battle
battle = await self.game_engine.battle_engine.start_battle(player["id"], player_pet, next_gym_pet_data)
self.send_message(channel,
f"⚔️ Your {player_pet['species_name']} (HP: {battle['player_hp']}/{player_pet['max_hp']}) vs {next_pet['species_name']} (HP: {battle['wild_hp']}/{next_pet['hp']})")
# Show available moves
moves_colored = " | ".join([
f"{self.get_move_color(move['type'])}{move['name']}\x0F"
for move in battle["available_moves"]
])
self.send_message(channel, f"🎯 Moves: {moves_colored} | Use !attack <move> or !use <item>")
else:
# All gym pets defeated - gym victory!
result = await self.database.end_gym_battle(player["id"], victory=True)
self.send_message(channel, f"🏆 {nickname}: You defeated all of {gym_battle['leader_name']}'s pets!")
self.send_message(channel,
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Impressive! You've earned the {gym_battle['gym_name']} badge!\"")
self.send_message(channel, f"🎉 {nickname} earned the {gym_battle['gym_name']} badge {gym_battle['badge_icon']}!")
# Award rewards based on difficulty
money_reward = 500 + (result["difficulty_level"] * 100)
self.send_message(channel, f"💰 Rewards: ${money_reward} | 🌟 Gym mastery increased!")
else:
# Player lost gym battle
result = await self.database.end_gym_battle(player["id"], victory=False)
self.send_message(channel, f"💀 {nickname}: Your pet fainted!")
self.send_message(channel,
f"{gym_battle['badge_icon']} {gym_battle['leader_name']}: \"Good battle! Train more and come back stronger!\"")
self.send_message(channel,
f"💡 {nickname}: Try leveling up your pets or bringing items to heal during battle!")
except Exception as e:
self.send_message(channel, f"{nickname}: Gym battle error occurred - please !forfeit and try again")
print(f"Gym battle completion error: {e}")
import traceback
traceback.print_exc()