Issues (24)

src/TreeGen/SingleEliminationTreeGen.php (2 issues)

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
    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
28
        return $this->createByeGroup($byeCount);
29
    }
30
31
    /**
32
     * Save Groups with their parent info.
33
     *
34
     * @param int $numRounds
35
     * @param int $numFighters
36
     */
37
    protected function pushGroups($numRounds, $numFighters)
38
    {
39
        // TODO Here is where you should change when enable several winners for preliminary
40
        for ($roundNumber = 2; $roundNumber <= $numRounds + 1; $roundNumber++) {
41
            // From last match to first match
42
            $maxMatches = ($numFighters / pow(2, $roundNumber));
43
44
            for ($matchNumber = 1; $matchNumber <= $maxMatches; $matchNumber++) {
45
                $fighters = $this->createByeGroup(2);
46
                $group = $this->saveGroup($matchNumber, $roundNumber, null);
47
                $this->syncGroup($group, $fighters);
48
            }
49
        }
50
        // Third place Group
51
        if ($numFighters >= $this->championship->getGroupSize() * 2) {
52
            $fighters = $this->createByeGroup(2);
53
54
            $group = $this->saveGroup($maxMatches, $numRounds, null);
55
56
            $this->syncGroup($group, $fighters);
57
        }
58
    }
59
60
    /**
61
     * Create empty groups after round 1.
62
     *
63
     * @param $numFighters
64
     */
65
    protected function pushEmptyGroupsToTree($numFighters)
66
    {
67
        if ($this->championship->hasPreliminary()) {
68
            /* Should add * prelimWinner but it add complexity about parent / children in tree
69
            */
70
            $numFightersElim = $numFighters / $this->settings->preliminaryGroupSize * 2;
71
            // We calculate how much rounds we will have
72
            $numRounds = intval(log($numFightersElim, 2)); // 3 rounds, but begining from round 2 ( ie => 4)
73
            return $this->pushGroups($numRounds, $numFightersElim);
74
        }
75
        // We calculate how much rounds we will have
76
        $numRounds = $this->getNumRounds($numFighters);
77
78
        return $this->pushGroups($numRounds, $numFighters);
79
    }
80
81
    /**
82
     * Chunk Fighters into groups for fighting, and optionnaly shuffle.
83
     *
84
     * @param $fightersByEntity
85
     *
86
     * @return Collection|null
87
     */
88
    protected function chunk(Collection $fightersByEntity)
89
    {
90
        //TODO Should Pull down to know if team or competitor
91
        if ($this->championship->hasPreliminary()) {
92
            return (new PlayOffCompetitorTreeGen($this->championship, null))->chunk($fightersByEntity);
93
        }
94
        $fightersGroup = $fightersByEntity->chunk(2);
95
96
        return $fightersGroup;
97
    }
98
99
    /**
100
     * Generate First Round Fights.
101
     */
102
    protected function generateFights()
103
    {
104
        //  First Round Fights
105
        $settings = $this->settings;
106
        $initialRound = 1;
107
        // Very specific case to common case : Preliminary with 3 fighters
108
        if ($this->championship->hasPreliminary() && $settings->preliminaryGroupSize == 3) {
109
            // First we make all first fights of all groups
110
            // Then we make all second fights of all groups
111
            // Then we make all third fights of all groups
112
            $groups = $this->championship->groupsByRound(1)->get();
113
            foreach ($groups as $numGroup => $group) {
114
                for ($numFight = 1; $numFight <= $settings->preliminaryGroupSize; $numFight++) {
115
                    $fight = new PreliminaryFight();
116
                    $order = $numGroup * $settings->preliminaryGroupSize + $numFight;
117
                    $fight->saveFight2($group, $numFight, $order);
118
                }
119
            }
120
            $initialRound++;
121
        }
122
        // Save Next rounds
123
        $fight = new SingleEliminationFight();
124
        $fight->saveFights($this->championship, $initialRound);
125
    }
126
127
    /**
128
     * Return number of rounds for the tree based on fighter count.
129
     *
130
     * @param $numFighters
131
     *
132
     * @return int
133
     */
134
    protected function getNumRounds($numFighters)
135
    {
136
        return intval(log($numFighters / $this->firstRoundGroupSize() * 2, 2));
137
    }
138
139
    /**
140
     * Get the group size for the first row.
141
     *
142
     * @return int - return 2 if not preliminary, 3,4,5 otherwise
143
     */
144
    private function firstRoundGroupSize()
145
    {
146
        return $this->championship->hasPreliminary()
147
            ? $this->settings->preliminaryGroupSize
148
            : 2;
149
    }
150
151
    /**
152
     * Generate all the groups, and assign figthers to group.
153
     *
154
     * @throws TreeGenerationException
155
     */
156
    protected function generateAllTrees()
157
    {
158
        $this->minFightersCheck(); // Check there is enough fighters to generate trees
159
        $usersByArea = $this->getFightersByArea(); // Get fighters by area (reparted by entities and filled with byes)
160
        $this->generateGroupsForRound($usersByArea, 1); // Generate all groups for round 1
161
        $numFighters = count($usersByArea->collapse());
162
        $this->pushEmptyGroupsToTree($numFighters); // Fill tree with empty groups
163
        $this->addParentToChildren($numFighters); // Build the entire tree and fill the next rounds if possible
164
    }
165
166
    /**
167
     * @param Collection $usersByArea
168
     * @param $round
169
     */
170
    public function generateGroupsForRound(Collection $usersByArea, $round)
