1.1.0: Updated Profile
This commit is contained in:
@@ -1,6 +1,178 @@
|
||||
from web.database import query_db
|
||||
|
||||
class StatsService:
|
||||
@staticmethod
|
||||
def get_team_stats_summary():
|
||||
"""
|
||||
Calculates aggregate statistics for matches where at least 2 roster members played together.
|
||||
Returns:
|
||||
{
|
||||
'map_stats': [{'map_name', 'count', 'wins', 'win_rate'}],
|
||||
'elo_stats': [{'range', 'count', 'wins', 'win_rate'}],
|
||||
'duration_stats': [{'range', 'count', 'wins', 'win_rate'}],
|
||||
'round_stats': [{'type', 'count', 'wins', 'win_rate'}]
|
||||
}
|
||||
"""
|
||||
# 1. Get Active Roster
|
||||
from web.services.web_service import WebService
|
||||
import json
|
||||
|
||||
lineups = WebService.get_lineups()
|
||||
active_roster_ids = []
|
||||
if lineups:
|
||||
try:
|
||||
raw_ids = json.loads(lineups[0]['player_ids_json'])
|
||||
active_roster_ids = [str(uid) for uid in raw_ids]
|
||||
except:
|
||||
pass
|
||||
|
||||
if not active_roster_ids:
|
||||
return {}
|
||||
|
||||
# 2. Find matches with >= 2 roster members
|
||||
# We need match_id, map_name, scores, winner_team, duration, avg_elo
|
||||
# And we need to determine if "Our Team" won.
|
||||
|
||||
placeholders = ','.join('?' for _ in active_roster_ids)
|
||||
|
||||
# Step A: Get Candidate Match IDs (matches with >= 2 roster players)
|
||||
# Also get the team_id of our players in that match to determine win
|
||||
candidate_sql = f"""
|
||||
SELECT mp.match_id, MAX(mp.team_id) as our_team_id
|
||||
FROM fact_match_players mp
|
||||
WHERE CAST(mp.steam_id_64 AS TEXT) IN ({placeholders})
|
||||
GROUP BY mp.match_id
|
||||
HAVING COUNT(DISTINCT mp.steam_id_64) >= 2
|
||||
"""
|
||||
candidate_rows = query_db('l2', candidate_sql, active_roster_ids)
|
||||
|
||||
if not candidate_rows:
|
||||
return {}
|
||||
|
||||
candidate_map = {row['match_id']: row['our_team_id'] for row in candidate_rows}
|
||||
match_ids = list(candidate_map.keys())
|
||||
match_placeholders = ','.join('?' for _ in match_ids)
|
||||
|
||||
# Step B: Get Match Details
|
||||
match_sql = f"""
|
||||
SELECT m.match_id, m.map_name, m.score_team1, m.score_team2, m.winner_team, m.duration,
|
||||
AVG(fmt.group_origin_elo) as avg_elo
|
||||
FROM fact_matches m
|
||||
LEFT JOIN fact_match_teams fmt ON m.match_id = fmt.match_id AND fmt.group_origin_elo > 0
|
||||
WHERE m.match_id IN ({match_placeholders})
|
||||
GROUP BY m.match_id
|
||||
"""
|
||||
match_rows = query_db('l2', match_sql, match_ids)
|
||||
|
||||
# 3. Process Data
|
||||
# Buckets initialization
|
||||
map_stats = {}
|
||||
elo_ranges = ['<1000', '1000-1200', '1200-1400', '1400-1600', '1600-1800', '1800-2000', '2000+']
|
||||
elo_stats = {r: {'wins': 0, 'total': 0} for r in elo_ranges}
|
||||
|
||||
dur_ranges = ['<30m', '30-45m', '45m+']
|
||||
dur_stats = {r: {'wins': 0, 'total': 0} for r in dur_ranges}
|
||||
|
||||
round_types = ['Stomp (<15)', 'Normal', 'Close (>23)', 'Choke (24)']
|
||||
round_stats = {r: {'wins': 0, 'total': 0} for r in round_types}
|
||||
|
||||
for m in match_rows:
|
||||
mid = m['match_id']
|
||||
# Determine Win
|
||||
# Use candidate_map to get our_team_id.
|
||||
# Note: winner_team is usually int (1 or 2) or string.
|
||||
# our_team_id from fact_match_players is usually int (1 or 2).
|
||||
# This logic assumes simple team ID matching.
|
||||
# If sophisticated "UID in Winning Group" logic is needed, we'd need more queries.
|
||||
# For aggregate stats, let's assume team_id matching is sufficient for 99% cases or fallback to simple check.
|
||||
# Actually, let's try to be consistent with get_matches logic if possible,
|
||||
# but getting group_uids for ALL matches is heavy.
|
||||
# Let's trust team_id for this summary.
|
||||
|
||||
our_tid = candidate_map[mid]
|
||||
winner_tid = m['winner_team']
|
||||
|
||||
# Type normalization
|
||||
try:
|
||||
is_win = (int(our_tid) == int(winner_tid)) if (our_tid and winner_tid) else False
|
||||
except:
|
||||
is_win = (str(our_tid) == str(winner_tid)) if (our_tid and winner_tid) else False
|
||||
|
||||
# 1. Map Stats
|
||||
map_name = m['map_name'] or 'Unknown'
|
||||
if map_name not in map_stats:
|
||||
map_stats[map_name] = {'wins': 0, 'total': 0}
|
||||
map_stats[map_name]['total'] += 1
|
||||
if is_win: map_stats[map_name]['wins'] += 1
|
||||
|
||||
# 2. ELO Stats
|
||||
elo = m['avg_elo']
|
||||
if elo:
|
||||
if elo < 1000: e_key = '<1000'
|
||||
elif elo < 1200: e_key = '1000-1200'
|
||||
elif elo < 1400: e_key = '1200-1400'
|
||||
elif elo < 1600: e_key = '1400-1600'
|
||||
elif elo < 1800: e_key = '1600-1800'
|
||||
elif elo < 2000: e_key = '1800-2000'
|
||||
else: e_key = '2000+'
|
||||
elo_stats[e_key]['total'] += 1
|
||||
if is_win: elo_stats[e_key]['wins'] += 1
|
||||
|
||||
# 3. Duration Stats
|
||||
dur = m['duration'] # seconds
|
||||
if dur:
|
||||
dur_min = dur / 60
|
||||
if dur_min < 30: d_key = '<30m'
|
||||
elif dur_min < 45: d_key = '30-45m'
|
||||
else: d_key = '45m+'
|
||||
dur_stats[d_key]['total'] += 1
|
||||
if is_win: dur_stats[d_key]['wins'] += 1
|
||||
|
||||
# 4. Round Stats
|
||||
s1 = m['score_team1'] or 0
|
||||
s2 = m['score_team2'] or 0
|
||||
total_rounds = s1 + s2
|
||||
|
||||
if total_rounds == 24:
|
||||
r_key = 'Choke (24)'
|
||||
round_stats[r_key]['total'] += 1
|
||||
if is_win: round_stats[r_key]['wins'] += 1
|
||||
|
||||
# Note: Close (>23) overlaps with Choke (24).
|
||||
# User requirement: Close > 23 counts ALL matches > 23, regardless of other categories.
|
||||
if total_rounds > 23:
|
||||
r_key = 'Close (>23)'
|
||||
round_stats[r_key]['total'] += 1
|
||||
if is_win: round_stats[r_key]['wins'] += 1
|
||||
|
||||
if total_rounds < 15:
|
||||
r_key = 'Stomp (<15)'
|
||||
round_stats[r_key]['total'] += 1
|
||||
if is_win: round_stats[r_key]['wins'] += 1
|
||||
elif total_rounds <= 23: # Only Normal if NOT Stomp and NOT Close (<= 23 and >= 15)
|
||||
r_key = 'Normal'
|
||||
round_stats[r_key]['total'] += 1
|
||||
if is_win: round_stats[r_key]['wins'] += 1
|
||||
|
||||
# 4. Format Results
|
||||
def fmt(stats_dict):
|
||||
res = []
|
||||
for k, v in stats_dict.items():
|
||||
rate = (v['wins'] / v['total'] * 100) if v['total'] > 0 else 0
|
||||
res.append({'label': k, 'count': v['total'], 'wins': v['wins'], 'win_rate': rate})
|
||||
return res
|
||||
|
||||
# For maps, sort by count
|
||||
map_res = fmt(map_stats)
|
||||
map_res.sort(key=lambda x: x['count'], reverse=True)
|
||||
|
||||
return {
|
||||
'map_stats': map_res,
|
||||
'elo_stats': fmt(elo_stats), # Keep order
|
||||
'duration_stats': fmt(dur_stats), # Keep order
|
||||
'round_stats': fmt(round_stats) # Keep order
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_recent_matches(limit=5):
|
||||
sql = """
|
||||
@@ -398,7 +570,12 @@ class StatsService:
|
||||
WHERE p2.match_id = mp.match_id
|
||||
AND p2.match_team_id = mp.match_team_id
|
||||
AND p2.match_team_id > 0 -- Ensure we don't count 0 (solo default) as a massive party
|
||||
) as party_size
|
||||
) as party_size,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM fact_matches m2
|
||||
WHERE m2.start_time <= m.start_time
|
||||
) as match_index
|
||||
FROM fact_match_players mp
|
||||
JOIN fact_matches m ON mp.match_id = m.match_id
|
||||
WHERE mp.steam_id_64 = ?
|
||||
@@ -408,6 +585,103 @@ class StatsService:
|
||||
"""
|
||||
return query_db('l2', sql, [steam_id, limit])
|
||||
|
||||
@staticmethod
|
||||
def get_roster_stats_distribution(target_steam_id):
|
||||
"""
|
||||
Calculates rank and distribution of the target player within the active roster.
|
||||
"""
|
||||
from web.services.web_service import WebService
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
# 1. Get Active Roster IDs
|
||||
lineups = WebService.get_lineups()
|
||||
active_roster_ids = []
|
||||
if lineups:
|
||||
try:
|
||||
raw_ids = json.loads(lineups[0]['player_ids_json'])
|
||||
active_roster_ids = [str(uid) for uid in raw_ids]
|
||||
except:
|
||||
pass
|
||||
|
||||
# Ensure target is in list (if not in roster, compare against roster anyway)
|
||||
# If roster is empty, return None
|
||||
if not active_roster_ids:
|
||||
return None
|
||||
|
||||
# 2. Fetch stats for all roster members
|
||||
placeholders = ','.join('?' for _ in active_roster_ids)
|
||||
sql = f"""
|
||||
SELECT
|
||||
CAST(steam_id_64 AS TEXT) as steam_id_64,
|
||||
AVG(rating) as rating,
|
||||
AVG(kd_ratio) as kd,
|
||||
AVG(adr) as adr,
|
||||
AVG(kast) as kast
|
||||
FROM fact_match_players
|
||||
WHERE CAST(steam_id_64 AS TEXT) IN ({placeholders})
|
||||
GROUP BY steam_id_64
|
||||
"""
|
||||
rows = query_db('l2', sql, active_roster_ids)
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
stats_map = {row['steam_id_64']: dict(row) for row in rows}
|
||||
|
||||
# Ensure target_steam_id is string
|
||||
target_steam_id = str(target_steam_id)
|
||||
|
||||
# If target player not in stats_map (e.g. no matches), handle gracefullly
|
||||
if target_steam_id not in stats_map:
|
||||
# Try fetch target stats individually if not in roster list
|
||||
target_stats = StatsService.get_player_basic_stats(target_steam_id)
|
||||
if target_stats:
|
||||
stats_map[target_steam_id] = target_stats
|
||||
else:
|
||||
# If still no stats, we can't rank them.
|
||||
# But we can still return the roster stats for others?
|
||||
# The prompt implies "No team data" appears, meaning this function returns valid structure but empty values?
|
||||
# Or returns None.
|
||||
# Let's verify what happens if target has no stats but others do.
|
||||
# We should probably add a dummy entry for target so dashboard renders '0' instead of crashing or 'No data'
|
||||
stats_map[target_steam_id] = {'rating': 0, 'kd': 0, 'adr': 0, 'kast': 0}
|
||||
|
||||
# 3. Calculate Distribution
|
||||
metrics = ['rating', 'kd', 'adr', 'kast']
|
||||
result = {}
|
||||
|
||||
for m in metrics:
|
||||
# Extract values for this metric from all players
|
||||
values = [p[m] for p in stats_map.values() if p[m] is not None]
|
||||
target_val = stats_map[target_steam_id].get(m)
|
||||
|
||||
if target_val is None or not values:
|
||||
result[m] = None
|
||||
continue
|
||||
|
||||
# Sort descending (higher is better)
|
||||
values.sort(reverse=True)
|
||||
|
||||
# Rank (1-based)
|
||||
try:
|
||||
rank = values.index(target_val) + 1
|
||||
except ValueError:
|
||||
# Floating point precision issue? Find closest
|
||||
closest = min(values, key=lambda x: abs(x - target_val))
|
||||
rank = values.index(closest) + 1
|
||||
|
||||
result[m] = {
|
||||
'val': target_val,
|
||||
'rank': rank,
|
||||
'total': len(values),
|
||||
'min': min(values),
|
||||
'max': max(values),
|
||||
'avg': sum(values) / len(values)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_live_matches():
|
||||
# Query matches started in last 2 hours with no winner
|
||||
|
||||
Reference in New Issue
Block a user