Scrutinizer GitHub App not installed

We could not synchronize checks via GitHub's checks API since Scrutinizer's GitHub App is not installed for this repository.

Install GitHub App

SmrGalaxy   F
last analyzed

Complexity

Total Complexity 93

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Importance

Changes 4
Bugs 1 Features 2
Metric Value
eloc 175
dl 0
loc 397
rs 2
c 4
b 1
f 2
wmc 93

37 Methods

Rating   Name   Duplication   Size   Complexity  
A getLocations() 0 2 1
A setHeight() 0 6 3
B setConnectivity() 0 34 10
A __construct() 0 26 5
A getGalaxyMapHREF() 0 2 1
A getEndSector() 0 2 1
A clearCache() 0 3 1
A setWidth() 0 6 3
A setMaxForceTime() 0 6 3
A setName() 0 6 3
A getPlayers() 0 2 1
A getPorts() 0 2 1
C getMapSectors() 0 35 16
A saveGalaxies() 0 4 3
A getGalaxyType() 0 2 1
A generateSectors() 0 10 2
A getDisplayName() 0 2 1
A getGameGalaxies() 0 12 4
A getSize() 0 2 1
A getGameID() 0 2 1
A getForces() 0 2 1
A getSectors() 0 2 1
A getName() 0 2 1
A setGalaxyType() 0 6 3
A getStartSector() 0 11 4
A save() 0 21 3
A createGalaxy() 0 6 2
A getPlanets() 0 2 1
A getWidth() 0 2 1
A getGalaxyID() 0 2 1
A getHeight() 0 2 1
A getMaxForceTime() 0 2 1
A getGalaxy() 0 6 3
A getConnectivity() 0 7 2
A contains() 0 5 3
A equals() 0 2 2
A getGalaxyContaining() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like SmrGalaxy 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 SmrGalaxy, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
use Smr\Database;
4
use Smr\DatabaseRecord;
5
use Smr\Exceptions\GalaxyNotFound;
6
7
class SmrGalaxy {
8
9
	/** @var array<int, array<int, self>> */
10
	protected static array $CACHE_GALAXIES = [];
11
	/** @var array<int, array<int, self>> */
12
	protected static array $CACHE_GAME_GALAXIES = [];
13
14
	public const TYPE_RACIAL = 'Racial';
15
	public const TYPE_NEUTRAL = 'Neutral';
16
	public const TYPE_PLANET = 'Planet';
17
	public const TYPES = [self::TYPE_RACIAL, self::TYPE_NEUTRAL, self::TYPE_PLANET];
18
19
	protected Database $db;
20
	protected readonly string $SQL;
21
22
	protected string $name;
23
	protected int $width;
24
	protected int $height;
25
	protected string $galaxyType;
26
	protected int $maxForceTime;
27
28
	protected int $startSector;
29
30
	protected bool $hasChanged = false;
31
	protected bool $isNew;
32
33
	public static function clearCache(): void {
34
		self::$CACHE_GALAXIES = [];
35
		self::$CACHE_GAME_GALAXIES = [];
36
	}
37
38
	/**
39
	 * @return array<int, self>
40
	 */
41
	public static function getGameGalaxies(int $gameID, bool $forceUpdate = false): array {
42
		if ($forceUpdate || !isset(self::$CACHE_GAME_GALAXIES[$gameID])) {
43
			$db = Database::getInstance();
44
			$dbResult = $db->read('SELECT * FROM game_galaxy WHERE game_id = ' . $db->escapeNumber($gameID) . ' ORDER BY galaxy_id ASC');
45
			$galaxies = [];
46
			foreach ($dbResult->records() as $dbRecord) {
47
				$galaxyID = $dbRecord->getInt('galaxy_id');
48
				$galaxies[$galaxyID] = self::getGalaxy($gameID, $galaxyID, $forceUpdate, $dbRecord);
49
			}
50
			self::$CACHE_GAME_GALAXIES[$gameID] = $galaxies;
51
		}
52
		return self::$CACHE_GAME_GALAXIES[$gameID];
53
	}
54
55
	public static function getGalaxy(int $gameID, int $galaxyID, bool $forceUpdate = false, DatabaseRecord $dbRecord = null): self {
56
		if ($forceUpdate || !isset(self::$CACHE_GALAXIES[$gameID][$galaxyID])) {
57
			$g = new self($gameID, $galaxyID, false, $dbRecord);
58
			self::$CACHE_GALAXIES[$gameID][$galaxyID] = $g;
59
		}
60
		return self::$CACHE_GALAXIES[$gameID][$galaxyID];
61
	}
62
63
	public static function saveGalaxies(): void {
64
		foreach (self::$CACHE_GALAXIES as $gameGalaxies) {
65
			foreach ($gameGalaxies as $galaxy) {
66
				$galaxy->save();
67
			}
68
		}
69
	}
70
71
	public static function createGalaxy(int $gameID, int $galaxyID): self {
72
		if (!isset(self::$CACHE_GALAXIES[$gameID][$galaxyID])) {
73
			$g = new self($gameID, $galaxyID, true);
74
			self::$CACHE_GALAXIES[$gameID][$galaxyID] = $g;
75
		}
76
		return self::$CACHE_GALAXIES[$gameID][$galaxyID];
77
	}
78
79
	protected function __construct(
80
		protected readonly int $gameID,
81
		protected readonly int $galaxyID,
82
		bool $create = false,
83
		DatabaseRecord $dbRecord = null
84
	) {
85
		$this->db = Database::getInstance();
86
		$this->SQL = 'game_id = ' . $this->db->escapeNumber($gameID) . '
0 ignored issues
show
Bug introduced by
The property SQL is declared read-only in SmrGalaxy.
Loading history...
87
		              AND galaxy_id = ' . $this->db->escapeNumber($galaxyID);
88
89
		if ($dbRecord === null) {
90
			$dbResult = $this->db->read('SELECT * FROM game_galaxy WHERE ' . $this->SQL);
91
			if ($dbResult->hasRecord()) {
92
				$dbRecord = $dbResult->record();
93
			}
94
		}
95
		$this->isNew = $dbRecord === null;
96
97
		if (!$this->isNew) {
98
			$this->name = $dbRecord->getString('galaxy_name');
99
			$this->width = $dbRecord->getInt('width');
100
			$this->height = $dbRecord->getInt('height');
101
			$this->galaxyType = $dbRecord->getString('galaxy_type');
102
			$this->maxForceTime = $dbRecord->getInt('max_force_time');
103
		} elseif ($create === false) {
104
			throw new GalaxyNotFound('No such galaxy: ' . $gameID . '-' . $galaxyID);
105
		}
106
	}
107
108
	public function save(): void {
109
		if ($this->isNew) {
110
			$this->db->insert('game_galaxy', [
111
				'game_id' => $this->db->escapeNumber($this->getGameID()),
112
				'galaxy_id' => $this->db->escapeNumber($this->getGalaxyID()),
113
				'galaxy_name' => $this->db->escapeString($this->getName()),
114
				'width' => $this->db->escapeNumber($this->getWidth()),
115
				'height' => $this->db->escapeNumber($this->getHeight()),
116
				'galaxy_type' => $this->db->escapeString($this->getGalaxyType()),
117
				'max_force_time' => $this->db->escapeNumber($this->getMaxForceTime()),
118
			]);
119
		} elseif ($this->hasChanged) {
120
			$this->db->write('UPDATE game_galaxy SET galaxy_name = ' . $this->db->escapeString($this->getName()) .
121
										', width = ' . $this->db->escapeNumber($this->getWidth()) .
122
										', height = ' . $this->db->escapeNumber($this->getHeight()) .
123
										', galaxy_type = ' . $this->db->escapeString($this->getGalaxyType()) .
124
										', max_force_time = ' . $this->db->escapeNumber($this->getMaxForceTime()) .
125
									' WHERE ' . $this->SQL);
126
		}
127
		$this->isNew = false;
128
		$this->hasChanged = false;
129
	}
130
131
	public function getGameID(): int {
132
		return $this->gameID;
133
	}
134
135
	public function getGalaxyID(): int {
136
		return $this->galaxyID;
137
	}
138
139
	public function getGalaxyMapHREF(): string {
140
		return 'map_galaxy.php?galaxy_id=' . $this->getGalaxyID();
141
	}
142
143
	/**
144
	 * Returns the galaxy name.
145
	 * Use getDisplayName for an HTML-safe version.
146
	 */
147
	public function getName(): string {
148
		return $this->name;
149
	}
150
151
	/**
152
	 * Returns the galaxy name, suitable for HTML display.
153
	 */
154
	public function getDisplayName(): string {
155
		return htmlentities($this->name);
156
	}
157
158
	public function setName(string $name): void {
159
		if (!$this->isNew && $this->name === $name) {
160
			return;
161
		}
162
		$this->name = $name;
163
		$this->hasChanged = true;
164
	}
165
166
	public function getWidth(): int {
167
		return $this->width;
168
	}
169
170
	public function setWidth(int $width): void {
171
		if (!$this->isNew && $this->width === $width) {
172
			return;
173
		}
174
		$this->width = $width;
175
		$this->hasChanged = true;
176
	}
177
178
	public function getHeight(): int {
179
		return $this->height;
180
	}
181
182
	public function setHeight(int $height): void {
183
		if (!$this->isNew && $this->height === $height) {
184
			return;
185
		}
186
		$this->height = $height;
187
		$this->hasChanged = true;
188
	}
189
190
	public function getStartSector(): int {
191
		if (!isset($this->startSector)) {
192
			$this->startSector = 1;
193
			if ($this->galaxyID != 1) {
194
				$galaxies = self::getGameGalaxies($this->gameID);
195
				for ($i = 1; $i < $this->galaxyID; $i++) {
196
					$this->startSector += $galaxies[$i]->getSize();
197
				}
198
			}
199
		}
200
		return $this->startSector;
201
	}
202
203
	public function getEndSector(): int {
204
		return $this->getStartSector() + $this->getSize() - 1;
205
	}
206
207
	public function getSize(): int {
208
		return $this->getHeight() * $this->getWidth();
209
	}
210
211
	/**
212
	 * @return array<int, SmrSector>
213
	 */
214
	public function getSectors(): array {
215
		return SmrSector::getGalaxySectors($this->getGameID(), $this->getGalaxyID());
216
	}
217
218
	/**
219
	 * @return array<int, SmrPort>
220
	 */
221
	public function getPorts(): array {
222
		return SmrPort::getGalaxyPorts($this->getGameID(), $this->getGalaxyID());
223
	}
224
225
	/**
226
	 * @return array<int, array<int, SmrLocation>>
227
	 */
228
	public function getLocations(): array {
229
		return SmrLocation::getGalaxyLocations($this->getGameID(), $this->getGalaxyID());
230
	}
231
232
	/**
233
	 * @return array<int, SmrPlanet>
234
	 */
235
	public function getPlanets(): array {
236
		return SmrPlanet::getGalaxyPlanets($this->getGameID(), $this->getGalaxyID());
237
	}
238
239
	/**
240
	 * @return array<int, array<int, SmrForce>>
241
	 */
242
	public function getForces(): array {
243
		return SmrForce::getGalaxyForces($this->getGameID(), $this->getGalaxyID());
244
	}
245
246
	/**
247
	 * @return array<int, array<int, SmrPlayer>>
248
	 */
249
	public function getPlayers(): array {
250
		return SmrPlayer::getGalaxyPlayers($this->getGameID(), $this->getGalaxyID());
251
	}
252
253
	/**
254
	 * Returns a 2D array of sectors in the galaxy.
255
	 * If $centerSectorID is specified, it will be in the center of the array.
256
	 * If $dist is also specified, only include sectors $dist away from center.
257
	 *
258
	 * NOTE: This routine queries sectors inefficiently. You may want to
259
	 * construct the cache efficiently before calling this.
260
	 *
261
	 * @return array<int, array<int, SmrSector>>
262
	 */
263
	public function getMapSectors(int $centerSectorID = null, int $dist = null): array {
264
		if ($centerSectorID === null) {
265
			$topLeft = SmrSector::getSector($this->getGameID(), $this->getStartSector());
266
		} else {
267
			$topLeft = SmrSector::getSector($this->getGameID(), $centerSectorID);
268
			// go left then up
269
			$halfWidth = floor($this->width / 2);
270
			for ($i = 0; ($dist === null || $i < $dist) && $i < $halfWidth; $i++) {
271
				$topLeft = $topLeft->getNeighbourSector('Left');
272
			}
273
			$halfHeight = floor($this->height / 2);
274
			for ($i = 0; ($dist === null || $i < $dist) && $i < $halfHeight; $i++) {
275
				$topLeft = $topLeft->getNeighbourSector('Up');
276
			}
277
		}
278
279
		$mapSectors = [];
280
		$rowLeft = $topLeft;
281
		for ($i = 0; ($dist === null || $i < 2 * $dist + 1) && $i < $this->height; $i++) {
282
			$mapSectors[$i] = [];
283
			// get left most sector for this row
284
			if ($i > 0) {
285
				$rowLeft = $rowLeft->getNeighbourSector('Down');
286
			}
287
288
			// iterate through the columns
289
			$nextSector = $rowLeft;
290
			for ($j = 0; ($dist === null || $j < 2 * $dist + 1) && $j < $this->width; $j++) {
291
				if ($j > 0) {
292
					$nextSector = $nextSector->getNeighbourSector('Right');
293
				}
294
				$mapSectors[$i][$j] = $nextSector;
295
			}
296
		}
297
		return $mapSectors;
298
	}
299
300
	public function getGalaxyType(): string {
301
		return $this->galaxyType;
302
	}
303
304
	public function setGalaxyType(string $galaxyType): void {
305
		if (!$this->isNew && $this->galaxyType === $galaxyType) {
306
			return;
307
		}
308
		$this->galaxyType = $galaxyType;
309
		$this->hasChanged = true;
310
	}
311
312
	public function getMaxForceTime(): int {
313
		return $this->maxForceTime;
314
	}
315
316
	public function setMaxForceTime(int $maxForceTime): void {
317
		if (!$this->isNew && $this->maxForceTime === $maxForceTime) {
318
			return;
319
		}
320
		$this->maxForceTime = $maxForceTime;
321
		$this->hasChanged = true;
322
	}
323
324
	public function generateSectors(): void {
325
		$sectorID = $this->getStartSector();
326
		$galSize = $this->getSize();
327
		for ($i = 0; $i < $galSize; $i++) {
328
			$sector = SmrSector::createSector($this->gameID, $sectorID);
329
			$sector->setGalaxyID($this->getGalaxyID());
330
			$sector->update(); //Have to save sectors after creating them
331
			$sectorID++;
332
		}
333
		$this->setConnectivity(100);
334
	}
335
336
	/**
337
	 * Randomly set the connections between all galaxy sectors.
338
	 * $connectivity = (average) percent of connections to enable.
339
	 */
340
	public function setConnectivity(float $connectivity): bool {
341
		// Only set down/right, otherwise we double-hit every link
342
		$linkDirs = ['Down', 'Right'];
343
344
		$problem = true;
345
		$problemTimes = 0;
346
		while ($problem) {
347
			$problem = false;
348
349
			foreach ($this->getSectors() as $galSector) {
350
				foreach ($linkDirs as $linkDir) {
351
					if (rand(1, 100) <= $connectivity) {
352
						$galSector->enableLink($linkDir);
353
					} else {
354
						$galSector->disableLink($linkDir);
355
					}
356
				}
357
			}
358
359
			// Try again if any sector has 0 connections (except 1-sector gals)
360
			if ($this->getSize() > 1) {
361
				foreach ($this->getSectors() as $galSector) {
362
					if ($galSector->getNumberOfConnections() == 0) {
363
						$problem = true;
364
						break;
365
					}
366
				}
367
			}
368
369
			if ($problem && $problemTimes++ > 350) {
370
				$connectivity = 100;
371
			}
372
		}
373
		return $problemTimes <= 350;
374
	}
375
376
	/**
377
	 * Returns the sector connectivity of the galaxy as a percent.
378
	 */
379
	public function getConnectivity(): float {
380
		$totalLinks = 0;
381
		foreach ($this->getSectors() as $galSector) {
382
			$totalLinks += $galSector->getNumberOfLinks();
383
		}
384
		// There are 4 possible links per sector
385
		return 100 * $totalLinks / (4 * $this->getSize());
386
	}
387
388
	/**
389
	 * Check if the galaxy contains a specific sector.
390
	 */
391
	public function contains(int|SmrSector $sectorID): bool {
392
		if ($sectorID instanceof SmrSector) {
393
			return $sectorID->getGalaxyID() == $this->getGalaxyID();
394
		}
395
		return $sectorID >= $this->getStartSector() && $sectorID <= $this->getEndSector();
396
	}
397
398
	public static function getGalaxyContaining(int $gameID, int $sectorID): self {
399
		return SmrSector::getSector($gameID, $sectorID)->getGalaxy();
400
	}
401
402
	public function equals(SmrGalaxy $otherGalaxy): bool {
403
		return $otherGalaxy->getGalaxyID() == $this->getGalaxyID() && $otherGalaxy->getGameID() == $this->getGameID();
404
	}
405
406
}
407