Skip to content

Commit 654ebd3

Browse files
Flegmalukepolo
andauthored
Improvement/tournament creation (#96)
Co-authored-by: Luke Policinski <luke@lukepolo.com>
1 parent 33394d1 commit 654ebd3

File tree

14 files changed

+12204
-9717
lines changed

14 files changed

+12204
-9717
lines changed

generated/schema.graphql

Lines changed: 631 additions & 0 deletions
Large diffs are not rendered by default.

generated/schema.ts

Lines changed: 760 additions & 19 deletions
Large diffs are not rendered by default.

generated/types.ts

Lines changed: 10593 additions & 9594 deletions
Large diffs are not rendered by default.

hasura/fixtures/fixtures.sql

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -951,8 +951,8 @@ BEGIN
951951
now() - interval '7 days'
952952
);
953953

954-
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id)
955-
VALUES (t2_stage_id, tournament_ids[2], 'SingleElimination', 1, 4, 8, t2_options_id);
954+
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id, third_place_match, decider_best_of)
955+
VALUES (t2_stage_id, tournament_ids[2], 'SingleElimination', 1, 4, 8, t2_options_id, true, 1);
956956

957957
-- Register all 6 teams
958958
FOR t IN 1..6 LOOP
@@ -1286,8 +1286,8 @@ BEGIN
12861286
now() - interval '2 days'
12871287
);
12881288

1289-
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id)
1290-
VALUES (t3_stage_id, tournament_ids[3], 'SingleElimination', 1, 4, 16, t3_options_id);
1289+
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id, default_best_of)
1290+
VALUES (t3_stage_id, tournament_ids[3], 'SingleElimination', 1, 4, 16, t3_options_id, 3);
12911291

12921292
-- Register 2 teams so far
12931293
FOR t IN 1..2 LOOP
@@ -1367,12 +1367,12 @@ BEGIN
13671367
);
13681368

13691369
-- Stage 1: Round Robin (2 groups of 4)
1370-
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id)
1371-
VALUES (t4_stage1_id, tournament_ids[4], 'RoundRobin', 1, 8, 8, t4_rr_options_id);
1370+
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id, groups)
1371+
VALUES (t4_stage1_id, tournament_ids[4], 'RoundRobin', 1, 8, 8, t4_rr_options_id, 2);
13721372

13731373
-- Stage 2: Double Elimination (top 2 from each group = 4 teams)
1374-
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id)
1375-
VALUES (t4_stage2_id, tournament_ids[4], 'DoubleElimination', 2, 4, 4, t4_de_options_id);
1374+
INSERT INTO tournament_stages (id, tournament_id, type, "order", min_teams, max_teams, match_options_id, default_best_of, settings)
1375+
VALUES (t4_stage2_id, tournament_ids[4], 'DoubleElimination', 2, 4, 4, t4_de_options_id, 3, '{"round_best_of": {"GF": 5}}');
13761376

13771377
-- Register all 8 teams
13781378
FOR t IN 1..8 LOOP
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
CREATE OR REPLACE FUNCTION clone_match_options_with_best_of(
2+
_match_options_id uuid,
3+
_target_best_of int
4+
)
5+
RETURNS uuid AS $$
6+
DECLARE
7+
match_options_record match_options%ROWTYPE;
8+
cloned_id uuid;
9+
BEGIN
10+
IF _match_options_id IS NULL THEN
11+
RETURN NULL;
12+
END IF;
13+
14+
SELECT * INTO match_options_record
15+
FROM match_options
16+
WHERE id = _match_options_id;
17+
18+
IF NOT FOUND THEN
19+
RETURN NULL;
20+
END IF;
21+
22+
-- If target best_of matches, no clone needed
23+
IF _target_best_of = match_options_record.best_of THEN
24+
RETURN _match_options_id;
25+
END IF;
26+
27+
-- Clone with new best_of
28+
INSERT INTO match_options (
29+
overtime, knife_round, mr, best_of, coaches, number_of_substitutes,
30+
map_veto, timeout_setting, tech_timeout_setting, map_pool_id, type,
31+
regions, prefer_dedicated_server, invite_code, lobby_access,
32+
region_veto, ready_setting, check_in_setting, default_models, tv_delay
33+
) VALUES (
34+
match_options_record.overtime, match_options_record.knife_round,
35+
match_options_record.mr, _target_best_of,
36+
match_options_record.coaches, match_options_record.number_of_substitutes,
37+
match_options_record.map_veto, match_options_record.timeout_setting,
38+
match_options_record.tech_timeout_setting, match_options_record.map_pool_id,
39+
match_options_record.type, match_options_record.regions,
40+
match_options_record.prefer_dedicated_server, match_options_record.invite_code,
41+
match_options_record.lobby_access, match_options_record.region_veto,
42+
match_options_record.ready_setting, match_options_record.check_in_setting,
43+
match_options_record.default_models, match_options_record.tv_delay
44+
)
45+
RETURNING id INTO cloned_id;
46+
47+
RETURN cloned_id;
48+
END;
49+
$$ LANGUAGE plpgsql;

hasura/functions/tournaments/generate_double_elimination_bracket.sql

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ DECLARE
2626
k int; -- WB round feeding this LB round (for FEED rounds)
2727

2828
-- Grand Final
29-
grand_finals_match_options_id uuid;
3029
gf_id uuid;
3130
lb_final_id uuid;
3231
BEGIN
@@ -154,36 +153,10 @@ BEGIN
154153

155154
-- Grand Finals: WB winner vs LB winner
156155
IF wb_rounds > 0 AND _next_stage_max_teams = 1 THEN
157-
grand_finals_match_options_id := update_match_options_best_of(_stage_id);
158-
159-
INSERT INTO tournament_brackets(round, tournament_stage_id, match_number, "group", path, match_options_id)
160-
VALUES (wb_rounds + 1, _stage_id, 1, g, 'WB', grand_finals_match_options_id)
156+
INSERT INTO tournament_brackets(round, tournament_stage_id, match_number, "group", path)
157+
VALUES (wb_rounds + 1, _stage_id, 1, g, 'WB')
161158
RETURNING id INTO gf_id;
162159

163-
-- Apply decider to WB Final (last WB round)
164-
IF grand_finals_match_options_id IS NOT NULL THEN
165-
UPDATE tournament_brackets
166-
SET match_options_id = grand_finals_match_options_id
167-
WHERE tournament_stage_id = _stage_id
168-
AND path = 'WB'
169-
AND round = wb_rounds
170-
AND "group" = g;
171-
172-
RAISE NOTICE 'DE decider: applied decider match_options to WB Final (round %, group %)', wb_rounds, g;
173-
END IF;
174-
175-
-- Apply decider to LB Final (last LB round)
176-
IF lb_rounds > 0 AND grand_finals_match_options_id IS NOT NULL THEN
177-
UPDATE tournament_brackets
178-
SET match_options_id = grand_finals_match_options_id
179-
WHERE tournament_stage_id = _stage_id
180-
AND path = 'LB'
181-
AND round = lb_rounds
182-
AND "group" = loser_group_num;
183-
184-
RAISE NOTICE 'DE decider: applied decider match_options to LB Final (round %, group %)', lb_rounds, loser_group_num;
185-
END IF;
186-
187160
IF lb_rounds > 0 THEN
188161
SELECT id INTO lb_final_id
189162
FROM tournament_brackets

hasura/functions/tournaments/generate_swiss_bracket.sql

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ DECLARE
1515
seed_1 int;
1616
seed_2 int;
1717
bracket_idx int;
18-
match_options_id uuid;
19-
is_elimination_match boolean;
20-
is_advancement_match boolean;
2118
BEGIN
2219
-- Valve-style Swiss system: teams need 3 wins to advance or 3 losses to be eliminated
2320
-- Max rounds formula: 2 × wins_needed - 1
@@ -184,19 +181,6 @@ BEGIN
184181
wins, losses, pool_group, matches_needed, ROUND(matches_calc, 2);
185182
END;
186183

