Files
duckhunt/src/levels.py
ComputerTech312 74f3afdf4b Restructure config.json with nested hierarchy and dot notation access
- 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
2025-09-24 20:26:49 +01:00

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']
}