Monday, May 10, 2010

Bad Javascript

‹prev | My Chain | next›

Now that I have my PlayerList object in each browser that encapsulates access to the list of players other than me, I need to user that in the fab.js/comet code that drives players. In the function that broadcasts to all listeners, I access the player being moved in the window's parent (this is comet rendered in an iframe):
function broadcast(obj) {
listeners.forEach(
function(listener) {
var body = '<script type="text/javascript">' + "\nif (console) console.debug('" + obj.body + "')\n</script>\n";
listener({body: body});

body = '<script type="text/javascript">' + "\n" +
"var attrs = " + obj.body + ";\n" +
"window.parent.player_list.add_player(attrs);\n" +
"var player = window.parent.player_list.get_player(attrs.id);" +
"if (typeof(player) != 'undefined') {\n" +
" player.walk_to(attrs.x, attrs.y);\n" +
"}" +
"</script>\n";
listener({body: body});
}
);
}
The add_player method is silently ignored if the player is already a member of the room (or is me):
PlayerList.prototype.add_player = function(obj) {
if (!this.other_players[obj.id] && obj.id != this.me.id) {
if (console) {console.debug("adding:" + obj.id);}
this.other_players[obj.id] = new Player(obj.id, obj);
}
};
The walk_to method is already a working method on Player so I should be good to go. Except I am not.

Sadly, I spend the bulk of my night tracking down a problem that boils down to this: in Javascript, do not give the same name to both a method and a property. By way of example, when I try to obtain the list of other players, this is bad:
PlayerList.prototype.others = function() {
var ret = [];
for (var id in this.others) {
ret.push(this.others[id]);
}
return ret;
};
After renaming the object property to other_players, I eventually get the code mostly working. Mostly, except the fab.js broadcast code is only broadcasting to one client. I will pick up with that next.

Day #99

Sunday, May 9, 2010

Baby Steps on Mother's Day

‹prev | My Chain | next›

It is doubtful that I will make much progress this Mother's day. Nevertheless, even on a special day like this, I want to make some progress and keep my chain unbroken. So here goes...

With my code now well-factored and well-behaved, I want to begin the process of seeing others in the <canvas> room as I move about it. Right now, all of the comet communication from the fab.js server is moving an explicitly referenced character around. If I am to move many named characters around, I need a list of all players with which I can interact. But first, players need to be able to identify themselves. For that, I add a form above the <canvas> element:
 <body>
<form id="login" method="get">
<label>Name
<input type="text" name="player">
</label>
<input type="submit" value="Play">
</form>

<canvas id="canvas" width="500" height="500"></canvas>
</body>
I am not doing anything fancy on the server side to prevent duplicate logins (and I do not even have accounts on the server side). For now, I simply want to identify myself via the form. If I have done so, show the room (and tell the server). If not, show the login form. This bit of jQuery does the trick:
$(function() {
var kv = location.search.substr(1).split(/=/);
if (kv[0] == 'player' && kv[1]) {
$('#login').hide();
new Room($("#canvas")[0], {me:kv[1]});
}
else {
$('#canvas').hide();
}
});
I am actually a bit surprised that jQuery does not make query parameter parsing easier than that (no doubt there is a plugin for that). For my purposes however, splitting the query paramter string (location.search) on the key-value separator (=) is enough. If the play parameter is present, then hide the login form and show the room. Otherwise hide the room and leave the form visible to prompt the user to login.

Now that players can identify themselves, I need a mechanism for fab.js to interact with a list of players on all connected browsers. My first attempt initializes with me (so that the server never tries to move me, only I can move me), but provides an interface for the server access players so that it can inform the client that Player objects have moved:
var PlayerList = function(me) {
this.me = me;
this.others = {};
};

PlayerList.prototype.add_player = function(obj) {
if (!this.others[obj.id] && obj.id != this.me.id) {
this.others[obj.id] = new Player(obj.id, obj);
}
};

PlayerList.prototype.get_player = function(id) {
return this.others[id];
};
With this, I can add others to my board (but I handle me as a special case) and I also provide a mechanism to access each player so that I can inform them that they need to move.

I stop there for tonight. Hopefully tomorrow, I will be able to get other players showing up in my room.

Day #98

Saturday, May 8, 2010

Keeping the Investors Happy with Firefox Support

‹prev | My Chain | next›

Tonight I need to to obtain buy-in from my investors... my kids. Unfortunately, they still use Firefox. I have developed so far on Chrome exclusively. When I tried to show my kids how things are working so far... well, I was a bit embarrassed.

After refactoring a bit, I get to the point that can fix things for Firefox. The view-only version works (the right window below):



