""" Lemontropia Suite - Crafting Tracker Track crafting attempts, success rates, near successes, and profitability. """ import json import logging from decimal import Decimal from pathlib import Path from dataclasses import dataclass, field from typing import Dict, List, Optional from datetime import datetime from collections import defaultdict logger = logging.getLogger(__name__) @dataclass class CraftAttempt: """Single crafting attempt record.""" blueprint: str qr_before: Decimal qr_after: Decimal success: bool near_success: bool materials_cost: Decimal output_value: Decimal timestamp: datetime clicks: int = 1 @dataclass class BlueprintStats: """Statistics for a specific blueprint.""" blueprint_name: str total_attempts: int = 0 successes: int = 0 near_successes: int = 0 failures: int = 0 qr_gained: Decimal = field(default_factory=lambda: Decimal("0")) total_cost: Decimal = field(default_factory=lambda: Decimal("0")) total_output_value: Decimal = field(default_factory=lambda: Decimal("0")) current_qr: Decimal = field(default_factory=lambda: Decimal("0")) @property def success_rate(self) -> Decimal: """Calculate success rate percentage.""" if self.total_attempts > 0: return (Decimal(self.successes) / Decimal(self.total_attempts)) * 100 return Decimal("0") @property def near_success_rate(self) -> Decimal: """Calculate near success rate.""" if self.total_attempts > 0: return (Decimal(self.near_successes) / Decimal(self.total_attempts)) * 100 return Decimal("0") @property def profit_loss(self) -> Decimal: """Calculate profit/loss.""" return self.total_output_value - self.total_cost @property def cost_per_success(self) -> Decimal: """Average cost per successful click.""" if self.successes > 0: return self.total_cost / self.successes return Decimal("0") class CraftingTracker: """ Comprehensive crafting session tracker. Tracks: - Success/failure/near-success rates - QR progression on blueprints - Material costs vs output value - Profitability per blueprint """ def __init__(self, data_dir: Optional[Path] = None): self.data_dir = data_dir or Path.home() / ".lemontropia" / "crafting" self.data_dir.mkdir(parents=True, exist_ok=True) # Current session data self.blueprint_stats: Dict[str, BlueprintStats] = {} self.session_attempts: List[CraftAttempt] = [] # Load historical data self._load_data() def _load_data(self): """Load blueprint history.""" try: data_file = self.data_dir / "blueprint_history.json" if data_file.exists(): with open(data_file, 'r') as f: data = json.load(f) for name, stats in data.items(): self.blueprint_stats[name] = BlueprintStats( blueprint_name=name, total_attempts=stats.get('attempts', 0), successes=stats.get('successes', 0), near_successes=stats.get('near_successes', 0), failures=stats.get('failures', 0), qr_gained=Decimal(str(stats.get('qr_gained', 0))), total_cost=Decimal(str(stats.get('total_cost', 0))), total_output_value=Decimal(str(stats.get('output_value', 0))), current_qr=Decimal(str(stats.get('current_qr', 0))), ) except Exception as e: logger.error(f"Failed to load crafting data: {e}") def _save_data(self): """Save blueprint history.""" try: data_file = self.data_dir / "blueprint_history.json" data = {} for name, stats in self.blueprint_stats.items(): data[name] = { 'attempts': stats.total_attempts, 'successes': stats.successes, 'near_successes': stats.near_successes, 'failures': stats.failures, 'qr_gained': str(stats.qr_gained), 'total_cost': str(stats.total_cost), 'output_value': str(stats.total_output_value), 'current_qr': str(stats.current_qr), } with open(data_file, 'w') as f: json.dump(data, f, indent=2) except Exception as e: logger.error(f"Failed to save crafting data: {e}") def record_attempt(self, blueprint: str, success: bool, near_success: bool, materials_cost: Decimal, output_value: Decimal, qr_before: Decimal = Decimal("0"), qr_after: Decimal = Decimal("0"), clicks: int = 1): """Record a crafting attempt.""" # Create or get blueprint stats if blueprint not in self.blueprint_stats: self.blueprint_stats[blueprint] = BlueprintStats(blueprint) stats = self.blueprint_stats[blueprint] # Update stats stats.total_attempts += clicks if success: stats.successes += clicks elif near_success: stats.near_successes += clicks else: stats.failures += clicks stats.total_cost += materials_cost stats.total_output_value += output_value stats.current_qr = qr_after qr_diff = qr_after - qr_before if qr_diff > 0: stats.qr_gained += qr_diff # Record attempt attempt = CraftAttempt( blueprint=blueprint, qr_before=qr_before, qr_after=qr_after, success=success, near_success=near_success, materials_cost=materials_cost, output_value=output_value, timestamp=datetime.now(), clicks=clicks ) self.session_attempts.append(attempt) # Auto-save self._save_data() logger.info(f"Craft: {blueprint} - {'SUCCESS' if success else 'NEAR' if near_success else 'FAIL'} " f"(QR: {qr_before}% -> {qr_after}%)") def get_blueprint_summary(self, blueprint: str) -> Optional[BlueprintStats]: """Get stats for a specific blueprint.""" return self.blueprint_stats.get(blueprint) def get_all_summaries(self) -> List[BlueprintStats]: """Get all blueprint stats sorted by usage.""" return sorted( self.blueprint_stats.values(), key=lambda x: x.total_attempts, reverse=True ) def get_session_summary(self) -> Dict: """Get current session summary.""" total_attempts = len(self.session_attempts) total_successes = sum(1 for a in self.session_attempts if a.success) total_near = sum(1 for a in self.session_attempts if a.near_success) total_cost = sum(a.materials_cost for a in self.session_attempts) total_output = sum(a.output_value for a in self.session_attempts) return { 'total_attempts': total_attempts, 'successes': total_successes, 'near_successes': total_near, 'failures': total_attempts - total_successes - total_near, 'success_rate': (Decimal(total_successes) / Decimal(total_attempts) * 100) if total_attempts > 0 else Decimal("0"), 'total_cost': total_cost, 'total_output': total_output, 'profit_loss': total_output - total_cost, 'blueprints_used': len(set(a.blueprint for a in self.session_attempts)), } def get_success_rate_by_qr_range(self) -> Dict[str, Decimal]: """Analyze success rate by QR ranges.""" ranges = { '0-25%': {'attempts': 0, 'successes': 0}, '25-50%': {'attempts': 0, 'successes': 0}, '50-75%': {'attempts': 0, 'successes': 0}, '75-100%': {'attempts': 0, 'successes': 0}, } for attempt in self.session_attempts: qr = attempt.qr_before if qr < 25: key = '0-25%' elif qr < 50: key = '25-50%' elif qr < 75: key = '50-75%' else: key = '75-100%' ranges[key]['attempts'] += 1 if attempt.success: ranges[key]['successes'] += 1 return { r: (Decimal(data['successes']) / Decimal(data['attempts']) * 100) if data['attempts'] > 0 else Decimal("0") for r, data in ranges.items() } def generate_report(self) -> str: """Generate detailed crafting report.""" session = self.get_session_summary() report = [] report.append("=" * 60) report.append("CRAFTING SESSION REPORT") report.append("=" * 60) report.append(f"Total Attempts: {session['total_attempts']}") report.append(f"Success Rate: {session['success_rate']:.1f}%") report.append(f"Material Cost: {session['total_cost']:.2f} PED") report.append(f"Output Value: {session['total_output']:.2f} PED") report.append(f"Profit/Loss: {session['profit_loss']:+.2f} PED") report.append("") report.append("BLUEPRINT BREAKDOWN:") for stats in self.get_all_summaries()[:10]: report.append(f" {stats.blueprint_name[:25]:25} " f"SR: {stats.success_rate:5.1f}% " f"P/L: {stats.profit_loss:+7.2f}") report.append("") report.append("SUCCESS RATE BY QR RANGE:") for range_name, rate in self.get_success_rate_by_qr_range().items(): report.append(f" {range_name}: {rate:.1f}%") return "\n".join(report) def should_continue_crafting(self, blueprint: str, max_loss: Decimal = Decimal("50")) -> bool: """Advise whether to continue crafting a blueprint.""" stats = self.blueprint_stats.get(blueprint) if not stats: return True # Stop if losing too much if stats.profit_loss < -max_loss: return False # Stop if success rate is terrible and QR is high if stats.success_rate < 20 and stats.current_qr > 80: return False return True class MaterialTracker: """ Track material inventory and consumption. """ def __init__(self, data_dir: Optional[Path] = None): self.data_dir = data_dir or Path.home() / ".lemontropia" / "materials" self.data_dir.mkdir(parents=True, exist_ok=True) self.inventory: Dict[str, Decimal] = {} self._load_inventory() def _load_inventory(self): """Load material inventory.""" try: inv_file = self.data_dir / "inventory.json" if inv_file.exists(): with open(inv_file, 'r') as f: data = json.load(f) self.inventory = {k: Decimal(str(v)) for k, v in data.items()} except Exception as e: logger.error(f"Failed to load inventory: {e}") def _save_inventory(self): """Save material inventory.""" try: inv_file = self.data_dir / "inventory.json" with open(inv_file, 'w') as f: json.dump({k: str(v) for k, v in self.inventory.items()}, f, indent=2) except Exception as e: logger.error(f"Failed to save inventory: {e}") def add_material(self, name: str, quantity: Decimal, unit_value: Optional[Decimal] = None): """Add materials to inventory.""" if name in self.inventory: self.inventory[name] += quantity else: self.inventory[name] = quantity self._save_inventory() def consume_material(self, name: str, quantity: Decimal) -> bool: """Consume materials from inventory.""" if name not in self.inventory or self.inventory[name] < quantity: logger.warning(f"Insufficient {name} (have {self.inventory.get(name, 0)}, need {quantity})") return False self.inventory[name] -= quantity if self.inventory[name] <= 0: del self.inventory[name] self._save_inventory() return True def get_inventory_value(self, price_lookup: Optional[Dict[str, Decimal]] = None) -> Decimal: """Calculate total inventory value.""" total = Decimal("0") for item, qty in self.inventory.items(): price = Decimal("1") # Default value if price_lookup and item in price_lookup: price = price_lookup[item] total += qty * price return total # Export main classes __all__ = ['CraftingTracker', 'MaterialTracker', 'BlueprintStats', 'CraftAttempt']