171
    {
172
        $order = 1;
173
        foreach ($usersByArea as $fightersByEntity) {
174
            // Chunking to make small round robin groups
175
            $chunkedFighters = $this->chunk($fightersByEntity);
176
//            dd($chunkedFighters->toArray());
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
177
            foreach ($chunkedFighters as $fighters) {
178
                $fighters = $fighters->pluck('id');
179
                $group = $this->saveGroup($order, $round, null);
180
                $this->syncGroup($group, $fighters);
181
                $order++;
182
            }
183
        }
184
    }
185
186
    /**
187
     * Check if there is enough fighters, throw exception otherwise.
188
     *
189
     * @throws TreeGenerationException
190
     */
191
    private function minFightersCheck()
192
    {
193
        $fighters = $this->getFighters();
194
        $areas = $this->settings->fightingAreas;
195
        $fighterType = $this->championship->category->isTeam
196
            ? trans_choice('laravel-tournaments::core.team', 2)
197
            : trans_choice('laravel-tournaments::core.competitor', 2);
198
199
        $minFighterCount = $fighters->count() / $areas;
200
201
        if ($this->settings->hasPreliminary && $fighters->count() / ($this->settings->preliminaryGroupSize * $areas) < 1) {
202
            throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [
203
                'number'       => $this->settings->preliminaryGroupSize * $areas,
204
                'fighter_type' => $fighterType,
205
            ]), 422);
206
        }
207
208
        if ($minFighterCount < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
209
            throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [
210
                'number'       => ChampionshipSettings::MIN_COMPETITORS_BY_AREA,
211
                'fighter_type' => $fighterType,
212
            ]), 422);
213
        }
214
    }
215
216
    /**
217
     * Insert byes group alternated with full groups.
218
     *
219
     * @param Collection $fighters    - List of fighters
220
     * @param int        $numByeTotal - Quantity of byes to insert
221
     *
222
     * @return Collection - Full list of fighters including byes
223
     */
224
    private function insertByes(Collection $fighters, $numByeTotal)
225
    {
226
        $bye = $this->createByeFighter();
227
        $groupSize = $this->firstRoundGroupSize();
228
        $frequency = $groupSize != 0
229
            ? (int) floor(count($fighters) / $groupSize / $groupSize)
230
            : -1;
231
        if ($frequency < $groupSize) {
232
            $frequency = $groupSize;
233
        }
234
235
        $newFighters = new Collection();
236
        $count = 0;
237
        $byeCount = 0;
238
        foreach ($fighters as $fighter) {
239
            // Each $frequency(3) try to insert $groupSize byes (3)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
240
            // Not the first iteration, and at the good frequency, and with $numByeTotal as limit
241
            if ($this->shouldInsertBye($frequency, $count, $byeCount, $numByeTotal)) { //
242
                for ($i = 0; $i < $groupSize; $i++) {
243
                    if ($byeCount < $numByeTotal) {
244
                        $newFighters->push($bye);
245
                        $byeCount++;
246
                    }
247
                }
248
            }
249
            $newFighters->push($fighter);
250
            $count++;
251
        }
252
253
        return $newFighters;
254
    }
255
256
    /**
257
     * @param $frequency
258
     * @param $groupSize
259
     * @param $count
260
     * @param $byeCount
261
     *
262
     * @return bool
263
     */
264
    private function shouldInsertBye($frequency, $count, $byeCount, $numByeTotal): bool
265
    {
266
        return $count != 0 && $count % $frequency == 0 && $byeCount < $numByeTotal;
267
    }
268
269
    /**
270
     * Method that fills fighters with Bye Groups at the end.
271
     *
272
     * @param $fighters
273
     * @param Collection $fighterGroups
274
     *
275
     * @return Collection
276
     */
277
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
278
    {
279
        $tmpFighterGroups = clone $fighterGroups;
280
        $numBye = count($this->getByeGroup($fighters));
281
282
        // Get biggest competitor's group
283
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
284
285
        // We put them so that we can mix them up and they don't fight with another competitor of his entity.
286
        $fighters = $this->repart($fighterGroups, $max);
287
288
        if (!app()->runningUnitTests()) {
289
            $fighters = $fighters->shuffle();
290
        }
291
        // Insert byes to fill the tree.
292
        // Strategy: first, one group full, one group empty with byes, then groups of 2 fighters
293
        $fighters = $this->insertByes($fighters, $numBye);
294
295
        return $fighters;
296
    }
297
298
    /**
299
     * Get the biggest entity group.
300
     *
301
     * @param $userGroups
302
     *
303
     * @return int
304
     */
305
    private function getMaxFightersByEntity($userGroups): int
306
    {
307
        return $userGroups
308
            ->sortByDesc(function ($group) {
309
                return $group->count();
310
            })
311
            ->first()
312
            ->count();
313
    }
314
315
    /**
316
     * Repart BYE in the tree,.
317
     *
318
     * @param $fighterGroups
319
     * @param int $max
320
     *
321
     * @return Collection
322
     */
323
    private function repart($fighterGroups, $max)
324
    {
325
        $fighters = new Collection();
326
        for ($i = 0; $i < $max; $i++) {
327
            foreach ($fighterGroups as $fighterGroup) {
328
                $fighter = $fighterGroup->values()->get($i);
329
                if ($fighter != null) {
330
                    $fighters->push($fighter);
331
                }
332
            }
333
        }
334
335
        return $fighters;
336
    }
337
}
338