summaryrefslogtreecommitdiff
path: root/interactor/game.js
diff options
context:
space:
mode:
Diffstat (limited to 'interactor/game.js')
-rw-r--r--interactor/game.js345
1 files changed, 345 insertions, 0 deletions
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();
+});