├── README.md ├── style.css ├── js ├── path │ ├── astar.js │ └── graph.js ├── placewall.js ├── board.js └── ai.js └── display.html /README.md: -------------------------------------------------------------------------------- 1 |

Quoridor AI

2 | 3 | A Quoridor AI written in JavaScript. Play against a computer right in the browser. 4 | 5 | Demo: danielborowski.com/quoridor-ai/ 6 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * {margin: 0; padding: 0; } 2 | html {overflow-y: scroll;} 3 | ul { list-style: none inside; } 4 | li { list-style: none inside; } 5 | a:focus { outline: none; } 6 | .clear { clear: both; } /*
*/ 7 | a { text-decoration: none; color: black;} 8 | input, textarea { outline: none; } 9 | fieldset { border: none; } 10 | table { border-collapse: collapse; border-spacing: 0; } 11 | ::-moz-selection{ background: pink; color: white; } 12 | ::selection { background: pink; color: white; } 13 | 14 | /* BOARD SETUP */ 15 | #board { margin-left: 20px; border: 9px solid #7da4b4; margin-top: 7px; margin-bottom: 7px; 16 | -moz-box-shadow: 0px 0px 0px 3px #324f5a; 17 | -webkit-box-shadow: 0px 0px 0px 3px #324f5a; 18 | box-shadow: 0px 0px 0px 3px #324f5a; } 19 | #board td { width: 50px; height: 50px; border: 0px solid black; background: #fff; } 20 | #board td.wallPlacementVert, 21 | #board td.wallPlacementVert_NOHOVER { width: 11px; border: 0px; background: #7da4b4; } 22 | #board td.wallPlacementHoriz, 23 | #board td.wallPlacementHoriz_NOHOVER { height: 11px; border: 0px; background: #7da4b4; } 24 | #board td.crossSpace { width: 11px; height: 11px; border: 0px; background: #7da4b4; } 25 | #board td.wallPlacementHoriz, #board td.wallPlacementVert { cursor: pointer; } 26 | #board td.movableGreenSpaces { background: #f3ff6a; } 27 | textarea { visibility: hidden; } 28 | 29 | /* PIECES */ 30 | .pieceComputer { border-radius: 50%; width: 39px; height: 39px; background: #bc2525; margin-left: 5px; border: 1px solid #a31d1d; } 31 | .piecePlayer { border-radius: 50%; width: 39px; height: 39px; background: #3ebc25; margin-left: 5px; border: 1px solid #34a51d; } 32 | 33 | /* WALL PLACEMENT */ 34 | #board td.possibleWall { background: #5f212d; } 35 | #board td.keepWall { background: #5f212d; } 36 | #showCompPieces, #showPlayerPieces { margin-top: 5px; } 37 | #showPlayerPieces span, #showCompPieces span { font-size: 17px; font-family: "Arial"; margin-left: 18px; } 38 | #showPlayerPieces span.wallVal, #showCompPieces span.wallVal { margin: 0; font-size: 24px; } 39 | 40 | /* RULES */ 41 | #play { float: left; overflow: auto; padding-right: 15px; margin-left: 10px; } 42 | #rules { float: left; overflow: auto; width: 520px; height: 400px; 43 | font-size: 15px; margin-top: 28px; margin-left: 20px; font-family: "Helvetica"; } 44 | #rules p:first-child { font-size: 26px; text-align: center; margin-bottom: 12px; } 45 | #rules a { color: #297598; } -------------------------------------------------------------------------------- /js/path/astar.js: -------------------------------------------------------------------------------- 1 | // javascript-astar 2 | // http://github.com/bgrins/javascript-astar 3 | // Freely distributable under the MIT License. 4 | // Implements the astar search algorithm in javascript using a binary heap. 5 | 6 | var astar = { 7 | init: function(grid) { 8 | for(var x = 0, xl = grid.length; x < xl; x++) { 9 | for(var y = 0, yl = grid[x].length; y < yl; y++) { 10 | var node = grid[x][y]; 11 | node.f = 0; 12 | node.g = 0; 13 | node.h = 0; 14 | node.cost = node.type; 15 | node.visited = false; 16 | node.closed = false; 17 | node.parent = null; 18 | } 19 | } 20 | }, 21 | heap: function() { 22 | return new BinaryHeap(function(node) { 23 | return node.f; 24 | }); 25 | }, 26 | search: function(grid, start, end, diagonal, heuristic) { 27 | astar.init(grid); 28 | heuristic = heuristic || astar.manhattan; 29 | diagonal = !!diagonal; 30 | 31 | var openHeap = astar.heap(); 32 | 33 | openHeap.push(start); 34 | 35 | while(openHeap.size() > 0) { 36 | 37 | // Grab the lowest f(x) to process next. Heap keeps this sorted for us. 38 | var currentNode = openHeap.pop(); 39 | 40 | // End case -- result has been found, return the traced path. 41 | if(currentNode === end) { 42 | var curr = currentNode; 43 | var ret = []; 44 | while(curr.parent) { 45 | ret.push(curr); 46 | curr = curr.parent; 47 | } 48 | return ret.reverse(); 49 | } 50 | 51 | // Normal case -- move currentNode from open to closed, process each of its neighbors. 52 | currentNode.closed = true; 53 | 54 | // Find all neighbors for the current node. Optionally find diagonal neighbors as well (false by default). 55 | var neighbors = astar.neighbors(grid, currentNode, diagonal); 56 | 57 | for(var i=0, il = neighbors.length; i < il; i++) { 58 | var neighbor = neighbors[i]; 59 | 60 | if(neighbor.closed || neighbor.isWall()) { 61 | // Not a valid node to process, skip to next neighbor. 62 | continue; 63 | } 64 | 65 | // The g score is the shortest distance from start to current node. 66 | // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet. 67 | var gScore = currentNode.g + neighbor.cost; 68 | var beenVisited = neighbor.visited; 69 | 70 | if(!beenVisited || gScore < neighbor.g) { 71 | 72 | // Found an optimal (so far) path to this node. Take score for node to see how good it is. 73 | neighbor.visited = true; 74 | neighbor.parent = currentNode; 75 | neighbor.h = neighbor.h || heuristic(neighbor.pos, end.pos); 76 | neighbor.g = gScore; 77 | neighbor.f = neighbor.g + neighbor.h; 78 | 79 | if (!beenVisited) { 80 | // Pushing to heap will put it in proper place based on the 'f' value. 81 | openHeap.push(neighbor); 82 | } 83 | else { 84 | // Already seen the node, but since it has been rescored we need to reorder it in the heap 85 | openHeap.rescoreElement(neighbor); 86 | } 87 | } 88 | } 89 | } 90 | 91 | // No result was found - empty array signifies failure to find path. 92 | return []; 93 | }, 94 | manhattan: function(pos0, pos1) { 95 | // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html 96 | 97 | var d1 = Math.abs (pos1.x - pos0.x); 98 | var d2 = Math.abs (pos1.y - pos0.y); 99 | return d1 + d2; 100 | }, 101 | neighbors: function(grid, node, diagonals) { 102 | var ret = []; 103 | var x = node.x; 104 | var y = node.y; 105 | 106 | // West 107 | if(grid[x-1] && grid[x-1][y]) { 108 | ret.push(grid[x-1][y]); 109 | } 110 | 111 | // East 112 | if(grid[x+1] && grid[x+1][y]) { 113 | ret.push(grid[x+1][y]); 114 | } 115 | 116 | // South 117 | if(grid[x] && grid[x][y-1]) { 118 | ret.push(grid[x][y-1]); 119 | } 120 | 121 | // North 122 | if(grid[x] && grid[x][y+1]) { 123 | ret.push(grid[x][y+1]); 124 | } 125 | 126 | if (diagonals) { 127 | 128 | // Southwest 129 | if(grid[x-1] && grid[x-1][y-1]) { 130 | ret.push(grid[x-1][y-1]); 131 | } 132 | 133 | // Southeast 134 | if(grid[x+1] && grid[x+1][y-1]) { 135 | ret.push(grid[x+1][y-1]); 136 | } 137 | 138 | // Northwest 139 | if(grid[x-1] && grid[x-1][y+1]) { 140 | ret.push(grid[x-1][y+1]); 141 | } 142 | 143 | // Northeast 144 | if(grid[x+1] && grid[x+1][y+1]) { 145 | ret.push(grid[x+1][y+1]); 146 | } 147 | 148 | } 149 | 150 | return ret; 151 | } 152 | }; -------------------------------------------------------------------------------- /js/path/graph.js: -------------------------------------------------------------------------------- 1 | // javascript-astar 2 | // http://github.com/bgrins/javascript-astar 3 | // Freely distributable under the MIT License. 4 | // Includes Binary Heap (with modifications) from Marijn Haverbeke. 5 | // http://eloquentjavascript.net/appendix2.html 6 | 7 | 8 | var GraphNodeType = { 9 | OPEN: 1, 10 | WALL: 0 11 | }; 12 | 13 | // Creates a Graph class used in the astar search algorithm. 14 | function Graph(grid) { 15 | var nodes = []; 16 | 17 | for (var x = 0; x < grid.length; x++) { 18 | nodes[x] = []; 19 | 20 | for (var y = 0, row = grid[x]; y < row.length; y++) { 21 | nodes[x][y] = new GraphNode(x, y, row[y]); 22 | } 23 | } 24 | 25 | this.input = grid; 26 | this.nodes = nodes; 27 | } 28 | 29 | Graph.prototype.toString = function() { 30 | var graphString = "\n"; 31 | var nodes = this.nodes; 32 | var rowDebug, row, y, l; 33 | for (var x = 0, len = nodes.length; x < len; x++) { 34 | rowDebug = ""; 35 | row = nodes[x]; 36 | for (y = 0, l = row.length; y < l; y++) { 37 | rowDebug += row[y].type + " "; 38 | } 39 | graphString = graphString + rowDebug + "\n"; 40 | } 41 | return graphString; 42 | }; 43 | 44 | function GraphNode(x,y,type) { 45 | this.data = { }; 46 | this.x = x; 47 | this.y = y; 48 | this.pos = { 49 | x: x, 50 | y: y 51 | }; 52 | this.type = type; 53 | } 54 | 55 | GraphNode.prototype.toString = function() { 56 | return "[" + this.x + " " + this.y + "]"; 57 | }; 58 | 59 | GraphNode.prototype.isWall = function() { 60 | return this.type == GraphNodeType.WALL; 61 | }; 62 | 63 | 64 | function BinaryHeap(scoreFunction){ 65 | this.content = []; 66 | this.scoreFunction = scoreFunction; 67 | } 68 | 69 | BinaryHeap.prototype = { 70 | push: function(element) { 71 | // Add the new element to the end of the array. 72 | this.content.push(element); 73 | 74 | // Allow it to sink down. 75 | this.sinkDown(this.content.length - 1); 76 | }, 77 | pop: function() { 78 | // Store the first element so we can return it later. 79 | var result = this.content[0]; 80 | // Get the element at the end of the array. 81 | var end = this.content.pop(); 82 | // If there are any elements left, put the end element at the 83 | // start, and let it bubble up. 84 | if (this.content.length > 0) { 85 | this.content[0] = end; 86 | this.bubbleUp(0); 87 | } 88 | return result; 89 | }, 90 | remove: function(node) { 91 | var i = this.content.indexOf(node); 92 | 93 | // When it is found, the process seen in 'pop' is repeated 94 | // to fill up the hole. 95 | var end = this.content.pop(); 96 | 97 | if (i !== this.content.length - 1) { 98 | this.content[i] = end; 99 | 100 | if (this.scoreFunction(end) < this.scoreFunction(node)) { 101 | this.sinkDown(i); 102 | } 103 | else { 104 | this.bubbleUp(i); 105 | } 106 | } 107 | }, 108 | size: function() { 109 | return this.content.length; 110 | }, 111 | rescoreElement: function(node) { 112 | this.sinkDown(this.content.indexOf(node)); 113 | }, 114 | sinkDown: function(n) { 115 | // Fetch the element that has to be sunk. 116 | var element = this.content[n]; 117 | 118 | // When at 0, an element can not sink any further. 119 | while (n > 0) { 120 | 121 | // Compute the parent element's index, and fetch it. 122 | var parentN = ((n + 1) >> 1) - 1, 123 | parent = this.content[parentN]; 124 | // Swap the elements if the parent is greater. 125 | if (this.scoreFunction(element) < this.scoreFunction(parent)) { 126 | this.content[parentN] = element; 127 | this.content[n] = parent; 128 | // Update 'n' to continue at the new position. 129 | n = parentN; 130 | } 131 | 132 | // Found a parent that is less, no need to sink any further. 133 | else { 134 | break; 135 | } 136 | } 137 | }, 138 | bubbleUp: function(n) { 139 | // Look up the target element and its score. 140 | var length = this.content.length, 141 | element = this.content[n], 142 | elemScore = this.scoreFunction(element); 143 | 144 | while(true) { 145 | // Compute the indices of the child elements. 146 | var child2N = (n + 1) << 1, child1N = child2N - 1; 147 | // This is used to store the new position of the element, 148 | // if any. 149 | var swap = null; 150 | // If the first child exists (is inside the array)... 151 | if (child1N < length) { 152 | // Look it up and compute its score. 153 | var child1 = this.content[child1N], 154 | child1Score = this.scoreFunction(child1); 155 | 156 | // If the score is less than our element's, we need to swap. 157 | if (child1Score < elemScore) 158 | swap = child1N; 159 | } 160 | 161 | // Do the same checks for the other child. 162 | if (child2N < length) { 163 | var child2 = this.content[child2N], 164 | child2Score = this.scoreFunction(child2); 165 | if (child2Score < (swap === null ? elemScore : child1Score)) { 166 | swap = child2N; 167 | } 168 | } 169 | 170 | // If the element needs to be moved, swap it, and continue. 171 | if (swap !== null) { 172 | this.content[n] = this.content[swap]; 173 | this.content[swap] = element; 174 | n = swap; 175 | } 176 | 177 | // Otherwise, we are done. 178 | else { 179 | break; 180 | } 181 | } 182 | } 183 | }; -------------------------------------------------------------------------------- /js/placewall.js: -------------------------------------------------------------------------------- 1 | 2 | var boardWidth = 9; 3 | 4 | /********************************************* 5 | COMPUTER PLACING ACTUAL WALLS 6 | *********************************************/ 7 | function placeHorizWall(thisWall) { 8 | var noWalls = ($("#nowalls").val()+",").split(','); 9 | var walls = $("#walls").val(); 10 | var thisWall = thisWall; 11 | var nextPart = $('#board td[data-pos='+(thisWall)+']').next().next().attr('data-pos'); 12 | walls = walls+","+thisWall; 13 | walls = walls+","+nextPart; 14 | if (walls[0]==',') { walls = walls.substr(1); } 15 | // delete cross-intersection and directly prior walls that overlap 16 | var intersec = thisWall.substr(0,thisWall.indexOf('-'))+"-"+nextPart.substr(0,nextPart.indexOf('-')); 17 | var prior = parseInt(thisWall.substr(0,thisWall.indexOf('-')))-1; 18 | var prior_2 = parseInt(thisWall.substr(thisWall.indexOf('-')+1))-1; 19 | var priorWall = prior+"-"+prior_2; 20 | // take these wall possibilities away 21 | noWalls.splice(noWalls.indexOf(thisWall),1); 22 | noWalls.splice(noWalls.indexOf(nextPart),1); 23 | if (noWalls.indexOf(intersec) != -1) { noWalls.splice(noWalls.indexOf(intersec),1); $("td[data-pos='"+intersec+"']").removeClass("wallPlacementVert").addClass('wallPlacementVert_NOHOVER'); } 24 | if (noWalls.indexOf(priorWall) != -1) { noWalls.splice(noWalls.indexOf(priorWall),1); $("td[data-pos='"+priorWall+"']").removeClass("wallPlacementHoriz").addClass('wallPlacementHoriz_NOHOVER'); } 25 | // update textareas and display 26 | $("#nowalls").val(noWalls); 27 | $("#walls").val(walls); 28 | $("td[data-pos='"+thisWall+"']").removeClass('wallPlacementHoriz').addClass("wallPlacementHoriz_NOHOVER").addClass("keepWall"); 29 | $("td[data-pos='"+thisWall+"']").next().addClass("keepWall"); 30 | $("td[data-pos='"+nextPart+"']").removeClass('wallPlacementHoriz').addClass("wallPlacementHoriz_NOHOVER").addClass("keepWall"); 31 | // update computer walls 32 | var compWallsLeft = $('#showCompPieces').children().next().text(); 33 | compWallsLeft--; 34 | $('#showCompPieces').children().next().text(compWallsLeft); 35 | } 36 | 37 | function placeVertWall(thisWall) { 38 | var noWalls = ($("#nowalls").val()+",").split(','); 39 | var walls = $("#walls").val(); 40 | var thisWall = thisWall; 41 | // get second part of wall for horiz 42 | var nvf = parseInt(thisWall.substr(0,thisWall.indexOf("-"))) + boardWidth; 43 | var nvs = parseInt(thisWall.substr(thisWall.indexOf("-")+1)) + boardWidth; 44 | var nextPart = nvf+"-"+nvs; 45 | walls = walls+","+thisWall; 46 | walls = walls+","+nextPart; 47 | if (walls[0]==',') { walls = walls.substr(1); } 48 | // delete cross-intersection and directly prior walls that overlap 49 | var intersec = thisWall.substr(0,thisWall.indexOf('-'))+"-"+nextPart.substr(0,nextPart.indexOf('-')); 50 | var prior = parseInt(thisWall.substr(0,thisWall.indexOf('-')))-boardWidth; 51 | var prior_2 = parseInt(thisWall.substr(thisWall.indexOf('-')+1))-boardWidth; 52 | var priorWall = prior+"-"+prior_2; 53 | // take these wall possibilities away 54 | noWalls.splice(noWalls.indexOf(thisWall),1); 55 | noWalls.splice(noWalls.indexOf(nextPart),1); 56 | if (noWalls.indexOf(intersec) != -1) { noWalls.splice(noWalls.indexOf(intersec),1); $("td[data-pos='"+intersec+"']").removeClass("wallPlacementHoriz").addClass('wallPlacementHoriz_NOHOVER'); } 57 | if (noWalls.indexOf(priorWall) != -1) { noWalls.splice(noWalls.indexOf(priorWall),1); $("td[data-pos='"+priorWall+"']").removeClass("wallPlacementVert").addClass('wallPlacementVert_NOHOVER'); } 58 | // update textareas and display 59 | $("#nowalls").val(noWalls); 60 | $("#walls").val(walls); 61 | var cross = nvf-boardWidth+"-"+nvf; 62 | $("td[data-pos='"+thisWall+"']").removeClass('wallPlacementVert').addClass("wallPlacementVert_NOHOVER").addClass("keepWall"); 63 | $("td[data-pos='"+cross+"']").next().addClass("keepWall"); 64 | $("td[data-pos='"+nextPart+"']").removeClass('wallPlacementVert').addClass("wallPlacementVert_NOHOVER").addClass("keepWall"); 65 | // update computer walls 66 | var compWallsLeft = $('#showCompPieces').children().next().text(); 67 | compWallsLeft--; 68 | $('#showCompPieces').children().next().text(compWallsLeft); 69 | } 70 | 71 | /************************************************** 72 | COMPUTER COMPUTING BOARD CHANGES 73 | **************************************************/ 74 | function placeHorizWall_COMPUTE(thisWall,noWalls,walls,playerWallsLeft,compWallsLeft,playerLoc,oppLoc,turn) { 75 | var nextPart = $('#board td[data-pos='+(thisWall)+']').next().next().attr('data-pos'); 76 | var possibleWalls = thisWall+","+nextPart; 77 | var checkLegal = pathToEndExists(possibleWalls,null,null); 78 | if (turn=='c') { var cwa = compWallsLeft; } 79 | else { var cwa = playerWallsLeft; } 80 | if ((checkLegal) && (cwa>0)) { 81 | walls = walls+","+thisWall; 82 | walls = walls+","+nextPart; 83 | if (walls[0]==',') { walls = walls.substr(1); } 84 | // delete cross-intersection and directly prior walls that overlap 85 | var intersec = thisWall.substr(0,thisWall.indexOf('-'))+"-"+nextPart.substr(0,nextPart.indexOf('-')); 86 | var prior = parseInt(thisWall.substr(0,thisWall.indexOf('-')))-1; 87 | var prior_2 = parseInt(thisWall.substr(thisWall.indexOf('-')+1))-1; 88 | var priorWall = prior+"-"+prior_2; 89 | // take these wall possibilities away 90 | noWalls.splice(noWalls.indexOf(thisWall),1); 91 | noWalls.splice(noWalls.indexOf(nextPart),1); 92 | if (noWalls.indexOf(intersec) != -1) { noWalls.splice(noWalls.indexOf(intersec),1); } 93 | if (noWalls.indexOf(priorWall) != -1) { noWalls.splice(noWalls.indexOf(priorWall),1); } 94 | // update number of walls 95 | playerWallsLeft = parseInt(playerWallsLeft); 96 | compWallsLeft = parseInt(compWallsLeft); 97 | if (turn=='c') { compWallsLeft--; var turn = 'p'; } 98 | else { playerWallsLeft--; var turn = 'c'; } 99 | return new Array(noWalls,walls,playerWallsLeft,compWallsLeft,playerLoc,oppLoc,turn); 100 | } else { 101 | return 'illegal'; 102 | } 103 | } 104 | 105 | function placeVertWall_COMPUTE(thisWall,noWalls,walls,playerWallsLeft,compWallsLeft,playerLoc,oppLoc,turn) { 106 | var nvf = parseInt(thisWall.substr(0,thisWall.indexOf("-"))) + boardWidth; 107 | var nvs = parseInt(thisWall.substr(thisWall.indexOf("-")+1)) + boardWidth; 108 | var nextPart = nvf+"-"+nvs; 109 | var possibleWalls = thisWall+","+nextPart; 110 | var checkLegal = pathToEndExists(possibleWalls,null,null); 111 | if (turn=='c') { var cwa = compWallsLeft; } 112 | else { var cwa = playerWallsLeft; } 113 | if ((checkLegal) && (cwa>0)) { 114 | walls = walls+","+thisWall; 115 | walls = walls+","+nextPart; 116 | if (walls[0]==',') { walls = walls.substr(1); } 117 | // delete cross-intersection and directly prior walls that overlap 118 | var intersec = thisWall.substr(0,thisWall.indexOf('-'))+"-"+nextPart.substr(0,nextPart.indexOf('-')); 119 | var prior = parseInt(thisWall.substr(0,thisWall.indexOf('-')))-boardWidth; 120 | var prior_2 = parseInt(thisWall.substr(thisWall.indexOf('-')+1))-boardWidth; 121 | var priorWall = prior+"-"+prior_2; 122 | // take these wall possibilities away 123 | noWalls.splice(noWalls.indexOf(thisWall),1); 124 | noWalls.splice(noWalls.indexOf(nextPart),1); 125 | if (noWalls.indexOf(intersec) != -1) { noWalls.splice(noWalls.indexOf(intersec),1); } 126 | if (noWalls.indexOf(priorWall) != -1) { noWalls.splice(noWalls.indexOf(priorWall),1); } 127 | // update number of walls 128 | playerWallsLeft = parseInt(playerWallsLeft); 129 | compWallsLeft = parseInt(compWallsLeft); 130 | if (turn=='c') { compWallsLeft--; var turn = 'p'; } 131 | else { playerWallsLeft--; var turn = 'c'; } 132 | return new Array(noWalls,walls,playerWallsLeft,compWallsLeft,playerLoc,oppLoc,turn); 133 | } else { 134 | return 'illegal'; 135 | } 136 | } -------------------------------------------------------------------------------- /js/board.js: -------------------------------------------------------------------------------- 1 | 2 | var boardWidth = 9; 3 | var lastSpace = (boardWidth-1)*(boardWidth+1); 4 | 5 | /**************************************************** 6 | RETURN POSSIBLE MOVEMENTS FROM CURRENT SPACE 7 | ****************************************************/ 8 | function possibleMoves(loc,compTesting,opponentLoc,possibleWalls) { 9 | // determine all directional moves 10 | loc = parseInt(loc); 11 | oppLoc = parseInt(opponentLoc); 12 | var moves = new Array(loc-1,loc+1,loc+boardWidth,loc-boardWidth); 13 | var possibleJumps = new Array(); 14 | var removeSpaces = new Array(); 15 | var removeOverlaps_col_first = new Array(8,17,26,35,44,53,62,71,80); 16 | var removeOverlaps_col_last = new Array(0,9,18,27,36,45,54,63,72); 17 | // remove overlapping and OOB moves 18 | for (var x=0;xlastSpace) { removeSpaces.push(moves[x]); } 20 | for (var c1=0;c1lastSpace) && removeSpaces.indexOf(moves[x]) == -1) { removeSpaces.push(moves[x]); } 80 | for (var c1=0;c1lastSpace) && (removeSpaces.indexOf(moves[x]) == -1)) { removeSpaces.push(moves[x]); } 132 | } 133 | // remove all illegal spaces 134 | for (var i=0;i"); 153 | $("#board td").removeClass('movableGreenSpaces'); 154 | var cw = checkWin(); 155 | if (whatPiece=='piecePlayer' && cw==-1) { computerMove(); } 156 | } 157 | 158 | /********************************* 159 | CHECK FOR WINNERS 160 | *********************************/ 161 | function checkWin() { 162 | var computer = $('.pieceComputer').parent().attr("data-pos"); 163 | var player = $('.piecePlayer').parent().attr("data-pos"); 164 | if (player>=0 && player<=8) { alert("YOU WIN!!!!!!!!!!!!"); return 'pwins'; } 165 | if (computer>=72 && computer<=80) { alert("COMPUTER WINS!!!!!!!!!!!!"); return 'cwins'; } 166 | else { return -1; } 167 | } 168 | 169 | /***************************************************** 170 | CHECK FOR ILLEGAL WALLS THAT TRAP PLAYER/COMP 171 | *****************************************************/ 172 | function pathToEndExists(possibleWalls,playerLoc,oppLoc) { 173 | var possibleWalls = possibleWalls; 174 | if (playerLoc==null) { var player = $('.piecePlayer').parent().attr("data-pos"); } 175 | else { var player = playerLoc; } 176 | player = parseInt(player); 177 | var p_visited = new Array(); p_visited.push(player); 178 | var p_path = false; 179 | if (oppLoc==null) { var computer = $('.pieceComputer').parent().attr("data-pos"); } 180 | else { var computer = oppLoc; } 181 | computer = parseInt(computer); 182 | var p_moves = possibleMoves(player,true,computer,possibleWalls); 183 | var c_moves = possibleMoves(computer,true,player,possibleWalls); 184 | var c_visited = new Array(); c_visited.push(computer); 185 | var c_path = false; 186 | // recursive path functions 187 | function followPathPlayer(loc,loop) { 188 | if ((loc>=0)&&(loc<=8)) { p_path = true; } 189 | if (!p_path || loop<8000) { 190 | p_visited.push(parseInt(loc)); 191 | var nextMoves = possibleMoves(loc,true,computer,possibleWalls); 192 | loop++; 193 | for (var c1=0;c1=72)&&(loc<=80)) { c_path = true; } 200 | if (!c_path || loop<8000) { 201 | c_visited.push(parseInt(loc)); 202 | var nextMoves = possibleMoves(loc,true,player,possibleWalls); 203 | loop++; 204 | for (var c1=0;c1=0 && f<=8) { fVal = 1; } 262 | else if (f>=9 && f<=17) { fVal = 3; } 263 | else if (f>=18 && f<=26) { fVal = 5; } 264 | else if (f>=27 && f<=35) { fVal = 7; } 265 | else if (f>=36 && f<=44) { fVal = 9; } 266 | else if (f>=45 && f<=53) { fVal = 11; } 267 | else if (f>=54 && f<=62) { fVal = 13; } 268 | else if (f>=63 && f<=71) { fVal = 15; } 269 | sVal = (s%9)*2; 270 | mat[fVal][sVal] = 0; 271 | mat[fVal][sVal+1] = 0; 272 | } 273 | // if vert wall 274 | else { 275 | if (f>=0 && f<=8) { fVal = 0; } 276 | else if (f>=9 && f<=17) { fVal = 2; } 277 | else if (f>=18 && f<=26) { fVal = 4; } 278 | else if (f>=27 && f<=35) { fVal = 6; } 279 | else if (f>=36 && f<=44) { fVal = 8; } 280 | else if (f>=45 && f<=53) { fVal = 10; } 281 | else if (f>=54 && f<=62) { fVal = 12; } 282 | else if (f>=63 && f<=71) { fVal = 14; } 283 | if (f>=0&&f<=71) { 284 | sVal = (s%9)*2-1; 285 | mat[fVal][sVal] = 0; 286 | mat[fVal+1][sVal] = 0; 287 | } 288 | } 289 | } 290 | // get shortest path in graph with walls 291 | var graph = new Graph(mat); 292 | var start_p = graph.nodes[Math.floor(player/9)*2][player%9*2]; 293 | var end_p_c_array = new Array(0,2,4,6,8,10,12,14,16); 294 | var s_path_player = 200; 295 | for (var i=0;i0) { s_path_player = result.length; } 298 | } 299 | var start_c = graph.nodes[Math.floor(computer/9)*2][computer%9*2]; 300 | var s_path_computer = 200; 301 | for (var i=0;i0) { s_path_computer = result.length; } } 305 | } 306 | if (s_path_player==200) { s_path_player = 0; } 307 | if (s_path_computer==200) { s_path_computer = 0; } 308 | return new Array(Math.ceil(s_path_player/2),Math.ceil(s_path_computer/2)); 309 | } -------------------------------------------------------------------------------- /display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Quoridor AI 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 159 | 160 | 161 | 162 | 163 |
164 |
Walls:
165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 |
499 |
Walls:
500 |
501 | 502 |
503 |

