- Add dropped items tracking with timestamps per channel - Items drop to ground (10% chance on duck kills) for any player to grab - Add 60-second timeout for unclaimed items - Background cleanup task removes expired items automatically - First-come-first-served basis for item collection - Eggdrop-style messaging for drops and successful snatches
326 lines
13 KiB
Python
326 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Game mechanics for DuckHunt Bot
|
||
"""
|
||
|
||
import asyncio
|
||
import random
|
||
import time
|
||
import uuid
|
||
import logging
|
||
from typing import Dict, Any, Optional, List
|
||
|
||
|
||
class DuckGame:
|
||
"""Game mechanics and duck management"""
|
||
|
||
def __init__(self, bot, db):
|
||
self.bot = bot
|
||
self.db = db
|
||
self.ducks = {} # Format: {channel: [{'alive': True, 'spawn_time': time, 'id': uuid}, ...]}
|
||
self.logger = logging.getLogger('DuckHuntBot.Game')
|
||
|
||
# Colors for IRC messages
|
||
self.colors = {
|
||
'red': '\x0304',
|
||
'green': '\x0303',
|
||
'yellow': '\x0308',
|
||
'blue': '\x0302',
|
||
'cyan': '\x0311',
|
||
'magenta': '\x0306',
|
||
'white': '\x0300',
|
||
'bold': '\x02',
|
||
'reset': '\x03',
|
||
'underline': '\x1f'
|
||
}
|
||
|
||
def get_config(self, path, default=None):
|
||
"""Helper method to get config values"""
|
||
return self.bot.get_config(path, default)
|
||
|
||
def get_player_level(self, xp):
|
||
"""Calculate player level from XP"""
|
||
if xp < 0:
|
||
return 0
|
||
return int((xp ** 0.5) / 2) + 1
|
||
|
||
def get_xp_for_next_level(self, xp):
|
||
"""Calculate XP needed for next level"""
|
||
level = self.get_player_level(xp)
|
||
return ((level * 2) ** 2) - xp
|
||
|
||
def calculate_penalty_by_level(self, base_penalty, xp):
|
||
"""Reduce penalties for higher level players"""
|
||
level = self.get_player_level(xp)
|
||
return max(1, base_penalty - (level - 1))
|
||
|
||
def update_karma(self, player, event):
|
||
"""Update player karma based on events"""
|
||
if not self.get_config('karma.enabled', True):
|
||
return
|
||
|
||
karma_changes = {
|
||
'hit': self.get_config('karma.hit_bonus', 2),
|
||
'golden_hit': self.get_config('karma.golden_hit_bonus', 5),
|
||
'teamkill': -self.get_config('karma.teamkill_penalty', 10),
|
||
'wild_shot': -self.get_config('karma.wild_shot_penalty', 3),
|
||
'miss': -self.get_config('karma.miss_penalty', 1),
|
||
'befriend_success': self.get_config('karma.befriend_success_bonus', 2),
|
||
'befriend_fail': -self.get_config('karma.befriend_fail_penalty', 1)
|
||
}
|
||
|
||
if event in karma_changes:
|
||
player['karma'] = player.get('karma', 0) + karma_changes[event]
|
||
|
||
def is_sleep_time(self):
|
||
"""Check if ducks should not spawn due to sleep hours"""
|
||
sleep_hours = self.get_config('sleep_hours', [])
|
||
if not sleep_hours or len(sleep_hours) != 2:
|
||
return False
|
||
|
||
import datetime
|
||
current_hour = datetime.datetime.now().hour
|
||
start_hour, end_hour = sleep_hours
|
||
|
||
if start_hour <= end_hour:
|
||
return start_hour <= current_hour <= end_hour
|
||
else: # Crosses midnight
|
||
return current_hour >= start_hour or current_hour <= end_hour
|
||
|
||
def calculate_gun_reliability(self, player):
|
||
"""Calculate gun reliability with modifiers"""
|
||
base_reliability = player.get('reliability', 70)
|
||
# Add weapon modifiers, items, etc.
|
||
return min(100, max(0, base_reliability))
|
||
|
||
def gun_jams(self, player):
|
||
"""Check if gun jams (eggdrop style)"""
|
||
# Base jamming probability is inverse of reliability
|
||
reliability = player.get('reliability', 70)
|
||
jam_chance = max(1, 101 - reliability) # Higher reliability = lower jam chance
|
||
|
||
# Additional factors that increase jam chance
|
||
if player.get('total_ammo_used', 0) > 100:
|
||
jam_chance += 2 # Gun gets more prone to jamming with use
|
||
|
||
if player.get('jammed_count', 0) > 5:
|
||
jam_chance += 1 # Previously jammed guns are more prone to jamming
|
||
|
||
# Roll for jam (1-100, jam if roll <= jam_chance)
|
||
return random.randint(1, 100) <= jam_chance
|
||
|
||
async def scare_other_ducks(self, channel, shot_duck_id):
|
||
"""Scare other ducks when one is shot"""
|
||
if channel not in self.ducks:
|
||
return
|
||
|
||
for duck in self.ducks[channel][:]: # Copy list to avoid modification during iteration
|
||
if duck['id'] != shot_duck_id and duck['alive']:
|
||
# 30% chance to scare away other ducks
|
||
if random.random() < 0.3:
|
||
duck['alive'] = False
|
||
self.ducks[channel].remove(duck)
|
||
|
||
async def scare_duck_on_miss(self, channel, target_duck):
|
||
"""Scare duck when someone misses"""
|
||
if target_duck and random.random() < 0.15: # 15% chance
|
||
target_duck['alive'] = False
|
||
if channel in self.ducks and target_duck in self.ducks[channel]:
|
||
self.ducks[channel].remove(target_duck)
|
||
|
||
async def find_bushes_items(self, nick, channel, player):
|
||
"""Find random items in bushes"""
|
||
if not self.get_config('items.enabled', True):
|
||
return
|
||
|
||
if random.random() < 0.1: # 10% chance
|
||
items = [
|
||
("a mirror", "mirror", "You can now deflect shots!"),
|
||
("some sand", "sand", "Throw this to blind opponents!"),
|
||
("a rusty bullet", None, "It's too rusty to use..."),
|
||
("some bread crumbs", "bread", "Feed ducks to make them friendly!"),
|
||
]
|
||
|
||
found_item, item_key, message = random.choice(items)
|
||
|
||
if item_key and item_key in player:
|
||
player[item_key] = player.get(item_key, 0) + 1
|
||
elif item_key in player:
|
||
player[item_key] = player.get(item_key, 0) + 1
|
||
|
||
await self.bot.send_user_message(nick, channel,
|
||
f"You found {found_item} in the bushes! {message}")
|
||
|
||
def get_duck_spawn_message(self):
|
||
"""Get random duck spawn message (eggdrop style)"""
|
||
messages = [
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUACK",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o< QUACK!",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< QUAAACK!",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_ö< Quack?",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O< *QUACK*"
|
||
]
|
||
return random.choice(messages)
|
||
|
||
async def spawn_duck_now(self, channel, force_golden=False):
|
||
"""Spawn a duck immediately in the specified channel"""
|
||
if channel not in self.ducks:
|
||
self.ducks[channel] = []
|
||
|
||
max_ducks = self.get_config('max_ducks_per_channel', 3)
|
||
if len([d for d in self.ducks[channel] if d['alive']]) >= max_ducks:
|
||
self.logger.debug(f"Max ducks already in {channel}")
|
||
return
|
||
|
||
# Determine duck type
|
||
if force_golden:
|
||
duck_type = "golden"
|
||
else:
|
||
rand = random.random()
|
||
if rand < 0.02:
|
||
duck_type = "armored"
|
||
elif rand < 0.10:
|
||
duck_type = "golden"
|
||
elif rand < 0.30:
|
||
duck_type = "rare"
|
||
elif rand < 0.40:
|
||
duck_type = "fast"
|
||
else:
|
||
duck_type = "normal"
|
||
|
||
# Get duck configuration
|
||
duck_config = self.get_config(f'duck_types.{duck_type}', {})
|
||
if not duck_config.get('enabled', True):
|
||
duck_type = "normal"
|
||
duck_config = self.get_config('duck_types.normal', {})
|
||
|
||
# Create duck
|
||
duck = {
|
||
'id': str(uuid.uuid4())[:8],
|
||
'type': duck_type,
|
||
'alive': True,
|
||
'spawn_time': time.time(),
|
||
'health': duck_config.get('health', 1),
|
||
'max_health': duck_config.get('health', 1)
|
||
}
|
||
|
||
self.ducks[channel].append(duck)
|
||
|
||
# Send spawn message
|
||
messages = duck_config.get('messages', [self.get_duck_spawn_message()])
|
||
spawn_message = random.choice(messages)
|
||
|
||
self.bot.send_message(channel, spawn_message)
|
||
self.logger.info(f"Spawned {duck_type} duck in {channel}")
|
||
|
||
# Alert users who have alerts enabled
|
||
await self.send_duck_alerts(channel, duck_type)
|
||
|
||
return duck
|
||
|
||
async def send_duck_alerts(self, channel, duck_type):
|
||
"""Send alerts to users who have them enabled"""
|
||
if not self.get_config('social.duck_alerts_enabled', True):
|
||
return
|
||
|
||
# Implementation would iterate through players with alerts enabled
|
||
# For now, just log
|
||
self.logger.debug(f"Duck alerts for {duck_type} duck in {channel}")
|
||
|
||
async def spawn_ducks(self):
|
||
"""Main duck spawning loop"""
|
||
while not self.bot.shutdown_requested:
|
||
try:
|
||
if self.is_sleep_time():
|
||
await asyncio.sleep(300) # Check every 5 minutes during sleep
|
||
continue
|
||
|
||
for channel in self.bot.channels_joined:
|
||
if self.bot.shutdown_requested:
|
||
break
|
||
|
||
if channel not in self.ducks:
|
||
self.ducks[channel] = []
|
||
|
||
# Clean up dead ducks
|
||
self.ducks[channel] = [d for d in self.ducks[channel] if d['alive']]
|
||
|
||
max_ducks = self.get_config('max_ducks_per_channel', 3)
|
||
alive_ducks = len([d for d in self.ducks[channel] if d['alive']])
|
||
|
||
if alive_ducks < max_ducks:
|
||
min_spawn_time = self.get_config('duck_spawn_min', 1800)
|
||
max_spawn_time = self.get_config('duck_spawn_max', 5400)
|
||
|
||
if random.random() < 0.1: # 10% chance each check
|
||
await self.spawn_duck_now(channel)
|
||
|
||
await asyncio.sleep(random.randint(60, 300)) # Check every 1-5 minutes
|
||
|
||
except asyncio.CancelledError:
|
||
self.logger.info("Duck spawning loop cancelled")
|
||
break
|
||
except Exception as e:
|
||
self.logger.error(f"Error in duck spawning: {e}")
|
||
await asyncio.sleep(60)
|
||
|
||
async def duck_timeout_checker(self):
|
||
"""Check for ducks that should timeout"""
|
||
while not self.bot.shutdown_requested:
|
||
try:
|
||
current_time = time.time()
|
||
|
||
for channel in list(self.ducks.keys()):
|
||
if self.bot.shutdown_requested:
|
||
break
|
||
|
||
if channel not in self.ducks:
|
||
continue
|
||
|
||
for duck in self.ducks[channel][:]: # Copy to avoid modification
|
||
if not duck['alive']:
|
||
continue
|
||
|
||
age = current_time - duck['spawn_time']
|
||
min_timeout = self.get_config('duck_timeout_min', 45)
|
||
max_timeout = self.get_config('duck_timeout_max', 75)
|
||
|
||
timeout = random.randint(min_timeout, max_timeout)
|
||
|
||
if age > timeout:
|
||
duck['alive'] = False
|
||
self.ducks[channel].remove(duck)
|
||
|
||
# Send timeout message (eggdrop style)
|
||
timeout_messages = [
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck flew away!",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *FLAP FLAP FLAP*",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_o> The duck got tired of waiting and left!",
|
||
"-.,¸¸.-·°'`'°·-.,¸¸.-·°'`'°· \\_O> *KWAK* The duck escaped!"
|
||
]
|
||
self.bot.send_message(channel, random.choice(timeout_messages))
|
||
self.logger.debug(f"Duck timed out in {channel}")
|
||
|
||
await asyncio.sleep(10) # Check every 10 seconds
|
||
|
||
except asyncio.CancelledError:
|
||
self.logger.info("Duck timeout checker cancelled")
|
||
break
|
||
except Exception as e:
|
||
self.logger.error(f"Error in duck timeout checker: {e}")
|
||
await asyncio.sleep(30)
|
||
|
||
def get_alive_ducks(self, channel):
|
||
"""Get list of alive ducks in channel"""
|
||
if channel not in self.ducks:
|
||
return []
|
||
return [d for d in self.ducks[channel] if d['alive']]
|
||
|
||
def get_duck_by_id(self, channel, duck_id):
|
||
"""Get duck by ID"""
|
||
if channel not in self.ducks:
|
||
return None
|
||
for duck in self.ducks[channel]:
|
||
if duck['id'] == duck_id and duck['alive']:
|
||
return duck
|
||
return None |