Moved schedule to database and made schedule only handle generating from params and returning randomized schedule + genid

main
noah metz 2024-01-27 12:50:26 -07:00
parent 7162a1de37
commit 6682f37635
3 changed files with 184 additions and 147 deletions

@ -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}.

@ -30,6 +30,9 @@ init(_) ->
#{id => schedule, #{id => schedule,
start => {schedule, start_link, []} start => {schedule, start_link, []}
}, },
#{id => database,
start => {database, start_link, []}
},
#{id => mqtt, #{id => mqtt,
start => {mqtt, start_link, []} start => {mqtt, start_link, []}
}, },

@ -4,20 +4,22 @@
-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([set_test_config/0]). -export([create/3, make_genid/5]).
-include("erlk.hrl"). create(Seed, Size, Teams) ->
gen_server:call(?MODULE, {generate, Seed, Size, Teams}).
-record(division, {teams = [], practice = 0, qualification = 0}).
-record(config, {divisions = []}).
-record(state, {database_file = none, database = none, teams = [], config = none}).
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) -> init([]) ->
rand:seed(default), {ok, []}.
{ok, #state{}}.
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(1, Original, Constructed) -> lists:append(Original, Constructed);
duplicate(N, Original, Constructed) -> duplicate(N-1, Original, 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, _} = lists:mapfoldl(fun fill_padding_match/2, TeamsRandom, Matches),
Schedule. Schedule.
init_db(Database) -> create_schedule(Seed, Size, Teams) ->
ok = sqlite3:create_table(Database, teams, [{name, text, [not_null]}, TeamsRepeated = duplicate(Size, Teams),
{inspection, blob}], TeamsPadded = TeamsRepeated ++ lists:duplicate(padding(length(Teams)*Size), padding),
[{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),
rand:seed(default, Seed), rand:seed(default, Seed),
TeamsRandom = [X || {_, X} <- lists:sort([{rand:uniform(), N} || N <- TeamsPadded])], TeamsRandom = [X || {_, X} <- lists:sort([{rand:uniform(), N} || N <- TeamsPadded])],
fill_padding_schedule(pick_matches(TeamsRandom), Teams). fill_padding_schedule(pick_matches(TeamsRandom), Teams).
@ -146,59 +68,5 @@ pick_matches(Teams, Matches) ->
pick_matches(Teams) -> pick_matches(Teams, []). pick_matches(Teams) -> pick_matches(Teams, []).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% make_genid(Type, Round, Teams, Seed, Size) ->
-define(TEST_CONFIG, #config{divisions = [#division{teams = ["A", "B", "C", "D"], practice = 4}]}). lists:sublist([ Y || <<X:4>> <= crypto:hash(sha, "TYPE:" ++ atom_to_list(Type) ++ "ROUND:" ++ atom_to_list(Round) ++ Teams ++ [Seed, Size]), Y <- integer_to_list(X, 16)], 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 || <<X:4>> <= 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}.