"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]) ); } } } 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; } 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 ) ); $return['min_points'] = $min_points[intval($parameters['over39_boards'])][intval($parameters['tournament_rank'])]; if (isset($parameters['manual']) && isset($parameters['manual']['min_points'])) { $return['min_points'] = intval($parameters['manual']['min_points']); } $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); } // 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); // BridgeNET type tournaments disregard all 'manual' parameters if (isset($return['tournament_rank']) && $return['tournament_rank'] == ApiPkl::RANK_BNET) { if (isset($return['manual'])) { unset($return['manual']); } } 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 { 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(); } } ?>