- Reorganized config.json into logical sections: connection, duck_spawning, duck_types, player_defaults, gameplay, features, limits - Enhanced get_config() method to support dot notation (e.g., 'duck_types.normal.xp') - Added comprehensive configurable parameters for all game mechanics - Updated player creation to use configurable starting values - Added individual timeout settings per duck type - Made XP rewards, accuracy mechanics, and game limits fully configurable - Fixed syntax errors in duck_spawn_loop function
231 lines
9.7 KiB
Python
231 lines
9.7 KiB
Python
"""
|
|
Level system for DuckHunt Bot
|
|
Manages player levels and difficulty scaling
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import logging
|
|
from typing import Dict, Any, Optional, Tuple
|
|
|
|
|
|
class LevelManager:
|
|
"""Manages the DuckHunt level system and difficulty scaling"""
|
|
|
|
def __init__(self, levels_file: str = "levels.json"):
|
|
self.levels_file = levels_file
|
|
self.levels_data = {}
|
|
self.logger = logging.getLogger('DuckHuntBot.Levels')
|
|
self.load_levels()
|
|
|
|
def load_levels(self):
|
|
"""Load level definitions from JSON file"""
|
|
try:
|
|
if os.path.exists(self.levels_file):
|
|
with open(self.levels_file, 'r', encoding='utf-8') as f:
|
|
self.levels_data = json.load(f)
|
|
level_count = len(self.levels_data.get('levels', {}))
|
|
self.logger.info(f"Loaded {level_count} levels from {self.levels_file}")
|
|
else:
|
|
# Fallback levels if file doesn't exist
|
|
self.levels_data = self._get_default_levels()
|
|
self.logger.warning(f"{self.levels_file} not found, using default levels")
|
|
except Exception as e:
|
|
self.logger.error(f"Error loading levels: {e}, using defaults")
|
|
self.levels_data = self._get_default_levels()
|
|
|
|
def _get_default_levels(self) -> Dict[str, Any]:
|
|
"""Default fallback level system"""
|
|
return {
|
|
"level_calculation": {
|
|
"method": "xp",
|
|
"description": "Level based on XP earned"
|
|
},
|
|
"levels": {
|
|
"1": {
|
|
"name": "Duck Novice",
|
|
"min_xp": 0,
|
|
"max_xp": 49,
|
|
"befriend_success_rate": 85,
|
|
"accuracy_modifier": 5,
|
|
"duck_spawn_speed_modifier": 1.0,
|
|
"description": "Just starting out"
|
|
},
|
|
"2": {
|
|
"name": "Duck Hunter",
|
|
"min_xp": 50,
|
|
"max_xp": 299,
|
|
"befriend_success_rate": 75,
|
|
"accuracy_modifier": 0,
|
|
"duck_spawn_speed_modifier": 0.8,
|
|
"description": "Getting experienced"
|
|
}
|
|
}
|
|
}
|
|
|
|
def calculate_player_level(self, player: Dict[str, Any]) -> int:
|
|
"""Calculate a player's current level based on their stats"""
|
|
method = self.levels_data.get('level_calculation', {}).get('method', 'xp')
|
|
|
|
if method == 'xp':
|
|
player_xp = player.get('xp', 0)
|
|
elif method == 'total_ducks':
|
|
# Fallback to duck-based calculation if specified
|
|
total_ducks = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
|
|
player_xp = total_ducks # Use duck count as if it were XP
|
|
else:
|
|
player_xp = player.get('xp', 0)
|
|
|
|
# Find the appropriate level
|
|
levels = self.levels_data.get('levels', {})
|
|
for level_num in sorted(levels.keys(), key=int, reverse=True):
|
|
level_data = levels[level_num]
|
|
# Check for XP-based thresholds first, fallback to duck-based
|
|
min_threshold = level_data.get('min_xp', level_data.get('min_ducks', 0))
|
|
if player_xp >= min_threshold:
|
|
return int(level_num)
|
|
|
|
return 1 # Default to level 1
|
|
|
|
def get_level_data(self, level: int) -> Optional[Dict[str, Any]]:
|
|
"""Get level data for a specific level"""
|
|
return self.levels_data.get('levels', {}).get(str(level))
|
|
|
|
def get_player_level_info(self, player: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Get complete level information for a player"""
|
|
level = self.calculate_player_level(player)
|
|
level_data = self.get_level_data(level)
|
|
|
|
if not level_data:
|
|
return {
|
|
"level": 1,
|
|
"name": "Duck Novice",
|
|
"description": "Default level",
|
|
"befriend_success_rate": 75,
|
|
"accuracy_modifier": 0,
|
|
"duck_spawn_speed_modifier": 1.0
|
|
}
|
|
|
|
method = self.levels_data.get('level_calculation', {}).get('method', 'xp')
|
|
if method == 'xp':
|
|
current_value = player.get('xp', 0)
|
|
value_type = "xp"
|
|
else:
|
|
current_value = player.get('ducks_shot', 0) + player.get('ducks_befriended', 0)
|
|
value_type = "ducks"
|
|
|
|
# Calculate progress to next level
|
|
next_level_data = self.get_level_data(level + 1)
|
|
if next_level_data:
|
|
threshold_key = f'min_{value_type}' if value_type == 'xp' else 'min_ducks'
|
|
next_threshold = next_level_data.get(threshold_key, 0)
|
|
needed_for_next = next_threshold - current_value
|
|
next_level_name = next_level_data.get('name', f"Level {level + 1}")
|
|
else:
|
|
needed_for_next = 0
|
|
next_level_name = "Max Level"
|
|
|
|
return {
|
|
"level": level,
|
|
"name": level_data.get('name', f"Level {level}"),
|
|
"description": level_data.get('description', ''),
|
|
"befriend_success_rate": level_data.get('befriend_success_rate', 75),
|
|
"accuracy_modifier": level_data.get('accuracy_modifier', 0),
|
|
"duck_spawn_speed_modifier": level_data.get('duck_spawn_speed_modifier', 1.0),
|
|
"current_xp": player.get('xp', 0),
|
|
"total_ducks": player.get('ducks_shot', 0) + player.get('ducks_befriended', 0),
|
|
"needed_for_next": max(0, needed_for_next),
|
|
"next_level_name": next_level_name,
|
|
"value_type": value_type
|
|
}
|
|
|
|
def get_modified_accuracy(self, player: Dict[str, Any]) -> int:
|
|
"""Get player's accuracy modified by their level"""
|
|
base_accuracy = player.get('accuracy', 75) # This will be updated by bot config in create_player
|
|
level_info = self.get_player_level_info(player)
|
|
modifier = level_info.get('accuracy_modifier', 0)
|
|
|
|
# Apply modifier and clamp between 10-100
|
|
modified_accuracy = base_accuracy + modifier
|
|
return max(10, min(100, modified_accuracy))
|
|
|
|
def get_modified_befriend_rate(self, player: Dict[str, Any], base_rate: float = 75.0) -> float:
|
|
"""Get player's befriend success rate modified by their level"""
|
|
level_info = self.get_player_level_info(player)
|
|
level_rate = level_info.get('befriend_success_rate', base_rate)
|
|
|
|
# Return as percentage (0-100) - these will be configurable later if bot reference is available
|
|
return max(5.0, min(95.0, level_rate))
|
|
|
|
def get_jam_chance(self, player: Dict[str, Any]) -> float:
|
|
"""Get player's gun jam chance based on their level"""
|
|
level_info = self.get_player_level_info(player)
|
|
level_data = self.get_level_data(level_info['level'])
|
|
|
|
if level_data and 'jam_chance' in level_data:
|
|
return level_data['jam_chance']
|
|
|
|
# Fallback to old system if no level-specific jam chance
|
|
return player.get('jam_chance', 5)
|
|
|
|
def get_duck_spawn_modifier(self, player_levels: list) -> float:
|
|
"""Get duck spawn speed modifier based on highest level player in channel"""
|
|
if not player_levels:
|
|
return 1.0
|
|
|
|
# Use the modifier from the highest level player (makes it harder for everyone)
|
|
max_level = max(player_levels)
|
|
level_data = self.get_level_data(max_level)
|
|
|
|
if level_data:
|
|
return level_data.get('duck_spawn_speed_modifier', 1.0)
|
|
|
|
return 1.0
|
|
|
|
def reload_levels(self) -> int:
|
|
"""Reload levels from file and return count"""
|
|
old_count = len(self.levels_data.get('levels', {}))
|
|
self.load_levels()
|
|
new_count = len(self.levels_data.get('levels', {}))
|
|
self.logger.info(f"Levels reloaded: {old_count} -> {new_count} levels")
|
|
return new_count
|
|
|
|
def update_player_magazines(self, player: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Update player's magazine count based on their current level"""
|
|
level_info = self.get_player_level_info(player)
|
|
level_magazines = level_info.get('magazines', 3)
|
|
level_bullets_per_mag = level_info.get('bullets_per_magazine', 6)
|
|
|
|
# Get current magazine status
|
|
current_magazines = player.get('magazines', 3)
|
|
current_ammo = player.get('current_ammo', 6)
|
|
current_bullets_per_mag = player.get('bullets_per_magazine', 6)
|
|
|
|
# Calculate total bullets they currently have
|
|
total_current_bullets = current_ammo + (current_magazines - 1) * current_bullets_per_mag
|
|
|
|
# Update magazine system to level requirements
|
|
player['magazines'] = level_magazines
|
|
player['bullets_per_magazine'] = level_bullets_per_mag
|
|
|
|
# Redistribute bullets across new magazine system
|
|
max_total_bullets = level_magazines * level_bullets_per_mag
|
|
new_total_bullets = min(total_current_bullets, max_total_bullets)
|
|
|
|
# Calculate how to distribute bullets
|
|
if new_total_bullets <= 0:
|
|
player['current_ammo'] = 0
|
|
elif new_total_bullets <= level_bullets_per_mag:
|
|
# All bullets fit in current magazine
|
|
player['current_ammo'] = new_total_bullets
|
|
else:
|
|
# Fill current magazine, save rest for other magazines
|
|
player['current_ammo'] = level_bullets_per_mag
|
|
|
|
return {
|
|
'old_magazines': current_magazines,
|
|
'new_magazines': level_magazines,
|
|
'old_total_bullets': total_current_bullets,
|
|
'new_total_bullets': new_total_bullets,
|
|
'current_ammo': player['current_ammo']
|
|
} |