CLAUDE: Refactor scorebug display and add dynamic channel visibility
- Created utils/scorebug_helpers.py with shared scorebug functions - create_scorebug_embed(): Unified embed creation for command and background task - create_team_progress_bar(): Win probability visualization - Fixed win probability bar to show dark blocks weighted toward winning team - Arrow extends from the side with advantage - Home winning: "POR ░▓▓▓▓▓▓▓▓▓► WV 95.0%" - Away winning: "POR ◄▓▓▓▓▓▓▓░░░ WV 30.0%" - Changed embed color from score-based to win probability-based - Embed shows color of team favored to win, not necessarily winning - Creates fun psychological element showing momentum/advantage - Added dynamic channel visibility for #live-sba-scores - Channel visible to @everyone when active games exist - Channel hidden when no games are active - Bot retains access via role permissions - Added set_channel_visibility() to utils/discord_helpers.py - Eliminated ~220 lines of duplicate code across files - Removed duplicate embed creation from commands/gameplay/scorebug.py - Removed duplicate embed creation from tasks/live_scorebug_tracker.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
913827b5f3
commit
8907841ec6
@ -7,10 +7,11 @@ import discord
|
|||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
from services.scorebug_service import ScorebugService
|
from services.scorebug_service import ScorebugData, ScorebugService
|
||||||
from services.team_service import team_service
|
from services.team_service import team_service
|
||||||
from utils.logging import get_contextual_logger
|
from utils.logging import get_contextual_logger
|
||||||
from utils.decorators import logged_command
|
from utils.decorators import logged_command
|
||||||
|
from utils.scorebug_helpers import create_scorebug_embed
|
||||||
from views.embeds import EmbedTemplate, EmbedColors
|
from views.embeds import EmbedTemplate, EmbedColors
|
||||||
from exceptions import SheetsException
|
from exceptions import SheetsException
|
||||||
from .scorecard_tracker import ScorecardTracker
|
from .scorecard_tracker import ScorecardTracker
|
||||||
@ -190,8 +191,8 @@ class ScorebugCommands(commands.Cog):
|
|||||||
if scorebug_data.home_team_id:
|
if scorebug_data.home_team_id:
|
||||||
home_team = await team_service.get_team(scorebug_data.home_team_id)
|
home_team = await team_service.get_team(scorebug_data.home_team_id)
|
||||||
|
|
||||||
# Create scorebug embed
|
# Create scorebug embed using shared utility
|
||||||
embed = await self._create_scorebug_embed(
|
embed = create_scorebug_embed(
|
||||||
scorebug_data,
|
scorebug_data,
|
||||||
away_team,
|
away_team,
|
||||||
home_team,
|
home_team,
|
||||||
@ -224,134 +225,6 @@ class ScorebugCommands(commands.Cog):
|
|||||||
)
|
)
|
||||||
await interaction.edit_original_response(content=None, embed=embed)
|
await interaction.edit_original_response(content=None, embed=embed)
|
||||||
|
|
||||||
async def _create_scorebug_embed(
|
|
||||||
self,
|
|
||||||
scorebug_data,
|
|
||||||
away_team,
|
|
||||||
home_team,
|
|
||||||
full_length: bool
|
|
||||||
) -> discord.Embed:
|
|
||||||
"""
|
|
||||||
Create a rich embed from scorebug data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scorebug_data: ScorebugData object
|
|
||||||
away_team: Away team object (optional)
|
|
||||||
home_team: Home team object (optional)
|
|
||||||
full_length: Include full details
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Discord embed with scorebug information
|
|
||||||
"""
|
|
||||||
# Determine winning team for embed color
|
|
||||||
if scorebug_data.away_score > scorebug_data.home_score and away_team:
|
|
||||||
embed_color = away_team.get_color_int()
|
|
||||||
thumbnail_url = away_team.thumbnail if away_team.thumbnail else None
|
|
||||||
elif scorebug_data.home_score > scorebug_data.away_score and home_team:
|
|
||||||
embed_color = home_team.get_color_int()
|
|
||||||
thumbnail_url = home_team.thumbnail if home_team.thumbnail else None
|
|
||||||
else:
|
|
||||||
embed_color = EmbedColors.INFO
|
|
||||||
thumbnail_url = None
|
|
||||||
|
|
||||||
# Create embed with header as title
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=scorebug_data.header,
|
|
||||||
color=embed_color
|
|
||||||
)
|
|
||||||
|
|
||||||
if thumbnail_url:
|
|
||||||
embed.set_thumbnail(url=thumbnail_url)
|
|
||||||
|
|
||||||
# Add score information
|
|
||||||
away_abbrev = away_team.abbrev if away_team else "AWAY"
|
|
||||||
home_abbrev = home_team.abbrev if home_team else "HOME"
|
|
||||||
|
|
||||||
score_text = (
|
|
||||||
f"```\n"
|
|
||||||
f"{away_abbrev:<6} {scorebug_data.away_score:>3}\n"
|
|
||||||
f"{home_abbrev:<6} {scorebug_data.home_score:>3}\n"
|
|
||||||
f"```"
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.add_field(
|
|
||||||
name="Score",
|
|
||||||
value=score_text,
|
|
||||||
inline=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add game state
|
|
||||||
if not scorebug_data.is_final:
|
|
||||||
embed.add_field(
|
|
||||||
name="Status",
|
|
||||||
value=f"**{scorebug_data.which_half}**",
|
|
||||||
inline=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add runners on base if present
|
|
||||||
if scorebug_data.runners and any(scorebug_data.runners):
|
|
||||||
runners_text = self._format_runners(scorebug_data.runners)
|
|
||||||
if runners_text:
|
|
||||||
embed.add_field(
|
|
||||||
name="Runners",
|
|
||||||
value=runners_text,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add matchups if full length
|
|
||||||
if full_length and scorebug_data.matchups and any(scorebug_data.matchups):
|
|
||||||
matchups_text = self._format_matchups(scorebug_data.matchups)
|
|
||||||
if matchups_text:
|
|
||||||
embed.add_field(
|
|
||||||
name="Matchups",
|
|
||||||
value=matchups_text,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add summary if full length
|
|
||||||
if full_length and scorebug_data.summary and any(scorebug_data.summary):
|
|
||||||
summary_text = self._format_summary(scorebug_data.summary)
|
|
||||||
if summary_text:
|
|
||||||
embed.add_field(
|
|
||||||
name="Summary",
|
|
||||||
value=summary_text,
|
|
||||||
inline=False
|
|
||||||
)
|
|
||||||
|
|
||||||
return embed
|
|
||||||
|
|
||||||
def _format_runners(self, runners) -> str:
|
|
||||||
"""Format runners on base for display."""
|
|
||||||
# runners is a list of [runner_name, runner_position] pairs
|
|
||||||
runner_lines = []
|
|
||||||
for runner_data in runners:
|
|
||||||
if runner_data and len(runner_data) >= 2 and runner_data[0]:
|
|
||||||
runner_lines.append(f"**{runner_data[1]}:** {runner_data[0]}")
|
|
||||||
|
|
||||||
return "\n".join(runner_lines) if runner_lines else ""
|
|
||||||
|
|
||||||
def _format_matchups(self, matchups) -> str:
|
|
||||||
"""Format current matchups for display."""
|
|
||||||
# matchups is a list of [batter, pitcher] pairs
|
|
||||||
matchup_lines = []
|
|
||||||
for matchup_data in matchups:
|
|
||||||
if matchup_data and len(matchup_data) >= 2 and matchup_data[0]:
|
|
||||||
matchup_lines.append(f"{matchup_data[0]} vs {matchup_data[1]}")
|
|
||||||
|
|
||||||
return "\n".join(matchup_lines) if matchup_lines else ""
|
|
||||||
|
|
||||||
def _format_summary(self, summary) -> str:
|
|
||||||
"""Format game summary for display."""
|
|
||||||
# summary is a list of summary line pairs
|
|
||||||
summary_lines = []
|
|
||||||
for summary_data in summary:
|
|
||||||
if summary_data and len(summary_data) >= 1 and summary_data[0]:
|
|
||||||
# Join both columns if present
|
|
||||||
line = " - ".join([str(x) for x in summary_data if x])
|
|
||||||
summary_lines.append(line)
|
|
||||||
|
|
||||||
return "\n".join(summary_lines) if summary_lines else ""
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot):
|
async def setup(bot: commands.Bot):
|
||||||
"""Load the scorebug commands cog."""
|
"""Load the scorebug commands cog."""
|
||||||
|
|||||||
@ -22,10 +22,24 @@ class ScorebugData:
|
|||||||
self.away_score = data.get('away_score', 0)
|
self.away_score = data.get('away_score', 0)
|
||||||
self.home_score = data.get('home_score', 0)
|
self.home_score = data.get('home_score', 0)
|
||||||
self.which_half = data.get('which_half', '')
|
self.which_half = data.get('which_half', '')
|
||||||
|
self.inning = data.get('inning', 1)
|
||||||
self.is_final = data.get('is_final', False)
|
self.is_final = data.get('is_final', False)
|
||||||
self.runners = data.get('runners', [])
|
self.outs = data.get('outs', 0)
|
||||||
self.matchups = data.get('matchups', [])
|
self.win_percentage = data.get('win_percentage', 50.0)
|
||||||
self.summary = data.get('summary', [])
|
|
||||||
|
# Current matchup information
|
||||||
|
self.pitcher_name = data.get('pitcher_name', '')
|
||||||
|
self.pitcher_url = data.get('pitcher_url', '')
|
||||||
|
self.pitcher_stats = data.get('pitcher_stats', '')
|
||||||
|
self.batter_name = data.get('batter_name', '')
|
||||||
|
self.batter_url = data.get('batter_url', '')
|
||||||
|
self.batter_stats = data.get('batter_stats', '')
|
||||||
|
self.on_deck_name = data.get('on_deck_name', '')
|
||||||
|
self.in_hole_name = data.get('in_hole_name', '')
|
||||||
|
|
||||||
|
# Additional data
|
||||||
|
self.runners = data.get('runners', []) # [Catcher, On First, On Second, On Third]
|
||||||
|
self.summary = data.get('summary', []) # Play-by-play summary lines
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def score_line(self) -> str:
|
def score_line(self) -> str:
|
||||||
@ -37,6 +51,22 @@ class ScorebugData:
|
|||||||
"""Check if game is currently active (not final)."""
|
"""Check if game is currently active (not final)."""
|
||||||
return not self.is_final
|
return not self.is_final
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_matchup(self) -> str:
|
||||||
|
"""Get formatted current matchup string."""
|
||||||
|
if self.batter_name and self.pitcher_name:
|
||||||
|
return f"{self.batter_name} vs {self.pitcher_name}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def situation(self) -> str:
|
||||||
|
"""Get game situation (outs and runners)."""
|
||||||
|
parts = []
|
||||||
|
if self.outs is not None:
|
||||||
|
outs_text = "out" if self.outs == 1 else "outs"
|
||||||
|
parts.append(f"{self.outs} {outs_text}")
|
||||||
|
return ", ".join(parts) if parts else ""
|
||||||
|
|
||||||
|
|
||||||
class ScorebugService(SheetsService):
|
class ScorebugService(SheetsService):
|
||||||
"""Google Sheets integration for reading live scorebug data."""
|
"""Google Sheets integration for reading live scorebug data."""
|
||||||
@ -69,9 +99,13 @@ class ScorebugService(SheetsService):
|
|||||||
Raises:
|
Raises:
|
||||||
SheetsException: If scorecard cannot be read
|
SheetsException: If scorecard cannot be read
|
||||||
"""
|
"""
|
||||||
|
self.logger.info(f"📖 Reading scorebug data from sheet: {sheet_url_or_key}")
|
||||||
|
self.logger.debug(f" Full length mode: {full_length}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Open scorecard
|
# Open scorecard
|
||||||
scorecard = await self.open_scorecard(sheet_url_or_key)
|
scorecard = await self.open_scorecard(sheet_url_or_key)
|
||||||
|
self.logger.debug(f" ✅ Scorecard opened successfully")
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
@ -88,101 +122,226 @@ class ScorebugService(SheetsService):
|
|||||||
lambda: scorebug_tab.get_values('B2', 'S20', include_tailing_empty_rows=True)
|
lambda: scorebug_tab.get_values('B2', 'S20', include_tailing_empty_rows=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.logger.debug(f"Raw scorebug data (first 10 rows): {all_data[:10]}")
|
self.logger.debug(f"📊 Raw scorebug data dimensions: {len(all_data)} rows")
|
||||||
|
self.logger.debug(f"📊 First row length: {len(all_data[0]) if all_data else 0} columns")
|
||||||
|
self.logger.debug(f"📊 Reading from range B2:S20 (columns B-S = indices 0-17 in data)")
|
||||||
|
self.logger.debug(f"📊 Raw data structure (all rows):")
|
||||||
|
for idx, row in enumerate(all_data):
|
||||||
|
self.logger.debug(f" Row {idx} (Sheet row {idx + 2}): {row}")
|
||||||
|
|
||||||
# Extract game state (B2:G8)
|
# Extract game state (B2:G8)
|
||||||
|
# This corresponds to columns B-G (indices 0-5 in all_data)
|
||||||
|
# Rows 2-8 in sheet (indices 0-6 in all_data)
|
||||||
game_state = [
|
game_state = [
|
||||||
all_data[0][:6], all_data[1][:6], all_data[2][:6], all_data[3][:6],
|
all_data[0][:6], all_data[1][:6], all_data[2][:6], all_data[3][:6],
|
||||||
all_data[4][:6], all_data[5][:6], all_data[6][:6]
|
all_data[4][:6], all_data[5][:6], all_data[6][:6]
|
||||||
]
|
]
|
||||||
|
|
||||||
self.logger.debug(f"Extracted game_state: {game_state}")
|
self.logger.debug(f"🎮 Extracted game_state (B2:G8):")
|
||||||
|
for idx, row in enumerate(game_state):
|
||||||
|
self.logger.debug(f" game_state[{idx}] (Sheet row {idx + 2}): {row}")
|
||||||
|
|
||||||
# Extract team IDs from game_state (already read from Scorebug tab)
|
# Extract team IDs from game_state (already read from Scorebug tab)
|
||||||
# game_state[3] is away team row, game_state[4] is home team row
|
# game_state[3] is away team row (Sheet row 5), game_state[4] is home team row (Sheet row 6)
|
||||||
# First column (index 0) contains the team ID
|
# First column (index 0) contains the team ID - this is column B in the sheet
|
||||||
try:
|
self.logger.debug(f"🏟️ Extracting team IDs from game_state:")
|
||||||
away_team_id = int(game_state[3][0]) if len(game_state) > 3 and len(game_state[3]) > 0 else None
|
self.logger.debug(f" Away team row: game_state[3] = Sheet row 5, column B (index 0)")
|
||||||
home_team_id = int(game_state[4][0]) if len(game_state) > 4 and len(game_state[4]) > 0 else None
|
self.logger.debug(f" Home team row: game_state[4] = Sheet row 6, column B (index 0)")
|
||||||
|
|
||||||
self.logger.debug(f"Parsed team IDs - Away: {away_team_id}, Home: {home_team_id}")
|
try:
|
||||||
|
away_team_id_raw = game_state[3][0] if len(game_state) > 3 and len(game_state[3]) > 0 else None
|
||||||
|
home_team_id_raw = game_state[4][0] if len(game_state) > 4 and len(game_state[4]) > 0 else None
|
||||||
|
|
||||||
|
self.logger.debug(f" Raw away team ID value: '{away_team_id_raw}'")
|
||||||
|
self.logger.debug(f" Raw home team ID value: '{home_team_id_raw}'")
|
||||||
|
|
||||||
|
away_team_id = int(away_team_id_raw) if away_team_id_raw else None
|
||||||
|
home_team_id = int(home_team_id_raw) if home_team_id_raw else None
|
||||||
|
|
||||||
|
self.logger.debug(f" ✅ Parsed team IDs - Away: {away_team_id}, Home: {home_team_id}")
|
||||||
|
|
||||||
if away_team_id is None or home_team_id is None:
|
if away_team_id is None or home_team_id is None:
|
||||||
raise ValueError(f'Team IDs not found in scorebug (away: {away_team_id}, home: {home_team_id})')
|
raise ValueError(f'Team IDs not found in scorebug (away: {away_team_id}, home: {home_team_id})')
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
self.logger.error(f"Failed to parse team IDs from scorebug: {e}")
|
self.logger.error(f"❌ Failed to parse team IDs from scorebug: {e}")
|
||||||
raise ValueError(f'Could not extract team IDs from scorecard')
|
raise ValueError(f'Could not extract team IDs from scorecard')
|
||||||
|
|
||||||
# Parse game state
|
# Parse game state
|
||||||
|
self.logger.debug(f"📝 Parsing header from game_state[0][0] (Sheet B2):")
|
||||||
header = game_state[0][0] if game_state[0] else ''
|
header = game_state[0][0] if game_state[0] else ''
|
||||||
is_final = header[-5:] == 'FINAL' if header else False
|
is_final = header[-5:] == 'FINAL' if header else False
|
||||||
|
self.logger.debug(f" Header value: '{header}'")
|
||||||
self.logger.debug(f"Header: '{header}', Is Final: {is_final}")
|
self.logger.debug(f" Is Final: {is_final}")
|
||||||
self.logger.debug(f"Away team row (game_state[3]): {game_state[3] if len(game_state) > 3 else 'N/A'}")
|
|
||||||
self.logger.debug(f"Home team row (game_state[4]): {game_state[4] if len(game_state) > 4 else 'N/A'}")
|
|
||||||
|
|
||||||
# Parse scores with validation
|
# Parse scores with validation
|
||||||
|
self.logger.debug(f"⚾ Parsing scores:")
|
||||||
|
self.logger.debug(f" Away score: game_state[3][2] (Sheet row 5, column D)")
|
||||||
|
self.logger.debug(f" Home score: game_state[4][2] (Sheet row 6, column D)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
away_score_raw = game_state[3][2] if len(game_state) > 3 and len(game_state[3]) > 2 else '0'
|
away_score_raw = game_state[3][2] if len(game_state) > 3 and len(game_state[3]) > 2 else '0'
|
||||||
self.logger.debug(f"Raw away score value: '{away_score_raw}'")
|
self.logger.debug(f" Raw away score value: '{away_score_raw}' (type: {type(away_score_raw).__name__})")
|
||||||
away_score = int(away_score_raw)
|
away_score = int(away_score_raw) if away_score_raw != '' else 0
|
||||||
|
self.logger.debug(f" ✅ Parsed away score: {away_score}")
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
self.logger.warning(f"Failed to parse away score: {e}")
|
self.logger.warning(f" ⚠️ Failed to parse away score: {e}")
|
||||||
away_score = 0
|
away_score = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
home_score_raw = game_state[4][2] if len(game_state) > 4 and len(game_state[4]) > 2 else '0'
|
home_score_raw = game_state[4][2] if len(game_state) > 4 and len(game_state[4]) > 2 else '0'
|
||||||
self.logger.debug(f"Raw home score value: '{home_score_raw}'")
|
self.logger.debug(f" Raw home score value: '{home_score_raw}' (type: {type(home_score_raw).__name__})")
|
||||||
home_score = int(home_score_raw)
|
home_score = int(home_score_raw) if home_score_raw != '' else 0
|
||||||
|
self.logger.debug(f" ✅ Parsed home score: {home_score}")
|
||||||
except (ValueError, IndexError) as e:
|
except (ValueError, IndexError) as e:
|
||||||
self.logger.warning(f"Failed to parse home score: {e}")
|
self.logger.warning(f" ⚠️ Failed to parse home score: {e}")
|
||||||
home_score = 0
|
home_score = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
inning_raw = game_state[3][5] if len(game_state) > 3 and len(game_state[3]) > 5 else '0'
|
||||||
|
self.logger.debug(f" Raw inning value: '{inning_raw}' (type: {type(inning_raw).__name__})")
|
||||||
|
inning = int(inning_raw) if inning_raw != '' else 1
|
||||||
|
self.logger.debug(f" ✅ Parsed inning: {inning}")
|
||||||
|
except (ValueError, IndexError) as e:
|
||||||
|
self.logger.warning(f" ⚠️ Failed to parse home score: {e}")
|
||||||
|
inning = 1
|
||||||
|
|
||||||
|
self.logger.debug(f"⏱️ Parsing game state from game_state[3][4] (Sheet row 5, column F):")
|
||||||
which_half = game_state[3][4] if len(game_state) > 3 and len(game_state[3]) > 4 else ''
|
which_half = game_state[3][4] if len(game_state) > 3 and len(game_state[3]) > 4 else ''
|
||||||
|
self.logger.debug(f" Which half value: '{which_half}'")
|
||||||
|
|
||||||
self.logger.debug(f"Parsed values - Away: {away_score}, Home: {home_score}, Which Half: '{which_half}'")
|
# Parse outs from all_data[4][4] (Sheet F6 - columns start at B, so F=index 4)
|
||||||
|
self.logger.debug(f"🔢 Parsing outs from F6 (all_data[4][4]):")
|
||||||
|
try:
|
||||||
|
outs_raw = all_data[4][4] if len(all_data) > 4 and len(all_data[4]) > 4 else '0'
|
||||||
|
self.logger.debug(f" Raw outs value: '{outs_raw}'")
|
||||||
|
# Handle "2" or any number
|
||||||
|
outs = int(outs_raw) if outs_raw and str(outs_raw).strip() else 0
|
||||||
|
self.logger.debug(f" ✅ Parsed outs: {outs}")
|
||||||
|
except (ValueError, IndexError, AttributeError) as e:
|
||||||
|
self.logger.warning(f" ⚠️ Failed to parse outs: {e}")
|
||||||
|
outs = 0
|
||||||
|
|
||||||
# Extract runners (K11:L14 → offset in all_data)
|
# Extract matchup information - K3:O6 (rows 3-6, columns K-O)
|
||||||
runners = [
|
# In all_data: rows 1-4 (sheet rows 3-6), columns 9-13 (sheet columns K-O)
|
||||||
all_data[9][9:11] if len(all_data) > 9 else [],
|
self.logger.debug(f"⚔️ Extracting matchups from K3:O6:")
|
||||||
all_data[10][9:11] if len(all_data) > 10 else [],
|
matchups = [
|
||||||
all_data[11][9:11] if len(all_data) > 11 else [],
|
all_data[1][9:14] if len(all_data) > 1 else [], # Pitcher (row 3)
|
||||||
all_data[12][9:11] if len(all_data) > 12 else []
|
all_data[2][9:14] if len(all_data) > 2 else [], # Batter (row 4)
|
||||||
|
all_data[3][9:14] if len(all_data) > 3 else [], # On Deck (row 5)
|
||||||
|
all_data[4][9:14] if len(all_data) > 4 else [], # In Hole (row 6)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Extract matchups if full_length (M11:N14 → offset in all_data)
|
# Pitcher: matchups[0][0]=name, [1]=URL, [2]=stats
|
||||||
matchups = []
|
pitcher_name = matchups[0][0] if len(matchups[0]) > 0 else ''
|
||||||
if full_length:
|
pitcher_url = matchups[0][1] if len(matchups[0]) > 1 else ''
|
||||||
matchups = [
|
pitcher_stats = matchups[0][2] if len(matchups[0]) > 2 else ''
|
||||||
all_data[9][11:13] if len(all_data) > 9 else [],
|
self.logger.debug(f" Pitcher: {pitcher_name} | {pitcher_stats} | {pitcher_url}")
|
||||||
all_data[10][11:13] if len(all_data) > 10 else [],
|
|
||||||
all_data[11][11:13] if len(all_data) > 11 else [],
|
|
||||||
all_data[12][11:13] if len(all_data) > 12 else []
|
|
||||||
]
|
|
||||||
|
|
||||||
# Extract summary if full_length (Q11:R14 → offset in all_data)
|
# Batter: matchups[1][0]=name, [1]=URL, [2]=stats, [3]=order, [4]=position
|
||||||
|
batter_name = matchups[1][0] if len(matchups[1]) > 0 else ''
|
||||||
|
batter_url = matchups[1][1] if len(matchups[1]) > 1 else ''
|
||||||
|
batter_stats = matchups[1][2] if len(matchups[1]) > 2 else ''
|
||||||
|
self.logger.debug(f" Batter: {batter_name} | {batter_stats} | {batter_url}")
|
||||||
|
|
||||||
|
# On Deck: matchups[2][0]=name
|
||||||
|
on_deck_name = matchups[2][0] if len(matchups[2]) > 0 else ''
|
||||||
|
on_deck_url = matchups[2][1] if len(matchups[2]) > 1 else ''
|
||||||
|
self.logger.debug(f" On Deck: {on_deck_name}")
|
||||||
|
|
||||||
|
# In Hole: matchups[3][0]=name
|
||||||
|
in_hole_name = matchups[3][0] if len(matchups[3]) > 0 else ''
|
||||||
|
in_hole_url = matchups[3][1] if len(matchups[3]) > 1 else ''
|
||||||
|
self.logger.debug(f" In Hole: {in_hole_name}")
|
||||||
|
|
||||||
|
# Parse win percentage from all_data[6][2] (Sheet D8 - row 8, column D)
|
||||||
|
self.logger.debug(f"📈 Parsing win percentage from D8 (all_data[6][2]):")
|
||||||
|
try:
|
||||||
|
win_pct_raw = all_data[6][2] if len(all_data) > 6 and len(all_data[6]) > 2 else '50%'
|
||||||
|
self.logger.debug(f" Raw win percentage value: '{win_pct_raw}'")
|
||||||
|
# Remove % sign if present and convert to float
|
||||||
|
win_pct_str = str(win_pct_raw).replace('%', '').strip()
|
||||||
|
win_percentage = float(win_pct_str) if win_pct_str else 50.0
|
||||||
|
self.logger.debug(f" ✅ Parsed win percentage: {win_percentage}%")
|
||||||
|
except (ValueError, IndexError, AttributeError) as e:
|
||||||
|
self.logger.warning(f" ⚠️ Failed to parse win percentage: {e}")
|
||||||
|
win_percentage = 50.0
|
||||||
|
|
||||||
|
self.logger.debug(f"📊 Final parsed values:")
|
||||||
|
self.logger.debug(f" Away team {away_team_id}: {away_score}")
|
||||||
|
self.logger.debug(f" Home team {home_team_id}: {home_score}")
|
||||||
|
self.logger.debug(f" Game state: '{which_half}'")
|
||||||
|
self.logger.debug(f" Outs: {outs}")
|
||||||
|
self.logger.debug(f" Win percentage: {win_percentage}%")
|
||||||
|
self.logger.debug(f" Current matchup: {batter_name} vs {pitcher_name}")
|
||||||
|
self.logger.debug(f" Status: {'FINAL' if is_final else 'IN PROGRESS'}")
|
||||||
|
|
||||||
|
# Extract runners - K11:L14 (rows 11-14, columns K-L)
|
||||||
|
# In all_data: rows 9-12 (sheet rows 11-14), columns 9-10 (sheet columns K-L)
|
||||||
|
# runners[0] = Catcher, runners[1] = On First, runners[2] = On Second, runners[3] = On Third
|
||||||
|
# Each runner is [name, URL]
|
||||||
|
self.logger.debug(f"🏃 Extracting runners from K11:L14:")
|
||||||
|
runners = [
|
||||||
|
all_data[9][9:11] if len(all_data) > 9 else [], # Catcher (row 11)
|
||||||
|
all_data[10][9:11] if len(all_data) > 10 else [], # On First (row 12)
|
||||||
|
all_data[11][9:11] if len(all_data) > 11 else [], # On Second (row 13)
|
||||||
|
all_data[12][9:11] if len(all_data) > 12 else [] # On Third (row 14)
|
||||||
|
]
|
||||||
|
self.logger.debug(f" Catcher: {runners[0]}")
|
||||||
|
self.logger.debug(f" On First: {runners[1]}")
|
||||||
|
self.logger.debug(f" On Second: {runners[2]}")
|
||||||
|
self.logger.debug(f" On Third: {runners[3]}")
|
||||||
|
|
||||||
|
# Extract summary if full_length (R3:S20 for inning-by-inning plays)
|
||||||
|
# This is the "Play by Play" summary section
|
||||||
summary = []
|
summary = []
|
||||||
if full_length:
|
if full_length:
|
||||||
summary = [
|
self.logger.debug(f"📋 Extracting summary from R3:S20:")
|
||||||
all_data[9][15:17] if len(all_data) > 9 else [],
|
# R3:S20 is columns 16-17 (R-S), rows 3-20 (indices 1-18)
|
||||||
all_data[10][15:17] if len(all_data) > 10 else [],
|
for row_idx in range(1, min(19, len(all_data))):
|
||||||
all_data[11][15:17] if len(all_data) > 11 else [],
|
if len(all_data[row_idx]) > 17:
|
||||||
all_data[12][15:17] if len(all_data) > 12 else []
|
play_line = [all_data[row_idx][16], all_data[row_idx][17]]
|
||||||
]
|
if play_line[0] or play_line[1]: # Only add if not empty
|
||||||
|
summary.append(play_line)
|
||||||
|
self.logger.debug(f" Found {len(summary)} summary lines")
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"📝 Skipping summary (compact view)")
|
||||||
|
|
||||||
return ScorebugData({
|
self.logger.debug(f"✅ Scorebug data extraction complete!")
|
||||||
|
|
||||||
|
scorebug_data = ScorebugData({
|
||||||
'away_team_id': away_team_id,
|
'away_team_id': away_team_id,
|
||||||
'home_team_id': home_team_id,
|
'home_team_id': home_team_id,
|
||||||
'header': header,
|
'header': header,
|
||||||
'away_score': away_score,
|
'away_score': away_score,
|
||||||
'home_score': home_score,
|
'home_score': home_score,
|
||||||
'which_half': which_half,
|
'which_half': which_half,
|
||||||
|
'inning': inning,
|
||||||
'is_final': is_final,
|
'is_final': is_final,
|
||||||
'runners': runners,
|
'outs': outs,
|
||||||
'matchups': matchups,
|
'win_percentage': win_percentage,
|
||||||
'summary': summary
|
'pitcher_name': pitcher_name,
|
||||||
|
'pitcher_url': pitcher_url,
|
||||||
|
'pitcher_stats': pitcher_stats,
|
||||||
|
'batter_name': batter_name,
|
||||||
|
'batter_url': batter_url,
|
||||||
|
'batter_stats': batter_stats,
|
||||||
|
'on_deck_name': on_deck_name,
|
||||||
|
'in_hole_name': in_hole_name,
|
||||||
|
'runners': runners, # [Catcher, On First, On Second, On Third], each is [name, URL]
|
||||||
|
'summary': summary # Play-by-play lines from R3:S20
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.logger.debug(f"🎯 Created ScorebugData object:")
|
||||||
|
self.logger.debug(f" Away Team ID: {scorebug_data.away_team_id}")
|
||||||
|
self.logger.debug(f" Home Team ID: {scorebug_data.home_team_id}")
|
||||||
|
self.logger.debug(f" Header: '{scorebug_data.header}'")
|
||||||
|
self.logger.debug(f" Score Line: {scorebug_data.score_line}")
|
||||||
|
self.logger.debug(f" Which Half: '{scorebug_data.which_half}'")
|
||||||
|
self.logger.debug(f" Is Final: {scorebug_data.is_final}")
|
||||||
|
self.logger.debug(f" Is Active: {scorebug_data.is_active}")
|
||||||
|
|
||||||
|
return scorebug_data
|
||||||
|
|
||||||
except pygsheets.WorksheetNotFound:
|
except pygsheets.WorksheetNotFound:
|
||||||
self.logger.error(f"Scorebug tab not found in scorecard")
|
self.logger.error(f"Scorebug tab not found in scorecard")
|
||||||
raise SheetsException("Scorebug tab not found. Is this a valid scorecard?")
|
raise SheetsException("Scorebug tab not found. Is this a valid scorecard?")
|
||||||
|
|||||||
@ -10,6 +10,8 @@ from discord.ext import tasks, commands
|
|||||||
|
|
||||||
from models.team import Team
|
from models.team import Team
|
||||||
from utils.logging import get_contextual_logger
|
from utils.logging import get_contextual_logger
|
||||||
|
from utils.scorebug_helpers import create_scorebug_embed
|
||||||
|
from utils.discord_helpers import set_channel_visibility
|
||||||
from services.scorebug_service import ScorebugData, ScorebugService
|
from services.scorebug_service import ScorebugData, ScorebugService
|
||||||
from services.team_service import team_service
|
from services.team_service import team_service
|
||||||
from commands.gameplay.scorecard_tracker import ScorecardTracker
|
from commands.gameplay.scorecard_tracker import ScorecardTracker
|
||||||
@ -92,8 +94,13 @@ class LiveScorebugTracker:
|
|||||||
all_scorecards = self.scorecard_tracker.get_all_scorecards()
|
all_scorecards = self.scorecard_tracker.get_all_scorecards()
|
||||||
|
|
||||||
if not all_scorecards:
|
if not all_scorecards:
|
||||||
# No active scorebugs - clear the channel
|
# No active scorebugs - clear the channel and hide it
|
||||||
await self._clear_live_scores_channel(live_scores_channel)
|
await self._clear_live_scores_channel(live_scores_channel)
|
||||||
|
await set_channel_visibility(
|
||||||
|
live_scores_channel,
|
||||||
|
visible=False,
|
||||||
|
reason="No active games"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Read all scorebugs and create embeds
|
# Read all scorebugs and create embeds
|
||||||
@ -114,11 +121,12 @@ class LiveScorebugTracker:
|
|||||||
if away_team is None or home_team is None:
|
if away_team is None or home_team is None:
|
||||||
raise ValueError(f'Error looking up teams in scorecard; IDs provided: {scorebug_data.away_team_id} & {scorebug_data.home_team_id}')
|
raise ValueError(f'Error looking up teams in scorecard; IDs provided: {scorebug_data.away_team_id} & {scorebug_data.home_team_id}')
|
||||||
|
|
||||||
# Create compact embed
|
# Create compact embed using shared utility
|
||||||
embed = await self._create_compact_scorebug_embed(
|
embed = create_scorebug_embed(
|
||||||
scorebug_data,
|
scorebug_data,
|
||||||
away_team,
|
away_team,
|
||||||
home_team
|
home_team,
|
||||||
|
full_length=False # Compact view for live channel
|
||||||
)
|
)
|
||||||
|
|
||||||
active_scorebugs.append(embed)
|
active_scorebugs.append(embed)
|
||||||
@ -140,66 +148,20 @@ class LiveScorebugTracker:
|
|||||||
|
|
||||||
# Update live scores channel
|
# Update live scores channel
|
||||||
if active_scorebugs:
|
if active_scorebugs:
|
||||||
|
await set_channel_visibility(
|
||||||
|
live_scores_channel,
|
||||||
|
visible=True,
|
||||||
|
reason="Active games in progress"
|
||||||
|
)
|
||||||
await self._post_scorebugs_to_channel(live_scores_channel, active_scorebugs)
|
await self._post_scorebugs_to_channel(live_scores_channel, active_scorebugs)
|
||||||
else:
|
else:
|
||||||
# All games finished - clear the channel
|
# All games finished - clear the channel and hide it
|
||||||
await self._clear_live_scores_channel(live_scores_channel)
|
await self._clear_live_scores_channel(live_scores_channel)
|
||||||
|
await set_channel_visibility(
|
||||||
async def _create_compact_scorebug_embed(
|
live_scores_channel,
|
||||||
self,
|
visible=False,
|
||||||
scorebug_data,
|
reason="No active games"
|
||||||
away_team: Team,
|
)
|
||||||
home_team: Team
|
|
||||||
) -> discord.Embed:
|
|
||||||
"""
|
|
||||||
Create a compact scorebug embed for the live channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scorebug_data: ScorebugData object
|
|
||||||
away_team: Away team object (optional)
|
|
||||||
home_team: Home team object (optional)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Discord embed with compact scorebug
|
|
||||||
"""
|
|
||||||
# Determine winning team for embed color
|
|
||||||
if scorebug_data.away_score > scorebug_data.home_score and away_team:
|
|
||||||
embed_color = away_team.get_color_int()
|
|
||||||
elif scorebug_data.home_score > scorebug_data.away_score and home_team:
|
|
||||||
embed_color = home_team.get_color_int()
|
|
||||||
else:
|
|
||||||
embed_color = EmbedColors.INFO
|
|
||||||
|
|
||||||
# Create compact embed
|
|
||||||
embed = discord.Embed(
|
|
||||||
title=scorebug_data.header,
|
|
||||||
color=embed_color
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add score
|
|
||||||
away_abbrev = away_team.abbrev if away_team else "AWAY"
|
|
||||||
home_abbrev = home_team.abbrev if home_team else "HOME"
|
|
||||||
|
|
||||||
score_text = (
|
|
||||||
f"```\n"
|
|
||||||
f"{away_abbrev:<4} {scorebug_data.away_score:>2}\n"
|
|
||||||
f"{home_abbrev:<4} {scorebug_data.home_score:>2}\n"
|
|
||||||
f"```"
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.add_field(
|
|
||||||
name="Score",
|
|
||||||
value=score_text,
|
|
||||||
inline=True
|
|
||||||
)
|
|
||||||
|
|
||||||
embed.add_field(
|
|
||||||
name="Status",
|
|
||||||
value=f"**{scorebug_data.which_half}**",
|
|
||||||
inline=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return embed
|
|
||||||
|
|
||||||
async def _post_scorebugs_to_channel(
|
async def _post_scorebugs_to_channel(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -119,3 +119,55 @@ def format_key_plays(
|
|||||||
key_plays_text += f"{play_description}\n"
|
key_plays_text += f"{play_description}\n"
|
||||||
|
|
||||||
return key_plays_text
|
return key_plays_text
|
||||||
|
|
||||||
|
|
||||||
|
async def set_channel_visibility(
|
||||||
|
channel: discord.TextChannel,
|
||||||
|
visible: bool,
|
||||||
|
reason: Optional[str] = None
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Set channel visibility for @everyone.
|
||||||
|
|
||||||
|
The bot's permissions are based on its role, not @everyone, so the bot
|
||||||
|
will retain access even when @everyone view permission is removed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Discord text channel to modify
|
||||||
|
visible: If True, grant @everyone view permission; if False, deny it
|
||||||
|
reason: Optional reason for audit log
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if permissions updated successfully, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
guild = channel.guild
|
||||||
|
everyone_role = guild.default_role
|
||||||
|
|
||||||
|
if visible:
|
||||||
|
# Grant @everyone permission to view channel
|
||||||
|
default_reason = "Channel made visible to all members"
|
||||||
|
await channel.set_permissions(
|
||||||
|
everyone_role,
|
||||||
|
view_channel=True,
|
||||||
|
reason=reason or default_reason
|
||||||
|
)
|
||||||
|
logger.info(f"Set #{channel.name} to VISIBLE for @everyone")
|
||||||
|
else:
|
||||||
|
# Remove @everyone view permission
|
||||||
|
default_reason = "Channel hidden from members"
|
||||||
|
await channel.set_permissions(
|
||||||
|
everyone_role,
|
||||||
|
view_channel=False,
|
||||||
|
reason=reason or default_reason
|
||||||
|
)
|
||||||
|
logger.info(f"Set #{channel.name} to HIDDEN for @everyone")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except discord.Forbidden:
|
||||||
|
logger.error(f"Missing permissions to modify #{channel.name} permissions")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting channel visibility for #{channel.name}: {e}")
|
||||||
|
return False
|
||||||
|
|||||||
207
utils/scorebug_helpers.py
Normal file
207
utils/scorebug_helpers.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
"""
|
||||||
|
Scorebug Display Helpers
|
||||||
|
|
||||||
|
Utility functions for formatting and displaying scorebug information.
|
||||||
|
"""
|
||||||
|
import discord
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from views.embeds import EmbedColors
|
||||||
|
|
||||||
|
|
||||||
|
def create_scorebug_embed(
|
||||||
|
scorebug_data,
|
||||||
|
away_team,
|
||||||
|
home_team,
|
||||||
|
full_length: bool = True
|
||||||
|
) -> discord.Embed:
|
||||||
|
"""
|
||||||
|
Create a rich embed from scorebug data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scorebug_data: ScorebugData object with game information
|
||||||
|
away_team: Away team object (optional)
|
||||||
|
home_team: Home team object (optional)
|
||||||
|
full_length: If True, includes pitcher/batter/runners/summary; if False, compact view
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Discord embed with scorebug information
|
||||||
|
"""
|
||||||
|
# Determine embed color based on win probability (not score!)
|
||||||
|
# This creates a fun twist where the favored team's color shows,
|
||||||
|
# even if they're currently losing
|
||||||
|
if scorebug_data.win_percentage > 50 and home_team:
|
||||||
|
embed_color = home_team.get_color_int() # Home team favored
|
||||||
|
elif scorebug_data.win_percentage < 50 and away_team:
|
||||||
|
embed_color = away_team.get_color_int() # Away team favored
|
||||||
|
else:
|
||||||
|
embed_color = EmbedColors.INFO # Even game (50/50)
|
||||||
|
|
||||||
|
# Create embed with header as title
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=scorebug_data.header,
|
||||||
|
color=embed_color
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get team abbreviations for use throughout
|
||||||
|
away_abbrev = away_team.abbrev if away_team else "AWAY"
|
||||||
|
home_abbrev = home_team.abbrev if home_team else "HOME"
|
||||||
|
|
||||||
|
# Create ASCII scorebug with bases visualization
|
||||||
|
occupied = '●'
|
||||||
|
unoccupied = '○'
|
||||||
|
|
||||||
|
# runners[0]=Catcher, [1]=On First, [2]=On Second, [3]=On Third
|
||||||
|
first_base = unoccupied if not scorebug_data.runners[1] or not scorebug_data.runners[1][0] else occupied
|
||||||
|
second_base = unoccupied if not scorebug_data.runners[2] or not scorebug_data.runners[2][0] else occupied
|
||||||
|
third_base = unoccupied if not scorebug_data.runners[3] or not scorebug_data.runners[3][0] else occupied
|
||||||
|
half = '▲' if scorebug_data.which_half == 'Top' else '▼'
|
||||||
|
|
||||||
|
if not scorebug_data.is_final:
|
||||||
|
inning = f'{half} {scorebug_data.inning}'
|
||||||
|
outs = f'{scorebug_data.outs} Out{"s" if scorebug_data.outs != 1 else ""}'
|
||||||
|
else:
|
||||||
|
# Final inning display
|
||||||
|
final_inning = scorebug_data.inning if scorebug_data.which_half == "Bot" else scorebug_data.inning - 1
|
||||||
|
inning = f'F/{final_inning}'
|
||||||
|
outs = ''
|
||||||
|
|
||||||
|
game_state_text = (
|
||||||
|
f'```\n'
|
||||||
|
f'{away_abbrev: ^4}{scorebug_data.away_score: ^3} {second_base}{inning: >10}\n'
|
||||||
|
f'{home_abbrev: ^4}{scorebug_data.home_score: ^3} {third_base} {first_base}{outs: >8}\n'
|
||||||
|
f'```'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add win probability bar
|
||||||
|
embed.add_field(
|
||||||
|
name='Win Probability',
|
||||||
|
value=create_team_progress_bar(
|
||||||
|
scorebug_data.win_percentage,
|
||||||
|
away_abbrev,
|
||||||
|
home_abbrev
|
||||||
|
),
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add game state
|
||||||
|
embed.add_field(
|
||||||
|
name='Game State',
|
||||||
|
value=game_state_text,
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# If not full_length, return compact version
|
||||||
|
if not full_length:
|
||||||
|
return embed
|
||||||
|
|
||||||
|
# Full length - add pitcher and batter info
|
||||||
|
if scorebug_data.pitcher_name:
|
||||||
|
embed.add_field(
|
||||||
|
name='Pitcher',
|
||||||
|
value=f'[{scorebug_data.pitcher_name}]({scorebug_data.pitcher_url})\n{scorebug_data.pitcher_stats}',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if scorebug_data.batter_name:
|
||||||
|
embed.add_field(
|
||||||
|
name='Batter',
|
||||||
|
value=f'[{scorebug_data.batter_name}]({scorebug_data.batter_url})\n{scorebug_data.batter_stats}',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add baserunners if present
|
||||||
|
on_first = scorebug_data.runners[1] if scorebug_data.runners[1] else ''
|
||||||
|
on_second = scorebug_data.runners[2] if scorebug_data.runners[2] else ''
|
||||||
|
on_third = scorebug_data.runners[3] if scorebug_data.runners[3] else ''
|
||||||
|
have_baserunners = len(on_first[0]) + len(on_second[0]) + len(on_third[0]) > 0
|
||||||
|
|
||||||
|
if have_baserunners > 0:
|
||||||
|
br_string = ''
|
||||||
|
if len(on_first) > 0:
|
||||||
|
br_string += f'On First: [{on_first[0]}]({on_first[1]})\n'
|
||||||
|
if len(on_second) > 0:
|
||||||
|
br_string += f'On Second: [{on_second[0]}]({on_second[1]})\n'
|
||||||
|
if len(on_third) > 0:
|
||||||
|
br_string += f'On Third: [{on_third[0]}]({on_third[1]})\n'
|
||||||
|
|
||||||
|
embed.add_field(name=' ', value=' ', inline=False) # Spacer
|
||||||
|
embed.add_field(
|
||||||
|
name='Baserunners',
|
||||||
|
value=br_string,
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add catcher
|
||||||
|
if scorebug_data.runners[0] and scorebug_data.runners[0][0]:
|
||||||
|
embed.add_field(
|
||||||
|
name='Catcher',
|
||||||
|
value=f'[{scorebug_data.runners[0][0]}]({scorebug_data.runners[0][1]})',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add inning summary if not final
|
||||||
|
if not scorebug_data.is_final and scorebug_data.summary:
|
||||||
|
i_string = ''
|
||||||
|
for line in scorebug_data.summary:
|
||||||
|
if line and len(line) >= 2 and line[0]:
|
||||||
|
i_string += f'- Play {line[0]}: {line[1]}\n'
|
||||||
|
|
||||||
|
if i_string and "IFERROR" not in i_string:
|
||||||
|
embed.add_field(
|
||||||
|
name='Inning Summary',
|
||||||
|
value=i_string,
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
|
return embed
|
||||||
|
|
||||||
|
|
||||||
|
def create_team_progress_bar(
|
||||||
|
win_percentage: float,
|
||||||
|
away_abbrev: str,
|
||||||
|
home_abbrev: str,
|
||||||
|
length: int = 10
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Create a proportional progress bar showing each team's win probability.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
win_percentage: Home team's win percentage (0-100)
|
||||||
|
away_abbrev: Away team abbreviation (e.g., "POR")
|
||||||
|
home_abbrev: Home team abbreviation (e.g., "WV")
|
||||||
|
length: Total bar length in blocks (default 10)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted bar with dark blocks (▓) weighted toward winning team.
|
||||||
|
Arrow extends from the side with the advantage.
|
||||||
|
Examples:
|
||||||
|
Home winning: "POR ░▓▓▓▓▓▓▓▓▓► WV 95.0%"
|
||||||
|
Away winning: "POR ◄▓▓▓▓▓▓▓░░░ WV 30.0%"
|
||||||
|
Even game: "POR =▓▓▓▓▓▓▓▓▓▓= WV 50.0%"
|
||||||
|
"""
|
||||||
|
# Calculate blocks for each team (home team's percentage)
|
||||||
|
home_blocks = int((win_percentage / 100) * length)
|
||||||
|
away_blocks = length - home_blocks
|
||||||
|
|
||||||
|
if win_percentage > 50:
|
||||||
|
# Home team (right side) is winning
|
||||||
|
away_char = '░' # Light blocks for losing team
|
||||||
|
home_char = '▓' # Dark blocks for winning team
|
||||||
|
bar = away_char * away_blocks + home_char * home_blocks
|
||||||
|
# Arrow extends from right side
|
||||||
|
return f'{away_abbrev} {bar}► {home_abbrev} {win_percentage:.1f}%'
|
||||||
|
elif win_percentage < 50:
|
||||||
|
# Away team (left side) is winning
|
||||||
|
away_char = '▓' # Dark blocks for winning team
|
||||||
|
home_char = '░' # Light blocks for losing team
|
||||||
|
bar = away_char * away_blocks + home_char * home_blocks
|
||||||
|
# Arrow extends from left side
|
||||||
|
return f'{away_abbrev} ◄{bar} {home_abbrev} {win_percentage:.1f}%'
|
||||||
|
else:
|
||||||
|
# Even game (50/50)
|
||||||
|
away_char = '▓'
|
||||||
|
home_char = '▓'
|
||||||
|
bar = away_char * away_blocks + home_char * home_blocks
|
||||||
|
# Arrows on both sides for balanced display
|
||||||
|
return f'{away_abbrev} ={bar}= {home_abbrev} {win_percentage:.1f}%'
|
||||||
Loading…
Reference in New Issue
Block a user