468 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			468 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php namespace MapGuesser\Controller;
 | 
						|
 | 
						|
use DateTime;
 | 
						|
use SokoWeb\Interfaces\Authentication\IAuthenticationRequired;
 | 
						|
use MapGuesser\Util\Geo\Position;
 | 
						|
use SokoWeb\Response\JsonContent;
 | 
						|
use SokoWeb\Interfaces\Response\IContent;
 | 
						|
use MapGuesser\Multi\MultiConnector;
 | 
						|
use MapGuesser\PersistentData\Model\Challenge;
 | 
						|
use MapGuesser\PersistentData\Model\Guess;
 | 
						|
use MapGuesser\PersistentData\Model\Map;
 | 
						|
use MapGuesser\PersistentData\Model\Place;
 | 
						|
use MapGuesser\PersistentData\Model\PlaceInChallenge;
 | 
						|
use MapGuesser\PersistentData\Model\User;
 | 
						|
use MapGuesser\PersistentData\Model\UserPlayedPlace;
 | 
						|
use MapGuesser\Repository\GuessRepository;
 | 
						|
use MapGuesser\Repository\MultiRoomRepository;
 | 
						|
use MapGuesser\Repository\PlaceInChallengeRepository;
 | 
						|
use MapGuesser\Repository\PlaceRepository;
 | 
						|
use MapGuesser\Repository\UserInChallengeRepository;
 | 
						|
use MapGuesser\Repository\UserPlayedPlaceRepository;
 | 
						|
 | 
						|
