360 lines
13 KiB
Python
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']
|