"Bad Request", 500 => "Internal Server Error" ); if (!isset($codes[$code])) { return; } $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; header($protocol . ' ' . $code . ' ' . $codes[$code]); } } function safe_ceil($value, $precision = 1e-6) { $ceilValue = ceil($value); return (abs($value - $ceilValue) < (1-$precision)) ? $ceilValue : round($value); } function recursive_ksort(&$array, $flags = SORT_REGULAR) { if (!is_array($array)) return false; ksort($array, $flags); foreach ($array as &$arr) { recursive_ksort($arr, $flags); } return true; } function is_integer_like($value) { return ctype_digit(strval($value)) || is_int($value); } class ParametersException extends Exception {}; function run($parameters) { $versionClasses = array( '1' => 'ApiPklV1', // RegKlas 2018.11.01 '2' => 'ApiPklV2', // RegKMP 2020.01.01 '3' => 'ApiPklV3', // local BNET 2020.05.01 '3.1' => 'ApiPklV3_1', // local BNET changes from 2021.01.10 '4.0' => 'ApiPklV4', // RegKlas 2025.01.01 (2025.02.12) '_default' => 'ApiPklV4' ); $version = isset($parameters['version']) ? $parameters['version'] : '_default'; $apiClass = isset($versionClasses[$version]) ? $versionClasses[$version] : $versionClasses['_default']; $api = new $apiClass($parameters); if (isset($parameters['tournament_rank']) && $parameters['tournament_rank'] == ApiPkl::RANK_KMP) { $result = $api->calculate_kmp_points(); } else if (isset($parameters['tournament_rank']) && $parameters['tournament_rank'] == ApiPkl::RANK_BNET) { $result = $api->calculate_bridgenet_points(); } else { $result = $api->calculate_points(); } return $result; } class ApiPkl { // constants for tournament ranks const RANK_OTXxxxx = 7; const RANK_OTXxxx = 6; const RANK_OTXxx = 5; const RANK_OTXx = 4; const RANK_OTX = 3; const RANK_REGIONAL = 2; const RANK_DISTRICT = 1; const RANK_CLUB = 0; const RANK_KMP = 101; const RANK_BNET = 102; // constants for tournament (participant) types const TYPE_INDIVIDUAL = 1; const TYPE_PAIRS = 2; const TYPE_TEAMS = 4; // default tournament weights const WEIGHT_CLUB_UNDER40 = 1; const WEIGHT_DISTRICT_UNDER40 = 2; const WEIGHT_REGIONAL_UNDER40 = 4; const WEIGHT_OTX_UNDER40 = 5; const WEIGHT_OTXx_UNDER40 = 7; const WEIGHT_OTXxx_UNDER40 = 10; const WEIGHT_OTXxxx_UNDER40 = 15; const WEIGHT_OTXxxxx_UNDER40 = 25; const WEIGHT_KMP_UNDER40 = 0; const WEIGHT_BNET_UNDER40 = 1; const WEIGHT_CLUB_OVER40 = 2; const WEIGHT_DISTRICT_OVER40 = 3; const WEIGHT_REGIONAL_OVER40 = 5; const WEIGHT_OTX_OVER40 = 7; const WEIGHT_OTXx_OVER40 = 10; const WEIGHT_OTXxx_OVER40 = 15; const WEIGHT_OTXxxx_OVER40 = 25; const WEIGHT_OTXxxxx_OVER40 = 40; const WEIGHT_KMP_OVER40 = 0; const WEIGHT_BNET_OVER40 = 1; // not 2, according to MarcinW // points for 1st place const POINTS_CLUB_UNDER40 = 0; const POINTS_DISTRICT_UNDER40 = 0; const POINTS_REGIONAL_UNDER40 = 0; const POINTS_OTX_UNDER40 = 0; const POINTS_OTXx_UNDER40 = 50; const POINTS_OTXxx_UNDER40 = 75; const POINTS_OTXxxx_UNDER40 = 150; const POINTS_OTXxxxx_UNDER40 = 200; const POINTS_KMP_UNDER40 = 0; const POINTS_BNET_UNDER40 = 0; const POINTS_CLUB_OVER40 = 0; const POINTS_DISTRICT_OVER40 = 0; const POINTS_REGIONAL_OVER40 = 0; const POINTS_OTX_OVER40 = 0; const POINTS_OTXx_OVER40 = 70; const POINTS_OTXxx_OVER40 = 100; const POINTS_OTXxxx_OVER40 = 200; const POINTS_OTXxxxx_OVER40 = 300; const POINTS_KMP_OVER40 = 0; const POINTS_BNET_OVER40 = 0; // points for 1st place are 1.25x for teams const TEAMS_BONUS = 0.25; // coefficient for number of players const PLAYERS_COEFFICIENT = 0.05; // points regression cutoff points const POINTS_CUTOFF_1 = 0.02; const POINTS_CUTOFF_1_VALUE = 0.9; const POINTS_CUTOFF_2 = 0.2; const POINTS_CUTOFF_2_VALUE = 0.2; const POINTS_CUTOFF_3 = 0.5; const POINTS_CUTOFF_3_VALUE = 0.0; // minimum for title (WK) average const MINIMUM_AVG_TITLE = 0.15; // bonus points for top KMP places const KMP_BONUS_MINIMUM_AVG_TITLE = 2.5; const KMP_BONUS_1ST = 5; const KMP_BONUS_2ND = 3; const KMP_BONUS_3RD = 1; protected $parameters; function __construct($parameters) { $parameters = $this->check_parameters($parameters); $this->parameters = $this->parse_parameters($parameters); } function ensure_parameters($parameters, $mandatory) { foreach ($mandatory as $param) { if (!isset($parameters[$param])) { throw new ParametersException( sprintf('Missing parameter: %s', $param) ); } if (!is_numeric($parameters[$param])) { throw new ParametersException( sprintf('Parameter: %s is not a numeric value (%s)', $param, $parameters[$param]) ); } } } function check_values($parameters, $tests) { foreach ($tests as $param => $test) { if (!$test($parameters[$param])) { throw new ParametersException( sprintf('Parameter: %s has incorrect value (%s)', $param, $parameters[$param]) ); } } } /* * This shouldn't alter any parameters, just validate them. * The exception is when a new version no longer marks parameter as mandatory, and assumes some default/derived values. * Or when a parameter value marks other parameters as ignored, meaning these parameters should never reach parse_parameters(). * Only then check_parameters should alter the provided parameter list, which is also why it's still passed around as an argument->return value. */ function check_parameters($parameters) { $this->ensure_parameters($parameters, array('type', 'contestants')); $this->check_values($parameters, array( 'type' => function($r) { return is_integer_like($r) && intval($r) > 0; }, 'contestants' => function($r) { return is_integer_like($r) && intval($r) > 0; } )); if (isset($parameters['players'])) { $this->check_values($parameters, array( 'players' => function($r) { return is_integer_like($r) && intval($r) > 0; } )); } if (!isset($parameters['manual']) || !isset($parameters['manual']['min_points'])) { $this->ensure_parameters($parameters, array('title_sum')); $this->check_values($parameters, array( 'title_sum' => function($r) { return floatval($r) >= 0; } )); if (!isset($parameters['manual']) || !isset($parameters['manual']['tournament_weight'])) { $this->ensure_parameters( $parameters, array('tournament_rank', 'over39_boards') ); $this->check_values($parameters, array( 'tournament_rank' => function($r) { return is_integer_like($r) && in_array(intval($r), array( ApiPkl::RANK_OTXxxxx, ApiPkl::RANK_OTXxxx, ApiPkl::RANK_OTXxx, ApiPkl::RANK_OTXx, ApiPkl::RANK_OTX, ApiPkl::RANK_REGIONAL, ApiPkl::RANK_DISTRICT, ApiPkl::RANK_CLUB, ApiPkl::RANK_KMP, ApiPkl::RANK_BNET )); }, 'over39_boards' => function($r) { return is_integer_like($r) && intval($r) >= 0 && intval($r) <= 1; } )); } else { $this->check_values($parameters['manual'], array( 'tournament_weight' => function($r) { return is_integer_like($r) && intval($r) > 0; } )); } } else { $this->check_values($parameters['manual'], array( 'min_points' => function($r) { return is_integer_like($r) && intval($r) >= 0; } )); } if (isset($parameters['manual']) && isset($parameters['manual']['players_coefficient'])) { $this->check_values($parameters['manual'], array( 'players_coefficient' => function($r) { return floatval($r) >= 0; } )); } return $parameters; } /* * This parses the parameters, transforms them and fills missing, derivable data. * When overriding, be careful when to use the original parameter list from method argument, and when to use parameter list parsed by parent::parse_parameters. * Good guideline is that when API version introduces new parameter, it must be parsed from method argument (as parent wouldn't parse it), and all other times, the return value of parent::parse_parameters is likely to be used. */ function parse_parameters($parameters) { $return = array(); $return['type'] = intval($parameters['type']); if (!in_array($return['type'], array( ApiPkl::TYPE_INDIVIDUAL, ApiPkl::TYPE_PAIRS, ApiPkl::TYPE_TEAMS ))) { throw new ParametersException( sprintf('Parameter: type has incorrect value (%s)', $return['type']) ); } if (($return['type'] != ApiPkl::TYPE_PAIRS) && isset($parameters['tournament_rank']) && $parameters['tournament_rank'] == ApiPkl::RANK_KMP) { throw new ParametersException( sprintf('Parameter: type has incorrect value (%s) for KMP tournament', $return['type']) ); } $return['contestants'] = intval($parameters['contestants']); $return['players'] = isset($parameters['players']) ? intval($parameters['players']) : intval($parameters['contestants']) * $return['type']; $return['title_sum'] = isset($parameters['title_sum']) ? floatval($parameters['title_sum']) : 0.0; $weights = array( array( ApiPkl::RANK_OTXxxxx => static::WEIGHT_OTXxxxx_UNDER40, ApiPkl::RANK_OTXxxx => static::WEIGHT_OTXxxx_UNDER40, ApiPkl::RANK_OTXxx => static::WEIGHT_OTXxx_UNDER40, ApiPkl::RANK_OTXx => static::WEIGHT_OTXx_UNDER40, ApiPkl::RANK_OTX => static::WEIGHT_OTX_UNDER40, ApiPkl::RANK_REGIONAL => static::WEIGHT_REGIONAL_UNDER40, ApiPkl::RANK_DISTRICT => static::WEIGHT_DISTRICT_UNDER40, ApiPkl::RANK_CLUB => static::WEIGHT_CLUB_UNDER40, ApiPkl::RANK_KMP => static::WEIGHT_KMP_UNDER40, ApiPkl::RANK_BNET => static::WEIGHT_BNET_UNDER40 ), array( ApiPkl::RANK_OTXxxxx => static::WEIGHT_OTXxxxx_OVER40, ApiPkl::RANK_OTXxxx => static::WEIGHT_OTXxxx_OVER40, ApiPkl::RANK_OTXxx => static::WEIGHT_OTXxx_OVER40, ApiPkl::RANK_OTXx => static::WEIGHT_OTXx_OVER40, ApiPkl::RANK_OTX => static::WEIGHT_OTX_OVER40, ApiPkl::RANK_REGIONAL => static::WEIGHT_REGIONAL_OVER40, ApiPkl::RANK_DISTRICT => static::WEIGHT_DISTRICT_OVER40, ApiPkl::RANK_CLUB => static::WEIGHT_CLUB_OVER40, ApiPkl::RANK_KMP => static::WEIGHT_KMP_OVER40, ApiPkl::RANK_BNET => static::WEIGHT_BNET_OVER40 ) ); $return['tournament_weight'] = 0; if (isset($parameters['manual']) && isset($parameters['manual']['tournament_weight'])) { $return['tournament_weight'] = intval($parameters['manual']['tournament_weight']); } else { if (isset($parameters['over39_boards']) && isset($parameters['tournament_rank'])) { $return['tournament_weight'] = $weights[intval($parameters['over39_boards'])][intval($parameters['tournament_rank'])]; } } $min_points = array( array( ApiPkl::RANK_OTXxxxx => static::POINTS_OTXxxxx_UNDER40, ApiPkl::RANK_OTXxxx => static::POINTS_OTXxxx_UNDER40, ApiPkl::RANK_OTXxx => static::POINTS_OTXxx_UNDER40, ApiPkl::RANK_OTXx => static::POINTS_OTXx_UNDER40, ApiPkl::RANK_OTX => static::POINTS_OTX_UNDER40, ApiPkl::RANK_REGIONAL => static::POINTS_REGIONAL_UNDER40, ApiPkl::RANK_DISTRICT => static::POINTS_DISTRICT_UNDER40, ApiPkl::RANK_CLUB => static::POINTS_CLUB_UNDER40, ApiPkl::RANK_KMP => static::POINTS_KMP_UNDER40, ApiPkl::RANK_BNET => static::POINTS_BNET_UNDER40 ), array( ApiPkl::RANK_OTXxxxx => static::POINTS_OTXxxxx_OVER40, ApiPkl::RANK_OTXxxx => static::POINTS_OTXxxx_OVER40, ApiPkl::RANK_OTXxx => static::POINTS_OTXxx_OVER40, ApiPkl::RANK_OTXx => static::POINTS_OTXx_OVER40, ApiPkl::RANK_OTX => static::POINTS_OTX_OVER40, ApiPkl::RANK_REGIONAL => static::POINTS_REGIONAL_OVER40, ApiPkl::RANK_DISTRICT => static::POINTS_DISTRICT_OVER40, ApiPkl::RANK_CLUB => static::POINTS_CLUB_OVER40, ApiPkl::RANK_KMP => static::POINTS_KMP_OVER40, ApiPkl::RANK_BNET => static::POINTS_BNET_OVER40 ) ); if (isset($parameters['manual']) && isset($parameters['manual']['min_points'])) { $return['min_points'] = intval($parameters['manual']['min_points']); } else { $return['min_points'] = $min_points[intval($parameters['over39_boards'])][intval($parameters['tournament_rank'])]; } $return['players_coefficient'] = static::PLAYERS_COEFFICIENT; if (isset($parameters['manual']) && isset($parameters['manual']['players_coefficient'])) { $return['players_coefficient'] = floatval($parameters['manual']['players_coefficient']); } $return['points_cutoffs'] = array( array(0.0, 1.0), array(static::POINTS_CUTOFF_1, static::POINTS_CUTOFF_1_VALUE), array(static::POINTS_CUTOFF_2, static::POINTS_CUTOFF_2_VALUE), array(static::POINTS_CUTOFF_3, static::POINTS_CUTOFF_3_VALUE) ); if (isset($parameters['manual']) && isset($parameters['manual']['points_cutoffs']) && is_array($parameters['manual']['points_cutoffs'])) { $return['points_cutoffs'] = $parameters['manual']['points_cutoffs']; } recursive_ksort($return['points_cutoffs']); if ($return['points_cutoffs'][0][0] != 0.0) { array_unshift($return['points_cutoffs'], array(0.0, 1.0)); } if ($return['points_cutoffs'][count($return['points_cutoffs'])-1][1] != 0.0) { array_push($return['points_cutoffs'], array(1.0, 0.0)); } foreach ($return['points_cutoffs'] as &$cutoff) { if (($cutoff[0] < 0.0) || ($cutoff[0] > 1.0)) { throw new ParametersException( sprintf('Cutoff points need to be between 0.0 and 1.0: %f', $cutoff[0]) ); } $cutoff[0] = floatval($cutoff[0]); if (($cutoff[1] < 0.0) || ($cutoff[1] > 1.0)) { throw new ParametersException( sprintf('Cutoff values need to be between 1.0 and 0.0: %f', $cutoff[1]) ); } $cutoff[1] = floatval($cutoff[1]); } unset($cutoff); for ($prev = 0; $prev < count($return['points_cutoffs']) - 1; $prev++) { $next = $prev + 1; if ($return['points_cutoffs'][$prev][0] >= $return['points_cutoffs'][$next][0]) { throw new ParametersException( sprintf('Cutoff points need to be ascending: %f, %f', $return['points_cutoffs'][$prev][0], $return['points_cutoffs'][$next][0]) ); } if ($return['points_cutoffs'][$prev][1] < $return['points_cutoffs'][$next][1]) { throw new ParametersException( sprintf('Cutoff values need to be non-ascending: %f, %f', $return['points_cutoffs'][$prev][1], $return['points_cutoffs'][$next][1]) ); } } return $return; } function get_position_percentage_from_position($position, $contestants) { return ($position - 1) / $contestants; } function get_percentage_from_position($position, $contestants, $cutoffs) { $position_percentage = $this->get_position_percentage_from_position( $position, $contestants ); for ($prev = 0; $prev < count($cutoffs) - 1; $prev++) { $next = $prev + 1; if (($cutoffs[$prev][0] <= $position_percentage) && ($cutoffs[$next][0] >= $position_percentage)) { $result = ($position_percentage - $cutoffs[$prev][0]) * ($cutoffs[$prev][1] - $cutoffs[$next][1]) / ($cutoffs[$prev][0] - $cutoffs[$next][0]) + $cutoffs[$prev][1]; return $result; } } return 0.0; } protected function _get_type_multiplier() { return (1 + static::TEAMS_BONUS * ($this->parameters['type'] == ApiPkl::TYPE_TEAMS)); } protected function _get_max_points() { return safe_ceil( max( $this->parameters['min_points'], $this->_get_type_multiplier() * ( max( static::MINIMUM_AVG_TITLE, $this->parameters['title_sum'] / $this->parameters['players']) * $this->parameters['tournament_weight'] + $this->parameters['players_coefficient'] * $this->parameters['contestants'] * $this->parameters['type'] ) ) ); } function calculate_points($min_points=1, $scale_factor=1.0) { $max_points = $this->_get_max_points(); $result = array( "sum" => 0, "points" => array() ); for ($place = 1; $place <= $this->parameters['contestants']; $place++) { $percentage = $this->get_percentage_from_position( $place, $this->parameters['contestants'], $this->parameters['points_cutoffs'] ); $points = safe_ceil( floatval($max_points) * $percentage * $scale_factor ); $points = max($min_points, intval($points)); if ($points > 0) { $result['points'][$place] = $points; $result['sum'] += $this->parameters['type'] * $result['points'][$place]; } } return $result; } function calculate_kmp_points() { $max_points = safe_ceil($this->parameters['contestants'] * 0.5); $min_points = 1; $result = array( "sum" => 0, "points" => array(1 => $max_points) ); for ($place = 2; $place <= $this->parameters['contestants']; $place++) { $result['points'][$place] = max( $min_points, $result['points'][$place-1] - 1 ); $result['sum'] += $this->parameters['type'] * $result['points'][$place]; } if ($this->parameters['title_sum'] / $this->parameters['players'] >= static::KMP_BONUS_MINIMUM_AVG_TITLE) { $result['points'][1] += static::KMP_BONUS_1ST; $result['points'][2] += static::KMP_BONUS_2ND; $result['points'][3] += static::KMP_BONUS_3RD; $result['sum'] += $this->parameters['type'] * (static::KMP_BONUS_1ST + static::KMP_BONUS_2ND + static::KMP_BONUS_3RD); } return $result; } function calculate_bridgenet_points() { throw new ParametersException( 'BridgeNET points not supported in this API version' ); } } class ApiPklV1 extends ApiPkl {} class ApiPklV2 extends ApiPklV1 { // get rid of old KMP bonus point values const KMP_BONUS_MINIMUM_AVG_TITLE = 99999; const KMP_BONUS_2ND = 0; const KMP_BONUS_3RD = 0; // and (re-)define new bonus for 1st place const KMP_BONUS_1ST = 10; function calculate_kmp_points() { // the line below works only because you can't play > 39 boards in KMP // please kill me if it ever changes // (the condition, not the line - that one I've just changed) $this->parameters['tournament_weight'] = static::WEIGHT_REGIONAL_UNDER40; $this->parameters['min_points'] = 0; $intermediate = $this->calculate_points(); $this->parameters['min_points'] = $intermediate['points'][1] + static::KMP_BONUS_1ST; return $this->calculate_points(); } } class ApiPklV3 extends ApiPklV2 { // parameters for multiplication factor const BNET_POINTS_FACTOR_QUOTIENT = 27.0; const BNET_POINTS_FACTOR_CAP = 1.0; protected function _check_bridgenet_parameters($parameters) { $this->ensure_parameters($parameters, array('boards', 'type')); if ($parameters['type'] == ApiPkl::TYPE_TEAMS) { throw new ParametersException( sprintf('Parameter: type has incorrect value (%s) for BridgeNET tournament', $parameters['type']) ); } } function check_parameters($parameters) { // Checks for BridgeNET parameters if type == BridgeNET if (isset($parameters['tournament_rank']) && $parameters['tournament_rank'] == ApiPkl::RANK_BNET) { $this->_check_bridgenet_parameters($parameters); // BridgeNET type tournaments disregard all 'manual' parameters if (isset($parameters['manual'])) { unset($parameters['manual']); } } // Validate newly introduced 'boards' parameter, if provided if (isset($parameters['boards'])) { $this->check_values($parameters, array( 'boards' => function($r) { return is_integer_like($r); } )); // It overrides 'over39_boards', // so we can force-set it and treat it as non-mandatory. // But we need to do this *before* parent::check_parameters, // as it checks for it occurence. $parameters['over39_boards'] = strval( intval($parameters['boards'] > 39) ); } return parent::check_parameters($parameters); } function parse_parameters($parameters) { $return = parent::parse_parameters($parameters); if (isset($parameters['boards'])) { // 'boards' param needs to be parsed and maintained, // as it's used in BridgeNET calculcations $return['boards'] = intval($parameters['boards']); } return $return; } function calculate_bridgenet_points() { $factor = min( $this->parameters['boards'] / static::BNET_POINTS_FACTOR_QUOTIENT, static::BNET_POINTS_FACTOR_CAP ); return $this->calculate_points(0, $factor); } } // 2021.02.10 - do not discard BridgeNET local team tournaments, but check for board count class ApiPklV3_1 extends ApiPklV3 { const BNET_MINIMUM_BOARD_COUNT = 20; protected function _check_bridgenet_parameters($parameters) { // 'boards' are mandatory for BridgeNET $this->ensure_parameters($parameters, array('boards')); // There's no longer a check against type == TEAMS // But there's one for minimum board count if (intval($parameters['boards']) < static::BNET_MINIMUM_BOARD_COUNT) { throw new ParametersException( sprintf( 'At least %d boards must be played in BridgeNET tournaments', static::BNET_MINIMUM_BOARD_COUNT ) ); } } } // 2025.01.01 - parameterized points for 1st place are no longer a minimum, they're strictly applied class ApiPklV4 extends ApiPklV3_1 { function check_parameters($parameters) { // Check for newly introduced 'override_type_multiplier' parameter if (isset($parameters['manual']) && isset($parameters['manual']['override_type_multiplier'])) { $this->check_values($parameters['manual'], array( 'override_type_multiplier' => function($r) { return is_integer_like($r) && intval($r) >= 0 && intval($r) <= 1; } )); } return parent::check_parameters($parameters); } function parse_parameters($parameters) { $return = parent::parse_parameters($parameters); // By default, leave the 1.25x multiplier for teams $return['override_type_multiplier'] = FALSE; // But if 'manual'->'override_type_multiplier' is set, // interpret it. if (isset($parameters['manual']) && isset($parameters['manual']['override_type_multiplier'])) { $return['override_type_multiplier'] = ($parameters['manual']['override_type_multiplier'] == 1); } return $return; } protected function _get_type_multiplier() { if ($this->parameters['override_type_multiplier']) { return 1.0; } return parent::_get_type_multiplier(); } protected function _get_max_points() { if ($this->parameters['min_points']) { // 2025.02.12 amendment: // RodzajTurnieju multipler applies to number of points for 1st place return $this->parameters['min_points'] * $this->_get_type_multiplier(); } return parent::_get_max_points(); } } ?>