diff --git a/app/features/tournament-bracket/core/PreparedMaps.test.ts b/app/features/tournament-bracket/core/PreparedMaps.test.ts index 84c9b7f3b..635c99482 100644 --- a/app/features/tournament-bracket/core/PreparedMaps.test.ts +++ b/app/features/tournament-bracket/core/PreparedMaps.test.ts @@ -133,6 +133,66 @@ describe("PreparedMaps - resolvePreparedForTheBracket", () => { expect(prepared).toBeNull(); }); + + test("multiple starting brackets (RR) feeding into SE brackets at same depth should share maps", () => { + const tournamentWithTwoStartingBrackets = testTournament({ + ctx: { + settings: { + bracketProgression: [ + { + type: "round_robin", + name: "Group A", + requiresCheckIn: false, + settings: {}, + }, + { + type: "round_robin", + name: "Group B", + requiresCheckIn: false, + settings: {}, + }, + { + type: "single_elimination", + name: "SE from Group A", + requiresCheckIn: false, + settings: {}, + sources: [ + { + bracketIdx: 0, + placements: [1, 2], + }, + ], + }, + { + type: "single_elimination", + name: "SE from Group B", + requiresCheckIn: false, + settings: {}, + sources: [ + { + bracketIdx: 1, + placements: [1, 2], + }, + ], + }, + ], + }, + }, + }); + + const prepared = PreparedMaps.resolvePreparedForTheBracket({ + tournament: tournamentWithTwoStartingBrackets, + bracketIdx: 3, + preparedByBracket: [ + null, + null, + { authorId: 1, createdAt: 1, maps: [] }, + null, + ], + }); + + expect(prepared).not.toBeNull(); + }); }); describe("PreparedMaps - eliminationTeamCountOptions", () => { diff --git a/app/features/tournament-bracket/core/PreparedMaps.ts b/app/features/tournament-bracket/core/PreparedMaps.ts index c4fff984a..12f432ae6 100644 --- a/app/features/tournament-bracket/core/PreparedMaps.ts +++ b/app/features/tournament-bracket/core/PreparedMaps.ts @@ -3,9 +3,10 @@ import type { PreparedMaps } from "~/db/tables"; import { nullFilledArray } from "~/utils/arrays"; import invariant from "~/utils/invariant"; import type { Bracket } from "./Bracket"; +import * as Progression from "./Progression"; import type { Tournament } from "./Tournament"; -/** Returns the prepared maps for one exact bracket index OR maps of a "sibling bracket" i.e. bracket that has the same sources */ +/** Returns the prepared maps for one exact bracket index OR maps of a "sibling bracket" i.e. bracket that has the same depth in progression */ export function resolvePreparedForTheBracket({ preparedByBracket, bracketIdx, @@ -23,19 +24,20 @@ export function resolvePreparedForTheBracket({ } const bracketPreparingFor = tournament.bracketByIdx(bracketIdx)!; + const bracketProgression = tournament.ctx.settings.bracketProgression; + const targetDepth = Progression.bracketDepth(bracketIdx, bracketProgression); // lets look for an "equivalent" prepared bracket to use // e.g. SoS RR -> 4x SE style the SE brackets can share maps - for (const [ - anotherBracketIdx, - bracket, - ] of tournament.ctx.settings.bracketProgression.entries()) { + for (const [anotherBracketIdx, bracket] of bracketProgression.entries()) { + const depth = Progression.bracketDepth( + anotherBracketIdx, + bracketProgression, + ); + if ( bracket.type === bracketPreparingFor.type && - R.isDeepEqual( - bracket.sources?.map((s) => s.bracketIdx), - bracketPreparingFor.sources?.map((s) => s.bracketIdx), - ) && + depth === targetDepth && R.isDeepEqual(bracket.settings, bracketPreparingFor.settings) ) { const bracketMaps = preparedByBracket?.[anotherBracketIdx]; diff --git a/app/features/tournament-bracket/core/Progression.test.ts b/app/features/tournament-bracket/core/Progression.test.ts index a9cacd296..bb4dd1c6e 100644 --- a/app/features/tournament-bracket/core/Progression.test.ts +++ b/app/features/tournament-bracket/core/Progression.test.ts @@ -814,3 +814,53 @@ describe("validatedBracketsToInputFormat", () => { ]); }); }); + +describe("bracketDepth", () => { + it("returns 0 for starting bracket with no sources", () => { + expect(Progression.bracketDepth(0, progressions.singleElimination)).toBe(0); + }); + + it("returns 0 for starting bracket and 1 for bracket sourced from it", () => { + expect( + Progression.bracketDepth(0, progressions.roundRobinToSingleElimination), + ).toBe(0); + expect( + Progression.bracketDepth(1, progressions.roundRobinToSingleElimination), + ).toBe(1); + }); + + it("handles complex progression with multiple depth levels", () => { + expect(Progression.bracketDepth(0, progressions.lowInk)).toBe(0); + expect(Progression.bracketDepth(1, progressions.lowInk)).toBe(1); + expect(Progression.bracketDepth(2, progressions.lowInk)).toBe(1); + expect(Progression.bracketDepth(3, progressions.lowInk)).toBe(2); + }); + + it("handles multiple starting brackets", () => { + expect(Progression.bracketDepth(0, progressions.manyStartBrackets)).toBe(0); + expect(Progression.bracketDepth(1, progressions.manyStartBrackets)).toBe(0); + expect(Progression.bracketDepth(2, progressions.manyStartBrackets)).toBe(1); + expect(Progression.bracketDepth(3, progressions.manyStartBrackets)).toBe(1); + }); + + it("handles underground brackets", () => { + expect( + Progression.bracketDepth( + 0, + progressions.doubleEliminationWithUnderground, + ), + ).toBe(0); + expect( + Progression.bracketDepth( + 1, + progressions.doubleEliminationWithUnderground, + ), + ).toBe(1); + }); + + it("throws if given idx is out of bounds", () => { + expect(() => + Progression.bracketDepth(1, progressions.singleElimination), + ).toThrow(); + }); +}); diff --git a/app/features/tournament-bracket/core/Progression.ts b/app/features/tournament-bracket/core/Progression.ts index 812037a9a..8ba8dd3b8 100644 --- a/app/features/tournament-bracket/core/Progression.ts +++ b/app/features/tournament-bracket/core/Progression.ts @@ -598,6 +598,27 @@ export function isUnderground(idx: number, brackets: ParsedBracket[]) { return !resolveMainBracketProgression(brackets).includes(idx); } +/** + * Returns the depth of a bracket in the tournament progression. + * Depth is the distance from a starting bracket (bracket with no sources). + * Starting brackets have depth 0, brackets sourced from them have depth 1, etc. + */ +export function bracketDepth(idx: number, brackets: ParsedBracket[]): number { + invariant(idx < brackets.length, "Bracket index out of bounds"); + + const bracket = brackets[idx]; + + if (!bracket.sources || bracket.sources.length === 0) { + return 0; + } + + const sourceDepths = bracket.sources.map((source) => + bracketDepth(source.bracketIdx, brackets), + ); + + return Math.max(...sourceDepths) + 1; +} + function resolveMainBracketProgression(brackets: ParsedBracket[]) { if (brackets.length === 1) return [0];