Compare commits

..
Author SHA1 Message Date
balazs 784037de6f Merge pull request 'feature/MAPG-235-basic-challenge-mode' (#48) from feature/MAPG-235-basic-challenge-mode into develop
default-pipeline default-pipeline #8
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/48
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-05-28 20:41:08 +02:00
balazs 6f27450423 MAPG-235 replaced explicit margin-top and margin-bottom with marginTop and marginBottom classes 2021-05-28 08:36:45 +02:00
balazs bbaa2fe1eb MAPG-235 removed font-family and font-weight css attributes for restrictions and now handled on higher level 2021-05-28 08:19:35 +02:00
balazs 28165d76d3 MAPG-235 refactored challenge token generation and check 2021-05-28 08:07:02 +02:00
balazs 1c1e5f051d MAPG-235 simplified highscore calculation 2021-05-26 08:16:32 +02:00
balazs 283c214c50 MAPG-235 noZoom label fix 2021-05-26 07:49:05 +02:00
balazs 7970927654 MAPG-235 refactored whitespaces 2021-05-22 21:17:49 +02:00
balazs 567602c749 MAPG-235 hide restrictions information if there are no restrictions for the challenge 2021-05-22 21:00:54 +02:00
balazs a5238234d2 MAPG-235 information of the current restrictions displayed on the ribbon in the top 2021-05-22 20:54:58 +02:00
balazs c965713c9c Merge branch 'develop' of gitea.e5tv.hu:esoko/mapguesser into feature/MAPG-235-basic-challenge-mode 2021-05-22 16:20:59 +02:00
balazs b00fe4ebe3 MAPG-235 highscores added to the end of challenge 2021-05-22 11:34:55 +02:00
balazs 599d8bf153 MAPG-235 fix unwanted time limit after page reload 2021-05-21 15:41:17 +02:00
balazs 636c47366a MAPG-235 fix loading other players' history and calculating final score 2021-05-21 15:40:43 +02:00
balazs 4520d11559 MAPG-235 fix calculating timelimit when 0 sec time was left from the last round 2021-05-21 14:45:40 +02:00
balazs 5f0b639fe7 MAPG-235 distance and score info added also to the player's own markers 2021-05-21 14:44:28 +02:00
balazs d4a279d2f4 MAPG-235 refactored human readable time format 2021-05-21 11:51:47 +02:00
balazs aadeda05af MAPG-235 timeLimit restored after reload of the page. Time limit selector usability improved 2021-05-21 11:30:30 +02:00
balazs b78d564f6f MAPG-235 time limit restriction for the whole game feature added 2021-05-20 21:32:01 +02:00
balazs c2df9b7713 MAPG-235 timelimit restriction per round functionality implemented for challenges 2021-05-20 20:55:44 +02:00
balazs bbb66ca979 MAPG-235 challenge token related error handling 2021-05-20 08:31:52 +02:00
bence c02f595606 Merge pull request 'MAPG-143 make it possible to disable game for guests (default is disable)' (#47) from feature/MAPG-143-allow-game-only-for-users into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/47
2021-05-11 18:29:55 +02:00
bence 54bc9c31db MAPG-143 make it possible to disable game for guests (default is disable) 2021-05-10 19:10:09 +02:00
15 changed files with 427 additions and 110 deletions

No files matched your search

+1
View File
@@ -20,3 +20,4 @@ MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000 MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090 MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090 MULTI_WS_PORT=8090
ENABLE_GAME_FOR_GUESTS=0
+37
View File
@@ -168,6 +168,43 @@
display: none; 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) { @media screen and (max-width: 599px) {
#mapName { #mapName {
display: none; display: none;
+7 -3
View File
@@ -31,11 +31,11 @@ main {
color: #ffffff; 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; font-family: 'Roboto', sans-serif;
} }
h1, h2 { h1, h2, h3 {
font-weight: 500; font-weight: 500;
} }
@@ -55,7 +55,11 @@ h2, header.small h1 {
font-size: 24px; font-size: 24px;
} }
p, h2 { h3 {
font-size: 18px;
}
p, h2, h3 {
line-height: 150%; line-height: 150%;
} }
+1 -10
View File
@@ -75,16 +75,6 @@ div.mapItem>div.buttonContainer {
grid-auto-flow: column; grid-auto-flow: column;
} }
#restrictions {
margin-top: 1em;
margin-bottom: 1em;
font-family: 'Roboto', sans-serif;
}
#restrictions h3 {
font-weight: 500;
}
#restrictions input { #restrictions input {
height: auto; height: auto;
margin: 0.5em; margin: 0.5em;
@@ -93,6 +83,7 @@ div.mapItem>div.buttonContainer {
#restrictions input[type=range] { #restrictions input[type=range] {
height: 1.5em; height: 1.5em;
margin-left: 2em; margin-left: 2em;
width: 70%;
} }
#timeLimitType { #timeLimitType {
+211 -38
View File
@@ -20,6 +20,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
adaptGuess: false, adaptGuess: false,
googleLink: null, googleLink: null,
history: [], history: [],
restrictions: null,
readyToContinue: true, readyToContinue: true,
timeoutEnd: null, timeoutEnd: null,
@@ -295,7 +296,10 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
return; return;
} }
Game.loadHistory(this.response.history); Game.loadHistory(this.response);
Game.restrictions = this.response.restrictions;
Game.displayRestrictions();
if (this.response.finished) { if (this.response.finished) {
@@ -304,18 +308,6 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
} else { } else {
if (this.response.restrictions) {
Game.panorama.setOptions({
clickToGo: !this.response.restrictions.noMove,
linksControl: !this.response.restrictions.noMove,
scrollwheel: !this.response.restrictions.noZoom
});
if (this.response.restrictions.noPan) {
document.getElementById('panningBlockerCover').style.display = 'block';
}
}
Game.panoId = this.response.place.panoId; Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov; Game.pov = this.response.place.pov;
@@ -327,6 +319,93 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
}); });
}, },
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 () { transitToResultMap: function () {
// TODO: refactor - it is necessary for mobile // TODO: refactor - it is necessary for mobile
if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') { if (window.getComputedStyle(document.getElementById('guess')).visibility === 'hidden') {
@@ -362,11 +441,11 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
} }
}, },
loadHistory: function (history) { loadHistory: function (response) {
if (!history) if (!response.history)
return; return;
Game.history = history; Game.history = response.history;
for (var i = 0; i < Game.rounds.length; ++i) { for (var i = 0; i < Game.rounds.length; ++i) {
var round = Game.rounds[i]; var round = Game.rounds[i];
@@ -385,6 +464,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
} }
Game.rounds = []; Game.rounds = [];
Game.scoreSum = 0;
for (var i = 0; i < Game.history.length; ++i) { for (var i = 0; i < Game.history.length; ++i) {
var round = Game.history[i]; var round = Game.history[i];
@@ -392,7 +472,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] }); Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.addPositionToResultMap(true); Game.addPositionToResultMap(true);
if (round.result.guessPosition) { if (round.result.guessPosition) {
Game.addGuessPositionToResultMap(round.result.guessPosition, null, true); Game.addGuessPositionToResultMap(round.result.guessPosition, round.result, true);
} }
Game.scoreSum += round.result.score; Game.scoreSum += round.result.score;
@@ -438,6 +518,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
distanceInfo.children[0].style.display = null; distanceInfo.children[0].style.display = null;
distanceInfo.children[1].style.display = null; distanceInfo.children[1].style.display = null;
distanceInfo.children[2].style.display = null; distanceInfo.children[2].style.display = null;
document.getElementById('summaryInfo').innerHTML = "Game finished."
var scoreInfo = document.getElementById('scoreInfo'); var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = null; scoreInfo.children[0].style.display = null;
scoreInfo.children[1].style.display = null; scoreInfo.children[1].style.display = null;
@@ -452,13 +533,13 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
// needs to be set visible after the show guess map hid it in mobile view // needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible'; document.getElementById("navigation").style.visibility = 'visible';
Game.panorama.setOptions({ Game.disableRestrictions();
clickToGo: true, Game.hideRestrictions();
linksControl: true,
scrollwheel: true
});
document.getElementById('panningBlockerCover').style.display = null; document.getElementById('panningBlockerCover').style.display = null;
Game.history = [];
Game.initialize(); Game.initialize();
}, },
@@ -511,6 +592,8 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
// update the compass // update the compass
const heading = Game.panorama.getPov().heading; const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)"; document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
Game.enableRestrictions();
}, },
handleErrorResponse: function (error) { handleErrorResponse: function (error) {
@@ -565,7 +648,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
resultBounds.extend(position); resultBounds.extend(position);
if (guessPosition) { if (guessPosition) {
Game.addGuessPositionToResultMap(guessPosition); Game.addGuessPositionToResultMap(guessPosition, result);
resultBounds.extend(guessPosition); resultBounds.extend(guessPosition);
} }
@@ -625,22 +708,29 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
}, },
guess: function () { 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.disableRestrictions();
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
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('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible'; 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'; document.getElementById('loading').style.visibility = 'visible';
var url = Game.getGameIdentifier() + '/guess.json'; var url = Game.getGameIdentifier() + '/guess.json';
MapGuesser.httpRequest('POST', url, function () { MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
@@ -649,7 +739,8 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
return; return;
} }
Game.history = this.response.history; Game.loadHistory(this.response);
Game.restrictions = this.response.restrictions;
Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults); Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
@@ -691,8 +782,8 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
var position = round.position; var position = round.position;
var guessMarker = { marker: null, line: null, info: null }; var guessMarker = { marker: null, line: null, info: null };
var markerSvg = result ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg'; var markerSvg = result && result.userName ? 'marker-gray-empty.svg' : 'marker-blue-empty.svg';
var markerLabel = result ? result.userName.charAt(0).toUpperCase() : '?'; var markerLabel = result && result.userName ? result.userName.charAt(0).toUpperCase() : '?';
guessMarker.marker = new google.maps.Marker({ guessMarker.marker = new google.maps.Marker({
map: Game.map, map: Game.map,
@@ -742,8 +833,9 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
}); });
if (result) { if (result) {
const userName = result.userName ? result.userName : 'me';
guessMarker.info = new google.maps.InfoWindow({ guessMarker.info = new google.maps.InfoWindow({
content: '<p class="small bold">' + result.userName + '</p>' + content: '<p class="small bold">' + userName + '</p>' +
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>', '<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
}); });
@@ -770,6 +862,36 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
return { width: percent + '%', backgroundColor: color }; 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 () { showSummary: function () {
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
distanceInfo.children[0].style.display = 'none'; distanceInfo.children[0].style.display = 'none';
@@ -829,6 +951,48 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
var scoreBar = document.getElementById('scoreBar'); var scoreBar = document.getElementById('scoreBar');
scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor; scoreBar.style.backgroundColor = scoreBarProperties.backgroundColor;
scoreBar.style.width = scoreBarProperties.width; 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! <span class="hideOnNarrowScreen">' + highscores[1].userName + ' got only ' + highscores[1].score + ' points.</span>';
} else {
summaryInfo.innerHTML = 'You lost! <span class="hideOnNarrowScreen">' + highscores[0].userName + ' won with ' + highscores[0].score + ' points.</span>';
}
} else if (highscores.length == 1) {
summaryInfo.innerHTML = 'You are the first to finish. <span class="hideOnNarrowScreen">Invite your friends by sending them the link.</span>'
}
}
}, },
rewriteGoogleLink: function () { rewriteGoogleLink: function () {
@@ -851,7 +1015,7 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
}, 1); }, 1);
}, },
startCountdown: function (timeout) { startCountdown: function (timeout, timedOutHandler) {
if (Game.countdownHandler) { if (Game.countdownHandler) {
clearInterval(Game.countdownHandler); clearInterval(Game.countdownHandler);
} }
@@ -873,7 +1037,12 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
Game.setCountdownTime(timeLeft); Game.setCountdownTime(timeLeft);
if (timeLeft <= 0) { if (timeLeft <= 0) {
document.getElementById('panoCover').style.visibility = 'visible'; if (typeof timedOutHandler === 'function') {
timedOutHandler();
} else {
document.getElementById('panoCover').style.visibility = 'visible';
}
clearInterval(Game.countdownHandler); clearInterval(Game.countdownHandler);
} }
}, 1000); }, 1000);
@@ -1071,4 +1240,8 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
document.getElementById('compassContainer').onclick = function () { document.getElementById('compassContainer').onclick = function () {
Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch }); Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch });
} }
document.getElementById('closeHighscoresButton').onclick = function () {
MapGuesser.hideModal();
};
})(); })();
+28 -2
View File
@@ -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.addStaticMaps();
Maps.initializeDescriptionDivs(); Maps.initializeDescriptionDivs();
@@ -119,7 +145,7 @@
document.getElementById('playMode').style.visibility = 'hidden'; document.getElementById('playMode').style.visibility = 'hidden';
var timeLimit = document.getElementById('timeLimit').value; var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + timeLimit + ' seconds per round'; document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
}; };
} }
@@ -149,7 +175,7 @@
document.getElementById('timeLimit').oninput = function () { document.getElementById('timeLimit').oninput = function () {
var timeLimit = document.getElementById('timeLimit').value; var timeLimit = document.getElementById('timeLimit').value;
document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + timeLimit + ' seconds per round'; document.getElementById('timeLimitLabel').innerText = 'Time limit of ' + Util.printTimeForHuman(timeLimit);
document.getElementById('timerEnabled').checked = true; document.getElementById('timerEnabled').checked = true;
} }
})(); })();
+13 -4
View File
@@ -2,6 +2,7 @@
use DateTime; use DateTime;
use Faker\Factory; use Faker\Factory;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Response\HtmlContent; use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
@@ -20,7 +21,7 @@ use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Repository\UserInChallengeRepository; use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Response\Redirect; use MapGuesser\Response\Redirect;
class GameController class GameController implements ISecured
{ {
const NUMBER_OF_ROUNDS = 5; const NUMBER_OF_ROUNDS = 5;
@@ -52,6 +53,11 @@ class GameController
$this->userInChallengeRepository = new UserInChallengeRepository(); $this->userInChallengeRepository = new UserInChallengeRepository();
} }
public function authorize(): bool
{
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
}
public function getGame(): IContent public function getGame(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
@@ -106,7 +112,10 @@ class GameController
public function createNewChallenge(): IContent public function createNewChallenge(): IContent
{ {
// create Challenge // create Challenge
$challengeToken = rand(); do {
// initiliaze or if a challenge with the same token already exists
$challengeToken = mt_rand();
} while ($this->challengeRepository->getByToken($challengeToken));
$challenge = new Challenge(); $challenge = new Challenge();
$challenge->setToken($challengeToken); $challenge->setToken($challengeToken);
@@ -198,7 +207,7 @@ class GameController
$room = $this->multiRoomRepository->getByRoomId($roomId); $room = $this->multiRoomRepository->getByRoomId($roomId);
if(!isset($room)) { if (!isset($room)) {
return new JsonContent(['error' => 'game_not_found']); return new JsonContent(['error' => 'game_not_found']);
} }
@@ -255,7 +264,7 @@ class GameController
return new JsonContent(['error' => 'game_not_found']); return new JsonContent(['error' => 'game_not_found']);
} }
if(!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) { if (!$this->userInChallengeRepository->isUserParticipatingInChallenge($userId, $challenge)) {
// new player is joining // new player is joining
$userInChallenge = new UserInChallenge(); $userInChallenge = new UserInChallenge();
$userInChallenge->setUserId($userId); $userInChallenge->setUserId($userId);
+74 -36
View File
@@ -1,6 +1,7 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime; use DateTime;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest; use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent; use MapGuesser\Response\JsonContent;
@@ -24,7 +25,7 @@ use MapGuesser\Repository\UserInChallengeRepository;
use MapGuesser\Repository\UserPlayedPlaceRepository; use MapGuesser\Repository\UserPlayedPlaceRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
class GameFlowController class GameFlowController implements ISecured
{ {
const NUMBER_OF_ROUNDS = 5; const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000; const MAX_SCORE = 1000;
@@ -69,6 +70,11 @@ class GameFlowController
$this->guessRepository = new GuessRepository(); $this->guessRepository = new GuessRepository();
} }
public function authorize(): bool
{
return !empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || $this->request->user() !== null;
}
public function initialData(): IContent public function initialData(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
@@ -149,7 +155,9 @@ class GameFlowController
{ {
$currentPlace = $this->placeRepository->getByRoundInChallenge($challenge, $currentRound); $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) { if (!isset($currentPlace) || $withHistory) {
$withRelations = [User::class, PlaceInChallenge::class, Place::class]; $withRelations = [User::class, PlaceInChallenge::class, Place::class];
foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) { foreach ($this->guessRepository->getAllInChallenge($challenge, $withRelations) as $guess) {
$round = $guess->getPlaceInChallenge()->getRound(); $round = $guess->getPlaceInChallenge()->getRound();
@@ -171,6 +179,22 @@ class GameFlowController
]; ];
} }
} }
// 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 if (!isset($currentPlace)) { // game finished
@@ -194,15 +218,16 @@ class GameFlowController
} }
} }
} }
$response['restrictions'] = [
'timeLimit' => $challenge->getTimeLimit(),
'noMove' => $challenge->getNoMove(),
'noPan' => $challenge->getNoPan(),
'noZoom' => $challenge->getNoZoom()
];
} }
$response['restrictions'] = [
'timeLimit' => $challenge->getTimeLimit() * 1000,
'timeLimitType' => $challenge->getTimeLimitType(),
'noMove' => $challenge->getNoMove(),
'noPan' => $challenge->getNoPan(),
'noZoom' => $challenge->getNoZoom()
];
return $response; return $response;
} }
@@ -222,6 +247,11 @@ class GameFlowController
$response = $this->prepareChallengeResponse($userId, $challenge, $currentRound, true); $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); return new JsonContent($response);
} }
@@ -271,9 +301,9 @@ class GameFlowController
$session = $this->request->session(); $session = $this->request->session();
$userId = $session->get('userId'); $userId = $session->get('userId');
if(isset($userId)) { if (isset($userId)) {
$userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId); $userPlayedPlace = $this->userPlayedPlaceRepository->getByUserIdAndPlaceId($userId, $placeId);
if(!$userPlayedPlace) { if (!$userPlayedPlace) {
$userPlayedPlace = new UserPlayedPlace(); $userPlayedPlace = new UserPlayedPlace();
$userPlayedPlace->setUserId($userId); $userPlayedPlace->setUserId($userId);
$userPlayedPlace->setPlaceId($placeId); $userPlayedPlace->setPlaceId($placeId);
@@ -339,39 +369,47 @@ class GameFlowController
$currentPlace = $currentPlaceInChallenge->getPlace(); $currentPlace = $currentPlaceInChallenge->getPlace();
$map = $currentPlace->getMap(); $map = $currentPlace->getMap();
$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);
// update round
$nextRound = $currentRound + 1;
$userInChallenge->setCurrentRound($nextRound);
$this->pdm->saveToDb($userInChallenge);
// creating response // creating response
$nextRound = $currentRound + 1;
$response = $this->prepareChallengeResponse($userId, $challenge, $nextRound); $response = $this->prepareChallengeResponse($userId, $challenge, $nextRound);
$response['position'] = $currentPlace->getPosition()->toArray(); $response['position'] = $currentPlace->getPosition()->toArray();
$response['result'] = $result;
if(isset($response['history'][$currentRound]['allResults'])) { if ($this->request->post('lat') && $this->request->post('lng')) {
$response['allResults'] = $response['history'][$currentRound]['allResults']; $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()); $this->saveVisit($currentPlace->getId());
+3 -3
View File
@@ -261,7 +261,7 @@ class Select
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0]; $queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
} }
if($this->isDerivedTable()) { if ($this->isDerivedTable()) {
$queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY]; $queryString = '(' . $queryString . ') AS ' . $this->tableAliases[Select::DERIVED_TABLE_KEY];
} }
@@ -276,7 +276,7 @@ class Select
return [(string) $table, $params]; return [(string) $table, $params];
} }
if($table instanceof Select) if ($table instanceof Select)
{ {
return $table->generateQuery(); return $table->generateQuery();
} }
@@ -332,7 +332,7 @@ class Select
$joinQueries = []; $joinQueries = [];
$params = []; $params = [];
foreach($this->joins as $join) { foreach ($this->joins as $join) {
list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true); list($joinQueryFragment, $paramsFragment) = $this->generateTable($join[1], true);
$joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]); $joinQueries[] = $join[0] . ' JOIN ' . $joinQueryFragment . ' ON ' . $this->generateColumn($join[2]) . ' ' . $join[3] . ' ' . $this->generateColumn($join[4]);
$params = array_merge($params, $paramsFragment); $params = array_merge($params, $paramsFragment);
+1 -1
View File
@@ -33,7 +33,7 @@ class Challenge extends Model
public function setTimeLimit(?int $timeLimit): void public function setTimeLimit(?int $timeLimit): void
{ {
if(isset($timeLimit)) { if (isset($timeLimit)) {
$this->timeLimit = $timeLimit; $this->timeLimit = $timeLimit;
} }
} }
+2 -2
View File
@@ -49,8 +49,8 @@ class UserInChallenge extends Model
public function setTimeLeft(?int $timeLeft): void public function setTimeLeft(?int $timeLeft): void
{ {
if(isset($timeLeft)) { if (isset($timeLeft)) {
$this->timeLeft = $timeLeft; $this->timeLeft = max(0, $timeLeft);
} }
} }
+7
View File
@@ -31,6 +31,13 @@ class ChallengeRepository
public function getByTokenStr(string $token_str): ?Challenge 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); $token = hexdec($token_str);
return $this->getByToken($token); return $this->getByToken($token);
@@ -32,6 +32,14 @@ class UserInChallengeRepository
yield from $this->pdm->selectMultipleFromDb($select, UserInChallenge::class); 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 public function getByUserIdAndChallenge(int $userId, Challenge $challenge): ?UserInChallenge
{ {
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
@@ -48,6 +56,11 @@ class UserInChallengeRepository
$withRelations = array_unique(array_merge($withRelations, $necessaryRelations)); $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); $token = hexdec($token_str);
$select = new Select(\Container::$dbConnection); $select = new Select(\Container::$dbConnection);
+16 -2
View File
@@ -12,11 +12,25 @@
<div id="players" class="marginTop"></div> <div id="players" class="marginTop"></div>
<button id="startMultiGameButton" class="button fullWidth marginTop green">Start game</button> <button id="startMultiGameButton" class="button fullWidth marginTop green">Start game</button>
</div> </div>
<div id="highscores" class="modal">
<h2>Highscores</h2>
<div>
<table id="highscoresTable">
<tr>
<th>Player</th>
<th>Score</th>
</tr>
</table>
</div>
<div class="right">
<button id="closeHighscoresButton" class="gray marginTop" type="button">Close</button>
</div>
</div>
@endsection @endsection
@section(subheader) @section(subheader)
<span id="mapName" class="bold">Loading map...</span><!-- <span id="mapName" class="bold">Loading map...</span><!--
--><span>Round <span id="currentRound" class="bold"></span></span><!-- --><span id="roundContainer">Round <span id="currentRound" class="bold"></span></span><!--
--><span>Score <span id="currentScoreSum" class="bold"></span></span> --><span>Score <span id="currentScoreSum" class="bold"></span></span>
@endsection @endsection
@@ -42,7 +56,7 @@
<div id="distanceInfo"> <div id="distanceInfo">
<p>You were <span id="distance" class="bold"></span> close.</p> <p>You were <span id="distance" class="bold"></span> close.</p>
<p>You didn't guess in this round.</p> <p>You didn't guess in this round.</p>
<p class="bold">Game finished.</p> <p id="summaryInfo" class="bold">Game finished.</p>
</div> </div>
<div id="scoreInfo"> <div id="scoreInfo">
<p>You earned <span id="score" class="bold"></span> points.</p> <p>You earned <span id="score" class="bold"></span> points.</p>
+13 -9
View File
@@ -43,7 +43,7 @@ TODO: condition!
<p class="bold center marginTop marginBottom">OR</p> <p class="bold center marginTop marginBottom">OR</p>
--> -->
<div id="restrictions"> <div id="restrictions" class="marginTop marginBottom">
<h3>Optional restrictions</h3> <h3>Optional restrictions</h3>
<div> <div>
<div> <div>
@@ -51,10 +51,10 @@ TODO: condition!
<label id="timeLimitLabel" for="timerEnabled">Time limit measured in seconds</label> <label id="timeLimitLabel" for="timerEnabled">Time limit measured in seconds</label>
</div> </div>
<div> <div>
<input type="range" id="timeLimit" name="timeLimit" min="10" max="600" value="120" /> <input type="range" id="timeLimit" name="timeLimit" min="10" max="1800" step="10" value="300" />
</div> </div>
<div id="timeLimitType"> <div id="timeLimitType">
Time limit <label>Time limit</label>
<input type="radio" id="timeLimitTypeGame" name="timeLimitType" value="game" checked /> <input type="radio" id="timeLimitTypeGame" name="timeLimitType" value="game" checked />
<label for="timeLimitTypeGame">for the whole game</label> <label for="timeLimitTypeGame">for the whole game</label>
<input type="radio" id="timeLimitTypeRound" name="timeLimitType" value="round" /> <input type="radio" id="timeLimitTypeRound" name="timeLimitType" value="round" />
@@ -67,7 +67,7 @@ TODO: condition!
</div> </div>
<div> <div>
<input type="checkbox" id="noZoom" name="noZoom" value="noZoom" /> <input type="checkbox" id="noZoom" name="noZoom" value="noZoom" />
<label for="noMove">No zoom allowed</label> <label for="noZoom">No zoom allowed</label>
</div> </div>
<div> <div>
<input type="checkbox" id="noPan" name="noPan" value="noPan" /> <input type="checkbox" id="noPan" name="noPan" value="noPan" />
@@ -114,12 +114,16 @@ TODO: condition!
</div> </div>
</div> </div>
<div class="buttonContainer"> <div class="buttonContainer">
<?php if ($isAdmin): ?> <?php if (!empty($_ENV['ENABLE_GAME_FOR_GUESTS']) || Container::$request->user()): ?>
<button class="button fullWidth noRightRadius playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button> <?php if ($isAdmin): ?>
<a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a> <button class="button fullWidth noRightRadius playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
<button class="button red fullWidth noLeftRadius deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Delete map '<?= $map['name'] ?>'">Delete</button> <a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a>
<button class="button red fullWidth noLeftRadius deleteButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Delete map '<?= $map['name'] ?>'">Delete</button>
<?php else: ?>
<button class="button fullWidth playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
<?php endif; ?>
<?php else: ?> <?php else: ?>
<button class="button fullWidth playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button> <a href="/login" class="button fullWidth" title="Play map '<?= $map['name'] ?>'">Play this map</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>