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
|
|
|
|