DoubleElimination::generateWinSide()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 13
nc 2
nop 9
dl 0
loc 19
ccs 13
cts 13
cp 1
crap 2
rs 9.8333
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
namespace TournamentGenerator\Preset;
4
5
use Exception;
6
use TournamentGenerator\Constants;
7
use TournamentGenerator\Group;
8
use TournamentGenerator\Helpers\Functions;
9
use TournamentGenerator\Round;
10
use TournamentGenerator\Team;
11
use TournamentGenerator\TeamFilter;
12
use TournamentGenerator\Tournament;
13
14
/**
15
 * Double elimination generator
16
 *
17
 * @author  Tomáš Vojík <[email protected]>
18
 * @package TournamentGenerator\Preset
19
 * @since   0.1
20
 */
21
class DoubleElimination extends Tournament implements Preset
22
{
23
24
	/**
25
	 * Generate all the games
26
	 *
27
	 * @return $this
28
	 * @throws Exception
29
	 */
30 15
	public function generate() : DoubleElimination {
31 15
		$this->allowSkip();
32
33 15
		$countTeams = count($this->getTeams());
34 15
		if ($countTeams < 3) {
35 1
			throw new Exception('Double elimination is possible for minimum of 3 teams - '.$countTeams.' teams given.');
36
		}
37
38
		// CALCULATE BYES
39 14
		$nextPow = 0;
40 14
		$byes = $this->calcByes($countTeams, $nextPow);
41
		/** If an extra winning round is generated first */
42 14
		$extraStart = $byes > 0;
43
44 14
		$startRound = $this->round('Start round');
45
46
		/** Total round count (minus final rounds) */
47 14
		$roundsNum = log($nextPow, 2) * 2;
48
49
		/** How many groups are in the first round */
50 14
		$startGroups = ($countTeams + $byes) / 2;
51
52
		/** How many losing teams there are after the first winning rounds */
53 14
		$losingTeams = (($countTeams - $byes) / 2) + ($extraStart ? $startGroups / 2 : 0);
54
		/** If an extra losing round is generated first */
55 14
		$extraLosingStart = !Functions::isPowerOf2($losingTeams);
56
57 14
		if ($extraLosingStart) {
58 8
			$roundsNum++;
59
		}
60
61 14
		$previousLosingGroups = [];
62 14
		$previousWinningGroups = [];
63 14
		$allGroups = [];
64
65
		// First round's groups
66 14
		for ($i = 1; $i <= $startGroups; $i++) {
67 14
			$g = $startRound->group('Start group ('.$i.')')->setInGame(2)->setType(Constants::ROUND_TWO);
68 14
			$allGroups[] = $g;
69
		}
70 14
		$previousGroups = $allGroups;
71
72
		// Split teams
73 14
		$this->splitTeamsEvenly();
74
75
		/** Counter for winning rounds only */
76 14
		$winR = 2;
77
78
		// Create an extra starting winning round.
79
		// This needs to be created because the first round will skip a lot of games if there are any byes.
80 14
		if ($extraStart) {
81 11
			$startRound = $this->round('Start round (2)');
82 11
			$groups = [];
83 11
			$winningGroups = [];
84 11
			$this->generateWinSide(2, $winR++, $byes, $countTeams, $startRound, $allGroups, $groups, $winningGroups, $previousGroups);
85 11
			$previousWinningGroups = $winningGroups;
86
		}
87
88 14
		$previousGroups = $allGroups;
89
90
		/** @var Group|null $lastLosingGroup The last group from the loser's side, to progress to the final round */
91 14
		$lastLosingGroup = null;
92
		/** @var Group $lastLosingGroup The last group from the winner's side, to progress to the final round */
93 14
		$lastWinningGroup = end($allGroups);
94
95
		// Create all rounds
96 14
		for ($r = $winR; $r <= $roundsNum - 1; $r++) {
97 14
			$groups = [];
98 14
			$losingGroups = [];
99 14
			$winningGroups = [];
100 14
			$round = $this->round('Round '.$r);
101
102
			// Always generate a losing side
103 14
			$this->generateLosingSide($r, $extraStart, $round, $allGroups, $previousLosingGroups, $previousGroups, $losingGroups);
104
105
			// Skip some winning rounds - losing side will have more rounds
106 14
			$rr = $r - ($extraStart ? 1 : 0) + ($extraLosingStart ? 1 : 0);
107 14
			if (($rr < 3 || $rr % 2 === 0) && (!$extraStart || count($previousWinningGroups) > 1)) {
108
				// First round after the starting rounds
109 13
				if ($extraStart && $r === 3) {
110 2
					$previousGroups = $previousWinningGroups;
111
				}
112
				/** @noinspection SlowArrayOperationsInLoopInspection */
113 13
				$previousGroups = array_merge($previousGroups, $previousWinningGroups);
114 13
				$this->generateWinSide($r, $winR++, $byes, $countTeams, $round, $allGroups, $groups, $winningGroups, $previousGroups);
115 13
				$previousWinningGroups = $winningGroups;
116
			}
117
118
			// Save last generated groups for next round's
119 14
			if (count($winningGroups) > 0) {
120 13
				$lastWinningGroup = end($winningGroups);
121
			}
122 14
			if (count($losingGroups) > 0) {
123 14
				$lastLosingGroup = end($losingGroups);
124
			}
125
126 14
			$previousGroups = $groups;
127 14
			$previousLosingGroups = $losingGroups;
128
		}
129
130
		// Final round
131 14
		$round = $this->round('Round '.$roundsNum.' - Finale');
132 14
		$groupFinal = $round->group('Round '.$r.' - finale')->setInGame(2)->setType(Constants::ROUND_TWO)->setOrder(1);
133 14
		$allGroups[] = $groupFinal;
134 14
		if (isset($lastLosingGroup)) {
135 14
			$lastLosingGroup->progression($groupFinal, 0, 1);
136
		}
137 14
		if (isset($lastWinningGroup)) {
138 14
			$lastWinningGroup->progression($groupFinal, 0, 1);
139
		}
140
141
		// Repeat the game if the winning team loses
142 14
		$group = $round->group('Round '.$r.' - finale (2)')->setInGame(2)->setType(Constants::ROUND_TWO)->setOrder(1);
143 14
		$twoLoss = new TeamFilter('losses', '=', 1, $allGroups);
144 14
		$groupFinal->progression($group, 0, 2)->addFilter($twoLoss);
145
146 14
		return $this;
147
	}
148
149
	/**
150
	 * Calculate how many teams should skip the first round
151
	 *
152
	 * @param int $countTeams Total teams
153
	 * @param int $nextPow    Next power of 2
154
	 *
155
	 * @return float|int
156
	 */
157 14
	private function calcByes(int $countTeams, int &$nextPow) {
158 14
		$byes = 0;
159 14
		$nextPow = $countTeams;
160 14
		if (!Functions::isPowerOf2($countTeams)) {
161 11
			$nextPow = Functions::nextPowerOf2($countTeams);
162 11
			$byes = $nextPow - $countTeams;
163
		}
164 14
		return $byes;
165
	}
166
167
	/**
168
	 * Generate the winning side (Single elimination with progressions into the losing side)
169
	 *
170
	 * @param int     $roundNum              Round number
171
	 * @param int     $winRoundNum           Real winning side round counter
172
	 * @param int     $byes                  Initial byes
173
	 * @param int     $countTeams            Total teams
174
	 * @param Round   $round                 Round object
175
	 * @param Group[] $allGroups             All groups
176
	 * @param Group[] $groups                Output groups
177
	 * @param Group[] $previousWinningGroups Winning side groups
178
	 * @param Group[] $previousGroups        Losing side groups
179
	 *
180
	 * @return void
181
	 * @throws Exception
182
	 */
183 14
	private function generateWinSide(int $roundNum, int $winRoundNum, int $byes, int $countTeams, Round $round, array &$allGroups, array &$groups, array &$previousWinningGroups = [], array $previousGroups = []) : void {
184 14
		$order = 1;
185
		// All groups
186 14
		for ($g = 1; $g <= (($countTeams + $byes) / (2 ** $winRoundNum)); $g++) {
187
			$group = $round
188 14
				->group('Round '.$roundNum.' (win '.$g.')')
189 14
				->setInGame(2)
190 14
				->setType(Constants::ROUND_TWO)
191 14
				->setOrder($order);
192 14
			$allGroups[] = $group;
193 14
			$order += 2;
194 14
			$groups[] = $group;
195
196
			// Save the last winning groups for the final round
197 14
			$previousWinningGroups[] = $group;
198
199
			// Progress from winning groups before
200 14
			$previousGroups[2 * ($g - 1)]->progression($group, 0, 1);
201 14
			$previousGroups[(2 * ($g - 1)) + 1]->progression($group, 0, 1);
202
		}
203 14
	}
204
205
	/**
206
	 * Generate the "losing side" - same as Single elimination
207
	 *
208
	 * @param int     $roundNum              Round number
209
	 * @param bool    $extraStart            If there was an extra starting round (because of byes)
210
	 * @param Round   $round                 Round object
211
	 * @param Group[] $allGroups             Array of all groups
212
	 * @param Group[] $previousLosingGroups  Last losing round's groups
213
	 * @param Group[] $previousWinningGroups Last winning round's groups
214
	 * @param Group[] $losingGroups          Array to save generated groups for later reference
215
	 *
216
	 * @return void
217
	 * @throws Exception
218
	 */
219 14
	private function generateLosingSide(int $roundNum, bool $extraStart, Round $round, array &$allGroups, array $previousLosingGroups = [], array $previousWinningGroups = [], array &$losingGroups = []) : void {
220
		// Filter winning groups - remove the ones without a game
221 14
		foreach ($previousWinningGroups as $key => $group) {
222 14
			if (count($group->getTeams()) === 1) {
223 11
				unset($previousWinningGroups[$key]);
224
			}
225
		}
226
		// Reset keys
227 14
		$previousWinningGroups = array_values($previousWinningGroups);
228
229
		// Save counts
230 14
		$losingCount = count($previousLosingGroups);
231 14
		$winningCount = count($previousWinningGroups);
232 14
		$teamsTotal = $losingCount + $winningCount;
233
234
		// Merge all groups in an alternating order for progressions
235
		/** @var array[] $progressGroups 0: Group, 1: int - progression offset */
236 14
		$progressGroups = [];
237 14
		$losingKey = 0;
238 14
		$winningKey = 0;
239 14
		while (count($progressGroups) < $teamsTotal && ($losingCount > $losingKey || $winningCount > $winningKey)) {
240 14
			if ($losingCount > $losingKey) {
241 13
				$progressGroups[] = [$previousLosingGroups[$losingKey++], 0];
242
			}
243 14
			if ($winningCount > $winningKey) {
244 14
				$progressGroups[] = [$previousWinningGroups[$winningKey++], 1];
245
			}
246
		}
247
248 14
		$order = 2;
249
		// Check byes
250 14
		if (Functions::isPowerOf2($teamsTotal)) {
251 14
			for ($g = 1; $g <= $teamsTotal / 2; $g++) {
252
				$group = $round
253 14
					->group('Round '.$roundNum.' (loss '.$g.')')
254 14
					->setInGame(2)
255 14
					->setType(Constants::ROUND_TWO)
256 14
					->setOrder($order);
257 14
				$allGroups[] = $group;
258 14
				$order += 2;
259 14
				$losingGroups[] = $group;
260
261
				// First losing round
262
				// Progress from winning teams only
263 14
				if (($roundNum === 2 && !$extraStart) || ($roundNum === 3 && $extraStart)) {
264 6
					$previousWinningGroups[2 * ($g - 1)]->progression($group, 1, 1);
265 6
					$previousWinningGroups[(2 * ($g - 1)) + 1]->progression($group, 1, 1);
266
				}
267 13
				elseif ($teamsTotal >= 2) {
268 13
					$key = 2 * ($g - 1);
269 13
					$progressGroups[$key][0]->progression($group, $progressGroups[$key][1], 1);
270 13
					$key++;
271 13
					$progressGroups[$key][0]->progression($group, $progressGroups[$key][1], 1);
272
				}
273
			}
274
		}
275
		else {
276
			// Calculate byes
277 10
			$nextPowerOf2 = Functions::nextPowerOf2($teamsTotal);
278 10
			$losingByes = $nextPowerOf2 - $teamsTotal;
279
280
			// Counters
281 10
			$byesProgressed = 0;
282 10
			$teamCounter = 0;
283
284
			// Generate groups
285 10
			$groupCount = $nextPowerOf2 / 2;
286 10
			for ($g = 1; $g <= $groupCount; $g++) {
287
				$group = $round
288 10
					->group('Round '.$roundNum.' (loss '.$g.')')
289 10
					->setInGame(2)
290 10
					->setType(Constants::ROUND_TWO)
291 10
					->setOrder($order);
292 10
				$allGroups[] = $group;
293 10
				$order += 2;
294 10
				$losingGroups[] = $group;
295
296
				// Create progressions from groups before
297 10
				$teamCounter++;
298 10
				$progressGroups[$byesProgressed][0]->progression($group, $progressGroups[$byesProgressed++][1], 1);
299 10
				if (isset($progressGroups[$byesProgressed]) && $teamCounter < $teamsTotal - $losingByes) {
300 10
					$teamCounter++;
301 10
					$progressGroups[$byesProgressed][0]->progression($group, $progressGroups[$byesProgressed++][1], 1);
302
				}
303
			}
304
		}
305 14
	}
306
307
	/**
308
	 * @return string
309
	 * @throws Exception
310
	 */
311 14
	public function printBracket() : string {
312 14
		$str = '';
313 14
		foreach ($this->getRounds() as $round) {
314 14
			$name = $round->getName();
315 14
			$len = strlen($name);
316 14
			$str .= "\n| ---------------------------------------- |\n| ".str_repeat('-', floor((40 - $len) / 2) - 1).' '.$name.' '.str_repeat('-', ceil((40 - $len) / 2) - 1)." |\n| ---------------------------------------- |\n\n";
317 14
			foreach ($round->getGroups() as $group) {
318 14
				$str .= '-- '.$group->getName().PHP_EOL;
319 14
				if (count($group->getGames()) === 0) {
320 14
					$str .= '| '.implode(' | ', array_map(static function(Team $team) {
321 12
							return $team->getName();
322 14
						}, $group->getTeams())).' |'.PHP_EOL;
323
				}
324
				else {
325 14
					foreach ($group->getGames() as $game) {
326 14
						$str .= '| '.implode(' | ', array_map(static function(Team $team) use ($game) {
327 14
								return ($team->getId() === $game->getWin() ? "\e[1m\e[4m" : '').$team->getName()."\e[0m";
328 14
							}, $game->getTeams())).' |'.(count($game->getDraw()) > 0 ? ' - draw' : '').PHP_EOL;
329
					}
330
				}
331
			}
332
		}
333 14
		return $str;
334
	}
335
336
}
337