Initial commit: pre match-history refactor

This commit is contained in:
FireHorse 2026-04-08 18:13:02 +10:00
commit 2e81cbb34f
378 changed files with 118860 additions and 0 deletions

157
README.txt Normal file
View file

@ -0,0 +1,157 @@
DashLeague Casting Tool — README
🎮 Caster Cheat Sheet (Quick Start)
1. Sync Stats
→ Press Sync Stats to pull the latest player data.
2. Build Career Database
→ Press Build Career Database
→ Wait for Career DB: ✓ Loaded (green)
3. Select Teams
→ Choose Team A and Team B from dropdowns
→ Optionally select specific players (or leave empty to auto‑select all)
4. Generate Player Slots
→ Press Generate Player Slots
→ Wait for Player Slots: ✓ Ready (green)
→ OBS now reads from –
5. Export Predictions
→ Button unlocks only when both ✓ are green
→ Press Export Predictions
→ Use for graphs/overlays
That’s it.
This is the entire casting workflow.
📘 Full Guide for First‑Time Users
1. Overview
The DashLeague Casting Tool is designed to:
• Prepare player slot folders (p0–p9) for OBS overlays
• Build and use a Career Database for advanced identity tags and predictions
• Export match predictions to JSON for downstream visuals
• Provide a caster‑friendly workflow with clear status indicators and safety prompts
You’ll mainly interact with:
• Toolbar buttons
• Team selectors
• Player lists
• Generate Player Slots
• Export Predictions
2. Prerequisites & Setup
Folder structure:
• — main script
• — generated by the tool
• – — OBS‑ready text files
• — teams + player data
Run the tool:
3. UI Layout
Top Toolbar
• Sync Stats — refresh raw stats
• Build Career Database — generate
• Player Spotlight — single‑player view
• Team Identity — team style summary
• Match Storyline — narrative hooks
• Rankings — player rankings & percentiles
Middle Section
• Team A / Team B selectors
• Player listboxes (multi‑select)
Bottom Section
• Generate Player Slots
• Export Predictions (locked until ready)
Status Indicators
• Career DB: ❌ / ✓
• Player Slots: ❌ / ✓
• Status bar text (bottom)
4. Recommended Workflow (Casting a Match)
Step 1 — Sync Stats
Click Sync Stats to refresh player data.
Step 2 — Build Career Database
Click Build Career Database.
You should see:
• Popup: “Career database has been built.”
• Status: Career DB: ✓ Loaded
This powers:
• Identity tags
• Slayer calculations
• League averages
Step 3 — Select Teams & Players
1. Choose Team A and Team B
2. Select players (or leave empty to auto‑select all)
Step 4 — Generate Player Slots
Click Generate Player Slots.
The tool writes:
•
•
• , ,
• ,
• , , etc.
• (from Career DB)
• , ,
You should see:
• Popup: “Player slots generated into p0–p9.”
• Status: Player Slots: ✓ Ready
OBS now reads from these folders.
Step 5 — Export Predictions
Button unlocks only when:
• Career DB: ✓ Loaded
• Player Slots: ✓ Ready
Click Export Predictions.
This generates:
Example:
Use this for:
• ASCII graphs
• Overlay widgets
• Automated visuals
5. Feature Details
Sync Stats
Refreshes raw stats from your source.
Build Career Database
Creates with:
• Identity tags
• League averages
• Historical stats
Generate Player Slots
Creates OBS‑ready text files for each player.
Export Predictions
Creates machine‑readable predictions for visuals.
Spotlight / Identity / Storyline / Rankings
Caster‑support tools for:
• Player deep dives
• Team style summaries
• Match narratives
• Percentiles & rankings
6. Status Indicators
• Career DB: ❌ Not Loaded
→ Build the DB first
• Player Slots: ❌ Not Ready
→ Generate slots
• Export Predictions disabled
→ One or both steps incomplete
7. Quick Reference Workflow
1. Sync Stats
2. Build Career Database
3. Select Team A / Team B
4. Generate Player Slots
5. Export Predictions
6. Use + in OBS
8. Troubleshooting
Export button greyed out
• Career DB must be ✓
• Player Slots must be ✓
Wrong players in OBS
• Check team selection
• Check listbox selection
• Regenerate slots
Empty predictions
• Build Career DB first
• Ensure player IDs match DB

0
__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

82
analysis/rankings.py Normal file
View file

