The idea for this project came when I was brainstorming new project ideas with a colleague. We wanted to combine the power of Operational Transforms with mapbox’s API. Ultimately however we had no need for Operational Transform.
The Rules of the game
A robber has just robbed a bank and is driving towards his safe-house, while some cops who were in the vicinity have been alerted of his presence and must chase him down.
Each player is shown a map with his current location tagged by an icon and a path which shows where he is headed. The robber is by default heading for his safe-house but the player can change his direction to evade the cops, who in turn are by default following the robber but also can change their routes if they desire.
The game ends if the thief reaches the safe-house or if a cop catches up with him.
Server side
Keeping things simple a node.js server is used with socket.io to receive and broadcast messages. Socket.io makes sending messages from the client side as simple as saying:
socket.emit('cop_spawn_loc', cop_spawn_list)
With this message the client can send to the server a list containing the latitude and longitude of the points at which the cops shown be spawned.
The only state that the server holds is a queue. When the server receives an incoming request announcing that a new cop wants to join, the server dequeues a number and sends it to the client who uses it as an ID to identify the cop. When a cop disconnects the server puts his ID back into the queue.
Client side
In it’s current state the server supports 4 cops. The first four users who log in become cops and the fifth user becomes the robber.
The Robber
His initial spawn location is hardcoded in midtown Manhattan. Mapbox’s Turf API allows users to find points which are within a certain radius from a given point. In order to do that one must first create a json object with the center coordinates like this:
var center_feature = {
'type' : 'Features',
"properties": {},
'geometry' : {
'type' : 'Point',
'coordinates' : [pos.lng, pos.lat]
},
'properties' : {
'name' : 'Thief Loc'
}
};
Then one needs to call the buffer function with the desired radius and the desired unit system:
var radial_points = turf.buffer(center_feature, radius, 'miles');
The returned list contains 33 points which are all within the radius of the center point and one point is chosen randomly as the position of the safe-house that the thief then navigates towards.
Navigation
Mapbox’s navigation API is remarkable for it’s simplicity. The user merely needs to give it two points which mark the start and end of the route wrapped in a json object and call an url with a callback.
this.get_directions = function(p1, p2, callback) {
var points = [p1, p2];
var waypoints= JSON.stringify(points).replace(/\],\[/g, ";").replace(/\[/g,'').replace(/\]/g,'');
var directionsAPI = 'https://api.tiles.mapbox.com/v4/directions/mapbox.driving/'+ waypoints +'.json?access_token='+ this.access_token;
$.get(directionsAPI, callback);
}
This returns a json object which contains among other things an array with the coordinates of the route, the total distance and the expected time that will be taken to travel the route.
The last two parameters are required to animate the user.
The Cops
Once the robber’s navigation array is obtained, it is used to compute the starting point of the cops.
In the first version of the game we spawned the cops within a 1 mile radius of the the robber’s initial starting position. However this skewed the game heavily in the robber’s favour. The robber would shoot off along his path leaving the poor hapless cops trailing along behind, all in a bundle (more on the bundle later).
In it’s current form we interpolate N(= number of cops) points within the robber’s path, using turf’s along function. Then we use each of these N points as center and find a point within a 0.5 mile radius, which is then used as the cop’s spawning location.
Turf’s along function takes in the array of points returned from the navigation api and finds a point in it specified by the interpolation amount like so:
var point = turf.along(linestring, curr_step, 'miles').geometry.coordinates;
linestring is the array and curr_step is the fraction of amount to interpolate.
With this setup the cops have a better chance of catching up with the robber.
Redirecting the cops
One of the early design decisions that we made was that the cop would go to the location where the robber was last seen. Once he reached that he’d then contact the chopper (unseen, who has eyes on the robber) and then navigate to the currently known position of the robber.
This created unusually lazy behavior among the cops as they’d go off in tangential directions whilst the robber had already sped miles ahead. So in order to keep the game interesting we had to ditch realism. The server was already broadcasting the robber’s current position after each frame update, so now the cop redirects himself after every 5 seconds based on the current known presence of the robber.
Bloopers
Those damned floating points
In order to determine if the robber has reached his goal or if a cop has caught up with him, the distance between the two points of interest is computed (this is of course a bad overhead, which we perform at every frame update). If this distance goes below a desired threshold then it is assumed that the two points are co-incident.
This, mostly works.
There are however instances when this doesn’t. The distance returned never goes below the threshold even though visually the two points look co-incident.
Teleporting cops
This usually happens when all the players are in a straight stretch of road. The first thing that happens is that all the cops bundle together to form a group. This means that if cop 1 was leading, followed by cop 2 and then 3, suddenly 2 and 3 will rush ahead and be co-incident with 1.
This happens (most likely) because of the way that the animation is performed.
var waypoint = turf.along(this.linestring, (this.increment * this.trip_distance * this.pollingInterval) / (this.trip_duration * 1000 * 1000), 'miles').geometry.coordinates;
The polling interval is a constant value and the increment is an integer value that is increased by 1 at every frame update. The best guess that we have right now is that the bug lies in the values returned by this function.
Refresh the page if the above image does not animate.