New Taipan! Port

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];
}();
Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in Projects, Web stuff. Bookmark the permalink.

Comments are closed.