summaryrefslogtreecommitdiff
path: root/interactor
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2018-07-01 21:12:11 +0200
committerTom Smeding <tom.smeding@gmail.com>2018-07-01 21:12:11 +0200
commit233160e17ff451e52621b10d5d00d99e7800c2db (patch)
treef700cf8f24896f72893ac4f9252d609d4a943449 /interactor
parentf283f00c084b3b563b923e33aabaffc4aa112e90 (diff)
Interactor
Diffstat (limited to 'interactor')
-rw-r--r--interactor/.gitignore1
-rw-r--r--interactor/board.js160
-rw-r--r--interactor/game.css28
-rw-r--r--interactor/game.html25
-rw-r--r--interactor/game.js345
-rw-r--r--interactor/package-lock.json652
-rw-r--r--interactor/package.json16
-rwxr-xr-xinteractor/server.js85
8 files changed, 1312 insertions, 0 deletions
diff --git a/interactor/.gitignore b/interactor/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/interactor/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/interactor/board.js b/interactor/board.js
new file mode 100644
index 0000000..35ba93a
--- /dev/null
+++ b/interactor/board.js
@@ -0,0 +1,160 @@
+function colourValue(value) {
+ return value < 4;
+}
+
+function parsePosition(str) {
+ if (str.length != 2) return null;
+ var a = str.toUpperCase().charCodeAt(0), b = str.charCodeAt(1);
+ if (a < 65 || a > 65+25) return null;
+ if (b < 48 || b > 48+9) return null;
+ return N * (N - (b - 48)) + a - 65;
+}
+
+function parseMove(str) {
+ if (str.length != 5) return null;
+ if (str[2] != "-" && str[2] != " ") return null;
+
+ var from, to;
+ from = parsePosition(str.substr(0, 2));
+ if (from != null) {
+ to = parsePosition(str.substr(3, 2));
+ if (to != null) {
+ return {from: from, to: to};
+ }
+ }
+
+ return null;
+}
+
+function stringifyPosition(pos) {
+ return String.fromCharCode(65 + pos % N) + String.fromCharCode(48 + N - ~~(pos / N));
+}
+
+function stringifyMove(mv) {
+ return stringifyPosition(mv.from) + '-' + stringifyPosition(mv.to);
+}
+
+function stringifyMoveExt(mv, captures) {
+ var s = stringifyMove(mv);
+ if (captures.length > 0) {
+ for (var i = 0; i < captures.length; i++) {
+ s += (i == 0 ? "x" : "/") + stringifyPosition(captures[i]);
+ }
+ }
+ return s;
+}
+
+
+function stoneFlankedH(pos, by) {
+ if (by & (WHITE|KING)) by = WHITE|KING;
+ return ((board[pos-1] & by) || (pos-1 == BOARDMID && board[BOARDMID] == EMPTY)) &&
+ ((board[pos+1] & by) || (pos+1 == BOARDMID && board[BOARDMID] == EMPTY));
+}
+
+function stoneFlankedV(pos, by) {
+ if (by & (WHITE|KING)) by = WHITE|KING;
+ return ((board[pos-N] & by) || (pos-N == BOARDMID && board[BOARDMID] == EMPTY)) &&
+ ((board[pos+N] & by) || (pos+N == BOARDMID && board[BOARDMID] == EMPTY));
+}
+
+function kingEncircled(pos) {
+ if (pos == BOARDMID) {
+ return board[pos-1] == BLACK && board[pos+1] == BLACK &&
+ board[pos-N] == BLACK && board[pos+N] == BLACK;
+ } else if (pos == BOARDMID-1) {
+ return board[pos-1] == BLACK && board[pos-N] == BLACK && board[pos+N] == BLACK;
+ } else if (pos == BOARDMID+1) {
+ return board[pos+1] == BLACK && board[pos-N] == BLACK && board[pos+N] == BLACK;
+ } else if (pos == BOARDMID-N) {
+ return board[pos-1] == BLACK && board[pos+1] == BLACK && board[pos-N] == BLACK;
+ } else if (pos == BOARDMID+N) {
+ return board[pos-1] == BLACK && board[pos+1] == BLACK && board[pos+N] == BLACK;
+ } else {
+ var x = pos % N, y = ~~(pos / N);
+ return (x > 0 && x < N-1 && board[pos-1] == BLACK && board[pos+1] == BLACK) ||
+ (y > 0 && y < N-1 && board[pos-N] == BLACK && board[pos+N] == BLACK);
+ }
+}
+
+function applyMove(mv) {
+ board[mv.to] = board[mv.from];
+ board[mv.from] = EMPTY;
+ var i = mv.to;
+ var x = i % N, y = ~~(i / N);
+
+ var captures = [];
+
+ if (x > 1 && board[i-1] != EMPTY && colourValue(board[i-1]) != colourValue(board[i])) {
+ if (board[i-1] == KING ? kingEncircled(i-1) : stoneFlankedH(i-1, board[i])) {
+ board[i-1] = EMPTY;
+ captures.push(i-1);
+ }
+ }
+
+ if (y > 1 && board[i-N] != EMPTY && colourValue(board[i-N]) != colourValue(board[i])) {
+ if (board[i-N] == KING ? kingEncircled(i-N) : stoneFlankedV(i-N, board[i])) {
+ board[i-N] = EMPTY;
+ captures.push(i-N);
+ }
+ }
+
+ if (x < N-2 && board[i+1] != EMPTY && colourValue(board[i+1]) != colourValue(board[i])) {
+ if (board[i+1] == KING ? kingEncircled(i+1) : stoneFlankedH(i+1, board[i])) {
+ board[i+1] = EMPTY;
+ captures.push(i+1);
+ }
+ }
+
+ if (y < N-2 && board[i+N] != EMPTY && colourValue(board[i+N]) != colourValue(board[i])) {
+ if (board[i+N] == KING ? kingEncircled(i+N) : stoneFlankedV(i+N, board[i])) {
+ board[i+N] = EMPTY;
+ captures.push(i+N);
+ }
+ }
+
+ return captures;
+}
+
+function checkWin() {
+ for (var x = 0; x < N; x++) if (board[x] == KING) return 1;
+ for (var y = 0; y < N; y++) if (board[N * y] == KING) return 1;
+ for (var x = 0; x < N; x++) if (board[N * (N-1) + x] == KING) return 1;
+ for (var y = 0; y < N; y++) if (board[N * y + N-1] == KING) return 1;
+
+ for (var i = 0; i < N * N; i++) {
+ if (board[i] == KING) return 0;
+ }
+
+ return -1;
+}
+
+function isValid(mv) {
+ if (mv.from < 0 || mv.from >= N * N || mv.to < 0 || mv.to >= N * N) return false;
+ if (mv.from == mv.to) return false;
+ if (board[mv.from] == EMPTY || board[mv.to] != EMPTY) return false;
+
+ if (mv.to == BOARDMID) return false;
+
+ var x1 = mv.from % N, y1 = ~~(mv.from / N);
+ var x2 = mv.to % N, y2 = ~~(mv.to / N);
+ if (x1 != x2 && y1 != y2) return false;
+
+ if (x1 == x2) {
+ var delta = y2 < y1 ? -1 : 1;
+ for (var y = y1 + delta; y != y2; y += delta) {
+ if (board[N * y + x1] != EMPTY) return false;
+ }
+ } else {
+ var delta = x2 < x1 ? -1 : 1;
+ for (var x = x1 + delta; x != x2; x += delta) {
+ if (board[N * y1 + x] != EMPTY) return false;
+ }
+ }
+
+ return true;
+}
+
+function isValidForPlayer(mv, player) {
+ var mask = player == 1 ? WHITE|KING : BLACK;
+ return isValid(mv) && (board[mv.from] & mask) != 0;
+}
diff --git a/interactor/game.css b/interactor/game.css
new file mode 100644
index 0000000..bce55f8
--- /dev/null
+++ b/interactor/game.css
@@ -0,0 +1,28 @@
+#cvs {
+ border: 1px #473d32 solid;
+}
+
+.invisible {
+ display: none;
+}
+
+#serverlog > div {
+ font-family: monospace;
+}
+
+#movelog_container {
+ width: 500px;
+ height: 200px;
+ overflow-y: scroll;
+}
+
+#movelog {
+ border-collapse: collapse;
+ width: 100%;
+ table-layout: fixed;
+}
+
+#movelog td {
+ border: 1px #000000 solid;
+ width: 50%;
+}
diff --git a/interactor/game.html b/interactor/game.html
new file mode 100644
index 0000000..20def4f
--- /dev/null
+++ b/interactor/game.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+<head>
+<title>Hnefatafl interactor</title>
+<meta charset="utf-8">
+<script src="/socket.io/socket.io.js"></script>
+<script src="/game.js"></script>
+<script src="/board.js"></script>
+<link rel="stylesheet" href="/game.css">
+</head>
+<body>
+<canvas id="cvs" width="370" height="370"></canvas> <br>
+
+<div id="status"></div>
+
+<input type="button" id="aifirst" class="invisible" value="First move for AI">
+
+<div id="serverlog"></div>
+<br>
+
+<div id="movelog_container">
+ <table id="movelog"><tbody id="movelog_tbody"></tbody></table>
+</div>
+</body>
+</html>
diff --git a/interactor/game.js b/interactor/game.js
new file mode 100644
index 0000000..85674b1
--- /dev/null
+++ b/interactor/game.js
@@ -0,0 +1,345 @@
+"use strict";
+
+var N = 9;
+var EMPTY = 0, WHITE = 1, KING = 2, BLACK = 4;
+var BOARDMID = N * ~~(N/2) + ~~(N/2);
+
+var cvs, ctx;
+var cellSize = 40, cvsSize = N * cellSize + N+1;
+var socket;
+var playing = false, onturn = null, aiplayer = null;
+var lastMove = null;
+
+var board;
+
+function rotateIndex(index, nturns) {
+ for (var i = 0; i < nturns % 4; i++) {
+ var x = index % N, y = ~~(index / N);
+ index = N * x + N-1 - y;
+ }
+ return index;
+}
+
+function makeBoard() {
+ var bd = new Array(N * N);
+ for (var i = 0; i < N * N; i++) bd[i] = EMPTY;
+
+ for (var i = 0; i < 4; i++) {
+ bd[rotateIndex(~~(N/2) - 1, i)] = BLACK;
+ bd[rotateIndex(~~(N/2) + 0, i)] = BLACK;
+ bd[rotateIndex(~~(N/2) + 1, i)] = BLACK;
+ bd[rotateIndex(N + ~~(N/2), i)] = BLACK;
+
+ bd[rotateIndex(BOARDMID - 2 * N, i)] = WHITE;
+ bd[rotateIndex(BOARDMID - 1 * N, i)] = WHITE;
+ }
+
+ bd[BOARDMID] = KING;
+ return bd;
+}
+
+function setupBoard() {
+ board = makeBoard();
+}
+
+function cellX(i) {return (cellSize + 1) * (i % N);}
+function cellY(i) {return (cellSize + 1) * ~~(i / N);}
+function cellMidX(i) {return (cellSize + 1) * (i % N + 0.5);}
+function cellMidY(i) {return (cellSize + 1) * (~~(i / N) + 0.5);}
+
+function redraw() {
+ ctx.fillStyle = "#e0c29f";
+ ctx.fillRect(0, 0, cvsSize, cvsSize);
+
+ ctx.fillStyle = "#c6ac8d";
+ ctx.fillRect(cellX(BOARDMID), cellY(BOARDMID), cellSize, cellSize);
+
+ ctx.strokeStyle = "#7a6a58";
+ ctx.beginPath();
+ for (var i = 1; i < N; i++) {
+ ctx.moveTo(0, cellY(N * i) - 0.5);
+ ctx.lineTo(cvsSize, cellY(N * i) - 0.5);
+
+ ctx.moveTo(cellX(i) - 0.5, 0);
+ ctx.lineTo(cellX(i) - 0.5, cvsSize);
+ }
+ ctx.stroke();
+
+ var drawers = [
+ function() {},
+ function(px, py) { drawCircle(px, py, "#ffffff"); },
+ function(px, py) { drawKing(px, py); },
+ function() {},
+ function(px, py) { drawCircle(px, py, "#000000"); },
+ ];
+
+ for (var i = 0; i < N * N; i++) {
+ drawers[board[i]](cellMidX(i), cellMidY(i));
+ }
+
+ if (lastMove != null) {
+ var thickness = 3;
+ for (var i = 0; i < thickness; i++) {
+ var alpha = (thickness - i) / thickness;
+ alpha *= alpha;
+ ctx.strokeStyle = "rgba(165, 165, 247, " + alpha + ")";
+ ctx.beginPath();
+ var sz = cellSize - 2*i - 1;
+ ctx.rect(cellX(lastMove.from) + i + 0.5, cellY(lastMove.from) + i + 0.5, sz, sz);
+ ctx.rect(cellX(lastMove.to) + i + 0.5, cellY(lastMove.to) + i + 0.5, sz, sz);
+ ctx.stroke();
+ }
+ }
+}
+
+function drawCircle(x, y, clr) {
+ ctx.fillStyle = clr;
+ ctx.beginPath();
+ ctx.arc(x, y, cellSize * 0.3, 0, 2 * Math.PI);
+ ctx.fill();
+}
+
+function drawKing(x, y) {
+ ctx.fillStyle = "#ffffff";
+ ctx.strokeStyle = "#7a6a58";
+ ctx.beginPath();
+
+ ctx.moveTo(x - cellSize * 0.25, y + cellSize * 0.25);
+ ctx.lineTo(x - cellSize * 0.3, y - cellSize * 0.25);
+ ctx.lineTo(x - cellSize * 0.15, y - cellSize * 0.05);
+ ctx.lineTo(x, y - cellSize * 0.25);
+ ctx.lineTo(x + cellSize * 0.15, y - cellSize * 0.05);
+ ctx.lineTo(x + cellSize * 0.3, y - cellSize * 0.25);
+ ctx.lineTo(x + cellSize * 0.25, y + cellSize * 0.25);
+ ctx.closePath();
+
+ ctx.stroke();
+ ctx.fill();
+}
+
+function displayStatus(msg) {
+ document.getElementById("status").innerHTML = msg;
+}
+
+function addMoveLog(player, mv, captures) {
+ if (captures == undefined) captures = [];
+
+ var log = document.getElementById("movelog_tbody");
+ var tr = document.createElement("tr");
+
+ var who = player == 1 ? "White" : "Black";
+ if (player == aiplayer) who += " (AI)";
+
+ var td = document.createElement("td");
+ td.appendChild(document.createTextNode(who));
+ tr.appendChild(td);
+
+ var desc = stringifyMoveExt(mv, captures);
+ if (checkWin() != 0) desc += "++";
+
+ td = document.createElement("td");
+ td.appendChild(document.createTextNode(desc));
+ tr.appendChild(td);
+
+ log.appendChild(tr);
+
+ tr.scrollIntoView(false);
+}
+
+function addServerLog(msg) {
+ var div = document.createElement("div");
+ div.appendChild(document.createTextNode(msg));
+ document.getElementById("serverlog").appendChild(div);
+}
+
+function openGame() {
+ socket = io();
+ socket.on("message", function(msg) {
+ addServerLog(msg);
+ });
+
+ socket.on("open", function() {
+ playing = true;
+ onturn = -1;
+ aiplayer = null;
+ lastMove = null;
+
+ document.getElementById("aifirst").classList.remove("invisible");
+
+ setupBoard();
+ redraw();
+
+ addServerLog("Game opened.");
+ displayStatus("Make first move, or let the AI take the first move.");
+ });
+
+ socket.on("close", function() {
+ playing = false;
+ addServerLog("Game closed.");
+ displayStatus("Game closed.");
+ });
+
+ socket.on("line", function(line) {
+ var mv = parseMove(line);
+ if (mv != null) {
+ processMove(mv);
+ displayStatus("Your turn.");
+ } else {
+ var msg = "Invalid move received from server:\n" + line;
+ addServerLog(msg);
+ alert(msg);
+ }
+ });
+
+ socket.emit("open");
+}
+
+function humanMove(mv) {
+ if (aiplayer != null && onturn != -aiplayer) return;
+ if (!isValidForPlayer(mv, onturn)) {
+ displayStatus("That's an invalid move.");
+ return;
+ }
+
+ if (aiplayer == null) {
+ aiplayer = -onturn;
+ }
+
+ processMove(mv);
+
+ socket.emit("line", stringifyMove(mv));
+ displayStatus("Waiting for AI...");
+}
+
+function processMove(mv) {
+ var captures = applyMove(mv);
+ addMoveLog(onturn, mv, captures);
+ onturn = -onturn;
+ lastMove = mv;
+ redraw();
+
+ var win = checkWin();
+ var msg = win == 1 ? "White won." : win == -1 ? "Black won." : null;
+ if (msg != null) {
+ var suffix = win != aiplayer ? "Congratulations!" : "Too bad.";
+ displayStatus(msg + " " + suffix);
+ addServerLog(msg);
+ }
+}
+
+function hookListeners() {
+ var startx = null, starty = null, endx = null, endy = null;
+ var starti = null, endi = null;
+
+ function calcI(x, y) {
+ x = Math.round(x / (cellSize + 1) - 0.5);
+ y = Math.round(y / (cellSize + 1) - 0.5);
+ if (x >= N) x = N - 1;
+ if (y >= N) y = N - 1;
+ return N * y + x;
+ }
+
+ function updateStart(ev) {
+ var bbox = cvs.getBoundingClientRect();
+ startx = ev.clientX - bbox.left;
+ starty = ev.clientY - bbox.top;
+ starti = calcI(startx, starty);
+ }
+
+ function updateEnd(ev) {
+ var bbox = cvs.getBoundingClientRect();
+ endx = ev.clientX - bbox.left;
+ endy = ev.clientY - bbox.top;
+ endi = calcI(endx, endy);
+ if (endi % N != starti % N && ~~(endi / N) != ~~(starti / N)) {
+ var dx = Math.abs(endi % N - starti % N);
+ var dy = Math.abs(~~(endi / N) - ~~(starti / N));
+ if (dx >= dy) endi = N * ~~(starti / N) + endi % N;
+ else endi = N * ~~(endi / N) + starti % N;
+ }
+ }
+
+ function reset() {
+ startx = starty = endx = endy = null;
+ starti = endi = null;
+ }
+
+ function drawArrow() {
+ ctx.beginPath();
+ ctx.moveTo(-cellSize * 0.2, -cellSize * 0.15);
+ ctx.lineTo(0, 0);
+ ctx.lineTo(-cellSize * 0.2, cellSize * 0.15);
+ ctx.stroke();
+ }
+
+ cvs.addEventListener("mousedown", function(ev) {
+ if (!playing || (aiplayer != null && onturn == aiplayer)) return;
+ updateStart(ev);
+ if (board[starti] == EMPTY || (onturn == 1 ? board[starti] & BLACK : board[starti] & (WHITE|KING))) {
+ reset();
+ }
+ });
+
+ cvs.addEventListener("mouseleave", function() {
+ if (startx == null) return;
+ reset();
+ redraw();
+ });
+
+ cvs.addEventListener("mousemove", function(ev) {
+ if (startx == null) return;
+ updateEnd(ev);
+ redraw();
+
+ ctx.strokeStyle = "#ff0000";
+ ctx.lineWidth = 2;
+
+ ctx.beginPath();
+ var x1 = (starti % N + 0.5) * (cellSize + 1), y1 = (~~(starti / N) + 0.5) * (cellSize + 1);
+ var x2 = (endi % N + 0.5) * (cellSize + 1), y2 = (~~(endi / N) + 0.5) * (cellSize + 1);
+ x1 = ~~x1 + 1; y1 = ~~y1 + 1; x2 = ~~x2 + 1; y2 = ~~y2 + 1;
+ ctx.moveTo(x1, y1);
+ ctx.lineTo(x2, y2);
+ ctx.stroke();
+
+ ctx.save();
+ ctx.translate(x2, y2);
+ if (endi % N > starti % N) ctx.rotate(0);
+ else if (~~(endi / N) > ~~(starti / N)) ctx.rotate(Math.PI / 2);
+ else if (endi % N < starti % N) ctx.rotate(Math.PI);
+ else if (~~(endi / N) < ~~(starti / N)) ctx.rotate(Math.PI * 3 / 2);
+ drawArrow();
+ ctx.restore();
+
+ ctx.lineWidth = 1;
+ });
+
+ cvs.addEventListener("mouseup", function(ev) {
+ if (startx == null) return;
+ updateEnd(ev);
+ console.log(starti, endi);
+ humanMove({from: starti, to: endi});
+ reset();
+ redraw();
+ });
+
+
+ document.getElementById("aifirst").addEventListener("click", function() {
+ if (aiplayer == null) { // paranoia
+ aiplayer = -1;
+ socket.emit("line", "Start");
+ displayStatus("Requested first move from AI...");
+ }
+ document.getElementById("aifirst").classList.add("invisible");
+ });
+}
+
+window.addEventListener("load", function() {
+ cvs = document.getElementById("cvs");
+ ctx = cvs.getContext("2d");
+
+ setupBoard();
+ redraw();
+ hookListeners();
+ openGame();
+});
diff --git a/interactor/package-lock.json b/interactor/package-lock.json
new file mode 100644
index 0000000..1718395
--- /dev/null
+++ b/interactor/package-lock.json
@@ -0,0 +1,652 @@
+{
+ "name": "interactor",
+ "version": "0.1.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "accepts": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+ "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+ "requires": {
+ "mime-types": "~2.1.18",
+ "negotiator": "0.6.1"
+ }
+ },
+ "after": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "arraybuffer.slice": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+ "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
+ },
+ "async-limiter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+ "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
+ },
+ "backo2": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+ "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
+ },
+ "base64-arraybuffer": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+ "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
+ },
+ "base64id": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+ "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
+ },
+ "better-assert": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+ "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+ "requires": {
+ "callsite": "1.0.0"
+ }
+ },
+ "blob": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+ "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE="
+ },
+ "body-parser": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+ "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+ "requires": {
+ "bytes": "3.0.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.1",
+ "http-errors": "~1.6.2",
+ "iconv-lite": "0.4.19",
+ "on-finished": "~2.3.0",
+ "qs": "6.5.1",
+ "raw-body": "2.3.2",
+ "type-is": "~1.6.15"
+ }
+ },
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "callsite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+ "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
+ },
+ "component-bind": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+ "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
+ },
+ "component-emitter": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+ "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+ },
+ "component-inherit": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+ "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
+ },
+ "content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+ },
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "engine.io": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.0.tgz",
+ "integrity": "sha512-mRbgmAtQ4GAlKwuPnnAvXXwdPhEx+jkc0OBCLrXuD/CRvwNK3AxRSnqK4FSqmAMRRHryVJP8TopOvmEaA64fKw==",
+ "requires": {
+ "accepts": "~1.3.4",
+ "base64id": "1.0.0",
+ "cookie": "0.3.1",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.0",
+ "ws": "~3.3.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-client": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+ "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "component-inherit": "0.0.3",
+ "debug": "~3.1.0",
+ "engine.io-parser": "~2.1.1",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "ws": "~3.3.1",
+ "xmlhttprequest-ssl": "~1.5.4",
+ "yeast": "0.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "engine.io-parser": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz",
+ "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==",
+ "requires": {
+ "after": "0.8.2",
+ "arraybuffer.slice": "~0.0.7",
+ "base64-arraybuffer": "0.1.5",
+ "blob": "0.0.4",
+ "has-binary2": "~1.0.2"
+ }
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "express": {
+ "version": "4.16.3",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
+ "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
+ "requires": {
+ "accepts": "~1.3.5",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.18.2",
+ "content-disposition": "0.5.2",
+ "content-type": "~1.0.4",
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.1.1",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.3",
+ "qs": "6.5.1",
+ "range-parser": "~1.2.0",
+ "safe-buffer": "5.1.1",
+ "send": "0.16.2",
+ "serve-static": "1.13.2",
+ "setprototypeof": "1.1.0",
+ "statuses": "~1.4.0",
+ "type-is": "~1.6.16",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+ "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.4.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "has-binary2": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
+ "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
+ "requires": {
+ "isarray": "2.0.1"
+ }
+ },
+ "has-cors": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+ "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.19",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+ "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ipaddr.js": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
+ "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
+ },
+ "isarray": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+ "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+ },
+ "mime-db": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+ "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
+ },
+ "mime-types": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+ "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+ "requires": {
+ "mime-db": "~1.33.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+ },
+ "object-component": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+ "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "parseqs": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+ "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+ "requires": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "parseuri": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+ "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+ "requires": {
+ "better-assert": "~1.0.0"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "proxy-addr": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
+ "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.6.0"
+ }
+ },
+ "qs": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+ "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
+ },
+ "range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+ },
+ "raw-body": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+ "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+ "requires": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.2",
+ "iconv-lite": "0.4.19",
+ "unpipe": "1.0.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+ "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
+ },
+ "http-errors": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+ "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+ "requires": {
+ "depd": "1.1.1",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.0.3",
+ "statuses": ">= 1.3.1 < 2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+ "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
+ }
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
+ },
+ "send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.6.2",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.4.0"
+ }
+ },
+ "serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
+ "send": "0.16.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ },
+ "socket.io": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.1.1.tgz",
+ "integrity": "sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==",
+ "requires": {
+ "debug": "~3.1.0",
+ "engine.io": "~3.2.0",
+ "has-binary2": "~1.0.2",
+ "socket.io-adapter": "~1.1.0",
+ "socket.io-client": "2.1.1",
+ "socket.io-parser": "~3.2.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "socket.io-adapter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+ "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
+ },
+ "socket.io-client": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.1.1.tgz",
+ "integrity": "sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==",
+ "requires": {
+ "backo2": "1.0.2",
+ "base64-arraybuffer": "0.1.5",
+ "component-bind": "1.0.0",
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "engine.io-client": "~3.2.0",
+ "has-binary2": "~1.0.2",
+ "has-cors": "1.1.0",
+ "indexof": "0.0.1",
+ "object-component": "0.0.3",
+ "parseqs": "0.0.5",
+ "parseuri": "0.0.5",
+ "socket.io-parser": "~3.2.0",
+ "to-array": "0.1.4"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "socket.io-parser": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.2.0.tgz",
+ "integrity": "sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==",
+ "requires": {
+ "component-emitter": "1.2.1",
+ "debug": "~3.1.0",
+ "isarray": "2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ },
+ "to-array": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
+ },
+ "type-is": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+ "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.18"
+ }
+ },
+ "ultron": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+ "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "ws": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+ "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+ "requires": {
+ "async-limiter": "~1.0.0",
+ "safe-buffer": "~5.1.0",
+ "ultron": "~1.1.0"
+ }
+ },
+ "xmlhttprequest-ssl": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
+ "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
+ },
+ "yeast": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+ "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
+ }
+ }
+}
diff --git a/interactor/package.json b/interactor/package.json
new file mode 100644
index 0000000..b0508c6
--- /dev/null
+++ b/interactor/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "interactor",
+ "version": "0.1.0",
+ "description": "",
+ "main": "server.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "node server.js"
+ },
+ "author": "Tom Smeding <tom.smeding@gmail.com> (https://tomsmeding.com)",
+ "license": "MIT",
+ "dependencies": {
+ "express": "^4.16.3",
+ "socket.io": "^2.1.1"
+ }
+}
diff --git a/interactor/server.js b/interactor/server.js
new file mode 100755
index 0000000..88e9f10
--- /dev/null
+++ b/interactor/server.js
@@ -0,0 +1,85 @@
+#!/usr/bin/env node
+const http = require("http");
+const express = require("express");
+const socketio = require("socket.io");
+const child_process = require("child_process");
+
+const PORT = 8080;
+
+const app = express();
+const server = http.Server(app);
+const io = socketio(server);
+
+// gameid -> {pl: [socket]*{1,2}, started: Bool, nextServe: Int, score: [Int, Int]}
+const games = new Map();
+
+app.get("/", (req, res) => {
+ res.sendFile(__dirname + "/game.html");
+});
+
+const staticFiles = [
+ "/game.css",
+ "/game.js",
+ "/board.js",
+];
+
+app.get(staticFiles, (req, res) => {
+ res.sendFile(__dirname + req.path);
+});
+
+io.on("connection", (conn) => {
+ let proc = null;
+
+ conn.on("open", () => {
+ console.log("open");
+
+ proc = child_process.spawn("../main", {
+ stdio: ["pipe", "pipe", process.stderr]
+ });
+
+ conn.emit("open");
+
+ let lineBuffer = "";
+
+ proc.stdout.on("data", (data) => {
+ lineBuffer += data;
+ let idx;
+ while ((idx = lineBuffer.indexOf("\n")) != -1) {
+ conn.emit("line", lineBuffer.slice(0, idx));
+ lineBuffer = lineBuffer.slice(idx + 1);
+ }
+ });
+
+ proc.on("close", () => {
+ if (proc != null) {
+ console.log("close");
+ conn.emit("close");
+ proc.kill();
+ proc = null;
+ }
+ });
+
+ proc.on("error", (err) => {
+ if (proc != null) {
+ console.log("close");
+ conn.emit("close");
+ conn.emit("message", "The AI process encountered an error.");
+ proc = null;
+ }
+ });
+ });
+
+ conn.on("disconnect", () => {
+ if (proc != null) {
+ console.log("close");
+ proc.kill();
+ proc = null;
+ }
+ });
+
+ conn.on("line", (line) => {
+ proc.stdin.write(line + "\n");
+ });
+});
+
+server.listen(PORT, () => console.log("Listening on port " + PORT));