major-domo-v2/commands/league/standings.py
Cal Corum 1dd930e4b3 CLAUDE: Complete dice command system with fielding mechanics
Implements comprehensive dice rolling system for gameplay:

## New Features
- `/roll` and `!roll` commands for XdY dice notation with multiple roll support
- `/ab` and `!atbat` commands for baseball at-bat dice shortcuts (1d6;2d6;1d20)
- `/fielding` and `!f` commands for Super Advanced fielding with full position charts

## Technical Implementation
- Complete dice command package in commands/dice/
- Full range and error charts for all 8 defensive positions (1B,2B,3B,SS,LF,RF,CF,C)
- Pre-populated position choices for user-friendly slash command interface
- Backwards compatibility with prefix commands (!roll, !r, !dice, !ab, !atbat, !f, !fielding, !saf)
- Type-safe implementation following "Raise or Return" pattern

## Testing & Quality
- 30 comprehensive tests with 100% pass rate
- Complete test coverage for all dice functionality, parsing, validation, and error handling
- Integration with bot.py command loading system
- Maintainable data structures replacing verbose original implementation

## User Experience
- Consistent embed formatting across all commands
- Detailed fielding results with range and error analysis
- Support for complex dice combinations and multiple roll formats
- Clear error messages for invalid inputs

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 22:30:31 -05:00

253 lines
9.1 KiB
Python

