brackt/app/models/__tests__/bracket-seeding.test.ts
Chris Parsons e2b178221a
Add oxlint linting setup with zero errors (#194)
* Add oxlint and fix all lint errors

- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-explicit-any warnings and upgrade tsconfig to ES2023

- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
  participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Promote no-explicit-any to error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00

210 lines
6.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
/**
* Tests for proper tournament bracket seeding
* Phase 2.7 Bug Fix: Ensure proper seeding (1v16, 8v9, etc.) instead of sequential (1v2, 3v4)
*/
/**
* Helper to simulate the seeding algorithm from playoff-match.ts
*/
function generateStandardSeeding(teamCount: number): [number, number][] {
if (![4, 8, 16, 32].includes(teamCount)) {
const pairs: [number, number][] = [];
for (let i = 0; i < teamCount; i += 2) {
pairs.push([i, i + 1]);
}
return pairs;
}
const rounds = Math.log2(teamCount);
let seeds = [0, 1];
for (let round = 1; round < rounds; round++) {
const newSeeds: number[] = [];
const maxSeed = Math.pow(2, round + 1) - 1;
for (const seed of seeds) {
newSeeds.push(seed);
newSeeds.push(maxSeed - seed);
}
seeds = newSeeds;
}
const pairs: [number, number][] = [];
for (let i = 0; i < seeds.length; i += 2) {
pairs.push([seeds[i], seeds[i + 1]]);
}
return pairs;
}
describe("Bracket Seeding", () => {
describe("4-Team Bracket", () => {
it("generates correct seeding pairs", () => {
const pairs = generateStandardSeeding(4);
// Should be: 1v4, 2v3 (0-indexed: 0v3, 1v2)
expect(pairs).toEqual([
[0, 3], // Seed 1 vs Seed 4
[1, 2], // Seed 2 vs Seed 3
]);
});
it("balances the bracket", () => {
const pairs = generateStandardSeeding(4);
// Top half: seeds 1,4 (sum = 5)
// Bottom half: seeds 2,3 (sum = 5)
const topSum = pairs[0][0] + pairs[0][1] + 2; // +2 for 1-indexing
const bottomSum = pairs[1][0] + pairs[1][1] + 2;
expect(topSum).toBe(bottomSum);
});
});
describe("8-Team Bracket", () => {
it("generates correct seeding pairs", () => {
const pairs = generateStandardSeeding(8);
// Standard 8-team seeding: 1v8, 4v5, 2v7, 3v6
expect(pairs).toEqual([
[0, 7], // Seed 1 vs Seed 8
[3, 4], // Seed 4 vs Seed 5
[1, 6], // Seed 2 vs Seed 7
[2, 5], // Seed 3 vs Seed 6
]);
});
it("ensures top seeds are separated", () => {
const pairs = generateStandardSeeding(8);
// Seeds 1 and 2 should be in different halves
const match1Seeds = [pairs[0][0] + 1, pairs[0][1] + 1];
const match2Seeds = [pairs[1][0] + 1, pairs[1][1] + 1];
const topHalf = [...match1Seeds, ...match2Seeds];
const match3Seeds = [pairs[2][0] + 1, pairs[2][1] + 1];
const match4Seeds = [pairs[3][0] + 1, pairs[3][1] + 1];
const bottomHalf = [...match3Seeds, ...match4Seeds];
// Seed 1 should be in top half, seed 2 in bottom half (or vice versa)
expect(topHalf.includes(1) && bottomHalf.includes(2)).toBe(true);
});
});
describe("16-Team Bracket", () => {
it("generates 8 matches", () => {
const pairs = generateStandardSeeding(16);
expect(pairs).toHaveLength(8);
});
it("uses all 16 seeds exactly once", () => {
const pairs = generateStandardSeeding(16);
const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b);
expect(allSeeds).toEqual(Array.from({ length: 16 }, (_, i) => i + 1));
});
it("ensures seed 1 plays seed 16", () => {
const pairs = generateStandardSeeding(16);
const firstMatch = pairs[0];
// Convert to 1-indexed seeds
const seed1 = firstMatch[0] + 1;
const seed2 = firstMatch[1] + 1;
expect([seed1, seed2].toSorted()).toEqual([1, 16]);
});
it("has proper matchup sums for balance", () => {
const pairs = generateStandardSeeding(16);
// Each matchup should have a sum that indicates proper seeding
// 1v16 (sum=17), 2v15 (sum=17), etc.
const sums = pairs.map(([a, b]) => (a + 1) + (b + 1));
// All matchups should sum to 17 (n + 1 for 16 teams)
expect(sums.every(sum => sum === 17)).toBe(true);
});
it("balances each quarterfinal pod", () => {
const pairs = generateStandardSeeding(16);
// Each pod of 4 teams should have balanced seeds
// Pod 1 (matches 0,1): 1,16,8,9 sum = 34
// Pod 2 (matches 2,3): 5,12,4,13 sum = 34
// Pod 3 (matches 4,5): 6,11,3,14 sum = 34
// Pod 4 (matches 6,7): 7,10,2,15 sum = 34
const pod1 = pairs[0].concat(pairs[1]).map(s => s + 1);
const pod2 = pairs[2].concat(pairs[3]).map(s => s + 1);
const pod3 = pairs[4].concat(pairs[5]).map(s => s + 1);
const pod4 = pairs[6].concat(pairs[7]).map(s => s + 1);
const sum1 = pod1.reduce((a, b) => a + b, 0);
const sum2 = pod2.reduce((a, b) => a + b, 0);
const sum3 = pod3.reduce((a, b) => a + b, 0);
const sum4 = pod4.reduce((a, b) => a + b, 0);
expect(sum1).toBe(sum2);
expect(sum2).toBe(sum3);
expect(sum3).toBe(sum4);
expect(sum1).toBe(34); // (1+16+8+9)
});
});
describe("32-Team Bracket", () => {
it("generates 16 matches", () => {
const pairs = generateStandardSeeding(32);
expect(pairs).toHaveLength(16);
});
it("ensures seed 1 plays seed 32", () => {
const pairs = generateStandardSeeding(32);
const firstMatch = pairs[0];
const seed1 = firstMatch[0] + 1;
const seed2 = firstMatch[1] + 1;
expect([seed1, seed2].toSorted()).toEqual([1, 32]);
});
it("uses all 32 seeds exactly once", () => {
const pairs = generateStandardSeeding(32);
const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b);
expect(allSeeds).toEqual(Array.from({ length: 32 }, (_, i) => i + 1));
});
});
describe("Non-Standard Sizes", () => {
it("returns sequential pairs for non-power-of-2 sizes", () => {
const pairs = generateStandardSeeding(68);
// For NCAA 68, should return sequential until custom logic is implemented
expect(pairs[0]).toEqual([0, 1]);
expect(pairs[1]).toEqual([2, 3]);
expect(pairs).toHaveLength(34);
});
});
describe("Seed Info Display", () => {
it("formats seed info correctly for 16-team bracket", () => {
const pairs = generateStandardSeeding(16);
// Verify seed info would be displayed correctly (1-indexed for users)
const match1SeedInfo = `${pairs[0][0] + 1} vs ${pairs[0][1] + 1}`;
expect(match1SeedInfo).toBe("1 vs 16");
// Check that all seed info strings are formatted correctly
const allSeedInfo = pairs.map(([a, b]) => `${a + 1} vs ${b + 1}`);
expect(allSeedInfo).toHaveLength(8);
// Each should be "N vs M" format where N + M = 17
allSeedInfo.forEach(info => {
expect(info).toMatch(/^\d+ vs \d+$/);
});
});
});
});