Fix no-shadow and consistent-function-scoping lint violations
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 <noreply@anthropic.com>
This commit is contained in:
parent
180aad1520
commit
4de6cb23a0
22 changed files with 208 additions and 201 deletions
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,6 +32,33 @@ interface StandingsTableProps {
|
|||
showPlacementBreakdown?: boolean;
|
||||
}
|
||||
|
||||
function getRankBadge(rank: number) {
|
||||
if (rank === 1) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Trophy className="h-5 w-5 text-yellow-500" />
|
||||
<span className="font-bold text-lg">{rank}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (rank === 2) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Medal className="h-5 w-5 text-gray-400" />
|
||||
<span className="font-semibold">{rank}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (rank === 3) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Award className="h-5 w-5 text-amber-700" />
|
||||
<span className="font-semibold">{rank}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <span className="font-medium">{rank}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
export function StandingsTable({
|
||||
standings,
|
||||
showMovement = true,
|
||||
|
|
@ -66,33 +93,6 @@ export function StandingsTable({
|
|||
}
|
||||
};
|
||||
|
||||
const getRankBadge = (rank: number) => {
|
||||
if (rank === 1) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Trophy className="h-5 w-5 text-yellow-500" />
|
||||
<span className="font-bold text-lg">{rank}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (rank === 2) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Medal className="h-5 w-5 text-gray-400" />
|
||||
<span className="font-semibold">{rank}</span>
|
||||
</div>
|
||||
);
|
||||
} else if (rank === 3) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Award className="h-5 w-5 text-amber-700" />
|
||||
<span className="font-semibold">{rank}</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <span className="font-medium">{rank}</span>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<DraftGrid
|
||||
draftSlots={mockDraftSlots}
|
||||
|
|
|
|||
|
|
@ -55,33 +55,10 @@ interface SeasonStandingsProps {
|
|||
* - Movement indicators (position changes)
|
||||
* - Handles ties (same points = same position)
|
||||
*/
|
||||
export function SeasonStandings({
|
||||
standings,
|
||||
teamOwnerships = [],
|
||||
userParticipantIds = [],
|
||||
showOwnership = true,
|
||||
isFinalized = false,
|
||||
title = "Championship Standings",
|
||||
description: _description,
|
||||
}: SeasonStandingsProps) {
|
||||
const userParticipantSet = new Set(userParticipantIds);
|
||||
// Create ownership map for fast lookup
|
||||
const ownershipMap = new Map<string, TeamOwnership>();
|
||||
teamOwnerships.forEach((ownership) => {
|
||||
ownershipMap.set(ownership.participantId, ownership);
|
||||
});
|
||||
|
||||
// Get ownership info for a participant
|
||||
const getOwnership = (participantId: string): TeamOwnership | null => {
|
||||
if (!showOwnership) return null;
|
||||
return ownershipMap.get(participantId) || null;
|
||||
};
|
||||
|
||||
// Get movement indicator
|
||||
const getMovementIndicator = (
|
||||
function getMovementIndicator(
|
||||
currentPosition: number,
|
||||
previousPosition?: number | null
|
||||
) => {
|
||||
) {
|
||||
if (!previousPosition) return null;
|
||||
|
||||
const change = previousPosition - currentPosition; // Positive means moved up
|
||||
|
|
@ -107,13 +84,34 @@ export function SeasonStandings({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Get position display
|
||||
const getPositionBadge = (position: number, isTied: boolean) => {
|
||||
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 <span className="font-medium">{positionText}</span>;
|
||||
}
|
||||
|
||||
export function SeasonStandings({
|
||||
standings,
|
||||
teamOwnerships = [],
|
||||
userParticipantIds = [],
|
||||
showOwnership = true,
|
||||
isFinalized = false,
|
||||
title = "Championship Standings",
|
||||
description: _description,
|
||||
}: SeasonStandingsProps) {
|
||||
const userParticipantSet = new Set(userParticipantIds);
|
||||
// Create ownership map for fast lookup
|
||||
const ownershipMap = new Map<string, TeamOwnership>();
|
||||
teamOwnerships.forEach((ownership) => {
|
||||
ownershipMap.set(ownership.participantId, ownership);
|
||||
});
|
||||
|
||||
// Get ownership info for a participant
|
||||
const getOwnership = (participantId: string): TeamOwnership | null => {
|
||||
if (!showOwnership) return null;
|
||||
return ownershipMap.get(participantId) || null;
|
||||
};
|
||||
|
||||
// Check if multiple participants share the same position
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<teamId, queue[]> 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([
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -36,22 +36,22 @@ export async function findSportBySlug(slug: string): Promise<Sport | undefined>
|
|||
export async function findAllSports(): Promise<Sport[]> {
|
||||
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<SportWithActiveSeasons[]> {
|
||||
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<Sport[]> {
|
|||
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)],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,10 @@ export async function findSportsSeasonsBySportId(sportId: string): Promise<Sport
|
|||
export async function findActiveSportsSeasonsBySportId(sportId: string): Promise<SportsSeason[]> {
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -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) };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -45,22 +45,7 @@ export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
|
|||
|
||||
export { loader, action };
|
||||
|
||||
export default function SportsSeasonEvents({
|
||||
loaderData,
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
const { sportsSeason, events, qpStandings, scoringRules } = loaderData;
|
||||
|
||||
const [createEventStartsAt, setCreateEventStartsAt] = useState("");
|
||||
|
||||
const defaultEventType =
|
||||
sportsSeason.scoringPattern === "qualifying_points"
|
||||
? "major_tournament"
|
||||
: sportsSeason.scoringPattern === "season_standings"
|
||||
? "schedule_event"
|
||||
: "playoff_game";
|
||||
|
||||
const getStatusBadge = (isComplete: boolean, eventType: string, eventDate?: string | null, eventStartsAt?: Date | string | null) => {
|
||||
function getStatusBadge(isComplete: boolean, eventType: string, eventDate?: string | null, eventStartsAt?: Date | string | null) {
|
||||
if (isComplete) {
|
||||
return (
|
||||
<Badge variant="default" className="bg-emerald-500">
|
||||
|
|
@ -79,7 +64,22 @@ export default function SportsSeasonEvents({
|
|||
);
|
||||
}
|
||||
return <Badge variant="secondary">In Progress</Badge>;
|
||||
};
|
||||
}
|
||||
|
||||
export default function SportsSeasonEvents({
|
||||
loaderData,
|
||||
actionData,
|
||||
}: Route.ComponentProps) {
|
||||
const { sportsSeason, events, qpStandings, scoringRules } = loaderData;
|
||||
|
||||
const [createEventStartsAt, setCreateEventStartsAt] = useState("");
|
||||
|
||||
const defaultEventType =
|
||||
sportsSeason.scoringPattern === "qualifying_points"
|
||||
? "major_tournament"
|
||||
: sportsSeason.scoringPattern === "season_standings"
|
||||
? "schedule_event"
|
||||
: "playoff_game";
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1103,23 +1103,23 @@ export default function LeagueSettings({ loaderData, actionData }: Route.Compone
|
|||
</Label>
|
||||
<div className="max-h-96 overflow-y-auto border rounded-md p-3 space-y-2">
|
||||
{allSportsSeasons.length > 0 ? (
|
||||
allSportsSeasons.map((season) => (
|
||||
<div key={season.id} className="flex items-center space-x-2">
|
||||
allSportsSeasons.map((ss) => (
|
||||
<div key={ss.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={season.id}
|
||||
id={ss.id}
|
||||
name="sportsSeasons"
|
||||
value={season.id}
|
||||
checked={selectedSports.has(season.id)}
|
||||
onCheckedChange={() => handleSportToggle(season.id)}
|
||||
value={ss.id}
|
||||
checked={selectedSports.has(ss.id)}
|
||||
onCheckedChange={() => handleSportToggle(ss.id)}
|
||||
disabled={!canEditSports}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={season.id}
|
||||
htmlFor={ss.id}
|
||||
className={`font-normal flex-1 ${canEditSports ? 'cursor-pointer' : 'cursor-not-allowed opacity-60'}`}
|
||||
>
|
||||
<span className="font-medium">{season.sport.name}</span> - {season.name} ({season.year})
|
||||
<span className="font-medium">{ss.sport.name}</span> - {ss.name} ({ss.year})
|
||||
<Badge variant="outline" className="ml-2 text-xs">
|
||||
{season.scoringType.replace("_", " ")}
|
||||
{ss.scoringType.replace("_", " ")}
|
||||
</Badge>
|
||||
</Label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -296,6 +296,10 @@ export function kenpomWinProbability(netrtgA: number, netrtgB: number): number {
|
|||
return 1 / (1 + Math.exp(-(netrtgA - netrtgB) / KENPOM_SCALE_FACTOR));
|
||||
}
|
||||
|
||||
function sortByMatchNumber<T extends { matchNumber: number }>(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;
|
||||
|
|
|
|||
|
|
@ -276,6 +276,10 @@ export function barthagWinProbability(barthagA: number, barthagB: number): numbe
|
|||
return pA / total;
|
||||
}
|
||||
|
||||
function sortByMatchNumber<T extends { matchNumber: number }>(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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
): 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>
|
||||
): 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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue