2025-11-04 22:09:44 -08:00
|
|
|
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);
|
2026-03-21 09:44:05 -07:00
|
|
|
const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b);
|
2025-11-04 22:09:44 -08:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
expect([seed1, seed2].toSorted()).toEqual([1, 16]);
|
2025-11-04 22:09:44 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
expect([seed1, seed2].toSorted()).toEqual([1, 32]);
|
2025-11-04 22:09:44 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("uses all 32 seeds exactly once", () => {
|
|
|
|
|
const pairs = generateStandardSeeding(32);
|
2026-03-21 09:44:05 -07:00
|
|
|
const allSeeds = pairs.flat().map(s => s + 1).toSorted((a, b) => a - b);
|
2025-11-04 22:09:44 -08:00
|
|
|
|
|
|
|
|
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+$/);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|