diff --git a/app/Helpers/FighterClassification.php b/app/Helpers/FighterClassification.php index e69de29..1b5755f 100644 --- a/app/Helpers/FighterClassification.php +++ b/app/Helpers/FighterClassification.php @@ -0,0 +1,293 @@ + [2014, 2019], + 'cadet' => [2011, 2013], + 'junior' => [2008, 2010], + 'senior' => [0, 2007], + ]; + + foreach ($ageGroups as $group => [$startYear, $endYear]) { + if ($startYear <= $birthYear && $birthYear <= $endYear) { + return $group; + } + } + return 'unknown'; +} + +/** + * Assign weight class based on age group, gender, and weight. + * + * @param string $ageGroup + * @param string $gender Lowercase 'male' or 'female' + * @param float $weight + * @return string Weight class label or 'unknown' + */ +function assignWeightClass(string $ageGroup, string $gender, float $weight): string +{ + $weightClasses = [ + 'senior' => [ + 'male' => [ + ['min' => 0, 'max' => 54, 'label' => '-54kg'], + ['min' => 54, 'max' => 58, 'label' => '-58kg'], + ['min' => 58, 'max' => 63, 'label' => '-63kg'], + ['min' => 63, 'max' => 68, 'label' => '-68kg'], + ['min' => 68, 'max' => 74, 'label' => '-74kg'], + ['min' => 74, 'max' => 80, 'label' => '-80kg'], + ['min' => 80, 'max' => 87, 'label' => '-87kg'], + ['min' => 87, 'max' => INF, 'label' => '+87kg'], + ], + 'female' => [ + ['min' => 0, 'max' => 46, 'label' => '-46kg'], + ['min' => 46, 'max' => 49, 'label' => '-49kg'], + ['min' => 49, 'max' => 53, 'label' => '-53kg'], + ['min' => 53, 'max' => 57, 'label' => '-57kg'], + ['min' => 57, 'max' => 62, 'label' => '-62kg'], + ['min' => 62, 'max' => 67, 'label' => '-67kg'], + ['min' => 67, 'max' => 73, 'label' => '-73kg'], + ['min' => 73, 'max' => INF, 'label' => '+73kg'], + ], + ], + 'junior' => [ + 'male' => [ + ['min' => 0, 'max' => 45, 'label' => '-45kg'], + ['min' => 45, 'max' => 48, 'label' => '-48kg'], + ['min' => 48, 'max' => 51, 'label' => '-51kg'], + ['min' => 51, 'max' => 55, 'label' => '-55kg'], + ['min' => 55, 'max' => 59, 'label' => '-59kg'], + ['min' => 59, 'max' => 63, 'label' => '-63kg'], + ['min' => 63, 'max' => 68, 'label' => '-68kg'], + ['min' => 68, 'max' => 73, 'label' => '-73kg'], + ['min' => 73, 'max' => 78, 'label' => '-78kg'], + ['min' => 78, 'max' => INF, 'label' => '+78kg'], + ], + 'female' => [ + ['min' => 0, 'max' => 42, 'label' => '-42kg'], + ['min' => 42, 'max' => 44, 'label' => '-44kg'], + ['min' => 44, 'max' => 46, 'label' => '-46kg'], + ['min' => 46, 'max' => 49, 'label' => '-49kg'], + ['min' => 49, 'max' => 52, 'label' => '-52kg'], + ['min' => 52, 'max' => 55, 'label' => '-55kg'], + ['min' => 55, 'max' => 59, 'label' => '-59kg'], + ['min' => 59, 'max' => 63, 'label' => '-63kg'], + ['min' => 63, 'max' => 68, 'label' => '-68kg'], + ['min' => 68, 'max' => INF, 'label' => '+68kg'], + ], + ], + 'cadet' => [ + 'male' => [ + ['min' => 0, 'max' => 33, 'label' => '-33kg'], + ['min' => 33, 'max' => 37, 'label' => '-37kg'], + ['min' => 37, 'max' => 41, 'label' => '-41kg'], + ['min' => 41, 'max' => 45, 'label' => '-45kg'], + ['min' => 45, 'max' => 49, 'label' => '-49kg'], + ['min' => 49, 'max' => 53, 'label' => '-53kg'], + ['min' => 53, 'max' => 57, 'label' => '-57kg'], + ['min' => 57, 'max' => 61, 'label' => '-61kg'], + ['min' => 61, 'max' => 65, 'label' => '-65kg'], + ['min' => 65, 'max' => INF, 'label' => '+65kg'], + ], + 'female' => [ + ['min' => 0, 'max' => 29, 'label' => '-29kg'], + ['min' => 29, 'max' => 33, 'label' => '-33kg'], + ['min' => 33, 'max' => 37, 'label' => '-37kg'], + ['min' => 37, 'max' => 41, 'label' => '-41kg'], + ['min' => 41, 'max' => 44, 'label' => '-44kg'], + ['min' => 44, 'max' => 47, 'label' => '-47kg'], + ['min' => 47, 'max' => 51, 'label' => '-51kg'], + ['min' => 51, 'max' => 55, 'label' => '-55kg'], + ['min' => 55, 'max' => 59, 'label' => '-59kg'], + ['min' => 59, 'max' => INF, 'label' => '+59kg'], + ], + ], + 'child' => [ + 'male' => [ + ['min' => 0, 'max' => 24, 'label' => '-24kg'], + ['min' => 24, 'max' => 28, 'label' => '-28kg'], + ['min' => 28, 'max' => 32, 'label' => '-32kg'], + ['min' => 32, 'max' => 36, 'label' => '-36kg'], + ['min' => 36, 'max' => 40, 'label' => '-40kg'], + ['min' => 40, 'max' => 44, 'label' => '-44kg'], + ['min' => 44, 'max' => 48, 'label' => '-48kg'], + ['min' => 48, 'max' => 52, 'label' => '-52kg'], + ['min' => 52, 'max' => 56, 'label' => '-56kg'], + ['min' => 56, 'max' => INF, 'label' => '+56kg'], + ], + 'female' => [ + ['min' => 0, 'max' => 29, 'label' => '-29kg'], + ['min' => 29, 'max' => 33, 'label' => '29-33kg'], + ['min' => 33, 'max' => 37, 'label' => '33-37kg'], + ['min' => 37, 'max' => 41, 'label' => '37-41kg'], + ['min' => 41, 'max' => 44, 'label' => '41-44kg'], + ['min' => 44, 'max' => 47, 'label' => '44-47kg'], + ['min' => 47, 'max' => 51, 'label' => '47-51kg'], + ['min' => 51, 'max' => 55, 'label' => '51-55kg'], + ['min' => 55, 'max' => 59, 'label' => '55-59kg'], + ['min' => 59, 'max' => INF, 'label' => '+59kg'], + ], + ], + ]; + + if (!isset($weightClasses[$ageGroup][$gender])) { + return 'unknown'; + } + + foreach ($weightClasses[$ageGroup][$gender] as $class) { + if ($weight > $class['min'] && $weight <= $class['max']) { + return $class['label']; + } + } + + return 'unknown'; +} + +/** + * Classify a single fighter by age group, gender, birth year, and weight class. + * + * @param array $fighter Array with keys 'date_of_birth', 'gender', 'weight'. + * @param int $competitionYear + * @return array Augmented fighter data. + */ +function classifyFighter(array $fighter, int $competitionYear = 2025): array +{ + $dob = Carbon::parse($fighter['date_of_birth']); + $birthYear = $dob->year; + $ageGroup = getAgeGroupByBirthYear($birthYear, $competitionYear); + $gender = strtolower($fighter['gender']); + $weight = floatval($fighter['weight']); + $weightClass = assignWeightClass($ageGroup, $gender, $weight); + + return array_merge($fighter, [ + 'age_group' => $ageGroup, + 'birth_year' => $birthYear, + 'gender' => $gender, + 'weight_class' => $weightClass, + ]); +} + +/** + * Group fighters by age group, gender, and weight class, + * then generate a Taekwondo single-elimination draw with colors. + * + * @param array $fighters + * @param int $competitionYear + * @return array Grouped fighters with tournament draws. + */ +function groupFightersWithTaekwondoDraw(array $fighters, int $competitionYear = 2025): array +{ + $classified = array_map(fn($f) => classifyFighter($f, $competitionYear), $fighters); + $collection = collect($classified); + + $grouped = $collection->groupBy([ + 'age_group', + 'gender', + 'weight_class', + ]); + + $result = []; + + foreach ($grouped as $ageGroup => $genders) { + foreach ($genders as $gender => $weightClasses) { + foreach ($weightClasses as $weightClass => $fightersInGroup) { + // Important: Reset array keys to prevent undefined index errors + $fightersArray = array_values($fightersInGroup->toArray()); + + // Create Taekwondo-style draw + $draw = createTaekwondoDraw($fightersArray); + + $result[] = [ + 'age_group' => $ageGroup, + 'gender' => $gender, + 'weight_class' => $weightClass, + 'fighters' => $fightersArray, + 'tournament_draw' => $draw, + ]; + } + } + } + + return $result; +} + +/** + * Create a Taekwondo single-elimination tournament draw. + * Assign colors (red/blue) properly to matches. + * Fixes index issues by ensuring the fighters array is properly zero-based and extends to bracket size with byes. + * + * @param array $fighters Array of fighters in a single group + * @return array Tournament draw data with rounds and matches. + */ +function createTaekwondoDraw(array $fighters): array +{ + // Reset keys to zero-indexed array to avoid undefined index errors + $fighters = array_values($fighters); + + $numFighters = count($fighters); + + $bracketSize = 1; + while ($bracketSize < $numFighters) { + $bracketSize <<= 1; + } + + $numByes = $bracketSize - $numFighters; + + // Add byes as null if needed + for ($i = 0; $i < $numByes; $i++) { + $fighters[] = null; // represent bye by null + } + + $numMatches = $bracketSize / 2; + $matches = []; + + for ($i = 0; $i < $numMatches; $i++) { + $p1Index = 2 * $i; + $p2Index = 2 * $i + 1; + + $player1 = isset($fighters[$p1Index]) ? $fighters[$p1Index] : null; + $player2 = isset($fighters[$p2Index]) ? $fighters[$p2Index] : null; + + $matches[] = [ + 'match_number' => $i + 1, + 'player1' => $player1 ? ['fighter_data' => $player1, 'color' => 'red'] : null, + 'player2' => $player2 ? ['fighter_data' => $player2, 'color' => 'blue'] : null, + 'winner' => null, + ]; + } + + $totalRounds = (int) log($bracketSize, 2); + + $rounds = [ + [ + 'round' => 1, + 'matches' => $matches, + ] + ]; + + // Add empty rounds for future matches (could be filled dynamically) + for ($r = 2; $r <= $totalRounds; $r++) { + $rounds[] = [ + 'round' => $r, + 'matches' => [], + ]; + } + + return [ + 'totalRounds' => $totalRounds, + 'rounds' => $rounds, + 'bracketSize' => $bracketSize, + 'originalFightersCount' => $numFighters, + ]; +} diff --git a/app/Http/Controllers/FighterController.php b/app/Http/Controllers/FighterController.php new file mode 100644 index 0000000..604d56f --- /dev/null +++ b/app/Http/Controllers/FighterController.php @@ -0,0 +1,16 @@ +toArray(); + $groupedJson = groupFighters($fighters, 2025); + return response($groupedJson, 200)->header('Content-Type', 'application/json'); + } + +} diff --git a/composer.json b/composer.json index 024809f..d54bb34 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,10 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/Helpers/FighterClassification.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/routes/web.php b/routes/web.php index b130397..cbf2689 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,9 @@ toArray(); + + // Call global function with leading backslash to avoid namespace issue + $groupedJson = \groupFighters($fighters, 2025); + + return response($groupedJson, 200)->header('Content-Type', 'application/json'); +}); + +Route::get('/taekwondo-draw', function () { + $fighters = Member::all()->toArray(); + $draws = groupFightersWithTaekwondoDraw($fighters, 2025); // Change year if you want + return response()->json($draws, 200); +}); +