"""
League Standings Commands
Implements slash commands for displaying league standings and playoff picture.
"""
from typing import Optional
import discord
from discord.ext import commands
from services.standings_service import standings_service
from utils.logging import get_contextual_logger
from utils.decorators import logged_command
from constants import SBA_CURRENT_SEASON
from views.embeds import EmbedColors, EmbedTemplate
class StandingsCommands(commands.Cog):
"""League standings command handlers."""
def __init__(self, bot: commands.Bot):
self.bot = bot
self.logger = get_contextual_logger(f'{__name__}.StandingsCommands')
@discord.app_commands.command(
name="standings",
description="Display league standings"
)
@discord.app_commands.describe(
season="Season to show standings for (defaults to current season)",
division="Show specific division only (optional)"
)
@logged_command("/standings")
async def standings(
self,
interaction: discord.Interaction,
season: Optional[int] = None,
division: Optional[str] = None
):
"""Display league standings by division."""
await interaction.response.defer()
search_season = season or SBA_CURRENT_SEASON
if division:
# Show specific division
await self._show_division_standings(interaction, search_season, division)
else:
# Show all divisions
await self._show_all_standings(interaction, search_season)
@discord.app_commands.command(
name="playoff-picture",
description="Display current playoff picture"
)
@discord.app_commands.describe(
season="Season to show playoff picture for (defaults to current season)"
)
@logged_command("/playoff-picture")
async def playoff_picture(
self,
interaction: discord.Interaction,
season: Optional[int] = None
):
"""Display playoff picture with division leaders and wild card race."""
await interaction.response.defer()
search_season = season or SBA_CURRENT_SEASON
self.logger.debug("Fetching playoff picture", season=search_season)
playoff_data = await standings_service.get_playoff_picture(search_season)
if not playoff_data["division_leaders"] and not playoff_data["wild_card"]:
await interaction.followup.send(
f"❌ No playoff data available for season {search_season}.",
ephemeral=True
)
return
embed = await self._create_playoff_picture_embed(playoff_data, search_season)
await interaction.followup.send(embed=embed)
async def _show_all_standings(self, interaction: discord.Interaction, season: int):
"""Show standings for all divisions."""
self.logger.debug("Fetching all division standings", season=season)
divisions = await standings_service.get_standings_by_division(season)
if not divisions:
await interaction.followup.send(
f"❌ No standings available for season {season}.",
ephemeral=True
)
return
embeds = []
# Create embed for each division
for div_name, teams in divisions.items():
if teams: # Only create embed if division has teams
embed = await self._create_division_embed(div_name, teams, season)
embeds.append(embed)
# Send first embed, then follow up with others
if embeds:
await interaction.followup.send(embed=embeds[0])
# Send additional embeds as follow-ups
for embed in embeds[1:]:
await interaction.followup.send(embed=embed)
async def _show_division_standings(self, interaction: discord.Interaction, season: int, division: str):
"""Show standings for a specific division."""
self.logger.debug("Fetching division standings", season=season, division=division)
divisions = await standings_service.get_standings_by_division(season)
# Find matching division (case insensitive)
target_division = None
division_lower = division.lower()
for div_name, teams in divisions.items():
if division_lower in div_name.lower():
target_division = (div_name, teams)
break
if not target_division:
available = ", ".join(divisions.keys())
await interaction.followup.send(
f"❌ Division '{division}' not found. Available divisions: {available}",
ephemeral=True
)
return
div_name, teams = target_division
if not teams:
await interaction.followup.send(
f"❌ No teams found in {div_name} division.",
ephemeral=True
)
return
embed = await self._create_division_embed(div_name, teams, season)
await interaction.followup.send(embed=embed)
async def _create_division_embed(self, division_name: str, teams, season: int) -> discord.Embed:
"""Create an embed for a division's standings."""
embed = EmbedTemplate.create_base_embed(
title=f"🏆 {division_name} Division - Season {season}",
color=EmbedColors.PRIMARY
)
# Create standings table
standings_lines = []
for i, team in enumerate(teams, 1):
# Format team line
team_line = (
f"{i}. **{team.team.abbrev}** {team.wins}-{team.losses} "
f"({team.winning_percentage:.3f})"
)
# Add games behind if not first place
if team.div_gb is not None and team.div_gb > 0:
team_line += f" *{team.div_gb:.1f} GB*"
standings_lines.append(team_line)
embed.add_field(
name="Standings",
value="\n".join(standings_lines),
inline=False
)
# Add additional stats for top teams
if len(teams) >= 3:
stats_lines = []
for team in teams[:3]: # Top 3 teams
stats_line = (
f"**{team.team.abbrev}**: "
f"Home {team.home_record}"
f"Last 8: {team.last8_record}"
f"Streak: {team.current_streak}"
)
stats_lines.append(stats_line)
embed.add_field(
name="Recent Form (Top 3)",
value="\n".join(stats_lines),
inline=False
)
embed.set_footer(text=f"Season {season}")
return embed
async def _create_playoff_picture_embed(self, playoff_data, season: int) -> discord.Embed:
"""Create playoff picture embed."""
embed = EmbedTemplate.create_base_embed(
title=f"🏅 Playoff Picture - Season {season}",
color=EmbedColors.SUCCESS
)
# Division Leaders
if playoff_data["division_leaders"]:
leaders_lines = []
for i, team in enumerate(playoff_data["division_leaders"], 1):
division = team.team.division.division_name if hasattr(team.team, 'division') and team.team.division else "Unknown"
leaders_lines.append(
f"{i}. **{team.team.abbrev}** {team.wins}-{team.losses} "
f"({team.winning_percentage:.3f}) - *{division}*"
)
embed.add_field(
name="🥇 Division Leaders",
value="\n".join(leaders_lines),
inline=False
)
# Wild Card Race
if playoff_data["wild_card"]:
wc_lines = []
for i, team in enumerate(playoff_data["wild_card"][:8], 1): # Top 8 wild card
wc_gb = team.wild_card_gb_display
wc_line = (
f"{i}. **{team.team.abbrev}** {team.wins}-{team.losses} "
f"({team.winning_percentage:.3f})"
)
# Add games behind info
if wc_gb != "-":
wc_line += f" *{wc_gb} GB*"
elif i <= 4:
wc_line += " *In playoffs*"
wc_lines.append(wc_line)
# Add playoff cutoff line after 4th team
if i == 4:
wc_lines.append("─────────── *Playoff Cutoff* ───────────")
embed.add_field(
name="🎯 Wild Card Race (Top 4 make playoffs)",
value="\n".join(wc_lines),
inline=False
)
embed.set_footer(text=f"Season {season}")
return embed
async def setup(bot: commands.Bot):
"""Load the standings commands cog."""
await bot.add_cog(StandingsCommands(bot))