fix: NBA play-in bracket advancement bugs (#292)
* fix: NBA play-in advancement writes E7/W7 to wrong participant slot advanceNBAPlayInWinner was writing the PIR1 7v8 winner to participant1Id of First Round M3/M7, but those slots already hold E2/W2 (seeded at generation time). The "already filled" guard fired, silently swallowing both the winner advancement and the loser's placement into Play-In Round 2 — so the 7/8 loser never appeared in the second play-in game. Fix: write the 7v8 winner to participant2Id (the correct null slot) and update the guard, doc comment, and load-bearing comment accordingly. Also swap the p2Override arguments in the simulator and test fixture to match the corrected slot layout (FR M3: E2=p1, E7=p2). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: hasGroupStage check for templates without groupStage property undefined !== null is true, so templates that omit groupStage (like nba_20) were incorrectly treated as group-stage templates, triggering the generate-groups action and the "Invalid template or template has no group stage" error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
040c97137b
commit
66b0235678
4 changed files with 16 additions and 14 deletions
|
|
@ -1255,7 +1255,7 @@ async function generateNBA20Bracket(
|
||||||
// ── First Round (8 series) ────────────────────────────────────────────────────
|
// ── First Round (8 series) ────────────────────────────────────────────────────
|
||||||
// IMPORTANT: This match ordering is load-bearing.
|
// IMPORTANT: This match ordering is load-bearing.
|
||||||
// NBASimulator.simulateBracketAware() hard-codes match numbers to derive play-in
|
// 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/
|
// Do not change the match order without updating the simulator's resolveGame/
|
||||||
// resolveSeries calls and the corresponding test fixtures.
|
// resolveSeries calls and the corresponding test fixtures.
|
||||||
const firstRoundSeeding: [number | null, number | null, string][] = [
|
const firstRoundSeeding: [number | null, number | null, string][] = [
|
||||||
|
|
@ -1308,9 +1308,9 @@ async function generateNBA20Bracket(
|
||||||
* NBA play-in advancement logic.
|
* NBA play-in advancement logic.
|
||||||
*
|
*
|
||||||
* Play-In Round 1:
|
* 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
|
* 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
|
* M4 (W9v10): winner → PIR2 M2 p2; loser eliminated
|
||||||
*
|
*
|
||||||
* Play-In Round 2:
|
* Play-In Round 2:
|
||||||
|
|
@ -1326,7 +1326,8 @@ async function advanceNBAPlayInWinner(
|
||||||
|
|
||||||
if (match.round === "Play-In Round 1") {
|
if (match.round === "Play-In Round 1") {
|
||||||
if (match.matchNumber === 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([
|
const [frMatches, pir2Matches] = await Promise.all([
|
||||||
findPlayoffMatchesByEventIdAndRound(eventId, "First Round"),
|
findPlayoffMatchesByEventIdAndRound(eventId, "First Round"),
|
||||||
findPlayoffMatchesByEventIdAndRound(eventId, "Play-In Round 2"),
|
findPlayoffMatchesByEventIdAndRound(eventId, "Play-In Round 2"),
|
||||||
|
|
@ -1335,10 +1336,10 @@ async function advanceNBAPlayInWinner(
|
||||||
const pir2M1 = pir2Matches.find((m) => m.matchNumber === 1);
|
const pir2M1 = pir2Matches.find((m) => m.matchNumber === 1);
|
||||||
if (!frM3) throw new Error("First Round match 3 not found");
|
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 (!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");
|
if (pir2M1.participant1Id) throw new Error("Play-In Round 2 M1 participant1 already filled");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updatePlayoffMatch(frM3.id, { participant1Id: winnerId }),
|
updatePlayoffMatch(frM3.id, { participant2Id: winnerId }),
|
||||||
updatePlayoffMatch(pir2M1.id, { participant1Id: loserId }),
|
updatePlayoffMatch(pir2M1.id, { participant1Id: loserId }),
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
|
|
@ -1355,7 +1356,8 @@ async function advanceNBAPlayInWinner(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (match.matchNumber === 3) {
|
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([
|
const [frMatches, pir2Matches] = await Promise.all([
|
||||||
findPlayoffMatchesByEventIdAndRound(eventId, "First Round"),
|
findPlayoffMatchesByEventIdAndRound(eventId, "First Round"),
|
||||||
findPlayoffMatchesByEventIdAndRound(eventId, "Play-In Round 2"),
|
findPlayoffMatchesByEventIdAndRound(eventId, "Play-In Round 2"),
|
||||||
|
|
@ -1364,10 +1366,10 @@ async function advanceNBAPlayInWinner(
|
||||||
const pir2M2 = pir2Matches.find((m) => m.matchNumber === 2);
|
const pir2M2 = pir2Matches.find((m) => m.matchNumber === 2);
|
||||||
if (!frM7) throw new Error("First Round match 7 not found");
|
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 (!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");
|
if (pir2M2.participant1Id) throw new Error("Play-In Round 2 M2 participant1 already filled");
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
updatePlayoffMatch(frM7.id, { participant1Id: winnerId }),
|
updatePlayoffMatch(frM7.id, { participant2Id: winnerId }),
|
||||||
updatePlayoffMatch(pir2M2.id, { participant1Id: loserId }),
|
updatePlayoffMatch(pir2M2.id, { participant1Id: loserId }),
|
||||||
]);
|
]);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export default function EventBracket({
|
||||||
const template = templates.find((t) => t.id === selectedTemplate);
|
const template = templates.find((t) => t.id === selectedTemplate);
|
||||||
|
|
||||||
// Determine if this is a group-stage event
|
// 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)
|
// Determine if this template uses named regions (e.g. NCAA 68)
|
||||||
const hasRegions = (template?.regions?.length ?? 0) > 0;
|
const hasRegions = (template?.regions?.length ?? 0) > 0;
|
||||||
|
|
|
||||||
|
|
@ -232,11 +232,11 @@ function buildFullBracket(
|
||||||
// First Round (play-in slots filled)
|
// First Round (play-in slots filled)
|
||||||
makeMatch("First Round", 1, { p1: t[0], p2: e8Seed, isScoring: false }),
|
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", 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", 4, { p1: t[2], p2: t[5], isScoring: false }),
|
||||||
makeMatch("First Round", 5, { p1: t[10], p2: w8Seed, 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", 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 }),
|
makeMatch("First Round", 8, { p1: t[12], p2: t[15], isScoring: false }),
|
||||||
// Conference Semifinals (TBD)
|
// Conference Semifinals (TBD)
|
||||||
makeMatch("Conference Semifinals", 1, { isScoring: true }),
|
makeMatch("Conference Semifinals", 1, { isScoring: true }),
|
||||||
|
|
|
||||||
|
|
@ -379,11 +379,11 @@ export class NBASimulator implements Simulator {
|
||||||
// coalescence (DB ?? simmed) always produces the correct team.
|
// coalescence (DB ?? simmed) always produces the correct team.
|
||||||
const frM1 = resolveSeries(eloMap, frByNum.get(1), undefined, e8Seed); // E1(DB) vs E8
|
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 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 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 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 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)
|
const frM8 = resolveSeries(eloMap, frByNum.get(8)); // W3(DB) vs W6(DB)
|
||||||
|
|
||||||
// ── Conference Semifinals (4 best-of-7 series) ────────────────────────
|
// ── Conference Semifinals (4 best-of-7 series) ────────────────────────
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue