Skip to content

Commit f5e4a69

Browse files
authored
feature: swiss tournaments (#85)
1 parent ac3a738 commit f5e4a69

18 files changed

Lines changed: 898 additions & 163 deletions

UNUSED_FUNCTIONS_ANALYSIS.md

Lines changed: 0 additions & 56 deletions
This file was deleted.

generated/schema.graphql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5365,6 +5365,9 @@ enum e_tournament_stage_types_enum {
53655365

53665366
"""Single Elimination"""
53675367
SingleElimination
5368+
5369+
"""Swiss"""
5370+
Swiss
53685371
}
53695372

53705373
"""

generated/schema.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1969,7 +1969,7 @@ export interface e_tournament_stage_types_aggregate_fields {
19691969
/** unique or primary key constraints on table "e_tournament_stage_types" */
19701970
export type e_tournament_stage_types_constraint = 'e_tournament_stage_types_pkey'
19711971

1972-
export type e_tournament_stage_types_enum = 'DoubleElimination' | 'RoundRobin' | 'SingleElimination'
1972+
export type e_tournament_stage_types_enum = 'DoubleElimination' | 'RoundRobin' | 'SingleElimination' | 'Swiss'
19731973

19741974

19751975
/** aggregate max on columns */
@@ -49208,7 +49208,8 @@ export const enumETournamentStageTypesConstraint = {
4920849208
export const enumETournamentStageTypesEnum = {
4920949209
DoubleElimination: 'DoubleElimination' as const,
4921049210
RoundRobin: 'RoundRobin' as const,
49211-
SingleElimination: 'SingleElimination' as const
49211+
SingleElimination: 'SingleElimination' as const,
49212+
Swiss: 'Swiss' as const
4921249213
}
4921349214

4921449215
export const enumETournamentStageTypesSelectColumn = {

hasura/enums/tournament-stage-types.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
insert into e_tournament_stage_types ("value", "description") values
2+
('Swiss', 'Swiss'),
23
('RoundRobin', 'Round Robin'),
34
('SingleElimination', 'Single Elimination'),
45
('DoubleElimination', 'Double Elimination')
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
CREATE OR REPLACE FUNCTION public.advance_swiss_teams(_stage_id uuid)
2+
RETURNS void
3+
LANGUAGE plpgsql
4+
AS $$
5+
DECLARE
6+
stage_record RECORD;
7+
next_stage_id uuid;
8+
advanced_teams uuid[];
9+
eliminated_count int;
10+
BEGIN
11+
SELECT ts.tournament_id, ts."order"
12+
INTO stage_record
13+
FROM tournament_stages ts
14+
WHERE ts.id = _stage_id;
15+
16+
IF stage_record IS NULL THEN
17+
RAISE EXCEPTION 'Stage % not found', _stage_id;
18+
END IF;
19+
20+
SELECT array_agg(vtsr.tournament_team_id)
21+
INTO advanced_teams
22+
FROM v_team_stage_results vtsr
23+
WHERE vtsr.tournament_stage_id = _stage_id
24+
AND vtsr.wins >= 3;
25+
26+
SELECT COUNT(*)
27+
INTO eliminated_count
28+
FROM v_team_stage_results vtsr
29+
WHERE vtsr.tournament_stage_id = _stage_id
30+
AND vtsr.losses >= 3;
31+
32+
RAISE NOTICE '=== Processing Swiss Advancement ===';
33+
RAISE NOTICE 'Teams with 3+ wins: %', COALESCE(array_length(advanced_teams, 1), 0);
34+
RAISE NOTICE 'Teams with 3+ losses: %', eliminated_count;
35+
36+
DECLARE
37+
remaining_teams int;
38+
stage_complete boolean;
39+
BEGIN
40+
SELECT COUNT(*)
41+
INTO remaining_teams
42+
FROM v_team_stage_results vtsr
43+
WHERE vtsr.tournament_stage_id = _stage_id
44+
AND vtsr.wins < 3
45+
AND vtsr.losses < 3;
46+
47+
stage_complete := (remaining_teams = 0);
48+
49+
IF advanced_teams IS NOT NULL AND array_length(advanced_teams, 1) > 0 THEN
50+
SELECT ts.id INTO next_stage_id
51+
FROM tournament_stages ts
52+
WHERE ts.tournament_id = stage_record.tournament_id
53+
AND ts."order" = stage_record."order" + 1;
54+
55+
IF next_stage_id IS NOT NULL THEN
56+
RAISE NOTICE 'Advancing % teams to next stage', array_length(advanced_teams, 1);
57+
58+
-- Only seed the next stage if the current stage is complete AND we have teams to advance
59+
IF stage_complete THEN
60+
RAISE NOTICE 'Swiss stage complete, advancing teams to next stage';
61+
PERFORM seed_stage(next_stage_id);
62+
END IF;
63+
ELSE
64+
RAISE NOTICE 'No next stage found - teams have won the tournament';
65+
END IF;
66+
ELSIF stage_complete THEN
67+
-- Stage is complete but no teams advanced (shouldn't happen in normal Swiss, but handle gracefully)
68+
RAISE NOTICE 'Swiss stage complete but no teams advanced';
69+
END IF;
70+
71+
END;
72+
73+
RAISE NOTICE '=== Swiss Advancement Complete ===';
74+
END;
75+
$$;
76+
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
CREATE OR REPLACE FUNCTION public.assign_teams_to_swiss_pools(_stage_id uuid, _round int)
2+
RETURNS void
3+
LANGUAGE plpgsql
4+
AS $$
5+
DECLARE
6+
pool_record RECORD;
7+
bracket_record RECORD;
8+
team_count int;
9+
matches_needed int;
10+
match_counter int;
11+
bracket_order int[];
12+
i int;
13+
seed_1_idx int;
14+
seed_2_idx int;
15+
team_1_id uuid;
16+
team_2_id uuid;
17+
adjacent_team_id uuid;
18+
used_teams uuid[];
19+
teams_to_pair uuid[];
20+
BEGIN
21+
RAISE NOTICE '=== Assigning Teams to Swiss Pools for Round % ===', _round;
22+
23+
used_teams := ARRAY[]::uuid[];
24+
25+
FOR pool_record IN
26+
SELECT * FROM get_swiss_team_pools(_stage_id, used_teams)
27+
ORDER BY wins DESC, losses ASC
28+
LOOP
29+
team_count := pool_record.team_count;
30+
31+
IF team_count = 0 THEN
32+
CONTINUE;
33+
END IF;
34+
35+
-- Calculate pool group: wins * 100 + losses
36+
DECLARE
37+
pool_group numeric;
38+
BEGIN
39+
pool_group := pool_record.wins * 100 + pool_record.losses;
40+
41+
RAISE NOTICE ' Pool %-% (group %): % teams',
42+
pool_record.wins, pool_record.losses, pool_group, team_count;
43+
44+
-- Handle odd number of teams
45+
adjacent_team_id := NULL;
46+
teams_to_pair := pool_record.team_ids;
47+
48+
IF team_count % 2 != 0 THEN
49+
-- Find a team from an adjacent pool
50+
adjacent_team_id := find_adjacent_swiss_team(_stage_id, pool_record.wins, pool_record.losses, used_teams);
51+
52+
IF adjacent_team_id IS NOT NULL THEN
53+
teams_to_pair := teams_to_pair || adjacent_team_id;
54+
used_teams := used_teams || adjacent_team_id;
55+
RAISE NOTICE ' Borrowed team % from adjacent pool', adjacent_team_id;
56+
ELSE
57+
RAISE EXCEPTION 'Odd number of teams in pool %-% and no adjacent team found',
58+
pool_record.wins, pool_record.losses;
59+
END IF;
60+
END IF;
61+
62+
matches_needed := array_length(teams_to_pair, 1) / 2;
63+
64+
-- For Swiss tournaments, use bracket order for pairing
65+
-- Filter bracket_order to only include valid seed positions (1 to teams_to_pair.length)
66+
bracket_order := generate_bracket_order(array_length(teams_to_pair, 1));
67+
DECLARE
68+
filtered_order int[];
69+
valid_seed int;
70+
BEGIN
71+
filtered_order := ARRAY[]::int[];
72+
FOREACH valid_seed IN ARRAY bracket_order LOOP
73+
IF valid_seed >= 1 AND valid_seed <= array_length(teams_to_pair, 1) THEN
74+
filtered_order := filtered_order || valid_seed;
75+
END IF;
76+
END LOOP;
77+
bracket_order := filtered_order;
78+
END;
79+
80+
-- Validate we have enough valid seed positions
81+
IF array_length(bracket_order, 1) < matches_needed * 2 THEN
82+
RAISE EXCEPTION 'Not enough valid seed positions in bracket order for pool %-% (needed: %, got: %)',
83+
pool_record.wins, pool_record.losses, matches_needed * 2, array_length(bracket_order, 1);
84+
END IF;
85+
86+
match_counter := 1;
87+
FOR i IN 1..matches_needed LOOP
88+
-- Get seed positions from filtered bracket order
89+
seed_1_idx := bracket_order[(i - 1) * 2 + 1];
90+
seed_2_idx := bracket_order[(i - 1) * 2 + 2];
91+
92+
team_1_id := teams_to_pair[seed_1_idx];
93+
team_2_id := teams_to_pair[seed_2_idx];
94+
95+
-- Validate that teams are not NULL
96+
IF team_1_id IS NULL OR team_2_id IS NULL THEN
97+
RAISE EXCEPTION 'NULL team found in pool %-% at match % (seed_1_idx: %, seed_2_idx: %, teams_to_pair length: %)',
98+
pool_record.wins, pool_record.losses, match_counter, seed_1_idx, seed_2_idx, array_length(teams_to_pair, 1);
99+
END IF;
100+
101+
SELECT id INTO bracket_record
102+
FROM tournament_brackets
103+
WHERE tournament_stage_id = _stage_id
104+
AND round = _round
105+
AND "group" = pool_group
106+
AND match_number = match_counter
107+
LIMIT 1;
108+
109+
IF bracket_record IS NULL THEN
110+
RAISE EXCEPTION 'Bracket record not found for match % in pool %-% (group %)',
111+
match_counter, pool_record.wins, pool_record.losses, pool_group;
112+
END IF;
113+
114+
UPDATE tournament_brackets
115+
SET tournament_team_id_1 = team_1_id,
116+
tournament_team_id_2 = team_2_id,
117+
bye = false
118+
WHERE id = bracket_record.id;
119+
120+
-- Mark both teams as used to prevent double-assignment
121+
used_teams := used_teams || team_1_id || team_2_id;
122+
123+
RAISE NOTICE ' Match %: Team % vs Team %', match_counter, team_1_id, team_2_id;
124+
match_counter := match_counter + 1;
125+
END LOOP;
126+
END;
127+
END LOOP;
128+
129+
RAISE NOTICE '=== Team Assignment Complete ===';
130+
END;
131+
$$;
132+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
CREATE OR REPLACE FUNCTION public.binomial_coefficient(n int, k int)
2+
RETURNS numeric
3+
LANGUAGE plpgsql
4+
IMMUTABLE
5+
AS $$
6+
DECLARE
7+
result numeric;
8+
i int;
9+
BEGIN
10+
-- Validate inputs
11+
IF n < 0 OR k < 0 OR k > n THEN
12+
RETURN 0;
13+
END IF;
14+
15+
-- C(n, 0) = C(n, n) = 1
16+
IF k = 0 OR k = n THEN
17+
RETURN 1;
18+
END IF;
19+
20+
-- Use symmetry: C(n, k) = C(n, n-k)
21+
-- Choose the smaller k for efficiency
22+
IF k > n - k THEN
23+
k := n - k;
24+
END IF;
25+
26+
-- Calculate iteratively: C(n, k) = (n * (n-1) * ... * (n-k+1)) / (k * (k-1) * ... * 1)
27+
result := 1;
28+
FOR i IN 1..k LOOP
29+
result := result * (n - k + i) / i;
30+
END LOOP;
31+
32+
RETURN result;
33+
END;
34+
$$;
35+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
CREATE OR REPLACE FUNCTION public.check_swiss_round_complete(_stage_id uuid, _round int)
2+
RETURNS boolean
3+
LANGUAGE plpgsql
4+
AS $$
5+
DECLARE
6+
unfinished_count int;
7+
total_matches int;
8+
BEGIN
9+
SELECT COUNT(*) INTO unfinished_count
10+
FROM tournament_brackets tb
11+
WHERE tb.tournament_stage_id = _stage_id
12+
AND tb.round = _round
13+
AND tb.finished = false;
14+
15+
SELECT COUNT(*) INTO total_matches
16+
FROM tournament_brackets tb
17+
WHERE tb.tournament_stage_id = _stage_id
18+
AND tb.round = _round;
19+
20+
RETURN unfinished_count = 0 AND total_matches > 0;
21+
END;
22+
$$;
23+

0 commit comments

Comments
 (0)