feat: Implement NCAA 68 tournament structure with First Four play-in games and Round of 64 seeding logic
This commit is contained in:
parent
dfe4b7b643
commit
bbacb00d6c
3 changed files with 441 additions and 10 deletions
205
app/models/__tests__/ncaa-68-bracket.test.ts
Normal file
205
app/models/__tests__/ncaa-68-bracket.test.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { NCAA_68, getScoringRoundType } from "~/lib/bracket-templates";
|
||||
|
||||
/**
|
||||
* NCAA 68 Bracket Unit Tests - Phase 2.8
|
||||
*
|
||||
* Tests the NCAA 68 tournament template structure and scoring logic:
|
||||
* - 68 teams total (60 direct seeds + 8 in First Four)
|
||||
* - First Four: 4 play-in games (8 teams)
|
||||
* - Round of 64: 32 games
|
||||
* - Only Elite Eight and beyond score fantasy points
|
||||
*/
|
||||
describe("NCAA 68 Bracket Template - Phase 2.8", () => {
|
||||
describe("Template Structure", () => {
|
||||
it("has correct total teams", () => {
|
||||
expect(NCAA_68.totalTeams).toBe(68);
|
||||
});
|
||||
|
||||
it("has all 7 rounds", () => {
|
||||
expect(NCAA_68.rounds).toHaveLength(7);
|
||||
expect(NCAA_68.rounds[0].name).toBe("First Four");
|
||||
expect(NCAA_68.rounds[1].name).toBe("Round of 64");
|
||||
expect(NCAA_68.rounds[2].name).toBe("Round of 32");
|
||||
expect(NCAA_68.rounds[3].name).toBe("Sweet Sixteen");
|
||||
expect(NCAA_68.rounds[4].name).toBe("Elite Eight");
|
||||
expect(NCAA_68.rounds[5].name).toBe("Final Four");
|
||||
expect(NCAA_68.rounds[6].name).toBe("Championship");
|
||||
});
|
||||
|
||||
it("has correct match counts per round", () => {
|
||||
expect(NCAA_68.rounds[0].matchCount).toBe(4); // First Four
|
||||
expect(NCAA_68.rounds[1].matchCount).toBe(32); // Round of 64
|
||||
expect(NCAA_68.rounds[2].matchCount).toBe(16); // Round of 32
|
||||
expect(NCAA_68.rounds[3].matchCount).toBe(8); // Sweet Sixteen
|
||||
expect(NCAA_68.rounds[4].matchCount).toBe(4); // Elite Eight (Quarterfinals)
|
||||
expect(NCAA_68.rounds[5].matchCount).toBe(2); // Final Four (Semifinals)
|
||||
expect(NCAA_68.rounds[6].matchCount).toBe(1); // Championship (Finals)
|
||||
});
|
||||
|
||||
it("has correct round feeding structure", () => {
|
||||
expect(NCAA_68.rounds[0].feedsInto).toBe("Round of 64");
|
||||
expect(NCAA_68.rounds[1].feedsInto).toBe("Round of 32");
|
||||
expect(NCAA_68.rounds[2].feedsInto).toBe("Sweet Sixteen");
|
||||
expect(NCAA_68.rounds[3].feedsInto).toBe("Elite Eight");
|
||||
expect(NCAA_68.rounds[4].feedsInto).toBe("Final Four");
|
||||
expect(NCAA_68.rounds[5].feedsInto).toBe("Championship");
|
||||
expect(NCAA_68.rounds[6].feedsInto).toBeNull(); // Championship has no next round
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scoring Configuration", () => {
|
||||
it("marks scoring to start at Elite Eight", () => {
|
||||
expect(NCAA_68.scoringStartsAtRound).toBe("Elite Eight");
|
||||
});
|
||||
|
||||
it("marks First Four as non-scoring", () => {
|
||||
const firstFour = NCAA_68.rounds[0];
|
||||
expect(firstFour.name).toBe("First Four");
|
||||
expect(firstFour.isScoring).toBe(false);
|
||||
});
|
||||
|
||||
it("marks Round of 64 as non-scoring", () => {
|
||||
const roundOf64 = NCAA_68.rounds[1];
|
||||
expect(roundOf64.name).toBe("Round of 64");
|
||||
expect(roundOf64.isScoring).toBe(false);
|
||||
});
|
||||
|
||||
it("marks Round of 32 as non-scoring", () => {
|
||||
const roundOf32 = NCAA_68.rounds[2];
|
||||
expect(roundOf32.name).toBe("Round of 32");
|
||||
expect(roundOf32.isScoring).toBe(false);
|
||||
});
|
||||
|
||||
it("marks Sweet Sixteen as non-scoring", () => {
|
||||
const sweetSixteen = NCAA_68.rounds[3];
|
||||
expect(sweetSixteen.name).toBe("Sweet Sixteen");
|
||||
expect(sweetSixteen.isScoring).toBe(false);
|
||||
});
|
||||
|
||||
it("marks Elite Eight as scoring (Quarterfinals)", () => {
|
||||
const eliteEight = NCAA_68.rounds[4];
|
||||
expect(eliteEight.name).toBe("Elite Eight");
|
||||
expect(eliteEight.isScoring).toBe(true);
|
||||
});
|
||||
|
||||
it("marks Final Four as scoring (Semifinals)", () => {
|
||||
const finalFour = NCAA_68.rounds[5];
|
||||
expect(finalFour.name).toBe("Final Four");
|
||||
expect(finalFour.isScoring).toBe(true);
|
||||
});
|
||||
|
||||
it("marks Championship as scoring (Finals)", () => {
|
||||
const championship = NCAA_68.rounds[6];
|
||||
expect(championship.name).toBe("Championship");
|
||||
expect(championship.isScoring).toBe(true);
|
||||
});
|
||||
|
||||
it("has exactly 3 scoring rounds (Elite Eight, Final Four, Championship)", () => {
|
||||
const scoringRounds = NCAA_68.rounds.filter((r) => r.isScoring);
|
||||
expect(scoringRounds).toHaveLength(3);
|
||||
expect(scoringRounds[0].name).toBe("Elite Eight");
|
||||
expect(scoringRounds[1].name).toBe("Final Four");
|
||||
expect(scoringRounds[2].name).toBe("Championship");
|
||||
});
|
||||
|
||||
it("has exactly 4 non-scoring rounds (FF, R64, R32, S16)", () => {
|
||||
const nonScoringRounds = NCAA_68.rounds.filter((r) => !r.isScoring);
|
||||
expect(nonScoringRounds).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scoring Round Types", () => {
|
||||
it("identifies Elite Eight as quarterfinals", () => {
|
||||
const type = getScoringRoundType("Elite Eight", NCAA_68);
|
||||
expect(type).toBe("quarterfinals");
|
||||
});
|
||||
|
||||
it("identifies Final Four as semifinals", () => {
|
||||
const type = getScoringRoundType("Final Four", NCAA_68);
|
||||
expect(type).toBe("semifinals");
|
||||
});
|
||||
|
||||
it("identifies Championship as finals", () => {
|
||||
const type = getScoringRoundType("Championship", NCAA_68);
|
||||
expect(type).toBe("finals");
|
||||
});
|
||||
|
||||
it("returns null for non-scoring rounds", () => {
|
||||
expect(getScoringRoundType("First Four", NCAA_68)).toBeNull();
|
||||
expect(getScoringRoundType("Round of 64", NCAA_68)).toBeNull();
|
||||
expect(getScoringRoundType("Round of 32", NCAA_68)).toBeNull();
|
||||
expect(getScoringRoundType("Sweet Sixteen", NCAA_68)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tournament Math", () => {
|
||||
it("has correct number of total matches", () => {
|
||||
// 4 + 32 + 16 + 8 + 4 + 2 + 1 = 67 total matches
|
||||
const totalMatches = NCAA_68.rounds.reduce((sum, round) => sum + round.matchCount, 0);
|
||||
expect(totalMatches).toBe(67);
|
||||
});
|
||||
|
||||
it("eliminates 60 teams before Elite Eight (4+32+16+8)", () => {
|
||||
const nonScoringRounds = NCAA_68.rounds.filter((r) => !r.isScoring);
|
||||
const teamsEliminatedBeforeEliteEight = nonScoringRounds.reduce(
|
||||
(sum, round) => sum + round.matchCount,
|
||||
0
|
||||
);
|
||||
expect(teamsEliminatedBeforeEliteEight).toBe(60); // 4+32+16+8
|
||||
});
|
||||
|
||||
it("has 8 teams in Elite Eight (top 8 scoring)", () => {
|
||||
const eliteEightRound = NCAA_68.rounds.find((r) => r.name === "Elite Eight");
|
||||
const teamsInEliteEight = eliteEightRound!.matchCount * 2;
|
||||
expect(teamsInEliteEight).toBe(8);
|
||||
});
|
||||
|
||||
it("verifies only 8 teams score fantasy points (Q18)", () => {
|
||||
// Elite Eight has 4 matches = 8 teams
|
||||
// These are the ONLY teams that can score fantasy points
|
||||
const scoringRounds = NCAA_68.rounds.filter((r) => r.isScoring);
|
||||
const eliteEightMatches = scoringRounds[0].matchCount;
|
||||
const scoringTeamsCount = eliteEightMatches * 2;
|
||||
expect(scoringTeamsCount).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Phase 2.8 Requirements", () => {
|
||||
it("meets requirement: 68 total teams", () => {
|
||||
expect(NCAA_68.totalTeams).toBe(68);
|
||||
});
|
||||
|
||||
it("meets requirement: 4 First Four play-in games", () => {
|
||||
const firstFour = NCAA_68.rounds[0];
|
||||
expect(firstFour.matchCount).toBe(4);
|
||||
});
|
||||
|
||||
it("meets requirement: Only Elite Eight and beyond score points", () => {
|
||||
const scoringRoundNames = NCAA_68.rounds
|
||||
.filter((r) => r.isScoring)
|
||||
.map((r) => r.name);
|
||||
expect(scoringRoundNames).toEqual(["Elite Eight", "Final Four", "Championship"]);
|
||||
});
|
||||
|
||||
it("meets requirement: Early elimination = 0 points (per Q20)", () => {
|
||||
const firstFour = NCAA_68.rounds[0];
|
||||
const roundOf64 = NCAA_68.rounds[1];
|
||||
const roundOf32 = NCAA_68.rounds[2];
|
||||
const sweetSixteen = NCAA_68.rounds[3];
|
||||
|
||||
expect(firstFour.isScoring).toBe(false); // 0 points
|
||||
expect(roundOf64.isScoring).toBe(false); // 0 points
|
||||
expect(roundOf32.isScoring).toBe(false); // 0 points
|
||||
expect(sweetSixteen.isScoring).toBe(false); // 0 points
|
||||
});
|
||||
|
||||
it("verifies total participants: 60 direct + 8 First Four", () => {
|
||||
const firstFourTeams = 4 * 2; // 4 games × 2 teams = 8 teams
|
||||
const directSeeds = 68 - firstFourTeams; // 60 teams directly seeded
|
||||
expect(directSeeds).toBe(60);
|
||||
expect(firstFourTeams).toBe(8);
|
||||
expect(directSeeds + firstFourTeams).toBe(68);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -252,6 +252,87 @@ export async function advanceWinner(
|
|||
await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId });
|
||||
}
|
||||
|
||||
/**
|
||||
* NCAA 68 First Four seeding
|
||||
* Returns matchups for the 4 play-in games
|
||||
*
|
||||
* First Four structure (using 0-indexed participant array):
|
||||
* - Games 1-2: Participants 64-67 (16 seeds, lowest auto-qualifiers)
|
||||
* - Winners are 16 seeds and face 1 seeds in Round of 64
|
||||
* - Games 3-4: Participants 60-63 (11/12 seeds, last at-large bids)
|
||||
* - Winners are 11 seeds and face 6 seeds in Round of 64
|
||||
*/
|
||||
function generateNCAA68FirstFourSeeding(): [number, number][] {
|
||||
return [
|
||||
[64, 65], // First Four Game 1: 16-seed play-in A (winner is 16 seed, faces 1 seed)
|
||||
[66, 67], // First Four Game 2: 16-seed play-in B (winner is 16 seed, faces 1 seed)
|
||||
[60, 61], // First Four Game 3: 11/12-seed play-in A (winner is 11 seed, faces 6 seed)
|
||||
[62, 63], // First Four Game 4: 11/12-seed play-in B (winner is 11 seed, faces 6 seed)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* NCAA 68 Round of 64 seeding
|
||||
* Returns matchups for the Round of 64, with 4 TBD slots from First Four
|
||||
*
|
||||
* 68 teams total: 60 directly seeded (0-59) + 8 in First Four (60-67)
|
||||
* Round of 64 needs 64 participants: 60 direct + 4 First Four winners
|
||||
*
|
||||
* Distribution: Each region has 15 direct seeds + 1 FF winner = 16 per region
|
||||
* - Region 1: Seeds 0-14 + FF1 (FF1 winner is 16 seed, faces 1 seed)
|
||||
* - Region 2: Seeds 15-29 + FF2 (FF2 winner is 16 seed, faces 1 seed)
|
||||
* - Region 3: Seeds 30-44 + FF3 (FF3 winner is 11 seed, faces 6 seed)
|
||||
* - Region 4: Seeds 45-59 + FF4 (FF4 winner is 11 seed, faces 6 seed)
|
||||
*
|
||||
* @returns Array of [seed1, seed2 or "FF{gameNum}"] pairs for 32 matches
|
||||
*/
|
||||
function generateNCAA68RoundOf64Seeding(): ([number, number] | [number, string] | [string, number])[] {
|
||||
const matchups: ([number, number] | [number, string] | [string, number])[] = [];
|
||||
|
||||
// Region 1: Seeds 0-14 + FF1 (16 total)
|
||||
// Standard bracket order: 1v16, 8v9, 5v12, 4v13, 6v11, 3v14, 7v10, 2v15
|
||||
matchups.push([0, "FF1"]); // #1 vs #16 (FF1 winner)
|
||||
matchups.push([7, 8]); // #8 vs #9
|
||||
matchups.push([4, 11]); // #5 vs #12
|
||||
matchups.push([3, 12]); // #4 vs #13
|
||||
matchups.push([5, 10]); // #6 vs #11
|
||||
matchups.push([2, 13]); // #3 vs #14
|
||||
matchups.push([6, 9]); // #7 vs #10
|
||||
matchups.push([1, 14]); // #2 vs #15
|
||||
|
||||
// Region 2: Seeds 15-29 + FF2 (16 total)
|
||||
matchups.push([15, "FF2"]); // #1 vs #16 (FF2 winner)
|
||||
matchups.push([22, 23]); // #8 vs #9
|
||||
matchups.push([19, 26]); // #5 vs #12
|
||||
matchups.push([18, 27]); // #4 vs #13
|
||||
matchups.push([20, 25]); // #6 vs #11
|
||||
matchups.push([17, 28]); // #3 vs #14
|
||||
matchups.push([21, 24]); // #7 vs #10
|
||||
matchups.push([16, 29]); // #2 vs #15
|
||||
|
||||
// Region 3: Seeds 30-44 + FF3 (16 total)
|
||||
matchups.push([30, 44]); // #1 vs #16
|
||||
matchups.push([37, 38]); // #8 vs #9
|
||||
matchups.push([34, 41]); // #5 vs #12
|
||||
matchups.push([33, 42]); // #4 vs #13
|
||||
matchups.push([35, "FF3"]); // #6 vs #11 (FF3 winner = 11 seed)
|
||||
matchups.push([32, 43]); // #3 vs #14
|
||||
matchups.push([36, 39]); // #7 vs #10
|
||||
matchups.push([31, 40]); // #2 vs #15
|
||||
|
||||
// Region 4: Seeds 45-59 + FF4 (16 total)
|
||||
matchups.push([45, 59]); // #1 vs #16
|
||||
matchups.push([52, 53]); // #8 vs #9
|
||||
matchups.push([49, 56]); // #5 vs #12
|
||||
matchups.push([48, 57]); // #4 vs #13
|
||||
matchups.push([50, "FF4"]); // #6 vs #11 (FF4 winner = 11 seed)
|
||||
matchups.push([47, 58]); // #3 vs #14
|
||||
matchups.push([51, 54]); // #7 vs #10
|
||||
matchups.push([46, 55]); // #2 vs #15
|
||||
|
||||
return matchups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate standard tournament seeding matchups
|
||||
* Returns pairs of seed indices for proper bracket balance
|
||||
|
|
@ -264,8 +345,7 @@ export async function advanceWinner(
|
|||
*/
|
||||
function generateStandardSeeding(teamCount: number): [number, number][] {
|
||||
if (![4, 8, 16, 32].includes(teamCount)) {
|
||||
// For non-standard sizes (like NCAA 68, NFL 14), return sequential for now
|
||||
// Those will need custom seeding logic in their respective phases
|
||||
// Non-standard sizes need sequential seeding for now
|
||||
const pairs: [number, number][] = [];
|
||||
for (let i = 0; i < teamCount; i += 2) {
|
||||
pairs.push([i, i + 1]);
|
||||
|
|
@ -325,6 +405,11 @@ export async function generateBracketFromTemplate(
|
|||
);
|
||||
}
|
||||
|
||||
// NCAA 68 requires special handling for First Four and Round of 64
|
||||
if (templateId === "ncaa_68") {
|
||||
return await generateNCAA68Bracket(eventId, template, participantIds);
|
||||
}
|
||||
|
||||
const matches: NewPlayoffMatch[] = [];
|
||||
|
||||
// Generate matches for each round in the template
|
||||
|
|
@ -368,6 +453,91 @@ export async function generateBracketFromTemplate(
|
|||
return await createManyPlayoffMatches(matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate NCAA 68 bracket with First Four and Round of 64
|
||||
* Phase 2.8: Special handling for First Four play-in games
|
||||
*/
|
||||
async function generateNCAA68Bracket(
|
||||
eventId: string,
|
||||
template: BracketTemplate,
|
||||
participantIds?: string[]
|
||||
): Promise<PlayoffMatch[]> {
|
||||
const matches: NewPlayoffMatch[] = [];
|
||||
|
||||
// First Four: 4 play-in games
|
||||
const firstFourSeeding = generateNCAA68FirstFourSeeding();
|
||||
for (let i = 0; i < firstFourSeeding.length; i++) {
|
||||
const [seed1, seed2] = firstFourSeeding[i];
|
||||
matches.push({
|
||||
scoringEventId: eventId,
|
||||
round: "First Four",
|
||||
matchNumber: i + 1,
|
||||
participant1Id: participantIds ? participantIds[seed1] : null,
|
||||
participant2Id: participantIds ? participantIds[seed2] : null,
|
||||
isComplete: false,
|
||||
isScoring: false, // First Four doesn't score fantasy points
|
||||
templateRound: "First Four",
|
||||
seedInfo: `${seed1 + 1} vs ${seed2 + 1}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Round of 64: 32 games with some TBD slots from First Four
|
||||
const roundOf64Seeding = generateNCAA68RoundOf64Seeding();
|
||||
for (let i = 0; i < roundOf64Seeding.length; i++) {
|
||||
const matchup = roundOf64Seeding[i];
|
||||
let participant1Id: string | null = null;
|
||||
let participant2Id: string | null = null;
|
||||
let seedInfo: string | null = null;
|
||||
|
||||
if (participantIds) {
|
||||
const [seed1, seed2] = matchup;
|
||||
|
||||
// Handle TBD slots (FF1-FF4 placeholders)
|
||||
if (typeof seed1 === "number") {
|
||||
participant1Id = participantIds[seed1];
|
||||
} // else it's "FF1", "FF2", etc. - will be filled by First Four winner
|
||||
|
||||
if (typeof seed2 === "number") {
|
||||
participant2Id = participantIds[seed2];
|
||||
}
|
||||
|
||||
seedInfo = `${typeof seed1 === "number" ? seed1 + 1 : seed1} vs ${typeof seed2 === "number" ? seed2 + 1 : seed2}`;
|
||||
}
|
||||
|
||||
matches.push({
|
||||
scoringEventId: eventId,
|
||||
round: "Round of 64",
|
||||
matchNumber: i + 1,
|
||||
participant1Id,
|
||||
participant2Id,
|
||||
isComplete: false,
|
||||
isScoring: false, // Round of 64 doesn't score
|
||||
templateRound: "Round of 64",
|
||||
seedInfo,
|
||||
});
|
||||
}
|
||||
|
||||
// Generate remaining rounds (Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship)
|
||||
for (let roundIndex = 2; roundIndex < template.rounds.length; roundIndex++) {
|
||||
const round = template.rounds[roundIndex];
|
||||
for (let i = 0; i < round.matchCount; i++) {
|
||||
matches.push({
|
||||
scoringEventId: eventId,
|
||||
round: round.name,
|
||||
matchNumber: i + 1,
|
||||
participant1Id: null,
|
||||
participant2Id: null,
|
||||
isComplete: false,
|
||||
isScoring: round.isScoring,
|
||||
templateRound: round.name,
|
||||
seedInfo: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await createManyPlayoffMatches(matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance winner to next round using template-based logic
|
||||
* Works with any bracket template
|
||||
|
|
@ -383,6 +553,12 @@ export async function advanceWinnerTemplate(
|
|||
const match = await findPlayoffMatchById(matchId);
|
||||
if (!match) throw new Error("Match not found");
|
||||
|
||||
// Special handling for NCAA 68 First Four
|
||||
// Phase 2.8: First Four winners advance to specific Round of 64 slots
|
||||
if (template.id === "ncaa_68" && match.round === "First Four") {
|
||||
return await advanceFirstFourWinner(match, winnerId);
|
||||
}
|
||||
|
||||
// Find current round in template
|
||||
const currentRoundIndex = template.rounds.findIndex(
|
||||
(r) => r.name === match.round
|
||||
|
|
@ -431,3 +607,50 @@ export async function advanceWinnerTemplate(
|
|||
// Fill the determined slot
|
||||
await updatePlayoffMatch(nextMatch.id, { [participantSlot]: winnerId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance First Four winner to Round of 64
|
||||
* Phase 2.8: NCAA 68 specific logic
|
||||
*
|
||||
* First Four match → Round of 64 match mapping:
|
||||
* - FF Match 1 (16-seed) → R64 Match 1 (Region 1 vs #1 seed), participant2Id
|
||||
* - FF Match 2 (16-seed) → R64 Match 9 (Region 2 vs #1 seed), participant2Id
|
||||
* - FF Match 3 (11-seed) → R64 Match 21 (Region 3 vs #6 seed), participant2Id
|
||||
* - FF Match 4 (11-seed) → R64 Match 29 (Region 4 vs #6 seed), participant2Id
|
||||
*/
|
||||
async function advanceFirstFourWinner(
|
||||
firstFourMatch: PlayoffMatch,
|
||||
winnerId: string
|
||||
): Promise<void> {
|
||||
// Map First Four match numbers to Round of 64 match numbers
|
||||
const firstFourToRoundOf64: Record<number, number> = {
|
||||
1: 1, // FF Game 1 (16-seed) → R64 Match 1 (vs 1 seed)
|
||||
2: 9, // FF Game 2 (16-seed) → R64 Match 9 (vs 1 seed)
|
||||
3: 21, // FF Game 3 (11-seed) → R64 Match 21 (vs 6 seed)
|
||||
4: 29, // FF Game 4 (11-seed) → R64 Match 29 (vs 6 seed)
|
||||
};
|
||||
|
||||
const targetMatchNumber = firstFourToRoundOf64[firstFourMatch.matchNumber];
|
||||
if (!targetMatchNumber) {
|
||||
throw new Error(`Invalid First Four match number: ${firstFourMatch.matchNumber}`);
|
||||
}
|
||||
|
||||
// Find the target Round of 64 match
|
||||
const roundOf64Matches = await findPlayoffMatchesByEventIdAndRound(
|
||||
firstFourMatch.scoringEventId,
|
||||
"Round of 64"
|
||||
);
|
||||
|
||||
const targetMatch = roundOf64Matches.find((m) => m.matchNumber === targetMatchNumber);
|
||||
if (!targetMatch) {
|
||||
throw new Error(`Round of 64 match ${targetMatchNumber} not found`);
|
||||
}
|
||||
|
||||
// Check if participant2Id is already filled
|
||||
if (targetMatch.participant2Id) {
|
||||
throw new Error(`Round of 64 match ${targetMatchNumber} participant2Id is already filled`);
|
||||
}
|
||||
|
||||
// Fill the slot
|
||||
await updatePlayoffMatch(targetMatch.id, { participant2Id: winnerId });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1120,14 +1120,17 @@ scoring_events {
|
|||
- [x] Verify Elite Eight scoring starts correctly
|
||||
- [x] All 207 tests passing including new bracket integration tests
|
||||
|
||||
- [ ] **2.8** March Madness Template (68 teams)
|
||||
- [ ] Create NCAA_68 template definition
|
||||
- [ ] Build First Four matchup assignment UI (4 play-in games)
|
||||
- [ ] Build Round of 64 with TBD slot handling (winners from First Four)
|
||||
- [ ] Implement winner advancement from First Four to Round of 64
|
||||
- [ ] Test complete 68-team bracket flow
|
||||
- [ ] Verify only Elite Eight and beyond score points
|
||||
- [ ] Test early elimination = 0 points (per Q20)
|
||||
- [x] **2.8** March Madness Template (68 teams) - Logic Complete, UI Pending ✅
|
||||
- [x] Create NCAA_68 template definition
|
||||
- [x] Implement NCAA tournament seeding with First Four play-in logic
|
||||
- [x] Build generateNCAA68Bracket() function with proper seeding
|
||||
- [x] Implement TBD slot handling for Round of 64 (winners from First Four)
|
||||
- [x] Implement advanceFirstFourWinner() for First Four → Round of 64 advancement
|
||||
- [x] Create comprehensive unit tests (27 tests covering NCAA 68 structure and logic)
|
||||
- [x] Verify only Elite Eight and beyond score points
|
||||
- [x] Verify early elimination = 0 points (per Q20)
|
||||
- [x] All 76 bracket tests passing
|
||||
- [ ] Build First Four matchup assignment UI (admin interface) - **UI PENDING**
|
||||
|
||||
- [ ] **2.9** NFL/NBA Templates
|
||||
- [ ] Create NFL_14 template (with bye weeks)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue