diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index 795c0e8e..caf9b926 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,6 +1,6 @@ -import {MessageUserPosition} from "./Websocket/MessageUserPosition"; import { World } from "./World"; import { UserInterface } from "./UserInterface"; +import {PositionInterface} from "_Model/PositionInterface"; export class Group { static readonly MAX_PER_GROUP = 4; @@ -24,10 +24,33 @@ export class Group { return this.users; } + /** + * Returns the barycenter of all users (i.e. the center of the group) + */ + getPosition(): PositionInterface { + let x = 0; + let y = 0; + // Let's compute the barycenter of all users. + this.users.forEach((user: UserInterface) => { + x += user.position.x; + y += user.position.y; + }); + x /= this.users.length; + y /= this.users.length; + return { + x, + y + }; + } + isFull(): boolean { return this.users.length >= Group.MAX_PER_GROUP; } + isEmpty(): boolean { + return this.users.length <= 1; + } + join(user: UserInterface): void { // Broadcast on the right event @@ -60,7 +83,7 @@ export class Group { return stillIn; } - removeFromGroup(users: UserInterface[]): void + /*removeFromGroup(users: UserInterface[]): void { for(let i = 0; i < users.length; i++){ let user = users[i]; @@ -69,5 +92,32 @@ export class Group { this.users.splice(index, 1); } } + }*/ + + leave(user: UserInterface): void + { + const index = this.users.indexOf(user, 0); + if (index === -1) { + throw new Error("Could not find user in the group"); + } + + this.users.splice(index, 1); + user.group = undefined; + + // Broadcast on the right event + this.users.forEach((groupUser: UserInterface) => { + this.disconnectCallback(user.id, groupUser.id); + }); + } + + /** + * Let's kick everybody out. + * Usually used when there is only one user left. + */ + destroy(): void + { + this.users.forEach((user: UserInterface) => { + this.leave(user); + }) } } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts new file mode 100644 index 00000000..d3b0dd47 --- /dev/null +++ b/back/src/Model/PositionInterface.ts @@ -0,0 +1,4 @@ +export interface PositionInterface { + x: number, + y: number +} diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index ff1f58ff..795cc8be 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -4,6 +4,7 @@ import {Group} from "./Group"; import {Distance} from "./Distance"; import {UserInterface} from "./UserInterface"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {PositionInterface} from "_Model/PositionInterface"; export class World { static readonly MIN_DISTANCE = 160; @@ -15,23 +16,28 @@ export class World { 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) + 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 }); + // Let's call update position to trigger the join / leave room + this.updatePosition(userPosition); } public leave(user : ExSocketInterface){ - /*TODO leaver user in group*/ + let userObj = this.users.get(user.id); + if (userObj !== undefined && typeof userObj.group !== 'undefined') { + this.leaveGroup(user); + } this.users.delete(user.userId); } @@ -47,22 +53,52 @@ export class World { 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); + let closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user); - if (closestUser !== null) { - // Is the closest user part of a group? - if (typeof closestUser.group === 'undefined') { + if (closestItem !== null) { + if (closestItem instanceof Group) { + // Let's join the group! + closestItem.join(user); + } else { + let closestUser : UserInterface = closestItem; let group: Group = new Group([ user, closestUser ], this.connectCallback, this.disconnectCallback); - } else { - closestUser.group.join(user); + this.groups.push(group); } } - + + } else { + // If the user is part of a group: + // should he leave the group? + let distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition()); + if (distance > World.MIN_DISTANCE) { + this.leaveGroup(user); + } + } + } + + /** + * Makes a user leave a group and closes and destroy the group if the group contains only one remaining person. + * + * @param user + */ + private leaveGroup(user: UserInterface): void { + let group = user.group; + if (typeof group === 'undefined') { + throw new Error("The user is part of no group"); + } + group.leave(user); + + if (group.isEmpty()) { + group.destroy(); + const index = this.groups.indexOf(group, 0); + if (index === -1) { + throw new Error("Could not find group"); + } + this.groups.splice(index, 1); } - // TODO : vérifier qu'ils ne sont pas déja dans un groupe plein } /** @@ -70,53 +106,37 @@ export class World { * - close enough (distance <= MIN_DISTANCE) * - not in a group OR in a group that is not full */ - private searchClosestAvailableUser(user: UserInterface): UserInterface|null + private searchClosestAvailableUserOrGroup(user: UserInterface): UserInterface|Group|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; + let matchingItem: UserInterface | Group | null = null; this.users.forEach(function(currentUser, userId) { + // Let's only check users that are not part of a group + if (typeof currentUser.group !== 'undefined') { + return; + } 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()) { + if(distance <= minimumDistanceFound) { + minimumDistanceFound = distance; + matchingItem = currentUser; + } + /*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 @@ -140,11 +160,20 @@ export class World { usersToBeGroupedWith.push(dist); } */ - } - - }, this.users); + }); - return matchingUser; + this.groups.forEach(function(group: Group) { + if (group.isFull()) { + return; + } + let distance = World.computeDistanceBetweenPositions(user.position, group.getPosition()); + if(distance <= minimumDistanceFound) { + minimumDistanceFound = distance; + matchingItem = group; + } + }); + + return matchingItem; } public static computeDistance(user1: UserInterface, user2: UserInterface): number @@ -152,6 +181,11 @@ export class World { return Math.sqrt(Math.pow(user2.position.x - user1.position.x, 2) + Math.pow(user2.position.y - user1.position.y, 2)); } + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number + { + return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); + } + /*getDistancesBetweenGroupUsers(group: Group): Distance[] { let i = 0; @@ -169,7 +203,7 @@ export class World { } }); }); - + distances.sort(World.compareDistances); return distances; @@ -195,7 +229,7 @@ export class World { // 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 + // TODO : Notify users un difference that they have left the group } let newgroup = new Group(groupTmp); @@ -212,4 +246,4 @@ export class World { } return 0; }*/ -} \ No newline at end of file +} diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 1f5affc8..1d499727 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -12,7 +12,7 @@ describe("World", () => { connectCalled = true; } let disconnect = (user1: string, user2: string): void => { - + } let world = new World(connect, disconnect); @@ -53,14 +53,103 @@ describe("World", () => { })); expect(connectCalled).toBe(false); }); - /** + + it("should connect 3 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); + + world.join(new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + })); + + world.join(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(200, 100) + })); + + expect(connectCalled).toBe(true); + connectCalled = false; + + // baz joins at the outer limit of the group + world.join(new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(311, 100) + })); + + expect(connectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(309, 100) + })); + + expect(connectCalled).toBe(true); + }); + + it("should disconnect user1 and user2", () => { + let connectCalled: boolean = false; + let disconnectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + disconnectCalled = true; + } + + 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(259, 100) + })); + + expect(connectCalled).toBe(true); + expect(disconnectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(100+160+160+1, 100) + })); + + expect(disconnectCalled).toBe(true); + + disconnectCalled = false; + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(262, 100) + })); + expect(disconnectCalled).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); @@ -100,4 +189,4 @@ describe("World", () => { //expect(distances).toBe([]); }) **/ -}) \ No newline at end of file +})