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>
110 lines
4.5 KiB
Python
110 lines
4.5 KiB
Python
"""
|
|
Team model for SBA teams
|
|
|
|
Represents a team in the league with all associated metadata.
|
|
"""
|
|
from typing import Optional
|
|
from enum import Enum
|
|
from pydantic import Field
|
|
|
|
from models.base import SBABaseModel
|
|
from models.division import Division
|
|
|
|
|
|
class RosterType(Enum):
|
|
"""Roster designation types."""
|
|
MAJOR_LEAGUE = "ml"
|
|
MINOR_LEAGUE = "mil"
|
|
INJURED_LIST = "il"
|
|
FREE_AGENCY = "fa"
|
|
|
|
|
|
class Team(SBABaseModel):
|
|
"""Team model representing an SBA team."""
|
|
|
|
# Override base model to make id required for database entities
|
|
id: int = Field(..., description="Team ID from database")
|
|
|
|
abbrev: str = Field(..., description="Team abbreviation (e.g., 'NYY')")
|
|
sname: str = Field(..., description="Short team name")
|
|
lname: str = Field(..., description="Long team name")
|
|
season: int = Field(..., description="Season number")
|
|
|
|
# Manager information
|
|
gmid: Optional[int] = Field(None, description="Primary general manager ID")
|
|
gmid2: Optional[int] = Field(None, description="Secondary general manager ID")
|
|
manager1_id: Optional[int] = Field(None, description="Primary manager ID")
|
|
manager2_id: Optional[int] = Field(None, description="Secondary manager ID")
|
|
|
|
# Team metadata
|
|
division_id: Optional[int] = Field(None, description="Division ID")
|
|
division: Optional[Division] = Field(None, description="Division object (populated from API)")
|
|
stadium: Optional[str] = Field(None, description="Home stadium name")
|
|
thumbnail: Optional[str] = Field(None, description="Team thumbnail URL")
|
|
color: Optional[str] = Field(None, description="Primary team color")
|
|
dice_color: Optional[str] = Field(None, description="Dice rolling color")
|
|
|
|
@classmethod
|
|
def from_api_data(cls, data: dict) -> 'Team':
|
|
"""
|
|
Create Team instance from API data, handling nested division structure.
|
|
|
|
The API returns division data as a nested object, but our model expects
|
|
both division_id (int) and division (optional Division object).
|
|
"""
|
|
# Make a copy to avoid modifying original data
|
|
team_data = data.copy()
|
|
|
|
# Handle nested division structure
|
|
if 'division' in team_data and isinstance(team_data['division'], dict):
|
|
division_data = team_data['division']
|
|
# Extract division_id from nested division object
|
|
team_data['division_id'] = division_data.get('id')
|
|
# Keep division object for optional population
|
|
if division_data.get('id'):
|
|
team_data['division'] = Division.from_api_data(division_data)
|
|
|
|
return super().from_api_data(team_data)
|
|
|
|
def roster_type(self) -> RosterType:
|
|
"""Determine the roster type based on team abbreviation."""
|
|
if len(self.abbrev) <= 3:
|
|
return RosterType.MAJOR_LEAGUE
|
|
|
|
# For teams with extended abbreviations, check suffix patterns
|
|
abbrev_lower = self.abbrev.lower()
|
|
|
|
# Pattern analysis:
|
|
# - Minor League: ends with 'mil' (e.g., NYYMIL, BHMMIL)
|
|
# - Injured List: ends with 'il' but not 'mil' (e.g., NYYIL, BOSIL)
|
|
# - Edge case: teams whose base abbrev ends in 'M' + 'IL' = 'MIL'
|
|
# Only applies if removing 'IL' gives us exactly a 3-char base team
|
|
|
|
if abbrev_lower.endswith('mil'):
|
|
# Check if this is actually [BaseTeam]IL where BaseTeam ends in 'M'
|
|
# E.g., BHMIL = BHM + IL (injured list), not minor league
|
|
if len(self.abbrev) == 5: # Exactly 5 chars: 3-char base + IL
|
|
potential_base = self.abbrev[:-2] # Remove 'IL'
|
|
if len(potential_base) == 3 and potential_base.upper().endswith('M'):
|
|
return RosterType.INJURED_LIST
|
|
return RosterType.MINOR_LEAGUE
|
|
elif abbrev_lower.endswith('il'):
|
|
return RosterType.INJURED_LIST
|
|
else:
|
|
return RosterType.MAJOR_LEAGUE
|
|
|
|
def get_major_league_affiliate(self) -> Optional[str]:
|
|
"""
|
|
Get the Major League affiliate abbreviation for Minor League teams.
|
|
|
|
Returns:
|
|
Major League team abbreviation if this is a Minor League team, None otherwise
|
|
"""
|
|
if self.roster_type() == RosterType.MINOR_LEAGUE:
|
|
# Minor League teams follow pattern: [MajorTeam]MIL (e.g., NYYMIL -> NYY)
|
|
if self.abbrev.upper().endswith('MIL'):
|
|
return self.abbrev[:-3] # Remove 'MIL' suffix
|
|
return None
|
|
|
|
def __str__(self):
|
|
return f"{self.abbrev} - {self.lname}" |