Refactor: integrated hybrid Consistency tag system, updated DB builder, GUI, rankings, predictions, storyline, identity, and added test suite
This commit is contained in:
parent
16f8032654
commit
3dc737d819
15 changed files with 548 additions and 148 deletions
|
|
@ -76,6 +76,25 @@ def generate_prediction(teamA_players, teamB_players, teamA_name="Team A", teamB
|
|||
lines.append(f" {teamA_name}: {', '.join(A_weak) if A_weak else 'No major weaknesses'}")
|
||||
lines.append(f" {teamB_name}: {', '.join(B_weak) if B_weak else 'No major weaknesses'}\n")
|
||||
|
||||
# -----------------------------------------
|
||||
# 6. Consistency comparison (new)
|
||||
# -----------------------------------------
|
||||
A_cons = A["consistency"]["avg_pct"]
|
||||
B_cons = B["consistency"]["avg_pct"]
|
||||
|
||||
if abs(A_cons - B_cons) < 5:
|
||||
cons_line = "Both teams show similar consistency across the season."
|
||||
elif A_cons > B_cons:
|
||||
cons_line = f"{teamA_name} have been the more stable team, with stronger match-to-match consistency."
|
||||
else:
|
||||
cons_line = f"{teamB_name} have been the more stable team, with stronger match-to-match consistency."
|
||||
|
||||
lines.append("\nConsistency Check:")
|
||||
lines.append(f" {cons_line}\n")
|
||||
|
||||
# -----------------------------------------
|
||||
# 7. Final prediction line
|
||||
# -----------------------------------------
|
||||
lines.append("Prediction:")
|
||||
lines.append(f" {favorite}")
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def summarize_team_tags(team_players):
|
|||
continue
|
||||
|
||||
summary[tag] = {
|
||||
"avg_pct": sum(pcts) / len(pcts),
|
||||
"avg_pct": sum(p for p in pcts if isinstance(p, (int, float))) / max(1, len([p for p in pcts if isinstance(p, (int, float))])),
|
||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||
"count_S": tiers.count("S"),
|
||||
"count_A": tiers.count("A"),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def summarize_team_tags(team_players):
|
|||
continue
|
||||
|
||||
summary[tag] = {
|
||||
"avg_pct": sum(pcts) / len(pcts),
|
||||
"avg_pct": sum(p for p in pcts if isinstance(p, (int, float))) / max(1, len([p for p in pcts if isinstance(p, (int, float))])),
|
||||
"top_tier": max(tiers, key=lambda t: "SABCD".index(t)),
|
||||
"count_S": tiers.count("S"),
|
||||
"count_A": tiers.count("A"),
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from match_engine import (
|
|||
rank_match_players_clutch,
|
||||
split_two_columns,
|
||||
compute_slayer_prediction,
|
||||
rank_match_players_consistency_career,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
|
|
@ -288,7 +289,7 @@ class DashLeagueGUI:
|
|||
self.team_b_listbox.insert(tk.END, p.get("name", "Unknown"))
|
||||
|
||||
|
||||
def normalize_player(p):
|
||||
def normalize_player(self, p):
|
||||
return {
|
||||
"id": p.get("PlayerUUID"),
|
||||
"name": p.get("PlayerGameName"),
|
||||
|
|
@ -490,9 +491,20 @@ class DashLeagueGUI:
|
|||
obj_pl = player.get("objective_payload_score")
|
||||
slayer_strength = player.get("slayer_strength", "Unknown")
|
||||
|
||||
consistency = player.get("consistency", {})
|
||||
cons_tier = consistency.get("tier", "N/A")
|
||||
cons_pct = consistency.get("pct")
|
||||
cons_summary = consistency.get("summary", "No consistency data available.")
|
||||
|
||||
if isinstance(cons_pct, (int, float)):
|
||||
cons_line = f"{cons_tier} Tier ({cons_pct:.1f} percentile)"
|
||||
else:
|
||||
cons_line = f"{cons_tier} Tier"
|
||||
|
||||
text = f"""PLAYER SPOTLIGHT — {name}
|
||||
|
||||
Slayer Tier: {slayer_strength}
|
||||
Consistency: {cons_line}
|
||||
Payload Objective Specialist: {obj_pl if obj_pl is not None else "N/A"}
|
||||
|
||||
Career Stats:
|
||||
|
|
@ -506,11 +518,13 @@ class DashLeagueGUI:
|
|||
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}
|
||||
|
||||
Consistency Summary:
|
||||
{cons_summary}
|
||||
"""
|
||||
|
||||
output.delete("1.0", tk.END)
|
||||
output.insert(tk.END, text)
|
||||
|
||||
def export():
|
||||
content = output.get("1.0", tk.END).strip()
|
||||
if not content:
|
||||
|
|
@ -560,35 +574,49 @@ class DashLeagueGUI:
|
|||
def generate_player_tags(self, player_entry, map_type):
|
||||
tags_out = []
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Slayer (career)
|
||||
# ---------------------------------------------------------
|
||||
slayer = player_entry.get("slayer", {})
|
||||
slayer_strength = slayer.get("strength") or player_entry.get("slayer_strength")
|
||||
if isinstance(slayer_strength, str):
|
||||
tags_out.append(slayer_strength)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Consistency (career - hybrid system)
|
||||
# ---------------------------------------------------------
|
||||
consistency = player_entry.get("consistency", {})
|
||||
if isinstance(consistency, dict):
|
||||
score = consistency.get("raw")
|
||||
if isinstance(score, (int, float)):
|
||||
tags_out.append(f"Cons {score:.2f}")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Payload Objective Specialist (match-based)
|
||||
# ---------------------------------------------------------
|
||||
if map_type == "Payload":
|
||||
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 Objective Specialist (match-based)
|
||||
# ---------------------------------------------------------
|
||||
if map_type == "Domination":
|
||||
dom_info = player_entry.get("objective_domination_components", {})
|
||||
dom_score = dom_info.get("raw", 0.0)
|
||||
tags_out.append(f"ObjDOM {dom_score:.2f}")
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Sharpshooter (career)
|
||||
# ---------------------------------------------------------
|
||||
sharp = player_entry.get("sharpshooter", {}).get("score") or player_entry.get("sharpshooter_score")
|
||||
if isinstance(sharp, (int, float)):
|
||||
tags_out.append(f"Sharp {sharp:.2f}")
|
||||
|
||||
print("DEBUG DOM:", player_entry.get("objective_domination_components"))
|
||||
|
||||
return ", ".join(tags_out)
|
||||
|
||||
|
||||
|
||||
def on_match_storyline(self):
|
||||
team_a = self.team_a_var.get()
|
||||
team_b = self.team_b_var.get()
|
||||
|
|
@ -713,6 +741,10 @@ class DashLeagueGUI:
|
|||
elif tag == "Clutch":
|
||||
ranked = rank_match_players_clutch(self.current_match_players)
|
||||
|
||||
elif tag == "Consistency":
|
||||
ranked = rank_match_players_consistency_career(self.current_match_players)
|
||||
|
||||
|
||||
else: # Sharpshooter
|
||||
ranked = rank_match_players_sharpshooter(self.current_match_players)
|
||||
|
||||
|
|
@ -841,13 +873,13 @@ class DashLeagueGUI:
|
|||
text += "Counters are weighted more heavily because they prevent enemy scoring.\n"
|
||||
|
||||
elif tag == "Consistency":
|
||||
text += "\nConsistency Formula:\n"
|
||||
text += "\nConsistency Formula (Hybrid Career Tag):\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 += "- Floor = lowest per-match performance score\n"
|
||||
text += "- Stability = 1 / (1 + Adjusted Variance)\n"
|
||||
text += "- Adjusted Variance = Variance × (1 + 1 / Match Count)\n"
|
||||
text += "- Average Performance = mean per-match performance score\n"
|
||||
|
||||
elif tag == "Clutch":
|
||||
text += "\nClutch Formula:\n"
|
||||
|
|
@ -1027,40 +1059,6 @@ class DashLeagueGUI:
|
|||
messagebox.showinfo("Exported", f"OBS files exported to:\n{output_folder}")
|
||||
|
||||
|
||||
def format_team_identity(identity):
|
||||
"""
|
||||
Converts the identity block dict into readable caster-friendly text.
|
||||
"""
|
||||
lines = []
|
||||
|
||||
lines.append(f"Team Style: {identity.get('team_style', 'Unknown')}")
|
||||
lines.append("")
|
||||
|
||||
lines.append("Strengths:")
|
||||
for s in identity.get("strengths", []):
|
||||
lines.append(f" - {s}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
lines.append("Weaknesses:")
|
||||
for w in identity.get("weaknesses", []):
|
||||
lines.append(f" - {w}")
|
||||
|
||||
lines.append("")
|
||||
|
||||
lines.append("Tag Breakdown:")
|
||||
for tag, data in identity.get("tags", {}).items():
|
||||
lines.append(
|
||||
f" - {tag.title()}: {data['tier']} Tier "
|
||||
f"(avg {data['avg_pct']:.1f} percentile)"
|
||||
)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
app = DashLeagueGUI(root)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from tags.slayer import compute_slayer_for_career_player
|
|||
from tags.objective_payload import compute_payload_tag
|
||||
from tags.objective_domination import compute_dom_objective_for_career_player
|
||||
from tags.sharpshooter import compute_sharpshooter_for_career_player
|
||||
from tags.consistency import compute_consistency, compute_consistency_tag
|
||||
from tags.consistency import build_consistency_tag
|
||||
from tags.clutch import compute_clutch, compute_clutch_raw
|
||||
|
||||
from data.league_metrics import compute_league_metrics, compute_league_clutch_metrics
|
||||
|
|
@ -146,14 +146,112 @@ def build_career_database(output_path):
|
|||
distributions["domination"]
|
||||
)
|
||||
|
||||
# Consistency
|
||||
# ---------------------------------------------------------
|
||||
# Consistency (Hybrid Career Tag - New System)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
matches = pdata["matches"]
|
||||
consistency_raw, components = compute_consistency(matches)
|
||||
pdata["consistency"] = compute_consistency_tag(
|
||||
components,
|
||||
distributions["consistency"]
|
||||
maps_played = pdata["career"].get("maps", 0)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Extract per-match performance components
|
||||
# ---------------------------------------------------------
|
||||
# NOTE:
|
||||
# If you later add per-match slayer/objective/accuracy scores,
|
||||
# this block will automatically support them.
|
||||
# For now, we compute a simple per-match performance score
|
||||
# using the same formula as the legacy system.
|
||||
# ---------------------------------------------------------
|
||||
|
||||
per_match_scores = []
|
||||
for m in matches:
|
||||
# Slayer-like component
|
||||
dmg = m.get("Damage", 0)
|
||||
kills = m.get("Kills", 0)
|
||||
deaths = m.get("Deaths", 0)
|
||||
kd = kills / deaths if deaths > 0 else kills
|
||||
|
||||
slayer_component = (
|
||||
(dmg / 10000) * 0.40 +
|
||||
(kills / 30) * 0.40 +
|
||||
(kd / 5) * 0.20
|
||||
)
|
||||
|
||||
# Objective-like component
|
||||
push = m.get("PAY_PushTime", 0)
|
||||
caps = m.get("DOM_Captures", 0)
|
||||
counters = m.get("DOM_Counters", 0)
|
||||
|
||||
objective_component = (
|
||||
(push / 300) * 0.40 +
|
||||
(caps / 20) * 0.30 +
|
||||
(counters / 20) * 0.30
|
||||
)
|
||||
|
||||
# Accuracy-like component
|
||||
shots = m.get("Shots", 0)
|
||||
shots_hit = m.get("ShotsHit", 0)
|
||||
accuracy = (shots_hit / shots) if shots > 0 else 0.0
|
||||
|
||||
accuracy_component = accuracy * 0.20
|
||||
|
||||
# Final per-match performance score
|
||||
perf = slayer_component + objective_component + accuracy_component
|
||||
per_match_scores.append(perf)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Compute floor, stability, average
|
||||
# ---------------------------------------------------------
|
||||
if per_match_scores:
|
||||
floor_value = min(per_match_scores)
|
||||
avg_value = sum(per_match_scores) / len(per_match_scores)
|
||||
|
||||
# Variance adjusted for match count
|
||||
mean = avg_value
|
||||
variance = sum((x - mean) ** 2 for x in per_match_scores) / len(per_match_scores)
|
||||
adjusted_variance = variance * (1 + 1 / max(1, len(per_match_scores)))
|
||||
stability_value = 1 / (1 + adjusted_variance)
|
||||
else:
|
||||
floor_value = 0.0
|
||||
avg_value = 0.0
|
||||
stability_value = 0.0
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Build hybrid consistency tag
|
||||
# ---------------------------------------------------------
|
||||
consistency_result = build_consistency_tag(
|
||||
floor_current=floor_value,
|
||||
floor_career=floor_value, # career = all matches
|
||||
floor_league=league_averages.get("consistency_floor", 0.0),
|
||||
|
||||
stability_current=stability_value,
|
||||
stability_career=stability_value,
|
||||
stability_league=league_averages.get("consistency_stability", 0.0),
|
||||
|
||||
average_current=avg_value,
|
||||
average_career=avg_value,
|
||||
average_league=league_averages.get("consistency_average", 0.0),
|
||||
|
||||
matches_played_current=maps_played,
|
||||
percentile=None
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Store in DB (GUI + Rankings + Spotlight compatible)
|
||||
# ---------------------------------------------------------
|
||||
pdata["consistency"] = {
|
||||
"raw": consistency_result.normalized_score,
|
||||
"pct": consistency_result.percentile,
|
||||
"tier": consistency_result.tier,
|
||||
"summary": consistency_result.summary_short,
|
||||
"components": {
|
||||
"floor": consistency_result.components.floor,
|
||||
"stability": consistency_result.components.stability,
|
||||
"average": consistency_result.components.average_performance,
|
||||
},
|
||||
"extras": consistency_result.extras,
|
||||
}
|
||||
|
||||
# Clutch (FINAL TAG)
|
||||
pdata["clutch"] = compute_clutch(
|
||||
pdata,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import math
|
|||
from tags.slayer import compute_slayer_raw
|
||||
from tags.sharpshooter import compute_sharpshooter_raw
|
||||
from tags.objective_payload import compute_payload_raw
|
||||
from tags.consistency import compute_consistency_raw
|
||||
from tags.objective_domination import compute_dom_raw
|
||||
|
||||
|
||||
|
|
@ -101,11 +100,6 @@ def compute_league_metrics(all_players):
|
|||
# Payload
|
||||
dist["payload"].append(compute_payload_raw(p))
|
||||
|
||||
# Consistency (with smoothing)
|
||||
raw_cons = compute_consistency_raw(matches)
|
||||
raw_cons = max(raw_cons, 0.05)
|
||||
dist["consistency"].append(raw_cons)
|
||||
|
||||
# Domination
|
||||
dist["domination"].append(compute_dom_raw(p))
|
||||
|
||||
|
|
|
|||
|
|
@ -56,3 +56,66 @@ def build_tag_output(raw_score, distribution, summary_fn):
|
|||
"tier": tier,
|
||||
"summary": summary
|
||||
}
|
||||
|
||||
|
||||
# ============================================================
|
||||
# CONSISTENCY TAG ADAPTER
|
||||
# ------------------------------------------------------------
|
||||
# Adapts the new ConsistencyResult object into the legacy
|
||||
# tag_framework format expected by the UI + rankings.
|
||||
# ============================================================
|
||||
|
||||
from tags.consistency import build_consistency_tag
|
||||
|
||||
def compute_consistency_tag(player_stats, distribution):
|
||||
"""
|
||||
Adapter layer:
|
||||
- Extracts needed stats from player_stats
|
||||
- Calls the new Consistency tag builder
|
||||
- Returns raw score + summary_fn for tag_framework
|
||||
"""
|
||||
|
||||
# -----------------------------------------
|
||||
# Extract stats (0–1 normalized values)
|
||||
# -----------------------------------------
|
||||
floor_current = player_stats.get("floor_current")
|
||||
floor_career = player_stats.get("floor_career")
|
||||
floor_league = player_stats.get("floor_league")
|
||||
|
||||
stability_current = player_stats.get("stability_current")
|
||||
stability_career = player_stats.get("stability_career")
|
||||
stability_league = player_stats.get("stability_league")
|
||||
|
||||
average_current = player_stats.get("average_current")
|
||||
average_career = player_stats.get("average_career")
|
||||
average_league = player_stats.get("average_league")
|
||||
|
||||
matches_played = player_stats.get("matches_played_current", 0)
|
||||
|
||||
# -----------------------------------------
|
||||
# Build full ConsistencyResult object
|
||||
# -----------------------------------------
|
||||
result = build_consistency_tag(
|
||||
floor_current=floor_current,
|
||||
floor_career=floor_career,
|
||||
floor_league=floor_league,
|
||||
stability_current=stability_current,
|
||||
stability_career=stability_career,
|
||||
stability_league=stability_league,
|
||||
average_current=average_current,
|
||||
average_career=average_career,
|
||||
average_league=average_league,
|
||||
matches_played_current=matches_played,
|
||||
percentile=None # tag_framework will compute this
|
||||
)
|
||||
|
||||
# -----------------------------------------
|
||||
# Summary function for tag_framework
|
||||
# -----------------------------------------
|
||||
def summary_fn(tier, pct):
|
||||
return result.summary_short
|
||||
|
||||
# -----------------------------------------
|
||||
# Return raw score + summary_fn
|
||||
# -----------------------------------------
|
||||
return result.normalized_score, summary_fn
|
||||
|
|
@ -183,6 +183,24 @@ def rank_match_players_objdom(match_players):
|
|||
|
||||
return ranked
|
||||
|
||||
|
||||
def rank_match_players_consistency_career(match_players):
|
||||
"""
|
||||
Wraps rank_match_players_by_tag('consistency') so the output matches
|
||||
the expected structure for the rankings UI.
|
||||
"""
|
||||
ranked = rank_match_players_by_tag(match_players, "consistency")
|
||||
out = []
|
||||
for i, p in enumerate(ranked, 1):
|
||||
out.append({
|
||||
"rank": i,
|
||||
"name": p["name"],
|
||||
"team": p.get("team", "Unknown"),
|
||||
"score_raw": p["raw"],
|
||||
"score_display": f"{p['raw']:.2f}",
|
||||
})
|
||||
return out
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Legacy Slayer Prediction Wrapper
|
||||
# ---------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,83 +1,122 @@
|
|||
import math
|
||||
from tags.tag_framework import build_tag_output
|
||||
# ============================================================
|
||||
# CONSISTENCY TAG
|
||||
# ------------------------------------------------------------
|
||||
# Template tag for the entire system.
|
||||
# Uses:
|
||||
# - fallback logic
|
||||
# - early-season blending
|
||||
# - normalization
|
||||
# - tier mapping
|
||||
# - summary generation
|
||||
# - spotlight extras
|
||||
# ============================================================
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Raw Consistency Score (Robust)
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_consistency_raw(matches):
|
||||
"""
|
||||
Computes a normalized 0–1 consistency score based on
|
||||
match-to-match stability in KD, damage, and accuracy.
|
||||
"""
|
||||
|
||||
if not matches:
|
||||
return 0.05 # minimum floor
|
||||
|
||||
perf = []
|
||||
|
||||
for m in matches:
|
||||
kills = m.get("Kills", 0)
|
||||
deaths = m.get("Deaths", 0)
|
||||
damage = m.get("Damage", 0)
|
||||
shots = m.get("Shots", 0)
|
||||
shots_hit = m.get("ShotsHit", 0)
|
||||
|
||||
kd = kills / max(1, deaths)
|
||||
acc = shots_hit / shots if shots > 0 else 0.0
|
||||
|
||||
# Normalize components into comparable ranges
|
||||
kd_norm = min(kd / 5.0, 1.0) # KD 0–5
|
||||
dmg_norm = min(damage / 3000.0, 1.0) # Damage 0–3000
|
||||
acc_norm = acc # Already 0–1
|
||||
|
||||
# Composite performance score per match
|
||||
perf_score = (0.4 * kd_norm) + (0.4 * dmg_norm) + (0.2 * acc_norm)
|
||||
perf.append(perf_score)
|
||||
|
||||
# Variance of performance
|
||||
if len(perf) <= 1:
|
||||
return 0.25 # floor for single-match players
|
||||
|
||||
mean = sum(perf) / len(perf)
|
||||
var = sum((p - mean) ** 2 for p in perf) / len(perf)
|
||||
std = math.sqrt(var)
|
||||
|
||||
# Smoothing
|
||||
std = max(std, 0.05)
|
||||
|
||||
# Convert std into consistency score
|
||||
raw = 1.0 / (1.0 + std)
|
||||
|
||||
# Soft clamp
|
||||
return max(0.05, min(raw, 1.0))
|
||||
from dataclasses import dataclass
|
||||
from utils.fallback import get_stat_with_fallback, blend_early_season
|
||||
from utils.tiers import map_score_to_tier
|
||||
from utils.summaries import build_consistency_summaries
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------
|
||||
# ------------------------------------------------------------
|
||||
# Data structures
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def consistency_summary(tier, pct):
|
||||
if tier == "S":
|
||||
return f"Ultra-stable performer — top {100 - pct:.0f}% in match-to-match consistency."
|
||||
if tier == "A":
|
||||
return "Very consistent across matches."
|
||||
if tier == "B":
|
||||
return "Above-average consistency."
|
||||
if tier == "C":
|
||||
return "Inconsistent performance."
|
||||
return "Highly volatile match-to-match output."
|
||||
@dataclass
|
||||
class ConsistencyComponents:
|
||||
floor: float
|
||||
stability: float
|
||||
average_performance: float
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------
|
||||
|
||||
def compute_consistency(matches):
|
||||
raw = compute_consistency_raw(matches)
|
||||
return raw, {"consistency_norm": raw}
|
||||
@dataclass
|
||||
class ConsistencyResult:
|
||||
raw_score: float
|
||||
normalized_score: float
|
||||
percentile: float | None
|
||||
tier: str
|
||||
components: ConsistencyComponents
|
||||
summary_short: str
|
||||
summary_long: str
|
||||
storyline_hook: str
|
||||
identity_signal: str
|
||||
extras: dict
|
||||
|
||||
|
||||
def compute_consistency_tag(components, consistency_distribution):
|
||||
raw = components.get("consistency_norm", 0.0)
|
||||
return build_tag_output(raw, consistency_distribution, consistency_summary)
|
||||
# ------------------------------------------------------------
|
||||
# Core formula (your original formula)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def compute_consistency_score(components: ConsistencyComponents) -> float:
|
||||
return (
|
||||
components.floor * 0.40 +
|
||||
components.stability * 0.40 +
|
||||
components.average_performance * 0.20
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Main builder
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def build_consistency_tag(
|
||||
*,
|
||||
floor_current,
|
||||
floor_career,
|
||||
floor_league,
|
||||
stability_current,
|
||||
stability_career,
|
||||
stability_league,
|
||||
average_current,
|
||||
average_career,
|
||||
average_league,
|
||||
matches_played_current,
|
||||
percentile=None
|
||||
) -> ConsistencyResult:
|
||||
|
||||
# --- FALLBACK ---
|
||||
floor_raw = get_stat_with_fallback(floor_current, floor_career, floor_league)
|
||||
stability_raw = get_stat_with_fallback(stability_current, stability_career, stability_league)
|
||||
average_raw = get_stat_with_fallback(average_current, average_career, average_league)
|
||||
|
||||
# --- EARLY SEASON BLENDING ---
|
||||
floor_value = blend_early_season(floor_raw, floor_career or floor_raw, matches_played_current)
|
||||
stability_value = blend_early_season(stability_raw, stability_career or stability_raw, matches_played_current)
|
||||
average_value = blend_early_season(average_raw, average_career or average_raw, matches_played_current)
|
||||
|
||||
# --- COMPONENTS ---
|
||||
components = ConsistencyComponents(
|
||||
floor=floor_value,
|
||||
stability=stability_value,
|
||||
average_performance=average_value
|
||||
)
|
||||
|
||||
# --- SCORE ---
|
||||
raw_score = compute_consistency_score(components)
|
||||
normalized_score = max(0.0, min(1.0, raw_score))
|
||||
|
||||
# --- TIER + SUMMARIES ---
|
||||
tier = map_score_to_tier(normalized_score)
|
||||
summary_short, summary_long, storyline, identity = build_consistency_summaries(
|
||||
normalized_score, components, tier
|
||||
)
|
||||
|
||||
# --- EXTRAS (Spotlight) ---
|
||||
extras = {
|
||||
"floor_value": floor_value,
|
||||
"stability_value": stability_value,
|
||||
"average_value": average_value,
|
||||
"matches_played_current": matches_played_current,
|
||||
}
|
||||
|
||||
return ConsistencyResult(
|
||||
raw_score=raw_score,
|
||||
normalized_score=normalized_score,
|
||||
percentile=percentile,
|
||||
tier=tier,
|
||||
components=components,
|
||||
summary_short=summary_short,
|
||||
summary_long=summary_long,
|
||||
storyline_hook=storyline,
|
||||
identity_signal=identity,
|
||||
extras=extras
|
||||
)
|
||||
31
tests/test_career_db_consistency.py
Normal file
31
tests/test_career_db_consistency.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import json
|
||||
from data.career_db import build_career_database
|
||||
|
||||
def test_career_db_consistency_output(tmp_path):
|
||||
out = tmp_path / "career_stats.json"
|
||||
ok = build_career_database(str(out))
|
||||
assert ok is True
|
||||
|
||||
data = json.loads(out.read_text())
|
||||
players = data["players"]
|
||||
|
||||
# Ensure at least one player has consistency data
|
||||
assert len(players) > 0
|
||||
|
||||
sample = next(iter(players.values()))
|
||||
cons = sample.get("consistency", None)
|
||||
assert cons is not None
|
||||
|
||||
# Check required fields
|
||||
assert "raw" in cons
|
||||
assert "pct" in cons
|
||||
assert "tier" in cons
|
||||
assert "summary" in cons
|
||||
assert "components" in cons
|
||||
assert "extras" in cons
|
||||
|
||||
# Check component structure
|
||||
comps = cons["components"]
|
||||
assert "floor" in comps
|
||||
assert "stability" in comps
|
||||
assert "average" in comps
|
||||
45
tests/test_consistency_math.py
Normal file
45
tests/test_consistency_math.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import math
|
||||
from tags.consistency import compute_consistency_score, build_consistency_tag
|
||||
|
||||
def test_consistency_score_basic():
|
||||
# Simple synthetic match performance scores
|
||||
perfs = [0.60, 0.65, 0.70, 0.75]
|
||||
|
||||
floor = min(perfs)
|
||||
avg = sum(perfs) / len(perfs)
|
||||
variance = sum((x - avg) ** 2 for x in perfs) / len(perfs)
|
||||
adjusted_variance = variance * (1 + 1 / len(perfs))
|
||||
stability = 1 / (1 + adjusted_variance)
|
||||
|
||||
score = compute_consistency_score(
|
||||
floor=floor,
|
||||
stability=stability,
|
||||
average=avg
|
||||
)
|
||||
|
||||
assert 0.0 <= score <= 1.0
|
||||
assert score > 0.60 # should be above floor
|
||||
assert score < 0.80 # should be below average
|
||||
|
||||
|
||||
def test_build_consistency_tag_structure():
|
||||
tag = build_consistency_tag(
|
||||
floor_current=0.60,
|
||||
floor_career=0.60,
|
||||
floor_league=0.50,
|
||||
stability_current=0.80,
|
||||
stability_career=0.80,
|
||||
stability_league=0.70,
|
||||
average_current=0.70,
|
||||
average_career=0.70,
|
||||
average_league=0.60,
|
||||
matches_played_current=10,
|
||||
percentile=None
|
||||
)
|
||||
|
||||
assert hasattr(tag, "normalized_score")
|
||||
assert hasattr(tag, "percentile")
|
||||
assert hasattr(tag, "tier")
|
||||
assert hasattr(tag, "summary_short")
|
||||
assert hasattr(tag, "components")
|
||||
assert hasattr(tag, "extras")
|
||||
17
tests/test_generate_player_tags.py
Normal file
17
tests/test_generate_player_tags.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from dashleague_cast_tool import DashLeagueGUI
|
||||
|
||||
def test_generate_player_tags_consistency():
|
||||
gui = DashLeagueGUI.__new__(DashLeagueGUI) # bypass Tk init
|
||||
|
||||
player = {
|
||||
"slayer": {"strength": "S+"},
|
||||
"consistency": {"raw": 0.82},
|
||||
"objective_payload_components": {"payload_score_raw": 0.55},
|
||||
"objective_domination_components": {"raw": 0.44},
|
||||
"sharpshooter": {"score": 0.66},
|
||||
}
|
||||
|
||||
tags = gui.generate_player_tags(player, map_type="Payload")
|
||||
assert "Cons 0.82" in tags
|
||||
assert "ObjPL 0.55" in tags
|
||||
assert "Sharp 0.66" in tags
|
||||
28
utils/fallback.py
Normal file
28
utils/fallback.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# ============================================================
|
||||
# FALLBACK + EARLY SEASON WEIGHTING
|
||||
# ------------------------------------------------------------
|
||||
# Provides universal fallback logic for all tags:
|
||||
# current → career → league average → neutral baseline
|
||||
# Also provides early-season blending to stabilize noisy stats.
|
||||
# ============================================================
|
||||
|
||||
def get_stat_with_fallback(current, career, league_avg, neutral_baseline=0.5):
|
||||
"""Universal fallback logic used by all tags."""
|
||||
if current is not None:
|
||||
return current
|
||||
if career is not None:
|
||||
return career
|
||||
if league_avg is not None:
|
||||
return league_avg
|
||||
return neutral_baseline
|
||||
|
||||
|
||||
def blend_early_season(current_value, career_value, matches_played_current, ramp_matches=5):
|
||||
"""Blend current season and career stats early in the season."""
|
||||
if matches_played_current <= 0:
|
||||
return career_value
|
||||
|
||||
weight_current = min(matches_played_current / ramp_matches, 1.0)
|
||||
weight_career = 1.0 - weight_current
|
||||
|
||||
return (current_value * weight_current) + (career_value * weight_career)
|
||||
34
utils/summaries.py
Normal file
34
utils/summaries.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# ============================================================
|
||||
# SUMMARY GENERATION
|
||||
# ------------------------------------------------------------
|
||||
# Builds caster-friendly summaries for tags.
|
||||
# ============================================================
|
||||
|
||||
def build_consistency_summaries(score, components, tier):
|
||||
summary_short = (
|
||||
f"Consistency: {score:.2f} ({tier} tier). "
|
||||
f"Floor {components.floor:.2f}, stability {components.stability:.2f}."
|
||||
)
|
||||
|
||||
summary_long = (
|
||||
f"This player shows a {tier}-tier level of consistency ({score:.2f}). "
|
||||
f"Their floor ({components.floor:.2f}) shows how rarely they drop off, "
|
||||
f"while stability ({components.stability:.2f}) reflects match-to-match variance. "
|
||||
f"Their average performance ({components.average_performance:.2f}) "
|
||||
f"sets expectations for today's matchup."
|
||||
)
|
||||
|
||||
if tier in ("S", "A"):
|
||||
storyline = "A rock for their team — expect dependable output every map."
|
||||
identity = "Anchor"
|
||||
elif tier == "B":
|
||||
storyline = "Generally reliable with occasional swings."
|
||||
identity = "Steady"
|
||||
elif tier == "C":
|
||||
storyline = "Inconsistent — performance varies significantly."
|
||||
identity = "Volatile"
|
||||
else:
|
||||
storyline = "Highly unpredictable — a true wildcard."
|
||||
identity = "Wildcard"
|
||||
|
||||
return summary_short, summary_long, storyline, identity
|
||||
16
utils/tiers.py
Normal file
16
utils/tiers.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# ============================================================
|
||||
# TIER MAPPING
|
||||
# ------------------------------------------------------------
|
||||
# Converts normalized tag scores (0–1) into caster-friendly tiers.
|
||||
# ============================================================
|
||||
|
||||
def map_score_to_tier(score: float) -> str:
|
||||
if score >= 0.90:
|
||||
return "S"
|
||||
if score >= 0.75:
|
||||
return "A"
|
||||
if score >= 0.60:
|
||||
return "B"
|
||||
if score >= 0.45:
|
||||
return "C"
|
||||
return "D"
|
||||
Loading…
Reference in a new issue