diff --git a/app/models/playoff-match.ts b/app/models/playoff-match.ts index 33c1790..f448db4 100644 --- a/app/models/playoff-match.ts +++ b/app/models/playoff-match.ts @@ -1255,7 +1255,7 @@ async function generateNBA20Bracket( // ── First Round (8 series) ──────────────────────────────────────────────────── // IMPORTANT: This match ordering is load-bearing. // NBASimulator.simulateBracketAware() hard-codes match numbers to derive play-in - // seed slots: FR M1 p2 = E8, FR M3 p1 = E7, FR M5 p2 = W8, FR M7 p1 = W7. + // seed slots: FR M1 p2 = E8, FR M3 p2 = E7, FR M5 p2 = W8, FR M7 p2 = W7. // Do not change the match order without updating the simulator's resolveGame/ // resolveSeries calls and the corresponding test fixtures. const firstRoundSeeding: [number | null, number | null, string][] = [ @@ -1308,9 +1308,9 @@ async function generateNBA20Bracket( * NBA play-in advancement logic. * * Play-In Round 1: - * M1 (E7v8): winner → First Round M3 p1; loser → PIR2 M1 p1 + * M1 (E7v8): winner → First Round M3 p2 (E7 seed); loser → PIR2 M1 p1 * M2 (E9v10): winner → PIR2 M1 p2; loser eliminated - * M3 (W7v8): winner → First Round M7 p1; loser → PIR2 M2 p1 + * M3 (W7v8): winner → First Round M7 p2 (W7 seed); loser → PIR2 M2 p1 * M4 (W9v10): winner → PIR2 M2 p2; loser eliminated * * Play-In Round 2: @@ -1326,7 +1326,8 @@ async function advanceNBAPlayInWinner( if (match.round === "Play-In Round 1") { if (match.matchNumber === 1) { - // E7v8: winner → First Round M3 p1; loser → PIR2 M1 p1 + // E7v8: winner → First Round M3 p2 (E7 seed); loser → PIR2 M1 p1 + // FR M3 is "E2 vs E7": E2 is already at p1, E7 fills p2. const [frMatches, pir2Matches] = await Promise.all([ findPlayoffMatchesByEventIdAndRound(eventId, "First Round"), findPlayoffMatchesByEventIdAndRound(eventId, "Play-In Round 2"), @@ -1335,10 +1336,10 @@ async function advanceNBAPlayInWinner( const pir2M1 = pir2Matches.find((m) => m.matchNumber === 1); if (!frM3) throw new Error("First Round match 3 not found"); if (!pir2M1) throw new Error("Play-In Round 2 match 1 not found"); - if (frM3.participant1Id) throw new Error("First Round M3 participant1 already filled"); + if (frM3.participant2Id) throw new Error("First Round M3 participant2 already filled"); if (pir2M1.participant1Id) throw new Error("Play-In Round 2 M1 participant1 already filled"); await Promise.all([ - updatePlayoffMatch(frM3.id, { participant1Id: winnerId }), + updatePlayoffMatch(frM3.id, { participant2Id: winnerId }), updatePlayoffMatch(pir2M1.id, { participant1Id: loserId }), ]); return; @@ -1355,7 +1356,8 @@ async function advanceNBAPlayInWinner( } if (match.matchNumber === 3) { - // W7v8: winner → First Round M7 p1; loser → PIR2 M2 p1 + // W7v8: winner → First Round M7 p2 (W7 seed); loser → PIR2 M2 p1 + // FR M7 is "W2 vs W7": W2 is already at p1, W7 fills p2. const [frMatches, pir2Matches] = await Promise.all([ findPlayoffMatchesByEventIdAndRound(eventId, "First Round"), findPlayoffMatchesByEventIdAndRound(eventId, "Play-In Round 2"), @@ -1364,10 +1366,10 @@ async function advanceNBAPlayInWinner( const pir2M2 = pir2Matches.find((m) => m.matchNumber === 2); if (!frM7) throw new Error("First Round match 7 not found"); if (!pir2M2) throw new Error("Play-In Round 2 match 2 not found"); - if (frM7.participant1Id) throw new Error("First Round M7 participant1 already filled"); + if (frM7.participant2Id) throw new Error("First Round M7 participant2 already filled"); if (pir2M2.participant1Id) throw new Error("Play-In Round 2 M2 participant1 already filled"); await Promise.all([ - updatePlayoffMatch(frM7.id, { participant1Id: winnerId }), + updatePlayoffMatch(frM7.id, { participant2Id: winnerId }), updatePlayoffMatch(pir2M2.id, { participant1Id: loserId }), ]); return; 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 98b8e2d..8008ea4 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx @@ -67,7 +67,7 @@ export default function EventBracket({ const template = templates.find((t) => t.id === selectedTemplate); // Determine if this is a group-stage event - const hasGroupStage = template?.groupStage !== null; + const hasGroupStage = template?.groupStage !== null && template?.groupStage !== undefined; // Determine if this template uses named regions (e.g. NCAA 68) const hasRegions = (template?.regions?.length ?? 0) > 0; diff --git a/app/services/simulations/__tests__/nba-simulator.test.ts b/app/services/simulations/__tests__/nba-simulator.test.ts index 6432be5..41551a8 100644 --- a/app/services/simulations/__tests__/nba-simulator.test.ts +++ b/app/services/simulations/__tests__/nba-simulator.test.ts @@ -232,11 +232,11 @@ function buildFullBracket( // First Round (play-in slots filled) makeMatch("First Round", 1, { p1: t[0], p2: e8Seed, isScoring: false }), makeMatch("First Round", 2, { p1: t[3], p2: t[4], isScoring: false }), - makeMatch("First Round", 3, { p1: e7, p2: t[1], isScoring: false }), + makeMatch("First Round", 3, { p1: t[1], p2: e7, isScoring: false }), makeMatch("First Round", 4, { p1: t[2], p2: t[5], isScoring: false }), makeMatch("First Round", 5, { p1: t[10], p2: w8Seed, isScoring: false }), makeMatch("First Round", 6, { p1: t[13], p2: t[14], isScoring: false }), - makeMatch("First Round", 7, { p1: w7, p2: t[11], isScoring: false }), + makeMatch("First Round", 7, { p1: t[11], p2: w7, isScoring: false }), makeMatch("First Round", 8, { p1: t[12], p2: t[15], isScoring: false }), // Conference Semifinals (TBD) makeMatch("Conference Semifinals", 1, { isScoring: true }), diff --git a/app/services/simulations/nba-simulator.ts b/app/services/simulations/nba-simulator.ts index 6476755..1e2884f 100644 --- a/app/services/simulations/nba-simulator.ts +++ b/app/services/simulations/nba-simulator.ts @@ -379,11 +379,11 @@ export class NBASimulator implements Simulator { // coalescence (DB ?? simmed) always produces the correct team. const frM1 = resolveSeries(eloMap, frByNum.get(1), undefined, e8Seed); // E1(DB) vs E8 const frM2 = resolveSeries(eloMap, frByNum.get(2)); // E4(DB) vs E5(DB) - const frM3 = resolveSeries(eloMap, frByNum.get(3), e7Seed, undefined); // E7 vs E2(DB) + const frM3 = resolveSeries(eloMap, frByNum.get(3), undefined, e7Seed); // E2(DB) vs E7 const frM4 = resolveSeries(eloMap, frByNum.get(4)); // E3(DB) vs E6(DB) const frM5 = resolveSeries(eloMap, frByNum.get(5), undefined, w8Seed); // W1(DB) vs W8 const frM6 = resolveSeries(eloMap, frByNum.get(6)); // W4(DB) vs W5(DB) - const frM7 = resolveSeries(eloMap, frByNum.get(7), w7Seed, undefined); // W7 vs W2(DB) + const frM7 = resolveSeries(eloMap, frByNum.get(7), undefined, w7Seed); // W2(DB) vs W7 const frM8 = resolveSeries(eloMap, frByNum.get(8)); // W3(DB) vs W6(DB) // ── Conference Semifinals (4 best-of-7 series) ────────────────────────