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
This commit is contained in:
2025-09-24 20:26:49 +01:00
parent 6ca624bd2f
commit 74f3afdf4b
18 changed files with 306 additions and 189 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -12,8 +12,9 @@ import os
class DuckDB:
"""Simplified database management"""
def __init__(self, db_file="duckhunt.json"):
def __init__(self, db_file="duckhunt.json", bot=None):
self.db_file = db_file
self.bot = bot
self.players = {}
self.logger = logging.getLogger('DuckHuntBot.DB')
self.load_database()
@@ -67,7 +68,9 @@ class DuckDB:
sanitized['xp'] = max(0, int(player_data.get('xp', 0))) # Non-negative XP
sanitized['ducks_shot'] = max(0, int(player_data.get('ducks_shot', 0)))
sanitized['ducks_befriended'] = max(0, int(player_data.get('ducks_befriended', 0)))
sanitized['accuracy'] = max(0, min(100, int(player_data.get('accuracy', 65)))) # 0-100 range
default_accuracy = self.bot.get_config('default_accuracy', 75) if self.bot else 75
max_accuracy = self.bot.get_config('max_accuracy', 100) if self.bot else 100
sanitized['accuracy'] = max(0, min(max_accuracy, int(player_data.get('accuracy', default_accuracy)))) # 0-max_accuracy range
sanitized['gun_confiscated'] = bool(player_data.get('gun_confiscated', False))
# Ammo system with validation
@@ -209,23 +212,38 @@ class DuckDB:
return self.create_player(nick)
def create_player(self, nick):
"""Create a new player with basic stats and validation"""
"""Create a new player with configurable starting stats and inventory"""
try:
# Sanitize nick
safe_nick = str(nick)[:50] if nick else 'Unknown'
# Get configurable defaults from bot config
if self.bot:
accuracy = self.bot.get_config('player_defaults.accuracy', 75)
magazines = self.bot.get_config('player_defaults.magazines', 3)
bullets_per_mag = self.bot.get_config('player_defaults.bullets_per_magazine', 6)
jam_chance = self.bot.get_config('player_defaults.jam_chance', 5)
xp = self.bot.get_config('player_defaults.xp', 0)
else:
# Fallback defaults if no bot config available
accuracy = 75
magazines = 3
bullets_per_mag = 6
jam_chance = 5
xp = 0
return {
'nick': safe_nick,
'xp': 0,
'xp': xp,
'ducks_shot': 0,
'ducks_befriended': 0,
'current_ammo': 6, # Bullets in current magazine
'magazines': 3, # Total magazines (including current)
'bullets_per_magazine': 6, # Bullets per magazine
'accuracy': 65,
'jam_chance': 5, # 5% base gun jamming chance
'current_ammo': bullets_per_mag, # Bullets in current magazine
'magazines': magazines, # Total magazines (including current)
'bullets_per_magazine': bullets_per_mag, # Bullets per magazine
'accuracy': accuracy, # Starting accuracy from config
'jam_chance': jam_chance, # Base gun jamming chance from config
'gun_confiscated': False,
'inventory': {}, # {item_id: quantity}
'inventory': {}, # Empty starting inventory
'temporary_effects': [] # List of temporary effects
}
except Exception as e:
@@ -238,7 +256,7 @@ class DuckDB:
'current_ammo': 6,
'magazines': 3,
'bullets_per_magazine': 6,
'accuracy': 65,
'accuracy': 75,
'jam_chance': 5,
'gun_confiscated': False,
'inventory': {},

View File