The problem only shows up when I try to control the the game from Firefox. So the problem is not with the <canvas> rendering, rather with the handling of the mouse events. Happily, I still have console.debug output that Firebug picks up:
x_diff: NaN, y_diffNaN, angle: NaN        player.js (line 32)
Tracing that back, I find the source of the trouble is in the event handling of my Player:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.stop();
this.walk_to(evt.offsetX, evt.offsetY);
this.notify_server({id:this.id,x:evt.offsetX, y:evt.offsetY});
break;
}
};
The trouble in there is the use of offsetX/offsetY, which varies between browser implementations. jQuery normalizes the location of events on the page in event.pageX and event.pageY. That is not quite what I need. Instead what I want is the coordinate of the event inside my <canvas> room.

To get that, I need the offset() of the <canvas> element relative to the page. If that is offset 8 pixels from the top and 8 pixels from the side of the page, then I can subtract 8 pixels from the event's pageX and pageY to get the <canvas> coordinate.

My worry here is that I do not want to couple the Player and the Room more than necessary. If possible, I want to notify the Player of an event such that the Player need only pull the x-y coordinate from the event in order to act. Given that reasoning, it sounds as if the best place to put this is in the Room:
Room.prototype.decorate_event = function(evt) {
return {
type: evt.type,
x: evt.pageX - $(this.canvas).offset().left,
y: evt.pageY - $(this.canvas).offset().top
};
};
I can then pass that "decorated" event to the Player, who is a subscriber to events on the <canvas> board:
Room.prototype.init_events = function() {
var self = this;
$(this.canvas).click(function(evt) {
var decorated_event = self.decorate_event(evt);
self.subscribers.forEach(
function(subscriber) { subscriber.notify(decorated_event); }
);
});
};
After updating Player to get the x-y coordinate from event.x and event.y, I have the code working in Firefox and Chrome. Hopefully my investors will be pleased.

Day #97

Friday, May 7, 2010

Stop the Canvas Walker with Comet

‹prev | My Chain | next›

I continue working on my <canvas> / fab.js "game" tonight. After some more refactoring and DRYing up of the code, I am ready to tackle a nagging bug.

The bug is best illustrated in video:



The view-only mode works fine as long as I don't change direction mid-walk. In that case, my character on the view-only screen goes a bit haywire. After a bit of noodling, I think that the cause of this problem is that new walk-to-coordinate does not cancel the previous walk-to-coordinate. Both combine to speed the character off-screen.

The reason this does not happen on the interactive screen is that the event notification method explicitly cancels the previous walk:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
if (this.walker) clearTimeout(this.walker);
this.walk_to(evt.offsetX, evt.offsetY);
this.notify_server({id:this.id,x:evt.offsetX, y:evt.offsetY});
break;
}
};
If I am correct, then I ought to be able to resolve this by moving the cancel-the-previous-walker code into a separate method that can be called by the comet code driving the view-only walker:
Player.prototype.notify = function(evt) {
switch(evt.type) {
case "click":
this.stop();
this.walk_to(evt.offsetX, evt.offsetY);
this.notify_server({id:this.id,x:evt.offsetX, y:evt.offsetY});
break;
}
};

Player.prototype.stop = function () {
if (this.walker) clearTimeout(this.walker);
};
The javascript code that I push via comet then needs to call this new stop() method:
function broadcast(obj) {
listeners.forEach(
function(listener) {
var body = '<script type="text/javascript">' + "\nconsole.debug('" + obj.body + "')\n</script>\n";
listener({body: body});

body = '<script type="text/javascript">' + "\nloc = " + obj.body + ";\nwindow.parent.me.stop();\nwindow.parent.me.walk_to(loc.x, loc.y);\n</script>\n";
listener({body: body});
}
);
}
With that, I restart my fab.js server and try again:



Ah, much better. Up tomorrow: combining the two <canvas> boards so that I can move around my room and see others as they move around as well.

Day #96

Thursday, May 6, 2010

Static HTTP Server in 4 Lines of Fab.js

‹prev | My Chain | next›

Building on my static fab.js file server from last night, I would like to ensure that it only serves up files from the html sub-directory. Right now, a URL that included ../../../etc/passwd might return something that I would prefer it not return:
  (fab.nodejs.fs)
(
function () {
var out = this;
return function (head) {
out({ body: "html/" + head.url.pathname.substr(1) + ".html" });
if (out) out();
};
}
)
Happily, I can make use of a fab.js RegExp app to accomplish this and make my static file server app even shorter:
  (/^\/([_\w]+)$/)
