Passed
Pull Request — master (#2146)
by Nico
35:16 queued 24:41
created

PirateCreation::calculateSpawnProbability()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 1
dl 0
loc 18
ccs 0
cts 12
cp 0
crap 20
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace Stu\Lib\Pirate;
4
5
use Doctrine\ORM\EntityManagerInterface;
6
use Override;
0 ignored issues
show
Bug introduced by
The type Override was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use RuntimeException;
8
use Stu\Component\Map\MapEnum;
0 ignored issues
show
Bug introduced by
The type Stu\Component\Map\MapEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use Stu\Component\Spacecraft\SpacecraftAlertStateEnum;
10
use Stu\Lib\Map\FieldTypeEffectEnum;
11
use Stu\Module\Control\GameControllerInterface;
12
use Stu\Module\Control\StuRandom;
13
use Stu\Module\Logging\LoggerUtilFactoryInterface;
14
use Stu\Module\Logging\PirateLoggerInterface;
15
use Stu\Module\PlayerSetting\Lib\UserEnum;
0 ignored issues
show
Bug introduced by
The type Stu\Module\PlayerSetting\Lib\UserEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use Stu\Module\Ship\Lib\ShipCreatorInterface;
17
use Stu\Orm\Entity\FleetInterface;
18
use Stu\Orm\Entity\MapInterface;
19
use Stu\Orm\Entity\PirateSetupInterface;
20
use Stu\Orm\Entity\PirateRoundInterface;
21
use Stu\Orm\Entity\ShipInterface;
22
use Stu\Orm\Repository\FleetRepositoryInterface;
23
use Stu\Orm\Repository\GameTurnRepositoryInterface;
24
use Stu\Orm\Repository\LayerRepositoryInterface;
25
use Stu\Orm\Repository\MapRepositoryInterface;
26
use Stu\Orm\Repository\NamesRepositoryInterface;
27
use Stu\Orm\Repository\PirateRoundRepositoryInterface;
28
use Stu\Orm\Repository\PirateSetupRepositoryInterface;
29
use Stu\Orm\Repository\ShipRepositoryInterface;
30
use Stu\Orm\Repository\UserRepositoryInterface;
31
32
class PirateCreation implements PirateCreationInterface
33
{
34
    public const MAX_PIRATE_FLEETS = 10;
35
    public const MAX_PIRATE_FLEETS_PER_TICK = 5;
36
    public const MAX_PIRATE_FLEETS_PER_10MIN = 3;
37
38
    public const FORBIDDEN_ADMIN_AREAS = [
39
        MapEnum::ADMIN_REGION_SUPERPOWER_CENTRAL,
40
        MapEnum::ADMIN_REGION_SUPERPOWER_PERIPHERAL
41
    ];
42
43
    private PirateLoggerInterface $logger;
44
45 1
    public function __construct(
46
        private FleetRepositoryInterface $fleetRepository,
47
        private ShipRepositoryInterface $shipRepository,
48
        private UserRepositoryInterface $userRepository,
49
        private PirateSetupRepositoryInterface $pirateSetupRepository,
50
        private PirateRoundRepositoryInterface $pirateRoundRepository,
51
        private GameTurnRepositoryInterface $gameTurnRepository,
52
        private ShipCreatorInterface $shipCreator,
53
        private LayerRepositoryInterface $layerRepository,
54
        private MapRepositoryInterface $mapRepository,
55
        private GameControllerInterface $game,
56
        private StuRandom $stuRandom,
57
        private EntityManagerInterface $entityManager,
58
        private NamesRepositoryInterface $namesRepository,
59
        LoggerUtilFactoryInterface $loggerUtilFactory
60
    ) {
61 1
        $this->logger = $loggerUtilFactory->getPirateLogger();
62
    }
63
64
    #[Override]
65
    public function createPirateFleetsIfNeeded(): array
66
    {
67
        $currentRound = $this->pirateRoundRepository->getCurrentActiveRound();
68
69
        if ($currentRound === null) {
70
            $this->logger->log('    Keine aktive Piratenrunde - keine neuen Flotten');
71
            return [];
72
        }
73
74
        if ($currentRound->getActualPrestige() <= 0) {
75
            $this->logger->log('    Prestige auf 0 - keine neuen Piratenflotten');
76
            return [];
77
        }
78
79
        $gameTurn = $this->game->getCurrentRound();
80
        $pirateFleets = $this->fleetRepository->getByUser(UserEnum::USER_NPC_KAZON);
81
        $currentFleetCount = count($pirateFleets);
82
83
        $dynamicLimits = $this->calculateDynamicLimits($currentRound);
84
85
        $this->logger->logf(
86
            '    Dynamische Limits: Max=%d, PerTick=%d, Per10Min=%d',
87
            $dynamicLimits['maxFleets'],
88
            $dynamicLimits['maxPerTick'],
89
            $dynamicLimits['maxPer10Min']
90
        );
91
92
        $missingFleetAmount = min(
93
            max(0, $dynamicLimits['maxPerTick'] - $gameTurn->getPirateFleets()),
94
            max(0, $dynamicLimits['maxFleets'] - $currentFleetCount)
95
        );
96
97
        if ($missingFleetAmount <= 0) {
98
            $this->logger->logf(
99
                '    Tick-Limit erreicht (%d/%d) oder max. Flotten erreicht (%d/%d)',
100
                $gameTurn->getPirateFleets(),
101
                $dynamicLimits['maxPerTick'],
102
                $currentFleetCount,
103
                $dynamicLimits['maxFleets']
104
            );
105
            return $pirateFleets;
106
        }
107
108
        $spawnProbability = $this->calculateSpawnProbability($currentRound);
109
110
        $this->logger->logf(
111
            '    Spawn-Wahrscheinlichkeit: %.2f%% (Prestige: %d/%d)',
112
            $spawnProbability * 100,
113
            $currentRound->getActualPrestige(),
114
            $currentRound->getMaxPrestige()
115
        );
116
117
        $randomValue = $this->stuRandom->rand(1, 100);
118
        $spawnThreshold = $spawnProbability * 100;
119
120
        if ($randomValue > $spawnThreshold) {
121
            $this->logger->logf(
122
                '    Spawn-Wahrscheinlichkeit nicht erreicht (Random: %d, Threshold: %.2f)',
123
                $randomValue,
124
                $spawnThreshold
125
            );
126
            return $pirateFleets;
127
        }
128
129
130
        /** @var int<0, max> $maxNewFleets */
131
        $maxNewFleets = min($dynamicLimits['maxPer10Min'], $missingFleetAmount);
132
133
        if ($maxNewFleets <= 0) {
134
            return $pirateFleets;
135
        }
136
137
        $fleetsToSpawn = $this->stuRandom->rand(1, $maxNewFleets);
138
139
        $this->logger->logf('    Spawne %d neue Piratenflotten', $fleetsToSpawn);
140
141
        for ($i = 0; $i < $fleetsToSpawn; $i++) {
142
            $this->logger->logf('  Flotte Nr. %d', $i + 1);
143
            $pirateFleets[] = $this->createPirateFleet();
144
            $gameTurn->setPirateFleets($gameTurn->getPirateFleets() + 1);
145
        }
146
147
        $this->gameTurnRepository->save($gameTurn);
148
149
        return $pirateFleets;
150
    }
151
152
    /**
153
     * 
154
     * @return array{maxFleets: int, maxPerTick: int, maxPer10Min: int}
155
     */
156
    private function calculateDynamicLimits(PirateRoundInterface $currentRound): array
157
    {
158
        $maxPrestige = $currentRound->getMaxPrestige();
159
        $actualPrestige = $currentRound->getActualPrestige();
160
161
        if ($maxPrestige <= 0) {
162
            return [
163
                'maxFleets' => 1,
164
                'maxPerTick' => 1,
165
                'maxPer10Min' => 1
166
            ];
167
        }
168
169
        $consumedRatio = 1.0 - ($actualPrestige / $maxPrestige);
170
        $scalingFactor = $this->calculateLimitScalingFactor($consumedRatio);
171
172
        $this->logger->logf(
173
            '    Prestige-Verbrauch: %.1f%%, Skalierungsfaktor: %.2f',
174
            $consumedRatio * 100,
175
            $scalingFactor
176
        );
177
178
        return [
179
            'maxFleets' => max(1, (int) round(self::MAX_PIRATE_FLEETS * $scalingFactor)),
180
            'maxPerTick' => max(1, (int) round(self::MAX_PIRATE_FLEETS_PER_TICK * $scalingFactor)),
181
            'maxPer10Min' => max(1, (int) round(self::MAX_PIRATE_FLEETS_PER_10MIN * $scalingFactor))
182
        ];
183
    }
184
185
    private function calculateLimitScalingFactor(float $consumedRatio): float
186
    {
187
        if ($consumedRatio <= 0.2) {
188
            return 0.1 + (0.9 * ($consumedRatio / 0.2));
189
        } elseif ($consumedRatio <= 0.7) {
190
            return 1.0;
191
        } else {
192
            $remainingRatio = (1.0 - $consumedRatio) / 0.3;
193
            return 0.25 + (0.75 * $remainingRatio);
194
        }
195
    }
196
197
    private function calculateSpawnProbability(PirateRoundInterface $currentRound): float
198
    {
199
        $maxPrestige = $currentRound->getMaxPrestige();
200
        $actualPrestige = $currentRound->getActualPrestige();
201
202
        if ($maxPrestige <= 0) {
203
            return 0.1;
204
        }
205
206
        $consumedRatio = 1.0 - ($actualPrestige / $maxPrestige);
207
208
        if ($consumedRatio <= 0.2) {
209
            return 0.15 + (0.55 * ($consumedRatio / 0.2));
210
        } elseif ($consumedRatio <= 0.7) {
211
            return 0.85;
212
        } else {
213
            $remainingRatio = (1.0 - $consumedRatio) / 0.3;
214
            return 0.25 + (0.6 * $remainingRatio);
215
        }
216
    }
217
218
    #[Override]
219
    public function createPirateFleet(?ShipInterface $supportCaller = null): FleetInterface
220
    {
221
        $pirateUser = $this->userRepository->find(UserEnum::USER_NPC_KAZON);
222
        if ($pirateUser === null) {
223
            throw new RuntimeException('this should not happen');
224
        }
225
226
        $pirateSetup = $this->getRandomPirateSetup();
227
228
        //create ships
229
        $ships = $this->createShips($pirateSetup, $supportCaller);
230
        $this->entityManager->flush();
231
232
        $fleetLeader = $ships[array_rand($ships)];
233
        $fleetLeader->setIsFleetLeader(true);
234
235
        //create fleet
236
        $fleet = $this->fleetRepository->prototype();
237
        $fleet->setUser($pirateUser);
238
        $fleet->setName($pirateSetup->getName());
239
        $fleet->setIsFleetFixed(true);
240
        $fleet->setLeadShip($fleetLeader);
241
242
        $this->logger->log(sprintf('    shipCount: %d', count($ships)));
243
244
        foreach ($ships as $ship) {
245
            $fleet->getShips()->add($ship);
246
            $ship->setFleet($fleet);
247
            $this->shipRepository->save($ship);
248
        }
249
250
        $this->fleetRepository->save($fleet);
251
        $this->entityManager->flush();
252
253
        return $fleet;
254
    }
255
256
    /** @return array<ShipInterface> */
257
    private function createShips(PirateSetupInterface $pirateSetup, ?ShipInterface $supportCaller): array
258
    {
259
        $randomLocation = $supportCaller === null ? $this->getRandomMapLocation() : $supportCaller->getLocation();
260
261
        $randomAlertLevel = SpacecraftAlertStateEnum::getRandomAlertLevel();
262
263
        $this->logger->log(sprintf('    randomAlertLevel: %d', $randomAlertLevel->value));
264
265
        $result = [];
266
267
        foreach ($pirateSetup->getSetupBuildplans() as $setupBuildplan) {
268
269
            $buildplan = $setupBuildplan->getBuildplan();
270
            $rump = $buildplan->getRump();
271
272
            for ($i = 0; $i < $setupBuildplan->getAmount(); $i++) {
273
274
                $mostUnusedNames = $this->namesRepository->mostUnusedNames();
275
                if ($mostUnusedNames !== []) {
276
                    $selectedNameEntry = $mostUnusedNames[array_rand($mostUnusedNames)];
277
                    $shipName = $selectedNameEntry->getName();
278
                    $selectedNameEntry->setCount($selectedNameEntry->getCount() + 1);
279
                    $this->namesRepository->save($selectedNameEntry);
280
                } else {
281
                    $shipName = "Pirate Ship";
282
                }
283
284
285
                $result[] = $this->shipCreator
286
                    ->createBy(
287
                        UserEnum::USER_NPC_KAZON,
288
                        $rump->getId(),
289
                        $buildplan->getId()
290
                    )
291
                    ->setLocation($randomLocation)
292
                    ->maxOutSystems()
293
                    ->createCrew()
294
                    ->setTorpedo()
295
                    ->setSpacecraftName($shipName)
296
                    ->setAlertState($randomAlertLevel)
297
                    ->finishConfiguration()
298
                    ->get();
299
            }
300
        }
301
302
        return $result;
303
    }
304
305
    private function getRandomPirateSetup(): PirateSetupInterface
306
    {
307
        $pirateSetups = $this->pirateSetupRepository->findAll();
308
309
        $pirateProbabilities = array_map(fn(PirateSetupInterface $setup): int => $setup->getProbabilityWeight(), $pirateSetups);
310
311
        return $pirateSetups[$this->stuRandom->randomKeyOfProbabilities($pirateProbabilities)];
312
    }
313
314
    private function getRandomMapLocation(): MapInterface
315
    {
316
        $defaultLayer = $this->layerRepository->getDefaultLayer();
317
318
        do {
319
            $map = $this->mapRepository->getRandomPassableUnoccupiedWithoutDamage($defaultLayer);
320
        } while (
321
            in_array($map->getAdminRegionId(), self::FORBIDDEN_ADMIN_AREAS)
322
            || $map->getFieldType()->hasEffect(FieldTypeEffectEnum::NO_PIRATES)
323
        );
324
325
        $this->logger->log(sprintf('    randomMapLocation: %s', $map->getSectorString()));
326
327
        return $map;
328
    }
329
}
330