Skip to content

Tests #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
158 changes: 93 additions & 65 deletions lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ module.exports = function (app) {

var gamedata = games.join(pin, { session: {} }, { ua: req.headers['user-agent'] });



console.log('-------------------');
console.log(gamedata.game.players);

Expand Down Expand Up @@ -115,62 +113,81 @@ module.exports = function (app) {
res.send(true);
});

/**
* Status: start a new game or hang an xhr until two players have joined.
*
* If there's no pin present (or no game for the given pin) then a new game is
* created by getPin, then retrieved by get(pin), and then the current user
* joins that new game.
*
* The pin is saves in req.session.pin and req.session.ingame, and the xhr
* is closed with a 'start' event.
*
* --
*
* If there is a pin and matching game, it will do one of two things...
*
* If the game is in one of a few states (isn't the FSM supposed to manage
* that stuff?) then it must be ready to go, so close the xhr right away (via
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"isn't the FSM supposed to manage that stuff" - it does manage it (we don't change the state or any properties of the FSM), but I think you mean shouldn't the FSM be responsible for firing the callback? If so, then you could (not you should, I mean for discussion) add a new method on the FSM in the various states that allow you to pass in the req object so that when the game is ready (or in a playing-type state) then it sends the response back to the client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one reason I listen for the state change here and not pass in the req object, is because I refer to see all the request and response handling in this simple router.

* ready()), passing back various bits of data.
*
* If it's not yet ready, (ie, waiting), then we wait on the FSM changing
* state before closing the XHR.
*/
app.get('/status/:pin?', function (req, res, next) {
var pin = req.params.pin,
game = games.get(req.pin);
if (req.xhr) {
if (pin && game) {
var ready = function () {
var turn = game.isCurrentPlayer(req.session.playerId),
otherLetter = req.session.playerId === 'a' ? 'b' : 'a',
me = {
letter: req.session.playerId,
score: game.getPlayerByLetter(req.session.playerId).score
},
them = {
letter: otherLetter,
score: game.getPlayerByLetter(otherLetter).score
};

res.send({
type: 'ready',
data: {
towin: game.towin,
gameover: game.state === 'gameover',
me: me,
them: them,
currentPlayer: turn ? req.session.playerId : otherLetter,
turn: req.session.debug ? true : turn
}
});
};

if (game.state === 'ready' || game.state === 'playing' || game.state === 'gameover') {
ready();
} else if (game.state === 'waiting') {
console.log('waiting for game to start');

// FIXME this event isn't being picked up at all
game.on('state.ready', ready);
}
} else {
// start a game
pin = req.session.pin = games.getPin(req.session);

game = games.get(pin);
var gamedata = games.join(pin, req, { ua: req.headers['user-agent'] });
console.log(pin, game);
// console.log('xhr?', req.xhr);
// if (!req.xhr) return next();
if (pin && game) {
var ready = function () {
var turn = game.isCurrentPlayer(req.session.playerId),
otherLetter = req.session.playerId === 'a' ? 'b' : 'a',
me = {
letter: req.session.playerId,
score: game.getPlayerByLetter(req.session.playerId).score
},
them = {
letter: otherLetter,
score: game.getPlayerByLetter(otherLetter).score
};

req.session.playerId = gamedata.playerId;
req.session.ingame = pin;
res.send({
type: 'start',
type: 'ready',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I added in the type property trying to add some functionality in the last week of dev - but realised I didn't need it at all. I can't confirm this second, but have a feeling this whole line can be removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignore me, I checked start.js and it is used....

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blimey - right, I realise what's happened here, the diff has said it's a change from 'start' to 'ready' which now looking makes no sense, otherwise the game would have never started, but in fact github is diffing two completely different lines - hence my confusion!

data: {
pin: pin
towin: game.towin,
gameover: game.state === 'gameover',
me: me,
them: them,
currentPlayer: turn ? req.session.playerId : otherLetter,
turn: req.session.debug ? true : turn
}
});
};

if (game.state === 'ready' || game.state === 'playing' || game.state === 'gameover') {
ready();
} else if (game.state === 'waiting') {
console.log(pin, 'waiting for game to start');

game.on('state.ready', ready);
}
} else {
next();
// start a game
pin = req.session.pin = games.getPin(req.session);

game = games.get(pin);
var gamedata = games.join(pin, req, { ua: req.headers['user-agent'] });

req.session.playerId = gamedata.playerId;
req.session.ingame = pin;
res.send({
type: 'start',
data: {
pin: pin
}
});
}
});

Expand All @@ -193,12 +210,17 @@ module.exports = function (app) {
var result = false;
if (req.session.ingame) {
// player should leave this game first
var game = games.get(req.session.ingame);
game.leave(game.getOtherPlayer(req.session.playerId).id);
var game = games.get(req.session.ingame),
player = game.getOtherPlayer(req.session.playerId);
if (player) game.leave(player.id);
}
res.send(result);
});

/**
* Join a game. Hit when join form is submitted. This may create the game if
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Hit" not to be confused with "hit" game wording! :)

* it does not exist.
*/
app.post('/join', function (req, res) {
// TODO test if there's an active game under that pin,
// if so, allow join and remove from cache
Expand All @@ -222,27 +244,33 @@ module.exports = function (app) {
req.session.ingame = pin;

if (req.xhr) {
res.send(true);
} else {
res.redirect('/play');
}
} else if (game) {
console.log('failed joining - game may be full');
if (req.xhr) {
res.send(false);
} else {
res.render('join', {
'message': "That game is full!"
});
return res.send(true);
}
} else if (req.xhr) {
console.log('no game data');
return res.redirect('/play');
}

// I don't think this can happen, becuase in the case of a falsey game, a
// new one is created (above)
console.log('failed joining - game may be full');
if (req.xhr) {
res.send(false);
} else {
res.redirect('/join/' + pin);
res.render('join', {
'message': "That game is full!"
});
}
});

/**
* All roads lead to the player view.
*
* Headshot is almost a single page app, but with server state and
* code injection.
*
* All scripts are injected simultanously and manage their own initialisation.
* Mostly it's in start.js where getState conspires with init to pick what to
* show to the user.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...because WebRTC under deadlines and upgrading beta browsers is a shit to work with - and all roads (apparently, against my better judgement) lead to SPAs.

*/
app.get(['/play', '/join', '/start', '/join/:pin?', '/start/:pin?'], function (req, res, next) {
var debug = req.session.debug || false;

Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
},
"main": "server.js",
"scripts": {
"start": "server.js"
"start": "server.js",
"test": "tap ./test"
},
"repository": {
"type": "git",
Expand All @@ -37,6 +38,9 @@
"async": "~0.1.22",
"grunt-contrib-concat": "~0.1.2",
"grunt-contrib-jshint": "~0.1.1",
"grunt-contrib-uglify": "~0.1.1"
"grunt-contrib-uglify": "~0.1.1",
"tap": "~0.4.3",
"request": "~2.21.0",
"xmlhttprequest": "~1.5.0"
}
}
3 changes: 2 additions & 1 deletion public/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var websocket = {
event: 'receive_chat_msg'
};


var dataChannel = {
send: function(message) {
for (var connection in rtc.dataChannels) {
Expand Down Expand Up @@ -169,7 +170,7 @@ $.on('pinchange', function () {
rtc.connect((proto.indexOf('https') !== -1 ? 'wss:' : 'ws:') + href.substring(proto.length).split('#')[0], pin);
});

// should/could be done with bitwise state checking,
// should/could be done with bitwise state checking,
// but it's late :(
var state = {
readyremote: false,
Expand Down
71 changes: 45 additions & 26 deletions public/js/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,53 @@ turnEl.on('animationend', removeTurnClass);
var timer = null,
title = document.title;

/**
* Status opens an xhr to /status or /status/:pin.
*
* This will:
* - if there's no pin
* - create a game and re-request /status/:pin
* - otherwise
* - hang until another player joins and the xhr closes
* - then:
* - the URL is updated
* - user's score and turn bindings are set up
* - the game ('play' in game.js) is init'd
*/
function status(callback) {
callback = callback || function () {};
clearTimeout(timer);
xhr.get('/status/' + (pin ? pin : ''), function (err, result) {
if (err) {
console.log(this);
timer = setTimeout(status, 5000);
}
if (result) {
if (result.type === 'ready') {
window.history.replaceState({}, title, '/play');
window.game = new Bind(result.data, {
'me.score': '#myscore',
'them.score': function (value) {
$.trigger('theirScore', value);
},
'turn': function (myturn) {
turnEl.dataset.turn = myturn;
$.trigger('myturn', myturn);
}
});
towin = window.game.towin;
play.init();
if (!ready) {
waiting[0].hidden = false;
if (!result) return callback();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to refer exit routes at the end - only for reading from top to bottom (and/or typical code routes taking higher presidence).

if (result.type === 'ready') {
window.history.replaceState({}, title, '/play');
window.game = new Bind(result.data, {
'me.score': '#myscore',
'them.score': function (value) {
$.trigger('theirScore', value);
},
'turn': function (myturn) {
turnEl.dataset.turn = myturn;
$.trigger('myturn', myturn);
}

// this is done after play.init to ensure the event order is right
} else if (result.type === 'start') {
setPin(result.data.pin);
window.history.replaceState({}, title, '/start/' + pin);
status();
});
towin = window.game.towin;
play.init();
if (!ready) {
waiting[0].hidden = false;
}
}

if (callback) {
callback();
// this is done after play.init to ensure the event order is right
} else if (result.type === 'start') {
setPin(result.data.pin);
window.history.replaceState({}, title, '/start/' + pin);
status();
}
callback();
});
}

Expand Down Expand Up @@ -126,6 +136,12 @@ function tap(el, handler) {
el.on('click', handler, false);
}

/**
* Extract state and pin from the URL.
*
* `pin` is in the global scope, so this just modifies that, and returns the
* state, which is the first part of the URL.
*/
function getState() {
var l = window.location,
state = 'join',
Expand Down Expand Up @@ -157,6 +173,9 @@ function getState() {
return state;
}

/**
* Decide how load the app. Initialiased based on the state from getState.
*/
function init(state) {
console.log(state, pin);
if (state === 'start' || (state === 'join' && pin)) {
Expand Down
Loading