adventure
Version:
quickly hack together a nodeschool adventure
326 lines (293 loc) • 9.38 kB
JavaScript
var inherits = require('inherits');
var EventEmitter = require('events').EventEmitter;
var mkdirp = require('mkdirp');
var fs = require('fs');
var path = require('path');
var x256 = require('x256');
var through = require('through2');
var split = require('split');
var minimist = require('minimist');
var showMenu = require('./lib/menu.js');
var showHelp = require('./lib/help.js');
module.exports = Shop;
inherits(Shop, EventEmitter);
function Shop (opts) {
if (!(this instanceof Shop)) return new Shop(opts);
if (!opts) opts = {};
if (typeof opts === 'string') opts = { name: opts };
this.name = opts.name;
this.options = opts;
if (!this.name) return this._error(
'Your adventure must have a name! '
+ 'Supply an `opts.name` to adventure().'
);
this.command = opts.command || commandify(this.name);
this.datadir = opts.datadir || path.resolve(
process.env.HOME || process.env.USERPROFILE,
'.config/' + this.name
);
mkdirp.sync(this.datadir);
this.files = {
completed: path.join(this.datadir, 'completed.json'),
current: path.join(this.datadir, 'current.json')
};
this.state = {
completed: [],
current: null
};
try { this.state.completed = require(this.files.completed) }
catch (err) {}
try { this.state.current = require(this.files.current) }
catch (err) {}
this.colors = opts.colors || {};
var c = {
pass: [0,255,0],
fail: [255,0,0],
info: [0,255,255]
};
var colors = Object.keys(c).reduce(function (acc, key) {
acc[key] = '\x1b[38;5;' + x256(c[key]) + 'm';
return acc;
}, {});
if (!this.colors.pass) this.colors.pass = colors.pass;
if (!this.colors.fail) this.colors.fail = colors.fail;
if (!this.colors.info) this.colors.info = colors.info;
this.colors.reset = '\x1b[00m';
this._adventures = [];
}
Shop.prototype.execute = function (args) {
var cmd = args[0];
var argv = minimist(args, { alias: { h: 'help' } });
if (cmd === 'verify') {
this.verify(args.slice(1), this.state.current);
}
else if (cmd === 'run') {
this.run(args.slice(1), this.state.current);
}
else if (cmd === 'help' || argv.help) {
showHelp({ command: this.command });
}
else if (cmd === 'selected') {
console.log(this.state.current);
}
else if (cmd === 'list') {
console.log(this._adventures
.map(function (adv) { return adv.name })
.join('\n')
);
}
else if (cmd === 'completed') {
console.log(this.state.completed.join('\n'));
}
else if (cmd === 'select') {
this.select(args[1]);
}
else if (cmd === 'print') {
this.select(this.state.current);
}
else if (cmd === 'next' || cmd === 'prev') {
var names = this._adventures
.map(function (adv) { return adv.name })
;
var ix = names.indexOf(this.state.current);
if (cmd === 'next') ix ++
else if (cmd === 'prev') ix --
if (names[ix]) this.select(names[ix])
}
else if (cmd === 'solution') {
var adv = this.find(this.state.current);
if (!adv) {
return console.log(
'No adventure is currently selected. '
+ 'Select an adventure from the menu.'
);
process.exit(1);
}
var p = adv.fn();
if (p.solution) this._show(p.solution);
else console.log('No reference solution available for this adventure.')
}
else if (cmd === 'reset') {
this.state.completed = [];
this.save('completed');
this.state.current = null;
this.save('current');
}
else if (!cmd || cmd === 'menu') {
this.showMenu(this.options);
}
else {
console.log('unrecognized command: ' + cmd);
}
};
Shop.prototype.add = function (name, fn) {
this._adventures.push({ name: name, fn: fn });
};
Shop.prototype.find = function (name) {
for (var i = 0; i < this._adventures.length; i++) {
var adv = this._adventures[i];
if (norm(adv.name) === norm(name)) return adv;
}
function norm (s) { return String(s).replace(/\W/g, '').toLowerCase() }
};
Shop.prototype.verify = function (args, name) {
var self = this;
var adv = this.find(name);
if (!adv) return this._error(
'No adventure is currently selected. '
+ 'Select an adventure from the menu.'
);
var p = adv.fn();
if (!p.verify) return this._error(
"This problem doesn't have a .verify function yet!"
);
if (typeof p.verify !== 'function') return this._error(
'This p.verify is a ' + typeof p.verify
+ '. It should be a function instead.'
);
var s = p.verify(args, function (ok) {
if (ok) self.pass(name, p)
else self.fail(name, p)
});
if (s) this._show(s);
};
Shop.prototype.run = function (args, name) {
var self = this;
var adv = this.find(name);
if (!adv) return this._error(
'No adventure is currently selected. '
+ 'Select an adventure from the menu.'
);
var p = adv.fn();
if (!p.run) return this._error(
"This problem doesn't have a .run function."
);
if (typeof p.run !== 'function') return this._error(
'This p.run is a ' + typeof p.run
+ '. It should be a function instead.'
);
var s = p.run(args);
if (s) this._show(s);
};
Shop.prototype.pass = function (name, p) {
var ix = this.state.completed.indexOf(name);
if (ix < 0) this.state.completed.push(name);
this.save('completed');
if (p.pass) {
this._show(p.pass);
console.log();
}
else {
console.log(
'\n' + this.colors.pass
+ '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@'
);
console.log(
'@@@' + this.colors.reset
+ ' YOUR SOLUTION IS CORRECT'
+ this.colors.pass + '! @@@'
);
console.log('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@');
console.log(this.colors.reset + '\n');
}
if (p.solution) this._show(p.solution);
this.emit('pass', name);
if (this.state.completed.length === this._adventures.length) {
this.emit('finished');
}
};
Shop.prototype.fail = function (name, p) {
if (p.fail) {
this._show(p.fail);
console.log();
}
else {
console.log(
this.colors.fail
+ '#########################################'
);
console.log(
'###' + this.colors.reset
+ ' YOUR SOLUTION IS NOT CORRECT!'
+ this.colors.fail + ' ###'
);
console.log('#########################################');
console.log(this.colors.reset + '\n');
}
this.emit('fail', name);
};
Shop.prototype.select = function (name) {
var adv = this.find(name);
this.state.current = name;
this.save('current');
var p = adv.fn();
if (!p.problem) {
p.problem = this.colors.info + Array(67).join('!') + '\n'
+ '!!!' + this.colors.reset
+ ' This adventure does not have a .problem description yet! '
+ this.colors.info + ' !!!\n!!!' + this.colors.reset
+ ' Set .problem to a string, buffer, stream or function that'
+ this.colors.info + ' !!!\n!!!' + this.colors.reset
+ ' returns a string, buffer, or stream. '
+ this.colors.info + ' !!!\n' + Array(67).join('!') + '\n'
;
}
if (p.problem) this._show(p.problem);
};
Shop.prototype.showMenu = function (opts) {
var self = this;
if (!opts) opts = {};
var menu = showMenu({
fg: opts.fg,
bg: opts.bg,
autoclose: typeof opts.autoclose === 'boolean' ? opts.autoclose : true,
command: this.command,
title: opts.title || this.name.toUpperCase(),
names: this._adventures.map(function (x) { return x.name }),
completed: this.state.completed
});
menu.on('select', function (name) {
console.log();
self.select(name);
});
menu.on('exit', function () {
menu.close();
console.log();
});
return menu;
};
Shop.prototype.save = function (key) {
fs.writeFile(
this.files[key],
JSON.stringify(this.state[key]),
function(){}
);
};
Shop.prototype._error = function (msg) {
console.error('ERROR: ' + msg);
process.exit(1);
};
Shop.prototype._show = function (m) {
var self = this;
if (typeof m === 'object' && m.pipe) {
m.pipe(split()).pipe(through(write)).pipe(process.stdout);
}
else if (typeof m === 'function') {
this._show(m());
}
else console.log(replace(m));
function write (buf, enc, next) {
this.push(replace(buf) + '\n');
next();
}
function replace (s) {
if (typeof s !== 'string') s = String(s);
return s
.replace(/\$ADVENTURE_COMMAND/g, self.command)
.replace(/\$ADVENTURE_NAME/g, self.name)
;
}
}
function commandify (s) {
return String(s).toLowerCase().replace(/\s+/g, '-');
}