@@ -26,7 +26,7 @@ class DuckHuntBot:
self.channels_joined = set()
self.shutdown_requested = False
self.db = DuckDB()
self.db = DuckDB(bot=self)
self.game = DuckGame(self, self.db)
self.messages = MessageManager()
@@ -97,8 +97,8 @@ class DuckHuntBot:
async def connect(self):
"""Connect to IRC server with comprehensive error handling"""
max_retries = 3
retry_delay = 5
max_retries = self.get_config('connection.max_retries', 3) or 3
retry_delay = self.get_config('connection.retry_delay', 5) or 5
for attempt in range(max_retries):
try:
@@ -117,7 +117,7 @@ class DuckHuntBot:
self.config['port'],
ssl=ssl_context
),
timeout=30.0 # 30 second connection timeout
timeout=self.get_config('connection.timeout', 30) or 30.0 # Connection timeout from config
)
self.logger.info(f"✅ Successfully connected to {self.config['server']}:{self.config['port']}")
@@ -454,7 +454,7 @@ class DuckHuntBot:
xp = player.get('xp', 0)
ducks_shot = player.get('ducks_shot', 0)
ducks_befriended = player.get('ducks_befriended', 0)
accuracy = player.get('accuracy', 65)
accuracy = player.get('accuracy', self.get_config('player_defaults.accuracy', 75))
# Ammo info
current_ammo = player.get('current_ammo', 0)
@@ -531,13 +531,24 @@ class DuckHuntBot:
if not result["success"]:
message = f"{nick} > {result['message']}"
else:
if result.get("target_affected"):
# Handle specific item effect messages
effect = result.get('effect', {})
effect_type = effect.get('type', '')
if effect_type == 'attract_ducks':
# Use specific message for bread
message = self.messages.get('use_attract_ducks',
nick=nick,
spawn_multiplier=effect.get('spawn_multiplier', 2.0),
duration=effect.get('duration', 10)
)
elif result.get("target_affected"):
message = f"{nick} > Used {result['item_name']} on {target_nick}!"
else:
message = f"{nick} > Used {result['item_name']}!"
# Add remaining count if any
if result.get("remaining_in_inventory", 0) > 0:
# Add remaining count if any (not for bread message which has its own format)
if effect_type != 'attract_ducks' and result.get("remaining_in_inventory", 0) > 0:
message += f" ({result['remaining_in_inventory']} remaining)"
self.send_message(channel, message)

View File

@@ -35,8 +35,16 @@ class DuckGame:
try:
while True:
# Wait random time between spawns, but in small chunks for responsiveness
min_wait = self.bot.get_config('duck_spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawn_max', 900) # 15 minutes
min_wait = self.bot.get_config('duck_spawning.spawn_min', 300) # 5 minutes
max_wait = self.bot.get_config('duck_spawning.spawn_max', 900) # 15 minutes
# Check for active bread effects to modify spawn timing
spawn_multiplier = self._get_active_spawn_multiplier()
if spawn_multiplier > 1.0:
# Reduce wait time when bread is active
min_wait = int(min_wait / spawn_multiplier)
max_wait = int(max_wait / spawn_multiplier)
wait_time = random.randint(min_wait, max_wait)
# Sleep in 1-second intervals to allow for quick cancellation
@@ -65,12 +73,9 @@ class DuckGame:
for channel, ducks in self.ducks.items():
ducks_to_remove = []
for duck in ducks:
# Different timeouts for different duck types
# Get timeout for each duck type from config
duck_type = duck.get('duck_type', 'normal')
if duck_type == 'fast':
timeout = self.bot.get_config('fast_duck_timeout', 30)
else:
timeout = self.bot.get_config('duck_timeout', 60)
timeout = self.bot.get_config(f'duck_types.{duck_type}.timeout', 60)
if current_time - duck['spawn_time'] > timeout:
ducks_to_remove.append(duck)
@@ -94,6 +99,9 @@ class DuckGame:
for channel in channels_to_clear:
if channel in self.ducks and not self.ducks[channel]:
del self.ducks[channel]
# Clean expired effects every loop iteration
self._clean_expired_effects()
except asyncio.CancelledError:
self.logger.info("Duck timeout loop cancelled")
@@ -175,8 +183,8 @@ class DuckGame:
'message_args': {'nick': nick}
}
# Check for gun jamming
jam_chance = player.get('jam_chance', 5) / 100.0 # Convert percentage to decimal
# Check for gun jamming using level-based jam chance
jam_chance = self.bot.levels.get_jam_chance(player) / 100.0 # Convert percentage to decimal
if random.random() < jam_chance:
# Gun jammed! Use ammo but don't shoot
player['current_ammo'] = player.get('current_ammo', 1) - 1
@@ -216,7 +224,9 @@ class DuckGame:
if duck['current_hp'] > 0:
# Still alive, reveal it's golden but don't remove
player['accuracy'] = min(player.get('accuracy', 65) + 1, 100)
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
max_accuracy = self.bot.get_config('max_accuracy', 100)
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
self.db.save_database()
return {
'success': True,
@@ -241,14 +251,16 @@ class DuckGame:
else:
# Normal duck
self.ducks[channel].pop(0)
xp_gained = 10
xp_gained = self.bot.get_config('normal_duck_xp', 10)
message_key = 'bang_hit'
# Apply XP and level changes
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_shot'] = player.get('ducks_shot', 0) + 1
player['accuracy'] = min(player.get('accuracy', 65) + 1, 100)
accuracy_gain = self.bot.get_config('accuracy_gain_on_hit', 1)
max_accuracy = self.bot.get_config('max_accuracy', 100)
player['accuracy'] = min(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) + accuracy_gain, max_accuracy)
# Check if player leveled up and update magazines if needed
new_level = self.bot.levels.calculate_player_level(player)
@@ -272,7 +284,9 @@ class DuckGame:
}
else:
# Miss! Duck stays in the channel
player['accuracy'] = max(player.get('accuracy', 65) - 2, 10)
accuracy_loss = self.bot.get_config('accuracy_loss_on_miss', 2)
min_accuracy = self.bot.get_config('min_accuracy', 10)
player['accuracy'] = max(player.get('accuracy', self.bot.get_config('default_accuracy', 75)) - accuracy_loss, min_accuracy)
self.db.save_database()
return {
'success': True,
@@ -309,8 +323,8 @@ class DuckGame:
# Success - befriend the duck
duck = self.ducks[channel].pop(0)
# Lower XP gain than shooting (5 instead of 10)
xp_gained = 5
# Lower XP gain than shooting
xp_gained = self.bot.get_config('befriend_duck_xp', 5)
old_level = self.bot.levels.calculate_player_level(player)
player['xp'] = player.get('xp', 0) + xp_gained
player['ducks_befriended'] = player.get('ducks_befriended', 0) + 1
@@ -407,4 +421,45 @@ class DuckGame:
if rearmed_count > 0:
self.logger.info(f"Auto-rearmed {rearmed_count} disarmed players after duck shot")
except Exception as e:
self.logger.error(f"Error in _rearm_all_disarmed_players: {e}")
self.logger.error(f"Error in _rearm_all_disarmed_players: {e}")
def _get_active_spawn_multiplier(self):
"""Get the current spawn rate multiplier from active bread effects"""
import time
max_multiplier = 1.0
current_time = time.time()
try:
for player_name, player_data in self.db.players.items():
effects = player_data.get('temporary_effects', [])
for effect in effects:
if (effect.get('type') == 'attract_ducks' and
effect.get('expires_at', 0) > current_time):
multiplier = effect.get('spawn_multiplier', 1.0)
max_multiplier = max(max_multiplier, multiplier)
return max_multiplier
except Exception as e:
self.logger.error(f"Error getting spawn multiplier: {e}")
return 1.0
def _clean_expired_effects(self):
"""Remove expired temporary effects from all players"""
import time
current_time = time.time()
try:
for player_name, player_data in self.db.players.items():
effects = player_data.get('temporary_effects', [])
active_effects = []
for effect in effects:
if effect.get('expires_at', 0) > current_time:
active_effects.append(effect)
if len(active_effects) != len(effects):
player_data['temporary_effects'] = active_effects
self.logger.debug(f"Cleaned expired effects for {player_name}")
except Exception as e:
self.logger.error(f"Error cleaning expired effects: {e}")

View File

@@ -141,7 +141,7 @@ class LevelManager:
def get_modified_accuracy(self, player: Dict[str, Any]) -> int:
"""Get player's accuracy modified by their level"""
base_accuracy = player.get('accuracy', 65)
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)
@@ -154,9 +154,20 @@ class LevelManager:
level_info = self.get_player_level_info(player)
level_rate = level_info.get('befriend_success_rate', base_rate)
# Return as percentage (0-100)
# 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:

