Merge pull request 'feature/MAPG-203-initial-multiplayer-implementation' (#8) from feature/MAPG-203-initial-multiplayer-implementation into develop
All checks were successful
default-pipeline default-pipeline #37
All checks were successful
default-pipeline default-pipeline #37
Reviewed-on: https://gitea.e5tv.hu/esoko/mapguesser/pulls/8
This commit is contained in:
commit
a417fbd760
@ -15,3 +15,7 @@ MAIL_PORT=2500
|
||||
GOOGLE_OAUTH_CLIENT_ID=your_google_oauth_client_id
|
||||
GOOGLE_OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
|
||||
GOOGLE_ANALITICS_ID=your_google_analytics_id
|
||||
MULTI_INTERNAL_HOST=multi
|
||||
MULTI_INTERNAL_PORT=5000
|
||||
MULTI_WS_HOST=mapguesser-dev.ch
|
||||
MULTI_WS_PORT=8090
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.env
|
||||
installed
|
||||
vendor
|
||||
node_modules
|
||||
|
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@ -9,6 +9,14 @@
|
||||
"pathMappings": {
|
||||
"/var/www/mapguesser": "${workspaceRoot}",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Listen for NodeJS Inspector in Docker",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 9229,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/var/www/mapguesser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -6,7 +6,8 @@
|
||||
"require": {
|
||||
"vlucas/phpdotenv": "^4.1",
|
||||
"symfony/console": "^5.1",
|
||||
"phpmailer/phpmailer": "^6.1"
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"fzaninotto/faker": "^1.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9",
|
||||
|
59
composer.lock
generated
59
composer.lock
generated
@ -4,8 +4,63 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "34563bfc619f47473b2e37a5639dd63e",
|
||||
"content-hash": "b71c0ffc0761a6b90f6242346b735a09",
|
||||
"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",
|
||||
"version": "v6.1.6",
|
||||
@ -2710,5 +2765,5 @@
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"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;
|
@ -1,14 +1,23 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
build: ./docker
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-app
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
links:
|
||||
- 'mariadb'
|
||||
- 'mail'
|
||||
multi:
|
||||
build:
|
||||
context: ./docker
|
||||
dockerfile: Dockerfile-multi
|
||||
ports:
|
||||
- 5000:5000
|
||||
- 8090:8090
|
||||
- 9229:9229
|
||||
volumes:
|
||||
- .:/var/www/mapguesser
|
||||
mariadb:
|
||||
image: mariadb:10.3
|
||||
ports:
|
||||
|
16
docker/Dockerfile-multi
Normal file
16
docker/Dockerfile-multi
Normal 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
|
292
multi/index.js
Normal file
292
multi/index.js
Normal file
@ -0,0 +1,292 @@
|
||||
'use strict';
|
||||
|
||||
process.title = 'mapguesser-multi';
|
||||
|
||||
class State {
|
||||
static OPEN = 1;
|
||||
static PLACE_RECEIVED = 2;
|
||||
static GUESS_SENT = 3;
|
||||
}
|
||||
|
||||
class MultiGame {
|
||||
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);
|
||||
}
|
||||
|
||||
createRoom(roomId) {
|
||||
this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() });
|
||||
}
|
||||
|
||||
joinRoom(roomId, token, userName) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
console.error('Room does not exist!')
|
||||
return;
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
if (room.members.has(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = { userName: userName };
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'member_joined', data);
|
||||
});
|
||||
|
||||
room.members.set(token, { userName: userName, state: State.OPEN, connection: null });
|
||||
}
|
||||
|
||||
startGame(roomId, places) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
//TODO: send something back
|
||||
console.log('Room does not exist!')
|
||||
return;
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
var rounds = [];
|
||||
places.forEach(function (place) {
|
||||
rounds.push({ place: place, results: new Map() })
|
||||
});
|
||||
|
||||
room.rounds = rounds;
|
||||
|
||||
this.nextRound(roomId, 0);
|
||||
}
|
||||
|
||||
guess(roomId, token, guessPosition, distance, score) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
//TODO: send something back
|
||||
console.log('Room does not exist!')
|
||||
return;
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
var member = this.rooms.get(roomId).members.get(token);
|
||||
|
||||
this._sendResultsUntilNow(room, member);
|
||||
|
||||
round.results.set(member.userName, { guessPosition: guessPosition, distance: distance, score: score });
|
||||
member.state = State.GUESS_SENT;
|
||||
|
||||
this._broadcastGuess(room, member.userName, guessPosition, distance, score);
|
||||
}
|
||||
|
||||
nextRound(roomId, currentRound) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
//TODO: send something back
|
||||
console.log('Room does not exist!')
|
||||
return;
|
||||
}
|
||||
|
||||
var room = this.rooms.get(roomId);
|
||||
room.updated = new Date();
|
||||
|
||||
room.currentRound = currentRound;
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
|
||||
var data = {};
|
||||
data.place = { panoId: round.place.panoId, pov: round.place.pov };
|
||||
|
||||
if (room.currentRound > 0) {
|
||||
data.result = { position: room.rounds[room.currentRound - 1].place.position };
|
||||
}
|
||||
|
||||
var self = this;
|
||||
room.members.forEach(function (member) {
|
||||
self._sendToMember(member, 'new_round', data);
|
||||
|
||||
member.state = State.PLACE_RECEIVED;
|
||||
});
|
||||
}
|
||||
|
||||
_sendInitialData(room, member) {
|
||||
var data = {};
|
||||
|
||||
if (room.currentRound >= 0) {
|
||||
data.place = room.rounds[room.currentRound].place;
|
||||
}
|
||||
|
||||
data.history = [];
|
||||
for (var i = 0; i < room.currentRound; ++i) {
|
||||
var round = room.rounds[i];
|
||||
var result;
|
||||
if (round.results.has(member.userName)) {
|
||||
result = round.results.get(member.userName);
|
||||
} else {
|
||||
result = { guessPosition: null, distance: null, score: 0 };
|
||||
}
|
||||
|
||||
data.history.push({
|
||||
position: round.place.position,
|
||||
guessPosition: result.guessPosition,
|
||||
distance: result.distance,
|
||||
score: result.score
|
||||
});
|
||||
}
|
||||
|
||||
data.members = [];
|
||||
room.members.forEach(function (currentMember) {
|
||||
data.members.push({ userName: currentMember.userName, me: member === currentMember });
|
||||
});
|
||||
|
||||
this._sendToMember(member, 'initialize', data);
|
||||
}
|
||||
|
||||
_sendResultsUntilNow(room, member) {
|
||||
if (member.state !== State.GUESS_SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
var round = room.rounds[room.currentRound];
|
||||
|
||||
var results = [];
|
||||
round.results.forEach(function (result, userName) {
|
||||
results.push({ userName: userName, guessPosition: result.guessPosition, distance: result.distance, score: result.score });
|
||||
});
|
||||
|
||||
this._sendToMember(member, 'results', results);
|
||||
}
|
||||
|
||||
_broadcastGuess(room, userName, guessPosition, distance, score) {
|
||||
var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score };
|
||||
|
||||
room.members.forEach(function (member) {
|
||||
if (!member.state !== State.GUESS_SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._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;
|
||||
}
|
||||
|
||||
switch (data.func) {
|
||||
case 'create_room':
|
||||
multiGame.createRoom(data.args.roomId);
|
||||
|
||||
break;
|
||||
|
||||
case 'join_room':
|
||||
multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName);
|
||||
|
||||
break;
|
||||
|
||||
case 'start_game':
|
||||
multiGame.startGame(data.args.roomId, data.args.places);
|
||||
|
||||
break
|
||||
|
||||
case 'guess':
|
||||
multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score);
|
||||
|
||||
break;
|
||||
|
||||
case 'next_round':
|
||||
multiGame.nextRound(data.args.roomId, data.args.currentRound);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
socket.write('OK');
|
||||
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
multi/package-lock.json
generated
Normal file
17
multi/package-lock.json
generated
Normal 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
multi/package.json
Normal file
13
multi/package.json
Normal 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 <bence@pocze.ch>",
|
||||
"license": "GNU AGPL 3.0"
|
||||
}
|
@ -102,6 +102,15 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#startMultiGameButton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#players > p {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
#mapName {
|
||||
display: none;
|
||||
|
@ -5,6 +5,8 @@
|
||||
NUMBER_OF_ROUNDS: 5,
|
||||
MAX_SCORE: 1000,
|
||||
|
||||
mapBounds: null,
|
||||
multi: { token: null, owner: false },
|
||||
rounds: [],
|
||||
scoreSum: 0,
|
||||
panoId: null,
|
||||
@ -15,8 +17,190 @@
|
||||
adaptGuess: false,
|
||||
googleLink: null,
|
||||
|
||||
initialize: function () {
|
||||
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 'results':
|
||||
//TODO
|
||||
break;
|
||||
|
||||
case 'guess':
|
||||
//TODO
|
||||
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) {
|
||||
if (data.history) {
|
||||
for (var i = 0; i < data.history.length; ++i) {
|
||||
var round = data.history[i];
|
||||
Game.rounds.push({ position: round.position, guessPosition: round.guessPosition, realMarker: null, guessMarker: null, line: null });
|
||||
Game.addRealGuessPair(round.position, round.guessPosition, true);
|
||||
Game.scoreSum += round.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);
|
||||
}
|
||||
|
||||
if (data.place) {
|
||||
Game.panoId = data.place.panoId;
|
||||
Game.pov = data.place.pov;
|
||||
|
||||
document.getElementById('panoCover').style.visibility = 'hidden';
|
||||
MapGuesser.hideModal();
|
||||
|
||||
Game.startNewRound();
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
memberJoined: function (data) {
|
||||
var div = document.getElementById('players');
|
||||
|
||||
var p = document.createElement('p');
|
||||
p.innerHTML = data.userName;
|
||||
div.appendChild(p);
|
||||
},
|
||||
|
||||
newRound: function (data) {
|
||||
//TODO: workaround until results are not sent
|
||||
if (Game.adaptGuess) {
|
||||
document.getElementById('guess').classList.remove('adapt');
|
||||
}
|
||||
|
||||
if (Game.rounds.length === Game.NUMBER_OF_ROUNDS) {
|
||||
Game.reset();
|
||||
}
|
||||
|
||||
// if player didn't guess - TODO: show everything on a map
|
||||
if (data.result && Game.rounds.length > 0 && !Game.rounds[Game.rounds.length - 1].position) {
|
||||
Game.rounds[Game.rounds.length - 1].position = data.result.position;
|
||||
Game.addRealGuessPair(data.result.position, null);
|
||||
}
|
||||
|
||||
Game.panoId = data.place.panoId;
|
||||
Game.pov = data.place.pov;
|
||||
|
||||
MapGuesser.hideModal();
|
||||
Game.resetRound();
|
||||
Game.startNewRound();
|
||||
}
|
||||
},
|
||||
|
||||
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';
|
||||
|
||||
var url = roomId ? '/multiGame/' + roomId + '/prepare.json' : '/game/' + mapId + '/prepare.json';
|
||||
MapGuesser.httpRequest('POST', url, function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
|
||||
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;
|
||||
|
||||
MapGuesser.showModal('multi');
|
||||
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('currentRound').innerHTML = '1/' + String(Game.NUMBER_OF_ROUNDS);
|
||||
document.getElementById('currentScoreSum').innerHTML = '0/0';
|
||||
@ -24,9 +208,16 @@
|
||||
Game.map.setOptions({
|
||||
draggableCursor: 'crosshair'
|
||||
});
|
||||
Game.map.fitBounds(mapBounds);
|
||||
Game.map.fitBounds(Game.mapBounds);
|
||||
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/initialData.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('panoCover').style.visibility = 'hidden';
|
||||
|
||||
@ -63,8 +254,10 @@
|
||||
for (var i = 0; i < Game.rounds.length; ++i) {
|
||||
var round = Game.rounds[i];
|
||||
|
||||
if (round.realMarker && round.guessMarker && round.line) {
|
||||
if (round.realMarker) {
|
||||
round.realMarker.setMap(null);
|
||||
}
|
||||
if (round.guessMarker) {
|
||||
round.guessMarker.setMap(null);
|
||||
round.line.setMap(null);
|
||||
}
|
||||
@ -96,8 +289,10 @@
|
||||
var lastRound = Game.rounds[Game.rounds.length - 1];
|
||||
|
||||
lastRound.realMarker.setVisible(false);
|
||||
lastRound.guessMarker.setVisible(false);
|
||||
lastRound.line.setVisible(false);
|
||||
if (lastRound.guessMarker) {
|
||||
lastRound.guessMarker.setVisible(false);
|
||||
lastRound.line.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('panoCover').style.visibility = 'hidden';
|
||||
@ -108,7 +303,12 @@
|
||||
Game.map.setOptions({
|
||||
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();
|
||||
},
|
||||
@ -124,8 +324,14 @@
|
||||
handleErrorResponse: function (error) {
|
||||
// for the time being we only handle the "no_session_found" error and reset the game
|
||||
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
|
||||
mapBounds = this.response.bounds;
|
||||
if (roomId) {
|
||||
//TODO: better error message
|
||||
alert('Your session is invalid, please start multiplayer again!')
|
||||
return;
|
||||
}
|
||||
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/prepare.json', function () {
|
||||
Game.mapBounds = this.response.bounds;
|
||||
|
||||
Game.reset();
|
||||
});
|
||||
@ -160,7 +366,8 @@
|
||||
data.append('lat', String(guessPosition.lat));
|
||||
data.append('lng', String(guessPosition.lng));
|
||||
|
||||
MapGuesser.httpRequest('POST', '/game/' + mapId + '/guess.json', function () {
|
||||
var url = roomId ? '/multiGame/' + roomId + '/guess.json' : '/game/' + mapId + '/guess.json';
|
||||
MapGuesser.httpRequest('POST', url, function () {
|
||||
if (this.response.error) {
|
||||
Game.handleErrorResponse(this.response.error);
|
||||
return;
|
||||
@ -204,6 +411,10 @@
|
||||
Game.panoId = this.response.place.panoId;
|
||||
Game.pov = this.response.place.pov;
|
||||
} else {
|
||||
if (!Game.multi.owner) {
|
||||
//TODO: "waiting for" disabled button
|
||||
document.getElementById('continueButton').style.display = 'none';
|
||||
}
|
||||
Game.panoId = null;
|
||||
Game.pov = null;
|
||||
}
|
||||
@ -233,6 +444,10 @@
|
||||
window.open('https://www.google.com/maps/search/?api=1&query=' + this.getPosition().toUrlValue(), '_blank');
|
||||
});
|
||||
|
||||
if (!guessPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
round.guessMarker = new google.maps.Marker({
|
||||
map: Game.map,
|
||||
visible: !hidden,
|
||||
@ -304,7 +519,10 @@
|
||||
scoreInfo.children[0].style.display = 'none';
|
||||
scoreInfo.children[1].style.display = 'block';
|
||||
document.getElementById('showSummaryButton').style.display = null;
|
||||
document.getElementById('startNewGameButton').style.display = 'block';
|
||||
|
||||
if (Game.multi.owner) {
|
||||
document.getElementById('startNewGameButton').style.display = 'block';
|
||||
}
|
||||
|
||||
var resultBounds = new google.maps.LatLngBounds();
|
||||
|
||||
@ -325,12 +543,17 @@
|
||||
fontWeight: '500',
|
||||
text: String(i + 1)
|
||||
});
|
||||
|
||||
round.realMarker.setVisible(true);
|
||||
round.guessMarker.setVisible(true);
|
||||
round.line.setVisible(true);
|
||||
if (round.guessMarker) {
|
||||
round.guessMarker.setVisible(true);
|
||||
round.line.setVisible(true);
|
||||
}
|
||||
|
||||
resultBounds.extend(round.position);
|
||||
resultBounds.extend(round.guessPosition);
|
||||
if (round.guessMarker) {
|
||||
resultBounds.extend(round.guessPosition);
|
||||
}
|
||||
}
|
||||
|
||||
Game.map.fitBounds(resultBounds);
|
||||
@ -378,13 +601,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
MapGuesser.sessionAvailableHooks.reinitializeGame = function () {
|
||||
MapGuesser.httpRequest('GET', '/game/' + mapId + '/json', function () {
|
||||
mapBounds = this.response.bounds;
|
||||
|
||||
Game.initialize();
|
||||
});
|
||||
};
|
||||
MapGuesser.sessionAvailableHooks.reinitializeGame = Game.prepare;
|
||||
|
||||
if (!('ontouchstart' in document.documentElement)) {
|
||||
Game.adaptGuess = true;
|
||||
@ -445,7 +662,9 @@
|
||||
Game.rewriteGoogleLink();
|
||||
});
|
||||
|
||||
Game.initialize();
|
||||
if (COOKIES_CONSENT) {
|
||||
Game.prepare();
|
||||
}
|
||||
|
||||
document.getElementById('showGuessButton').onclick = function () {
|
||||
this.style.visibility = 'hidden';
|
||||
@ -462,7 +681,19 @@
|
||||
}
|
||||
|
||||
document.getElementById('continueButton').onclick = function () {
|
||||
Game.resetRound();
|
||||
if (roomId) {
|
||||
if (!Game.multi.owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/nextRound.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
});
|
||||
} else {
|
||||
Game.resetRound();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('showSummaryButton').onclick = function () {
|
||||
@ -470,6 +701,31 @@
|
||||
}
|
||||
|
||||
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';
|
||||
});
|
||||
} else {
|
||||
Game.reset();
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('startMultiGameButton').onclick = function () {
|
||||
if (!roomId || !Game.multi.owner) {
|
||||
return;
|
||||
}
|
||||
|
||||
MapGuesser.hideModal();
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
|
||||
MapGuesser.httpRequest('POST', '/multiGame/' + roomId + '/initialData.json', function () {
|
||||
document.getElementById('loading').style.visibility = 'hidden';
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
@ -1,4 +1,5 @@
|
||||
var MapGuesser = {
|
||||
isSecure: window.location.protocol === 'https:',
|
||||
cookiesAgreed: false,
|
||||
sessionAvailableHooks: {},
|
||||
|
||||
|
@ -73,4 +73,40 @@
|
||||
window.onresize = function () {
|
||||
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;
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
@ -9,6 +9,9 @@ if [ -f ${ROOT_DIR}/installed ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
|
@ -11,6 +11,9 @@ else
|
||||
(cd ${ROOT_DIR} && composer install --dev)
|
||||
fi
|
||||
|
||||
echo "Installing NPM packages..."
|
||||
(cd ${ROOT_DIR}/multi && npm install)
|
||||
|
||||
echo "Installing Yarn packages..."
|
||||
(cd ${ROOT_DIR}/public/static && yarn install)
|
||||
|
||||
|
@ -5,6 +5,7 @@ use MapGuesser\Database\Query\Modify;
|
||||
use MapGuesser\Database\Query\Select;
|
||||
use MapGuesser\Interfaces\Database\IResultSet;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\UserConfirmationRepository;
|
||||
use MapGuesser\Repository\UserPasswordResetterRepository;
|
||||
use MapGuesser\Repository\UserRepository;
|
||||
@ -22,6 +23,8 @@ class MaintainDatabaseCommand extends Command
|
||||
|
||||
private UserPasswordResetterRepository $userPasswordResetterRepository;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
@ -30,6 +33,7 @@ class MaintainDatabaseCommand extends Command
|
||||
$this->userRepository = new UserRepository();
|
||||
$this->userConfirmationRepository = new UserConfirmationRepository();
|
||||
$this->userPasswordResetterRepository = new UserPasswordResetterRepository();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
}
|
||||
|
||||
public function configure(): void
|
||||
@ -43,6 +47,7 @@ class MaintainDatabaseCommand extends Command
|
||||
try {
|
||||
$this->deleteInactiveExpiredUsers();
|
||||
$this->deleteExpiredPasswordResetters();
|
||||
$this->deleteExpiredRooms();
|
||||
$this->deleteExpiredSessions();
|
||||
} catch (\Exception $e) {
|
||||
$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
|
||||
{
|
||||
//TODO: model may be used for sessions too
|
||||
|
@ -1,20 +1,37 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use Faker\Factory;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Response\HtmlContent;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
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\MultiRoomRepository;
|
||||
use MapGuesser\Response\Redirect;
|
||||
|
||||
class GameController
|
||||
{
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private MapRepository $mapRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->mapRepository = new MapRepository();
|
||||
}
|
||||
|
||||
@ -22,18 +39,49 @@ class GameController
|
||||
{
|
||||
$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');
|
||||
$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()
|
||||
{
|
||||
$roomId = $this->request->query('roomId');
|
||||
|
||||
return new HtmlContent('game', ['roomId' => $roomId]);
|
||||
}
|
||||
|
||||
public function prepareGame(int $mapId): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$map = $this->mapRepository->getById($mapId);
|
||||
$session = $this->request->session();
|
||||
|
||||
@ -46,6 +94,73 @@ class GameController
|
||||
]);
|
||||
}
|
||||
|
||||
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, bool $forceNew = false)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
<?php namespace MapGuesser\Controller;
|
||||
|
||||
use DateTime;
|
||||
use MapGuesser\Interfaces\Request\IRequest;
|
||||
use MapGuesser\Util\Geo\Position;
|
||||
use MapGuesser\Response\JsonContent;
|
||||
use MapGuesser\Interfaces\Response\IContent;
|
||||
use MapGuesser\Multi\MultiConnector;
|
||||
use MapGuesser\PersistentData\PersistentDataManager;
|
||||
use MapGuesser\Repository\MultiRoomRepository;
|
||||
use MapGuesser\Repository\PlaceRepository;
|
||||
|
||||
class GameFlowController
|
||||
@ -13,15 +17,24 @@ class GameFlowController
|
||||
|
||||
private IRequest $request;
|
||||
|
||||
private PersistentDataManager $pdm;
|
||||
|
||||
private MultiConnector $multiConnector;
|
||||
|
||||
private MultiRoomRepository $multiRoomRepository;
|
||||
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(IRequest $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->pdm = new PersistentDataManager();
|
||||
$this->multiConnector = new MultiConnector();
|
||||
$this->multiRoomRepository = new MultiRoomRepository();
|
||||
$this->placeRepository = new PlaceRepository();
|
||||
}
|
||||
|
||||
public function getInitialData(): IContent
|
||||
public function initialData(): IContent
|
||||
{
|
||||
$mapId = (int) $this->request->query('mapId');
|
||||
$session = $this->request->session();
|
||||
@ -32,6 +45,7 @@ class GameFlowController
|
||||
|
||||
if (!isset($state['currentRound']) || $state['currentRound'] == -1 || $state['currentRound'] >= static::NUMBER_OF_ROUNDS) {
|
||||
$this->startNewGame($state, $mapId);
|
||||
$session->set('state', $state);
|
||||
}
|
||||
|
||||
$response = [];
|
||||
@ -56,7 +70,45 @@ class GameFlowController
|
||||
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');
|
||||
$session = $this->request->session();
|
||||
@ -66,28 +118,23 @@ class GameFlowController
|
||||
}
|
||||
|
||||
$last = $state['rounds'][$state['currentRound']];
|
||||
|
||||
$position = $last['position'];
|
||||
$guessPosition = new Position((float) $this->request->post('lat'), (float) $this->request->post('lng'));
|
||||
|
||||
$distance = $this->calculateDistance($position, $guessPosition);
|
||||
$score = $this->calculateScore($distance, $state['area']);
|
||||
$result = $this->evalueteGuess($last['position'], $guessPosition, $state['area']);
|
||||
|
||||
$last['guessPosition'] = $guessPosition;
|
||||
$last['distance'] = $distance;
|
||||
$last['score'] = $score;
|
||||
|
||||
$state['rounds'][$state['currentRound']] = $last;
|
||||
$state['currentRound'] += 1;
|
||||
$last['distance'] = $result['distance'];
|
||||
$last['score'] = $result['score'];
|
||||
|
||||
$response = [
|
||||
'result' => [
|
||||
'position' => $position->toArray(),
|
||||
'distance' => $distance,
|
||||
'score' => $score
|
||||
'position' => $last['position']->toArray(),
|
||||
'distance' => $result['distance'],
|
||||
'score' => $result['score']
|
||||
]
|
||||
];
|
||||
|
||||
$state['rounds'][$state['currentRound']] = $last;
|
||||
$state['currentRound'] += 1;
|
||||
if ($state['currentRound'] < static::NUMBER_OF_ROUNDS) {
|
||||
$next = $state['rounds'][$state['currentRound']];
|
||||
|
||||
@ -102,7 +149,79 @@ class GameFlowController
|
||||
return new JsonContent($response);
|
||||
}
|
||||
|
||||
private function startNewGame(array &$state, int $mapId)
|
||||
public function multiGuess(): 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();
|
||||
|
||||
$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']);
|
||||
|
||||
$this->multiConnector->sendMessage('guess', [
|
||||
'roomId' => $roomId,
|
||||
'token' => $multiState['token'],
|
||||
'guess' => $guessPosition->toArray(),
|
||||
'distance' => $result['distance'],
|
||||
'score' => $result['score']
|
||||
]);
|
||||
|
||||
$response = [
|
||||
'result' => [
|
||||
'position' => $last['position']->toArray(),
|
||||
'distance' => $result['distance'],
|
||||
'score' => $result['score']
|
||||
]
|
||||
];
|
||||
|
||||
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);
|
||||
|
||||
@ -117,8 +236,6 @@ class GameFlowController
|
||||
'pov' => $place->getPov()
|
||||
];
|
||||
}
|
||||
|
||||
$this->request->session()->set('state', $state);
|
||||
}
|
||||
|
||||
private function calculateDistance(Position $realPosition, Position $guessPosition): float
|
||||
|
24
src/Multi/MultiConnector.php
Normal file
24
src/Multi/MultiConnector.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php namespace MapGuesser\Multi;
|
||||
|
||||
class MultiConnector
|
||||
{
|
||||
public function sendMessage(string $func, array $args = []): void
|
||||
{
|
||||
$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);
|
||||
|
||||
if ($response !== 'OK') {
|
||||
throw new \Exception('Sending message failed with response: ' . $response);
|
||||
}
|
||||
}
|
||||
}
|
88
src/PersistentData/Model/MultiRoom.php
Normal file
88
src/PersistentData/Model/MultiRoom.php
Normal 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
src/Repository/MultiRoomRepository.php
Normal file
38
src/Repository/MultiRoomRepository.php
Normal 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);
|
||||
}
|
||||
}
|
@ -5,10 +5,19 @@
|
||||
|
||||
@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)
|
||||
<span id="mapName" class="bold"><?= $mapName ?></span><!--
|
||||
--><span>Round <span id="currentRound" class="bold"></span></span><!--
|
||||
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
|
||||
<span id="mapName" class="bold"></span><!--
|
||||
--><span>Round <span id="currentRound" class="bold"></span></span><!--
|
||||
--><span>Score <span id="currentScoreSum" class="bold"></span></span>
|
||||
@endsection
|
||||
|
||||
@section(main)
|
||||
@ -50,7 +59,8 @@
|
||||
|
||||
@section(pageScript)
|
||||
<script>
|
||||
var mapId = <?= $mapId ?>;
|
||||
var mapBounds = <?= json_encode($bounds) ?>;
|
||||
var multiUrl = '<?= $_ENV['MULTI_WS_HOST'] . ':' . $_ENV['MULTI_WS_PORT'] ?>';
|
||||
var roomId = <?= isset($roomId) ? '\'' . $roomId . '\'' : 'null' ?>;
|
||||
var mapId = <?= isset($mapId) ? '\'' . $mapId . '\'' : 'null' ?>;
|
||||
</script>
|
||||
@endsection
|
||||
|
@ -5,6 +5,32 @@ TODO: condition!
|
||||
|
||||
@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)
|
||||
<div id="mapContainer">
|
||||
<?php foreach ($maps as $map): ?>
|
||||
@ -35,15 +61,15 @@ TODO: condition!
|
||||
<p class="small center"><?= $map['description'] ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($isAdmin): ?>
|
||||
<div class="buttonContainer">
|
||||
<a class="button fullWidth noRightRadius" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<div class="buttonContainer">
|
||||
<?php if ($isAdmin): ?>
|
||||
<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>
|
||||
<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: ?>
|
||||
<a class="button fullWidth" href="/game/<?= $map['id']; ?>" title="Play map '<?= $map['name'] ?>'">Play this map</a>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<button class="button fullWidth playButton" data-map-id="<?= $map['id'] ?>" data-map-name="<?= htmlspecialchars($map['name']) ?>" title="Play map '<?= $map['name'] ?>'">Play this map</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php if ($isAdmin): ?>
|
||||
|
14
web.php
14
web.php
@ -50,9 +50,17 @@ Container::$routeCollection->group('account', function (MapGuesser\Routing\Route
|
||||
//Container::$routeCollection->get('maps', 'maps', [MapGuesser\Controller\MapsController::class, 'getMaps']);
|
||||
Container::$routeCollection->group('game', function (MapGuesser\Routing\RouteCollection $routeCollection) {
|
||||
$routeCollection->get('game', '{mapId}', [MapGuesser\Controller\GameController::class, 'getGame']);
|
||||
$routeCollection->get('game-json', '{mapId}/json', [MapGuesser\Controller\GameController::class, 'getGameJson']);
|
||||
$routeCollection->get('initialData-json', '{mapId}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'getInitialData']);
|
||||
$routeCollection->post('guess-json', '{mapId}/guess.json', [MapGuesser\Controller\GameFlowController::class, 'evaluateGuess']);
|
||||
$routeCollection->post('game.prepare-json', '{mapId}/prepare.json', [MapGuesser\Controller\GameController::class, 'prepareGame']);
|
||||
$routeCollection->post('game.initialData-json', '{mapId}/initialData.json', [MapGuesser\Controller\GameFlowController::class, 'initialData']);
|
||||
$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) {
|
||||
$routeCollection->get('admin.mapEditor', 'mapEditor/{mapId?}', [MapGuesser\Controller\MapAdminController::class, 'getMapEditor']);
|
||||
|
Loading…
Reference in New Issue
Block a user