@ -2,7 +2,7 @@
- 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 ] ) .
- record ( state , { database_file = none , database = none , tick_start = none } ) .
@ -46,37 +46,67 @@ init([DatabaseFile]) ->
end .
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 ] } ] ) ,
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 ] } ,
{ division , integer } ,
{ division , text } ,
{ inspection , blob } ] ,
[ { 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 ] } ,
{ number , integer , [ not_null ] } ,
{ instance , integer , [ not_null ] } ] ,
[ { primary_key , [ division , type , number , instance ] } ,
{ number , integer , [ not_null ] } ] ,
[ { primary_key , [ division , type , number ] } ,
{ check , " type IN ('practice', 'qualification', 'elimination', 'final') " } ,
{ 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 ] } ,
{ number , integer , [ not_null ] } ,
{ instance , integer , [ not_null ] } ,
{ side , text , [ not_null ] } ,
{ anum , integer , [ 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 , " 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 ] , " " } } ] ) ,
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 ] } ,
{ number , 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 , " 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 , 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 ] } ,
{ number , integer , [ not_null ] } ,
{ instance , integer , [ not_null ] } ,
{ tick , integer , [ not_null ] } ,
{ state , text , [ not_null ] } ] ,
[ { primary_key , [ division , type , number , instance, tick] } ,
{ foreign_key , { [ division , type , number , instance ], matches , [ division , type , number , instance ] , " ON DELETE CASCADE " } } ] ) ,
[ { 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 , integer , [ not_null ] } ,
{ number , integer , [ not_null ] } ,
{ team1 , text , [ not_null ] } ,
{ team2 , text , [ not_null ] } ] ,
ok = sqlite3 : create_table ( Database , alliances , [ { division , text , [ not_null ] } ,
{ number , integer , [ not_null ] } ] ,
[ { primary_key , [ division , number ] } ,
{ foreign_key , { [ team1 ] , teams , [ team ] , " ON DELETE CASCADE " } } ,
{ foreign_key , { [ team2 ] , teams , [ team ] , " ON DELETE CASCADE " } } ] ) ,
{ foreign_key , { [ division ] , divisions , [ division ] , " ON DELETE CASCADE " } } ] ) ,
ok = sqlite3 : create_table ( Database , rankings , [ { division , integer , [ not_null ] } ,
{ rank , integer , [ not_null ] } ,
ok = sqlite3 : create_table ( Database , alliance_members , [ { division , text , [ not_null ] } ,
{ number , integer , [ not_null ] } ,
{ anum , integer , [ not_null ] } ,
{ team , text , [ not_null ] } ] ,
[ { primary_key , [ division , rank ] } ,
{ foreign_key , { [ division ], divisions , [ division ] , " ON DELETE CASCADE " } } ,
[ { primary_key , [ division , number, anum ] } ,
{ foreign_key , { [ division , number ] , alliances , [ division , number ] , " 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 ] } ,
{ type , text , [ not_null ] } ,
{ attempt , integer , [ not_null ] } ,
@ -163,21 +195,21 @@ add_teams(Database, Teams) ->
assign_divisions ( Database , Teams ) - >
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; " ] ) ,
first_error ( sqlite3 : sql_exec_script ( Database , SQL ) ) .
write_division_matches ( Database , Division , Type , Matches ) - >
Round = atom_to_list ( Type ) ,
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 ] ) ,
lists : append ( [ io_lib : format (
" INSERT INTO matches(division, type, number , instance) VALUES (~p , ' ~s ', ~p , 1 );
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 , instance, side, anum , team ) VALUES ( ~ p , '~s' , ~ p , 1 , '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 , instance, side, anum , team ) VALUES ( ~ p , '~s' , ~ p , 1 , 'blue' , 2 , '~s' ) ; " ,
" 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' , 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' ) ; " ,
[ 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 ) ] ) ,
" 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 , [ _ | 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 ( Name , Columns , Rows ) - >
Index = index_of ( Name , Columns ) ,
[ element ( Index , Row ) | | Row < - Rows ] .
handle_call ( get_tick_start , _ , State ) - >
{ reply , State #state.tick_start , State } ;
handle_call ( { time_sync , T0 } , _ , State ) - >
{ reply , { T0 , get_tick ( State ) } , State } ;
handle_call ( { new_db , DatabaseFile } , _ , State ) - >
ok = if State #state.database =:= none - > ok ;
true - > sqlite3 : close ( State #state.database )
@ -218,6 +247,52 @@ handle_call({load_db, DatabaseFile}, _, State) ->
end ,
open_db ( DatabaseFile )
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 } } ;
handle_call ( close_db , _ , State ) - >
if State #state.database =:= none - > { reply , ok , State } ;
@ -234,8 +309,10 @@ handle_call({assign_divisions, Teams}, _, State) ->
{ reply , ok , State } ;
handle_call ( split_teams , _ , State ) - >
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; " ) ,
Assignments = [ { Team , ( I rem length ( Divisions ) ) + 1 } | | { I , Team } < - lists : enumerate ( Teams ) ] ,
[ { columns , Cols } , { rows , Rows } ] = sqlite3 : sql_exec ( State #state.database , " SELECT division FROM divisions; " ) ,
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 ) ,
{ reply , ok , State } ;
handle_call ( { delete_teams , Removed } , _ , State ) - >
@ -253,31 +330,38 @@ handle_call({match_score, Division, Round, Number, Instance, Red, Blue, Score},
end ,
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_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 } ;
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 ,
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 ) , MatchState ] ) ) ,
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 ) , MatchState ] ) ) ,
{ reply , ok , State } ;
handle_call ( add_division , _ , State ) - >
[ { columns , Columns } , { rows , Rows } ] = sqlite3 : sql_exec ( State #state.database , " SELECT division FROM divisions; " ) ,
Divisions = lists : sort ( get_column ( " division " , Columns , Rows ) ) ,
NextDivision = first_empty ( [ 0 | Divisions ] ) ,
{ rowid , _ } = sqlite3 : sql_exec ( State #state.database ,
io_lib : format ( " INSERT INTO divisions(division) VALUES( ~p ); " ,
[ NextDivision ] ) ) ,
handle_call ( { add_division , Name } , _ , State ) - >
{ rowid , _ } = sqlite3 : sql_exec (
State #state.database ,
io_lib : format ( " INSERT INTO divisions(division) VALUES(' ~s '); " ,
[ Name ] ) ) ,
{ reply , ok , 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 } ;
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 ) ,
io : fwrite ( " Generating division ~p / ~s with teams ~p , size ~p , and seed ~p ~n " , [ Division , Round , Teams , Size , Seed ] ) ,
Matches = schedule : create ( Seed , Size , Teams ) ,
ok = write_division_matches ( State #state.database , Division , Round , Matches ) ,
{ reply , ok , State } .
Seed = rand : uniform ( 10000000 ) ,
io : fwrite ( " Generating division ~s / ~s with teams ~p , size ~p , and seed ~p ~n " , [ Division , Round , Teams , Size , Seed ] ) ,
case schedule : create ( Seed , Size , Teams ) of
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 ) - >
[ { 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 ( ) - >
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 .