DoubleElimination   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 313
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
eloc 153
dl 0
loc 313
ccs 154
cts 154
cp 1
rs 8.5599
c 0
b 0
f 0
wmc 48

5 Methods

Rating   Name   Duplication   Size   Complexity  
B printBracket() 0 23 7
F generate() 0 117 19
A calcByes() 0 8 2
D generateLosingSide() 0 83 18
A generateWinSide() 0 19 2

How to fix   Complexity   

Complex Class

Complex classes like DoubleElimination often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DoubleElimination, and based on these observations, apply Extract Interface, too.

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