// --- utility methods --- var range = function (n) { var r = []; for (var i = 0; i < n; i++) { r.push(i); } return r; }; var contains = function (array, value) { for (var k in array) { if (array[k] == value) { return true; } } return false; }; Number.prototype.range = function () { return range(this); }; // --- secret sharing methods --- var mod5 = function (v) { v %= 5; if (v < 0) v += 5; return v; }; var splitSecretIntoShares = function (secret) { var d = Math.floor(Math.random() * 5); return range(3).map(function (i) { return secret + d * (i + 1); }).map(mod5); }; var combineSharesIntoSecret = function (shares) { return mod5(shares[0] * 3 + shares[1] * 2 + shares[2] * 1); }; // --- player interaction (movement and SMPC) --- var areaWidth = 12; var areaHeight = 12; var makeAreaGrid = function () { return areaWidth.range().map(function (i1) { return areaHeight.range().map(function (i2) { return -1; }); }); }; var players = []; players = range(3).map(function (i) { return { id: i, x: 0, y: 0, dx: 1, dy: 0, latestArea: makeAreaGrid(), // messages that have arrived, withing to be consumed by actions messageQueues: [ [], [], [] ], // queued actions to take once messages arrive onMessageQueues: [ [], [], [] ], // the player's portion of the stack of the SMPC virtual machine valueStack: [], // queued operations for the VM to perform once previous operations finish onOperationQueue: [], // determines visibility around the player character canSee: function (x, y) { var ux = x - this.x; var uy = y - this.y; // loop around to see other side if (ux > areaWidth / 2) ux -= areaWidth; if (ux < -areaWidth / 2) ux += areaWidth; if (uy > areaHeight / 2) uy -= areaHeight; if (uy < -areaHeight / 2) uy += areaHeight; // reduce to facing-right case if (this.dx !== 0) ux *= this.dx; if (this.dy !== 0) { uy *= this.dy; var t = ux; ux = uy; uy = t; } // see not quite as far to the sides and back if (ux < 0) ux *= 1.5; uy *= 2; return ux * ux + uy * uy < 30; }, // changes the position of the player's character move: function (dir) { var tx = 0; var ty = 0; if (dir == "left") { tx = -1; } else if (dir == "up") { ty = -1; } else if (dir == "right") { tx = 1; } else if (dir == "down") { ty = 1; } if (tx !== 0 || ty !== 0) { this.dx = tx; this.dy = ty; var nx = (this.x + tx) % areaWidth; var ny = (this.y + ty) % areaHeight; if (nx < 0) nx += areaWidth; if (ny < 0) ny += areaHeight; if (this.latestArea[nx][ny] !== 0) return; for (var i in players) { if (players[i].x == nx && players[i].y == ny) return; } this.x = nx; this.y = ny; } }, // enqueues an action to perform once a message has arrived from the given sender // if there's already a message queued, just runs the action inline doOnReceive: function (senderId, action) { if (this.messageQueues[senderId].length > 0) { action(this.messageQueues[senderId].shift()); } else { this.onMessageQueues[senderId].push(action); } }, // queues a message on self from the given sender // if there's already a message action queued, the message is consumed immediately transmit: function (senderId, message) { if (this.onMessageQueues[senderId].length > 0) { this.onMessageQueues[senderId].shift()(message); } else { this.messageQueues[senderId].push(message); } }, // queues an operation to perform or, if none is running, starts the given operation // must call doneOperation once the operation is finished doStartOperationAfterPrevious: function (action) { this.onOperationQueue.push(action); if (this.onOperationQueue.length == 1) { action(); } }, // runs the next queued operation, if there is one doneOperation: function () { this.onOperationQueue.shift(); if (this.onOperationQueue.length > 0) { this.onOperationQueue[0](); } }, // VM operation: the given sender splits a private input value // the value enters the VM and is pushed onto the stack // (all players must run opPushPrivateInput, but the inputValue arg is ignored for non-senders) opPushPrivateInput: function (senderId, inputValue) { var self = this; self.doStartOperationAfterPrevious(function () { if (senderId == self.id) { var shares = splitSecretIntoShares(inputValue); for (var i in range(3)) { players[i].transmit(self.id, shares[i]); } } self.doOnReceive(senderId, function (message) { self.valueStack.push(message); self.doneOperation(); }); }); }, // VM operation: push a value known to all players onto the VM's stack // (all players must run opPushConstant and give it the same value) opPushConstant: function (value) { self.doStartOperationAfterPrevious(function () { this.valueStack.push(value); self.doneOperation(); }); }, // receive a message from each player and interpret them as shares of a secret value // (does not affect the VM's stack) opReceiveCombineIndexedShares: function (actionForValue) { var shares = [null, null, null]; var n = 0; var self = this; for (var i_ in range(3)) { var f = function (i) { self.doOnReceive(i, function (v) { shares[i] = v; n += 1; if (n == 3) { var value = combineSharesIntoSecret(shares); actionForValue(value); } }); }; f(i_); } }, // pop a value from the SMPC-VM's stack, and reveal it to the given players // (every players must run opPopRevealTo, but only receivers will get called back) opPopRevealTo: function (receiverIds, actionForValue) { var self = this; self.doStartOperationAfterPrevious(function () { if (contains(receiverIds, self.id)) { self.opReceiveCombineIndexedShares(actionForValue); } var value = self.valueStack.pop(); for (var i in receiverIds) { players[receiverIds[i]].transmit(self.id, value); } self.doneOperation(); }); }, // pops two values off the SMPC-VM's stack, adds them, and pushes the result onto the stack opPopPopAddPush: function () { var self = this; self.doStartOperationAfterPrevious(function () { var s1 = self.valueStack.pop(); var s2 = self.valueStack.pop(); var s3 = mod5(s1 + s2); self.valueStack.push(s3); self.doneOperation(); }); }, // pops two values off the SMPC-VM's stack, multiplies them, and pushes the result onto the stack // requires a message round trip to do degree reduction opPopPopMultiplyPush: function () { var self = this; self.doStartOperationAfterPrevious(function () { var s1 = self.valueStack.pop(); var s2 = self.valueStack.pop(); var q = mod5(s1 * s2); var sq = splitSecretIntoShares(q); for (var i in range(3)) { players[i].transmit(self.id, sq[i]); } self.opReceiveCombineIndexedShares(function (e) { self.valueStack.push(e); self.doneOperation(); }); }); }, // uses the VM to do oblivious transfer of player positions // for each cell, the querying player sends 0/1 based on their visibility // other players send 0/1 based on if they are on that cell // the other players' inputs are added, then multiplied against the visibility mask // thus only the positions of players within the visible area are transferred // // WARNING: although we COULD, to keep things we are NOT VERIFYING the inputs // i.e. players don't record all data and require revelation after the game to verify correct behavior requeryArea: function () { var self = this; var queryResult = makeAreaGrid(); var n = 0; for (var queryerId in range(3)) { for (var a_ in areaWidth.range()) { for (var b_ in areaWidth.range()) { var f = function (a, b) { // compute # of other players on cell (only ever 0 or 1) for (var senderId in range(3)) { if (queryerId == senderId) continue; self.opPushPrivateInput(senderId, self.x == a && self.y == b ? 1 : 0); } self.opPopPopAddPush(); // mask against visibility self.opPushPrivateInput(queryerId, self.canSee(a, b) ? 1 : 0); self.opPopPopMultiplyPush(); // querying player gets result to fill in their map self.opPopRevealTo([queryerId], function (e) { queryResult[a][b] = e; n += 1; if (n == areaWidth * areaHeight) { self.latestArea = queryResult; } }); }; f(a_, b_); } } } } }; }); // --- initial positions --- players[0].x = Math.round(areaWidth / 2); players[0].y = Math.round(areaHeight / 2); players[0].dy = 1; players[0].dx = 0; players[1].x = 2; players[1].y = 2; players[2].x = Math.round(areaWidth / 2); players[2].y = areaHeight - 4; // --- super fancy graphics --- var redraw = function () { for (var i in players) { players[i].requeryArea(); } var tileSpan = 25; var canvas = document.getElementById("gameCanvas"); var selfimg = document.getElementById("selfimg"); var fogimg = document.getElementById("fogimg"); var groundimg = document.getElementById("groundimg"); var otherimg = document.getElementById("otherimg"); var sw = groundimg.width / areaWidth; var sh = groundimg.height / areaHeight; var c = canvas.getContext("2d"); var p = players[0]; c.drawImage(fogimg, 0, 0, areaWidth * tileSpan, areaHeight * tileSpan); for (i in range(areaWidth)) { for (var j in range(areaHeight)) { var x = i * tileSpan; var y = j * tileSpan; if (!p.canSee(i, j)) { if (p.latestArea[i][j] !== 0) { // uh oh, they shouldn't see anything here! c.fillStyle = "red"; c.fillRect(x, y, tileSpan, tileSpan); } continue; } if (p.latestArea[i][j] == -1) { // ugh, querying failed and players aren't getting results c.fillStyle = "red"; c.fillRect(x, y, tileSpan, tileSpan); } else { c.drawImage(groundimg, i * sw, j * sh, sw, sh, x, y, tileSpan, tileSpan); } if (p.latestArea[i][j] == 1) { c.drawImage(otherimg, x, y, tileSpan, tileSpan); } if (i == p.x && j == p.y) { c.drawImage(selfimg, x, y, tileSpan, tileSpan); } } } }; // --- control --- var pressed = []; var keyUpHandler = function (e) { pressed.splice(pressed.indexOf(e.keyCode), 1); }; var keyPressHandler = function (e) { // ignore held keys if (pressed.indexOf(e.keyCode) != -1) return; pressed.push(e.keyCode); // move player var p = players[0]; if (e.keyCode == 65) { p.move("left"); } else if (e.keyCode == 87) { p.move("up"); } else if (e.keyCode == 68) { p.move("right"); } else if (e.keyCode == 83) { p.move("down"); } // drunken walk the other players for (var i = 1; i < 3; i++) { if (Math.random() < 0.5) continue; players[i].move(["left", "up", "right", "down"][Math.floor(Math.random() * 4)]); } redraw(); }; redraw(); document.addEventListener("keydown", keyPressHandler, false); document.addEventListener("keyup", keyUpHandler, false);
<img id="selfimg" src="http://i.imgur.com/3tX4Yww.png" style="display:none" /> <img id="fogimg" src="http://i.imgur.com/LrMXSum.png" style="display:none" /> <img id="groundimg" src="http://i.imgur.com/IHzBZyI.png" style="display:none" /> <img id="otherimg" src="http://i.imgur.com/HkGYyxc.png" style="display:none" /> Click to focus, then WASD to move<br/> <canvas id="gameCanvas" width="300" height="300" style="border:1px solid #000000;" />