Skip to content

Commit 2adf2eb

Browse files
authored
bug: double elimination (#86)
1 parent 0aaafbd commit 2adf2eb

4 files changed

Lines changed: 205 additions & 160 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
CREATE OR REPLACE FUNCTION generate_double_elimination_bracket(
2+
_stage_id uuid,
3+
_teams_per_group int,
4+
_groups int,
5+
_next_stage_max_teams int
6+
)
7+
RETURNS void AS $$
8+
DECLARE
9+
P int;
10+
wb_rounds int;
11+
lb_rounds int;
12+
grand_finals_reset boolean;
13+
g int;
14+
r int;
15+
match_num int;
16+
loser_group_num int;
17+
wb_match_ids uuid[];
18+
lb_match_ids uuid[];
19+
lb_prev_match_ids uuid[];
20+
wb_match_count int;
21+
lb_match_count int;
22+
target_lb_round int;
23+
i int;
24+
j int;
25+
new_id uuid;
26+
wb_final_id uuid;
27+
lb_final_id uuid;
28+
lb_consolidation_id uuid;
29+
gf_id uuid;
30+
reset_final_id uuid;
31+
BEGIN
32+
P := POWER(2, CEIL(LOG(_teams_per_group::numeric) / LOG(2)))::int;
33+
34+
-- Winners bracket rounds
35+
wb_rounds := LOG(P::numeric) / LOG(2);
36+
37+
-- Losers bracket rounds (enough for intake/elimination/final)
38+
lb_rounds := 2 * (wb_rounds - 1);
39+
40+
-- DE bracket: teams=4, P=4, WB rounds=2, LB rounds=3
41+
RAISE NOTICE 'DE bracket: teams=%, P=%, WB rounds=%, LB rounds=%',
42+
_teams_per_group, P, wb_rounds, lb_rounds;
43+
44+
-- Loop per group
45+
FOR g IN 1.._groups LOOP
46+
loser_group_num := g + _groups;
47+
lb_prev_match_ids := NULL;
48+
49+
-- Generate LB rounds
50+
FOR r IN 1..lb_rounds LOOP
51+
-- Calculate number of matches in this LB round
52+
IF r = 1 THEN
53+
lb_match_count := P / 4; -- WB R1 losers, paired 2-at-a-time
54+
ELSE
55+
-- Pattern: rounds 1-2 use 2^2, rounds 3-4 use 2^3, rounds 5-6 use 2^4, etc.
56+
-- Formula: exponent = CEIL((r+2)/2)
57+
lb_match_count := P / POWER(2, CEIL((r + 2)::numeric / 2)::int);
58+
END IF;
59+
60+
RAISE NOTICE 'LB round %: % matches', r, lb_match_count;
61+
62+
lb_match_ids := ARRAY[]::uuid[];
63+
FOR match_num IN 1..lb_match_count LOOP
64+
INSERT INTO tournament_brackets(round, tournament_stage_id, match_number, "group", path)
65+
VALUES (r, _stage_id, match_num, loser_group_num, 'LB')
66+
RETURNING id INTO new_id;
67+
lb_match_ids := lb_match_ids || new_id;
68+
END LOOP;
69+
70+
lb_prev_match_ids := lb_match_ids;
71+
END LOOP;
72+
73+
-- Link WB losers to LB
74+
FOR r IN 1..wb_rounds LOOP
75+
SELECT array_agg(id ORDER BY match_number ASC) INTO wb_match_ids
76+
FROM tournament_brackets
77+
WHERE tournament_stage_id = _stage_id AND path='WB' AND round=r AND "group"=g;
78+
79+
wb_match_count := COALESCE(array_length(wb_match_ids,1),0);
80+
IF wb_match_count=0 THEN CONTINUE; END IF;
81+
82+
-- Determine LB target round
83+
IF r=1 THEN
84+
target_lb_round := 1;
85+
ELSE
86+
target_lb_round := LEAST(r + (r - 1), lb_rounds);
87+
END IF;
88+
89+
SELECT array_agg(id ORDER BY match_number ASC) INTO lb_match_ids
90+
FROM tournament_brackets
91+
WHERE tournament_stage_id=_stage_id AND path='LB' AND round=target_lb_round AND "group"=loser_group_num;
92+
93+
lb_match_count := COALESCE(array_length(lb_match_ids,1),0);
94+
95+
-- Assign WB losers to LB matches
96+
IF r=1 THEN
97+
-- pair 2-at-a-time
98+
FOR i IN 1..wb_match_count LOOP
99+
j := ((i-1)/2)+1;
100+
IF j <= lb_match_count THEN
101+
UPDATE tournament_brackets
102+
SET loser_parent_bracket_id = lb_match_ids[j]
103+
WHERE id = wb_match_ids[i];
104+
END IF;
105+
END LOOP;
106+
ELSE
107+
-- 1-to-1 mapping
108+
FOR i IN 1..LEAST(wb_match_count, lb_match_count) LOOP
109+
UPDATE tournament_brackets
110+
SET loser_parent_bracket_id = lb_match_ids[i]
111+
WHERE id = wb_match_ids[i];
112+
END LOOP;
113+
END IF;
114+
END LOOP;
115+
116+
-- Create Grand Final / Reset Final if needed
117+
IF wb_rounds > 0 AND _next_stage_max_teams=1 THEN
118+
-- WB Grand Final
119+
INSERT INTO tournament_brackets(round, tournament_stage_id, match_number, "group", path)
120+
VALUES (wb_rounds+1, _stage_id, 1, g, 'WB')
121+
RETURNING id INTO gf_id;
122+
123+
SELECT id INTO lb_final_id
124+
FROM tournament_brackets
125+
WHERE tournament_stage_id=_stage_id AND path='LB' AND round=lb_rounds AND "group"=loser_group_num
126+
ORDER BY match_number ASC LIMIT 1;
127+
128+
UPDATE tournament_brackets
129+
SET parent_bracket_id = gf_id
130+
WHERE id = lb_final_id;
131+
132+
-- -- Reset Final
133+
-- INSERT INTO tournament_brackets(round, tournament_stage_id, match_number, "group", path)
134+
-- VALUES (wb_rounds+2, _stage_id, 1, g, 'WB')
135+
-- RETURNING id INTO reset_final_id;
136+
137+
RAISE NOTICE 'Group %: LB consolidation round % and WB Grand Final created', g, lb_rounds+1;
138+
END IF;
139+
140+
END LOOP;
141+
END;
142+
$$ LANGUAGE plpgsql;
143+
144+
145+
update tournament_stages set max_teams = 8 where id = '1f09d465-1dfe-47b3-a839-deb7e5a68257'

hasura/functions/tournaments/link_tournament_stage_matches.sql

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,62 @@ BEGIN
4646
PERFORM link_round_group_matches(_stage_id, round_record.round, round_record."group"::int, path_record.path);
4747
END LOOP;
4848
END LOOP;
49+
50+
-- Handle first round byes: update parent matches with seeds, then delete bye matches
51+
DECLARE
52+
bye_match record;
53+
bye_seed int;
54+
first_child_match_number int;
55+
BEGIN
56+
FOR bye_match IN
57+
SELECT tb.id, tb.match_number, tb."group", tb.parent_bracket_id,
58+
tb.team_1_seed, tb.team_2_seed, COALESCE(tb.path, 'WB') AS path
59+
FROM tournament_brackets tb
60+
WHERE tb.tournament_stage_id = _stage_id
61+
AND tb.round = 1
62+
AND (tb.team_1_seed IS NULL OR tb.team_2_seed IS NULL)
63+
LOOP
64+
-- Get the seed from the bye match (the non-NULL one)
65+
bye_seed := COALESCE(bye_match.team_1_seed, bye_match.team_2_seed);
66+
67+
-- Skip if no parent or no seed
68+
IF bye_match.parent_bracket_id IS NULL OR bye_seed IS NULL THEN
69+
CONTINUE;
70+
END IF;
71+
72+
-- Determine which slot in the parent match to populate
73+
-- Find the lowest match_number among all children of the parent (same group and path)
74+
SELECT MIN(tb2.match_number) INTO first_child_match_number
75+
FROM tournament_brackets tb2
76+
WHERE tb2.parent_bracket_id = bye_match.parent_bracket_id
77+
AND tb2.round = 1
78+
AND tb2."group" = bye_match."group"
79+
AND COALESCE(tb2.path, 'WB') = bye_match.path;
80+
81+
-- If this bye match has the lowest match_number, populate team_1_seed
82+
-- Otherwise, populate team_2_seed
83+
-- Only populate if the slot is empty
84+
IF bye_match.match_number = first_child_match_number THEN
85+
UPDATE tournament_brackets
86+
SET team_1_seed = bye_seed
87+
WHERE id = bye_match.parent_bracket_id
88+
AND team_1_seed IS NULL;
89+
ELSE
90+
UPDATE tournament_brackets
91+
SET team_2_seed = bye_seed
92+
WHERE id = bye_match.parent_bracket_id
93+
AND team_2_seed IS NULL;
94+
END IF;
95+
96+
RAISE NOTICE ' Advanced seed % from bye match % (round 1, match %) to parent match %',
97+
bye_seed, bye_match.id, bye_match.match_number, bye_match.parent_bracket_id;
98+
END LOOP;
99+
100+
DELETE FROM tournament_brackets
101+
WHERE tournament_stage_id = _stage_id
102+
AND round = 1
103+
AND (team_1_seed IS NULL OR team_2_seed IS NULL);
104+
END;
105+
49106
END;
50107
$$ LANGUAGE plpgsql;

hasura/functions/tournaments/seed_stage.sql

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@ BEGIN
9696
SELECT tb.id, tb.round, tb."group", tb.match_number, tb.team_1_seed, tb.team_2_seed
9797
FROM tournament_brackets tb
9898
WHERE tb.tournament_stage_id = stage.id
99-
AND tb.round = 1
10099
AND COALESCE(tb.path, 'WB') = 'WB' -- never seed or mark byes on loser brackets
101100
AND (tb.team_1_seed IS NOT NULL OR tb.team_2_seed IS NOT NULL)
102101
ORDER BY tb."group" ASC, tb.match_number ASC
@@ -160,7 +159,7 @@ BEGIN
160159
SET tournament_team_id_1 = team_1_id,
161160
tournament_team_id_2 = team_2_id,
162161
bye = (team_1_id IS NULL OR team_2_id IS NULL)
163-
WHERE id = bracket.id;
162+
WHERE id = bracket.id and round = 1;
164163

165164
RAISE NOTICE ' Bracket %: Seed % (team %) vs Seed % (team %)',
166165
bracket.match_number,

hasura/functions/tournaments/update_tournament_stages.sql

Lines changed: 2 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -288,163 +288,7 @@ BEGIN
288288
-- Losers bracket uses separate group numbers to make it easier to reason about:
289289
-- For N winner groups (1..N), we create N loser groups (N+1..2N) with path='LB'.
290290
IF stage_type = 'DoubleElimination' THEN
291-
RAISE NOTICE ' => Generating double elimination structure for stage %', stage."order";
292-
DECLARE
293-
g int;
294-
loser_group_num int;
295-
wb_round_count int;
296-
r int;
297-
wb_round_matches int;
298-
lb_round_matches int;
299-
wb_match_ids uuid[];
300-
lb_match_ids uuid[];
301-
lb_prev_match_ids uuid[];
302-
i int;
303-
j int;
304-
teams_advancing int;
305-
wb_final_id uuid;
306-
lb_final_id uuid;
307-
gf_id uuid;
308-
BEGIN
309-
-- Determine winners bracket round count
310-
SELECT MAX(round) INTO wb_round_count
311-
FROM tournament_brackets
312-
WHERE tournament_stage_id = stage.id AND path = 'WB';
313-
314-
RAISE NOTICE ' => Winners bracket has % rounds', wb_round_count;
315-
316-
-- Calculate teams advancing from this stage (WB champion + LB champion per group)
317-
teams_advancing := 2 * stage.groups;
318-
319-
-- Build losers bracket per group (separate loser group id with path='LB')
320-
FOR g IN 1..stage.groups LOOP
321-
-- Loser group number: winners_group + stage.groups (e.g. WB group 1 -> LB group N+1)
322-
loser_group_num := g + stage.groups;
323-
RAISE NOTICE ' => Creating loser group % for winners group %', loser_group_num, g;
324-
325-
lb_prev_match_ids := NULL;
326-
327-
-- Generate LB rounds (same number as WB rounds)
328-
FOR r IN 1..wb_round_count LOOP
329-
-- Get WB matches for this round
330-
SELECT array_agg(id ORDER BY match_number ASC) INTO wb_match_ids
331-
FROM tournament_brackets
332-
WHERE tournament_stage_id = stage.id AND path = 'WB' AND round = r AND "group" = g;
333-
334-
wb_round_matches := COALESCE(array_length(wb_match_ids, 1), 0);
335-
IF wb_round_matches = 0 THEN
336-
CONTINUE;
337-
END IF;
338-
339-
-- Calculate LB matches for this round
340-
IF r = 1 THEN
341-
-- LB Round 1: receives losers from WB Round 1 (paired 2 at a time)
342-
lb_round_matches := wb_round_matches / 2;
343-
ELSE
344-
-- LB Round r: receives losers from WB Round r (1-to-1) + winners from LB Round (r-1)
345-
lb_round_matches := wb_round_matches;
346-
END IF;
347-
348-
-- Create LB matches for this round in the loser group
349-
lb_match_ids := ARRAY[]::uuid[];
350-
FOR i IN 1..lb_round_matches LOOP
351-
INSERT INTO tournament_brackets (round, tournament_stage_id, match_number, "group", path)
352-
VALUES (r, stage.id, i, loser_group_num, 'LB')
353-
RETURNING id INTO new_id;
354-
lb_match_ids := lb_match_ids || new_id;
355-
END LOOP;
356-
357-
-- Link WB losers to LB matches in the loser group
358-
IF r = 1 THEN
359-
-- Pair WB Round 1 losers two-at-a-time into LB Round 1
360-
FOR i IN 1..wb_round_matches LOOP
361-
j := ((i - 1) / 2) + 1;
362-
IF j <= lb_round_matches THEN
363-
UPDATE tournament_brackets
364-
SET loser_parent_bracket_id = lb_match_ids[j]
365-
WHERE id = wb_match_ids[i];
366-
END IF;
367-
END LOOP;
368-
ELSE
369-
-- Map WB Round r losers one-to-one into LB Round r
370-
FOR i IN 1..LEAST(wb_round_matches, lb_round_matches) LOOP
371-
UPDATE tournament_brackets
372-
SET loser_parent_bracket_id = lb_match_ids[i]
373-
WHERE id = wb_match_ids[i];
374-
END LOOP;
375-
-- Note: LB round winners linking is handled by link_tournament_stage_matches
376-
-- which uses 1-to-1 mapping by match number for LB paths
377-
END IF;
378-
379-
lb_prev_match_ids := lb_match_ids;
380-
END LOOP;
381-
382-
-- For stages that produce a single champion, create a consolidation final in LB
383-
-- and a Grand Final in WB:
384-
-- - LB extra round (round = wb_round_count + 1, path='LB'):
385-
-- participants = loser of WB final + winner of LB final
386-
-- - WB extra round (round = wb_round_count + 1, path='WB'):
387-
-- participants = winner of WB final + winner of LB extra round
388-
IF wb_round_count > 0 AND next_stage_max_teams = 1 THEN
389-
-- Identify WB and LB finals (last round in each path for this group)
390-
SELECT id INTO wb_final_id
391-
FROM tournament_brackets
392-
WHERE tournament_stage_id = stage.id AND path = 'WB' AND round = wb_round_count AND "group" = g
393-
ORDER BY match_number ASC LIMIT 1;
394-
395-
SELECT id INTO lb_final_id
396-
FROM tournament_brackets
397-
WHERE tournament_stage_id = stage.id AND path = 'LB' AND round = wb_round_count AND "group" = loser_group_num
398-
ORDER BY match_number ASC LIMIT 1;
399-
400-
IF wb_final_id IS NOT NULL AND lb_final_id IS NOT NULL THEN
401-
-- Create LB consolidation final (extra LB round)
402-
INSERT INTO tournament_brackets (round, tournament_stage_id, match_number, "group", path)
403-
VALUES (wb_round_count + 1, stage.id, 1, loser_group_num, 'LB')
404-
RETURNING id INTO gf_id;
405-
406-
-- Winner of LB final advances to LB consolidation via parent_bracket_id
407-
UPDATE tournament_brackets
408-
SET parent_bracket_id = gf_id
409-
WHERE id = lb_final_id;
410-
411-
-- Loser of WB final drops into LB consolidation via loser_parent_bracket_id
412-
UPDATE tournament_brackets
413-
SET loser_parent_bracket_id = gf_id
414-
WHERE id = wb_final_id;
415-
416-
RAISE NOTICE ' => Created LB consolidation final for group % (round %, path LB)', g, wb_round_count + 1;
417-
418-
-- Create WB Grand Final as an extra WB round
419-
INSERT INTO tournament_brackets (round, tournament_stage_id, match_number, "group", path)
420-
VALUES (wb_round_count + 1, stage.id, 1, g, 'WB')
421-
RETURNING id INTO gf_id;
422-
423-
-- Winner of WB final advances to Grand Final
424-
UPDATE tournament_brackets
425-
SET parent_bracket_id = gf_id
426-
WHERE id = wb_final_id;
427-
428-
-- Winner of LB consolidation final advances to Grand Final
429-
UPDATE tournament_brackets
430-
SET parent_bracket_id = gf_id
431-
WHERE id = (
432-
SELECT id
433-
FROM tournament_brackets
434-
WHERE tournament_stage_id = stage.id
435-
AND path = 'LB'
436-
AND round = wb_round_count + 1
437-
AND "group" = loser_group_num
438-
ORDER BY match_number ASC LIMIT 1
439-
);
440-
441-
RAISE NOTICE ' => Created WB Grand Final for group % (round %, path WB)', g, wb_round_count + 1;
442-
END IF;
443-
ELSE
444-
RAISE NOTICE ' => Skipping Grand Final for group % (next_stage_max_teams=%, both WB and LB champions advance directly)', g, next_stage_max_teams;
445-
END IF;
446-
END LOOP;
447-
END;
291+
PERFORM generate_double_elimination_bracket(stage.id, teams_per_group, stage.groups, next_stage_max_teams);
448292
END IF;
449293

450294
RAISE NOTICE ' => Linking matches within stage %', stage."order";
@@ -458,4 +302,4 @@ BEGIN
458302

459303
PERFORM calculate_tournament_bracket_start_times(_tournament_id);
460304
END;
461-
$$ LANGUAGE plpgsql;
305+
$$ LANGUAGE plpgsql;

0 commit comments

Comments
 (0)