- Add DraftSheetService with write_pick(), write_picks_batch(), clear_picks_range(), and get_sheet_url() methods - Integrate sheet writes in /draft command (fire-and-forget pattern) - Integrate sheet writes in draft_monitor.py for auto-draft picks - Add /draft-admin resync-sheet command for bulk recovery - Add sheet link to /draft-status embed - Add draft_sheet_keys config with env var overrides per season - Add get_picks_with_players() to draft_pick_service for resync - Add 13 unit tests for DraftSheetService (all passing) - Update CLAUDE.md documentation files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
13 KiB
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-admincommands (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_perioddecorator - 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_overallandplayer_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
# 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
# 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
- Lock Check: Verify no active pick in progress (or stale lock >30s)
- GM Validation: Verify user is team owner (cached lookup - fast!)
- Draft State: Get current draft configuration
- Turn Validation: Verify user's team is on the clock
- Player Validation: Verify player is FA (team_id = 498)
- Cap Space: Validate 32 sWAR limit won't be exceeded
- Execution: Update pick, update player team, advance draft
- Sheet Write: Write pick to Google Sheets (fire-and-forget)
- Announcements: Post success message and player card
FA Player Autocomplete
The autocomplete function filters to FA players only:
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():
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-statusembed
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
# 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 12DRAFT_SHEET_KEY_13- Sheet ID for season 13DRAFT_SHEET_ENABLED- Feature flag (default: True)
Config file defaults in config.py:
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:
- Fetches all picks for current season with player data
- Clears existing sheet data (columns D-G, rows 2+)
- Batch writes all completed picks
- 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:
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 operationsconfig.get_draft_sheet_key()- Sheet ID lookup by seasonconfig.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_owneruses@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
-
"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
-
"Not Your Turn" message:
- Current pick belongs to different team
- Wait for your turn in draft order
- Admin can use
/draft-adminto adjust
-
"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
-
"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:
# 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():
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:
- Shared Lock: Monitor acquires same
pick_lockfor auto-draft - Timer Expiry: When deadline passes, monitor auto-drafts
- Draft List: Monitor tries players from team's draft list in order
- 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_serviceservices.draft_pick_serviceservices.draft_sheet_service(Google Sheets integration)services.player_serviceservices.team_service(with caching)utils.decorators.logged_commandutils.draft_helpers.validate_cap_spaceviews.draft_views.*asyncio.Lockfor 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