Quoridor AI

504 | Quoridor is a game played on a 9x9 board where your objective is to get your piece to the opposite side of the board from where it begins. You play as green and on 505 | each turn you can either press on your piece and move it or you can place one of your available walls on the board. 506 |

507 | > Click here to view the full rules. 508 |
509 | > The AI was written in JavaScript by Daniel Borowski. See the code on GitHub. 510 |
511 | 512 | 514 |
515 | 516 |
517 | 518 | 519 | 520 | -------------------------------------------------------------------------------- /js/ai.js: -------------------------------------------------------------------------------- 1 | 2 | var boardWidth = 9; 3 | 4 | /*********************************************** 5 | COMPUTER AI WILL BEGIN CALCULATIONS 6 | ***********************************************/ 7 | function computerMove() { 8 | 9 | // get possible computer moves 10 | var compMoves = possibleMoves($('.pieceComputer').parent().attr('data-pos'),true,$('.piecePlayer').parent().attr('data-pos'),null); 11 | 12 | // get possible wall placements 13 | var noWalls = $("#nowalls").val(); 14 | noWalls = noWalls.split(','); 15 | 16 | // begin simulation 17 | simulation(noWalls,compMoves[1]); 18 | 19 | function simulation(wallSimulations,moveSimulations) { 20 | var playerWallsLeft = $('#showPlayerPieces').children().next().text(); 21 | var compWallsLeft = $('#showCompPieces').children().next().text(); 22 | var playerLoc = parseInt($('.piecePlayer').parent().attr('data-pos')); 23 | var oppLoc = parseInt($('.pieceComputer').parent().attr('data-pos')); 24 | var walls = $("#walls").val(); 25 | var noWalls = $("#nowalls").val().split(','); 26 | var arrWins_moves = new Array(0,0,0,0,0,0); 27 | var arrShortestMoves_b = shortestPath(null,null,null); 28 | var arrShortestMoves_a = new Array(); 29 | for (var p=0;p=low_change_computer+2 && wallSimulations[q]!='' && arrShortestMoves_a[q][1]>=worstLen) { 60 | worstWallForComp=q; 61 | worstLen = arrShortestMoves_a[q][1]; 62 | } 63 | } 64 | // find SINGLE wall that can trap computer and prevent it 65 | var walls_t = $("#walls").val(); 66 | var noWalls_t = $("#nowalls").val().split(','); 67 | if (worstWallForComp!=null && parseInt($('#showPlayerPieces').children().next().text())>1 && parseInt($('#showCompPieces').children().next().text())>0) { 68 | var whatWallBlocker = $('#board td[data-pos='+wallSimulations[worstWallForComp]+']').attr('class'); 69 | if (whatWallBlocker=="wallPlacementHoriz") { 70 | var nextPart = $('#board td[data-pos='+(wallSimulations[worstWallForComp])+']').next().next().attr('data-pos'); 71 | var walls_with_blocker = wallSimulations[worstWallForComp] + "," + nextPart; 72 | var inNo = placeHorizWall_COMPUTE(wallSimulations[worstWallForComp],noWalls_t,walls_t,10,10,playerLoc,oppLoc,'c'); 73 | } 74 | else if (whatWallBlocker=="wallPlacementVert") { 75 | var nvf = parseInt(wallSimulations[worstWallForComp].substr(0,wallSimulations[worstWallForComp].indexOf("-"))) + boardWidth; 76 | var nvs = parseInt(wallSimulations[worstWallForComp].substr(wallSimulations[worstWallForComp].indexOf("-")+1)) + boardWidth; 77 | var nextPart = nvf+"-"+nvs; 78 | var walls_with_blocker = wallSimulations[worstWallForComp] + "," + nextPart; 79 | var inNo = placeVertWall_COMPUTE(wallSimulations[worstWallForComp],noWalls_t,walls_t,10,10,playerLoc,oppLoc,'c'); 80 | } 81 | for (var t=0;t1 && parseInt($('#showCompPieces').children().next().text())>0 && 116 | arrShortestMoves_b[1]-arrShortestMoves_b[0]<=-2) { 117 | var compLoc = parseInt($('.pieceComputer').parent().attr('data-pos')); 118 | var finalLocToCheck = null; 119 | var fromLoc = wallSimulations.indexOf(compLoc-1+"-"+compLoc); 120 | if (fromLoc==-1) { 121 | var nextLoc = wallSimulations.indexOf(compLoc+"-"+compLoc+1); 122 | if (nextLoc==-1) { 123 | nextLoc = wallSimulations.indexOf(compLoc-9+"-"+compLoc); 124 | if (nextLoc==-1) { finalLocToCheck = null; } 125 | else { finalLocToCheck = nextLoc; } 126 | } else { finalLocToCheck = nextLoc; } 127 | } else { finalLocToCheck = fromLoc; } 128 | // find player double blocking walls 129 | for (var t=finalLocToCheck+35;t>finalLocToCheck-35;t--) { 130 | if (wallPreventTrap==null && typeof wallSimulations[t]!='undefined') { 131 | var whatWallPlace_0 = $('#board td[data-pos='+wallSimulations[t]+']').attr('class'); 132 | if (whatWallPlace_0=="wallPlacementHoriz") { 133 | var test_wall_1 = wallSimulations[t]; 134 | var test_wall_2 = $('#board td[data-pos='+(test_wall_1)+']').next().next().attr('data-pos'); 135 | var checkLegal = placeHorizWall_COMPUTE(test_wall_1,noWalls_td,walls_td,10,10,playerLoc,oppLoc,'c'); 136 | } else if (whatWallPlace_0=="wallPlacementVert") { 137 | var test_wall_1 = wallSimulations[t]; 138 | var nvf = parseInt(test_wall_1.substr(0,test_wall_1.indexOf("-"))) + boardWidth; 139 | var nvs = parseInt(test_wall_1.substr(test_wall_1.indexOf("-")+1)) + boardWidth; 140 | var test_wall_2 = nvf+"-"+nvs; 141 | var checkLegal = placeVertWall_COMPUTE(test_wall_1,noWalls_td,walls_td,10,10,playerLoc,oppLoc,'c'); 142 | } 143 | if (checkLegal!='illegal' && typeof checkLegal!='undefined' && typeof checkLegal[0]!='undefined' && checkLegal[0]!='' && checkLegal[0]!=' ') { 144 | for (var a=finalLocToCheck+35;a>finalLocToCheck-35;a--) { 145 | if (test_wall_1!=wallSimulations[a] && test_wall_2!=wallSimulations[a] && wallPreventTrap==null && 146 | checkLegal[0].toString().indexOf(wallSimulations[a])!=-1 && typeof wallSimulations[a]!='undefined') { 147 | var whatWallPlace_2 = $('#board td[data-pos='+wallSimulations[a]+']').attr('class'); 148 | if (whatWallPlace_2=="wallPlacementHoriz") { 149 | var test_wall_1_1 = wallSimulations[a]; 150 | var test_wall_2_2 = $('#board td[data-pos='+(test_wall_1_1)+']').next().next().attr('data-pos'); 151 | var checkLegal_2 = placeHorizWall_COMPUTE(test_wall_1_1,checkLegal[0].join(",").split(","),checkLegal[1],10,10,playerLoc,oppLoc,'c'); 152 | } 153 | else if (whatWallPlace_2=="wallPlacementVert") { 154 | var test_wall_1_1 = wallSimulations[a]; 155 | var nvf = parseInt(test_wall_1_1.substr(0,test_wall_1_1.indexOf("-"))) + boardWidth; 156 | var nvs = parseInt(test_wall_1_1.substr(test_wall_1_1.indexOf("-")+1)) + boardWidth; 157 | var test_wall_2_2 = nvf+"-"+nvs; 158 | var checkLegal_2 = placeVertWall_COMPUTE(test_wall_1_1,checkLegal[0].join(",").split(","),checkLegal[1],10,10,playerLoc,oppLoc,'c'); 159 | } 160 | if (checkLegal_2!='illegal' && typeof checkLegal_2!='undefined') { 161 | var sp_t = shortestPath($("#walls").val()+","+test_wall_1+","+test_wall_2+","+test_wall_1_1+","+test_wall_2_2,null,null); 162 | if (sp_t[1]>=low_change_computer+5) { 163 | // find one wall computer can place to make it all illegal 164 | for (var z=finalLocToCheck-35;z5000) { wallPreventTrap='tempfake'; } 228 | console.log("DOUBLE TRAP"); 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | } 241 | } 242 | } 243 | if (wallPreventTrap=='tempfake') { wallPreventTrap=null; } 244 | // determine SINGLE wall that slows player 245 | for (var k=0;k=high_change_player) { 247 | bestWall_sp=k; 248 | high_change_player = arrShortestMoves_a[k][0]; 249 | } 250 | } 251 | // play N=2 moves into future and see if SINGLE wall should wait (only if player has no walls) 252 | var pMoves = possibleMoves($('.piecePlayer').parent().attr('data-pos'),true,$('.pieceComputer').parent().attr('data-pos'),null); 253 | if (parseInt($('#showPlayerPieces').children().next().text())==0 && parseInt($('#showCompPieces').children().next().text())>0 && 254 | pMoves[1].indexOf(0)==-1 && pMoves[1].indexOf(1)==-1 && pMoves[1].indexOf(2)==-1 && pMoves[1].indexOf(3)==-1 && 255 | pMoves[1].indexOf(4)==-1 && pMoves[1].indexOf(5)==-1 && pMoves[1].indexOf(6)==-1 && pMoves[1].indexOf(7)==-1 && pMoves[1].indexOf(8)==-1) { 256 | var playerMoves_1 = possibleMoves($('.piecePlayer').parent().attr('data-pos'),true,$('.pieceComputer').parent().attr('data-pos'),null); 257 | var move_sp = 100; 258 | var tempLoc_1 = null; 259 | for (var p=0;phigh_change_player) { 307 | futureWall = true; 308 | console.log("WALL WILL BE BETTER IN THE FUTURE"); 309 | } 310 | } 311 | if (wallDown_2!='illegal') { 312 | if (sp_a_2[0]>high_change_player) { 313 | futureWall = true; 314 | console.log("WALL WILL BE BETTER IN THE FUTURE"); 315 | } 316 | } 317 | } 318 | } 319 | } 320 | var playerLoc = parseInt($('.piecePlayer').parent().attr('data-pos')); 321 | var compWallsLeft = $('#showCompPieces').children().next().text(); 322 | var finalLocToCheck = null; 323 | var fromLoc = wallSimulations.indexOf(playerLoc-1+"-"+playerLoc); 324 | if (fromLoc==-1) { 325 | var nextLoc = wallSimulations.indexOf(playerLoc+"-"+playerLoc+1); 326 | if (nextLoc==-1) { 327 | nextLoc = wallSimulations.indexOf(playerLoc-9+"-"+playerLoc); 328 | if (nextLoc==-1) { 329 | nextLoc = wallSimulations.indexOf(playerLoc+"-"+playerLoc+9); 330 | if (nextLoc==-1) { 331 | finalLocToCheck = null; 332 | console.log("CANT ATTEMPT DOUBLE WALL"); 333 | } 334 | } else { finalLocToCheck = nextLoc; } 335 | } else { finalLocToCheck = nextLoc; } 336 | } else { finalLocToCheck = fromLoc; } 337 | var walls_d = $("#walls").val(); 338 | var noWalls_d = $("#nowalls").val().split(','); 339 | // determine DOUBLE wall that slows player if computer is behind at least 2 spaces (takes ~ 4 seconds) 340 | if (finalLocToCheck!=null) { 341 | for (var t=finalLocToCheck-25;t1 && typeof wallSimulations[t] != 'undefined' && playerWallsLeft-compWallsLeft<4 && 343 | arrShortestMoves_b[1]-arrShortestMoves_b[0]>=2) { 344 | var whatWallPlace = $('#board td[data-pos='+wallSimulations[t]+']').attr('class'); 345 | if (whatWallPlace=="wallPlacementHoriz") { 346 | var test_wall_1 = wallSimulations[t]; 347 | var test_wall_2 = $('#board td[data-pos='+(test_wall_1)+']').next().next().attr('data-pos'); 348 | var checkLegal = placeHorizWall_COMPUTE(test_wall_1,noWalls_d,walls_d,10,10,playerLoc,oppLoc,'c'); 349 | } else if (whatWallPlace=="wallPlacementVert") { 350 | var test_wall_1 = wallSimulations[t]; 351 | var nvf = parseInt(test_wall_1.substr(0,test_wall_1.indexOf("-"))) + boardWidth; 352 | var nvs = parseInt(test_wall_1.substr(test_wall_1.indexOf("-")+1)) + boardWidth; 353 | var test_wall_2 = nvf+"-"+nvs; 354 | var checkLegal = placeVertWall_COMPUTE(test_wall_1,noWalls_d,walls_d,10,10,playerLoc,oppLoc,'c'); 355 | } 356 | if (checkLegal!='illegal') { 357 | for (var a=finalLocToCheck-25;ahigh_change_player/*+1*/ && checkSP2walls[1]<=low_change_computer+1) { 375 | doublePlaceWall = a; 376 | doubleWallPathLen = checkSP2walls[0]; 377 | break; 378 | } 379 | } 380 | } 381 | } 382 | } 383 | } 384 | } 385 | } 386 | // find best move with shortest path 387 | var fm_pos = 0; 388 | var fastestMove = 100; 389 | for (var p=0;p=3 || 437 | (arrShortestMoves_b[0]<8 && ranWallMove<0.3) || 438 | (arrShortestMoves_b[0]<7 && compMoves[0]>8 && ranWallMove<0.6) || 439 | (arrShortestMoves_b[0]<6 && compMoves[0]>8)) && 440 | high_change_player-arrShortestMoves_b[0]>0 && 441 | (playerWallsLeft-compWallsLeft<3 || arrShortestMoves_b[0]<4 || high_change_player-arrShortestMoves_b[0]>=4)) { 442 | 443 | // if losing dont slow down computer only player 444 | var getDiff = high_change_player-arrShortestMoves_b[0]; 445 | if (arrShortestMoves_b[1]-arrShortestMoves_b[0]>=3 && arrShortestMoves_a[bestWall_sp][1]-arrShortestMoves_b[1]<=getDiff) { 446 | console.log("WALL IS OK FOR COMPUTER (LOSING)"); 447 | var whatWallPlace = $('#board td[data-pos='+(bestWall)+']').attr('class'); 448 | if (whatWallPlace=="wallPlacementHoriz") { var wall = placeHorizWall(bestWall); } 449 | else if (whatWallPlace=="wallPlacementVert") { var wall = placeVertWall(bestWall); } 450 | } else if (arrShortestMoves_b[1]-arrShortestMoves_b[0]<3) { 451 | console.log("WALL IS OK FOR COMPUTER (NOT LOSING)") 452 | var whatWallPlace = $('#board td[data-pos='+(bestWall)+']').attr('class'); 453 | if (whatWallPlace=="wallPlacementHoriz") { var wall = placeHorizWall(bestWall); } 454 | else if (whatWallPlace=="wallPlacementVert") { var wall = placeVertWall(bestWall); } 455 | } else { 456 | console.log("WALL NOT HELPFUL FOR COMPUTER"); 457 | movePiece(compMoves[0],bestMove); 458 | } 459 | 460 | } 461 | else { 462 | movePiece(compMoves[0],bestMove); 463 | } 464 | } 465 | } 466 | 467 | --------------------------------------------------------------------------------