1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Xoco70\LaravelTournaments\TreeGen; |
4
|
|
|
|
5
|
|
|
use Illuminate\Support\Collection; |
6
|
|
|
use Illuminate\Support\Facades\App; |
7
|
|
|
use Xoco70\LaravelTournaments\Contracts\TreeGenerable; |
8
|
|
|
use Xoco70\LaravelTournaments\Exceptions\TreeGenerationException; |
9
|
|
|
use Xoco70\LaravelTournaments\Models\Championship; |
10
|
|
|
use Xoco70\LaravelTournaments\Models\ChampionshipSettings; |
11
|
|
|
use Xoco70\LaravelTournaments\Models\Fight; |
12
|
|
|
use Xoco70\LaravelTournaments\Models\FightersGroup; |
13
|
|
|
|
14
|
|
|
abstract class TreeGen implements TreeGenerable |
15
|
|
|
{ |
16
|
|
|
protected $groupBy; |
17
|
|
|
protected $tree; |
18
|
|
|
public $championship; |
19
|
|
|
public $settings; |
20
|
|
|
protected $numFighters; |
21
|
|
|
|
22
|
|
|
abstract protected function pushEmptyGroupsToTree($numFighters); |
23
|
|
|
|
24
|
|
|
abstract protected function generateFights(); |
25
|
|
|
|
26
|
|
|
abstract protected function createByeFighter(); |
27
|
|
|
|
28
|
|
|
abstract protected function chunkAndShuffle(Collection $fightersByEntity); |
29
|
|
|
|
30
|
|
|
abstract protected function addFighterToGroup(FightersGroup $group, $fighter); |
31
|
|
|
|
32
|
|
|
abstract protected function syncGroup(FightersGroup $group, $fighters); |
33
|
|
|
|
34
|
|
|
abstract protected function getByeGroup($fighters); |
35
|
|
|
|
36
|
|
|
abstract protected function getFighter($fighterId); |
37
|
|
|
|
38
|
|
|
abstract protected function getFighters(); |
39
|
|
|
|
40
|
|
|
abstract protected function getNumRounds($fightersCount); |
41
|
|
|
|
42
|
|
|
abstract protected function generateGroupsForRound(Collection $usersByArea, $round); |
43
|
|
|
|
44
|
|
|
abstract protected function generateAllTrees(); |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @param Championship $championship |
48
|
|
|
* @param $groupBy |
49
|
|
|
*/ |
50
|
|
|
public function __construct(Championship $championship, $groupBy) |
51
|
|
|
{ |
52
|
|
|
$this->championship = $championship; |
53
|
|
|
$this->groupBy = $groupBy; |
54
|
|
|
$this->settings = $championship->getSettings(); |
55
|
|
|
$this->tree = new Collection(); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Generate tree groups for a championship. |
60
|
|
|
* |
61
|
|
|
* @throws TreeGenerationException |
62
|
|
|
*/ |
63
|
|
|
public function run() |
64
|
|
|
{ |
65
|
|
|
$this->generateAllTrees(); |
66
|
|
|
$this->generateAllFights(); |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* Get the biggest entity group. |
71
|
|
|
* |
72
|
|
|
* @param $userGroups |
73
|
|
|
* |
74
|
|
|
* @return int |
75
|
|
|
*/ |
76
|
|
|
private function getMaxFightersByEntity($userGroups): int |
77
|
|
|
{ |
78
|
|
|
return $userGroups |
79
|
|
|
->sortByDesc(function ($group) { |
80
|
|
|
return $group->count(); |
81
|
|
|
}) |
82
|
|
|
->first() |
83
|
|
|
->count(); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Get Competitor's list ordered by entities |
88
|
|
|
* Countries for Internation Tournament, State for a National Tournament, etc. |
89
|
|
|
* |
90
|
|
|
* @param $fighters |
91
|
|
|
* |
92
|
|
|
* @return Collection |
93
|
|
|
*/ |
94
|
|
|
private function getFightersByEntity($fighters): Collection |
95
|
|
|
{ |
96
|
|
|
// Right now, we are treating users and teams as equals. |
97
|
|
|
// It doesn't matter right now, because we only need name attribute which is common to both models |
98
|
|
|
|
99
|
|
|
// $this->groupBy contains federation_id, association_id, club_id, etc. |
100
|
|
|
if (($this->groupBy) != null) { |
101
|
|
|
return $fighters->groupBy($this->groupBy); // Collection of Collection |
102
|
|
|
} |
103
|
|
|
return $fighters->chunk(1); // Collection of Collection |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
/** |
107
|
|
|
* Get the size the first round will have. |
108
|
|
|
* |
109
|
|
|
* @param $fighterCount |
110
|
|
|
* @param $groupSize |
111
|
|
|
* |
112
|
|
|
* @return int |
113
|
|
|
*/ |
114
|
|
|
protected function getTreeSize($fighterCount, $groupSize) |
115
|
|
|
{ |
116
|
|
|
$squareMultiplied = collect([1, 2, 4, 8, 16, 32, 64]) |
117
|
|
|
->map(function ($item) use ($groupSize) { |
118
|
|
|
return $item * $groupSize; |
119
|
|
|
}); // [4, 8, 16, 32, 64,...] |
|
|
|
|
120
|
|
|
|
121
|
|
|
foreach ($squareMultiplied as $limit) { |
122
|
|
|
if ($fighterCount <= $limit) { |
123
|
|
|
$treeSize = $limit; |
124
|
|
|
$numAreas = $this->championship->getSettings()->fightingAreas; |
125
|
|
|
$fighterCountPerArea = $treeSize / $numAreas; |
126
|
|
|
if ($fighterCountPerArea < $groupSize) { |
127
|
|
|
$treeSize = $treeSize * $numAreas; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
return $treeSize; |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
return 64 * $groupSize; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Repart BYE in the tree,. |
139
|
|
|
* |
140
|
|
|
* @param $fighterGroups |
141
|
|
|
* @param int $max |
142
|
|
|
* |
143
|
|
|
* @return Collection |
144
|
|
|
*/ |
145
|
|
|
private function repart($fighterGroups, $max) |
146
|
|
|
{ |
147
|
|
|
$fighters = new Collection(); |
148
|
|
|
for ($i = 0; $i < $max; $i++) { |
149
|
|
|
foreach ($fighterGroups as $fighterGroup) { |
150
|
|
|
$fighter = $fighterGroup->values()->get($i); |
151
|
|
|
if ($fighter != null) { |
152
|
|
|
$fighters->push($fighter); |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
return $fighters; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Insert byes in an homogen way. |
162
|
|
|
* |
163
|
|
|
* @param Collection $fighters |
164
|
|
|
* @param Collection $byeGroup |
165
|
|
|
* |
166
|
|
|
* @return Collection |
167
|
|
|
*/ |
168
|
|
|
private function insertByes(Collection $fighters, Collection $byeGroup) |
169
|
|
|
{ |
170
|
|
|
$bye = count($byeGroup) > 0 ? $byeGroup[0] : []; |
171
|
|
|
$sizeFighters = count($fighters); |
172
|
|
|
$sizeGroupBy = count($byeGroup); |
173
|
|
|
|
174
|
|
|
$frequency = $sizeGroupBy != 0 |
175
|
|
|
? (int)floor($sizeFighters / $sizeGroupBy) |
176
|
|
|
: -1; |
177
|
|
|
|
178
|
|
|
// Create Copy of $competitors |
179
|
|
|
return $this->getFullFighterList($fighters, $frequency, $sizeGroupBy, $bye); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* @param $order |
185
|
|
|
* @param $round |
186
|
|
|
* @param $parent |
187
|
|
|
* |
188
|
|
|
* @return FightersGroup |
189
|
|
|
*/ |
190
|
|
|
protected function saveGroup($order, $round, $parent): FightersGroup |
191
|
|
|
{ |
192
|
|
|
$group = new FightersGroup(); |
193
|
|
|
$this->championship->isDirectEliminationType() |
194
|
|
|
? $group->area = $this->getNumArea($round, $order) |
195
|
|
|
: $group->area = 1; // Area limited to 1 in playoff |
196
|
|
|
|
197
|
|
|
|
198
|
|
|
$group->order = $order; |
199
|
|
|
$group->round = $round; |
200
|
|
|
$group->championship_id = $this->championship->id; |
201
|
|
|
if ($parent != null) { |
202
|
|
|
$group->parent_id = $parent->id; |
203
|
|
|
} |
204
|
|
|
$group->save(); |
205
|
|
|
|
206
|
|
|
return $group; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/** |
210
|
|
|
* @param int $groupSize |
211
|
|
|
* |
212
|
|
|
* @return Collection |
213
|
|
|
*/ |
214
|
|
|
public function createByeGroup($groupSize): Collection |
215
|
|
|
{ |
216
|
|
|
$byeFighter = $this->createByeFighter(); |
217
|
|
|
$group = new Collection(); |
218
|
|
|
for ($i = 0; $i < $groupSize; $i++) { |
219
|
|
|
$group->push($byeFighter); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
return $group; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* @param $fighters |
227
|
|
|
* @param Collection $fighterGroups |
228
|
|
|
* |
229
|
|
|
* @return Collection |
230
|
|
|
*/ |
231
|
|
|
public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection |
232
|
|
|
{ |
233
|
|
|
$tmpFighterGroups = clone $fighterGroups; |
234
|
|
|
$byeGroup = $this->getByeGroup($fighters); |
235
|
|
|
|
236
|
|
|
// Get biggest competitor's group |
237
|
|
|
$max = $this->getMaxFightersByEntity($tmpFighterGroups); |
238
|
|
|
|
239
|
|
|
// We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity. |
240
|
|
|
|
241
|
|
|
$fighters = $this->repart($fighterGroups, $max); |
242
|
|
|
$fighters = $this->insertByes($fighters, $byeGroup); |
243
|
|
|
|
244
|
|
|
return $fighters; |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Get All Groups on previous round. |
249
|
|
|
* |
250
|
|
|
* @param $currentRound |
251
|
|
|
* |
252
|
|
|
* @return Collection |
253
|
|
|
*/ |
254
|
|
|
private function getPreviousRound($currentRound) |
255
|
|
|
{ |
256
|
|
|
$previousRound = $this->championship->groupsByRound($currentRound + 1)->get(); |
257
|
|
|
|
258
|
|
|
return $previousRound; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Get the next group on the right ( parent ), final round being the ancestor. |
263
|
|
|
* |
264
|
|
|
* @param $matchNumber |
265
|
|
|
* @param Collection $previousRound |
266
|
|
|
* |
267
|
|
|
* @return mixed |
268
|
|
|
*/ |
269
|
|
|
private function getParentGroup($matchNumber, $previousRound) |
270
|
|
|
{ |
271
|
|
|
$parentIndex = intval(($matchNumber + 1) / 2); |
272
|
|
|
$parent = $previousRound->get($parentIndex - 1); |
273
|
|
|
|
274
|
|
|
return $parent; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Group Fighters by area. |
279
|
|
|
* |
280
|
|
|
* @throws TreeGenerationException |
281
|
|
|
* |
282
|
|
|
* @return Collection |
283
|
|
|
*/ |
284
|
|
|
protected function getFightersByArea() |
285
|
|
|
{ |
286
|
|
|
// If previous trees already exists, delete all |
287
|
|
|
$this->championship->fightersGroups()->delete(); |
288
|
|
|
$areas = $this->settings->fightingAreas; |
289
|
|
|
$fighters = $this->getFighters(); |
290
|
|
|
$fighterType = $this->settings->isTeam |
291
|
|
|
? trans_choice('.team', 2) |
292
|
|
|
: trans_choice('laravel-tournaments::core.competitor', 2); |
293
|
|
|
// If there is less than 2 competitors average by area |
294
|
|
|
$minFighterCount = $fighters->count() / $areas; |
295
|
|
|
|
296
|
|
|
|
297
|
|
|
if ($this->settings->hasPreliminary && $fighters->count() / ($this->settings->preliminaryGroupSize * $areas) < 1) { |
298
|
|
|
throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [ |
299
|
|
|
'number' => $this->settings->preliminaryGroupSize * $areas, |
300
|
|
|
'fighter_type' => $fighterType |
301
|
|
|
])); |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
if ($minFighterCount < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) { |
305
|
|
|
throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [ |
306
|
|
|
'number' => ChampionshipSettings::MIN_COMPETITORS_BY_AREA, |
307
|
|
|
'fighter_type' => $fighterType |
308
|
|
|
])); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
// Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...) |
312
|
|
|
$fighterByEntity = $this->getFightersByEntity($fighters); // Chunk(1) |
313
|
|
|
$fightersWithBye = $this->adjustFightersGroupWithByes($fighters, $fighterByEntity); |
314
|
|
|
// Chunk user by areas |
315
|
|
|
return $fightersWithBye->chunk(count($fightersWithBye) / $areas); |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Attach a parent to every child for nestedSet Navigation. |
320
|
|
|
* |
321
|
|
|
* @param $numFightersElim |
322
|
|
|
*/ |
323
|
|
|
protected function addParentToChildren($numFightersElim) |
324
|
|
|
{ |
325
|
|
|
$numRounds = $this->getNumRounds($numFightersElim); |
326
|
|
|
$groupsDesc = $this->championship |
327
|
|
|
->fightersGroups() |
328
|
|
|
->where('round', '<', $numRounds) |
329
|
|
|
->orderByDesc('id')->get(); |
330
|
|
|
|
331
|
|
|
$groupsDescByRound = $groupsDesc->groupBy('round'); |
332
|
|
|
|
333
|
|
|
foreach ($groupsDescByRound as $round => $groups) { |
334
|
|
|
$previousRound = $this->getPreviousRound($round); |
335
|
|
|
foreach ($groups->reverse()->values() as $matchNumber => $group) { |
336
|
|
|
$parent = $this->getParentGroup($matchNumber + 1, $previousRound); |
337
|
|
|
$group->parent_id = $parent->id; |
338
|
|
|
$group->save(); |
339
|
|
|
} |
340
|
|
|
} |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* @param Collection $fighters |
345
|
|
|
* @param $frequency |
346
|
|
|
* @param $sizeGroupBy |
347
|
|
|
* @param $bye |
348
|
|
|
* |
349
|
|
|
* @return Collection |
350
|
|
|
*/ |
351
|
|
|
private function getFullFighterList(Collection $fighters, $frequency, $sizeGroupBy, $bye): Collection |
352
|
|
|
{ |
353
|
|
|
$newFighters = new Collection(); |
354
|
|
|
$count = 0; |
355
|
|
|
$byeCount = 0; |
356
|
|
|
foreach ($fighters as $fighter) { |
357
|
|
|
if ($this->shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount)) { |
358
|
|
|
$newFighters->push($bye); |
359
|
|
|
$byeCount++; |
360
|
|
|
} |
361
|
|
|
$newFighters->push($fighter); |
362
|
|
|
$count++; |
363
|
|
|
} |
364
|
|
|
|
365
|
|
|
return $newFighters; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* @param $frequency |
370
|
|
|
* @param $sizeGroupBy |
371
|
|
|
* @param $count |
372
|
|
|
* @param $byeCount |
373
|
|
|
* |
374
|
|
|
* @return bool |
375
|
|
|
*/ |
376
|
|
|
private function shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount): bool |
377
|
|
|
{ |
378
|
|
|
return $frequency != -1 && $count % $frequency == 0 && $byeCount < $sizeGroupBy; |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Destroy Previous Fights for demo. |
383
|
|
|
*/ |
384
|
|
|
protected function destroyPreviousFights() |
385
|
|
|
{ |
386
|
|
|
// Delete previous fight for this championship |
387
|
|
|
$arrGroupsId = $this->championship->fightersGroups()->get()->pluck('id'); |
388
|
|
|
Fight::destroy($arrGroupsId); |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Generate Fights for next rounds. |
393
|
|
|
*/ |
394
|
|
|
public function generateNextRoundsFights() |
395
|
|
|
{ |
396
|
|
|
$fightersCount = $this->championship->competitors->count() + $this->championship->teams->count(); |
397
|
|
|
$maxRounds = $this->getNumRounds($fightersCount); |
398
|
|
|
for ($numRound = 1; $numRound < $maxRounds; $numRound++) { |
399
|
|
|
$groupsByRound = $this->championship->fightersGroups()->where('round', $numRound)->with('parent', 'children')->get(); |
400
|
|
|
$this->updateParentFight($groupsByRound); // should be groupsByRound |
401
|
|
|
} |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
/** |
405
|
|
|
* @param $groupsByRound |
406
|
|
|
*/ |
407
|
|
|
protected function updateParentFight($groupsByRound) |
408
|
|
|
{ |
409
|
|
|
foreach ($groupsByRound as $keyGroup => $group) { |
410
|
|
|
$parentGroup = $group->parent; |
411
|
|
|
if ($parentGroup == null) { |
412
|
|
|
break; |
413
|
|
|
} |
414
|
|
|
$parentFight = $parentGroup->fights->get(0); |
415
|
|
|
|
416
|
|
|
// determine whether c1 or c2 must be updated |
417
|
|
|
$this->chooseAndUpdateParentFight($keyGroup, $group, $parentFight); |
418
|
|
|
} |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* @param $group |
423
|
|
|
* @param $parentFight |
424
|
|
|
*/ |
425
|
|
|
protected function chooseAndUpdateParentFight($keyGroup, FightersGroup $group, Fight $parentFight) |
426
|
|
|
{ |
427
|
|
|
// we need to know if the child has empty fighters, is this BYE or undetermined |
428
|
|
|
if ($group->hasDeterminedParent()) { |
429
|
|
|
$valueToUpdate = $group->getValueToUpdate(); // This should be OK |
430
|
|
|
if ($valueToUpdate != null) { |
431
|
|
|
$fighterToUpdate = $group->getParentFighterToUpdate($keyGroup); |
432
|
|
|
$parentFight->$fighterToUpdate = $valueToUpdate; |
433
|
|
|
$parentFight->save(); |
434
|
|
|
// Add fighter to pivot table |
435
|
|
|
$parentGroup = $parentFight->group; |
436
|
|
|
|
437
|
|
|
$fighter = $this->getFighter($valueToUpdate); |
438
|
|
|
$this->addFighterToGroup($parentGroup, $fighter); |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
/** |
444
|
|
|
* Calculate the area of the group ( group is still not created ). |
445
|
|
|
* |
446
|
|
|
* @param $round |
447
|
|
|
* @param $order |
448
|
|
|
* |
449
|
|
|
* @return int |
450
|
|
|
*/ |
451
|
|
|
protected function getNumArea($round, $order) |
452
|
|
|
{ |
453
|
|
|
$totalAreas = $this->settings->fightingAreas; |
454
|
|
|
$numFighters = $this->championship->fighters->count(); // 4 |
455
|
|
|
$numGroups = $this->getTreeSize($numFighters, $this->championship->getGroupSize()) / $this->championship->getGroupSize(); // 1 -> 1 |
|
|
|
|
456
|
|
|
|
457
|
|
|
$areaSize = $numGroups / ($totalAreas * pow(2, $round - 1)); |
458
|
|
|
|
459
|
|
|
$numArea = intval(ceil($order / $areaSize)); // if round == 4, and second match 2/2 = 1 BAD |
460
|
|
|
// dump($numArea); |
461
|
|
|
return $numArea; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
|
465
|
|
|
protected function generateAllFights() |
466
|
|
|
{ |
467
|
|
|
$this->generateFights(); // Abstract |
468
|
|
|
|
469
|
|
|
//TODO In direct elimination without Prelim, short_id are not generating well |
470
|
|
|
$this->generateNextRoundsFights(); |
471
|
|
|
Fight::generateFightsId($this->championship); |
472
|
|
|
} |
473
|
|
|
} |
474
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.