import json import os import time from typing import List, Dict, Tuple import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import io import base64 class ELOSystem: """Core ELO rating calculation system.""" def __init__(self, k_factor: int = 32, initial_elo: float = 1000.0): self.k_factor = k_factor self.initial_elo = initial_elo def calculate_expected_score(self, elo_a: float, elo_b: float) -> float: """Calculate expected win probability for player A vs player B.""" return 1.0 / (1.0 + 10.0 ** ((elo_b - elo_a) / 400.0)) def update_elo(self, winner_elo: float, loser_elo: float) -> Tuple[float, float]: """Update ELO ratings after a match. Returns (new_winner_elo, new_loser_elo).""" expected_winner = self.calculate_expected_score(winner_elo, loser_elo) expected_loser = 1.0 - expected_winner new_winner = winner_elo + self.k_factor * (1.0 - expected_winner) new_loser = loser_elo + self.k_factor * (0.0 - expected_loser) return round(new_winner, 2), round(new_loser, 2) class PlaybookELOTracker: """Tracks ELO ratings for playbook versions with persistence.""" def __init__(self, storage_path: str = "./elo_history/"): self.storage_path = storage_path self.elo_system = ELOSystem() self.history: List[Dict] = [] os.makedirs(storage_path, exist_ok=True) self._load() def _load(self) -> None: """Load ELO history from disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") if os.path.exists(history_file): with open(history_file, "r") as f: self.history = json.load(f) def _save(self) -> None: """Save ELO history to disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") with open(history_file, "w") as f: json.dump(self.history, f, indent=2, default=str) def register_playbook(self, version: str, elo: float = 1000.0) -> None: """Register a new playbook version with initial ELO.""" entry = { "version": version, "elo": elo, "timestamp": time.time(), "matches_played": 0, "matches_won": 0 } self.history.append(entry) self._save() def get_current_elo(self, version: str) -> float: """Get the current ELO for a playbook version.""" for entry in reversed(self.history): if entry["version"] == version: return entry["elo"] return self.elo_system.initial_elo def record_matchup(self, version_a: str, version_b: str, winner: str) -> Tuple[float, float]: """Record a match between two playbook versions. Returns (new_elo_a, new_elo_b).""" elo_a = self.get_current_elo(version_a) elo_b = self.get_current_elo(version_b) if winner == version_a: new_a, new_b = self.elo_system.update_elo(elo_a, elo_b) elif winner == version_b: new_b, new_a = self.elo_system.update_elo(elo_b, elo_a) else: raise ValueError(f"Winner must be one of the two versions: {version_a} or {version_b}") self._update_entry(version_a, new_a, version_a == winner) self._update_entry(version_b, new_b, version_b == winner) return new_a, new_b def _update_entry(self, version: str, new_elo: float, won: bool) -> None: """Update a single entry in history.""" for entry in reversed(self.history): if entry["version"] == version: entry["elo"] = new_elo entry["matches_played"] += 1 if won: entry["matches_won"] += 1 entry["timestamp"] = time.time() break else: self.register_playbook(version, new_elo) for entry in reversed(self.history): if entry["version"] == version: entry["matches_played"] = 1 if won: entry["matches_won"] = 1 break self._save() def get_elo_history(self) -> List[Dict]: """Return full ELO history for plotting.""" return self.history def get_current_champion(self) -> str: """Return the version with the highest ELO.""" if not self.history: return "none" return max(self.history, key=lambda x: x["elo"])["version"] def get_elo_curve_data(self) -> Dict: """Return data formatted for plotting: {versions: [], elos: [], timestamps: []}.""" sorted_history = sorted(self.history, key=lambda x: x["timestamp"]) return { "versions": [h["version"] for h in sorted_history], "elos": [h["elo"] for h in sorted_history], "timestamps": [h["timestamp"] for h in sorted_history], "matches_played": [h["matches_played"] for h in sorted_history], "matches_won": [h["matches_won"] for h in sorted_history] } class ELOSystem: """Core ELO rating calculation system.""" def __init__(self, k_factor: int = 32, initial_elo: float = 1000.0): self.k_factor = k_factor self.initial_elo = initial_elo def calculate_expected_score(self, elo_a: float, elo_b: float) -> float: """Calculate expected win probability for player A vs player B.""" return 1.0 / (1.0 + 10.0 ** ((elo_b - elo_a) / 400.0)) def update_elo(self, winner_elo: float, loser_elo: float) -> Tuple[float, float]: """Update ELO ratings after a match. Returns (new_winner_elo, new_loser_elo).""" expected_winner = self.calculate_expected_score(winner_elo, loser_elo) expected_loser = 1.0 - expected_winner new_winner = winner_elo + self.k_factor * (1.0 - expected_winner) new_loser = loser_elo + self.k_factor * (0.0 - expected_loser) return round(new_winner, 2), round(new_loser, 2) class PlaybookELOTracker: """Tracks ELO ratings for playbook versions with persistence.""" def __init__(self, storage_path: str = "./elo_history/"): self.storage_path = storage_path self.elo_system = ELOSystem() self.history: List[Dict] = [] os.makedirs(storage_path, exist_ok=True) self._load() def _load(self) -> None: """Load ELO history from disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") if os.path.exists(history_file): with open(history_file, "r") as f: self.history = json.load(f) def _save(self) -> None: """Save ELO history to disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") with open(history_file, "w") as f: json.dump(self.history, f, indent=2, default=str) def register_playbook(self, version: str, elo: float = 1000.0) -> None: """Register a new playbook version with initial ELO.""" entry = { "version": version, "elo": elo, "timestamp": time.time(), "matches_played": 0, "matches_won": 0 } self.history.append(entry) self._save() def get_current_elo(self, version: str) -> float: """Get the current ELO for a playbook version.""" for entry in reversed(self.history): if entry["version"] == version: return entry["elo"] return self.elo_system.initial_elo def record_matchup(self, version_a: str, version_b: str, winner: str) -> Tuple[float, float]: """Record a match between two playbook versions. Returns (new_elo_a, new_elo_b).""" elo_a = self.get_current_elo(version_a) elo_b = self.get_current_elo(version_b) if winner == version_a: new_a, new_b = self.elo_system.update_elo(elo_a, elo_b) elif winner == version_b: new_b, new_a = self.elo_system.update_elo(elo_b, elo_a) else: raise ValueError(f"Winner must be one of the two versions: {version_a} or {version_b}") self._update_entry(version_a, new_a, version_a == winner) self._update_entry(version_b, new_b, version_b == winner) return new_a, new_b def _update_entry(self, version: str, new_elo: float, won: bool) -> None: """Update a single entry in history.""" for entry in reversed(self.history): if entry["version"] == version: entry["elo"] = new_elo entry["matches_played"] += 1 if won: entry["matches_won"] += 1 entry["timestamp"] = time.time() break else: self.register_playbook(version, new_elo) for entry in reversed(self.history): if entry["version"] == version: entry["matches_played"] = 1 if won: entry["matches_won"] = 1 break self._save() def get_elo_history(self) -> List[Dict]: """Return full ELO history for plotting.""" return self.history def get_current_champion(self) -> str: """Return the version with the highest ELO.""" if not self.history: return "none" return max(self.history, key=lambda x: x["elo"])["version"] def get_elo_curve_data(self) -> Dict: """Return data formatted for plotting: {versions: [], elos: [], timestamps: []}.""" sorted_history = sorted(self.history, key=lambda x: x["timestamp"]) return { "versions": [h["version"] for h in sorted_history], "elos": [h["elo"] for h in sorted_history], "timestamps": [h["timestamp"] for h in sorted_history], "matches_played": [h["matches_played"] for h in sorted_history], "matches_won": [h["matches_won"] for h in sorted_history] } class ELOSystem: """Core ELO rating calculation system.""" def __init__(self, k_factor: int = 32, initial_elo: float = 1000.0): self.k_factor = k_factor self.initial_elo = initial_elo def calculate_expected_score(self, elo_a: float, elo_b: float) -> float: """Calculate expected win probability for player A vs player B.""" return 1.0 / (1.0 + 10.0 ** ((elo_b - elo_a) / 400.0)) def update_elo(self, winner_elo: float, loser_elo: float) -> Tuple[float, float]: """Update ELO ratings after a match. Returns (new_winner_elo, new_loser_elo).""" expected_winner = self.calculate_expected_score(winner_elo, loser_elo) expected_loser = 1.0 - expected_winner new_winner = winner_elo + self.k_factor * (1.0 - expected_winner) new_loser = loser_elo + self.k_factor * (0.0 - expected_loser) return round(new_winner, 2), round(new_loser, 2) class PlaybookELOTracker: """Tracks ELO ratings for playbook versions with persistence.""" def __init__(self, storage_path: str = "./elo_history/"): self.storage_path = storage_path self.elo_system = ELOSystem() self.history: List[Dict] = [] os.makedirs(storage_path, exist_ok=True) self._load() def _load(self) -> None: """Load ELO history from disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") if os.path.exists(history_file): with open(history_file, "r") as f: self.history = json.load(f) def _save(self) -> None: """Save ELO history to disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") with open(history_file, "w") as f: json.dump(self.history, f, indent=2, default=str) def register_playbook(self, version: str, elo: float = 1000.0) -> None: """Register a new playbook version with initial ELO.""" entry = { "version": version, "elo": elo, "timestamp": time.time(), "matches_played": 0, "matches_won": 0 } self.history.append(entry) self._save() def get_current_elo(self, version: str) -> float: """Get the current ELO for a playbook version.""" for entry in reversed(self.history): if entry["version"] == version: return entry["elo"] return self.elo_system.initial_elo def record_matchup(self, version_a: str, version_b: str, winner: str) -> Tuple[float, float]: """Record a match between two playbook versions. Returns (new_elo_a, new_elo_b).""" elo_a = self.get_current_elo(version_a) elo_b = self.get_current_elo(version_b) if winner == version_a: new_a, new_b = self.elo_system.update_elo(elo_a, elo_b) elif winner == version_b: new_b, new_a = self.elo_system.update_elo(elo_b, elo_a) else: raise ValueError(f"Winner must be one of the two versions: {version_a} or {version_b}") self._update_entry(version_a, new_a, version_a == winner) self._update_entry(version_b, new_b, version_b == winner) return new_a, new_b def _update_entry(self, version: str, new_elo: float, won: bool) -> None: """Update a single entry in history.""" for entry in reversed(self.history): if entry["version"] == version: entry["elo"] = new_elo entry["matches_played"] += 1 if won: entry["matches_won"] += 1 entry["timestamp"] = time.time() break else: self.register_playbook(version, new_elo) for entry in reversed(self.history): if entry["version"] == version: entry["matches_played"] = 1 if won: entry["matches_won"] = 1 break self._save() def get_elo_history(self) -> List[Dict]: """Return full ELO history for plotting.""" return self.history def get_current_champion(self) -> str: """Return the version with the highest ELO.""" if not self.history: return "none" return max(self.history, key=lambda x: x["elo"])["version"] def get_elo_curve_data(self) -> Dict: """Return data formatted for plotting: {versions: [], elos: [], timestamps: []}.""" sorted_history = sorted(self.history, key=lambda x: x["timestamp"]) return { "versions": [h["version"] for h in sorted_history], "elos": [h["elo"] for h in sorted_history], "timestamps": [h["timestamp"] for h in sorted_history], "matches_played": [h["matches_played"] for h in sorted_history], "matches_won": [h["matches_won"] for h in sorted_history] } class ELOSystem: """Core ELO rating calculation system.""" def __init__(self, k_factor: int = 32, initial_elo: float = 1000.0): self.k_factor = k_factor self.initial_elo = initial_elo def calculate_expected_score(self, elo_a: float, elo_b: float) -> float: """Calculate expected win probability for player A vs player B.""" return 1.0 / (1.0 + 10.0 ** ((elo_b - elo_a) / 400.0)) def update_elo(self, winner_elo: float, loser_elo: float) -> Tuple[float, float]: """Update ELO ratings after a match. Returns (new_winner_elo, new_loser_elo).""" expected_winner = self.calculate_expected_score(winner_elo, loser_elo) expected_loser = 1.0 - expected_winner new_winner = winner_elo + self.k_factor * (1.0 - expected_winner) new_loser = loser_elo + self.k_factor * (0.0 - expected_loser) return round(new_winner, 2), round(new_loser, 2) class PlaybookELOTracker: """Tracks ELO ratings for playbook versions with persistence.""" def __init__(self, storage_path: str = "./elo_history/"): self.storage_path = storage_path self.elo_system = ELOSystem() self.history: List[Dict] = [] os.makedirs(storage_path, exist_ok=True) self._load() def _load(self) -> None: """Load ELO history from disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") if os.path.exists(history_file): with open(history_file, "r") as f: self.history = json.load(f) def _save(self) -> None: """Save ELO history to disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") with open(history_file, "w") as f: json.dump(self.history, f, indent=2, default=str) def register_playbook(self, version: str, elo: float = 1000.0) -> None: """Register a new playbook version with initial ELO.""" entry = { "version": version, "elo": elo, "timestamp": time.time(), "matches_played": 0, "matches_won": 0 } self.history.append(entry) self._save() def get_current_elo(self, version: str) -> float: """Get the current ELO for a playbook version.""" for entry in reversed(self.history): if entry["version"] == version: return entry["elo"] return self.elo_system.initial_elo def record_matchup(self, version_a: str, version_b: str, winner: str) -> Tuple[float, float]: """Record a match between two playbook versions. Returns (new_elo_a, new_elo_b).""" elo_a = self.get_current_elo(version_a) elo_b = self.get_current_elo(version_b) if winner == version_a: new_a, new_b = self.elo_system.update_elo(elo_a, elo_b) elif winner == version_b: new_b, new_a = self.elo_system.update_elo(elo_b, elo_a) else: raise ValueError(f"Winner must be one of the two versions: {version_a} or {version_b}") self._update_entry(version_a, new_a, version_a == winner) self._update_entry(version_b, new_b, version_b == winner) return new_a, new_b def _update_entry(self, version: str, new_elo: float, won: bool) -> None: """Update a single entry in history.""" for entry in reversed(self.history): if entry["version"] == version: entry["elo"] = new_elo entry["matches_played"] += 1 if won: entry["matches_won"] += 1 entry["timestamp"] = time.time() break else: self.register_playbook(version, new_elo) for entry in reversed(self.history): if entry["version"] == version: entry["matches_played"] = 1 if won: entry["matches_won"] = 1 break self._save() def get_elo_history(self) -> List[Dict]: """Return full ELO history for plotting.""" return self.history def get_current_champion(self) -> str: """Return the version with the highest ELO.""" if not self.history: return "none" return max(self.history, key=lambda x: x["elo"])["version"] def get_elo_curve_data(self) -> Dict: """Return data formatted for plotting: {versions: [], elos: [], timestamps: []}.""" sorted_history = sorted(self.history, key=lambda x: x["timestamp"]) return { "versions": [h["version"] for h in sorted_history], "elos": [h["elo"] for h in sorted_history], "timestamps": [h["timestamp"] for h in sorted_history], "matches_played": [h["matches_played"] for h in sorted_history], "matches_won": [h["matches_won"] for h in sorted_history] } class ELOSystem: """Core ELO rating calculation system.""" def __init__(self, k_factor: int = 32, initial_elo: float = 1000.0): self.k_factor = k_factor self.initial_elo = initial_elo def calculate_expected_score(self, elo_a: float, elo_b: float) -> float: """Calculate expected win probability for player A vs player B.""" return 1.0 / (1.0 + 10.0 ** ((elo_b - elo_a) / 400.0)) def update_elo(self, winner_elo: float, loser_elo: float) -> Tuple[float, float]: """Update ELO ratings after a match. Returns (new_winner_elo, new_loser_elo).""" expected_winner = self.calculate_expected_score(winner_elo, loser_elo) expected_loser = 1.0 - expected_winner new_winner = winner_elo + self.k_factor * (1.0 - expected_winner) new_loser = loser_elo + self.k_factor * (0.0 - expected_loser) return round(new_winner, 2), round(new_loser, 2) class PlaybookELOTracker: """Tracks ELO ratings for playbook versions with persistence.""" def __init__(self, storage_path: str = "./elo_history/"): self.storage_path = storage_path self.elo_system = ELOSystem() self.history: List[Dict] = [] os.makedirs(storage_path, exist_ok=True) self._load() def _load(self) -> None: """Load ELO history from disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") if os.path.exists(history_file): with open(history_file, "r") as f: self.history = json.load(f) def _save(self) -> None: """Save ELO history to disk.""" history_file = os.path.join(self.storage_path, "elo_history.json") with open(history_file, "w") as f: json.dump(self.history, f, indent=2, default=str) def register_playbook(self, version: str, elo: float = 1000.0) -> None: """Register a new playbook version with initial ELO.""" entry = { "version": version, "elo": elo, "timestamp": time.time(), "matches_played": 0, "matches_won": 0 } self.history.append(entry) self._save() def get_current_elo(self, version: str) -> float: """Get the current ELO for a playbook version.""" for entry in reversed(self.history): if entry["version"] == version: return entry["elo"] return self.elo_system.initial_elo def record_matchup(self, version_a: str, version_b: str, winner: str) -> Tuple[float, float]: """Record a match between two playbook versions. Returns (new_elo_a, new_elo_b).""" elo_a = self.get_current_elo(version_a) elo_b = self.get_current_elo(version_b) if winner == version_a: new_a, new_b = self.elo_system.update_elo(elo_a, elo_b) elif winner == version_b: new_b, new_a = self.elo_system.update_elo(elo_b, elo_a) else: raise ValueError(f"Winner must be one of the two versions: {version_a} or {version_b}") self._update_entry(version_a, new_a, version_a == winner) self._update_entry(version_b, new_b, version_b == winner) return new_a, new_b def _update_entry(self, version: str, new_elo: float, won: bool) -> None: """Update a single entry in history.""" for entry in reversed(self.history): if entry["version"] == version: entry["elo"] = new_elo entry["matches_played"] += 1 if won: entry["matches_won"] += 1 entry["timestamp"] = time.time() break else: self.register_playbook(version, new_elo) for entry in reversed(self.history): if entry["version"] == version: entry["matches_played"] = 1 if won: entry["matches_won"] = 1 break self._save() def get_elo_history(self) -> List[Dict]: """Return full ELO history for plotting.""" return self.history def get_current_champion(self) -> str: """Return the version with the highest ELO.""" if not self.history: return "none" return max(self.history, key=lambda x: x["elo"])["version"] def get_elo_curve_data(self) -> Dict: """Return data formatted for plotting: {versions: [], elos: [], timestamps: []}.""" sorted_history = sorted(self.history, key=lambda x: x["timestamp"]) return { "versions": [h["version"] for h in sorted_history], "elos": [h["elo"] for h in sorted_history], "timestamps": [h["timestamp"] for h in sorted_history], "matches_played": [h["matches_played"] for h in sorted_history], "matches_won": [h["matches_won"] for h in sorted_history] } def plot_elo_curve(history: List[Dict]) -> str: """Generate a dark-themed ELO curve plot and return as base64 PNG string. Args: history: List of ELO history entries with version, elo, timestamp keys Returns: Base64 encoded PNG image string """ matplotlib.use('Agg') plt.style.use('dark_background') sorted_history = sorted(history, key=lambda x: x.get("timestamp", 0)) versions = [h.get("version", "") for h in sorted_history] elos = [h.get("elo", 1000) for h in sorted_history] fig, ax = plt.subplots(figsize=(12, 6), facecolor='#1a1a2e') ax.set_facecolor('#1a1a2e') ax.plot(range(len(elos)), elos, color='#00ff88', linewidth=2, marker='o', markersize=8, markerfacecolor='#00ff88', markeredgecolor='white', markeredgewidth=1) for i, (v, e) in enumerate(zip(versions, elos)): ax.annotate(v, (i, e), textcoords="offset points", xytext=(0, 15), ha='center', fontsize=9, color='white', fontweight='bold') ax.set_xlabel('Match Number', color='white', fontsize=12, fontweight='bold') ax.set_ylabel('ELO Rating', color='white', fontsize=12, fontweight='bold') ax.set_title('🧬 Playbook ELO Evolution', color='#00ff88', fontsize=14, fontweight='bold') ax.tick_params(axis='x', colors='white', labelsize=10) ax.tick_params(axis='y', colors='white', labelsize=10) ax.grid(True, alpha=0.3, color='gray', linestyle='--') ax.spines['bottom'].set_color('white') ax.spines['top'].set_color('white') ax.spines['right'].set_color('white') ax.spines['left'].set_color('white') plt.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png', dpi=100, bbox_inches='tight', facecolor='#1a1a2e') plt.close() buf.seek(0) return base64.b64encode(buf.read()).decode('utf-8')