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(['places', '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(['places', 'map_id'], '=', $mapId);
 | 
						|
        $numberOfOldPlaces = $selectOldPlaces->count();
 | 
						|
 | 
						|
        // set order by datetime, oldest first
 | 
						|
        $selectOldPlaces->orderBy(['places_by_current_user', '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);
 | 
						|
    }
 | 
						|
 | 
						|
}
 |