├── .nojekyll ├── CNAME ├── README.md ├── LICENSE ├── app.css ├── index.html └── app.js /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | alphabeta.alekskamko.com 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | abTreePractice ([demo](http://alphabeta.alekskamko.com)) 2 | ============== 3 | 4 | D3.js web app for visualizing and understanding the Alpha-Beta Pruning 5 | algorithm. Developed for UC Berkeley's CS61B. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014, Aleks Kamko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #EEE; 3 | overflow: hidden; 4 | } 5 | 6 | svg { 7 | background-color: #EEE; 8 | cursor: default; 9 | -webkit-user-select: none; 10 | -moz-user-select: none; 11 | -ms-user-select: none; 12 | -o-user-select: none; 13 | user-select: none; 14 | } 15 | 16 | .navbar .navbar-nav a, 17 | .navbar .navbar-header span { 18 | color: #FFF; 19 | cursor: default; 20 | } 21 | 22 | .nopadding { 23 | padding: 0 !important; 24 | } 25 | 26 | .nomargin { 27 | margin: 0 !important; 28 | } 29 | 30 | .ab-tree { 31 | position: fixed; 32 | } 33 | 34 | .info { 35 | z-index: 10; 36 | position: absolute; 37 | bottom: 6px; 38 | left: 6px; 39 | font-size: 9px; 40 | } 41 | 42 | .prune-info { 43 | z-index: 10; 44 | position: absolute; 45 | bottom: 6px; 46 | right: 6px; 47 | font-size: 14px; 48 | } 49 | 50 | .ctrl-panel { 51 | z-index: 10; 52 | position: relative; 53 | float: right; 54 | width: 325px; 55 | padding: 10px; 56 | margin: 10px; 57 | background: rgba(0, 0, 0, 0.08); 58 | border-radius: 4px; 59 | font-size: 12px; 60 | } 61 | 62 | .ctrl-panel .btn { 63 | font-size: 12px; 64 | } 65 | 66 | .ctrl-panel hr { 67 | margin: 10px; 68 | height: 0; 69 | border: 0; 70 | border-top: 1px solid #ccc; 71 | } 72 | 73 | .ctrl-panel .row { 74 | margin: 5px 0; 75 | } 76 | 77 | .ctrl-panel .row > div { 78 | padding-left: 2.5px; 79 | padding-right: 2.5px; 80 | margin-left: 0; 81 | margin-right: 0; 82 | } 83 | 84 | .ctrl-panel .row .progress { 85 | margin-bottom: 0; 86 | padding: 0; 87 | } 88 | 89 | .ctrl-panel .row .slider-handle { 90 | background-color: #428BCA; 91 | background-image: none; 92 | opacity: 1; 93 | } 94 | 95 | .button-label-container { 96 | padding: 7px 0 0 0; 97 | margin: 0; 98 | } 99 | 100 | .button-label { 101 | padding: 0 3px; 102 | } 103 | 104 | .slider-label.left { 105 | padding: 0 10px 0 0; 106 | } 107 | 108 | .slider-label.right { 109 | padding: 0 0 0 10px; 110 | } 111 | 112 | .answer-true { 113 | color: #008800; 114 | } 115 | 116 | path.link { 117 | fill: none; 118 | stroke: #000; 119 | stroke-width: 4px; 120 | } 121 | 122 | path.link.hover { 123 | stroke: #666666; 124 | } 125 | 126 | path.link.entered, 127 | path.link.entered.hover { 128 | stroke: #008800; 129 | } 130 | 131 | path.link.pruned { 132 | stroke-dasharray: 10,10; 133 | stroke: #880000; 134 | } 135 | 136 | path.link.pruned.hover { 137 | stroke: #cd0000; 138 | } 139 | 140 | path.mouselink { 141 | stroke-width: 30px; 142 | stroke: #000; 143 | stroke-opacity: 0; 144 | cursor: pointer; 145 | } 146 | 147 | 148 | g.node path { 149 | cursor: pointer; 150 | fill: #FFF; 151 | stroke: #000; 152 | } 153 | g.node path:hover { 154 | fill: #EEE; 155 | } 156 | g.node.entered path { 157 | fill: #EEFFEE; 158 | } 159 | 160 | g.node.leaf path { 161 | fill: #FFF; 162 | } 163 | g.node.leaf path:hover { 164 | fill: #EEE; 165 | } 166 | g.node.entered.leaf path { 167 | fill: #EEEEFF; 168 | } 169 | 170 | rect.cursor { 171 | fill: #000; 172 | } 173 | 174 | text { 175 | font-family: 'Helvetica Neue', sans-serif; 176 | font-size: 18px; 177 | pointer-events: none; 178 | } 179 | 180 | text.value { 181 | text-anchor: middle; 182 | font-weight: bold; 183 | } 184 | 185 | g.node.pruned .alpha, 186 | g.node.pruned .beta { 187 | fill: #AA0000; 188 | } 189 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Alpha-Beta Pruning Practice 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 |
34 | 35 |
36 |
37 | 40 |
41 |
42 |
43 |
44 | 47 | 50 | 53 | 56 | 59 | 62 |
63 |
64 |
65 |
66 |
73 | {{((actionLQ.lastAction + 1) / actionLQ.length) * 100}}% Complete (success) 74 |
75 |
76 |
77 |
78 | 79 | 87 | 88 |
89 |
90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 | 99 | 100 |
101 |
102 |
103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 | 111 |
112 |
113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 | 121 |
122 |
123 |
124 |
125 | 126 |
127 |
128 | 129 |
130 |
131 |
132 |
133 | 134 |
135 |
136 | 139 |
140 |
141 |
142 |
143 |
144 |
145 | Star 151 | Developed by Aleks Kamko for UC Berkeley CS61B 153 | 154 |
155 |
156 | Nodes are pruned when {{useAb ? 'β ≤ α' : 'value is in cutoff range'}}. 157 |
158 | 159 | 160 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | Array.prototype.extend = function(array) { 2 | Array.prototype.push.apply(this, array); 3 | } 4 | 5 | angular.module('d3', []) 6 | .factory('d3Service', ['$document', '$q', '$rootScope', 7 | function($document, $q, $rootScope) { 8 | var d = $q.defer(); 9 | function onScriptLoad() { 10 | // Load client in the browser 11 | $rootScope.$apply(function() { d.resolve(window.d3); }); 12 | } 13 | 14 | var scriptTag = $document[0].createElement('script'); 15 | scriptTag.type = 'text/javascript'; 16 | scriptTag.async = true; 17 | scriptTag.src = 'https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js'; 18 | scriptTag.onreadystatechange = function () { 19 | if (this.readyState == 'complete') { onScriptLoad(); } 20 | } 21 | scriptTag.onload = onScriptLoad; 22 | 23 | var s = $document[0].getElementsByTagName('body')[0]; 24 | s.appendChild(scriptTag); 25 | 26 | return { 27 | d3: function() { return d.promise; } 28 | }; 29 | } 30 | ]); 31 | 32 | angular.module('Enums', []) 33 | .factory('EnumService', [function() { 34 | var TreeNodeTypeEnum = { 35 | maxNode: 'maxNode', 36 | minNode: 'minNode', 37 | randNode: 'randNode', 38 | leafNode: 'leafNode', 39 | 40 | opposite: function(t) { 41 | if (t == this.maxNode) { 42 | return this.minNode; 43 | } else if (t == this.minNode) { 44 | return this.maxNode; 45 | } 46 | return t; 47 | }, 48 | }; 49 | 50 | return { 51 | TreeNodeTypeEnum: TreeNodeTypeEnum, 52 | }; 53 | 54 | }]); 55 | 56 | angular.module('ActionListQueue', []) 57 | .factory('ActionLQService', [function() { 58 | 59 | function Action(object, key, oldVal, newVal) { 60 | this.object = object; 61 | this.key = key; 62 | this.oldVal = oldVal; 63 | this.newVal = newVal; 64 | } 65 | Action.prototype.apply = function() { 66 | if (!this.object) { return; } 67 | this.object[this.key] = this.newVal; 68 | } 69 | Action.prototype.reverse = function() { 70 | if (!this.object) { return; } 71 | this.object[this.key] = this.oldVal; 72 | } 73 | 74 | function ActionListQueue() { 75 | this.inAction = false; 76 | this.lastAction = -1; 77 | this.actionListQueue = [] 78 | this.length = 0; 79 | } 80 | ActionListQueue.prototype.pushActionList = function(actionList) { 81 | if (this.inAction) { return false; } 82 | this.actionListQueue.push(actionList); 83 | this.length += 1; 84 | return true; 85 | } 86 | ActionListQueue.prototype.extendActionList = function(actionLists) { 87 | if (this.inAction) { return false; } 88 | this.actionListQueue.extend(actionLists); 89 | this.length += actionLists.length; 90 | return true; 91 | } 92 | ActionListQueue.prototype.stepForward = function() { 93 | if (!this.inAction || 94 | this.lastAction == (this.actionListQueue.length - 1)) { 95 | return false; 96 | } 97 | this.lastAction += 1; 98 | var actionList = this.actionListQueue[this.lastAction]; 99 | var a; 100 | for (var i = 0; i < actionList.length; i++) { 101 | a = actionList[i]; 102 | a.apply(); 103 | } 104 | return true; 105 | } 106 | ActionListQueue.prototype.stepBackward = function() { 107 | if (!this.inAction || this.lastAction == -1) { 108 | return false; 109 | } 110 | var actionList = this.actionListQueue[this.lastAction]; 111 | var a; 112 | for (var i = 0; i < actionList.length; i++) { 113 | a = actionList[i]; 114 | a.reverse(); 115 | } 116 | this.lastAction -= 1; 117 | return true; 118 | } 119 | ActionListQueue.prototype.goToEnd = function() { 120 | if (!this.inAction) { return; } 121 | while (this.stepForward()) {} 122 | } 123 | ActionListQueue.prototype.goToBeginning = function() { 124 | if (!this.inAction) { return; } 125 | while (this.stepBackward()) {} 126 | } 127 | ActionListQueue.prototype.play = function() { 128 | if (!this.inAction) { return; } 129 | var end = false; 130 | var time = 300; 131 | var step = function(aq) { 132 | var res = aq.stepForward(); 133 | if (res) { 134 | setTimeout(step.bind(aq)); 135 | } 136 | }; 137 | step(this); 138 | } 139 | ActionListQueue.prototype.pause = function() { 140 | clearTimeout(this.playTimeout); 141 | } 142 | 143 | return { 144 | Action: Action, 145 | ActionListQueue: ActionListQueue, 146 | } 147 | 148 | }]); 149 | 150 | angular.module('Tree', ['Enums', 'ActionListQueue']) 151 | .factory('TreeService', ['EnumService', 'ActionLQService', function(EnumService, ActionLQService) { 152 | var TreeNodeTypeEnum = EnumService.TreeNodeTypeEnum; 153 | var Action = ActionLQService.Action; 154 | var ActionListQueue = ActionLQService.ActionListQueue; 155 | 156 | function Tree(rootNode, treeType, depth, branchingFactor) { 157 | this.rootNode = rootNode; 158 | this.treeType = treeType; 159 | this.depth = depth; 160 | this.branchingFactor = branchingFactor; 161 | this.mutable = true; 162 | } 163 | Tree.generateABTreeRootNode = function(treeType, maxDepth, branchingFactor, minVal, maxVal) { 164 | function generateSubTree(parentNode, nodeType, depth, bFac) { 165 | var curNode = new TreeNode(nodeType, parentNode, depth, bFac); 166 | if (depth == maxDepth) { 167 | curNode.nodeType = TreeNodeTypeEnum.leafNode; 168 | curNode.value = Math.round(Math.random() * (maxVal - minVal)) - maxVal; 169 | } else { 170 | for (var k = 0; k < bFac; k++) { 171 | curNode.setKthChild(k, 172 | generateSubTree( 173 | curNode, 174 | TreeNodeTypeEnum.opposite(nodeType), 175 | depth + 1, 176 | bFac 177 | ) 178 | ); 179 | } 180 | } 181 | return curNode; 182 | } 183 | return generateSubTree(null, treeType, 1, branchingFactor); 184 | } 185 | Tree.prototype.alphaBeta = function() { 186 | var thisTree = this; 187 | var generatePruneActionList = function(node, bFac) { 188 | actions = []; 189 | var pruneInner = function(node, bFac, actions) { 190 | if (!node) { return; } 191 | 192 | if (node.edgeToParent) { 193 | actions.push(new Action(node.edgeToParent, 'pruned', false, true)); 194 | node.edgeToParent.__pruned = true; 195 | } 196 | var child; 197 | for (var k = 0; k < bFac; k++) { 198 | child = node.getKthChild(k); 199 | pruneInner(child, bFac, actions); 200 | } 201 | } 202 | pruneInner(node, bFac, actions); 203 | return actions; 204 | } 205 | 206 | var abActions = function(node, bFac, a, b, maxNode, actionLQ) { 207 | var enterActions = [ 208 | new Action(node.edgeToParent, 'entered', false, true), 209 | new Action(node, 'entered', false, true), 210 | ]; 211 | var childActionsList = []; 212 | 213 | if (node.nodeType == TreeNodeTypeEnum.leafNode) { 214 | return { 215 | returnVal: node.value, 216 | enterActions: enterActions, 217 | childActionsList: childActionsList, 218 | exitActions: [new Action(node.edgeToParent, 'entered', true, false)], 219 | }; 220 | } 221 | 222 | enterActions.extend([ 223 | new Action(node, 'alpha', node.alpha, a), 224 | new Action(node, 'beta', node.beta, b), 225 | ]); 226 | node.__alpha = a; 227 | node.__beta = b; 228 | 229 | var k = 0, 230 | pruneRest = false, 231 | lastChildExitActions = [], 232 | curVal = maxNode ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY, 233 | child, 234 | childVal, 235 | setValActions, 236 | res; 237 | if (maxNode) { 238 | for (; k < bFac; k++) { 239 | child = node.getKthChild(k); 240 | if (pruneRest) { 241 | lastChildExitActions.extend(generatePruneActionList(child, bFac)); 242 | lastChildExitActions.push( 243 | new Action(node, 'pruned', false, true) 244 | ); 245 | node.__pruned = true; 246 | } else { 247 | res = abActions(child, bFac, a, b, !maxNode, actionLQ); 248 | setValActions = []; 249 | if (res.returnVal > curVal) { 250 | curVal = res.returnVal; 251 | setValActions.push(new Action(node, 'value', node.__value, curVal)); 252 | node.__value = curVal; 253 | } 254 | if (res.returnVal > a) { 255 | a = res.returnVal; 256 | setValActions.extend([ 257 | new Action(node, 'alpha', node.__alpha, a), 258 | ]); 259 | node.__alpha = a; 260 | } 261 | if (res.childActionsList.length) { 262 | res.exitActions.extend(setValActions); 263 | } else { 264 | res.enterActions.extend(setValActions); 265 | } 266 | res.enterActions.extend(lastChildExitActions); 267 | childActionsList.push(res.enterActions); 268 | childActionsList.extend(res.childActionsList); 269 | lastChildExitActions = res.exitActions; 270 | if (b <= a) { 271 | pruneRest = true; 272 | } 273 | } 274 | } 275 | } else { 276 | for (; k < bFac; k++) { 277 | child = node.getKthChild(k); 278 | if (pruneRest) { 279 | lastChildExitActions.extend(generatePruneActionList(child, bFac)); 280 | lastChildExitActions.push( 281 | new Action(node, 'pruned', false, true) 282 | ); 283 | node.__pruned = true; 284 | } else { 285 | res = abActions(child, bFac, a, b, !maxNode, actionLQ); 286 | setValActions = []; 287 | if (res.returnVal < curVal) { 288 | curVal = res.returnVal; 289 | setValActions.push(new Action(node, 'value', node.__value, curVal)); 290 | node.__value = curVal; 291 | } 292 | if (res.returnVal < b) { 293 | b = res.returnVal; 294 | setValActions.extend([ 295 | new Action(node, 'beta', node.__beta, b), 296 | ]); 297 | node.__beta = b; 298 | } 299 | if (res.childActionsList.length) { 300 | res.exitActions.extend(setValActions); 301 | } else { 302 | res.enterActions.extend(setValActions); 303 | } 304 | res.enterActions.extend(lastChildExitActions); 305 | childActionsList.push(res.enterActions); 306 | childActionsList.extend(res.childActionsList); 307 | lastChildExitActions = res.exitActions; 308 | if (b <= a) { 309 | pruneRest = true; 310 | } 311 | } 312 | } 313 | } 314 | childActionsList.push(lastChildExitActions); 315 | var exitActions = [ 316 | new Action(node.edgeToParent, 'entered', true, false), 317 | new Action(node, 'entered', true, false), 318 | ]; 319 | 320 | return { 321 | returnVal: curVal, 322 | enterActions: enterActions, 323 | childActionsList: childActionsList, 324 | exitActions: exitActions, 325 | }; 326 | } 327 | var actionLQ = new ActionListQueue(); 328 | var res = abActions( 329 | this.rootNode, 330 | this.branchingFactor, 331 | Number.NEGATIVE_INFINITY, 332 | Number.POSITIVE_INFINITY, 333 | (this.treeType == TreeNodeTypeEnum.maxNode) 334 | ); 335 | actionLQ.pushActionList(res.enterActions); 336 | actionLQ.extendActionList(res.childActionsList); 337 | actionLQ.pushActionList(res.exitActions); 338 | return actionLQ; 339 | } 340 | Tree.prototype.checkAnswer = function() { 341 | function checkSubTree(node) { 342 | if (node.nodeType == TreeNodeTypeEnum.leafNode) { return true; } 343 | if (node.value != node.__value) { 344 | return false; 345 | } 346 | if (node.edgeToParent && 347 | node.edgeToParent.__pruned && 348 | node.edgeToParent.__pruned != node.edgeToParent.pruned) { 349 | return false; 350 | } 351 | var res = true; 352 | for (var k = 0; k < node.childNum; k++) { 353 | res = res && checkSubTree(node.getKthChild(k)); 354 | } 355 | return res; 356 | } 357 | return checkSubTree(this.rootNode); 358 | } 359 | Tree.prototype.reset = function() { 360 | function resetSubTree(node) { 361 | if (node.edgeToParent) { 362 | node.edgeToParent.entered = false; 363 | node.edgeToParent.pruned = false; 364 | } 365 | node.entered = false; 366 | if (node.nodeType == TreeNodeTypeEnum.leafNode) { return; } 367 | node.value = null; 368 | node.alpha = null; 369 | node.beta = null; 370 | for (var k = 0; k < node.childNum; k++) { 371 | resetSubTree(node.getKthChild(k)); 372 | } 373 | } 374 | resetSubTree(this.rootNode); 375 | } 376 | Tree.prototype.setSolution = function() { 377 | this.alphaBeta(); 378 | function setSolutionForSubTree(node) { 379 | if (node.edgeToParent) { 380 | node.edgeToParent.pruned = node.edgeToParent.__pruned; 381 | } 382 | if (node.nodeType == TreeNodeTypeEnum.leafNode) { return; } 383 | node.value = node.__value; 384 | node.alpha = node.__alpha; 385 | node.beta = node.__beta; 386 | node.pruned = node.__pruned; 387 | for (var k = 0; k < node.childNum; k++) { 388 | setSolutionForSubTree(node.getKthChild(k)); 389 | } 390 | } 391 | setSolutionForSubTree(this.rootNode); 392 | } 393 | 394 | 395 | function TreeNode(nodeType, parentNode, depth, childNum) { 396 | this.nodeType = nodeType; 397 | this.setParent(parentNode); 398 | this.depth = depth; 399 | this.childNum = childNum; 400 | this.children = new Array(childNum); 401 | this.value = null; 402 | } 403 | TreeNode.prototype.setKthChild = function(k, child) { 404 | if (k >= this.childNum) { 405 | throw 'Error: node only holds ' + k + ' children.' 406 | } 407 | this.children[k] = child; 408 | } 409 | TreeNode.prototype.getKthChild = function(k) { 410 | if (k >= this.childNum) { 411 | throw 'Error: node only holds ' + k + ' children.' 412 | } 413 | return this.children[k]; 414 | } 415 | TreeNode.prototype.setParent = function(parentNode) { 416 | if (parentNode) { 417 | this.edgeToParent = new TreeEdge(parentNode, this); 418 | this.parentNode = parentNode; 419 | } 420 | } 421 | 422 | function TreeEdge(source, target) { 423 | this.source = source; 424 | this.target = target; 425 | this.pruned = false; 426 | } 427 | 428 | return { 429 | Tree: Tree, 430 | TreeNode: TreeNode, 431 | TreeEdge: TreeEdge, 432 | } 433 | 434 | }]); 435 | 436 | angular.module('abTreePractice', ['d3', 'Enums', 'Tree']) 437 | .controller('MainCtrl', [ 438 | 'EnumService', 439 | 'TreeService', 440 | '$scope', 441 | '$timeout', 442 | function(EnumService, TreeService, $scope, $timeout) { 443 | var TreeNodeTypeEnum = EnumService.TreeNodeTypeEnum; 444 | var Tree = TreeService.Tree; 445 | 446 | $scope.useAb = true; 447 | $scope.setUseAb = function(bool) { 448 | $scope.useAb = bool; 449 | } 450 | $scope.maxVal = 20; 451 | 452 | $scope.generateRootNode = function(maxFirst) { 453 | $scope.tree.rootNode = Tree.generateABTreeRootNode( 454 | $scope.tree.treeType, 455 | $scope.tree.depth, 456 | $scope.tree.branchingFactor, 457 | -$scope.maxVal, 458 | $scope.maxVal 459 | ); 460 | $scope.actionLQ = null; 461 | }; 462 | $scope.tree = new Tree(null, TreeNodeTypeEnum.maxNode, 4, 3); 463 | $scope.generateRootNode(); 464 | 465 | $scope.incrBranchingFactor = function(incr) { 466 | $scope.tree.branchingFactor = Math.max(2, 467 | $scope.tree.branchingFactor + incr); 468 | $scope.generateRootNode(); 469 | }; 470 | $scope.incrDepth = function(incr) { 471 | $scope.tree.depth = Math.max(3, 472 | $scope.tree.depth + incr); 473 | $scope.generateRootNode(); 474 | }; 475 | $scope.flipMax = function() { 476 | $scope.tree.treeType = TreeNodeTypeEnum.opposite($scope.tree.treeType); 477 | $scope.generateRootNode(); 478 | }; 479 | 480 | $scope.checkAnswer = function() { 481 | if (!$scope.actionLQ) { 482 | $scope.actionLQ = $scope.tree.alphaBeta(); 483 | } 484 | $scope.correct = $scope.tree.checkAnswer(); 485 | } 486 | $scope.correct = null; 487 | 488 | $scope.resetTree = function() { 489 | $scope.tree.reset(); 490 | $scope.reRender(); 491 | } 492 | $scope.showSolution = function() { 493 | $scope.tree.setSolution(); 494 | $scope.reRender(); 495 | } 496 | 497 | $scope.reRender = function() { return; } 498 | $scope.actionLQ = null; 499 | 500 | $scope.toggleABVisual = function() { 501 | if (!$scope.actionLQ) { 502 | $scope.actionLQ = $scope.tree.alphaBeta(); 503 | } 504 | $scope.resetTree(); 505 | if ($scope.actionLQ.inAction) { 506 | $scope.actionLQ.goToBeginning(); 507 | $scope.tree.mutable = true; 508 | $scope.actionLQ.inAction = false; 509 | return; 510 | } 511 | $scope.tree.mutable = false; 512 | $scope.actionLQ.inAction = true; 513 | $scope.correct = null; 514 | } 515 | $scope.stepBackward = function() { 516 | var ret = false; 517 | if ($scope.actionLQ) { 518 | ret = $scope.actionLQ.stepBackward(); 519 | $scope.reRender(); 520 | } 521 | return ret; 522 | } 523 | $scope.stepForward = function() { 524 | var ret = false; 525 | if ($scope.actionLQ) { 526 | ret = $scope.actionLQ.stepForward(); 527 | $scope.reRender(); 528 | } 529 | return ret; 530 | } 531 | $scope.goToBeginning = function() { 532 | $scope.actionLQ.goToBeginning(); 533 | $scope.reRender(); 534 | } 535 | $scope.goToEnd = function() { 536 | $scope.actionLQ.goToEnd(); 537 | $scope.reRender(); 538 | } 539 | 540 | $scope.timeStep = 850; 541 | $scope.play = function() { 542 | var end = false; 543 | var step = function() { 544 | var res = $scope.stepForward(); 545 | if (res) { 546 | $scope.playTimeout = $timeout(step, $scope.timeStep); 547 | } 548 | }; 549 | step(); 550 | } 551 | $scope.pause = function() { 552 | $timeout.cancel($scope.playTimeout); 553 | } 554 | $scope.slider = new Slider('input.slider', { 555 | reversed: true, 556 | formatter: function(value) { 557 | return (value / 1000) + 's per action'; 558 | }, 559 | }).on('slide', function(e) { 560 | $scope.timeStep = e; 561 | }); 562 | 563 | }]) 564 | .directive('abTree', 565 | ['EnumService', 566 | 'd3Service', 567 | '$window', 568 | '$document', 569 | function(EnumService, d3Service, $window, $document) { 570 | var TreeNodeTypeEnum = EnumService.TreeNodeTypeEnum; 571 | return { 572 | restrict: 'E', 573 | scope: { 574 | tree: '=', 575 | reRender: '=', 576 | useAb: '=', 577 | }, 578 | link: function(scope, element, attrs) { 579 | angular.element($document).ready(function () { 580 | var svgMargin = 100, 581 | topMargin = 100, 582 | nodeSideLength = 80, 583 | triNodeHeight = Math.sqrt(Math.pow(nodeSideLength, 2) - 584 | Math.pow((nodeSideLength/2), 2)), 585 | triCenterFromBaseDist = Math.sqrt( 586 | Math.pow((nodeSideLength / Math.sqrt(3)), 2) - 587 | Math.pow((nodeSideLength / 2),2)); 588 | 589 | d3Service.d3().then(function(d3) { 590 | var svgWidth = 0, 591 | svgHeight = 0; 592 | 593 | var svg = d3.select(element[0]) 594 | .append('svg'); 595 | 596 | scope.onResize = function() { 597 | var navbarHeight = angular 598 | .element($document[0].getElementsByClassName('navbar'))[0] 599 | .offsetHeight; 600 | svgWidth = $window.innerWidth; 601 | svgHeight = $window.innerHeight - navbarHeight; 602 | svg.attr('width', svgWidth) 603 | .attr('height', svgHeight); 604 | } 605 | scope.onResize(); 606 | 607 | var lastNodeId = -1; 608 | scope.renderD3Tree = function() { 609 | scope.nodes = []; 610 | scope.links = []; 611 | var root = scope.tree.rootNode, 612 | bFac = scope.tree.branchingFactor, 613 | maxDepth = scope.tree.depth, 614 | yOffset = (svgHeight - (svgMargin + topMargin)) / (maxDepth - 1); 615 | 616 | var renderD3SubTree = function(curNode, xMin, xMax, nodes, links) { 617 | if (!curNode) { return; } 618 | var range = xMax - xMin; 619 | var newOffset = range / bFac; 620 | var yPos = topMargin + (yOffset * (curNode.depth - 1)); 621 | var xPos = xMin + (range / 2); 622 | 623 | curNode.id = ++lastNodeId; 624 | curNode.x = xPos; 625 | curNode.y = yPos; 626 | nodes.push(curNode); 627 | if (curNode.edgeToParent) { 628 | links.push(curNode.edgeToParent); 629 | } 630 | for (var k = 0; k < bFac; k++) { 631 | var kthChild = curNode.getKthChild(k); 632 | renderD3SubTree(kthChild, 633 | xMin + (newOffset * k), 634 | xMin + (newOffset * (k + 1)), 635 | nodes, 636 | links 637 | ); 638 | } 639 | }; 640 | renderD3SubTree(root, svgMargin, svgWidth - svgMargin, 641 | scope.nodes, scope.links); 642 | scope.reRender(); 643 | }; 644 | 645 | scope.$watch(function() { return scope.tree.rootNode; }, 646 | scope.renderD3Tree 647 | ); 648 | scope.$watch('useAb', function() { 649 | scope.reRender(); 650 | }); 651 | 652 | angular.element($window).bind('resize', function() { 653 | scope.onResize(); 654 | clearTimeout(scope.resizeTimeout); 655 | scope.resizeTimeout = setTimeout(function() { 656 | scope.renderD3Tree(); 657 | scope.reRender(); 658 | }, 500); 659 | }); 660 | 661 | // handles to link and node element groups 662 | var path = svg.append('svg:g').selectAll('path'), 663 | vertex = svg.append('svg:g').selectAll('g'); 664 | 665 | // mouse event vars 666 | var selectedNode = null, 667 | mousedownNode = null; 668 | 669 | // compute text width for cursor 670 | function computeTextWidth(text, font) { 671 | // re-use canvas object for better performance 672 | var canvas = computeTextWidth.canvas || 673 | (computeTextWidth.canvas = $document[0].createElement('canvas')); 674 | var context = canvas.getContext('2d'); 675 | context.font = font; 676 | var metrics = context.measureText(text); 677 | return metrics.width; 678 | }; 679 | 680 | // update graph (called when needed) 681 | scope.reRender = function() { 682 | // path (link) group 683 | path = path.data(scope.links, function(link) { 684 | return link.source.id + ',' + link.target.id 685 | }); 686 | 687 | // add new links 688 | var newLinks = path.enter().append('svg:g'); 689 | newLinks.append('svg:path') 690 | .attr('class', 'link'); 691 | newLinks.append('svg:path') 692 | .attr('class', 'mouselink') 693 | .on('mousedown', function(d) { 694 | if (!scope.tree.mutable) { return; } 695 | d.pruned = !d.pruned; 696 | scope.reRender(); 697 | }) 698 | .on('mouseover', function(d) { 699 | // color target link 700 | d3.select(this.parentNode).select('path.link') 701 | .classed('hover', true); 702 | }) 703 | .on('mouseout', function(d) { 704 | // uncolor target link 705 | d3.select(this.parentNode).select('path.link') 706 | .classed('hover', false); 707 | }); 708 | 709 | // remove old links 710 | path.exit().remove(); 711 | 712 | // update existing links 713 | path.select('path.link') 714 | .attr('d', function(d) { 715 | return 'M' + d.source.x + ',' + d.source.y + 716 | 'L' + d.target.x + ',' + d.target.y; 717 | }) 718 | .classed('pruned', function(d) { return d.pruned; }) 719 | .classed('entered', function(d) { 720 | return d.entered; 721 | }); 722 | path.select('path.mouselink') 723 | .attr('d', function(d) { 724 | return 'M' + d.source.x + ',' + d.source.y + 725 | 'L' + d.target.x + ',' + d.target.y; 726 | }); 727 | 728 | // vertex (node) group 729 | vertex = vertex.data(scope.nodes, function(d) { return d.id; }); 730 | 731 | // add new nodes 732 | var newNodes = vertex.enter().append('svg:g') 733 | .classed('node', true) 734 | .classed('leaf', function(d) { 735 | return (d.nodeType == TreeNodeTypeEnum.leafNode); 736 | }); 737 | newNodes.append('svg:path') 738 | .classed('nodepath', true) 739 | .each(function(d) { 740 | d.nodeEle = d3.select(this.parentNode); 741 | }) 742 | .attr('d', function(d) { 743 | var s = nodeSideLength; 744 | if (d.nodeType == TreeNodeTypeEnum.leafNode) { 745 | var ns = s / 2.1; 746 | var a = (s - ns) / 2; 747 | // square leaf nodes 748 | return 'M' + a + ',' + -a + 749 | 'L' + (ns + a) + ',' + -a + 750 | 'L' + (ns + a) + ',' + (-ns - a) + 751 | 'L' + a + ',' + (-ns - a) + 752 | 'L' + a + ',' + -a; 753 | } 754 | var h = triNodeHeight; 755 | // triangular min/max nodes 756 | return 'M' + 0 + ',' + 0 + 757 | 'L' + s + ',' + 0 + 758 | 'L' + (s/2) + ',' + -h + 759 | 'L' + 0 + ',' + 0; 760 | }) 761 | .on('mousedown', function(d) { 762 | // select node 763 | if (!scope.tree.mutable) { return; } 764 | mousedownNode = d; 765 | d.oldVal = d.value; 766 | scope.reRender(); 767 | }); 768 | // show node IDs and alpha-beta 769 | newNodes.append('svg:text') 770 | .attr('class', 'value'); 771 | newNodes.append('svg:text') 772 | .attr('class', 'prunemsg'); 773 | newNodes.append('svg:text') 774 | .attr('class', 'alpha'); 775 | newNodes.append('svg:text') 776 | .attr('class', 'beta'); 777 | 778 | // remove old nodes 779 | vertex.exit().remove(); 780 | 781 | // update existing nodes 782 | vertex 783 | .classed('entered', function(d) { return d.entered; }) 784 | .classed('pruned', function(d) { return d.pruned; }); 785 | vertex.select('path.nodepath') 786 | .attr('transform', function(d) { 787 | var halfSide = nodeSideLength / 2; 788 | var t = '', r = ''; 789 | if (d.nodeType == TreeNodeTypeEnum.leafNode) { 790 | t = 'translate(' + 791 | (d.x - halfSide) + ',' + 792 | (d.y + halfSide) + ')'; 793 | r = ''; 794 | } else if (d.nodeType == TreeNodeTypeEnum.maxNode) { 795 | t = 'translate(' + 796 | (d.x - halfSide) + ',' + 797 | (d.y + triCenterFromBaseDist) + ')'; 798 | } else if (d.nodeType == TreeNodeTypeEnum.minNode) { 799 | t = 'translate(' + 800 | (d.x + halfSide) + ',' + 801 | (d.y - triCenterFromBaseDist) + ')'; 802 | r = ' rotate(180)'; 803 | } 804 | return t + r; 805 | }); 806 | // update existing node values 807 | vertex.select('text.value') 808 | .attr('x', function(d) { return d.x }) 809 | .attr('y', function(d) { return d.y + 6; }) 810 | .text(function(d) { return (d.value != null) ? d.value : ''; }); 811 | // update existing alpha-beta values 812 | vertex.select('text.alpha') 813 | .attr('x', function(d) { return d.x + 45 }) 814 | .attr('y', function(d) { return d.y - 4; }) 815 | .text(function(d) { 816 | if (d.alpha == null || d.beta == null) { return; }; 817 | if (!scope.useAb) { 818 | var val; 819 | if (d.nodeType == TreeNodeTypeEnum.maxNode) { 820 | val = '≥ ' + d.beta.toString().replace('Infinity', '∞'); 821 | } else if (d.nodeType == TreeNodeTypeEnum.minNode) { 822 | val = '≤ ' + d.alpha.toString().replace('Infinity', '∞'); 823 | } 824 | return 'c ' + val; 825 | } 826 | return 'α: ' + d.alpha.toString().replace('Infinity', '∞'); 827 | }); 828 | vertex.select('text.beta') 829 | .attr('x', function(d) { return d.x + 45 }) 830 | .attr('y', function(d) { return d.y + 16; }) 831 | .text(function(d) { 832 | if (d.alpha == null || d.beta == null) { return; }; 833 | if (!scope.useAb) { return; }; 834 | return 'β: ' + d.beta.toString().replace('Infinity', '∞'); 835 | }); 836 | // update existing cursor 837 | vertex.select('rect.cursor') 838 | .attr('x', function(node) { 839 | var nodeVal = node.value; 840 | var valStr = (nodeVal == null) ? '' : nodeVal.toString(); 841 | 842 | var valSVG = d3.select(this.parentNode).select('text').node(); 843 | var valSVGLength = valSVG ? valSVG.getComputedTextLength() : 0; 844 | 845 | var subStrLength = computeTextWidth( 846 | valStr.substring(0, valCharIndex), 847 | '18px Helvetica Neue' 848 | ); 849 | 850 | return node.x + (subStrLength - (valSVGLength / 2)); 851 | }) 852 | .attr('y', function(node) { 853 | return node.y - 9; 854 | }); 855 | }; 856 | 857 | // node value editing variables and functions 858 | var valCharIndex = null, 859 | cursorRect = null, 860 | valStr = null; 861 | function incrValCharIndex() { 862 | valCharIndex = Math.min(valCharIndex + 1, valStr.length); 863 | } 864 | function decrValCharIndex() { 865 | valCharIndex = Math.max(0, valCharIndex - 1); 866 | } 867 | function parseAndSetNodeValue() { 868 | var newVal = parseFloat(valStr); 869 | newVal = (isNaN(newVal)) ? null : newVal; 870 | selectedNode.value = newVal; 871 | 872 | valCharIndex = null; 873 | valStr = null; 874 | selectedNode = null; 875 | cursorRect.remove(); 876 | cursorRect = null; 877 | } 878 | function discardNodeValueChanges() { 879 | selectedNode.value = selectedNode.oldVal; 880 | valCharIndex = null; 881 | valStr = null; 882 | selectedNode = null; 883 | cursorRect.remove(); 884 | cursorRect = null; 885 | } 886 | 887 | function svgMouseDown() { 888 | if (selectedNode && (mousedownNode !== selectedNode)) { 889 | parseAndSetNodeValue(); 890 | } 891 | 892 | if (mousedownNode) { 893 | if (mousedownNode === selectedNode) { return; } 894 | selectedNode = mousedownNode; 895 | mousedownNode = null; 896 | 897 | nodeValue = selectedNode.value 898 | valStr = (nodeValue == null) ? '' : nodeValue.toString(); 899 | valCharIndex = valStr.length; 900 | 901 | cursorRect = selectedNode.nodeEle 902 | .append('svg:rect') 903 | .attr('class', 'cursor') 904 | .attr('height', 16.5) 905 | .attr('width', 1.5) 906 | .attr('opacity', 1); 907 | 908 | (function fadeRepeat() { 909 | if (!cursorRect) { return; } 910 | cursorRect.transition() 911 | .duration(750) 912 | .attr('opacity', 0) 913 | .transition() 914 | .duration(750) 915 | .attr('opacity', 1) 916 | .each('end', fadeRepeat); 917 | })(); 918 | 919 | scope.reRender(); 920 | } 921 | } 922 | 923 | // only respond once per keydown 924 | var lastKeyDown = -1; 925 | 926 | function windowKeyDown() { 927 | 928 | lastKeyDown = d3.event.keyCode; 929 | 930 | // Editing Edge Weights 931 | if (selectedNode) { 932 | var nodeVal = selectedNode.value; 933 | valStr = (nodeVal == null) ? '' : nodeVal.toString(); 934 | if ((lastKeyDown > 47 && lastKeyDown < 58) // number keys 935 | || lastKeyDown == 189 // minus dash 936 | || lastKeyDown == 190) { // decimal point 937 | var leftSlice = valStr.slice(0, valCharIndex), 938 | rightSlice = valStr.slice(valCharIndex, valStr.length), 939 | lastKeyDown = (lastKeyDown > 188) ? (lastKeyDown - 144) : lastKeyDown, 940 | newNum = String.fromCharCode(lastKeyDown); 941 | valStr = leftSlice + newNum + rightSlice; 942 | selectedNode.value = valStr; 943 | incrValCharIndex(); 944 | } else if (lastKeyDown == 8) { // backspace 945 | d3.event.preventDefault(); 946 | var leftSlice = valStr.slice(0, Math.max(0, valCharIndex - 1)), 947 | rightSlice = valStr.slice(valCharIndex, valStr.length); 948 | valStr = leftSlice + rightSlice; 949 | selectedNode.value = valStr; 950 | decrValCharIndex(); 951 | } else if (lastKeyDown == 37) { // left arrow 952 | d3.event.preventDefault(); 953 | decrValCharIndex(); 954 | } else if (lastKeyDown == 39) { // right arrow 955 | d3.event.preventDefault(); 956 | incrValCharIndex(); 957 | } else if (lastKeyDown == 13) { // enter 958 | parseAndSetNodeValue(); 959 | } else if (lastKeyDown == 27) { // escape 960 | discardNodeValueChanges(); 961 | } 962 | scope.reRender(); 963 | return; 964 | } 965 | } 966 | 967 | function windowKeyUp() { 968 | lastKeyDown = -1; 969 | } 970 | 971 | svg.on('mousedown', svgMouseDown); 972 | d3.select(window) 973 | .on('keydown', windowKeyDown) 974 | .on('keyup', windowKeyUp) 975 | }); 976 | }); 977 | }, 978 | }; 979 | }]); 980 | --------------------------------------------------------------------------------