|
1
|
|
|
<?php declare(strict_types=1); |
|
2
|
|
|
|
|
3
|
|
|
use Smr\Exceptions\UserError; |
|
4
|
|
|
use Smr\HardwareType; |
|
5
|
|
|
use Smr\Path; |
|
6
|
|
|
use Smr\PlotGroup; |
|
7
|
|
|
use Smr\TradeGood; |
|
8
|
|
|
use Smr\TransactionType; |
|
9
|
|
|
|
|
10
|
|
|
class Plotter { |
|
11
|
|
|
|
|
12
|
|
|
public static function getX(PlotGroup $xType, int|string $X, int $gameID, AbstractSmrPlayer $player = null): mixed { |
|
13
|
|
|
// Special case for Location categories (i.e. Bar, HQ, SafeFed) |
|
14
|
|
|
if (!is_numeric($X)) { |
|
15
|
|
|
if ($xType != PlotGroup::Locations) { |
|
16
|
|
|
throw new Exception('Non-numeric X only exists for Locations'); |
|
17
|
|
|
} |
|
18
|
|
|
return $X; |
|
19
|
|
|
} |
|
20
|
|
|
|
|
21
|
|
|
// In all other cases, X is either an int or a numeric string |
|
22
|
|
|
if (is_string($X)) { |
|
23
|
|
|
$X = str2int($X); |
|
|
|
|
|
|
24
|
|
|
} |
|
25
|
|
|
|
|
26
|
|
|
// Helper function for plots to trade goods |
|
27
|
|
|
$getGoodWithTransaction = function(int $goodID) use ($xType, $player) { |
|
28
|
|
|
$good = TradeGood::get($goodID); |
|
29
|
|
|
if (isset($player) && !$player->meetsAlignmentRestriction($good->alignRestriction)) { |
|
30
|
|
|
throw new Exception('Player trying to access alignment-restricted good!'); |
|
31
|
|
|
} |
|
32
|
|
|
return [ |
|
33
|
|
|
'Type' => 'Good', |
|
34
|
|
|
'GoodID' => $goodID, |
|
35
|
|
|
'TransactionType' => TransactionType::from(explode(' ', $xType->value)[0]), |
|
36
|
|
|
]; |
|
37
|
|
|
}; |
|
38
|
|
|
|
|
39
|
|
|
return match ($xType) { |
|
40
|
|
|
PlotGroup::Technology => HardwareType::get($X), |
|
41
|
|
|
PlotGroup::Ships => SmrShipType::get($X), |
|
42
|
|
|
PlotGroup::Weapons => SmrWeaponType::getWeaponType($X), |
|
43
|
|
|
PlotGroup::Locations => SmrLocation::getLocation($gameID, $X), |
|
44
|
|
|
PlotGroup::SellGoods, PlotGroup::BuyGoods => $getGoodWithTransaction($X), |
|
45
|
|
|
PlotGroup::Galaxies => SmrGalaxy::getGalaxy($gameID, $X), // $X is the galaxyID |
|
46
|
|
|
}; |
|
47
|
|
|
} |
|
48
|
|
|
|
|
49
|
|
|
/** |
|
50
|
|
|
* Returns the shortest path from $sector to $x as a Distance object. |
|
51
|
|
|
* The path is guaranteed reversible ($x -> $sector == $sector -> $x), which |
|
52
|
|
|
* is not true for findDistanceToX. If $x is not a SmrSector, then this |
|
53
|
|
|
* function does 2x the work. |
|
54
|
|
|
*/ |
|
55
|
|
|
public static function findReversiblePathToX(mixed $x, SmrSector $sector, AbstractSmrPlayer $needsToHaveBeenExploredBy = null, AbstractSmrPlayer $player = null): Path { |
|
56
|
|
|
if ($x instanceof SmrSector) { |
|
57
|
|
|
|
|
58
|
|
|
// To ensure reversibility, always plot lowest to highest. |
|
59
|
|
|
$reverse = $sector->getSectorID() > $x->getSectorID(); |
|
60
|
|
|
if ($reverse) { |
|
61
|
|
|
$start = $x; |
|
62
|
|
|
$end = $sector; |
|
63
|
|
|
} else { |
|
64
|
|
|
$start = $sector; |
|
65
|
|
|
$end = $x; |
|
66
|
|
|
} |
|
67
|
|
|
$path = self::findDistanceToX($end, $start, true, $needsToHaveBeenExploredBy, $player); |
|
68
|
|
|
if ($path === false) { |
|
|
|
|
|
|
69
|
|
|
throw new UserError('Unable to plot from ' . $sector->getSectorID() . ' to ' . $x->getSectorID() . '.'); |
|
70
|
|
|
} |
|
71
|
|
|
// Reverse if we plotted $x -> $sector (since we want $sector -> $x) |
|
72
|
|
|
if ($reverse) { |
|
73
|
|
|
$path->reversePath(); |
|
74
|
|
|
} |
|
75
|
|
|
|
|
76
|
|
|
} else { |
|
77
|
|
|
|
|
78
|
|
|
// At this point we don't know what sector $x will be at |
|
79
|
|
|
$path = self::findDistanceToX($x, $sector, true, $needsToHaveBeenExploredBy, $player); |
|
80
|
|
|
if ($path === false) { |
|
|
|
|
|
|
81
|
|
|
throw new UserError('Unable to find what you\'re looking for, it either hasn\'t been added to this game or you haven\'t explored it yet.'); |
|
82
|
|
|
} |
|
83
|
|
|
// Now that we know where $x is, make sure path is reversible |
|
84
|
|
|
// (i.e. start sector < end sector) |
|
85
|
|
|
if ($path->getEndSectorID() < $sector->getSectorID()) { |
|
86
|
|
|
$endSector = SmrSector::getSector($sector->getGameID(), $path->getEndSectorID()); |
|
87
|
|
|
$path = self::findDistanceToX($sector, $endSector, true); |
|
88
|
|
|
if ($path === false) { |
|
89
|
|
|
throw new Exception('Unable to find reverse path'); |
|
90
|
|
|
} |
|
91
|
|
|
$path->reversePath(); |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
|
|
} |
|
95
|
|
|
return $path; |
|
96
|
|
|
} |
|
97
|
|
|
|
|
98
|
|
|
/** |
|
99
|
|
|
* Returns the shortest path from $sector to $x as a Path object. |
|
100
|
|
|
* The resulting path prefers neighbors in their order in SmrSector->links, |
|
101
|
|
|
* (i.e. up, down, left, right). |
|
102
|
|
|
* |
|
103
|
|
|
* @param mixed $x If the string 'Distance', then distances to all visited sectors will |
|
104
|
|
|
* be returned. Otherwise, must be a type implemented by SmrSector::hasX, |
|
105
|
|
|
* and will only return distances to sectors for which hasX returns true. |
|
106
|
|
|
* |
|
107
|
|
|
* @return ($useFirst is true ? Smr\Path|false : array<int, Smr\Path>) |
|
|
|
|
|
|
108
|
|
|
*/ |
|
109
|
|
|
public static function findDistanceToX(mixed $x, SmrSector $sector, bool $useFirst, AbstractSmrPlayer $needsToHaveBeenExploredBy = null, AbstractSmrPlayer $player = null, int $distanceLimit = 10000, int $lowLimit = 0, int $highLimit = 100000): array|Path|false { |
|
110
|
|
|
$warpAddIndex = TURNS_WARP_SECTOR_EQUIVALENCE - 1; |
|
111
|
|
|
|
|
112
|
|
|
$checkSector = $sector; |
|
113
|
|
|
$gameID = $sector->getGameID(); |
|
114
|
|
|
$distances = []; |
|
115
|
|
|
$sectorsTravelled = 0; |
|
116
|
|
|
$visitedSectors = []; |
|
117
|
|
|
$visitedSectors[$checkSector->getSectorID()] = true; |
|
118
|
|
|
|
|
119
|
|
|
$distanceQ = []; |
|
120
|
|
|
for ($i = 0; $i <= TURNS_WARP_SECTOR_EQUIVALENCE; $i++) { |
|
121
|
|
|
$distanceQ[] = []; |
|
122
|
|
|
} |
|
123
|
|
|
//Warps first as a slight optimisation due to how visitedSectors is set. |
|
124
|
|
|
if ($checkSector->hasWarp() === true) { |
|
125
|
|
|
$d = new Path($checkSector->getSectorID()); |
|
126
|
|
|
$d->addWarp($checkSector->getWarp()); |
|
127
|
|
|
$distanceQ[$warpAddIndex][] = $d; |
|
128
|
|
|
} |
|
129
|
|
|
foreach ($checkSector->getLinks() as $nextSector) { |
|
130
|
|
|
$visitedSectors[$nextSector] = true; |
|
131
|
|
|
$d = new Path($checkSector->getSectorID()); |
|
132
|
|
|
$d->addLink($nextSector); |
|
133
|
|
|
$distanceQ[0][] = $d; |
|
134
|
|
|
} |
|
135
|
|
|
$maybeWarps = 0; |
|
136
|
|
|
while ($maybeWarps <= TURNS_WARP_SECTOR_EQUIVALENCE) { |
|
137
|
|
|
$sectorsTravelled++; |
|
138
|
|
|
if ($sectorsTravelled > $distanceLimit) { |
|
139
|
|
|
break; |
|
140
|
|
|
} |
|
141
|
|
|
$distanceQ[] = []; |
|
142
|
|
|
$q = array_shift($distanceQ); |
|
143
|
|
|
if (count($q) === 0) { |
|
144
|
|
|
$maybeWarps++; |
|
145
|
|
|
continue; |
|
146
|
|
|
} |
|
147
|
|
|
$maybeWarps = 0; |
|
148
|
|
|
while (($distance = array_shift($q)) !== null) { |
|
149
|
|
|
$checkSectorID = $distance->getEndSectorID(); |
|
150
|
|
|
$visitedSectors[$checkSectorID] = true; // This is here for warps, because they are delayed visits if we set this before the actual visit we'll get sectors marked as visited long before they are actually visited - causes problems when it's quicker to walk to the warp exit than to warp there. |
|
151
|
|
|
// We still need to mark walked sectors as visited before we go to each one otherwise we get a huge number of paths being checked twice (up then left, left then up are essentially the same but if we set up-left as visited only when we actually check it then it gets queued up twice - nasty) |
|
152
|
|
|
if ($checkSectorID >= $lowLimit && $checkSectorID <= $highLimit) { |
|
153
|
|
|
$checkSector = SmrSector::getSector($gameID, $checkSectorID); |
|
154
|
|
|
// Does this sector satisfy our criteria? |
|
155
|
|
|
if ($x == 'Distance' || (($needsToHaveBeenExploredBy === null || $needsToHaveBeenExploredBy->hasVisitedSector($checkSector->getSectorID())) === true |
|
156
|
|
|
&& $checkSector->hasX($x, $player) === true)) { |
|
157
|
|
|
if ($useFirst === true) { |
|
158
|
|
|
return $distance; |
|
159
|
|
|
} |
|
160
|
|
|
$distances[$checkSector->getSectorID()] = $distance; |
|
161
|
|
|
} |
|
162
|
|
|
//Warps first as a slight optimisation due to how visitedSectors is set. |
|
163
|
|
|
if ($checkSector->hasWarp() === true) { |
|
164
|
|
|
if (!isset($visitedSectors[$checkSector->getWarp()])) { |
|
165
|
|
|
$cloneDistance = clone($distance); |
|
166
|
|
|
$cloneDistance->addWarp($checkSector->getWarp()); |
|
167
|
|
|
$distanceQ[$warpAddIndex][] = $cloneDistance; |
|
168
|
|
|
} |
|
169
|
|
|
} |
|
170
|
|
|
foreach ($checkSector->getLinks() as $nextSector) { |
|
171
|
|
|
if (!isset($visitedSectors[$nextSector])) { |
|
172
|
|
|
$visitedSectors[$nextSector] = true; |
|
173
|
|
|
|
|
174
|
|
|
$cloneDistance = clone($distance); |
|
175
|
|
|
$cloneDistance->addLink($nextSector); |
|
176
|
|
|
$distanceQ[0][] = $cloneDistance; |
|
177
|
|
|
} |
|
178
|
|
|
} |
|
179
|
|
|
} |
|
180
|
|
|
} |
|
181
|
|
|
} |
|
182
|
|
|
if ($useFirst === true) { |
|
183
|
|
|
return false; |
|
184
|
|
|
} |
|
185
|
|
|
return $distances; |
|
186
|
|
|
} |
|
187
|
|
|
|
|
188
|
|
|
/** |
|
189
|
|
|
* @param array<int, \SmrPort> $ports |
|
190
|
|
|
* @param array<int, bool> $races |
|
191
|
|
|
* @return array<int, array<int, Smr\Path>> |
|
192
|
|
|
*/ |
|
193
|
|
|
public static function calculatePortToPortDistances(array $ports, array $races, int $distanceLimit = 10000, int $lowLimit = 0, int $highLimit = 100000): array { |
|
194
|
|
|
$distances = []; |
|
195
|
|
|
foreach ($ports as $port) { |
|
196
|
|
|
$sectorID = $port->getSectorID(); |
|
197
|
|
|
if ($races[$port->getRaceID()] && $sectorID >= $lowLimit && $sectorID <= $highLimit) { |
|
198
|
|
|
$distances[$sectorID] = self::findDistanceToOtherPorts($port->getSector(), $distanceLimit, $lowLimit, $highLimit); |
|
199
|
|
|
} |
|
200
|
|
|
} |
|
201
|
|
|
return $distances; |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
|
|
/** |
|
205
|
|
|
* @return array<int, Smr\Path> |
|
206
|
|
|
*/ |
|
207
|
|
|
public static function findDistanceToOtherPorts(SmrSector $sector, int $distanceLimit = 10000, int $lowLimit = 0, int $highLimit = 100000): array { |
|
208
|
|
|
return self::findDistanceToX('Port', $sector, false, null, null, $distanceLimit, $lowLimit, $highLimit); |
|
209
|
|
|
} |
|
210
|
|
|
|
|
211
|
|
|
} |
|
212
|
|
|
|