2026-04-08 08:13:02 +00:00
import os
import json
import math
from collections import defaultdict
2026-04-09 10:11:39 +00:00
2026-04-08 08:13:02 +00:00
import tkinter as tk
2026-04-09 10:11:39 +00:00
from tkinter import ttk , messagebox , filedialog
2026-04-08 08:13:02 +00:00
import requests
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
# Internal modules — clean, correct, no duplicates
# ---------------------------------------------------------
# Career DB builder
2026-04-08 08:13:02 +00:00
from data . career_db import build_career_database
2026-04-22 07:25:42 +00:00
# Career rankings (used for global leaderboards)
from rankings import (
top_players_by_tag ,
split_two_columns ,
rank_match_players_by_tag ,
)
# Team identity + storyline + predictions
2026-04-10 08:08:47 +00:00
from analysis . team_identity_v2 import generate_team_identity_block
from analysis . storylines import generate_storyline , matchup_storyline
from analysis . predictions_v2 import generate_prediction
2026-04-23 09:24:48 +00:00
from analysis . rookie_leaderboard import rank_rookies_by_tag
2026-04-22 07:25:42 +00:00
# OBS export
2026-04-10 08:08:47 +00:00
from analysis . obs_export_v2 import export_all_obs
2026-04-09 10:11:39 +00:00
2026-04-22 07:25:42 +00:00
# Domination objective (match + career)
2026-04-09 10:11:39 +00:00
from tags . objective_domination import (
compute_dom_objective_for_career_player ,
compute_dom_objective_for_match_player ,
compute_dom_objective_team_scores ,
)
2026-04-22 07:25:42 +00:00
# Prediction engine (legacy but still used)
2026-04-08 08:13:02 +00:00
from predictions . prediction_engine import generate_predictions
2026-04-22 07:25:42 +00:00
# Match-based tag engines
2026-04-08 08:13:02 +00:00
from match_engine import (
rank_match_players_slayer ,
rank_match_players_objpl ,
rank_match_players_objdom ,
2026-04-09 10:11:39 +00:00
rank_match_players_sharpshooter ,
2026-04-08 08:13:02 +00:00
rank_match_players_clutch ,
compute_slayer_prediction ,
2026-04-14 07:32:50 +00:00
rank_match_players_consistency_career ,
2026-04-23 09:24:48 +00:00
load_career_db ,
attach_career_tags_to_match_player ,
2026-04-08 08:13:02 +00:00
)
2026-04-22 07:25:42 +00:00
from data . tag_framework import percentile_rank , tier_from_percentile
2026-04-09 10:11:39 +00:00
# ---------------------------------------------------------
# PATHS + CONFIG
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
BASE_DIR = os . path . dirname ( os . path . abspath ( __file__ ) )
CONFIG_PATH = os . path . join ( BASE_DIR , " config.json " )
2026-04-21 08:55:26 +00:00
SCREENS_DIR = os . path . join ( BASE_DIR , " screens " )
2026-04-09 10:11:39 +00:00
# Load config (or default)
if os . path . exists ( CONFIG_PATH ) :
with open ( CONFIG_PATH , " r " , encoding = " utf-8 " ) as f :
config = json . load ( f )
else :
config = { }
# Default output folder (outside repo)
DEFAULT_OUTPUT_ROOT = os . path . abspath ( os . path . join ( BASE_DIR , " .. " , " stats " ) )
# Allow override from config.json
output_root = config . get ( " output_root " , DEFAULT_OUTPUT_ROOT )
# Build runtime directories
STATS_DIR = output_root
PLAYERS_DIR = os . path . join ( STATS_DIR , " players " )
OBS_EXPORT_DIR = os . path . join ( STATS_DIR , " obs_exports " )
os . makedirs ( PLAYERS_DIR , exist_ok = True )
2026-04-08 08:13:02 +00:00
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
2026-04-10 08:08:47 +00:00
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
# Breaker (match-based)
# ---------------------------------------------------------
def compute_breaker_for_match_player ( p ) :
kills = p . get ( " kills " , 0 ) or 0
damage = p . get ( " damage " , 0 ) or 0
dom_counters = p . get ( " DOM_Counters " , 0 ) or 0
dom_caps = p . get ( " DOM_Captures " , 0 ) or 0
cp_caps = p . get ( " CP_Captures " , 0 ) or 0
raw = (
dom_counters * 1.0 +
dom_caps * 0.6 +
cp_caps * 0.4 +
kills * 0.05 +
damage / 20000.0
)
return max ( raw , 0.0 )
2026-04-10 08:08:47 +00:00
# ---------------------------------------------------------
# Team Identity Formatter (Standalone Helper)
# ---------------------------------------------------------
def format_team_identity ( identity ) :
"""
Converts the identity block dict into readable caster - friendly text .
"""
lines = [ ]
lines . append ( f " Team Style: { identity . get ( ' team_style ' , ' Unknown ' ) } " )
lines . append ( " " )
lines . append ( " Strengths: " )
for s in identity . get ( " strengths " , [ ] ) :
lines . append ( f " - { s } " )
lines . append ( " " )
lines . append ( " Weaknesses: " )
for w in identity . get ( " weaknesses " , [ ] ) :
lines . append ( f " - { w } " )
2026-04-08 08:13:02 +00:00
2026-04-10 08:08:47 +00:00
lines . append ( " " )
lines . append ( " Tag Breakdown: " )
for tag , data in identity . get ( " tags " , { } ) . items ( ) :
lines . append (
f " - { tag . replace ( ' _ ' , ' ' ) . title ( ) } : "
f " { data [ ' tier ' ] } Tier (avg { data [ ' avg_pct ' ] : .1f } percentile) "
)
return " \n " . join ( lines )
2026-04-08 08:13:02 +00:00
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 " )
2026-04-22 07:25:42 +00:00
self . team_a_var = tk . StringVar ( )
self . team_b_var = tk . StringVar ( )
2026-04-08 08:13:02 +00:00
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 ) )
2026-04-13 07:56:38 +00:00
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 = 0 , padx = ( 0 , 10 ) )
2026-04-08 08:13:02 +00:00
self . sync_button = ttk . Button ( top_frame , text = " Sync Stats " , command = self . on_sync_stats )
2026-04-13 07:56:38 +00:00
self . sync_button . grid ( row = 0 , column = 1 , padx = ( 0 , 10 ) )
2026-04-08 08:13:02 +00:00
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 ) )
2026-04-23 09:24:48 +00:00
self . rookie_button = ttk . Button ( top_frame , text = " Rookie Leaderboard " , command = self . on_rookie_leaderboard )
self . rookie_button . grid ( row = 0 , column = 6 , padx = ( 0 , 10 ) )
2026-04-09 10:11:39 +00:00
self . output_folder_button = ttk . Button (
top_frame ,
text = " Select Output Folder " ,
command = self . choose_output_folder
)
2026-04-23 09:24:48 +00:00
self . output_folder_button . grid ( row = 0 , column = 7 , padx = ( 0 , 10 ) )
2026-04-09 10:11:39 +00:00
2026-04-08 08:13:02 +00:00
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 " , [ ] )
2026-04-23 09:24:48 +00:00
2026-04-22 07:25:42 +00:00
print ( " DEBUG: Checking raw API teams... " )
for p in stats :
print ( " TEAM FIELD: " , repr ( p . get ( " team " ) ) )
2026-04-23 09:24:48 +00:00
2026-04-21 08:55:26 +00:00
# ---------------------------------------------------------
# Load PIR + Career DB
# ---------------------------------------------------------
from analysis . player_identity_registry import PlayerIdentityRegistry
pir = PlayerIdentityRegistry ( )
career_path = os . path . join ( BASE_DIR , " career_stats.json " )
with open ( career_path , " r " , encoding = " utf-8 " ) as f :
career_db = json . load ( f )
2026-04-23 09:24:48 +00:00
players_db = career_db . setdefault ( " players " , { } )
2026-04-21 08:55:26 +00:00
# ---------------------------------------------------------
2026-04-23 09:24:48 +00:00
# BUILD stats_by_id (canonical → enriched player object)
2026-04-21 08:55:26 +00:00
# ---------------------------------------------------------
new_stats_by_id = { }
for p in stats :
2026-04-23 09:24:48 +00:00
raw_id = p . get ( " id " )
if not raw_id :
continue
2026-04-21 08:55:26 +00:00
2026-04-23 09:24:48 +00:00
# Resolve canonical + stamp seasons
canonical = pir . resolve ( raw_id , season = self . current_season )
2026-04-24 06:30:50 +00:00
canonical = pir . resolve ( raw_id , season = self . current_season )
# Update Registry and History with clean names
clean_name = p . get ( " name " )
clean_team = ( p . get ( " team " ) or " " ) . strip ( )
pir . update_name ( canonical , clean_name )
if clean_team :
pir . update_team ( canonical , clean_team )
# Ensure career_stats has this history
if canonical in players_db :
players_db [ canonical ] [ " team_history " ] = pir . data [ " canonical " ] [ canonical ] [ " team_history " ]
2026-04-23 09:24:48 +00:00
if not canonical :
continue
2026-04-22 07:25:42 +00:00
2026-04-23 09:24:48 +00:00
# Ensure first_season is set if missing
entry = pir . data [ " canonical " ] . get ( canonical )
#if entry is not None and entry.get("first_season") is None:
# entry["first_season"] = self.current_season
# Normalize DOM fields from API (for DomObj)
p [ " DOM_Captures " ] = p . get ( " DOM_captures " , p . get ( " DOM_Captures " , 0 ) )
p [ " DOM_Counters " ] = p . get ( " DOM_counters " , p . get ( " DOM_Counters " , 0 ) )
merged = {
" canonical " : canonical ,
" name " : p . get ( " name " ) ,
" team " : ( p . get ( " team " ) or " " ) . strip ( ) ,
" id " : raw_id ,
" kills " : p . get ( " kills " , 0 ) ,
" deaths " : p . get ( " deaths " , 0 ) ,
" damage " : p . get ( " damage " , 0 ) ,
" shots " : p . get ( " shots " , 0 ) ,
" shots_hit " : p . get ( " shots_hit " , 0 ) ,
" match_stats " : p ,
" seasons_played " : p . get ( " seasons_played " ) or p . get ( " seasons " ) or " " ,
}
2026-04-21 08:55:26 +00:00
2026-04-23 09:24:48 +00:00
# Ensure player exists in career DB (so rookies are present)
if canonical not in players_db :
players_db [ canonical ] = {
" name " : merged [ " name " ] ,
" team_history " : [ merged [ " team " ] ] if merged [ " team " ] else [ ] ,
" seasons_played " : merged . get ( " seasons_played " ) ,
" slayer " : { } ,
" sharpshooter " : { } ,
" objective_payload " : { } ,
" objective_domination " : { } ,
" consistency " : { } ,
" clutch " : { } ,
" anchor " : { } ,
" breaker " : { } ,
}
2026-04-24 06:30:50 +00:00
pdata = players_db . setdefault ( canonical , { } )
pdata [ " seasons_played " ] = merged [ " seasons_played " ]
print ( " DEBUG: seasons fields: " )
#for p in stats:
# print(p.get("name"), "seasons:", p.get("seasons"), "seasons_played:", p.get("seasons_played"))
2026-04-23 09:24:48 +00:00
# Store basic stats into career DB so rookies have real data
pdata [ " kills " ] = merged . get ( " kills " , 0 )
pdata [ " deaths " ] = merged . get ( " deaths " , 0 )
pdata [ " damage " ] = merged . get ( " damage " , 0 )
pdata [ " shots " ] = merged . get ( " shots " , 0 )
pdata [ " shots_hit " ] = merged . get ( " shots_hit " , 0 )
# Score is inside match_stats
ms = merged . get ( " match_stats " , { } )
pdata [ " score " ] = ms . get ( " score " , 0 )
# KD
if pdata [ " deaths " ] > 0 :
pdata [ " kd " ] = pdata [ " kills " ] / pdata [ " deaths " ]
else :
pdata [ " kd " ] = pdata [ " kills " ]
# Keep seasons_played updated
#if merged.get("seasons_played"):
# pdata["seasons_played"] = merged["seasons_played"]
2026-04-24 06:30:50 +00:00
#pdata["seasons_played"] = merged.get("seasons_played")
2026-04-21 08:55:26 +00:00
2026-04-22 07:25:42 +00:00
# Attach career tags
2026-04-23 09:24:48 +00:00
for tag in (
" slayer " ,
" sharpshooter " ,
" objective_payload " ,
" objective_domination " ,
" consistency " ,
" clutch " ,
" anchor " ,
" breaker " ,
) :
merged [ tag ] = pdata . get ( tag , { } )
2026-04-22 07:25:42 +00:00
new_stats_by_id [ canonical ] = merged
2026-04-23 09:24:48 +00:00
# Write merged stats into career DB
players_db [ canonical ] [ " kills " ] = merged [ " kills " ]
players_db [ canonical ] [ " deaths " ] = merged [ " deaths " ]
players_db [ canonical ] [ " damage " ] = merged [ " damage " ]
players_db [ canonical ] [ " shots " ] = merged [ " shots " ]
players_db [ canonical ] [ " shots_hit " ] = merged [ " shots_hit " ]
players_db [ canonical ] [ " match_stats " ] = merged [ " match_stats " ]
players_db [ canonical ] [ " seasons_played " ] = merged [ " seasons_played " ]
# Compute KD
d = merged [ " deaths " ]
players_db [ canonical ] [ " kd " ] = merged [ " kills " ] / d if d > 0 else merged [ " kills " ]
# Score (inside match_stats)
players_db [ canonical ] [ " score " ] = merged [ " match_stats " ] . get ( " score " , 0 )
# ---------------------------------------------------------
# Save updated career DB (rookies now included)
# ---------------------------------------------------------
with open ( career_path , " w " , encoding = " utf-8 " ) as f :
json . dump ( career_db , f , indent = 2 )
2026-04-22 07:25:42 +00:00
2026-04-23 09:24:48 +00:00
# ---------------------------------------------------------
# Save PIR (first_season now persisted)
# ---------------------------------------------------------
pir . save ( )
# ---------------------------------------------------------
# Replace stats_by_id with enriched version
# ---------------------------------------------------------
2026-04-21 08:55:26 +00:00
self . stats_by_id = new_stats_by_id
2026-04-22 07:25:42 +00:00
2026-04-23 09:24:48 +00:00
# ---------------------------------------------------------
# Debug: show teams detected
# ---------------------------------------------------------
2026-04-22 07:25:42 +00:00
teams_in_api = sorted ( { p [ " team " ] for p in new_stats_by_id . values ( ) } )
print ( " TEAMS IN API: " , teams_in_api )
2026-04-23 09:24:48 +00:00
2026-04-08 08:13:02 +00:00
self . set_status ( " Stats synced. " )
messagebox . showinfo ( " Success " , f " Synced { len ( stats ) } player stats. " )
2026-04-23 09:24:48 +00:00
2026-04-08 08:13:02 +00:00
except Exception as e :
messagebox . showerror ( " Error " , f " Failed to sync stats: \n { e } " )
self . set_status ( " Sync failed. " )
2026-04-23 09:24:48 +00:00
2026-04-22 07:25:42 +00:00
# Re-trigger team selection so listboxes repopulate
if self . team_a_var . get ( ) :
self . on_team_select ( " A " )
if self . team_b_var . get ( ) :
2026-04-23 09:24:48 +00:00
self . on_team_select ( " B " )
2026-04-22 07:25:42 +00:00
2026-04-08 08:13:02 +00:00
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 ( )
2026-04-22 07:25:42 +00:00
print ( " TEAM SELECT FIRED: " , side , " team_name= " , team_name )
2026-04-08 08:13:02 +00:00
2026-04-22 07:25:42 +00:00
# Debug: show how many players match this team
matching = [ ]
for p in self . stats_by_id . values ( ) :
if p . get ( " team " ) == team_name or p . get ( " TeamUUID " ) == team_name :
matching . append ( p )
print ( f " DEBUG: Found { len ( matching ) } players for team { team_name } " )
for p in matching :
print ( " PLAYER: " , p . get ( " name " ) , " | TEAM: " , p . get ( " team " ) )
# Now assign to UI
2026-04-08 08:13:02 +00:00
if side == " A " :
2026-04-22 07:25:42 +00:00
self . team_a_players = matching
2026-04-08 08:13:02 +00:00
self . team_a_listbox . delete ( 0 , tk . END )
2026-04-22 07:25:42 +00:00
for p in matching :
2026-04-08 08:13:02 +00:00
self . team_a_listbox . insert ( tk . END , p . get ( " name " , " Unknown " ) )
else :
2026-04-22 07:25:42 +00:00
self . team_b_players = matching
2026-04-08 08:13:02 +00:00
self . team_b_listbox . delete ( 0 , tk . END )
2026-04-22 07:25:42 +00:00
for p in matching :
2026-04-08 08:13:02 +00:00
self . team_b_listbox . insert ( tk . END , p . get ( " name " , " Unknown " ) )
2026-04-09 10:11:39 +00:00
2026-04-14 07:32:50 +00:00
def normalize_player ( self , p ) :
2026-04-09 10:11:39 +00:00
return {
2026-04-22 07:25:42 +00:00
" id " : p . get ( " id " ) ,
" name " : p . get ( " name " ) ,
" team " : p . get ( " team " ) ,
" KD " : p . get ( " KD " , 0 ) ,
" kills " : p . get ( " kills " , 0 ) ,
" deaths " : p . get ( " deaths " , 0 ) ,
" headshots " : p . get ( " headshots " , 0 ) ,
" accuracy " : p . get ( " accuracy " , 0 ) ,
" PAY_PushTime " : p . get ( " PAY_PushTime " , 0 ) ,
" DOM_captures " : p . get ( " DOM_captures " , 0 ) ,
" DOM_counters " : p . get ( " DOM_counters " , 0 ) ,
}
2026-04-09 10:11:39 +00:00
2026-04-08 08:13:02 +00:00
def on_generate_slots ( self ) :
try :
ensure_player_slots ( )
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
2026-04-09 10:11:39 +00:00
# 1. Collect selected players
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
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
2026-04-22 07:25:42 +00:00
# Limit to 5 per team
selected_a = selected_a [ : 5 ]
selected_b = selected_b [ : 5 ]
all_players = selected_a + selected_b
2026-04-23 09:24:48 +00:00
print ( " === DEBUG MATCH_STATS SAMPLE === " )
print ( all_players [ 0 ] . get ( " match_stats " , { } ) )
2026-04-08 08:13:02 +00:00
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
# 2. Normalize stat keys BEFORE attaching tags
# ---------------------------------------------------------
2026-04-23 09:24:48 +00:00
2026-04-09 10:11:39 +00:00
for p in all_players :
2026-04-23 09:24:48 +00:00
ms = p . get ( " match_stats " , { } )
# DOM
p [ " DOM_Captures " ] = (
2026-04-24 06:30:50 +00:00
p . get ( " DOM_Captures " )
or p . get ( " DOM_captures " )
or ms . get ( " DOM_Captures " )
or ms . get ( " DOM_captures " )
2026-04-23 09:24:48 +00:00
or 0
)
p [ " DOM_Counters " ] = (
p . get ( " DOM_Counters " )
or p . get ( " DOM_counters " )
or ms . get ( " DOM_Counters " )
or ms . get ( " DOM_counters " )
or 0
)
2026-04-09 10:11:39 +00:00
2026-04-21 08:55:26 +00:00
# ---------------------------------------------------------
2026-04-22 07:25:42 +00:00
# 3. Resolve canonical IDs AND normalize player ID
2026-04-21 08:55:26 +00:00
# ---------------------------------------------------------
from analysis . player_identity_registry import PlayerIdentityRegistry
pir = PlayerIdentityRegistry ( )
for p in all_players :
2026-04-22 07:25:42 +00:00
# Resolve canonical ID first
2026-04-21 08:55:26 +00:00
raw_uuid = (
p . get ( " id " )
or p . get ( " PlayerUUID " )
or p . get ( " uuid " )
)
if raw_uuid :
2026-04-23 09:24:48 +00:00
p [ " canonical " ] = pir . resolve ( raw_uuid , season = self . current_season )
2026-04-21 08:55:26 +00:00
2026-04-22 07:25:42 +00:00
# Normalize ID (AFTER canonical resolution)
pid = (
p . get ( " id " )
or p . get ( " PlayerUUID " )
or p . get ( " uuid " )
2026-04-23 09:24:48 +00:00
or p . get ( " canonical " )
2026-04-22 07:25:42 +00:00
or p . get ( " name " )
)
p [ " id " ] = pid
2026-04-21 08:55:26 +00:00
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
# 4. Compute match-based Breaker
# ---------------------------------------------------------
for p in all_players :
breaker_raw = compute_breaker_for_match_player ( p )
p [ " breaker " ] = { " raw " : breaker_raw }
# ---------------------------------------------------------
# 5. Load career DB
# ---------------------------------------------------------
career_db = None
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 )
# ---------------------------------------------------------
# 6. Compute match-based Consistency (raw only)
# ---------------------------------------------------------
2026-04-23 09:24:48 +00:00
print ( " \n === DEBUG: Player objects BEFORE consistency === " )
for p in all_players :
print ( p . get ( " name " ) , " | id: " , p . get ( " id " ) , " | uuid: " , p . get ( " uuid " ) , " | PlayerUUID: " , p . get ( " PlayerUUID " ) , " | canonical: " , p . get ( " canonical_id " ) )
2026-04-22 07:25:42 +00:00
consistency_rows = rank_match_players_consistency_career ( all_players )
consistency_map = { row [ " id " ] : row [ " score_raw " ] for row in consistency_rows }
for p in all_players :
raw = consistency_map . get ( p [ " id " ] , 0.0 )
p [ " consistency " ] = { " raw " : raw }
# ---------------------------------------------------------
# 7. Build match-based consistency distribution
# ---------------------------------------------------------
consistency_distribution = [ p [ " consistency " ] [ " raw " ] for p in all_players ]
# ---------------------------------------------------------
# 8. Compute percentile + tier for match-based Consistency
# ---------------------------------------------------------
from data . tag_framework import percentile_rank , tier_from_percentile
for p in all_players :
raw = p [ " consistency " ] [ " raw " ]
pct = percentile_rank ( raw , consistency_distribution )
tier = tier_from_percentile ( pct )
p [ " consistency " ] [ " pct " ] = pct
p [ " consistency " ] [ " tier " ] = tier
p [ " consistency " ] [ " summary " ] = f " { tier } Tier ( { pct : .1f } percentile) "
# ---------------------------------------------------------
# 9. Attach career tags (DO NOT overwrite match consistency)
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
if career_db is not None :
for p in all_players :
2026-04-23 09:24:48 +00:00
cid = p . get ( " canonical " )
2026-04-22 07:25:42 +00:00
if not cid :
2026-04-21 08:55:26 +00:00
continue
2026-04-22 07:25:42 +00:00
pdata = career_db [ " players " ] . get ( cid )
2026-04-21 08:55:26 +00:00
if not pdata :
continue
p [ " slayer " ] = pdata . get ( " slayer " , { } )
p [ " sharpshooter " ] = pdata . get ( " sharpshooter " , { } )
p [ " objective_payload " ] = pdata . get ( " objective_payload " , { } )
2026-04-22 07:25:42 +00:00
p [ " clutch " ] = pdata . get ( " clutch " , { } )
p [ " anchor " ] = pdata . get ( " anchor " , { } )
2026-04-23 09:24:48 +00:00
p [ " breaker " ] = pdata . get ( " breaker " , { } )
2026-04-21 08:55:26 +00:00
# Do NOT overwrite match-based DomObj
2026-04-23 09:24:48 +00:00
if " objective_domination " not in p or not p [ " objective_domination " ] :
2026-04-21 08:55:26 +00:00
p [ " objective_domination " ] = pdata . get ( " objective_domination " , { } )
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
# 10. Compute match-based Domination Objective
# ---------------------------------------------------------
2026-04-13 07:56:38 +00:00
for p in all_players :
dom = compute_dom_objective_for_match_player ( p )
caps = dom . get ( " captures " , 0 )
counters = dom . get ( " counters " , 0 )
dom_score = caps * 0.4 + counters * 0.6
2026-04-23 09:24:48 +00:00
print ( " DOM DEBUG: " , p [ " name " ] , " caps= " , caps , " counters= " , counters )
p [ " objective_domination " ] = {
2026-04-13 07:56:38 +00:00
" raw " : dom_score ,
2026-04-23 09:24:48 +00:00
" pct " : dom_score , # <- match panel can now read this
" tier " : " - " , # <- neutral tier for single-match view
2026-04-13 07:56:38 +00:00
" captures " : caps ,
" counters " : counters ,
2026-04-23 09:24:48 +00:00
" summary " : " " , # optional, keeps shape consistent
2026-04-13 07:56:38 +00:00
}
2026-04-23 09:24:48 +00:00
# 11. Ensure stats_by_id contains all enriched tags
2026-04-22 07:25:42 +00:00
for p in all_players :
2026-04-23 09:24:48 +00:00
cid = p . get ( " canonical " )
2026-04-22 07:25:42 +00:00
if not cid :
continue
enriched = self . stats_by_id . get ( cid )
if not enriched :
continue
2026-04-23 09:24:48 +00:00
# Copy match fields into stats_by_id
enriched [ " team " ] = p . get ( " team " )
enriched [ " name " ] = p . get ( " name " )
enriched [ " id " ] = p . get ( " id " )
2026-04-13 07:56:38 +00:00
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
# 12. Update current_match_players
# ---------------------------------------------------------
self . current_match_players = all_players
# ---------------------------------------------------------
# 13. Write OBS slot files
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
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 " , " " ) )
2026-04-22 07:25:42 +00:00
2026-04-08 08:13:02 +00:00
write_text ( os . path . join ( slot_dir , " PushTime.txt " ) , p . get ( " PAY_PushTime " , " " ) )
2026-04-09 10:11:39 +00:00
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 " , " " ) )
2026-04-08 08:13:02 +00:00
2026-04-22 07:25:42 +00:00
# Consistency
write_text ( os . path . join ( slot_dir , " ConsistencyRaw.txt " ) , p [ " consistency " ] [ " raw " ] )
write_text ( os . path . join ( slot_dir , " ConsistencyPct.txt " ) , p [ " consistency " ] [ " pct " ] )
write_text ( os . path . join ( slot_dir , " ConsistencyTier.txt " ) , p [ " consistency " ] [ " tier " ] )
2026-04-21 08:55:26 +00:00
2026-04-08 08:13:02 +00:00
self . set_status ( " Player slots generated. " )
messagebox . showinfo ( " Success " , " Player slots generated into p0– p9. " )
2026-04-09 10:11:39 +00:00
2026-04-08 08:13:02 +00:00
except Exception as e :
messagebox . showerror ( " Error " , f " Failed to generate slots: \n { e } " )
self . set_status ( " Generate failed. " )
2026-04-09 10:11:39 +00:00
2026-04-08 08:13:02 +00:00
self . export_all_obs_files ( )
def on_build_career_db ( self ) :
try :
self . set_status ( " Building career database... " )
2026-04-10 08:08:47 +00:00
2026-04-13 07:56:38 +00:00
# Use the same file the rest of the tool reads
output_path = os . path . join ( BASE_DIR , " career_stats.json " )
2026-04-10 08:08:47 +00:00
2026-04-08 08:13:02 +00:00
build_career_database ( output_path )
2026-04-21 08:55:26 +00:00
#print("DEBUG CAREER DB KEYS:", career_db.keys())
#print("DEBUG PLAYER COUNT:", len(career_db.get("players", {})))
2026-04-10 08:08:47 +00:00
2026-04-08 08:13:02 +00:00
self . set_status ( " Career database built successfully. " )
messagebox . showinfo ( " Success " , " Career database has been built. " )
2026-04-10 08:08:47 +00:00
2026-04-08 08:13:02 +00:00
except Exception as e :
import traceback
traceback . print_exc ( )
messagebox . showerror ( " Error " , f " Failed to build career database: \n { e } " )
self . set_status ( " Career DB build failed. " )
2026-04-13 07:56:38 +00:00
2026-04-08 08:13:02 +00:00
def on_player_spotlight ( self ) :
2026-04-24 06:30:50 +00:00
# 1. Check if players are actually selected for a match
if not self . current_match_players :
messagebox . showerror ( " Error " , " Please select teams and click ' Generate Player Slots ' first. " )
2026-04-08 08:13:02 +00:00
return
2026-04-24 06:30:50 +00:00
# 2. Get names ONLY from the 10 players in the current match
names = [ p [ " name " ] for p in self . current_match_players ]
names . sort ( )
# Load the career DB once so the 'show' function can pull match history for trends
career_path = os . path . join ( BASE_DIR , " career_stats.json " )
2026-04-08 08:13:02 +00:00
with open ( career_path , " r " , encoding = " utf-8 " ) as f :
db = json . load ( f )
win = tk . Toplevel ( self . root )
2026-04-24 06:30:50 +00:00
win . title ( " Match Player Spotlight " )
win . geometry ( " 450x550 " )
2026-04-08 08:13:02 +00:00
2026-04-24 06:30:50 +00:00
ttk . Label ( win , text = " Select a Player from this Match: " ) . pack ( anchor = " w " , padx = 10 , pady = 5 )
2026-04-08 08:13:02 +00:00
combo = ttk . Combobox ( win , values = names , state = " readonly " )
combo . pack ( fill = " x " , padx = 10 )
2026-04-24 06:30:50 +00:00
output = tk . Text ( win , wrap = " word " , font = ( " Courier " , 10 ) )
2026-04-08 08:13:02 +00:00
output . pack ( expand = True , fill = " both " , padx = 10 , pady = 10 )
def show ( ) :
2026-04-24 06:30:50 +00:00
selected_name = combo . get ( )
if not selected_name :
2026-04-08 08:13:02 +00:00
return
2026-04-24 06:30:50 +00:00
# Find the match player object (this has the current team/side info)
match_p = next ( ( p for p in self . current_match_players if p [ " name " ] == selected_name ) , None )
# Find the full career object (this has the history/matches for the graphics engine)
cid = match_p . get ( " canonical " )
player_data = db [ " players " ] . get ( cid )
if not player_data :
2026-04-08 08:13:02 +00:00
output . delete ( " 1.0 " , tk . END )
2026-04-24 06:30:50 +00:00
output . insert ( tk . END , " Error: Player data not found in career database. " )
2026-04-08 08:13:02 +00:00
return
2026-04-24 06:30:50 +00:00
# Determine Team Side for Colors
# If the player is in the Team B listbox, they are 'Red'
side = " Blue "
team_b_names = [ self . team_b_listbox . get ( i ) for i in range ( self . team_b_listbox . size ( ) ) ]
if selected_name in team_b_names :
side = " Red "
# 3. Trigger the 4K Graphic
from utils . graphics_engine import generate_player_card
try :
img_path = generate_player_card ( player_data , team_side = side )
self . set_status ( f " Generated 4K Spotlight: { selected_name } " )
except Exception as e :
print ( f " Graphics Error: { e } " )
# 4. Generate the Text Preview for the GUI window
career = player_data [ " career " ]
text = f " PLAYER SPOTLIGHT: { selected_name } \n "
text + = f " Team: { match_p . get ( ' team ' , ' Unknown ' ) } \n "
text + = f " ----------------------------------- \n \n "
text + = f " CAREER STATS: \n "
text + = f " Kills: { int ( career [ ' kills ' ] ) : , } \n "
text + = f " Maps: { int ( career [ ' maps ' ] ) } \n "
text + = f " KD: { career [ ' KD ' ] : .2f } \n \n "
text + = f " TAG PERCENTILES: \n "
for tag in [ " slayer " , " sharpshooter " , " consistency " , " clutch " , " anchor " ] :
t_obj = player_data . get ( tag , { } )
text + = f " { tag . title ( ) : <14 } : { t_obj . get ( ' tier ' , ' D ' ) } ( { t_obj . get ( ' pct ' , 0 ) : .0f } %) \n "
2026-04-08 08:13:02 +00:00
output . delete ( " 1.0 " , tk . END )
output . insert ( tk . END , text )
2026-04-24 06:30:50 +00:00
ttk . Button ( win , text = " Generate 4K Graphic " , command = show ) . pack ( pady = 10 )
2026-04-08 08:13:02 +00:00
def on_team_identity ( self ) :
team = self . team_a_var . get ( )
if not team :
messagebox . showerror ( " Error " , " Select a team first. " )
return
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
2026-04-10 08:08:47 +00:00
identity_data = generate_team_identity_block ( team_players , team )
identity_text = format_team_identity ( identity_data )
2026-04-08 08:13:02 +00:00
win = tk . Toplevel ( self . root )
win . title ( f " Team Identity — { team } " )
2026-04-10 08:08:47 +00:00
win . geometry ( " 500x600 " )
2026-04-08 08:13:02 +00:00
output = tk . Text ( win , wrap = " word " )
output . pack ( expand = True , fill = " both " , padx = 10 , pady = 10 )
2026-04-10 08:08:47 +00:00
output . insert ( tk . END , identity_text )
2026-04-08 08:13:02 +00:00
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 )
2026-04-22 07:25:42 +00:00
2026-04-08 08:13:02 +00:00
def generate_player_tags ( self , player_entry , map_type ) :
tags_out = [ ]
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-13 07:56:38 +00:00
# Slayer (career)
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-13 07:56:38 +00:00
slayer = player_entry . get ( " slayer " , { } )
slayer_strength = slayer . get ( " strength " ) or player_entry . get ( " slayer_strength " )
if isinstance ( slayer_strength , str ) :
2026-04-08 08:13:02 +00:00
tags_out . append ( slayer_strength )
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
# Consistency (career - hybrid system)
# ---------------------------------------------------------
consistency = player_entry . get ( " consistency " , { } )
if isinstance ( consistency , dict ) :
score = consistency . get ( " raw " )
if isinstance ( score , ( int , float ) ) :
tags_out . append ( f " Cons { score : .2f } " )
# ---------------------------------------------------------
2026-04-09 10:11:39 +00:00
# Payload Objective Specialist (match-based)
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
if map_type == " Payload " :
2026-04-09 10:11:39 +00:00
pl_info = player_entry . get ( " objective_payload_components " , { } )
pl_score = pl_info . get ( " payload_score_raw " , 0.0 )
tags_out . append ( f " ObjPL { pl_score : .2f } " )
2026-04-08 08:13:02 +00:00
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-09 10:11:39 +00:00
# Domination Objective Specialist (match-based)
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
if map_type == " Domination " :
2026-04-24 06:30:50 +00:00
dom_info = player_entry . get ( " objective_domination " , { } )
2026-04-13 07:56:38 +00:00
dom_score = dom_info . get ( " raw " , 0.0 )
2026-04-09 10:11:39 +00:00
tags_out . append ( f " ObjDOM { dom_score : .2f } " )
2026-04-08 08:13:02 +00:00
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-13 07:56:38 +00:00
# Sharpshooter (career)
2026-04-14 07:32:50 +00:00
# ---------------------------------------------------------
2026-04-13 07:56:38 +00:00
sharp = player_entry . get ( " sharpshooter " , { } ) . get ( " score " ) or player_entry . get ( " sharpshooter_score " )
if isinstance ( sharp , ( int , float ) ) :
2026-04-08 08:13:02 +00:00
tags_out . append ( f " Sharp { sharp : .2f } " )
2026-04-09 10:11:39 +00:00
return " , " . join ( tags_out )
2026-04-08 08:13:02 +00:00
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 :
2026-04-10 08:08:47 +00:00
messagebox . showerror ( " Error " , " Select both Team A and Team B. " )
2026-04-08 08:13:02 +00:00
return
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
2026-04-23 09:24:48 +00:00
# 1. Attach career tags to ALL match players (CRITICAL)
# ---------------------------------------------------------
from match_engine import load_career_db , attach_career_tags_to_match_player
career_db = load_career_db ( )
# self.stats_by_id keys ARE canonical IDs
for cid , p in self . stats_by_id . items ( ) :
if cid in career_db [ " players " ] :
attach_career_tags_to_match_player ( p , career_db [ " players " ] [ cid ] )
# ---------------------------------------------------------
# 2. Extract players for each team (using enriched objects)
2026-04-22 07:25:42 +00:00
# ---------------------------------------------------------
teamA_players = [ ]
teamB_players = [ ]
for p in self . current_match_players :
2026-04-23 09:24:48 +00:00
cid = p . get ( " canonical " ) # <-- correct key
2026-04-22 07:25:42 +00:00
if not cid :
continue
enriched = self . stats_by_id . get ( cid )
if not enriched :
continue
if enriched . get ( " team " ) == team_a :
teamA_players . append ( enriched )
elif enriched . get ( " team " ) == team_b :
teamB_players . append ( enriched )
2026-04-08 08:13:02 +00:00
2026-04-10 08:08:47 +00:00
if not teamA_players or not teamB_players :
messagebox . showerror ( " Error " , " No players selected for one or both teams. " )
2026-04-08 08:13:02 +00:00
return
2026-04-23 09:24:48 +00:00
# ---------------------------------------------------------
# 3. Generate storyline + identity blocks
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
storyline_text = matchup_storyline ( teamA_players , teamB_players )
2026-04-10 08:08:47 +00:00
identity_data_a = generate_team_identity_block ( teamA_players , team_a )
identity_text_a = format_team_identity ( identity_data_a )
identity_data_b = generate_team_identity_block ( teamB_players , team_b )
identity_text_b = format_team_identity ( identity_data_b )
2026-04-08 08:13:02 +00:00
full_story = (
storyline_text
2026-04-10 08:08:47 +00:00
+ " \n \n TEAM IDENTITY SUMMARY – " + team_a + " \n "
+ identity_text_a
+ " \n \n TEAM IDENTITY SUMMARY – " + team_b + " \n "
+ identity_text_b
2026-04-08 08:13:02 +00:00
)
2026-04-23 09:24:48 +00:00
# ---------------------------------------------------------
# 4. Popup window
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
win = tk . Toplevel ( self . root )
win . title ( " Match Storyline " )
2026-04-10 08:08:47 +00:00
win . geometry ( " 600x700 " )
2026-04-08 08:13:02 +00:00
output = tk . Text ( win , wrap = " word " )
output . pack ( expand = True , fill = " both " , padx = 10 , pady = 10 )
output . insert ( tk . END , full_story )
2026-04-23 09:24:48 +00:00
# ---------------------------------------------------------
# 5. Export to OBS
# ---------------------------------------------------------
2026-04-08 08:13:02 +00:00
def export ( ) :
content = output . get ( " 1.0 " , tk . END ) . strip ( )
if not content :
messagebox . showerror ( " Error " , " No storyline text to export. " )
return
2026-04-10 08:08:47 +00:00
teamA_name = self . team_a_var . get ( )
teamB_name = self . team_b_var . get ( )
export_all_obs (
OBS_EXPORT_DIR ,
teamA_players ,
teamB_players ,
teamA_name ,
teamB_name ,
)
messagebox . showinfo ( " Exported " , f " OBS files exported to: \n { OBS_EXPORT_DIR } " )
2026-04-08 08:13:02 +00:00
2026-04-10 08:08:47 +00:00
ttk . Button ( win , text = " Export to OBS " , command = export ) . pack ( pady = 5 )
2026-04-08 08:13:02 +00:00
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 " )
2026-04-13 07:56:38 +00:00
win . geometry ( " 800x600 " )
2026-04-08 08:13:02 +00:00
tags = [
" Slayer " ,
" Payload Objective Specialist " ,
" Sharpshooter " ,
" Domination Objective Specialist " ,
" Consistency " ,
" Clutch " ,
2026-04-21 08:55:26 +00:00
" Anchor " ,
" Breaker " ,
2026-04-08 08:13:02 +00:00
]
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
2026-04-24 06:30:50 +00:00
from match_engine import load_career_db , attach_career_tags_to_match_player
career_db = load_career_db ( )
if career_db :
for p in self . current_match_players :
cid = p . get ( " canonical " )
if cid and cid in career_db [ " players " ] :
attach_career_tags_to_match_player ( p , career_db [ " players " ] [ cid ] )
2026-04-08 08:13:02 +00:00
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 )
2026-04-21 08:55:26 +00:00
elif tag == " Sharpshooter " :
ranked = rank_match_players_sharpshooter ( self . current_match_players )
2026-04-08 08:13:02 +00:00
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 )
2026-04-14 07:32:50 +00:00
elif tag == " Consistency " :
2026-04-22 07:25:42 +00:00
ranked = rank_match_players_by_tag ( self . current_match_players , " consistency " )
2026-04-21 08:55:26 +00:00
elif tag == " Clutch " :
ranked = rank_match_players_clutch ( self . current_match_players )
elif tag == " Anchor " :
2026-04-22 07:25:42 +00:00
ranked = rank_match_players_by_tag ( self . current_match_players , " anchor " )
2026-04-21 08:55:26 +00:00
elif tag == " Breaker " :
2026-04-22 07:25:42 +00:00
ranked = rank_match_players_by_tag ( self . current_match_players , " breaker " )
elif tag == " Rookie " :
ranked = compute_rookie_leaderboard ( career_db , tag = " slayer " )
# convert rookie rows to the same format as other rankings
2026-04-21 08:55:26 +00:00
else :
messagebox . showerror ( " Error " , f " Unknown tag: { tag } " )
return
2026-04-08 08:13:02 +00:00
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 ]
2026-04-22 07:25:42 +00:00
#print("DEBUG LP ROW:", lp) # <--- add this
pct = lp . get ( " pct " )
if pct is not None :
pct_str = f " { pct : .0f } % "
left_str = f " { lp [ ' rank ' ] : >2 } . { lp [ ' name ' ] : <16 } { lp [ ' score_display ' ] : >6 } ( { pct_str } ) "
else :
left_str = f " { lp [ ' rank ' ] : >2 } . { lp [ ' name ' ] : <16 } { lp [ ' score_display ' ] : >6 } "
2026-04-08 08:13:02 +00:00
if i < len ( right ) :
rp = right [ i ]
2026-04-22 07:25:42 +00:00
pct = rp . get ( " pct " )
if pct is not None :
pct_str = f " { pct : .0f } % "
right_str = f " { rp [ ' rank ' ] : >2 } . { rp [ ' name ' ] : <16 } { rp [ ' score_display ' ] : >6 } ( { pct_str } ) "
else :
right_str = f " { rp [ ' rank ' ] : >2 } . { rp [ ' name ' ] : <16 } { rp [ ' score_display ' ] : >6 } "
2026-04-08 08:13:02 +00:00
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 + = " \n Slayer Formula: \n "
text + = " Slayer Score = (Damage per Map × 0.40) + (Kills per Map × 0.40) + (KD × 0.20) \n "
elif tag == " Sharpshooter " :
text + = " \n Sharpshooter 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 + = " \n Payload 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 + = " \n Domination 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 " :
2026-04-14 07:32:50 +00:00
text + = " \n Consistency Formula (Hybrid Career Tag): \n "
2026-04-08 08:13:02 +00:00
text + = " Consistency = (Floor × 0.40) + (Stability × 0.40) + (Average Performance × 0.20) \n "
text + = " Where: \n "
2026-04-14 07:32:50 +00:00
text + = " - Floor = lowest per-match performance score \n "
2026-04-08 08:13:02 +00:00
text + = " - Stability = 1 / (1 + Adjusted Variance) \n "
text + = " - Adjusted Variance = Variance × (1 + 1 / Match Count) \n "
2026-04-14 07:32:50 +00:00
text + = " - Average Performance = mean per-match performance score \n "
2026-04-08 08:13:02 +00:00
elif tag == " Clutch " :
text + = " \n Clutch 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 "
2026-04-21 08:55:26 +00:00
text + = " - Conversion Rate = (Accuracy × 0.50) + (Headshot Rate × 0.50) \n "
elif tag == " Anchor " :
text + = " \n Anchor Formula: \n "
text + = " Anchor = (Survivability × 0.45) + (DefensivePresence × 0.30) + (Consistency × 0.25) \n "
text + = " Where: \n "
text + = " - Survivability = (LeagueAvgDeathsPerMap / PlayerDeathsPerMap), clamped to [0.25, 1.50] then normalized to 0– 1 \n "
text + = " - DefensivePresence = (DamagePerMap / LeagueAvgDamagePerMap), clamped to [0, 1.5] then normalized to 0– 1 \n "
text + = " - Consistency = normalized 0– 1 stability score from the Consistency tag \n "
text + = " Survivability reflects how hard the player is to remove; Defensive Presence measures baseline contribution; Consistency rewards stable defensive performance. \n "
elif tag == " Breaker " :
text + = " \n Breaker Formula: \n "
text + = " Breaker = (DamageNorm × 0.60) + (KillsNorm × 0.40) \n "
text + = " Where: \n "
text + = " - DamageNorm = Damage per map normalized against league average \n "
text + = " - KillsNorm = Kills per map normalized against league average \n "
text + = " Breaker measures disruptive offensive pressure through high damage output and aggressive fragging. \n "
2026-04-08 08:13:02 +00:00
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 )
2026-04-23 09:24:48 +00:00
2026-04-24 06:30:50 +00:00
# dashleague_cast_tool.py -> on_rookie_leaderboard()
2026-04-23 09:24:48 +00:00
def on_rookie_leaderboard ( self ) :
from analysis . rookie_leaderboard import rank_rookies_by_tag
season = self . current_season
2026-04-24 06:30:50 +00:00
tag = " slayer "
2026-04-23 09:24:48 +00:00
rookies = rank_rookies_by_tag ( season , tag )
lines = [ ]
2026-04-24 06:30:50 +00:00
lines . append ( f " ROOKIE LEADERBOARD — Season { season } " )
lines . append ( " Rookies: < 50 maps and < 1000 kills. " )
2026-04-23 09:24:48 +00:00
lines . append ( " " )
2026-04-24 06:30:50 +00:00
# Updated Header
lines . append ( f " { ' Rank ' : <5 } { ' Name ' : <18 } { ' Team ' : <12 } { ' Score ' : <10 } { ' KD ' : <6 } { ' Maps ' : <6 } { ' Kills ' : <8 } " )
lines . append ( " - " * 75 )
2026-04-23 09:24:48 +00:00
for r in rookies :
2026-04-24 06:30:50 +00:00
# Format thousands for score (e.g. 96,107)
score_str = f " { int ( r [ ' score ' ] ) : , } "
kd_str = f " { r [ ' kd ' ] : .2f } "
maps_str = f " { int ( r [ ' maps ' ] ) } "
kills_str = f " { int ( r [ ' kills ' ] ) } "
lines . append ( f " { r [ ' rank ' ] : <5 } { r [ ' name ' ] : <18 } { r [ ' team ' ] : <12 } { score_str : <10 } { kd_str : <6 } { maps_str : <6 } { kills_str : <8 } " )
2026-04-23 09:24:48 +00:00
text = " \n " . join ( lines )
win = tk . Toplevel ( self . root )
2026-04-24 06:30:50 +00:00
win . title ( " Rookie Standings " )
win . geometry ( " 800x550 " )
2026-04-23 09:24:48 +00:00
2026-04-24 06:30:50 +00:00
output = tk . Text ( win , wrap = " none " , font = ( " Courier " , 10 ) )
2026-04-23 09:24:48 +00:00
output . pack ( expand = True , fill = " both " , padx = 10 , pady = 10 )
output . insert ( tk . END , text )
2026-04-24 06:30:50 +00:00
output . config ( state = " disabled " )
2026-04-23 09:24:48 +00:00
2026-04-08 08:13:02 +00:00
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 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 len ( teamA_players ) == 0 or len ( teamB_players ) == 0 :
print ( " OBS export skipped: no selected players for one or both teams. " )
return
2026-04-10 08:08:47 +00:00
# Define names for identity block
teamA_name = team_a
teamB_name = team_b
2026-04-08 08:13:02 +00:00
# --- STORYLINE + TEAM IDENTITY ---
try :
storyline_text = matchup_storyline ( teamA_players , teamB_players )
2026-04-10 08:08:47 +00:00
identity_data_a = generate_team_identity_block ( teamA_players , teamA_name )
identity_data_b = generate_team_identity_block ( teamB_players , teamB_name )
identity_text_a = format_team_identity ( identity_data_a )
identity_text_b = format_team_identity ( identity_data_b )
full_story = (
storyline_text
+ f " \n \n TEAM IDENTITY SUMMARY — { teamA_name } \n "
+ identity_text_a
+ f " \n \n TEAM IDENTITY SUMMARY — { teamB_name } \n "
+ identity_text_b
)
2026-04-08 08:13:02 +00:00
2026-04-09 10:11:39 +00:00
write_text ( os . path . join ( OBS_EXPORT_DIR , " storyline.txt " ) , full_story )
2026-04-10 08:08:47 +00:00
2026-04-08 08:13:02 +00:00
except Exception as e :
print ( " Storyline export failed: " , e )
2026-04-10 08:08:47 +00:00
# --- PREDICTIONS ---
2026-04-08 08:13:02 +00:00
try :
if hasattr ( self , " generate_predictions " ) :
predictions = self . generate_predictions ( teamA_players , teamB_players )
2026-04-10 08:08:47 +00:00
write_text ( os . path . join ( OBS_EXPORT_DIR , " predictions.txt " ) , predictions )
2026-04-08 08:13:02 +00:00
except Exception as e :
print ( " Prediction export failed: " , e )
2026-04-13 07:56:38 +00:00
# --- PLAYER TAG EXPORTS ---
2026-04-08 08:13:02 +00:00
try :
2026-04-13 07:56:38 +00:00
for idx , p in enumerate ( self . current_match_players ) :
# p IS the enriched match player object
tags_text = self . generate_player_tags ( p , map_type )
2026-04-08 08:13:02 +00:00
2026-04-13 07:56:38 +00:00
slot_path = os . path . join ( PLAYERS_DIR , f " p { idx } " , " Tags.txt " )
write_text ( slot_path , tags_text )
2026-04-10 08:08:47 +00:00
2026-04-08 08:13:02 +00:00
except Exception as e :
print ( " Tag export failed: " , e )
2026-04-10 08:08:47 +00:00
print ( f " OBS files updated for { map_type } " )
2026-04-09 10:11:39 +00:00
def refresh_output_dirs ( self ) :
global STATS_DIR , PLAYERS_DIR , OBS_EXPORT_DIR
STATS_DIR = self . output_root
PLAYERS_DIR = os . path . join ( STATS_DIR , " players " )
OBS_EXPORT_DIR = os . path . join ( STATS_DIR , " obs_exports " )
os . makedirs ( PLAYERS_DIR , exist_ok = True )
os . makedirs ( OBS_EXPORT_DIR , exist_ok = True )
def choose_output_folder ( self ) :
folder = filedialog . askdirectory ( title = " Select output folder for OBS + players " )
if not folder :
return
self . output_root = os . path . abspath ( folder )
config [ " output_root " ] = self . output_root
self . refresh_output_dirs ( )
with open ( CONFIG_PATH , " w " , encoding = " utf-8 " ) as f :
json . dump ( config , f , indent = 4 )
messagebox . showinfo (
" Output Folder Set " ,
f " Output folder set to: \n { self . output_root } \n \n "
" Point OBS to this folder for players and exports. "
)
2026-04-10 08:08:47 +00:00
def on_match_prediction ( 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 Team A and Team B. " )
return
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 not teamA_players or not teamB_players :
messagebox . showerror ( " Error " , " No players selected for one or both teams. " )
return
prediction_text = generate_prediction ( teamA_players , teamB_players , team_a , team_b )
win = tk . Toplevel ( self . root )
win . title ( " Match Prediction " )
win . geometry ( " 600x600 " )
output = tk . Text ( win , wrap = " word " )
output . pack ( expand = True , fill = " both " , padx = 10 , pady = 10 )
output . insert ( tk . END , prediction_text )
def export ( ) :
content = output . get ( " 1.0 " , tk . END ) . strip ( )
if not content :
messagebox . showerror ( " Error " , " No prediction text to export. " )
return
path = export_to_obs ( " match_prediction.txt " , content )
messagebox . showinfo ( " Exported " , f " Prediction exported to: \n { path } " )
ttk . Button ( win , text = " Export to OBS " , command = export ) . pack ( pady = 5 )
def on_export_obs_all ( 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 Team A and Team B. " )
return
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 not teamA_players or not teamB_players :
messagebox . showerror ( " Error " , " No players selected for one or both teams. " )
return
output_folder = os . path . join ( os . getcwd ( ) , " obs_output " )
export_all_obs ( output_folder , teamA_players , teamB_players , team_a , team_b )
messagebox . showinfo ( " Exported " , f " OBS files exported to: \n { output_folder } " )
2026-04-14 07:32:50 +00:00
2026-04-09 10:11:39 +00:00
2026-04-08 08:13:02 +00:00
def main ( ) :
root = tk . Tk ( )
app = DashLeagueGUI ( root )
root . mainloop ( )
if __name__ == " __main__ " :
main ( )