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

Failed Conditions
Push — main ( 30fe2e...c58695 )
by Dan
04:45
created

AbstractSmrPortTest::test_addPortGood()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 9
rs 10
1
<?php declare(strict_types=1);
2
3
namespace SmrTest\lib\DefaultGame;
4
5
use AbstractSmrPort;
6
use Exception;
7
use PHPUnit\Framework\TestCase;
8
use Smr\BountyType;
9
use Smr\Container\DiContainer;
10
use Smr\Database;
11
use Smr\TransactionType;
12
use SmrGame;
13
use SmrPlayer;
14
use SmrSector;
15
16
/**
17
 * @covers AbstractSmrPort
18
 */
19
class AbstractSmrPortTest extends TestCase {
20
21
	protected function tearDown(): void {
22
		AbstractSmrPort::clearCache();
23
		DiContainer::initialize(false);
24
	}
25
26
	public static function tearDownAfterClass(): void {
27
		SmrSector::clearCache();
28
		SmrGame::clearCache();
29
	}
30
31
	public function test_new_port_does_not_exist_yet(): void {
32
		$port = AbstractSmrPort::createPort(1, 1);
33
		self::assertFalse($port->exists());
34
	}
35
36
	public function test_port_primary_keys(): void {
37
		// Ports are keyed on game ID and sector ID
38
		$gameID = 2;
39
		$sectorID = 3;
40
		$port = AbstractSmrPort::createPort($gameID, $sectorID);
41
		self::assertSame($gameID, $port->getGameID());
42
		self::assertSame($sectorID, $port->getSectorID());
43
	}
44
45
	public function test_setRaceID(): void {
46
		$port = AbstractSmrPort::createPort(1, 1);
47
		// New ports start as Neutral
48
		self::assertSame(RACE_NEUTRAL, $port->getRaceID());
49
		// Then check changing the race
50
		$port->setRaceID(RACE_HUMAN);
51
		self::assertSame(RACE_HUMAN, $port->getRaceID());
52
	}
53
54
	public function test_addPortGood(): void {
55
		// When we add the good
56
		$port = AbstractSmrPort::createPort(1, 1);
57
		$port->addPortGood(GOODS_ORE, TransactionType::Buy);
58
		// Insert smaller ID port good second to test sorting
59
		$port->addPortGood(GOODS_WOOD, TransactionType::Sell);
60
		self::assertSame([GOODS_WOOD, GOODS_ORE], $port->getAllGoodIDs());
61
		self::assertSame([GOODS_WOOD], $port->getSellGoodIDs());
62
		self::assertSame([GOODS_ORE], $port->getBuyGoodIDs());
63
	}
64
65
	/**
66
	 * @dataProvider provider_removePortGood
67
	 *
68
	 * @param array<int> $removeGoodIDs
69
	 * @param array<int> $sellRemain
70
	 * @param array<int> $buyRemain
71
	 */
72
	public function test_removePortGood(array $removeGoodIDs, array $sellRemain, array $buyRemain): void {
73
		// Set up a port with a couple goods
74
		$port = AbstractSmrPort::createPort(1, 1);
75
		$port->addPortGood(GOODS_WOOD, TransactionType::Sell);
76
		$port->addPortGood(GOODS_ORE, TransactionType::Buy);
77
		foreach ($removeGoodIDs as $goodID) {
78
			$port->removePortGood($goodID);
79
		}
80
		self::assertSame($sellRemain, $port->getSellGoodIDs());
81
		self::assertSame($buyRemain, $port->getBuyGoodIDs());
82
		self::assertSame(array_merge($sellRemain, $buyRemain), $port->getAllGoodIDs());
83
	}
84
85
	/**
86
	 * @return array<array<array<int>>>
87
	 */
88
	public function provider_removePortGood(): array {
89
		return [
90
			// Remove a good that the port doesn't have
91
			[[GOODS_CIRCUITRY], [GOODS_WOOD], [GOODS_ORE]],
92
			// Remove a buyable good
93
			[[GOODS_WOOD], [], [GOODS_ORE]],
94
			// Remove a sellable good
95
			[[GOODS_ORE], [GOODS_WOOD], []],
96
			// Remove both goods
97
			[[GOODS_WOOD, GOODS_ORE], [], []],
98
		];
99
	}
100
101
	/**
102
	 * @dataProvider provider_getGoodTransaction
103
	 */
104
	public function test_getGoodTransaction(TransactionType $transaction): void {
105
		$port = AbstractSmrPort::createPort(1, 1);
106
		$port->addPortGood(GOODS_ORE, $transaction);
107
		self::assertSame($transaction, $port->getGoodTransaction(GOODS_ORE));
108
	}
109
110
	/**
111
	 * @return array<array<TransactionType>>
112
	 */
113
	public function provider_getGoodTransaction(): array {
114
		return [[TransactionType::Buy], [TransactionType::Sell]];
115
	}
116
117
	public function test_getGoodTransaction_throws_if_port_does_not_have_good(): void {
118
		// New ports don't have any goods yet, so this will throw on any good
119
		$port = AbstractSmrPort::createPort(1, 1);
120
		$this->expectException(Exception::class);
121
		$this->expectExceptionMessage('Port does not trade goodID 3');
122
		$port->getGoodTransaction(GOODS_ORE);
123
	}
124
125
	public function test_setPortGoods(): void {
126
		$port = AbstractSmrPort::createPort(1, 1);
127
		$port->setLevel(1);
128
129
		// By default, a port's goods are empty
130
		self::assertSame([], $port->getGoodTransactions());
131
132
		// If we try to add an out-of-order good, we don't modify the port
133
		// and we return false.
134
		$badGoods = [
135
			GOODS_TEXTILES => TransactionType::Buy,
136
		];
137
		self::assertFalse($port->setPortGoods($badGoods));
138
		self::assertSame([], $port->getGoodTransactions());
139
140
		// If we add valid goods, the port is properly modified
141
		// (A level 1 port requires 3 goods)
142
		$validGoods = [
143
			GOODS_WOOD => TransactionType::Buy,
144
			GOODS_FOOD => TransactionType::Buy,
145
			GOODS_ORE => TransactionType::Sell,
146
		];
147
		self::assertTrue($port->setPortGoods($validGoods));
148
		self::assertSame($validGoods, $port->getGoodTransactions());
149
150
		// If we specify new goods, they are completely overriden
151
		$validGoods2 = [
152
			GOODS_WOOD => TransactionType::Sell, // opposite transaction
153
			GOODS_FOOD => TransactionType::Buy, // same transaction
154
			GOODS_SLAVES => TransactionType::Buy, // different good
155
		];
156
		self::assertTrue($port->setPortGoods($validGoods2));
157
		self::assertSame($validGoods2, $port->getGoodTransactions());
158
	}
159
160
	public function test_shields(): void {
161
		$port = AbstractSmrPort::createPort(1, 1);
162
		// newly created ports start with no shields
163
		self::assertSame(0, $port->getShields());
164
		self::assertFalse($port->hasShields());
165
166
		// Test setting shields explicitly
167
		$port->setShields(100);
168
		self::assertSame(100, $port->getShields());
169
		self::assertTrue($port->hasShields());
170
171
		// Test decreasing shields
172
		$port->decreaseShields(2);
173
		self::assertSame(98, $port->getShields());
174
	}
175
176
	public function test_cds(): void {
177
		$port = AbstractSmrPort::createPort(1, 1);
178
		// newly created ports start with no CDs
179
		self::assertSame(0, $port->getCDs());
180
		self::assertFalse($port->hasCDs());
181
182
		// Test setting CDs explicitly
183
		$port->setCDs(100);
184
		self::assertSame(100, $port->getCDs());
185
		self::assertTrue($port->hasCDs());
186
187
		// Test decreasing CDs
188
		$port->decreaseCDs(2);
189
		self::assertSame(98, $port->getCDs());
190
	}
191
192
	public function test_armour(): void {
193
		$port = AbstractSmrPort::createPort(1, 1);
194
		// newly created ports start with no armour
195
		self::assertSame(0, $port->getArmour());
196
		self::assertFalse($port->hasArmour());
197
198
		// Test setting shields explicitly
199
		$port->setArmour(100);
200
		self::assertSame(100, $port->getArmour());
201
		self::assertTrue($port->hasArmour());
202
203
		// Test decreasing shields
204
		$port->decreaseArmour(2);
205
		self::assertSame(98, $port->getArmour());
206
	}
207
208
	/**
209
	 * @dataProvider dataProvider_takeDamage
210
	 *
211
	 * @param array<string, int|bool> $damage
212
	 * @param array<string, int|bool> $expected
213
	 */
214
	public function test_takeDamage(string $case, array $damage, array $expected, int $shields, int $cds, int $armour): void {
215
		// Set up a port with a fixed amount of defenses
216
		$port = AbstractSmrPort::createPort(1, 1);
217
		$port->setShields($shields);
218
		$port->setCDs($cds);
219
		$port->setArmour($armour);
220
		// Test taking damage
221
		$result = $port->takeDamage($damage);
222
		self::assertSame($expected, $result, $case);
223
	}
224
225
	/**
226
	 * @return array<array<mixed>>
227
	 */
228
	public function dataProvider_takeDamage(): array {
229
		return [
230
			[
231
				'Do overkill damage (e.g. 1000 drone damage)',
232
				[
233
					'Shield' => 1000,
234
					'Armour' => 1000,
235
					'Rollover' => true,
236
				],
237
				[
238
					'KillingShot' => true,
239
					'TargetAlreadyDead' => false,
240
					'Shield' => 100,
241
					'CDs' => 30,
242
					'NumCDs' => 10,
243
					'HasCDs' => false,
244
					'Armour' => 100,
245
					'TotalDamage' => 230,
246
				],
247
				100, 10, 100,
248
			],
249
			[
250
				'Do exactly lethal damage (e.g. 230 drone damage)',
251
				[
252
					'Shield' => 230,
253
					'Armour' => 230,
254
					'Rollover' => true,
255
				],
256
				[
257
					'KillingShot' => true,
258
					'TargetAlreadyDead' => false,
259
					'Shield' => 100,
260
					'CDs' => 30,
261
					'NumCDs' => 10,
262
					'HasCDs' => false,
263
					'Armour' => 100,
264
					'TotalDamage' => 230,
265
				],
266
				100, 10, 100,
267
			],
268
			[
269
				'Do damage to drones behind shields (e.g. armour-only weapon)',
270
				[
271
					'Shield' => 0,
272
					'Armour' => 100,
273
					'Rollover' => false,
274
				],
275
				[
276
					'KillingShot' => false,
277
					'TargetAlreadyDead' => false,
278
					'Shield' => 0,
279
					'CDs' => 18,
280
					'NumCDs' => 6,
281
					'HasCDs' => true,
282
					'Armour' => 0,
283
					'TotalDamage' => 18,
284
				],
285
				100, 10, 100,
286
			],
287
			[
288
				'Do NOT do damage to armour behind shields (e.g. armour-only weapon)',
289
				[
290
					'Shield' => 0,
291
					'Armour' => 100,
292
					'Rollover' => false,
293
				],
294
				[
295
					'KillingShot' => false,
296
					'TargetAlreadyDead' => false,
297
					'Shield' => 0,
298
					'CDs' => 0,
299
					'NumCDs' => 0,
300
					'HasCDs' => false,
301
					'Armour' => 0,
302
					'TotalDamage' => 0,
303
				],
304
				100, 0, 100,
305
			],
306
			[
307
				'Overkill shield damage only (e.g. shield/armour weapon)',
308
				[
309
					'Shield' => 150,
310
					'Armour' => 150,
311
					'Rollover' => false,
312
				],
313
				[
314
					'KillingShot' => false,
315
					'TargetAlreadyDead' => false,
316
					'Shield' => 100,
317
					'CDs' => 0,
318
					'NumCDs' => 0,
319
					'HasCDs' => true,
320
					'Armour' => 0,
321
					'TotalDamage' => 100,
322
				],
323
				100, 10, 100,
324
			],
325
			[
326
				'Overkill CD damage only (e.g. shield/armour weapon)',
327
				[
328
					'Shield' => 150,
329
					'Armour' => 150,
330
					'Rollover' => false,
331
				],
332
				[
333
					'KillingShot' => false,
334
					'TargetAlreadyDead' => false,
335
					'Shield' => 0,
336
					'CDs' => 30,
337
					'NumCDs' => 10,
338
					'HasCDs' => false,
339
					'Armour' => 0,
340
					'TotalDamage' => 30,
341
				],
342
				0, 10, 100,
343
			],
344
			[
345
				'Overkill armour damage only (e.g. shield/armour weapon)',
346
				[
347
					'Shield' => 150,
348
					'Armour' => 150,
349
					'Rollover' => false,
350
				],
351
				[
352
					'KillingShot' => true,
353
					'TargetAlreadyDead' => false,
354
					'Shield' => 0,
355
					'CDs' => 0,
356
					'NumCDs' => 0,
357
					'HasCDs' => false,
358
					'Armour' => 100,
359
					'TotalDamage' => 100,
360
				],
361
				0, 0, 100,
362
			],
363
			[
364
				'Target is already dead',
365
				[
366
					'Shield' => 100,
367
					'Armour' => 100,
368
					'Rollover' => true,
369
				],
370
				[
371
					'KillingShot' => false,
372
					'TargetAlreadyDead' => true,
373
					'Shield' => 0,
374
					'CDs' => 0,
375
					'NumCDs' => 0,
376
					'HasCDs' => false,
377
					'Armour' => 0,
378
					'TotalDamage' => 0,
379
				],
380
				0, 0, 0,
381
			],
382
		];
383
	}
384
385
	/**
386
	 * Ensure that the state of the port is self-consistent when it is
387
	 * destroyed and loses a level in the same attack.
388
	 */
389
	public function test_port_loses_level_on_raid_killshot(): void {
390
		// We're not testing database modifications, so stub it
391
		$db = $this->createStub(Database::class);
392
		DiContainer::getContainer()->set(Database::class, $db);
393
394
		// Add a few basic checks on the player that gets the killshot
395
		$player = $this->createMock(SmrPlayer::class);
396
		$player
397
			->expects(self::once())
398
			->method('decreaseRelations')
399
			->with(AbstractSmrPort::KILLER_RELATIONS_LOSS, RACE_NEUTRAL);
400
		$player
401
			->expects(self::once())
402
			->method('increaseCurrentBountyAmount')
403
			->with(BountyType::HQ, 0);
404
405
		// Make objects that must be accessed statically (can't be mocked)
406
		SmrSector::createSector(1, 1);
407
		SmrGame::createGame(1)->setGameTypeID(SmrGame::GAME_TYPE_DEFAULT);
408
409
		// Set up the port
410
		$portLevel = 3;
411
		$port = AbstractSmrPort::createPort(1, 1);
412
		$port->upgradeToLevel($portLevel);
413
414
		// Imitate the scenario of de-leveling a port in the same attack that
415
		// destroys the port. While there's a lot we could verify here, most
416
		// important is to make sure that it doesn't throw.
417
		$result = $port->killPortByPlayer($player);
418
		$port->upgradeToLevel($portLevel - 1);
419
		$port->update();
420
421
		// killPortByPlayer should always return an empty array
422
		self::assertSame([], $result);
423
	}
424
425
}
426