1.1.0: Updated Profile

This commit is contained in:
2026-01-26 18:36:47 +08:00
parent 727105f11e
commit 8cc359b0ec
20 changed files with 856 additions and 251 deletions

View File

@@ -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