major-domo-v2/models/team.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

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}"