From 6682f3763597928c4d2fdc25d574a33bb893be42 Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Sat, 27 Jan 2024 12:50:26 -0700 Subject: [PATCH] Moved schedule to database and made schedule only handle generating from params and returning randomized schedule + genid --- src/database.erl | 166 +++++++++++++++++++++++++++++++++++++++++++++++ src/erlk_sup.erl | 3 + src/schedule.erl | 162 +++++---------------------------------------- 3 files changed, 184 insertions(+), 147 deletions(-) create mode 100644 src/database.erl diff --git a/src/database.erl b/src/database.erl new file mode 100644 index 0000000..e410e18 --- /dev/null +++ b/src/database.erl @@ -0,0 +1,166 @@ +-module(database). + +-behaviour(gen_server). + +-export([start_link/0, init/1, handle_cast/2, handle_call/3, add_teams/1]). + +-export([set_test_config/0, new/1, load/1]). + +-record(division, {teams = [], practice = 0, qualification = 0}). +-record(config, {divisions = []}). +-record(state, {database_file = none, database = none, teams = [], config = none}). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). + +init([]) -> + {ok, #state{}}. + +init_db(Database) -> + ok = sqlite3:create_table(Database, genids, [{genid, text, [not_null]}, + {type, text, [not_null]}, + {size, integer, [not_null]}, + {seed, integer, [not_null]}], + [{primary_key, [genid]}, + {check, "type IN ('division', 'final')"}]), + ok = sqlite3:create_table(Database, teams, [{name, text, [not_null]}, + {inspection, blob}], + [{primary_key, [name]}]), + ok = sqlite3:create_table(Database, matches, [{genid, text, [not_null]}, + {round, text, [not_null]}, + {number, integer, [not_null]}, + {red_1, text, [not_null]}, + {red_2, text, [not_null]}, + {blue_1, text, [not_null]}, + {blue_2, text, [not_null]}], + [{primary_key, [genid, round, number]}, + {check, "round IN ('practice', 'qualification', 'elimination', 'final')"}, + {foreign_key, {[genid], genids, [genid], "ON DELETE RESTRICT"}}, + {foreign_key, {[blue_1], teams, [name], "ON DELETE RESTRICT"}}, + {foreign_key, {[blue_2], teams, [name], "ON DELETE RESTRICT"}}, + {foreign_key, {[red_1], teams, [name], "ON DELETE RESTRICT"}}, + {foreign_key, {[red_2], teams, [name], "ON DELETE RESTRICT"}}]), + + ok = sqlite3:create_table(Database, match_scores, [{genid, text, [not_null]}, + {round, text, [not_null]}, + {number, integer, [not_null]}, + {instance, integer, [not_null]}, + {score, blob, [not_null]}], + [{primary_key, [genid, round, number, instance]}, + {foreign_key, {[genid, round, number], matches, [genid, round, number], "ON DELETE CASCADE"}}]), + ok = sqlite3:create_table(Database, match_states, [{genid, text, [not_null]}, + {round, text, [not_null]}, + {number, integer, [not_null]}, + {time, integer, [not_null]}, + {state, blob, [not_null]}], + [{primary_key, [genid, round, number, time]}, + {foreign_key, {[genid, round, number], matches, [genid, round, number], "ON DELETE CASCADE"}}]), + + ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]}, + {type, text, [not_null]}, + {attempt, integer, [not_null]}, + {score, blob, [not_null]}], + [{primary_key, [team, type, attempt]}, + {foreign_key, {[team], teams, [name], "ON DELETE CASCADE"}}]), + ok. + +delete_teams(Database, Teams) -> + SQL = lists:append(["BEGIN; ", + lists:append([io_lib:format("DELETE FROM teams WHERE name = ~p; ", + [Team]) || Team <- Teams]), + "COMMIT;"]), + first_error(sqlite3:sql_exec_script(Database, SQL)). + +add_teams(Database, Teams) -> + SQL = lists:append(["BEGIN; ", + lists:append([io_lib:format("INSERT INTO teams(name) VALUES('~s') ON CONFLICT(name) DO NOTHING; ", + [Team]) || Team <- Teams]), + "COMMIT;"]), + first_error(sqlite3:sql_exec_script(Database, SQL)). + +first_error([]) -> ok; +first_error([ok | Rest]) -> first_error(Rest); +first_error([Error | _]) -> Error. + +delete_matches(Database, GenID) -> + SQL = io_lib:format("DELETE FROM matches WHERE genid = ~p", [GenID]), + first_error(sqlite3:sql_exec_script(Database, SQL)). + +write_matches(Database, Type, Teams, Seed, Size, Round, Matches) -> + GenID = schedule:make_genid(Type, Round, Teams, Seed, Size), + SQL = lists:append(["BEGIN; ", + io_lib:format("INSERT INTO genids(genid, type, size, seed) VALUES('~s', '~s', ~p, ~p); ", + [GenID, atom_to_list(Type), Size, Seed]), + lists:append([io_lib:format( + "INSERT INTO matches(genid, round, number, blue_1, blue_2, red_1, red_2) + VALUES ('~s', '~s', ~p, '~s', '~s', '~s', '~s'); ", + [GenID, atom_to_list(Round), N, B1, B2, R1, R2]) + || {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]), + "COMMIT;"]), + {first_error(sqlite3:sql_exec_script(Database, SQL)), GenID}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-define(TEST_CONFIG, #config{divisions = [#division{teams = ["A", "B", "C", "D"], practice = 4}]}). +set_test_config() -> + gen_server:call(?MODULE, {set_config, ?TEST_CONFIG}). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +add_teams(Teams) -> + gen_server:call(?MODULE, {add_teams, Teams}). + +new(DatabaseFile) -> + gen_server:call(?MODULE, {new_db, DatabaseFile}). + +load(DatabaseFile) -> + gen_server:call(?MODULE, {load_db, DatabaseFile}). + +handle_call({set_config, Config}, _, State) when is_record(Config, config) -> + {reply, ok, State#state{config = Config}}; +handle_call({new_db, DatabaseFile}, _, State) -> + ok = if State#state.database =:= none -> ok; + true -> sqlite3:close(State#state.database) + end, + Exists = filelib:is_regular(DatabaseFile), + ok = if Exists -> file:delete(DatabaseFile); + true -> ok + end, + {ok, Database} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), + ok = sqlite3:sql_exec(Database, "PRAGMA foreign_keys = ON;"), + {reply, init_db(Database), State#state{database_file = DatabaseFile, database = Database}}; +handle_call({load_db, DatabaseFile}, _, State) -> + true = filelib:is_regular(DatabaseFile), + {ok, Database} = if DatabaseFile == State#state.database_file -> + {ok, State#state.database}; + true -> + ok = if State#state.database =:= none -> ok; + true -> sqlite3:close(State#state.database) + end, + {ok, DB} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), + {sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), DB} + end, + {reply, ok, State#state{database_file = DatabaseFile, database = Database}}; +handle_call({delete_teams, Removed}, _, State) -> + Teams = lists:filter(fun(X) -> lists:member(X, Removed) =:= false end, State#state.teams), + ok = delete_teams(State#state.database, Removed), + {reply, ok, State#state{teams = Teams}}; +handle_call({add_teams, Teams}, _, State) -> + ok = add_teams(State#state.database, Teams), + {reply, ok, State#state{teams = Teams}}; +handle_call({new_schedule, Division, Round, Seed}, _, State) -> + DivInfo = lists:nth(Division, State#state.config#config.divisions), + Teams = DivInfo#division.teams, + Size = case Round of + practice when DivInfo#division.practice =/= 0 -> + DivInfo#division.practice; + qualification when DivInfo#division.qualification =/= 0-> + DivInfo#division.qualification + end, + Matches = schedule:create(Seed, Size, Teams), + {ok, GenID} = write_matches(State#state.database, division, Teams, Seed, Size, Round, Matches), + {reply, GenID, State}; +handle_call({delete_schedule, GenID}, _, State) -> + ok = delete_matches(State#state.database, GenID), + {reply, ok, State}. + +handle_cast(_, State) -> + {noreply, State}. diff --git a/src/erlk_sup.erl b/src/erlk_sup.erl index 8d0ec15..3055527 100644 --- a/src/erlk_sup.erl +++ b/src/erlk_sup.erl @@ -30,6 +30,9 @@ init(_) -> #{id => schedule, start => {schedule, start_link, []} }, + #{id => database, + start => {database, start_link, []} + }, #{id => mqtt, start => {mqtt, start_link, []} }, diff --git a/src/schedule.erl b/src/schedule.erl index 6acd7b1..34c699f 100644 --- a/src/schedule.erl +++ b/src/schedule.erl @@ -4,20 +4,22 @@ -export([start_link/0, init/1, handle_cast/2, handle_call/3]). --export([set_test_config/0]). +-export([create/3, make_genid/5]). --include("erlk.hrl"). - --record(division, {teams = [], practice = 0, qualification = 0}). --record(config, {divisions = []}). --record(state, {database_file = none, database = none, teams = [], config = none}). +create(Seed, Size, Teams) -> + gen_server:call(?MODULE, {generate, Seed, Size, Teams}). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). init([]) -> - rand:seed(default), - {ok, #state{}}. + {ok, []}. + +handle_cast(_, State) -> + {noreply, State}. + +handle_call({generate, Seed, Size, Teams}, _, State) -> + {reply, create_schedule(Seed, Size, Teams), State}. duplicate(1, Original, Constructed) -> lists:append(Original, Constructed); duplicate(N, Original, Constructed) -> duplicate(N-1, Original, lists:append(Original, Constructed)). @@ -43,89 +45,9 @@ fill_padding_schedule(Matches, Teams) -> {Schedule, _} = lists:mapfoldl(fun fill_padding_match/2, TeamsRandom, Matches), Schedule. -init_db(Database) -> - ok = sqlite3:create_table(Database, teams, [{name, text, [not_null]}, - {inspection, blob}], - [{primary_key, [name]}]), - ok = sqlite3:create_table(Database, matches, [{division, integer, [not_null]}, - {round, text, [not_null]}, - {number, integer, [not_null]}, - {genid, text, [not_null]}, - {red_1, text, [not_null]}, - {red_2, text, [not_null]}, - {blue_1, text, [not_null]}, - {blue_2, text, [not_null]}], - [{primary_key, [division, round, number, genid]}, - {check, "round IN ('practice', 'qualification', 'elimination')"}, - {foreign_key, {[blue_1], teams, [name], "ON DELETE RESTRICT"}}, - {foreign_key, {[blue_2], teams, [name], "ON DELETE RESTRICT"}}, - {foreign_key, {[red_1], teams, [name], "ON DELETE RESTRICT"}}, - {foreign_key, {[red_2], teams, [name], "ON DELETE RESTRICT"}}]), - - ok = sqlite3:create_table(Database, match_scores, [{division, integer, [not_null]}, - {round, text, [not_null]}, - {number, integer, [not_null]}, - {genid, text, [not_null]}, - {instance, integer, [not_null]}, - {score, blob, [not_null]}], - [{primary_key, [division, round, number, genid, instance]}, - {foreign_key, {[division, round, number, genid], matches, [division, round, number, genid], "ON DELETE CASCADE"}}]), - ok = sqlite3:create_table(Database, match_states, [{division, integer, [not_null]}, - {round, text, [not_null]}, - {number, integer, [not_null]}, - {genid, text, [not_null]}, - {time, integer, [not_null]}, - {state, blob, [not_null]}], - [{primary_key, [division, round, number, genid, time]}, - {foreign_key, {[division, round, number, genid], matches, [division, round, number, genid], "ON DELETE CASCADE"}}]), - - % TODO: finals - % ok = sqlite3:create_table(Database, finals, [], [{primary_key, []}]), - ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]}, - {type, text, [not_null]}, - {attempt, integer, [not_null]}, - {score, blob, [not_null]}], - [{primary_key, [team, type, attempt]}, - {foreign_key, {[team], teams, [name], "ON DELETE CASCADE"}}]), - ok. - -delete_teams(Database, Teams) -> - SQL = lists:append(["BEGIN; ", - lists:append([io_lib:format("DELETE FROM teams WHERE name = ~p; ", - [Team]) || Team <- Teams]), - "COMMIT;"]), - sqlite3:sql_exec_script(Database, SQL). - -add_teams(Database, Teams) -> - SQL = lists:append(["BEGIN; ", - lists:append([io_lib:format("INSERT INTO teams(name) VALUES('~s') ON CONFLICT(name) DO NOTHING; ", - [Team]) || Team <- Teams]), - "COMMIT;"]), - sqlite3:sql_exec_script(Database, SQL). - -first_error([]) -> ok; -first_error([ok | Rest]) -> first_error(Rest); -first_error([Error | _]) -> Error. - -delete_matches(Database, Division, Round, GenID) -> - SQL = io_lib:format("DELETE FROM matches WHERE division = ~p AND genid = ~p AND round = '~p'", [Division, GenID, Round]), - sqlite3:sql_exec_script(Database, SQL). - -write_matches(Database, Division, Round, GenID, Matches) -> - Schedule = [[{division, Division}, - {round, atom_to_list(Round)}, - {number, N}, - {genid, GenID}, - {blue_1, B1}, - {blue_2, B2}, - {red_1, R1}, - {red_2, R2}] || {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)], - [ok | Resp] = sqlite3:write_many(Database, matches, Schedule), - lists:last(Resp). - -create_schedule(Seed, Matches, Teams) -> - TeamsRepeated = duplicate(Matches, Teams), - TeamsPadded = TeamsRepeated ++ lists:duplicate(padding(length(Teams)*Matches), padding), +create_schedule(Seed, Size, Teams) -> + TeamsRepeated = duplicate(Size, Teams), + TeamsPadded = TeamsRepeated ++ lists:duplicate(padding(length(Teams)*Size), padding), rand:seed(default, Seed), TeamsRandom = [X || {_, X} <- lists:sort([{rand:uniform(), N} || N <- TeamsPadded])], fill_padding_schedule(pick_matches(TeamsRandom), Teams). @@ -146,59 +68,5 @@ pick_matches(Teams, Matches) -> pick_matches(Teams) -> pick_matches(Teams, []). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --define(TEST_CONFIG, #config{divisions = [#division{teams = ["A", "B", "C", "D"], practice = 4}]}). -set_test_config() -> - gen_server:call(?MODULE, {set_config, ?TEST_CONFIG}). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -handle_call({set_config, Config}, _, State) when is_record(Config, config) -> - {reply, ok, State#state{config = Config}}; -handle_call({new_schedule, Division, Round, Seed}, _, State) -> - DivInfo = lists:nth(Division, State#state.config#config.divisions), - Teams = DivInfo#division.teams, - Matches = case Round of - practice when DivInfo#division.practice =/= 0 -> - DivInfo#division.practice; - qualification when DivInfo#division.qualification =/= 0-> - DivInfo#division.qualification - end, - MatchTeams = create_schedule(Seed, Matches, Teams), - GenID = lists:sublist([ Y || <> <= crypto:hash(sha, Teams ++ [Seed, Matches]), - Y <- integer_to_list(X, 16)], 4), - ok = write_matches(State#state.database, Division, Round, GenID, MatchTeams), - {reply, GenID, State}; -handle_call({new_db, DatabaseFile}, _, State) -> - ok = if State#state.database =:= none -> ok; - true -> sqlite3:close(State#state.database) - end, - Exists = filelib:is_regular(DatabaseFile), - ok = if Exists -> file:delete(DatabaseFile); - true -> ok - end, - {ok, Database} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), - ok = sqlite3:sql_exec(Database, "PRAGMA foreign_keys = ON;"), - {reply, init_db(Database), State#state{database_file = DatabaseFile, database = Database}}; -handle_call({load_db, DatabaseFile}, _, State) -> - true = filelib:is_regular(DatabaseFile), - {ok, Database} = if DatabaseFile == State#state.database_file -> - {ok, State#state.database}; - true -> - ok = if State#state.database =:= none -> ok; - true -> sqlite3:close(State#state.database) - end, - {ok, DB} = sqlite3:open(schedule_db, [{file, DatabaseFile}]), - {sqlite3:sql_exec(DB, "PRAGMA foreign_keys = ON;"), DB} - end, - {reply, ok, State#state{database_file = DatabaseFile, database = Database}}. - -handle_cast({delete_teams, Removed}, State) -> - Teams = lists:filter(fun(X) -> lists:member(X, Removed) =:= false end, State#state.teams), - ok = first_error(delete_teams(State#state.database, Removed)), - {noreply, State#state{teams = Teams}}; -handle_cast({add_teams, Teams}, State) -> - ok = first_error(add_teams(State#state.database, Teams)), - {noreply, State#state{teams = Teams}}; -handle_cast({delete_schedule, Division, Round, GenID}, State) -> - ok = first_error(delete_matches(State#state.database, Division, Round, GenID)), - {noreply, State}. +make_genid(Type, Round, Teams, Seed, Size) -> + lists:sublist([ Y || <> <= crypto:hash(sha, "TYPE:" ++ atom_to_list(Type) ++ "ROUND:" ++ atom_to_list(Round) ++ Teams ++ [Seed, Size]), Y <- integer_to_list(X, 16)], 4).