├── .gitignore ├── README.md ├── client.js ├── clock.js ├── entity.js ├── event.js ├── index.html ├── intersect-cauchy.js ├── minkowski.js ├── package.json ├── server.js ├── test-hit.js ├── test ├── cauchy.js ├── collide.js └── trajectory.js ├── trajectory.js ├── visualize.html ├── visualize.js ├── world.js └── worldvis.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules/* 16 | *.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Capture the flag demo 2 | ===================== 3 | 4 | This is a multiplayer capture the flag game which uses space-time causality (for asynchronous execution) and local perception filters to hide latency. For more information, see the following blog posts: 5 | 6 | * Replication in networked games: [Part 1](http://0fps.net/2014/02/10/replication-in-networked-games-overview-part-1/) [Part 2](http://0fps.net/2014/02/17/replication-in-networked-games-latency-part-2/) [Part 3](http://0fps.net/2014/02/26/replication-in-networked-games-spacetime-consistency-part-3/) [Part 4](http://0fps.net/2014/03/09/replication-in-network-games-bandwidth-part-4/) 7 | 8 | ## First time set up 9 | 10 | #### 1. Install node.js, npm and git 11 | 12 | You can get [node.js here](http://nodejs.org/download/), and this website (github.com) has instructions on how to set up git on various systems. 13 | 14 | #### 2. Clone the repo 15 | 16 | Open up a shell and type: 17 | 18 | ``` 19 | git clone https://github.com/mikolalysenko/lpf-ctf 20 | ``` 21 | 22 | #### 3. Install all dependencies 23 | 24 | Go into the folder that was just cloned and type: 25 | 26 | ``` 27 | npm install 28 | ``` 29 | 30 | #### 4. Start the server 31 | 32 | Again from the same folder, type: 33 | 34 | ``` 35 | npm start 36 | ``` 37 | 38 | #### 5. Open a page to connect to it 39 | 40 | Open up a browser tab for `localhost:8080` and you should be good to go. 41 | 42 | ## Using the software 43 | 44 | To play the game, use the arrow keys to move your player and spacebar to shoot. The shift key slows down time, though using this excessively can cause your client to drop. 45 | 46 | To view the history of the game, open the `/visualize.html` file which is hosted by the server. -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var url = require('parsed-url') 4 | var createCanvas = require('canvas-testbed') 5 | var vkey = require('vkey') 6 | var colormap = require('colormap') 7 | var createWorld = require('./world') 8 | var createEvent = require('./event') 9 | var intersectCauchy = require('./intersect-cauchy') 10 | 11 | var socket = new WebSocket('ws://' + url.host) 12 | var netLag = 0.1 13 | var netMass = 0.05 14 | var lagMass = 0.001 15 | var world, player, heartbeatInterval, deadTimeout 16 | 17 | var TEST_BULLET_TIME = false 18 | 19 | var colors = colormap({ 20 | colormap: 'jet', 21 | nshades: 256, 22 | format: 'rgb' 23 | }) 24 | 25 | var SYMBOLS = { 26 | 'flag': ['⚑',0,0.25], 27 | 'player': ['☺',0,0.25], 28 | 'bullet': ['⁍',0,0.25], 29 | 'score': ['',0,0] 30 | } 31 | 32 | socket.onmessage = function(socketEvent) { 33 | var event = createEvent.parse(socketEvent.data) 34 | if(!event) { 35 | console.warn('bad event:', socketEvent.data) 36 | return 37 | } 38 | switch(event.type) { 39 | case 'init': 40 | initWorld(event.world, event.id) 41 | break 42 | 43 | case 'join': 44 | case 'move': 45 | case 'shoot': 46 | case 'leave': 47 | 48 | //world.clock.interpolate(event.now) 49 | world.handleEvent(event) 50 | 51 | var entity = world._entityIndex[event.id] 52 | entity._netLag = world.maxBulletTime 53 | break 54 | 55 | case 'sync': 56 | netLag = (1.0-netMass) * netLag + netMass * (world.clock.wall() - event.then) 57 | 58 | world.clock.interpolate(event.now) 59 | break 60 | } 61 | } 62 | 63 | socket.onerror = socket.onclose = function() { 64 | console.log('lost connection to server') 65 | world = null 66 | } 67 | 68 | function initWorld(initState, id) { 69 | world = createWorld.fromJSON(initState) 70 | world.debugTrace = true 71 | player = world._entityIndex[id] 72 | netLag = 2.0*world.syncRate 73 | 74 | setInterval(heartbeat, 1000.0 * world.syncRate) 75 | 76 | var gadget = createCanvas(render, { 77 | context: '2d', 78 | retina: false 79 | }) 80 | 81 | //Hook up input handlers 82 | var keyState = { 83 | '': false, 84 | '': false, 85 | '': false, 86 | '': false, 87 | '': false, 88 | '': false 89 | } 90 | 91 | var shootDirection = [world.bulletSpeed,0] 92 | 93 | function updateMovement() { 94 | if(!world) { 95 | return 96 | } 97 | 98 | if(TEST_BULLET_TIME || keyState['']) { 99 | if(!world.clock._bulletTime && (world.clock.bulletDelay()+netLag) < 0.1*world.maxBulletTime) { 100 | console.log('bullet time on') 101 | world.clock.startBulletTime(0.5) 102 | } 103 | } else { 104 | if(world.clock._bulletTime) { 105 | console.log('bullet time off') 106 | world.clock.stopBulletTime() 107 | } 108 | } 109 | 110 | //Compute new movement event 111 | var v = [0,0] 112 | if(keyState['']) { 113 | v[0] -= 1 114 | } 115 | if(keyState['']) { 116 | v[0] += 1 117 | } 118 | if(keyState['']) { 119 | v[1] -= 1 120 | } 121 | if(keyState['']) { 122 | v[1] += 1 123 | } 124 | 125 | //Normalize velocity 126 | var vl = Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)) 127 | if(vl > 1e-4) { 128 | shootDirection[0] = world.bulletSpeed*v[0]/vl 129 | shootDirection[1] = world.bulletSpeed*v[1]/vl 130 | vl = world.playerSpeed / vl 131 | } 132 | v[0] *= vl 133 | v[1] *= vl 134 | 135 | //Apply movement event 136 | var t = world.clock.now() 137 | var cv = player.trajectory.v(t) 138 | if(cv && Math.abs(v[0]-cv[0]) + Math.abs(v[1]-cv[1]) > 1e-4) { 139 | var moveEvent = createEvent({ 140 | type: 'move', 141 | t: t, 142 | now: world.clock.wall(), 143 | id: player.id, 144 | x: player.trajectory.x(t), 145 | v: v 146 | }) 147 | world.handleEvent(moveEvent) 148 | socket.send(JSON.stringify(moveEvent)) 149 | } 150 | 151 | if(keyState['']) { 152 | if(player.data.lastShot + world.shootRate < t) { 153 | var shootEvent = createEvent({ 154 | type: 'shoot', 155 | t: t, 156 | now: world.clock.wall(), 157 | id: player.id, 158 | x: player.trajectory.x(t), 159 | v: shootDirection 160 | }) 161 | world.handleEvent(shootEvent) 162 | socket.send(JSON.stringify(shootEvent)) 163 | } 164 | } 165 | } 166 | 167 | document.body.addEventListener('keydown', function(event) { 168 | var key = vkey[event.keyCode] 169 | if(key in keyState) { 170 | keyState[key] = true 171 | } 172 | if(key === 'A') { 173 | //TEST_BULLET_TIME = true 174 | } 175 | updateMovement() 176 | }) 177 | 178 | document.body.addEventListener('keyup', function(event) { 179 | var key = vkey[event.keyCode] 180 | if(key in keyState) { 181 | keyState[key] = false 182 | } 183 | 184 | updateMovement() 185 | }) 186 | 187 | function handleBlur() { 188 | for(var id in keyState) { 189 | keyState[id] = false 190 | } 191 | updateMovement() 192 | } 193 | document.body.addEventListener('blur', handleBlur) 194 | window.addEventListener('blur', handleBlur) 195 | } 196 | 197 | 198 | var sentKillMessage = false 199 | function heartbeat() { 200 | if(!world) { 201 | return 202 | } 203 | var t = world.clock.now() 204 | var n = world.clock.wall() 205 | var x = player.trajectory.x(t) 206 | if(x) { 207 | socket.send(JSON.stringify({ 208 | type: 'move', 209 | id: player.id, 210 | now: n, 211 | t: t, 212 | x: x, 213 | v: player.trajectory.v(t) 214 | })) 215 | } else if(player.trajectory.destroyTime < Infinity && !sentKillMessage) { 216 | sentKillMessage = true 217 | socket.send(JSON.stringify({ 218 | type: 'move', 219 | id: player.id, 220 | now: n, 221 | t: t, 222 | x: player.trajectory.states[player.trajectory.states.length-1].x, 223 | v: player.trajectory.states[player.trajectory.states.length-1].v 224 | })) 225 | } 226 | } 227 | 228 | 229 | function tickLocal() { 230 | if(!world) { 231 | return 232 | } 233 | var t = world.clock.now() 234 | var x = player.trajectory.x(t) 235 | 236 | if(TEST_BULLET_TIME && !world.clock._bulletTime && (world.clock.bulletDelay()+netLag+world.syncRate) < 0.1*world.maxBulletTime) { 237 | console.log('bullet time on') 238 | world.clock.startBulletTime(0.5) 239 | } 240 | 241 | if(x) { 242 | 243 | if(world.clock.bulletDelay()+netLag > world.maxBulletTime) { 244 | console.log('bullet time drained') 245 | world.clock.stopBulletTime() 246 | } 247 | 248 | world.handleEvent(createEvent({ 249 | type: 'move', 250 | id: player.id, 251 | now: t, 252 | t: t, 253 | x: x, 254 | v: player.trajectory.v(t) 255 | })) 256 | } else if(!sentKillMessage) { 257 | heartbeat() 258 | } 259 | } 260 | 261 | function computeCauchySurface(deltaT) { 262 | var now = world.clock.now() 263 | var horizonPoints = [] 264 | var t0 = now 265 | var t1 = now 266 | for(var i=0; i= 0) { 387 | //Draw flag 388 | if(Math.random() < 0.5) { 389 | context.fillStyle = 'white' 390 | } else if(e.team === 'red') { 391 | context.fillStyle = 'blue' 392 | } else { 393 | context.fillStyle = 'red' 394 | } 395 | context.fillText(SYMBOLS['flag'][0], x[0]+0.2, x[1]-0.2) 396 | } 397 | if(e.type === 'flag' && 398 | !(s === 'ready' || s === 'dropped')) { 399 | continue 400 | } 401 | 402 | var symbol = SYMBOLS[e.type] 403 | if(e.id === player.id) { 404 | context.fillStyle = 'white' 405 | context.fillText('☻', x[0]+symbol[1], x[1]+symbol[2]) 406 | } 407 | 408 | context.fillStyle = e.team 409 | if(e.type === 'bullet') { 410 | var v = e.trajectory.v(t) 411 | var theta = Math.atan2(v[1], v[0]) 412 | context.save() 413 | context.translate(x[0], x[1]) 414 | context.rotate(theta) 415 | context.fillText(symbol[0], symbol[1], symbol[2]) 416 | context.restore() 417 | } else { 418 | context.fillText(symbol[0], x[0]+symbol[1], x[1]+symbol[2]) 419 | } 420 | } 421 | } 422 | 423 | context.fillStyle = 'red' 424 | context.fillText(''+redScore, -10,-10) 425 | 426 | context.fillStyle = 'blue' 427 | context.fillText(''+blueScore, 10,10) 428 | 429 | //At start of game pop up instructions 430 | if(t1 <= player.trajectory.createTime) { 431 | context.fillStyle = 'white' 432 | context.fillText('arrow keys move. space shoots.', 0, 0) 433 | } else if(player.trajectory.destroyTime < Infinity) { 434 | context.fillStyle = 'white' 435 | context.fillText('you died.', 0, 0) 436 | 437 | if(!deadTimeout) { 438 | deadTimeout = setTimeout(function() { 439 | location.reload() 440 | }, 2500) 441 | } 442 | } 443 | } -------------------------------------------------------------------------------- /clock.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = createClock 4 | 5 | var nowMS = require('right-now') 6 | 7 | function now() { 8 | return nowMS() / 1000.0 9 | } 10 | 11 | function Clock(shift) { 12 | this.shift = shift 13 | this._target = shift 14 | this._interpolating = false 15 | this._interpStart = 0.0 16 | this._interpRate = 0.5 17 | this._bulletTime = false 18 | this._bulletScale = 1.0 19 | this._bulletStart = 0.0 20 | this._bulletShift = 0.0 21 | } 22 | 23 | var proto = Clock.prototype 24 | 25 | proto.wall = function() { 26 | return now() 27 | } 28 | 29 | proto.now = function() { 30 | if(this._bulletTime) { 31 | return this._bulletScale*(now() - this._bulletStart) + this._bulletShift 32 | } 33 | 34 | if(this._interpolating) { 35 | var delta = this._target - this.shift 36 | var interpT = now() - this._interpStart 37 | var offset = interpT * this._interpRate 38 | if(delta < 0) { 39 | this.shift = this.shift - offset 40 | if(this.shift < this._target) { 41 | this.shift = this._target 42 | this._interpolating = false 43 | } 44 | } else if(delta > 0) { 45 | this.shift = this.shift + offset 46 | if(this.shift > this.target) { 47 | this.shift = this._target 48 | this._interpolating = false 49 | } 50 | } else { 51 | this._interpolating = false 52 | } 53 | } 54 | return now() + this.shift 55 | } 56 | 57 | proto.reset = function(currentTime) { 58 | this.shift = currentTime - now() 59 | this._interpolating = false 60 | } 61 | 62 | proto.interpolate = function(targetTime) { 63 | this._interpStart = now() 64 | this._target = targetTime - now() 65 | this._interpolating = true 66 | this._interpRate = 0.15 67 | } 68 | 69 | proto.startBulletTime = function(slowFactor) { 70 | if(this._bulletTime) { 71 | this.stopBulletTime() 72 | } 73 | var ctime = this.now() 74 | this._bulletScale = slowFactor 75 | this._bulletStart = now() 76 | this._bulletShift = ctime 77 | this._bulletTime = true 78 | } 79 | 80 | proto.stopBulletTime = function() { 81 | if(!this._bulletTime) { 82 | return 83 | } 84 | var ctime = this.now() 85 | var t = now() 86 | var elapsedTime = t - this._bulletStart 87 | this._bulletTime = false 88 | this._interpStart = t 89 | this._target = elapsedTime + this._bulletShift 90 | this._interpolating = true 91 | this._interpRate = 0.15 92 | this.shift = ctime - t 93 | } 94 | 95 | proto.bulletDelay = function() { 96 | if(this._bulletTime) { 97 | return this.elapsedBulletTime() * (1.0 - this._bulletScale) 98 | } 99 | var interp = this.now() 100 | var target = now() + this._target 101 | return Math.max(target-interp, 0) 102 | } 103 | 104 | proto.elapsedBulletTime = function() { 105 | if(!this._bulletTime) { 106 | return 107 | } 108 | return now() - this._bulletStart 109 | } 110 | 111 | function createClock(wallTime) { 112 | var shift = wallTime - now() 113 | return new Clock(shift) 114 | } -------------------------------------------------------------------------------- /entity.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = createEntity 4 | module.exports.fromJSON = entityFromJSON 5 | 6 | var createTrajectory = require('./trajectory') 7 | 8 | function Entity( 9 | id, 10 | team, 11 | type, 12 | lastUpdate, 13 | data, 14 | active, 15 | trajectory) { 16 | this.id = id 17 | this.team = team 18 | this.type = type 19 | this.lastUpdate = lastUpdate 20 | this.data = data 21 | this.active = active 22 | this.trajectory = trajectory 23 | } 24 | 25 | var proto = Entity.prototype 26 | 27 | proto.toJSON = function() { 28 | return this 29 | } 30 | 31 | function createEntity(t, id, x, v, type, team, data, active, state) { 32 | return new Entity( 33 | id, 34 | team, 35 | type, 36 | t, 37 | data, 38 | active, 39 | createTrajectory(t, x, v, state)) 40 | } 41 | 42 | function entityFromJSON(object) { 43 | var trajectory = createTrajectory.fromJSON(object.trajectory) 44 | return new Entity( 45 | object.id, 46 | object.team, 47 | object.type, 48 | +object.lastUpdate, 49 | object.data||null, 50 | !!object.active, 51 | trajectory) 52 | } -------------------------------------------------------------------------------- /event.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = createEvent 4 | module.exports.parse = parseEvent 5 | 6 | function InitEvent(id, world) { 7 | this.type = 'init' 8 | this.id = id 9 | this.world = world 10 | } 11 | 12 | function JoinEvent(t, now, x, id, team) { 13 | this.type = 'join' 14 | this.t = t 15 | this.now = now 16 | this.x = x 17 | this.id = id 18 | this.team = team 19 | } 20 | 21 | function LeaveEvent(t, now, x, id) { 22 | this.type = 'leave' 23 | this.t = t 24 | this.now = now 25 | this.x = x 26 | this.id = id 27 | } 28 | 29 | function MoveEvent(t, now, x, id, v) { 30 | this.type = 'move' 31 | this.t = t 32 | this.now = now 33 | this.x = x 34 | this.id = id 35 | this.v = v 36 | } 37 | 38 | function ShootEvent(t, now, x, id, v) { 39 | this.type = 'shoot' 40 | this.t = t 41 | this.now = now 42 | this.x = x 43 | this.id = id 44 | this.v = v 45 | } 46 | 47 | function SyncEvent(now, then) { 48 | this.type = 'sync' 49 | this.now = now 50 | this.then = then 51 | } 52 | 53 | //Turn description of event into object 54 | function createEvent(object) { 55 | 56 | if(object.type === 'sync') { 57 | return new SyncEvent( 58 | +object.now, 59 | +object.then) 60 | } 61 | 62 | if(object.type === 'init') { 63 | return new InitEvent( 64 | ''+object.id, 65 | object.world) 66 | } 67 | 68 | if(typeof object.id !== 'string' || 69 | typeof object.t !== 'number' || 70 | object.t < 0 || 71 | isNaN(object.t) || 72 | !Array.isArray(object.x) || 73 | object.x.length !== 2) { 74 | return null 75 | } 76 | 77 | switch(object.type) { 78 | case 'join': 79 | if(!Array.isArray(object.x)) { 80 | return null 81 | } 82 | return new JoinEvent( 83 | +object.t, 84 | +object.now, 85 | [ +object.x[0], +object.x[1] ], 86 | ''+object.id, 87 | ''+object.team) 88 | break 89 | 90 | case 'shoot': 91 | if(!Array.isArray(object.v)) { 92 | return null 93 | } 94 | return new ShootEvent( 95 | +object.t, 96 | +object.now, 97 | [ +object.x[0], +object.x[1] ], 98 | ''+object.id, 99 | [ +object.v[0], +object.v[1] ]) 100 | break 101 | 102 | case 'move': 103 | if(!Array.isArray(object.v)) { 104 | return null 105 | } 106 | return new MoveEvent( 107 | +object.t, 108 | +object.now, 109 | [ +object.x[0], +object.x[1] ], 110 | ''+object.id, 111 | [ +object.v[0], +object.v[1] ]) 112 | break 113 | 114 | case 'leave': 115 | return new LeaveEvent( 116 | +object.t, 117 | +object.now, 118 | [ +object.x[0], +object.x[1] ], 119 | ''+object.id) 120 | break 121 | } 122 | return null 123 | } 124 | 125 | //Parse an event 126 | function parseEvent(message) { 127 | if(typeof message !== 'string') { 128 | return null 129 | } 130 | var object 131 | try { 132 | object = JSON.parse(message) 133 | } catch(e) { 134 | return null 135 | } 136 | return createEvent(object) 137 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Capture the flag 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /intersect-cauchy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = intersectCauchySurface 4 | 5 | //Intersect a world line with a Cauchy surface 6 | function intersectCauchySurface(phi, q, t0, t1, n) { 7 | n = n || 32 8 | t0 = t0 || 0 9 | t1 = t1 || 1e10 10 | 11 | if(t1 < q.createTime) { 12 | return -1 13 | } 14 | if(t0 > q.destroyTime) { 15 | return -1 16 | } 17 | 18 | t0 = Math.max(t0, q.createTime) 19 | t1 = Math.min(t1, q.destroyTime) 20 | if(t1 < t0) { 21 | return -1 22 | } 23 | 24 | var x = [0,0] 25 | 26 | //Test end points 27 | q.x(t1, x) 28 | var thi = phi(x[0], x[1]) 29 | if(t1 < thi) { 30 | return -1 31 | } 32 | q.x(t0, x) 33 | var tlo = phi(x[0], x[1]) 34 | if(tlo < t0) { 35 | return -1 36 | } 37 | 38 | //Curve crosses surface, so binary search 39 | for(var i=0; i 1e-6) { 54 | console.log(dd, tlo, thi) 55 | } 56 | 57 | return 0.5 * (t0 + t1) 58 | } -------------------------------------------------------------------------------- /minkowski.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function minkowskiDistance2(c, x0, t0, x1, t1) { 4 | return Math.pow(x0[0] - x1[0], 2) + 5 | Math.pow(x0[1] - x1[1], 2) + 6 | Math.pow(c * (t0 - t1), 2) 7 | } 8 | exports.dist2 = minkowskiDistance2 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lpf-ctf", 3 | "version": "1.0.0", 4 | "description": "Multiplayer capture the flag", 5 | "main": "server.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "tape test/*.js", 9 | "start": "node server.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/mikolalysenko/lpf-ctf.git" 14 | }, 15 | "keywords": [ 16 | "multiplayer", 17 | "capture", 18 | "the", 19 | "flag", 20 | "relativity", 21 | "network", 22 | "game" 23 | ], 24 | "author": "Mikola Lysenko", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/mikolalysenko/lpf-ctf/issues" 28 | }, 29 | "homepage": "https://github.com/mikolalysenko/lpf-ctf", 30 | "dependencies": { 31 | "right-now": "^1.0.0", 32 | "binary-search-bounds": "^1.0.0", 33 | "nextafter": "^1.0.0", 34 | "canvas-testbed": "^0.4.0", 35 | "browserify": "^5.10.0", 36 | "beefy": "^2.1.0", 37 | "ws": "^0.4.32", 38 | "parsed-url": "0.0.0", 39 | "gl-matrix": "^2.1.0", 40 | "game-shell-orbit-camera": "^1.0.0", 41 | "gl-now": "^1.3.1", 42 | "xhr": "^1.14.1", 43 | "gl-line-plot": "^2.0.0", 44 | "gl-scatter-plot": "^5.0.0", 45 | "gl-axes": "^5.0.0", 46 | "vkey": "^1.0.0", 47 | "colormap": "^1.1.1" 48 | }, 49 | "devDependencies": { 50 | "tape": "^2.14.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var PORTNUM = 8080 4 | 5 | var beefy = require('beefy') 6 | var http = require('http') 7 | var path = require('path') 8 | var url = require('url') 9 | var ws = require('ws') 10 | var nextafter = require('nextafter') 11 | 12 | var createWorld = require('./world') 13 | var createEvent = require('./event') 14 | 15 | //Initialize http server, websockets and beefy 16 | var beefyHandler = beefy({ 17 | entries: [ 18 | 'client.js', 19 | 'visualize.js' 20 | ], 21 | cwd: __dirname, 22 | live: true, 23 | quiet: false, 24 | watchify: false 25 | }) 26 | var server = http.createServer(function(req, res) { 27 | var parsedURL = url.parse(req.url) 28 | if(parsedURL && parsedURL.pathname === '/world' ) { 29 | res.end(JSON.stringify(world.toJSON())) 30 | return 31 | } 32 | return beefyHandler(req, res) 33 | }) 34 | var wss = new ws.Server({ 35 | server: server, 36 | clientTracking: false 37 | }) 38 | server.listen(PORTNUM) 39 | console.log('listening on:', PORTNUM) 40 | 41 | //World state 42 | var world = createWorld({ 43 | debugTrace: true 44 | }) 45 | 46 | //Initialize flags 47 | world.createFlag({ 48 | team: 'red', 49 | x: [0,-9.5] 50 | }) 51 | world.createFlag({ 52 | team: 'blue', 53 | x: [0, 9.5] 54 | }) 55 | 56 | //Client list 57 | var clients = [] 58 | 59 | //Broadcast event to all clients 60 | function broadcast(event, skipID) { 61 | event.now = world.clock.now() 62 | var message = JSON.stringify(event) 63 | for(var i=0; i= 0) { 153 | clients.splice(idx, 1) 154 | } 155 | var destroyT = player.trajectory.destroyTime 156 | if(player.trajectory.destroyTime >= Infinity) { 157 | destroyT = nextafter(player.lastUpdate, Infinity) 158 | } 159 | var destroyEvent = createEvent({ 160 | type: 'leave', 161 | id: player.id, 162 | t: destroyT, 163 | x: player.trajectory.x(destroyT) 164 | }) 165 | world.handleEvent(destroyEvent) 166 | broadcast(destroyEvent) 167 | closed = true 168 | } 169 | socket.on('close', disconnect) 170 | socket.on('error', disconnect) 171 | }) -------------------------------------------------------------------------------- /test-hit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = testCollision 4 | 5 | var bsearch = require('binary-search-bounds') 6 | 7 | function solveT(ax, av, at, 8 | bx, bv, bt, 9 | t0, t1, r) { 10 | 11 | var x0 = ax[0] - bx[0] - av[0]*at + bv[0] * bt 12 | var x1 = ax[1] - bx[1] - av[1]*at + bv[1] * bt 13 | 14 | var v0 = av[0] - bv[0] 15 | var v1 = av[1] - bv[1] 16 | 17 | var xx = x0*x0 + x1*x1 - r*r 18 | var xv = x0*v0 + x1*v1 19 | var vv = v0*v0 + v1*v1 20 | 21 | //Check collision at time t0 22 | if(xx + 2.0*xv*t0 + vv*t0*t0 <= 0) { 23 | return t0 24 | } 25 | 26 | //Solve roots 27 | var discr = 4.0*(xv*xv - xx*vv) 28 | if(discr < 0) { 29 | return -1 30 | } 31 | 32 | var d = Math.sqrt(discr) 33 | var s0 = 0.5 * (-2.0*xv - d) / vv 34 | if(t0 <= s0 && s0 <= t1) { 35 | return s0 36 | } 37 | 38 | var s1 = 0.5 * (-2.0*xv + d) / vv 39 | if(t0 <= s1 && s1 <= t1) { 40 | return s1 41 | } 42 | 43 | return -1 44 | } 45 | 46 | function compareT(state, t) { 47 | return state.t - t 48 | } 49 | 50 | //Find first collision between trajectories 51 | function testCollision(a, b, t0, t1, radius) { 52 | t0 = Math.max(a.createTime, b.createTime, +t0) 53 | t1 = Math.min(a.destroyTime, b.destroyTime, +t1) 54 | 55 | var a0 = Math.max(bsearch.le(a.states, t0, compareT), 0) 56 | var b0 = Math.max(bsearch.le(b.states, t0, compareT), 0) 57 | var t = t0 58 | while(t < t1) { 59 | var nt = t1 60 | if(a0 + 1 < a.states.length) { 61 | nt = Math.min(nt, a.states[a0+1].t) 62 | } 63 | if(b0 + 1 < b.states.length) { 64 | nt = Math.min(nt, b.states[b0+1].t) 65 | } 66 | var as = a.states[a0] 67 | var bs = b.states[b0] 68 | var r = solveT(as.x, as.v, as.t, 69 | bs.x, bs.v, bs.t, 70 | t, nt, radius) 71 | if(r >= 0) { 72 | return r 73 | } 74 | t = nt 75 | if(a0 + 1 < a.states.length && a.states[a0+1].t <= t) { 76 | a0 += 1 77 | } 78 | if(b0 + 1 < b.states.length && b.states[b0+1].t <= t) { 79 | b0 += 1 80 | } 81 | } 82 | return -1 83 | } -------------------------------------------------------------------------------- /test/cauchy.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tape = require('tape') 4 | var createTrajectory = require('../trajectory') 5 | var intersectCauchy = require('../intersect-cauchy') 6 | 7 | tape('cauchy surface', function(t) { 8 | 9 | var q = createTrajectory(0, [0,0], [0,0]) 10 | q.destroy(10) 11 | 12 | var phi = function(x,y) { 13 | return Math.abs(x-5) 14 | } 15 | t.equals(intersectCauchy(phi, q, 0, 100), 5) 16 | 17 | t.end() 18 | }) -------------------------------------------------------------------------------- /test/collide.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tape = require('tape') 4 | var createTrajectory = require('../trajectory') 5 | var testHit = require('../test-hit') 6 | 7 | tape('collision detection', function(t) { 8 | 9 | //TODO: test collision detection 10 | 11 | var a = createTrajectory(0, [0,0], [0,0]) 12 | var b = createTrajectory(0, [-1,0], [1,0]) 13 | 14 | t.equals(testHit(a, b, 0, 10, 1), 0) 15 | t.equals(testHit(a, b, 0, 10, 0.5), 0.5) 16 | 17 | a.setVelocity(0.25, [0,1]) 18 | t.equals(testHit(a, b, 0, 10, 0.25), -1) 19 | 20 | b.setVelocity(0.25, [2,0]) 21 | t.ok(testHit(a, b, 0, 10, 0.5) > 0) 22 | 23 | t.end() 24 | }) -------------------------------------------------------------------------------- /test/trajectory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tape = require('tape') 4 | var createTrajectory = require('../trajectory') 5 | 6 | tape('trajectory', function(t) { 7 | 8 | //TODO: Test trajectory code 9 | 10 | t.end() 11 | }) -------------------------------------------------------------------------------- /trajectory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = createTrajectory 4 | module.exports.fromJSON = fromJSON 5 | 6 | var bsearch = require('binary-search-bounds') 7 | 8 | function State(t, x, v, s) { 9 | this.t = t 10 | this.x = x 11 | this.v = v 12 | this.s = s 13 | } 14 | 15 | function Trajectory(states, createTime, destroyTime) { 16 | this.states = states 17 | this.createTime = createTime 18 | this.destroyTime = destroyTime 19 | } 20 | 21 | var proto = Trajectory.prototype 22 | 23 | proto.exists = function(t) { 24 | return (t >= this.createTime) && (t <= this.destroyTime) 25 | } 26 | 27 | function compareT(state, t) { 28 | return state.t - t 29 | } 30 | 31 | proto.state = function(t) { 32 | if(t > this.destroyTime || t < this.createTime) { 33 | return '' 34 | } 35 | var idx = bsearch.le(this.states, t, compareT) 36 | if(idx < 0) { 37 | return '' 38 | } 39 | return this.states[idx].s 40 | } 41 | 42 | proto.x = function(t, result) { 43 | if(t > this.destroyTime || t < this.createTime) { 44 | return null 45 | } 46 | var idx = bsearch.le(this.states, t, compareT) 47 | if(idx < 0) { 48 | return null 49 | } 50 | var a = this.states[idx] 51 | var dt = t - a.t 52 | if(!result) { 53 | result = a.x.slice() 54 | } else { 55 | result[0] = a.x[0] 56 | result[1] = a.x[1] 57 | } 58 | for(var i=0; i<2; ++i) { 59 | result[i] += dt * a.v[i] 60 | } 61 | return result 62 | } 63 | 64 | proto.v = function(t, result) { 65 | if(t > this.destroyTime || t < this.createTime) { 66 | return null 67 | } 68 | var idx = bsearch.le(this.states, t, compareT) 69 | if(idx < 0) { 70 | return null 71 | } 72 | var a = this.states[idx] 73 | if(!result) { 74 | result = a.v.slice() 75 | } else { 76 | result[0] = a.v[0] 77 | result[1] = a.v[1] 78 | } 79 | return result 80 | } 81 | 82 | function statesEqual(a, b) { 83 | if(Math.abs(b.v[0] - a.v[0]) + Math.abs(a.v[1] - b.v[1]) > 1e-6) { 84 | return false 85 | } 86 | if(a.s !== b.s) { 87 | return false 88 | } 89 | var t0 = a.t 90 | var t1 = b.t 91 | var dt = t1 - t0 92 | var nx = a.x[0] + dt*a.v[0] 93 | var ny = a.x[1] + dt*a.v[1] 94 | if(Math.abs(b.x[0]-nx) + Math.abs(b.x[1]-ny) > 1e-6) { 95 | return false 96 | } 97 | return true 98 | } 99 | 100 | 101 | function compressStates(states) { 102 | for(var i=states.length-1; i>0; --i) { 103 | if(statesEqual(states[i-1], states[i])) { 104 | states.pop() 105 | } else { 106 | break 107 | } 108 | } 109 | } 110 | 111 | proto.setVelocity = function(t, v) { 112 | var nextState = new State(t, this.x(t), v.slice(), this.states[this.states.length-1].s) 113 | compressStates(this.states) 114 | this.states.push(nextState) 115 | } 116 | 117 | proto.setState = function(t, value) { 118 | compressStates(this.states) 119 | var insertIndex = this.states.length 120 | for(var i=this.states.length-1; i>=0; --i) { 121 | if(t <= this.states[i].t) { 122 | this.states[i].s = value 123 | insertIndex = i 124 | } 125 | } 126 | var nextState = new State(t, this.x(t), this.v(t), value) 127 | this.states.splice(insertIndex, 0, nextState) 128 | } 129 | 130 | proto.setFull = function(t, x, v, s) { 131 | compressStates(this.states) 132 | this.states.push(new State(t, x, v, s)) 133 | } 134 | 135 | proto.destroy = function(t) { 136 | var idx = bsearch.ge(this.states, t, compareT)+1 137 | this.states = this.states.slice(0, idx) 138 | compressStates(this.states) 139 | 140 | var x = this.x(t) 141 | this.states.push(new State(t, x, [0,0], this.states[this.states.length-1].s)) 142 | this.destroyTime = t 143 | } 144 | 145 | proto.toJSON = function() { 146 | return this 147 | } 148 | 149 | function createTrajectory(t, x, v, state) { 150 | var initState = new State(t, x.slice(), v.slice(), state) 151 | return new Trajectory([initState], t, Infinity) 152 | } 153 | 154 | function fromJSON(object) { 155 | var createTime = +object.createTime 156 | var destroyTime = +object.destroyTime 157 | if(!object.destroyTime) { 158 | destroyTime = Infinity 159 | } 160 | return new Trajectory(object.states.map(function(s) { 161 | return new State(s.t, s.x.slice(), s.v.slice(), s.s) 162 | }), createTime, destroyTime) 163 | } -------------------------------------------------------------------------------- /visualize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | World state visualizer 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /visualize.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var xhr = require('xhr') 4 | var createVisualizer = require('./worldvis') 5 | 6 | xhr({ 7 | uri: '/world' 8 | }, function(err, resp, body) { 9 | if(err) { 10 | console.error(err) 11 | return 12 | } 13 | createVisualizer(JSON.parse(body)) 14 | }) -------------------------------------------------------------------------------- /world.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = createWorld 4 | module.exports.fromJSON = worldFromJSON 5 | 6 | var createClock = require('./clock') 7 | var createEntity = require('./entity') 8 | var minkowski = require('./minkowski') 9 | var intersectCauchy = require('./intersect-cauchy') 10 | var testHit = require('./test-hit') 11 | 12 | function World( 13 | speedOfLight, 14 | maxRTT, 15 | debugTrace, 16 | playerSpeed, 17 | bulletSpeed, 18 | shootRate, 19 | bulletLife, 20 | playerRadius, 21 | bulletRadius, 22 | flagRadius, 23 | maxBulletTime, 24 | syncRate, 25 | entities, 26 | clock) { 27 | //Constants 28 | this.speedOfLight = speedOfLight 29 | this.maxRTT = maxRTT 30 | this.playerSpeed = playerSpeed 31 | this.bulletSpeed = bulletSpeed 32 | this.shootRate = shootRate 33 | this.bulletLife = bulletLife 34 | this.playerRadius = playerRadius 35 | this.bulletRadius = bulletRadius 36 | this.flagRadius = flagRadius 37 | this.syncRate = syncRate 38 | this.maxBulletTime = maxBulletTime 39 | 40 | this.entities = entities 41 | 42 | this.clock = clock 43 | 44 | //Logging and debug flags 45 | this.debugTrace = debugTrace 46 | 47 | this._lastTick = 0.0 48 | this._oldestEvent = 0.0 49 | this._horizonEvents = [] 50 | this._entityIndex = {} 51 | this._interactionRadius = this.playerRadius + Math.max(this.bulletRadius, this.flagRadius) 52 | } 53 | 54 | var proto = World.prototype 55 | 56 | proto.createEntity = function(params) { 57 | params = params|| {} 58 | 59 | var t = params.t || this.clock.now() 60 | var id = params.id || '' + this.entities.length 61 | var x = params.x || [0,0] 62 | var v = params.v || [0,0] 63 | var type = params.type || '' 64 | var team = params.team || '' 65 | var data = params.data || null 66 | var active = params.active || false 67 | var state = params.state || '' 68 | 69 | var entity = createEntity(t, id, x, v, type, team, data, active, state) 70 | this.entities.push(entity) 71 | this._entityIndex[id] = entity 72 | return entity 73 | } 74 | 75 | proto.destroyEntity = function(t, id) { 76 | var entity = this._entityIndex[id] 77 | if(!entity || 78 | !entity.trajectory.exists(t)) { 79 | return false 80 | } 81 | entity.active = false 82 | entity.lastUpdate = t 83 | entity.trajectory.destroy(t) 84 | return true 85 | } 86 | 87 | proto.destroyPlayer = function(t, id) { 88 | var p = this._entityIndex[id] 89 | if(!p || p.type !== 'player') { 90 | return false 91 | } 92 | var s = p.trajectory.state(t) 93 | if(!this.destroyEntity(t, id)) { 94 | return false 95 | } 96 | 97 | //Drop flag 98 | if(s.indexOf('carry') >= 0) { 99 | var parts = s.split(':') 100 | p.trajectory.setState(t, '') 101 | var flag = this._entityIndex[parts[1]] 102 | flag.trajectory.setFull(t, p.trajectory.x(t), [0,0], 'dropped') 103 | if(this.debugTrace) { 104 | console.log('dropped flag', p.id, flag.team) 105 | } 106 | } 107 | 108 | //Kill all bullets fired by p after t 109 | for(var j=0, n=this.entities.length; j= t) { 112 | var parts = ob.id.split('.') 113 | if(parts[1] === id) { 114 | this.destroyEntity(ob.trajectory.createTime, ob.id) 115 | } 116 | } 117 | } 118 | 119 | return true 120 | } 121 | 122 | proto.createFlag = function(params) { 123 | var baseLocation = params.x || [0,0] 124 | var team = params.team || 'red' 125 | var score = '' + (+(params.score||0)) 126 | this.createEntity({ 127 | type: 'score', 128 | team: params.team, 129 | x: baseLocation.slice(), 130 | state: score 131 | }) 132 | return this.createEntity({ 133 | type: 'flag', 134 | team: team, 135 | x: baseLocation.slice(), 136 | state: 'ready', 137 | data: { 138 | base: baseLocation.slice() 139 | } 140 | }) 141 | } 142 | 143 | proto.createPlayer = function(params) { 144 | var numRed = 0 145 | var numBlue = 0 146 | var entities = this.entities 147 | for(var i=0; i numBlue) { 160 | nextTeam = 1.0 161 | } else if(numRed < numBlue) { 162 | nextTeam = 0.0 163 | } 164 | 165 | params = params || {} 166 | 167 | var spawnPos = [Math.random()*16-8, Math.random()*2+6] 168 | if(nextTeam < 0.5) { 169 | spawnPos[1] *= -1 170 | } 171 | 172 | return this.createEntity({ 173 | id: params.id, 174 | t: params.t || this.clock.now() + this.maxRTT, 175 | x: params.x || spawnPos, 176 | team: params.team || (nextTeam < 0.5 ? 'red' : 'blue'), 177 | type: 'player', 178 | active: true, 179 | data: { 180 | numShots: 0, 181 | lastShot: 0 182 | }, 183 | state: '' 184 | }) 185 | } 186 | 187 | proto.validateEvent = function(event) { 188 | switch(event.type) { 189 | case 'move': 190 | case 'shoot': 191 | //Check entity is valid 192 | var entity = this._entityIndex[event.id] 193 | if(!entity || 194 | entity.type !== 'player' || 195 | !entity.trajectory.exists(event.t)) { 196 | if(this.debugTrace) { 197 | console.log('event: invalid entity') 198 | } 199 | return false 200 | } 201 | //Check that event is in future light cone 202 | var t0 = entity.lastUpdate 203 | var x0 = entity.trajectory.x(t0) 204 | if(t0 < event.t && 205 | minkowski.dist2(x0, t0, event.x, event.t, this.speedOfLight) > 0) { 206 | if(this.debugTrace) { 207 | console.log('event: violates causality') 208 | } 209 | return false 210 | } 211 | 212 | //Check event specific constraints 213 | var v = event.v 214 | var vl = Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)) 215 | if(event.type === 'move') { 216 | if(vl > this.playerSpeed + 1e-4) { 217 | if(this.debugTrace) { 218 | console.log('move event: player too fast') 219 | } 220 | return false 221 | } 222 | } 223 | if(event.type === 'shoot') { 224 | if(Math.abs(vl - this.bulletSpeed) > 1e-4) { 225 | if(this.debugTrace) { 226 | console.log('shoot event: bullet too fast -- ', vl/this.bulletSpeed) 227 | } 228 | return false 229 | } 230 | if(this.shootRate + entity.data.lastShot > event.t) { 231 | if(this.debugTrace) { 232 | console.log('shoot event: fired too fast') 233 | } 234 | return false 235 | } 236 | } 237 | 238 | return true 239 | break 240 | } 241 | if(this.debugTrace) { 242 | console.log('event: bad type') 243 | } 244 | return false 245 | } 246 | 247 | 248 | function collideBullets(world, bullets, players) { 249 | var n = bullets.length 250 | var m = players.length 251 | var hitEvents = [] 252 | for(var i=0; i 0) { 269 | hitEvents.push([t, p, b]) 270 | } 271 | } 272 | } 273 | //Sort events by t 274 | hitEvents.sort(function(a,b) { 275 | return a[0] - b[0] 276 | }) 277 | 278 | //Scan hit events in order of t 279 | for(var i=0; i= 0) { 342 | if(world.debugTrace) { 343 | console.log(hitP.team, 'captured the flag') 344 | } 345 | var cflagId = hs.split(':')[1] 346 | var cflag = world._entityIndex[cflagId] 347 | if(cflag) { 348 | cflag.trajectory.setFull(t, cflag.data.base.slice(), [0,0], 'teleport') 349 | cflag.trajectory.setFull(t+world.maxRTT, cflag.data.base.slice(), [0,0], 'ready') 350 | } 351 | hitP.trajectory.setState(t, '') 352 | score.trajectory.setState(t, ''+(+(score.trajectory.state(t))+1)) 353 | } 354 | } 355 | } else { 356 | //Pickup flag 357 | if(world.debugTrace) { 358 | console.log('picked up flag', hitP.id, flag.team) 359 | } 360 | flag.trajectory.setFull(t, flag.data.base.slice(), [0,0], 'carry:' + hitP.id) 361 | hitP.trajectory.setState(t, 'carry:' + flag.id) 362 | 363 | //If player dies in window, then we need to drop the flag 364 | if(hitP.trajectory.destroyTime < 0) { 365 | flag.trajectory.setFull(t, hitP.trajectory.x(hitP.trajectory.destroyTime), [0,0], 'dropped') 366 | hitT = hitP.trajectory.destroyTime 367 | } 368 | } 369 | } 370 | 371 | return hitT 372 | } 373 | 374 | function collideFlags(world, redScore, blueScore, flags, players) { 375 | var n = flags.length 376 | for(var i=0; i= t1) { 393 | return 394 | } 395 | 396 | //First filter entities into groups 397 | var teams = { 398 | red: { 399 | bullet: [], 400 | player: [], 401 | flag: [], 402 | score: [] 403 | }, 404 | blue: { 405 | bullet: [], 406 | player: [], 407 | flag: [], 408 | score: [] 409 | } 410 | } 411 | var entities = world.entities 412 | for(var i=0, n=entities.length; i= 0) { 425 | e0 = e.trajectory.createTime 426 | } else if(e1 < 0 && e0 >= 0) { 427 | e1 = e.trajectory.destroyTime 428 | } 429 | teams[e.team][e.type].push([e, e0, e1]) 430 | } 431 | 432 | //Handle bullet-player interactions 433 | collideBullets(world, teams.red.bullet, teams.blue.player) 434 | collideBullets(world, teams.blue.bullet, teams.red.player) 435 | 436 | //Handle flag-player interactions 437 | collideFlags(world, 438 | teams.red.score[0][0], 439 | teams.blue.score[0][0], 440 | teams.red.flag.concat(teams.blue.flag), 441 | teams.red.player.concat(teams.blue.player)) 442 | } 443 | 444 | proto.handleEvent = function(event) { 445 | 446 | if(this.debugTrace) { 447 | if(event.type !== 'move') { 448 | console.log('event:', event) 449 | } 450 | } 451 | 452 | var oldHorizon = this.horizon() 453 | var lowerBoundT = this._oldestEvent 454 | switch(event.type) { 455 | case 'join': 456 | this.createPlayer({ 457 | id: event.id, 458 | t: event.t, 459 | x: event.x, 460 | team: event.team 461 | }) 462 | break 463 | 464 | case 'move': 465 | var entity = this._entityIndex[event.id] 466 | entity.trajectory.setVelocity(event.t, event.v) 467 | entity.lastUpdate = event.t 468 | break 469 | 470 | case 'shoot': 471 | var entity = this._entityIndex[event.id] 472 | 473 | //Update player position 474 | entity.trajectory.setVelocity(event.t, entity.trajectory.v(event.t)) 475 | entity.lastUpdate = event.t 476 | 477 | //Spawn bullet 478 | var t = event.t 479 | var bulletId = event.id + '.' + (entity.data.numShots++) 480 | entity.data.lastShot = t 481 | var bullet = this.createEntity({ 482 | id: bulletId, 483 | t: t, 484 | x: event.x, 485 | v: event.v, 486 | type: 'bullet', 487 | team: entity.team, 488 | data: { 489 | owner: event.id 490 | } 491 | }) 492 | bullet.trajectory.destroy(t + this.bulletLife) 493 | break 494 | 495 | case 'leave': 496 | this.destroyPlayer(event.t, event.id) 497 | break 498 | } 499 | this._lastTick = this.clock.now() 500 | var newHorizon = this.horizon() 501 | 502 | //Simulate all events in oldHorizon \ newHorizon 503 | simulate(this, oldHorizon, newHorizon, lowerBoundT, this._lastTick) 504 | } 505 | 506 | function createHorizon(events, now, cr, ir) { 507 | var n = events.length 508 | return function(x,y) { 509 | var result = now 510 | for(var i=0; i