# Draft Commands This directory contains Discord slash commands for draft system operations. ## 🚨 Important: Draft Period Restriction **Interactive draft commands are restricted to the offseason (week ≤ 0).** ### Restricted Commands The following commands can **only be used during the offseason** (when league week ≤ 0): - `/draft` - Make draft picks - `/draft-list` - View auto-draft queue - `/draft-list-add` - Add player to queue - `/draft-list-remove` - Remove player from queue - `/draft-list-clear` - Clear entire queue **Implementation:** The `@requires_draft_period` decorator automatically checks the current league week and returns an error message if the league is in-season. ### Unrestricted Commands These commands remain **available year-round**: - `/draft-board` - View draft picks by round - `/draft-status` - View current draft state - `/draft-on-clock` - View detailed on-the-clock information - All `/draft-admin` commands (administrator only) ### User Experience When a user tries to run a restricted command during the season, they see: ``` ❌ Not Available Draft commands are only available in the offseason. ``` ## Files ### `picks.py` - **Command**: `/draft` - **Description**: Make a draft pick with FA player autocomplete - **Restriction**: Offseason only (week ≤ 0) via `@requires_draft_period` decorator - **Parameters**: - `player` (required): Player name to draft (autocomplete shows FA players with position and sWAR) - **Service Dependencies**: - `draft_service.get_draft_data()` - `draft_pick_service.get_pick()` - `draft_pick_service.update_pick_selection()` - `team_service.get_team_by_owner()` (CACHED) - `team_service.get_team_roster()` - `player_service.get_players_by_name()` - `player_service.update_player_team()` - `league_service.get_current_state()` (for period check) ## Key Features ### Global Pick Lock - **Purpose**: Prevent concurrent draft picks that could cause race conditions - **Implementation**: `asyncio.Lock()` stored in cog instance - **Location**: Local only (not in database) - **Timeout**: 30-second stale lock auto-override - **Integration**: Background monitor task respects same lock ```python # In DraftPicksCog self.pick_lock = asyncio.Lock() self.lock_acquired_at: Optional[datetime] = None self.lock_acquired_by: Optional[int] = None # Lock acquisition with timeout check if self.pick_lock.locked(): if time_held > 30: # Override stale lock pass else: # Reject with wait time return async with self.pick_lock: # Process pick pass ``` ### Pick Validation Flow 1. **Lock Check**: Verify no active pick in progress (or stale lock >30s) 2. **GM Validation**: Verify user is team owner (cached lookup - fast!) 3. **Draft State**: Get current draft configuration 4. **Turn Validation**: Verify user's team is on the clock 5. **Player Validation**: Verify player is FA (team_id = 498) 6. **Cap Space**: Validate 32 sWAR limit won't be exceeded 7. **Execution**: Update pick, update player team, advance draft 8. **Announcements**: Post success message and player card ### FA Player Autocomplete The autocomplete function filters to FA players only: ```python async def fa_player_autocomplete(interaction, current: str): # Search all players players = await player_service.search_players(current, limit=25) # Filter to FA only (team_id = 498) fa_players = [p for p in players if p.team_id == 498] # Return choices with position and sWAR return [Choice(name=f"{p.name} ({p.pos}) - {p.wara:.2f} sWAR", value=p.name)] ``` ### Cap Space Validation Uses `utils.draft_helpers.validate_cap_space()`: ```python async def validate_cap_space(roster: dict, new_player_wara: float): # Calculate how many players count (top 26 of 32 roster spots) max_counted = min(26, 26 - (32 - projected_roster_size)) # Sort all players + new player by sWAR descending sorted_wara = sorted(all_players_wara, reverse=True) # Sum top N projected_total = sum(sorted_wara[:max_counted]) # Check against limit (with tiny float tolerance) return projected_total <= 32.00001, projected_total ``` ## Architecture Notes ### Command Pattern - Uses `@logged_command("/draft")` decorator (no manual error handling) - Always defers response: `await interaction.response.defer()` - Service layer only (no direct API client access) - Comprehensive logging with contextual information ### Race Condition Prevention The global lock ensures: - Only ONE pick can be processed at a time league-wide - Co-GMs cannot both draft simultaneously - Background auto-draft respects same lock - Stale locks (crashes/network issues) auto-clear after 30s ### Performance Optimizations - **Team lookup cached** (`get_team_by_owner` uses `@cached_single_item`) - **80% reduction** in API calls for GM validation - **Sub-millisecond cache hits** vs 50-200ms API calls - Draft data NOT cached (changes too frequently) ## Troubleshooting ### Common Issues 1. **"Pick In Progress" message**: - Another user is currently making a pick - Wait ~30 seconds for pick to complete - If stuck, lock will auto-clear after 30s 2. **"Not Your Turn" message**: - Current pick belongs to different team - Wait for your turn in draft order - Admin can use `/draft-admin` to adjust 3. **"Cap Space Exceeded" message**: - Drafting player would exceed 32.00 sWAR limit - Only top 26 players count toward cap - Choose player with lower sWAR value 4. **"Player Not Available" message**: - Player is not a free agent - May have been drafted by another team - Check draft board for available players ### Lock State Debugging Check lock status with admin tools: ```python # Lock state draft_picks_cog.pick_lock.locked() # True if held draft_picks_cog.lock_acquired_at # When lock was acquired draft_picks_cog.lock_acquired_by # User ID holding lock ``` Admin can force-clear locks: - Use `/draft-admin clear-lock` (when implemented) - Restart bot (lock is local only) ## Draft Format ### Hybrid Linear + Snake - **Rounds 1-10**: Linear draft (same order every round) - **Rounds 11+**: Snake draft (reverse on even rounds) - **Special Rule**: Round 11 Pick 1 = same team as Round 10 Pick 16 ### Pick Order Calculation Uses `utils.draft_helpers.calculate_pick_details()`: ```python def calculate_pick_details(overall: int) -> tuple[int, int]: round_num = math.ceil(overall / 16) if round_num <= 10: # Linear: 1-16, 1-16, 1-16, ... position = ((overall - 1) % 16) + 1 else: # Snake: odd rounds forward, even rounds reverse if round_num % 2 == 1: position = ((overall - 1) % 16) + 1 else: position = 16 - ((overall - 1) % 16) return round_num, position ``` ## Integration with Background Task The draft monitor task (`tasks/draft_monitor.py`) integrates with this command: 1. **Shared Lock**: Monitor acquires same `pick_lock` for auto-draft 2. **Timer Expiry**: When deadline passes, monitor auto-drafts 3. **Draft List**: Monitor tries players from team's draft list in order 4. **Pick Advancement**: Monitor calls same `draft_service.advance_pick()` ## Future Commands ### `/draft-status` (Pending Implementation) Display current draft state, timer, lock status ### `/draft-admin` (Pending Implementation) Admin controls: - Timer on/off - Set current pick - Configure channels - Wipe picks - Clear stale locks - Set keepers ### `/draft-list` (Pending Implementation) Manage auto-draft queue: - View current list - Add players - Remove players - Reorder players - Clear list ### `/draft-board` (Pending Implementation) View draft board by round with pagination ## Dependencies - `config.get_config()` - `services.draft_service` - `services.draft_pick_service` - `services.player_service` - `services.team_service` (with caching) - `utils.decorators.logged_command` - `utils.draft_helpers.validate_cap_space` - `views.draft_views.*` - `asyncio.Lock` for race condition prevention ## Testing Run tests with: `python -m pytest tests/test_commands_draft.py -v` (when implemented) Test scenarios: - **Concurrent picks**: Two users try to draft simultaneously - **Stale lock**: Lock held >30s gets overridden - **Cap validation**: Player would exceed 32 sWAR limit - **Turn validation**: User tries to draft out of turn - **Player availability**: Player already drafted ## Security Considerations ### Permission Validation - Only team owners (GMs) can make draft picks - Validated via `team_service.get_team_by_owner()` - Cached for performance (30-minute TTL) ### Data Integrity - Global lock prevents duplicate picks - Cap validation prevents roster violations - Turn validation enforces draft order - All updates atomic (pick + player team) ## Database Requirements - Draft data table (configuration and state) - Draft picks table (all picks for season) - Draft list table (auto-draft queues) - Player records with team associations - Team records with owner associations --- **Last Updated:** October 2025 **Status:** Core `/draft` command implemented and tested **Next:** Implement `/draft-status`, `/draft-admin`, `/draft-list` commands