pcottle.learnGitBranching/src/js/level/index.js
2013-01-06 14:48:04 -08:00

383 lines
10 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 ModalAlert = require('../views').ModalAlert;
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 Level = Sandbox.extend({
initialize: function(options) {
options = options || {};
options.level = options.level || {};
this.level = options.level;
this.gitCommandsIssued = 0;
this.commandsThatCount = this.getCommandsThatCount();
this.solved = false;
// possible options on how stringent to be on comparisons go here
this.treeCompare = new TreeCompare();
this.initGoalData(options);
this.initName(options);
Level.__super__.initialize.apply(this, [options]);
this.startOffCommand();
},
initName: function(options) {
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) {
this.goalTreeString = this.level.goalTreeString;
this.solutionCommand = this.level.solutionCommand;
if (!this.goalTreeString) {
console.warn('woah no goal, using random other one');
this.goalTreeString = '{"branches":{"master":{"target":"C1","id":"master"},"win":{"target":"C2","id":"win"}},"commits":{"C0":{"parents":[],"id":"C0","rootCommit":true},"C1":{"parents":["C0"],"id":"C1"},"C2":{"parents":["C1"],"id":"C2"}},"HEAD":{"target":"win","id":"HEAD"}}';
this.solutionCommand = 'git checkout -b win; git commit';
}
if (!this.solutionCommand) {
console.warn('no solution provided, really bad form');
}
},
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() {
Main.getEventBaton().trigger(
'commandSubmitted',
'hint; delay 3000; show goal'
);
},
initVisualization: function(options) {
this.mainVis = new Visualization({
el: options.el || this.getDefaultVisEl(),
treeString: options.level.startTree
});
this.initGoalVisualization(options);
},
getDefaultGoalVisEl: function() {
return $('#commandLineHistory');
},
initGoalVisualization: function(options) {
// 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.goalTreeString,
noKeyboardInput: true,
noClick: true
});
},
showSolution: function(command, deferred) {
var confirmDefer = Q.defer();
var confirmView = new ConfirmCancelTerminal({
modalAlert: {
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.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();
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) {
this.parseWaterfall = new ParseWaterfall();
// add our specific functionaity
this.parseWaterfall.addFirst(
'parseWaterfall',
require('../level/commands').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++;
}
},
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 = this.treeCompare.compareAllBranchesWithinTrees(current, this.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;
},
levelSolved: function(defer) {
this.solved = true;
this.hideGoal();
var nextLevel = Main.getLevelArbiter().getNextLevel(this.level.id);
var numCommands = this.gitCommandsIssued;
var best = this.getNumSolutionCommands();
this.mainVis.gitVisuals.finishAnimation()
.then(function() {
// TODO if there is no future level...
// we want to ask if they will move onto the next level...
var nextDialog = new NextLevelConfirm({
nextLevelName: nextLevel.name,
numCommands: numCommands,
best: best
});
return nextDialog.getPromise();
})
.then(function() {
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.goalCanvasHolder.die();
this.mainVis.die();
this.goalVis.die();
this.releaseControl();
this.clear();
delete this.commandCollection;
delete this.mainVis;
delete this.goalVis;
delete this.goalCanvasHolder;
},
getInstantCommands: function() {
var hintMsg = (this.level.hint) ?
this.level.hint :
"Hmm, there doesn't seem to be a hint for this level :-/";
var instants = [
[/^hint$/, function() {
throw new Errors.CommandResult({
msg: hintMsg
});
}]
];
if (!this.solutionCommand) {
instants.push([/^show solution$/, function() {
throw new Errors.CommandResult({
msg: 'No solution provided for this level :-/'
});
}]);
}
return instants;
},
reset: function() {
this.gitCommandsIssued = 0;
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
};
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;