# 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) - `draft_sheet_service.write_pick()` (Google Sheets integration) ## Key Features ### Skipped Pick Support - **Purpose**: Allow teams to make up picks they missed when not on the clock - **Detection**: Checks for picks with `overall < current_overall` and `player_id = None` - **Behavior**: If team is not on the clock but has skipped picks, allows drafting with earliest skipped pick - **User Experience**: Success message includes footer noting this is a "skipped pick makeup" - **Draft Advancement**: Does NOT advance the draft when using a skipped pick ```python # Skipped pick detection flow if current_pick.owner.id != team.id: skipped_picks = await draft_pick_service.get_skipped_picks_for_team( season, team.id, current_overall ) if skipped_picks: pick_to_use = skipped_picks[0] # Earliest skipped pick else: # Return "Not Your Turn" error ``` ### 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. **Sheet Write**: Write pick to Google Sheets (fire-and-forget) 9. **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 ``` ## Google Sheets Integration ### Overview Draft picks are automatically written to a shared Google Sheet for easy tracking. This feature: - Writes pick data to configured sheet after each successful pick - Uses **fire-and-forget** pattern (non-blocking, doesn't fail the pick) - Supports manual resync via `/draft-admin resync-sheet` - Shows sheet link in `/draft-status` embed ### Sheet Structure Each pick writes 4 columns starting at column D: | Column | Content | |--------|---------| | D | Original owner abbreviation | | E | Current owner abbreviation | | F | Player name | | G | Player sWAR | Row calculation: `overall + 1` (pick 1 → row 2, leaving row 1 for headers) ### Fire-and-Forget Pattern ```python # After successful pick execution try: sheet_success = await draft_sheet_service.write_pick( season=config.sba_season, overall=pick.overall, orig_owner_abbrev=original_owner.abbrev, owner_abbrev=team.abbrev, player_name=player.name, swar=player.wara ) if not sheet_success: self.logger.warning(f"Draft sheet write failed for pick #{pick.overall}") # Post notification to ping channel except Exception as e: self.logger.error(f"Draft sheet write error: {e}") # Non-critical - don't fail the draft pick ``` ### Configuration Environment variables (optional, defaults in config): - `DRAFT_SHEET_KEY_12` - Sheet ID for season 12 - `DRAFT_SHEET_KEY_13` - Sheet ID for season 13 - `DRAFT_SHEET_ENABLED` - Feature flag (default: True) Config file defaults in `config.py`: ```python draft_sheet_keys: dict[int, str] = { 12: "1OF-sAFykebc_2BrcYCgxCR-4rJo0GaNmTstagV-PMBU", # Add new seasons as needed } draft_sheet_worksheet: str = "Ordered List" draft_sheet_start_column: str = "D" draft_sheet_enabled: bool = True ``` ### `/draft-admin resync-sheet` Command Bulk resync all picks from database to sheet: 1. Fetches all picks for current season with player data 2. Clears existing sheet data (columns D-G, rows 2+) 3. Batch writes all completed picks 4. Reports success/failure count Use cases: - Sheet corruption recovery - Credential issues during draft - Manual corrections needed ### `/draft-status` Sheet Link The draft status embed includes a clickable link to the sheet: ```python sheet_url = config.get_draft_sheet_url(config.sba_season) embed = await create_draft_status_embed(draft_data, current_pick, lock_status, sheet_url) ``` ### Service Dependencies - `services.draft_sheet_service` - Google Sheets operations - `config.get_draft_sheet_key()` - Sheet ID lookup by season - `config.get_draft_sheet_url()` - Sheet URL generation ### Failure Handling - Sheet write failures don't block draft picks - Failures logged with warning level - Optional: Post failure notice to ping channel - Admins can use resync-sheet for recovery ## 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()` ## Implemented Commands ### `/draft-status` Display current draft state, timer, lock status ### `/draft-admin` (Administrator Only) Admin controls: - `/draft-admin timer` - Enable/disable timer (auto-starts monitor task) - `/draft-admin set-pick` - Set current pick (auto-starts monitor if timer active) - `/draft-admin channels` - Configure ping/result channels - `/draft-admin wipe` - Clear all picks for season - `/draft-admin info` - View detailed draft configuration - `/draft-admin resync-sheet` - Resync all picks to Google Sheet ### `/draft-list` View auto-draft queue for your team ### `/draft-list-add` Add player to auto-draft queue ### `/draft-list-remove` Remove player from auto-draft queue ### `/draft-list-clear` Clear entire auto-draft queue ### `/draft-board` View draft board by round with pagination ### `/draft-on-clock` View detailed on-the-clock information including: - Current team on the clock - Deadline with relative timestamp - Team's current sWAR and cap space - Last 5 picks - Top 5 roster players by sWAR ## Dependencies - `config.get_config()` - `services.draft_service` - `services.draft_pick_service` - `services.draft_sheet_service` (Google Sheets integration) - `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:** December 2025 **Status:** All draft commands implemented and tested **Recent:** Google Sheets integration for automatic pick tracking, `/draft-admin resync-sheet` command, sheet link in `/draft-status`