Initial commit: pre match-history refactor
This commit is contained in:
commit
2e81cbb34f
378 changed files with 118860 additions and 0 deletions
157
README.txt
Normal file
157
README.txt
Normal 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
0
__init__.py
Normal file
BIN
__pycache__/match_engine.cpython-313.pyc
Normal file
BIN
__pycache__/match_engine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/rankings.cpython-313.pyc
Normal file
BIN
__pycache__/rankings.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/storylines.cpython-313.pyc
Normal file
BIN
__pycache__/storylines.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/team_identity.cpython-313.pyc
Normal file
BIN
__pycache__/team_identity.cpython-313.pyc
Normal file
Binary file not shown.
82
analysis/rankings.py
Normal file
82
analysis/rankings.py
Normal 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
38
analysis/rankings1.py
Normal 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
54
analysis/storylines.py
Normal 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
29
analysis/storylines1.py
Normal 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
77
analysis/team_identity.py
Normal 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"]
|
||||
}
|
||||
46
analysis/team_identity1.py
Normal file
46
analysis/team_identity1.py
Normal 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
111535
career_stats.json
Normal file
File diff suppressed because it is too large
Load diff
49
config.json
Normal file
49
config.json
Normal 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
829
dashleague_cast_tool.py
Normal 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
0
data/__init__.py
Normal file
BIN
data/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
data/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
data/__pycache__/career_db.cpython-313.pyc
Normal file
BIN
data/__pycache__/career_db.cpython-313.pyc
Normal file
Binary file not shown.
BIN
data/__pycache__/match_cache.cpython-313.pyc
Normal file
BIN
data/__pycache__/match_cache.cpython-313.pyc
Normal file
Binary file not shown.
BIN
data/__pycache__/player_history.cpython-313.pyc
Normal file
BIN
data/__pycache__/player_history.cpython-313.pyc
Normal file
Binary file not shown.
BIN
data/__pycache__/season_cache.cpython-313.pyc
Normal file
BIN
data/__pycache__/season_cache.cpython-313.pyc
Normal file
Binary file not shown.
1
data/cache/players/00bc107a-8e56-4699-8b1d-523e68499c64_matches.json
vendored
Normal file
1
data/cache/players/00bc107a-8e56-4699-8b1d-523e68499c64_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0165ef8c-4eac-491c-ab20-29f8d4227a75_matches.json
vendored
Normal file
1
data/cache/players/0165ef8c-4eac-491c-ab20-29f8d4227a75_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/030d4a73-96a4-4ba0-90b0-1c44e76baad5_matches.json
vendored
Normal file
1
data/cache/players/030d4a73-96a4-4ba0-90b0-1c44e76baad5_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/03226580-d383-43ed-a7dd-8dea9ca10c10_matches.json
vendored
Normal file
1
data/cache/players/03226580-d383-43ed-a7dd-8dea9ca10c10_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0457b5ac-a0b5-41dd-ad39-94fe5bdbd45e_matches.json
vendored
Normal file
1
data/cache/players/0457b5ac-a0b5-41dd-ad39-94fe5bdbd45e_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/04a62b9e-e513-4acf-83d8-3c9422283831_matches.json
vendored
Normal file
1
data/cache/players/04a62b9e-e513-4acf-83d8-3c9422283831_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/06ac2449-5266-4046-b768-253909702662_matches.json
vendored
Normal file
1
data/cache/players/06ac2449-5266-4046-b768-253909702662_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/08b194d5-3dfb-4516-abef-d324190722fe_matches.json
vendored
Normal file
1
data/cache/players/08b194d5-3dfb-4516-abef-d324190722fe_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/09781aa5-bc82-4ed3-908d-3b172ad5dbed_matches.json
vendored
Normal file
1
data/cache/players/09781aa5-bc82-4ed3-908d-3b172ad5dbed_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0aee07b7-d5b3-41e9-9ebb-13f106b5a778_matches.json
vendored
Normal file
1
data/cache/players/0aee07b7-d5b3-41e9-9ebb-13f106b5a778_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0bcd3bf7-2197-4b00-87cf-195934cb779d_matches.json
vendored
Normal file
1
data/cache/players/0bcd3bf7-2197-4b00-87cf-195934cb779d_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0cd4e541-df32-4140-a942-090fe646e768_matches.json
vendored
Normal file
1
data/cache/players/0cd4e541-df32-4140-a942-090fe646e768_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0d02fa3b-98e2-4c86-be46-b0eca6d3f3cb_matches.json
vendored
Normal file
1
data/cache/players/0d02fa3b-98e2-4c86-be46-b0eca6d3f3cb_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0d2d2d69-d2ed-4ca0-bef9-72cf28c4ae6c_matches.json
vendored
Normal file
1
data/cache/players/0d2d2d69-d2ed-4ca0-bef9-72cf28c4ae6c_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0d67320d-a31d-47c1-85d0-cc33a0e65579_matches.json
vendored
Normal file
1
data/cache/players/0d67320d-a31d-47c1-85d0-cc33a0e65579_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0e2e6e7f-94dd-4285-a4c3-9b4b8063824d_matches.json
vendored
Normal file
1
data/cache/players/0e2e6e7f-94dd-4285-a4c3-9b4b8063824d_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0e7eb1b9-03c7-41a6-b211-3ada1d85935c_matches.json
vendored
Normal file
1
data/cache/players/0e7eb1b9-03c7-41a6-b211-3ada1d85935c_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/0f91f4c1-46b6-4062-bef4-b736496bc6be_matches.json
vendored
Normal file
1
data/cache/players/0f91f4c1-46b6-4062-bef4-b736496bc6be_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/101c6fd7-4e80-4656-86df-03255d65c307_matches.json
vendored
Normal file
1
data/cache/players/101c6fd7-4e80-4656-86df-03255d65c307_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/10c10be0-353c-456a-96f5-d333e6dd128f_matches.json
vendored
Normal file
1
data/cache/players/10c10be0-353c-456a-96f5-d333e6dd128f_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/110af0b2-ba2f-417c-8279-dc5d0fab7cb2_matches.json
vendored
Normal file
1
data/cache/players/110af0b2-ba2f-417c-8279-dc5d0fab7cb2_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/124d0ff5-084d-40d9-96fb-97fddb2d5cbb_matches.json
vendored
Normal file
1
data/cache/players/124d0ff5-084d-40d9-96fb-97fddb2d5cbb_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/12896c21-b9bc-45c7-a26b-4848ce1db13b_matches.json
vendored
Normal file
1
data/cache/players/12896c21-b9bc-45c7-a26b-4848ce1db13b_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/129c2178-dd18-4ed7-b75e-74f7e7aeae6d_matches.json
vendored
Normal file
1
data/cache/players/129c2178-dd18-4ed7-b75e-74f7e7aeae6d_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/13503f5d-e066-4c90-ab3e-ef74f83948c9_matches.json
vendored
Normal file
1
data/cache/players/13503f5d-e066-4c90-ab3e-ef74f83948c9_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/13984231-c8a5-4a2a-a0bc-ed87647ef59e_matches.json
vendored
Normal file
1
data/cache/players/13984231-c8a5-4a2a-a0bc-ed87647ef59e_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/152180fe-ad8b-4c63-a7be-b2d429c943e6_matches.json
vendored
Normal file
1
data/cache/players/152180fe-ad8b-4c63-a7be-b2d429c943e6_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1612275e-e155-4054-96e0-4bd0646bd68e_matches.json
vendored
Normal file
1
data/cache/players/1612275e-e155-4054-96e0-4bd0646bd68e_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/16333798-37d3-4f72-a0b6-113550983880_matches.json
vendored
Normal file
1
data/cache/players/16333798-37d3-4f72-a0b6-113550983880_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/16ed3312-d5ac-491b-99a3-6013d11144ca_matches.json
vendored
Normal file
1
data/cache/players/16ed3312-d5ac-491b-99a3-6013d11144ca_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/180cc34a-48ee-43c8-a176-85da9c5ed978_matches.json
vendored
Normal file
1
data/cache/players/180cc34a-48ee-43c8-a176-85da9c5ed978_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/18c4c319-6341-4cfb-a9be-6f66cee6f428_matches.json
vendored
Normal file
1
data/cache/players/18c4c319-6341-4cfb-a9be-6f66cee6f428_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/19a6fc83-394b-45ff-919e-579c7fec7675_matches.json
vendored
Normal file
1
data/cache/players/19a6fc83-394b-45ff-919e-579c7fec7675_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/19b97f37-d1b6-4083-ba14-b99095933146_matches.json
vendored
Normal file
1
data/cache/players/19b97f37-d1b6-4083-ba14-b99095933146_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1a20546f-870d-47e5-80b7-63b78b2fb819_matches.json
vendored
Normal file
1
data/cache/players/1a20546f-870d-47e5-80b7-63b78b2fb819_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1a6f28c8-6a9b-42b0-959c-ae4424373d34_matches.json
vendored
Normal file
1
data/cache/players/1a6f28c8-6a9b-42b0-959c-ae4424373d34_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1aa9d2e3-22cb-43d4-805d-8d11836874ce_matches.json
vendored
Normal file
1
data/cache/players/1aa9d2e3-22cb-43d4-805d-8d11836874ce_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1b6897d2-c5cb-4ecf-88fe-f8f584aff793_matches.json
vendored
Normal file
1
data/cache/players/1b6897d2-c5cb-4ecf-88fe-f8f584aff793_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1bf77d12-04f1-4037-ade3-b720a785778a_matches.json
vendored
Normal file
1
data/cache/players/1bf77d12-04f1-4037-ade3-b720a785778a_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1c058009-5bf8-4ed2-b32e-392bd3d9aac9_matches.json
vendored
Normal file
1
data/cache/players/1c058009-5bf8-4ed2-b32e-392bd3d9aac9_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1c2d7a81-e4f2-42fd-bead-99ef20a476c9_matches.json
vendored
Normal file
1
data/cache/players/1c2d7a81-e4f2-42fd-bead-99ef20a476c9_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1d2b826c-0134-4584-828e-295521f15609_matches.json
vendored
Normal file
1
data/cache/players/1d2b826c-0134-4584-828e-295521f15609_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1d3d534b-5147-4f6e-a908-14c44d316778_matches.json
vendored
Normal file
1
data/cache/players/1d3d534b-5147-4f6e-a908-14c44d316778_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1d572b54-5d02-40fa-9a95-d70fbfb19e82_matches.json
vendored
Normal file
1
data/cache/players/1d572b54-5d02-40fa-9a95-d70fbfb19e82_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1f3b424c-936d-46d9-8dde-3817125c47bd_matches.json
vendored
Normal file
1
data/cache/players/1f3b424c-936d-46d9-8dde-3817125c47bd_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1fe27770-56a8-4548-bc73-e99c1762e696_matches.json
vendored
Normal file
1
data/cache/players/1fe27770-56a8-4548-bc73-e99c1762e696_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1fe8e121-7902-4081-bbff-77c35203d837_matches.json
vendored
Normal file
1
data/cache/players/1fe8e121-7902-4081-bbff-77c35203d837_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/1ff4db15-792f-439c-98e0-c40e54f06627_matches.json
vendored
Normal file
1
data/cache/players/1ff4db15-792f-439c-98e0-c40e54f06627_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2038d932-8e7e-497d-8b60-5e36bf32da95_matches.json
vendored
Normal file
1
data/cache/players/2038d932-8e7e-497d-8b60-5e36bf32da95_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/207903a0-740b-49c8-a8e1-9e4c0545200c_matches.json
vendored
Normal file
1
data/cache/players/207903a0-740b-49c8-a8e1-9e4c0545200c_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2248c049-950d-428e-a962-44372c69b0c0_matches.json
vendored
Normal file
1
data/cache/players/2248c049-950d-428e-a962-44372c69b0c0_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2333718e-5867-473c-8ad6-18b1475e62ce_matches.json
vendored
Normal file
1
data/cache/players/2333718e-5867-473c-8ad6-18b1475e62ce_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/23894346-a051-4a96-a2dc-8903d07a5a41_matches.json
vendored
Normal file
1
data/cache/players/23894346-a051-4a96-a2dc-8903d07a5a41_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/24fe4134-a7c8-4f7a-993c-8fc7f58da4ca_matches.json
vendored
Normal file
1
data/cache/players/24fe4134-a7c8-4f7a-993c-8fc7f58da4ca_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/26f76448-e7c0-45b7-a73a-4fa0960ac7b5_matches.json
vendored
Normal file
1
data/cache/players/26f76448-e7c0-45b7-a73a-4fa0960ac7b5_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/271efae0-c29b-4b2b-bb0d-c9494e06215b_matches.json
vendored
Normal file
1
data/cache/players/271efae0-c29b-4b2b-bb0d-c9494e06215b_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/271f01d1-e4ff-4c57-9fbc-7358b358264f_matches.json
vendored
Normal file
1
data/cache/players/271f01d1-e4ff-4c57-9fbc-7358b358264f_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/276d62b9-ee20-43ba-bc55-9d2e6deb6a68_matches.json
vendored
Normal file
1
data/cache/players/276d62b9-ee20-43ba-bc55-9d2e6deb6a68_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/286c5cf2-4e7e-4a03-af88-0203e59cf3f3_matches.json
vendored
Normal file
1
data/cache/players/286c5cf2-4e7e-4a03-af88-0203e59cf3f3_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2b9fd651-b8d1-4729-a3f6-f63adcbc5442_matches.json
vendored
Normal file
1
data/cache/players/2b9fd651-b8d1-4729-a3f6-f63adcbc5442_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2cb7cd71-45a2-4317-9fba-48b349ed88eb_matches.json
vendored
Normal file
1
data/cache/players/2cb7cd71-45a2-4317-9fba-48b349ed88eb_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2e52161a-3eec-433c-baa8-2f664ac5e027_matches.json
vendored
Normal file
1
data/cache/players/2e52161a-3eec-433c-baa8-2f664ac5e027_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2ef71bbd-390a-47d4-b5f5-5219bce84ee0_matches.json
vendored
Normal file
1
data/cache/players/2ef71bbd-390a-47d4-b5f5-5219bce84ee0_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2f4740e2-320c-474f-8a55-bf01debc27a5_matches.json
vendored
Normal file
1
data/cache/players/2f4740e2-320c-474f-8a55-bf01debc27a5_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2f63dc1c-a1fa-48a5-863f-5c3ea92991f8_matches.json
vendored
Normal file
1
data/cache/players/2f63dc1c-a1fa-48a5-863f-5c3ea92991f8_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/2f9cb49b-df5d-48c0-9178-64e53b7a9c28_matches.json
vendored
Normal file
1
data/cache/players/2f9cb49b-df5d-48c0-9178-64e53b7a9c28_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/31e7b9ea-529c-4f87-a716-6f4ef4fd5673_matches.json
vendored
Normal file
1
data/cache/players/31e7b9ea-529c-4f87-a716-6f4ef4fd5673_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/31eaabe5-dcf0-42bb-b8cd-273139ab8b8b_matches.json
vendored
Normal file
1
data/cache/players/31eaabe5-dcf0-42bb-b8cd-273139ab8b8b_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/327dad86-c445-45df-af55-18fc866d6038_matches.json
vendored
Normal file
1
data/cache/players/327dad86-c445-45df-af55-18fc866d6038_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/33b942b9-b326-4b40-a353-1cc56cfd22c8_matches.json
vendored
Normal file
1
data/cache/players/33b942b9-b326-4b40-a353-1cc56cfd22c8_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/34d97aca-ff13-4e27-b6c7-4203e3a1e287_matches.json
vendored
Normal file
1
data/cache/players/34d97aca-ff13-4e27-b6c7-4203e3a1e287_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/3508faac-5182-435a-9068-ef4a7ab2a1d1_matches.json
vendored
Normal file
1
data/cache/players/3508faac-5182-435a-9068-ef4a7ab2a1d1_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/352099ee-f1b3-4574-8da8-956cdbbf7759_matches.json
vendored
Normal file
1
data/cache/players/352099ee-f1b3-4574-8da8-956cdbbf7759_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/361deb12-087a-4655-81c5-a21e378073e7_matches.json
vendored
Normal file
1
data/cache/players/361deb12-087a-4655-81c5-a21e378073e7_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/3737f7b8-ed60-4b94-9499-e4a239ca1ec3_matches.json
vendored
Normal file
1
data/cache/players/3737f7b8-ed60-4b94-9499-e4a239ca1ec3_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/37dd698c-2285-46d7-a4f1-a25f04830aa7_matches.json
vendored
Normal file
1
data/cache/players/37dd698c-2285-46d7-a4f1-a25f04830aa7_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/3816d381-7e51-4268-b3fb-06ee28ab62ae_matches.json
vendored
Normal file
1
data/cache/players/3816d381-7e51-4268-b3fb-06ee28ab62ae_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/399ea0d2-65fc-461a-b6ae-7c140036f83c_matches.json
vendored
Normal file
1
data/cache/players/399ea0d2-65fc-461a-b6ae-7c140036f83c_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
1
data/cache/players/3b5df1ac-fa13-484e-9841-717dc2e897b6_matches.json
vendored
Normal file
1
data/cache/players/3b5df1ac-fa13-484e-9841-717dc2e897b6_matches.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue