diff --git a/back/package.json b/back/package.json index d4bf6349..1ea43f30 100644 --- a/back/package.json +++ b/back/package.json @@ -8,7 +8,8 @@ "dev": "ts-node-dev --respawn --transpileOnly ./server.ts", "prod": "tsc && node ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", - "lint": "node_modules/.bin/eslint src/ . --ext .ts" + "lint": "node_modules/.bin/eslint src/ . --ext .ts", + "fix": "node_modules/.bin/eslint src/ . --ext .ts" }, "repository": { "type": "git", diff --git a/back/position-test.js b/back/position-test.js new file mode 100644 index 00000000..01fb5cdf --- /dev/null +++ b/back/position-test.js @@ -0,0 +1,148 @@ +// Constants +let MIN_DISTANCE = 12; +let MAX_PER_GROUP = 3; +let NB_USERS = 10; + +// Utils +let rand = function(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1)) + min; +}; + +let compareDistances = function(distA, distB) { + if (distA.distance < distB.distance) { + return -1; + } + if (distA.distance > distB.distance) { + return 1; + } + return 0; +}; + +let computeDistance = function (user1, user2) { + return Math.sqrt(Math.pow(user2.X - user1.X, 2) + Math.pow(user2.Y - user1.Y, 2)); +}; + +// Test Data +let users = []; +for(let i = 1; i <= NB_USERS; i++) { + let user = {}; + user.id = rand(0,99999); + user.X = rand(0, 40); + user.Y = rand(0, 40); + users.push(user); +} + +// Compute distance between each user +let getDistanceOfEachUser = function(users) { + let i = 0; + let distances = []; + + users.forEach(function(user1, key1) { + users.forEach(function(user2, key2) { + if(key1 < key2) { + let distanceObj = {}; + distanceObj.distance = computeDistance(user1, user2); + distanceObj.first = user1; + distanceObj.second = user2; + + distances[i] = distanceObj; + i++; + } + }); + }); + + return distances; +}; + +// Organise groups +let createGroups = function(distances) { + let i = 0; + let groups = []; + let alreadyInAGroup = []; + + for(let j = 0; j < distances.length; j++) { + let dist = distances[j]; + + if(dist.distance <= MIN_DISTANCE) { + if(typeof groups[i] === 'undefined') { + groups[i] = []; + } + + if(groups[i].indexOf(dist.first) === -1 && typeof alreadyInAGroup[dist.first.id] === 'undefined') { + if(groups[i].length > 1) { + // if group is not empty we check current user can be added in the group according to its distance to the others already in it + for(let l = 0; l < groups[i].length; l++) { + let userTotest = groups[i][l]; + if(computeDistance(dist.first, userTotest) <= MIN_DISTANCE) { + groups[i].push(dist.first); + alreadyInAGroup[dist.first.id] = true; + break; + } + } + } else { + groups[i].push(dist.first); + alreadyInAGroup[dist.first.id] = true; + } + } + + if(groups[i].length === MAX_PER_GROUP) { + i++; // on créé un nouveau groupe + if(i > (NB_USERS / MAX_PER_GROUP)) { + console.log('There is no room left for user ID : ' + dist.second.id + ' !'); + break; + } + continue; + } + + if(groups[i].indexOf(dist.second) === -1 && typeof alreadyInAGroup[dist.second.id] === 'undefined') { + if(groups[i].length > 1) { + // if group is not empty we check current user can be added in the group according to its distance to the others already in it + for(let l = 0; l < groups[i].length; l++) { + let userTotest = groups[i][l]; + if(computeDistance(dist.second, userTotest) <= MIN_DISTANCE) { + groups[i].push(dist.second); + alreadyInAGroup[dist.second.id] = true; + break; + } + } + } else { + groups[i].push(dist.second); + alreadyInAGroup[dist.second.id] = true; + } + } + } + } + + return groups; +}; + +let distances = getDistanceOfEachUser(users); + +// ordonner par distance pour prioriser l'association en groupe des utilisateurs les plus proches +distances.sort(compareDistances); + +let groups = createGroups(distances); + +// Compute distance between each user of a already existing group +let checkGroupDistance = function(groups) { + for(let i = 0; i < groups.length; i++) { + let group = groups[i]; + group.forEach(function(user1, key1) { + group.forEach(function(user2, key2) { + if(key1 < key2) { + let distance = computeDistance(user1, user2); + if(distance > MIN_DISTANCE) { + // TODO : message a user1 et user2 + } + } + }); + }); + } +}; + +console.log(users); +console.log(distances); +console.log(groups); + diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 1fc114a4..acd5843f 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -87,7 +87,8 @@ export class IoSocketController{ //Hydrate and manage error hydrateMessageReceive(message : string) : MessageUserPosition | Error{ try { - return new MessageUserPosition(message); + let data = JSON.parse(message); + return new MessageUserPosition(data); }catch (err) { //TODO log error return new Error(err); diff --git a/back/src/Model/Distance.ts b/back/src/Model/Distance.ts new file mode 100644 index 00000000..150c144f --- /dev/null +++ b/back/src/Model/Distance.ts @@ -0,0 +1,7 @@ +import {MessageUserPosition} from "../Model/Websocket/MessageUserPosition"; + +export interface Distance { + distance: number, + first: MessageUserPosition, + second: MessageUserPosition, +} \ No newline at end of file diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts new file mode 100644 index 00000000..795c0e8e --- /dev/null +++ b/back/src/Model/Group.ts @@ -0,0 +1,73 @@ +import {MessageUserPosition} from "./Websocket/MessageUserPosition"; +import { World } from "./World"; +import { UserInterface } from "./UserInterface"; + +export class Group { + static readonly MAX_PER_GROUP = 4; + + private users: UserInterface[]; + private connectCallback: (user1: string, user2: string) => void; + private disconnectCallback: (user1: string, user2: string) => void; + + + constructor(users: UserInterface[], connectCallback: (user1: string, user2: string) => void, disconnectCallback: (user1: string, user2: string) => void) { + this.users = []; + this.connectCallback = connectCallback; + this.disconnectCallback = disconnectCallback; + + users.forEach((user: UserInterface) => { + this.join(user); + }); + } + + getUsers(): UserInterface[] { + return this.users; + } + + isFull(): boolean { + return this.users.length >= Group.MAX_PER_GROUP; + } + + join(user: UserInterface): void + { + // Broadcast on the right event + this.users.forEach((groupUser: UserInterface) => { + this.connectCallback(user.id, groupUser.id); + }); + this.users.push(user); + user.group = this; + } + + isPartOfGroup(user: UserInterface): boolean + { + return this.users.indexOf(user) !== -1; + } + + isStillIn(user: UserInterface): boolean + { + if(!this.isPartOfGroup(user)) { + return false; + } + let stillIn = true; + for(let i = 0; i <= this.users.length; i++) { + let userInGroup = this.users[i]; + let distance = World.computeDistance(user, userInGroup); + if(distance > World.MIN_DISTANCE) { + stillIn = false; + break; + } + } + return stillIn; + } + + removeFromGroup(users: UserInterface[]): void + { + for(let i = 0; i < users.length; i++){ + let user = users[i]; + const index = this.users.indexOf(user, 0); + if (index > -1) { + this.users.splice(index, 1); + } + } + } +} diff --git a/back/src/Model/UserInterface.ts b/back/src/Model/UserInterface.ts new file mode 100644 index 00000000..743f8b4d --- /dev/null +++ b/back/src/Model/UserInterface.ts @@ -0,0 +1,8 @@ +import { Group } from "./Group"; +import { PointInterface } from "./Websocket/PointInterface"; + +export interface UserInterface { + id: string, + group?: Group, + position: PointInterface +} \ No newline at end of file diff --git a/back/src/Model/Websocket/Message.ts b/back/src/Model/Websocket/Message.ts index 8e6f2c9a..d726968f 100644 --- a/back/src/Model/Websocket/Message.ts +++ b/back/src/Model/Websocket/Message.ts @@ -2,8 +2,7 @@ export class Message { userId: string; roomId: string; - constructor(message: string) { - let data = JSON.parse(message); + constructor(data: any) { if(!data.userId || !data.roomId){ throw Error("userId or roomId cannot be null"); } diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index 117c99cb..a3161b69 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -27,9 +27,8 @@ export class Point implements PointInterface{ export class MessageUserPosition extends Message{ position: PointInterface; - constructor(message: string) { - super(message); - let data = JSON.parse(message); + constructor(data: any) { + super(data); this.position = new Point(data.position.x, data.position.y, data.position.direction); } @@ -42,4 +41,4 @@ export class MessageUserPosition extends Message{ }) ); } -} \ No newline at end of file +} diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts new file mode 100644 index 00000000..804a176b --- /dev/null +++ b/back/src/Model/World.ts @@ -0,0 +1,210 @@ +import {MessageUserPosition, Point} from "./Websocket/MessageUserPosition"; +import {PointInterface} from "./Websocket/PointInterface"; +import {Group} from "./Group"; +import {Distance} from "./Distance"; +import {UserInterface} from "./UserInterface"; + +export class World { + static readonly MIN_DISTANCE = 160; + + // Users, sorted by ID + private users: Map; + private groups: Group[]; + + private connectCallback: (user1: string, user2: string) => void; + private disconnectCallback: (user1: string, user2: string) => void; + + constructor(connectCallback: (user1: string, user2: string) => void, disconnectCallback: (user1: string, user2: string) => void) + { + this.users = new Map(); + this.groups = []; + this.connectCallback = connectCallback; + this.disconnectCallback = disconnectCallback; + } + + public join(userPosition: MessageUserPosition): void { + this.users.set(userPosition.userId, { + id: userPosition.userId, + position: userPosition.position + }); + } + + public updatePosition(userPosition: MessageUserPosition): void { + let context = this; + let user = this.users.get(userPosition.userId); + if(typeof user === 'undefined') { + return; + } + + user.position.x = userPosition.position.x; + user.position.y = userPosition.position.y; + + if (typeof user.group === 'undefined') { + // If the user is not part of a group: + // should he join a group? + let closestUser: UserInterface|null = this.searchClosestAvailableUser(user); + + if (closestUser !== null) { + // Is the closest user part of a group? + if (typeof closestUser.group === 'undefined') { + let group: Group = new Group([ + user, + closestUser + ], this.connectCallback, this.disconnectCallback); + } else { + closestUser.group.join(user); + } + } + + } + // TODO : vérifier qu'ils ne sont pas déja dans un groupe plein + } + + /** + * Looks for the closest user that is: + * - close enough (distance <= MIN_DISTANCE) + * - not in a group OR in a group that is not full + */ + private searchClosestAvailableUser(user: UserInterface): UserInterface|null + { +/* + let sortedUsersByDistance: UserInteface[] = Array.from(this.users.values()).sort((user1: UserInteface, user2: UserInteface): number => { + let distance1 = World.computeDistance(user, user1); + let distance2 = World.computeDistance(user, user2); + return distance1 - distance2; + }); + + // The first element should be the current user (distance 0). Let's remove it. + if (sortedUsersByDistance[0] === user) { + sortedUsersByDistance.shift(); + } + + for(let i = 0; i < sortedUsersByDistance.length; i++) { + let currentUser = sortedUsersByDistance[i]; + let distance = World.computeDistance(currentUser, user); + if(distance > World.MIN_DISTANCE) { + return; + } + } +*/ + let usersToBeGroupedWith: Distance[] = []; + let minimumDistanceFound: number = World.MIN_DISTANCE; + let matchingUser: UserInterface | null = null; + this.users.forEach(function(currentUser, userId) { + if(currentUser === user) { + return; + } + + let distance = World.computeDistance(user, currentUser); // compute distance between peers. + + if(distance <= minimumDistanceFound) { + + if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { + // We found a user we can bind to. + minimumDistanceFound = distance; + matchingUser = currentUser; + return; + } + /* + if(context.groups.length > 0) { + + context.groups.forEach(group => { + if(group.isPartOfGroup(userPosition)) { // Is the user in a group ? + if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player) + + // Should we split the group? (is each player reachable from the current player?) + // This is needed if + // A <==> B <==> C <===> D + // becomes A <==> B <=====> C <> D + // If C moves right, the distance between B and C is too great and we must form 2 groups + + } + } else { + // If the user is in no group + // Is there someone in a group close enough and with room in the group ? + } + }); + + } else { + // Aucun groupe n'existe donc je stock les users assez proches de moi + let dist: Distance = { + distance: distance, + first: userPosition, + second: user // TODO: convertir en messageUserPosition + } + usersToBeGroupedWith.push(dist); + } + */ + } + + }, this.users); + + return matchingUser; + } + + public static computeDistance(user1: UserInterface, user2: UserInterface): number + { + return Math.sqrt(Math.pow(user2.position.x - user1.position.x, 2) + Math.pow(user2.position.y - user1.position.y, 2)); + } + + /*getDistancesBetweenGroupUsers(group: Group): Distance[] + { + let i = 0; + let users = group.getUsers(); + let distances: Distance[] = []; + users.forEach(function(user1, key1) { + users.forEach(function(user2, key2) { + if(key1 < key2) { + distances[i] = { + distance: World.computeDistance(user1, user2), + first: user1, + second: user2 + }; + i++; + } + }); + }); + + distances.sort(World.compareDistances); + + return distances; + } + + filterGroup(distances: Distance[], group: Group): void + { + let users = group.getUsers(); + let usersToRemove = false; + let groupTmp: MessageUserPosition[] = []; + distances.forEach(dist => { + if(dist.distance <= World.MIN_DISTANCE) { + let users = [dist.first]; + let usersbis = [dist.second] + groupTmp.push(dist.first); + groupTmp.push(dist.second); + } else { + usersToRemove = true; + } + }); + + if(usersToRemove) { + // Detecte le ou les users qui se sont fait sortir du groupe + let difference = users.filter(x => !groupTmp.includes(x)); + + // TODO : Notify users un difference that they have left the group + } + + let newgroup = new Group(groupTmp); + this.groups.push(newgroup); + } + + private static compareDistances(distA: Distance, distB: Distance): number + { + if (distA.distance < distB.distance) { + return -1; + } + if (distA.distance > distB.distance) { + return 1; + } + return 0; + }*/ +} \ No newline at end of file diff --git a/back/tests/MessageTest.ts b/back/tests/MessageTest.ts index 69d57fce..070ddf57 100644 --- a/back/tests/MessageTest.ts +++ b/back/tests/MessageTest.ts @@ -3,27 +3,27 @@ import {Message} from "../src/Model/Websocket/Message"; describe("Message Model", () => { it("should find userId and roomId", () => { - let message = JSON.stringify({userId: "test1", roomId: "test2"}); + let message = {userId: "test1", roomId: "test2"}; let messageObject = new Message(message); expect(messageObject.userId).toBe("test1"); expect(messageObject.roomId).toBe("test2"); }) it("should expose a toJson method", () => { - let message = JSON.stringify({userId: "test1", roomId: "test2"}); + let message = {userId: "test1", roomId: "test2"}; let messageObject = new Message(message); expect(messageObject.toJson()).toEqual({userId: "test1", roomId: "test2"}); }) it("should find throw error when no userId", () => { - let message = JSON.stringify({roomId: "test2"}); + let message = {roomId: "test2"}; expect(() => { let messageObject = new Message(message); }).toThrow(new Error("userId or roomId cannot be null")); }) it("should find throw error when no roomId", () => { - let message = JSON.stringify({userId: "test1"}); + let message = {userId: "test1"}; expect(() => { let messageObject = new Message(message); }).toThrow(new Error("userId or roomId cannot be null")); diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts new file mode 100644 index 00000000..1f5affc8 --- /dev/null +++ b/back/tests/WorldTest.ts @@ -0,0 +1,103 @@ +import "jasmine"; +import {Message} from "../src/Model/Websocket/Message"; +import {World} from "../src/Model/World"; +import {MessageUserPosition, Point} from "../src/Model/Websocket/MessageUserPosition"; +import { Group } from "../src/Model/Group"; +import {Distance} from "../src/Model//Distance"; + +describe("World", () => { + it("should connect user1 and user2", () => { + let connectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + + } + + let world = new World(connect, disconnect); + + world.join(new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + })); + + world.join(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(500, 100) + })); + + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(261, 100) + })); + + expect(connectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(101, 100) + })); + + expect(connectCalled).toBe(true); + + connectCalled = false; + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(102, 100) + })); + expect(connectCalled).toBe(false); + }); + /** + it('Should return the distances between all users', () => { + let connectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + + } + + let world = new World(connect, disconnect); + let user1 = new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + }); + + world.join(user1); + + let user2 = new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(500, 100) + }); + world.join(user2); + + let user3 = new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(101, 100) + }); + + let user4 = new MessageUserPosition({ + userId: "buz", + roomId: 1, + position: new Point(105, 100) + }) + + let group = new Group([user1, user2, user3, user4]); + + let distances = world.getDistancesBetweenGroupUsers(group) + + console.log(distances); + + //expect(distances).toBe([]); + }) + **/ +}) \ No newline at end of file