Generator::r_r4Games()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 9
ccs 8
cts 8
cp 1
crap 2
rs 10
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 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 258
	public function __construct(Group $group) {
42 258
		$this->group = $group;
43 258
	}
44
45
	/**
46
	 * Get round type
47
	 *
48
	 * @return string
49
	 */
50 22
	public function getType() : string {
51 22
		return $this->type;
52
	}
53
54
	/**
55
	 * Set round type
56
	 *
57
	 * @param string $type
58
	 *
59
	 * @return $this
60
	 * @throws Exception
61
	 */
62 59
	public function setType(string $type = Constants::ROUND_ROBIN) : Generator {
63 59
		if (in_array($type, Constants::GroupTypes, true)) {
64 59
			$this->type = $type;
65
		}
66
		else {
67 1
			throw new Exception('Unknown group type: '.$type);
68
		}
69 59
		return $this;
70
	}
71
72
	/**
73
	 * Get how many teams are playing in one game
74
	 *
75
	 * @return int
76
	 */
77 119
	public function getInGame() : int {
78 119
		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 92
	public function setInGame(int $inGame) : Generator {
90 92
		if ($inGame < 2 || $inGame > 4) {
91 1
			throw new Exception('Expected 2,3 or 4 as inGame '.$inGame.' given');
92
		}
93 92
		$this->inGame = $inGame;
94 92
		return $this;
95
	}
96
97
	/**
98
	 * Get tha maximum group size
99
	 *
100
	 * @return int
101
	 */
102 22
	public function getMaxSize() : int {
103 22
		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 25
	public function setMaxSize(int $size) : Generator {
115 25
		if ($size < 2) {
116 1
			throw new Exception('Max group size has to be at least 2, '.$size.' given');
117
		}
118 24
		$this->maxSize = $size;
119 24
		return $this;
120
	}
121
122
	/**
123
	 * Generate games for a round
124
	 *
125
	 * @return Game[] List of generated games
126
	 * @throws Exception
127
	 */
128 65
	public function genGames() : array {
129 65
		switch ($this->type) {
130
			case Constants::ROUND_ROBIN:
131 34
				$this->group->addGame(...$this->r_rGames());
132 34
				break;
133
			case Constants::ROUND_TWO:
134 30
				$this->two_twoGames();
135 29
				break;
136
			case Constants::ROUND_SPLIT:
137 5
				$this->cond_splitGames();
138 5
				break;
139
		}
140 64
		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 37
	protected function r_rGames(array $teams = []) : array {
152 37
		$games = [];
153 37
		if (count($teams) === 0) {
154 35
			$teams = $this->group->getTeams();
155
		}
156 37
		switch ($this->inGame) {
157 37
			case 2:
158 32
				$games = self::circle_genGames2($this->group, $teams);
159 32
				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 37
		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 37
	public static function circle_genGames2(Group $group, array $teams = []) : array {
180
181 37
		if (count($teams) % 2 !== 0) {
182 7
			$teams[] = Constants::DUMMY_TEAM;
183
		} // IF NOT EVEN NUMBER OF TEAMS, ADD DUMMY
184
185 37
		shuffle($teams); // SHUFFLE TEAMS FOR MORE RANDOMNESS
186
187 37
		$rounds = [];
188 37
		for ($i = 0; $i < count($teams) - 1; $i++) {
189 37
			$rounds[] = self::circle_saveBracket($teams, $group); // SAVE CURRENT ROUND
190
191 37
			$teams = self::circle_rotateBracket($teams); // ROTATE TEAMS IN BRACKET
192
		}
193
194 37
		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 37
	public static function circle_saveBracket(array $teams, Group $group) : array {
207
208 37
		$bracket = [];
209
210 37
		for ($i = 0; $i < count($teams) / 2; $i++) { // GO THROUGH HALF OF THE TEAMS
211
212 37
			$home = $teams[$i];
213 37
			$reverse = array_reverse($teams);
214 37
			$away = $reverse[$i];
215
216 37
			if ($home === Constants::DUMMY_TEAM || $away === Constants::DUMMY_TEAM) {
217 7
				continue;
218
			} // SKIP WHEN DUMMY_TEAM IS PRESENT
219
220 37
			$bracket[] = new Game([$home, $away], $group);
221
222
		}
223
224 37
		return $bracket;
225
226
	}
227
228
	/**
229
	 * Rotate array of teams
230
	 *
231
	 * @param array $teams
232
	 *
233
	 * @return array
234
	 */
235 37
	public static function circle_rotateBracket(array $teams) : array {
236
237 37
		$first = array_shift($teams); // THE FIRST TEAM REMAINS FIRST
238 37
		$last = array_shift($teams);  // THE SECOND TEAM MOVES TO LAST PLACE
239
240 37
		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);
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);
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 30
	protected function two_twoGames(array $teams = []) : Generator {
301 30
		if (count($teams) === 0) {
302 30
			$teams = $this->group->getTeams();
303
		}
304 30
		shuffle($teams);
305 30
		$count = count($teams); // Store original count for exception
306
307 30
		$discard = array_splice($teams, 0, $count % $this->inGame);
308
309 30
		$teams = $this->saveTwoTwoGames($teams);
0 ignored issues
show
Unused Code introduced by
The assignment to $teams is dead and can be removed.
Loading history...
310
311 30
		if (!$this->allowSkip && count($discard) > 0) {
312 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'));
313
		}
314 29
		return $this;
315
	}
316
317
	/**
318
	 * Create games from array of teams for a Two-Two game
319
	 *
320
	 * @param Team[] $teams
321
	 *
322
	 * @pre The input array must be divisible by the Generator::$inGame value
323
	 *
324
	 * @return array
325
	 * @throws Exception
326
	 */
327 30
	protected function saveTwoTwoGames(array $teams) : array {
328 30
		while (count($teams) > 0) {
329 30
			$tInGame = [];
330 30
			for ($i = 0; $i < $this->inGame; $i++) {
331 30
				$tInGame[] = array_shift($teams);
332
			}
333 30
			$this->group->game($tInGame);
334
		}
335 30
		return $teams;
336
	}
337
338
	/**
339
	 * Automatically split teams in a group
340
	 *
341
	 * @param array $teams
342
	 *
343
	 * @return Generator
344
	 * @throws Exception
345
	 */
346 5
	protected function cond_splitGames(array $teams = []) : Generator {
347 5
		if (count($teams) === 0) {
348 5
			$teams = $this->group->getTeams();
349
		}
350
351 5
		$count = count($teams);
352 5
		if ($count > $this->maxSize) {
353 3
			$games = [];
354
355
			// Split teams into chunks of maximum size
356 3
			$groups = array_chunk($teams, (int) ceil($count / ceil($count / $this->maxSize)));
357
			// Generate games for each chunk
358 3
			foreach ($groups as $group) {
359 3
				$games[] = $this->r_rGames($group);
360
			}
361
362 3
			$this->fillGamesFromChunks($games);
363
364 3
			return $this;
365
		}
366 2
		$this->group->addGame(...$this->r_rGames());
367
368 2
		return $this;
369
	}
370
371
	/**
372
	 * Add games from chunks to groups
373
	 *
374
	 * Fills game in alternating order (chunk 1 - 1, chunk 2 - 1 , chunk 3 - 1, chunk 1 - 2, chunk 2 - 2...)
375
	 *
376
	 * @param Game[][] $games
377
	 *
378
	 * @throws Exception
379
	 */
380 3
	protected function fillGamesFromChunks(array $games) : void {
381 3
		$gameCount = Functions::nestedCount($games);
382 3
		while ($gameCount > 0) {
383 3
			foreach ($games as $key => $group) {
384 3
				$this->group->addGame(array_shift($games[$key]));
385 3
				if (count($games[$key]) === 0) {
386 3
					unset($games[$key]);
387
				}
388 3
				$gameCount--;
389
			}
390
		}
391 3
	}
392
393
	/**
394
	 * Sort games to minimize teams playing multiple games after one other
395
	 *
396
	 * @pre  The autoincrement must be reset on the group's container
397
	 * @post All games will have reset ids in the new order
398
	 *
399
	 * @return array
400
	 * @throws Exception
401
	 */
402 5
	public function orderGames() : array {
403 5
		$sorter = new Sorter\GameSorter($this->group);
404
405 5
		return $sorter->sort($this->group->getGames());
406
	}
407
408
}
409