I enjoyed Jay Link’s JS version of Taipan! so much that I just had to play a complete version. So, I took his C/ncurses version and ported it to JavaScript. The port is available here. (Warning: It’s feature-complete in terms of gameplay, but some graphics effects are missing. Also, it hasn’t been tested extensively, although I’ve got it running in versions of Safari, FF, and IE.) Aside from producing a diverting game, this exercise taught me something interesting.
Easy?
At first, I thought the port might be border-line trivial; Simple C and JS have a high degree of source-code compatibility, so the biggest difficulty seemed to be porting those parts of the ncurses library that the game used to JS. The game didn’t seem to use that much of the library, and performance didn’t look to be an issue, so all signs pointed to an easy job.
Input
In fact, the source code ported over easily enough, and the game used only a handful of easily ported functions. Well, easily ported except for one: getch
. The getch
function does synchronous (i.e. blocking) user input, and it’s used all over the place. It turns out that synchronous user input is something that browser-based JS just will not do.
(Well, based on this post, I think you might be able to make JS turn this trick with some generator-based deep magic. Since generators aren’t available in (all versions of?) IE, however, that technique is little help.)
Therefore, I had to code getch
to take a callback, and, in order to make the callback work, I had to recode the whole game to use continuation passing style for its flow-of-control. I think that this one issue (user input) took up 95% of the effort of the port. Too bad that feature couldn’t be cut!
bcurses
If you’re interested in my extremely incomplete port of ncurses to the browser, you can download it here. I’ve also posted it below, as it’s only a couple hundred lines, at present.
// ncurses for the browser
//
// Development build
// Extremely incomplete
var bcurses = new function () {
var m, p, e, nwaits, target, buffer, screen, tymout;
m = this;
// Private class - Screen
function Screen(w, h)
{
this.w = w;
this.h = h;
this.p = {r:0, c:0};
this.data = new Array(w*h);
this.flags = 0;
this.clear();
};
p = Screen.prototype;
// Screen constants
p.CLEAROK = 1;
// Screen methods
p.clear = function () {
this.p = {r:0, c:0};
for (var i = this.data.length-1; i >= 0; i--) this.data[i] = ' ';
};
p.clrtoeol = function () {
var r=this.p.r, c=this.p.c;
while (c < this.w) this.data[r*this.w+c++] = ' ';
};
p.clrtobot = function () {
var i, stop=this.p.r*this.w + this.p.c;
for (i = this.data.length-1; i >= stop; i--) this.data[i] = ' ';
};
p.printw = function (s) {
var i, r, c;
for (i=0, r=this.p.r, c=this.p.c; i < s.length; i++)
{
if (s.charAt(i) == '\n')
{
r += 1; c = 0;
}
else if (s.charAt(i) == '\b')
{
if (c) c--;
}
else
{
this.data[r*this.w+c] = s.charAt(i); c += 1;
if (c >= this.w)
{
r += 1; c = 0;
}
}
}
this.p.r = r;
this.p.c = c;
};
p.to_markup = function () {
// ToDo
// Need to render cursor
// Need to handle effects
var i, a;
for (i = 0, a = []; i < this.data.length; i += this.w)
a.push(this.data.slice(i,i+this.w).join(''));
return a.join('\n');
};
// Private State
nwaits = 0;
target = undefined;
buffer = undefined;
screen = undefined;
tymout = undefined;
// Private Functions
function HandleKeyboard(ev)
{
buffer.push(ev.key().string);
ev.stop();
};
// Constants
m.A_REVERSE = 0;
m.A_NORMAL = 0;
m.ERR = 0;
// Functions
m.initscr = function () {
// Initialize the library
target = document.getElementById('screen');
buffer = [];
screen = new Screen(target.cols, target.rows);
tymout = -1;
disconnectAll(document, 'onkeydown');
connect(document, 'onkeydown', HandleKeyboard);
// ToDo: IE6 doesn't support blur()? (for TEXTAREAs?)
// disconnectAll(target, 'onfocus');
// connect(target, 'onfocus', target, "blur");
};
m.flushinp = function () {
// Discard typeahead
buffer = [];
};
m.clear = function () {
screen.clear();
screen.flags != screen.CLEAROK;
};
m.clrtoeol = function () {
screen.clrtoeol();
};
m.clrtobot = function () {
screen.clrtobot();
};
m.printw = function (s) {
screen.printw(s);
};
m.attrset = function (attr) {
// ToDo
};
m.curs_set = function (flag) {
// ToDo
};
m.refresh = function () {
target.value = screen.to_markup();
};
m.getch = function (c) {
var timeout = undefined;
if (tymout >= 0)
{
timeout = new Date();
timeout.setMilliseconds(timeout.getMilliseconds() + tymout);
}
if (nwaits)
alert('Uh-Oh');
nwaits++;
function test () {
if (buffer.length)
{
nwaits--;
c(buffer.shift());
}
else if (timeout && (new Date() >= timeout))
{
nwaits--;
c(m.ERR);
}
else
setTimeout(test, 50);
}
test();
};
m.move = function (r, c) {
screen.p.r = r;
screen.p.c = c;
};
m.timeout = function (t) {
tymout = t;
};
m.napms = function (ms, c) {
if (nwaits)
alert('Uh-Oh');
nwaits++;
setTimeout(function () { nwaits--; c(); }, ms);
};
// Export Globals
for (e in this) window[e] = this[e];
}();