"use strict"; /** * @module utab * @author taulukointipalvelut@gmail.com (nswa17) * @file Interfaces for UTab core. Github Page is [here]{@link https://github.com/taulukointipalvelut/utab-core}. * @version 2.0 * @example * var utab = require('./utab.js') * * var t1 = new utab.Tournament({name: 't1', style: 'NA'})//create a tournament 't1' * t1.teams.read().then(console.log)//show all teams * * t1.close()//close connection to t1 database */ const loggers = require('./src/general/loggers.js')//Must be the first to require loggers.init() const alloc = require('./src/allocations.js') const res = require('./src/results.js') const checks = require('./src/checks.js') const controllers = require('./src/controllers.js') const _ = require('underscore/underscore.js') /** * Represents a pair/set of teams in a venue. A minimum unit to be an allocation. * @typedef Square * @property {Number} id id of the Square * @property {Number[]} teams teams in the Square * @property {Number[]} chairs chairs in the Square * @property {Number[]} panels adjudicators(panels) in the Square * @property {Number[]} trainees adjudicators(trainees) in the Square * @property {String[]} warnings warnings * @property {Number} venue */ /** * Represents a team. * @typedef Team * @property {Number} id id of the Team * @property {String} name name of the Team * @property {Boolean} available available * @property {Object} user_defined_data user defined data */ /** * Represents an adjudicator. * @typedef Adjudicator * @property {Number} id id of the Adjudicator * @property {Number} preev pre evaluation(judge test) of the Adjudicator * @property {String} name name of the Adjudicator * @property {Boolean} available available * @property {Object} user_defined_data user defined data */ /** * Represents a venue. * @typedef Venue * @property {Number} id id of the Venue * @property {Number} priority priority of the Venue * @property {String} name name of the Venue * @property {Boolean} available available * @property {Object} user_defined_data user defined data */ /** * Represents an institution. * @typedef Institution * @property {Number} id id of the Institution * @property {String} name name of the Institution * @property {Boolean} available available * @property {Object} user_defined_data user defined data */ /** * Represents raw team result. * @typedef RawTeamResult * @property {Number} id id of the team to evaluate * @property {Number} from_id id of the sender * @property {Number} r round number at which the result is sent * @property {Number} win in NA it's either 1(win) or 0(lose), in BP it's the win-points * @property {Number[]} opponents opponents of the team * @property {String} side side of the team * @property {Object} user_defined_data user defined data * @example * { * id: 1, * from_id: 2, * r: 1, * win: 1, * opponents: [2], * side: "gov" * } */ /** * Represents raw debater result. * @typedef RawDebaterResult * @property {Number} id id of the debater to evaluate * @property {Number} from_id id of the sender * @property {Number} r round number at which the result is sent * @property {Number[]} scores scores the sender writes * @property {Object} user_defined_data user defined data * @example * { * id: 1, * from_id: 2, * r: 1, * scores: [75, 0, 36.5] * } */ /** * Represents raw adjudicator result. * @typedef RawAdjudicatorResult * @property {Number} id id of the adjudicator to evaluate * @property {Number} from_id id of the sender * @property {Number} r round number at which the result is sent * @property {Number} score the score of the adjudicator the sender writes * @property {Number[]} watched_teams teams the adjudicator watched * @property {String} comment the comment for the adjudicator from the sender * @property {Object} user_defined_data user defined data */ /** * Represents debate style. * @typedef Style * @property {String} name style name * @property {Number} debater_num_per_team number of debaters per team * @property {Number} team_num number of team in a [Square]{@link Square} * @property {Number[]} score_weights weights of the scores * @property {Number} replies candidates of replies (Necessary only for testing) * @property {Number} reply_num number of replies in a [Square]{@link Square} (Necessary only for testing) * @example * { * name: "ASIAN", * debater_num_per_team: 3, * team_num: 2, * score_weights: [1, 1, 1, 0.5], * replies: [0, 1], * reply_num: 1 * } */ /** * Represents a tournament. * @typedef TournamentInformation * @property {Number} id id of the tournament * @property {String} name name of the tournament * @property {Number} current_round_num current round * @property {Number} total_round_num total round * @property {Style} style style of the tournament * @property {Object} user_defined_data user defined data */ /** * A class to operate a tournament. */ class Tournament { /** * @param {Object} dict * @param {Number} dict.id tournament id * @param {String} [dict.name] tournament name * @param {Style} [dict.style] debate style */ constructor (dict) { var con = new controllers.CON(dict) var utab = this /** * Provides Interfaces related to teams * @memberof Tournament * @namespace Tournament.teams */ this.teams = con.teams /** * returns all teams(No side effect) * @name Tournament.teams.read * @memberof! Tournament.teams * @function Tournament.teams.read * @return {Promise.<Team[]>} Teams */ /** * creates team.//TESTED// * Attention: It throws an error if the specified team already exists. * @name Tournament.teams.create * @memberof! Tournament.teams * @function Tournament.teams.create * @param team * @param {Number} team.id id of the team to create * @param {Number} [team.name=""] name of the team to create * @param {Number} [team.available=true] id of the team to create * @param {Boolean} [force=false] if true, it creates the specified team regardless of wheter the name specified already exists * @return {Promise.<Team>} Created team * @throws {Promise} AlreadyExists */ /** * deletes specified team.//TESTED// * Attention: It throws an error if the specified team does not exist. * @name Tournament.teams.delete * @memberof! Tournament.teams * @function Tournament.teams.delete * @param team * @param {Number} team.id id of the team to delete * @return {Promise.<Team>} Deleted team * @throws {Promise} DoesNotExist */ /** * finds on specified condition(No side effect)//TESTED// * @name Tournament.teams.find * @memberof! Tournament.teams * @function Tournament.teams.find * @param team * @param {Number} [team.id] id of the team to find * @param {Number} [team.name] name of the team to find * @param {Number} [team.available] id of the team to find * @return {Promise.<Team[]>} Teams */ /** * updates specified team//TESTED// * Attention: It throws an error if the specified team does not exist. * @name Tournament.teams.update * @memberof! Tournament.teams * @function Tournament.teams.update * @param team * @param {Number} team.id id of the team to update * @param {Number} [team.name=""] name of the team to update * @param {Number} [team.available=true] id of the team to update * @return {Promise.<Team>} Updated team * @throws DoesNotExist */ /** * checks whether specified team exists * @name Tournament.teams.update * @memberof! Tournament.teams * @function Tournament.teams.update * @param team * @param {Number} [team.id] id of the team to update * @param {Number} [team.name] name of the team to update * @param {Number} [team.available] id of the team to update * @return {Promise.<Boolean>} * @throws DoesNotExist */ /** * @namespace Tournament.teams.results * @memberof Tournament.teams */ /** * reads all raw team results(No side effect) * @name Tournament.teams.results.read * @memberof! Tournament.teams.results * @function Tournament.teams.results.read * @returns {Promise.<RawTeamResult[]>} */ /** * Summarizes team results(No side effect) * @alias Tournament.teams.results.organize * @memberof! Tournament.teams.results * @param {(Number | Number[])} r_or_rs round number(s) used to summarize results * @param options [options] for summarization * @param {Boolean} [options.simple=false] only use team results. No debater results is considered thus unable to output team points * @param {Boolean} [options.force=false] if true, it does not check raw results(not recommended) * @return {Promise} summarized team results */ this.teams.results.organize = function(r_or_rs, {simple: simple=false, force: force=false}={}) { loggers.results('teams.results.organize is called') loggers.results('debug', 'arguments are: '+JSON.stringify(arguments)) if (simple) { return Promise.all([con.teams.read(), con.teams.results.read()]).then(function (vs) { var [teams, raw_team_results] = vs var team_num = round_info.style.team_num if (!force) { Array.isArray(r_or_rs) ? r_or_rs.map(r => checks.results.teams.check(raw_team_results, teams, r, team_num)) : checks.results.teams.check(raw_team_results, teams, r_or_rs, team_num) } return Array.isArray(r_or_rs) ? res.teams.simplified_compile(teams, raw_team_results, r_or_rs, round_info.style) : res.teams.simplified_summarize(teams, raw_team_results, r_or_rs, round_info.style) }) } else { return Promise.all([con.teams.read(), con.teams.debaters.read(), con.teams.debaters.read(), con.teams.results.read(), con.debaters.results.read(), con.teams.debaters.read(), con.rounds.read()]).then(function (vs) { var [teams, debaters, teams_to_debaters, raw_team_results, raw_debater_results, teams_to_debaters, round_info] = vs var team_num = round_info.style.team_num if (!force) { checks.results.check(teams, teams_to_debaters, debaters, team_num) if (Array.isArray(r_or_rs)) { r_or_rs.map(r => checks.results.teams.check(raw_team_results, teams, r, team_num)) r_or_rs.map(r => checks.results.debaters.check(raw_debater_results, debaters, r, team_num)) } else { checks.results.teams.check(raw_team_results, teams, r_or_rs, team_num) checks.results.debaters.check(raw_debater_results, debaters, r_or_rs, team_num) } } return Array.isArray(r_or_rs) ? res.teams.compile(teams, debaters, teams_to_debaters, raw_team_results, raw_debater_results, r_or_rs, round_info.style) : res.teams.summarize(teams, debaters, teams_to_debaters, raw_team_results, raw_debater_results, r_or_rs, round_info.style) }) } } /** * Interfaces related to teams to debaters * @namespace Tournament.teams.debaters * @memberof Tournament.teams */ /** * returns teams to debaters(No side effect) * @name Tournament.teams.debaters.read * @memberof! Tournament.teams.debaters * @function Tournament.teams.debaters.read * @return {Promise} Teams to debaters */ /** * sets debaters to a team. * Attention: It throws an error if the specified team has debaters. * @name Tournament.teams.debaters.create * @memberof! Tournament.teams.debaters * @function Tournament.teams.debaters.create * @param dict * @param {Number} dict.id id of the team to set debaters * @param {Number[]} dict.debaters debaters to set * @param {Number} dict.r round where the team has the debaters * @return {Promise} Created team * @throws {Promise} AlreadyExists */ /** * deletes debaters from specified team. * Attention: It throws an error if the specified team does not exist. * @deprecated * @name Tournament.teams.debaters.delete * @memberof! Tournament.teams.debaters * @function Tournament.teams.debaters.delete * @param options * @param {Number} options.id id of the team to delete * @param {Number} options.r round where the team has the debaters * @return {Promise} Team * @throws {Promise} DoesNotExist */ /** * finds on specified condition(No side effect) * @name Tournament.teams.debaters.find * @memberof! Tournament.teams.debaters * @function Tournament.teams.debaters.find * @param options * @param {Number} [options.id] id of the team to delete * @param {Number} [options.r] round where the team has the debaters * @param {Number[]} [options.debaters] debaters * @return {Promise} Teams */ /** * updates debaters of specified team * Attention: It throws an error if the specified team does not exist. * @name Tournament.teams.debaters.update * @memberof! Tournament.teams.debaters * @function Tournament.teams.debaters.update * @param options * @param {Number} options.id id of the team * @param {Number} options.r round where the team has the debaters * @param {Number[]} options.debaters debaters of the team * @return {Promi} Team * @throws DoesNotExist */ /** * Provides interefaces related to teams to institutions */ /** * Provides interfaces related to adjudicators * @namespace Tournament.adjudicators * @memberof Tournament */ this.adjudicators = con.adjudicators /** * @namespace Tournament.adjudicators.results * @memberof Tournament.adjudicators */ /** * Summarizes adjudicator results(No side effect) * @alias Tournament.adjudicators.results.organize * @memberof! Tournament.adjudicators.results * @param {(Number | Number[])} r_or_rs round number(s) used to summarize results * @param [options] * @param {Boolean} [options.force=false] if true, it does not check raw results(not recommended) * @return {Promise} summarized adjudicator results */ this.adjudicators.results.organize = function(r_or_rs, {force: force=false}={}) { loggers.results('adjudicators.results.organize is called') loggers.results('debug', 'arguments are: '+JSON.stringify(arguments)) return Promise.all([con.adjudicators.read(), con.adjudicators.results.read(), con.rounds.get()]).then(function(vs) { var [adjudicators, raw_adjudicator_results, round_info] = vs var team_num = round_info.style.team_num if (!force) { Array.isArray(r_or_rs) ? r_or_rs.map(r => checks.results.adjudicators.check(raw_adjudicator_results, adjudicators, r, team_num)) : checks.results.adjudicators.check(raw_adjudicator_results, adjudicators, r_or_rs, team_num) } return Array.isArray(r_or_rs) ? res.adjudicators.compile(adjudicators, raw_adjudicator_results, r_or_rs) : res.adjudicators.summarize(adjudicators, raw_adjudicator_results, r_or_rs) }) } /** * Interfaces related to tournament operation * @namespace Tournament.rounds * @memberof Tournament */ this.rounds = con.rounds /** * Interfaces related to venues * @namespace Tournament.venues * @memberof Tournament */ this.venues = con.venues /** * Interfaces related to debaters * @namespace Tournament.debaters * @memberof Tournament */ this.debaters = con.debaters /** * Interfaces related to debater results * @namespace Tournament.debaters.results * @memberof Tournament.debaters */ /** * Summarizes debater results(No side effect) * @alias Tournament.debaters.results.organize * @memberof! Tournament.debaters.results * @param {(Number | Number[])} r_or_rs round number(s) used to summarize results * @param [options] * @param {Boolean} [options.force=false] if true, it does not check raw results(not recommended) * @return {Promise} summarized debater results */ this.debaters.results.organize = function(r_or_rs, {force: force=false}={}) { loggers.results('debaters.results.organize is called') loggers.results('debug', 'arguments are: '+JSON.stringify(arguments)) return Promise.all([con.debaters.read(), con.debaters.results.read(), con.rounds.read()]).then(function(vs) { var [debaters, raw_debater_results, round_info] = vs var team_num = round_info.style.team_num if (!force) { Array.isArray(r_or_rs) ? r_or_rs.map(r => checks.results.debaters.check(raw_debater_results, debaters, r, team_num)) : checks.results.debaters.check(raw_debater_results, debaters, r_or_rs, team_num) } return Array.isArray(r_or_rs) ? res.debaters.compile(debaters, raw_debater_results, round_info.style, r_or_rs) : res.debaters.summarize(debaters, raw_debater_results, round_info.style, r_or_rs) }) } /*/** * checks debater results are all gathered * @alias Tournament.debaters.results.check * @memberof! Tournament.debaters.results * @throws error */ //this.debaters.results.check = checks.debaters.check /** * Interfaces related to institutions * @namespace Tournament.institutions * @memberof Tournament */ this.institutions = con.institutions /** * Provides interfaces related to allocations * @namespace Tournament.allocations * @memberof Tournament */ this.allocations = con.allocations this.allocations.check = function() { loggers.allocations('allocations.check is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) return Promise.all([con.teams.read(), con.adjudicators.read(), con.venues.read(), con.institutions.read(), utab.teams.results.organize(considering_rounds), utab.adjudicators.results.organize(considering_rounds), con.teams.institutions.read(), con.adjudicators.institutions.read(), con.adjudicators.conflicts.read()]).then(function (vs) { var [teams, adjudicators, venues, institutions, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts] = vs checks.allocations.check(teams, adjudicators, venues, institutions, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts, round_info.style, current_round_num) }) }) } /** * Provides interfaces related to team allocation * @namespace Tournament.allocations.teams * @memberof Tournament.allocations */ this.allocations.teams = { //@param {String[]} [options.adjudicator_filters=['by_bubble', 'by_strength', 'by_attendance', 'by_conflict', 'by_institution', 'by_past']] filters on computing adjudicator allocation //@param {Square[]} [options.allocation] if specified, adjudicator/venue allocation will be created based on the allocation /** * get allocation(No side effect) * @alias Tournament.allocations.teams.get * @memberof! Tournament.allocations.teams * @param {Object} [options] * @param {Boolean} [options.simple=false] if true, it does not use debater results * @param {String[]} [options.filters=['by_strength', 'by_side', 'by_past_opponent', 'by_institution']] filters to compute team allocation in `standard` algorithm * @param {Boolean} [options.force=false] if true, it does not check the database before creating matchups. (false recommended) * @param {String} [options.algorithm='standard'] it computes the allocation using specified algorithm * @return {Promise.<Square[]>} allocation */ get: function({ simple: simple = false, filters: filters=['by_strength', 'by_side', 'by_past_opponent', 'by_institution'], force: force=false, // ignores warnings from processing results algorithm: algorithm = 'standard' }={}) { loggers.allocations('allocations.teams.get is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) var team_num = round_info.style.team_num return Promise.all([con.teams.read(), utab.teams.results.organize(considering_rounds, {simple: simple, force: force}), con.teams.institutions.read()]).then(function (vs) { var [teams, compiled_team_results, teams_to_institutions] = vs var allocation = algorithm === 'standard' ? alloc.standard.teams.get(teams, compiled_team_results, teams_to_institutions, filters, round_info) : alloc.wudc.teams.get(teams, compiled_team_results, round_info) var new_allocation = checks.allocations.teams.check(allocation, teams, compiled_team_results, teams_to_institutions, team_num)/////// return allocation }) }) }, /** * checks allocation(No side effect) * @memberof! Tournament.allocations.teams * @function Tournament.allocations.teams.check * @param allocation * @return {Promise.<Square[]>} */ check: function(allocation) { loggers.allocations('allocations.teams.check is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) return Promise.all([con.teams.read(), teams.results.organize(considering_rounds), con.teams.institutions.read()]).then(function (vs) { var [teams, compiled_team_results, teams_to_institutions] = vs var new_allocation = checks.allocations.teams.check(allocation, teams, compiled_team_results, teams_to_institutions, team_num)/////// return new_allocation }) }) } } /** * Provides interfaces related to adjudicator allocation * @namespace Tournament.allocations.adjudicators * @memberof Tournament.allocations */ this.allocations.adjudicators = { get: function(allocation, { filters: filters=['by_bubble', 'by_strength', 'by_attendance', 'by_conflict', 'by_institution', 'by_past'], simple: simple = false, force: force = false, algorithm: algorithm = 'standard', assign: assign = 'high_to_high',// or middle_to_high, middle_to_slight, high_to_slight scatter: scatter = false, numbers: numbers = {chairs: 2, panels: 1, trainees: 1} }={}) { loggers.allocations('allocations.adjudicators.get is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) return Promise.all([con.teams.read(), con.adjudicators.read(), utab.teams.results.organize(considering_rounds, {force: force, simple: simple}), utab.adjudicators.results.organize(considering_rounds, {force: force}), con.teams.institutions.read(), con.adjudicators.institutions.read(), con.adjudicators.conflicts.read()]).then(function (vs) { var [teams, adjudicators, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts] = vs if (algorithm === 'standard') { var new_allocation = alloc.standard.adjudicators.get(allocation, adjudicators, teams, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts, filters, numbers) } else if (algorithm === 'traditional') { var new_allocation = alloc.traditional.adjudicators.get(allocation, adjudicators, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts, numbers, assign, scatter) } new_allocation = checks.allocations.adjudicators.check(allocation, adjudicators, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts) return new_allocation }) }) }, check: function(allocation) { loggers.allocations('allocations.adjudicators.check is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) return Promise.all([con.teams.read(), con.adjudicators.read(), con.venues.read(), teams.results.organize(considering_rounds), adjudicators.results.organize(considering_rounds), con.teams.institutions.read(), con.adjudicators.institutions.read(), con.adjudicators.conflicts.read()]).then(function (vs) { var [teams, adjudicators, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts] = vs var new_allocation = checks.allocations.adjudicators.check(allocation, adjudicators, compiled_team_results, compiled_adjudicator_results, teams_to_institutions, adjudicators_to_institutions, adjudicators_to_conflicts) return new_allocation }) }) } } /** * Provides interfaces related to venue allocation * @namespace Tournament.allocations.venues * @memberof Tournament.allocations */ this.allocations.venues = { /** * get venue allocation from allocation * @param {Square[]} allocation allocation * @param {Object} options * @param {Boolean} [options.shuffle=false] if true, it randomly allocates venues to squares so that no one can guess the current rankings of teams. * @return {Promise.<Square[]>} */ get: function(allocation, options={shuffle: false}) { loggers.allocations('allocations.venues.get is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) return Promise.all([con.venues.read(), con.teams.results.organize(considering_rounds)]).then(function (vs) { var [venues, compiled_team_results] = vs var new_allocation = alloc.standard.venues.get(allocation, venues, compiled_team_results, round_info, options) new_allocation = checks.allocations.venues.check(new_allocation, venues) return new_allocation }) }) }, check: function(allocation) { loggers.allocations('allocations.venues.check is called') loggers.allocations('debug', 'arguments are: '+JSON.stringify(arguments)) return con.rounds.read().then(function (round_info) { var current_round_num = round_info.current_round_num var considering_rounds = _.range(1, current_round_num) return Promise.all([con.venues.read(), con.teams.results.organize(considering_rounds)]).then(function (vs) { var [venues, compiled_team_results] = vs var new_allocation = checks.allocations.venues.check(allocation, venues) return new_allocation }) }) } } /** * closes connection to the tournament database. * @memberof! Tournament * @function Tournament.close */ this.close = con.close } } exports.Tournament = Tournament