@ -0,0 +1,82 @@
import json
import os
from typing import List, Dict, Any
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CAREER_DB_PATH = os.path.join(BASE_DIR, "career_stats.json")
def load_career_db() -> Dict:
"""Load career database with proper error handling."""
if not os.path.exists(CAREER_DB_PATH):
raise FileNotFoundError(f"Career database not found at: {CAREER_DB_PATH}\nPlease build it first from the GUI.")
try:
with open(CAREER_DB_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
raise ValueError("Career database is corrupted. Please rebuild it.")
except Exception as e:
raise RuntimeError(f"Failed to load career database: {e}")
def top_players_by_tag(
tag: str,
players_only: List[Dict] = None,
career_db: Dict = None,
limit: int = 10
) -> List[Dict]:
"""
Return top players for a given identity tag.
If players_only is provided → only rank within current match lineup (recommended for GUI).
Otherwise → rank across entire career database.
"""
if players_only is not None:
# Use only the current match players (live stats)
players = players_only[:]
else:
# Full career database
db = career_db or load_career_db()
players = list(db["players"].values())
if not players:
return []
# Filter players who have this tag
tagged_players = [
p for p in players
if tag in p.get("identity_tags", [])
]
if not tagged_players:
return []
# Define scoring function based on tag
def get_score(p: Dict) -> float:
norm = p.get("normalized", {})
career = p.get("career", {})
if tag == "Slayer":
return norm.get("slayer_index", 0)
elif tag == "Objective Specialist":
return norm.get("objective_index", 0)
elif tag == "Sharpshooter":
return career.get("accuracy", 0)
elif tag == "Anchor":
return norm.get("survivability_index", 0)
elif tag == "Aggressor":
return norm.get("kills_per_map", 0)
elif tag == "Clutch":
return norm.get("win_rate", 0)
elif tag == "Consistent":
# Could add variance logic if stored
return norm.get("kills_per_map", 0)
else:
# Default fallback
return career.get("KD", 0)
# Sort by score descending
tagged_players.sort(key=get_score, reverse=True)
return tagged_players[:limit]

38
analysis/rankings1.py Normal file
View file

@ -0,0 +1,38 @@
import json
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CAREER_DB = os.path.join(BASE_DIR, "career_stats.json")
def load_db():
with open(CAREER_DB, "r", encoding="utf-8") as f:
return json.load(f)
def top_players_by_tag(tag, limit=10):
db = load_db()
players = db["players"]
ranked = [
p for p in players.values()
if tag in p.get("identity_tags", [])
]
# Sort by slayer_index for Slayers, objective_index for Objective Specialists, etc.
def score(p):
norm = p["normalized"]
if tag == "Slayer":
return norm["slayer_index"]
if tag == "Objective Specialist":
return norm["objective_index"]
if tag == "Sharpshooter":
return p["career"]["accuracy"]
if tag == "Anchor":
return norm["survivability_index"]
if tag == "Aggressor":
return norm["kills_per_map"]
if tag == "Clutch":
return norm["win_rate"]
return p["career"]["KD"]
ranked.sort(key=score, reverse=True)
return ranked[:limit]

54
analysis/storylines.py Normal file
View file

@ -0,0 +1,54 @@
from team_identity import team_identity
def matchup_storyline(team_a: str, team_b: str) -> str:
"""Generate rich, caster-ready storyline with talking points and predictive flavor."""
A = team_identity(team_a)
B = team_identity(team_b)
story = [f"🎙️ MATCH STORYLINE: {team_a} vs {team_b}\n"]
# Slayer Power
slayer_a = A["tag_counts"].get("Slayer", 0)
slayer_b = B["tag_counts"].get("Slayer", 0)
if slayer_a > slayer_b:
story.append(f"• {team_a} brings **superior slayer firepower** ({slayer_a} tagged Slayers vs {slayer_b}).")
elif slayer_b > slayer_a:
story.append(f"• {team_b} brings **superior slayer firepower** ({slayer_b} tagged Slayers vs {slayer_a}).")
else:
story.append(f"• Both teams have equal slayer power — this could come down to execution.")
# Objective Control
obj_a = A["tag_counts"].get("Objective Specialist", 0)
obj_b = B["tag_counts"].get("Objective Specialist", 0)
if obj_a > obj_b:
story.append(f"• {team_a} has **deeper objective presence** and map control potential.")
elif obj_b > obj_a:
story.append(f"• {team_b} has **deeper objective presence** and map control potential.")
# Anchor / Stability
if A["tag_counts"].get("Anchor", 0) >= 1 and B["tag_counts"].get("Anchor", 0) == 0:
story.append(f"• {team_a} has a clear **anchor advantage** — expect a rock-solid backline.")
elif B["tag_counts"].get("Anchor", 0) >= 1 and A["tag_counts"].get("Anchor", 0) == 0:
story.append(f"• {team_b} has a clear **anchor advantage** — expect a rock-solid backline.")
# Experience / Rookie Factor
vet_a = A["tag_counts"].get("Veteran", 0)
rook_a = A["tag_counts"].get("Rookie", 0)
vet_b = B["tag_counts"].get("Veteran", 0)
rook_b = B["tag_counts"].get("Rookie", 0)
if rook_a >= 2 and vet_b >= 2:
story.append(f"• {team_a} is young and hungry — {team_b} brings veteran experience. Classic youth vs experience matchup.")
# Direct Exploit Lines
if "Weak objective presence" in A["weaknesses"] and "Strong objective depth" in B["strengths"]:
story.append(f"🔥 {team_b} is perfectly positioned to **exploit {team_a}'s weak objective play**.")
if "Weak objective presence" in B["weaknesses"] and "Strong objective depth" in A["strengths"]:
story.append(f"🔥 {team_a} is perfectly positioned to **exploit {team_b}'s weak objective play**.")
# Final prediction flavor
story.append("\n**Caster Talking Points Summary:**")
story.append(f"• Watch for early slayer duels — the team that wins the Slayer battle usually controls the pace.")
story.append(f"• Objective maps will heavily favor the side with more Objective Specialists.")
story.append(f"• This matchup has high upset potential if the younger team executes cleanly.")
return "\n".join(story)

29
analysis/storylines1.py Normal file
View file

@ -0,0 +1,29 @@
from team_identity import team_identity
def matchup_storyline(team_a, team_b):
A = team_identity(team_a)
B = team_identity(team_b)
story = []
story.append(f"MATCH STORYLINE: {team_a} vs {team_b}\n")
# Style comparison
if A["tag_counts"].get("Slayer", 0) > B["tag_counts"].get("Slayer", 0):
story.append(f"{team_a} brings stronger slayer power.")
elif B["tag_counts"].get("Slayer", 0) > A["tag_counts"].get("Slayer", 0):
story.append(f"{team_b} brings stronger slayer power.")
if A["tag_counts"].get("Objective Specialist", 0) > B["tag_counts"].get("Objective Specialist", 0):
story.append(f"{team_a} has deeper objective presence.")
elif B["tag_counts"].get("Objective Specialist", 0) > A["tag_counts"].get("Objective Specialist", 0):
story.append(f"{team_b} has deeper objective presence.")
# Strength vs weakness
if "Weak objective presence" in A["weaknesses"] and "Strong objective depth" in B["strengths"]:
story.append(f"{team_b} may exploit {team_a}'s weak objective play.")
if "Weak objective presence" in B["weaknesses"] and "Strong objective depth" in A["strengths"]:
story.append(f"{team_a} may exploit {team_b}'s weak objective play.")
return "\n".join(story)

77
analysis/team_identity.py Normal file
View file

@ -0,0 +1,77 @@
import json
import os
from collections import Counter
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CAREER_DB_PATH = os.path.join(BASE_DIR, "career_stats.json")
def load_career_db():
"""Load career database with proper error handling."""
if not os.path.exists(CAREER_DB_PATH):
raise FileNotFoundError(f"Career database not found at: {CAREER_DB_PATH}\nPlease build it first.")
try:
with open(CAREER_DB_PATH, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError:
raise ValueError("Career database is corrupted. Please rebuild it.")
except Exception as e:
raise RuntimeError(f"Failed to load career database: {e}")
def team_identity(team_name: str):
"""Generate team identity summary based on career tags."""
db = load_career_db()
players = db.get("players", {})
# Find all players who have ever played for this team
team_players = [
p for p in players.values()
if team_name in p.get("team_history", [])
]
if not team_players:
return {
"team": team_name,
"players": [],
"tag_counts": {},
"strengths": ["No players found with this team in history"],
"weaknesses": []
}
# Count all identity tags across the team
all_tags = [tag for p in team_players for tag in p.get("identity_tags", [])]
tag_counts = Counter(all_tags)
strengths = []
weaknesses = []
# --- Strengths ---
if tag_counts.get("Slayer", 0) >= 2:
strengths.append("High slayer firepower")
if tag_counts.get("Objective Specialist", 0) >= 2:
strengths.append("Strong objective control")
if tag_counts.get("Anchor", 0) >= 1:
strengths.append("Reliable anchor/backline")
if tag_counts.get("Playmaker", 0) >= 1:
strengths.append("Creative playmaking")
if tag_counts.get("Consistent", 0) >= 2:
strengths.append("Very consistent performers")
# --- Weaknesses ---
if tag_counts.get("Objective Specialist", 0) == 0:
weaknesses.append("Lacking objective specialists")
if tag_counts.get("Sharpshooter", 0) == 0:
weaknesses.append("No dedicated sharpshooter")
if tag_counts.get("Anchor", 0) == 0:
weaknesses.append("Weak backline stability")
if tag_counts.get("Veteran", 0) == 0 and tag_counts.get("Rookie", 0) >= 2:
weaknesses.append("Very inexperienced roster")
return {
"team": team_name,
"players": team_players, # kept for potential future use
"tag_counts": dict(tag_counts),
"strengths": strengths or ["Balanced roster — no dominant strength"],
"weaknesses": weaknesses or ["No major weaknesses identified"]
}

View file

@ -0,0 +1,46 @@
import json
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CAREER_DB = os.path.join(BASE_DIR, "career_stats.json")
def load_db():
with open(CAREER_DB, "r", encoding="utf-8") as f:
return json.load(f)
def team_identity(team_name):
db = load_db()
players = db["players"]
team_players = [
p for p in players.values()
if team_name in p.get("team_history", [])
]
tag_counts = {}
for p in team_players:
for tag in p.get("identity_tags", []):
tag_counts[tag] = tag_counts.get(tag, 0) + 1
strengths = []
weaknesses = []
if tag_counts.get("Slayer", 0) >= 2:
strengths.append("High slayer power")
if tag_counts.get("Objective Specialist", 0) >= 2:
strengths.append("Strong objective depth")
if tag_counts.get("Anchor", 0) >= 1:
strengths.append("Stable backline")
if tag_counts.get("Objective Specialist", 0) == 0:
weaknesses.append("Weak objective presence")
if tag_counts.get("Sharpshooter", 0) == 0:
weaknesses.append("No true sharpshooter")
return {
"team": team_name,
"players": team_players,
"tag_counts": tag_counts,
"strengths": strengths,
"weaknesses": weaknesses
}

111535
career_stats.json Normal file

File diff suppressed because it is too large Load diff

49
config.json Normal file
View file

@ -0,0 +1,49 @@
{
"season": 10,
"teams": [
"TANK",
"MEX",
"TBDi",
"EMRD",
"DMND",
"Doc!",
"HAXX",
"SWUA",
"ZTi",
"SLNT",
"HOBO",
"just",
"VRUS",
"FAM!",
"SKY",
"LMB0",
"F1R3",
"RVNG",
"DRGN",
"STHX",
"EMU"
],
"tiers": {
"TANK": "Dasher",
"MEX": "Sprinter",
"TBDi": "Walker",
"EMRD": "Dasher",
"DMND": "Sprinter",
"Doc!": "Walker",
"HAXX": "Dasher",
"SWUA": "Sprinter",
"ZTi": "Walker",
"SLNT": "Dasher",
"HOBO": "Sprinter",
"just": "Walker",
"VRUS": "Dasher",
"FAM!": "Sprinter",
"SKY": "Walker",
"LMB0": "Dasher",
"F1R3": "Sprinter",
"RVNG": "Walker",
"DRGN": "Dasher",
"STHX": "Sprinter",
"EMU": "Walker"
}
}

829
dashleague_cast_tool.py Normal file
View file

@ -0,0 +1,829 @@
import os
import json
import math
from collections import defaultdict
import tkinter as tk
from tkinter import ttk, messagebox
import requests
from data.career_db import build_career_database
from rankings import top_players_by_tag
from team_identity import generate_team_identity, compare_team_identity
from storylines import matchup_storyline
from tags.objective_domination import compute_dom_objective
from predictions.prediction_engine import generate_predictions
from match_engine import (
rank_match_players_sharpshooter,
rank_match_players_slayer,
rank_match_players_objpl,
rank_match_players_objdom,
rank_match_players_clutch,
split_two_columns,
compute_slayer_prediction,
)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_PATH = os.path.join(BASE_DIR, "config.json")
PLAYERS_DIR = os.path.join(BASE_DIR, "players")
OBS_EXPORT_DIR = os.path.join(BASE_DIR, "obs_exports")
os.makedirs(OBS_EXPORT_DIR, exist_ok=True)
API_BASE = "https://dashleague.games/api/v1"
def export_to_obs(filename, text):
path = os.path.join(OBS_EXPORT_DIR, filename)
with open(path, "w", encoding="utf-8") as f:
f.write(text)
return path
def fetch_json(url):
resp = requests.get(url, timeout=10)
resp.raise_for_status()
return resp.json()
def ensure_player_slots():
os.makedirs(PLAYERS_DIR, exist_ok=True)
for i in range(10):
slot_dir = os.path.join(PLAYERS_DIR, f"p{i}")
os.makedirs(slot_dir, exist_ok=True)
def write_text(path, value):
with open(path, "w", encoding="utf-8") as f:
f.write(str(value))
class DashLeagueGUI:
def __init__(self, root):
self.root = root
self.root.title("DashLeague Casting Tool")
self.config = {}
self.current_season = None
self.team_a_var = tk.StringVar()
self.team_b_var = tk.StringVar()
self.team_a_players = []
self.team_b_players = []
self.stats_by_id = {}
self.status_var = tk.StringVar(value="Ready")
self.load_config()
ensure_player_slots()
self._build_ui()
self.current_match_players = []
def load_config(self):
if not os.path.exists(CONFIG_PATH):
messagebox.showerror("Error", f"config.json not found at {CONFIG_PATH}")
self.config = {}
return
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
self.config = json.load(f)
self.current_season = self.config.get("current_season", 10)
def _build_ui(self):
main_frame = ttk.Frame(self.root, padding=10)
main_frame.grid(row=0, column=0, sticky="nsew")
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
top_frame = ttk.Frame(main_frame)
top_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 10))
self.sync_button = ttk.Button(top_frame, text="Sync Stats", command=self.on_sync_stats)
self.sync_button.grid(row=0, column=0, padx=(0, 10))
self.build_db_button = ttk.Button(top_frame, text="Build Career Database", command=self.on_build_career_db)
self.build_db_button.grid(row=0, column=1, padx=(0, 10))
self.spotlight_button = ttk.Button(top_frame, text="Player Spotlight", command=self.on_player_spotlight)
self.spotlight_button.grid(row=0, column=2, padx=(0, 10))
self.team_id_button = ttk.Button(top_frame, text="Team Identity", command=self.on_team_identity)
self.team_id_button.grid(row=0, column=3, padx=(0, 10))
self.storyline_button = ttk.Button(top_frame, text="Match Storyline", command=self.on_match_storyline)
self.storyline_button.grid(row=0, column=4, padx=(0, 10))
self.rankings_button = ttk.Button(top_frame, text="Rankings", command=self.on_rankings)
self.rankings_button.grid(row=0, column=5, padx=(0, 10))
self.map_var = tk.StringVar()
self.map_var.set("Payload") # default
teams = self.config.get("teams", [])
left_frame = ttk.LabelFrame(main_frame, text="Team A")
left_frame.grid(row=1, column=0, sticky="nsew", padx=(0, 5))
right_frame = ttk.LabelFrame(main_frame, text="Team B")
right_frame.grid(row=1, column=1, sticky="nsew", padx=(5, 0))
main_frame.rowconfigure(1, weight=1)
left_frame.rowconfigure(1, weight=1)
right_frame.rowconfigure(1, weight=1)
ttk.Label(left_frame, text="Team:").grid(row=0, column=0, sticky="w")
self.team_a_combo = ttk.Combobox(left_frame, textvariable=self.team_a_var, values=teams, state="readonly")
self.team_a_combo.grid(row=0, column=1, sticky="ew", pady=2)
self.team_a_combo.bind("<<ComboboxSelected>>", lambda e: self.on_team_select("A"))
ttk.Label(right_frame, text="Team:").grid(row=0, column=0, sticky="w")
self.team_b_combo = ttk.Combobox(right_frame, textvariable=self.team_b_var, values=teams, state="readonly")
self.team_b_combo.grid(row=0, column=1, sticky="ew", pady=2)
self.team_b_combo.bind("<<ComboboxSelected>>", lambda e: self.on_team_select("B"))
self.team_a_listbox = tk.Listbox(left_frame, selectmode=tk.MULTIPLE, exportselection=False)
self.team_a_listbox.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(5, 0))
self.team_b_listbox = tk.Listbox(right_frame, selectmode=tk.MULTIPLE, exportselection=False)
self.team_b_listbox.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(5, 0))
self.team_a_listbox.bind("<<ListboxSelect>>", lambda e: self.enforce_rolling_five(e, "A"))
self.team_b_listbox.bind("<<ListboxSelect>>", lambda e: self.enforce_rolling_five(e, "B"))
bottom_frame = ttk.Frame(main_frame)
bottom_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(10, 0))
self.generate_button = ttk.Button(bottom_frame, text="Generate Player Slots", command=self.on_generate_slots)
self.generate_button.grid(row=0, column=0, sticky="ew")
ttk.Label(bottom_frame, text="Map:").grid(row=0, column=1, padx=(10, 5))
ttk.OptionMenu(bottom_frame, self.map_var, "Payload", "Payload", "Domination").grid(row=0, column=2)
status_frame = ttk.Frame(self.root)
status_frame.grid(row=1, column=0, sticky="ew")
status_label = ttk.Label(status_frame, textvariable=self.status_var, anchor="w")
status_label.grid(row=0, column=0, sticky="ew")
status_frame.columnconfigure(0, weight=1)
def set_status(self, text):
self.status_var.set(text)
self.root.update_idletasks()
def on_sync_stats(self):
try:
self.set_status(f"Syncing stats for season {self.current_season}...")
url = f"{API_BASE}/stats?season={self.current_season}"
data = fetch_json(url)
stats = data.get("data", [])
self.stats_by_id = {p["id"]: p for p in stats}
self.set_status("Stats synced.")
messagebox.showinfo("Success", f"Synced {len(stats)} player stats.")
except Exception as e:
messagebox.showerror("Error", f"Failed to sync stats:\n{e}")
self.set_status("Sync failed.")
def enforce_rolling_five(self, event, side):
listbox = self.team_a_listbox if side == "A" else self.team_b_listbox
selection = listbox.curselection()
# If 5 or fewer selected, nothing to do
if len(selection) <= 5:
return
# More than 5 selected → rolling behavior:
# Deselect the *oldest* selected index
oldest = selection[0]
listbox.selection_clear(oldest)
def on_team_select(self, side):
team_name = self.team_a_var.get() if side == "A" else self.team_b_var.get()
team_players = [p for p in self.stats_by_id.values() if p.get("team") == team_name]
if side == "A":
self.team_a_players = team_players
self.team_a_listbox.delete(0, tk.END)
for p in team_players:
self.team_a_listbox.insert(tk.END, p.get("name", "Unknown"))
else:
self.team_b_players = team_players
self.team_b_listbox.delete(0, tk.END)
for p in team_players:
self.team_b_listbox.insert(tk.END, p.get("name", "Unknown"))
def on_generate_slots(self):
try:
ensure_player_slots()
selected_a = [self.team_a_players[i] for i in self.team_a_listbox.curselection()]
selected_b = [self.team_b_players[i] for i in self.team_b_listbox.curselection()]
if not selected_a:
selected_a = self.team_a_players
if not selected_b:
selected_b = self.team_b_players
all_players = sorted(selected_a, key=lambda x: x.get("name", "")) + \
sorted(selected_b, key=lambda x: x.get("name", ""))
all_players = all_players[:10]
self.current_match_players = all_players
career_path = os.path.join(BASE_DIR, "career_stats.json")
career_db = None
if os.path.exists(career_path):
with open(career_path, "r", encoding="utf-8") as f:
career_db = json.load(f)
# Attach tag components from career DB to match players
if career_db is not None:
for p in all_players:
pid = p.get("id")
if pid and pid in career_db["players"]:
pdata = career_db["players"][pid]
# Attach all tag components
p["clutch_components"] = pdata.get("clutch_components", {})
p["consistency_components"] = pdata.get("consistency_components", {})
p["objective_payload_components"] = pdata.get("objective_payload_components", {})
p["objective_domination_components"] = pdata.get("objective_domination_components", {})
p["sharpshooter_components"] = pdata.get("sharpshooter_components", {})
p["slayer_score_raw"] = pdata.get("slayer_score_raw", 0.0)
for idx, p in enumerate(all_players):
slot_dir = os.path.join(PLAYERS_DIR, f"p{idx}")
os.makedirs(slot_dir, exist_ok=True)
write_text(os.path.join(slot_dir, "IGN.txt"), p.get("name", ""))
write_text(os.path.join(slot_dir, "Team.txt"), p.get("team", ""))
write_text(os.path.join(slot_dir, "KD.txt"), p.get("KD", ""))
write_text(os.path.join(slot_dir, "Kills.txt"), p.get("kills", ""))
write_text(os.path.join(slot_dir, "Deaths.txt"), p.get("deaths", ""))
write_text(os.path.join(slot_dir, "Headshots.txt"), p.get("headshots", ""))
write_text(os.path.join(slot_dir, "Accuracy.txt"), p.get("accuracy", ""))
write_text(os.path.join(slot_dir, "PushTime.txt"), p.get("PAY_PushTime", ""))
write_text(os.path.join(slot_dir, "Captures.txt"), p.get("DOM_captures", ""))
write_text(os.path.join(slot_dir, "Counters.txt"), p.get("DOM_counters", ""))
# Tag exports now come from real tag engines via rankings/career_db
if career_db is not None:
pid = p.get("id")
if pid and pid in career_db["players"]:
player_entry = career_db["players"][pid]
map_type = self.map_var.get()
tags_text = self.generate_player_tags(player_entry, map_type)
write_text(os.path.join(slot_dir, "Tags.txt"), tags_text)
self.set_status("Player slots generated.")
messagebox.showinfo("Success", "Player slots generated into p0–p9.")
except Exception as e:
messagebox.showerror("Error", f"Failed to generate slots:\n{e}")
self.set_status("Generate failed.")
self.export_all_obs_files()
def on_build_career_db(self):
try:
self.set_status("Building career database...")
output_path = os.path.join(BASE_DIR, "career_stats.json")
build_career_database(output_path)
self.set_status("Career database built successfully.")
messagebox.showinfo("Success", "Career database has been built.")
except Exception as e:
import traceback
traceback.print_exc()
raise
messagebox.showerror("Error", f"Failed to build career database:\n{e}")
self.set_status("Career DB build failed.")
def on_player_spotlight(self):
career_path = os.path.join(BASE_DIR, "career_stats.json")
if not os.path.exists(career_path):
messagebox.showerror("Error", "Build career database first.")
return
with open(career_path, "r", encoding="utf-8") as f:
db = json.load(f)
names = [p["name"] for p in db["players"].values()]
names.sort()
win = tk.Toplevel(self.root)
win.title("Player Spotlight")
win.geometry("400x500")
ttk.Label(win, text="Select Player:").pack(anchor="w", padx=10, pady=5)
combo = ttk.Combobox(win, values=names, state="readonly")
combo.pack(fill="x", padx=10)
output = tk.Text(win, wrap="word")
output.pack(expand=True, fill="both", padx=10, pady=10)
def show():
name = combo.get()
if not name:
return
player = next((p for p in db["players"].values() if p["name"] == name), None)
if not player:
output.delete("1.0", tk.END)
output.insert(tk.END, "Player not found in career DB.")
return
career = player["career"]
derived = player.get("derived", {})
obj_pl = player.get("objective_payload_score")
slayer_strength = player.get("slayer_strength", "Unknown")
text = f"""PLAYER SPOTLIGHT — {name}
Slayer Tier: {slayer_strength}
Payload Objective Specialist: {obj_pl:.2f} (if not None)
Career Stats:
KD: {career['KD']:.2f}
Accuracy: {career['accuracy']:.2f}
Kills: {career['kills']}
Deaths: {career['deaths']}
Maps Played: {career['maps']}
Derived Metrics:
Kills/Map: {derived.get('kills_per_map', 0):.2f}
Deaths/Map: {derived.get('deaths_per_map', 0):.2f}
PushTime/Season: {derived.get('push_time_per_season', 0):.2f}
"""
output.delete("1.0", tk.END)
output.insert(tk.END, text)
def export():
content = output.get("1.0", tk.END).strip()
if not content:
messagebox.showerror("Error", "No spotlight text to export.")
return
path = export_to_obs("spotlight.txt", content)
messagebox.showinfo("Exported", f"Spotlight exported to:\n{path}")
ttk.Button(win, text="Show Spotlight", command=show).pack(pady=5)
ttk.Button(win, text="Export to OBS", command=export).pack(pady=5)
def on_team_identity(self):
team = self.team_a_var.get()
if not team:
messagebox.showerror("Error", "Select a team first.")
return
# Filter selected players for this team
team_players = [p for p in self.current_match_players if p.get("team") == team]
if len(team_players) == 0:
messagebox.showerror("Error", "No players selected for this team.")
return
# Use the identity engine with the selected players
identity = generate_team_identity(team_players)
if identity is None:
messagebox.showerror("Error", "Not enough data to generate team identity.")
return
strengths = []
weaknesses = []
# Slayer
if identity["slayer_score"] > 0.55:
strengths.append("Strong slaying presence")
elif identity["slayer_score"] < 0.45:
weaknesses.append("Below-average slaying power")
# Payload Objective
if identity["payload_score"] > 0.55:
strengths.append("Strong Payload objective focus")
elif identity["payload_score"] < 0.45:
weaknesses.append("Weak Payload objective presence")
# Consistency
if identity["consistency_score"] > 0.55:
strengths.append("Strong map-to-map consistency")
elif identity["consistency_score"] < 0.45:
weaknesses.append("Inconsistent map-to-map performance")
# Clutch
if identity["clutch_score"] > 0.55:
strengths.append("Performs well in clutch moments")
elif identity["clutch_score"] < 0.45:
weaknesses.append("Struggles in clutch situations")
# Sharpshooter
if identity["sharpshooter_score"] > 0.55:
strengths.append("Strong aim and headshot conversion")
elif identity["sharpshooter_score"] < 0.45:
weaknesses.append("Weak aim consistency")
# Domination Objective
if identity["objdom_score"] > 0.55:
strengths.append("Strong Domination objective presence")
elif identity["objdom_score"] < 0.45:
weaknesses.append("Weak Domination objective presence")
# --- UI Window ---
win = tk.Toplevel(self.root)
win.title(f"Team Identity — {team}")
win.geometry("400x500")
output = tk.Text(win, wrap="word")
output.pack(expand=True, fill="both", padx=10, pady=10)
text = f"TEAM IDENTITY — {team}\n\n"
text += "Strengths:\n"
for s in strengths:
text += f"- {s}\n"
text += "\nWeaknesses:\n"
for w in weaknesses:
text += f"- {w}\n"
output.insert(tk.END, text)
def export():
content = output.get("1.0", tk.END).strip()
if not content:
messagebox.showerror("Error", "No team identity text to export.")
return
filename = f"team_identity_{team}.txt"
path = export_to_obs(filename, content)
messagebox.showinfo("Exported", f"Team identity exported to:\n{path}")
ttk.Button(win, text="Export to OBS", command=export).pack(pady=5)
def generate_player_tags(self, player_entry, map_type):
tags_out = []
# Slayer
slayer_strength = player_entry.get("slayer_strength")
if slayer_strength:
tags_out.append(slayer_strength)
# Payload Objective Specialist
if map_type == "Payload":
obj_pl = player_entry.get("objective_payload_score")
if obj_pl is not None:
tags_out.append(f"ObjPL {obj_pl:.2f}")
# Domination Specialist
if map_type == "Domination":
obj_dom = compute_dom_objective(player_entry)
tags_out.append(f"ObjDOM {obj_dom:.2f}")
# Sharpshooter
sharp = player_entry.get("sharpshooter_score")
if sharp is not None:
tags_out.append(f"Sharp {sharp:.2f}")
return ", ".join(tags_out)
def on_match_storyline(self):
team_a = self.team_a_var.get()
team_b = self.team_b_var.get()
if not team_a or not team_b:
messagebox.showerror("Error", "Select both teams first.")
return
# --- Extract selected players for each team ---
teamA_players = [p for p in self.current_match_players if p.get("team") == team_a]
teamB_players = [p for p in self.current_match_players if p.get("team") == team_b]
if len(teamA_players) == 0 or len(teamB_players) == 0:
messagebox.showerror("Error", "No selected players for one or both teams.")
return
# --- Generate storyline + identity summary using SELECTED PLAYERS ---
storyline_text = matchup_storyline(teamA_players, teamB_players)
identity_text = compare_team_identity(teamA_players, teamB_players)
full_story = (
storyline_text
+ "\n\n"
+ "TEAM IDENTITY SUMMARY\n"
+ identity_text
)
# --- Export to OBS ---
try:
with open("obs_exports/storyline.txt", "w", encoding="utf-8") as f:
f.write(full_story)
except Exception as e:
messagebox.showerror("File Error", f"Could not write storyline export:\n{e}")
# --- Display in popup window ---
win = tk.Toplevel(self.root)
win.title("Match Storyline")
win.geometry("500x600")
output = tk.Text(win, wrap="word")
output.pack(expand=True, fill="both", padx=10, pady=10)
output.insert(tk.END, full_story)
def export():
content = output.get("1.0", tk.END).strip()
if not content:
messagebox.showerror("Error", "No storyline text to export.")
return
path = export_to_obs("storyline.txt", content)
messagebox.showinfo("Exported", f"Storyline exported to:\n{path}")
ttk.Button(win, text="Export to OBS", command=export).pack(pady=5)
def on_rankings(self):
if not self.current_match_players:
messagebox.showerror("Error", "Generate player slots first.")
return
win = tk.Toplevel(self.root)
win.title("Rankings")
win.geometry("500x600")
tags = [
"Slayer",
"Payload Objective Specialist",
"Sharpshooter",
"Domination Objective Specialist",
"Consistency",
"Clutch",
]
ttk.Label(win, text="Select Tag:").pack(anchor="w", padx=10, pady=5)
combo = ttk.Combobox(win, values=tags, state="readonly")
combo.pack(fill="x", padx=10)
output = tk.Text(win, wrap="word")
output.pack(expand=True, fill="both", padx=10, pady=10)
def show():
tag = combo.get()
if not tag:
return
team_a = self.team_a_var.get()
team_b = self.team_b_var.get()
if not team_a or not team_b:
messagebox.showerror("Error", "Select both teams first.")
return
if tag == "Slayer":
ranked = rank_match_players_slayer(self.current_match_players)
elif tag == "Payload Objective Specialist":
ranked = rank_match_players_objpl(self.current_match_players)
elif tag == "Domination Objective Specialist":
ranked = rank_match_players_objdom(self.current_match_players)
elif tag == "Clutch":
ranked = rank_match_players_clutch(self.current_match_players)
else: # Sharpshooter
ranked = rank_match_players_sharpshooter(self.current_match_players)
if not ranked:
output.delete("1.0", tk.END)
output.insert(tk.END, "No data available for this tag.")
return
left, right = split_two_columns(ranked, team_a, team_b)
text = f"TOP {tag.upper()} PLAYERS (THIS MATCH):\n\n"
# Two-column layout, but keep global rank
max_rows = max(len(left), len(right))
text += f"{team_a:<24}{team_b}\n"
text += "-" * 48 + "\n"
for i in range(max_rows):
left_str = ""
right_str = ""
if i < len(left):
lp = left[i]
left_str = f"{lp['rank']:>2}. {lp['name']:<16} {lp['score_display']:>6}"
if i < len(right):
rp = right[i]
right_str = f"{rp['rank']:>2}. {rp['name']:<16} {rp['score_display']:>6}"
text += f"{left_str:<24}{right_str}\n"
# -----------------------------
# TEAM TOTALS & AVERAGES (COLUMN FORMAT)
# -----------------------------
teamA_total = sum(p["score_raw"] for p in left)
teamB_total = sum(p["score_raw"] for p in right)
teamA_avg = teamA_total / max(1, len(left))
teamB_avg = teamB_total / max(1, len(right))
text += "-" * 48 + "\n"
text += f"{team_a} Total: {teamA_total:.2f}".ljust(24)
text += f"{team_b} Total: {teamB_total:.2f}\n"
text += f"{team_a} Avg: {teamA_avg:.2f}".ljust(24)
text += f"{team_b} Avg: {teamB_avg:.2f}\n"
# -----------------------------
# DELTA (relative strength)
# -----------------------------
delta = teamB_avg - teamA_avg
if delta > 0:
fav_delta = team_b
elif delta < 0:
fav_delta = team_a
else:
fav_delta = "Neither team"
delta_mag = abs(delta)
text += f"Delta: {delta_mag:.2f} in favour of {fav_delta}\n"
stronger_avg = max(teamA_avg, teamB_avg)
if stronger_avg > 0:
rel_adv = (delta_mag / stronger_avg) * 100
else:
rel_adv = 0.0
text += f"Relative Advantage: {rel_adv:.1f}%\n"
# -----------------------------
# WIN CHANCE + SWING
# -----------------------------
if tag == "Slayer":
pred = compute_slayer_prediction(left, right)
teamA_win = pred["teamA_win"]
teamB_win = pred["teamB_win"]
else:
# ObjPL win factor (simple logistic)
if max(teamA_avg, teamB_avg) == 0:
edge = 0
else:
edge = (teamB_avg - teamA_avg) / max(teamA_avg, teamB_avg)
k = 1.1
pB = 1 / (1 + math.exp(-k * edge))
pA = 1 - pB
teamA_win = round(pA * 100)
teamB_win = round(pB * 100)
swing = abs(teamA_win - teamB_win)
fav = team_a if teamA_win > teamB_win else team_b
text += f"Win Chance: {team_a} {teamA_win}% — {team_b} {teamB_win}%\n"
text += f"Win Swing: {swing}% in favour of {fav}\n"
# -----------------------------
# FORMULA EXPLANATION
# -----------------------------
if tag == "Slayer":
text += "\nSlayer Formula:\n"
text += "Slayer Score = (Damage per Map × 0.40) + (Kills per Map × 0.40) + (KD × 0.20)\n"
elif tag == "Sharpshooter":
text += "\nSharpshooter Formula:\n"
text += "Sharpshooter = (Accuracy × 0.40) + (Headshot Rate × 0.40) + (Damage per Shot × 0.20)\n"
text += "Where:\n"
text += "- Accuracy = Shots Hit / Shots Fired\n"
text += "- Headshot Rate = Headshots / Shots Hit\n"
text += "- Damage per Shot = Damage / Shots Fired\n"
elif tag == "Payload Objective Specialist":
text += "\nPayload Objective Formula:\n"
text += "ObjPL = (Presence × 0.40) + (PushTime Normalized × 0.40) + (PSI × 0.20)\n"
text += "Where:\n"
text += "- Presence = Player PushTime / Team PushTime\n"
text += "- PSI = Damage / (Deaths + 1) × 1 / sqrt(PushTime + 1)\n"
elif tag == "Domination Objective Specialist":
text += "\nDomination Objective Formula:\n"
text += "ObjDOM = (CapRate × 0.40) + (CounterRate × 0.60)\n"
text += "Where:\n"
text += "- CapRate = log(1 + DOM_captures) / log(21)\n"
text += "- CounterRate = log(1 + DOM_counters) / log(21)\n"
text += "Counters are weighted more heavily because they prevent enemy scoring.\n"
elif tag == "Consistency":
text += "\nConsistency Formula:\n"
text += "Consistency = (Floor × 0.40) + (Stability × 0.40) + (Average Performance × 0.20)\n"
text += "Where:\n"
text += "- Per-Match Performance = (Slayer × 0.40) + (Objective × 0.40) + (Accuracy × 0.20)\n"
text += "- Floor = the player's lowest per-match performance score\n"
text += "- Stability = 1 / (1 + Adjusted Variance)\n"
text += "- Adjusted Variance = Variance × (1 + 1 / Match Count)\n"
elif tag == "Clutch":
text += "\nClutch Formula:\n"
text += "Clutch = (Pressure Efficiency × 0.40) + (Collapse Avoidance × 0.40) + (Conversion Rate × 0.20)\n"
text += "Where:\n"
text += "- Pressure Efficiency = Damage / (Deaths + 1)\n"
text += "- Collapse Avoidance = Player's lowest per-map performance score\n"
text += "- Conversion Rate = (Accuracy × 0.50) + (Headshot Rate × 0.50)\n"
output.delete("1.0", tk.END)
output.insert(tk.END, text)
def export():
content = output.get("1.0", tk.END).strip()
if not content:
messagebox.showerror("Error", "No rankings text to export.")
return
tag = combo.get() or "rankings"
filename = f"rankings_{tag}.txt"
path = export_to_obs(filename, content)
messagebox.showinfo("Exported", f"Rankings exported to:\n{path}")
ttk.Button(win, text="Show Rankings", command=show).pack(pady=5)
def export_all_obs_files(self):
"""
Unified export function that writes all OBS text files
based on the current map type and current match players.
"""
map_type = self.map_var.get()
team_a = self.team_a_var.get()
team_b = self.team_b_var.get()
# If teams or map not selected, skip silently
if not map_type or not team_a or not team_b:
return
# --- Extract selected players for each team ---
teamA_players = [p for p in self.current_match_players if p.get("team") == team_a]
teamB_players = [p for p in self.current_match_players if p.get("team") == team_b]
# If either team has no selected players, skip storyline/predictions
if len(teamA_players) == 0 or len(teamB_players) == 0:
print("OBS export skipped: no selected players for one or both teams.")
return
# --- STORYLINE + TEAM IDENTITY ---
try:
storyline_text = matchup_storyline(teamA_players, teamB_players)
identity_text = compare_team_identity(teamA_players, teamB_players)
full_story = storyline_text + "\n\nTEAM IDENTITY SUMMARY\n" + identity_text
write_text(os.path.join("obs_exports", "storyline.txt"), full_story)
except Exception as e:
print("Storyline export failed:", e)
# --- PREDICTIONS (composition-aware) ---
try:
if hasattr(self, "generate_predictions"):
predictions = self.generate_predictions(teamA_players, teamB_players)
write_text(os.path.join("obs_exports", "predictions.txt"), predictions)
except Exception as e:
print("Prediction export failed:", e)
# --- PLAYER TAG EXPORTS ---
try:
career_path = os.path.join(BASE_DIR, "career_stats.json")
if os.path.exists(career_path):
with open(career_path, "r", encoding="utf-8") as f:
career_db = json.load(f)
for idx, p in enumerate(self.current_match_players):
pid = p.get("id")
if pid and pid in career_db["players"]:
player_entry = career_db["players"][pid]
tags_text = self.generate_player_tags(player_entry, map_type)
slot_path = os.path.join(PLAYERS_DIR, f"p{idx}", "Tags.txt")
write_text(slot_path, tags_text)
except Exception as e:
print("Tag export failed:", e)
print(f"OBS files updated for {map_type}")
def main():
root = tk.Tk()
app = DashLeagueGUI(root)
root.mainloop()
if __name__ == "__main__":
main()

0
data/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

View file

@ -0,0 +1 @@
[]

Some files were not shown because too many files have changed in this diff Show more