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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/__pycache__/levels.cpython-312.pyc
Normal file
BIN
src/__pycache__/levels.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/shop.cpython-312.pyc
Normal file
BIN
src/__pycache__/shop.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
40
src/db.py
40
src/db.py
@@ -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': {},
|
||||
|
||||
@@ -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)
|
||||
|
||||
87
src/game.py
87
src/game.py
@@ -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}")
|
||||
@@ -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:
|
||||
|
||||
25
src/shop.py
25
src/shop.py
@@ -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}"}
|
||||
|
||||
Reference in New Issue
Block a user