"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);} var tileDrawers = [ function(px, py) {}, function(px, py) { drawCircle(px, py, "#ffffff"); }, function(px, py) { drawKing(px, py); }, function(px, py) {}, // unused function(px, py) { drawCircle(px, py, "#000000"); }, ]; 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(); for (var i = 0; i < N * N; i++) { tileDrawers[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(); } } } var animateSlideDuration = 0.2; function animateSlideInterpolate(a, b, t) {return (1-t) * a + t * b;} var animateSlideState = { value: null, move: null, progress: null, lastStamp: null, imgdata: null, aniframe: null, }; function animateSlide(mv) { cancelAnimateSlide(); var value = board[mv.from]; // Bit of a hack, the board being global state. We capture the imgdata while it's // hot, to be able to repeatedly re-apply it during the animation. board[mv.from] = EMPTY; redraw(); animateSlideState.imgdata = ctx.getImageData(0, 0, cvsSize, cvsSize); board[mv.from] = value; animateSlideState.value = value; animateSlideState.move = mv; animateSlideState.progress = 0; animateSlideState.lastStamp = performance.now(); animateSlideState.aniframe = requestAnimationFrame(animateFrame); animateFrame(animateSlideState.lastStamp); } function animateFrame(stamp) { // stamp can be 0 in case of the first frame var st = animateSlideState; var delta = stamp - st.lastStamp; st.lastStamp = stamp; st.progress += delta / 1000; if (st.progress >= animateSlideDuration) { cancelAnimateSlide(); return; } ctx.putImageData(st.imgdata, 0, 0); var x1 = cellMidX(st.move.from), y1 = cellMidY(st.move.from); var x2 = cellMidX(st.move.to), y2 = cellMidY(st.move.to); var x = animateSlideInterpolate(x1, x2, st.progress / animateSlideDuration); var y = animateSlideInterpolate(y1, y2, st.progress / animateSlideDuration); tileDrawers[st.value](x, y); requestAnimationFrame(animateFrame); } function cancelAnimateSlide() { if (animateSlideState.move == null) return; board[animateSlideState.move.from] = EMPTY; board[animateSlideState.move.to] = animateSlideState.value; cancelAnimationFrame(animateSlideState.aniframe); animateSlideState.move = null; redraw(); } 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); document.getElementById("movelog_container").scrollTop = 99999999; } 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; } document.getElementById("aifirst").classList.add("invisible"); if (aiplayer == null) { aiplayer = -onturn; } processMove(mv); socket.emit("line", stringifyMove(mv)); displayStatus("Waiting for AI..."); } function processMove(mv) { cancelAnimateSlide(); animateSlide(mv); var captures = applyMove(mv); addMoveLog(onturn, mv, captures); onturn = -onturn; lastMove = mv; 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(); });