major-domo-v2/services/trade_builder.py
Cal Corum 3aa95ef98c CLAUDE: Refine injury roll display and cleanup imports
## Injury Command Enhancements

### Pitcher-Specific Injury Display
- Added rest requirement note for pitcher injuries with game duration
- Shows "X games plus their current rest requirement" for pitchers
- Removed redundant footer text from FATIGUED result
- Cleaner, more concise pitcher injury messaging

### Bot Configuration
- Registered injuries command package in bot.py
- Added proper import and setup for InjuryGroup

### Code Cleanup
- Fixed misplaced import in views/embeds.py (moved to top)
- Standardized import ordering across command files
- Minor formatting improvements

## Files Changed
- commands/injuries/management.py: Pitcher rest requirement display
- bot.py: Injuries package registration
- views/embeds.py: Import cleanup
- Various: Import standardization

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 22:20:13 -05:00

478 lines
17 KiB
Python

"""
Trade Builder Service
Extends the TransactionBuilder to support multi-team trades and player exchanges.
"""
import logging
from typing import Dict, List, Optional, Tuple
from datetime import datetime, timezone
import uuid
from config import get_config
from models.trade import Trade, TradeParticipant, TradeMove, TradeStatus
from models.team import Team, RosterType
from models.player import Player
from services.transaction_builder import TransactionBuilder, RosterValidationResult, TransactionMove
from services.team_service import team_service
from services.roster_service import roster_service
from services.league_service import league_service
logger = logging.getLogger(f'{__name__}.TradeBuilder')
class TradeValidationResult:
"""Results of trade validation across all participating teams."""
def __init__(self):
self.is_legal: bool = True
self.participant_validations: Dict[int, RosterValidationResult] = {}
self.trade_errors: List[str] = []
self.trade_warnings: List[str] = []
self.trade_suggestions: List[str] = []
@property
def all_errors(self) -> List[str]:
"""Get all errors including trade-level and roster-level errors."""
errors = self.trade_errors.copy()
for validation in self.participant_validations.values():
errors.extend(validation.errors)
return errors
@property
def all_warnings(self) -> List[str]:
"""Get all warnings across trade and roster levels."""
warnings = self.trade_warnings.copy()
for validation in self.participant_validations.values():
warnings.extend(validation.warnings)
return warnings
@property
def all_suggestions(self) -> List[str]:
"""Get all suggestions across trade and roster levels."""
suggestions = self.trade_suggestions.copy()
for validation in self.participant_validations.values():
suggestions.extend(validation.suggestions)
return suggestions
def get_participant_validation(self, team_id: int) -> Optional[RosterValidationResult]:
"""Get validation result for a specific team."""
return self.participant_validations.get(team_id)
class TradeBuilder:
"""
Interactive trade builder for multi-team player exchanges.
Extends the functionality of TransactionBuilder to support trades between teams.
"""
def __init__(self, initiated_by: int, initiating_team: Team, season: int = get_config().sba_current_season):
"""
Initialize trade builder.
Args:
initiated_by: Discord user ID who initiated the trade
initiating_team: Team that initiated the trade
season: Season number
"""
self.trade = Trade(
trade_id=str(uuid.uuid4())[:8], # Short trade ID
participants=[],
status=TradeStatus.DRAFT,
initiated_by=initiated_by,
created_at=datetime.now(timezone.utc).isoformat(),
season=season
)
# Add the initiating team as first participant
self.trade.add_participant(initiating_team)
# Cache transaction builders for each participating team
self._team_builders: Dict[int, TransactionBuilder] = {}
logger.info(f"TradeBuilder initialized: {self.trade.trade_id} by user {initiated_by} for {initiating_team.abbrev}")
@property
def trade_id(self) -> str:
"""Get the trade ID."""
return self.trade.trade_id
@property
def participating_teams(self) -> List[Team]:
"""Get all participating teams."""
return self.trade.participating_teams
@property
def team_count(self) -> int:
"""Get number of participating teams."""
return self.trade.team_count
@property
def is_empty(self) -> bool:
"""Check if trade has no moves."""
return self.trade.total_moves == 0
@property
def move_count(self) -> int:
"""Get total number of moves in trade."""
return self.trade.total_moves
async def add_team(self, team: Team) -> tuple[bool, str]:
"""
Add a team to the trade.
Args:
team: Team to add
Returns:
Tuple of (success: bool, error_message: str)
"""
# Check if team is already participating
if self.trade.get_participant_by_team_id(team.id):
return False, f"{team.abbrev} is already participating in this trade"
# Add team to trade
participant = self.trade.add_participant(team)
# Create transaction builder for this team
self._team_builders[team.id] = TransactionBuilder(team, self.trade.initiated_by, self.trade.season)
logger.info(f"Added team {team.abbrev} to trade {self.trade_id}")
return True, ""
async def remove_team(self, team_id: int) -> tuple[bool, str]:
"""
Remove a team from the trade.
Args:
team_id: ID of team to remove
Returns:
Tuple of (success: bool, error_message: str)
"""
participant = self.trade.get_participant_by_team_id(team_id)
if not participant:
return False, "Team is not participating in this trade"
# Check if team has moves - prevent removal if they do
if participant.all_moves:
return False, f"{participant.team.abbrev} has moves in this trade and cannot be removed"
# Remove team
removed = self.trade.remove_participant(team_id)
if removed and team_id in self._team_builders:
del self._team_builders[team_id]
if removed:
logger.info(f"Removed team {team_id} from trade {self.trade_id}")
return removed, "" if removed else "Failed to remove team"
async def add_player_move(
self,
player: Player,
from_team: Team,
to_team: Team,
from_roster: RosterType,
to_roster: RosterType
) -> tuple[bool, str]:
"""
Add a player move to the trade.
Args:
player: Player being moved
from_team: Team giving up the player
to_team: Team receiving the player
from_roster: Source roster type
to_roster: Destination roster type
Returns:
Tuple of (success: bool, error_message: str)
"""
# Validate player is not from Free Agency
if player.team_id == get_config().free_agent_team_id:
return False, f"Cannot add {player.name} from Free Agency. Players must be traded from teams within the organizations involved in the trade."
# Validate player has a valid team assignment
if not player.team_id:
return False, f"{player.name} does not have a valid team assignment"
# Validate that from_team matches the player's actual team organization
player_team = await team_service.get_team(player.team_id)
if not player_team:
return False, f"Could not find team for {player.name}"
# Check if player's team is in the same organization as from_team
if not player_team.is_same_organization(from_team):
return False, f"{player.name} is on {player_team.abbrev}, they are not eligible to be added to the trade."
# Ensure both teams are participating (check by organization for ML authority)
from_participant = self.trade.get_participant_by_organization(from_team)
to_participant = self.trade.get_participant_by_organization(to_team)
if not from_participant:
return False, f"{from_team.abbrev} is not participating in this trade"
if not to_participant:
return False, f"{to_team.abbrev} is not participating in this trade"
# Check if player is already involved in a move
for participant in self.trade.participants:
for existing_move in participant.all_moves:
if existing_move.player.id == player.id:
return False, f"{player.name} is already involved in a move in this trade"
# Create trade move
trade_move = TradeMove(
player=player,
from_roster=from_roster,
to_roster=to_roster,
from_team=from_team,
to_team=to_team,
source_team=from_team,
destination_team=to_team
)
# Add to giving team's moves
from_participant.moves_giving.append(trade_move)
# Add to receiving team's moves
to_participant.moves_receiving.append(trade_move)
# Create corresponding transaction moves for each team's builder
from_builder = self._get_or_create_builder(from_team)
to_builder = self._get_or_create_builder(to_team)
# Move for giving team (player leaving)
from_move = TransactionMove(
player=player,
from_roster=from_roster,
to_roster=RosterType.FREE_AGENCY, # Conceptually leaving the org
from_team=from_team,
to_team=None
)
# Move for receiving team (player joining)
to_move = TransactionMove(
player=player,
from_roster=RosterType.FREE_AGENCY, # Conceptually joining from outside
to_roster=to_roster,
from_team=None,
to_team=to_team
)
# Add moves to respective builders
from_success, from_error = from_builder.add_move(from_move)
if not from_success:
# Remove from trade if builder failed
from_participant.moves_giving.remove(trade_move)
to_participant.moves_receiving.remove(trade_move)
return False, f"Error adding move to {from_team.abbrev}: {from_error}"
to_success, to_error = to_builder.add_move(to_move)
if not to_success:
# Rollback both if second failed
from_builder.remove_move(player.id)
from_participant.moves_giving.remove(trade_move)
to_participant.moves_receiving.remove(trade_move)
return False, f"Error adding move to {to_team.abbrev}: {to_error}"
logger.info(f"Added player move to trade {self.trade_id}: {trade_move.description}")
return True, ""
async def add_supplementary_move(
self,
team: Team,
player: Player,
from_roster: RosterType,
to_roster: RosterType
) -> tuple[bool, str]:
"""
Add a supplementary move (internal organizational move) for roster legality.
Args:
team: Team making the internal move
player: Player being moved
from_roster: Source roster type
to_roster: Destination roster type
Returns:
Tuple of (success: bool, error_message: str)
"""
participant = self.trade.get_participant_by_organization(team)
if not participant:
return False, f"{team.abbrev} is not participating in this trade"
# Create supplementary move (internal to organization)
supp_move = TradeMove(
player=player,
from_roster=from_roster,
to_roster=to_roster,
from_team=team,
to_team=team,
source_team=team,
destination_team=team
)
# Add to participant's supplementary moves
participant.supplementary_moves.append(supp_move)
# Add to team's transaction builder
builder = self._get_or_create_builder(team)
trans_move = TransactionMove(
player=player,
from_roster=from_roster,
to_roster=to_roster,
from_team=team,
to_team=team
)
success, error = builder.add_move(trans_move)
if not success:
participant.supplementary_moves.remove(supp_move)
return False, error
logger.info(f"Added supplementary move for {team.abbrev}: {supp_move.description}")
return True, ""
async def remove_move(self, player_id: int) -> tuple[bool, str]:
"""
Remove a move from the trade.
Args:
player_id: ID of player whose move to remove
Returns:
Tuple of (success: bool, error_message: str)
"""
# Find and remove the move from all participants
removed_move = None
for participant in self.trade.participants:
# Check moves_giving
for move in participant.moves_giving[:]:
if move.player.id == player_id:
participant.moves_giving.remove(move)
removed_move = move
break
# Check moves_receiving
for move in participant.moves_receiving[:]:
if move.player.id == player_id:
participant.moves_receiving.remove(move)
# Don't set removed_move again, we already got it from giving
break
# Check supplementary_moves
for move in participant.supplementary_moves[:]:
if move.player.id == player_id:
participant.supplementary_moves.remove(move)
removed_move = move
break
if not removed_move:
return False, "No move found for that player"
# Remove from transaction builders
for builder in self._team_builders.values():
builder.remove_move(player_id)
logger.info(f"Removed move from trade {self.trade_id}: {removed_move.description}")
return True, ""
async def validate_trade(self, next_week: Optional[int] = None) -> TradeValidationResult:
"""
Validate the entire trade including all teams' roster legality.
Args:
next_week: Week to validate for (optional)
Returns:
TradeValidationResult with comprehensive validation
"""
result = TradeValidationResult()
# Validate trade structure
is_balanced, balance_errors = self.trade.validate_trade_balance()
if not is_balanced:
result.is_legal = False
result.trade_errors.extend(balance_errors)
# Validate each team's roster after the trade
for participant in self.trade.participants:
team_id = participant.team.id
if team_id in self._team_builders:
builder = self._team_builders[team_id]
roster_validation = await builder.validate_transaction(next_week)
result.participant_validations[team_id] = roster_validation
if not roster_validation.is_legal:
result.is_legal = False
# Add trade-level suggestions
if self.is_empty:
result.trade_suggestions.append("Add player moves to build your trade")
if self.team_count < 2:
result.trade_suggestions.append("Add another team to create a trade")
logger.debug(f"Trade validation for {self.trade_id}: Legal={result.is_legal}, Errors={len(result.all_errors)}")
return result
def _get_or_create_builder(self, team: Team) -> TransactionBuilder:
"""Get or create a transaction builder for a team."""
if team.id not in self._team_builders:
self._team_builders[team.id] = TransactionBuilder(team, self.trade.initiated_by, self.trade.season)
return self._team_builders[team.id]
def clear_trade(self) -> None:
"""Clear all moves from the trade."""
for participant in self.trade.participants:
participant.moves_giving.clear()
participant.moves_receiving.clear()
participant.supplementary_moves.clear()
for builder in self._team_builders.values():
builder.clear_moves()
logger.info(f"Cleared all moves from trade {self.trade_id}")
def get_trade_summary(self) -> str:
"""Get human-readable trade summary."""
return self.trade.get_trade_summary()
# Global cache for active trade builders
_active_trade_builders: Dict[str, TradeBuilder] = {}
def get_trade_builder(user_id: int, initiating_team: Team) -> TradeBuilder:
"""
Get or create a trade builder for a user.
Args:
user_id: Discord user ID
initiating_team: Team initiating the trade
Returns:
TradeBuilder instance
"""
# For now, use user_id as the key. In the future, could support multiple concurrent trades
trade_key = f"{user_id}:trade"
if trade_key not in _active_trade_builders:
_active_trade_builders[trade_key] = TradeBuilder(user_id, initiating_team)
return _active_trade_builders[trade_key]
def clear_trade_builder(user_id: int) -> None:
"""Clear trade builder for a user."""
trade_key = f"{user_id}:trade"
if trade_key in _active_trade_builders:
del _active_trade_builders[trade_key]
logger.info(f"Cleared trade builder for user {user_id}")
def get_active_trades() -> Dict[str, TradeBuilder]:
"""Get all active trade builders (for debugging/admin purposes)."""
return _active_trade_builders.copy()