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

10 KiB
Raw Blame 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:

flexPicksUsed = sum of (picks in sport - 1) for each sport with >1 pick
flexPicksAvailable = totalRounds - totalSports
hasFlexesRemaining = flexPicksUsed < flexPicksAvailable

Global Sport Availability:

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

// 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

// 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

  • All code written and tested
  • TypeScript compilation successful
  • 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