Lemontropia-Suite/modules/crafting_tracker.py

360 lines
13 KiB
Python

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