All checks were successful
mapguesser/pipeline/pr-develop This commit looks good
195 lines
7.5 KiB
PHP
195 lines
7.5 KiB
PHP
<?php namespace MapGuesser\Repository;
|
|
|
|
use Generator;
|
|
use SokoWeb\Database\Query\Select;
|
|
use MapGuesser\PersistentData\Model\Challenge;
|
|
use MapGuesser\PersistentData\Model\Map;
|
|
use MapGuesser\PersistentData\Model\Place;
|
|
|
|
class PlaceRepository
|
|
{
|
|
public function getById(int $placeId): ?Place
|
|
{
|
|
return \Container::$persistentDataManager->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);
|
|
}
|
|
|
|
}
|