feat: Initial commit of Clutch-IQ project

This commit is contained in:
xunyulin230420
2026-02-05 23:26:03 +08:00
commit a355239861
66 changed files with 12922 additions and 0 deletions

View File

@@ -0,0 +1,293 @@
"""
Event Processor - Handles kill and bomb events
Responsibilities:
- Process leetify show_event data (kills with score impacts)
- Process classic all_kill and c4_event data
- Generate unique event_ids
- Store twin probability changes (leetify only)
- Handle bomb plant/defuse events
"""
import sqlite3
import json
import logging
import uuid
logger = logging.getLogger(__name__)
class EventProcessor:
@staticmethod
def process_leetify_events(match_data, conn: sqlite3.Connection) -> bool:
"""
Process leetify event data
Args:
match_data: MatchData object with leetify_data parsed
conn: L2 database connection
Returns:
bool: True if successful
"""
try:
if not hasattr(match_data, 'data_leetify') or not match_data.data_leetify:
return True
leetify_data = match_data.data_leetify.get('leetify_data', {})
round_stats = leetify_data.get('round_stat', [])
if not round_stats:
return True
cursor = conn.cursor()
event_count = 0
for r in round_stats:
round_num = r.get('round', 0)
show_events = r.get('show_event', [])
for evt in show_events:
event_type_code = evt.get('event_type', 0)
# event_type: 3=kill, others for bomb/etc
if event_type_code == 3 and evt.get('kill_event'):
# Process kill event
k = evt['kill_event']
event_id = str(uuid.uuid4())
event_time = evt.get('ts', 0)
attacker_steam_id = str(k.get('Killer', ''))
victim_steam_id = str(k.get('Victim', ''))
weapon = k.get('WeaponName', '')
is_headshot = bool(k.get('Headshot', False))
is_wallbang = bool(k.get('Penetrated', False))
is_blind = bool(k.get('AttackerBlind', False))
is_through_smoke = bool(k.get('ThroughSmoke', False))
is_noscope = bool(k.get('NoScope', False))
# Extract assist info
assister_steam_id = None
flash_assist_steam_id = None
trade_killer_steam_id = None
if evt.get('assist_killer_score_change'):
assister_steam_id = str(list(evt['assist_killer_score_change'].keys())[0])
if evt.get('flash_assist_killer_score_change'):
flash_assist_steam_id = str(list(evt['flash_assist_killer_score_change'].keys())[0])
if evt.get('trade_score_change'):
trade_killer_steam_id = str(list(evt['trade_score_change'].keys())[0])
# Extract score changes
score_change_attacker = 0.0
score_change_victim = 0.0
if evt.get('killer_score_change'):
vals = list(evt['killer_score_change'].values())
if vals and isinstance(vals[0], dict):
score_change_attacker = float(vals[0].get('score', 0))
if evt.get('victim_score_change'):
vals = list(evt['victim_score_change'].values())
if vals and isinstance(vals[0], dict):
score_change_victim = float(vals[0].get('score', 0))
# Extract twin (team win probability) changes
twin = evt.get('twin', 0.0)
c_twin = evt.get('c_twin', 0.0)
twin_change = evt.get('twin_change', 0.0)
c_twin_change = evt.get('c_twin_change', 0.0)
cursor.execute('''
INSERT OR REPLACE INTO fact_round_events (
event_id, match_id, round_num, event_type, event_time,
attacker_steam_id, victim_steam_id, assister_steam_id,
flash_assist_steam_id, trade_killer_steam_id, weapon,
is_headshot, is_wallbang, is_blind, is_through_smoke,
is_noscope, score_change_attacker, score_change_victim,
twin, c_twin, twin_change, c_twin_change, data_source_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
event_id, match_data.match_id, round_num, 'kill', event_time,
attacker_steam_id, victim_steam_id, assister_steam_id,
flash_assist_steam_id, trade_killer_steam_id, weapon,
is_headshot, is_wallbang, is_blind, is_through_smoke,
is_noscope, score_change_attacker, score_change_victim,
twin, c_twin, twin_change, c_twin_change, 'leetify'
))
event_count += 1
logger.debug(f"Processed {event_count} leetify events for match {match_data.match_id}")
return True
except Exception as e:
logger.error(f"Error processing leetify events for match {match_data.match_id}: {e}")
import traceback
traceback.print_exc()
return False
@staticmethod
def process_classic_events(match_data, conn: sqlite3.Connection) -> bool:
"""
Process classic event data (all_kill, c4_event)
Args:
match_data: MatchData object with round_list parsed
conn: L2 database connection
Returns:
bool: True if successful
"""
try:
if not hasattr(match_data, 'data_round_list') or not match_data.data_round_list:
return True
round_list = match_data.data_round_list.get('round_list', [])
if not round_list:
return True
cursor = conn.cursor()
event_count = 0
for idx, rd in enumerate(round_list, start=1):
round_num = idx
# Extract round basic info for fact_rounds
current_score = rd.get('current_score', {})
ct_score = current_score.get('ct', 0)
t_score = current_score.get('t', 0)
win_type = current_score.get('type', 0)
pasttime = current_score.get('pasttime', 0)
final_round_time = current_score.get('final_round_time', 0)
# Determine winner_side from win_type
winner_side = _map_win_type_to_side(win_type)
# Insert/update fact_rounds
cursor.execute('''
INSERT OR REPLACE INTO fact_rounds (
match_id, round_num, winner_side, win_reason, win_reason_desc,
duration, ct_score, t_score, end_time_stamp, final_round_time,
pasttime, data_source_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
match_data.match_id, round_num, winner_side, win_type,
_map_win_type_desc(win_type), float(pasttime), ct_score, t_score,
'', final_round_time, pasttime, 'classic'
))
# Process kill events
all_kill = rd.get('all_kill', [])
for kill in all_kill:
event_id = str(uuid.uuid4())
event_time = kill.get('pasttime', 0)
attacker = kill.get('attacker', {})
victim = kill.get('victim', {})
attacker_steam_id = str(attacker.get('steamid_64', ''))
victim_steam_id = str(victim.get('steamid_64', ''))
weapon = kill.get('weapon', '')
is_headshot = bool(kill.get('headshot', False))
is_wallbang = bool(kill.get('penetrated', False))
is_blind = bool(kill.get('attackerblind', False))
is_through_smoke = bool(kill.get('throughsmoke', False))
is_noscope = bool(kill.get('noscope', False))
# Classic has spatial data - will be filled by spatial_processor
# But we still need to insert the event
cursor.execute('''
INSERT OR REPLACE INTO fact_round_events (
event_id, match_id, round_num, event_type, event_time,
attacker_steam_id, victim_steam_id, weapon, is_headshot,
is_wallbang, is_blind, is_through_smoke, is_noscope,
data_source_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
event_id, match_data.match_id, round_num, 'kill', event_time,
attacker_steam_id, victim_steam_id, weapon, is_headshot,
is_wallbang, is_blind, is_through_smoke, is_noscope, 'classic'
))
event_count += 1
# Process bomb events
c4_events = rd.get('c4_event', [])
for c4 in c4_events:
event_id = str(uuid.uuid4())
event_name = c4.get('event_name', '')
event_time = c4.get('pasttime', 0)
steam_id = str(c4.get('steamid_64', ''))
# Map event_name to event_type
if 'plant' in event_name.lower():
event_type = 'bomb_plant'
attacker_steam_id = steam_id
victim_steam_id = None
elif 'defuse' in event_name.lower():
event_type = 'bomb_defuse'
attacker_steam_id = steam_id
victim_steam_id = None
else:
event_type = 'unknown'
attacker_steam_id = steam_id
victim_steam_id = None
cursor.execute('''
INSERT OR REPLACE INTO fact_round_events (
event_id, match_id, round_num, event_type, event_time,
attacker_steam_id, victim_steam_id, data_source_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
event_id, match_data.match_id, round_num, event_type,
event_time, attacker_steam_id, victim_steam_id, 'classic'
))
event_count += 1
logger.debug(f"Processed {event_count} classic events for match {match_data.match_id}")
return True
except Exception as e:
logger.error(f"Error processing classic events for match {match_data.match_id}: {e}")
import traceback
traceback.print_exc()
return False
def _map_win_type_to_side(win_type):
"""Map win_type to winner_side for classic data"""
# Based on CS:GO win types
t_win_types = {1, 8, 12, 17}
ct_win_types = {2, 7, 9, 11}
if win_type in t_win_types:
return 'T'
elif win_type in ct_win_types:
return 'CT'
else:
return 'None'
def _map_win_type_desc(win_type):
"""Map win_type to description"""
type_map = {
0: 'None',
1: 'TargetBombed',
2: 'BombDefused',
7: 'CTsWin',
8: 'TerroristsWin',
9: 'TargetSaved',
11: 'CTSurrender',
12: 'TSurrender',
17: 'TerroristsPlanted'
}
return type_map.get(win_type, f'Unknown_{win_type}')