Last active
December 22, 2015 00:09
-
-
Save creatorrr/6386865 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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