├── .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 |
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 |
--------------------------------------------------------------------------------