brackt/plans/omni-league-draft-rules-IMPLEMENTATION-SUMMARY.md
Chris Parsons decb28dc19 Implement Omni League Draft Eligibility Rules
- Created draft eligibility calculation logic in `app/lib/draft-eligibility.ts`
- Added interfaces for sport availability and draft eligibility
- Implemented `calculateDraftEligibility` function to determine eligible sports based on team picks and global availability
- Developed `getEligibilitySummary` function for human-readable eligibility status
- Updated API endpoints to validate eligibility before making picks
- Enhanced frontend draft room UI to reflect eligibility status and provide visual indicators for ineligible participants
- Comprehensive implementation summary and testing documentation added
- All changes type-checked and unit tests passed successfully
2025-10-24 21:12:07 -07:00

336 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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