diff --git a/database/migrations/structure/20210510_2000_challenge.sql b/database/migrations/structure/20210510_2000_challenge.sql new file mode 100644 index 0000000..a5c4941 --- /dev/null +++ b/database/migrations/structure/20210510_2000_challenge.sql @@ -0,0 +1,54 @@ +CREATE TABLE `challenges` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `token` int(10) unsigned NOT NULL, + `time_limit` int(10) unsigned, + `time_limit_type` enum('game', 'round') NOT NULL DEFAULT 'game', + `no_move` tinyint(1) NOT NULL DEFAULT 0, + `no_pan` tinyint(1) NOT NULL DEFAULT 0, + `no_zoom` tinyint(1) NOT NULL DEFAULT 0, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +CREATE TABLE `user_in_challenge` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `challenge_id` int(10) unsigned NOT NULL, + `current_round` smallint(5) signed NOT NULL DEFAULT 0, + `time_left` int(10) unsigned, + `is_owner` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `challenge_id` (`challenge_id`), + CONSTRAINT `user_in_challenge_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + CONSTRAINT `user_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +CREATE TABLE `place_in_challenge` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `place_id` int(10) unsigned NOT NULL, + `challenge_id` int(10) unsigned NOT NULL, + `round` smallint(5) unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `place_id` (`place_id`), + KEY `challenge_id` (`challenge_id`), + CONSTRAINT `place_in_challenge_place_id` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`), + CONSTRAINT `place_in_challenge_challenge_id` FOREIGN KEY (`challenge_id`) REFERENCES `challenges` (`id`), + CONSTRAINT `unique_order_in_challenge` UNIQUE (`round`, `challenge_id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; + +CREATE TABLE `guesses` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `place_in_challenge_id` int(10) unsigned NOT NULL, + `lat` decimal(8,6) NOT NULL, + `lng` decimal(9,6) NOT NULL, + `score` int(10) NOT NULL, + `distance` int(10) NOT NULL, + `time_spent` int(10), + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `place_in_challenge_id` (`place_in_challenge_id`), + CONSTRAINT `guesses_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), + CONSTRAINT `guesses_place_in_challenge_id` FOREIGN KEY (`place_in_challenge_id`) REFERENCES `place_in_challenge` (`id`) +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; \ No newline at end of file diff --git a/public/static/css/game.css b/public/static/css/game.css index a23d889..2970387 100644 --- a/public/static/css/game.css +++ b/public/static/css/game.css @@ -15,6 +15,17 @@ right: 0; background-color: #000000; opacity: 0.5; + z-index: 4; +} + +#panningBlockerCover { + display: none; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0; z-index: 3; } @@ -22,7 +33,7 @@ position: absolute; bottom: 30px; right: 20px; - z-index: 2; + z-index: 3; } #guess.result { @@ -153,6 +164,47 @@ z-index: 2; } +#goToStart { + display: none; +} + +#highscoresTable { + margin: 1em; + border-collapse: collapse; + width: 90%; +} + +#highscoresTable td, #highscoresTable th { + border: 1px solid #ddd; + padding: 8px; +} + +#highscoresTable tr:nth-child(even) { + background-color: #f2f2f2; +} + +#highscoresTable tr:hover { + background-color: #ddd; +} + +#highscoresTable th { + padding-top: 12px; + padding-bottom: 12px; + text-align: left; + background-color: #e8a349; + color: white; +} + +#highscoresTable tr.ownPlayer { + font-weight: 500; +} + +@media screen and (max-width: 899px) { + .hideOnNarrowScreen { + display: none; + } +} + @media screen and (max-width: 599px) { #mapName { display: none; diff --git a/public/static/css/mapguesser.css b/public/static/css/mapguesser.css index 3a8e64e..a5eb801 100644 --- a/public/static/css/mapguesser.css +++ b/public/static/css/mapguesser.css @@ -31,11 +31,11 @@ main { color: #ffffff; } -p, h1, h2, input, textarea, select, button, a { +p, h1, h2, h3, input, textarea, select, button, a, table, label { font-family: 'Roboto', sans-serif; } -h1, h2 { +h1, h2, h3 { font-weight: 500; } @@ -55,7 +55,11 @@ h2, header.small h1 { font-size: 24px; } -p, h2 { +h3 { + font-size: 18px; +} + +p, h2, h3 { line-height: 150%; } diff --git a/public/static/css/maps.css b/public/static/css/maps.css index c8a58fe..70e7821 100644 --- a/public/static/css/maps.css +++ b/public/static/css/maps.css @@ -75,6 +75,21 @@ div.mapItem>div.buttonContainer { grid-auto-flow: column; } +#restrictions input { + height: auto; + margin: 0.5em; +} + +#restrictions input[type=range] { + height: 1.5em; + margin-left: 2em; + width: 70%; +} + +#timeLimitType { + margin-left: 2em; +} + @media screen and (min-width: 1504px) { #mapContainer { grid-template-columns: auto auto auto auto; diff --git a/public/static/js/game.js b/public/static/js/game.js index b52292d..8d73f7f 100644 --- a/public/static/js/game.js +++ b/public/static/js/game.js @@ -1,10 +1,13 @@ 'use strict'; +const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 }); + (function () { var Game = { NUMBER_OF_ROUNDS: 5, MAX_SCORE: 1000, + type: GameType.SINGLE, mapBounds: null, multi: { token: null, owner: false }, rounds: [], @@ -16,6 +19,8 @@ guessMarker: null, adaptGuess: false, googleLink: null, + history: [], + restrictions: null, readyToContinue: true, timeoutEnd: null, @@ -211,6 +216,19 @@ } }, + getGameIdentifier: function () { + switch (Game.type) { + case GameType.SINGLE: + return '/game/' + mapId; + case GameType.MULTI: + return '/multiGame/' + roomId; + case GameType.CHALLENGE: + return '/challenge/' + challengeToken; + default: + return '/game/' + mapId; + } + }, + prepare: function () { var data = new FormData(); var userNames; @@ -226,7 +244,7 @@ } document.getElementById('loading').style.visibility = 'visible'; - var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json'; + var url = Game.getGameIdentifier() + '/prepare.json'; MapGuesser.httpRequest('POST', url, function () { document.getElementById('loading').style.visibility = 'hidden'; @@ -269,7 +287,7 @@ } document.getElementById('loading').style.visibility = 'visible'; - MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () { + MapGuesser.httpRequest('POST', Game.getGameIdentifier() + '/initialData.json', function () { document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('panoCover').style.visibility = 'hidden'; @@ -278,24 +296,199 @@ return; } - Game.panoId = this.response.place.panoId; - Game.pov = this.response.place.pov; + Game.loadHistory(this.response); - for (var i = 0; i < this.response.history.length; ++i) { - var round = this.response.history[i]; - Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] }); - Game.addPositionToResultMap(true); - Game.addGuessPositionToResultMap(round.result.guessPosition, null, true); - Game.scoreSum += round.result.score; + Game.restrictions = this.response.restrictions; + Game.displayRestrictions(); + + if (this.response.finished) { + + Game.transitToResultMap(); + Game.showSummary(); + + } else { + + Game.panoId = this.response.place.panoId; + Game.pov = this.response.place.pov; + + Game.startNewRound(); } document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS); document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE); - - Game.startNewRound(); }); }, + enableRestrictions: function () { + if (!Game.restrictions) { + return; + } + + Game.panorama.setOptions({ + clickToGo: !Game.restrictions.noMove, + linksControl: !(Game.restrictions.noMove || Game.restrictions.noPan), + scrollwheel: !Game.restrictions.noZoom + }); + + if (Game.restrictions.noPan) { + document.getElementById('panningBlockerCover').style.display = 'block'; + } + + if (Game.restrictions.timeLimit) { + Game.startCountdown(Game.restrictions.timeLimit, function () { + Game.guess(); + }); + } + + }, + + displayRestrictions: function () { + if (!Game.restrictions) { + return; + } + + var restrictionsForDisplay = []; + if (Game.restrictions.timeLimit) { + restrictionsForDisplay.push('time limit per ' + Game.restrictions.timeLimitType); + } + if (Game.restrictions.noPan) { + restrictionsForDisplay.push('no camera change'); + } + else { + if (Game.restrictions.noMove) { + restrictionsForDisplay.push('no move'); + } + if (Game.restrictions.noZoom) { + restrictionsForDisplay.push('no zoom'); + } + } + + if (restrictionsForDisplay.length == 0) { + return; + } + + // create restrictions span for header + var restrictions = document.createElement('span'); + restrictions.setAttribute('id', 'restrictions'); + restrictions.setAttribute('class', 'hideOnNarrowScreen'); + var restrictionsTitle = document.createElement('span'); + restrictionsTitle.setAttribute('class', 'bold'); + restrictionsTitle.innerText = 'Restrictions: '; + var restrictionsList = document.createElement('span'); + restrictionsList.innerText = restrictionsForDisplay.join(', '); + restrictions.appendChild(restrictionsTitle); + restrictions.appendChild(restrictionsList); + + var roundContainer = document.getElementById('roundContainer'); + var header = roundContainer.parentNode; + header.insertBefore(restrictions, roundContainer); + }, + + disableRestrictions: function () { + + Game.panorama.setOptions({ + clickToGo: true, + linksControl: true, + scrollwheel: true + }); + + document.getElementById('panningBlockerCover').style.display = null; + + Game.startCountdown(0); + Game.timeoutEnd = null; + }, + + hideRestrictions: function () { + var restrictions = document.getElementById('restrictions'); + if (restrictions) { + var header = restrictions.parentNode; + header.removeChild(restrictions); + } + }, + + transitToResultMap: function () { + // TODO: refactor - it is necessary for mobile + if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') { + document.getElementById('showGuessButton').click(); + } + + if (Game.adaptGuess) { + document.getElementById('guess').classList.remove('adapt'); + } + + if (Game.guessMarker) { + Game.guessMarker.setMap(null); + Game.guessMarker = null; + } + + document.getElementById('guess').classList.add('result'); + + Game.map.setOptions({ + draggableCursor: 'grab' + }); + + if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) { + document.getElementById('continueButton').style.display = 'none'; + document.getElementById('showSummaryButton').style.display = 'block'; + } else if (Game.type == GameType.MULTI) { + if (Game.multi.owner) { + if (!Game.readyToContinue) { + document.getElementById('continueButton').disabled = true; + } + } else { + document.getElementById('continueButton').style.display = 'none'; + } + } + }, + + loadHistory: function (response) { + if (!response.history) + return; + + Game.history = response.history; + + for (var i = 0; i < Game.rounds.length; ++i) { + var round = Game.rounds[i]; + + if (round.realMarker) { + round.realMarker.setMap(null); + } + for (var j = 0; j < round.guessMarkers.length; ++j) { + var guessMarker = round.guessMarkers[j]; + guessMarker.marker.setMap(null); + guessMarker.line.setMap(null); + if (guessMarker.info) { + guessMarker.info.close(); + } + } + } + Game.rounds = []; + + Game.scoreSum = 0; + for (var i = 0; i < Game.history.length; ++i) { + var round = Game.history[i]; + + if (round.result) { + Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] }); + Game.addPositionToResultMap(true); + if (round.result.guessPosition) { + Game.addGuessPositionToResultMap(round.result.guessPosition, round.result, true); + } + Game.scoreSum += round.result.score; + + + if (round.allResults !== undefined) { + for (var j = 0; j < round.allResults.length; ++j) { + var result = round.allResults[j]; + if (result.guessPosition) { + Game.addGuessPositionToResultMap(result.guessPosition, result, true); + } + } + } + } + } + }, + reset: function () { if (Game.guessMarker) { Game.guessMarker.setMap(null); @@ -325,6 +518,7 @@ distanceInfo.children[0].style.display = null; distanceInfo.children[1].style.display = null; distanceInfo.children[2].style.display = null; + document.getElementById('summaryInfo').innerHTML = "Game finished." var scoreInfo = document.getElementById('scoreInfo'); scoreInfo.children[0].style.display = null; scoreInfo.children[1].style.display = null; @@ -339,6 +533,13 @@ // needs to be set visible after the show guess map hid it in mobile view document.getElementById("navigation").style.visibility = 'visible'; + Game.disableRestrictions(); + Game.hideRestrictions(); + + document.getElementById('panningBlockerCover').style.display = null; + + Game.history = []; + Game.initialize(); }, @@ -391,6 +592,8 @@ // update the compass const heading = Game.panorama.getPov().heading; document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)"; + + Game.enableRestrictions(); }, handleErrorResponse: function (error) { @@ -411,7 +614,11 @@ break; case 'game_not_found': - MapGuesser.showModalWithContent('Error', 'The game room was not found by this ID. Please check the link.'); + MapGuesser.showModalWithContent('Error', 'The game was not found by this ID. Please check the link.'); + break; + + case 'anonymous_user': + MapGuesser.showModalWithContent('Error', 'You have to login to join a challenge!'); break; default: @@ -441,7 +648,7 @@ resultBounds.extend(position); if (guessPosition) { - Game.addGuessPositionToResultMap(guessPosition); + Game.addGuessPositionToResultMap(guessPosition, result); resultBounds.extend(guessPosition); } @@ -477,25 +684,9 @@ }, showResultMap: function (result, resultBounds) { - // TODO: refactor - it is necessary for mobile - if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') { - document.getElementById('showGuessButton').click(); - } - if (Game.adaptGuess) { - document.getElementById('guess').classList.remove('adapt'); - } + Game.transitToResultMap(); - if (Game.guessMarker) { - Game.guessMarker.setMap(null); - Game.guessMarker = null; - } - - document.getElementById('guess').classList.add('result'); - - Game.map.setOptions({ - draggableCursor: 'grab' - }); Game.map.fitBounds(resultBounds); var distanceInfo = document.getElementById('distanceInfo'); @@ -514,38 +705,32 @@ var scoreBar = document.getElementById('scoreBar'); scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.width = scoreBarProperties.width; - - if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) { - document.getElementById('continueButton').style.display = 'none'; - document.getElementById('showSummaryButton').style.display = 'block'; - } else if (roomId) { - if (Game.multi.owner) { - if (!Game.readyToContinue) { - document.getElementById('continueButton').disabled = true; - } - } else { - document.getElementById('continueButton').style.display = 'none'; - } - } }, guess: function () { - if (!Game.guessMarker) { - return; + + var data = new FormData(); + + if (Game.timeoutEnd) { + var timeLeft = Math.ceil((Game.timeoutEnd - new Date()) / 1000); + data.append('timeLeft', timeLeft); } - var guessPosition = Game.guessMarker.getPosition().toJSON(); - Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition; + Game.disableRestrictions(); + + if (Game.guessMarker) { + var guessPosition = Game.guessMarker.getPosition().toJSON(); + Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition; + + data.append('lat', String(guessPosition.lat)); + data.append('lng', String(guessPosition.lng)); + } document.getElementById('guessButton').disabled = true; document.getElementById('panoCover').style.visibility = 'visible'; - - var data = new FormData(); - data.append('lat', String(guessPosition.lat)); - data.append('lng', String(guessPosition.lng)); - document.getElementById('loading').style.visibility = 'visible'; - var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json'; + var url = Game.getGameIdentifier() + '/guess.json'; + MapGuesser.httpRequest('POST', url, function () { document.getElementById('loading').style.visibility = 'hidden'; @@ -554,12 +739,16 @@ return; } + Game.loadHistory(this.response); + Game.restrictions = this.response.restrictions; + Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults); if (this.response.place) { Game.panoId = this.response.place.panoId; Game.pov = this.response.place.pov; } + }, data); }, @@ -593,8 +782,8 @@ var position = round.position; var guessMarker = { marker: null, line: null, info: null }; - var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg'; - var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?'; + var markerSvg = result && result.userName ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg'; + var markerLabel = result && result.userName ? result.userName.charAt(0).toUpperCase() : '?'; guessMarker.marker = new google.maps.Marker({ map: Game.map, @@ -644,8 +833,9 @@ }); if (result) { + const userName = result.userName ? result.userName : 'me'; guessMarker.info = new google.maps.InfoWindow({ - content: '

' + result.userName + '

' + + content: '

' + userName + '

' + '

' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points

', }); @@ -672,6 +862,36 @@ return { width: percent + '%', backgroundColor: color }; }, + calculateHighScores: function () { + + var highscores = new Map(); + highscores.set('me', Game.scoreSum); + + // collect the results of users who are through the last round + const round = Game.history[Game.history.length - 1]; + if (round.allResults) { + for (const result of round.allResults) { + highscores.set(result.userName, result.score); + } + } + + // add up scores only for the finishers + for (var i = Game.history.length - 2; i >= 0; --i) { + const round = Game.history[i]; + if (round.allResults) { + for (const result of round.allResults) { + if (highscores.has(result.userName)) { + highscores.set(result.userName, highscores.get(result.userName) + result.score); + } + } + } + } + + var sortedHighscores = Array.from(highscores, ([userName, score]) => ({ 'userName': userName, 'score': score })) + .sort(function (resultA, resultB) { return resultB.score - resultA.score }); + return sortedHighscores; + }, + showSummary: function () { var distanceInfo = document.getElementById('distanceInfo'); distanceInfo.children[0].style.display = 'none'; @@ -682,11 +902,13 @@ scoreInfo.children[1].style.display = 'block'; document.getElementById('showSummaryButton').style.display = null; - if (!roomId || Game.multi.owner) { + if (Game.type == GameType.SINGLE || Game.multi.owner) { document.getElementById('startNewGameButton').style.display = 'block'; if (!Game.readyToContinue) { document.getElementById('startNewGameButton').disabled = true; } + } else if (Game.type == GameType.CHALLENGE) { + document.getElementById('goToStart').style.display = 'block'; } var resultBounds = new google.maps.LatLngBounds(); @@ -729,6 +951,48 @@ var scoreBar = document.getElementById('scoreBar'); scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.width = scoreBarProperties.width; + + Game.showHighscores(); + + }, + + showHighscores: function () { + + if (Game.type == GameType.CHALLENGE) { + var highscores = this.calculateHighScores(); + var summaryInfo = document.getElementById('summaryInfo'); + + if (highscores.length > 2) { + var table = document.getElementById('highscoresTable'); + for (const result of highscores) { + var userName = document.createElement('td'); + userName.innerHTML = result.userName; + var score = document.createElement('td'); + score.innerHTML = result.score; + var line = document.createElement('tr'); + line.appendChild(userName); + line.appendChild(score); + table.appendChild(line); + + if (result.userName === 'me') { + line.setAttribute('class', 'ownPlayer'); + } + } + + MapGuesser.showModal('highscores'); + } else if (highscores.length == 2) { + + if (highscores[0].userName === 'me') { + summaryInfo.innerHTML = 'You won! ' + highscores[1].userName + ' got only ' + highscores[1].score + ' points.'; + } else { + summaryInfo.innerHTML = 'You lost! ' + highscores[0].userName + ' won with ' + highscores[0].score + ' points.'; + } + + } else if (highscores.length == 1) { + summaryInfo.innerHTML = 'You are the first to finish. Invite your friends by sending them the link.' + } + + } }, rewriteGoogleLink: function () { @@ -751,7 +1015,7 @@ }, 1); }, - startCountdown: function (timeout) { + startCountdown: function (timeout, timedOutHandler) { if (Game.countdownHandler) { clearInterval(Game.countdownHandler); } @@ -773,7 +1037,12 @@ Game.setCountdownTime(timeLeft); if (timeLeft <= 0) { - document.getElementById('panoCover').style.visibility = 'visible'; + if (typeof timedOutHandler === 'function') { + timedOutHandler(); + } else { + document.getElementById('panoCover').style.visibility = 'visible'; + } + clearInterval(Game.countdownHandler); } }, 1000); @@ -876,6 +1145,12 @@ document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)"; }); + if (roomId !== null) { + Game.type = GameType.MULTI; + } else if (challengeToken !== null) { + Game.type = GameType.CHALLENGE; + } + if (COOKIES_CONSENT) { Game.prepare(); } @@ -965,4 +1240,8 @@ document.getElementById('compassContainer').onclick = function () { Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch }); } + + document.getElementById('closeHighscoresButton').onclick = function () { + MapGuesser.hideModal(); + }; })(); diff --git a/public/static/js/maps.js b/public/static/js/maps.js index 6d4199f..35c4e33 100644 --- a/public/static/js/maps.js +++ b/public/static/js/maps.js @@ -65,6 +65,32 @@ } }; + var Util = { + printTimeForHuman: function (time) { + const minutes = Math.floor(time / 60); + const seconds = time % 60; + var time_str = ''; + + if (minutes == 1) { + time_str += '1 minute'; + } else if (minutes > 1) { + time_str += minutes + ' minutes'; + } + + if (minutes > 0 && seconds > 0) { + time_str += ' and '; + } + + if (seconds == 1) { + time_str += '1 second'; + } else if (seconds > 1) { + time_str += seconds + ' seconds'; + } + + return time_str; + } + }; + Maps.addStaticMaps(); Maps.initializeDescriptionDivs(); @@ -85,10 +111,42 @@ window.location.href = '/multiGame/' + this.elements.roomId.value; }; + document.getElementById('challengeForm').onsubmit = function (e) { + e.preventDefault(); + + var url = '/challenge/create.json'; + var formData = new FormData(this); + + document.getElementById('loading').style.visibility = 'visible'; + MapGuesser.httpRequest('POST', url, function() { + document.getElementById('loading').style.visibility = 'hidden'; + + if (this.response.error) { + Game.handleErrorResponse(this.response.error); + return; + } + + window.location.href = '/challenge/' + this.response.challengeToken; + + }, formData); + }; + document.getElementById('multiButton').onclick = function () { MapGuesser.showModal('multi'); document.getElementById('createNewRoomButton').href = '/multiGame/new/' + this.dataset.mapId; document.getElementById('multiForm').elements.roomId.select(); + document.getElementById('playMode').style.visibility = 'hidden'; + } + + if (document.getElementById('challengeButton')) { + document.getElementById('challengeButton').onclick = function () { + MapGuesser.showModal('challenge'); + document.getElementById('createNewChallengeButton').href = '/challenge/new/' + this.dataset.mapId; + document.getElementById('playMode').style.visibility = 'hidden'; + + var timeLimit = document.getElementById('timeLimit').value; + document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit); + }; } document.getElementById('closePlayModeButton').onclick = function () { @@ -99,6 +157,10 @@ MapGuesser.hideModal(); }; + document.getElementById('closeChallengeButton').onclick = function () { + MapGuesser.hideModal(); + } + var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton'); for (var i = 0; i < buttons.length; i++) { var button = buttons[i]; @@ -107,6 +169,13 @@ MapGuesser.showModal('playMode'); document.getElementById('singleButton').href = '/game/' + this.dataset.mapId; document.getElementById('multiButton').dataset.mapId = this.dataset.mapId; + document.getElementById('challengeMapId').value = this.dataset.mapId; }; } + + document.getElementById('timeLimit').oninput = function () { + var timeLimit = document.getElementById('timeLimit').value; + document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit); + document.getElementById('timerEnabled').checked = true; + } })(); diff --git a/src/Controller/GameController.php b/src/Controller/GameController.php index de9c832..9a9de40 100644 --- a/src/Controller/GameController.php +++ b/src/Controller/GameController.php @@ -9,14 +9,22 @@ use MapGuesser\Response\JsonContent; use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IRedirect; use MapGuesser\Multi\MultiConnector; +use MapGuesser\PersistentData\Model\Challenge; use MapGuesser\PersistentData\Model\MultiRoom; +use MapGuesser\PersistentData\Model\PlaceInChallenge; +use MapGuesser\PersistentData\Model\UserInChallenge; use MapGuesser\PersistentData\PersistentDataManager; +use MapGuesser\Repository\ChallengeRepository; use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MultiRoomRepository; +use MapGuesser\Repository\PlaceRepository; +use MapGuesser\Repository\UserInChallengeRepository; use MapGuesser\Response\Redirect; class GameController implements ISecured { + const NUMBER_OF_ROUNDS = 5; + private IRequest $request; private PersistentDataManager $pdm; @@ -27,6 +35,12 @@ class GameController implements ISecured private MapRepository $mapRepository; + private PlaceRepository $placeRepository; + + private ChallengeRepository $challengeRepository; + + private UserInChallengeRepository $userInChallengeRepository; + public function __construct(IRequest $request) { $this->request = $request; @@ -34,6 +48,9 @@ class GameController implements ISecured $this->multiConnector = new MultiConnector(); $this->multiRoomRepository = new MultiRoomRepository(); $this->mapRepository = new MapRepository(); + $this->placeRepository = new PlaceRepository(); + $this->challengeRepository = new ChallengeRepository(); + $this->userInChallengeRepository = new UserInChallengeRepository(); } public function authorize(): bool @@ -85,6 +102,75 @@ class GameController implements ISecured return new HtmlContent('game', ['roomId' => $roomId]); } + public function getChallenge(): IContent + { + $challengeToken = $this->request->query('challengeToken'); + + return new HtmlContent('game', ['challengeToken' => $challengeToken]); + } + + public function createNewChallenge(): IContent + { + // create Challenge + do { + // initiliaze or if a challenge with the same token already exists + $challengeToken = mt_rand(); + } while ($this->challengeRepository->getByToken($challengeToken)); + + $challenge = new Challenge(); + $challenge->setToken($challengeToken); + $challenge->setCreatedDate(new DateTime()); + + if ($this->request->post('timerEnabled') !== null && $this->request->post('timeLimit') !== null) { + $challenge->setTimeLimit($this->request->post('timeLimit')); + } + if ($this->request->post('timeLimitType') !== null) { + $challenge->setTimeLimitType($this->request->post('timeLimitType')); + } + if ($this->request->post('noMove') !== null) { + $challenge->setNoMove(true); + } + if ($this->request->post('noPan') !== null) { + $challenge->setNoPan(true); + } + if ($this->request->post('noZoom') !== null) { + $challenge->setNoZoom(true); + } + + $this->pdm->saveToDb($challenge); + + // save owner/creator + + $session = $this->request->session(); + $userId = $session->get('userId'); + + $userInChallenge = new UserInChallenge(); + $userInChallenge->setUserId($userId); + $userInChallenge->setChallenge($challenge); + $userInChallenge->setTimeLeft($challenge->getTimeLimit()); + $userInChallenge->setIsOwner(true); + + $this->pdm->saveToDb($userInChallenge); + + // select places + + $mapId = (int) $this->request->post('mapId'); + // $map = $this->mapRepository->getById($mapId); + + $places = $this->placeRepository->getRandomNPlaces($mapId, static::NUMBER_OF_ROUNDS, $userId); + + $round = 0; + foreach ($places as $place) { + $placeInChallenge = new PlaceInChallenge(); + $placeInChallenge->setPlace($place); + $placeInChallenge->setChallenge($challenge); + $placeInChallenge->setRound($round++); + $this->pdm->saveToDb($placeInChallenge); + } + + return new JsonContent(['challengeToken' => dechex($challengeToken)]); + } + public function prepareGame(): IContent { $mapId = (int) $this->request->query('mapId'); @@ -121,7 +207,7 @@ class GameController implements ISecured $room = $this->multiRoomRepository->getByRoomId($roomId); - if(!isset($room)) { + if (!isset($room)) { return new JsonContent(['error' => 'game_not_found']); } @@ -160,6 +246,42 @@ class GameController implements ISecured ]); } + public function prepareChallenge(): IContent + { + $challengeToken_str = $this->request->query('challengeToken'); + $session = $this->request->session(); + $userId = $session->get('userId'); + + if (!isset($userId)) + { + return new JsonContent(['error' => 'anonymous_user']); + } + + $challenge = $this->challengeRepository->getByTokenStr($challengeToken_str); + + if (!isset($challenge)) + { + return new JsonContent(['error' => 'game_not_found']); + } + + if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) { + // new player is joining + $userInChallenge = new UserInChallenge(); + $userInChallenge->setUserId($userId); + $userInChallenge->setChallenge($challenge); + $userInChallenge->setTimeLeft($challenge->getTimeLimit()); + $this->pdm->saveToDb($userInChallenge); + } + + $map = $this->mapRepository->getByChallenge($challenge); + + return new JsonContent([ + 'mapId' => $map->getId(), + 'mapName' => $map->getName(), + 'bounds' => $map->getBounds()->toArray() + ]); + } + private function getMultiToken(string $roomId): string { $session = $this->request->session(); diff --git a/src/Controller/GameFlowController.php b/src/Controller/GameFlowController.php index 5dfc4c6..3f4c586 100644 --- a/src/Controller/GameFlowController.php +++ b/src/Controller/GameFlowController.php @@ -8,10 +8,22 @@ use MapGuesser\Response\JsonContent; use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Multi\MultiConnector; use MapGuesser\PersistentData\PersistentDataManager; +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\ChallengeRepository; +use MapGuesser\Repository\GuessRepository; +use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MultiRoomRepository; +use MapGuesser\Repository\PlaceInChallengeRepository; use MapGuesser\Repository\PlaceRepository; +use MapGuesser\Repository\UserInChallengeRepository; use MapGuesser\Repository\UserPlayedPlaceRepository; +use MapGuesser\Repository\UserRepository; class GameFlowController implements ISecured { @@ -28,8 +40,20 @@ class GameFlowController implements ISecured private PlaceRepository $placeRepository; + private MapRepository $mapRepository; + + private UserRepository $userRepository; + private UserPlayedPlaceRepository $userPlayedPlaceRepository; + private ChallengeRepository $challengeRepository; + + private UserInChallengeRepository $userInChallengeRepository; + + private PlaceInChallengeRepository $placeInChallengeRepository; + + private GuessRepository $guessRepository; + public function __construct(IRequest $request) { $this->request = $request; @@ -37,7 +61,13 @@ class GameFlowController implements ISecured $this->multiConnector = new MultiConnector(); $this->multiRoomRepository = new MultiRoomRepository(); $this->placeRepository = new PlaceRepository(); + $this->mapRepository = new MapRepository(); + $this->userRepository = new UserRepository(); $this->userPlayedPlaceRepository = new UserPlayedPlaceRepository(); + $this->challengeRepository = new ChallengeRepository(); + $this->userInChallengeRepository = new UserInChallengeRepository(); + $this->placeInChallengeRepository = new PlaceInChallengeRepository(); + $this->guessRepository = new GuessRepository(); } public function authorize(): bool @@ -121,6 +151,110 @@ class GameFlowController implements ISecured 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 = $this->request->session(); + $userId = $session->get('userId'); + $challengeToken_str = $this->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) $this->request->query('mapId'); @@ -132,7 +266,7 @@ class GameFlowController implements ISecured $last = $state['rounds'][$state['currentRound']]; $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); - $result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']); + $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']); $last['guessPosition'] = $guessPosition; $last['distance'] = $result['distance']; @@ -156,21 +290,20 @@ class GameFlowController implements ISecured $session->set('state', $state); - $this->saveVisit($last); + $this->saveVisit($last['placeId']); return new JsonContent($response); } // save the selected place for the round in UserPlayedPlace - private function saveVisit($last): void + private function saveVisit($placeId): void { $session = $this->request->session(); $userId = $session->get('userId'); - if(isset($userId)) { - $placeId = $last['placeId']; + if (isset($userId)) { $userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId); - if(!$userPlayedPlace) { + if (!$userPlayedPlace) { $userPlayedPlace = new UserPlayedPlace(); $userPlayedPlace->setUserId($userId); $userPlayedPlace->setPlaceId($placeId); @@ -196,7 +329,7 @@ class GameFlowController implements ISecured $last = $state['rounds'][$state['currentRound']]; $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); - $result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']); + $result = $this->evaluateGuess($last['position'], $guessPosition, $state['area']); $responseFromMulti = $this->multiConnector->sendMessage('guess', [ 'roomId' => $roomId, @@ -219,6 +352,70 @@ class GameFlowController implements ISecured return new JsonContent($response); } + public function challengeGuess(): IContent + { + $session = $this->request->session(); + $userId = $session->get('userId'); + $challengeToken_str = $this->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 ($this->request->post('lat') && $this->request->post('lng')) { + $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->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']); + $this->pdm->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 = $this->request->post('timeLeft'); + if (isset($timeLeft)) { + $userInChallenge->setTimeLeft(intval($timeLeft)); + } + $this->pdm->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 = $this->request->query('roomId'); @@ -248,7 +445,7 @@ class GameFlowController implements ISecured return new JsonContent(['ok' => true]); } - private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area) + private function evaluateGuess(Position $realPosition, Position $guessPosition, float $area) { $distance = $this->calculateDistance($realPosition, $guessPosition); $score = $this->calculateScore($distance, $area); diff --git a/src/Controller/MapAdminController.php b/src/Controller/MapAdminController.php index 12b260a..2a10ce9 100644 --- a/src/Controller/MapAdminController.php +++ b/src/Controller/MapAdminController.php @@ -5,11 +5,16 @@ use MapGuesser\Interfaces\Authentication\IUser; use MapGuesser\Interfaces\Authorization\ISecured; use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Response\IContent; +use MapGuesser\PersistentData\Model\Challenge; use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\PersistentDataManager; +use MapGuesser\Repository\ChallengeRepository; +use MapGuesser\Repository\GuessRepository; use MapGuesser\Repository\MapRepository; +use MapGuesser\Repository\PlaceInChallengeRepository; use MapGuesser\Repository\PlaceRepository; +use MapGuesser\Repository\UserInChallengeRepository; use MapGuesser\Repository\UserPlayedPlaceRepository; use MapGuesser\Response\HtmlContent; use MapGuesser\Response\JsonContent; @@ -30,6 +35,14 @@ class MapAdminController implements ISecured private UserPlayedPlaceRepository $userPlayedPlaceRepository; + private ChallengeRepository $challengeRepository; + + private GuessRepository $guessRepository; + + private PlaceInChallengeRepository $placeInChallengeRepository; + + private UserInChallengeRepository $userInChallengeRepository; + public function __construct(IRequest $request) { $this->request = $request; @@ -37,6 +50,10 @@ class MapAdminController implements ISecured $this->mapRepository = new MapRepository(); $this->placeRepository = new PlaceRepository(); $this->userPlayedPlaceRepository = new UserPlayedPlaceRepository(); + $this->challengeRepository = new ChallengeRepository(); + $this->guessRepository = new GuessRepository(); + $this->placeInChallengeRepository = new PlaceInChallengeRepository(); + $this->userInChallengeRepository = new UserInChallengeRepository(); } public function authorize(): bool @@ -188,6 +205,10 @@ class MapAdminController implements ISecured $this->pdm->deleteFromDb($userPlayedPlace); } + foreach ($this->challengeRepository->getAllByPlace($place) as $challenge) { + $this->deleteChallenge($challenge); + } + $this->pdm->deleteFromDb($place); } @@ -198,6 +219,23 @@ class MapAdminController implements ISecured } } + private function deleteChallenge(Challenge $challenge): void + { + foreach ($this->userInChallengeRepository->getAllByChallenge($challenge) as $userInChallenge) { + $this->pdm->deleteFromDb($userInChallenge); + } + + foreach ($this->guessRepository->getAllInChallenge($challenge, [PlaceInChallenge::class]) as $guess) { + $this->pdm->deleteFromDb($guess); + } + + foreach ($this->placeInChallengeRepository->getAllByChallenge($challenge) as $placeInChallenge) { + $this->pdm->deleteFromDb($placeInChallenge); + } + + $this->pdm->deleteFromDb($challenge); + } + private function calculateMapBounds(Map $map): Bounds { $bounds = new Bounds(); diff --git a/src/Controller/MapsController.php b/src/Controller/MapsController.php index 4385e76..87bca4c 100644 --- a/src/Controller/MapsController.php +++ b/src/Controller/MapsController.php @@ -48,6 +48,7 @@ class MapsController $user = $this->request->user(); return new HtmlContent('maps', [ 'maps' => $maps, + 'isLoggedIn' => $user !== null, 'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN) ]); } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 36397d8..2860d97 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -9,7 +9,10 @@ use MapGuesser\Interfaces\Response\IRedirect; use MapGuesser\OAuth\GoogleOAuth; use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\Model\User; +use MapGuesser\PersistentData\Model\UserInChallenge; +use MapGuesser\Repository\GuessRepository; use MapGuesser\Repository\UserConfirmationRepository; +use MapGuesser\Repository\UserInChallengeRepository; use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPlayedPlaceRepository; use MapGuesser\Response\HtmlContent; @@ -29,6 +32,10 @@ class UserController implements ISecured private UserPlayedPlaceRepository $userPlayedPlaceRepository; + private UserInChallengeRepository $userInChallengeRepository; + + private GuessRepository $guessRepository; + public function __construct(IRequest $request) { $this->request = $request; @@ -36,6 +43,8 @@ class UserController implements ISecured $this->userConfirmationRepository = new UserConfirmationRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPlayedPlaceRepository = new UserPlayedPlaceRepository(); + $this->userInChallengeRepository = new UserInChallengeRepository(); + $this->guessRepository = new GuessRepository(); } public function authorize(): bool @@ -209,6 +218,14 @@ class UserController implements ISecured $this->pdm->deleteFromDb($userPlayedPlace); } + foreach ($this->userInChallengeRepository->getAllByUser($user) as $userInChallenge) { + $this->pdm->deleteFromDb($userInChallenge); + } + + foreach ($this->guessRepository->getAllByUser($user) as $guess) { + $this->pdm->deleteFromDb($guess); + } + $this->pdm->deleteFromDb($user); \Container::$dbConnection->commit(); diff --git a/src/Database/Query/Select.php b/src/Database/Query/Select.php index d62bd84..f9c2879 100644 --- a/src/Database/Query/Select.php +++ b/src/Database/Query/Select.php @@ -261,7 +261,7 @@ class Select $queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0]; } - if($this->isDerivedTable()) { + if ($this->isDerivedTable()) { $queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY]; } @@ -276,7 +276,7 @@ class Select return [(string) $table, $params]; } - if($table instanceof Select) + if ($table instanceof Select) { return $table->generateQuery(); } @@ -332,7 +332,7 @@ class Select $joinQueries = []; $params = []; - foreach($this->joins as $join) { + foreach ($this->joins as $join) { list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true); $joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]); $params = array_merge($params, $paramsFragment); diff --git a/src/PersistentData/Model/Challenge.php b/src/PersistentData/Model/Challenge.php new file mode 100644 index 0000000..4e61cc9 --- /dev/null +++ b/src/PersistentData/Model/Challenge.php @@ -0,0 +1,112 @@ +token = $token; + } + + public function setTimeLimit(?int $timeLimit): void + { + if (isset($timeLimit)) { + $this->timeLimit = $timeLimit; + } + } + + public function setTimeLimitType(string $timeLimitType): void + { + if (in_array($timeLimitType, self::$timeLimitTypes)) { + $this->timeLimitType = $timeLimitType; + } + } + + public function setNoMove(bool $noMove): void + { + $this->noMove = $noMove; + } + + public function setNoPan(bool $noPan): void + { + $this->noPan = $noPan; + } + + public function setNoZoom(bool $noZoom): void + { + $this->noZoom = $noZoom; + } + + public function setCreatedDate(DateTime $created): void + { + $this->created = $created; + } + + public function setCreated(string $created): void + { + $this->created = new DateTime($created); + } + + public function getToken(): int + { + return $this->token; + } + + public function getTimeLimit(): ?int + { + return $this->timeLimit; + } + + public function getTimeLimitType(): string + { + return $this->timeLimitType; + } + + public function getNoMove(): bool + { + return $this->noMove; + } + + public function getNoPan(): bool + { + return $this->noPan; + } + + public function getNoZoom(): bool + { + return $this->noZoom; + } + + public function getCreatedDate(): DateTime + { + return $this->created; + } + + public function getCreated(): string + { + return $this->created->format('Y-m-d H:i:s'); + } +} diff --git a/src/PersistentData/Model/Guess.php b/src/PersistentData/Model/Guess.php new file mode 100644 index 0000000..913f65f --- /dev/null +++ b/src/PersistentData/Model/Guess.php @@ -0,0 +1,133 @@ + User::class, 'place_in_challenge' => PlaceInChallenge::class]; + + private ?User $user = null; + + private ?int $userId = null; + + private ?PlaceInChallenge $placeInChallenge = null; + + private ?int $placeInChallengeId = null; + + private Position $position; + + private int $score = 0; + + private int $distance = 0; + + private int $timeSpent = 0; + + public function __construct() + { + $this->position = new Position(0.0, 0.0); + } + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function setUserId(int $userId): void + { + $this->userId = $userId; + } + + public function setPlaceInChallenge(PlaceInChallenge $placeInChallenge): void + { + $this->placeInChallenge = $placeInChallenge; + } + + public function setPlaceInChallengeId(int $placeInChallengeId): void + { + $this->placeInChallengeId = $placeInChallengeId; + } + + public function setPosition(Position $position): void + { + $this->position = $position; + } + + public function setLat(float $lat): void + { + $this->position->setLat($lat); + } + + public function setLng(float $lng): void + { + $this->position->setLng($lng); + } + + public function setScore(int $score): void + { + $this->score = $score; + } + + public function setDistance(int $distance): void + { + $this->distance = $distance; + } + + public function setTimeSpent(int $timeSpent): void + { + $this->timeSpent = $timeSpent; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function getUserId(): ?int + { + return $this->userId; + } + + public function getPlaceInChallenge(): ?PlaceInChallenge + { + return $this->placeInChallenge; + } + + public function getPlaceInChallengeId(): ?int + { + return $this->placeInChallengeId; + } + + public function getPosition(): Position + { + return $this->position; + } + + public function getLat(): float + { + return $this->position->getLat(); + } + + public function getLng(): float + { + return $this->position->getLng(); + } + + public function getScore(): int + { + return $this->score; + } + + public function getDistance(): int + { + return $this->distance; + } + + public function getTimeSpent(): ?int + { + return $this->timeSpent; + } +} diff --git a/src/PersistentData/Model/PlaceInChallenge.php b/src/PersistentData/Model/PlaceInChallenge.php new file mode 100644 index 0000000..6bbe504 --- /dev/null +++ b/src/PersistentData/Model/PlaceInChallenge.php @@ -0,0 +1,70 @@ + Place::class, 'challenge' => Challenge::class]; + + private ?Place $place = null; + + private ?int $placeId = null; + + private ?Challenge $challenge = null; + + private ?int $challengeId = null; + + private int $round; + + public function setPlace(Place $place): void + { + $this->place = $place; + } + + public function setPlaceId(int $placeId): void + { + $this->placeId = $placeId; + } + + public function setChallenge(Challenge $challenge): void + { + $this->challenge = $challenge; + } + + public function setChallengeId(int $challengeId): void + { + $this->challengeId = $challengeId; + } + + public function setRound(int $round): void + { + $this->round = $round; + } + + public function getPlace(): ?Place + { + return $this->place; + } + + public function getPlaceId(): ?int + { + return $this->placeId; + } + + public function getChallenge(): ?Challenge + { + return $this->challenge; + } + + public function getChallengeId(): ?int + { + return $this->challengeId; + } + + public function getRound(): int + { + return $this->round; + } +} diff --git a/src/PersistentData/Model/UserInChallenge.php b/src/PersistentData/Model/UserInChallenge.php new file mode 100644 index 0000000..0b74c9c --- /dev/null +++ b/src/PersistentData/Model/UserInChallenge.php @@ -0,0 +1,96 @@ + User::class, 'challenge' => Challenge::class]; + + private ?User $user = null; + + private ?int $userId = null; + + private ?Challenge $challenge = null; + + private ?int $challengeId = null; + + private int $currentRound = 0; + + private ?int $timeLeft = null; + + private bool $isOwner = false; + + public function setUser(User $user): void + { + $this->user = $user; + } + + public function setUserId(int $userId): void + { + $this->userId = $userId; + } + + public function setChallenge(Challenge $challenge): void + { + $this->challenge = $challenge; + } + + public function setChallengeId(int $challengeId): void + { + $this->challengeId = $challengeId; + } + + public function setCurrentRound(int $currentRound): void + { + $this->currentRound = $currentRound; + } + + public function setTimeLeft(?int $timeLeft): void + { + if (isset($timeLeft)) { + $this->timeLeft = max(0, $timeLeft); + } + } + + public function setIsOwner(bool $isOwner): void + { + $this->isOwner = $isOwner; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function getUserId(): ?int + { + return $this->userId; + } + + public function getChallenge(): ?Challenge + { + return $this->challenge; + } + + public function getChallengeId(): ?int + { + return $this->challengeId; + } + + public function getCurrentRound(): int + { + return $this->currentRound; + } + + public function getTimeLeft(): ?int + { + return $this->timeLeft; + } + + public function getIsOwner(): bool + { + return $this->isOwner; + } +} diff --git a/src/PersistentData/PersistentDataManager.php b/src/PersistentData/PersistentDataManager.php index 126f695..b0274e4 100644 --- a/src/PersistentData/PersistentDataManager.php +++ b/src/PersistentData/PersistentDataManager.php @@ -8,9 +8,9 @@ use MapGuesser\PersistentData\Model\Model; class PersistentDataManager { - public function selectFromDb(Select $select, string $type, bool $withRelations = false) + public function selectFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = []) { - $select = $this->createSelect($select, $type, $withRelations); + $select = $this->createSelect($select, $type, $useRelations, $withRelations); $data = $select->execute()->fetch(IResultSet::FETCH_ASSOC); @@ -19,50 +19,75 @@ class PersistentDataManager } $model = new $type(); - $this->fillWithData($data, $model); + $this->fillWithData($data, $model, $withRelations); return $model; } - public function selectMultipleFromDb(Select $select, string $type, bool $withRelations = false): Generator + public function selectMultipleFromDb(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Generator { - $select = $this->createSelect($select, $type, $withRelations); + $select = $this->createSelect($select, $type, $useRelations, $withRelations); $result = $select->execute(); while ($data = $result->fetch(IResultSet::FETCH_ASSOC)) { $model = new $type(); - $this->fillWithData($data, $model); + $this->fillWithData($data, $model, $withRelations); yield $model; } } - public function selectFromDbById($id, string $type, bool $withRelations = false) + public function selectFromDbById($id, string $type, bool $useRelations = false) { $select = new Select(\Container::$dbConnection); $select->whereId($id); - return $this->selectFromDb($select, $type, $withRelations); + return $this->selectFromDb($select, $type, $useRelations); } - public function fillWithData(array $data, Model $model): void + public function fillWithData(array &$data, Model $model, array $withRelations = [], ?string $modelKey = null): void { $relations = $model::getRelations(); - $relationData = []; - - foreach ($data as $key => $value) { - if ($this->extractRelationData($key, $value, $relationData, $relations)) { - continue; - } - - $method = 'set' . str_replace('_', '', ucwords($key, '_')); - - if (method_exists($model, $method)) { - $model->$method($value); - } + if (count($withRelations)) { + $relations = array_intersect($relations, $withRelations); } - $this->setRelations($model, $relationData); + while (key($data)) { + $key = key($data); + $value = current($data); + $relation = key($relations); + + if (strpos($key, '__') == false) { + $method = 'set' . str_replace('_', '', ucwords($key, '_')); + + if (method_exists($model, $method) && isset($value)) { + $model->$method($value); + } + + next($data); + } else if (isset($modelKey) && substr($key, 0, strlen($modelKey . '__')) === $modelKey . '__') { + $key = substr($key, strlen($modelKey) + 2); + + $method = 'set' . str_replace('_', '', ucwords($key, '_')); + + if (method_exists($model, $method) && isset($value)) { + $model->$method($value); + } + + next($data); + } else if (substr($key, 0, strlen($relation . '__')) === $relation . '__') { + $relationType = current($relations); + $relationModel = new $relationType(); + $this->fillWithData($data, $relationModel, $withRelations, $relation); + + $method = 'set' . str_replace('_', '', ucwords($relation, '_')); + $model->$method($relationModel); + + next($relations); + } else { + return; + } + } $model->saveSnapshot(); } @@ -128,35 +153,37 @@ class PersistentDataManager $model->resetSnapshot(); } - private function createSelect(Select $select, string $type, bool $withRelations = false): Select + private function createSelect(Select $select, string $type, bool $useRelations = false, array $withRelations = []): Select { $table = call_user_func([$type, 'getTable']); $fields = call_user_func([$type, 'getFields']); + $columns = []; + + foreach ($fields as $field) { + $columns[] = [$table, $field]; + } + $select->from($table); - //TODO: only with some relations? - if ($withRelations) { + if ($useRelations) { $relations = call_user_func([$type, 'getRelations']); - - $columns = []; - - foreach ($fields as $field) { - $columns[] = [$table, $field]; + if (count($withRelations)) { + $relations = array_intersect($relations, $withRelations); } - $columns = array_merge($columns, $this->getRelationColumns($relations)); + $columns = array_merge($columns, $this->getRelationColumns($relations, $withRelations)); - $this->leftJoinRelations($select, $table, $relations); + $this->leftJoinRelations($select, $table, $relations, $withRelations); $select->columns($columns); } else { - $select->columns($fields); + $select->columns($columns); } return $select; } - private function getRelationColumns(array $relations): array + private function getRelationColumns(array $relations, array $withRelations): array { $columns = []; @@ -165,46 +192,28 @@ class PersistentDataManager foreach (call_user_func([$relationType, 'getFields']) as $relationField) { $columns[] = [$relationTable, $relationField, $relation . '__' . $relationField]; } + + $nextOrderRelations = call_user_func([$relationType, 'getRelations']); + if (count($withRelations)) { + $nextOrderRelations = array_intersect($nextOrderRelations, $withRelations); + } + $columns = array_merge($columns, $this->getRelationColumns($nextOrderRelations, $withRelations)); } return $columns; } - private function leftJoinRelations(Select $select, string $table, array $relations): void + private function leftJoinRelations(Select $select, string $table, array $relations, array $withRelations): void { foreach ($relations as $relation => $relationType) { $relationTable = call_user_func([$relationType, 'getTable']); $select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']); - } - } - private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool - { - $found = false; - - foreach ($relations as $relation => $relationType) { - if (substr($key, 0, strlen($relation . '__')) === $relation . '__') { - $found = true; - $relationData[$relation][substr($key, strlen($relation . '__'))] = $value; - break; - } - } - - return $found; - } - - private function setRelations(Model $model, array &$relations): void - { - foreach ($model::getRelations() as $relation => $relationType) { - if (isset($relations[$relation])) { - $object = new $relationType(); - - $this->fillWithData($relations[$relation], $object); - - $method = 'set' . str_replace('_', '', ucwords($relation, '_')); - - $model->$method($object); + $nextOrderRelations = call_user_func([$relationType, 'getRelations']); + if (count($withRelations)) { + $nextOrderRelations = array_intersect($nextOrderRelations, $withRelations); } + $this->leftJoinRelations($select, $relationTable, $nextOrderRelations, $withRelations); } } diff --git a/src/Repository/ChallengeRepository.php b/src/Repository/ChallengeRepository.php new file mode 100644 index 0000000..bcce4d9 --- /dev/null +++ b/src/Repository/ChallengeRepository.php @@ -0,0 +1,75 @@ +pdm = new PersistentDataManager(); + } + + public function getById(int $challengeId): ?Challenge + { + return $this->pdm->selectFromDbById($challengeId, Challenge::class); + } + + public function getByToken(int $token): ?Challenge + { + $select = new Select(\Container::$dbConnection); + $select->where('token', '=', $token); + + return $this->pdm->selectFromDb($select, Challenge::class); + } + + public function getByTokenStr(string $token_str): ?Challenge + { + // validate token string + foreach (str_split($token_str) as $char) { + if (!(('0' <= $char && $char <= '9') || ('a' <= $char && $char <= 'f'))) { + return null; + } + } + // convert token to int + $token = hexdec($token_str); + + return $this->getByToken($token); + } + + public function getAllByParticipant(User $user): Generator + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']); + $select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']); + $select->where('user_id', '=', $user->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, Challenge::class); + } + + public function getAllByOwner(User $user): Generator + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('user_in_challenge', ['challenge', 'id'], '=', ['user_in_challenge', 'challenge_id']); + $select->innerJoin('users', ['users', 'id'], '=', ['user_in_challenge', 'user_id']); + $select->where('user_id', '=', $user->getId()); + $select->where('is_owner', '=', true); + + yield from $this->pdm->selectMultipleFromDb($select, Challenge::class); + } + + public function getAllByPlace(Place $place): Generator + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('place_in_challenge', ['challenges', 'id'], '=', ['place_in_challenge', 'challenge_id']); + $select->where('place_id', '=', $place->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, Challenge::class); + } +} \ No newline at end of file diff --git a/src/Repository/GuessRepository.php b/src/Repository/GuessRepository.php new file mode 100644 index 0000000..9e54b1b --- /dev/null +++ b/src/Repository/GuessRepository.php @@ -0,0 +1,98 @@ +pdm = new PersistentDataManager(); + } + + public function getAllByUser(User $user): Generator + { + $select = new Select(\Container::$dbConnection); + $select->where('user_id', '=', $user->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, Guess::class); + } + + public function getAllByUserAndChallenge(User $user, Challenge $challenge): Generator + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']); + $select->where('user_id', '=', $user->getId()); + $select->where('challenge_id', '=', $challenge->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, Guess::class); + } + + public function getByUserAndPlaceInChallenge(User $user, Challenge $challenge, Place $place): ?Guess + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']); + $select->where('user_id', '=', $user->getId()); + $select->where('challenge_id', '=', $challenge->getId()); + $select->where('place_id', '=', $place->getId()); + + return $this->pdm->selectFromDb($select, Guess::class); + } + + public function getAllInChallengeByUser(int $userId, Challenge $challenge): Generator + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']); + $select->where('user_id', '=', $userId); + $select->where('challenge_id', '=', $challenge->getId()); + $select->orderBy('round'); + + yield from $this->pdm->selectMultipleFromDb($select, Guess::class); + } + + public function getAllInChallenge(Challenge $challenge, array $withRelations = []): Generator + { + if (count($withRelations)) { + $necessaryRelations = [PlaceInChallenge::class]; + $withRelations = array_unique(array_merge($withRelations, $necessaryRelations)); + } + + $select = new Select(\Container::$dbConnection); + $select->where('challenge_id', '=', $challenge->getId()); + $select->orderBy('round'); + + yield from $this->pdm->selectMultipleFromDb($select, Guess::class, true, $withRelations); + } + + public function getAllInChallengeByRound(int $round, Challenge $challenge, array $withRelations = []): Generator + { + if (count($withRelations)) { + $necessaryRelations = [PlaceInChallenge::class]; + $withRelations = array_unique(array_merge($withRelations, $necessaryRelations)); + } + + $select = new Select(\Container::$dbConnection); + $select->where('challenge_id', '=', $challenge->getId()); + $select->where('round', '=', $round); + + yield from $this->pdm->selectMultipleFromDb($select, Guess::class, true, $withRelations); + } + + public function getAllByPlace(Place $place): Generator + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('place_in_challenge', ['place_in_challenge', 'id'], '=', ['guesses', 'place_in_challenge_id']); + $select->where('place_id', '=', $place->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, Guess::class); + } +} diff --git a/src/Repository/MapRepository.php b/src/Repository/MapRepository.php index cfb9885..6b41dc0 100644 --- a/src/Repository/MapRepository.php +++ b/src/Repository/MapRepository.php @@ -1,6 +1,9 @@ pdm->selectFromDbById($mapId, Map::class); } + + public function getByPlace(Place $place): ?Map + { + return $this->getById($place->getMapId()); + } + + public function getByChallenge(Challenge $challenge): ?Map + { + $select = new Select(\Container::$dbConnection); + $select->innerJoin('places', ['maps', 'id'], '=', ['places', 'map_id']); + $select->innerJoin('place_in_challenge', ['places', 'id'], '=', ['place_in_challenge', 'place_id']); + $select->where('challenge_id', '=', $challenge->getId()); + $select->limit(1); + + return $this->pdm->selectFromDb($select, Map::class); + } } diff --git a/src/Repository/PlaceInChallengeRepository.php b/src/Repository/PlaceInChallengeRepository.php new file mode 100644 index 0000000..8383962 --- /dev/null +++ b/src/Repository/PlaceInChallengeRepository.php @@ -0,0 +1,54 @@ +pdm = new PersistentDataManager(); + } + + public function getAllByPlace(Place $place, array $withRelations = []) : Generator + { + $select = new Select(\Container::$dbConnection); + $select->where('place_id', '=', $place->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, PlaceInChallenge::class, true, $withRelations); + } + + public function getAllByChallenge(Challenge $challenge) : Generator + { + $select = new Select(\Container::$dbConnection); + $select->where('challenge_id', '=', $challenge->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, PlaceInChallenge::class); + } + + public function getByPlaceAndChallenge(Place $place, Challenge $challenge) : ?PlaceInChallenge + { + $select = new Select(\Container::$dbConnection); + $select->where('place_id', '=', $place->getId()); + $select->where('challenge_id', '=', $challenge->getId()); + + return $this->pdm->selectFromDb($select, PlaceInChallenge::class); + } + + public function getByRoundInChallenge(int $round, Challenge $challenge, array $withRelations = []): ?PlaceInChallenge + { + $select = new Select(\Container::$dbConnection); + $select->where('challenge_id', '=', $challenge->getId()); + $select->orderBy('round'); + $select->limit(1, $round); + + return $this->pdm->selectFromDb($select, PlaceInChallenge::class, true, $withRelations); + } +} diff --git a/src/Repository/PlaceRepository.php b/src/Repository/PlaceRepository.php index 4c36edc..f676282 100644 --- a/src/Repository/PlaceRepository.php +++ b/src/Repository/PlaceRepository.php @@ -2,6 +2,7 @@ use Generator; use MapGuesser\Database\Query\Select; +use MapGuesser\PersistentData\Model\Challenge; use MapGuesser\PersistentData\Model\Map; use MapGuesser\PersistentData\Model\Place; use MapGuesser\PersistentData\PersistentDataManager; @@ -176,5 +177,26 @@ class PlaceRepository 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('challenge_id', '=', $challenge->getId()); + $select->orderBy('round'); + $select->limit(1, $round); + + return $this->pdm->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('challenge_id', '=', $challenge->getId()); + $select->orderBy('round'); + + yield from $this->pdm->selectMultipleFromDb($select, Place::class); + } } diff --git a/src/Repository/UserInChallengeRepository.php b/src/Repository/UserInChallengeRepository.php new file mode 100644 index 0000000..f50c7f8 --- /dev/null +++ b/src/Repository/UserInChallengeRepository.php @@ -0,0 +1,81 @@ +pdm = new PersistentDataManager(); + } + + public function getAllByUser(User $user) : Generator + { + $select = new Select(\Container::$dbConnection); + $select->where('user_id', '=', $user->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class); + } + + public function getAllByChallenge(Challenge $challenge) : Generator + { + $select = new Select(\Container::$dbConnection); + $select->where('challenge_id', '=', $challenge->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class); + } + + public function getAllByChallengeWithUsers(Challenge $challenge) : Generator + { + $select = new Select(\Container::$dbConnection); + $select->where('challenge_id', '=', $challenge->getId()); + + yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class, true, [User::class]); + } + + public function getByUserIdAndChallenge(int $userId, Challenge $challenge): ?UserInChallenge + { + $select = new Select(\Container::$dbConnection); + $select->where('user_id', '=', $userId); + $select->where('challenge_id', '=', $challenge->getId()); + + return $this->pdm->selectFromDb($select, UserInChallenge::class); + } + + public function getByUserIdAndToken(int $userId, string $token_str, array $withRelations = []): ?UserInChallenge + { + if (count($withRelations)) { + $necessaryRelations = [Challenge::class]; + $withRelations = array_unique(array_merge($withRelations, $necessaryRelations)); + } + + // validate token string + if (!ctype_xdigit($token_str)) { + return null; + } + // convert token to int + $token = hexdec($token_str); + + $select = new Select(\Container::$dbConnection); + $select->where('user_id', '=', $userId); + $select->where('token', '=', $token); + + return $this->pdm->selectFromDb($select, UserInChallenge::class, true, $withRelations); + } + + public function isUserParticipatingInChallenge(int $userId, Challenge $challenge): bool + { + $select = new Select(\Container::$dbConnection, 'user_in_challenge'); + $select->where('user_id', '=', $userId); + $select->where('challenge_id', '=', $challenge->getId()); + + return $select->count(); + } +} diff --git a/src/Repository/UserPlayedPlaceRepository.php b/src/Repository/UserPlayedPlaceRepository.php index 5daaaaa..9115af5 100644 --- a/src/Repository/UserPlayedPlaceRepository.php +++ b/src/Repository/UserPlayedPlaceRepository.php @@ -38,7 +38,7 @@ class UserPlayedPlaceRepository $select = new Select(\Container::$dbConnection); $select->where('user_id', '=', $user->getId()); - yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class, true); + yield from $this->pdm->selectMultipleFromDb($select, UserPlayedPlace::class); } public function getByUserIdAndPlaceId(int $userId, int $placeId) : ?UserPlayedPlace diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index be96c79..d9ed9bd 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -3,6 +3,7 @@ use DateTime; use Generator; use MapGuesser\Database\Query\Select; +use MapGuesser\PersistentData\Model\Guess; use MapGuesser\PersistentData\Model\User; use MapGuesser\PersistentData\PersistentDataManager; @@ -44,4 +45,9 @@ class UserRepository yield from $this->pdm->selectMultipleFromDb($select, User::class); } + + public function getByGuess(Guess $guess): ?User + { + return $this->getById($guess->getUserId()); + } } diff --git a/views/game.php b/views/game.php index 218b937..b901cca 100644 --- a/views/game.php +++ b/views/game.php @@ -12,11 +12,25 @@
+ @endsection @section(subheader) Loading map...Round Round Score @endsection @@ -25,6 +39,7 @@

+
@@ -41,7 +56,7 @@

You were close.

You didn't guess in this round.

-

Game finished.

+

Game finished.

You earned points.

@@ -57,6 +72,7 @@ +
+ @endsection @section(main) diff --git a/web.php b/web.php index 4d52673..8e72033 100644 --- a/web.php +++ b/web.php @@ -62,6 +62,13 @@ Container::$routeCollection->group('multiGame', function (MapGuesser\Routing\Rou $routeCollection->post('multiGame.nextRound-json', '{roomId}/nextRound.json', [MapGuesser\Controller\GameFlowController::class, 'multiNextRound']); $routeCollection->post('multiGame.guess-json', '{roomId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'multiGuess']); }); +Container::$routeCollection->group('challenge', function (MapGuesser\Routing\RouteCollection $routeCollection) { + $routeCollection->post('challenge.create', 'create.json', [\MapGuesser\Controller\GameController::class, 'createNewChallenge']); + $routeCollection->get('challenge', '{challengeToken}', [MapGuesser\Controller\GameController::class, 'getChallenge']); + $routeCollection->post('challenge.prepare-json', '{challengeToken}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareChallenge']); + $routeCollection->post('challenge.initialData-json', '{challengeToken}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'challengeInitialData']); + $routeCollection->post('challenge.guess-json', '{challengeToken}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'challengeGuess']); +}); Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) { $routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']); $routeCollection->get('admin.place', 'place.json/{placeId}', [MapGuesser\Controller\MapAdminController::class, 'getPlace']);