From bc4f62dcf8c42f781bdfbf88bda71f3434e52bc0 Mon Sep 17 00:00:00 2001 From: xbl Date: Mon, 29 Dec 2025 14:00:21 +0100 Subject: [PATCH] lul --- .gitignore | 1 + config.py | 4 +- parser.py | 70 ++++++++++++++++++------- state.py | 21 ++++++++ ui.py | 147 ++++++++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 197 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 3afd689..3136810 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ venv3/ +*.json *.log cvarlist.txt curztest.py diff --git a/config.py b/config.py index 8fcdb8d..e7e1006 100644 --- a/config.py +++ b/config.py @@ -10,10 +10,10 @@ DEFAULT_HOST = 'tcp://127.0.0.1:27961' POLL_TIMEOUT = 100 # UI dimensions -INFO_WINDOW_HEIGHT = 10 +INFO_WINDOW_HEIGHT = 11 INFO_WINDOW_Y = 2 OUTPUT_WINDOW_Y = 12 -INPUT_WINDOW_HEIGHT = 1 +INPUT_WINDOW_HEIGHT = 2 # Event deduplication MAX_RECENT_EVENTS = 10 diff --git a/parser.py b/parser.py index 361be2b..609c751 100644 --- a/parser.py +++ b/parser.py @@ -66,7 +66,7 @@ class EventParser: 'PLAYER_STATS': self._handle_player_stats, 'PLAYER_CONNECT': lambda d: None, 'PLAYER_DISCONNECT': lambda d: None, - 'ROUND_OVER': lambda d: None, + 'ROUND_OVER': self._handle_round_over, } handler = handler_map.get(event_type) @@ -130,61 +130,93 @@ class EventParser: """Handle PLAYER_DEATH and PLAYER_KILL events""" if 'VICTIM' not in data: return None - + victim = data['VICTIM'] victim_name = victim.get('NAME', 'Unknown') - + # Check for duplicate time_val = data.get('TIME', 0) killer_name = data.get('KILLER', {}).get('NAME', '') if data.get('KILLER') else '' - + if self.game_state.event_deduplicator.is_duplicate('PLAYER_DEATH', time_val, killer_name, victim_name): return None - + # Update victim team if 'TEAM' in victim: self.game_state.player_tracker.update_team(victim_name, victim['TEAM']) self.game_state.player_tracker.add_player(victim_name) - + victim_prefix = get_team_prefix(victim_name, self.game_state.player_tracker) warmup = " ^0^3(Warmup)^9" if data.get('WARMUP', False) else "" - + score_prefix = "" + # Environmental death (no killer) if 'KILLER' not in data or not data['KILLER']: + # -1 for environmental death + if not data.get('WARMUP', False): + self.game_state.player_tracker.update_score(victim_name, -1) + score_prefix = "^0^1[-1]^9 " + mod = data.get('MOD', 'UNKNOWN') msg_template = DEATH_MESSAGES.get(mod, "%s^0%s^9 ^1DIED FROM %s^7") - + if mod in DEATH_MESSAGES: msg = msg_template % (victim_prefix, victim_name) else: msg = msg_template % (victim_prefix, victim_name, mod) - - return f"{msg}{warmup}\n" - + + return f"{score_prefix}{msg}{warmup}\n" + # Player killed by another player killer = data['KILLER'] killer_name = killer.get('NAME', 'Unknown') - + # Update killer team if 'TEAM' in killer: self.game_state.player_tracker.update_team(killer_name, killer['TEAM']) self.game_state.player_tracker.add_player(killer_name) - + killer_prefix = get_team_prefix(killer_name, self.game_state.player_tracker) - + # Suicide if killer_name == victim_name: + # -1 for suicide + if not data.get('WARMUP', False): + self.game_state.player_tracker.update_score(victim_name, -1) + score_prefix = "^0^1[-1]^9 " + weapon = killer.get('WEAPON', 'OTHER_WEAPON') if weapon != 'OTHER_WEAPON': weapon_name = WEAPON_NAMES.get(weapon, weapon) - return f"{killer_prefix}^0{killer_name}^9 ^7committed suicide with the ^7{weapon_name}{warmup}\n" + return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7committed suicide with the ^7{weapon_name}{warmup}\n" return None - - # Regular kill + + # Regular kill: +1 for killer + if not data.get('WARMUP', False): + self.game_state.player_tracker.update_score(killer_name, 1) + score_prefix = "^0^2[+1]^9 " + + else: + score_prefix = "" + weapon = killer.get('WEAPON', 'UNKNOWN') weapon_name = WEAPON_KILL_NAMES.get(weapon, f'the {weapon}') - - return f"{killer_prefix}^0{killer_name}^9 ^7fragged^7 {victim_prefix}^0{victim_name}^9 ^7with {weapon_name}{warmup}\n" + + return f"{score_prefix}{killer_prefix}^0{killer_name}^9 ^7fragged^7 {victim_prefix}^0{victim_name}^9 ^7with {weapon_name}{warmup}\n" + + def _handle_round_over(self, data): + """Handle ROUND_OVER events for CA""" + team_won = data.get('TEAM_WON') + round_num = data.get('ROUND', 0) + + if team_won == 'RED': + self.game_state.server_info.red_rounds += 1 + logger.info(f"Round {round_num}: RED wins (RED: {self.game_state.server_info.red_rounds}, BLUE: {self.game_state.server_info.blue_rounds})") + elif team_won == 'BLUE': + self.game_state.server_info.blue_rounds += 1 + logger.info(f"Round {round_num}: BLUE wins (RED: {self.game_state.server_info.red_rounds}, BLUE: {self.game_state.server_info.blue_rounds})") + + return None # Don't display in chat def _handle_medal(self, data): """Handle PLAYER_MEDAL event""" diff --git a/state.py b/state.py index 5508bae..f39d4a8 100644 --- a/state.py +++ b/state.py @@ -25,7 +25,9 @@ class ServerInfo: self.maxclients = '0' self.curclients = '0' self.red_score = 0 + self.red_rounds = 0 self.blue_score = 0 + self.blue_rounds = 0 self.players = [] self.last_update = 0 self.warmup = False @@ -33,6 +35,11 @@ class ServerInfo: def is_team_mode(self): """Check if current gametype is a team mode""" return self.gametype in TEAM_MODES + + def reset_round_scores(self): + """Reset round scores (for new matches)""" + self.red_rounds = 0 + self.blue_rounds = 0 def update_from_cvar(self, cvar_name, value): """Update server info from a cvar response""" @@ -162,6 +169,20 @@ class PlayerTracker: logger.debug(f'Renamed player: {old_name} -> {new_name} (team: {team})') + def update_score(self, name, delta): + """Update player's score by delta (+1 for kill, -1 for death/suicide)""" + clean_name = re.sub(r'\^\d', '', name) + + for player in self.server_info.players: + player_clean = re.sub(r'\^\d', '', player['name']) + if player['name'] == name or player_clean == clean_name: + current_score = int(player.get('score', 0)) + player['score'] = str(current_score + delta) + logger.debug(f"Score update: {name} {delta:+d} -> {player['score']}") + return + + logger.warning(f"Could not update score for {name} - player not found") + class EventDeduplicator: """Prevents duplicate kill/death events""" diff --git a/ui.py b/ui.py index 1b6d178..b637394 100644 --- a/ui.py +++ b/ui.py @@ -97,7 +97,6 @@ class UIManager: curses.use_default_colors() curses.cbreak() curses.curs_set(0) - #curses.setsyx(-1, -1) self.screen.addstr(f"Quake Live PyCon: {self.host}") self.screen.noutrefresh() @@ -308,7 +307,8 @@ class UIManager: # Line 1: Hostname hostname = server_info.hostname - print_colored(self.info_window, f"^3Name:^0 {hostname} ^7\n", 0) + warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9" + print_colored(self.info_window, f"^3Name:^0 {hostname} {warmup_display}\n", 0) # Line 2: Game info gametype = server_info.gametype @@ -328,57 +328,154 @@ class UIManager: teams = game_state.player_tracker.get_players_by_team() if server_info.gametype in TEAM_MODES: - warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9" - print_colored(self.info_window, f"^0^1(RED) ^4(BLUE) ^3(SPEC) {warmup_display}\n", 0) + if server_info.gametype == 'CA': + red_score = f"{server_info.red_rounds:>3} " + blue_score = f"{server_info.blue_rounds:>3} " + + else: + red_total = 0 + blue_total = 0 + for player in server_info.players: + player_name = player['name'] + team = game_state.player_tracker.get_team(player_name) + score = int(player.get('score', 0)) + + if team == 'RED': + red_total += score + elif team == 'BLUE': + blue_total += score + + red_score = f"{red_total:>3} " + blue_score = f"{blue_total:>3} " + + print_colored(self.info_window, f"^0^1RED TEAM: ^7{red_score} ^4BLUE TEAM: ^7{blue_score} ^3(SPEC)\n", 0) - red_players = teams['RED'][:4] - blue_players = teams['BLUE'][:4] + # Sort players by score within each team + red_players_with_scores = [] + blue_players_with_scores = [] + spec_players = [] + + for player_name in teams['RED']: + score = 0 + for player in server_info.players: + if player['name'] == player_name: + score = int(player.get('score', 0)) + break + red_players_with_scores.append((player_name, score)) + + for player_name in teams['BLUE']: + score = 0 + for player in server_info.players: + if player['name'] == player_name: + score = int(player.get('score', 0)) + break + blue_players_with_scores.append((player_name, score)) + + # Sort by score descending + red_players_with_scores.sort(key=lambda x: x[1], reverse=True) + blue_players_with_scores.sort(key=lambda x: x[1], reverse=True) + + red_players = [name for name, score in red_players_with_scores[:4]] + blue_players = [name for name, score in blue_players_with_scores[:4]] spec_players = teams['SPECTATOR'][:4] - + for i in range(4): - red = red_players[i] if i < len(red_players) else '' - blue = blue_players[i] if i < len(blue_players) else '' + red_name = red_players[i] if i < len(red_players) else '' + blue_name = blue_players[i] if i < len(blue_players) else '' spec = spec_players[i] if i < len(spec_players) else '' - - # Strip color codes for padding calculation + + # Get scores for team players + red_score = '' + blue_score = '' + if red_name: + for player in server_info.players: + if player['name'] == red_name: + red_score = player.get('score', '0') + break + if blue_name: + for player in server_info.players: + if player['name'] == blue_name: + blue_score = player.get('score', '0') + break + + # Format: " 9 PlayerName" with right-aligned score + red = f"{red_score:>3} {red_name}" if red_name else '' + blue = f"{blue_score:>3} {blue_name}" if blue_name else '' + from formatter import strip_color_codes red_clean = strip_color_codes(red) blue_clean = strip_color_codes(blue) spec_clean = strip_color_codes(spec) - - # Calculate padding (24 chars per column) + red_pad = 24 - len(red_clean) blue_pad = 24 - len(blue_clean) - + line = f"^0{red}^9{' ' * red_pad}^0{blue}^9{' ' * blue_pad}^0{spec}^9\n" print_colored(self.info_window, line, 0) else: - warmup_display = "^3Warmup: ^2YES^9" if server_info.warmup else "^3Warmup: ^1NO^9" - print_colored(self.info_window, f"^0^5(FREE) ^5(FREE) ^3(SPEC) {warmup_display}\n", 0) + print_colored(self.info_window, f" ^0^5(FREE) ^5(FREE) ^3(SPEC)\n", 0) + # Sort FREE players by score (highest first) free_players = teams['FREE'] + free_players_with_scores = [] + for player_name in free_players: + score = 0 + for player in server_info.players: + if player['name'] == player_name: + score = int(player.get('score', 0)) + break + free_players_with_scores.append((player_name, score)) + + # Sort by score descending + free_players_with_scores.sort(key=lambda x: x[1], reverse=True) + sorted_free_players = [name for name, score in free_players_with_scores] + spec_players = teams['SPECTATOR'][:4] - free_col1 = free_players[:4] - free_col2 = free_players[4:8] - + free_col1 = sorted_free_players[:4] + free_col2 = sorted_free_players[4:8] + for i in range(4): - col1 = free_col1[i] if i < len(free_col1) else '' - col2 = free_col2[i] if i < len(free_col2) else '' + col1_name = free_col1[i] if i < len(free_col1) else '' + col2_name = free_col2[i] if i < len(free_col2) else '' spec = spec_players[i] if i < len(spec_players) else '' - + + # Get scores for FREE players + col1_score = '' + col2_score = '' + if col1_name: + for player in server_info.players: + if player['name'] == col1_name: + col1_score = player.get('score', '0') + break + if col2_name: + for player in server_info.players: + if player['name'] == col2_name: + col2_score = player.get('score', '0') + break + + # Format: " 9 PlayerName" with right-aligned score + col1 = f"{col1_score:>3} {col1_name}" if col1_name else '' + col2 = f"{col2_score:>3} {col2_name}" if col2_name else '' + from formatter import strip_color_codes col1_clean = strip_color_codes(col1) col2_clean = strip_color_codes(col2) spec_clean = strip_color_codes(spec) - + col1_pad = 24 - len(col1_clean) col2_pad = 24 - len(col2_clean) - + line = f"^0{col1}^9{' ' * col1_pad}^0{col2}^9{' ' * col2_pad}^0{spec}^9\n" print_colored(self.info_window, line, 0) # Blank lines to fill - self.info_window.addstr("\n\n") + self.info_window.addstr("\n") + + line=f"^0^3(SPEC)^7^9\n" + print_colored(self.info_window, line, 0) + # Blank lines to fill + self.info_window.addstr("\n") + # Separator separator = "^7" + "═" * (max_x - 1) + "^7" print_colored(self.info_window, separator, 0)