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 | 
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 |
--------------------------------------------------------------------------------