Moved around some DB stuff so that brackets and matches are in different tables

main
noah metz 2024-01-31 23:14:58 -07:00
parent 2daec3e90b
commit 88293f86b0
3 changed files with 146 additions and 121 deletions

@ -0,0 +1 @@
-record(bracket, {num = none, red = none, blue = none}).

@ -3,10 +3,9 @@
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/1, init/1, handle_cast/2, handle_call/3, terminate/2]). -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(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;
first_error([ok | Rest]) -> first_error(Rest); first_error([ok | Rest]) -> first_error(Rest);
@ -26,49 +25,6 @@ sql_begin(Database, Script) ->
start_link(DatabaseFile) -> start_link(DatabaseFile) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [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) -> open_db(DatabaseFile) ->
{ok, DB} = sqlite3:open(event_db, [{file, DatabaseFile}]), {ok, DB} = sqlite3:open(event_db, [{file, DatabaseFile}]),
ok = sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), ok = sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"),
@ -122,14 +78,22 @@ init_db(Database) ->
[{primary_key, [team]}, [{primary_key, [team]},
{foreign_key, {[division], divisions, [division], "ON DELETE SET NULL"}}]), {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]}, {type, text, [not_null]},
{number, integer, [not_null]}], {number, integer, [not_null]}],
[{primary_key, [division, type, number]}, [{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"}}]), {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]}, {type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{side, text, [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, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}},
{foreign_key, {[team], teams, [team], ""}}]), {foreign_key, {[team], teams, [team], ""}}]),
ok = sqlite3:create_table(Database, elim_alliances, [{division, text}, ok = sqlite3:create_table(Database, match_scores, [{division, text, [not_null]},
{type, 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]}, {number, integer, [not_null]},
{side, text, [not_null]}, {tick, integer, [not_null]},
{anum, integer, [not_null]}], {state, text, [not_null]}],
[{primary_key, [division, type, number, side]}, [{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, "side IN ('red', 'blue')"},
{check, "type == 'elimination'"}, {foreign_key, {[division, number], brackets, [division, number], "ON DELETE CASCADE"}},
{foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}},
{foreign_key, {[division, anum], alliances, [division, number], ""}}]), {foreign_key, {[division, anum], alliances, [division, number], ""}}]),
ok = sqlite3:create_table(Database, finals_alliances, [{division, text}, ok = sqlite3:create_table(Database, finals_alliances, [{division, text},
{type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{side, text, [not_null]}, {side, text, [not_null]},
{adiv, text, [not_null]}], {adiv, text, [not_null]}],
[{primary_key, [division, type, number, side]}, [{primary_key, [division, number, side]},
{check, "side IN ('red', 'blue')"}, {check, "side IN ('red', 'blue')"},
{check, "division IS NULL"}, {check, "division IS NULL"},
{check, "type == 'final'"}, {foreign_key, {[division, number], brackets, [division, number], "ON DELETE CASCADE"}},
{foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}},
{foreign_key, {[adiv], winners, [division], ""}}]), {foreign_key, {[adiv], winners, [division], ""}}]),
ok = sqlite3:create_table(Database, match_scores, [{division, text}, ok = sqlite3:create_table(Database, bracket_scores, [{division, text},
{type, text, [not_null]}, {number, integer, [not_null]},
{number, integer, [not_null]}, {instance, integer, [not_null]},
{instance, integer, [not_null]}, {history, integer, [not_null]},
{history, integer, [not_null]}, {uuid, integer, [not_null]}],
{autonomous, integer, [not_null]}, [{primary_key, [division, number, instance, history]},
{red_elevation_1, integer, [not_null]}, {foreign_key, {[uuid], scores, [uuid], "ON DELETE CASCADE"}},
{red_elevation_2, integer, [not_null]}, {foreign_key, {[division, number],
{red_goal, integer, [not_null]}, brackets,
{red_zone, integer, [not_null]}, [division, number],
{red_agoal, integer, [not_null]}, "ON DELETE CASCADE"}}]),
{red_azone, integer, [not_null]},
{red_1, text}, ok = sqlite3:create_table(Database, scores, [{uuid, integer, [not_null]},
{red_2, text}, {autonomous, integer, [not_null]},
{blue_elevation_1, integer, [not_null]}, {red_elevation_1, integer, [not_null]},
{blue_elevation_2, integer, [not_null]}, {red_elevation_2, integer, [not_null]},
{blue_goal, integer, [not_null]}, {red_goal, integer, [not_null]},
{blue_zone, integer, [not_null]}, {red_zone, integer, [not_null]},
{blue_agoal, integer, [not_null]}, {red_agoal, integer, [not_null]},
{blue_azone, integer, [not_null]}, {red_azone, integer, [not_null]},
{blue_1, text}, {red_1, text},
{blue_2, text}], {red_2, text},
[{primary_key, [division, type, number, instance, history]}, {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_1 IS NULL OR red_1 IN ('dq', 'no show')"},
{check, "red_2 IS NULL OR red_2 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_1 IS NULL OR blue_1 IN ('dq', 'no show')"},
{check, "blue_2 IS NULL OR blue_2 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"}}]),
ok = sqlite3:create_table(Database, alliances, [{division, text, [not_null]}, ok = sqlite3:create_table(Database, alliances, [{division, text, [not_null]},
{number, integer, [not_null]}], {number, integer, [not_null]}],
@ -257,7 +240,7 @@ write_division_matches(Database, Division, Type, Matches) ->
[Division, Round]), [Division, Round]),
lists:append([io_lib:format( lists:append([io_lib:format(
"INSERT INTO matches(division, type, number) VALUES ('~s', '~s', ~p); "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, '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', 1, '~s');
INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'blue', 2, '~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) -> handle_call({generate_practice, Division, Size}, _, State) ->
Teams = get_div_teams(State#state.database, Division), Teams = get_div_teams(State#state.database, Division),
Resp = case schedule:create(rand:uniform(10000000), Size, Teams) of Resp = case schedule:create(rand:uniform(10000000), Size, Teams) of
error -> error; error -> error;
Matches -> write_division_matches(State#state.database, Division, practice, Matches) Matches -> write_division_matches(State#state.database, Division, practice, Matches)
end, end,
{reply, Resp, State}; {reply, Resp, State};
handle_call({generate_qualification, Division, Size}, _, State) -> handle_call({generate_qualification, Division, Size}, _, State) ->
Teams = get_div_teams(State#state.database, Division), Teams = get_div_teams(State#state.database, Division),
Resp = case schedule:create(rand:uniform(10000000), Size, Teams) of Resp = case schedule:create(rand:uniform(10000000), Size, Teams) of
error -> error; error -> error;
Matches -> write_division_matches(State#state.database, Division, qualification, Matches) Matches -> write_division_matches(State#state.database, Division, qualification, Matches)
end, end,
{reply, Resp, State}; {reply, Resp, State};
handle_call({generate_elimination, Division, Size}, _, State) -> handle_call({generate_elimination, Division, Size}, _, State) ->
BracketSize = higher_pow2(Size), Bracket = schedule:generate_bracket(Size),
Total = Size-1,
SQL = lists:append( SQL = lists:append(
[ [
io_lib:format("DELETE FROM alliances WHERE division = '~s'; ", [Division]), io_lib:format("DELETE FROM alliances WHERE division = '~s'; ", [Division]),
io_lib:format("DELETE FROM matches WHERE division = '~s' AND type = 'elimination'; ", [Division]), io_lib:format("DELETE FROM brackets WHERE division = '~s'; ", [Division]),
lists:append([io_lib:format("INSERT INTO alliances(division, number) VALUES('~s', ~p); ", lists:append([io_lib:format("INSERT INTO alliances(division, number) VALUES('~s', ~p); ",
[Division, N]) [Division, N])
|| N <- lists:seq(1, Total)]), || N <- lists:seq(1, Size)]),
lists:append([io_lib:format("INSERT INTO matches(division, type, number) lists:append([io_lib:format("INSERT INTO brackets(division, number)
VALUES('~s', 'elimination', ~p); VALUES('~s', ~p);",
INSERT INTO elim_alliances(division, type, number, side, anum) [Division, N])
VALUES('~s', 'elimination', ~p, 'red', ~p); || N <- lists:seq(1, Size-1)])]),
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)])]),
{reply, sql_begin(State#state.database, SQL), State}; {reply, sql_begin(State#state.database, SQL), State};
handle_call(generate_finals, _, State) -> handle_call(generate_finals, _, State) ->
[{columns, _}, {rows, [{Size}]}] = sqlite3:sql_exec(State#state.database, "SELECT COUNT(*) FROM divisions;"),
{reply, noimpl, State}; {reply, noimpl, State};
handle_call(_, _, State) -> handle_call(_, _, State) ->
{reply, noimpl, State}. {reply, noimpl, State}.

@ -4,7 +4,57 @@
-export([start_link/0, init/1, handle_cast/2, handle_call/3]). -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) -> create(Seed, Size, Teams) ->
gen_server:call(?MODULE, {generate, Seed, Size, Teams}). gen_server:call(?MODULE, {generate, Seed, Size, Teams}).