class GameFlowController implements IAuthenticationRequired
 | 
						|
{
 | 
						|
    const NUMBER_OF_ROUNDS = 5;
 | 
						|
    const MAX_SCORE = 1000;
 | 
						|
 | 
						|
    private MultiConnector $multiConnector;
 | 
						|
 | 
						|
    private MultiRoomRepository $multiRoomRepository;
 | 
						|
 | 
						|
    private PlaceRepository $placeRepository;
 | 
						|
 | 
						|
    private UserPlayedPlaceRepository $userPlayedPlaceRepository;
 | 
						|
 | 
						|
    private UserInChallengeRepository $userInChallengeRepository;
 | 
						|
 | 
						|
    private PlaceInChallengeRepository $placeInChallengeRepository;
 | 
						|
 | 
						|
    private GuessRepository $guessRepository;
 | 
						|
 | 
						|
    public function __construct()
 | 
						|
    {
 | 
						|
        $this->multiConnector = new MultiConnector();
 | 
						|
        $this->multiRoomRepository = new MultiRoomRepository();
 | 
						|
        $this->placeRepository = new PlaceRepository();
 | 
						|
        $this->userPlayedPlaceRepository = new UserPlayedPlaceRepository();
 | 
						|
        $this->userInChallengeRepository = new UserInChallengeRepository();
 | 
						|
        $this->placeInChallengeRepository = new PlaceInChallengeRepository();
 | 
						|
        $this->guessRepository = new GuessRepository();
 | 
						|
    }
 | 
						|
 | 
						|
    public function isAuthenticationRequired(): bool
 | 
						|
    {
 | 
						|
        return empty($_ENV['ENABLE_GAME_FOR_GUESTS']);
 | 
						|
    }
 | 
						|
 | 
						|
    public function initialData(): IContent
 | 
						|
    {
 | 
						|
        $mapId = (int) \Container::$request->query('mapId');
 | 
						|
        $session = \Container::$request->session();
 | 
						|
 | 
						|
        if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
 | 
						|
            return new JsonContent(['error' => 'no_session_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
 | 
						|
            $this->startNewGame($state, $mapId);
 | 
						|
            $session->set('state', $state);
 | 
						|
        }
 | 
						|
 | 
						|
        $response = [];
 | 
						|
 | 
						|
        $last = $state['rounds'][$state['currentRound']];
 | 
						|
        $response['place'] = [
 | 
						|
            'panoId' => $last['panoId'],
 | 
						|
            'pov' => $last['pov']->toArray()
 | 
						|
        ];
 | 
						|
 | 
						|
        $response['history'] = [];
 | 
						|
        for ($i = 0; $i < $state['currentRound']; ++$i) {
 | 
						|
            $round = $state['rounds'][$i];
 | 
						|
            $response['history'][] = [
 | 
						|
                'position' => $round['position']->toArray(),
 | 
						|
                'result' => [
 | 
						|
                    'guessPosition' => $round['guessPosition']->toArray(),
 | 
						|
                    'distance' => $round['distance'],
 | 
						|
                    'score' => $round['score']
 | 
						|
                ]
 | 
						|
            ];
 | 
						|
        }
 | 
						|
 | 
						|
        return new JsonContent($response);
 | 
						|
    }
 | 
						|
 | 
						|
    public function multiInitialData(): IContent
 | 
						|
    {
 | 
						|
        $roomId = \Container::$request->query('roomId');
 | 
						|
        $session = \Container::$request->session();
 | 
						|
 | 
						|
        if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
 | 
						|
            return new JsonContent(['error' => 'no_session_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        $room = $this->multiRoomRepository->getByRoomId($roomId);
 | 
						|
        $state = $room->getStateArray();
 | 
						|
        $members = $room->getMembersArray();
 | 
						|
 | 
						|
        if ($members['owner'] !== $multiState['token']) {
 | 
						|
            return new JsonContent(['error' => 'not_owner_of_room']);
 | 
						|
        }
 | 
						|
 | 
						|
        if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
 | 
						|
            $this->startNewGame($state, $state['mapId']);
 | 
						|
            $room->setStateArray($state);
 | 
						|
            $room->setUpdatedDate(new DateTime());
 | 
						|
            \Container::$persistentDataManager->saveToDb($room);
 | 
						|
        }
 | 
						|
 | 
						|
        $places = [];
 | 
						|
        foreach ($state['rounds'] as $round) {
 | 
						|
            $places[] = [
 | 
						|
                'position' => $round['position']->toArray(),
 | 
						|
                'panoId' => $round['panoId'],
 | 
						|
                'pov' => $round['pov']->toArray()
 | 
						|
            ];
 | 
						|
        }
 | 
						|
 | 
						|
        $this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
 | 
						|
 | 
						|
        return new JsonContent(['ok' => true]);
 | 
						|
    }
 | 
						|
 | 
						|
    private function prepareChallengeResponse(int $userId, Challenge $challenge, int $currentRound, bool $withHistory = false): array
 | 
						|
    {
 | 
						|
        $currentPlace = $this->placeRepository->getByRoundInChallenge($challenge, $currentRound);
 | 
						|
 | 
						|
        // if the last round was played ($currentPlace == null) or history is explicitly requested (for initializing)
 | 
						|
        if (!isset($currentPlace) || $withHistory) {
 | 
						|
 | 
						|
            $withRelations = [User::class, PlaceInChallenge::class, Place::class];
 | 
						|
            foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) {
 | 
						|
                $round = $guess->getPlaceInChallenge()->getRound();
 | 
						|
 | 
						|
                if ($guess->getUser()->getId() === $userId) {
 | 
						|
                    $response['history'][$round]['position'] =
 | 
						|
                            $guess->getPlaceInChallenge()->getPlace()->getPosition()->toArray();
 | 
						|
                    $response['history'][$round]['result'] = [
 | 
						|
                            'guessPosition' => $guess->getPosition()->toArray(),
 | 
						|
                            'distance' => $guess->getDistance(),
 | 
						|
                            'score' => $guess->getScore()
 | 
						|
                    ];
 | 
						|
                } else {
 | 
						|
                    $response['history'][$round]['allResults'][] = [
 | 
						|
                        'userName' => $guess->getUser()->getDisplayName(),
 | 
						|
                        'guessPosition' => $guess->getPosition()->toArray(),
 | 
						|
                        'distance' => $guess->getDistance(),
 | 
						|
                        'score' => $guess->getScore()
 | 
						|
                    ];
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            // setting default values for rounds without guesses (because of timeout)
 | 
						|
            for ($i = 0; $i < $currentRound; ++$i) {
 | 
						|
                if (!isset($response['history'][$i]) || !isset($response['history'][$i]['result'])) {
 | 
						|
                    $response['history'][$i]['result'] = [
 | 
						|
                            'guessPosition' => null,
 | 
						|
                            'distance' => null,
 | 
						|
                            'score' => 0
 | 
						|
                    ];
 | 
						|
 | 
						|
                    $response['history'][$i]['position'] =
 | 
						|
                            $this->placeRepository->getByRoundInChallenge($challenge, $i)->getPosition()->toArray();
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            $response['history']['length'] = $currentRound;
 | 
						|
        }
 | 
						|
 | 
						|
        if (!isset($currentPlace)) { // game finished
 | 
						|
            $response['finished'] = true;
 | 
						|
        } else { // continue game
 | 
						|
            $response['place'] = [
 | 
						|
                'panoId' => $currentPlace->getPanoIdCached(),
 | 
						|
                'pov' => $currentPlace->getPov()->toArray()
 | 
						|
            ];
 | 
						|
 | 
						|
            $prevRound = $currentRound - 1;
 | 
						|
            if ($prevRound >= 0) {
 | 
						|
                foreach ($this->guessRepository->getAllInChallengeByRound($prevRound, $challenge, [User::class]) as $guess) {
 | 
						|
                    if ($guess->getUser()->getId() != $userId) {
 | 
						|
                        $response['allResults'][] = [
 | 
						|
                            'userName' => $guess->getUser()->getDisplayName(),
 | 
						|
                            'guessPosition' => $guess->getPosition()->toArray(),
 | 
						|
                            'distance' => $guess->getDistance(),
 | 
						|
                            'score' => $guess->getScore()
 | 
						|
                        ];
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        $response['restrictions'] = [
 | 
						|
            'timeLimit' => $challenge->getTimeLimit() * 1000,
 | 
						|
            'timeLimitType' => $challenge->getTimeLimitType(),
 | 
						|
            'noMove' => $challenge->getNoMove(),
 | 
						|
            'noPan' => $challenge->getNoPan(),
 | 
						|
            'noZoom' => $challenge->getNoZoom()
 | 
						|
        ];
 | 
						|
 | 
						|
        return $response;
 | 
						|
    }
 | 
						|
 | 
						|
    public function challengeInitialData(): IContent
 | 
						|
    {
 | 
						|
        $session = \Container::$request->session();
 | 
						|
        $userId = $session->get('userId');
 | 
						|
        $challengeToken_str = \Container::$request->query('challengeToken');
 | 
						|
        $userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, [Challenge::class]);
 | 
						|
 | 
						|
        if (!isset($userInChallenge)) {
 | 
						|
            return new JsonContent(['error' => 'game_not_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        $challenge = $userInChallenge->getChallenge();
 | 
						|
        $currentRound = $userInChallenge->getCurrentRound();
 | 
						|
 | 
						|
        $response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true);
 | 
						|
 | 
						|
        if ($challenge->getTimeLimitType() === 'game' && $challenge->getTimeLimit() !== null && $userInChallenge->getCurrentRound() > 0) {
 | 
						|
            $timeLimit = max(10, $userInChallenge->getTimeLeft());
 | 
						|
            $response['restrictions']['timeLimit'] = $timeLimit * 1000;
 | 
						|
        }
 | 
						|
 | 
						|
        return new JsonContent($response);
 | 
						|
    }
 | 
						|
 | 
						|
    public function guess(): IContent
 | 
						|
    {
 | 
						|
        $mapId = (int) \Container::$request->query('mapId');
 | 
						|
        $session = \Container::$request->session();
 | 
						|
 | 
						|
        if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
 | 
						|
            return new JsonContent(['error' => 'no_session_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        $last = $state['rounds'][$state['currentRound']];
 | 
						|
        $guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
 | 
						|
        $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
 | 
						|
 | 
						|
        $last['guessPosition'] = $guessPosition;
 | 
						|
        $last['distance'] = $result['distance'];
 | 
						|
        $last['score'] = $result['score'];
 | 
						|
 | 
						|
        $response = [
 | 
						|
            'position' => $last['position']->toArray(),
 | 
						|
            'result' => $result
 | 
						|
        ];
 | 
						|
 | 
						|
        $state['rounds'][$state['currentRound']] = $last;
 | 
						|
        $state['currentRound'] += 1;
 | 
						|
        if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
 | 
						|
            $next = $state['rounds'][$state['currentRound']];
 | 
						|
 | 
						|
            $response['place'] = [
 | 
						|
                'panoId' => $next['panoId'],
 | 
						|
                'pov' => $next['pov']->toArray()
 | 
						|
            ];
 | 
						|
        }
 | 
						|
 | 
						|
        $session->set('state', $state);
 | 
						|
 | 
						|
        $this->saveVisit($last['placeId']);
 | 
						|
 | 
						|
        return new JsonContent($response);
 | 
						|
    }
 | 
						|
 | 
						|
    // save the selected place for the round in UserPlayedPlace
 | 
						|
    private function saveVisit($placeId): void
 | 
						|
    {
 | 
						|
        $session = \Container::$request->session();
 | 
						|
        $userId = $session->get('userId');
 | 
						|
 | 
						|
        if (isset($userId)) {
 | 
						|
            $userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
 | 
						|
            if (!$userPlayedPlace) {
 | 
						|
                $userPlayedPlace = new UserPlayedPlace();
 | 
						|
                $userPlayedPlace->setUserId($userId);
 | 
						|
                $userPlayedPlace->setPlaceId($placeId);
 | 
						|
            } else {
 | 
						|
                $userPlayedPlace->incrementOccurrences();
 | 
						|
            }
 | 
						|
            $userPlayedPlace->setLastTimeDate(new DateTime());
 | 
						|
            \Container::$persistentDataManager->saveToDb($userPlayedPlace);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public function multiGuess(): IContent
 | 
						|
    {
 | 
						|
        $roomId = \Container::$request->query('roomId');
 | 
						|
        $session = \Container::$request->session();
 | 
						|
 | 
						|
        if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
 | 
						|
            return new JsonContent(['error' => 'no_session_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        $room = $this->multiRoomRepository->getByRoomId($roomId);
 | 
						|
        $state = $room->getStateArray();
 | 
						|
 | 
						|
        $last = $state['rounds'][$state['currentRound']];
 | 
						|
        $guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
 | 
						|
        $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']);
 | 
						|
 | 
						|
        $responseFromMulti = $this->multiConnector->sendMessage('guess', [
 | 
						|
            'roomId' => $roomId,
 | 
						|
            'token' => $multiState['token'],
 | 
						|
            'guessPosition' => $guessPosition->toArray(),
 | 
						|
            'distance' => $result['distance'],
 | 
						|
            'score' => $result['score']
 | 
						|
        ]);
 | 
						|
 | 
						|
        if (isset($responseFromMulti['error'])) {
 | 
						|
            return new JsonContent(['error' => $responseFromMulti['error']]);
 | 
						|
        }
 | 
						|
 | 
						|
        $response = [
 | 
						|
            'position' => $last['position']->toArray(),
 | 
						|
            'result' => $result,
 | 
						|
            'allResults' => $responseFromMulti['allResults']
 | 
						|
        ];
 | 
						|
 | 
						|
        return new JsonContent($response);
 | 
						|
    }
 | 
						|
 | 
						|
    public function challengeGuess(): IContent
 | 
						|
    {
 | 
						|
        $session = \Container::$request->session();
 | 
						|
        $userId = $session->get('userId');
 | 
						|
        $challengeToken_str = \Container::$request->query('challengeToken');
 | 
						|
        $userInChallenge = $this->userInChallengeRepository->getByUserIdAndToken($userId, $challengeToken_str, [Challenge::class]);
 | 
						|
 | 
						|
        if (!isset($userInChallenge)) {
 | 
						|
            return new JsonContent(['error' => 'game_not_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        $challenge = $userInChallenge->getChallenge();
 | 
						|
        $currentRound = $userInChallenge->getCurrentRound();
 | 
						|
        $currentPlaceInChallenge = $this->placeInChallengeRepository->getByRoundInChallenge($currentRound, $challenge, [Place::class, Map::class]);
 | 
						|
        $currentPlace = $currentPlaceInChallenge->getPlace();
 | 
						|
        $map = $currentPlace->getMap();
 | 
						|
 | 
						|
        // creating response
 | 
						|
        $nextRound = $currentRound + 1;
 | 
						|
        $response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
 | 
						|
        $response['position'] = $currentPlace->getPosition()->toArray();
 | 
						|
 | 
						|
        if (\Container::$request->post('lat') && \Container::$request->post('lng')) {
 | 
						|
            $guessPosition = new Position((float) \Container::$request->post('lat'), (float) \Container::$request->post('lng'));
 | 
						|
            $result = $this->evaluateGuess($currentPlace->getPosition(), $guessPosition, $map->getArea());
 | 
						|
 | 
						|
            // save guess
 | 
						|
            $guess = new Guess();
 | 
						|
            $guess->setUserId($userId);
 | 
						|
            $guess->setPlaceInChallenge($currentPlaceInChallenge);
 | 
						|
            $guess->setPosition($guessPosition);
 | 
						|
            $guess->setDistance($result['distance']);
 | 
						|
            $guess->setScore($result['score']);
 | 
						|
            \Container::$persistentDataManager->saveToDb($guess);
 | 
						|
 | 
						|
            $response['result'] = $result;
 | 
						|
 | 
						|
        } else {
 | 
						|
            // user didn't manage to guess in the round in the given timeframe
 | 
						|
            $response['result'] = ['distance' => null, 'score' => 0];
 | 
						|
        }
 | 
						|
 | 
						|
        // save user relevant state of challenge
 | 
						|
        $userInChallenge->setCurrentRound($nextRound);
 | 
						|
        $timeLeft = \Container::$request->post('timeLeft');
 | 
						|
        if (isset($timeLeft)) {
 | 
						|
            $userInChallenge->setTimeLeft(intval($timeLeft));
 | 
						|
        }
 | 
						|
        \Container::$persistentDataManager->saveToDb($userInChallenge);
 | 
						|
 | 
						|
        if ($challenge->getTimeLimitType() === 'game' && isset($timeLeft)) {
 | 
						|
            $timeLimit = max(10, intval($timeLeft));
 | 
						|
            $response['restrictions']['timeLimit'] = $timeLimit * 1000;
 | 
						|
        }
 | 
						|
 | 
						|
        if (isset($response['history'][$currentRound]['allResults'])) {
 | 
						|
            $response['allResults'] = $response['history'][$currentRound]['allResults'];
 | 
						|
        }
 | 
						|
 | 
						|
        $this->saveVisit($currentPlace->getId());
 | 
						|
 | 
						|
        return new JsonContent($response);
 | 
						|
    }
 | 
						|
 | 
						|
    public function multiNextRound(): IContent
 | 
						|
    {
 | 
						|
        $roomId = \Container::$request->query('roomId');
 | 
						|
        $session = \Container::$request->session();
 | 
						|
 | 
						|
        if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
 | 
						|
            return new JsonContent(['error' => 'no_session_found']);
 | 
						|
        }
 | 
						|
 | 
						|
        $room = $this->multiRoomRepository->getByRoomId($roomId);
 | 
						|
        $state = $room->getStateArray();
 | 
						|
        $members = $room->getMembersArray();
 | 
						|
 | 
						|
        if ($members['owner'] !== $multiState['token']) {
 | 
						|
            return new JsonContent(['error' => 'not_owner_of_room']);
 | 
						|
        }
 | 
						|
 | 
						|
        $state['currentRound'] += 1;
 | 
						|
        if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
 | 
						|
            $this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
 | 
						|
        }
 | 
						|
 | 
						|
        $room->setStateArray($state);
 | 
						|
        $room->setUpdatedDate(new DateTime());
 | 
						|
        \Container::$persistentDataManager->saveToDb($room);
 | 
						|
 | 
						|
        return new JsonContent(['ok' => true]);
 | 
						|
    }
 | 
						|
 | 
						|
    private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area)
 | 
						|
    {
 | 
						|
        $distance = $this->calculateDistance($realPosition, $guessPosition);
 | 
						|
        $score = $this->calculateScore($distance, $area);
 | 
						|
 | 
						|
        return ['distance' => $distance, 'score' => $score];
 | 
						|
    }
 | 
						|
 | 
						|
    private function startNewGame(array &$state, int $mapId): void
 | 
						|
    {
 | 
						|
        $session = \Container::$request->session();
 | 
						|
        $userId = $session->get('userId');
 | 
						|
 | 
						|
        $places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId);
 | 
						|
 | 
						|
        $state['rounds'] = [];
 | 
						|
        $state['currentRound'] = 0;
 | 
						|
 | 
						|
        foreach ($places as $place) {
 | 
						|
            $state['rounds'][] = [
 | 
						|
                'placeId' => $place->getId(),
 | 
						|
                'position' => $place->getPosition(),
 | 
						|
                'panoId' => $place->getPanoIdCached(),
 | 
						|
                'pov' => $place->getPov()
 | 
						|
            ];
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private function calculateDistance(Position $realPosition, Position $guessPosition): float
 | 
						|
    {
 | 
						|
        return $realPosition->calculateDistanceTo($guessPosition);
 | 
						|
    }
 | 
						|
 | 
						|
    private function calculateScore(float $distance, float $area): int
 | 
						|
    {
 | 
						|
        $goodness = 1.0 - ($distance / (sqrt($area) * 1000));
 | 
						|
 | 
						|
        return (int) round(pow(static::MAX_SCORE, $goodness));
 | 
						|
    }
 | 
						|
}
 |