From 4de6cb23a0f8187273e361ef4a93c748f41b6366 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 21 Mar 2026 10:14:54 -0700 Subject: [PATCH] Fix no-shadow and consistent-function-scoping lint violations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves all 11 no-shadow and 16 consistent-function-scoping oxlint warnings and promotes both rules to errors in .oxlintrc.json. no-shadow: renamed Drizzle callback params (sports→s, matches→m, seasons→s) to avoid shadowing outer imports; removed shadowed destructures (eq, inArray) from where callbacks; renamed inner template→bracketTemplate, prev→currentTimers, season→ss, name→teamName (with name: teamName fix to preserve semantics). consistent-function-scoping: moved formatDate, getRankBadge, getMovementIndicator, getPositionBadge, getStatusBadge, toDateStr, elo (×2), weightedPick, sortByMatchNumber (×2) to module scope; moved formatTime (×2), isValidLeagueName, getDraftTimes, makeSeasonQueues to file scope in test files. Co-Authored-By: Claude Sonnet 4.6 --- .oxlintrc.json | 5 +- app/components/StandingsTable.tsx | 54 ++++++------- app/components/__tests__/DraftGrid.test.tsx | 14 ++-- app/components/scoring/SeasonStandings.tsx | 76 +++++++++---------- .../standings/PointProgressionChart.tsx | 11 ++- app/lib/__tests__/utils.test.ts | 52 ++++++------- app/models/__tests__/auto-pick.test.ts | 9 +-- app/models/scoring-calculator.ts | 2 +- app/models/sport.ts | 14 ++-- app/models/sports-season.ts | 6 +- app/routes/admin._index.tsx | 5 +- ...ts-seasons.$id.events.$eventId.bracket.tsx | 4 +- .../admin.sports-seasons.$id.events.tsx | 42 +++++----- app/routes/admin.standings-snapshots.tsx | 2 +- .../leagues/$leagueId.draft.$seasonId.tsx | 4 +- app/routes/leagues/$leagueId.settings.tsx | 18 ++--- ...d.sports-seasons.$sportsSeasonId.server.ts | 3 +- app/routes/leagues/new.tsx | 4 +- app/services/simulations/nba-simulator.ts | 10 ++- app/services/simulations/ncaam-simulator.ts | 7 +- app/services/simulations/ncaaw-simulator.ts | 7 +- app/services/simulations/nhl-simulator.ts | 60 ++++++++------- 22 files changed, 208 insertions(+), 201 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 8d3c6b6..5e9ece2 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -36,7 +36,10 @@ "react/no-array-index-key": "error", "react/self-closing-comp": "warn", - "import/no-duplicates": "error" + "import/no-duplicates": "error", + + "no-shadow": "error", + "unicorn/consistent-function-scoping": "error" }, "overrides": [ { diff --git a/app/components/StandingsTable.tsx b/app/components/StandingsTable.tsx index 8a6be01..99372c6 100644 --- a/app/components/StandingsTable.tsx +++ b/app/components/StandingsTable.tsx @@ -32,6 +32,33 @@ interface StandingsTableProps { showPlacementBreakdown?: boolean; } +function getRankBadge(rank: number) { + if (rank === 1) { + return ( +
+ + {rank} +
+ ); + } else if (rank === 2) { + return ( +
+ + {rank} +
+ ); + } else if (rank === 3) { + return ( +
+ + {rank} +
+ ); + } else { + return {rank}; + } +} + export function StandingsTable({ standings, showMovement = true, @@ -66,33 +93,6 @@ export function StandingsTable({ } }; - const getRankBadge = (rank: number) => { - if (rank === 1) { - return ( -
- - {rank} -
- ); - } else if (rank === 2) { - return ( -
- - {rank} -
- ); - } else if (rank === 3) { - return ( -
- - {rank} -
- ); - } else { - return {rank}; - } - }; - return ( diff --git a/app/components/__tests__/DraftGrid.test.tsx b/app/components/__tests__/DraftGrid.test.tsx index a7f069d..4c80d46 100644 --- a/app/components/__tests__/DraftGrid.test.tsx +++ b/app/components/__tests__/DraftGrid.test.tsx @@ -3,6 +3,13 @@ import { render, screen } from '@testing-library/react'; import { DraftGrid } from '../DraftGrid'; import { mockDraftSlots } from '~/test/fixtures/team'; +function formatTime(seconds: number | undefined) { + if (seconds === undefined) return '--:--'; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + describe('DraftGrid Component', () => { const mockGrid = [ [ @@ -117,13 +124,6 @@ describe('DraftGrid Component', () => { }); it('should display timers when provided', () => { - const formatTime = (seconds: number | undefined) => { - if (seconds === undefined) return '--:--'; - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - return `${mins}:${secs.toString().padStart(2, '0')}`; - }; - render( 0) { + return ( +
+ + +{change} +
+ ); + } else if (change < 0) { + return ( +
+ + {change} +
+ ); + } else { + return ( +
+ +
+ ); + } +} + +function getPositionBadge(position: number, isTied: boolean) { + const suffix = position === 1 ? "st" : position === 2 ? "nd" : position === 3 ? "rd" : "th"; + const positionText = isTied ? `T${position}` : `${position}${suffix}`; + return {positionText}; +} + export function SeasonStandings({ standings, teamOwnerships = [], @@ -77,45 +114,6 @@ export function SeasonStandings({ return ownershipMap.get(participantId) || null; }; - // Get movement indicator - const getMovementIndicator = ( - currentPosition: number, - previousPosition?: number | null - ) => { - if (!previousPosition) return null; - - const change = previousPosition - currentPosition; // Positive means moved up - - if (change > 0) { - return ( -
- - +{change} -
- ); - } else if (change < 0) { - return ( -
- - {change} -
- ); - } else { - return ( -
- -
- ); - } - }; - - // Get position display - const getPositionBadge = (position: number, isTied: boolean) => { - const suffix = position === 1 ? "st" : position === 2 ? "nd" : position === 3 ? "rd" : "th"; - const positionText = isTied ? `T${position}` : `${position}${suffix}`; - return {positionText}; - }; - // Check if multiple participants share the same position const checkIfTied = (standing: SeasonStanding): boolean => { return standings.some( diff --git a/app/components/standings/PointProgressionChart.tsx b/app/components/standings/PointProgressionChart.tsx index aa2f3eb..ec867bc 100644 --- a/app/components/standings/PointProgressionChart.tsx +++ b/app/components/standings/PointProgressionChart.tsx @@ -30,6 +30,11 @@ const COLORS = [ '#6366f1', // indigo ]; +function formatDate(dateStr: string) { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); +} + export function PointProgressionChart({ chartData, teams }: PointProgressionChartProps) { if (chartData.length === 0) { return ( @@ -48,12 +53,6 @@ export function PointProgressionChart({ chartData, teams }: PointProgressionChar ); } - // Format date for display - const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); - }; - return ( diff --git a/app/lib/__tests__/utils.test.ts b/app/lib/__tests__/utils.test.ts index 6c8b98b..9b16715 100644 --- a/app/lib/__tests__/utils.test.ts +++ b/app/lib/__tests__/utils.test.ts @@ -1,6 +1,32 @@ import { describe, it, expect } from 'vitest'; import { cn } from '~/lib/utils'; +function formatTime(seconds: number | undefined): string { + if (seconds === undefined) return '--:--'; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +function isValidLeagueName(name: string): boolean { + return name.trim().length >= 3 && name.trim().length <= 50; +} + +type DraftSpeed = 'fast' | 'standard' | 'slow' | 'very-slow'; + +function getDraftTimes(speed: DraftSpeed): { initial: number; increment: number } { + switch (speed) { + case 'fast': + return { initial: 60, increment: 10 }; + case 'standard': + return { initial: 120, increment: 15 }; + case 'slow': + return { initial: 28800, increment: 3600 }; + case 'very-slow': + return { initial: 43200, increment: 3600 }; + } +} + describe('Utils', () => { describe('cn (className merger)', () => { it('should merge class names', () => { @@ -32,13 +58,6 @@ describe('Utils', () => { }); describe('Draft Time Formatting', () => { - const formatTime = (seconds: number | undefined): string => { - if (seconds === undefined) return '--:--'; - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; - return `${mins}:${secs.toString().padStart(2, '0')}`; - }; - it('should format time correctly', () => { expect(formatTime(120)).toBe('2:00'); expect(formatTime(90)).toBe('1:30'); @@ -57,10 +76,6 @@ describe('Draft Time Formatting', () => { }); describe('League Name Validation', () => { - const isValidLeagueName = (name: string): boolean => { - return name.trim().length >= 3 && name.trim().length <= 50; - }; - it('should accept valid league names', () => { expect(isValidLeagueName('My League')).toBe(true); expect(isValidLeagueName('Test League 2025')).toBe(true); @@ -85,21 +100,6 @@ describe('League Name Validation', () => { }); describe('Draft Speed Presets', () => { - type DraftSpeed = 'fast' | 'standard' | 'slow' | 'very-slow'; - - const getDraftTimes = (speed: DraftSpeed): { initial: number; increment: number } => { - switch (speed) { - case 'fast': - return { initial: 60, increment: 10 }; - case 'standard': - return { initial: 120, increment: 15 }; - case 'slow': - return { initial: 28800, increment: 3600 }; - case 'very-slow': - return { initial: 43200, increment: 3600 }; - } - }; - it('should return correct times for fast speed', () => { const times = getDraftTimes('fast'); expect(times.initial).toBe(60); diff --git a/app/models/__tests__/auto-pick.test.ts b/app/models/__tests__/auto-pick.test.ts index e1dc588..f432448 100644 --- a/app/models/__tests__/auto-pick.test.ts +++ b/app/models/__tests__/auto-pick.test.ts @@ -406,6 +406,10 @@ describe("autoPickForTeam – queueOnly constraint (AC2 & AC3)", () => { }); }); +function makeSeasonQueues(entries: [string, { id: string; participantId: string; queuePosition: number }[]][]) { + return new Map(entries.map(([teamId, items]) => [teamId, items as any])); +} + describe("pruneIneligibleQueueItems", () => { beforeEach(() => { vi.clearAllMocks(); @@ -415,11 +419,6 @@ describe("pruneIneligibleQueueItems", () => { vi.mocked(getSeasonSportsSimple).mockResolvedValue([]); }); - // Helper to build the Map returned by getAllQueuesForSeason - function makeSeasonQueues(entries: [string, { id: string; participantId: string; queuePosition: number }[]][]) { - return new Map(entries.map(([teamId, items]) => [teamId, items as any])); - } - it("removes queued snooker player when snooker is no longer eligible for a team", async () => { vi.mocked(getDraftPicksWithSports).mockResolvedValue([]); vi.mocked(getParticipantsForSeasonWithSports).mockResolvedValue([ diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 3de3e83..f10a49d 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -166,7 +166,7 @@ export async function processPlayoffEvent( eq(schema.playoffMatches.scoringEventId, eventId), eq(schema.playoffMatches.round, event.playoffRound) ), - orderBy: (matches, { asc }) => [asc(matches.matchNumber)], + orderBy: (m, { asc }) => [asc(m.matchNumber)], }); // Process based on round diff --git a/app/models/sport.ts b/app/models/sport.ts index 05ab849..be39e8e 100644 --- a/app/models/sport.ts +++ b/app/models/sport.ts @@ -36,22 +36,22 @@ export async function findSportBySlug(slug: string): Promise export async function findAllSports(): Promise { const db = database(); return await db.query.sports.findMany({ - orderBy: (sports, { asc }) => [asc(sports.name)], + orderBy: (s, { asc }) => [asc(s.name)], }); } export async function findAllSportsWithActiveSeasons(): Promise { const db = database(); const sports = await db.query.sports.findMany({ - orderBy: (sports, { asc }) => [asc(sports.name)], + orderBy: (s, { asc }) => [asc(s.name)], with: { sportsSeasons: { - where: (sportsSeasons, { eq, or }) => + where: (ss, { or }) => or( - eq(sportsSeasons.status, "active"), - eq(sportsSeasons.status, "upcoming") + eq(ss.status, "active"), + eq(ss.status, "upcoming") ), - orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)], + orderBy: (ss, { desc }) => [desc(ss.year)], }, }, }); @@ -70,7 +70,7 @@ export async function findSportsByType(type: SportType): Promise { const db = database(); return await db.query.sports.findMany({ where: eq(schema.sports.type, type), - orderBy: (sports, { asc }) => [asc(sports.name)], + orderBy: (s, { asc }) => [asc(s.name)], }); } diff --git a/app/models/sports-season.ts b/app/models/sports-season.ts index 56bc9a1..d2557d0 100644 --- a/app/models/sports-season.ts +++ b/app/models/sports-season.ts @@ -43,10 +43,10 @@ export async function findSportsSeasonsBySportId(sportId: string): Promise { const db = database(); return await db.query.sportsSeasons.findMany({ - where: (sportsSeasons, { eq, and }) => + where: (ss, { and }) => and( - eq(sportsSeasons.sportId, sportId), - eq(sportsSeasons.status, "active") + eq(ss.sportId, sportId), + eq(ss.status, "active") ), orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)], with: { diff --git a/app/routes/admin._index.tsx b/app/routes/admin._index.tsx index 001f2b9..1dd8759 100644 --- a/app/routes/admin._index.tsx +++ b/app/routes/admin._index.tsx @@ -21,11 +21,14 @@ export function meta(): Route.MetaDescriptors { return [{ title: "Admin - Brackt" }]; } +function toDateStr(d: Date) { + return d.toISOString().split("T")[0]; +} + function getTodayAndTomorrowDates() { const today = new Date(); const tomorrow = new Date(today); tomorrow.setDate(today.getDate() + 1); - const toDateStr = (d: Date) => d.toISOString().split("T")[0]; return { today: toDateStr(today), tomorrow: toDateStr(tomorrow) }; } diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx index aff3ba8..29595ac 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx @@ -277,8 +277,8 @@ export default function EventBracket({ // Memoized: Get available rounds in chronological order const availableRounds = useMemo(() => { - const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined; - return getOrderedRoundsFromMatches(matches, template); + const bracketTemplate = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined; + return getOrderedRoundsFromMatches(matches, bracketTemplate); }, [matches, event.bracketTemplateId]); // Check if all matches are complete diff --git a/app/routes/admin.sports-seasons.$id.events.tsx b/app/routes/admin.sports-seasons.$id.events.tsx index 151bb95..033a633 100644 --- a/app/routes/admin.sports-seasons.$id.events.tsx +++ b/app/routes/admin.sports-seasons.$id.events.tsx @@ -45,6 +45,27 @@ export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors { export { loader, action }; +function getStatusBadge(isComplete: boolean, eventType: string, eventDate?: string | null, eventStartsAt?: Date | string | null) { + if (isComplete) { + return ( + + Completed + + ); + } + if (eventType === "schedule_event") { + const isPast = eventStartsAt + ? new Date(eventStartsAt) < new Date() + : eventDate !== null && eventDate !== undefined && eventDate < new Date().toISOString().split("T")[0]; + return isPast ? ( + Results Pending + ) : ( + Upcoming + ); + } + return In Progress; +} + export default function SportsSeasonEvents({ loaderData, actionData, @@ -60,27 +81,6 @@ export default function SportsSeasonEvents({ ? "schedule_event" : "playoff_game"; - const getStatusBadge = (isComplete: boolean, eventType: string, eventDate?: string | null, eventStartsAt?: Date | string | null) => { - if (isComplete) { - return ( - - Completed - - ); - } - if (eventType === "schedule_event") { - const isPast = eventStartsAt - ? new Date(eventStartsAt) < new Date() - : eventDate !== null && eventDate !== undefined && eventDate < new Date().toISOString().split("T")[0]; - return isPast ? ( - Results Pending - ) : ( - Upcoming - ); - } - return In Progress; - }; - return (
diff --git a/app/routes/admin.standings-snapshots.tsx b/app/routes/admin.standings-snapshots.tsx index 7247d41..5f34883 100644 --- a/app/routes/admin.standings-snapshots.tsx +++ b/app/routes/admin.standings-snapshots.tsx @@ -40,7 +40,7 @@ export async function loader() { with: { league: true, }, - orderBy: (seasons, { desc }) => [desc(seasons.createdAt)], + orderBy: (s, { desc }) => [desc(s.createdAt)], }); // For each season, get the most recent snapshot diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index 3ee1996..fea1292 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -494,8 +494,8 @@ export default function DraftRoom() { // Sync timers — the server-side timer state may have changed while // we were disconnected (time bank adjustments, new timers, etc.) if (timers.length > 0) { - setTeamTimers((prev) => { - const updated = { ...prev }; + setTeamTimers((currentTimers) => { + const updated = { ...currentTimers }; timers.forEach((timer) => { updated[timer.teamId] = timer.timeRemaining; }); diff --git a/app/routes/leagues/$leagueId.settings.tsx b/app/routes/leagues/$leagueId.settings.tsx index c5ccf2e..aaae51d 100644 --- a/app/routes/leagues/$leagueId.settings.tsx +++ b/app/routes/leagues/$leagueId.settings.tsx @@ -1103,23 +1103,23 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
{allSportsSeasons.length > 0 ? ( - allSportsSeasons.map((season) => ( -
+ allSportsSeasons.map((ss) => ( +
handleSportToggle(season.id)} + value={ss.id} + checked={selectedSports.has(ss.id)} + onCheckedChange={() => handleSportToggle(ss.id)} disabled={!canEditSports} />
diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts index 32c8ea3..0b959ab 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts @@ -163,8 +163,7 @@ export async function loader(args: Route.LoaderArgs) { if (events.length > 0) { const eventIds = events.map((e) => e.id); const matches = await db.query.playoffMatches.findMany({ - where: (playoffMatches, { inArray }) => - inArray(playoffMatches.scoringEventId, eventIds), + where: (pm) => inArray(pm.scoringEventId, eventIds), with: { participant1: true, participant2: true, diff --git a/app/routes/leagues/new.tsx b/app/routes/leagues/new.tsx index 99b7167..3d77e8a 100644 --- a/app/routes/leagues/new.tsx +++ b/app/routes/leagues/new.tsx @@ -182,9 +182,9 @@ export async function action(args: Route.ActionArgs) { // Create teams with fun random names const joinAsPlayer = formData.get("joinAsPlayer") === "on"; const teamNames = generateUniqueTeamNames(teamCountNum); - const teams = teamNames.map((name, i) => ({ + const teams = teamNames.map((teamName, i) => ({ seasonId: season.id, - name, + name: teamName, ownerId: joinAsPlayer && i === 0 ? userId : null, })); diff --git a/app/services/simulations/nba-simulator.ts b/app/services/simulations/nba-simulator.ts index 147ecfc..4f4e438 100644 --- a/app/services/simulations/nba-simulator.ts +++ b/app/services/simulations/nba-simulator.ts @@ -240,6 +240,12 @@ interface TeamEntry { data: NbaTeamData | undefined; } +/** Get Elo for a team entry. + * Fallback 1400 = conservative below-average estimate for unknown/unrecognized teams. */ +function elo(entry: TeamEntry): number { + return entry.data?.elo ?? 1400; +} + // ─── Simulator ──────────────────────────────────────────────────────────────── export class NBASimulator implements Simulator { @@ -297,10 +303,6 @@ export class NBASimulator implements Simulator { return 11; // Missed playoffs }; - /** Get Elo for a team entry. - * Fallback 1400 = conservative below-average estimate for unknown/unrecognized teams. */ - const elo = (entry: TeamEntry): number => entry.data?.elo ?? 1400; - /** Simulate a single playoff game. Returns the winner. */ const simGame = (a: TeamEntry, b: TeamEntry): TeamEntry => Math.random() < eloWinProbability(elo(a), elo(b)) ? a : b; diff --git a/app/services/simulations/ncaam-simulator.ts b/app/services/simulations/ncaam-simulator.ts index 12d4aba..4dd2d72 100644 --- a/app/services/simulations/ncaam-simulator.ts +++ b/app/services/simulations/ncaam-simulator.ts @@ -296,6 +296,10 @@ export function kenpomWinProbability(netrtgA: number, netrtgB: number): number { return 1 / (1 + Math.exp(-(netrtgA - netrtgB) / KENPOM_SCALE_FACTOR)); } +function sortByMatchNumber(matches: T[]): T[] { + return [...matches].toSorted((a, b) => a.matchNumber - b.matchNumber); +} + // ─── Simulator ──────────────────────────────────────────────────────────────── export class NCAAMSimulator implements Simulator { @@ -342,9 +346,6 @@ export class NCAAMSimulator implements Simulator { byRound.get(m.round)!.push(m); } - const sortByMatchNumber = (matches: typeof allMatches) => - [...matches].toSorted((a, b) => a.matchNumber - b.matchNumber); - const firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : []; const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null; const r32Matches = byRound.has("Round of 32") ? sortByMatchNumber(byRound.get("Round of 32")!) : null; diff --git a/app/services/simulations/ncaaw-simulator.ts b/app/services/simulations/ncaaw-simulator.ts index 78452f8..253f572 100644 --- a/app/services/simulations/ncaaw-simulator.ts +++ b/app/services/simulations/ncaaw-simulator.ts @@ -276,6 +276,10 @@ export function barthagWinProbability(barthagA: number, barthagB: number): numbe return pA / total; } +function sortByMatchNumber(matches: T[]): T[] { + return [...matches].toSorted((a, b) => a.matchNumber - b.matchNumber); +} + // ─── Simulator ──────────────────────────────────────────────────────────────── export class NCAAWSimulator implements Simulator { @@ -319,9 +323,6 @@ export class NCAAWSimulator implements Simulator { byRound.get(m.round)!.push(m); } - const sortByMatchNumber = (matches: typeof allMatches) => - [...matches].toSorted((a, b) => a.matchNumber - b.matchNumber); - const firstFourMatches = byRound.has("First Four") ? sortByMatchNumber(byRound.get("First Four")!) : []; const r64Matches = byRound.has("Round of 64") ? sortByMatchNumber(byRound.get("Round of 64")!) : null; const r32Matches = byRound.has("Round of 32") ? sortByMatchNumber(byRound.get("Round of 32")!) : null; diff --git a/app/services/simulations/nhl-simulator.ts b/app/services/simulations/nhl-simulator.ts index e296af3..3c5a702 100644 --- a/app/services/simulations/nhl-simulator.ts +++ b/app/services/simulations/nhl-simulator.ts @@ -291,6 +291,37 @@ type DivisionSeedings = { third: TeamEntry; }; +/** Get Elo for a team entry. Fallback 1400 for unknown teams. */ +function elo(entry: TeamEntry): number { + return entry.data?.elo ?? 1400; +} + +/** + * Weighted pick without replacement. + * Draws one team from `pool` using the given seeding weight key, excluding `excluded`. + * Returns undefined if no eligible team has positive weight — the caller should + * treat that as a degenerate draw and skip the iteration. + */ +function weightedPick( + pool: TeamEntry[], + weightKey: "p_div1" | "p_div2" | "p_div3" | "p_wc1" | "p_wc2", + excluded: Set +): TeamEntry | undefined { + const eligible = pool.filter((t) => !excluded.has(t)); + if (eligible.length === 0) return undefined; + + const weights = eligible.map((t) => t.data?.[weightKey] ?? 0); + const total = weights.reduce((s, w) => s + w, 0); + if (total === 0) return undefined; + + let r = Math.random() * total; + for (let i = 0; i < eligible.length; i++) { + r -= weights[i]; + if (r <= 0) return eligible[i]; + } + return eligible[eligible.length - 1]; +} + // ─── Simulator ──────────────────────────────────────────────────────────────── export class NHLSimulator implements Simulator { @@ -348,9 +379,6 @@ export class NHLSimulator implements Simulator { // ─── Helpers (defined once, outside the hot loop) ───────────────────────── - /** Get Elo for a team entry. Fallback 1400 for unknown teams. */ - const elo = (entry: TeamEntry): number => entry.data?.elo ?? 1400; - /** Simulate a best-of-7 series. Returns winner and loser. */ const simSeries = (a: TeamEntry, b: TeamEntry): { winner: TeamEntry; loser: TeamEntry } => { const winProb = eloWinProbability(elo(a), elo(b)); @@ -362,32 +390,6 @@ export class NHLSimulator implements Simulator { return winsA === 4 ? { winner: a, loser: b } : { winner: b, loser: a }; }; - /** - * Weighted pick without replacement. - * Draws one team from `pool` using the given seeding weight key, excluding `excluded`. - * Returns undefined if no eligible team has positive weight — the caller should - * treat that as a degenerate draw and skip the iteration. - */ - const weightedPick = ( - pool: TeamEntry[], - weightKey: "p_div1" | "p_div2" | "p_div3" | "p_wc1" | "p_wc2", - excluded: Set - ): TeamEntry | undefined => { - const eligible = pool.filter((t) => !excluded.has(t)); - if (eligible.length === 0) return undefined; - - const weights = eligible.map((t) => t.data?.[weightKey] ?? 0); - const total = weights.reduce((s, w) => s + w, 0); - if (total === 0) return undefined; // no team has positive weight for this slot - - let r = Math.random() * total; - for (let i = 0; i < eligible.length; i++) { - r -= weights[i]; - if (r <= 0) return eligible[i]; - } - return eligible[eligible.length - 1]; - }; - /** * Draw the top-3 playoff seeds for a single division. * Returns undefined for each position if no team has weight for that slot