A bot used for https://code-your-snake.codingmaster398.repl.co/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
507 lines
15 KiB
507 lines
15 KiB
/**
|
|
* The visualization controller will works as a state machine.
|
|
* See files under the `doc` folder for transition descriptions.
|
|
* See https://github.com/jakesgordon/javascript-state-machine
|
|
* for the document of the StateMachine module.
|
|
*/
|
|
var Controller = StateMachine.create({
|
|
initial: 'none',
|
|
events: [
|
|
{
|
|
name: 'init',
|
|
from: 'none',
|
|
to: 'ready'
|
|
},
|
|
{
|
|
name: 'search',
|
|
from: 'starting',
|
|
to: 'searching'
|
|
},
|
|
{
|
|
name: 'pause',
|
|
from: 'searching',
|
|
to: 'paused'
|
|
},
|
|
{
|
|
name: 'finish',
|
|
from: 'searching',
|
|
to: 'finished'
|
|
},
|
|
{
|
|
name: 'resume',
|
|
from: 'paused',
|
|
to: 'searching'
|
|
},
|
|
{
|
|
name: 'cancel',
|
|
from: 'paused',
|
|
to: 'ready'
|
|
},
|
|
{
|
|
name: 'modify',
|
|
from: 'finished',
|
|
to: 'modified'
|
|
},
|
|
{
|
|
name: 'reset',
|
|
from: '*',
|
|
to: 'ready'
|
|
},
|
|
{
|
|
name: 'clear',
|
|
from: ['finished', 'modified'],
|
|
to: 'ready'
|
|
},
|
|
{
|
|
name: 'start',
|
|
from: ['ready', 'modified', 'restarting'],
|
|
to: 'starting'
|
|
},
|
|
{
|
|
name: 'restart',
|
|
from: ['searching', 'finished'],
|
|
to: 'restarting'
|
|
},
|
|
{
|
|
name: 'dragStart',
|
|
from: ['ready', 'finished'],
|
|
to: 'draggingStart'
|
|
},
|
|
{
|
|
name: 'dragEnd',
|
|
from: ['ready', 'finished'],
|
|
to: 'draggingEnd'
|
|
},
|
|
{
|
|
name: 'drawWall',
|
|
from: ['ready', 'finished'],
|
|
to: 'drawingWall'
|
|
},
|
|
{
|
|
name: 'eraseWall',
|
|
from: ['ready', 'finished'],
|
|
to: 'erasingWall'
|
|
},
|
|
{
|
|
name: 'rest',
|
|
from: ['draggingStart', 'draggingEnd', 'drawingWall', 'erasingWall'],
|
|
to : 'ready'
|
|
},
|
|
],
|
|
});
|
|
|
|
$.extend(Controller, {
|
|
gridSize: [64, 36], // number of nodes horizontally and vertically
|
|
operationsPerSecond: 300,
|
|
|
|
/**
|
|
* Asynchronous transition from `none` state to `ready` state.
|
|
*/
|
|
onleavenone: function() {
|
|
var numCols = this.gridSize[0],
|
|
numRows = this.gridSize[1];
|
|
|
|
this.grid = new PF.Grid(numCols, numRows);
|
|
|
|
View.init({
|
|
numCols: numCols,
|
|
numRows: numRows
|
|
});
|
|
View.generateGrid(function() {
|
|
Controller.setDefaultStartEndPos();
|
|
Controller.bindEvents();
|
|
Controller.transition(); // transit to the next state (ready)
|
|
});
|
|
|
|
this.$buttons = $('.control_button');
|
|
|
|
this.hookPathFinding();
|
|
|
|
return StateMachine.ASYNC;
|
|
// => ready
|
|
},
|
|
ondrawWall: function(event, from, to, gridX, gridY) {
|
|
this.setWalkableAt(gridX, gridY, false);
|
|
// => drawingWall
|
|
},
|
|
oneraseWall: function(event, from, to, gridX, gridY) {
|
|
this.setWalkableAt(gridX, gridY, true);
|
|
// => erasingWall
|
|
},
|
|
onsearch: function(event, from, to) {
|
|
var grid,
|
|
timeStart, timeEnd,
|
|
finder = Panel.getFinder();
|
|
|
|
timeStart = window.performance ? performance.now() : Date.now();
|
|
grid = this.grid.clone();
|
|
this.path = finder.findPath(
|
|
this.startX, this.startY, this.endX, this.endY, grid
|
|
);
|
|
this.operationCount = this.operations.length;
|
|
timeEnd = window.performance ? performance.now() : Date.now();
|
|
this.timeSpent = (timeEnd - timeStart).toFixed(4);
|
|
|
|
this.loop();
|
|
// => searching
|
|
},
|
|
onrestart: function() {
|
|
// When clearing the colorized nodes, there may be
|
|
// nodes still animating, which is an asynchronous procedure.
|
|
// Therefore, we have to defer the `abort` routine to make sure
|
|
// that all the animations are done by the time we clear the colors.
|
|
// The same reason applies for the `onreset` event handler.
|
|
setTimeout(function() {
|
|
Controller.clearOperations();
|
|
Controller.clearFootprints();
|
|
Controller.start();
|
|
}, View.nodeColorizeEffect.duration * 1.2);
|
|
// => restarting
|
|
},
|
|
onpause: function(event, from, to) {
|
|
// => paused
|
|
},
|
|
onresume: function(event, from, to) {
|
|
this.loop();
|
|
// => searching
|
|
},
|
|
oncancel: function(event, from, to) {
|
|
this.clearOperations();
|
|
this.clearFootprints();
|
|
// => ready
|
|
},
|
|
onfinish: function(event, from, to) {
|
|
View.showStats({
|
|
pathLength: PF.Util.pathLength(this.path),
|
|
timeSpent: this.timeSpent,
|
|
operationCount: this.operationCount,
|
|
});
|
|
View.drawPath(this.path);
|
|
// => finished
|
|
},
|
|
onclear: function(event, from, to) {
|
|
this.clearOperations();
|
|
this.clearFootprints();
|
|
// => ready
|
|
},
|
|
onmodify: function(event, from, to) {
|
|
// => modified
|
|
},
|
|
onreset: function(event, from, to) {
|
|
setTimeout(function() {
|
|
Controller.clearOperations();
|
|
Controller.clearAll();
|
|
Controller.buildNewGrid();
|
|
}, View.nodeColorizeEffect.duration * 1.2);
|
|
// => ready
|
|
},
|
|
|
|
/**
|
|
* The following functions are called on entering states.
|
|
*/
|
|
|
|
onready: function() {
|
|
console.log('=> ready');
|
|
this.setButtonStates({
|
|
id: 1,
|
|
text: 'Start Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.start, this),
|
|
}, {
|
|
id: 2,
|
|
text: 'Pause Search',
|
|
enabled: false,
|
|
}, {
|
|
id: 3,
|
|
text: 'Clear Walls',
|
|
enabled: true,
|
|
callback: $.proxy(this.reset, this),
|
|
});
|
|
// => [starting, draggingStart, draggingEnd, drawingStart, drawingEnd]
|
|
},
|
|
onstarting: function(event, from, to) {
|
|
console.log('=> starting');
|
|
// Clears any existing search progress
|
|
this.clearFootprints();
|
|
this.setButtonStates({
|
|
id: 2,
|
|
enabled: true,
|
|
});
|
|
this.search();
|
|
// => searching
|
|
},
|
|
onsearching: function() {
|
|
console.log('=> searching');
|
|
this.setButtonStates({
|
|
id: 1,
|
|
text: 'Restart Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.restart, this),
|
|
}, {
|
|
id: 2,
|
|
text: 'Pause Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.pause, this),
|
|
});
|
|
// => [paused, finished]
|
|
},
|
|
onpaused: function() {
|
|
console.log('=> paused');
|
|
this.setButtonStates({
|
|
id: 1,
|
|
text: 'Resume Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.resume, this),
|
|
}, {
|
|
id: 2,
|
|
text: 'Cancel Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.cancel, this),
|
|
});
|
|
// => [searching, ready]
|
|
},
|
|
onfinished: function() {
|
|
console.log('=> finished');
|
|
this.setButtonStates({
|
|
id: 1,
|
|
text: 'Restart Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.restart, this),
|
|
}, {
|
|
id: 2,
|
|
text: 'Clear Path',
|
|
enabled: true,
|
|
callback: $.proxy(this.clear, this),
|
|
});
|
|
},
|
|
onmodified: function() {
|
|
console.log('=> modified');
|
|
this.setButtonStates({
|
|
id: 1,
|
|
text: 'Start Search',
|
|
enabled: true,
|
|
callback: $.proxy(this.start, this),
|
|
}, {
|
|
id: 2,
|
|
text: 'Clear Path',
|
|
enabled: true,
|
|
callback: $.proxy(this.clear, this),
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Define setters and getters of PF.Node, then we can get the operations
|
|
* of the pathfinding.
|
|
*/
|
|
hookPathFinding: function() {
|
|
|
|
PF.Node.prototype = {
|
|
get opened() {
|
|
return this._opened;
|
|
},
|
|
set opened(v) {
|
|
this._opened = v;
|
|
Controller.operations.push({
|
|
x: this.x,
|
|
y: this.y,
|
|
attr: 'opened',
|
|
value: v
|
|
});
|
|
},
|
|
get closed() {
|
|
return this._closed;
|
|
},
|
|
set closed(v) {
|
|
this._closed = v;
|
|
Controller.operations.push({
|
|
x: this.x,
|
|
y: this.y,
|
|
attr: 'closed',
|
|
value: v
|
|
});
|
|
},
|
|
get tested() {
|
|
return this._tested;
|
|
},
|
|
set tested(v) {
|
|
this._tested = v;
|
|
Controller.operations.push({
|
|
x: this.x,
|
|
y: this.y,
|
|
attr: 'tested',
|
|
value: v
|
|
});
|
|
},
|
|
};
|
|
|
|
this.operations = [];
|
|
},
|
|
bindEvents: function() {
|
|
$('#draw_area').mousedown($.proxy(this.mousedown, this));
|
|
$(window)
|
|
.mousemove($.proxy(this.mousemove, this))
|
|
.mouseup($.proxy(this.mouseup, this));
|
|
},
|
|
loop: function() {
|
|
var interval = 1000 / this.operationsPerSecond;
|
|
(function loop() {
|
|
if (!Controller.is('searching')) {
|
|
return;
|
|
}
|
|
Controller.step();
|
|
setTimeout(loop, interval);
|
|
})();
|
|
},
|
|
step: function() {
|
|
var operations = this.operations,
|
|
op, isSupported;
|
|
|
|
do {
|
|
if (!operations.length) {
|
|
this.finish(); // transit to `finished` state
|
|
return;
|
|
}
|
|
op = operations.shift();
|
|
isSupported = View.supportedOperations.indexOf(op.attr) !== -1;
|
|
} while (!isSupported);
|
|
|
|
View.setAttributeAt(op.x, op.y, op.attr, op.value);
|
|
},
|
|
clearOperations: function() {
|
|
this.operations = [];
|
|
},
|
|
clearFootprints: function() {
|
|
View.clearFootprints();
|
|
View.clearPath();
|
|
},
|
|
clearAll: function() {
|
|
this.clearFootprints();
|
|
View.clearBlockedNodes();
|
|
},
|
|
buildNewGrid: function() {
|
|
this.grid = new PF.Grid(this.gridSize[0], this.gridSize[1]);
|
|
},
|
|
mousedown: function (event) {
|
|
var coord = View.toGridCoordinate(event.pageX, event.pageY),
|
|
gridX = coord[0],
|
|
gridY = coord[1],
|
|
grid = this.grid;
|
|
|
|
if (this.can('dragStart') && this.isStartPos(gridX, gridY)) {
|
|
this.dragStart();
|
|
return;
|
|
}
|
|
if (this.can('dragEnd') && this.isEndPos(gridX, gridY)) {
|
|
this.dragEnd();
|
|
return;
|
|
}
|
|
if (this.can('drawWall') && grid.isWalkableAt(gridX, gridY)) {
|
|
this.drawWall(gridX, gridY);
|
|
return;
|
|
}
|
|
if (this.can('eraseWall') && !grid.isWalkableAt(gridX, gridY)) {
|
|
this.eraseWall(gridX, gridY);
|
|
}
|
|
},
|
|
mousemove: function(event) {
|
|
var coord = View.toGridCoordinate(event.pageX, event.pageY),
|
|
grid = this.grid,
|
|
gridX = coord[0],
|
|
gridY = coord[1];
|
|
|
|
if (this.isStartOrEndPos(gridX, gridY)) {
|
|
return;
|
|
}
|
|
|
|
switch (this.current) {
|
|
case 'draggingStart':
|
|
if (grid.isWalkableAt(gridX, gridY)) {
|
|
this.setStartPos(gridX, gridY);
|
|
}
|
|
break;
|
|
case 'draggingEnd':
|
|
if (grid.isWalkableAt(gridX, gridY)) {
|
|
this.setEndPos(gridX, gridY);
|
|
}
|
|
break;
|
|
case 'drawingWall':
|
|
this.setWalkableAt(gridX, gridY, false);
|
|
break;
|
|
case 'erasingWall':
|
|
this.setWalkableAt(gridX, gridY, true);
|
|
break;
|
|
}
|
|
},
|
|
mouseup: function(event) {
|
|
if (Controller.can('rest')) {
|
|
Controller.rest();
|
|
}
|
|
},
|
|
setButtonStates: function() {
|
|
$.each(arguments, function(i, opt) {
|
|
var $button = Controller.$buttons.eq(opt.id - 1);
|
|
if (opt.text) {
|
|
$button.text(opt.text);
|
|
}
|
|
if (opt.callback) {
|
|
$button
|
|
.unbind('click')
|
|
.click(opt.callback);
|
|
}
|
|
if (opt.enabled === undefined) {
|
|
return;
|
|
} else if (opt.enabled) {
|
|
$button.removeAttr('disabled');
|
|
} else {
|
|
$button.attr({ disabled: 'disabled' });
|
|
}
|
|
});
|
|
},
|
|
/**
|
|
* When initializing, this method will be called to set the positions
|
|
* of start node and end node.
|
|
* It will detect user's display size, and compute the best positions.
|
|
*/
|
|
setDefaultStartEndPos: function() {
|
|
var width, height,
|
|
marginRight, availWidth,
|
|
centerX, centerY,
|
|
endX, endY,
|
|
nodeSize = View.nodeSize;
|
|
|
|
width = $(window).width();
|
|
height = $(window).height();
|
|
|
|
marginRight = $('#algorithm_panel').width();
|
|
availWidth = width - marginRight;
|
|
|
|
centerX = Math.ceil(availWidth / 2 / nodeSize);
|
|
centerY = Math.floor(height / 2 / nodeSize);
|
|
|
|
this.setStartPos(centerX - 5, centerY);
|
|
this.setEndPos(centerX + 5, centerY);
|
|
},
|
|
setStartPos: function(gridX, gridY) {
|
|
this.startX = gridX;
|
|
this.startY = gridY;
|
|
View.setStartPos(gridX, gridY);
|
|
},
|
|
setEndPos: function(gridX, gridY) {
|
|
this.endX = gridX;
|
|
this.endY = gridY;
|
|
View.setEndPos(gridX, gridY);
|
|
},
|
|
setWalkableAt: function(gridX, gridY, walkable) {
|
|
this.grid.setWalkableAt(gridX, gridY, walkable);
|
|
View.setAttributeAt(gridX, gridY, 'walkable', walkable);
|
|
},
|
|
isStartPos: function(gridX, gridY) {
|
|
return gridX === this.startX && gridY === this.startY;
|
|
},
|
|
isEndPos: function(gridX, gridY) {
|
|
return gridX === this.endX && gridY === this.endY;
|
|
},
|
|
isStartOrEndPos: function(gridX, gridY) {
|
|
return this.isStartPos(gridX, gridY) || this.isEndPos(gridX, gridY);
|
|
},
|
|
});
|
|
|