Compare commits

...
112 Commits
Author SHA1 Message Date
balazs 8218883faf Merge pull request 'Feature In-Game Compass' (#27) from feature/in-game-compass into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/27
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 19:15:44 +02:00
balazs 145cb5f637 Merge branch 'develop' into feature/in-game-compass
default-pipeline default-pipeline #151
2021-04-29 19:00:11 +02:00
balazs 6d3f74f911 increased the left margin for mobile view
default-pipeline default-pipeline #150
2021-04-29 18:59:24 +02:00
balazs 3d663fb5d6 Merge pull request 'Quickfix for single player game cannot be restarted' (#30) from bugfix/quickfix-for-single-play-cannot-be-restarted into develop
default-pipeline default-pipeline #149
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/30
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:58 +02:00
balazs 759f654a0d Merge pull request 'feature/return-to-starting-point-button' (#26) from feature/return-to-starting-point-button into develop
default-pipeline default-pipeline #148
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/26
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-29 18:55:22 +02:00
bence c09cce3c97 Merge pull request 'MAPG-167 set background color' (#31) from feature/MAPG-167-set-background-color into develop
default-pipeline default-pipeline #147
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/31
2021-04-29 18:46:26 +02:00
bence 276e331a47 MAPG-167 set background color
default-pipeline default-pipeline #146
2021-04-29 18:43:30 +02:00
balazs 62f4fd7b84 readyToContinue is true by default and set to false only in the multigame initializing
default-pipeline default-pipeline #145
2021-04-29 13:57:32 +02:00
balazs b22f13887f clicking on compass sets the heading direction to north
default-pipeline default-pipeline #144
2021-04-29 12:39:44 +02:00
balazs 2a3f2212a7 replaced span with div for the compass item
default-pipeline default-pipeline #143
2021-04-29 12:12:17 +02:00
balazs 98df08e5fa Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 12:10:02 +02:00
balazs 8f0cfd4489 replaced spans with divs again and fixed the css issue
default-pipeline default-pipeline #142
2021-04-29 12:08:04 +02:00
balazs d42280eb10 only the image is rotated, not the whole div
default-pipeline default-pipeline #141
2021-04-29 09:18:24 +02:00
balazs 0739c65961 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 09:11:32 +02:00
balazs 3122696364 added the visibility reset to resetRound and reverted the span div replacement in the view
default-pipeline default-pipeline #140
2021-04-29 09:07:44 +02:00
balazs 462f6cde80 Merge branch 'feature/return-to-starting-point-button' of gitea.e5tv.hu:esoko/mapguesser into feature/return-to-starting-point-button
default-pipeline default-pipeline #139
2021-04-29 09:05:32 +02:00
balazs 3f4fa83d87 reverted replacing spans with divs 2021-04-29 09:04:50 +02:00
balazs ea599809b8 Merge branch 'feature/return-to-starting-point-button' into feature/in-game-compass 2021-04-29 08:34:39 +02:00
balazs f0b0de405c Merge branch 'develop' into feature/return-to-starting-point-button
default-pipeline default-pipeline #138
2021-04-29 08:25:52 +02:00
balazs ffc4aaf110 fixed findings
default-pipeline default-pipeline #137
2021-04-29 08:24:20 +02:00
bence 58cdc1be8d Merge pull request 'fix typo' (#29) from feature/modify-author into develop
default-pipeline default-pipeline #134
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/29
2021-04-29 00:58:57 +02:00
bence 4039202268 fix typo
default-pipeline default-pipeline #133
2021-04-29 00:58:20 +02:00
bence 4677cc5229 Merge pull request 'generalize author attribution' (#28) from feature/modify-author into develop
default-pipeline default-pipeline #131
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/28
2021-04-29 00:53:26 +02:00
bence 8800724aa9 generalize author attribution
default-pipeline default-pipeline #130
2021-04-29 00:52:46 +02:00
balazs 08c38c6374 reverted style tailoring for guess
default-pipeline default-pipeline #129
2021-04-28 20:46:52 +02:00
balazs 854775011c tailored the style for the navigation buttons
default-pipeline default-pipeline #128
2021-04-28 20:42:42 +02:00
balazs e33d8c02e9 added compass button and coupled its transform:rotate style attribute with the POV heading value 2021-04-28 20:33:17 +02:00
balazs 3d52c967b6 reformatted: added new line breaks to the game.css
default-pipeline default-pipeline #127
2021-04-28 18:30:58 +02:00
balazs 5c8f3d6fb9 returns to start when clicked on home symbol
default-pipeline default-pipeline #126
2021-04-28 18:26:46 +02:00
balazs b9927f79fc added a new button to the view for the return to starting point feature 2021-04-28 17:58:48 +02:00
balazs 56f3a9b380 Merge pull request 'Stuck scoring at zero fixed by regular map area update in session' (#24) from bugfix/scoring-stuck-at-zero into develop
default-pipeline default-pipeline #124
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/24
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 20:36:41 +02:00
balazs a0d41c388b Merge branch 'develop' into bugfix/scoring-stuck-at-zero
default-pipeline default-pipeline #123
2021-04-27 17:56:52 +02:00
balazs 092cc63148 Merge pull request 'jump to coordinate feature added to the header stripe' (#25) from feature/jump-to-specific-coordinates-in-mapeditor into develop
default-pipeline default-pipeline #122
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/25
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:56:35 +02:00
balazs 95553296bd removed extra line break
default-pipeline default-pipeline #121
2021-04-27 17:54:47 +02:00
balazs 9330227879 Merge branch 'feature/jump-to-specific-coordinates-in-mapeditor' of gitea.e5tv.hu:esoko/mapguesser into feature/jump-to-specific-coordinates-in-mapeditor
default-pipeline default-pipeline #120
2021-04-27 17:50:10 +02:00
balazs faaff33841 removed #jumpForm and reformatted the code 2021-04-27 17:48:45 +02:00
balazs 508a6899d8 Merge branch 'develop' into bugfix/scoring-stuck-at-zero
default-pipeline default-pipeline #119
2021-04-27 17:36:18 +02:00
balazs be0f7b64b8 refactored comment
default-pipeline default-pipeline #118
2021-04-27 17:35:06 +02:00
balazs 15e3248ee1 Merge branch 'develop' into feature/jump-to-specific-coordinates-in-mapeditor
default-pipeline default-pipeline #117
2021-04-27 17:26:44 +02:00
balazs 04a897c958 Merge pull request 'folder name db changed to database in the path' (#23) from bugfix/change-path-to-renamed-folder into develop
default-pipeline default-pipeline #116
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/23
Reviewed-by: Pőcze Bence <bence@pocze.ch>
2021-04-27 17:25:54 +02:00
balazs 845d77ebda area of map is updated in the session at each game preparation to avoid outdated map area and wrong score calculation
default-pipeline default-pipeline #115
2021-04-27 16:46:40 +02:00
balazs 8bc3fda49b jump to coordinate feature added to the header stripe
default-pipeline default-pipeline #114
2021-04-27 10:18:28 +02:00
balazs d78817180b folder name db changed to database in the path
default-pipeline default-pipeline #113
2021-04-26 20:21:21 +02:00
bence 0f810bd2ba Merge pull request 'feature/MAPG-228-deploy-signed-and-verified-tags-only' (#22) from feature/MAPG-228-deploy-signed-and-verified-tags-only into develop
default-pipeline default-pipeline #111
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/22
2021-04-25 15:16:46 +02:00
bence 9d1fc8b60c MAPG-228 prune local tags when fetching
default-pipeline default-pipeline #107
2021-04-25 15:11:49 +02:00
bence dd46fb3d32 MAPG-228 check if the Release* tag is signed and verified 2021-04-25 15:11:20 +02:00
bence 1a40e3a18d Merge pull request 'MAPG-226 change remaining time calculation' (#21) from feature/MAPG-226-change-timeout-algorithm into develop
default-pipeline default-pipeline #106
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/21
2021-04-10 19:00:20 +02:00
bence 1aa5d3b43e MAPG-226 change remaining time calculation 2021-04-10 18:59:27 +02:00
bence a418d5cf66 Merge pull request 'MAPG-213 move visibility workaround to Game.showResultMap' (#20) from bugfix/MAPG-213-result-map-not-show-on-mobile-after-reload into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/20
2021-04-04 23:41:05 +02:00
bence 5cae286a5b MAPG-213 move visibility workaround to Game.showResultMap 2021-04-04 23:40:27 +02:00
bence c5d2591371 Merge pull request 'MAPG-223 send error if member already guessed' (#19) from feature/MAPG-223-user-should-not-be-able-to-guess-after-round-is-over into develop
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/19
2021-04-04 23:29:04 +02:00
bence cf59f937c9 MAPG-223 send error if member already guessed
default-pipeline default-pipeline #104
2021-04-04 23:28:44 +02:00
bence 257f59c96e Merge pull request 'MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0' (#18) from bugfix/MAPG-222-show-summary-can-be-clicked-when-game-is-restarted into develop
default-pipeline default-pipeline #98
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/18
2021-04-04 23:05:47 +02:00
bence c2f5806394 MAPG-222 fixes: skip invalid guessPosition, hide showSummaryButton, handle countdown time if timeout is 0
default-pipeline default-pipeline #96
2021-04-04 23:04:07 +02:00
bence ac0b71fe09 Merge pull request 'feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded' (#17) from feature/MAPG-213-user-shouldn-t-be-able-to-re-guess-when-page-is-reloaded into develop
default-pipeline default-pipeline #94
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/17
2021-04-04 22:31:29 +02:00
bence 19f7b50002 MAPG-213 show results immediately if user already guessed
default-pipeline default-pipeline #91
2021-04-04 22:24:40 +02:00
bence 6342bb1e79 MAPG-213 send round data at initialization if user guessed 2021-04-04 22:18:00 +02:00
bence 3abdb6910e MAPG-213 clean up init
default-pipeline default-pipeline #90
2021-04-04 20:52:28 +02:00
bence 7d2588f7cd Merge pull request 'feature/MAPG-219-disable-continue-if-timeout-is-not-reached' (#16) from feature/MAPG-219-disable-continue-if-timeout-is-not-reached into develop
default-pipeline default-pipeline #88
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/16
2021-04-04 19:23:08 +02:00
bence be40acdb2d MAPG-219 disable continue button until round is over
default-pipeline default-pipeline #84
2021-04-04 18:48:31 +02:00
bence a2f2080959 MAPG-219 send 'end_round' when all members are finished
default-pipeline default-pipeline #83
2021-04-04 17:43:37 +02:00
bence cc49c56075 Merge pull request 'feature/MAPG-205-set-timeout-in-multiplayer-rooms' (#15) from feature/MAPG-205-set-timeout-in-multiplayer-rooms into develop
default-pipeline default-pipeline #82
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/15
2021-04-04 14:22:59 +02:00
bence 69329b9f81 MAPG-205 show countdown time
default-pipeline default-pipeline #80
2021-04-04 14:22:10 +02:00
bence 20914b466e MAPG-205 add elements for showing countdown 2021-04-04 14:20:12 +02:00
bence 3630637ec1 MAPG-205 handle round timeout on frontend 2021-04-04 14:20:12 +02:00
bence 42224b074e MAPG-205 set timeout for rounds 2021-04-04 14:20:12 +02:00
bence 0e8ed14305 MAPG-205 delete unnecessary empty lines 2021-04-03 23:39:20 +02:00
bence d3a12fb6f1 Merge pull request 'feature/MAPG-204-show-guesses-from-others' (#14) from feature/MAPG-204-show-guesses-from-others into develop
default-pipeline default-pipeline #76
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/14
2021-04-03 22:13:52 +02:00
bence 3f9d104adc MAPG-204 show guesses from others
default-pipeline default-pipeline #72
2021-04-03 22:10:21 +02:00
bence 9e5d20796c MAPG-204 clean up result sending 2021-04-03 18:24:43 +02:00
bence 7433813337 Merge pull request 'MAPG-208 error handling adaptations' (#13) from bugfix/MAPG-208-error-messages into develop
default-pipeline default-pipeline #75
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/13
2021-03-21 16:39:43 +01:00
bence 42854fe975 MAPG-208 error handling adaptations
default-pipeline default-pipeline #53
2021-03-21 14:37:08 +01:00
bence 71681f1e55 Merge pull request 'MAPG-207 don't use MapGuesser.showmodal for modal 'multi'' (#12) from bugfix/MAPG-207-prevent-closing-multi-modal into develop
default-pipeline default-pipeline #50
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/12
2021-03-21 12:47:35 +01:00
bence 1cf0ab44fd MAPG-207 don't use MapGuesser.showmodal for modal 'multi'
default-pipeline default-pipeline #49
2021-03-21 12:45:57 +01:00
bence fc9eb183f1 Merge pull request 'MAPG-203 fix wrong variable name' (#11) from bugfix/MAPG-203-fix-condition-in-js into develop
default-pipeline default-pipeline #46
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/11
2021-03-20 22:26:33 +01:00
bence d431a693fc MAPG-203 fix wrong variable name
default-pipeline default-pipeline #45
2021-03-20 22:24:47 +01:00
bence ace490b7e9 Merge pull request 'MAPG-203 use MULTI_WS_URL environment variable' (#10) from feature/MAPG-203-fine-tune-env-variables into develop
default-pipeline default-pipeline #43
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/10
2021-03-20 21:24:20 +01:00
bence e3771d36ce MAPG-203 use MULTI_WS_URL environment variable
default-pipeline default-pipeline #42
2021-03-20 21:21:40 +01:00
bence 6e03476173 Merge pull request 'bugfix/MAPG-203-fix-singleplayer' (#9) from bugfix/MAPG-203-fix-singleplayer into develop
default-pipeline default-pipeline #39
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/9
2021-03-20 21:00:14 +01:00
bence f3ec71d6dd MAPG-203 fix condition in game.js
default-pipeline default-pipeline #38
2021-03-20 20:59:10 +01:00
bence c2f0a1dd51 MAPG-203 fix signatures in GameController 2021-03-20 20:58:57 +01:00
bence a417fbd760 Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#8) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #37
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/8
2021-03-20 20:46:36 +01:00
bence a9cda56586 MAPG-203 install NPM packages on install and update
default-pipeline default-pipeline #36
2021-03-20 19:45:21 +01:00
bence 02fcbd2f9c MAPG-203 prepare GameFlowController for multiplayer 2021-03-20 19:45:21 +01:00
bence 2f665381c3 MAPG-203 prepare GameController for multiplayer 2021-03-20 19:45:21 +01:00
bence 573440868e MAPG-203 add new routes for multiplayer 2021-03-20 19:45:21 +01:00
bence 79490a0616 MAPG-203 add class for multiplayer internal connection (PHP-NodeJS TCP) 2021-03-20 19:45:21 +01:00
bence ed343f2359 MAPG-203 add new environment variables 2021-03-20 19:45:20 +01:00
bence 563f900423 MAPG-203 implement game mode selection UI 2021-03-20 19:45:20 +01:00
bence 24a10c534e MAPG-203 frontend adaptations for multiplayer 2021-03-20 19:44:48 +01:00
bence 0cbb7ba145 MAPG-203 add debugger config for NodeJS 2021-03-20 19:44:48 +01:00
bence 5dd2ce0d5a MAPG-203 add cleanup for multi_rooms to db:maintain 2021-03-20 19:44:48 +01:00
bence e5fb725c69 MAPG-203 add new table and model for multi_room 2021-03-20 19:44:48 +01:00
bence fc40c18679 MAPG-203 multiplayer handler server (NodeJS) 2021-03-20 19:44:48 +01:00
bence 4fabc39d44 MAPG-203 restructure Docker stack 2021-03-20 19:44:48 +01:00
bence 2120fbebbc MAPG-203 add Dockerfile for multiplayer (NodeJS) 2021-03-20 19:44:47 +01:00
bence b9f0529dce MAPG-203 add new Composer packages 2021-03-20 19:44:47 +01:00
bence a3ca7638c7 MAPG-203 add NPM packages for multiplayer 2021-03-20 15:21:07 +01:00
bence 8538d0a119 MAPG-203 add node_modules to gitignore 2021-03-17 23:09:07 +01:00
bence 0cc63ef936 Merge pull request 'MAPG-203 this.response.newPlace -> this.response.place' (#7) from bugfix/MAPG-203-fix-wrong-object-key into develop
default-pipeline default-pipeline #28
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/7
2021-03-17 22:54:45 +01:00
bence 10d239bfb4 MAPG-203 this.response.newPlace -> this.response.place
default-pipeline default-pipeline #25
2021-03-17 22:54:06 +01:00
bence e2493e1b7e Merge pull request 'MAPG-203 select places at the beginning of the game' (#6) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #24
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/6
2021-03-17 22:48:43 +01:00
bence edd8c0f8a4 MAPG-203 select places at the beginning of the game
default-pipeline default-pipeline #21
2021-03-17 22:47:11 +01:00
bence 01f0f7d4bc Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#5) from feature/MAPG-203-initial-multiplayer-implementation into develop
default-pipeline default-pipeline #17
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/5
2021-03-15 12:32:54 +01:00
bence c317a0abf4 MAPG-203 adapt game.js to the new endpoints
default-pipeline default-pipeline #15
2021-03-15 12:28:25 +01:00
bence 9c4b7448a9 MAPG-203 refactor GameFlowController 2021-03-15 12:28:06 +01:00
bence 60e20bd92b MAPG-203 rename game/newPlace endpoint 2021-03-15 12:26:15 +01:00
bence 2c4d809d49 MAPG-203 remove placesWithoutPano logic from PlaceRepository 2021-03-15 12:25:49 +01:00
bence bf27f15709 Merge pull request 'MAPG-2020 run PHPStan with 1G memory limit' (#4) from bugfix/MAPG-2020-phpstan-needs-bigger-memory-limit into develop
default-pipeline default-pipeline #12
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/4
2021-01-01 22:36:56 +01:00
bence db3094eb62 MAPG-2020 run PHPStan with 1G memory limit
default-pipeline default-pipeline #11
2021-01-01 22:34:32 +01:00
bence 77c71ab2da Merge pull request 'MAPG-201 update Git link to gitea.e5tv.hu' (#2) from feature/MAPG-201-migrate-to-own-git-server into develop
Check 2 Check 2 was successful
Check 3 Check 3 was not successful
Check 4 Check 4 was successful
Check 5 Check 5 was successful
Check Check was successful
Pipeline Pipeline #1
test_pipeline test_pipeline #3
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/2
2020-12-27 22:12:33 +00:00
bence 8327cd975a MAPG-201 update Git link to gitea.e5tv.hu 2020-12-27 23:08:30 +01:00
39 changed files with 1928 additions and 222 deletions

No files matched your search

+4
View File
@@ -15,3 +15,7 @@ MAIL_PORT=2500
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
GOOGLE_ANALITICS_ID=your_google_analytics_id GOOGLE_ANALITICS_ID=your_google_analytics_id
MULTI_INTERNAL_HOST=multi
MULTI_INTERNAL_PORT=5000
MULTI_WS_URL=mapguesser-dev.ch:8090
MULTI_WS_PORT=8090
+1
View File
@@ -1,3 +1,4 @@
.env .env
installed installed
vendor vendor
node_modules
+8
View File
@@ -9,6 +9,14 @@
"pathMappings": { "pathMappings": {
"/var/www/mapguesser": "${workspaceRoot}", "/var/www/mapguesser": "${workspaceRoot}",
} }
},
{
"name": "Listen for NodeJS Inspector in Docker",
"type": "node",
"request": "attach",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/var/www/mapguesser"
} }
] ]
} }
+1 -1
View File
@@ -9,7 +9,7 @@ This is the MapGuesser Application project. This is a game about guessing where
The first step is obviously cloning the repository to your machine: The first step is obviously cloning the repository to your machine:
``` ```
git clone https://bitbucket.org/esoko/mapguesser.git git clone https://gitea.e5tv.hu/esoko/mapguesser.git
``` ```
All the commands listed here should be executed from the repository root. All the commands listed here should be executed from the repository root.
+1 -1
View File
@@ -23,4 +23,4 @@ pipelines:
- apt-get update && apt-get install -y unzip - apt-get update && apt-get install -y unzip
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install - composer install
- vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json - php -d memory_limit=1G vendor/bin/phpstan analyse -c phpstan.neon --error-format=prettyJson > static_code_analysis_results.json
+2 -1
View File
@@ -6,7 +6,8 @@
"require": { "require": {
"vlucas/phpdotenv": "^4.1", "vlucas/phpdotenv": "^4.1",
"symfony/console": "^5.1", "symfony/console": "^5.1",
"phpmailer/phpmailer": "^6.1" "phpmailer/phpmailer": "^6.1",
"fzaninotto/faker": "^1.9"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9", "phpunit/phpunit": "^9",
Generated
+57 -2
View File
@@ -4,8 +4,63 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "34563bfc619f47473b2e37a5639dd63e", "content-hash": "b71c0ffc0761a6b90f6242346b735a09",
"packages": [ "packages": [
{
"name": "fzaninotto/faker",
"version": "v1.9.2",
"source": {
"type": "git",
"url": "https://github.com/fzaninotto/Faker.git",
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fzaninotto/Faker/zipball/848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
"reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"ext-intl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7",
"squizlabs/php_codesniffer": "^2.9.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "François Zaninotto"
}
],
"description": "Faker is a PHP library that generates fake data for you.",
"keywords": [
"data",
"faker",
"fixtures"
],
"support": {
"issues": "https://github.com/fzaninotto/Faker/issues",
"source": "https://github.com/fzaninotto/Faker/tree/v1.9.2"
},
"abandoned": true,
"time": "2020-12-11T09:56:16+00:00"
},
{ {
"name": "phpmailer/phpmailer", "name": "phpmailer/phpmailer",
"version": "v6.1.6", "version": "v6.1.6",
@@ -2710,5 +2765,5 @@
"prefer-lowest": false, "prefer-lowest": false,
"platform": [], "platform": [],
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "1.1.0" "plugin-api-version": "2.0.0"
} }
@@ -0,0 +1,9 @@
CREATE TABLE `multi_rooms` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`room_id` varchar(6) NOT NULL,
`state` text NOT NULL,
`members` text NOT NULL,
`updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `room_id` (`room_id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+13 -4
View File
@@ -1,14 +1,23 @@
version: '3' version: '3'
services: services:
app: app:
build: ./docker build:
context: ./docker
dockerfile: Dockerfile-app
ports: ports:
- 80:80 - 80:80
volumes: volumes:
- .:/var/www/mapguesser - .:/var/www/mapguesser
links: multi:
- 'mariadb' build:
- 'mail' context: ./docker
dockerfile: Dockerfile-multi
ports:
- 5000:5000
- 8090:8090
- 9229:9229
volumes:
- .:/var/www/mapguesser
mariadb: mariadb:
image: mariadb:10.3 image: mariadb:10.3
ports: ports:
File renamed without changes.
+16
View File
@@ -0,0 +1,16 @@
FROM ubuntu:focal
ENV DEBIAN_FRONTEND noninteractive
# Install necessary packages
RUN apt update --fix-missing
RUN apt install -y curl build-essential
# Install Node.js and required packages
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt install -y nodejs
VOLUME /var/www/mapguesser
WORKDIR /var/www/mapguesser
ENTRYPOINT /usr/bin/node --inspect=0.0.0.0:9229 multi
+369
View File
@@ -0,0 +1,369 @@
'use strict';
process.title = 'mapguesser-multi';
class MultiGame {
static ROUND_TIMEOUT_DEFAULT = 120000;
static ROUND_TIMEOUT_MINIMUM = 15000;
static ROUND_TIMEOUT_DIVIDER = 1.5;
static ROUND_TIMEOUT_OFFSET = 500;
constructor() {
this.rooms = new Map();
}
cleanupRooms() {
this.rooms.forEach(function (room, roomId) {
var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000);
if (room.updated < lastValidDate) {
this.rooms.delete(roomId);
}
});
}
connectToRoom(roomId, token, connection) {
if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) {
return;
}
var room = this.rooms.get(roomId)
var member = room.members.get(token);
member.connection = connection;
this._sendInitialData(room, member, token);
}
createRoom(roomId) {
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
return { ok: true };
}
joinRoom(roomId, token, userName) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
if (room.members.has(token)) {
return { error: 'member_already_joined' };
}
var data = { userName: userName };
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'member_joined', data);
});
room.members.set(token, { userName: userName, connection: null });
return { ok: true };
}
startGame(roomId, places) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var rounds = [];
places.forEach(function (place) {
rounds.push({
place: place,
results: new Map(),
timeout: MultiGame.ROUND_TIMEOUT_DEFAULT,
timeoutStarted: null,
timeoutHandler: null
})
});
room.rounds = rounds;
this.nextRound(roomId, 0);
return { ok: true };
}
guess(roomId, token, guessPosition, distance, score) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
var round = room.rounds[room.currentRound];
if (round.results.has(token)) {
return { error: 'already_guessed' };
}
var member = room.members.get(token);
var allResults = this._collectResultsInRound(room, round);
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score });
this._setNewTimeout(room, round);
return { allResults: allResults };
}
nextRound(roomId, currentRound) {
if (!this.rooms.has(roomId)) {
return { error: 'room_not_found' };
}
var room = this.rooms.get(roomId);
room.updated = new Date();
room.currentRound = currentRound;
var round = room.rounds[room.currentRound];
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
var data = {};
data.place = { panoId: round.place.panoId, pov: round.place.pov };
data.timeout = round.timeout;
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'new_round', data);
});
return { ok: true };
}
_setNewTimeout(room, round) {
clearTimeout(round.timeoutHandler);
if (room.members.size === round.results.size) {
round.timeout = 0;
round.timeoutStarted = new Date();
this._endRound(room, round);
} else {
round.timeout = round.timeout - (new Date() - round.timeoutStarted);
if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER);
} else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) {
round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM;
}
round.timeoutStarted = new Date();
var self = this;
round.timeoutHandler = setTimeout(function () {
self._endRound(room, round);
}, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET);
this._broadcastTimeout(room, round);
}
}
_endRound(room, round) {
var allResults = this._collectResultsInRound(room, round);
var self = this;
room.members.forEach(function (member, token) {
var result = { guessPosition: null, distance: null, score: 0 };
if (round.results.has(token)) {
result = round.results.get(token);
} else {
round.results.set(token, result);
}
var data = { position: round.place.position, result: result, allResults: allResults };
self._sendToMember(member, 'end_round', data);
});
}
_sendInitialData(room, member, token) {
var data = {};
if (room.currentRound >= 0) {
var round = room.rounds[room.currentRound];
data.place = round.place;
data.timeout = round.timeout - (new Date() - round.timeoutStarted);
}
data.history = [];
for (var i = 0; i <= room.currentRound; ++i) {
var round = room.rounds[i];
if (i === room.currentRound && !round.results.has(token)) {
continue;
}
var result = { guessPosition: null, distance: null, score: 0 };
var allResults = [];
round.results.forEach(function (currentResult, currentToken) {
if (token === currentToken) {
result = currentResult;
return;
}
allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score });
});
data.history.push({
position: round.place.position,
result: result,
allResults: allResults
});
}
data.members = [];
room.members.forEach(function (currentMember) {
data.members.push({ userName: currentMember.userName, me: member === currentMember });
});
data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size
this._sendToMember(member, 'initialize', data);
}
_collectResultsInRound(room, round) {
var results = [];
round.results.forEach(function (result, token) {
results.push({
userName: room.members.get(token).userName,
guessPosition: result.guessPosition,
distance: result.distance,
score: result.score
});
});
return results;
}
_broadcastTimeout(room, round) {
var self = this;
room.members.forEach(function (member) {
self._sendToMember(member, 'timeout_changed', { timeout: round.timeout });
});
}
_broadcastGuess(room, userName, guessPosition, distance, score) {
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
var round = room.rounds[room.currentRound];
var self = this;
room.members.forEach(function (member, token) {
if (!round.results.has(token)) {
return;
}
self._sendToMember(member, 'guess', data);
});
}
_sendToMember(member, type, data) {
if (!member.connection) {
return;
}
if (member.connection.readyState !== ws.OPEN) {
member.connection = null;
return;
}
member.connection.send(JSON.stringify({ type: type, data: data }));
}
}
require('dotenv').config();
var
net = require('net'),
ws = require('ws');
var multiGame = new MultiGame();
//TODO: following should be in a separate class/function
var tcpServer = net.createServer(function (socket) {
socket.on('data', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
var response;
switch (data.func) {
case 'create_room':
response = multiGame.createRoom(data.args.roomId);
break;
case 'join_room':
response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
break;
case 'start_game':
response = multiGame.startGame(data.args.roomId, data.args.places);
break
case 'guess':
response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
break;
case 'next_round':
response = multiGame.nextRound(data.args.roomId, data.args.currentRound);
break;
}
socket.write(JSON.stringify(response));
socket.end();
});
});
tcpServer.on('listening', function () {
console.log('[INFO] TCP server started');
});
tcpServer.listen(process.env.MULTI_INTERNAL_PORT);
var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT });
wsServer.on('connection', function (connection, request) {
console.log('[INFO] New WS connection: ' + request.connection.remoteAddress);
connection.on('message', function (data) {
try {
data = JSON.parse(data);
} catch (e) {
console.error('Cannot parse data: ' + data);
return;
}
switch (data.func) {
case 'connect_to_room':
multiGame.connectToRoom(data.args.roomId, data.args.token, connection);
break;
}
});
connection.on('close', function () {
console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress);
});
});
wsServer.on('listening', function () {
console.log('[INFO] WS server started');
});
setInterval(function () {
multiGame.cleanupRooms();
}, 24 * 60 * 60 * 1000);
+17
View File
@@ -0,0 +1,17 @@
{
"name": "mapguesser-multi",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
},
"ws": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz",
"integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw=="
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "mapguesser-multi",
"version": "",
"description": "MapGuesser Application - Multiplayer",
"main": "index.js",
"dependencies": {
"dotenv": "^8.2.0",
"ws": "^7.4.4"
},
"scripts": {},
"author": "Pőcze Bence and The MapGuesser Contributors <bence@pocze.ch>",
"license": "GNU AGPL 3.0"
}
+107 -2
View File
@@ -79,7 +79,7 @@
line-height: 1; line-height: 1;
} }
#distanceInfo>p:nth-child(2), #scoreInfo>p:nth-child(2) { #distanceInfo>p:nth-child(2), #distanceInfo>p:nth-child(3), #scoreInfo>p:nth-child(2) {
display: none; display: none;
} }
@@ -102,13 +102,94 @@
display: none; display: none;
} }
#startMultiGameButton {
display: none;
}
#players > p {
font-size: 14px;
font-weight: bold;
}
#countdown {
position: absolute;
top: 5px;
right: 5px;
height: 28px;
line-height: 28px;
padding: 0 8px;
background-color: #eeeeee;
border: solid 1px #555555;
border-radius: 3px;
opacity: 0.95;
z-index: 5;
visibility: hidden;
}
#countdown.yellow {
background-color: #f7c789;
border: solid 1px #e8a349;
}
#countdown.red {
background-color: #f7a5a5;
border: solid 1px #aa5e5e;
}
#countdown p {
font-size: 16px;
line-height: inherit;
}
#countdown.yellow p {
color: #9c4308;
}
#countdown.red p {
color: #701919;
}
#navigation {
position: absolute;
z-index: 2;
}
#navigation .navigationItem {
margin-top: 10px;
opacity: 70%;
cursor: pointer;
}
#navigation .navigationItem:hover {
opacity: 100%;
}
#navigation .navigationItem div {
position: absolute;
}
#navigation .circleBackground {
width: 100%;
height: 100%;
opacity: 50%;
}
#navigation .navigationIcon {
width: 75%;
height: 75%;
margin: auto;
margin-top: 50%;
transform: translateY(-50%);
-ms-transform: translateY(-50%);
}
@media screen and (max-width: 599px) { @media screen and (max-width: 599px) {
#mapName { #mapName {
display: none; display: none;
} }
#showGuessButtonContainer { #showGuessButtonContainer {
position: absolute; position: absolute;
left: 20px; left: 65px;
bottom: 30px; bottom: 30px;
right: 20px; right: 20px;
z-index: 2; z-index: 2;
@@ -125,6 +206,14 @@
#scoreBarBase { #scoreBarBase {
width: 100%; width: 100%;
} }
#navigation {
bottom: 25px;
left: 10px;
width: 45px;
}
#navigation .navigationItem {
height: 45px;
}
} }
@media screen and (min-width: 600px) { @media screen and (min-width: 600px) {
@@ -168,6 +257,14 @@
#scoreBarBase { #scoreBarBase {
width: 60%; width: 60%;
} }
#navigation {
bottom: 50px;
left: 20px;
width: 60px;
}
#navigation .navigationItem {
height: 60px;
}
@media screen and (max-height: 424px) { @media screen and (max-height: 424px) {
#guess { #guess {
top: 10px; top: 10px;
@@ -182,5 +279,13 @@
right: 20px; right: 20px;
bottom: 30px; bottom: 30px;
} }
#navigation {
bottom: 30px;
left: 10px;
width: 45px;
}
#navigation .navigationItem {
height: 45px;
}
} }
} }
+5
View File
@@ -12,6 +12,10 @@ html, body {
padding: 0; padding: 0;
} }
body {
background-color: #cccccc;
}
button::-moz-focus-inner, input::-moz-focus-inner { button::-moz-focus-inner, input::-moz-focus-inner {
padding: 0; padding: 0;
border: 0; border: 0;
@@ -374,6 +378,7 @@ header>p>span:not(:last-child) {
} }
main { main {
background-color: #ffffff;
padding: 6px 12px; padding: 6px 12px;
} }
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 16 16">
<circle cx="50%" cy="50%" r="50%" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 146 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<polygon id="needleNorth" points="50,0 33,50 66,50" style="fill:red;stroke:black;stroke-width:1" />
<polygon id="needleSouth" points="50,100 33,50 66,50" style="fill:white;stroke:black;stroke-width:1" />
Sorry, your browser does not support inline SVG.
</svg>

After

Width:  |  Height:  |  Size: 328 B

+5
View File
@@ -0,0 +1,5 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="#3183ce" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 556 B

@@ -0,0 +1,10 @@
<!-- Original image: Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. -->
<svg viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg">
<path
fill="#3183ce"
fill-rule="evenodd"
stroke="#19456d"
stroke-width="0.3"
stroke-linecap="round"
d="m 5.9999998,15.849652 c 0,0 5.8511182,-5.579947 5.8511182,-9.8134832 a 5.8511179,5.8880898 0 0 0 -11.7022358,0 c 0,4.2335362 5.8511176,9.8134832 5.8511176,9.8134832" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

+590 -96
View File
@@ -5,6 +5,8 @@
NUMBER_OF_ROUNDS: 5, NUMBER_OF_ROUNDS: 5,
MAX_SCORE: 1000, MAX_SCORE: 1000,
mapBounds: null,
multi: { token: null, owner: false },
rounds: [], rounds: [],
scoreSum: 0, scoreSum: 0,
panoId: null, panoId: null,
@@ -15,41 +17,281 @@
adaptGuess: false, adaptGuess: false,
googleLink: null, googleLink: null,
initialize: function () { readyToContinue: true,
timeoutEnd: null,
countdownHandler: null,
MultiConnector: {
connection: null,
reconnectCounter: 0,
connect: function () {
if (Game.MultiConnector.connection && Game.MultiConnector.connection.readyState !== WebSocket.CLOSED) {
return;
}
Game.MultiConnector.connection = new WebSocket((MapGuesser.isSecure ? 'wss' : 'ws') + '://' + multiUrl);
Game.MultiConnector.connection.onopen = function () {
document.getElementById('loading').style.visibility = 'hidden';
Game.MultiConnector.reconnectCounter = 0;
Game.MultiConnector.connection.send(JSON.stringify({ func: 'connect_to_room', args: { roomId: roomId, token: Game.multi.token } }));
};
Game.MultiConnector.connection.onclose = Game.MultiConnector.noConnection;
Game.MultiConnector.connection.onerror = function (event) {
console.error('WebSocket error in Game.MultiConnector:', event);
};
Game.MultiConnector.connection.onmessage = function (message) {
var json;
try {
json = JSON.parse(message.data);
} catch (e) {
console.error('Cannot parse message!');
console.error(message.data);
return;
}
switch (json.type) {
case 'initialize':
Game.MultiConnector.initialize(json.data);
break;
case 'member_joined':
Game.MultiConnector.memberJoined(json.data);
break;
case 'new_round':
Game.MultiConnector.newRound(json.data);
break;
case 'guess':
Game.MultiConnector.guess(json.data);
break;
case 'timeout_changed':
Game.MultiConnector.timeoutChanged(json.data);
break;
case 'end_round':
Game.MultiConnector.endRound(json.data);
break;
}
};
},
noConnection: function () {
if (Game.MultiConnector.reconnectCounter === 2) {
console.error('Could not reconnect WebSocket for Game.MultiConnector...')
}
setTimeout(function () {
Game.MultiConnector.reconnectCounter++;
console.log('Reconnecting WebSocket for Game.MultiConnector... ' + Game.MultiConnector.reconnectCounter);
Game.MultiConnector.connect();
}, 1000 + Math.min(Game.MultiConnector.reconnectCounter * 500, 9000));
},
initialize: function (data) {
Game.readyToContinue = false;
if (data.history.length === 0 && !data.place) {
var div = document.getElementById('players');
for (var i = 0; i < data.members.length; ++i) {
var member = data.members[i];
var p = document.createElement('p');
p.innerHTML = member.userName + (member.me ? ' (me)' : '');
div.appendChild(p);
}
}
for (var i = 0; i < data.history.length; ++i) {
var round = data.history[i];
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, null, true);
}
Game.scoreSum += round.result.score;
for (var j = 0; j < round.allResults.length; ++j) {
var result = round.allResults[j];
if (result.guessPosition) {
Game.addGuessPositionToResultMap(result.guessPosition, result, true);
}
}
}
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);
if (data.timeout) {
Game.startCountdown(data.timeout);
}
if (data.place) {
Game.readyToContinue = data.readyToContinue;
Game.panoId = data.place.panoId;
Game.pov = data.place.pov;
document.getElementById('multi').style.visibility = 'hidden';
if (Game.rounds.length === 0 || !Game.rounds[Game.rounds.length - 1].position) {
document.getElementById('panoCover').style.visibility = 'hidden';
Game.startNewRound();
} else {
Game.loadPano(Game.panoId, Game.pov);
Game.showResultFromHistory(data.history[data.history.length - 1].result);
}
}
document.getElementById('loading').style.visibility = 'hidden';
},
memberJoined: function (data) {
var div = document.getElementById('players');
var p = document.createElement('p');
p.innerHTML = data.userName;
div.appendChild(p);
},
newRound: function (data) {
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
Game.reset();
}
Game.readyToContinue = false;
Game.panoId = data.place.panoId;
Game.pov = data.place.pov;
document.getElementById('multi').style.visibility = 'hidden';
Game.resetRound();
Game.startNewRound();
Game.startCountdown(data.timeout);
},
guess: function (data) {
var resultBounds = Game.map.getBounds();
Game.addGuessPositionToResultMap(data.guessPosition, data);
resultBounds.extend(data.guessPosition);
Game.map.fitBounds(resultBounds);
},
timeoutChanged: function (data) {
Game.startCountdown(data.timeout);
},
endRound: function (data) {
Game.readyToContinue = true;
Game.startCountdown(0);
if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
if (Game.multi.owner) {
document.getElementById('continueButton').disabled = false;
document.getElementById('startNewGameButton').disabled = false;
}
return;
}
document.getElementById('guessButton').disabled = true;
document.getElementById('panoCover').style.visibility = 'visible';
Game.receiveResult(data.position, data.result.guessPosition, { distance: data.result.distance, score: data.result.score }, data.allResults);
}
},
prepare: function () {
var data = new FormData();
var userNames;
if (roomId) {
var userNames = localStorage.userNames ? JSON.parse(localStorage.userNames) : {};
if (!userNames.hasOwnProperty(roomId)) {
userNames[roomId] = prompt('Your name: ');
localStorage.userNames = JSON.stringify(userNames);
}
data.append('userName', userNames[roomId]);
}
document.getElementById('loading').style.visibility = 'visible'; document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json';
MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
document.getElementById('mapName').innerHTML = this.response.mapName;
Game.mapBounds = this.response.bounds;
Game.initialize();
if (roomId) {
Game.multi.token = this.response.token;
Game.multi.owner = this.response.owner;
document.getElementById('multi').style.visibility = 'visible';
if (Game.multi.owner) {
document.getElementById('startMultiGameButton').style.display = 'block';
}
document.getElementById('loading').style.visibility = 'visible';
Game.MultiConnector.connect();
}
}, data);
},
initialize: function () {
document.getElementById('panoCover').style.visibility = 'visible'; document.getElementById('panoCover').style.visibility = 'visible';
document.getElementById('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
document.getElementById('currentScoreSum').innerHTML = '0/0';
Game.map.setOptions({ Game.map.setOptions({
draggableCursor: 'crosshair' draggableCursor: 'crosshair'
}); });
Game.map.fitBounds(mapBounds); Game.map.fitBounds(Game.mapBounds);
MapGuesser.httpRequest('GET', '/game/' + mapId + '/newPlace.json', function () { if (roomId) {
// if it is multiplayer mode, data is sent via WS
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/game/' + mapId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden'; document.getElementById('loading').style.visibility = 'hidden';
document.getElementById('panoCover').style.visibility = 'hidden'; document.getElementById('panoCover').style.visibility = 'hidden';
if (this.response.error) { if (this.response.error) {
//TODO: handle this error Game.handleErrorResponse(this.response.error);
return; return;
} }
Game.panoId = this.response.panoId; Game.panoId = this.response.place.panoId;
Game.pov = this.response.pov; Game.pov = this.response.place.pov;
if (this.response.history) { for (var i = 0; i < this.response.history.length; ++i) {
for (var i = 0; i < this.response.history.length; ++i) { var round = this.response.history[i];
var round = this.response.history[i]; Game.rounds.push({ position: round.position, guessPosition: round.result.guessPosition, realMarker: null, guessMarkers: [] });
Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null }); Game.addPositionToResultMap(true);
Game.addRealGuessPair(round.position, round.guessPosition, true); Game.addGuessPositionToResultMap(round.result.guessPosition, null, true);
Game.scoreSum += round.score; Game.scoreSum += round.result.score;
}
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);
} }
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(); Game.startNewRound();
}); });
}, },
@@ -63,10 +305,16 @@
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];
if (round.realMarker && round.guessMarker && round.line) { if (round.realMarker) {
round.realMarker.setMap(null); round.realMarker.setMap(null);
round.guessMarker.setMap(null); }
round.line.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();
}
} }
} }
@@ -76,16 +324,21 @@
var distanceInfo = document.getElementById('distanceInfo'); var distanceInfo = document.getElementById('distanceInfo');
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;
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;
document.getElementById('continueButton').style.display = null; document.getElementById('continueButton').style.display = null;
document.getElementById('showSummaryButton').style.display = null;
document.getElementById('startNewGameButton').style.display = null; document.getElementById('startNewGameButton').style.display = null;
document.getElementById('showGuessButton').style.visibility = null; document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null; document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result'); document.getElementById('guess').classList.remove('result');
// needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible';
Game.initialize(); Game.initialize();
}, },
@@ -96,39 +349,71 @@
var lastRound = Game.rounds[Game.rounds.length - 1]; var lastRound = Game.rounds[Game.rounds.length - 1];
lastRound.realMarker.setVisible(false); lastRound.realMarker.setVisible(false);
lastRound.guessMarker.setVisible(false); for (var i = 0; i < lastRound.guessMarkers.length; ++i) {
lastRound.line.setVisible(false); var guessMarker = lastRound.guessMarkers[i];
guessMarker.marker.setVisible(false);
guessMarker.line.setVisible(false);
if (guessMarker.info) {
guessMarker.info.close();
}
}
} }
document.getElementById('panoCover').style.visibility = 'hidden'; document.getElementById('panoCover').style.visibility = 'hidden';
document.getElementById('showGuessButton').style.visibility = null; document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null; document.getElementById('guess').style.visibility = null;
document.getElementById('guess').classList.remove('result') document.getElementById('guess').classList.remove('result');
// needs to be set visible after the show guess map hid it in mobile view
document.getElementById("navigation").style.visibility = 'visible';
Game.map.setOptions({ Game.map.setOptions({
draggableCursor: 'crosshair' draggableCursor: 'crosshair'
}); });
Game.map.fitBounds(mapBounds); Game.map.fitBounds(Game.mapBounds);
if (roomId) {
// if it is multiplayer mode, data is sent via WS
return;
}
Game.startNewRound(); Game.startNewRound();
}, },
startNewRound: function () { startNewRound: function () {
Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarker: null, line: null }); Game.rounds.push({ position: null, guessPosition: null, realMarker: null, guessMarkers: [] });
document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS); document.getElementById('currentRound').innerHTML = String(Game.rounds.length) + '/' + String(Game.NUMBER_OF_ROUNDS);
Game.loadPano(Game.panoId, Game.pov); Game.loadPano(Game.panoId, Game.pov);
// update the compass
const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
}, },
handleErrorResponse: function (error) { handleErrorResponse: function (error) {
// for the time being we only handle the "no_session_found" error and reset the game switch (error) {
case 'no_session_found':
MapGuesser.showModalWithContent('Error', 'Your session is invalid!', [{
type: 'button',
classNames: [],
text: 'Restart game',
onclick: function () {
window.location.reload();
}
}]);
break;
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () { case 'game_already_started':
mapBounds = this.response.bounds; MapGuesser.showModalWithContent('Error', 'This game is already started, you cannot join.');
break;
Game.reset(); default:
}); MapGuesser.showModalWithContent('Error', 'Error code: \'' + error + '\'');
break
}
}, },
loadPano: function (panoId, pov) { loadPano: function (panoId, pov) {
@@ -141,7 +426,106 @@
Game.panorama.setPano(panoId); Game.panorama.setPano(panoId);
}, },
evaluateGuess: function () { receiveResult: function (position, guessPosition, result, allResults) {
Game.scoreSum += result.score;
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
var resultBounds = new google.maps.LatLngBounds();
Game.rounds[Game.rounds.length - 1].position = position;
Game.addPositionToResultMap();
resultBounds.extend(position);
if (guessPosition) {
Game.addGuessPositionToResultMap(guessPosition);
resultBounds.extend(guessPosition);
}
if (allResults) {
for (var i = 0; i < allResults.length; ++i) {
var currentResult = allResults[i];
if (currentResult.guessPosition) {
Game.addGuessPositionToResultMap(currentResult.guessPosition, currentResult);
resultBounds.extend(currentResult.guessPosition);
}
}
}
Game.showResultMap(result, resultBounds);
},
showResultFromHistory: function (result) {
var round = Game.rounds[Game.rounds.length - 1];
var resultBounds = new google.maps.LatLngBounds();
round.realMarker.setVisible(true);
resultBounds.extend(round.position);
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setVisible(true);
guessMarker.line.setVisible(true);
resultBounds.extend(guessMarker.marker.getPosition());
}
Game.showResultMap(result, resultBounds);
},
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');
}
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');
if (result.distance === null) {
distanceInfo.children[0].style.display = 'none';
distanceInfo.children[1].style.display = 'block';
} else {
distanceInfo.children[0].style.display = 'block';
distanceInfo.children[1].style.display = 'none';
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(result.distance);
}
document.getElementById('score').innerHTML = result.score;
var scoreBarProperties = Game.calculateScoreBarProperties(result.score, Game.MAX_SCORE);
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) { if (!Game.guessMarker) {
return; return;
} }
@@ -150,63 +534,34 @@
Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition; Game.rounds[Game.rounds.length - 1].guessPosition = guessPosition;
document.getElementById('guessButton').disabled = true; document.getElementById('guessButton').disabled = true;
if (Game.adaptGuess) {
document.getElementById('guess').classList.remove('adapt');
}
document.getElementById('loading').style.visibility = 'visible';
document.getElementById('panoCover').style.visibility = 'visible'; document.getElementById('panoCover').style.visibility = 'visible';
var data = new FormData(); var data = new FormData();
data.append('lat', String(guessPosition.lat)); data.append('lat', String(guessPosition.lat));
data.append('lng', String(guessPosition.lng)); data.append('lng', String(guessPosition.lng));
MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () { document.getElementById('loading').style.visibility = 'visible';
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json';
MapGuesser.httpRequest('POST', url, function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) { if (this.response.error) {
Game.handleErrorResponse(this.response.error); Game.handleErrorResponse(this.response.error);
return; return;
} }
Game.guessMarker.setMap(null); Game.receiveResult(this.response.position, guessPosition, this.response.result, this.response.allResults);
Game.guessMarker = null;
document.getElementById('loading').style.visibility = 'hidden'; if (this.response.place) {
document.getElementById('guess').classList.add('result'); Game.panoId = this.response.place.panoId;
Game.pov = this.response.place.pov;
Game.scoreSum += this.response.result.score;
document.getElementById('currentScoreSum').innerHTML = String(Game.scoreSum) + '/' + String(Game.rounds.length * Game.MAX_SCORE);
Game.rounds[Game.rounds.length - 1].position = this.response.result.position;
Game.addRealGuessPair(this.response.result.position, guessPosition);
var resultBounds = new google.maps.LatLngBounds();
resultBounds.extend(this.response.result.position);
resultBounds.extend(guessPosition);
Game.map.setOptions({
draggableCursor: 'grab'
});
Game.map.fitBounds(resultBounds);
document.getElementById('distance').innerHTML = Util.printDistanceForHuman(this.response.result.distance);
document.getElementById('score').innerHTML = this.response.result.score;
var scoreBarProperties = Game.calculateScoreBarProperties(this.response.result.score, Game.MAX_SCORE);
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';
} }
Game.panoId = this.response.panoId;
Game.pov = this.response.pov;
}, data); }, data);
}, },
addRealGuessPair: function (position, guessPosition, hidden) { addPositionToResultMap: function (hidden) {
var round = Game.rounds[Game.rounds.length - 1]; var round = Game.rounds[Game.rounds.length - 1];
var position = round.position;
round.realMarker = new google.maps.Marker({ round.realMarker = new google.maps.Marker({
map: Game.map, map: Game.map,
@@ -227,16 +582,25 @@
round.realMarker.addListener('click', function () { round.realMarker.addListener('click', function () {
window.open('https://www.google.com/maps/search/?api=1&query=' + this.getPosition().toUrlValue(), '_blank'); window.open('https://www.google.com/maps/search/?api=1&query=' + this.getPosition().toUrlValue(), '_blank');
}); });
},
round.guessMarker = new google.maps.Marker({ addGuessPositionToResultMap: function (guessPosition, result, hidden) {
var round = Game.rounds[Game.rounds.length - 1];
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() : '?';
guessMarker.marker = new google.maps.Marker({
map: Game.map, map: Game.map,
visible: !hidden, visible: !hidden,
position: guessPosition, position: guessPosition,
zIndex: Game.rounds.length, zIndex: Game.rounds.length,
clickable: false, clickable: !!result,
draggable: false, draggable: false,
icon: { icon: {
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION, url: STATIC_ROOT + '/img/markers/' + markerSvg + '?rev=' + REVISION,
size: new google.maps.Size(24, 32), size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32), scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32), anchor: new google.maps.Point(12, 32),
@@ -247,11 +611,11 @@
fontFamily: 'Roboto', fontFamily: 'Roboto',
fontSize: '16px', fontSize: '16px',
fontWeight: '500', fontWeight: '500',
text: '?' text: markerLabel
} }
}); });
round.line = new google.maps.Polyline({ guessMarker.line = new google.maps.Polyline({
map: Game.map, map: Game.map,
visible: !hidden, visible: !hidden,
path: [ path: [
@@ -274,6 +638,19 @@
draggable: false, draggable: false,
editable: false editable: false
}); });
if (result) {
guessMarker.info = new google.maps.InfoWindow({
content: '<p class="small bold">' + result.userName + '</p>' +
'<p class="small">' + Util.printDistanceForHuman(result.distance) + ' | ' + result.score + ' points</p>',
});
guessMarker.marker.addListener('click', function () {
guessMarker.info.open(Game.map, this);
});
}
round.guessMarkers.push(guessMarker);
}, },
calculateScoreBarProperties: function (score, maxScore) { calculateScoreBarProperties: function (score, maxScore) {
@@ -294,12 +671,19 @@
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';
distanceInfo.children[1].style.display = 'block'; distanceInfo.children[1].style.display = 'none';
distanceInfo.children[2].style.display = 'block';
var scoreInfo = document.getElementById('scoreInfo'); var scoreInfo = document.getElementById('scoreInfo');
scoreInfo.children[0].style.display = 'none'; scoreInfo.children[0].style.display = 'none';
scoreInfo.children[1].style.display = 'block'; scoreInfo.children[1].style.display = 'block';
document.getElementById('showSummaryButton').style.display = null; document.getElementById('showSummaryButton').style.display = null;
document.getElementById('startNewGameButton').style.display = 'block';
if (!roomId || Game.multi.owner) {
document.getElementById('startNewGameButton').style.display = 'block';
if (!Game.readyToContinue) {
document.getElementById('startNewGameButton').disabled = true;
}
}
var resultBounds = new google.maps.LatLngBounds(); var resultBounds = new google.maps.LatLngBounds();
@@ -320,12 +704,17 @@
fontWeight: '500', fontWeight: '500',
text: String(i + 1) text: String(i + 1)
}); });
round.realMarker.setVisible(true);
round.guessMarker.setVisible(true);
round.line.setVisible(true);
round.realMarker.setVisible(true);
resultBounds.extend(round.position); resultBounds.extend(round.position);
resultBounds.extend(round.guessPosition);
for (var j = 0; j < round.guessMarkers.length; ++j) {
var guessMarker = round.guessMarkers[j];
guessMarker.marker.setVisible(true);
guessMarker.line.setVisible(true);
resultBounds.extend(guessMarker.marker.getPosition());
}
} }
Game.map.fitBounds(resultBounds); Game.map.fitBounds(resultBounds);
@@ -356,6 +745,52 @@
Game.googleLink.href = 'https://maps.google.com/maps'; Game.googleLink.href = 'https://maps.google.com/maps';
} }
}, 1); }, 1);
},
startCountdown: function (timeout) {
if (Game.countdownHandler) {
clearInterval(Game.countdownHandler);
}
Game.countdownElement = document.getElementById('countdown');
Game.countdownTimeElement = document.getElementById('countdownTime');
Game.setCountdownTime(Math.round(timeout / 1000));
if (timeout <= 0) {
return;
}
Game.timeoutEnd = new Date(new Date().getTime() + timeout);
Game.countdownHandler = setInterval(function () {
var timeLeft = Math.round((Game.timeoutEnd - new Date()) / 1000);
Game.setCountdownTime(timeLeft);
if (timeLeft <= 0) {
document.getElementById('panoCover').style.visibility = 'visible';
clearInterval(Game.countdownHandler);
}
}, 1000);
},
setCountdownTime: function (time) {
if (time <= 0) {
Game.countdownElement.style.visibility = 'hidden';
return;
}
if (time <= 15) {
Game.countdownElement.className = 'red';
} else if (time <= 30) {
Game.countdownElement.className = 'yellow';
} else {
Game.countdownElement.className = '';
}
Game.countdownElement.style.visibility = 'visible';
Game.countdownTimeElement.innerHTML = time;
} }
}; };
@@ -373,13 +808,7 @@
} }
}; };
MapGuesser.sessionAvailableHooks.reinitializeGame = function () { MapGuesser.sessionAvailableHooks.reinitializeGame = Game.prepare;
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
mapBounds = this.response.bounds;
Game.initialize();
});
};
if (!('ontouchstart' in document.documentElement)) { if (!('ontouchstart' in document.documentElement)) {
Game.adaptGuess = true; Game.adaptGuess = true;
@@ -392,7 +821,7 @@
}); });
Game.map.addListener('click', function (e) { Game.map.addListener('click', function (e) {
if (Game.rounds[Game.rounds.length - 1].guessPosition) { if (Game.rounds[Game.rounds.length - 1].guessPosition || Game.rounds[Game.rounds.length - 1].position) {
return; return;
} }
@@ -407,7 +836,7 @@
clickable: false, clickable: false,
draggable: true, draggable: true,
icon: { icon: {
url: STATIC_ROOT + '/img/markers/marker-gray-empty.svg?rev=' + REVISION, url: STATIC_ROOT + '/img/markers/marker-blue-empty.svg?rev=' + REVISION,
size: new google.maps.Size(24, 32), size: new google.maps.Size(24, 32),
scaledSize: new google.maps.Size(24, 32), scaledSize: new google.maps.Size(24, 32),
anchor: new google.maps.Point(12, 32), anchor: new google.maps.Point(12, 32),
@@ -438,26 +867,49 @@
Game.panorama.addListener('pov_changed', function () { Game.panorama.addListener('pov_changed', function () {
Game.rewriteGoogleLink(); Game.rewriteGoogleLink();
const heading = Game.panorama.getPov().heading;
document.getElementById("compass").style.transform = "translateY(-50%) rotate(" + heading + "deg)";
}); });
Game.initialize(); if (COOKIES_CONSENT) {
Game.prepare();
}
document.getElementById('showGuessButton').onclick = function () { document.getElementById('showGuessButton').onclick = function () {
this.style.visibility = 'hidden'; this.style.visibility = 'hidden';
document.getElementById('guess').style.visibility = 'visible'; document.getElementById('guess').style.visibility = 'visible';
document.getElementById('navigation').style.visibility = 'hidden';
} }
document.getElementById('closeGuessButton').onclick = function () { document.getElementById('closeGuessButton').onclick = function () {
document.getElementById('showGuessButton').style.visibility = null; document.getElementById('showGuessButton').style.visibility = null;
document.getElementById('guess').style.visibility = null; document.getElementById('guess').style.visibility = null;
document.getElementById('navigation').style.visibility = 'visible';
} }
document.getElementById('guessButton').onclick = function () { document.getElementById('guessButton').onclick = function () {
Game.evaluateGuess(); Game.guess();
} }
document.getElementById('continueButton').onclick = function () { document.getElementById('continueButton').onclick = function () {
Game.resetRound(); if (roomId) {
if (!Game.multi.owner || !Game.readyToContinue) {
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/nextRound.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
} else {
Game.resetRound();
}
} }
document.getElementById('showSummaryButton').onclick = function () { document.getElementById('showSummaryButton').onclick = function () {
@@ -465,6 +917,48 @@
} }
document.getElementById('startNewGameButton').onclick = function () { document.getElementById('startNewGameButton').onclick = function () {
Game.reset(); if (roomId) {
if (!Game.multi.owner) {
return;
}
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
} else {
Game.reset();
}
}
document.getElementById('startMultiGameButton').onclick = function () {
if (!roomId || !Game.multi.owner) {
return;
}
document.getElementById('multi').style.visibility = 'hidden';
document.getElementById('loading').style.visibility = 'visible';
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
document.getElementById('loading').style.visibility = 'hidden';
if (this.response.error) {
Game.handleErrorResponse(this.response.error);
return;
}
});
}
document.getElementById('returnToStart').onclick = function () {
Game.loadPano(Game.panoId, Game.pov);
}
document.getElementById('compassContainer').onclick = function () {
Game.panorama.setPov({ heading: 0, pitch: Game.panorama.getPov().pitch });
} }
})(); })();
+69 -10
View File
@@ -310,6 +310,21 @@
var placeId = addedPlaces[i].id; var placeId = addedPlaces[i].id;
places[tempId].id = placeId; places[tempId].id = placeId;
} }
},
// TODO: check whether marker is already existing on the map for the coordinates
// or alternatively block saving for matching coordinates
placeMarker: function (latlng) {
var marker = L.marker(latlng, {
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
} }
}; };
@@ -340,6 +355,32 @@
} else { } else {
return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 }; return { ppi: 72, tileSize: 512, zoomOffset: -1, minZoom: 2, maxZoom: 20 };
} }
},
extractCoordinates: function (coordinatesStr) {
var coordinates = { valid: false, latlng: { lat: 0., lng: 0. } };
var delimiters = [',', ' ', ';'];
coordinatesStr = coordinatesStr.trim();
if (coordinatesStr.length == 0) {
return coordinates;
}
for (var delimiter of delimiters) {
if (coordinatesStr.indexOf(delimiter) != -1) {
var coordinatesArr = coordinatesStr.split(delimiter);
coordinates.latlng.lat = parseFloat(coordinatesArr[0]);
coordinates.latlng.lng = parseFloat(coordinatesArr[1]);
if (!isNaN(coordinates.latlng.lat) && !isNaN(coordinates.latlng.lng)) {
coordinates.valid = true;
return coordinates;
}
}
}
return coordinates;
} }
}; };
@@ -348,16 +389,7 @@
}); });
MapEditor.map.on('click', function (e) { MapEditor.map.on('click', function (e) {
var marker = L.marker(e.latlng, { MapEditor.placeMarker(e.latlng);
icon: IconCollection.iconBlue,
zIndexOffset: 2000
})
.addTo(MapEditor.map)
.on('click', function () {
MapEditor.select(this);
});
MapEditor.select(marker);
}); });
var highResData = Util.getHighResData(); var highResData = Util.getHighResData();
@@ -440,4 +472,31 @@
document.getElementById('deleteButton').onclick = function () { document.getElementById('deleteButton').onclick = function () {
MapEditor.deletePlace(); MapEditor.deletePlace();
}; };
document.getElementById('jumpButton').onclick = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
if (coordinates.valid) {
MapEditor.placeMarker(coordinates.latlng);
}
};
document.getElementById('jumpCoordinates').onkeyup = function (e) {
var coordinatesStr = document.getElementById("jumpCoordinates").value;
var coordinates = Util.extractCoordinates(coordinatesStr);
var jumpButton = document.getElementById("jumpButton");
if (coordinates.valid) {
jumpButton.disabled = false;
if (e.key == 'Enter') {
MapEditor.placeMarker(coordinates.latlng);
}
}
else {
jumpButton.disabled = true;
}
};
})(); })();
+1
View File
@@ -1,4 +1,5 @@
var MapGuesser = { var MapGuesser = {
isSecure: window.location.protocol === 'https:',
cookiesAgreed: false, cookiesAgreed: false,
sessionAvailableHooks: {}, sessionAvailableHooks: {},
+36
View File
@@ -73,4 +73,40 @@
window.onresize = function () { window.onresize = function () {
Maps.calculateDescriptionDivHeights(); Maps.calculateDescriptionDivHeights();
}; };
document.getElementById('multiForm').onsubmit = function (e) {
e.preventDefault();
var roomId = this.elements.roomId.value;
if (roomId.length !== 6) {
return;
}
window.location.href = '/multiGame/' + this.elements.roomId.value;
};
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('closePlayModeButton').onclick = function () {
MapGuesser.hideModal();
};
document.getElementById('closeMultiButton').onclick = function () {
MapGuesser.hideModal();
};
var buttons = document.getElementById('mapContainer').getElementsByClassName('playButton');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
button.onclick = function () {
MapGuesser.showModal('playMode');
document.getElementById('singleButton').href = '/game/' + this.dataset.mapId;
document.getElementById('multiButton').dataset.mapId = this.dataset.mapId;
};
}
})(); })();
+17 -2
View File
@@ -59,10 +59,25 @@ def getRevisionForRef(ref):
return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip() return subprocess.check_output(["git", "rev-list", "-1", ref], cwd=REPO).decode().strip()
def getLatestReleaseTag(): def getLatestReleaseTag():
return subprocess.check_output(["git", "for-each-ref", "refs/tags/Release*", "--count=1", "--sort=-creatordate", "--format=%(refname:short)"], cwd=REPO).decode().strip() process = subprocess.Popen(["git", "for-each-ref", "refs/tags/Release*", "--sort=-creatordate", "--format=%(refname:short)"], stdout=subprocess.PIPE, cwd=REPO)
for line in process.stdout:
tag = line.decode().rstrip()
if isTagVerified(tag):
return tag
print(f"[WARNING] Tag '{tag}' is not verified, skipping.")
raise Exception("No verified 'Release*' tag found!")
def isTagVerified(tag):
process = subprocess.run(["git", "tag", "--verify", tag], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=REPO)
return process.returncode == 0
def updateRepoFromRemote(): def updateRepoFromRemote():
subprocess.call(["git", "fetch", "origin", "--prune"], cwd=REPO) subprocess.call(["git", "fetch", "origin", "--prune", "--prune-tags"], cwd=REPO)
def checkoutWorktree(worktreePath, ref): def checkoutWorktree(worktreePath, ref):
subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath) subprocess.call(["git", "checkout", "-f", ref], cwd=worktreePath)
+4 -1
View File
@@ -9,11 +9,14 @@ if [ -f ${ROOT_DIR}/installed ]; then
exit 1 exit 1
fi fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..." echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install) (cd ${ROOT_DIR}/public/static && yarn install)
echo "Installing MapGuesser DB..." echo "Installing MapGuesser DB..."
mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/db/mapguesser.sql mysql --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} ${DB_NAME} < ${ROOT_DIR}/database/mapguesser.sql
echo "Migrating DB..." echo "Migrating DB..."
(cd ${ROOT_DIR} && ./mapg db:migrate) (cd ${ROOT_DIR} && ./mapg db:migrate)
+3
View File
@@ -11,6 +11,9 @@ else
(cd ${ROOT_DIR} && composer install --dev) (cd ${ROOT_DIR} && composer install --dev)
fi fi
echo "Installing NPM packages..."
(cd ${ROOT_DIR}/multi && npm install)
echo "Installing Yarn packages..." echo "Installing Yarn packages..."
(cd ${ROOT_DIR}/public/static && yarn install) (cd ${ROOT_DIR}/public/static && yarn install)
+12
View File
@@ -5,6 +5,7 @@ use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select; use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet; use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\PersistentDataManager; use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\UserConfirmationRepository; use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserPasswordResetterRepository; use MapGuesser\Repository\UserPasswordResetterRepository;
use MapGuesser\Repository\UserRepository; use MapGuesser\Repository\UserRepository;
@@ -22,6 +23,8 @@ class MaintainDatabaseCommand extends Command
private UserPasswordResetterRepository $userPasswordResetterRepository; private UserPasswordResetterRepository $userPasswordResetterRepository;
private MultiRoomRepository $multiRoomRepository;
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@@ -30,6 +33,7 @@ class MaintainDatabaseCommand extends Command
$this->userRepository = new UserRepository(); $this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository(); $this->userConfirmationRepository = new UserConfirmationRepository();
$this->userPasswordResetterRepository = new UserPasswordResetterRepository(); $this->userPasswordResetterRepository = new UserPasswordResetterRepository();
$this->multiRoomRepository = new MultiRoomRepository();
} }
public function configure(): void public function configure(): void
@@ -43,6 +47,7 @@ class MaintainDatabaseCommand extends Command
try { try {
$this->deleteInactiveExpiredUsers(); $this->deleteInactiveExpiredUsers();
$this->deleteExpiredPasswordResetters(); $this->deleteExpiredPasswordResetters();
$this->deleteExpiredRooms();
$this->deleteExpiredSessions(); $this->deleteExpiredSessions();
} catch (\Exception $e) { } catch (\Exception $e) {
$output->writeln('<error>Maintenance failed!</error>'); $output->writeln('<error>Maintenance failed!</error>');
@@ -89,6 +94,13 @@ class MaintainDatabaseCommand extends Command
} }
} }
private function deleteExpiredRooms(): void
{
foreach ($this->multiRoomRepository->getAllExpired() as $multiRoom) {
$this->pdm->deleteFromDb($multiRoom);
}
}
private function deleteExpiredSessions(): void private function deleteExpiredSessions(): void
{ {
//TODO: model may be used for sessions too //TODO: model may be used for sessions too
+125 -7
View File
@@ -1,20 +1,37 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime;
use Faker\Factory;
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;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MapRepository; use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Response\Redirect;
class GameController class GameController
{ {
private IRequest $request; private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private MapRepository $mapRepository; private MapRepository $mapRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->mapRepository = new MapRepository(); $this->mapRepository = new MapRepository();
} }
@@ -22,30 +39,131 @@ class GameController
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
return new HtmlContent('game', $this->prepareGame($mapId)); return new HtmlContent('game', ['mapId' => $mapId]);
} }
public function getGameJson(): IContent public function getNewMultiGame(): IRedirect
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$roomId = bin2hex(random_bytes(3));
$token = $this->getMultiToken($roomId);
return new JsonContent($this->prepareGame($mapId)); $room = new MultiRoom();
$room->setRoomId($roomId);
$room->setStateArray([
'mapId' => $mapId,
'area' => $map->getArea(),
'rounds' => [],
'currentRound' => -1
]);
$room->setMembersArray(['owner' => $token, 'all' => []]);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('create_room', ['roomId' => $roomId]);
return new Redirect(
\Container::$routeCollection
->getRoute('multiGame')
->generateLink(['roomId' => $roomId]),
IRedirect::TEMPORARY
);
} }
private function prepareGame(int $mapId): array public function getMultiGame(): IContent
{ {
$map = $this->mapRepository->getById($mapId); $roomId = $this->request->query('roomId');
return new HtmlContent('game', ['roomId' => $roomId]);
}
public function prepareGame(): IContent
{
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
$session = $this->request->session(); $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [ $session->set('state', [
'mapId' => $mapId, 'mapId' => $mapId,
'area' => $map->getArea(), 'area' => $map->getArea(),
'rounds' => [] 'rounds' => [],
'currentRound' => -1
]); ]);
} else { // update the area of the map in the session in any case
$state['area'] = $map->getArea();
$session->set('state', $state);
} }
return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()]; return new JsonContent([
'mapId' => $mapId,
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
public function prepareMultiGame(): IContent
{
$roomId = $this->request->query('roomId');
$userName = $this->request->post('userName');
if (empty($userName)) {
$faker = Factory::create();
$userName = $faker->userName;
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$map = $this->mapRepository->getById($state['mapId']);
$token = $this->getMultiToken($roomId);
$members = $room->getMembersArray();
if (!in_array($token, $members['all'])) {
if ($state['currentRound'] >= 0) {
return new JsonContent(['error' => 'game_already_started']);
}
$members['all'][] = $token;
}
$room->setMembersArray($members);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
$this->multiConnector->sendMessage('join_room', [
'roomId' => $roomId,
'token' => $token,
'userName' => $userName
]);
return new JsonContent([
'roomId' => $roomId,
'token' => $token,
'owner' => $members['owner'] == $token,
'mapId' => $state['mapId'],
'mapName' => $map->getName(),
'bounds' => $map->getBounds()->toArray()
]);
}
private function getMultiToken(string $roomId): string
{
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
$token = bin2hex(random_bytes(16));
$session->set('multiState', [
'roomId' => $roomId,
'token' => $token
]);
} else {
$token = $multiState['token'];
}
return $token;
} }
} }
+175 -75
View File
@@ -1,10 +1,13 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use DateTime;
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;
use MapGuesser\Interfaces\Response\IContent; use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Place; use MapGuesser\Multi\MultiConnector;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MultiRoomRepository;
use MapGuesser\Repository\PlaceRepository; use MapGuesser\Repository\PlaceRepository;
class GameFlowController class GameFlowController
@@ -14,129 +17,226 @@ class GameFlowController
private IRequest $request; private IRequest $request;
private PersistentDataManager $pdm;
private MultiConnector $multiConnector;
private MultiRoomRepository $multiRoomRepository;
private PlaceRepository $placeRepository; private PlaceRepository $placeRepository;
public function __construct(IRequest $request) public function __construct(IRequest $request)
{ {
$this->request = $request; $this->request = $request;
$this->pdm = new PersistentDataManager();
$this->multiConnector = new MultiConnector();
$this->multiRoomRepository = new MultiRoomRepository();
$this->placeRepository = new PlaceRepository(); $this->placeRepository = new PlaceRepository();
} }
public function getNewPlace(): IContent public function initialData(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$session = $this->request->session(); $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
} }
if (count($state['rounds']) === 0) { if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
$placesWithoutPano = []; $this->startNewGame($state, $mapId);
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$session->set('state', $state); $session->set('state', $state);
}
$data = [ $response = [];
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()->toArray()
];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
$history = []; $last = $state['rounds'][$state['currentRound']];
for ($i = 0; $i < $rounds - 1; ++$i) { $response['place'] = [
$round = $state['rounds'][$i]; 'panoId' => $last['panoId'],
$history[] = [ 'pov' => $last['pov']->toArray()
'position' => $round['position']->toArray(), ];
$response['history'] = [];
for ($i = 0; $i < $state['currentRound']; ++$i) {
$round = $state['rounds'][$i];
$response['history'][] = [
'position' => $round['position']->toArray(),
'result' => [
'guessPosition' => $round['guessPosition']->toArray(), 'guessPosition' => $round['guessPosition']->toArray(),
'distance' => $round['distance'], 'distance' => $round['distance'],
'score' => $round['score'] 'score' => $round['score']
]; ]
}
$data = [
'history' => $history,
'panoId' => $last['panoId'],
'pov' => isset($last['pov']) ? // should be checked not to break with old sessions
$last['pov']->toArray() :
['heading' => 0.0, 'pitch' => 0.0, 'zoom' => 0.0],
]; ];
} }
return new JsonContent($data); return new JsonContent($response);
} }
public function evaluateGuess(): IContent public function multiInitialData(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
if ($state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS - 1) {
$this->startNewGame($state, $state['mapId']);
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
}
$places = [];
foreach ($state['rounds'] as $round) {
$places[] = [
'position' => $round['position']->toArray(),
'panoId' => $round['panoId'],
'pov' => $round['pov']->toArray()
];
}
$this->multiConnector->sendMessage('start_game', ['roomId' => $roomId, 'places' => $places]);
return new JsonContent(['ok' => true]);
}
public function guess(): IContent
{ {
$mapId = (int) $this->request->query('mapId'); $mapId = (int) $this->request->query('mapId');
$session = $this->request->session(); $session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) { if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
return new JsonContent(['error' => 'no_session_found']); return new JsonContent(['error' => 'no_session_found']);
} }
$last = $state['rounds'][count($state['rounds']) - 1]; $last = $state['rounds'][$state['currentRound']];
$position = $last['position'];
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng')); $guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
$distance = $this->calculateDistance($position, $guessPosition);
$score = $this->calculateScore($distance, $state['area']);
$last['guessPosition'] = $guessPosition; $last['guessPosition'] = $guessPosition;
$last['distance'] = $distance; $last['distance'] = $result['distance'];
$last['score'] = $score; $last['score'] = $result['score'];
$state['rounds'][count($state['rounds']) - 1] = $last;
if (count($state['rounds']) < static::NUMBER_OF_ROUNDS) { $response = [
$exclude = []; 'position' => $last['position']->toArray(),
'result' => $result
];
foreach ($state['rounds'] as $round) { $state['rounds'][$state['currentRound']] = $last;
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]); $state['currentRound'] += 1;
} if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$next = $state['rounds'][$state['currentRound']];
$placesWithoutPano = []; $response['place'] = [
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano); 'panoId' => $next['panoId'],
'pov' => $next['pov']->toArray()
$this->addNewRoundToState($state, $place, $placesWithoutPano); ];
$panoId = $place->getPanoIdCached();
$pov = $place->getPov()->toArray();
} else {
$state['rounds'] = [];
$panoId = null;
$pov = null;
} }
$session->set('state', $state); $session->set('state', $state);
return new JsonContent([ return new JsonContent($response);
'result' => [
'position' => $position->toArray(),
'distance' => $distance,
'score' => $score
],
'panoId' => $panoId,
'pov' => $pov
]);
} }
private function addNewRoundToState(array &$state, Place $place, array $placesWithoutPano): void public function multiGuess(): IContent
{ {
$state['rounds'][] = [ $roomId = $this->request->query('roomId');
'placesWithoutPano' => $placesWithoutPano, $session = $this->request->session();
'placeId' => $place->getId(),
'position' => $place->getPosition(), if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
'panoId' => $place->getPanoIdCached(), return new JsonContent(['error' => 'no_session_found']);
'pov' => $place->getPov() }
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$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']);
$responseFromMulti = $this->multiConnector->sendMessage('guess', [
'roomId' => $roomId,
'token' => $multiState['token'],
'guessPosition' => $guessPosition->toArray(),
'distance' => $result['distance'],
'score' => $result['score']
]);
if (isset($responseFromMulti['error'])) {
return new JsonContent(['error' => $responseFromMulti['error']]);
}
$response = [
'position' => $last['position']->toArray(),
'result' => $result,
'allResults' => $responseFromMulti['allResults']
]; ];
return new JsonContent($response);
}
public function multiNextRound(): IContent
{
$roomId = $this->request->query('roomId');
$session = $this->request->session();
if (!($multiState = $session->get('multiState')) || $multiState['roomId'] !== $roomId) {
return new JsonContent(['error' => 'no_session_found']);
}
$room = $this->multiRoomRepository->getByRoomId($roomId);
$state = $room->getStateArray();
$members = $room->getMembersArray();
if ($members['owner'] !== $multiState['token']) {
return new JsonContent(['error' => 'not_owner_of_room']);
}
$state['currentRound'] += 1;
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
$this->multiConnector->sendMessage('next_round', ['roomId' => $roomId, 'currentRound' => $state['currentRound']]);
}
$room->setStateArray($state);
$room->setUpdatedDate(new DateTime());
$this->pdm->saveToDb($room);
return new JsonContent(['ok' => true]);
}
private function evalueteGuess(Position $realPosition, Position $guessPosition, float $area)
{
$distance = $this->calculateDistance($realPosition, $guessPosition);
$score = $this->calculateScore($distance, $area);
return ['distance' => $distance, 'score' => $score];
}
private function startNewGame(array &$state, int $mapId): void
{
$places = $this->placeRepository->getRandomNForMapWithValidPano($mapId, static::NUMBER_OF_ROUNDS);
$state['rounds'] = [];
$state['currentRound'] = 0;
foreach ($places as $place) {
$state['rounds'][] = [
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached(),
'pov' => $place->getPov()
];
}
} }
private function calculateDistance(Position $realPosition, Position $guessPosition): float private function calculateDistance(Position $realPosition, Position $guessPosition): float
+22
View File
@@ -0,0 +1,22 @@
<?php namespace MapGuesser\Multi;
class MultiConnector
{
public function sendMessage(string $func, array $args = [])
{
$message = json_encode([
'func' => $func,
'args' => $args
]);
$connection = fsockopen($_ENV['MULTI_INTERNAL_HOST'], $_ENV['MULTI_INTERNAL_PORT']);
fwrite($connection, $message);
$response = '';
while (!feof($connection)) {
$response .= fgets($connection);
}
fclose($connection);
return json_decode($response, true);
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateTime;
class MultiRoom extends Model
{
protected static string $table = 'multi_rooms';
protected static array $fields = ['room_id', 'state', 'members', 'updated'];
private string $roomId = '';
private array $state = [];
private array $members = [];
private DateTime $updated;
public function setRoomId(string $roomId): void
{
$this->roomId = $roomId;
}
public function setStateArray(array $state): void
{
$this->state = $state;
}
public function setMembersArray(array $members): void
{
$this->members = $members;
}
public function setState(string $state): void
{
$this->state = unserialize($state);
}
public function setMembers(string $members): void
{
$this->members = unserialize($members);
}
public function setUpdatedDate(DateTime $updated): void
{
$this->updated = $updated;
}
public function setUpdated(string $updated): void
{
$this->updated = new DateTime($updated);
}
public function getRoomId(): string
{
return $this->roomId;
}
public function getStateArray(): array
{
return $this->state;
}
public function getState(): string
{
return serialize($this->state);
}
public function getMembersArray(): array
{
return $this->members;
}
public function getMembers(): string
{
return serialize($this->members);
}
public function getUpdatedDate(): DateTime
{
return $this->updated;
}
public function getUpdated(): string
{
return $this->updated->format('Y-m-d H:i:s');
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php namespace MapGuesser\Repository;
use DateTime;
use Generator;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\MultiRoom;
use MapGuesser\PersistentData\PersistentDataManager;
class MultiRoomRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $id): ?MultiRoom
{
return $this->pdm->selectFromDbById($id, MultiRoom::class);
}
public function getByRoomId(string $roomId): ?MultiRoom
{
$select = new Select(\Container::$dbConnection);
$select->where('room_id', '=', $roomId);
return $this->pdm->selectFromDb($select, MultiRoom::class);
}
public function getAllExpired(): Generator
{
$select = new Select(\Container::$dbConnection);
$select->where('updated', '<', (new DateTime('-7 day'))->format('Y-m-d H:i:s'));
yield from $this->pdm->selectMultipleFromDb($select, MultiRoom::class);
}
}
+16 -4
View File
@@ -29,17 +29,29 @@ class PlaceRepository
} }
//TODO: use Map instead of id //TODO: use Map instead of id
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place public function getRandomNForMapWithValidPano(int $mapId, int $n, array $exclude = []): array
{ {
$placesWithoutPano = []; $places = [];
for ($i = 1; $i <= $n; ++$i) {
$place = $this->getRandomForMapWithValidPano($mapId, $exclude);
$places[] = $place;
$exclude[] = $place->getId();
}
return $places;
}
//TODO: use Map instead of id
public function getRandomForMapWithValidPano(int $mapId, array $exclude = []): Place
{
do { do {
$place = $this->selectRandomFromDbForMap($mapId, $exclude); $place = $this->selectRandomFromDbForMap($mapId, $exclude);
$panoId = $place->getFreshPanoId(); $panoId = $place->getFreshPanoId();
if ($panoId === null) { if ($panoId === null) {
$placesWithoutPano[] = $exclude[] = $place->getId(); $exclude[] = $place->getId();
} }
} while ($panoId === null); } while ($panoId === null);
+4
View File
@@ -12,6 +12,10 @@
@section(subheader) @section(subheader)
<span><a href="javascript:;" id="mapName" title="Edit map data"><?= $mapName ?></a></span><!-- <span><a href="javascript:;" id="mapName" title="Edit map data"><?= $mapName ?></a></span><!--
--><span>
<input type="text" id="jumpCoordinates" placeholder="Insert coordinates here" />
<button id="jumpButton" disabled >Jump</button>
</span><!--
--><span><!-- --><span><!--
<?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?> <?php /* Copyright (c) 2019 The Bootstrap Authors. License can be found in 'USED_SOFTWARE' in section 'Bootstrap Icons'. */ ?>
--><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> --><svg class="inline" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
+37 -5
View File
@@ -5,13 +5,25 @@
@extends(templates/layout_full) @extends(templates/layout_full)
@section(pagemodal)
<div id="multi" class="modal">
<h2>Multiplayer (beta)</h2>
<p class="marginTop">Waiting for players...</p>
<div id="players" class="marginTop"></div>
<button id="startMultiGameButton" class="button fullWidth marginTop green">Start game</button>
</div>
@endsection
@section(subheader) @section(subheader)
<span id="mapName" class="bold"><?= $mapName ?></span><!-- <span id="mapName" class="bold">Loading map...</span><!--
--><span>Round <span id="currentRound" class="bold"></span></span><!-- --><span>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
@section(main) @section(main)
<div id="countdown">
<p id="countdownTime" class="mono bold"></p>
</div>
<div id="panoCover"></div> <div id="panoCover"></div>
<div id="panorama"></div> <div id="panorama"></div>
<div id="showGuessButtonContainer"> <div id="showGuessButtonContainer">
@@ -28,6 +40,7 @@
<div id="resultInfo"> <div id="resultInfo">
<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 class="bold">Game finished.</p> <p class="bold">Game finished.</p>
</div> </div>
<div id="scoreInfo"> <div id="scoreInfo">
@@ -46,11 +59,30 @@
<button id="startNewGameButton" class="fullWidth">Play this map again</button> <button id="startNewGameButton" class="fullWidth">Play this map again</button>
</div> </div>
</div> </div>
<div id="navigation">
<div id="compassContainer" class="navigationItem">
<div>
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/circle_background.svg?rev=<?= REVISION ?>" alt="Circle Background" class="circleBackground" />
</div>
<div>
<img id="compass" src="<?= $_ENV['STATIC_ROOT'] ?>/img/compass.svg?rev=<?= REVISION ?>" alt="compass icon" class="navigationIcon" />
</div>
</div>
<div id="returnToStart" class="navigationItem">
<div>
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/circle_background.svg?rev=<?= REVISION ?>" alt="Circle Background" class="circleBackground" />
</div>
<div>
<img src="<?= $_ENV['STATIC_ROOT'] ?>/img/house-fill.svg?rev=<?= REVISION ?>" alt="Return to Start icon" class="navigationIcon" />
</div>
</div>
</div>
@endsection @endsection
@section(pageScript) @section(pageScript)
<script> <script>
var mapId = <?= $mapId ?>; var multiUrl = '<?= $_ENV['MULTI_WS_URL'] ?>';
var mapBounds = <?= json_encode($bounds) ?>; var roomId = <?= isset($roomId) ? '\'' . $roomId . '\'' : 'null' ?>;
var mapId = <?= isset($mapId) ? '\'' . $mapId . '\'' : 'null' ?>;
</script> </script>
@endsection @endsection
+33 -7
View File
@@ -5,6 +5,32 @@ TODO: condition!
@extends(templates/layout_normal) @extends(templates/layout_normal)
@section(pagemodal)
<div id="playMode" class="modal">
<h2>Play map</h2>
<a id="singleButton" class="button fullWidth marginTop" href="" title="Single player">Single player</a>
<p class="bold center marginTop marginBottom">OR</p>
<button id="multiButton" class="fullWidth green" data-map-id="">Multiplayer (beta)</button>
<div class="right">
<button id="closePlayModeButton" class="gray marginTop" type="button">Close</button>
</div>
</div>
<div id="multi" class="modal">
<h2>Multiplayer (beta)</h2>
<form id="multiForm" class="marginTop" data-no-submit="true">
<a id="createNewRoomButton" class="button fullWidth green" href="" title="Create new room">Create new room</a>
<p class="bold center marginTop marginBottom">OR</p>
<div class="inputWithButton">
<input type="text" name="roomId" placeholder="Room to connect" required minlength="6" maxlength="6"><!--
--><button id="authenticateWithGoogleButton" type="submit">Connect</button>
</div>
<div class="right">
<button id="closeMultiButton" class="gray marginTop" type="button">Close</button>
</div>
</form>
</div>
@endsection
@section(main) @section(main)
<div id="mapContainer"> <div id="mapContainer">
<?php foreach ($maps as $map): ?> <?php foreach ($maps as $map): ?>
@@ -35,15 +61,15 @@ TODO: condition!
<p class="small center"><?= $map['description'] ?></p> <p class="small center"><?= $map['description'] ?></p>
</div> </div>
</div> </div>
<?php if ($isAdmin): ?> <div class="buttonContainer">
<div class="buttonContainer"> <?php if ($isAdmin): ?>
<a class="button fullWidth noRightRadius" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</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>
<a class="button yellow fullWidth noLeftRadius noRightRadius" href="/admin/mapEditor/<?= $map['id']; ?>" title="Edit map '<?= $map['name'] ?>'">Edit</a> <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> <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>
</div> <?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 class="button fullWidth" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a> <?php endif; ?>
<?php endif; ?> </div>
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if ($isAdmin): ?> <?php if ($isAdmin): ?>
+1 -1
View File
@@ -29,6 +29,6 @@
</main> </main>
<footer> <footer>
<p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!-- <p><span class="bold"><?= $_ENV['APP_NAME'] ?></span> <?= str_replace('Release_', '', VERSION) ?></p><!--
--><p>&copy; Pőcze Bence <?= (new DateTime(REVISION_DATE))->format('Y') ?></p> --><p>&copy; The MapGuesser Contributors <?= (new DateTime(REVISION_DATE))->format('Y') ?></p>
</footer> </footer>
@endsection @endsection
+11 -3
View File
@@ -50,9 +50,17 @@ Container::$routeCollection->group('account', function (MapGuesser\Routing\Route
//Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']); //Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']); $routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']); $routeCollection->post('game.prepare-json', '{mapId}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareGame']);
$routeCollection->get('newPlace-json', '{mapId}/newPlace.json', [MapGuesser\Controller\GameFlowController::class, 'getNewPlace']); $routeCollection->post('game.initialData-json', '{mapId}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'initialData']);
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'evaluateGuess']); $routeCollection->post('game.guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'guess']);
});
Container::$routeCollection->group('multiGame', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('multiGame.new', 'new/{mapId}', [MapGuesser\Controller\GameController::class, 'getNewMultiGame']);
$routeCollection->get('multiGame', '{roomId}', [MapGuesser\Controller\GameController::class, 'getMultiGame']);
$routeCollection->post('multiGame.prepare-json', '{roomId}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareMultiGame']);
$routeCollection->post('multiGame.initialData-json', '{roomId}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'multiInitialData']);
$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('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) { Container::$routeCollection->group('admin', function (MapGuesser\Routing\RouteCollection $routeCollection) {
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']); $routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);