Passed
Push — master ( 795926...7012fc )
by Tomáš
11:11
created

Generator::circle_saveBracket()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 9
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 4
rs 9.9666
1
<?php
2
3
namespace TournamentGenerator\Helpers;
4
5
use Exception;
6
use TournamentGenerator\Constants;
7
use TournamentGenerator\Game;
8
use TournamentGenerator\Group;
9
use TournamentGenerator\Interfaces\WithGeneratorSetters;
10
use TournamentGenerator\Interfaces\WithSkipSetters;
11
use TournamentGenerator\Team;
12
use TournamentGenerator\Traits\WithSkipSetters as WithSkipSettersTrait;
13
14
/**
15
 * Generator class is responsible for generating all different games in rounds.
16
 *
17
 * @author  Tomáš Vojík <[email protected]>
18
 *
19
 * @package TournamentGenerator\Helpers
20
 *
21
 * @since   0.3
22
 */
23
class Generator implements WithGeneratorSetters, WithSkipSetters
24
{
25
	use WithSkipSettersTrait;
26
27
	/** @var Group|null Group object */
28
	private ?Group $group;
29
	/** @var string Type of a round to create */
30
	private string $type = Constants::ROUND_ROBIN;
31
	/** @var int Number of teams in one game - 2/3/4 */
32
	private int $inGame = 2;
33
	/** @var int Maximum size of group before split */
34
	private int $maxSize = 4;
35
36
	/**
37
	 * Generator constructor.
38
	 *
39
	 * @param Group $group Group object to generate the games from
40
	 */
41 138
	public function __construct(Group $group) {
42 138
		$this->group = $group;
43 138
	}
44
45
	/**
46
	 * Get round type
47
	 *
48
	 * @return string
49
	 */
50 1
	public function getType() : string {
51 1
		return $this->type;
52
	}
53
54
	/**
55
	 * Set round type
56
	 *
57
	 * @param string $type
58
	 *
59
	 * @return $this
60
	 * @throws Exception
61
	 */
62 20
	public function setType(string $type = Constants::ROUND_ROBIN) : Generator {
63 20
		if (in_array($type, Constants::GroupTypes, true)) {
64 20
			$this->type = $type;
65
		}
66
		else {
67 1
			throw new Exception('Unknown group type: '.$type);
68
		}
69 20
		return $this;
70
	}
71
72
	/**
73
	 * Get how many teams are playing in one game
74
	 *
75
	 * @return int
76
	 */
77 76
	public function getInGame() : int {
78 76
		return $this->inGame;
79
	}
80
81
	/**
82
	 * Set how many teams are playing in one game
83
	 *
84
	 * @param int $inGame
85
	 *
86
	 * @return $this
87
	 * @throws Exception
88
	 */
89 53
	public function setInGame(int $inGame) : Generator {
90 53
		if ($inGame < 2 || $inGame > 4) {
91 1
			throw new Exception('Expected 2,3 or 4 as inGame '.$inGame.' given');
92
		}
93 53
		$this->inGame = $inGame;
94 53
		return $this;
95
	}
96
97
	/**
98
	 * Get tha maximum group size
99
	 *
100
	 * @return int
101
	 */
102 1
	public function getMaxSize() : int {
103 1
		return $this->maxSize;
104
	}
105
106
	/**
107
	 * Set the maximum group size
108
	 *
109
	 * @param int $size
110
	 *
111
	 * @return $this
112
	 * @throws Exception
113
	 */
114 8
	public function setMaxSize(int $size) : Generator {
115 8
		if ($size < 2) {
116 1
			throw new Exception('Max group size has to be at least 2, '.$size.' given');
117
		}
118 7
		$this->maxSize = $size;
119 7
		return $this;
120
	}
121
122
	/**
123
	 * Generate games for a round
124
	 *
125
	 * @return Game[] List of generated games
126
	 * @throws Exception
127
	 */
128 42
	public function genGames() : array {
129 42
		switch ($this->type) {
130
			case Constants::ROUND_ROBIN:
131 32
				$this->group->addGame(...$this->r_rGames());
0 ignored issues
show
Bug introduced by
The method addGame() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

131
				$this->group->/** @scrutinizer ignore-call */ 
132
                  addGame(...$this->r_rGames());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
132 32
				break;
133
			case Constants::ROUND_TWO:
134 9
				$this->two_twoGames();
135 8
				break;
136
			case Constants::ROUND_SPLIT:
137 5
				$this->cond_splitGames();
138 5
				break;
139
		}
140 41
		return $this->group->getGames();
141
	}
142
143
	/**
144
	 * Generate round-robin games
145
	 *
146
	 * @param array $teams
147
	 *
148
	 * @return Game[]
149
	 * @throws Exception
150
	 */
151 35
	protected function r_rGames(array $teams = []) : array {
152 35
		$games = [];
153 35
		if (count($teams) === 0) {
154 33
			$teams = $this->group->getTeams();
155
		}
156 35
		switch ($this->inGame) {
157 35
			case 2:
158 30
				$games = self::circle_genGames2($this->group, $teams);
0 ignored issues
show
Bug introduced by
It seems like $this->group can also be of type null; however, parameter $group of TournamentGenerator\Help...tor::circle_genGames2() does only seem to accept TournamentGenerator\Group, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
				$games = self::circle_genGames2(/** @scrutinizer ignore-type */ $this->group, $teams);
Loading history...
159 30
				break;
160 5
			case 3:
161 2
				$games = $this->r_r3Games($teams, $games);
162 2
				break;
163 3
			case 4:
164 3
				$games = $this->r_r4Games($teams, $games);
165 3
				break;
166
		}
167 35
		return $games;
168
	}
169
170
	/**
171
	 * Generate games for round robin and 2 teams in one game
172
	 *
173
	 * @param array $teams
174
	 * @param Group $group
175
	 *
176
	 * @return Game[]
177
	 * @throws Exception
178
	 */
179 35
	public static function circle_genGames2(Group $group, array $teams = []) : array {
180
181 35
		if (count($teams) % 2 !== 0) {
182 6
			$teams[] = Constants::DUMMY_TEAM;
183
		} // IF NOT EVEN NUMBER OF TEAMS, ADD DUMMY
184
185 35
		shuffle($teams); // SHUFFLE TEAMS FOR MORE RANDOMNESS
186
187 35
		$rounds = [];
188 35
		for ($i = 0; $i < count($teams) - 1; $i++) {
189 35
			$rounds[] = self::circle_saveBracket($teams, $group); // SAVE CURRENT ROUND
190
191 35
			$teams = self::circle_rotateBracket($teams); // ROTATE TEAMS IN BRACKET
192
		}
193
194 35
		return array_merge(...$rounds);
195
	}
196
197
	/**
198
	 * Get one generated round-robin round
199
	 *
200
	 * @param array $teams
201
	 * @param Group $group
202
	 *
203
	 * @return Game[]
204
	 * @throws Exception
205
	 */
206 35
	public static function circle_saveBracket(array $teams, Group $group) : array {
207
208 35
		$bracket = [];
209
210 35
		for ($i = 0; $i < count($teams) / 2; $i++) { // GO THROUGH HALF OF THE TEAMS
211
212 35
			$home = $teams[$i];
213 35
			$reverse = array_reverse($teams);
214 35
			$away = $reverse[$i];
215
216 35
			if ($home === Constants::DUMMY_TEAM || $away === Constants::DUMMY_TEAM) {
217 6
				continue;
218
			} // SKIP WHEN DUMMY_TEAM IS PRESENT
219
220 35
			$bracket[] = new Game([$home, $away], $group);
221
222
		}
223
224 35
		return $bracket;
225
226
	}
227
228
	/**
229
	 * Rotate array of teams
230
	 *
231
	 * @param array $teams
232
	 *
233
	 * @return array
234
	 */
235 35
	public static function circle_rotateBracket(array $teams) : array {
236
237 35
		$first = array_shift($teams); // THE FIRST TEAM REMAINS FIRST
238 35
		$last = array_shift($teams);  // THE SECOND TEAM MOVES TO LAST PLACE
239
240 35
		return array_merge([$first], $teams, [$last]); // MERGE BACK TOGETHER
241
242
	}
243
244
	/**
245
	 * Generate a round-robin for three teams in one game
246
	 *
247
	 * @param array     $teams
248
	 * @param array     $games
249
	 * @param Team|null $lockedTeam1
250
	 *
251
	 * @return array
252
	 * @throws Exception
253
	 */
254 5
	protected function r_r3Games(array $teams, array &$games, Team $lockedTeam1 = null) : array {
255 5
		$teamsB = $teams;
256 5
		$generatedGames = [];
257 5
		while (count($teamsB) >= 3) {
258 5
			$lockedTeam = array_shift($teamsB);
259 5
			$gamesTemp = self::circle_genGames2($this->group, $teamsB);
0 ignored issues
show
Bug introduced by
It seems like $this->group can also be of type null; however, parameter $group of TournamentGenerator\Help...tor::circle_genGames2() does only seem to accept TournamentGenerator\Group, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

259
			$gamesTemp = self::circle_genGames2(/** @scrutinizer ignore-type */ $this->group, $teamsB);
Loading history...
260 5
			foreach ($gamesTemp as $game) {
261 5
				if (isset($lockedTeam1)) {
262 3
					$game->addTeam($lockedTeam1);
263
				}
264 5
				$game->addTeam($lockedTeam);
265
			}
266 5
			$generatedGames[] = $gamesTemp;
267
		}
268 5
		$games = array_merge($games, ...$generatedGames);
269 5
		return $games;
270
	}
271
272
	/**
273
	 * Generate a round-robin for four teams in one game
274
	 *
275
	 * @param array $teams
276
	 * @param array $games
277
	 *
278
	 * @return array
279
	 * @throws Exception
280
	 */
281 3
	protected function r_r4Games(array $teams, array &$games) : array {
282 3
		$teamsB = $teams;
283 3
		$lockedTeam1 = array_shift($teamsB);
284 3
		while (count($teamsB) >= 4) {
285 3
			$this->r_r3Games($teamsB, $games, $lockedTeam1);
286 3
			$lockedTeam1 = array_shift($teamsB);
287
		}
288 3
		$games[] = new Game(array_merge([$lockedTeam1], $teamsB), $this->group);
0 ignored issues
show
Bug introduced by
It seems like $this->group can also be of type null; however, parameter $group of TournamentGenerator\Game::__construct() does only seem to accept TournamentGenerator\Group, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

288
		$games[] = new Game(array_merge([$lockedTeam1], $teamsB), /** @scrutinizer ignore-type */ $this->group);
Loading history...
289 3
		return $games;
290
	}
291
292
	/**
293
	 * Generates games for teams, where a team plays only against one other team
294
	 *
295
	 * @param array $teams
296
	 *
297
	 * @return Generator
298
	 * @throws Exception
299
	 */
300 9
	protected function two_twoGames(array $teams = []) : Generator {
301 9
		if (count($teams) === 0) {
302 9
			$teams = $this->group->getTeams();
303
		}
304 9
		$discard = [];
305 9
		shuffle($teams);
306 9
		$count = count($teams);
307 9
		while (count($teams) % $this->inGame !== 0) {
308 4
			$discard[] = array_shift($teams);
309
		}
310
311 9
		while (count($teams) > 0) {
312 9
			$tInGame = [];
313 9
			for ($i = 0; $i < $this->inGame; $i++) {
314 9
				$tInGame[] = array_shift($teams);
315
			}
316 9
			$this->group->game($tInGame);
317
		}
318
319 9
		if (!$this->allowSkip && count($discard) > 0) {
320 1
			throw new Exception('Couldn\'t make games with all teams. Expected k*'.$this->inGame.' teams '.$count.' teams given - discarting '.count($discard).' teams ('.implode(', ', $discard).') in group '.$this->group.' - allow skip '.($this->allowSkip ? 'True' : 'False'));
321
		}
322 8
		return $this;
323
	}
324
325
326
	/**
327
	 * Automatically split teams in a group
328
	 *
329
	 * @param array $teams
330
	 *
331
	 * @return Generator
332
	 * @throws Exception
333
	 */
334 5
	protected function cond_splitGames(array $teams = []) : Generator {
335 5
		$games = [];
336 5
		if (count($teams) === 0) {
337 5
			$teams = $this->group->getTeams();
338
		}
339
340 5
		if (count($teams) > $this->maxSize) {
341 3
			$groups = array_chunk($teams, (int) ceil(count($teams) / ceil(count($teams) / $this->maxSize))); // SPLIT TEAMS INTO GROUP OF MAXIMUM SIZE OF $this->maxSize
342 3
			foreach ($groups as $group) {
343 3
				$games[] = $this->r_rGames($group);
344
			}
345 3
			$g = 0;
346 3
			foreach ($games as $group) {
347 3
				$g += count($group);
348
			}
349 3
			while ($g > 0) {
350 3
				foreach ($games as $key => $group) {
351 3
					$this->group->addGame(array_shift($games[$key]));
352 3
					if (count($games[$key]) === 0) {
353 3
						unset($games[$key]);
354
					}
355 3
					$g--;
356
				}
357
			}
358 3
			return $this;
359
		}
360 2
		$this->group->addGame(...$this->r_rGames());
361
362 2
		return $this;
363
	}
364
365
	/**
366
	 * Sort games to minimize teams playing multiple games after one other
367
	 *
368
	 * @return array
369
	 */
370 23
	public function orderGames() : array {
371 23
		$sorter = new Sorter\Games($this->group);
0 ignored issues
show
Bug introduced by
It seems like $this->group can also be of type null; however, parameter $group of TournamentGenerator\Help...er\Games::__construct() does only seem to accept TournamentGenerator\Group, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

371
		$sorter = new Sorter\Games(/** @scrutinizer ignore-type */ $this->group);
Loading history...
372
373 23
		return $sorter->orderGames();
374
	}
375
376
}
377