mirror of
https://github.com/pcottle/learnGitBranching.git
synced 2025-06-26 07:58:34 +02:00
446 lines
12 KiB
JavaScript
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;
|