// Created by Joel Duggan (joelgduggan@gmail.com), May 2013 // // CONSTANTS // var TOP_MARGIN = 75, BOTTOM_MARGIN = 20, SIDE_MARGIN = 40, LINE_LENGTH = 64, LINE_WIDTH = 5, DOT_RADIUS = 5, TOUCH_CLOSE = 20; // how close they need to be to activate line when mouse over var PLAYER_LETTER = 'A'; var AI_LETTER = 'B'; var BLACK = 'rgb(0,0,0)'; var DOT_COLOR = 'rgb(100, 100, 100)'; var TOUCH_COLOR = 'rgba(255, 0, 0, 0.5)'; var LAST_LINE_COLOR = 'rgb(0, 140, 0)'; var PLAYER_COLOR = 'rgb(40, 40, 255)'; var AI_COLOR = 'rgb(255, 100, 0)'; var TURN_ARROW_COLOR = 'rgb(255, 0, 255)'; // ai difficulty settings var VERY_EASY = 0, EASY = 1, MEDIUM = 2, HARD = 3, VERY_HARD = 4; var TIME_LIMIT = [1000, 1000, 2000, 3000, 5000]; // in milliseconds var DEPTH_LIMIT = [1, 2, 2, 3, 99]; var DUMB_EVAL = [true, true, false, false, false]; // // GLOBAL VARIABLES // var canvas; // canvas html element var ctx; // 2d context to the canvas var numDots; // number of dots in in each row and column var numLines; // number of lines in each row and column var gameStarted; // true if their is a game being played var gameBoard; // holds current state of the game var undoGameBoard; // holds the state that will be returned to if undo is hit var playerTurn; // true if waiting for player to make a move var aiDifficulty; // predefined difficulty level var touchLine; // if < 0 then not touching anything var lastAILine; // the last line that the computer placed oneTimeInitialize(); function oneTimeInitialize() { // attempt to get canvas context canvas = document.getElementById("canvas"); if (canvas.getContext) ctx = canvas.getContext("2d"); else alert("HTML5 Canvas not supported. Sorry."); // listeners document.onmousemove = onMouseMoved; document.onmousedown = onMouseDown; document.ontouchstart= onMouseMoved; document.ontouchend = onMouseDown; gameStarted = false; } // called when the start game button is clicked on the page function newGameInitialize() { if (gameStarted && undoGameBoard != null) { if (!confirm("Are you sure you want to end your current game?")) return; } // change grid to size selected on page changeGridSize(document.getElementById("gridSizeList").value); ctx.textBaseline = "top"; // check difficulty setting if (document.getElementById("diffVeryEasy").checked) aiDifficulty = VERY_EASY; else if (document.getElementById("diffEasy").checked) aiDifficulty = EASY; else if (document.getElementById("diffMedium").checked) aiDifficulty = MEDIUM; else if (document.getElementById("diffHard").checked) aiDifficulty = HARD; else aiDifficulty = VERY_HARD; // create new empty board gameBoard = new Board(); gameBoard.create(numDots, null, null); undoGameBoard = null; document.getElementById('undoButton').disabled = true; gameStarted = true; touchingHoriz = false; touchingVert = false; lastAILine = null; // check to see who should go first if (document.getElementById("playerFirstButton").checked) playerTurn = true; else { playerTurn = false; requestAIMove(); } drawGame(); } function changeGridSize(newSize) { numDots = newSize; numLines = numDots - 1; canvas.width = SIDE_MARGIN * 2 + LINE_LENGTH * numLines; canvas.height = TOP_MARGIN + BOTTOM_MARGIN + LINE_LENGTH * numLines; } function checkMouseTouch(mx, my) { // find mouse position relative to start of dots mx -= SIDE_MARGIN; my -= TOP_MARGIN; touchLine = -1; // out of bounds? if (mx < -TOUCH_CLOSE || my < -TOUCH_CLOSE || mx >= (LINE_LENGTH * numLines + TOUCH_CLOSE) || my >= (LINE_LENGTH * numLines + TOUCH_CLOSE)) return; // x = distance to closest vert. line, y = dist to closest horiz. line x = mx % LINE_LENGTH; y = my % LINE_LENGTH; x = Math.min(x, LINE_LENGTH - x); y = Math.min(y, LINE_LENGTH - y); if (x >= TOUCH_CLOSE && y >= TOUCH_CLOSE) return; if ((x < TOUCH_CLOSE && y >= TOUCH_CLOSE) || (y < TOUCH_CLOSE && x < y)) { // vert. line x = Math.round(mx / LINE_LENGTH); y = clamp(Math.floor(my / LINE_LENGTH), 0, numLines - 1); touchLine = (numLines * numDots) + x * numLines + y; } else { // horiz. line x = clamp(Math.floor(mx / LINE_LENGTH), 0, numLines - 1); y = Math.round(my / LINE_LENGTH); touchLine = x + y * numLines; } if (gameBoard.isLineSet(touchLine)) // is line already set? touchLine = -1; } function onMouseMoved(e) { if (!gameStarted) return; var mx = e.pageX - canvas.offsetLeft; var my = e.pageY - canvas.offsetTop; checkMouseTouch(mx, my); drawGame(); } function onMouseDown(e) { if (!gameStarted) return; if (touchLine >= 0 && playerTurn == true) { // do player's move playerTurn = false; document.getElementById('undoButton').disabled = false; undoGameBoard = gameBoard; gameBoard = gameBoard.applyMove(touchLine, PLAYER_LETTER); touchLine = -1; // do ai's move if (gameBoard.anotherMove == false) requestAIMove(); else playerTurn = true; drawGame(); checkGameOver(); } } // this allows the page to redraw the canvas before ai blocks the thread function requestAIMove() { setTimeout('doAIMove()', 200); } function doAIMove() { var aiMove = AIFindNextMove(gameBoard, TIME_LIMIT[aiDifficulty], DEPTH_LIMIT[aiDifficulty], DUMB_EVAL[aiDifficulty]); gameBoard = gameBoard.applyMove(aiMove, AI_LETTER); lastAILine = aiMove; drawGame(); if (gameBoard.anotherMove == true && gameBoard.isGameOver() == false) requestAIMove(); else playerTurn = true; drawGame(); checkGameOver(); } function checkGameOver() { if (gameBoard.isGameOver()) { gameStarted = false; var players = gameBoard.numBoxesOwnedBy(PLAYER_LETTER); var comps = gameBoard.numBoxesOwnedBy(AI_LETTER); if (players > comps) alert("You Won!"); else if (comps > players) alert("Sorry. You lost."); else alert("Tie game."); drawGame(); } } function undoLastMove() { if (undoGameBoard == null) return; gameBoard = undoGameBoard; undoGameBoard = null; gameStarted = true; document.getElementById('undoButton').disabled = true; drawGame(); } function drawLine(lineNum, color) { ctx.strokeStyle = color; ctx.beginPath(); if (lineNum < gameBoard.q) { // horiz. lines var r = Math.floor(lineNum / numLines); var c = lineNum - r * numLines; ctx.moveTo(SIDE_MARGIN + LINE_LENGTH * c, TOP_MARGIN + LINE_LENGTH * r); ctx.lineTo(SIDE_MARGIN + LINE_LENGTH * (c+1), TOP_MARGIN + LINE_LENGTH * r); } else { lineNum -= gameBoard.q; var c = Math.floor(lineNum / numLines); var r = lineNum - c * numLines ctx.moveTo(SIDE_MARGIN + LINE_LENGTH * c, TOP_MARGIN + LINE_LENGTH * r); ctx.lineTo(SIDE_MARGIN + LINE_LENGTH * c, TOP_MARGIN + LINE_LENGTH * (r+1)); } ctx.closePath(); ctx.stroke(); } function drawGame() { ctx.clearRect(0, 0, canvas.width, canvas.height); // draw filled boxes for (var r = 0; r < numLines; r++) { for (var c = 0; c < numLines; c++) { if (gameBoard.boxes[r][c].getCount() == 4) { if (gameBoard.boxes[r][c].ownedBy == PLAYER_LETTER) ctx.fillStyle = PLAYER_COLOR; else ctx.fillStyle = AI_COLOR; ctx.fillRect(SIDE_MARGIN + c * LINE_LENGTH, TOP_MARGIN + r * LINE_LENGTH, LINE_LENGTH, LINE_LENGTH); } } } // draw the dots ctx.fillStyle = DOT_COLOR; for (var r = 0; r < numDots; r++) { for (var c = 0; c < numDots; c++) { ctx.beginPath(); ctx.arc(SIDE_MARGIN + c * LINE_LENGTH, TOP_MARGIN + r * LINE_LENGTH, DOT_RADIUS, 0,2*Math.PI); ctx.fill(); } } // draw line segments that are set ctx.lineWidth = LINE_WIDTH; for (var i = 0; i < gameBoard.numLines; i++) { if (gameBoard.isLineSet(i)) { if (i != lastAILine) drawLine(i, BLACK); else drawLine(i, LAST_LINE_COLOR); } } // draw line segment that is currently selected if (touchLine >= 0) drawLine(touchLine, TOUCH_COLOR); // draw scores ctx.font = "bold 14px Arial"; ctx.fillStyle = PLAYER_COLOR; ctx.fillText("Player", 9, 5); ctx.fillText(gameBoard.numBoxesOwnedBy(PLAYER_LETTER), 28, 25); ctx.fillStyle = AI_COLOR; ctx.fillText("Computer", canvas.width - 77, 5); ctx.fillText(gameBoard.numBoxesOwnedBy(AI_LETTER), canvas.width - 45, 25); // whose turn arrow if (gameStarted) { if (playerTurn) drawHorizArrow(90, 12, 30, 10, 7, false, TURN_ARROW_COLOR, 2); else drawHorizArrow(canvas.width - 114, 12, 30, 10, 7, true, TURN_ARROW_COLOR, 2); } } function drawHorizArrow(x, y, length, backX, backY, isRight, color, lineWidth) { if (!isRight) { length = -length; backX = -backX; } ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + length, y); ctx.lineTo(x + length - backX, y - backY); ctx.moveTo(x + length, y); ctx.lineTo(x + length - backX, y + backY); ctx.closePath(); ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.stroke(); } function clamp(num, low, high) { if (num < low) return low; else if (num > high) return high; else return num; }