Modernize tag system, match engine, OBS exports, and output folder architecture
This commit is contained in:
parent
5c055ebee6
commit
c8f043155a
23 changed files with 326883 additions and 110282 deletions
30
.gitignore
vendored
30
.gitignore
vendored
|
|
@ -1,24 +1,12 @@
|
|||
# Python
|
||||
# Runtime / generated data
|
||||
../stats/
|
||||
stats/
|
||||
players/
|
||||
obs_exports/
|
||||
*.txt
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.env
|
||||
.venv
|
||||
venv/
|
||||
env/
|
||||
|
||||
# VSCode / IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Our caches
|
||||
stats/data/cache/
|
||||
cache/
|
||||
|
||||
# Ignore OBS output folder
|
||||
../stats/
|
||||
*.db
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
435672
career_stats.json
435672
career_stats.json
File diff suppressed because it is too large
Load diff
95
config.json
95
config.json
|
|
@ -1,49 +1,50 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
"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"
|
||||
},
|
||||
"output_root": "C:\\Projects\\Casting\\stats"
|
||||
}
|
||||
|
|
@ -2,32 +2,62 @@ import os
|
|||
import json
|
||||
import math
|
||||
from collections import defaultdict
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
|
||||
import requests
|
||||
|
||||
# --- Internal modules ---
|
||||
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 tags.objective_domination import (
|
||||
compute_dom_objective_for_career_player,
|
||||
compute_dom_objective_for_match_player,
|
||||
compute_dom_objective_team_scores,
|
||||
)
|
||||
|
||||
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_sharpshooter,
|
||||
rank_match_players_clutch,
|
||||
split_two_columns,
|
||||
compute_slayer_prediction,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# PATHS + CONFIG
|
||||
# ---------------------------------------------------------
|
||||
|
||||
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")
|
||||
|
||||
# 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)
|
||||
os.makedirs(OBS_EXPORT_DIR, exist_ok=True)
|
||||
|
||||
API_BASE = "https://dashleague.games/api/v1"
|
||||
|
|
@ -120,6 +150,13 @@ class DashLeagueGUI:
|
|||
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.output_folder_button = ttk.Button(
|
||||
top_frame,
|
||||
text="Select Output Folder",
|
||||
command=self.choose_output_folder
|
||||
)
|
||||
self.output_folder_button.grid(row=0, column=6, padx=(0, 10))
|
||||
|
||||
self.map_var = tk.StringVar()
|
||||
self.map_var.set("Payload") # default
|
||||
|
||||
|
|
@ -213,11 +250,32 @@ class DashLeagueGUI:
|
|||
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 normalize_player(p):
|
||||
return {
|
||||
"id": p.get("PlayerUUID"),
|
||||
"name": p.get("PlayerGameName"),
|
||||
"team": p.get("TeamUUID"),
|
||||
"KD": p.get("KD", ""),
|
||||
"kills": p.get("Kills", ""),
|
||||
"deaths": p.get("Deaths", ""),
|
||||
"headshots": p.get("Headshots", ""),
|
||||
"accuracy": p.get("Accuracy", ""),
|
||||
"PAY_PushTime": p.get("PAY_PushTime", ""),
|
||||
"DOM_captures": p.get("DOM_Captures", ""),
|
||||
"DOM_counters": p.get("DOM_Counters", ""),
|
||||
}
|
||||
|
||||
|
||||
|
||||
def on_generate_slots(self):
|
||||
try:
|
||||
ensure_player_slots()
|
||||
|
||||
# -----------------------------
|
||||
# 1. Collect selected players
|
||||
# -----------------------------
|
||||
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()]
|
||||
|
||||
|
|
@ -226,34 +284,128 @@ class DashLeagueGUI:
|
|||
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", ""))
|
||||
# Sort and combine
|
||||
all_players = (
|
||||
sorted(selected_a, key=lambda x: x.get("name", "")) +
|
||||
sorted(selected_b, key=lambda x: x.get("name", ""))
|
||||
)
|
||||
|
||||
# Limit to 10
|
||||
all_players = all_players[:10]
|
||||
|
||||
# -----------------------------
|
||||
# 2. Normalize keys BEFORE attaching tags
|
||||
# -----------------------------
|
||||
for p in all_players:
|
||||
# Normalize stat keys to match DB/tag expectations
|
||||
p["CP_Captures"] = p.get("CP_captures", 0)
|
||||
p["DOM_Captures"] = p.get("DOM_captures", 0)
|
||||
p["DOM_Counters"] = p.get("DOM_counters", 0)
|
||||
|
||||
# Save match players
|
||||
self.current_match_players = all_players
|
||||
|
||||
# Debug
|
||||
if self.current_match_players:
|
||||
print("DEBUG MATCH PLAYER:", self.current_match_players[0])
|
||||
else:
|
||||
print("DEBUG MATCH PLAYER: EMPTY LIST")
|
||||
|
||||
# -----------------------------
|
||||
# 3. Load career DB
|
||||
# -----------------------------
|
||||
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
|
||||
|
||||
# -----------------------------
|
||||
# 4. Attach tag components
|
||||
# -----------------------------
|
||||
if career_db is not None:
|
||||
for p in all_players:
|
||||
pid = p.get("id")
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 4b. Compute match-based ObjDOM
|
||||
# -----------------------------
|
||||
from tags.objective_domination import compute_dom_objective_team_scores
|
||||
|
||||
team_entries = []
|
||||
for p in all_players:
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
entry = career_db["players"].get(pid, {})
|
||||
team_entries.append((pid, entry, p))
|
||||
|
||||
# Compute match ObjDOM scores
|
||||
dom_scores = compute_dom_objective_team_scores(team_entries)
|
||||
|
||||
# Attach to each player
|
||||
for pid, entry, mp in team_entries:
|
||||
score = dom_scores.get(pid, 0.0)
|
||||
mp["objective_domination_components"] = {
|
||||
"dom_score_raw": score,
|
||||
"dom_score_display": f"{score:.2f}",
|
||||
}
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 4c. Compute match-based ObjPL
|
||||
# -----------------------------
|
||||
from tags.objective_payload import compute_payload_objective_team_scores
|
||||
|
||||
pl_team_entries = []
|
||||
for p in all_players:
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
entry = career_db["players"].get(pid, {}) if career_db else {}
|
||||
pl_team_entries.append((pid, entry, p))
|
||||
|
||||
if pl_team_entries:
|
||||
pl_scores = compute_payload_objective_team_scores(pl_team_entries)
|
||||
|
||||
for pid, entry, mp in pl_team_entries:
|
||||
score = pl_scores.get(pid, 0.0)
|
||||
mp["objective_payload_components"] = {
|
||||
"payload_score_raw": score,
|
||||
"payload_score_display": f"{score:.2f}",
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# 5. Write OBS slot files
|
||||
# -----------------------------
|
||||
for idx, p in enumerate(all_players):
|
||||
slot_dir = os.path.join(PLAYERS_DIR, f"p{idx}")
|
||||
os.makedirs(slot_dir, exist_ok=True)
|
||||
|
|
@ -267,24 +419,21 @@ class DashLeagueGUI:
|
|||
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", ""))
|
||||
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)
|
||||
# Tag exports (use enriched match player dict)
|
||||
map_type = self.map_var.get()
|
||||
tags_text = self.generate_player_tags(p, 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):
|
||||
|
|
@ -344,7 +493,7 @@ class DashLeagueGUI:
|
|||
text = f"""PLAYER SPOTLIGHT — {name}
|
||||
|
||||
Slayer Tier: {slayer_strength}
|
||||
Payload Objective Specialist: {obj_pl:.2f} (if not None)
|
||||
Payload Objective Specialist: {obj_pl if obj_pl is not None else "N/A"}
|
||||
|
||||
Career Stats:
|
||||
KD: {career['KD']:.2f}
|
||||
|
|
@ -474,23 +623,24 @@ PushTime/Season: {derived.get('push_time_per_season', 0):.2f}
|
|||
if slayer_strength:
|
||||
tags_out.append(slayer_strength)
|
||||
|
||||
# Payload Objective Specialist
|
||||
# Payload Objective Specialist (match-based)
|
||||
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}")
|
||||
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}")
|
||||
|
||||
# Domination Specialist
|
||||
# Domination Objective Specialist (match-based)
|
||||
if map_type == "Domination":
|
||||
obj_dom = compute_dom_objective(player_entry)
|
||||
tags_out.append(f"ObjDOM {obj_dom:.2f}")
|
||||
dom_info = player_entry.get("objective_domination_components", {})
|
||||
dom_score = dom_info.get("dom_score_raw", 0.0)
|
||||
tags_out.append(f"ObjDOM {dom_score:.2f}")
|
||||
|
||||
# Sharpshooter
|
||||
sharp = player_entry.get("sharpshooter_score")
|
||||
if sharp is not None:
|
||||
tags_out.append(f"Sharp {sharp:.2f}")
|
||||
|
||||
return ", ".join(tags_out)
|
||||
return ", ".join(tags_out)
|
||||
|
||||
|
||||
|
||||
|
|
@ -523,7 +673,7 @@ PushTime/Season: {derived.get('push_time_per_season', 0):.2f}
|
|||
|
||||
# --- Export to OBS ---
|
||||
try:
|
||||
with open("obs_exports/storyline.txt", "w", encoding="utf-8") as f:
|
||||
with open(os.path.join(OBS_EXPORT_DIR, "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}")
|
||||
|
|
@ -786,7 +936,7 @@ PushTime/Season: {derived.get('push_time_per_season', 0):.2f}
|
|||
|
||||
full_story = storyline_text + "\n\nTEAM IDENTITY SUMMARY\n" + identity_text
|
||||
|
||||
write_text(os.path.join("obs_exports", "storyline.txt"), full_story)
|
||||
write_text(os.path.join(OBS_EXPORT_DIR, "storyline.txt"), full_story)
|
||||
except Exception as e:
|
||||
print("Storyline export failed:", e)
|
||||
|
||||
|
|
@ -819,6 +969,35 @@ PushTime/Season: {derived.get('push_time_per_season', 0):.2f}
|
|||
print(f"OBS files updated for {map_type}")
|
||||
|
||||
|
||||
from tkinter import filedialog, messagebox
|
||||
|
||||
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."
|
||||
)
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = DashLeagueGUI(root)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,221 +1,120 @@
|
|||
import os
|
||||
import json
|
||||
from collections import defaultdict
|
||||
import requests
|
||||
|
||||
CACHE_DIR = os.path.join(os.path.dirname(__file__), "cache")
|
||||
SEASON_CACHE_DIR = os.path.join(CACHE_DIR, "seasons")
|
||||
|
||||
os.makedirs(SEASON_CACHE_DIR, exist_ok=True)
|
||||
import data.db_access as db_access
|
||||
|
||||
from tags.slayer import compute_slayer_for_career_player
|
||||
from tags.objective_payload import compute_payload_objective_for_career_player
|
||||
from tags.sharpshooter import compute_sharpshooter_for_career_player
|
||||
from tags.consistency import compute_consistency
|
||||
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
API_BASE = "https://dashleague.games/api/v1"
|
||||
|
||||
|
||||
def fetch_json(url):
|
||||
resp = requests.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
|
||||
def detect_valid_seasons(max_season=50):
|
||||
valid = []
|
||||
for season in range(0, max_season + 1):
|
||||
try:
|
||||
url = f"{API_BASE}/stats?season={season}"
|
||||
resp = requests.get(url, timeout=10)
|
||||
if resp.status_code != 200:
|
||||
continue
|
||||
data = resp.json()
|
||||
season_stats = data.get("data", [])
|
||||
if isinstance(season_stats, list) and len(season_stats) > 0:
|
||||
valid.append(season)
|
||||
except Exception:
|
||||
continue
|
||||
return valid
|
||||
|
||||
|
||||
def fetch_season_stats(season):
|
||||
url = f"{API_BASE}/stats?season={season}"
|
||||
data = fetch_json(url)
|
||||
return data.get("data", [])
|
||||
from tags.clutch import compute_clutch
|
||||
|
||||
|
||||
def build_career_database(output_path):
|
||||
seasons = detect_valid_seasons()
|
||||
if not seasons:
|
||||
print("No valid seasons found. Aborting career DB build.")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 1. Load all players from the local SQLite DB
|
||||
# ---------------------------------------------------------
|
||||
player_rows = db_access.query("SELECT PlayerUUID, PlayerGameName FROM players;")
|
||||
|
||||
if not player_rows:
|
||||
print("No players found in local DB.")
|
||||
return False
|
||||
|
||||
players = {}
|
||||
league_accumulator = defaultdict(float)
|
||||
league_counts = defaultdict(int)
|
||||
final_db = {"players": {}, "league_averages": {}}
|
||||
|
||||
for season in seasons:
|
||||
season_stats = fetch_season_stats(season)
|
||||
if not isinstance(season_stats, list):
|
||||
# ---------------------------------------------------------
|
||||
# 2. Build per-player career stats from local DB
|
||||
# ---------------------------------------------------------
|
||||
for row in player_rows:
|
||||
pid = row["PlayerUUID"]
|
||||
name = row.get("PlayerGameName", "Unknown")
|
||||
|
||||
# Pull all matches from SQLite
|
||||
matches = db_access.get_player_match_history(pid)
|
||||
|
||||
if not matches:
|
||||
# Skip players with no match history
|
||||
continue
|
||||
|
||||
for entry in season_stats:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
|
||||
# Normalize inconsistent keys
|
||||
if "loss" in entry and "losses" not in entry:
|
||||
entry["losses"] = entry["loss"]
|
||||
# Aggregate raw career totals
|
||||
career_raw = defaultdict(float)
|
||||
|
||||
if "shots" not in entry:
|
||||
entry["shots"] = 0
|
||||
for m in matches:
|
||||
career_raw["kills"] += m["Kills"]
|
||||
career_raw["deaths"] += m["Deaths"]
|
||||
career_raw["damage"] += m["Damage"]
|
||||
career_raw["shots"] += m["Shots"]
|
||||
career_raw["shots_hit"] += m["ShotsHit"]
|
||||
career_raw["headshots"] += m["Headshots"]
|
||||
career_raw["PAY_PushTime"] += m["PAY_PushTime"]
|
||||
career_raw["DOM_captures"] += m["DOM_Captures"]
|
||||
career_raw["DOM_counters"] += m["DOM_Counters"]
|
||||
career_raw["maps"] += 1 # each match = 1 map for now
|
||||
|
||||
if "shots_hit" not in entry:
|
||||
entry["shots_hit"] = 0
|
||||
|
||||
if "headshots" not in entry:
|
||||
entry["headshots"] = 0
|
||||
|
||||
if "damage" not in entry:
|
||||
entry["damage"] = 0
|
||||
|
||||
if "maps" not in entry or entry["maps"] in (None, "Coming Soon!"):
|
||||
entry["maps"] = 0
|
||||
|
||||
if "accuracy" not in entry or isinstance(entry["accuracy"], str):
|
||||
entry["accuracy"] = 0.0
|
||||
|
||||
pid = entry.get("id")
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
if pid not in players:
|
||||
players[pid] = {
|
||||
"name": entry.get("name", "Unknown"),
|
||||
"team_history": set(),
|
||||
"seasons_played": set(),
|
||||
"per_season": {},
|
||||
"career_raw": defaultdict(float)
|
||||
}
|
||||
|
||||
p = players[pid]
|
||||
team = entry.get("team") or "No Team"
|
||||
p["team_history"].add(team)
|
||||
p["seasons_played"].add(season)
|
||||
p["per_season"][str(season)] = entry
|
||||
|
||||
for key in [
|
||||
"kills",
|
||||
"deaths",
|
||||
"headshots",
|
||||
"accuracy",
|
||||
"PAY_PushTime",
|
||||
"DOM_captures",
|
||||
"DOM_counters",
|
||||
"maps",
|
||||
"wins",
|
||||
"losses",
|
||||
"damage",
|
||||
"shots",
|
||||
"shots_hit",
|
||||
]:
|
||||
if key in entry:
|
||||
p["career_raw"][key] += entry.get(key, 0)
|
||||
|
||||
if "KD" in entry:
|
||||
league_accumulator["KD"] += entry["KD"]
|
||||
league_counts["KD"] += 1
|
||||
if "accuracy" in entry:
|
||||
league_accumulator["accuracy"] += entry["accuracy"]
|
||||
league_counts["accuracy"] += 1
|
||||
if "kills" in entry and "maps" in entry and entry["maps"] > 0:
|
||||
league_accumulator["kills_per_map"] += entry["kills"] / entry["maps"]
|
||||
league_counts["kills_per_map"] += 1
|
||||
if "damage" in entry and "maps" in entry and entry["maps"] > 0:
|
||||
league_accumulator["damage_per_map"] += entry["damage"] / entry["maps"]
|
||||
league_counts["damage_per_map"] += 1
|
||||
if "PAY_PushTime" in entry:
|
||||
league_accumulator["push_time_per_season"] += entry["PAY_PushTime"]
|
||||
league_counts["push_time_per_season"] += 1
|
||||
|
||||
league_averages = {}
|
||||
for key in league_accumulator:
|
||||
league_averages[key] = league_accumulator[key] / max(1, league_counts[key])
|
||||
|
||||
final_db = {"players": {}, "league_averages": league_averages}
|
||||
|
||||
for pid, pdata in players.items():
|
||||
|
||||
# 🔥 Skip players with no valid stats
|
||||
if "career_raw" not in pdata or not pdata["career_raw"]:
|
||||
continue
|
||||
|
||||
# print("DEBUG PLAYER:", pid, pdata["career_raw"])
|
||||
|
||||
raw = pdata["career_raw"]
|
||||
|
||||
# 🔥 Ensure maps always exists
|
||||
if "maps" not in raw:
|
||||
raw["maps"] = 0
|
||||
|
||||
maps = raw["maps"] if raw["maps"] > 0 else 1
|
||||
seasons_played = len(pdata["seasons_played"])
|
||||
|
||||
kills_per_map = raw["kills"] / maps
|
||||
deaths_per_map = raw["deaths"] / maps
|
||||
push_time_per_season = raw["PAY_PushTime"] / max(1, seasons_played)
|
||||
|
||||
KD = raw["kills"] / raw["deaths"] if raw["deaths"] > 0 else raw["kills"]
|
||||
accuracy = raw["accuracy"] / max(1, seasons_played)
|
||||
# Derived stats
|
||||
maps = max(1, career_raw["maps"])
|
||||
KD = career_raw["kills"] / career_raw["deaths"] if career_raw["deaths"] > 0 else career_raw["kills"]
|
||||
accuracy = (career_raw["shots_hit"] / career_raw["shots"]) if career_raw["shots"] > 0 else 0.0
|
||||
|
||||
derived = {
|
||||
"kills_per_map": kills_per_map,
|
||||
"deaths_per_map": deaths_per_map,
|
||||
"push_time_per_season": push_time_per_season,
|
||||
"kills_per_map": career_raw["kills"] / maps,
|
||||
"deaths_per_map": career_raw["deaths"] / maps,
|
||||
"push_time_per_season": career_raw["PAY_PushTime"], # no seasons now
|
||||
}
|
||||
|
||||
final_db["players"][pid] = {
|
||||
"name": pdata["name"],
|
||||
"team_history": list(pdata["team_history"]),
|
||||
"seasons_played": list(pdata["seasons_played"]),
|
||||
"per_season": pdata["per_season"],
|
||||
"name": name,
|
||||
"career": {
|
||||
"kills": raw["kills"],
|
||||
"deaths": raw["deaths"],
|
||||
"kills": career_raw["kills"],
|
||||
"deaths": career_raw["deaths"],
|
||||
"KD": KD,
|
||||
"accuracy": accuracy,
|
||||
"push_time": raw["PAY_PushTime"],
|
||||
"captures": raw["DOM_captures"],
|
||||
"counters": raw["DOM_counters"],
|
||||
"maps": raw["maps"],
|
||||
"wins": raw["wins"],
|
||||
"losses": raw["losses"],
|
||||
"damage": raw["damage"],
|
||||
"shots": raw.get("shots", 0),
|
||||
"shots_hit": raw.get("shots_hit", 0),
|
||||
"headshots": raw.get("headshots", 0),
|
||||
"damage_dealt": raw.get("damage", 0),
|
||||
"push_time": career_raw["PAY_PushTime"],
|
||||
"captures": career_raw["DOM_captures"],
|
||||
"counters": career_raw["DOM_counters"],
|
||||
"maps": career_raw["maps"],
|
||||
"damage": career_raw["damage"],
|
||||
"shots": career_raw["shots"],
|
||||
"shots_hit": career_raw["shots_hit"],
|
||||
"headshots": career_raw["headshots"],
|
||||
},
|
||||
"derived": derived,
|
||||
"matches": matches,
|
||||
}
|
||||
|
||||
from data.player_history import get_player_match_history
|
||||
|
||||
final_db["players"][pid]["matches"] = get_player_match_history(pid)
|
||||
# ---------------------------------------------------------
|
||||
# 3. Compute league averages (local-only)
|
||||
# ---------------------------------------------------------
|
||||
league_acc = defaultdict(float)
|
||||
league_count = defaultdict(int)
|
||||
|
||||
|
||||
|
||||
|
||||
# Compute all tag scores per player
|
||||
for pid, pdata in final_db["players"].items():
|
||||
c = pdata["career"]
|
||||
|
||||
# 🔥 Skip players missing a career block
|
||||
if "career" not in pdata:
|
||||
print("SKIPPING PLAYER WITH NO CAREER:", pid)
|
||||
continue
|
||||
league_acc["KD"] += c["KD"]
|
||||
league_count["KD"] += 1
|
||||
|
||||
league_acc["accuracy"] += c["accuracy"]
|
||||
league_count["accuracy"] += 1
|
||||
|
||||
league_acc["kills_per_map"] += pdata["derived"]["kills_per_map"]
|
||||
league_count["kills_per_map"] += 1
|
||||
|
||||
league_acc["damage"] += c["damage"]
|
||||
league_count["damage"] += 1
|
||||
|
||||
league_averages = {
|
||||
key: league_acc[key] / max(1, league_count[key])
|
||||
for key in league_acc
|
||||
}
|
||||
|
||||
final_db["league_averages"] = league_averages
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 4. Compute all tags
|
||||
# ---------------------------------------------------------
|
||||
for pid, pdata in final_db["players"].items():
|
||||
|
||||
# Slayer
|
||||
slayer_info = compute_slayer_for_career_player(pdata, league_averages)
|
||||
|
|
@ -224,34 +123,30 @@ def build_career_database(output_path):
|
|||
pdata["slayer_strength"] = slayer_info["strength"]
|
||||
|
||||
# Sharpshooter
|
||||
sharp = compute_sharpshooter_for_career_player(pdata["career"])
|
||||
pdata["sharpshooter"] = sharp
|
||||
pdata["sharpshooter"] = compute_sharpshooter_for_career_player(pdata["career"])
|
||||
|
||||
# Payload Objective Specialist
|
||||
obj_components = compute_payload_objective_for_career_player(pdata, league_averages)
|
||||
pdata["objective_payload_components"] = obj_components
|
||||
|
||||
|
||||
# --- Consistency Tag ---
|
||||
from tags.consistency import compute_consistency
|
||||
|
||||
matches = pdata.get("matches", [])
|
||||
pdata["objective_payload_components"] = compute_payload_objective_for_career_player(
|
||||
pdata, league_averages
|
||||
)
|
||||
|
||||
# Consistency
|
||||
matches = pdata["matches"]
|
||||
consistency_raw, components = compute_consistency(matches)
|
||||
|
||||
pdata["consistency_components"] = {
|
||||
**components,
|
||||
"consistency_raw": consistency_raw,
|
||||
"consistency_norm": components.get("consistency_norm", consistency_raw)
|
||||
"consistency_norm": components.get("consistency_norm", consistency_raw),
|
||||
}
|
||||
|
||||
# --- Clutch Tag ---
|
||||
from tags.clutch import compute_clutch
|
||||
|
||||
# Clutch
|
||||
pdata["clutch_components"] = {
|
||||
"clutch_norm": compute_clutch(pdata, league_averages)
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 5. Save DB
|
||||
# ---------------------------------------------------------
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
json.dump(final_db, f, indent=2)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
import requests
|
||||
|
||||
from data.season_cache import fetch_season_match_list, BASE as API_BASE
|
||||
from data.match_cache import fetch_match_details
|
||||
|
||||
CACHE_DIR = os.path.join(os.path.dirname(__file__), "cache")
|
||||
PLAYER_CACHE_DIR = os.path.join(CACHE_DIR, "players")
|
||||
SEASON_CACHE_DIR = os.path.join(CACHE_DIR, "seasons")
|
||||
|
||||
os.makedirs(PLAYER_CACHE_DIR, exist_ok=True)
|
||||
os.makedirs(SEASON_CACHE_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def _detect_cached_seasons():
|
||||
out = []
|
||||
for filename in os.listdir(SEASON_CACHE_DIR):
|
||||
if filename.endswith("_matches.json"):
|
||||
sid = filename.replace("_matches.json", "")
|
||||
if sid.isdigit():
|
||||
out.append(sid)
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_all_seasons_from_api():
|
||||
try:
|
||||
resp = requests.get(f"{API_BASE}/seasons", timeout=10)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except Exception as e:
|
||||
print("ERROR fetching seasons:", e)
|
||||
return []
|
||||
|
||||
|
||||
def get_player_match_history(player_id):
|
||||
"""Build full match history for a player using cached data."""
|
||||
|
||||
cache_path = os.path.join(PLAYER_CACHE_DIR, f"{player_id}_matches.json")
|
||||
|
||||
# --- Use cached player history if available ---
|
||||
if os.path.exists(cache_path):
|
||||
try:
|
||||
with open(cache_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print("ERROR reading cached player history:", e)
|
||||
|
||||
history = []
|
||||
|
||||
# --- Load cached seasons ---
|
||||
seasons = _detect_cached_seasons()
|
||||
|
||||
# --- If no cached seasons exist, fetch them ---
|
||||
if not seasons:
|
||||
print("No cached seasons found. Fetching from API...")
|
||||
season_list = _fetch_all_seasons_from_api()
|
||||
|
||||
for s in season_list:
|
||||
sid = s.get("id")
|
||||
if sid:
|
||||
print(f"Caching season {sid} match list...")
|
||||
fetch_season_match_list(sid)
|
||||
|
||||
seasons = _detect_cached_seasons()
|
||||
|
||||
# --- Process each season ---
|
||||
for season_id in seasons:
|
||||
print(f"Processing season {season_id}...")
|
||||
match_list = fetch_season_match_list(season_id)
|
||||
|
||||
for match_entry in match_list:
|
||||
match_id = match_entry.get("id")
|
||||
if not match_id:
|
||||
continue
|
||||
|
||||
match_data = fetch_match_details(match_id)
|
||||
if not match_data:
|
||||
continue
|
||||
|
||||
mode = match_data.get("mode", "").lower()
|
||||
|
||||
for p in match_data.get("players", []):
|
||||
if p.get("id") == player_id:
|
||||
|
||||
entry = {
|
||||
"kills": p.get("kills", 0),
|
||||
"deaths": p.get("deaths", 0),
|
||||
"accuracy": p.get("accuracy", 0.0),
|
||||
"damage": p.get("damage", 0),
|
||||
"push_time": p.get("PAY_PushTime", 0),
|
||||
"captures": p.get("DOM_captures", 0),
|
||||
"counters": p.get("DOM_counters", 0),
|
||||
"mode": mode
|
||||
}
|
||||
|
||||
history.append(entry)
|
||||
|
||||
# --- Cache the built history ---
|
||||
try:
|
||||
with open(cache_path, "w", encoding="utf-8") as f:
|
||||
json.dump(history, f, indent=2)
|
||||
except Exception as e:
|
||||
print("ERROR writing player history:", e)
|
||||
|
||||
return history
|
||||
127
match_engine.py
127
match_engine.py
|
|
@ -5,7 +5,10 @@ import json
|
|||
import math
|
||||
|
||||
from tags.objective_payload import compute_payload_objective_team_scores
|
||||
from tags.objective_domination import compute_dom_objective
|
||||
from tags.objective_domination import (
|
||||
compute_dom_objective_for_match_player,
|
||||
compute_dom_objective_team_scores,
|
||||
)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
CAREER_DB_PATH = os.path.join(BASE_DIR, "career_stats.json")
|
||||
|
|
@ -44,7 +47,7 @@ def rank_match_players_slayer(match_players):
|
|||
ranked.append({
|
||||
"id": pid,
|
||||
"name": entry["name"],
|
||||
"team": entry["team_history"][-1] if entry["team_history"] else p.get("team", "Unknown"),
|
||||
"team": p.get("team", "Unknown"),
|
||||
"score_raw": score,
|
||||
"score_display": entry.get("slayer_score_display", f"{score:.2f}"),
|
||||
})
|
||||
|
|
@ -53,53 +56,82 @@ def rank_match_players_slayer(match_players):
|
|||
for i, r in enumerate(ranked, 1):
|
||||
r["rank"] = i
|
||||
return ranked
|
||||
|
||||
def rank_match_players_objdom(players):
|
||||
|
||||
|
||||
def rank_match_players_objdom(match_players):
|
||||
"""
|
||||
Rank players by Domination Objective Specialist score.
|
||||
players = list of player_entry dicts for the current match.
|
||||
Rank players by Domination Objective Specialist score (match-based).
|
||||
Uses DOM_Captures and DOM_Counters from match player stats.
|
||||
"""
|
||||
|
||||
ranked = []
|
||||
for p in players:
|
||||
score = compute_dom_objective(p)
|
||||
ranked.append({
|
||||
"name": p["name"],
|
||||
"team": p["team"],
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
})
|
||||
|
||||
# Sort high → low
|
||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
||||
# Assign global rank
|
||||
for i, entry in enumerate(ranked, start=1):
|
||||
entry["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
|
||||
def rank_match_players_objpl(match_players):
|
||||
"""
|
||||
match_players: list of dicts from stats API (current season),
|
||||
each with at least: id, name, team
|
||||
Returns: list of ranked players with ObjPL score
|
||||
"""
|
||||
db = load_career_db()
|
||||
if db is None:
|
||||
return []
|
||||
|
||||
# Build team entries for ObjPL engine
|
||||
team_entries = []
|
||||
for p in match_players:
|
||||
pid = p.get("id")
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
entry = _get_player_entry(db, pid)
|
||||
if not entry:
|
||||
continue
|
||||
if "objective_payload_components" not in entry:
|
||||
|
||||
team_entries.append((pid, entry, p))
|
||||
|
||||
if not team_entries:
|
||||
return []
|
||||
|
||||
obj_scores = compute_dom_objective_team_scores(team_entries)
|
||||
|
||||
ranked = []
|
||||
for pid, entry, mp in team_entries:
|
||||
score = obj_scores.get(pid)
|
||||
if score is None:
|
||||
continue
|
||||
team_entries.append((pid, entry))
|
||||
|
||||
ranked.append({
|
||||
"id": pid,
|
||||
"name": entry["name"],
|
||||
"team": mp.get("team", "Unknown"),
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
})
|
||||
|
||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
||||
for i, r in enumerate(ranked, 1):
|
||||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
|
||||
def rank_match_players_objpl(match_players):
|
||||
db = load_career_db()
|
||||
if db is None:
|
||||
return []
|
||||
|
||||
team_entries = []
|
||||
for p in match_players:
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
entry = _get_player_entry(db, pid)
|
||||
if not entry:
|
||||
continue
|
||||
|
||||
# ObjPL is match-based; we still attach career entry for name/team history
|
||||
team_entries.append((pid, entry, p))
|
||||
|
||||
if not team_entries:
|
||||
return []
|
||||
|
|
@ -107,21 +139,24 @@ def rank_match_players_objpl(match_players):
|
|||
obj_scores = compute_payload_objective_team_scores(team_entries)
|
||||
|
||||
ranked = []
|
||||
for pid, entry in team_entries:
|
||||
score = obj_scores.get(pid, 0.0)
|
||||
if score <= 0:
|
||||
for pid, entry, mp in team_entries:
|
||||
score = obj_scores.get(pid)
|
||||
if score is None:
|
||||
continue
|
||||
|
||||
ranked.append({
|
||||
"id": pid,
|
||||
"name": entry["name"],
|
||||
"team": entry["team_history"][-1] if entry["team_history"] else "Unknown",
|
||||
"team": mp.get("team", "Unknown"),
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
})
|
||||
|
||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
||||
for i, r in enumerate(ranked, 1):
|
||||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
|
||||
|
|
@ -137,7 +172,6 @@ def split_two_columns(ranked_players, team_a_name, team_b_name):
|
|||
elif p["team"] == team_b_name:
|
||||
right.append(p)
|
||||
else:
|
||||
# If team name mismatch, leave them out of columns
|
||||
pass
|
||||
return left, right
|
||||
|
||||
|
|
@ -168,7 +202,8 @@ def compute_slayer_prediction(team_a_players, team_b_players):
|
|||
"teamA_win": round(pA * 100),
|
||||
"teamB_win": round(pB * 100),
|
||||
}
|
||||
|
||||
|
||||
|
||||
def rank_match_players_sharpshooter(match_players):
|
||||
db = load_career_db()
|
||||
if db is None:
|
||||
|
|
@ -189,7 +224,7 @@ def rank_match_players_sharpshooter(match_players):
|
|||
ranked.append({
|
||||
"id": pid,
|
||||
"name": entry["name"],
|
||||
"team": entry["team_history"][-1] if entry["team_history"] else p.get("team", "Unknown"),
|
||||
"team": p.get("team", "Unknown"),
|
||||
"score_raw": score,
|
||||
"score_display": sharp.get("score_display", f"{score:.2f}"),
|
||||
})
|
||||
|
|
@ -199,7 +234,8 @@ def rank_match_players_sharpshooter(match_players):
|
|||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
|
||||
|
||||
def rank_match_players_clutch(players):
|
||||
ranked = []
|
||||
for p in players:
|
||||
|
|
@ -216,5 +252,4 @@ def rank_match_players_clutch(players):
|
|||
for i, r in enumerate(ranked, start=1):
|
||||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
return ranked
|
||||
Binary file not shown.
|
|
@ -1,8 +1,8 @@
|
|||
import math
|
||||
from tags.objective_domination import compute_dom_objective
|
||||
from tags.objective_domination import compute_dom_objective_for_career_player
|
||||
|
||||
def logistic_win_chance(teamA_avg, teamB_avg, k=1.1):
|
||||
"""Generic logistic win chance for non-Slayer tags."""
|
||||
"""Generic logistic win chance for all tags."""
|
||||
if max(teamA_avg, teamB_avg) == 0:
|
||||
return 50, 50
|
||||
|
||||
|
|
@ -14,19 +14,24 @@ def logistic_win_chance(teamA_avg, teamB_avg, k=1.1):
|
|||
|
||||
|
||||
def compute_tag_averages(teamA_players, teamB_players):
|
||||
"""Compute all tag averages for both teams."""
|
||||
"""Compute all tag averages for both teams using career identity scores."""
|
||||
|
||||
def avg_slayer(players):
|
||||
return sum(p.get("slayer_score_raw", 0.0) for p in players) / max(1, len(players))
|
||||
|
||||
def avg_objpl(players):
|
||||
return sum(
|
||||
p.get("objective_payload_components", {}).get("push_time_norm", 0.0)
|
||||
p.get("objective_payload_components", {}).get("payload_score_raw", 0.0)
|
||||
for p in players
|
||||
) / max(1, len(players))
|
||||
|
||||
def avg_objdom(players):
|
||||
return sum(compute_dom_objective(p) for p in players) / max(1, len(players))
|
||||
return sum(
|
||||
compute_dom_objective_for_career_player(
|
||||
p.get("career_entry", {})
|
||||
).get("dom_score_raw", 0.0)
|
||||
for p in players
|
||||
) / max(1, len(players))
|
||||
|
||||
def avg_sharp(players):
|
||||
return sum(
|
||||
|
|
@ -57,7 +62,7 @@ def compute_tag_averages(teamA_players, teamB_players):
|
|||
|
||||
|
||||
def generate_predictions(teamA_players, teamB_players):
|
||||
"""Full Option D prediction engine."""
|
||||
"""Full Option D prediction engine (modernized)."""
|
||||
|
||||
team_a = teamA_players[0].get("team", "Team A")
|
||||
team_b = teamB_players[0].get("team", "Team B")
|
||||
|
|
@ -67,14 +72,8 @@ def generate_predictions(teamA_players, teamB_players):
|
|||
|
||||
# --- Per-tag win chances ---
|
||||
per_tag = {}
|
||||
|
||||
for tag, (a_avg, b_avg) in avgs.items():
|
||||
if tag == "Slayer":
|
||||
# Slayer uses its own prediction function
|
||||
# Convert to simple logistic for consistency
|
||||
per_tag[tag] = logistic_win_chance(a_avg, b_avg)
|
||||
else:
|
||||
per_tag[tag] = logistic_win_chance(a_avg, b_avg)
|
||||
per_tag[tag] = logistic_win_chance(a_avg, b_avg)
|
||||
|
||||
# --- Weighted overall prediction ---
|
||||
weights = {
|
||||
|
|
|
|||
76
rankings.py
76
rankings.py
|
|
@ -41,24 +41,20 @@ def top_players_by_tag(tag_name):
|
|||
return results
|
||||
|
||||
if tag_name == "Payload Objective Specialist":
|
||||
# For rankings, we approximate team context by using league‑wide push components
|
||||
# and treat all players as if they were on one "virtual team".
|
||||
team_entries = [(pid, p) for pid, p in players.items()]
|
||||
obj_scores = compute_payload_objective_team_scores(team_entries)
|
||||
|
||||
for pid, p in players.items():
|
||||
score = obj_scores.get(pid, 0.0)
|
||||
comp = p.get("objective_payload_components", {})
|
||||
score = comp.get("payload_score_raw", 0.0)
|
||||
if score <= 0:
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"id": pid,
|
||||
"name": p["name"],
|
||||
"team": p["team_history"][-1] if p["team_history"] else "Unknown",
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
"score_display": comp.get("payload_score_display", f"{score:.2f}"),
|
||||
})
|
||||
|
||||
results = [r for r in results if r["score_raw"] > 0]
|
||||
results.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
return results
|
||||
|
||||
|
|
@ -105,4 +101,68 @@ def rank_match_players_clutch(players):
|
|||
for i, r in enumerate(ranked, start=1):
|
||||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
||||
|
||||
def rank_match_players_objpl(players):
|
||||
"""
|
||||
Rank players by match-based Payload Objective Specialist score.
|
||||
Uses match stats only (PAY_PushTime, damage, deaths).
|
||||
"""
|
||||
|
||||
db = load_career_db()
|
||||
if db is None:
|
||||
return []
|
||||
|
||||
team_entries = []
|
||||
for p in players:
|
||||
pid = (
|
||||
p.get("id")
|
||||
or p.get("PlayerUUID")
|
||||
or p.get("uuid")
|
||||
)
|
||||
if not pid:
|
||||
continue
|
||||
|
||||
career_entry = db["players"].get(pid, {})
|
||||
|
||||
# Normalize match keys
|
||||
push = p.get("PAY_PushTime", 0) or 0
|
||||
dmg = p.get("damage", p.get("Damage", 0)) or 0
|
||||
deaths = p.get("deaths", p.get("Deaths", 0)) or 0
|
||||
|
||||
# Only include players with actual payload participation
|
||||
if push <= 0:
|
||||
continue
|
||||
|
||||
# Attach normalized values back to match player
|
||||
p["PAY_PushTime"] = push
|
||||
p["damage"] = dmg
|
||||
p["deaths"] = deaths
|
||||
|
||||
team_entries.append((pid, career_entry, p))
|
||||
|
||||
if not team_entries:
|
||||
return []
|
||||
|
||||
# Compute match-based ObjPL
|
||||
obj_scores = compute_payload_objective_team_scores(team_entries)
|
||||
|
||||
ranked = []
|
||||
for pid, entry, mp in team_entries:
|
||||
score = obj_scores.get(pid, 0.0)
|
||||
|
||||
ranked.append({
|
||||
"id": pid,
|
||||
"name": entry.get("name", mp.get("name", "Unknown")),
|
||||
"team": mp.get("team", "Unknown"),
|
||||
"score_raw": score,
|
||||
"score_display": f"{score:.2f}",
|
||||
})
|
||||
|
||||
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
||||
|
||||
for i, r in enumerate(ranked, 1):
|
||||
r["rank"] = i
|
||||
|
||||
return ranked
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
from rankings import load_career_db
|
||||
from tags.objective_domination import compute_dom_objective
|
||||
from tags.objective_domination import (
|
||||
compute_dom_objective_for_career_player,
|
||||
compute_dom_objective_for_match_player,
|
||||
compute_dom_objective_team_scores
|
||||
)
|
||||
|
||||
|
||||
def matchup_storyline(teamA_players, teamB_players):
|
||||
|
|
@ -17,7 +21,7 @@ def matchup_storyline(teamA_players, teamB_players):
|
|||
|
||||
def avg_obj_payload(players):
|
||||
return sum(
|
||||
p.get("objective_payload_components", {}).get("push_time_norm", 0.0)
|
||||
p.get("objective_payload_components", {}).get("payload_score_raw", 0.0)
|
||||
for p in players
|
||||
) / max(1, len(players))
|
||||
|
||||
|
|
@ -28,7 +32,12 @@ def matchup_storyline(teamA_players, teamB_players):
|
|||
) / max(1, len(players))
|
||||
|
||||
def avg_obj_dom(players):
|
||||
return sum(compute_dom_objective(p) for p in players) / max(1, len(players))
|
||||
return sum(
|
||||
compute_dom_objective_for_career_player(
|
||||
p.get("career_entry", {})
|
||||
).get("dom_score_raw", 0.0)
|
||||
for p in players
|
||||
) / max(1, len(players))
|
||||
|
||||
def avg_consistency(players):
|
||||
return sum(
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2,89 +2,61 @@ import math
|
|||
|
||||
def compute_consistency(matches):
|
||||
"""
|
||||
matches = list of dicts, each containing:
|
||||
- kills
|
||||
- deaths
|
||||
- accuracy
|
||||
- damage
|
||||
- push_time (payload)
|
||||
- captures
|
||||
- counters
|
||||
- mode
|
||||
Compute consistency based on per-match performance stability.
|
||||
Uses:
|
||||
- KD per match
|
||||
- Damage per match
|
||||
- Accuracy per match
|
||||
"""
|
||||
|
||||
# --- Not enough matches to compute variance ---
|
||||
if len(matches) < 2:
|
||||
if not matches:
|
||||
return 0.0, {
|
||||
"per_match_perf": [],
|
||||
"floor": 0.0,
|
||||
"variance": 0.0,
|
||||
"variance_adj": 0.0,
|
||||
"variance_score": 0.0,
|
||||
"avg_perf": 0.0,
|
||||
"consistency_norm": 0.0
|
||||
"match_count": 0,
|
||||
"kd_std": 0.0,
|
||||
"dmg_std": 0.0,
|
||||
"acc_std": 0.0,
|
||||
"consistency_norm": 0.0,
|
||||
}
|
||||
|
||||
per_match_perf_list = []
|
||||
kds = []
|
||||
dmgs = []
|
||||
accs = []
|
||||
|
||||
for m in matches:
|
||||
kills = m["Kills"]
|
||||
deaths = m["Deaths"]
|
||||
damage = m["Damage"]
|
||||
shots = m["Shots"]
|
||||
shots_hit = m["ShotsHit"]
|
||||
|
||||
# --- Slayer percentile (per match) ---
|
||||
kd = m["kills"] / max(1, m["deaths"])
|
||||
slayer_pct = percentile_of_kd(kd)
|
||||
kd = kills / max(1, deaths)
|
||||
acc = shots_hit / shots if shots > 0 else 0.0
|
||||
|
||||
# --- Objective percentile (per match) ---
|
||||
if m["mode"] == "payload":
|
||||
obj_raw = m["push_time"]
|
||||
elif m["mode"] == "domination":
|
||||
obj_raw = m["captures"] + m["counters"]
|
||||
else:
|
||||
obj_raw = 0
|
||||
kds.append(kd)
|
||||
dmgs.append(damage)
|
||||
accs.append(acc)
|
||||
|
||||
objective_pct = percentile_of_objective(obj_raw)
|
||||
def std(values):
|
||||
if len(values) <= 1:
|
||||
return 0.0
|
||||
mean = sum(values) / len(values)
|
||||
var = sum((v - mean) ** 2 for v in values) / len(values)
|
||||
return math.sqrt(var)
|
||||
|
||||
# --- Accuracy percentile (per match) ---
|
||||
accuracy_pct = percentile_of_accuracy(m["accuracy"])
|
||||
kd_std = std(kds)
|
||||
dmg_std = std(dmgs)
|
||||
acc_std = std(accs)
|
||||
|
||||
# --- Composite per-match performance ---
|
||||
perf = (
|
||||
slayer_pct * 0.40 +
|
||||
objective_pct * 0.40 +
|
||||
accuracy_pct * 0.20
|
||||
)
|
||||
# Lower variance = more consistent
|
||||
# Normalize into a 0–1 score
|
||||
raw = 1.0 / (1.0 + kd_std + dmg_std + acc_std)
|
||||
|
||||
per_match_perf_list.append(perf)
|
||||
|
||||
# --- Floor ---
|
||||
floor_score = percentile(min(per_match_perf_list))
|
||||
|
||||
# --- Variance (normalized by match count) ---
|
||||
raw_variance = variance(per_match_perf_list)
|
||||
adj_variance = raw_variance * (1 + (1 / len(matches)))
|
||||
variance_score = 1 / (1 + adj_variance)
|
||||
|
||||
# --- Average performance ---
|
||||
avg_perf = sum(per_match_perf_list) / len(per_match_perf_list)
|
||||
|
||||
# --- Final Consistency ---
|
||||
consistency_raw = (
|
||||
floor_score * 0.40 +
|
||||
variance_score * 0.40 +
|
||||
avg_perf * 0.20
|
||||
)
|
||||
|
||||
# --- Normalized consistency (already 0–1) ---
|
||||
consistency_norm = round(consistency_raw, 4)
|
||||
|
||||
# --- Component block returned to DB builder ---
|
||||
components = {
|
||||
"per_match_perf": per_match_perf_list,
|
||||
"floor": floor_score,
|
||||
"variance": raw_variance,
|
||||
"variance_adj": adj_variance,
|
||||
"variance_score": variance_score,
|
||||
"avg_perf": avg_perf,
|
||||
"consistency_norm": consistency_norm
|
||||
"match_count": len(matches),
|
||||
"kd_std": kd_std,
|
||||
"dmg_std": dmg_std,
|
||||
"acc_std": acc_std,
|
||||
"consistency_norm": raw,
|
||||
}
|
||||
|
||||
return consistency_norm, components
|
||||
return raw, components
|
||||
|
|
@ -1,29 +1,140 @@
|
|||
import math
|
||||
|
||||
def compute_dom_objective(player_entry):
|
||||
# ---------------------------------------------------------
|
||||
# 1. CAREER DOMINATION OBJECTIVE SPECIALIST (ObjDOM)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_dom_objective_for_career_player(pdata, league_averages=None):
|
||||
"""
|
||||
Domination Objective Specialist (ObjDOM)
|
||||
Compute a career Domination Objective score using ONLY Domination stats.
|
||||
|
||||
Uses only website stats:
|
||||
- DOM_captures
|
||||
- DOM_counters
|
||||
|
||||
Log-scaled normalization:
|
||||
CapRate = log(1 + captures) / log(1 + 20)
|
||||
CounterRate = log(1 + counters) / log(1 + 20)
|
||||
|
||||
Formula (0–1 scale):
|
||||
ObjDOM = 0.40 * CapRate + 0.60 * CounterRate
|
||||
|
||||
Counters are weighted more heavily because they prevent enemy scoring.
|
||||
A "Domination map" is any match where DOM_Captures > 0 or DOM_Counters > 0.
|
||||
Payload and Control Point stats are ignored.
|
||||
"""
|
||||
|
||||
caps = player_entry.get("DOM_captures", 0) or 0
|
||||
counters = player_entry.get("DOM_counters", 0) or 0
|
||||
matches = pdata.get("matches", [])
|
||||
if not matches:
|
||||
return {
|
||||
"dom_matches": 0,
|
||||
"total_captures": 0,
|
||||
"total_counters": 0,
|
||||
"avg_captures": 0.0,
|
||||
"avg_counters": 0.0,
|
||||
"dom_score_raw": 0.0,
|
||||
"dom_score_display": "0.00",
|
||||
}
|
||||
|
||||
# Log-scaled normalization (smooths extremes, expands mid-range)
|
||||
cap_rate = math.log1p(caps) / math.log1p(20)
|
||||
counter_rate = math.log1p(counters) / math.log1p(20)
|
||||
dom_matches = 0
|
||||
total_caps = 0
|
||||
total_counters = 0
|
||||
|
||||
score = (0.40 * cap_rate) + (0.60 * counter_rate)
|
||||
return round(score, 3)
|
||||
for m in matches:
|
||||
caps = m.get("DOM_Captures", 0) or 0
|
||||
counters = m.get("DOM_Counters", 0) or 0
|
||||
|
||||
# Only count Domination maps
|
||||
if caps > 0 or counters > 0:
|
||||
dom_matches += 1
|
||||
total_caps += caps
|
||||
total_counters += counters
|
||||
|
||||
if dom_matches == 0:
|
||||
return {
|
||||
"dom_matches": 0,
|
||||
"total_captures": 0,
|
||||
"total_counters": 0,
|
||||
"avg_captures": 0.0,
|
||||
"avg_counters": 0.0,
|
||||
"dom_score_raw": 0.0,
|
||||
"dom_score_display": "0.00",
|
||||
}
|
||||
|
||||
avg_caps = total_caps / dom_matches
|
||||
avg_counters = total_counters / dom_matches
|
||||
|
||||
# Log-scaled normalization (smooth extremes, expand mid-range)
|
||||
# Soft caps: ~20 caps / 20 counters across career
|
||||
cap_rate = math.log1p(avg_caps) / math.log1p(20.0)
|
||||
counter_rate = math.log1p(avg_counters) / math.log1p(20.0)
|
||||
|
||||
# Counters weighted more heavily (deny enemy scoring)
|
||||
raw = (0.40 * cap_rate) + (0.60 * counter_rate)
|
||||
|
||||
return {
|
||||
"dom_matches": dom_matches,
|
||||
"total_captures": total_caps,
|
||||
"total_counters": total_counters,
|
||||
"avg_captures": avg_caps,
|
||||
"avg_counters": avg_counters,
|
||||
"dom_score_raw": raw,
|
||||
"dom_score_display": f"{raw:.2f}",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 2. MATCH-BASED DOMINATION OBJECTIVE SPECIALIST
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_dom_objective_for_match_player(p):
|
||||
"""
|
||||
Extract match-level Domination stats for a single player.
|
||||
Used only for match-based ObjDOM.
|
||||
"""
|
||||
|
||||
caps = p.get("DOM_Captures", 0) or 0
|
||||
counters = p.get("DOM_Counters", 0) or 0
|
||||
|
||||
return {
|
||||
"captures": caps,
|
||||
"counters": counters,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 3. TEAM SCORE CALCULATION (MATCH-BASED ObjDOM)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_dom_objective_team_scores(team_entries):
|
||||
"""
|
||||
team_entries = list of (pid, entry, match_player)
|
||||
entry = career DB entry (ignored for match ObjDOM)
|
||||
match_player = match player dict with DOM stats
|
||||
"""
|
||||
|
||||
# Total team captures / counters
|
||||
team_caps = sum(mp.get("DOM_Captures", 0) or 0 for _, _, mp in team_entries)
|
||||
team_counters = sum(mp.get("DOM_Counters", 0) or 0 for _, _, mp in team_entries)
|
||||
|
||||
# Avoid division by zero
|
||||
if team_caps <= 0:
|
||||
team_caps = 1
|
||||
if team_counters <= 0:
|
||||
team_counters = 1
|
||||
|
||||
scores = {}
|
||||
|
||||
for pid, entry, mp in team_entries:
|
||||
caps = mp.get("DOM_Captures", 0) or 0
|
||||
counters = mp.get("DOM_Counters", 0) or 0
|
||||
|
||||
# 1. Presence on objective (share of team captures)
|
||||
cap_presence = caps / team_caps
|
||||
|
||||
# 2. Counter presence (share of team counters)
|
||||
counter_presence = counters / team_counters
|
||||
|
||||
# 3. Log scaling to smooth extremes
|
||||
cap_rate = math.log1p(caps) / math.log1p(10.0) # per-map soft cap ~10 caps
|
||||
counter_rate = math.log1p(counters) / math.log1p(10.0)
|
||||
|
||||
# Blend presence + impact
|
||||
# Captures: both presence + rate
|
||||
# Counters: weighted more heavily (deny enemy scoring)
|
||||
cap_component = 0.5 * cap_presence + 0.5 * cap_rate
|
||||
counter_component = 0.5 * counter_presence + 0.5 * counter_rate
|
||||
|
||||
score = (0.40 * cap_component) + (0.60 * counter_component)
|
||||
|
||||
scores[pid] = round(score, 4)
|
||||
|
||||
return scores
|
||||
|
|
@ -1,79 +1,158 @@
|
|||
import math
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 1. CAREER PAYLOAD OBJECTIVE SPECIALIST (MODERNIZED)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_payload_objective_for_career_player(p, league_averages):
|
||||
career = p["career"]
|
||||
derived = p["derived"]
|
||||
def compute_payload_objective_for_career_player(pdata, league_averages=None):
|
||||
"""
|
||||
Modernized career Payload Objective Specialist score.
|
||||
Mirrors the structure of Domination's career tag.
|
||||
|
||||
push_time_total = career.get("push_time", 0) or 0
|
||||
damage = career.get("damage", 0) or 0
|
||||
deaths = career.get("deaths", 0) or 0
|
||||
push_time_per_season = derived.get("push_time_per_season", 0) or 0
|
||||
Components:
|
||||
- PresenceNorm: fraction of matches that were Payload
|
||||
- PushNorm: average push time normalized to 300s soft cap
|
||||
- PSINorm: damage-per-death survivability normalized
|
||||
|
||||
# Push Survivability Index (PSI)
|
||||
# PSI = Damage / (Deaths + 1) * 1 / sqrt(PushTime + 1)
|
||||
psi = 0.0
|
||||
if push_time_total > 0:
|
||||
psi = (damage / (deaths + 1)) * (1.0 / math.sqrt(push_time_total + 1))
|
||||
Final score:
|
||||
0.40 * PushNorm
|
||||
+ 0.40 * PresenceNorm
|
||||
+ 0.20 * PSINorm
|
||||
"""
|
||||
|
||||
league_push_time_per_season = league_averages.get("push_time_per_season", 1.0)
|
||||
push_time_norm = push_time_per_season / league_push_time_per_season if league_push_time_per_season > 0 else 0.0
|
||||
matches = pdata.get("matches", [])
|
||||
if not matches:
|
||||
return {
|
||||
"payload_matches": 0,
|
||||
"total_push_time": 0.0,
|
||||
"avg_push_time": 0.0,
|
||||
"presence_norm": 0.0,
|
||||
"push_norm": 0.0,
|
||||
"psi_norm": 0.0,
|
||||
"payload_score_raw": 0.0,
|
||||
"payload_score_display": "0.00",
|
||||
}
|
||||
|
||||
total_matches = len(matches)
|
||||
payload_matches = 0
|
||||
total_push = 0.0
|
||||
total_damage = 0.0
|
||||
total_deaths = 0
|
||||
|
||||
for m in matches:
|
||||
push = m.get("PAY_PushTime", 0) or 0
|
||||
dmg = m.get("Damage", 0) or 0
|
||||
deaths = m.get("Deaths", 0) or 0
|
||||
|
||||
if push > 0:
|
||||
payload_matches += 1
|
||||
total_push += push
|
||||
total_damage += dmg
|
||||
total_deaths += deaths
|
||||
|
||||
if payload_matches == 0:
|
||||
return {
|
||||
"payload_matches": 0,
|
||||
"total_push_time": 0.0,
|
||||
"avg_push_time": 0.0,
|
||||
"presence_norm": 0.0,
|
||||
"push_norm": 0.0,
|
||||
"psi_norm": 0.0,
|
||||
"payload_score_raw": 0.0,
|
||||
"payload_score_display": "0.00",
|
||||
}
|
||||
|
||||
# --- PresenceNorm ---
|
||||
presence_norm = payload_matches / total_matches
|
||||
|
||||
# --- PushNorm ---
|
||||
avg_push = total_push / payload_matches
|
||||
push_norm = min(avg_push / 300.0, 1.0)
|
||||
|
||||
# --- PSINorm ---
|
||||
psi_raw = total_damage / (total_deaths + 1)
|
||||
psi_norm = psi_raw / (psi_raw + 300.0)
|
||||
|
||||
# --- Final Score ---
|
||||
raw = (0.40 * push_norm) + (0.40 * presence_norm) + (0.20 * psi_norm)
|
||||
|
||||
return {
|
||||
"push_time_total": push_time_total,
|
||||
"push_time_per_season": push_time_per_season,
|
||||
"push_time_norm": push_time_norm,
|
||||
"psi": psi,
|
||||
"payload_matches": payload_matches,
|
||||
"total_push_time": total_push,
|
||||
"avg_push_time": avg_push,
|
||||
"presence_norm": presence_norm,
|
||||
"push_norm": push_norm,
|
||||
"psi_norm": psi_norm,
|
||||
"payload_score_raw": raw,
|
||||
"payload_score_display": f"{raw:.2f}",
|
||||
}
|
||||
|
||||
|
||||
def compute_payload_objective_team_scores(team_player_entries):
|
||||
"""
|
||||
team_player_entries: list of (player_id, player_entry_from_career_db)
|
||||
# ---------------------------------------------------------
|
||||
# 2. MATCH-BASED PAYLOAD OBJECTIVE SPECIALIST (MODERNIZED)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
Returns dict {pid: obj_pl_score}
|
||||
def compute_payload_objective_for_match_player(p):
|
||||
"""
|
||||
# PresencePL: share of team push time
|
||||
total_push_time = sum(p["objective_payload_components"]["push_time_total"]
|
||||
for _, p in team_player_entries)
|
||||
if total_push_time <= 0:
|
||||
total_push_time = 1.0
|
||||
Extract match-level Payload stats for a single player.
|
||||
Normalized naming for consistency.
|
||||
"""
|
||||
|
||||
push = p.get("PAY_PushTime", 0) or 0
|
||||
dmg = p.get("damage", p.get("Damage", 0)) or 0
|
||||
deaths = p.get("deaths", p.get("Deaths", 0)) or 0
|
||||
|
||||
return {
|
||||
"push_time": push,
|
||||
"damage": dmg,
|
||||
"deaths": deaths,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 3. TEAM SCORE CALCULATION (MATCH-BASED, MODERNIZED)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_payload_objective_team_scores(team_entries):
|
||||
"""
|
||||
team_entries = list of (pid, career_entry, match_player)
|
||||
|
||||
Components:
|
||||
- Presence: push_time / team_total_push
|
||||
- PushNorm: push_time / 300s soft cap
|
||||
- PSINorm: damage-per-death normalized
|
||||
|
||||
Final score:
|
||||
0.40 * Presence
|
||||
+ 0.40 * PushNorm
|
||||
+ 0.20 * PSINorm
|
||||
"""
|
||||
|
||||
# Total team push time
|
||||
team_push = sum(mp.get("PAY_PushTime", 0) or 0 for _, _, mp in team_entries)
|
||||
if team_push <= 0:
|
||||
team_push = 1 # avoid div-by-zero
|
||||
|
||||
scores = {}
|
||||
for pid, pdata in team_player_entries:
|
||||
comp = pdata["objective_payload_components"]
|
||||
presence_pl = comp["push_time_total"] / total_push_time
|
||||
|
||||
push_time_norm = comp["push_time_norm"]
|
||||
psi = comp["psi"]
|
||||
for pid, entry, mp in team_entries:
|
||||
push = mp.get("PAY_PushTime", 0) or 0
|
||||
dmg = mp.get("damage", mp.get("Damage", 0)) or 0
|
||||
deaths = mp.get("deaths", mp.get("Deaths", 0)) or 0
|
||||
|
||||
# Normalize PSI within team to avoid extreme outliers dominating
|
||||
# We'll compute team PSI mean and std first
|
||||
scores[pid] = {
|
||||
"presence_pl": presence_pl,
|
||||
"push_time_norm": push_time_norm,
|
||||
"psi": psi,
|
||||
}
|
||||
# --- Presence ---
|
||||
presence = push / team_push
|
||||
|
||||
# PSI normalization within team
|
||||
psi_values = [v["psi"] for v in scores.values()]
|
||||
if psi_values:
|
||||
mean_psi = sum(psi_values) / len(psi_values)
|
||||
var_psi = sum((x - mean_psi) ** 2 for x in psi_values) / max(1, len(psi_values))
|
||||
std_psi = math.sqrt(var_psi)
|
||||
else:
|
||||
mean_psi = 0.0
|
||||
std_psi = 1.0
|
||||
# --- PushNorm ---
|
||||
push_norm = min(push / 300.0, 1.0)
|
||||
|
||||
for pid, v in scores.items():
|
||||
if std_psi > 0:
|
||||
psi_norm = (v["psi"] - mean_psi) / std_psi
|
||||
else:
|
||||
psi_norm = 0.0
|
||||
# --- PSINorm ---
|
||||
psi_raw = dmg / (deaths + 1)
|
||||
psi_norm = psi_raw / (psi_raw + 300.0)
|
||||
|
||||
# Final ObjPL score (website‑only version)
|
||||
# 0.40 Presence + 0.40 PushTimeNorm + 0.20 PSI_norm
|
||||
obj_pl = (0.40 * v["presence_pl"]) + (0.40 * v["push_time_norm"]) + (0.20 * psi_norm)
|
||||
v["obj_pl"] = obj_pl
|
||||
# --- Final Score ---
|
||||
score = (0.40 * presence) + (0.40 * push_norm) + (0.20 * psi_norm)
|
||||
|
||||
return {pid: v["obj_pl"] for pid, v in scores.items()}
|
||||
scores[pid] = round(score, 4)
|
||||
|
||||
return scores
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
from rankings import load_career_db
|
||||
from tags.objective_domination import compute_dom_objective
|
||||
from tags.objective_domination import (
|
||||
compute_dom_objective_for_career_player,
|
||||
compute_dom_objective_for_match_player,
|
||||
compute_dom_objective_team_scores
|
||||
)
|
||||
|
||||
|
||||
def generate_team_identity(player_list):
|
||||
|
|
@ -17,7 +21,7 @@ def generate_team_identity(player_list):
|
|||
|
||||
def avg_payload(players):
|
||||
return sum(
|
||||
p.get("objective_payload_components", {}).get("push_time_norm", 0.0)
|
||||
p.get("objective_payload_components", {}).get("payload_score_raw", 0.0)
|
||||
for p in players
|
||||
) / max(1, len(players))
|
||||
|
||||
|
|
@ -40,7 +44,12 @@ def generate_team_identity(player_list):
|
|||
) / max(1, len(players))
|
||||
|
||||
def avg_objdom(players):
|
||||
return sum(compute_dom_objective(p) for p in players) / max(1, len(players))
|
||||
return sum(
|
||||
compute_dom_objective_for_career_player(
|
||||
p.get("career_entry", {})
|
||||
).get("dom_score_raw", 0.0)
|
||||
for p in players
|
||||
) / max(1, len(players))
|
||||
|
||||
# --- Compute identity scores ---
|
||||
identity = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue