brackt/plans/omni-league-draft-rules.md

425 lines
16 KiB
Markdown
Raw Normal View History

# Omni League Draft Rules Implementation Plan
## Overview
Implement draft restrictions for Omni leagues where teams must draft at least one participant from each sport, with limited "flex" picks for duplicates.
## Business Rules
### Core Rule 1: Team Flex Pick Limit
- **Must draft at least one from each sport**
- **Flex picks** = Total rounds - Number of sports
- Once all flex picks are used, can only draft from sports not yet drafted
### Core Rule 2: Global Sport Availability (NEW)
- **Cannot draft from a sport if it would prevent other teams from fulfilling their requirement**
- For each sport, track:
- Undrafted participants remaining in that sport
- Teams that haven't drafted from that sport yet
- A team can draft from a sport only if:
- **First pick in sport**: `undraftedInSport >= teamsStillNeedingSport`
- **Flex pick in sport**: `undraftedInSport > teamsStillNeedingSport`
### Example 1: Team Flex Limit
- 5 rounds, 3 sports (NFL, NBA, NHL)
- Flex picks available = 5 - 3 = 2
- Scenario:
- Round 1: NFL player (0 flexes used)
- Round 2: NBA player (0 flexes used)
- Round 3: NFL player (1 flex used - duplicate NFL)
- Round 4: NFL player (2 flexes used - second duplicate NFL)
- Round 5: MUST be NHL (all flexes exhausted, NHL is only sport with 0 picks)
### Example 2: Global Sport Availability
- 10 teams, 5 rounds, 3 sports (NFL, NBA, NHL)
- 12 NFL participants total, 3 teams haven't drafted NFL yet, 3 NFL participants left undrafted
- Current team HAS already drafted 1 NFL participant
- Scenario:
- If current team drafts another NFL participant: 2 NFL participants remain
- But 3 teams still need to draft their first NFL participant
- This would make it impossible for all teams to get NFL
- **Result**: Current team is BLOCKED from drafting NFL (even if they have flex picks available)
- Same scenario, but current team has NOT drafted NFL yet:
- If current team drafts NFL participant: 2 NFL participants remain
- And 2 teams still need to draft their first NFL participant (3 - 1)
- This is exactly enough (2 = 2)
- **Result**: Current team is ALLOWED to draft NFL
### Combined Rules
Both constraints must be satisfied:
1. Team must have flex picks available OR not have drafted from this sport
2. AND drafting from this sport won't prevent other teams from fulfilling requirements
## Technical Changes Required
### 1. Data Model Changes
**Location**: `database/schema.ts`
No schema changes needed - we can derive this from existing data:
- Season has `draftRounds` field
- Season is linked to sports via `seasonSports`
- Draft picks are linked to participants which are linked to sports
### 2. Utility Function - Calculate Eligible Sports
**Location**: `app/lib/draft-eligibility.ts` (new file)
Create utility to determine which sports a team can draft from:
```typescript
interface SportAvailability {
sportId: string;
undraftedCount: number;
teamsStillNeeding: number;
canDraftAsFirst: boolean; // undraftedCount >= teamsStillNeeding
canDraftAsFlex: boolean; // undraftedCount > teamsStillNeeding
}
interface DraftEligibility {
teamId: string;
eligibleSportIds: Set<string>;
ineligibleReasons: Record<string, string>; // sportId -> reason
flexPicksUsed: number;
flexPicksAvailable: number;
picksBySport: Record<string, number>;
sportAvailability: Record<string, SportAvailability>;
}
function calculateDraftEligibility(
teamId: string,
teamPicks: Array<{ participant: { sport: { id: string } } }>,
allPicks: Array<{ teamId: string; participant: { sport: { id: string } } }>,
allParticipants: Array<{ id: string; sport: { id: string } }>,
seasonSports: Array<{ id: string; name: string }>,
totalRounds: number,
allTeams: Array<{ id: string }>
): DraftEligibility
```
**Logic**:
**Step 1: Calculate team's flex pick status**
1. Count picks per sport for the team
2. Calculate flex picks used = sum of (picks in sport - 1) for each sport with >1 pick
3. Calculate flex picks available = totalRounds - totalSports
4. Determine if team has flexes remaining: `flexPicksUsed < flexPicksAvailable`
**Step 2: Calculate global sport availability**
For each sport:
1. Count total participants in sport
2. Count drafted participants in sport (from allPicks)
3. Calculate undrafted = total - drafted
4. Count teams with 0 picks in this sport
5. Determine:
- `canDraftAsFirst = undraftedCount >= teamsStillNeeding`
- `canDraftAsFlex = undraftedCount > teamsStillNeeding`
**Step 3: Combine constraints**
For each sport, determine eligibility:
1. Check if team has drafted from sport: `currentTeamHasSport = picksBySport[sportId] > 0`
2. If NOT yet drafted from sport:
- Allowed if: sport availability `canDraftAsFirst` is true
- Reason if blocked: "Only X participants left for Y teams needing this sport"
3. If already drafted from sport (would be flex):
- Allowed if: team has flexes remaining AND sport availability `canDraftAsFlex` is true
- Reason if blocked (flex exhausted): "All flex picks used"
- Reason if blocked (global): "Would prevent other teams from getting this sport"
**Step 4: Return eligibility**
- Set of eligible sport IDs
- Map of ineligible reasons for UI display
### 3. Frontend - Draft Room UI
**Location**: `app/routes/leagues/$leagueId.draft.$seasonId.tsx`
#### Changes to Loader
- Load all sports for the season (already done via `seasonSports`)
- Load ALL participants for the season (already done)
- Load ALL teams for the season (already done via `season.teams`)
- Pass sport count to client
- All data needed for eligibility calculation is available
#### Changes to Component State
- Track team's picks by sport (can derive from `picks` state filtered by `userTeam.id`)
- Calculate eligibility when rendering participant list
#### Changes to Participant Table
- Add eligibility check before rendering buttons
- Disable "Pick" button if sport is not eligible
- Disable "Add to Queue" if sport is not eligible
- Show visual indicator (badge/tooltip) explaining why ineligible
```typescript
// Pseudo-code
const eligibility = calculateDraftEligibility(
userTeam.id,
picks.filter(p => p.team.id === userTeam.id),
picks, // all picks
availableParticipants, // all participants
seasonSports,
season.draftRounds,
season.teams
);
// In participant rendering
const isEligible = eligibility.eligibleSportIds.has(participant.sport.id);
const ineligibleReason = eligibility.ineligibleReasons[participant.sport.id];
```
#### Visual Indicators
- Add a status banner showing:
- Flex picks remaining for current team
- Sport-by-sport breakdown showing current team's picks
- Sport availability warnings (e.g., "Only 2 NFL participants left for 3 teams")
- Show pick counts by sport in My Queue section
- Grayed out rows for ineligible participants
- Tooltip/badge explaining why a participant is ineligible:
- "Flex picks exhausted - must draft from [sports list]"
- "Only X participants left for Y teams needing this sport"
- Combination of both constraints
### 4. Backend - API Validation
**Location**: `app/routes/api/draft/make-pick.tsx`
Add validation before creating pick:
1. Load team's existing picks
2. Load season sports count
3. Calculate eligibility
4. Return 400 error if sport is not eligible
```typescript
// Validation logic
const teamPicks = await getTeamPicks(teamId, seasonId);
const allPicks = await getAllPicksForSeason(seasonId);
const allParticipants = await getAllParticipantsForSeason(seasonId);
const seasonSports = await getSeasonSports(seasonId);
const allTeams = await getTeamsForSeason(seasonId);
const eligibility = calculateDraftEligibility(
teamId,
teamPicks,
allPicks,
allParticipants,
seasonSports,
season.draftRounds,
allTeams
);
if (!eligibility.eligibleSportIds.has(participant.sport.id)) {
const reason = eligibility.ineligibleReasons[participant.sport.id] ||
"Cannot draft from this sport";
return json(
{ error: reason },
{ status: 400 }
);
}
```
### 5. Backend - Autodraft Logic
**Location**: `app/models/draft-pick.ts` (autodraft function)
Update autodraft logic to respect eligibility:
1. Calculate team's eligibility
2. Filter queue by eligible sports only
3. If queue has no eligible participants:
- Fall back to highest EV participant from eligible sports
4. If no eligible participants at all (edge case):
- Log error and pause draft
### 6. Force Manual Pick Dialog
**Location**: `app/routes/leagues/$leagueId.draft.$seasonId.tsx` (Force Manual Pick Dialog)
Update the dialog:
1. Calculate target team's eligibility
2. Filter participant list to only eligible sports
3. Gray out / hide ineligible participants
4. Show status message about sport restrictions
### 7. Commissioner Force Autopick
**Location**: `app/routes/api/draft/force-autopick.tsx`
Use same autodraft logic as regular autopick - it will automatically respect eligibility.
## Implementation Steps
### Phase 1: Core Logic
1. Create `app/lib/draft-eligibility.ts` with calculation function
2. Add unit tests for eligibility calculation covering:
- Team flex pick constraints
- Global sport availability constraints
- Combined constraints
- Edge cases
3. Add helper functions to models:
- Get all picks for season with sport data
- Get all participants for season grouped by sport
- Get all teams for season with their picks
### Phase 2: Backend Validation
1. Update `app/routes/api/draft/make-pick.tsx` with validation
2. Update autodraft logic in `app/models/draft-pick.ts`
3. Update `app/routes/api/draft/force-autopick.tsx`
4. Update `app/routes/api/draft/force-manual-pick.tsx`
### Phase 3: Frontend UI
1. Update draft room loader to include sports count
2. Add eligibility calculation to component
3. Update participant table with disabled states
4. Add visual indicators (badges, tooltips)
5. Update Force Manual Pick dialog
6. Add status banner showing flex picks remaining
### Phase 4: Testing
1. Unit tests for eligibility calculation
2. Integration tests for API validation
3. E2E test for draft flow with restrictions
4. Test autodraft respects restrictions
5. Test force picks respect restrictions
## Edge Cases to Handle
1. **Single sport league**: No global restrictions apply (everyone needs same sport, but flex picks still matter)
2. **More sports than rounds**: Error - invalid configuration (impossible to satisfy)
3. **Queue only has ineligible participants**: Autodraft falls back to available pool filtered by eligibility
4. **All participants in eligible sports are drafted**: Draft becomes impossible - should show error/warning
5. **Mid-draft rule change**: Rules apply based on current season configuration
6. **Last few picks**: Global constraints become very restrictive - may force specific picks
7. **All teams have drafted from a sport**: Global constraint for that sport becomes `undrafted > 0` (no minimum needed)
8. **Participant pool imbalanced**: Some sports have very few participants - may trigger global constraints early
9. **Multiple teams on same pick in round**: Each team's eligibility calculated independently
## Testing Scenarios
### Scenario 1: Team Flex Limit
- 3 sports, 5 rounds, 10 teams
- Team drafts 2 from sport A, 1 from sport B
- Verify sport A is disabled (flexes exhausted), sports B and C are enabled
- Make pick from sport C
- Verify only sport B is enabled
### Scenario 2: Global Constraint - Flex Pick Blocked
- 3 sports, 5 rounds, 8 teams
- Sport A has 10 total participants
- 8 picks made from sport A (2 left)
- 3 teams haven't drafted sport A yet (including current team)
- Current team tries to draft sport A as their first from that sport
- Should be ALLOWED (2 >= 3 - 1, which is 2 >= 2)
- Different team that HAS sport A tries to draft another
- Should be BLOCKED (2 > 3 is false)
### Scenario 3: Global Constraint - First Pick Allowed
- 3 sports, 5 rounds, 6 teams
- Sport B has 8 total participants
- 5 picks made from sport B (3 left)
- 3 teams haven't drafted sport B yet
- Current team (one of the 3) tries to draft sport B
- Should be ALLOWED (3 >= 3)
- After pick: 2 left, 2 teams need it
- Next team tries to draft sport B as first
- Should be ALLOWED (2 >= 2)
### Scenario 4: Autodraft with Global Constraints
- 3 sports, 5 rounds
- Queue has only sport A participants
- Team has 2 flexes used (3 sport A picks)
- Global constraint: only 1 sport A left, 2 teams need it
- Autodraft should skip queue AND skip sport A
- Should pick highest EV from sport B or C that's eligible
### Scenario 5: Force Pick with Combined Constraints
- Commissioner forces pick for team
- Team has exhausted flexes (can only pick from sports not yet drafted)
- Global constraints eliminate one of those sports
- Dialog should only show participants from eligible sports
- Should display clear reason for each ineligible sport
### Scenario 6: Late Draft Deadlock Prevention
- 4 teams, 3 sports, 4 rounds
- Near end of draft, only sport C participants left
- 2 teams haven't drafted sport C
- 2 sport C participants left
- Teams should both be able to complete draft
- Verify global constraint math: 2 >= 2 allows first team, then 1 >= 1 allows second
### Scenario 7: Impossible Draft State
- 5 teams, 3 sports, 5 rounds
- Sport A has only 4 total participants
- 4 teams need sport A, 0 picks from sport A made yet
- System should detect this is impossible early
- Consider adding pre-draft validation for commissioners
## Data Dependencies
### For Eligibility Calculation:
- **Season**: `draftRounds`, `id`
- **Season Sports**: All sports in the season (id, name)
- **All Teams**: List of all teams in the season
- **All Draft Picks**: Complete pick history with team and sport info
- Needed to calculate: picks per sport per team, total drafted per sport
- **All Participants**: Complete participant pool with sport association
- Needed to calculate: undrafted count per sport
- **Current Team Picks**: Filtered list for flex calculation
### Query Optimization:
- Most queries already exist in draft room loader
- Need to add: pick counts grouped by (teamId, sportId)
- Need to add: undrafted participant counts by sportId
- Consider caching eligibility calculation result per render
- Recalculate only when picks array changes
## UI/UX Considerations
1. **Clear feedback**: User should understand why they can't pick certain participants
2. **Status visibility**: Show flex picks remaining prominently
3. **Sport breakdown**: Show picks per sport in a visible location
4. **Error messages**: Clear explanation when API rejects a pick
5. **Queue validation**: Warn if queue contains ineligible participants
## Performance Considerations
- Eligibility calculation complexity:
- Team picks iteration: O(picks per team) = O(rounds) ≈ O(20)
- All picks iteration for sport counts: O(total picks) = O(teams × rounds) ≈ O(200)
- Per-sport calculations: O(sports) ≈ O(3-5)
- **Total: O(teams × rounds)** - acceptable for real-time calculation
- Calculate once per render and memoize result
- Recalculate only when picks state changes (use useMemo)
- No database schema changes needed
- API validation adds one calculation step (same complexity)
- Consider pre-computing pick counts in loader for faster initial render
- Frontend calculation more responsive than API call per participant
## Future Enhancements
1. **Pre-draft validation**:
- Validate season configuration (enough participants per sport for all teams)
- Show warnings to commissioner before starting draft
2. **Queue validation and management**:
- Real-time warnings if queue contains ineligible participants
- Auto-reorder queue suggestions
- "Smart queue" that prioritizes based on scarcity
3. **Draft strategy tools**:
- Recommendations: "Sport X is running low, consider drafting soon"
- Scarcity indicators on participant list
- Probability calculations for participant availability
4. **Configurable rules**:
- League settings to customize flex pick rules
- Option to disable global constraints (less restrictive mode)
- Minimum picks per sport (beyond just 1)
5. **Advanced restrictions**:
- Position-based restrictions within sports (QB, RB, etc.)
- Conference/division restrictions
- Team ownership limits (e.g., max 2 players from same NFL team)
6. **Undo/Correction tools**:
- Commissioner ability to undo picks if rules were violated
- Draft state debugging tools
7. **Analytics**:
- Track how often global constraints triggered
- Identify bottleneck sports
- Post-draft report on draft efficiency