From 88293f86b00fdef5d651c34129b65aac8f7b2f4f Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Wed, 31 Jan 2024 23:14:58 -0700 Subject: [PATCH] Moved around some DB stuff so that brackets and matches are in different tables --- include/schedule.hrl | 1 + src/database.erl | 214 +++++++++++++++++++------------------------ src/schedule.erl | 52 ++++++++++- 3 files changed, 146 insertions(+), 121 deletions(-) create mode 100644 include/schedule.hrl diff --git a/include/schedule.hrl b/include/schedule.hrl new file mode 100644 index 0000000..06d929a --- /dev/null +++ b/include/schedule.hrl @@ -0,0 +1 @@ +-record(bracket, {num = none, red = none, blue = none}). diff --git a/src/database.erl b/src/database.erl index 9702ce3..b7bbbe1 100644 --- a/src/database.erl +++ b/src/database.erl @@ -3,10 +3,9 @@ -behaviour(gen_server). -export([start_link/1, init/1, handle_cast/2, handle_call/3, terminate/2]). --export([get_tick/0, generate_bracket/1, print_bracket/1]). +-export([get_tick/0]). -record(state, {database_file = none, database = none, tick_start = none}). --record(bracket, {depth = none, num = none, red = none, blue = none}). first_error([]) -> ok; first_error([ok | Rest]) -> first_error(Rest); @@ -26,49 +25,6 @@ sql_begin(Database, Script) -> start_link(DatabaseFile) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [DatabaseFile], []). -higher_pow2(N) -> round(math:pow(2, ceil(math:log2(N)))). - -first_layer(BracketSize, _, N) when N > BracketSize/2 -> []; -first_layer(BracketSize, Size, N) when BracketSize - N + 1 > Size -> - [N | first_layer(BracketSize, Size, N+1)]; -first_layer(BracketSize, Size, N) -> - [{N, BracketSize - N + 1} | first_layer(BracketSize, Size, N+1)]. - -first_layer(Size) when Size > 1 -> - BracketSize = higher_pow2(Size), - first_layer(BracketSize, Size, 1). - -merge_layer([Element], []) -> Element; -merge_layer([], Merged) -> - Final = lists:reverse(Merged), - merge_layer(Final); -merge_layer([First | Rest], Merged) -> - Last = lists:last(Rest), - List = lists:droplast(Rest), - merge_layer(List, [{First, Last} | Merged]). -merge_layer(List) -> merge_layer(List, []). - -label_bracket({Left, Right}, [N | Counters], Depth) -> - {LC, L} = label_bracket(Left, Counters, Depth+1), - {RC, R} = label_bracket(Right, LC, Depth+1), - {[N+1 | RC], #bracket{depth = Depth, num=N, red=L, blue=R}}; -label_bracket(Elem, Counters, _) -> {Counters, Elem}. - -label_bracket(Bracket, Depth) -> - {_, Labeled} = label_bracket(Bracket, lists:duplicate(Depth, 1), 1), - Labeled. - -generate_bracket(Size) -> - label_bracket(merge_layer(first_layer(Size)), ceil(math:log2(Size))). - -print_bracket(Bracket) -> - RF = fun(R,L) when R == element(1,Bracket) -> - Flds = record_info(fields, bracket), - true = (L == length(Flds)), - Flds - end, - io:fwrite([io_lib_pretty:print(Bracket, RF),"\n"]). - open_db(DatabaseFile) -> {ok, DB} = sqlite3:open(event_db, [{file, DatabaseFile}]), ok = sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), @@ -122,14 +78,22 @@ init_db(Database) -> [{primary_key, [team]}, {foreign_key, {[division], divisions, [division], "ON DELETE SET NULL"}}]), - ok = sqlite3:create_table(Database, matches, [{division, text}, + ok = sqlite3:create_table(Database, matches, [{division, text, [not_null]}, {type, text, [not_null]}, {number, integer, [not_null]}], [{primary_key, [division, type, number]}, - {check, "type IN ('practice', 'qualification', 'elimination', 'final')"}, + {check, "type IN ('practice', 'qualification')"}, {foreign_key, {[division], divisions, [division], "ON DELETE CASCADE"}}]), - ok = sqlite3:create_table(Database, match_teams, [{division, text}, + ok = sqlite3:create_table(Database, match_states, [{division, text, [not_null]}, + {type, text, [not_null]}, + {number, integer, [not_null]}, + {tick, integer, [not_null]}, + {state, text, [not_null]}], + [{primary_key, [division, type, number, tick]}, + {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]), + + ok = sqlite3:create_table(Database, match_teams, [{division, text, [not_null]}, {type, text, [not_null]}, {number, integer, [not_null]}, {side, text, [not_null]}, @@ -142,65 +106,84 @@ init_db(Database) -> {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}, {foreign_key, {[team], teams, [team], ""}}]), - ok = sqlite3:create_table(Database, elim_alliances, [{division, text}, - {type, text, [not_null]}, + ok = sqlite3:create_table(Database, match_scores, [{division, text, [not_null]}, + {type, text, [not_null]}, + {number, integer, [not_null]}, + {history, integer, [not_null]}, + {uuid, integer, [not_null]}], + [{primary_key, [division, type, number, history]}, + {foreign_key, {[uuid], scores, [uuid], "ON DELETE CASCADE"}}, + {foreign_key, {[division, type, number], + matches, + [division, type, number], + "ON DELETE CASCADE"}}]), + + ok = sqlite3:create_table(Database, brackets, [{division, text}, + {number, integer, [not_null]}], + [{primary_key, [division, number]}, + {foreign_key, {[division], divisions, [division], "ON DELETE CASCADE"}}]), + + ok = sqlite3:create_table(Database, bracket_states, [{division, text}, {number, integer, [not_null]}, - {side, text, [not_null]}, - {anum, integer, [not_null]}], - [{primary_key, [division, type, number, side]}, + {tick, integer, [not_null]}, + {state, text, [not_null]}], + [{primary_key, [division, number, tick]}, + {foreign_key, {[division, number], brackets, [division, number], "ON DELETE CASCADE"}}]), + + ok = sqlite3:create_table(Database, elimination_alliances, [{division, text, [not_null]}, + {number, integer, [not_null]}, + {side, text, [not_null]}, + {anum, integer, [not_null]}], + [{primary_key, [division, number, side]}, {check, "side IN ('red', 'blue')"}, - {check, "type == 'elimination'"}, - {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}, + {foreign_key, {[division, number], brackets, [division, number], "ON DELETE CASCADE"}}, {foreign_key, {[division, anum], alliances, [division, number], ""}}]), ok = sqlite3:create_table(Database, finals_alliances, [{division, text}, - {type, text, [not_null]}, {number, integer, [not_null]}, {side, text, [not_null]}, {adiv, text, [not_null]}], - [{primary_key, [division, type, number, side]}, + [{primary_key, [division, number, side]}, {check, "side IN ('red', 'blue')"}, {check, "division IS NULL"}, - {check, "type == 'final'"}, - {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}, + {foreign_key, {[division, number], brackets, [division, number], "ON DELETE CASCADE"}}, {foreign_key, {[adiv], winners, [division], ""}}]), - ok = sqlite3:create_table(Database, match_scores, [{division, text}, - {type, text, [not_null]}, - {number, integer, [not_null]}, - {instance, integer, [not_null]}, - {history, integer, [not_null]}, - {autonomous, integer, [not_null]}, - {red_elevation_1, integer, [not_null]}, - {red_elevation_2, integer, [not_null]}, - {red_goal, integer, [not_null]}, - {red_zone, integer, [not_null]}, - {red_agoal, integer, [not_null]}, - {red_azone, integer, [not_null]}, - {red_1, text}, - {red_2, text}, - {blue_elevation_1, integer, [not_null]}, - {blue_elevation_2, integer, [not_null]}, - {blue_goal, integer, [not_null]}, - {blue_zone, integer, [not_null]}, - {blue_agoal, integer, [not_null]}, - {blue_azone, integer, [not_null]}, - {blue_1, text}, - {blue_2, text}], - [{primary_key, [division, type, number, instance, history]}, + ok = sqlite3:create_table(Database, bracket_scores, [{division, text}, + {number, integer, [not_null]}, + {instance, integer, [not_null]}, + {history, integer, [not_null]}, + {uuid, integer, [not_null]}], + [{primary_key, [division, number, instance, history]}, + {foreign_key, {[uuid], scores, [uuid], "ON DELETE CASCADE"}}, + {foreign_key, {[division, number], + brackets, + [division, number], + "ON DELETE CASCADE"}}]), + + ok = sqlite3:create_table(Database, scores, [{uuid, integer, [not_null]}, + {autonomous, integer, [not_null]}, + {red_elevation_1, integer, [not_null]}, + {red_elevation_2, integer, [not_null]}, + {red_goal, integer, [not_null]}, + {red_zone, integer, [not_null]}, + {red_agoal, integer, [not_null]}, + {red_azone, integer, [not_null]}, + {red_1, text}, + {red_2, text}, + {blue_elevation_1, integer, [not_null]}, + {blue_elevation_2, integer, [not_null]}, + {blue_goal, integer, [not_null]}, + {blue_zone, integer, [not_null]}, + {blue_agoal, integer, [not_null]}, + {blue_azone, integer, [not_null]}, + {blue_1, text}, + {blue_2, text}], + [{primary_key, [uuid]}, {check, "red_1 IS NULL OR red_1 IN ('dq', 'no show')"}, {check, "red_2 IS NULL OR red_2 IN ('dq', 'no show')"}, {check, "blue_1 IS NULL OR blue_1 IN ('dq', 'no show')"}, - {check, "blue_2 IS NULL OR blue_2 IN ('dq', 'no show')"}, - {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]), - - ok = sqlite3:create_table(Database, match_states, [{division, text}, - {type, text, [not_null]}, - {number, integer, [not_null]}, - {tick, integer, [not_null]}, - {state, text, [not_null]}], - [{primary_key, [division, type, number, tick]}, - {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]), + {check, "blue_2 IS NULL OR blue_2 IN ('dq', 'no show')"}]), ok = sqlite3:create_table(Database, alliances, [{division, text, [not_null]}, {number, integer, [not_null]}], @@ -257,7 +240,7 @@ write_division_matches(Database, Division, Type, Matches) -> [Division, Round]), lists:append([io_lib:format( "INSERT INTO matches(division, type, number) VALUES ('~s', '~s', ~p); - INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'red', 1, '~s'); + INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'red', 1, '~s'); INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'red', 2, '~s'); INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'blue', 1, '~s'); INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'blue', 2, '~s');", @@ -403,42 +386,33 @@ handle_call({delete_division, Division}, _, State) -> handle_call({generate_practice, Division, Size}, _, State) -> Teams = get_div_teams(State#state.database, Division), Resp = case schedule:create(rand:uniform(10000000), Size, Teams) of - error -> error; - Matches -> write_division_matches(State#state.database, Division, practice, Matches) - end, + error -> error; + Matches -> write_division_matches(State#state.database, Division, practice, Matches) + end, {reply, Resp, State}; handle_call({generate_qualification, Division, Size}, _, State) -> Teams = get_div_teams(State#state.database, Division), Resp = case schedule:create(rand:uniform(10000000), Size, Teams) of - error -> error; - Matches -> write_division_matches(State#state.database, Division, qualification, Matches) - end, + error -> error; + Matches -> write_division_matches(State#state.database, Division, qualification, Matches) + end, {reply, Resp, State}; handle_call({generate_elimination, Division, Size}, _, State) -> - BracketSize = higher_pow2(Size), - Total = Size-1, + Bracket = schedule:generate_bracket(Size), SQL = lists:append( - [ - io_lib:format("DELETE FROM alliances WHERE division = '~s'; ", [Division]), - io_lib:format("DELETE FROM matches WHERE division = '~s' AND type = 'elimination'; ", [Division]), - lists:append([io_lib:format("INSERT INTO alliances(division, number) VALUES('~s', ~p); ", - [Division, N]) - || N <- lists:seq(1, Total)]), - lists:append([io_lib:format("INSERT INTO matches(division, type, number) - VALUES('~s', 'elimination', ~p); - INSERT INTO elim_alliances(division, type, number, side, anum) - VALUES('~s', 'elimination', ~p, 'red', ~p); - INSERT INTO elim_alliances(division, type, number, side, anum) - VALUES('~s', 'elimination', ~p, 'blue', ~p);", - [Division, N, Division, N, BracketSize/2+N+1, Division, N, BracketSize/2-N]) - || N <- lists:seq(1, Size - BracketSize/2)]), - lists:append([io_lib:format("INSERT INTO elim_alliances(division, type, number, side, anum) - VALUES('~s', 'elimination', ~p, 'red', ~p);", - [Division, N, BracketSize/2-N+1]) - || N <- lists:seq(Size - BracketSize/2+1, BracketSize/2)])]), + [ + io_lib:format("DELETE FROM alliances WHERE division = '~s'; ", [Division]), + io_lib:format("DELETE FROM brackets WHERE division = '~s'; ", [Division]), + lists:append([io_lib:format("INSERT INTO alliances(division, number) VALUES('~s', ~p); ", + [Division, N]) + || N <- lists:seq(1, Size)]), + lists:append([io_lib:format("INSERT INTO brackets(division, number) + VALUES('~s', ~p);", + [Division, N]) + || N <- lists:seq(1, Size-1)])]), + {reply, sql_begin(State#state.database, SQL), State}; handle_call(generate_finals, _, State) -> - [{columns, _}, {rows, [{Size}]}] = sqlite3:sql_exec(State#state.database, "SELECT COUNT(*) FROM divisions;"), {reply, noimpl, State}; handle_call(_, _, State) -> {reply, noimpl, State}. diff --git a/src/schedule.erl b/src/schedule.erl index cdd5ec7..bf92c74 100644 --- a/src/schedule.erl +++ b/src/schedule.erl @@ -4,7 +4,57 @@ -export([start_link/0, init/1, handle_cast/2, handle_call/3]). --export([create/3]). +-export([create/3, generate_bracket/1, parent/3]). + +-include("schedule.hrl"). + +higher_pow2(N) -> round(math:pow(2, ceil(math:log2(N)))). + +first_layer(BracketSize, _, N) when N > BracketSize/2 -> []; +first_layer(BracketSize, Size, N) when BracketSize - N + 1 > Size -> + [N | first_layer(BracketSize, Size, N+1)]; +first_layer(BracketSize, Size, N) -> + [{N, BracketSize - N + 1} | first_layer(BracketSize, Size, N+1)]. + +first_layer(Size) when Size > 1 -> + BracketSize = higher_pow2(Size), + first_layer(BracketSize, Size, 1). + +merge_layer([Element], []) -> Element; +merge_layer([], Merged) -> + Final = lists:reverse(Merged), + merge_layer(Final); +merge_layer([First | Rest], Merged) -> + Last = lists:last(Rest), + List = lists:droplast(Rest), + merge_layer(List, [{First, Last} | Merged]). +merge_layer(List) -> merge_layer(List, []). + +label_bracket({Left, Right}, [N | Counters], Depth) -> + {LC, L} = label_bracket(Left, Counters, Depth+1), + {RC, R} = label_bracket(Right, LC, Depth+1), + {[N+1 | RC], #bracket{num=N, red=L, blue=R}}; +label_bracket(Elem, Counters, _) -> {Counters, Elem}. + +label_bracket(Bracket, Depth) -> + {_, Labeled} = label_bracket(Bracket, lists:duplicate(Depth, 1), 1), + Labeled. + +generate_bracket(Size) -> + label_bracket(merge_layer(first_layer(Size)), ceil(math:log2(Size))). + +parent(Depth, Level, Index, #bracket{num = N, red = #bracket{num=Index}}) when Depth == Level - 1 -> + {N, red}; +parent(Depth, Level, Index, #bracket{num = N, blue = #bracket{num=Index}}) when Depth == Level - 1 -> + {N, blue}; +parent(Depth, Level, _, #bracket{}) when Depth >= Level -> false; +parent(Depth, Level, Index, #bracket{red = Left, blue = Right}) -> + L = parent(Depth + 1, Level, Index, Left), + if L =:= false -> parent(Depth + 1, Level, Index, Right); + true -> L + end. + +parent(Level, Index, Root) -> parent(1, Level, Index, Root). create(Seed, Size, Teams) -> gen_server:call(?MODULE, {generate, Seed, Size, Teams}).