major-domo-v2/commands/draft/CLAUDE.md
Cal Corum 9093055bb5 Add Google Sheets integration for draft pick tracking
- 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>
2025-12-11 11:18:27 -06:00

415 lines
13 KiB
Markdown

# 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`