Fix database corruption and enhance duck messages
- Fix missing field errors that caused 'ducks_shot' message format errors - Enhanced _sanitize_player_data to ensure all required fields exist - Added comprehensive field validation and type conversion - Added multiple variations for duck_flies_away messages (normal, fast, golden) - Improved error handling for corrupted/incomplete player data
This commit is contained in:
@@ -1,12 +1,36 @@
|
|||||||
{
|
{
|
||||||
"duck_spawn": [
|
"duck_spawn": [
|
||||||
"・゜゜・。。・゜゜\\_O< QUACK!",
|
"・゜゜・。。・゜゜\\_O< {light_grey}QUACK!{reset}",
|
||||||
|
"{light_grey}・゜゜・。。・゜゜{reset}\\_O< {light_grey}QUACK!{reset}",
|
||||||
|
"・゜゜・。。・゜゜{black}\\_O< QUACK!{reset}",
|
||||||
"・゜゜・。。・゜゜\\_o< quack~",
|
"・゜゜・。。・゜゜\\_o< quack~",
|
||||||
"・゜゜・。。・゜゜\\_O> *flap flap*"
|
"・゜゜・。。・゜゜\\_O> *flap flap*"
|
||||||
],
|
],
|
||||||
"duck_flies_away": "The duck flies away. ·°'`'°-.,¸¸.·°'`",
|
"duck_flies_away": [
|
||||||
"fast_duck_flies_away": "The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`",
|
"The duck flies away. ·°'`'°-.,¸¸.·°'`",
|
||||||
"golden_duck_flies_away": "The {gold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`",
|
"The duck escapes into the sky! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< *quack* The duck waddles away safely.",
|
||||||
|
"The duck flaps away, living another day. ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< The duck disappears into the distance.",
|
||||||
|
"The duck takes flight and vanishes! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< *flap* *flap* The duck has escaped!"
|
||||||
|
],
|
||||||
|
"fast_duck_flies_away": [
|
||||||
|
"The fast duck quickly flies away! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< *ZOOM* The speedy duck vanishes in a flash!",
|
||||||
|
"The fast duck zips away at lightning speed! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< Too slow! The fast duck has already escaped!",
|
||||||
|
"The swift duck darts away before you can blink! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< *whoosh* The fast duck is gone!"
|
||||||
|
],
|
||||||
|
"golden_duck_flies_away": [
|
||||||
|
"The {gold}golden duck{reset} flies away majestically. ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< The {gold}golden duck{reset} glides away gracefully, its feathers shimmering.",
|
||||||
|
"The precious {gold}golden duck{reset} escapes to safety! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< The {gold}golden duck{reset} spreads its magnificent wings and soars away.",
|
||||||
|
"The valuable {gold}golden duck{reset} disappears into the sunset! ·°'`'°-.,¸¸.·°'`",
|
||||||
|
"\\o< *glimmer* The {gold}golden duck{reset} vanishes like a treasure in the wind."
|
||||||
|
],
|
||||||
"bang_hit": "{nick} > {red}*BANG*{reset} You shot the duck! \\_X< *KWAK* {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
"bang_hit": "{nick} > {red}*BANG*{reset} You shot the duck! \\_X< *KWAK* {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
"bang_hit_golden": "{nick} > {red}*BANG*{reset} You shot a {gold}GOLDEN DUCK!{reset} [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
"bang_hit_golden": "{nick} > {red}*BANG*{reset} You shot a {gold}GOLDEN DUCK!{reset} [{hp_remaining} HP remaining] {green}[+{xp_gained} xp]{reset} [Total ducks: {ducks_shot}]",
|
||||||
"bang_hit_golden_killed": "{nick} > {red}*BANG*{reset} You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
|
"bang_hit_golden_killed": "{nick} > {red}*BANG*{reset} You killed the GOLDEN DUCK! [+{xp_gained} xp] [Total ducks: {ducks_shot}]",
|
||||||
|
|||||||
188
src/db.py
188
src/db.py
@@ -1,6 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
Simplified Database management for DuckHunt Bot
|
Simplified Database management for DuckHunt Bot
|
||||||
Only essential player fields
|
Focus on fixing missing field errors
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -23,7 +23,6 @@ class DuckDB:
|
|||||||
"""Load player data from JSON file with comprehensive error handling"""
|
"""Load player data from JSON file with comprehensive error handling"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self.db_file):
|
if os.path.exists(self.db_file):
|
||||||
# Try to load the main database file
|
|
||||||
with open(self.db_file, 'r', encoding='utf-8') as f:
|
with open(self.db_file, 'r', encoding='utf-8') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ class DuckDB:
|
|||||||
if not isinstance(players_data, dict):
|
if not isinstance(players_data, dict):
|
||||||
raise ValueError("Players data is not a dictionary")
|
raise ValueError("Players data is not a dictionary")
|
||||||
|
|
||||||
# Validate each player entry
|
# Validate each player entry and ensure required fields
|
||||||
valid_players = {}
|
valid_players = {}
|
||||||
for nick, player_data in players_data.items():
|
for nick, player_data in players_data.items():
|
||||||
if isinstance(player_data, dict) and isinstance(nick, str):
|
if isinstance(player_data, dict) and isinstance(nick, str):
|
||||||
@@ -59,53 +58,93 @@ class DuckDB:
|
|||||||
self.players = {}
|
self.players = {}
|
||||||
|
|
||||||
def _sanitize_player_data(self, player_data):
|
def _sanitize_player_data(self, player_data):
|
||||||
"""Sanitize and validate player data"""
|
"""Sanitize and validate player data, ensuring ALL required fields exist"""
|
||||||
try:
|
try:
|
||||||
sanitized = {}
|
sanitized = {}
|
||||||
|
|
||||||
# Ensure required fields with safe defaults
|
# Get default values from config or fallbacks
|
||||||
sanitized['nick'] = str(player_data.get('nick', 'Unknown'))[:50] # Limit nick length
|
default_accuracy = self.bot.get_config('player_defaults.accuracy', 75) if self.bot else 75
|
||||||
sanitized['xp'] = max(0, int(player_data.get('xp', 0))) # Non-negative XP
|
max_accuracy = self.bot.get_config('gameplay.max_accuracy', 100) if self.bot else 100
|
||||||
sanitized['ducks_shot'] = max(0, int(player_data.get('ducks_shot', 0)))
|
default_magazines = self.bot.get_config('player_defaults.magazines', 3) if self.bot else 3
|
||||||
sanitized['ducks_befriended'] = max(0, int(player_data.get('ducks_befriended', 0)))
|
default_bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6) if self.bot else 6
|
||||||
sanitized['shots_fired'] = max(0, int(player_data.get('shots_fired', 0)))
|
default_jam_chance = self.bot.get_config('player_defaults.jam_chance', 15) if self.bot else 15
|
||||||
sanitized['shots_missed'] = max(0, int(player_data.get('shots_missed', 0)))
|
|
||||||
default_accuracy = self.bot.get_config('default_accuracy', 75) if self.bot else 75
|
# Core required fields - these MUST exist for messages to work
|
||||||
max_accuracy = self.bot.get_config('max_accuracy', 100) if self.bot else 100
|
sanitized['nick'] = str(player_data.get('nick', 'Unknown'))[:50]
|
||||||
sanitized['accuracy'] = max(0, min(max_accuracy, int(player_data.get('accuracy', default_accuracy)))) # 0-max_accuracy range
|
sanitized['xp'] = max(0, int(float(player_data.get('xp', 0))))
|
||||||
|
sanitized['ducks_shot'] = max(0, int(float(player_data.get('ducks_shot', 0))))
|
||||||
|
sanitized['ducks_befriended'] = max(0, int(float(player_data.get('ducks_befriended', 0))))
|
||||||
|
sanitized['shots_fired'] = max(0, int(float(player_data.get('shots_fired', 0))))
|
||||||
|
sanitized['shots_missed'] = max(0, int(float(player_data.get('shots_missed', 0))))
|
||||||
|
|
||||||
|
# Equipment and stats
|
||||||
|
sanitized['accuracy'] = max(0, min(max_accuracy, int(float(player_data.get('accuracy', default_accuracy)))))
|
||||||
sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
|
sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
|
||||||
|
|
||||||
# Ammo system with validation
|
# Ammo system with validation
|
||||||
sanitized['current_ammo'] = max(0, min(50, int(player_data.get('current_ammo', 6))))
|
sanitized['current_ammo'] = max(0, min(50, int(float(player_data.get('current_ammo', default_bullets_per_mag)))))
|
||||||
sanitized['magazines'] = max(0, min(20, int(player_data.get('magazines', 3))))
|
sanitized['magazines'] = max(0, min(20, int(float(player_data.get('magazines', default_magazines)))))
|
||||||
|
sanitized['bullets_per_magazine'] = max(1, min(50, int(float(player_data.get('bullets_per_magazine', default_bullets_per_mag)))))
|
||||||
|
sanitized['jam_chance'] = max(0, min(100, int(float(player_data.get('jam_chance', default_jam_chance)))))
|
||||||
|
|
||||||
# Confiscated ammo (optional fields)
|
# Confiscated ammo (optional fields but with safe defaults)
|
||||||
if 'confiscated_ammo' in player_data:
|
sanitized['confiscated_ammo'] = max(0, min(50, int(float(player_data.get('confiscated_ammo', 0)))))
|
||||||
sanitized['confiscated_ammo'] = max(0, min(50, int(player_data.get('confiscated_ammo', 0))))
|
sanitized['confiscated_magazines'] = max(0, min(20, int(float(player_data.get('confiscated_magazines', 0)))))
|
||||||
if 'confiscated_magazines' in player_data:
|
|
||||||
sanitized['confiscated_magazines'] = max(0, min(20, int(player_data.get('confiscated_magazines', 0))))
|
|
||||||
sanitized['bullets_per_magazine'] = max(1, min(50, int(player_data.get('bullets_per_magazine', 6))))
|
|
||||||
sanitized['jam_chance'] = max(0, min(100, int(player_data.get('jam_chance', 5))))
|
|
||||||
|
|
||||||
# Safe inventory handling
|
# Safe inventory handling
|
||||||
inventory = player_data.get('inventory', {})
|
inventory = player_data.get('inventory', {})
|
||||||
if isinstance(inventory, dict):
|
if isinstance(inventory, dict):
|
||||||
sanitized['inventory'] = {str(k)[:10]: max(0, int(v)) for k, v in inventory.items() if isinstance(v, (int, float))}
|
clean_inventory = {}
|
||||||
|
for k, v in inventory.items():
|
||||||
|
try:
|
||||||
|
clean_key = str(k)[:20]
|
||||||
|
clean_value = max(0, int(float(v))) if isinstance(v, (int, float, str)) else 0
|
||||||
|
if clean_value > 0:
|
||||||
|
clean_inventory[clean_key] = clean_value
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
sanitized['inventory'] = clean_inventory
|
||||||
else:
|
else:
|
||||||
sanitized['inventory'] = {}
|
sanitized['inventory'] = {}
|
||||||
|
|
||||||
# Safe temporary effects
|
# Safe temporary effects
|
||||||
temp_effects = player_data.get('temporary_effects', [])
|
temp_effects = player_data.get('temporary_effects', [])
|
||||||
if isinstance(temp_effects, list):
|
if isinstance(temp_effects, list):
|
||||||
sanitized['temporary_effects'] = temp_effects[:20] # Limit to 20 effects
|
clean_effects = []
|
||||||
|
for effect in temp_effects[:20]:
|
||||||
|
if isinstance(effect, dict) and 'type' in effect:
|
||||||
|
clean_effects.append(effect)
|
||||||
|
sanitized['temporary_effects'] = clean_effects
|
||||||
else:
|
else:
|
||||||
sanitized['temporary_effects'] = []
|
sanitized['temporary_effects'] = []
|
||||||
|
|
||||||
|
# Add any missing fields that messages might reference
|
||||||
|
additional_fields = {
|
||||||
|
'best_time': 0.0,
|
||||||
|
'worst_time': 0.0,
|
||||||
|
'total_time_hunting': 0.0,
|
||||||
|
'level': 1,
|
||||||
|
'xp_gained': 0, # For message templates
|
||||||
|
'hp_remaining': 0, # For golden duck messages
|
||||||
|
'victim': '', # For friendly fire messages
|
||||||
|
'xp_lost': 0, # For penalty messages
|
||||||
|
'ammo': 0, # Legacy field
|
||||||
|
'max_ammo': 0, # Legacy field
|
||||||
|
'chargers': 0 # Legacy field
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, default_value in additional_fields.items():
|
||||||
|
if field not in sanitized:
|
||||||
|
if field in ['best_time', 'worst_time', 'total_time_hunting']:
|
||||||
|
sanitized[field] = max(0.0, float(player_data.get(field, default_value)))
|
||||||
|
else:
|
||||||
|
sanitized[field] = player_data.get(field, default_value)
|
||||||
|
|
||||||
return sanitized
|
return sanitized
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error sanitizing player data: {e}")
|
self.logger.error(f"Error sanitizing player data: {e}")
|
||||||
return self.create_player('Unknown')
|
return self.create_player(player_data.get('nick', 'Unknown') if isinstance(player_data, dict) else 'Unknown')
|
||||||
|
|
||||||
def save_database(self):
|
def save_database(self):
|
||||||
"""Save all player data to JSON file with comprehensive error handling"""
|
"""Save all player data to JSON file with comprehensive error handling"""
|
||||||
@@ -128,10 +167,10 @@ class DuckDB:
|
|||||||
# Write to temporary file first (atomic write)
|
# Write to temporary file first (atomic write)
|
||||||
with open(temp_file, 'w', encoding='utf-8') as f:
|
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
f.flush() # Ensure data is written to disk
|
f.flush()
|
||||||
os.fsync(f.fileno()) # Force write to disk
|
os.fsync(f.fileno())
|
||||||
|
|
||||||
# Atomic replace: move temp file to actual file
|
# Atomic replace
|
||||||
if os.name == 'nt': # Windows
|
if os.name == 'nt': # Windows
|
||||||
if os.path.exists(self.db_file):
|
if os.path.exists(self.db_file):
|
||||||
os.remove(self.db_file)
|
os.remove(self.db_file)
|
||||||
@@ -141,12 +180,8 @@ class DuckDB:
|
|||||||
|
|
||||||
self.logger.debug(f"Database saved successfully with {len(data['players'])} players")
|
self.logger.debug(f"Database saved successfully with {len(data['players'])} players")
|
||||||
|
|
||||||
except PermissionError:
|
|
||||||
self.logger.error("Permission denied when saving database")
|
|
||||||
except OSError as e:
|
|
||||||
self.logger.error(f"OS error saving database: {e}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Unexpected error saving database: {e}")
|
self.logger.error(f"Error saving database: {e}")
|
||||||
finally:
|
finally:
|
||||||
# Clean up temp file if it still exists
|
# Clean up temp file if it still exists
|
||||||
try:
|
try:
|
||||||
@@ -163,12 +198,12 @@ class DuckDB:
|
|||||||
self.logger.warning(f"Invalid nick provided: {nick}")
|
self.logger.warning(f"Invalid nick provided: {nick}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
nick_lower = nick.lower().strip()[:50] # Limit nick length and sanitize
|
nick_lower = nick.lower().strip()[:50]
|
||||||
|
|
||||||
if nick_lower not in self.players:
|
if nick_lower not in self.players:
|
||||||
self.players[nick_lower] = self.create_player(nick)
|
self.players[nick_lower] = self.create_player(nick)
|
||||||
else:
|
else:
|
||||||
# Ensure existing players have all required fields and sanitize data
|
# Ensure existing players have all required fields
|
||||||
player = self.players[nick_lower]
|
player = self.players[nick_lower]
|
||||||
if not isinstance(player, dict):
|
if not isinstance(player, dict):
|
||||||
self.logger.warning(f"Invalid player data for {nick_lower}, recreating")
|
self.logger.warning(f"Invalid player data for {nick_lower}, recreating")
|
||||||
@@ -189,17 +224,7 @@ class DuckDB:
|
|||||||
# Start with sanitized data
|
# Start with sanitized data
|
||||||
validated_player = self._sanitize_player_data(player)
|
validated_player = self._sanitize_player_data(player)
|
||||||
|
|
||||||
# Ensure new fields exist (migration from older versions)
|
# Migrate from old ammo/chargers system to magazine system if needed
|
||||||
if 'ducks_befriended' not in player:
|
|
||||||
validated_player['ducks_befriended'] = 0
|
|
||||||
if 'inventory' not in player:
|
|
||||||
validated_player['inventory'] = {}
|
|
||||||
if 'temporary_effects' not in player:
|
|
||||||
validated_player['temporary_effects'] = []
|
|
||||||
if 'jam_chance' not in player:
|
|
||||||
validated_player['jam_chance'] = 5 # Default 5% jam chance
|
|
||||||
|
|
||||||
# Migrate from old ammo/chargers system to magazine system
|
|
||||||
if 'magazines' not in player and ('ammo' in player or 'chargers' in player):
|
if 'magazines' not in player and ('ammo' in player or 'chargers' in player):
|
||||||
self.logger.info(f"Migrating {nick} from old ammo system to magazine system")
|
self.logger.info(f"Migrating {nick} from old ammo system to magazine system")
|
||||||
|
|
||||||
@@ -207,7 +232,7 @@ class DuckDB:
|
|||||||
old_chargers = player.get('chargers', 2)
|
old_chargers = player.get('chargers', 2)
|
||||||
|
|
||||||
validated_player['current_ammo'] = max(0, min(50, int(old_ammo)))
|
validated_player['current_ammo'] = max(0, min(50, int(old_ammo)))
|
||||||
validated_player['magazines'] = max(1, min(20, int(old_chargers) + 1)) # +1 for current loaded magazine
|
validated_player['magazines'] = max(1, min(20, int(old_chargers) + 1))
|
||||||
validated_player['bullets_per_magazine'] = 6
|
validated_player['bullets_per_magazine'] = 6
|
||||||
|
|
||||||
# Update nick in case it changed
|
# Update nick in case it changed
|
||||||
@@ -220,9 +245,8 @@ class DuckDB:
|
|||||||
return self.create_player(nick)
|
return self.create_player(nick)
|
||||||
|
|
||||||
def create_player(self, nick):
|
def create_player(self, nick):
|
||||||
"""Create a new player with configurable starting stats and inventory"""
|
"""Create a new player with all required fields"""
|
||||||
try:
|
try:
|
||||||
# Sanitize nick
|
|
||||||
safe_nick = str(nick)[:50] if nick else 'Unknown'
|
safe_nick = str(nick)[:50] if nick else 'Unknown'
|
||||||
|
|
||||||
# Get configurable defaults from bot config
|
# Get configurable defaults from bot config
|
||||||
@@ -230,14 +254,13 @@ class DuckDB:
|
|||||||
accuracy = self.bot.get_config('player_defaults.accuracy', 75)
|
accuracy = self.bot.get_config('player_defaults.accuracy', 75)
|
||||||
magazines = self.bot.get_config('player_defaults.magazines', 3)
|
magazines = self.bot.get_config('player_defaults.magazines', 3)
|
||||||
bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6)
|
bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6)
|
||||||
jam_chance = self.bot.get_config('player_defaults.jam_chance', 5)
|
jam_chance = self.bot.get_config('player_defaults.jam_chance', 15)
|
||||||
xp = self.bot.get_config('player_defaults.xp', 0)
|
xp = self.bot.get_config('player_defaults.xp', 0)
|
||||||
else:
|
else:
|
||||||
# Fallback defaults if no bot config available
|
|
||||||
accuracy = 75
|
accuracy = 75
|
||||||
magazines = 3
|
magazines = 3
|
||||||
bullets_per_mag = 6
|
bullets_per_mag = 6
|
||||||
jam_chance = 5
|
jam_chance = 15
|
||||||
xp = 0
|
xp = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -245,16 +268,30 @@ class DuckDB:
|
|||||||
'xp': xp,
|
'xp': xp,
|
||||||
'ducks_shot': 0,
|
'ducks_shot': 0,
|
||||||
'ducks_befriended': 0,
|
'ducks_befriended': 0,
|
||||||
'shots_fired': 0, # Total shots fired
|
'shots_fired': 0,
|
||||||
'shots_missed': 0, # Total shots that missed
|
'shots_missed': 0,
|
||||||
'current_ammo': bullets_per_mag, # Bullets in current magazine
|
'current_ammo': bullets_per_mag,
|
||||||
'magazines': magazines, # Total magazines (including current)
|
'magazines': magazines,
|
||||||
'bullets_per_magazine': bullets_per_mag, # Bullets per magazine
|
'bullets_per_magazine': bullets_per_mag,
|
||||||
'accuracy': accuracy, # Starting accuracy from config
|
'accuracy': accuracy,
|
||||||
'jam_chance': jam_chance, # Base gun jamming chance from config
|
'jam_chance': jam_chance,
|
||||||
'gun_confiscated': False,
|
'gun_confiscated': False,
|
||||||
'inventory': {}, # Empty starting inventory
|
'confiscated_ammo': 0,
|
||||||
'temporary_effects': [] # List of temporary effects
|
'confiscated_magazines': 0,
|
||||||
|
'inventory': {},
|
||||||
|
'temporary_effects': [],
|
||||||
|
# Additional fields to prevent KeyErrors
|
||||||
|
'best_time': 0.0,
|
||||||
|
'worst_time': 0.0,
|
||||||
|
'total_time_hunting': 0.0,
|
||||||
|
'level': 1,
|
||||||
|
'xp_gained': 0,
|
||||||
|
'hp_remaining': 0,
|
||||||
|
'victim': '',
|
||||||
|
'xp_lost': 0,
|
||||||
|
'ammo': bullets_per_mag, # Legacy
|
||||||
|
'max_ammo': bullets_per_mag, # Legacy
|
||||||
|
'chargers': magazines - 1 # Legacy
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"Error creating player for {nick}: {e}")
|
self.logger.error(f"Error creating player for {nick}: {e}")
|
||||||
@@ -263,33 +300,48 @@ class DuckDB:
|
|||||||
'xp': 0,
|
'xp': 0,
|
||||||
'ducks_shot': 0,
|
'ducks_shot': 0,
|
||||||
'ducks_befriended': 0,
|
'ducks_befriended': 0,
|
||||||
|
'shots_fired': 0,
|
||||||
|
'shots_missed': 0,
|
||||||
'current_ammo': 6,
|
'current_ammo': 6,
|
||||||
'magazines': 3,
|
'magazines': 3,
|
||||||
'bullets_per_magazine': 6,
|
'bullets_per_magazine': 6,
|
||||||
'accuracy': 75,
|
'accuracy': 75,
|
||||||
'jam_chance': 5,
|
'jam_chance': 15,
|
||||||
'gun_confiscated': False,
|
'gun_confiscated': False,
|
||||||
|
'confiscated_ammo': 0,
|
||||||
|
'confiscated_magazines': 0,
|
||||||
'inventory': {},
|
'inventory': {},
|
||||||
'temporary_effects': []
|
'temporary_effects': [],
|
||||||
|
'best_time': 0.0,
|
||||||
|
'worst_time': 0.0,
|
||||||
|
'total_time_hunting': 0.0,
|
||||||
|
'level': 1,
|
||||||
|
'xp_gained': 0,
|
||||||
|
'hp_remaining': 0,
|
||||||
|
'victim': '',
|
||||||
|
'xp_lost': 0,
|
||||||
|
'ammo': 6,
|
||||||
|
'max_ammo': 6,
|
||||||
|
'chargers': 2
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_leaderboard(self, category='xp', limit=3):
|
def get_leaderboard(self, category='xp', limit=3):
|
||||||
"""Get top players by specified category"""
|
"""Get top players by specified category"""
|
||||||
try:
|
try:
|
||||||
# Create list of (nick, value) tuples
|
|
||||||
leaderboard = []
|
leaderboard = []
|
||||||
|
|
||||||
for nick, player_data in self.players.items():
|
for nick, player_data in self.players.items():
|
||||||
|
sanitized_data = self._sanitize_player_data(player_data)
|
||||||
|
|
||||||
if category == 'xp':
|
if category == 'xp':
|
||||||
value = player_data.get('xp', 0)
|
value = sanitized_data.get('xp', 0)
|
||||||
elif category == 'ducks_shot':
|
elif category == 'ducks_shot':
|
||||||
value = player_data.get('ducks_shot', 0)
|
value = sanitized_data.get('ducks_shot', 0)
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
leaderboard.append((nick, value))
|
leaderboard.append((nick, value))
|
||||||
|
|
||||||
# Sort by value (descending) and take top N
|
|
||||||
leaderboard.sort(key=lambda x: x[1], reverse=True)
|
leaderboard.sort(key=lambda x: x[1], reverse=True)
|
||||||
return leaderboard[:limit]
|
return leaderboard[:limit]
|
||||||
|
|
||||||
|
|||||||
@@ -1251,7 +1251,7 @@ class DuckHuntBot:
|
|||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
self.logger.warning("Task cancellation timed out")
|
self.logger.warning("Task cancellation timed out")
|
||||||
|
|
||||||
# Quick database save
|
# Final database save
|
||||||
try:
|
try:
|
||||||
self.db.save_database()
|
self.db.save_database()
|
||||||
self.logger.info("Database saved")
|
self.logger.info("Database saved")
|
||||||
@@ -1294,4 +1294,5 @@ class DuckHuntBot:
|
|||||||
finally:
|
finally:
|
||||||
# Ensure writer is cleared regardless of errors
|
# Ensure writer is cleared regardless of errors
|
||||||
self.writer = None
|
self.writer = None
|
||||||
self.reader = None
|
self.reader = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user