187-
-- Check if this is an elimination or advancement match
188-
-- Elimination: losses = wins_needed - 1 (next loss eliminates)
189-
-- Advancement: wins = wins_needed - 1 (next win advances)
190-
is_elimination_match := (losses = wins_needed - 1);
191-
is_advancement_match := (wins = wins_needed - 1);
192-
193-
-- Get match_options_id, creating a new one with best_of=3 if it's elimination/advancement
194-
IF is_elimination_match OR is_advancement_match THEN
195-
match_options_id := update_match_options_best_of(_stage_id);
196-
ELSE
197-
match_options_id := NULL;
198-
END IF;
199-
200184
-- Create placeholder matches for this pool
201185
-- Each pool gets its own match_number sequence starting from 1
202186
FOR match_num IN 1..matches_needed LOOP
@@ -205,16 +189,14 @@ BEGIN
205189
tournament_stage_id,
206190
match_number,
207191
"group",
208-
path,
209-
match_options_id
192+
path
210193
)
211194
VALUES (
212195
round_num,
213196
_stage_id,
214197
match_num,
215198
pool_group,
216-
'WB',
217-
match_options_id
199+
'WB'
218200
);
219201
matches_created := matches_created + 1;
220202
END LOOP;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
CREATE OR REPLACE FUNCTION get_bracket_best_of(
2+
_stage_id uuid,
3+
_path text,
4+
_round int
5+
)
6+
RETURNS int AS $$
7+
DECLARE
8+
_stage tournament_stages;
9+
_round_key text;
10+
_round_best_of int;
11+
BEGIN
12+
SELECT * INTO _stage
13+
FROM tournament_stages
14+
WHERE id = _stage_id;
15+
16+
IF NOT FOUND THEN
17+
RETURN 1;
18+
END IF;
19+
20+
-- Build the lookup key based on stage type
21+
IF _stage.type = 'Swiss' THEN
22+
-- For Swiss, the "round" is encoded as wins*100+losses in the group field
23+
-- But when called from schedule_tournament_match, we pass the bracket's group as round
24+
-- The caller should pass the appropriate key: "regular", "advancement", or "elimination"
25+
-- This function receives path as the swiss match type key
26+
_round_key := _path;
27+
ELSE
28+
-- For SE/DE: key is "path:round" e.g. "WB:1", "LB:3", "GF"
29+
IF _path = 'WB' AND _stage.type = 'DoubleElimination' THEN
30+
-- Check if this is the Grand Final round (WB round > ceil(log2(teams)))
31+
DECLARE
32+
_wb_rounds int;
33+
_teams_per_group int;
34+
BEGIN
35+
SELECT CEIL(LOG(CEIL(ts.max_teams::float / ts.groups)::numeric) / LOG(2))::int
36+
INTO _wb_rounds
37+
FROM tournament_stages ts
38+
WHERE ts.id = _stage_id;
39+
40+
IF _round > _wb_rounds THEN
41+
_round_key := 'GF';
42+
ELSE
43+
_round_key := _path || ':' || _round::text;
44+
END IF;
45+
END;
46+
ELSE
47+
_round_key := _path || ':' || _round::text;
48+
END IF;
49+
END IF;
50+
51+
-- Look up in settings JSONB
52+
IF _stage.settings IS NOT NULL AND _stage.settings ? 'round_best_of' THEN
53+
_round_best_of := (_stage.settings->'round_best_of'->>_round_key)::int;
54+
IF _round_best_of IS NOT NULL THEN
55+
RETURN _round_best_of;
56+
END IF;
57+
END IF;
58+
59+
-- Fall back to default_best_of
60+
RETURN _stage.default_best_of;
61+
END;
62+
$$ LANGUAGE plpgsql;

hasura/functions/tournaments/schedule_tournament_match.sql

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn
1212
feeders_with_team int := 0;
1313
winner_id UUID;
1414
_match_options_id UUID;
15+
_round_best_of int;
16+
_swiss_match_type text;
1517
BEGIN
1618
IF bracket.match_id IS NOT NULL THEN
1719
RETURN bracket.match_id;
@@ -44,7 +46,7 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn
4446
INNER JOIN tournaments t ON t.id = ts.tournament_id
4547
WHERE tb.id = bracket.id;
4648