(fab.nodejs.fs)
( fab.tmpl, "html/<%= this %>.html" )
( fab.capture.at, 0 )
Fab.js puts the matched portion of the URL into the capture attribute of the request URL. The fab.capture.at app, with an argument of zero pulls out the zero element from the RegExp matched URL. That gets passed back upstream to the fab.tmpl app which uses micro-templating to build a file path.

I do something similar to serve up Javascript files:
  (/^\/javascript/)
(/^\/([_\w]+)\.js$/)
(fab.nodejs.fs)
( fab.tmpl, "javascript/<%= this %>.js" )
( fab.capture.at, 0 )
(404)
The trailing 404 is the second app for the ternary RegExp that tries to match the .js file. The HTML file app does something similar, but passes on to the next application.

With that, I re-organize some of my javascript and spend the rest of my night readying the code for release on github. No guarantees that it will work, especially with all of the changes fab.js is undergoing currently. Still, hopefully it will be of some use to others.

Day #95

Wednesday, May 5, 2010

Static HTTP Server in 6 Lines of Fab.js

‹prev | My Chain | next›

Tonight, I start the process of moving my <canvas>/fab.js game out of a "play" sandbox into a real code repository. So far, the game consists of moving a character about in one browser screen and watching the results in another browser:



At this point, the entire application consists of three files: game.js (script containing the fab.js backend) and two static HTML files. I create a repository:
cstrom@whitefall:~$ mkdir repos/my_fab_game/
I put game.js directly in that directory and the two static HTML files into an html sub-directory such that:
cstrom@whitefall:~/repos/my_fab_game$ find
.
./html
./html/board.html
./html/view_only.html
./game.js
With that, I need to make game.js aware of the fab.js library—preferably without hard-coding any paths. This is accomplished by putting the library node.js's load path. The easiest way to do this is to create a symbolic link to fab.js in the ~/.node_libraries directory:
cstrom@whitefall:~$ mkdir ~/.node_libraries
cstrom@whitefall:~$ cd .node_libraries/
cstrom@whitefall:~/.node_libraries$ ln -s ../repos/fab
Now, my fab.js script can start with:
with ( require( "fab" ) )
And it will find the fab.js library.

Next up, how to serve those static files?

I had accomplished this during play-time by hard coding the fab.nodejs.fs call to the route. I would prefer something that took the requested URL and pulled the corresponding HTML file from the file system. Sure I could accomplish this in Apache, by why introduce that if I can do it stand alone?

This is the code that I settle on to accomplish just that:

(fab.nodejs.fs)
(
function () {
var out = this;
return function (head) {
out({ body: "html/" + head.url.pathname.substr(1) + ".html" });
if (out) out();
};
}
)
The anonymous upstream function uses the header information to tell fab.nodejs.fs where to find the correct file. This only works if I make a change to fab.nodejs.fs similar to the one I had to make to fab.nodeje.http—putting an upstream listener into fab.nodejs.fs so that my upstream, anonymous function will have access to the URL.

I fork fab.js on github to encapsulate these two changes. I don't know if these changes are needed for others (there are major changes on the way which might address this), but I need it now.

With that, I can run my fab.js app:
cstrom@whitefall:~/repos/my_fab_game$ node game.js 
And request my static files:
cstrom@whitefall:~/repos/my_fab_game$ curl http://localhost:4011/view_only -Ni
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked

<html>
<head>
<style type="text/css">
#canvas {
...
That is a good stopping point for tonight. I will pick up tomorrow fixing a bug and re-organizing the <canvas> javascript code.

Day #94

Tuesday, May 4, 2010

Retrospective: Week Thirteen

‹prev | My Chain | next›

In an ongoing effort to make this chain as valuable as possible to myself (and possibly others), I perform a weekly retrospective of how the past week went (notes from last week). I do this on Tuesdays to avoid conflicts with B'more on Rails, which usually holds events then.

WHAT WENT WELL

  • Shifted off my application-to-update-CouchDB in favor of a fab.js application (at least for a little while). Fab.js is just that cool.
  • Solved a minor mystery with the fab.nodejs.http proxy app.
  • Started playing with the <canvas> tag. I have heard much about it, but never had the chance to do anything with it myself. It was quite easy and very fun.
  • Hooked said <canvas> tag up to fab.js both via AJAX and comet—both were quite easy to do in fab.js.

OBSTACLES / THINGS THAT WENT NOT SO WELL

  • Ran into a problem with comet and fab.js, which I eventually tracked down to oddness with Google Chrome.
  • I am spending a little too much time with this stuff some nights. Sleep is important.

WHAT I'LL TRY NEXT WEEK

  • I foresee no real changes next week, this one has been incredibly productive and I would be lucky to continue such a streak
  • I would like to figure out how test with fab.js. I have not even gotten the build suite to run at this point.


Day #93