selectFromDbById($placeId, Place::class); } public function getAllForMap(Map $map): Generator { $select = new Select(\Container::$dbConnection); $select->where('map_id', '=', $map->getId()); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class); } //TODO: use Map and User instead of id public function getRandomNPlaces(int $mapId, int $n, ?int $userId): array { if (!isset($userId)) { // anonymous single player return $this->getRandomNForMapWithValidPano($mapId, $n); } else { // authorized user or multiplayer game with selection based on what the host played before $unvisitedPlaces = $this->getRandomUnvisitedNForMapWithValidPano($mapId, $n, $userId); if (count($unvisitedPlaces) == $n) { return $unvisitedPlaces; } $oldPlaces = $this->getRandomOldNForMapWithValidPano($mapId, $n - count($unvisitedPlaces), $userId); return array_merge($unvisitedPlaces, $oldPlaces); } } //TODO: use Map instead of id private function getRandomNForMapWithValidPano(int $mapId, int $n): array { $places = []; $select = new Select(\Container::$dbConnection, 'places'); $select->where('map_id', '=', $mapId); $numberOfPlaces = $select->count(); $exclude = []; for ($i = 1; $i <= $n; ++$i) { $place = $this->getRandomForMapWithValidPano($numberOfPlaces, $select, $exclude); $places[] = $place; $exclude[] = $place->getId(); } return $places; } private function getRandomForMapWithValidPano(int $numberOfPlaces, Select $select, array &$exclude, ?callable $pickRandomInt = null): ?Place { do { $numberOfPlacesLeft = $numberOfPlaces - count($exclude); $place = $this->selectRandomFromDbForMap($numberOfPlacesLeft, $select, $exclude, $pickRandomInt); if ($place === null) { // there is no more never visited place left return null; } $panoId = $place->getFreshPanoId(); if ($panoId === null) { $exclude[] = $place->getId(); } } while ($panoId === null); return $place; } private function selectRandomFromDbForMap(int $numberOfPlacesLeft, Select $select, array $exclude, ?callable $pickRandomInt): ?Place { if ($numberOfPlacesLeft <= 0) return null; if (!isset($pickRandomInt)) { $randomOffset = random_int(0, $numberOfPlacesLeft - 1); } else { $randomOffset = $pickRandomInt($numberOfPlacesLeft); } $select->where('id', 'NOT IN', $exclude); $select->limit(1, $randomOffset); return \Container::$persistentDataManager->selectFromDb($select, Place::class); } // Never visited places private function getRandomUnvisitedNForMapWithValidPano(int $mapId, int $n, int $userId): array { $places = []; $exclude = []; // list of places visited by user $selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place'); $selectPlacesByCurrentUser->columns(['place_id', 'last_time']); $selectPlacesByCurrentUser->where('user_id', '=', $userId); $selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user'); // count the places never visited $selectUnvisited = new Select(\Container::$dbConnection, 'places'); $selectUnvisited->leftJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']); $selectUnvisited->where('map_id', '=', $mapId); $selectUnvisited->where(['places_by_current_user', 'last_time'], '=', null); $numberOfUnvisitedPlaces = $selectUnvisited->count(); // look for as many new places as possible but maximum $n do { $place = $this->getRandomForMapWithValidPano($numberOfUnvisitedPlaces, $selectUnvisited, $exclude); if (isset($place)) { $places[] = $place; $exclude[] = $place->getId(); } } while (count($places) < $n && isset($place)); return $places; } // Places visited in the longest time private function getRandomOldNForMapWithValidPano(int $mapId, int $n, int $userId): array { $places = []; $exclude = []; // list of places visited by user $selectPlacesByCurrentUser = new Select(\Container::$dbConnection, 'user_played_place'); $selectPlacesByCurrentUser->columns(['place_id', 'last_time']); $selectPlacesByCurrentUser->where('user_id', '=', $userId); $selectPlacesByCurrentUser->setDerivedTableAlias('places_by_current_user'); // count places that were visited at least once $selectOldPlaces = new Select(\Container::$dbConnection, 'places'); $selectOldPlaces->innerJoin($selectPlacesByCurrentUser, ['places', 'id'], '=', ['places_by_current_user', 'place_id']); $selectOldPlaces->where('map_id', '=', $mapId); $numberOfOldPlaces = $selectOldPlaces->count(); // set order by datetime, oldest first $selectOldPlaces->orderBy('last_time'); // selection algorithm with preference (weighting) for older places using Box-Muller transform $pickGaussianRandomInt = function($numberOfPlaces) { $stdev = 0.2; $avg = 0.0; $x = mt_rand() / mt_getrandmax(); $y = mt_rand() / mt_getrandmax(); $randomNum = abs(sqrt(-2 * log($x)) * cos(2 * pi() * $y) * $stdev + $avg); return (int) min($randomNum * $numberOfPlaces, $numberOfPlaces - 1); }; // look for n - numberOfUnvisitedPlaces places while (count($places) < $n) { $place = $this->getRandomForMapWithValidPano($numberOfOldPlaces, $selectOldPlaces, $exclude, $pickGaussianRandomInt); if (isset($place)) { $places[] = $place; $exclude[] = $place->getId(); } } return $places; } public function getByRoundInChallenge(Challenge $challenge, int $round): ?Place { $select = new Select(\Container::$dbConnection); $select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']); $select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId()); $select->orderBy(['place_in_challenge', 'round']); $select->limit(1, $round); return \Container::$persistentDataManager->selectFromDb($select, Place::class); } public function getAllInChallenge(Challenge $challenge): Generator { $select = new Select(\Container::$dbConnection); $select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']); $select->where(['place_in_challenge', 'challenge_id'], '=', $challenge->getId()); $select->orderBy(['place_in_challenge', 'round']); yield from \Container::$persistentDataManager->selectMultipleFromDb($select, Place::class); } }