pcottle.learnGitBranching/src/js/level/index.js
2013-01-12 23:07:14 -08:00

446 lines
12 KiB
JavaScript

var _ = require('underscore');
var Backbone = require('backbone');
var Q = require('q');
var util = require('../util');
var Main = require('../app');
var Errors = require('../util/errors');
var Sandbox = require('../level/sandbox').Sandbox;
var Visualization = require('../visuals/visualization').Visualization;
var ParseWaterfall = require('../level/parseWaterfall').ParseWaterfall;
var DisabledMap = require('../level/disabledMap').DisabledMap;
var Command = require('../models/commandModel').Command;
var GitShim = require('../git/gitShim').GitShim;
var MultiView = require('../views/multiView').MultiView;
var CanvasTerminalHolder = require('../views').CanvasTerminalHolder;
var ConfirmCancelTerminal = require('../views').ConfirmCancelTerminal;
var NextLevelConfirm = require('../views').NextLevelConfirm;
var LevelToolbar = require('../views').LevelToolbar;
var TreeCompare = require('../git/treeCompare').TreeCompare;
var regexMap = {
'help level': /^help level$/,
'start dialog': /^start dialog$/,
'show goal': /^show goal$/,
'hide goal': /^hide goal$/,
'show solution': /^show solution$/
};
var parse = util.genParseCommand(regexMap, 'processLevelCommand');
var Level = Sandbox.extend({
initialize: function(options) {
options = options || {};
options.level = options.level || {};
this.level = options.level;
this.gitCommandsIssued = [];
this.commandsThatCount = this.getCommandsThatCount();
this.solved = false;
this.treeCompare = new TreeCompare();
this.initGoalData(options);
this.initName(options);
Level.__super__.initialize.apply(this, [options]);
this.startOffCommand();
this.handleOpen(options.deferred);
},
handleOpen: function(deferred) {
deferred = deferred || Q.defer();
// if there is a multiview in the beginning, open that
// and let it resolve our deferred
if (this.level.startDialog) {
new MultiView(_.extend(
{},
this.level.startDialog,
{ deferred: deferred }
));
return;
}
// otherwise, resolve after a 700 second delay to allow
// for us to animate easily
setTimeout(function() {
deferred.resolve();
}, this.getAnimationTime() * 1.2);
},
startDialog: function(command, deferred) {
if (!this.level.startDialog) {
command.set('error', new Errors.GitError({
msg: 'There is no start dialog to show for this level!'
}));
deferred.resolve();
return;
}
this.handleOpen(deferred);
deferred.promise.then(function() {
command.set('status', 'finished');
});
},
initName: function() {
if (!this.level.name || !this.level.id) {
this.level.name = 'Rebase Classic';
console.warn('REALLY BAD FORM need ids and names');
}
this.levelToolbar = new LevelToolbar({
name: this.level.name
});
},
initGoalData: function(options) {
if (!this.level.goalTreeString || !this.level.solutionCommand) {
throw new Error('need goal tree and solution');
}
},
takeControl: function() {
Main.getEventBaton().stealBaton('processLevelCommand', this.processLevelCommand, this);
Level.__super__.takeControl.apply(this);
},
releaseControl: function() {
Main.getEventBaton().releaseBaton('processLevelCommand', this.processLevelCommand, this);
Level.__super__.releaseControl.apply(this);
},
startOffCommand: function() {
if (!this.testOption('noStartCommand')) {
Main.getEventBaton().trigger(
'commandSubmitted',
'hint; delay 2000; show goal'
);
}
},
initVisualization: function(options) {
this.mainVis = new Visualization({
el: options.el || this.getDefaultVisEl(),
treeString: options.level.startTree
});
this.initGoalVisualization();
},
initGoalVisualization: function() {
// first we make the goal visualization holder
this.goalCanvasHolder = new CanvasTerminalHolder();
// then we make a visualization. the "el" here is the element to
// track for size information. the container is where the canvas will be placed
this.goalVis = new Visualization({
el: this.goalCanvasHolder.getCanvasLocation(),
containerElement: this.goalCanvasHolder.getCanvasLocation(),
treeString: this.level.goalTreeString,
noKeyboardInput: true,
noClick: true
});
},
showSolution: function(command, deferred) {
var confirmDefer = Q.defer();
var confirmView = new ConfirmCancelTerminal({
markdowns: [
'## Are you sure you want to see the solution?',
'',
'I believe in you! You can do it'
],
deferred: confirmDefer
});
confirmDefer.promise
.then(_.bind(function() {
// ok great add the solution command
Main.getEventBaton().trigger(
'commandSubmitted',
'reset;' + this.level.solutionCommand
);
this.hideGoal();
command.setResult('Solution command added to the command queue...');
}, this))
.fail(function() {
command.setResult("Great! I'll let you get back to it");
})
.done(function() {
// either way we animate, so both options can share this logic
setTimeout(function() {
command.finishWith(deferred);
}, confirmView.getAnimationTime());
});
},
showGoal: function(command, defer) {
this.goalCanvasHolder.slideIn();
if (!command || !defer) { return; }
setTimeout(function() {
command.finishWith(defer);
}, this.goalCanvasHolder.getAnimationTime());
},
hideGoal: function(command, defer) {
this.goalCanvasHolder.slideOut();
if (!command || !defer) { return; }
setTimeout(function() {
command.finishWith(defer);
}, this.goalCanvasHolder.getAnimationTime());
},
initParseWaterfall: function(options) {
Level.__super__.initParseWaterfall.apply(this, [options]);
// add our specific functionaity
this.parseWaterfall.addFirst(
'parseWaterfall',
parse
);
this.parseWaterfall.addFirst(
'instantWaterfall',
this.getInstantCommands()
);
// if we want to disable certain commands...
if (options.level.disabledMap) {
// disable these other commands
this.parseWaterfall.addFirst(
'instantWaterfall',
new DisabledMap({
disabledMap: options.level.disabledMap
}).getInstantCommands()
);
}
},
initGitShim: function(options) {
// ok we definitely want a shim here
this.gitShim = new GitShim({
afterCB: _.bind(this.afterCommandCB, this),
afterDeferHandler: _.bind(this.afterCommandDefer, this)
});
},
getCommandsThatCount: function() {
var GitCommands = require('../git/commands');
var toCount = [
'git commit',
'git checkout',
'git rebase',
'git reset',
'git branch',
'git revert',
'git merge',
'git cherry-pick'
];
var myRegexMap = {};
_.each(toCount, function(method) {
if (!GitCommands.regexMap[method]) { throw new Error('wut no regex'); }
myRegexMap[method] = GitCommands.regexMap[method];
});
return myRegexMap;
},
afterCommandCB: function(command) {
var matched = false;
_.each(this.commandsThatCount, function(regex) {
matched = matched || regex.test(command.get('rawStr'));
});
if (matched) {
this.gitCommandsIssued.push(command.get('rawStr'));
}
},
afterCommandDefer: function(defer, command) {
if (this.solved) {
command.addWarning(
"You've already solved this level, try other levels with 'show levels'" +
"or go back to the sandbox with 'sandbox'"
);
defer.resolve();
return;
}
// ok so lets see if they solved it...
var current = this.mainVis.gitEngine.exportTree();
var solved;
if (this.level.compareOnlyMaster) {
solved = this.treeCompare.compareBranchWithinTrees(current, this.level.goalTreeString, 'master');
} else if (this.level.compareOnlyBranches) {
solved = this.treeCompare.compareAllBranchesWithinTrees(current, this.level.goalTreeString);
} else {
solved = this.treeCompare.compareAllBranchesWithinTreesAndHEAD(current, this.level.goalTreeString);
}
if (!solved) {
defer.resolve();
return;
}
// woohoo!!! they solved the level, lets animate and such
this.levelSolved(defer);
},
getNumSolutionCommands: function() {
// strip semicolons in bad places
var toAnalyze = this.level.solutionCommand.replace(/^;|;$/g, '');
return toAnalyze.split(';').length;
},
testOption: function(option) {
return this.options.command && new RegExp('--' + option).test(this.options.command.get('rawStr'));
},
levelSolved: function(defer) {
this.solved = true;
Main.getEvents().trigger('levelSolved', this.level.id);
this.hideGoal();
var nextLevel = Main.getLevelArbiter().getNextLevel(this.level.id);
var numCommands = this.gitCommandsIssued.length;
var best = this.getNumSolutionCommands();
var skipFinishDialog = this.testOption('noFinishDialog');
var finishAnimationChain = this.mainVis.gitVisuals.finishAnimation();
if (!skipFinishDialog) {
finishAnimationChain = finishAnimationChain
.then(function() {
// we want to ask if they will move onto the next level
// while giving them their results...
var nextDialog = new NextLevelConfirm({
nextLevel: nextLevel,
numCommands: numCommands,
best: best
});
return nextDialog.getPromise();
});
}
finishAnimationChain
.then(function() {
if (!skipFinishDialog && nextLevel) {
Main.getEventBaton().trigger(
'commandSubmitted',
'level ' + nextLevel.id
);
}
})
.fail(function() {
// nothing to do, we will just close
})
.done(function() {
defer.resolve();
});
},
die: function() {
this.levelToolbar.die();
this.goalDie();
this.mainVis.die();
this.releaseControl();
this.clear();
delete this.commandCollection;
delete this.mainVis;
delete this.goalVis;
delete this.goalCanvasHolder;
},
goalDie: function() {
this.goalCanvasHolder.die();
this.goalVis.die();
},
getInstantCommands: function() {
var hintMsg = (this.level.hint) ?
this.level.hint :
"Hmm, there doesn't seem to be a hint for this level :-/";
return [
[/^help$|^\?$/, function() {
throw new Errors.CommandResult({
msg: 'You are in a level, so multiple forms of help are available. Please select either ' +
'"help level" or "help general"'
});
}],
[/^hint$/, function() {
throw new Errors.CommandResult({
msg: hintMsg
});
}],
[/^build level$/, function() {
throw new Errors.GitError({
msg: "You can't build a level inside a level! Please exit level first"
});
}]
];
},
reset: function() {
this.gitCommandsIssued = [];
this.solved = false;
Level.__super__.reset.apply(this, arguments);
},
startLevel: function(command, deferred) {
this.exitLevel();
setTimeout(function() {
Main.getSandbox().startLevel(command, deferred);
}, this.getAnimationTime() * 1.5);
// wow! that was simple :D
},
exitLevel: function(command, deferred) {
this.die();
if (!command || !deferred) {
return;
}
setTimeout(function() {
command.finishWith(deferred);
}, this.getAnimationTime());
// we need to fade in the sandbox
Main.getEventBaton().trigger('levelExited');
},
processLevelCommand: function(command, defer) {
var methodMap = {
'show goal': this.showGoal,
'hide goal': this.hideGoal,
'show solution': this.showSolution,
'start dialog': this.startDialog,
'help level': this.startDialog
};
var method = methodMap[command.get('method')];
if (!method) {
throw new Error('woah we dont support that method yet', method);
}
method.apply(this, [command, defer]);
}
});
exports.Level = Level;
exports.regexMap = regexMap;