47-
-- Check bracket first (decider overrides), then stage, then tournament
49+
-- Check bracket first (organizer overrides), then stage, then tournament
4850
IF bracket.match_options_id IS NOT NULL THEN
4951
_match_options_id := bracket.match_options_id;
5052
ELSIF stage.match_options_id IS NOT NULL THEN
@@ -53,6 +55,45 @@ CREATE OR REPLACE FUNCTION public.schedule_tournament_match(bracket public.tourn
5355
_match_options_id := tournament.match_options_id;
5456
END IF;
5557

58+
-- Per-round best_of resolution (only when bracket has no custom match_options)
59+
IF bracket.match_options_id IS NULL THEN
60+
IF stage.type = 'Swiss' THEN
61+
-- Swiss: decode group (wins*100+losses) to determine match type
62+
DECLARE
63+
_wins int;
64+
_losses int;
65+
_wins_needed int := 3;
66+
BEGIN
67+
_wins := (bracket."group" / 100)::int;
68+
_losses := (bracket."group" % 100)::int;
69+
IF _wins = _wins_needed - 1 THEN
70+
_swiss_match_type := 'advancement';
71+
ELSIF _losses = _wins_needed - 1 THEN
72+
_swiss_match_type := 'elimination';
73+
ELSE
74+
_swiss_match_type := 'regular';
75+
END IF;
76+
_round_best_of := get_bracket_best_of(stage.id, _swiss_match_type, bracket.round);
77+
END;
78+
ELSE
79+
_round_best_of := get_bracket_best_of(stage.id, bracket.path, bracket.round);
80+
END IF;
81+
82+
-- If round best_of differs from the resolved match_options, clone with new best_of
83+
IF _round_best_of IS NOT NULL THEN
84+
DECLARE
85+
_current_best_of int;
86+
BEGIN
87+
SELECT mo.best_of INTO _current_best_of
88+
FROM match_options mo WHERE mo.id = _match_options_id;
89+
90+
IF _current_best_of IS DISTINCT FROM _round_best_of THEN
91+
_match_options_id := clone_match_options_with_best_of(_match_options_id, _round_best_of);
92+
END IF;
93+
END;
94+
END IF;
95+
END IF;
96+
5697
-- Create the match first
5798
INSERT INTO matches (
5899
status,

hasura/functions/tournaments/update_tournament_stages.sql

Lines changed: 21 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -294,59 +294,35 @@ BEGIN
294294
RAISE NOTICE ' => Linking matches within stage %', stage."order";
295295
PERFORM link_tournament_stage_matches(stage.id);
296296

297-
-- SE decider: apply decider best_of to final and optionally create 3rd place match
298-
IF stage.decider_best_of IS NOT NULL THEN
297+
-- 3rd place match: create if enabled and there are at least 2 rounds (semifinals exist)
298+
IF stage.third_place_match = true AND total_rounds >= 2 THEN
299299
DECLARE
300-
_decider_match_options_id uuid;
301-
_final_bracket_id uuid;
302300
_third_place_id uuid;
303301
_semi_record RECORD;
304302
BEGIN
305-
_decider_match_options_id := update_match_options_best_of(stage.id);
303+
INSERT INTO tournament_brackets (
304+
round, tournament_stage_id, match_number, "group", path
305+
) VALUES (
306+
total_rounds, stage.id, 2, 1, 'WB'
307+
)
308+
RETURNING id INTO _third_place_id;
306309

307-
-- Apply decider match_options to the final (last round, match 1)
308-
SELECT id INTO _final_bracket_id
309-
FROM tournament_brackets
310-
WHERE tournament_stage_id = stage.id
311-
AND path = 'WB'
312-
AND round = total_rounds
313-
AND match_number = 1
314-
LIMIT 1;
310+
RAISE NOTICE ' => 3rd place: created bracket % in round %', _third_place_id, total_rounds;
315311

316-
IF _final_bracket_id IS NOT NULL AND _decider_match_options_id IS NOT NULL THEN
312+
-- Set loser_parent_bracket_id on semifinal brackets to route losers to 3rd place match
313+
FOR _semi_record IN
314+
SELECT id FROM tournament_brackets
315+
WHERE tournament_stage_id = stage.id
316+
AND path = 'WB'
317+
AND round = total_rounds - 1
318+
ORDER BY match_number ASC
319+
LOOP
317320
UPDATE tournament_brackets
318-
SET match_options_id = _decider_match_options_id
319-
WHERE id = _final_bracket_id;
321+
SET loser_parent_bracket_id = _third_place_id
322+
WHERE id = _semi_record.id;
320323

321-
RAISE NOTICE ' => SE decider: applied decider match_options to final bracket %', _final_bracket_id;
322-
END IF;
323-
324-
-- Create 3rd place match if there are at least 2 rounds (semifinals exist)
325-
IF total_rounds >= 2 THEN
326-
INSERT INTO tournament_brackets (
327-
round, tournament_stage_id, match_number, "group", path, match_options_id
328-
) VALUES (
329-
total_rounds, stage.id, 2, 1, 'WB', _decider_match_options_id
330-
)
331-
RETURNING id INTO _third_place_id;
332-
333-
RAISE NOTICE ' => SE decider: created 3rd place bracket % in round %', _third_place_id, total_rounds;
334-
335-
-- Set loser_parent_bracket_id on semifinal brackets to route losers to 3rd place match
336-
FOR _semi_record IN
337-
SELECT id FROM tournament_brackets
338-
WHERE tournament_stage_id = stage.id
339-
AND path = 'WB'
340-
AND round = total_rounds - 1
341-
ORDER BY match_number ASC
342-
LOOP
343-
UPDATE tournament_brackets
344-
SET loser_parent_bracket_id = _third_place_id
345-
WHERE id = _semi_record.id;
346-
347-
RAISE NOTICE ' => SE decider: set semifinal % loser -> 3rd place %', _semi_record.id, _third_place_id;
348-
END LOOP;
349-
END IF;
324+
RAISE NOTICE ' => 3rd place: set semifinal % loser -> 3rd place %', _semi_record.id, _third_place_id;
325+
END LOOP;
350326
END;
351327
END IF;
352328
END IF;

0 commit comments

Comments
 (0)