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

337 lines
10 KiB
Markdown
Raw Normal View History

# Omni League Draft Rules - Implementation Summary
## Overview
Successfully implemented Omni league draft eligibility rules that enforce:
1. **Team Flex Pick Limits**: Teams can only use (totalRounds - totalSports) flex picks
2. **Global Sport Availability**: Prevents teams from drafting in a way that blocks other teams from meeting the "at least one from each sport" requirement
## Implementation Status: ✅ COMPLETE
All phases have been completed and type-checked successfully.
---
## Phase 1: Core Logic ✅
### Files Created
- `app/lib/draft-eligibility.ts` - Core eligibility calculation with full TypeScript types
- `app/lib/__tests__/draft-eligibility.test.ts` - Comprehensive test suite (13 tests, all passing)
### Key Functions
- `calculateDraftEligibility()` - Main eligibility calculator
- Inputs: team picks, all picks, all participants, sports, rounds, teams
- Returns: eligible sports, ineligible reasons, flex pick status, sport availability
- `getEligibilitySummary()` - Human-readable status summary
### Test Coverage
- Team flex pick constraints (3 tests)
- Global sport availability constraints (4 tests)
- Combined constraints (2 tests)
- Edge cases (3 tests)
- Summary generation (1 test)
All tests pass ✅
---
## Phase 2: Backend Validation ✅
### Model Layer Enhancements
**`app/models/draft-pick.ts`**
- Added `getDraftPicksWithSports()` - Get all picks with sport data
- Added `getTeamDraftPicksWithSports()` - Get team-specific picks with sport data
**`app/models/participant.ts`**
- Added `getParticipantsForSeasonWithSports()` - Get all participants with sport association
**`app/models/season-sport.ts`**
- Added `getSeasonSportsSimple()` - Get simple sport list for calculations
**`app/models/draft-utils.ts`**
- Updated `autoPickForTeam()` - Now respects eligibility constraints
- Filters queue by eligible sports
- Falls back to eligible sports when queue is empty
- Signature changed to require `draftRounds` and `allTeamIds`
- Updated `getTopAvailableParticipant()` - Now accepts optional `eligibleSportIds` filter
### API Endpoints Updated
**`app/routes/api/draft.make-pick.ts`** ✅
- Added eligibility validation before creating pick
- Returns specific error reason if sport is ineligible
- Validates both team flex limits and global availability
**`app/routes/api/draft.force-manual-pick.ts`** ✅
- Added same eligibility validation
- Commissioner picks must also respect rules
**`app/routes/api/draft.force-autopick.ts`** ✅
- Refactored to use updated `autoPickForTeam()` function
- Automatically respects eligibility via autodraft logic
### Validation Flow
1. Load all necessary data (picks, participants, sports, teams)
2. Calculate eligibility using `calculateDraftEligibility()`
3. Check if participant's sport is in `eligibleSportIds`
4. Return 400 error with specific reason if ineligible
5. Proceed with pick if eligible
---
## Phase 3: Frontend UI ✅
### Draft Room Updates (`app/routes/leagues/$leagueId.draft.$seasonId.tsx`)
**New Imports**
- `useMemo` from React
- `calculateDraftEligibility` and `getEligibilitySummary` from eligibility lib
**Eligibility Calculation**
- Added `useMemo` hook to calculate user team's eligibility
- Recalculates when picks, participants, or team changes
- Transforms loader data to match eligibility function signature
**Force Pick Eligibility**
- Separate `useMemo` for force manual pick dialog
- Calculates eligibility for the selected team (not current user)
### Visual Indicators
**Participant Table**
1. **Row Styling**
- Ineligible participants: red background (`bg-red-50/30 dark:bg-red-950/20`)
- Drafted participants: muted with opacity
- Eligible participants: normal hover state
2. **Badges**
- "Ineligible" badge (destructive variant) for blocked sports
- Shows ineligible reason on hover
- "Drafted" badge for already picked participants
- "Queued" badge only shown if participant is also eligible
3. **Buttons**
- "Pick" button disabled when sport is ineligible
- "Add to Queue" button disabled for ineligible sports
- Tooltips explain why buttons are disabled
### Status Banner (in "My Queue" section)
**Displays:**
- Flex picks used/available with color coding:
- Green when flexes remain
- Red when exhausted
- Picks by sport breakdown (e.g., "NFL: 2, NBA: 1")
- Active restrictions with reasons (first 2 shown)
- Example: "NFL: Would prevent other teams from getting NFL (2 left for 3 teams)"
**Styling:**
- Bordered card with muted background
- Clear hierarchy with sections
- Color-coded warnings
### Force Manual Pick Dialog
**Enhancements:**
- Uses `forcePickEligibility` calculation for target team
- Filters out ineligible participants:
- Red background for ineligible rows
- "Ineligible" badge with reason
- Disabled "Draft" button
- Shows participant sport and EV
- Maintains existing search and filter functionality
---
## Technical Details
### Eligibility Algorithm
**Team Flex Pick Check:**
```typescript
flexPicksUsed = sum of (picks in sport - 1) for each sport with >1 pick
flexPicksAvailable = totalRounds - totalSports
hasFlexesRemaining = flexPicksUsed < flexPicksAvailable
```
**Global Sport Availability:**
```typescript
For each sport:
undraftedCount = total participants - drafted participants
teamsStillNeeding = teams with 0 picks in this sport
canDraftAsFirst = undraftedCount >= teamsStillNeeding
canDraftAsFlex = undraftedCount > teamsStillNeeding
```
**Combined Logic:**
- If team hasn't drafted from sport: allowed if `canDraftAsFirst`
- If team has drafted from sport (flex): allowed if `hasFlexesRemaining AND canDraftAsFlex`
### Data Flow
1. **Loader** loads all necessary data (picks, participants, sports, teams)
2. **Component** calculates eligibility in `useMemo`
3. **UI** renders with eligibility state
4. **User action** triggers API call
5. **API** validates eligibility server-side
6. **Socket** broadcasts pick to all clients
7. **Clients** update state and recalculate eligibility
### Performance
- Eligibility calculation: O(teams × rounds) ≈ O(200) for typical draft
- Memoized in React - only recalculates when picks change
- No database changes needed
- Minimal API overhead (one calculation per pick attempt)
---
## Testing
### Unit Tests ✅
- **21 comprehensive tests** covering all eligibility scenarios:
- Team flex pick constraints (3 tests)
- Global sport availability constraints (4 tests)
- Combined constraints (2 tests)
- Edge cases (3 tests)
- Summary generation (1 test)
- **Exact boundary conditions (6 tests)** - Testing critical thresholds
- **Draft progression simulation (1 test)** - Multi-pick scenarios
- **Late-draft forced choices (2 tests)** - Constraint-driven selections
- Run with: `npm test -- app/lib/__tests__/draft-eligibility.test.ts`
- **All 21 tests passing** ✅
### Test Coverage Details
The test suite thoroughly validates:
- ✅ Team cannot exceed flex pick limit
- ✅ Global sport availability protects future teams
- ✅ Exact boundary conditions (e.g., undrafted === teamsNeeding)
- ✅ Eligibility changes as draft progresses
- ✅ Late-draft scenarios where only one sport is eligible
- ✅ Complex multi-team scenarios
### Type Safety ✅
- Full TypeScript coverage
- Run with: `npm run typecheck`
- No errors
### Integration Testing Notes
- The core eligibility algorithm is thoroughly unit tested (21 tests)
- Backend API endpoints include inline eligibility validation
- Autodraft logic (`draft-utils.ts`) uses the tested eligibility function
- Manual/E2E testing recommended for full integration validation
### Manual Testing Needed
- [ ] E2E test for draft flow with eligibility restrictions
- [ ] Test autodraft respects eligibility in live draft
- [ ] Test force picks respect eligibility
- [ ] Test UI shows correct eligibility status and messaging
- [ ] Test with various team/sport/round configurations
---
## Files Modified
### Created
1. `app/lib/draft-eligibility.ts`
2. `app/lib/__tests__/draft-eligibility.test.ts`
### Modified
1. `app/models/draft-pick.ts`
2. `app/models/participant.ts`
3. `app/models/season-sport.ts`
4. `app/models/draft-utils.ts`
5. `app/routes/api/draft.make-pick.ts`
6. `app/routes/api/draft.force-manual-pick.ts`
7. `app/routes/api/draft.force-autopick.ts`
8. `app/routes/leagues/$leagueId.draft.$seasonId.tsx`
---
## Usage Examples
### Backend Validation
```typescript
// In API route
const eligibility = calculateDraftEligibility(
teamId,
teamPicks,
allPicks,
allParticipants,
seasonSports,
season.draftRounds,
allTeams
);
if (!eligibility.eligibleSportIds.has(participant.sport.id)) {
return Response.json({
error: eligibility.ineligibleReasons[participant.sport.id]
}, { status: 400 });
}
```
### Frontend UI
```typescript
// In component
const eligibility = useMemo(() => {
if (!userTeam) return null;
return calculateDraftEligibility(/*...*/);
}, [userTeam, picks, availableParticipants, season]);
// In render
const isEligible = eligibility?.eligibleSportIds.has(participant.sport.id);
const ineligibleReason = eligibility?.ineligibleReasons[participant.sport.id];
<Button disabled={!isEligible} title={ineligibleReason}>
Pick
</Button>
```
---
## Edge Cases Handled
1. **Single sport league**: Flex picks still apply (rounds - 1)
2. **No eligible sports**: API returns clear error, autodraft falls back
3. **All teams drafted from sport**: Global constraint becomes `undrafted > 0`
4. **Empty participant pool**: Returns null, handled gracefully
5. **Mid-draft state changes**: Eligibility recalculates on every pick
---
## Future Enhancements
As documented in the plan:
1. Pre-draft validation (warn commissioners of impossible configs)
2. Queue validation warnings
3. Scarcity indicators on participant list
4. Draft strategy recommendations
5. Configurable rules per league
6. Position-based restrictions within sports
7. Commissioner undo/correction tools
8. Analytics on constraint triggers
---
## Deployment Checklist
- [x] All code written and tested
- [x] TypeScript compilation successful
- [x] Unit tests passing
- [ ] Manual testing complete
- [ ] E2E tests written
- [ ] Database migrations (none needed)
- [ ] Documentation updated
- [ ] Code review completed
---
## Notes
- No database schema changes required
- All validation happens in application layer
- Backward compatible (will work with existing drafts)
- Performance impact minimal (simple calculations)
- User experience significantly improved with clear feedback