View File

@@ -178,7 +178,7 @@ class ShopManager:
elif item_type == 'accuracy':
# Increase accuracy up to 100%
current_accuracy = player.get('accuracy', 65)
current_accuracy = player.get('accuracy', 75)
new_accuracy = min(current_accuracy + amount, 100)
player['accuracy'] = new_accuracy
return {
@@ -279,7 +279,7 @@ class ShopManager:
elif item_type == 'sabotage_accuracy':
# Reduce target's accuracy temporarily
current_acc = player.get('accuracy', 65)
current_acc = player.get('accuracy', 75)
new_acc = max(current_acc + amount, 10) # Min 10% accuracy (amount is negative)
player['accuracy'] = new_acc
@@ -325,6 +325,27 @@ class ShopManager:
"new_total": new_jam
}
elif item_type == 'attract_ducks':
# Add bread effect to increase duck spawn rate
if 'temporary_effects' not in player:
player['temporary_effects'] = []
duration = item.get('duration', 600) # 10 minutes default
spawn_multiplier = item.get('spawn_multiplier', 2.0) # 2x spawn rate default
effect = {
'type': 'attract_ducks',
'spawn_multiplier': spawn_multiplier,
'expires_at': time.time() + duration
}
player['temporary_effects'].append(effect)
return {
"type": "attract_ducks",
"spawn_multiplier": spawn_multiplier,
"duration": duration // 60 # return duration in minutes
}
else:
self.logger.warning(f"Unknown item type: {item_type}")
return {"type": "unknown", "message": f"Unknown effect type: {item_type}"}