Skip to content

Instantly share code, notes, and snippets.

@creatorrr
Last active December 22, 2015 00:09
Show Gist options
  • Select an option

  • Save creatorrr/6386865 to your computer and use it in GitHub Desktop.

Select an option

Save creatorrr/6386865 to your computer and use it in GitHub Desktop.
<?xml version="1.0" encoding="UTF-8"?>
<Module>
<!-- Set preferences for app -->
<ModulePrefs title="Keep Bluffin">
<Require feature="rpc" />
<Require feature="views" />
<Require feature="locked-domain" />
</ModulePrefs>
<!-- Begin app logic -->
<Content type="html">
<![CDATA[
<html>
<head>
<script src="//plus.google.com/hangouts/_/api/v1/hangout.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/thorax/2.0.0rc6/thorax.js"></script>
<!-- App logic -->
<script type="text/javascript">
// App module
(function($, _, Thorax, Backbone, Handlebars){
var app, forwardEvents, rewriteDataInterface, rewriteSync;
/*-- utils --*/
// Tidy up the gapi events interface
forwardEvents = function(self) {
if(!self) self = this;
var capitalize = function(str) {
return str[0].toUpperCase() + _.rest(str).join('');
};
return _.extend( self, {
on: function(name, fn) {
self['on'+capitalize(name)].add(fn);
},
off: function(name, fn) {
self['on'+capitalize(name)].remove(fn);
}
});
};
// Rewrite data interface
rewriteDataInterface = function(self) {
if(!self) self = this;
return _.extend( self, {
clear: function(key) {
if(key !== void 0) {
// Simply forward function call
return self.clearValue( key );
} else {
// Clear all
var keys = self.getKeys();
for(var i; i < keys.length; i++) self.clearValue( keys[i] );
}
},
get: function(key) {
if(key !== void 0) {
// Forward call
return self.getValue( key );
} else {
// Get all
return self.getState();
}
},
set: function(key, value) {
if(_.isObject(key)) {
for(var k in key) self.setValue( k, key[k] );
return true;
} else {
return self.getValue( key, value );
}
}
});
};
/*--/ utils --*/
/*-- namespace --*/
// Global app object
app = _.extend({
hangout: forwardEvents(gapi.hangout),
data: forwardEvents(rewriteDataInterface(gapi.hangout.data)),
models: {},
collections: {},
views: {},
state: {} // For storing shared app state
}, Backbone.Events);
// Rewrite Backbone Sync
(rewriteSync = function(Backbone, store) {
// Cache a reference to original sync function
Backbone.ajaxSync = Backbone.sync;
// Rewrite sync to use local state cache
Backbone.sync = function(method, model, options) {
var key, coll, isCollection, url, resp, data, errorMessage;
options || options = {};
url = options.url || _.result(model, 'url');
if(model instanceof Backbone.Model) {
key = _.first(url.split('/'));
} else {
// Get collection endpoint
key = url;
isCollection = true;
}
try {
switch (method) {
case 'read':
resp = state[key];
// Lookup model
if(!isCollection)
resp = _.findWhere(resp, {id: model.id});
break;
case 'create':
case 'update':
data = model.toJSON();
if(isCollection) {
// Add to shared state and then make a local copy
app.data.set(key, data);
state[key] = data;
} else {
coll = app.data.get(key) || [];
// Append data to collection and save it
coll.push(data);
app.data.set(key, coll);
app.state[key] = coll;
}
// Generate response
resp = coll;
break;
case 'delete':
app.data.clear(key);
delete app.state[key];
resp = true;
break;
}
} catch(error) {
// Twiddle thumbs for a bit
errorMessage = error.message || 'Storage error';
}
// Send response
if(resp && options.success)
_.defer(options.success, resp);
else if(errorMessage && options.error)
_.defer(options.error, errorMessage);
return resp || errorMessage;
};
})(Backbone, app.state);
/*--/ namespace --*/
/*-- models --*/
// Define a Card Model
app.models.Card = Thorax.Model.extend({});
// Define Deck
app.collections.Deck = Thorax.Collection.extend({
model: app.models.Card,
// Distribute deck among players
distribute: function(players) {
var shuffled, sets, slice;
shuffled = this.shuffle();
sets = [];
slice = shuffled.length / players;
for(var i = 0; i < shuffled.length; i += slice)
sets.push(shuffled.slice(i, i + slice));
return sets;
}
});
// Initialize a deck.
app.deck = new app.collections.Deck();
// Add cards to deck
app.deck.add((function(){
var i, j, suits, ranks, cards;
suits = ['hearts', 'spades', 'diamonds', 'clubs'];
ranks = ['A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K'];
cards = _.flatten(_.map(ranks, function(rank){
return _.map(suits, function(suit){
return {rank: rank, suit: suit};
});
}));
return cards;
})());
// Define player model
app.models.Player = Thorax.Model.extend({
defaults: function(){
return {
addedAt: new Date().getTime()
};
},
isMaster: function() {
this.id = app.state['master'];
return this.id;
}
});
// Define players collection
app.collections.Players = Thorax.Collection.extend({
model: app.models.Player,
comparator: function(player) { return player.get('addedAt'); },
getMaster: function() {
return this.find(function(model) { return model.isMaster(); });
}
});
// Define notification model
app.models.Notification = Thorax.Model.extend({});
// Define layout view
app.views.layout = Thorax.LayoutView;
// Define card view
app.views.card = Thorax.View.extend({
events: {
click: function() {
// Return if deck covered
if(this.parent && this.parent.covered) return;
// Select cards
this.$el.find('.card').toggleClass('selected');
this.model.set('selected', !this.model.get('selected'));
}
},
template: Handlebars.compile("<div class=\"card suit{{ suit }}\"><p>{{ rank }}</p>")
});
// Define deck view
app.views.deck = Thorax.View.extend({
initialize: function() {
// List of card views grouped by rank
this.hands = {};
},
events: {
collection: {
all: function(e) {
var collection;
// Get collection value
switch(e) {
case 'add':
case 'remove':
collection = arguments[2]; // (event, model, collection)
break;
case 'reset':
collection = arguments[1]; // (event, collection)
break;
default: return;
}
this.setHands(collection);
this.render();
}
}
},
setHands: function(coll) {
var groupedHands = coll.groupBy('rank');
// Reset hands
_.each(_.flatten(_.pairs(this.hands)), function(view) { view.remove(); });
this.hands = {};
for(var rank in groupedHands) {
var group = groupedHands[rank];
for(var i = 0; i < group.length; i++)
group[i] = new app.views.card({ model: group[i] });
this.hands[rank] = group;
}
},
template: Handlebars.compile(
"<div class=\"hand-container\">"+
"{{#each hands}}"+
"<div class=\"hand spread {{#if covered}}covered{{/if}}\">"+
"{{#each this}}"+
"{{view this}}"+
"{{/each}}"+
"</div>"+
"{{/each}}"+
"</div>"
)
});
// Define view for "covered" deck, for the round.
app.views.coveredDeck = app.views.deck.extend({
covered: true,
showLastHand: function() {
this.$el.find('.covered:last').removeClass('covered');
}
});
// Define notification view which disappears after some time
app.views.notification = Thorax.View.extend({
_defaultExpiration: 8 * 1000, // 8 seconds
events: {
model: {
change: function() {
// Hide after expiration time.
_.delay(this.hide, this.model.expiration || this._defaultExpiration);
// Render notification and display it.
this.render();
this.show();
},
},
},
hide: function() { this.$el.hide(); },
show: function() { this.$el.show(); },
template: Handlebars.compile(
'<div class="notification">'+
'<span>{{ message }}</span>'+
'</div>'
),
});
// Define starting screen
app.views.gameStart = Thorax.View.extend({
initialize: function() {
// Add isMaster predicate
this.isMaster = this.model instanceof this.models.Player && this.model.isMaster();
},
template: Handlebars.compile(
"<div class=\"welcome\">"+
"</div>"
),
});
// Define ending screen view
app.views.gameEnd = Thorax.View.extend({
events: {
'click a[data-action="restartGame"]': function() {
app.trigger('game:start');
},
},
template: Handlebars.compile(
"<div class=\"results\">"+
"<h2>{{ person.displayName }} has won!</h2>"+
"<a data-action=\"restartGame\">Play another game</a>"+
"</div>"
),
});
// Init
$(function(){
var setMaster, setTurn, setPlayers, nextTurn;
// Function to set master (first player to join)
setMaster = function() {
var master;
if(!app.data.get('master')) {
master = app.hangout.getLocalParticipantId();
app.data.set({ master: master });
app.state['master'] = master;
}
return app.state['master'];
};
// Set turn
setTurn = function(player) {
var turn = { currentTurn: player.id };
app.data.set(turn);
app.trigger('send:message', player.id, { turn: true });
return turn;
};
// Set up players
setPlayers = function() {
var playerData, hands, length, i;
// Distribute deck
hands = app.deck.distribute(length = app.players.length);
// Go over each player and send them their hands
i = 0;
playerData = [];
app.players.each(function(player) {
// Send hand
app.trigger('send:message', player.id, { hand: hands[i] });
// Get data
playerData.push({
id: player.id,
cards: hands[i].length
});
// Check for turn
if(!_.isEmpty(_.findWhere(hands[i], { rank: 'A', suit: 'spades' })))
setTurn(player);
// Incr
i++;
});
// Persist player data
app.data.set('players', playerData);
app.state['players'] = playerData;
return playerData;
};
// Advance turn
nextTurn = function(last) {
var players = app.state['players'];
for(var i = 0; i < players.length; i++) {
if(players[i].id == last.id) break;
}
return setTurn(players[i]);
};
// Initialize local player
app.me = new app.models.Player();
app.players = new app.collections.Players();
// Set up app
app.hangout.on('apiReady', function(){
// Send and receive messages
app.data.on('messageReceived', function(message) {
var to;
// Parse message
message = JSON.parse(message) || message;
// If not addressed to any id, then broadcast.
if(!(to = _.result(message, 'to'))) {
app.trigger('broadcast:received', message);
} else if(to == app.me.id) {
app.trigger('message:received', message);
}
});
app.on('message:send', function(id, message) {
// Add metadata
if(_.isObject(message)) {
message.to = id;
message.from = app.me.id;
}
app.data.sendMessage(JSON.stringify(message));
});
app.on('broadcast:send', function(message) {
app.data.sendMessage(JSON.stringify(message));
});
app.on('notification:send', function(message, expiration) {
var msg = {
notification: {
message: message
}
};
if(expiration) msg.notification.expiration = expiration;
// Send it off
app.trigger('broadcast:send', msg);
});
// Dispatch messages
app.on('message:received', function(message) {
if(message.turn) {
app.trigger('turn:play');
} else if(message.hand) {
app.trigger('hand:init', message.hand);
} else if(message.notification) {
app.trigger('notification:received', message.notification);
}
});
// Auto update internal state
app.data.on('stateChanged', function(e) {
app.state = _.clone(e.state);
app.trigger('state:change', app.state);
});
app.on('state:refresh', function() {
app.state = _.clone(app.data.getState());
});
// Add local players
app.me.set(app.hangout.getLocalParticipant());
app.players.reset(app.hangout.getEnabledParticipants());
app.hangout.on('participantsEnabled', function(participants) {
app.players.reset(participants);
});
// Set master
setMaster();
app.hangout.on('participantsChanged', _.once(setMaster));
app.hangout.on('appVisible', function() {
var notificationView;
// Initialize notification container
notificationView = new app.views.notification({
model: new app.models.Notification()
});
notificationView.appendTo('body');
// Listen for new notifications
app.on('notification:received', function(notification) {
notificationView.model.set(notification);
});
// Do stuff here
// Testing
$('body').css('background-color', 'red');
});
});
});
}).call(this, jQuery, _, Thorax, Backbone, Handlebars);
</script>
<!-- Google fonts: 'Megrim', 'Bangers', 'Cabin' -->
<link href='http://fonts.googleapis.com/css?family=Bangers|Megrim|Cabin' rel='stylesheet' type='text/css'/>
<style type="text/css">
* {margin: 0; padding: 0;}
body {
font-family: 'Cabin', sans-serif;
background: #00a651;
}
/* Begin: Card styles */
/* (Copied shamelessly from http://designshack.net/articles/css/css-card-tricks/) */
.hand:before,
.hand:after {
content:"";
display:table;
}
.hand:after {
clear:both;
}
.card {
position: relative;
float: left;
margin-right: 10px;
width: 150px;
height: 220px;
border-radius: 10px;
background: white;
-webkit-box-shadow: 3px 3px 7px rgba(0,0,0,0.3);
box-shadow: 3px 3px 7px rgba(0,0,0,0.3);
}
.card p {
font-family: 'Megrim' cursive;
text-align: center;
text-transform: capitalize;
color: black;
font: 100px/220px Georgia, serif;
}
.suitdiamonds p,
.suithearts p {
color: #ff0000;
}
.suitdiamonds:before, .suitdiamonds:after {
content: "♦";
color: #ff0000;
}
.suithearts:before, .suithearts:after {
content: "♥";
color: #ff0000;
}
.suitclubs:before, .suitclubs:after {
content: "♣";
color: #000;
}
.suitspades:before, .suitspades:after {
content: "♠";
color: #000;
}
div[class*='suit']:before {
position: absolute;
font-size: 35px;
left: 5px;
top: 5px;
}
div[class*='suit']:after {
position: absolute;
font-size: 35px;
right: 5px;
bottom: 5px;
}
.card:hover {
cursor: pointer;
-webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.4);
box-shadow: 1px 1px 7px rgba(0,0,0,0.4);
}
.spread-container {
overflow-x: auto;
overflow-y: hidden;
width: auto;
white-space: nowrap; /* Expand horizontally instead of wrapping around */
padding-right: 135px; /* Dirty hack, look away :( */
}
/* SPREAD */
.spread {
width: 20px;
height: 148px;
position: relative;
}
.spread > .card {
position: absolute;
top: 0;
left: 0;
-webkit-transition: left 0.3s ease;
-moz-transition: top 0.3s ease, left 0.3s ease;
-o-transition: top 0.3s ease, left 0.3s ease;
-ms-transition: top 0.3s ease, left 0.3s ease;
transition: top 0.3s ease, left 0.3s ease;
}
.spread:hover .suitdiamonds {
left: 0px;
}
.spread:hover .suithearts {
left: 30px;
}
.spread:hover .suitclubs {
left: 60px;
}
.spread:hover .suitspades{
left: 90px;
}
.spread > .card:hover {
-webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.4);
box-shadow: 1px 1px 7px rgba(0,0,0,0.4);
}
/*SELECTED*/
.selected.card {
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
-ms-transition: all 0.2s ease;
transition: all 0.2s ease;
-webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.9);
box-shadow: 1px 1px 7px rgba(0,0,0,0.9);
-webkit-transform: translateY(-30px);
-moz-transform: translateY(-30px);
-o-transform: translateY(-30px);
-ms-transform: translateY(-30px);
transform: translateY(-30px);
}
/*COVERED*/
/* Background style from http://lea.verou.me/css3patterns/#madras */
.covered > .card {
background-color: hsl(34, 53%, 82%);
background-image: repeating-linear-gradient(45deg, transparent 5px, hsla(197, 62%, 11%, 0.5) 5px, hsla(197, 62%, 11%, 0.5) 10px,
hsla(5, 53%, 63%, 0) 10px, hsla(5, 53%, 63%, 0) 35px, hsla(5, 53%, 63%, 0.5) 35px, hsla(5, 53%, 63%, 0.5) 40px,
hsla(197, 62%, 11%, 0.5) 40px, hsla(197, 62%, 11%, 0.5) 50px, hsla(197, 62%, 11%, 0) 50px, hsla(197, 62%, 11%, 0) 60px,
hsla(5, 53%, 63%, 0.5) 60px, hsla(5, 53%, 63%, 0.5) 70px, hsla(35, 91%, 65%, 0.5) 70px, hsla(35, 91%, 65%, 0.5) 80px,
hsla(35, 91%, 65%, 0) 80px, hsla(35, 91%, 65%, 0) 90px, hsla(5, 53%, 63%, 0.5) 90px, hsla(5, 53%, 63%, 0.5) 110px,
hsla(5, 53%, 63%, 0) 110px, hsla(5, 53%, 63%, 0) 120px, hsla(197, 62%, 11%, 0.5) 120px, hsla(197, 62%, 11%, 0.5) 140px
),
repeating-linear-gradient(135deg, transparent 5px, hsla(197, 62%, 11%, 0.5) 5px, hsla(197, 62%, 11%, 0.5) 10px,
hsla(5, 53%, 63%, 0) 10px, hsla(5, 53%, 63%, 0) 35px, hsla(5, 53%, 63%, 0.5) 35px, hsla(5, 53%, 63%, 0.5) 40px,
hsla(197, 62%, 11%, 0.5) 40px, hsla(197, 62%, 11%, 0.5) 50px, hsla(197, 62%, 11%, 0) 50px, hsla(197, 62%, 11%, 0) 60px,
hsla(5, 53%, 63%, 0.5) 60px, hsla(5, 53%, 63%, 0.5) 70px, hsla(35, 91%, 65%, 0.5) 70px, hsla(35, 91%, 65%, 0.5) 80px,
hsla(35, 91%, 65%, 0) 80px, hsla(35, 91%, 65%, 0) 90px, hsla(5, 53%, 63%, 0.5) 90px, hsla(5, 53%, 63%, 0.5) 110px,
hsla(5, 53%, 63%, 0) 110px, hsla(5, 53%, 63%, 0) 140px, hsla(197, 62%, 11%, 0.5) 140px, hsla(197, 62%, 11%, 0.5) 160px
);
}
.covered > .card p,
.covered > .card:before, .covered > .card:after {
display: none;
}
/* End: Card styles */
/* Begin: Spinner */
.spinner {
height: 60px;
width: 60px;
margin: 0 auto;
position: relative;
-webkit-animation: rotation .6s infinite linear;
-moz-animation: rotation .6s infinite linear;
-o-animation: rotation .6s infinite linear;
animation: rotation .6s infinite linear;
border: 6px solid rgba(0,239,174,.15); /* Light green */
border-radius: 100%;
}
.spinner:before {
content: "";
display: block;
position: absolute;
left: -6px;
top: -6px;
height: 100%;
width: 100%;
border-top: 6px solid rgba(0,239,174,.8); /* Green */
border-left: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid transparent;
border-radius: 100%;
}
@-webkit-keyframes rotation {
from {-webkit-transform: rotate(0deg);}
to {-webkit-transform: rotate(359deg);}
}
@-moz-keyframes rotation {
from {-moz-transform: rotate(0deg);}
to {-moz-transform: rotate(359deg);}
}
@-o-keyframes rotation {
from {-o-transform: rotate(0deg);}
to {-o-transform: rotate(359deg);}
}
@keyframes rotation {
from {transform: rotate(0deg);}
to {transform: rotate(359deg);}
}
/* End: Spinner */
</style>
</head>
<body>
<div id="container">
</div>
</body>
</html>
]]>
</Content>
</Module>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment