220 lines
5.9 KiB
Python
220 lines
5.9 KiB
Python
|
|
# match_engine.py
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import json
|
|||
|
|
import math
|
|||
|
|
|
|||
|
|
from tags.objective_payload import compute_payload_objective_team_scores
|
|||
|
|
from tags.objective_domination import compute_dom_objective
|
|||
|
|
|
|||
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
CAREER_DB_PATH = os.path.join(BASE_DIR, "career_stats.json")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_career_db():
|
|||
|
|
if not os.path.exists(CAREER_DB_PATH):
|
|||
|
|
return None
|
|||
|
|
with open(CAREER_DB_PATH, "r", encoding="utf-8") as f:
|
|||
|
|
return json.load(f)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_player_entry(db, pid):
|
|||
|
|
return db["players"].get(str(pid)) or db["players"].get(pid)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def rank_match_players_slayer(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 slayer info
|
|||
|
|
"""
|
|||
|
|
db = load_career_db()
|
|||
|
|
if db is None:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
ranked = []
|
|||
|
|
for p in match_players:
|
|||
|
|
pid = p.get("id")
|
|||
|
|
entry = _get_player_entry(db, pid)
|
|||
|
|
if not entry:
|
|||
|
|
continue
|
|||
|
|
score = entry.get("slayer_score_raw", 0.0)
|
|||
|
|
if score <= 0:
|
|||
|
|
continue
|
|||
|
|
ranked.append({
|
|||
|
|
"id": pid,
|
|||
|
|
"name": entry["name"],
|
|||
|
|
"team": entry["team_history"][-1] if entry["team_history"] else p.get("team", "Unknown"),
|
|||
|
|
"score_raw": score,
|
|||
|
|
"score_display": entry.get("slayer_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_objdom(players):
|
|||
|
|
"""
|
|||
|
|
Rank players by Domination Objective Specialist score.
|
|||
|
|
players = list of player_entry dicts for the current match.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
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")
|
|||
|
|
entry = _get_player_entry(db, pid)
|
|||
|
|
if not entry:
|
|||
|
|
continue
|
|||
|
|
if "objective_payload_components" not in entry:
|
|||
|
|
continue
|
|||
|
|
team_entries.append((pid, entry))
|
|||
|
|
|
|||
|
|
if not team_entries:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
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:
|
|||
|
|
continue
|
|||
|
|
ranked.append({
|
|||
|
|
"id": pid,
|
|||
|
|
"name": entry["name"],
|
|||
|
|
"team": entry["team_history"][-1] if entry["team_history"] else "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 split_two_columns(ranked_players, team_a_name, team_b_name):
|
|||
|
|
"""
|
|||
|
|
Keeps global rank 1–N, but splits into left/right columns by team.
|
|||
|
|
"""
|
|||
|
|
left = []
|
|||
|
|
right = []
|
|||
|
|
for p in ranked_players:
|
|||
|
|
if p["team"] == team_a_name:
|
|||
|
|
left.append(p)
|
|||
|
|
elif p["team"] == team_b_name:
|
|||
|
|
right.append(p)
|
|||
|
|
else:
|
|||
|
|
# If team name mismatch, leave them out of columns
|
|||
|
|
pass
|
|||
|
|
return left, right
|
|||
|
|
|
|||
|
|
|
|||
|
|
def compute_slayer_prediction(team_a_players, team_b_players):
|
|||
|
|
"""
|
|||
|
|
team_a_players / team_b_players: lists of ranked player dicts with slayer_score_raw
|
|||
|
|
Returns dict with team averages and win chances.
|
|||
|
|
"""
|
|||
|
|
A = [p["score_raw"] for p in team_a_players]
|
|||
|
|
B = [p["score_raw"] for p in team_b_players]
|
|||
|
|
|
|||
|
|
teamA_avg = sum(A) / max(1, len(A))
|
|||
|
|
teamB_avg = sum(B) / max(1, len(B))
|
|||
|
|
|
|||
|
|
if max(teamA_avg, teamB_avg) == 0:
|
|||
|
|
slayer_edge = 0
|
|||
|
|
else:
|
|||
|
|
slayer_edge = (teamB_avg - teamA_avg) / max(teamA_avg, teamB_avg)
|
|||
|
|
|
|||
|
|
k = 1.1
|
|||
|
|
pB = 1 / (1 + math.exp(-k * slayer_edge))
|
|||
|
|
pA = 1 - pB
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"teamA_avg": teamA_avg,
|
|||
|
|
"teamB_avg": teamB_avg,
|
|||
|
|
"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:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
ranked = []
|
|||
|
|
for p in match_players:
|
|||
|
|
pid = p.get("id")
|
|||
|
|
entry = _get_player_entry(db, pid)
|
|||
|
|
if not entry:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
sharp = entry.get("sharpshooter", {})
|
|||
|
|
score = sharp.get("score_raw", 0.0)
|
|||
|
|
if score <= 0:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
ranked.append({
|
|||
|
|
"id": pid,
|
|||
|
|
"name": entry["name"],
|
|||
|
|
"team": entry["team_history"][-1] if entry["team_history"] else p.get("team", "Unknown"),
|
|||
|
|
"score_raw": score,
|
|||
|
|
"score_display": sharp.get("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_clutch(players):
|
|||
|
|
ranked = []
|
|||
|
|
for p in players:
|
|||
|
|
score = p.get("clutch_components", {}).get("clutch_norm", 0.0)
|
|||
|
|
ranked.append({
|
|||
|
|
"name": p.get("name", "Unknown"),
|
|||
|
|
"team": p.get("team", ""),
|
|||
|
|
"score_raw": score,
|
|||
|
|
"score_display": f"{score:.2f}",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
ranked.sort(key=lambda x: x["score_raw"], reverse=True)
|
|||
|
|
|
|||
|
|
for i, r in enumerate(ranked, start=1):
|
|||
|
|
r["rank"] = i
|
|||
|
|
|
|||
|
|
return ranked
|
|||
|
|
|