Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e435ef6be0 | ||
|
|
90528e61e5
|
||
|
|
d02aa4abe0
|
||
|
|
4f45e213c3 | ||
|
|
d877d2fe03
|
||
|
|
66240c2d65 | ||
|
|
0654b65940
|
||
|
|
272ec3568d | ||
|
|
be4a2038e5
|
||
|
|
f305c97513
|
||
|
|
784037de6f | ||
|
|
6f27450423 | ||
|
|
bbaa2fe1eb | ||
|
|
28165d76d3 | ||
|
|
1c1e5f051d | ||
|
|
283c214c50 | ||
|
|
7970927654 | ||
|
|
567602c749 |
No files matched your search
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
pipeline {
|
||||
agent {
|
||||
node {
|
||||
label 'mapguesser'
|
||||
customWorkspace 'workspace/mapguesser'
|
||||
}
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Install composer') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
dir '.'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'composer install'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Unit Testing') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
dir '.'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'vendor/bin/phpunit --log-junit unit_test_results.xml --testdox tests'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
archiveArtifacts 'unit_test_results.xml'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Static Code Analysis') {
|
||||
agent {
|
||||
dockerfile {
|
||||
filename 'docker/Dockerfile-test'
|
||||
dir '.'
|
||||
reuseNode true
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh 'php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
archiveArtifacts 'static_code_analysis_results.json'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
# MapGuesser
|
||||
|
||||
[](https://jenkins.e5tv.hu/job/mapguesser/job/develop/)
|
||||
|
||||
This is the MapGuesser Application project. This is a game about guessing where you are based on a street view panorama - inspired by existing applications.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `maps`
|
||||
ADD `unlisted` TINYINT(1) NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM php:7.4.7-cli-buster
|
||||
|
||||
RUN apt-get update && apt-get install -y unzip
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||
@@ -31,11 +31,11 @@ main {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
p, h1, h2, input, textarea, select, button, a, table {
|
||||
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%;
|
||||
}
|
||||
|
||||
@@ -237,7 +241,7 @@ button.green:enabled:hover, button.green:enabled:focus, a.button.green:hover, a.
|
||||
background-color: #1b7d31;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
input[type=text], select, textarea {
|
||||
background-color: #f9fafb;
|
||||
border: solid #c8d2e1 1px;
|
||||
border-radius: 2px;
|
||||
@@ -246,22 +250,26 @@ input, select, textarea {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
input, select {
|
||||
input[type=text], select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
input[type=checkbox], input[type=radio] {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
textarea {
|
||||
padding: 5px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input.big, select.big, textarea.big, div.inputWithButton>input {
|
||||
input[type=text].big, select.big, textarea.big, div.inputWithButton>input[type=text] {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
input.big, select.big, div.inputWithButton>input {
|
||||
input[type=text].big, select.big, div.inputWithButton>input[type=text] {
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
padding: 0 6px;
|
||||
@@ -276,19 +284,19 @@ input.fullWidth, select.fullWidth, textarea.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:disabled, select:disabled, textarea:disabled {
|
||||
input[type=text]:disabled, select:disabled, textarea:disabled {
|
||||
background-color: #dfdfdf;
|
||||
border: solid #dfdfdf 1px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
input[type=text]:focus, select:focus, textarea:focus {
|
||||
background-color: #ffffff;
|
||||
border: solid #29457f 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
input[type=text]:focus, select:focus {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
@@ -296,16 +304,16 @@ textarea:focus {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
input.big:focus, select.big:focus {
|
||||
input[type=text].big:focus, select.big:focus {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input {
|
||||
div.inputWithButton>input[type=text] {
|
||||
width: 100%;
|
||||
padding: 0 83px 0 6px;
|
||||
}
|
||||
|
||||
div.inputWithButton>input:focus {
|
||||
div.inputWithButton>input[type=text]:focus {
|
||||
padding: 0 82px 0 5px;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,10 @@ div.mapItem.new {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
div.mapItem.unlisted {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
div.mapItem>div.title {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
@@ -75,29 +79,8 @@ div.mapItem>div.buttonContainer {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
|
||||
#restrictions {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
#restrictions h3 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
#restrictions input {
|
||||
height: auto;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
#restrictions input[type=range] {
|
||||
height: 1.5em;
|
||||
margin-left: 2em;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
#timeLimitType {
|
||||
margin-left: 2em;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1504px) {
|
||||
|
||||
+14
-13
@@ -21,7 +21,6 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
|
||||
googleLink: null,
|
||||
history: [],
|
||||
restrictions: null,
|
||||
finishers: null,
|
||||
|
||||
readyToContinue: true,
|
||||
timeoutEnd: null,
|
||||
@@ -348,8 +347,6 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
var restrictionsForDisplay = [];
|
||||
if (Game.restrictions.timeLimit) {
|
||||
restrictionsForDisplay.push('time limit per ' + Game.restrictions.timeLimitType);
|
||||
@@ -366,6 +363,10 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
|
||||
}
|
||||
}
|
||||
|
||||
if (restrictionsForDisplay.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// create restrictions span for header
|
||||
var restrictions = document.createElement('span');
|
||||
restrictions.setAttribute('id', 'restrictions');
|
||||
@@ -486,10 +487,6 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response.finishers) {
|
||||
Game.finishers = new Set(response.finishers);
|
||||
}
|
||||
},
|
||||
|
||||
reset: function () {
|
||||
@@ -542,7 +539,6 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
|
||||
document.getElementById('panningBlockerCover').style.display = null;
|
||||
|
||||
Game.history = [];
|
||||
Game.finishers = null;
|
||||
|
||||
Game.initialize();
|
||||
},
|
||||
@@ -871,16 +867,21 @@ const GameType = Object.freeze({ 'SINGLE': 0, 'MULTI': 1, 'CHALLENGE': 2 });
|
||||
var highscores = new Map();
|
||||
highscores.set('me', Game.scoreSum);
|
||||
|
||||
for (var i = 0; i < Game.history.length; ++i) {
|
||||
// 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 (Game.finishers.has(result.userName)) {
|
||||
if (highscores.has(result.userName)) {
|
||||
highscores.set(result.userName, highscores.get(result.userName) + result.score);
|
||||
} else {
|
||||
highscores.set(result.userName, result.score);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
MapEditor.metadata.name = form.elements.name.value;
|
||||
MapEditor.metadata.description = form.elements.description.value;
|
||||
MapEditor.metadata.unlisted = form.elements.unlisted.checked;
|
||||
|
||||
document.getElementById('mapName').innerHTML = form.elements.name.value ? form.elements.name.value : '[unnamed map]';
|
||||
|
||||
@@ -254,6 +255,9 @@
|
||||
if (MapEditor.metadata.description !== null) {
|
||||
data.append('description', MapEditor.metadata.description);
|
||||
}
|
||||
if (MapEditor.metadata.unlisted !== null) {
|
||||
data.append('unlisted', MapEditor.metadata.unlisted);
|
||||
}
|
||||
|
||||
for (var placeId in MapEditor.added) {
|
||||
if (!MapEditor.added.hasOwnProperty(placeId)) {
|
||||
|
||||
@@ -112,11 +112,10 @@ class GameController implements ISecured
|
||||
public function createNewChallenge(): IContent
|
||||
{
|
||||
// create Challenge
|
||||
$challengeToken = rand();
|
||||
while ($this->challengeRepository->getByToken($challengeToken)) {
|
||||
// if a challenge with the same token already exists
|
||||
$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->setToken($challengeToken);
|
||||
|
||||
@@ -199,14 +199,6 @@ class GameFlowController implements ISecured
|
||||
|
||||
if (!isset($currentPlace)) { // game finished
|
||||
$response['finished'] = true;
|
||||
|
||||
// list all players who finished the challenge
|
||||
$response['finishers'] = [];
|
||||
foreach ($this->userInChallengeRepository->getAllByChallengeWithUsers($challenge) as $userInChallenge) {
|
||||
if ($userInChallenge->getCurrentRound() == $currentRound && $userInChallenge->getUser()->getId() != $userId) {
|
||||
$response['finishers'][] = $userInChallenge->getUser()->getDisplayName();
|
||||
}
|
||||
}
|
||||
} else { // continue game
|
||||
$response['place'] = [
|
||||
'panoId' => $currentPlace->getPanoIdCached(),
|
||||
|
||||
@@ -8,6 +8,7 @@ use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\PersistentData\Model\Challenge;
|
||||
use MapGuesser\PersistentData\Model\Map;
|
||||
use MapGuesser\PersistentData\Model\Place;
|
||||
use MapGuesser\PersistentData\Model\PlaceInChallenge;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\ChallengeRepository;
|
||||
use MapGuesser\Repository\GuessRepository;
|
||||
@@ -80,6 +81,7 @@ class MapAdminController implements ISecured
|
||||
'mapId' => $mapId,
|
||||
'mapName' => $map->getName(),
|
||||
'mapDescription' => str_replace('<br>', "\n", $map->getDescription()),
|
||||
'mapUnlisted' => $map->getUnlisted(),
|
||||
'bounds' => $map->getBounds()->toArray(),
|
||||
'places' => &$places
|
||||
]);
|
||||
@@ -174,6 +176,9 @@ class MapAdminController implements ISecured
|
||||
if (isset($_POST['description'])) {
|
||||
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
|
||||
}
|
||||
if (isset($_POST['unlisted'])) {
|
||||
$map->setUnlisted((bool)$_POST['unlisted']);
|
||||
}
|
||||
|
||||
$this->pdm->saveToDb($map);
|
||||
|
||||
|
||||
@@ -30,12 +30,19 @@ class MapsController
|
||||
['maps', 'bound_north_lat'],
|
||||
['maps', 'bound_east_lng'],
|
||||
['maps', 'area'],
|
||||
['maps', 'unlisted'],
|
||||
new RawExpression('COUNT(places.id) AS num_places')
|
||||
]);
|
||||
$select->leftJoin('places', ['places', 'map_id'], '=', ['maps', 'id']);
|
||||
$select->groupBy(['maps', 'id']);
|
||||
$select->orderBy('name');
|
||||
|
||||
$user = $this->request->user();
|
||||
$isAdmin = $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN);
|
||||
if (!$isAdmin) {
|
||||
$select->where(['maps', 'unlisted'], '=', false);
|
||||
}
|
||||
|
||||
$result = $select->execute();
|
||||
|
||||
$maps = [];
|
||||
@@ -45,11 +52,10 @@ class MapsController
|
||||
$maps[] = $map;
|
||||
}
|
||||
|
||||
$user = $this->request->user();
|
||||
return new HtmlContent('maps', [
|
||||
'maps' => $maps,
|
||||
'isLoggedIn' => $user !== null,
|
||||
'isAdmin' => $user !== null && $user->hasPermission(IUser::PERMISSION_ADMIN)
|
||||
'isAdmin' => $isAdmin
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ class Map extends Model
|
||||
{
|
||||
protected static string $table = 'maps';
|
||||
|
||||
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area'];
|
||||
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area', 'unlisted'];
|
||||
|
||||
private string $name = '';
|
||||
|
||||
@@ -16,6 +16,8 @@ class Map extends Model
|
||||
|
||||
private float $area = 0.0;
|
||||
|
||||
private bool $unlisted = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
|
||||
@@ -61,6 +63,11 @@ class Map extends Model
|
||||
$this->area = $area;
|
||||
}
|
||||
|
||||
public function setUnlisted(bool $unlisted): void
|
||||
{
|
||||
$this->unlisted = $unlisted;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
@@ -100,4 +107,9 @@ class Map extends Model
|
||||
{
|
||||
return $this->area;
|
||||
}
|
||||
|
||||
public function getUnlisted(): bool
|
||||
{
|
||||
return $this->unlisted;
|
||||
}
|
||||
}
|
||||
@@ -57,11 +57,9 @@ class UserInChallengeRepository
|
||||
}
|
||||
|
||||
// validate token string
|
||||
foreach (str_split($token_str) as $char) {
|
||||
if (!(('0' <= $char && $char <= '9') || ('a' <= $char && $char <= 'f'))) {
|
||||
if (!ctype_xdigit($token_str)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// convert token to int
|
||||
$token = hexdec($token_str);
|
||||
|
||||
@@ -78,6 +76,6 @@ class UserInChallengeRepository
|
||||
$select->where('user_id', '=', $userId);
|
||||
$select->where('challenge_id', '=', $challenge->getId());
|
||||
|
||||
return $select->count();
|
||||
return $select->count() != 0;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,9 @@
|
||||
<form id="metadataForm" class="marginTop" data-no-submit="true">
|
||||
<input class="fullWidth" type="text" name="name" value="<?= $mapName ?>" placeholder="Name of the map">
|
||||
<textarea class="fullWidth marginTop" name="description" rows="4" placeholder="Description of the map"><?= $mapDescription ?></textarea>
|
||||
<div class="marginTop">
|
||||
<input type="checkbox" id="unlisted" name="unlisted" <?= $mapUnlisted ? 'checked' : '' ?>><label for="unlisted">Unlisted</label>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button class="marginTop marginRight" type="submit">Apply</button><!--
|
||||
--><button id="closeMetadataButton" class="gray marginTop" type="button">Close</button>
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
|
||||
@section(subheader)
|
||||
<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>
|
||||
@endsection
|
||||
|
||||
|
||||
+13
-24
@@ -43,35 +43,24 @@ TODO: condition!
|
||||
<p class="bold center marginTop marginBottom">OR</p>
|
||||
-->
|
||||
|
||||
<div id="restrictions">
|
||||
<div id="restrictions" class="marginTop marginBottom">
|
||||
<h3>Optional restrictions</h3>
|
||||
<div>
|
||||
<div>
|
||||
<input type="checkbox" id="timerEnabled" name="timerEnabled" value="timerEnabled" />
|
||||
<label id="timeLimitLabel" for="timerEnabled">Time limit measured in seconds</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="range" id="timeLimit" name="timeLimit" min="10" max="1800" step="10" value="300" />
|
||||
<div class="marginTop">
|
||||
<input type="checkbox" id="timerEnabled" name="timerEnabled" value="timerEnabled" /><label id="timeLimitLabel" for="timerEnabled">Time limit measured in seconds</label>
|
||||
</div>
|
||||
<div id="timeLimitType">
|
||||
Time limit
|
||||
<input type="radio" id="timeLimitTypeGame" name="timeLimitType" value="game" checked />
|
||||
<label for="timeLimitTypeGame">for the whole game</label>
|
||||
<input type="radio" id="timeLimitTypeRound" name="timeLimitType" value="round" />
|
||||
<label for="timeLimitTypeRound">per round</label>
|
||||
<input class="fullWidth" type="range" id="timeLimit" name="timeLimit" min="10" max="1800" step="10" value="300" />
|
||||
<input type="radio" id="timeLimitTypeGame" name="timeLimitType" value="game" checked /><label for="timeLimitTypeGame">for the whole game</label>
|
||||
<input class="marginLeft" type="radio" id="timeLimitTypeRound" name="timeLimitType" value="round" /><label for="timeLimitTypeRound">per round</label>
|
||||
</div>
|
||||
<div class="marginTop">
|
||||
<input type="checkbox" id="noMove" name="noMove" value="noMove" /><label for="noMove">No movement allowed</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="noMove" name="noMove" value="noMove" />
|
||||
<label for="noMove">No movement allowed</label>
|
||||
<div class="marginTop">
|
||||
<input type="checkbox" id="noZoom" name="noZoom" value="noZoom" /><label for="noZoom">No zoom allowed</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="noZoom" name="noZoom" value="noZoom" />
|
||||
<label for="noMove">No zoom allowed</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" id="noPan" name="noPan" value="noPan" />
|
||||
<label for="noPan">No camera change allowed</label>
|
||||
<div class="marginTop">
|
||||
<input type="checkbox" id="noPan" name="noPan" value="noPan" /><label for="noPan">No camera change allowed</label>
|
||||
</div>
|
||||
<input type="hidden" name="mapId" id="challengeMapId" />
|
||||
</div>
|
||||
@@ -86,7 +75,7 @@ TODO: condition!
|
||||
@section(main)
|
||||
<div id="mapContainer">
|
||||
<?php foreach ($maps as $map): ?>
|
||||
<div class="mapItem">
|
||||
<div class="mapItem <?= $map['unlisted'] ? 'unlisted' : '' ?>">
|
||||
<div class="title">
|
||||
<p class="title"><?= $map['name'] ?></p>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user