├── data ├── ballistic │ └── example_data.csv └── dynamic │ └── example_data.csv ├── static ├── game_over.html └── disconnected.html ├── package.json ├── index.css ├── index.html ├── LICENSE ├── database.js ├── lib └── sprintf.min.js ├── app.js ├── drawing.js ├── README.md ├── game.server.js ├── game.client.js └── game.core.js /data/ballistic/example_data.csv: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/dynamic/example_data.csv: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/game_over.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Collective Behavior on MTurk 5 | 6 | 7 | 8 |

Thank you for playing!

9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "MWERT", 2 | "description": "Platform for running multiplayer experiments on the web", 3 | "version": "1.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "express": "~4.0.0", 7 | "socket.io": "~1.0.0", 8 | "mysql": "2.0.0-rc2", 9 | "node-uuid": "1.4.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | html , body { 2 | background: #212121; 3 | color: #fff; 4 | font-family: Helvetica; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | #viewport { 10 | border:solid 1px #333; 11 | height : 480px; 12 | margin: auto; 13 | left:0; right:0; top:0; bottom:0; 14 | position: absolute; 15 | width : 720px; 16 | z-index:100; 17 | } -------------------------------------------------------------------------------- /static/disconnected.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Collective Behavior on MTurk 5 | 6 | 7 | 8 |

Other player disconnected!

9 |

We're sorry you didn’t have a chance to complete the experiment. Please follow the link to complete our exit survey and receive your bonus. Thank you for participating!

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Collective Behavior 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 hawkrobe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | var mysql = require("mysql"); 2 | 3 | module.exports.getConnection = function() { 4 | // Test connection health before returning it to caller. 5 | if ((module.exports.connection) && (module.exports.connection._socket) 6 | && (module.exports.connection._socket.readable) 7 | && (module.exports.connection._socket.writable)) { 8 | return module.exports.connection; 9 | } 10 | 11 | console.log(((module.exports.connection) ? 12 | "UNHEALTHY SQL CONNECTION; RE" : "") + "CONNECTING TO SQL."); 13 | 14 | var connection = mysql.createConnection({ 15 | database : '', // Enter mysql 16 | user : '', // info 17 | password : '', // here 18 | multipleStatements : true, 19 | }); 20 | 21 | connection.connect(function(err) { 22 | if (err) { 23 | console.log("SQL CONNECT ERROR: " + err); 24 | } else { 25 | console.log("SQL CONNECT SUCCESSFUL."); 26 | } 27 | }); 28 | 29 | connection.on("close", function (err) { 30 | console.log("SQL CONNECTION CLOSED."); 31 | }); 32 | 33 | connection.on("error", function (err) { 34 | console.log("SQL CONNECTION ERROR: " + err); 35 | }); 36 | 37 | module.exports.connection = connection; 38 | 39 | return module.exports.connection; 40 | } 41 | 42 | // Open a connection automatically at app startup. 43 | module.exports.getConnection(); 44 | 45 | // If you've saved this file as database.js, then get and use the 46 | // connection as in the following example: 47 | // var database = require(__dirname + "/database"); 48 | // var connection = database.getConnection(); 49 | // connection.query(query, function(err, results) { 50 | -------------------------------------------------------------------------------- /lib/sprintf.min.js: -------------------------------------------------------------------------------- 1 | /*! sprintf.js | Copyright (c) 2007-2013 Alexandru Marasteanu | 3 clause BSD license */(function(e){function r(e){return Object.prototype.toString.call(e).slice(8,-1).toLowerCase()}function i(e,t){for(var n=[];t>0;n[--t]=e);return n.join("")}var t=function(){return t.cache.hasOwnProperty(arguments[0])||(t.cache[arguments[0]]=t.parse(arguments[0])),t.format.call(null,t.cache[arguments[0]],arguments)};t.format=function(e,n){var s=1,o=e.length,u="",a,f=[],l,c,h,p,d,v;for(l=0;l>>=0;break;case"x":a=a.toString(16);break;case"X":a=a.toString(16).toUpperCase()}a=/[def]/.test(h[8])&&h[3]&&a>=0?"+"+a:a,d=h[4]?h[4]=="0"?"0":h[4].charAt(1):" ",v=h[6]-String(a).length,p=h[6]?i(d,v):"",f.push(h[5]?a+p:p+a)}}return f.join("")},t.cache={},t.parse=function(e){var t=e,n=[],r=[],i=0;while(t){if((n=/^[^\x25]+/.exec(t))!==null)r.push(n[0]);else if((n=/^\x25{2}/.exec(t))!==null)r.push("%");else{if((n=/^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(t))===null)throw"[sprintf] huh?";if(n[2]){i|=1;var s=[],o=n[2],u=[];if((u=/^([a-z_][a-z_\d]*)/i.exec(o))===null)throw"[sprintf] huh?";s.push(u[1]);while((o=o.substring(u[0].length))!=="")if((u=/^\.([a-z_][a-z_\d]*)/i.exec(o))!==null)s.push(u[1]);else{if((u=/^\[(\d+)\]/.exec(o))===null)throw"[sprintf] huh?";s.push(u[1])}n[2]=s}else i|=2;if(i===3)throw"[sprintf] mixing positional and named placeholders is not (yet) supported";r.push(n)}t=t.substring(n[0].length)}return r};var n=function(e,n,r){return r=n.slice(0),r.splice(0,0,e),t.apply(null,r)};e.sprintf=t,e.vsprintf=n})(typeof exports!="undefined"?exports:window); -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Sven "FuzzYspo0N" Bergström, 2013 Robert XD Hawkins 2 | 3 | originally written for: http://buildnewgames.com/real-time-multiplayer/ 4 | 5 | substantially modified for collective behavior experiments 6 | 7 | MIT Licensed. 8 | */ 9 | 10 | var 11 | use_db = false, 12 | gameport = 8000, 13 | app = require('express')(), 14 | server = app.listen(gameport), 15 | io = require('socket.io')(server), 16 | UUID = require('node-uuid'); 17 | 18 | if (use_db) { 19 | database = require(__dirname + "/database"), 20 | connection = database.getConnection(); 21 | } 22 | 23 | game_server = require('./game.server.js'); 24 | 25 | // Log something so we know that server-side setup succeeded 26 | console.log("info - socket.io started"); 27 | console.log('\t :: Express :: Listening on port ' + gameport ); 28 | 29 | // This handler will listen for requests on /*, any file from the 30 | // root of our server. See expressjs documentation for more info 31 | app.get( '/*' , function( req, res ) { 32 | // this is the current file they have requested 33 | var file = req.params[0]; 34 | console.log('\t :: Express :: file requested: ' + file); 35 | 36 | // give them what they want 37 | res.sendfile("./" + file); 38 | }); 39 | 40 | // Socket.io will call this function when a client connects. We check 41 | // to see if the client supplied a id. If so, we distinguish them by 42 | // that, otherwise we assign them one at random 43 | io.on('connection', function (client) { 44 | // Recover query string information and set condition 45 | var hs = client.handshake; 46 | var query = require('url').parse(client.handshake.headers.referer, true).query; 47 | var id = (query.id) ? query.id : UUID(); // use id from query string if exists 48 | client.condition = query.condition; 49 | if (use_db) { 50 | // Only let a player join if they are already in the database. 51 | // Otherwise, send an alert message 52 | var q = 'SELECT EXISTS(SELECT * FROM game_participant WHERE workerId = ' + 53 | connection.escape(id) + ') AS b'; 54 | connection.query(q, function(err, results) { 55 | player_exists = results[0].b; 56 | if (id && player_exists) { 57 | initialize(query, client, id); 58 | } else { 59 | client.userid = 'none'; 60 | client.send('s.alert'); 61 | } 62 | }); 63 | } else { 64 | initialize(query, client, id); 65 | }}); 66 | 67 | var initialize = function(query, client, id) { 68 | client.userid = id; 69 | client.emit('onconnected', { id: client.userid } ); 70 | 71 | // Good to know when they connected 72 | console.log('\t socket.io:: player ' + client.userid + ' connected'); 73 | 74 | //Pass off to game.server.js code 75 | game_server.findGame(client); 76 | 77 | // Now we want set up some callbacks to handle messages that clients will send. 78 | // We'll just pass messages off to the server_onMessage function for now. 79 | client.on('message', function(m) { 80 | game_server.server_onMessage(client, m); 81 | }); 82 | 83 | // When this client disconnects, we want to tell the game server 84 | // about that as well, so it can remove them from the game they are 85 | // in, and make sure the other player knows that they left and so on. 86 | client.on('disconnect', function () { 87 | console.log('\t socket.io:: client id ' + client.userid 88 | + ' disconnected from game id' + client.game.id); 89 | 90 | //If the client was in a game set by game_server.findGame, 91 | //we can tell the game server to update that game state. 92 | if(client.userid && client.game && client.game.id) 93 | //player leaving a game should destroy that game 94 | game_server.endGame(client.game.id, client.userid); 95 | }); 96 | }; 97 | 98 | -------------------------------------------------------------------------------- /drawing.js: -------------------------------------------------------------------------------- 1 | // Draw players as triangles using HTML5 canvas 2 | draw_player = function(game, player){ 3 | game.ctx.font = "10pt Helvetica"; 4 | 5 | // Draw avatar as triangle 6 | var v = [[0,-8],[-5,8],[5,8]]; 7 | game.ctx.save(); 8 | game.ctx.translate(player.pos.x, player.pos.y); 9 | // draw_enabled is set to false during the countdown, so that 10 | // players can set their destinations but won't turn to face them. 11 | // As soon as the countdown is over, it's set to true and they 12 | // immediately start using that new angle 13 | if (player.game.draw_enabled) { 14 | game.ctx.rotate((player.angle * Math.PI) / 180); 15 | } else { 16 | game.ctx.rotate((player.start_angle * Math.PI) / 180); 17 | } 18 | // This draws the triangle 19 | game.ctx.fillStyle = player.color; 20 | game.ctx.strokeStyle = player.color; 21 | game.ctx.beginPath(); 22 | game.ctx.moveTo(v[0][0],v[0][1]); 23 | game.ctx.lineTo(v[1][0],v[1][1]); 24 | game.ctx.lineTo(v[2][0],v[2][1]); 25 | game.ctx.closePath(); 26 | game.ctx.stroke(); 27 | game.ctx.fill(); 28 | 29 | game.ctx.beginPath(); 30 | game.ctx.restore(); 31 | 32 | // Draw destination as an 'x' if it exists 33 | if (player.destination) { 34 | game.ctx.strokeStyle = player.color; 35 | game.ctx.beginPath(); 36 | game.ctx.moveTo(player.destination.x - 5, player.destination.y - 5); 37 | game.ctx.lineTo(player.destination.x + 5, player.destination.y + 5); 38 | 39 | game.ctx.moveTo(player.destination.x + 5, player.destination.y - 5); 40 | game.ctx.lineTo(player.destination.x - 5, player.destination.y + 5); 41 | game.ctx.stroke(); 42 | } 43 | 44 | //Draw tag underneath players 45 | game.ctx.fillStyle = player.info_color; 46 | game.ctx.fillText(player.state, player.pos.x+10, player.pos.y + 20); 47 | 48 | // Draw message in center (for countdown, e.g.) 49 | game.ctx.fillStyle = 'white'; 50 | game.ctx.fillText(player.message, 290, 240); 51 | 52 | // Represent speeds in corner as a sort of bar graph (to visualize 53 | // the effect of noise) 54 | game.ctx.fillText("Your current speed: ", 5, 15); 55 | game.ctx.fillText("Other's current speed: ", 5, 40); 56 | game.ctx.beginPath(); 57 | game.ctx.strokeStyle = 'rgba(255,255,255,0.1)'; 58 | 59 | // Light gray vertical line as base 60 | game.ctx.moveTo(145, 0); 61 | game.ctx.lineTo(145, 45); 62 | game.ctx.stroke(); 63 | 64 | // Self line for speed counter 65 | game.ctx.beginPath(); 66 | game.ctx.moveTo(145, 12); 67 | if (game.players.self.curr_distance_moved == 0) { 68 | game.ctx.strokeStyle = 'rgba(255,255,255,0.1)'; 69 | game.ctx.lineTo(145 + 30, 12); 70 | game.ctx.stroke(); 71 | } else { 72 | game.ctx.lineWidth = 15; 73 | game.ctx.strokeStyle = 'white'; 74 | game.ctx.lineTo(145 + 3*game.players.self.curr_distance_moved.fixed(2), 12); 75 | game.ctx.stroke(); 76 | game.ctx.lineWidth = 1; 77 | } 78 | 79 | // Other line... 80 | game.ctx.beginPath(); 81 | game.ctx.moveTo(145, 37); 82 | if(game.players.other.curr_distance_moved == 0) { 83 | game.ctx.strokeStyle = 'rgba(255,255,255,0.1)'; 84 | game.ctx.lineTo(145 + 30, 37); 85 | game.ctx.stroke(); 86 | } else { 87 | game.ctx.lineWidth = 15; 88 | game.ctx.strokeStyle = 'white'; 89 | game.ctx.lineTo(145 + 3*game.players.other.curr_distance_moved.fixed(2), 37); 90 | game.ctx.stroke(); 91 | game.ctx.lineWidth = 1; 92 | } 93 | game.ctx.stroke(); 94 | 95 | }; 96 | 97 | // player.targets_enabled is set to true when both people have joined. 98 | // Uses HTML5 canvas 99 | 100 | draw_targets = function(game, player) { 101 | // Draw targets 102 | if (player.targets_enabled) { 103 | var centerX1 = game.targets.top.location.x; 104 | var centerY1 = game.targets.top.location.y; 105 | var centerX2 = game.targets.bottom.location.x; 106 | var centerY2 = game.targets.bottom.location.y; 107 | var radius = game.targets.top.radius; 108 | var outer_radius = game.targets.top.outer_radius; 109 | 110 | // Filled in top target 111 | game.ctx.beginPath(); 112 | game.ctx.arc(centerX1, centerY1, radius, 0, 2 * Math.PI, false); 113 | game.ctx.fillStyle = game.targets.top.color; 114 | game.ctx.fill(); 115 | game.ctx.lineWidth = 1; 116 | game.ctx.strokeStyle = 'gray'; 117 | game.ctx.stroke(); 118 | 119 | // Outer line around top target 120 | game.ctx.beginPath(); 121 | game.ctx.arc(centerX1, centerY1, outer_radius, 0, 2 * Math.PI, false); 122 | game.ctx.stroke(); 123 | 124 | // Filled in bottom target 125 | game.ctx.beginPath(); 126 | game.ctx.arc(centerX2, centerY2, radius, 0, 2 * Math.PI, false); 127 | game.ctx.fillStyle = game.targets.bottom.color; 128 | game.ctx.fill(); 129 | game.ctx.stroke(); 130 | 131 | // Outer line around bottom target 132 | game.ctx.beginPath(); 133 | game.ctx.arc(centerX2, centerY2, outer_radius, 0, 2 * Math.PI, false); 134 | game.ctx.stroke(); 135 | 136 | // Draw tag next to targets (for payoff info) 137 | game.ctx.fillStyle = 'white'; 138 | game.ctx.font = "15pt Helvetica"; 139 | targets = game.targets; 140 | game.ctx.fillText("$0.0" + targets.top.payoff, 141 | targets.top.location.x - 27, targets.top.location.y - 50 ); 142 | game.ctx.fillText("$0.0" + targets.bottom.payoff, 143 | targets.bottom.location.x-27, targets.bottom.location.y+65); 144 | } 145 | }; 146 | 147 | // draws instructions at the bottom in a nice style 148 | draw_info = function(game, info) { 149 | //Draw information shared by both players 150 | game.ctx.font = "8pt Helvetica"; 151 | game.ctx.fillStyle = 'rgba(255,255,255,1)'; 152 | game.ctx.fillText(info, 10 , 465); 153 | 154 | //Reset the style back to full white. 155 | game.ctx.fillStyle = 'rgba(255,255,255,1)'; 156 | }; 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Related projects 2 | ================ 3 | 4 | In addition to the real-time game theory task implemented in this directory and discussed in [this paper](http://link.springer.com/article/10.3758%2Fs13428-014-0515-6), we have 5 | 6 | 1. scaled up the MWERT framework for running a [collective behavior experiment](https://github.com/hawkrobe/couzin_replication) with arbitrary numbers of participants. 7 | 8 | 2. implemented a Keysar-style [director-agent task](https://github.com/hawkrobe/keysar_replication) with a chatbox for players to communicate, and an HTML5 canvas interface in which the agent can drag objects around. 9 | 10 | 3. implemented a [guessing game experiment](https://github.com/hawkrobe/Q_and_A/tree/master/MultiExperiment2/experiment/) in which one player is assigned a goal (e.g. find the whale) and must form a question to ask the other player, who knows where the objects are hidden. 11 | 12 | 4. implemented a family of iterated [reference game tasks](https://github.com/hawkrobe/reference_games), which factors out shared server functions. 13 | 14 | 15 | Local demo (from scratch) 16 | ========================= 17 | 18 | ![Example Experiment Screenshot](/../screenshot/static/Example_Image.jpg?raw=true) 19 | 20 | 1. Git is a popular version control and source code management system. If you're new to git, you'll need to install the latest version by following the link for [Mac](http://sourceforge.net/projects/git-osx-installer/) or [Windows](http://msysgit.github.io/) and downloading the first option in the list. On Mac, this will give you a set of command-line tools (restart the terminal if the git command is still not found after installation). On Windows it will give you a shell to type commands into. For Linux users, more information can be found [here](http://git-scm.com/book/en/Getting-Started-Installing-Git). 21 | 22 | 2. On Mac or Linux, use the Terminal to navigate to the location where you want to create your project, and enter 23 | ``` 24 | git clone https://github.com/hawkrobe/MWERT.git 25 | ``` 26 | at the command line to create a local copy of this repository. On Windows, run this command in the shell you installed in the previous step. 27 | 28 | 3. Install node and npm (the node package manager) on your machine. Node.js sponsors an [official download](http://nodejs.org/download/) for all systems. For an advanced installation, there are good instructions [here](https://gist.github.com/isaacs/579814). 29 | 30 | 4. Navigate into the repository you created. You should see a file called package.json, which contains the dependencies. To install these dependencies, enter ```npm install``` at the command line. This may take a few minutes. 31 | 32 | 5. Finally, to run the experiment, enter ```node app.js``` at the command line. You should expect to see the following message: 33 | ``` 34 | info - socket.io started 35 | :: Express :: Listening on port 8000 36 | ``` 37 | This means that you've successfully created a 'server' that can be accessed by copying and pasting 38 | ``` 39 | http://localhost:8000/index.html?id=1000&condition=dynamic 40 | ``` 41 | in one tab of your browser. You should see an avatar in a waiting room. To connect the other player in another tab for test purposes, open a new tab and use this URL with a different id: 42 | ``` 43 | http://localhost:8000/index.html?id=1001&condition=dynamic 44 | ``` 45 | To see the other (staged) version of the experiment, just change "dynamic" to "ballistic" in the URL query string. Also note that if no id is provided, a unique id will be randomly generated. 46 | 47 | Putting experiment on web server 48 | ================================ 49 | 50 | To make your experiment accessible over the internet, you'll need to put it in a publicly accessible directory of a web server, and run ```node app.js``` from that directory. To link clients to the experiment, replace "localhost" in the links given above with your web server's name. Sample templates are given for 'disconnected' and 'game over' web pages, but you may want to serve up a customized HTML document containing an exit survey or a portal to submit work (if you are using an online labor market like Amazon Mechanical Turk). To do so, you can change the URL in the ```client_newgame()``` and ```client_ondisconnect()``` functions contained in **client.js**. 51 | 52 | Integrating with MySQL 53 | ====================== 54 | 55 | First, enter your database information (i.e. user, password, and database name) in **database.js**. Next, set the ```use_db``` variable to ```true``` at the top of **app.js** and **game.server.js**. By default, the code assumes a table called ```game_participant``` with fields ```workerID``` and ```bonus_pay```, but you can change the queries at the following places in the code to fit your database: 56 | 57 | The database is queried at only two points in the provided code. One is in **app.js** to check whether the id supplied in the query string exists in the database. If it isn't, the player is notified and referred to another site rather than being assigned a unique random id. The other location is the ```server_newgame()``` function in **game.core.js**, where we save each player’s current winnings to the database at the end of each round, just in case someone disconnects and we must pay however much they accumulated. 58 | 59 | 60 | Code Glossary 61 | ============= 62 | 63 | The code is divided across several distinct files to make it easier to understand the roles played by different functions. Here are the high-level description of the contents so that you can find the part you need to change for your own application: 64 | 65 | * **game.core.js**: Contains the game logic and core client-side functions. Creates game, player, and target objects and specifies their properties. This is the primary code that must be changed when specifying a different game logic (e.g. ```server_update_physics()```, ```server_newgame()```, ```writeData()```). 66 | * **app.js**: This is the Node.js script that sets everything up to listen for clients on the specified port. It will not need to be changed, except if you want to listen on a different port (default 8000). 67 | * **game.client.js**: Runs in a client's browser upon accessing the URL being served by our Node.js app. Creates a client-side game_core object, establishes a Socket.io connection between the client and the server, and specifies what happens upon starting or joining a game. This needs to be changed if introducing new Socket.io messsage (i.e. if you want to track a new client-side event), new shared variables, or new details of how games begin. 68 | * **game.server.js**: Contains the functions to pair people up into separate 'rooms' (```findGame()```) and also species how the server acts upon messages passed from clients (```onMessage()```). This needs to be changed if you want groups of more than two people, or if you're adding new events to the Socket.io pipeline. 69 | * **drawing.js**: Contains the HTML5 code to render graphics. Only needs to be changed if you desire a different graphical representation. 70 | * **index.html, index.css**: Define background of page and runs necessary client-side scripts. Do not need to be changed. 71 | * **disconnected.html, game_over.html**: Template pages to demonstrate how to refer a player out of the game upon some event. Should be replaced by pages specific to your purpose (e.g. an exit survey, a debriefing, or a link to submit the HIT on Mechanical Turk). 72 | -------------------------------------------------------------------------------- /game.server.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Sven "FuzzYspo0N" Bergström, 2013 Robert XD Hawkins 2 | 3 | written by : http://underscorediscovery.com 4 | written for : http://buildnewgames.com/real-time-multiplayer/ 5 | 6 | modified for collective behavior experiments on Amazon Mechanical Turk 7 | 8 | MIT Licensed. 9 | */ 10 | 11 | var 12 | use_db = false, 13 | game_server = module.exports = { games : {}, game_count:0 }, 14 | UUID = require('node-uuid'), 15 | fs = require('fs'); 16 | 17 | if (use_db) { 18 | database = require(__dirname + "/database"), 19 | connection = database.getConnection(); 20 | } 21 | 22 | global.window = global.document = global; 23 | 24 | require('./game.core.js'); 25 | 26 | // This is the function where the server parses and acts on messages 27 | // sent from 'clients' aka the browsers of people playing the 28 | // game. For example, if someone clicks on the map, they send a packet 29 | // to the server (check the client_on_click function in game.client.js) 30 | // with the coordinates of the click, which this function reads and 31 | // applies. 32 | game_server.server_onMessage = function(client,message) { 33 | //Cut the message up into sub components 34 | var message_parts = message.split('.'); 35 | 36 | //The first is always the type of message 37 | var message_type = message_parts[0]; 38 | 39 | //Extract important variables 40 | if (client.game.player_host.userid == client.userid) { 41 | var other_client = client.game.player_client; 42 | var change_target = client.game.gamecore.players.self; 43 | } else { 44 | var other_client = client.game.player_host; 45 | var change_target = client.game.gamecore.players.other; 46 | } 47 | 48 | if(message_type == 'c') { // Client clicked somewhere 49 | if(!change_target.targets_enabled) { 50 | change_target.speed = client.game.gamecore.global_speed; 51 | } else { 52 | if(client.game.gamecore.good2write) { 53 | change_target.speed = client.game.gamecore.global_speed; 54 | } 55 | } 56 | // Set their (server) angle 57 | change_target.angle = message_parts[1]; 58 | 59 | // Set their (server) destination to the point that was clicked 60 | change_target.destination = {x : message_parts[2], y : message_parts[3]}; 61 | 62 | // Notify other client of angle change 63 | if(other_client){ 64 | other_client.send('s.a.' + message_parts[1]); 65 | } 66 | } else if (message_type == "h") { // Receive message when browser focus shifts 67 | change_target.visible = message_parts[1]; 68 | } 69 | // else if(...) { 70 | 71 | // Any other ways you want players to interact with the game can be added 72 | // here as "else if" statements. 73 | }; 74 | 75 | /* 76 | The following functions should not need to be modified for most purposes 77 | */ 78 | 79 | game_server.createGame = function(player) { 80 | var id = UUID(); 81 | //Create a new game instance 82 | var thegame = { 83 | id : id, //generate a new id for the game 84 | player_host:player, //so we know who initiated the game 85 | player_client:null, //nobody else joined yet, since its new 86 | player_count:1 //for simple checking of state 87 | }; 88 | 89 | //Store it in the list of game 90 | this.games[ thegame.id ] = thegame; 91 | 92 | //Keep track of how many there are total 93 | this.game_count++; 94 | 95 | //Create a new game core instance (defined in game.core.js) 96 | thegame.gamecore = new game_core(thegame); 97 | 98 | // Tell the game about its own id 99 | thegame.gamecore.game_id = id; 100 | 101 | // Set up the filesystem variable we'll use to write later 102 | thegame.gamecore.fs = fs; 103 | 104 | // When workers are directed to the page, they specify which 105 | // version of the task they're running. 106 | thegame.gamecore.condition = player.condition; 107 | 108 | // Pass the database connection to the game 109 | if (use_db) { 110 | thegame.gamecore.mysql_conn = connection; 111 | thegame.gamecore.use_db = use_db; 112 | } 113 | 114 | //Start updating the game loop on the server 115 | thegame.gamecore.update(); 116 | 117 | //tell the player that they are now the host 118 | //The client will parse this message in the "client_onMessage" function 119 | // in client.js, which redirects to other functions based on the command 120 | player.send('s.h.') 121 | player.game = thegame; 122 | 123 | // Start 'em moving 124 | thegame.gamecore.players.self.speed = thegame.gamecore.global_speed; 125 | this.log('player ' + player.userid + ' created a game with id ' + player.game.id); 126 | 127 | //return it 128 | return thegame; 129 | 130 | }; //game_server.createGame 131 | 132 | // we are requesting to kill a game in progress. 133 | // This gets called if someone disconnects 134 | game_server.endGame = function(gameid, userid) { 135 | var thegame = this.games[gameid]; 136 | if(thegame) { 137 | //stop the game updates immediately 138 | thegame.gamecore.stop_update(); 139 | 140 | //if the game has two players, then one is leaving 141 | if(thegame.player_count > 1) { 142 | 143 | //send the players the message the game is ending 144 | if(userid == thegame.player_host.userid) { 145 | //the host left, oh snap. Let's update the database and tell them. 146 | if(thegame.player_client) { 147 | //tell them the game is over, and redirect to exit survey 148 | thegame.player_client.send('s.e'); 149 | } 150 | } else { 151 | //the other player left, we were hosting 152 | if(thegame.player_host) { 153 | //tell the client the game is ended 154 | thegame.player_host.send('s.e'); 155 | //i am no longer hosting, this game is going down 156 | thegame.player_host.hosting = false; 157 | } 158 | } 159 | } 160 | delete this.games[gameid]; 161 | this.game_count--; 162 | this.log('game removed. there are now ' + this.game_count + ' games' ); 163 | } else { 164 | this.log('that game was not found!'); 165 | } 166 | }; 167 | 168 | // When second person joins the game, this gets called 169 | game_server.startGame = function(game) { 170 | 171 | // Tell host so that their title will flash if not visible 172 | game.player_host.send('s.b'); 173 | 174 | //s=server message, j=you are joining, send player the host id 175 | game.player_client.send('s.j.' + game.player_host.userid); 176 | game.player_client.game = game; 177 | 178 | //now we tell the server that the game is ready to start 179 | game.gamecore.server_newgame(); 180 | 181 | //set this flag, so that the update loop can run it. 182 | game.active = true; 183 | 184 | }; 185 | 186 | // This is the important function that pairs people up into 'rooms' 187 | // all independent of one another. 188 | game_server.findGame = function(player) { 189 | this.log('looking for a game. We have : ' + this.game_count); 190 | 191 | //if there are any games created, check if one needs another player 192 | if(this.game_count) { 193 | var joined_a_game = false; 194 | //Check through the list of all games for an open game 195 | for(var gameid in this.games) { 196 | //only care about our own properties. 197 | if(!this.games.hasOwnProperty(gameid)) continue; 198 | //get the game we are checking against 199 | var game_instance = this.games[gameid]; 200 | //If the game is a player short 201 | if(game_instance.player_count < 2) { 202 | joined_a_game = true; 203 | //increase the player count and store 204 | //the player as the client of this game 205 | game_instance.player_client = player; 206 | game_instance.gamecore.players.other.instance = player; 207 | game_instance.gamecore.players.other.id = player.userid; 208 | game_instance.player_count++; 209 | // start the server update loop 210 | game_instance.gamecore.update(); 211 | this.startGame(game_instance); 212 | } 213 | } 214 | if(!joined_a_game) { // if we didn't join a game, we must create one 215 | this.createGame(player); 216 | } 217 | } else { 218 | //no games? create one! 219 | this.createGame(player); 220 | } 221 | }; 222 | 223 | //A simple wrapper for logging so we can toggle it, 224 | //and augment it for clarity. 225 | game_server.log = function() { 226 | console.log.apply(this,arguments); 227 | }; 228 | -------------------------------------------------------------------------------- /game.client.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Sven "FuzzYspo0N" Bergström, 2 | 2013 Robert XD Hawkins 3 | 4 | written by : http://underscorediscovery.com 5 | written for : http://buildnewgames.com/real-time-multiplayer/ 6 | 7 | modified for collective behavior experiments on Amazon Mechanical Turk 8 | 9 | MIT Licensed. 10 | */ 11 | 12 | /* 13 | THE FOLLOWING FUNCTIONS MAY NEED TO BE CHANGED 14 | */ 15 | 16 | var visible; 17 | 18 | // This function is called whenever a player clicks. 19 | // Input: 20 | // * game = the current game object for extracting current state 21 | // * newX = the X coordinate of the player's click 22 | // * newY = the Y coordinate of the player's click 23 | client_on_click = function(game, newX, newY ) { 24 | // Auto-correcting input, but only between rounds 25 | if (game.condition == 'ballistic' && !game.draw_enabled) { 26 | if (game.distance_between({x : newX, y : newY}, 27 | game.targets.top.location) 28 | < game.targets.top.outer_radius) { 29 | newX = game.targets.top.location.x; 30 | newY = game.targets.top.location.y; 31 | } else if (game.distance_between({x : newX, y: newY}, 32 | game.targets.bottom.location) 33 | < game.targets.bottom.outer_radius) { 34 | newX = game.targets.bottom.location.x; 35 | newY = game.targets.bottom.location.y; 36 | } 37 | } 38 | 39 | oldX = game.players.self.pos.x; 40 | oldY = game.players.self.pos.y; 41 | dx = newX - oldX; 42 | dy = newY - oldY; 43 | 44 | // Complicated logic. If you're in the dynamic condition, your clicks will 45 | // ALWAYS register. If you're in the ballistic condition, they'll only register 46 | // if you're in the pre- (or between-)game period where nothing's being written. 47 | if((game.condition == "ballistic" && !game.good2write) || 48 | game.condition == "dynamic") { 49 | game.players.self.destination = {x : Math.round(newX), y : Math.round(newY)}; 50 | game.players.self.angle = Math.round((Math.atan2(dy,dx) * 180 / Math.PI) + 90); 51 | 52 | // Send game information to server so that other player (and server) 53 | // can update information 54 | info_packet = ("c." + game.players.self.angle + 55 | "." + game.players.self.destination.x + 56 | "." + game.players.self.destination.y); 57 | game.socket.send(info_packet); 58 | } 59 | }; 60 | 61 | // Function that gets called client-side when someone disconnects 62 | client_ondisconnect = function(data) { 63 | // Everything goes offline! 64 | game.players.self.info_color = 'rgba(255,255,255,0.1)'; 65 | game.players.self.state = 'not-connected'; 66 | game.players.self.online = false; 67 | game.players.self.destination = null; 68 | game.players.other.info_color = 'rgba(255,255,255,0.1)'; 69 | game.players.other.state = 'not-connected'; 70 | 71 | console.log("Disconnecting..."); 72 | 73 | if(game.games_remaining == 0) { 74 | // If the game is done, redirect them to an exit survey 75 | URL = './static/game_over.html'; 76 | URL += '?id=' + game.players.self.id; 77 | window.location.replace(URL); 78 | } else { 79 | // Otherwise, redirect them to a "we're sorry, the other 80 | // player disconnected" page 81 | URL = './static/disconnected.html' 82 | URL += '?id=' + game.players.self.id; 83 | window.location.replace(URL); 84 | } 85 | }; 86 | 87 | /* 88 | Note: If you add some new variable to your game that must be shared 89 | across server and client, add it both here and the server_send_update 90 | function in game.core.js to make sure it syncs 91 | 92 | Explanation: This function is at the center of the problem of 93 | networking -- everybody has different INSTANCES of the game. The 94 | server has its own, and both players have theirs too. This can get 95 | confusing because the server will update a variable, and the variable 96 | of the same name won't change in the clients (because they have a 97 | different instance of it). To make sure everybody's on the same page, 98 | the server regularly sends news about its variables to the clients so 99 | that they can update their variables to reflect changes. 100 | */ 101 | client_onserverupdate_received = function(data){ 102 | var player_host =this.players.self.host ? this.players.self : this.players.other; 103 | var player_client=this.players.self.host ? this.players.other : this.players.self; 104 | var game_player =this.players.self; 105 | 106 | // Update client versions of variables with data received from 107 | // server_send_update function in game.core.js 108 | if(data.hpos) 109 | player_host.pos = this.pos(data.hpos); 110 | if(data.cpos) 111 | player_client.pos = this.pos(data.cpos); 112 | 113 | player_host.points_earned = data.hpoi; 114 | player_client.points_earned = data.cpoi; 115 | player_host.curr_distance_moved = data.hcdm; 116 | player_client.curr_distance_moved = data.ccdm; 117 | this.targets.top.payoff = data.tcp; 118 | this.targets.bottom.payoff = data.bcp; 119 | this.targets.top.color = data.tcc; 120 | this.targets.bottom.color = data.bcc; 121 | this.condition = data.cond; 122 | this.draw_enabled = data.de; 123 | this.good2write = data.g2w; 124 | }; 125 | 126 | // This is where clients parse socket.io messages from the server. If 127 | // you want to add another event (labeled 'x', say), just add another 128 | // case here, then call 129 | 130 | // this.instance.player_host.send("s.x. ") 131 | 132 | // The corresponding function where the server parses messages from 133 | // clients, look for "server_onMessage" in game.server.js. 134 | client_onMessage = function(data) { 135 | 136 | var commands = data.split('.'); 137 | var command = commands[0]; 138 | var subcommand = commands[1] || null; 139 | var commanddata = commands[2] || null; 140 | 141 | switch(command) { 142 | case 's': //server message 143 | 144 | switch(subcommand) { 145 | case 'p' :// Permanent Message 146 | game.players.self.message = commanddata; 147 | break; 148 | case 'm' :// Temporary Message 149 | game.players.self.message = commanddata; 150 | var local_game = game; 151 | setTimeout(function(){local_game.players.self.message = '';}, 1000); 152 | break; 153 | case 'alert' : // Not in database, so you can't play... 154 | alert('You did not enter an ID'); 155 | window.location.replace('http://nodejs.org'); break; 156 | case 'h' : //host a game requested 157 | client_onhostgame(); break; 158 | case 'j' : //join a game requested 159 | client_onjoingame(); break; 160 | case 'b' : //blink title 161 | flashTitle("GO!"); break; 162 | case 'n' : //ready a game requested 163 | client_new_game(); break; 164 | case 'e' : //end game requested 165 | client_ondisconnect(); break; 166 | case 'a' : // other player changed angle 167 | game.players.other.angle = commanddata; break; 168 | game.players.other.draw(); 169 | } 170 | break; 171 | } 172 | }; 173 | 174 | // Restarts things on the client side. Necessary for iterated games. 175 | client_new_game = function() { 176 | if (game.games_remaining == 0) { 177 | // Redirect to exit survey 178 | var URL = './static/game_over.html'; 179 | URL += '?id=' + game.players.self.id; 180 | window.location.replace(URL); 181 | } else { 182 | // Decrement number of games remaining 183 | game.games_remaining -= 1; 184 | } 185 | 186 | var player_host = game.players.self.host ? game.players.self : game.players.other; 187 | var player_client = game.players.self.host ? game.players.other : game.players.self; 188 | 189 | // Reset angles 190 | player_host.angle = game.left_player_start_angle; 191 | player_client.angle = game.right_player_start_angle; 192 | 193 | //Update their destinations 194 | player_host.destination = null; 195 | player_client.destination = null; 196 | 197 | // They SHOULD see the targets information 198 | game.players.self.targets_enabled = true; 199 | game.players.other.targets_enabled = true; 200 | 201 | // Initiate countdown (with timeouts) 202 | if (game.condition == 'dynamic') 203 | client_countdown(); 204 | 205 | // Set text beneath player 206 | game.players.self.state = 'YOU'; 207 | game.players.other.state = ''; 208 | }; 209 | 210 | client_countdown = function() { 211 | game.players.self.message = ' Begin in 3...'; 212 | setTimeout(function(){game.players.self.message = ' Begin in 2...';}, 213 | 1000); 214 | setTimeout(function(){game.players.self.message = ' Begin in 1...';}, 215 | 2000); 216 | 217 | // At end of countdown, say "GO" and start using their real angle 218 | setTimeout(function(){ 219 | game.players.self.message = ' GO'; 220 | }, 3000); 221 | 222 | // Remove message text 223 | setTimeout(function(){game.players.self.message = '';}, 4000); 224 | } 225 | 226 | client_update = function() { 227 | //Clear the screen area 228 | game.ctx.clearRect(0,0,720,480); 229 | 230 | //draw help/information if required 231 | draw_info(game, "Instructions: Click where you want to go"); 232 | 233 | //Draw targets first, so in background 234 | draw_targets(game, game.players.self); 235 | 236 | //Draw opponent next 237 | draw_player(game, game.players.other); 238 | 239 | // Draw points scoreboard 240 | game.ctx.fillText("Money earned: $" + (game.players.self.points_earned / 100).fixed(2), 300, 15); 241 | game.ctx.fillText("Games remaining: " + game.games_remaining, 580, 15) 242 | 243 | //And then we draw ourself so we're always in front 244 | draw_player(game, game.players.self); 245 | }; 246 | 247 | 248 | /* 249 | The following code should NOT need to be changed 250 | */ 251 | 252 | // A window global for our game root variable. 253 | var game = {}; 254 | 255 | // When loading the page, we store references to our 256 | // drawing canvases, and initiate a game instance. 257 | window.onload = function(){ 258 | //Create our game client instance. 259 | game = new game_core(); 260 | 261 | //Connect to the socket.io server! 262 | client_connect_to_server(game); 263 | 264 | //Fetch the viewport 265 | game.viewport = document.getElementById('viewport'); 266 | 267 | //Adjust its size 268 | game.viewport.width = game.world.width; 269 | game.viewport.height = game.world.height; 270 | 271 | // Assign click handler ONCE, with the associated data. 272 | // Just sends click info to the client_on_click function, 273 | // since the things we care about haven't been defined yet 274 | $('#viewport').click(function(e){ 275 | e.preventDefault(); 276 | // e.pageX is relative to whole page -- we want 277 | // relative to GAME WORLD (i.e. viewport) 278 | var offset = $(this).offset(); 279 | var relX = e.pageX - offset.left; 280 | var relY = e.pageY - offset.top; 281 | client_on_click(game, relX, relY); 282 | }); 283 | 284 | //Fetch the rendering contexts 285 | game.ctx = game.viewport.getContext('2d'); 286 | 287 | //Set the draw style for the font 288 | game.ctx.font = '11px "Helvetica"'; 289 | 290 | //Finally, start the loop 291 | game.update(); 292 | }; 293 | 294 | // Associates callback functions corresponding to different socket messages 295 | client_connect_to_server = function(game) { 296 | 297 | //Store a local reference to our connection to the server 298 | game.socket = io.connect(); 299 | 300 | //When we connect, we are not 'connected' until we have a server id 301 | //and are placed in a game by the server. The server sends us a message for that. 302 | game.socket.on('connect', function(){ 303 | game.players.self.state = 'connecting'; 304 | }.bind(game)); 305 | 306 | //Sent when we are disconnected (network, server down, etc) 307 | game.socket.on('disconnect', client_ondisconnect.bind(game)); 308 | //Sent each tick of the server simulation. This is our authoritive update 309 | game.socket.on('onserverupdate', client_onserverupdate_received.bind(game)); 310 | //Handle when we connect to the server, showing state and storing id's. 311 | game.socket.on('onconnected', client_onconnected.bind(game)); 312 | //On message from the server, we parse the commands and send it to the handlers 313 | game.socket.on('message', client_onMessage.bind(game)); 314 | }; 315 | 316 | client_onconnected = function(data) { 317 | //The server responded that we are now in a game, 318 | //this lets us store the information about ourselves 319 | this.players.self.id = data.id; 320 | this.players.self.online = true; 321 | }; 322 | 323 | client_reset_positions = function() { 324 | 325 | var player_host =game.players.self.host ? game.players.self : game.players.other; 326 | var player_client=game.players.self.host ? game.players.other : game.players.self; 327 | 328 | //Host always spawns on the left facing inward. 329 | player_host.pos = game.left_player_start_pos; 330 | player_client.pos = game.right_player_start_pos; 331 | player_host.angle = game.left_player_start_angle; 332 | player_client.angle = game.right_player_start_angle; 333 | }; 334 | 335 | client_onjoingame = function() { 336 | //We are not the host 337 | game.players.self.host = false; 338 | game.players.other.pos = game.left_player_start_pos; 339 | game.players.self.pos = game.right_player_start_pos; 340 | game.players.other.start_angle = game.left_player_start_angle; 341 | game.players.self.start_angle = game.right_player_start_angle; 342 | game.players.other.color = game.players.other.info_color = game.left_player_color; 343 | game.players.self.color = game.players.self.info_color = game.right_player_color; 344 | 345 | //Make sure the positions match servers and other clients 346 | client_reset_positions(); 347 | 348 | }; //client_onjoingame 349 | 350 | // This function is triggered in a client when they first join and start a new game 351 | client_onhostgame = function() { 352 | //Set the flag that we are hosting, this helps us position respawns correctly 353 | game.players.self.host = true; 354 | game.players.self.pos = game.left_player_start_pos; 355 | game.players.other.pos = game.right_player_start_pos; 356 | game.players.self.start_angle = game.left_player_start_angle; 357 | game.players.other.start_angle = game.right_player_start_angle; 358 | game.players.self.color = game.players.self.info_color = game.left_player_color; 359 | game.players.other.color = game.players.other.info_color = game.right_player_color; 360 | 361 | //Update tags below players to display state 362 | game.players.self.state = 'waiting for other player to join'; 363 | game.players.other.state = 'not-connected'; 364 | 365 | //Make sure we start in the correct place as the host. 366 | client_reset_positions(); 367 | }; 368 | 369 | // Automatically registers whether user has switched tabs... 370 | (function() { 371 | document.hidden = hidden = "hidden"; 372 | 373 | // Standards: 374 | if (hidden in document) 375 | document.addEventListener("visibilitychange", onchange); 376 | else if ((hidden = "mozHidden") in document) 377 | document.addEventListener("mozvisibilitychange", onchange); 378 | else if ((hidden = "webkitHidden") in document) 379 | document.addEventListener("webkitvisibilitychange", onchange); 380 | else if ((hidden = "msHidden") in document) 381 | document.addEventListener("msvisibilitychange", onchange); 382 | // IE 9 and lower: 383 | else if ('onfocusin' in document) 384 | document.onfocusin = document.onfocusout = onchange; 385 | // All others: 386 | else 387 | window.onpageshow = window.onpagehide = window.onfocus 388 | = window.onblur = onchange; 389 | })(); 390 | 391 | function onchange (evt) { 392 | var v = 'visible', h = 'hidden', 393 | evtMap = { 394 | focus:v, focusin:v, pageshow:v, blur:h, focusout:h, pagehide:h 395 | }; 396 | evt = evt || window.event; 397 | if (evt.type in evtMap) { 398 | document.body.className = evtMap[evt.type]; 399 | } else { 400 | document.body.className = evt.target.hidden ? "hidden" : "visible"; 401 | } 402 | visible = document.body.className; 403 | game.socket.send("h." + document.body.className); 404 | }; 405 | 406 | // Flashes title to notify user that game has started 407 | (function () { 408 | 409 | var original = document.title; 410 | var timeout; 411 | 412 | window.flashTitle = function (newMsg, howManyTimes) { 413 | function step() { 414 | document.title = (document.title == original) ? newMsg : original; 415 | if (visible == "hidden") { 416 | timeout = setTimeout(step, 500); 417 | } else { 418 | document.title = original; 419 | } 420 | }; 421 | cancelFlashTitle(timeout); 422 | step(); 423 | }; 424 | 425 | window.cancelFlashTitle = function (timeout) { 426 | clearTimeout(timeout); 427 | document.title = original; 428 | }; 429 | 430 | }()); 431 | -------------------------------------------------------------------------------- /game.core.js: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Sven "FuzzYspo0N" Bergström, 2 | 2013 Robert XD Hawkins 3 | 4 | written by : http://underscorediscovery.com 5 | written for : http://buildnewgames.com/real-time-multiplayer/ 6 | 7 | substantially modified for collective behavior experiments on the web 8 | 9 | MIT Licensed. 10 | */ 11 | 12 | /* 13 | The main game class. This gets created on both server and 14 | client. Server creates one for each game that is hosted, and each 15 | client creates one for itself to play the game. When you set a 16 | variable, remember that it's only set in that instance. 17 | */ 18 | var game_core = function(game_instance){ 19 | 20 | // Define some variables specific to our game to avoid 21 | // 'magic numbers' elsewhere 22 | this.left_player_start_angle = 90; 23 | this.right_player_start_angle = 270; 24 | this.left_player_start_pos = { x:180, y:240 } 25 | this.right_player_start_pos = { x:540, y:240 } 26 | this.left_player_color = '#2288cc'; 27 | this.right_player_color = '#cc0000'; 28 | this.big_payoff = 4 29 | this.little_payoff = 1 30 | 31 | // Create targets and assign fixed position 32 | this.targets = { 33 | top : new target({x : 360, y : 120}), 34 | bottom : new target({x : 360, y : 360})}; 35 | 36 | //Store the instance, if any (passed from game.server.js) 37 | this.instance = game_instance; 38 | 39 | //Store a flag if we are the server instance 40 | this.server = this.instance !== undefined; 41 | 42 | //Store a flag if a newgame has been initiated. 43 | //Used to prevent the loop from continuing to start newgames during timeout. 44 | this.newgame_initiated_flag = false; 45 | 46 | //Dimensions of world -- Used in collision detection, etc. 47 | this.world = {width : 720, height : 480}; 48 | 49 | //We create a player set, passing them the game that is running 50 | //them, as well. Both the server and the clients need separate 51 | //instances of both players, but the server has more information 52 | //about who is who. Clients will be given this info later. 53 | if(this.server) { 54 | this.players = { 55 | self : new game_player(this,this.instance.player_host), 56 | other : new game_player(this,this.instance.player_client)}; 57 | this.game_clock = 0; 58 | 59 | } else { 60 | this.players = { 61 | self : new game_player(this), 62 | other : new game_player(this)}; 63 | } 64 | 65 | //The speed at which the clients move (e.g. 10px/tick) 66 | this.global_speed = 10; 67 | 68 | //Set to true if we want players to act under noise 69 | this.noise = false; 70 | 71 | //How often the players move forward px in ms. 72 | this.tick_frequency = 666; 73 | 74 | //Number of games left 75 | this.games_remaining = 50; 76 | 77 | //Players will replay over and over, so we keep track of which number we're on, 78 | //to print out to data file 79 | this.game_number = 1; 80 | 81 | //If draw_enabled is true, players will see their true angle. If it's false, 82 | //players can set their destination and keep it hidden from the other player. 83 | this.draw_enabled = true; 84 | 85 | //Start a physics loop, this is separate to the rendering 86 | //as this happens at a fixed frequency. Capture the id so 87 | //we can shut it down at end. 88 | this.physics_interval_id = this.create_physics_simulation(); 89 | }; 90 | 91 | /* The player class 92 | A simple class to maintain state of a player on screen, 93 | as well as to draw that state when required. 94 | */ 95 | var game_player = function( game_instance, player_instance ) { 96 | //Store the instance, if any 97 | this.instance = player_instance; 98 | this.game = game_instance; 99 | 100 | //Set up initial values for our state information 101 | this.size = { x:16, y:16, hx:8, hy:8 }; 102 | this.state = 'not-connected'; 103 | this.visible = "visible"; // Tracks whether client is watching game 104 | this.message = ''; 105 | 106 | this.info_color = 'rgba(255,255,255,0)'; 107 | this.id = ''; 108 | this.targets_enabled = false; // If true, will display targets 109 | this.destination = null; // Last place client clicked 110 | this.points_earned = 0; // keep track of number of points 111 | this.speed = 0; 112 | this.curr_distance_moved = 0; 113 | 114 | //These are used in moving us around later 115 | this.old_state = {pos:{x:0,y:0}}; 116 | this.cur_state = {pos:{x:0,y:0}}; 117 | 118 | //The world bounds we are confined to 119 | this.pos_limits = { 120 | x_min: this.size.hx, 121 | x_max: this.game.world.width - this.size.hx, 122 | y_min: this.size.hy, 123 | y_max: this.game.world.height - this.size.hy 124 | }; 125 | 126 | //For client instances, we'll set up these variables in client_onhostgame 127 | // and client_onjoingame 128 | if(player_instance) { // Host on left 129 | this.pos = this.game.left_player_start_pos; 130 | this.color = this.game.left_player_color; 131 | this.angle = this.start_angle = this.game.left_player_start_angle; 132 | } else { // other on right 133 | this.pos = this.game.right_player_start_pos; 134 | this.color = this.game.right_player_color; 135 | this.angle = this.start_angle = this.game.right_player_start_angle; 136 | } 137 | }; 138 | 139 | // The target is the payoff-bearing goal. We construct it with these properties 140 | var target = function(location) { 141 | this.payoff = 1; 142 | this.location = location; 143 | this.visited = false; 144 | this.radius = 10; 145 | this.outer_radius = this.radius + 35; 146 | this.color = 'white'; 147 | }; 148 | 149 | // server side we set the 'game_core' class to a global type, so that 150 | // it can use it in other files (specifically, game.server.js) 151 | if('undefined' != typeof global) { 152 | module.exports = global.game_core = game_core; 153 | } 154 | 155 | 156 | // Notifies clients of changes on the server side. Server totally 157 | // handles position and points. 158 | game_core.prototype.server_send_update = function(){ 159 | 160 | //Make a snapshot of the current state, for updating the clients 161 | this.laststate = { 162 | hpos: this.players.self.pos, //'host position', the game creators position 163 | cpos: this.players.other.pos, //'client position', the person that joined, their position 164 | hpoi: this.players.self.points_earned, //'host points' 165 | cpoi: this.players.other.points_earned, //'client points' 166 | hcdm: this.players.self.curr_distance_moved, //'host speed' 167 | ccdm: this.players.other.curr_distance_moved,//'client speed' 168 | tcc : this.targets.top.color, //'top target color' 169 | bcc : this.targets.bottom.color, //'bottom target color' 170 | tcp : this.targets.top.payoff, //'top target payoff' 171 | bcp : this.targets.bottom.payoff, //'bottom target payoff' 172 | cond: this.condition, //dynamic or ballistic? 173 | de : this.draw_enabled, // true to see angle 174 | g2w : this.good2write, // true when game's started 175 | }; 176 | //Send the snapshot to the 'host' player 177 | if(this.players.self.instance) 178 | this.players.self.instance.emit( 'onserverupdate', this.laststate ); 179 | 180 | //Send the snapshot to the 'client' player 181 | if(this.players.other.instance) 182 | this.players.other.instance.emit( 'onserverupdate', this.laststate ); 183 | }; 184 | 185 | // This is called every 666ms and simulates the world state. This is 186 | // where we update positions and check whether targets have been reached. 187 | game_core.prototype.server_update_physics = function() { 188 | 189 | host_player = this.players.self; 190 | other_player = this.players.other; 191 | top_target = this.targets.top; 192 | bottom_target = this.targets.bottom; 193 | 194 | // If a player has reached their destination, stop. Have to put 195 | // other wrapper because destination is null until player clicks 196 | // Must use distance from, since the player's position is at the 197 | // center of the body, which is long. As long as any part of the 198 | // body is where it should be, we want them to stop. 199 | if (host_player.destination) { 200 | if (this.distance_between(host_player.pos,host_player.destination) < 8) 201 | host_player.speed = 0; 202 | } 203 | if (other_player.destination) { 204 | if (this.distance_between(other_player.pos,other_player.destination) < 8) 205 | other_player.speed = 0; 206 | } 207 | 208 | // Impose Gaussian noise on movement to create uncertainty 209 | // Recall base speed is 10, so to avoid moving backward, need that to be rare. 210 | // Set the standard deviation of the noise distribution. 211 | if (this.noise) { 212 | var noise_sd = 4; 213 | var nd = new NormalDistribution(noise_sd,0); 214 | 215 | // If a player isn't moving, no noise. Otherwise they'll wiggle in place. 216 | // Use !good2write as a proxy for the 'waiting room' state 217 | if (host_player.speed == 0 || !this.good2write) 218 | host_player.noise = 0; 219 | else 220 | host_player.noise = nd.sample(); 221 | 222 | if (other_player.speed == 0 || !this.good2write) 223 | other_player.noise = 0; 224 | else 225 | other_player.noise = nd.sample(); 226 | } else { 227 | host_player.noise = 0; 228 | other_player.noise = 0; 229 | } 230 | 231 | //Handle player one movement (calculate using polar coordinates) 232 | r1 = host_player.curr_distance_moved = host_player.speed + host_player.noise; 233 | theta1 = (host_player.angle - 90) * Math.PI / 180; 234 | host_player.old_state.pos = this.pos( host_player.pos ); 235 | var new_dir = {x : r1 * Math.cos(theta1), 236 | y : r1 * Math.sin(theta1)}; 237 | host_player.pos = this.v_add( host_player.old_state.pos, new_dir ); 238 | 239 | //Handle player two movement 240 | r2 = other_player.curr_distance_moved = other_player.speed + other_player.noise; 241 | theta2 = (other_player.angle - 90) * Math.PI / 180; 242 | other_player.old_state.pos = this.pos( other_player.pos ); 243 | var other_new_dir = {x : r2 * Math.cos(theta2), 244 | y : r2 * Math.sin(theta2)}; 245 | other_player.pos = this.v_add( other_player.old_state.pos, other_new_dir); 246 | 247 | //Keep the players in the world 248 | this.check_collision( host_player ); 249 | this.check_collision( other_player ); 250 | 251 | // Check whether either plays has reached a target 252 | // Make sure this can't happen before both players have connected 253 | if (this.good2write) { 254 | this.server_check_for_payoff(host_player, other_player, 'host'); 255 | this.server_check_for_payoff(other_player, host_player, 'other'); 256 | } 257 | 258 | // For ballistic version, if game hasn't started yet, check whether destinations 259 | // are valid. If so, start game! 260 | var condition1 = false; 261 | var condition2 = false; 262 | if (!this.good2write && this.instance.player_client && this.condition == 'ballistic') { 263 | if (host_player.destination) { 264 | condition1 = (this.distance_between(host_player.destination, 265 | top_target.location) < 10 || 266 | this.distance_between(host_player.destination, 267 | bottom_target.location) < 10); 268 | } 269 | if (other_player.destination) { 270 | condition2 = (this.distance_between(other_player.destination, 271 | top_target.location) < 10 || 272 | this.distance_between(other_player.destination, 273 | bottom_target.location) < 10); 274 | } 275 | // define some situations once destinations have been set 276 | if (condition1 && condition2) { 277 | this.instance.player_host.send('s.m. GO!'); 278 | this.instance.player_client.send('s.m. GO!'); 279 | this.good2write = true; 280 | this.draw_enabled = true; 281 | this.players.self.speed = this.global_speed; 282 | this.players.other.speed = this.global_speed; 283 | this.game_clock = 0; 284 | } else if (condition1 && !condition2) { 285 | this.instance.player_host.send('s.p. Waiting for other player'); 286 | this.instance.player_client.send('s.p. Choose a target.'); 287 | } else if (!condition1 && condition2) { 288 | this.instance.player_client.send('s.p. Waiting for other player'); 289 | this.instance.player_host.send('s.p. Choose a target.'); 290 | } else if (this.instance.player_client) { 291 | this.instance.player_host.send('s.p. Choose a target'); 292 | this.instance.player_client.send('s.p. Choose a target'); 293 | } 294 | } 295 | }; 296 | 297 | // A lot of our specific game logic is buried in this function. The dictates when 298 | // players get payoffs (i.e. if they're close, the other player is far, and the 299 | // target hasn't been reached yet). If you want to change the "win" condition, it's here. 300 | game_core.prototype.server_check_for_payoff = function(player1, player2, whoisplayer1){ 301 | // Check whether players have reached 302 | var top_target = this.targets.top; 303 | var bottom_target = this.targets.bottom; 304 | 305 | // Check whether either target has been reached 306 | this.check_target_reached(top_target,bottom_target,player1,player2,whoisplayer1); 307 | this.check_target_reached(bottom_target,top_target,player1,player2,whoisplayer1); 308 | 309 | // If both targets have been marked as visited, we tell the server 310 | // we're ready to start a new game. But we only do it once, thus the flag. 311 | if ((top_target.visited 312 | && bottom_target.visited 313 | && !this.newgame_initiated_flag)) { 314 | 315 | console.log("Both targets visited..."); 316 | this.players.self.speed = 0; 317 | this.players.other.speed = 0; 318 | this.newgame_initiated_flag = true; 319 | var local_this = this; 320 | // Need to wait a second before resetting so players can see what happened 321 | setTimeout(function(){ 322 | // Keep track of which game we're on 323 | local_this.game_number += 1; 324 | local_this.newgame_initiated_flag = false; 325 | local_this.server_newgame(); 326 | }, 1500); 327 | } 328 | }; 329 | 330 | // Messy helper function for our specific game -- implements 'end-game' logic 331 | game_core.prototype.check_target_reached = function(main_target, other_target,player1,player2,whoisplayer1) { 332 | // If player1 reaches the top target before player2, reward them and 333 | // end the game 334 | if (this.distance_between(player1.pos,main_target.location) < main_target.radius + player1.size.hy 335 | && !main_target.visited 336 | && this.distance_between(player2.pos,main_target.location) > main_target.outer_radius + player2.size.hy) { 337 | main_target.visited = true; 338 | main_target.color = player1.color; 339 | player1.points_earned += main_target.payoff; 340 | other_target.visited = true; 341 | other_target.color = player2.color; 342 | player2.points_earned += other_target.payoff; 343 | if (whoisplayer1 == 'host') { 344 | this.instance.player_host.send('s.m. You earned ' + main_target.payoff + '\xA2'); 345 | this.instance.player_client.send('s.m. You earned ' + other_target.payoff + '\xA2'); 346 | } else if (whoisplayer1 == 'other') { 347 | this.instance.player_host.send('s.m. You earned ' + other_target.payoff + '\xA2'); 348 | this.instance.player_client.send('s.m. You earned ' + main_target.payoff + '\xA2'); 349 | } 350 | // If it's a tie, no one wins and game over (i.e. set both targets to visited) 351 | } else if(this.distance_between(player1.pos,main_target.location) < main_target.radius + player1.size.hy 352 | && !main_target.visited 353 | && this.distance_between(player2.pos, main_target.location) < main_target.outer_radius + player2.size.hy) { 354 | // Let them know they tied... 355 | this.instance.player_client.send('s.m.Tie! No money awarded!'); 356 | this.instance.player_host.send('s.m.Tie! No money awarded!'); 357 | main_target.visited = true; 358 | other_target.visited = true; 359 | main_target.color = 'black'; 360 | } 361 | }; 362 | 363 | // Every second, we print out a bunch of information to a file in a 364 | // "data" directory. We keep EVERYTHING so that we 365 | // can analyze the data to an arbitrary precision later on. 366 | game_core.prototype.writeData = function() { 367 | // Some funny business going on with angles being negative, so we correct for that 368 | var host_angle_to_write = this.players.self.angle; 369 | var other_angle_to_write = this.players.other.angle; 370 | var file_path ; 371 | if (this.players.self.angle < 0) 372 | host_angle_to_write = parseInt(this.players.self.angle, 10) + 360; 373 | if (this.players.other.angle < 0) 374 | other_angle_to_write = parseInt(this.players.other.angle, 10) + 360; 375 | if (this.condition == "ballistic") 376 | file_path = "data/ballistic/game_" + this.game_id + ".csv"; 377 | else if (this.condition == "dynamic") 378 | file_path = "data/dynamic/game_" + this.game_id + ".csv"; 379 | 380 | // Write data for the host player 381 | var host_data_line = String(this.game_number) + ','; 382 | host_data_line += String(this.game_clock) + ','; 383 | host_data_line += this.best_target_string + ','; 384 | host_data_line += "host,"; 385 | host_data_line += this.players.self.visible + ','; 386 | host_data_line += this.players.self.pos.x + ','; 387 | host_data_line += this.players.self.pos.y + ','; 388 | host_data_line += host_angle_to_write + ','; 389 | host_data_line += this.players.self.points_earned + ','; 390 | host_data_line += this.players.self.noise.fixed(2) + ','; 391 | this.fs.appendFile(file_path, 392 | String(host_data_line) + "\n", 393 | function (err) { 394 | if(err) throw err; 395 | }); 396 | console.log("Wrote: " + host_data_line); 397 | 398 | // Write data for the other player 399 | var other_data_line = String(this.game_number) + ','; 400 | other_data_line += String(this.game_clock) + ','; 401 | other_data_line += this.best_target_string + ','; 402 | other_data_line += "other,"; 403 | other_data_line += this.players.other.visible + ','; 404 | other_data_line += this.players.other.pos.x + ','; 405 | other_data_line += this.players.other.pos.y + ','; 406 | other_data_line += other_angle_to_write + ','; 407 | other_data_line += this.players.other.points_earned + ','; 408 | other_data_line += this.players.other.noise.fixed(2) + ','; 409 | this.fs.appendFile(file_path, 410 | String(other_data_line) + "\n", 411 | function (err) { 412 | if(err) throw err; 413 | }); 414 | console.log("Wrote: " + other_data_line); 415 | }; 416 | 417 | // This also gets called at the beginning of every new game. 418 | // It randomizes payoffs, resets colors, and makes the targets "fresh and 419 | // available" again. 420 | game_core.prototype.server_reset_targets = function() { 421 | 422 | top_target = this.targets.top; 423 | bottom_target = this.targets.bottom; 424 | top_target.color = bottom_target.color = 'white'; 425 | top_target.visited = bottom_target.visited = false; 426 | 427 | // Randomly reset payoffs 428 | var r = Math.floor(Math.random() * 2); 429 | 430 | if (r == 0) { 431 | this.targets.top.payoff = this.little_payoff; 432 | this.targets.bottom.payoff = this.big_payoff; 433 | this.best_target_string = 'bottom'; 434 | } else { 435 | this.targets.top.payoff = this.big_payoff; 436 | this.targets.bottom.payoff = this.little_payoff; 437 | this.best_target_string = 'top'; 438 | } 439 | }; 440 | 441 | 442 | // This is a really important function -- it gets called when a round 443 | // has been completed, and updates the database with how much money 444 | // people have made so far. This way, if somebody gets disconnected or 445 | // something, we'll still know what to pay them. 446 | game_core.prototype.server_newgame = function() { 447 | if (this.use_db) { // set in game.server.js 448 | console.log("USING DB"); 449 | var sql1 = 'UPDATE game_participant SET bonus_pay = ' + 450 | (this.players.self.points_earned / 100).toFixed(2); 451 | sql1 += ' WHERE workerId = "' + this.players.self.instance.userid + '"'; 452 | this.mysql_conn.query(sql1, function(err, rows, fields) { 453 | if (err) throw err; 454 | console.log('Updated sql with command: ', sql1); 455 | }); 456 | var sql2 = 'UPDATE game_participant SET bonus_pay = ' + 457 | (this.players.other.points_earned / 100).toFixed(2); 458 | sql2 += ' WHERE workerId = "' + this.players.other.instance.userid + '"'; 459 | this.mysql_conn.query(sql2, function(err, rows, fields) { 460 | if (err) throw err; 461 | console.log('Updated sql with command: ', sql2); 462 | }); 463 | } 464 | 465 | // Update number of games remaining 466 | this.games_remaining -= 1; 467 | 468 | // Don't want players moving during countdown 469 | this.players.self.speed = 0; 470 | this.players.other.speed = 0; 471 | 472 | // Tell the server about targets being enabled, so it can use it as a flag elsewhere 473 | this.players.self.targets_enabled = true; 474 | this.players.other.targets_enabled = true; 475 | 476 | // Don't want to write to file during countdown -- too confusing 477 | this.good2write = false; 478 | 479 | // Reset destinations 480 | this.players.self.destination = null; 481 | this.players.other.destination = null; 482 | 483 | // Don't want people signalling until after countdown/validated input 484 | this.draw_enabled = false; 485 | 486 | //Reset positions 487 | this.server_reset_positions(); 488 | 489 | //Reset targets 490 | this.server_reset_targets(); 491 | 492 | //Tell clients about it so they can call their newgame procedure (which does countdown) 493 | this.instance.player_client.send('s.n.'); 494 | this.instance.player_host.send('s.n.'); 495 | 496 | var local_this = this; 497 | 498 | // For the dynamic version, we want there to be a countdown. 499 | // For the ballistic version, the game won't start until both players have 500 | // made valid choices so the function must be checked over and over. It's in 501 | // server_update_physics. 502 | if(this.condition == "dynamic"){ 503 | // After countdown, players start moving, we start writing data, and clock resets 504 | setTimeout(function(){ 505 | local_this.good2write = true; 506 | local_this.draw_enabled = true; 507 | console.log("GOOOOOO!"); 508 | local_this.players.self.speed = local_this.global_speed; 509 | local_this.players.other.speed = local_this.global_speed; 510 | local_this.game_clock = 0; 511 | }, 3000); 512 | } 513 | }; 514 | 515 | /* 516 | The following code should NOT need to be changed 517 | */ 518 | 519 | //Main update loop -- don't worry about it 520 | game_core.prototype.update = function() { 521 | 522 | //Update the game specifics 523 | if(!this.server) 524 | client_update(); 525 | else 526 | this.server_send_update(); 527 | 528 | //schedule the next update 529 | this.updateid = window.requestAnimationFrame(this.update.bind(this), 530 | this.viewport); 531 | }; 532 | 533 | // This gets called every iteration of a new game to reset positions 534 | game_core.prototype.server_reset_positions = function() { 535 | 536 | var player_host = this.players.self.host ? this.players.self : this.players.other; 537 | var player_client = this.players.self.host ? this.players.other : this.players.self; 538 | 539 | player_host.pos = this.right_player_start_pos; 540 | player_client.pos = this.left_player_start_pos; 541 | 542 | player_host.angle = this.right_player_start_angle; 543 | player_client.angle = this.left_player_start_angle; 544 | 545 | }; 546 | 547 | //For the server, we need to cancel the setTimeout that the polyfill creates 548 | game_core.prototype.stop_update = function() { 549 | 550 | // Stop old game from animating anymore 551 | window.cancelAnimationFrame( this.updateid ); 552 | 553 | // Stop loop still running from old game (if someone is still left, 554 | // game_server.endGame will start a new game for them). 555 | clearInterval(this.physics_interval_id); 556 | }; 557 | 558 | game_core.prototype.create_physics_simulation = function() { 559 | return setInterval(function(){ 560 | this.update_physics(); 561 | this.game_clock += 1; 562 | if (this.good2write) { 563 | this.writeData(); 564 | } 565 | }.bind(this), this.tick_frequency); 566 | }; 567 | 568 | game_core.prototype.update_physics = function() { 569 | if(this.server) 570 | this.server_update_physics(); 571 | }; 572 | 573 | //Prevents people from leaving the arena 574 | game_core.prototype.check_collision = function( item ) { 575 | //Left wall. 576 | if(item.pos.x <= item.pos_limits.x_min) 577 | item.pos.x = item.pos_limits.x_min; 578 | 579 | //Right wall 580 | if(item.pos.x >= item.pos_limits.x_max ) 581 | item.pos.x = item.pos_limits.x_max; 582 | 583 | //Roof wall. 584 | if(item.pos.y <= item.pos_limits.y_min) 585 | item.pos.y = item.pos_limits.y_min; 586 | 587 | //Floor wall 588 | if(item.pos.y >= item.pos_limits.y_max ) 589 | item.pos.y = item.pos_limits.y_max; 590 | 591 | //Fixed point helps be more deterministic 592 | item.pos.x = item.pos.x.fixed(4); 593 | item.pos.y = item.pos.y.fixed(4); 594 | }; 595 | 596 | // Just in case we want to draw from Gaussian to get noise on movement... 597 | function NormalDistribution(sigma, mu) { 598 | return new Object({ 599 | sigma: sigma, 600 | mu: mu, 601 | sample: function() { 602 | var res; 603 | if (this.storedDeviate) { 604 | res = this.storedDeviate * this.sigma + this.mu; 605 | this.storedDeviate = null; 606 | } else { 607 | var dist = Math.sqrt(-1 * Math.log(Math.random())); 608 | var angle = 2 * Math.PI * Math.random(); 609 | this.storedDeviate = dist*Math.cos(angle); 610 | res = dist*Math.sin(angle) * this.sigma + this.mu; 611 | } 612 | return res; 613 | }, 614 | sampleInt : function() { 615 | return Math.round(this.sample()); 616 | } 617 | }); 618 | } 619 | 620 | /* Helper functions for the game code: 621 | Here we have some common maths and game related code to make 622 | working with 2d vectors easy, as well as some helpers for 623 | rounding numbers to fixed point. 624 | */ 625 | 626 | // (4.22208334636).fixed(n) will return fixed point value to n places, default n = 3 627 | Number.prototype.fixed = function(n) { n = n || 3; return parseFloat(this.toFixed(n)); }; 628 | 629 | // Takes two location objects and computes the distance between them 630 | game_core.prototype.distance_between = function(obj1, obj2) { 631 | x1 = obj1.x; 632 | x2 = obj2.x; 633 | y1 = obj1.y; 634 | y2 = obj2.y; 635 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 636 | }; 637 | 638 | //copies a 2d vector like object from one to another 639 | game_core.prototype.pos = function(a) { return {x:a.x,y:a.y}; }; 640 | 641 | //Add a 2d vector with another one and return the resulting vector 642 | game_core.prototype.v_add = function(a,b) { return { x:(a.x+b.x).fixed(), y:(a.y+b.y).fixed() }; }; 643 | 644 | //The remaining code runs the update animations 645 | 646 | //The main update loop runs on requestAnimationFrame, 647 | //Which falls back to a setTimeout loop on the server 648 | //Code below is from Three.js, and sourced from links below 649 | 650 | //http://paulirish.com/2011/requestanimationframe-for-smart-animating/ 651 | //http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating 652 | 653 | //requestAnimationFrame polyfill by Erik Möller 654 | //fixes from Paul Irish and Tino Zijdel 655 | var frame_time = 60/1000; // run the local game at 16ms/ 60hz 656 | if('undefined' != typeof(global)) frame_time = 45; //on server we run at 45ms, 22hz 657 | 658 | ( function () { 659 | 660 | var lastTime = 0; 661 | var vendors = [ 'ms', 'moz', 'webkit', 'o' ]; 662 | 663 | for ( var x = 0; x < vendors.length && !window.requestAnimationFrame; ++ x ) { 664 | window.requestAnimationFrame = window[ vendors[ x ] + 'RequestAnimationFrame' ]; 665 | window.cancelAnimationFrame = window[ vendors[ x ] + 'CancelAnimationFrame' ] || window[ vendors[ x ] + 'CancelRequestAnimationFrame' ]; 666 | } 667 | 668 | if ( !window.requestAnimationFrame ) { 669 | window.requestAnimationFrame = function ( callback, element ) { 670 | var currTime = Date.now(), timeToCall = Math.max( 0, frame_time - ( currTime - lastTime ) ); 671 | var id = window.setTimeout( function() { callback( currTime + timeToCall ); }, timeToCall ); 672 | lastTime = currTime + timeToCall; 673 | return id; 674 | }; 675 | } 676 | 677 | if ( !window.cancelAnimationFrame ) { 678 | window.cancelAnimationFrame = function ( id ) { clearTimeout( id ); }; 679 | } 680 | }() ); 681 | --------------------------------------------------------------------------------