Reorganized DB, and fixed crashes

main
noah metz 2024-01-30 14:43:05 -07:00
parent 90afd62436
commit 02ad16a459
4 changed files with 180 additions and 72 deletions

@ -2,7 +2,7 @@
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/1, init/1, handle_cast/2, handle_call/3]). -export([start_link/1, init/1, handle_cast/2, handle_call/3, terminate/2]).
-export([get_tick/0]). -export([get_tick/0]).
-record(state, {database_file = none, database = none, tick_start = none}). -record(state, {database_file = none, database = none, tick_start = none}).
@ -46,37 +46,67 @@ init([DatabaseFile]) ->
end. end.
init_db(Database) -> init_db(Database) ->
ok = sqlite3:create_table(Database, divisions, [{division, integer, [not_null]}], ok = sqlite3:create_table(Database, divisions, [{division, text, [not_null]}],
[{primary_key, [division]}]), [{primary_key, [division]}]),
ok = sqlite3:create_table(Database, fields, [{field, text, [not_null]},
{serial, text, [unique]},
{division, text},
{other, text}],
[{primary_key, [field]},
{foreign_key, {[division], divisions, [division], "ON DELETE SET NULL"}},
{check, "other IN ('testing', 'skills')"}]),
ok = sqlite3:create_table(Database, teams, [{team, text, [not_null]}, ok = sqlite3:create_table(Database, teams, [{team, text, [not_null]},
{division, integer}, {division, text},
{inspection, blob}], {inspection, blob}],
[{primary_key, [team]}, [{primary_key, [team]},
{foreign_key, {[division], divisions, [division], ""}}]), {foreign_key, {[division], divisions, [division], "ON DELETE SET NULL"}}]),
ok = sqlite3:create_table(Database, matches, [{division, integer}, ok = sqlite3:create_table(Database, matches, [{division, text},
{type, text, [not_null]}, {type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]}],
{instance, integer, [not_null]}], [{primary_key, [division, type, number]},
[{primary_key, [division, type, number, instance]},
{check, "type IN ('practice', 'qualification', 'elimination', 'final')"}, {check, "type IN ('practice', 'qualification', 'elimination', 'final')"},
{foreign_key, {[division], divisions, [division], "ON DELETE CASCADE"}}]), {foreign_key, {[division], divisions, [division], "ON DELETE CASCADE"}}]),
ok = sqlite3:create_table(Database, match_teams, [{division, integer, [not_null]}, ok = sqlite3:create_table(Database, match_teams, [{division, text},
{type, text, [not_null]}, {type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{instance, integer, [not_null]},
{side, text, [not_null]}, {side, text, [not_null]},
{anum, integer, [not_null]}, {anum, integer, [not_null]},
{team, text, [not_null]}], {team, text, [not_null]}],
[{primary_key, [division, type, number, instance, side, anum]}, [{primary_key, [division, type, number, side, anum]},
{check, "side IN ('red', 'blue')"}, {check, "side IN ('red', 'blue')"},
{check, "anum >= 1 AND anum <= 2"}, {check, "anum >= 1 AND anum <= 2"},
{foreign_key, {[division, type, number, instance], matches, [division, type, number, instance], "ON DELETE CASCADE"}}, {check, "type IN ('practice', 'qualification')"},
{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, match_scores, [{division, integer}, ok = sqlite3:create_table(Database, elim_alliances, [{division, text},
{type, text, [not_null]},
{number, integer, [not_null]},
{side, text, [not_null]},
{anum, integer, [not_null]}],
[{primary_key, [division, type, 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, 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]},
{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, {[adiv], winners, [division], ""}}]),
ok = sqlite3:create_table(Database, match_scores, [{division, text},
{type, text, [not_null]}, {type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{instance, integer, [not_null]}, {instance, integer, [not_null]},
@ -103,32 +133,34 @@ init_db(Database) ->
{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, instance], matches, [division, type, number, instance], "ON DELETE CASCADE"}}]), {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]),
ok = sqlite3:create_table(Database, match_states, [{division, integer}, ok = sqlite3:create_table(Database, match_states, [{division, text},
{type, text, [not_null]}, {type, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]},
{instance, integer, [not_null]},
{tick, integer, [not_null]}, {tick, integer, [not_null]},
{state, text, [not_null]}], {state, text, [not_null]}],
[{primary_key, [division, type, number, instance, tick]}, [{primary_key, [division, type, number, tick]},
{foreign_key, {[division, type, number, instance], matches, [division, type, number, instance], "ON DELETE CASCADE"}}]), {foreign_key, {[division, type, number], matches, [division, type, number], "ON DELETE CASCADE"}}]),
ok = sqlite3:create_table(Database, alliances, [{division, integer, [not_null]}, ok = sqlite3:create_table(Database, alliances, [{division, text, [not_null]},
{number, integer, [not_null]}, {number, integer, [not_null]}],
{team1, text, [not_null]},
{team2, text, [not_null]}],
[{primary_key, [division, number]}, [{primary_key, [division, number]},
{foreign_key, {[team1], teams, [team], "ON DELETE CASCADE"}}, {foreign_key, {[division], divisions, [division], "ON DELETE CASCADE"}}]),
{foreign_key, {[team2], teams, [team], "ON DELETE CASCADE"}}]),
ok = sqlite3:create_table(Database, rankings, [{division, integer, [not_null]}, ok = sqlite3:create_table(Database, alliance_members, [{division, text, [not_null]},
{rank, integer, [not_null]}, {number, integer, [not_null]},
{anum, integer, [not_null]},
{team, text, [not_null]}], {team, text, [not_null]}],
[{primary_key, [division, rank]}, [{primary_key, [division, number, anum]},
{foreign_key, {[division], divisions, [division], "ON DELETE CASCADE"}}, {foreign_key, {[division, number], alliances, [division, number], "ON DELETE CASCADE"}},
{foreign_key, {[team], teams, [team], "ON DELETE CASCADE"}}]), {foreign_key, {[team], teams, [team], "ON DELETE CASCADE"}}]),
ok = sqlite3:create_table(Database, winners, [{division, text, [not_null]},
{alliance, integer, [not_null]}],
[{primary_key, [division]},
{foreign_key, {[division, alliance], alliances, [division, number], "ON DELETE CASCADE"}}]),
ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]}, ok = sqlite3:create_table(Database, skills_scores, [{team, text, [not_null]},
{type, text, [not_null]}, {type, text, [not_null]},
{attempt, integer, [not_null]}, {attempt, integer, [not_null]},
@ -163,21 +195,21 @@ add_teams(Database, Teams) ->
assign_divisions(Database, Teams) -> assign_divisions(Database, Teams) ->
SQL = lists:append(["BEGIN; ", SQL = lists:append(["BEGIN; ",
lists:append([io_lib:format("UPDATE teams SET division = ~p WHERE team = '~s'; ", [Division, Team]) || {Team, Division} <- Teams]), lists:append([io_lib:format("UPDATE teams SET division = '~s' WHERE team = '~s'; ", [Division, Team]) || {Team, Division} <- Teams]),
"COMMIT;"]), "COMMIT;"]),
first_error(sqlite3:sql_exec_script(Database, SQL)). first_error(sqlite3:sql_exec_script(Database, SQL)).
write_division_matches(Database, Division, Type, Matches) -> write_division_matches(Database, Division, Type, Matches) ->
Round = atom_to_list(Type), Round = atom_to_list(Type),
SQL = lists:append(["BEGIN; ", SQL = lists:append(["BEGIN; ",
io_lib:format("DELETE FROM matches WHERE division = ~p AND type = '~s';", io_lib:format("DELETE FROM matches WHERE division = '~s' AND type = '~s';",
[Division, Round]), [Division, Round]),
lists:append([io_lib:format( lists:append([io_lib:format(
"INSERT INTO matches(division, type, number, instance) VALUES (~p, '~s', ~p, 1); "INSERT INTO matches(division, type, number) VALUES ('~s', '~s', ~p);
INSERT INTO match_teams(division, type, number, instance, side, anum, team) VALUES (~p, '~s', ~p, 1, '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, instance, side, anum, team) VALUES (~p, '~s', ~p, 1, '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, instance, side, anum, team) VALUES (~p, '~s', ~p, 1, '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, instance, side, anum, team) VALUES (~p, '~s', ~p, 1, 'blue', 2, '~s');", INSERT INTO match_teams(division, type, number, side, anum, team) VALUES ('~s', '~s', ~p, 'blue', 2, '~s');",
[Division, Round, N, Division, Round, N, B1, Division, Round, N, B2, Division, Round, N, R1, Division, Round, N, R2]) [Division, Round, N, Division, Round, N, B1, Division, Round, N, B2, Division, Round, N, R1, Division, Round, N, R2])
|| {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]), || {N, [B1, B2, R1, R2]} <- lists:enumerate(Matches)]),
"COMMIT;"]), "COMMIT;"]),
@ -187,17 +219,14 @@ index_of(Item, List) -> index_of(Item, List, 1).
index_of(Item, [Element | _], N) when Item == Element -> N; index_of(Item, [Element | _], N) when Item == Element -> N;
index_of(Item, [_ | List], N) -> index_of(Item, List, N+1). index_of(Item, [_ | List], N) -> index_of(Item, List, N+1).
first_empty([Num1, Num2 | Nums]) when Num2 == (Num1 + 1) -> first_empty([Num2 | Nums]);
first_empty([Num | _]) -> Num + 1.
get_column(_, [], _) -> []; get_column(_, [], _) -> [];
get_column(_, _, []) -> []; get_column(_, _, []) -> [];
get_column(Name, Columns, Rows) -> get_column(Name, Columns, Rows) ->
Index = index_of(Name, Columns), Index = index_of(Name, Columns),
[element(Index, Row) || Row <- Rows]. [element(Index, Row) || Row <- Rows].
handle_call(get_tick_start, _, State) -> handle_call({time_sync, T0}, _, State) ->
{reply, State#state.tick_start, State}; {reply, {T0, get_tick(State)}, State};
handle_call({new_db, DatabaseFile}, _, State) -> handle_call({new_db, DatabaseFile}, _, State) ->
ok = if State#state.database =:= none -> ok; ok = if State#state.database =:= none -> ok;
true -> sqlite3:close(State#state.database) true -> sqlite3:close(State#state.database)
@ -218,6 +247,52 @@ handle_call({load_db, DatabaseFile}, _, State) ->
end, end,
open_db(DatabaseFile) open_db(DatabaseFile)
end, end,
% Publish division/{division} with fields
[_, {rows, Divisions}] = sqlite3:sql_exec(Database, "SELECT division FROM divisions;"),
[_, {rows, DivisionFields}] = sqlite3:sql_exec(Database, "SELECT field, division FROM fields WHERE division IS NOT NULL;"),
DivisionMessages = [{list_to_binary(io_lib:format("division/~s", [Name])),
jsone:encode(#{<<"fields">> =>
lists:filtermap(fun({Field, FieldDivision}) ->
if FieldDivision == Name ->
{'true', Field};
true -> false
end
end, DivisionFields)
})}
|| {Name} <- Divisions],
% Publish team/{team} with inspection
% Publish division/{division}/teams with team list
[_, {rows, Teams}] = sqlite3:sql_exec(Database, "SELECT team, division, inspection FROM teams"),
TeamsMessages = [{list_to_binary(io_lib:format("team/~s", [Team])),
jsone:encode(#{<<"inspection">> => Inspection})}
|| {Team, _, Inspection} <- Teams],
DivisionTeams = [{list_to_binary(io_lib:format("division/~s/teams", [Name])),
jsone:encode(lists:filtermap(fun({Team, Div, _}) ->
if Div == Name ->
{'true', Team};
true ->
false
end
end, Teams))}
|| {Name} <- Divisions],
[_, {rows, Alliances}] = sqlite3:sql_exec(Database, "SELECT team, div"),
% Read matches and match_teams tables and publish to match/{div}/{round}/{number}
% Read match_sates and publish to match/{div}/{round}/{number}/state
% Read match_scores and publish to match/{div}/{round}/{number}/state
% Read rankings and publish divisions/{div}/rankings
% Read skills scores and publish team/{team}/skills
% Read alliances and publish to division/{div}/alliances
% DEBUG PRINTS
io:fwrite("DIVISIONS: ~p~n", [DivisionMessages]),
io:fwrite("TEAMS: ~p~n", [TeamsMessages]),
io:fwrite("INSPECTION: ~p~n", [DivisionTeams]),
{reply, ok, State#state{database_file = DatabaseFile, database = Database, tick_start = TickStart}}; {reply, ok, State#state{database_file = DatabaseFile, database = Database, tick_start = TickStart}};
handle_call(close_db, _, State) -> handle_call(close_db, _, State) ->
if State#state.database =:= none -> {reply, ok, State}; if State#state.database =:= none -> {reply, ok, State};
@ -234,8 +309,10 @@ handle_call({assign_divisions, Teams}, _, State) ->
{reply, ok, State}; {reply, ok, State};
handle_call(split_teams, _, State) -> handle_call(split_teams, _, State) ->
Teams = [X||{_,X} <- lists:sort([{rand:uniform(), N} || N <- get_teams(State#state.database)])], Teams = [X||{_,X} <- lists:sort([{rand:uniform(), N} || N <- get_teams(State#state.database)])],
[{columns, _}, {rows, Divisions}] = sqlite3:sql_exec(State#state.database, "SELECT division FROM divisions;"), [{columns, Cols}, {rows, Rows}] = sqlite3:sql_exec(State#state.database, "SELECT division FROM divisions;"),
Assignments = [{Team, (I rem length(Divisions)) + 1} || {I, Team} <- lists:enumerate(Teams)], Divisions = get_column("division", Cols, Rows),
Assignments = [{Team, lists:nth((I rem length(Divisions)) + 1, Divisions)}
|| {I, Team} <- lists:enumerate(Teams)],
ok = assign_divisions(State#state.database, Assignments), ok = assign_divisions(State#state.database, Assignments),
{reply, ok, State}; {reply, ok, State};
handle_call({delete_teams, Removed}, _, State) -> handle_call({delete_teams, Removed}, _, State) ->
@ -253,31 +330,38 @@ handle_call({match_score, Division, Round, Number, Instance, Red, Blue, Score},
end, end,
ok = first_error(sqlite3:sql_exec_script(State#state.database, lists:append( ok = first_error(sqlite3:sql_exec_script(State#state.database, lists:append(
[io_lib:format("INSERT INTO match_scores(division, type, number, instance, history, red, blue, score) VALUES(~p, '~s', ~p, ~p, ~p, ~p, ~p, '~s');", [Division, atom_to_list(Round), Number, Instance, NextInstance, Red, Blue, Score]), [io_lib:format("INSERT INTO match_scores(division, type, number, instance, history, red, blue, score) VALUES(~p, '~s', ~p, ~p, ~p, ~p, ~p, '~s');", [Division, atom_to_list(Round), Number, Instance, NextInstance, Red, Blue, Score]),
io_lib:format("INSERT INTO match_states(division, type, number, instance, tick, state) VALUES(~p, '~s', ~p, ~p, ~p, '~s');", [Division, atom_to_list(Round), Number, Instance, get_tick(State), "scored"])]))), io_lib:format("INSERT INTO match_states(division, type, number, tick, state) VALUES(~p, '~s', ~p, ~p, '~s');", [Division, atom_to_list(Round), Number, get_tick(State), "scored"])]))),
{reply, ok, State}; {reply, ok, State};
handle_call({match_state, Division, Round, Number, Instance, MatchState}, _, State) -> handle_call({match_state, Division, Round, Number, MatchState}, _, State) ->
{rowid, _} = sqlite3:sql_exec(State#state.database, {rowid, _} = sqlite3:sql_exec(State#state.database,
io_lib:format("INSERT INTO match_states(division, type, number, instance, tick, state) VALUES(~p, '~s', ~p, ~p, ~p, '~s');", io_lib:format("INSERT INTO match_states(division, type, number, tick, state) VALUES(~p, '~s', ~p, ~p, '~s');",
[Division, atom_to_list(Round), Number, Instance, get_tick(State), MatchState])), [Division, atom_to_list(Round), Number, get_tick(State), MatchState])),
{reply, ok, State}; {reply, ok, State};
handle_call(add_division, _, State) -> handle_call({add_division, Name}, _, State) ->
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(State#state.database, "SELECT division FROM divisions;"), {rowid, _} = sqlite3:sql_exec(
Divisions = lists:sort(get_column("division", Columns, Rows)), State#state.database,
NextDivision = first_empty([0 | Divisions]), io_lib:format("INSERT INTO divisions(division) VALUES('~s');",
{rowid, _} = sqlite3:sql_exec(State#state.database, [Name])),
io_lib:format("INSERT INTO divisions(division) VALUES(~p);",
[NextDivision])),
{reply, ok, State}; {reply, ok, State};
handle_call({delete_division, Division}, _, State) -> handle_call({delete_division, Division}, _, State) ->
ok = first_error(sqlite3:sql_exec_script(State#state.database, io_lib:format("UPDATE teams SET division = NULL WHERE division = ~p; DELETE FROM divisions WHERE division = ~p;", [Division, Division]))), ok = first_error(sqlite3:sql_exec_script(
State#state.database,
io_lib:format(
"UPDATE teams SET division = NULL WHERE division = '~s';
DELETE FROM divisions WHERE division = '~s';",
[Division, Division]))),
{reply, ok, State}; {reply, ok, State};
handle_call({generate_division, Division, Round, Size, Seed}, _, State) -> handle_call({generate_division, Division, Round, Size}, _, State) ->
Teams = get_div_teams(State#state.database, Division), Teams = get_div_teams(State#state.database, Division),
io:fwrite("Generating division ~p/~s with teams ~p, size ~p, and seed ~p~n", [Division, Round, Teams, Size, Seed]), Seed = rand:uniform(10000000),
Matches = schedule:create(Seed, Size, Teams), io:fwrite("Generating division ~s/~s with teams ~p, size ~p, and seed ~p~n", [Division, Round, Teams, Size, Seed]),
ok = write_division_matches(State#state.database, Division, Round, Matches), case schedule:create(Seed, Size, Teams) of
{reply, ok, State}. error -> {reply, error, State};
Matches -> {reply, write_division_matches(State#state.database, Division, Round, Matches), State}
end;
handle_call(_, _, State) ->
{reply, nofunc, State}.
get_div_teams(Database, Division) -> get_div_teams(Database, Division) ->
[{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(Database, io_lib:format("SELECT team FROM teams WHERE division = ~p;", [Division])), [{columns, Columns}, {rows, Rows}] = sqlite3:sql_exec(Database, io_lib:format("SELECT team FROM teams WHERE division = ~p;", [Division])),
@ -292,4 +376,14 @@ handle_cast(_, State) ->
get_tick(State) -> erlang:monotonic_time(millisecond) - State#state.tick_start. get_tick(State) -> erlang:monotonic_time(millisecond) - State#state.tick_start.
get_tick() -> get_tick() ->
erlang:monotonic_time(millisecond) - gen_server:call(?MODULE, get_tick_start). T0 = erlang:monotonic_time(millisecond),
{T0, T1} = gen_server:call(?MODULE, {time_sync, T0}),
T2 = erlang:monotonic_time(millisecond),
Offset = trunc(((T1-T0) + (T1-T2))/2),
NetworkDelay = trunc((T2-T0)/2),
ServerTick = T2 + Offset + NetworkDelay,
{Offset, ServerTick}.
terminate(_, State) ->
DB = State#state.database,
ok = if DB =:= none -> ok; true -> sqlite3:close(DB) end.

@ -12,7 +12,7 @@
-export([init/1]). -export([init/1]).
start_link(DBPath) -> start_link(DBPath) ->
supervisor:start_link(?MODULE, [DBPath]). supervisor:start_link({local, ?MODULE}, ?MODULE, [DBPath]).
%% sup_flags() = #{strategy => strategy(), % optional %% sup_flags() = #{strategy => strategy(), % optional
%% intensity => non_neg_integer(), % optional %% intensity => non_neg_integer(), % optional
@ -25,7 +25,8 @@ start_link(DBPath) ->
%% modules => modules()} % optional %% modules => modules()} % optional
init(DBPath) -> init(DBPath) ->
SupFlags = #{strategy => rest_for_one}, SupFlags = #{strategy => one_for_one,
intensity => 3},
ChildSpecs = [ ChildSpecs = [
#{id => schedule, #{id => schedule,
start => {schedule, start_link, []} start => {schedule, start_link, []}

@ -3,6 +3,7 @@
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/0, init/1, handle_cast/2, handle_info/2, handle_call/3]). -export([start_link/0, init/1, handle_cast/2, handle_info/2, handle_call/3]).
-export([publish/2, publish/3]).
start_link() -> start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
@ -12,12 +13,18 @@ init(_) ->
{ok, Props} = emqtt:connect(PID), {ok, Props} = emqtt:connect(PID),
{ok, {PID, Props}}. {ok, {PID, Props}}.
handle_info(Request, State) -> handle_info(_, State) ->
io:fwrite("~p~n", [Request]),
{noreply, State}. {noreply, State}.
handle_cast(_, State) -> handle_cast(_, State) ->
{noreply, State}. {noreply, State}.
handle_call(_, _, State) -> publish(Topic, Payload) ->
{noreply, State}. gen_server:call(?MODULE, {publish, Topic, Payload, []}).
publish(Topic, Payload, Options) ->
gen_server:call(?MODULE, {publish, Topic, Payload, Options}).
handle_call({publish, Topic, Payload, Options}, _, {PID, Props}) ->
ok = emqtt:publish(PID, Topic, Payload, Options),
{reply, ok, {PID, Props}}.

@ -50,8 +50,12 @@ create_schedule(Seed, Size, Teams) ->
TeamsPadded = TeamsRepeated ++ lists:duplicate(padding(length(Teams)*Size), padding), TeamsPadded = TeamsRepeated ++ lists:duplicate(padding(length(Teams)*Size), 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). case pick_matches(TeamsRandom) of
error -> error;
Matches -> fill_padding_schedule(Matches, Teams)
end.
pick_teams(_, _, _, 0) -> error;
pick_teams(Teams, Picked, 0, Safety) when Safety > 0 -> {Teams, Picked}; pick_teams(Teams, Picked, 0, Safety) when Safety > 0 -> {Teams, Picked};
pick_teams([Team | Teams], Picked, N, Safety) when Safety > 0 -> pick_teams([Team | Teams], Picked, N, Safety) when Safety > 0 ->
Duplicate = lists:member(Team, Picked), Duplicate = lists:member(Team, Picked),
@ -59,11 +63,13 @@ pick_teams([Team | Teams], Picked, N, Safety) when Safety > 0 ->
true -> pick_teams(Teams, [Team | Picked], N-1, Safety) true -> pick_teams(Teams, [Team | Picked], N-1, Safety)
end. end.
pick_teams(Teams, N) -> pick_teams(Teams, [], N, 1000). pick_teams(Teams, N) -> pick_teams(Teams, [], N, 4*length(Teams)).
pick_matches([], Matches) -> Matches; pick_matches([], Matches) -> Matches;
pick_matches(Teams, Matches) -> pick_matches(Teams, Matches) ->
{NewTeams, Match} = pick_teams(Teams, 4), case pick_teams(Teams, 4) of
pick_matches(NewTeams, [Match | Matches]). {NewTeams, Match} -> pick_matches(NewTeams, [Match | Matches]);
error -> error
end.
pick_matches(Teams) -> pick_matches(Teams, []). pick_matches(Teams) -> pick_matches(Teams, []).