created a classifier and draw generator

This commit is contained in:
Ghassan Yusuf 2025-07-30 21:38:19 +03:00
parent fec7cb655b
commit 3fe9417af4
4 changed files with 333 additions and 1 deletions

View File

@ -0,0 +1,293 @@
<?php
use Carbon\Carbon;
use Illuminate\Support\Collection;
/**
* Determine the age group based on birth year.
*
* @param int $birthYear
* @param int $competitionYear
* @return string
*/
function getAgeGroupByBirthYear(int $birthYear, int $competitionYear = 2025): string
{
// Age group birth year ranges (adjust as per your competition)
$ageGroups = [
'child' => [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,
];
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers;
use App\Models\Member;
class FighterController extends Controller
{
public function grouped()
{
$fighters = Member::all()->toArray();
$groupedJson = groupFighters($fighters, 2025);
return response($groupedJson, 200)->header('Content-Type', 'application/json');
}
}

View File

@ -25,7 +25,10 @@
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"files": [
"app/Helpers/FighterClassification.php"
]
},
"autoload-dev": {
"psr-4": {

View File

@ -1,6 +1,9 @@
<?php
use Illuminate\Support\Facades\Route;
use app\Helpers\FighterClassification;
use App\Models\Member;
/*
|--------------------------------------------------------------------------
@ -16,3 +19,20 @@ use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::get('/find', function () {
$fighters = \App\Models\Member::all()->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);
});