1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Xoco70\LaravelTournaments\TreeGen; |
4
|
|
|
|
5
|
|
|
use Illuminate\Support\Collection; |
6
|
|
|
use Xoco70\LaravelTournaments\Exceptions\TreeGenerationException; |
7
|
|
|
use Xoco70\LaravelTournaments\Models\ChampionshipSettings; |
8
|
|
|
use Xoco70\LaravelTournaments\Models\PreliminaryFight; |
9
|
|
|
use Xoco70\LaravelTournaments\Models\SingleEliminationFight; |
10
|
|
|
|
11
|
|
|
abstract class SingleEliminationTreeGen extends TreeGen |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* Calculate the Byes needed to fill the Championship Tree. |
15
|
|
|
* |
16
|
|
|
* @param $fighters |
17
|
|
|
* |
18
|
|
|
* @return Collection |
19
|
|
|
*/ |
20
|
|
View Code Duplication |
protected function getByeGroup($fighters) |
|
|
|
|
21
|
|
|
{ |
22
|
|
|
$fighterCount = $fighters->count(); |
23
|
|
|
$firstRoundGroupSize = $this->firstRoundGroupSize(); // Get the size of groups in the first round (2,3,4) |
24
|
|
|
// Max number of fighters for the first round |
25
|
|
|
$treeSize = $this->getTreeSize($fighterCount, $firstRoundGroupSize); |
26
|
|
|
$byeCount = $treeSize - $fighterCount; |
27
|
|
|
return $this->createByeGroup($byeCount); |
28
|
|
|
} |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Save Groups with their parent info. |
32
|
|
|
* |
33
|
|
|
* @param int $numRounds |
34
|
|
|
* @param int $numFighters |
35
|
|
|
*/ |
36
|
|
|
protected function pushGroups($numRounds, $numFighters) |
37
|
|
|
{ |
38
|
|
|
// TODO Here is where you should change when enable several winners for preliminary |
39
|
|
View Code Duplication |
for ($roundNumber = 2; $roundNumber <= $numRounds + 1; $roundNumber++) { |
|
|
|
|
40
|
|
|
// From last match to first match |
41
|
|
|
$maxMatches = ($numFighters / pow(2, $roundNumber)); |
42
|
|
|
|
43
|
|
|
for ($matchNumber = 1; $matchNumber <= $maxMatches; $matchNumber++) { |
44
|
|
|
$fighters = $this->createByeGroup(2); |
45
|
|
|
$group = $this->saveGroup($matchNumber, $roundNumber, null); |
46
|
|
|
$this->syncGroup($group, $fighters); |
47
|
|
|
} |
48
|
|
|
} |
49
|
|
|
// Third place Group |
50
|
|
|
if ($numFighters >= $this->championship->getGroupSize() * 2) { |
51
|
|
|
$fighters = $this->createByeGroup(2); |
52
|
|
|
|
53
|
|
|
$group = $this->saveGroup($maxMatches, $numRounds, null); |
|
|
|
|
54
|
|
|
|
55
|
|
|
$this->syncGroup($group, $fighters); |
56
|
|
|
} |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Create empty groups after round 1. |
61
|
|
|
* |
62
|
|
|
* @param $numFighters |
63
|
|
|
*/ |
64
|
|
|
protected function pushEmptyGroupsToTree($numFighters) |
65
|
|
|
{ |
66
|
|
|
if ($this->championship->hasPreliminary()) { |
67
|
|
|
/* Should add * prelimWinner but it add complexity about parent / children in tree |
68
|
|
|
*/ |
69
|
|
|
$numFightersElim = $numFighters / $this->settings->preliminaryGroupSize * 2; |
70
|
|
|
// We calculate how much rounds we will have |
71
|
|
|
$numRounds = intval(log($numFightersElim, 2)); // 3 rounds, but begining from round 2 ( ie => 4) |
72
|
|
|
return $this->pushGroups($numRounds, $numFightersElim); |
73
|
|
|
} |
74
|
|
|
// We calculate how much rounds we will have |
75
|
|
|
$numRounds = $this->getNumRounds($numFighters); |
76
|
|
|
|
77
|
|
|
return $this->pushGroups($numRounds, $numFighters); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Chunk Fighters into groups for fighting, and optionnaly shuffle. |
82
|
|
|
* |
83
|
|
|
* @param $fightersByEntity |
84
|
|
|
* |
85
|
|
|
* @return Collection|null |
86
|
|
|
*/ |
87
|
|
|
protected function chunk(Collection $fightersByEntity) |
88
|
|
|
{ |
89
|
|
|
//TODO Should Pull down to know if team or competitor |
90
|
|
|
if ($this->championship->hasPreliminary()) { |
91
|
|
|
return (new PlayOffCompetitorTreeGen($this->championship, null))->chunk($fightersByEntity); |
|
|
|
|
92
|
|
|
} |
93
|
|
|
$fightersGroup = $fightersByEntity->chunk(2); |
94
|
|
|
return $fightersGroup; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Generate First Round Fights. |
99
|
|
|
*/ |
100
|
|
|
protected function generateFights() |
101
|
|
|
{ |
102
|
|
|
// First Round Fights |
103
|
|
|
$settings = $this->settings; |
104
|
|
|
$initialRound = 1; |
105
|
|
|
// Very specific case to common case : Preliminary with 3 fighters |
106
|
|
|
if ($this->championship->hasPreliminary() && $settings->preliminaryGroupSize == 3) { |
107
|
|
|
// First we make all first fights of all groups |
108
|
|
|
// Then we make all second fights of all groups |
109
|
|
|
// Then we make all third fights of all groups |
110
|
|
|
$groups = $this->championship->groupsByRound(1)->get(); |
111
|
|
|
foreach ($groups as $numGroup => $group) { |
112
|
|
|
for ($numFight = 1; $numFight <= $settings->preliminaryGroupSize; $numFight++) { |
113
|
|
|
$fight = new PreliminaryFight(); |
114
|
|
|
$order = $numGroup * $settings->preliminaryGroupSize + $numFight; |
115
|
|
|
$fight->saveFight2($group, $numFight, $order); |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
$initialRound++; |
119
|
|
|
} |
120
|
|
|
// Save Next rounds |
121
|
|
|
$fight = new SingleEliminationFight(); |
122
|
|
|
$fight->saveFights($this->championship, $initialRound); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Return number of rounds for the tree based on fighter count. |
127
|
|
|
* |
128
|
|
|
* @param $numFighters |
129
|
|
|
* |
130
|
|
|
* @return int |
131
|
|
|
*/ |
132
|
|
|
protected function getNumRounds($numFighters) |
133
|
|
|
{ |
134
|
|
|
return intval(log($numFighters / $this->firstRoundGroupSize() * 2, 2)); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Get the group size for the first row |
139
|
|
|
* |
140
|
|
|
* @return int - return 2 if not preliminary, 3,4,5 otherwise |
141
|
|
|
*/ |
142
|
|
|
private function firstRoundGroupSize() |
143
|
|
|
{ |
144
|
|
|
return $this->championship->hasPreliminary() |
145
|
|
|
? $this->settings->preliminaryGroupSize |
146
|
|
|
: 2; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Generate all the groups, and assign figthers to group |
151
|
|
|
* @throws TreeGenerationException |
152
|
|
|
*/ |
153
|
|
|
protected function generateAllTrees() |
154
|
|
|
{ |
155
|
|
|
$this->minFightersCheck(); // Check there is enough fighters to generate trees |
156
|
|
|
$usersByArea = $this->getFightersByArea(); // Get fighters by area (reparted by entities and filled with byes) |
157
|
|
|
$this->generateGroupsForRound($usersByArea, 1); // Generate all groups for round 1 |
158
|
|
|
$numFighters = count($usersByArea->collapse()); |
159
|
|
|
$this->pushEmptyGroupsToTree($numFighters); // Fill tree with empty groups |
160
|
|
|
$this->addParentToChildren($numFighters); // Build the entire tree and fill the next rounds if possible |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* @param Collection $usersByArea |
165
|
|
|
* @param $round |
166
|
|
|
*/ |
167
|
|
|
public function generateGroupsForRound(Collection $usersByArea, $round) |
168
|
|
|
{ |
169
|
|
|
$order = 1; |
170
|
|
|
foreach ($usersByArea as $fightersByEntity) { |
171
|
|
|
// Chunking to make small round robin groups |
172
|
|
|
$chunkedFighters = $this->chunk($fightersByEntity); |
173
|
|
|
// dd($chunkedFighters->toArray()); |
|
|
|
|
174
|
|
|
foreach ($chunkedFighters as $fighters) { |
|
|
|
|
175
|
|
|
$fighters = $fighters->pluck('id'); |
176
|
|
|
$group = $this->saveGroup($order, $round, null); |
177
|
|
|
$this->syncGroup($group, $fighters); |
178
|
|
|
$order++; |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Check if there is enough fighters, throw exception otherwise. |
185
|
|
|
* |
186
|
|
|
* @throws TreeGenerationException |
187
|
|
|
*/ |
188
|
|
|
private function minFightersCheck() |
189
|
|
|
{ |
190
|
|
|
$fighters = $this->getFighters(); |
|
|
|
|
191
|
|
|
$areas = $this->settings->fightingAreas; |
192
|
|
|
$fighterType = $this->championship->category->isTeam |
|
|
|
|
193
|
|
|
? trans_choice('laravel-tournaments::core.team', 2) |
194
|
|
|
: trans_choice('laravel-tournaments::core.competitor', 2); |
195
|
|
|
|
196
|
|
|
$minFighterCount = $fighters->count() / $areas; |
197
|
|
|
|
198
|
|
|
if ($this->settings->hasPreliminary && $fighters->count() / ($this->settings->preliminaryGroupSize * $areas) < 1) { |
199
|
|
|
throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [ |
200
|
|
|
'number' => $this->settings->preliminaryGroupSize * $areas, |
201
|
|
|
'fighter_type' => $fighterType, |
202
|
|
|
]), 422); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
if ($minFighterCount < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) { |
206
|
|
|
throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [ |
207
|
|
|
'number' => ChampionshipSettings::MIN_COMPETITORS_BY_AREA, |
208
|
|
|
'fighter_type' => $fighterType, |
209
|
|
|
]), 422); |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Insert byes group alternated with full groups. |
215
|
|
|
* |
216
|
|
|
* @param Collection $fighters - List of fighters |
217
|
|
|
* @param integer $numByeTotal - Quantity of byes to insert |
218
|
|
|
* @return Collection - Full list of fighters including byes |
219
|
|
|
*/ |
220
|
|
|
private function insertByes(Collection $fighters, $numByeTotal) |
221
|
|
|
{ |
222
|
|
|
$bye = $this->createByeFighter(); |
223
|
|
|
$groupSize = $this->firstRoundGroupSize(); |
224
|
|
|
$frequency = $groupSize != 0 |
225
|
|
|
? (int)floor(count($fighters) / $groupSize / $groupSize) |
226
|
|
|
: -1; |
227
|
|
|
if ($frequency < $groupSize) { |
228
|
|
|
$frequency = $groupSize; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$newFighters = new Collection(); |
232
|
|
|
$count = 0; |
233
|
|
|
$byeCount = 0; |
234
|
|
|
foreach ($fighters as $fighter) { |
235
|
|
|
// Each $frequency(3) try to insert $groupSize byes (3) |
|
|
|
|
236
|
|
|
// Not the first iteration, and at the good frequency, and with $numByeTotal as limit |
237
|
|
|
if ($this->shouldInsertBye($frequency, $count, $byeCount, $numByeTotal)) { // |
238
|
|
|
for ($i = 0; $i < $groupSize; $i++) { |
239
|
|
|
if ($byeCount < $numByeTotal) { |
240
|
|
|
$newFighters->push($bye); |
241
|
|
|
$byeCount++; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
$newFighters->push($fighter); |
246
|
|
|
$count++; |
247
|
|
|
} |
248
|
|
|
return $newFighters; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* @param $frequency |
253
|
|
|
* @param $groupSize |
254
|
|
|
* @param $count |
255
|
|
|
* @param $byeCount |
256
|
|
|
* |
257
|
|
|
* @return bool |
258
|
|
|
*/ |
259
|
|
|
private |
260
|
|
|
function shouldInsertBye($frequency, $count, $byeCount, $numByeTotal): bool |
261
|
|
|
{ |
262
|
|
|
return $count != 0 && $count % $frequency == 0 && $byeCount < $numByeTotal; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Method that fills fighters with Bye Groups at the end |
268
|
|
|
* @param $fighters |
269
|
|
|
* @param Collection $fighterGroups |
270
|
|
|
* |
271
|
|
|
* @return Collection |
272
|
|
|
*/ |
273
|
|
|
public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection |
274
|
|
|
{ |
275
|
|
|
$tmpFighterGroups = clone $fighterGroups; |
276
|
|
|
$numBye = count($this->getByeGroup($fighters)); |
277
|
|
|
|
278
|
|
|
// Get biggest competitor's group |
279
|
|
|
$max = $this->getMaxFightersByEntity($tmpFighterGroups); |
280
|
|
|
|
281
|
|
|
// We put them so that we can mix them up and they don't fight with another competitor of his entity. |
282
|
|
|
$fighters = $this->repart($fighterGroups, $max); |
283
|
|
|
|
284
|
|
|
if (!app()->runningUnitTests()) { |
285
|
|
|
$fighters = $fighters->shuffle(); |
286
|
|
|
} |
287
|
|
|
// Insert byes to fill the tree. |
288
|
|
|
// Strategy: first, one group full, one group empty with byes, then groups of 2 fighters |
289
|
|
|
$fighters = $this->insertByes($fighters, $numBye); |
290
|
|
|
return $fighters; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Get the biggest entity group. |
295
|
|
|
* |
296
|
|
|
* @param $userGroups |
297
|
|
|
* |
298
|
|
|
* @return int |
299
|
|
|
*/ |
300
|
|
|
private |
301
|
|
|
function getMaxFightersByEntity($userGroups): int |
302
|
|
|
{ |
303
|
|
|
return $userGroups |
304
|
|
|
->sortByDesc(function ($group) { |
305
|
|
|
return $group->count(); |
306
|
|
|
}) |
307
|
|
|
->first() |
308
|
|
|
->count(); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Repart BYE in the tree,. |
313
|
|
|
* |
314
|
|
|
* @param $fighterGroups |
315
|
|
|
* @param int $max |
316
|
|
|
* |
317
|
|
|
* @return Collection |
318
|
|
|
*/ |
319
|
|
|
private |
320
|
|
|
function repart($fighterGroups, $max) |
321
|
|
|
{ |
322
|
|
|
$fighters = new Collection(); |
323
|
|
|
for ($i = 0; $i < $max; $i++) { |
324
|
|
|
foreach ($fighterGroups as $fighterGroup) { |
325
|
|
|
$fighter = $fighterGroup->values()->get($i); |
326
|
|
|
if ($fighter != null) { |
327
|
|
|
$fighters->push($fighter); |
328
|
|
|
} |
329
|
|
|
} |
330
|
|
|
} |
331
|
|
|
return $fighters; |
332
|
|
|
} |
333
|
|
|
} |
334
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.