├── app ├── .DS_Store ├── scripts │ ├── .DS_Store │ ├── models │ │ ├── .DS_Store │ │ ├── robotModel.js │ │ ├── playerModel.js │ │ ├── appModel.js │ │ ├── quadrantHolderModel.js │ │ ├── scoreModel.js │ │ └── boardModel.js │ ├── collections │ │ ├── .DS_Store │ │ ├── players.js │ │ └── robots.js │ ├── views │ │ ├── instructionsView.js │ │ ├── scoreView.js │ │ ├── playerView.js │ │ ├── boardView.js │ │ ├── appView.js │ │ ├── playersView.js │ │ ├── robotView.js │ │ └── canvasDrawView.js │ └── dialogs │ │ └── introDialogs.js ├── styles │ ├── robotStyles.css │ └── buttonAndPlayerStyles.css ├── package.json └── init.js ├── server.js ├── .gitignore ├── package.json ├── README.md ├── CONTRIBUTING.md ├── index.html └── STYLE-GUIDE.md /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdtsui/ricochet-backbone/HEAD/app/.DS_Store -------------------------------------------------------------------------------- /app/scripts/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdtsui/ricochet-backbone/HEAD/app/scripts/.DS_Store -------------------------------------------------------------------------------- /app/scripts/models/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdtsui/ricochet-backbone/HEAD/app/scripts/models/.DS_Store -------------------------------------------------------------------------------- /app/scripts/collections/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdtsui/ricochet-backbone/HEAD/app/scripts/collections/.DS_Store -------------------------------------------------------------------------------- /app/scripts/views/instructionsView.js: -------------------------------------------------------------------------------- 1 | window.instructionsView = Backbone.View.extend({ 2 | template: _.template($('#instructionsTemplate').html()), 3 | initialize: function(){}, 4 | render: function(){ 5 | this.$el.html(this.template()); 6 | return this.$el; 7 | } 8 | }); -------------------------------------------------------------------------------- /app/scripts/collections/players.js: -------------------------------------------------------------------------------- 1 | window.players = Backbone.Collection.extend({ 2 | model: playerModel, 3 | initialize: function(){ 4 | Backbone.Events.on('roundStart', function(){ 5 | // reset all bids 6 | this.forEach(function(value, key){ 7 | value.resetBids(); 8 | }) 9 | }, this) 10 | } 11 | }); -------------------------------------------------------------------------------- /app/scripts/views/scoreView.js: -------------------------------------------------------------------------------- 1 | window.scoreView = Backbone.View.extend({ 2 | template: _.template($("#scoreViewTemplate").html()), 3 | initialize: function(){ 4 | this.model.on('change:timerValue change:activeMoves change:tokensRemaining', this.render, this) 5 | }, 6 | render: function(){ 7 | this.$el.html(this.template(this.model.attributes)); 8 | return this.$el; 9 | } 10 | }); -------------------------------------------------------------------------------- /app/scripts/views/playerView.js: -------------------------------------------------------------------------------- 1 | window.playerView = Backbone.View.extend({ 2 | template: _.template($('#playerViewTemplate').html()), 3 | initialize: function(){ 4 | this.model.on('change:currentBid change:tokensWon', this.render, this); 5 | this.render(); 6 | }, 7 | render: function(){ 8 | this.$el.html(this.template(this.model.attributes)); 9 | return this.$el; 10 | } 11 | }); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var bodyParser = require('body-parser'); 3 | var cors = require('cors'); 4 | 5 | var app = express(); 6 | 7 | app.use(bodyParser.urlencoded({extended: true})); 8 | app.use(bodyParser.json()); 9 | app.use(cors()); 10 | 11 | app.use('/',express.static(__dirname)); 12 | 13 | var port = process.env.PORT || 8080; 14 | console.log("Listening on Port : " ,port); 15 | app.listen(port); -------------------------------------------------------------------------------- /app/styles/robotStyles.css: -------------------------------------------------------------------------------- 1 | .Y { 2 | position:absolute; 3 | top: 45vh; 4 | left: 55vw; 5 | } 6 | .R { 7 | position:absolute; 8 | top: 45vh; 9 | left: 55vw; 10 | 11 | } 12 | .G{ 13 | position: absolute; 14 | top: 45vh; 15 | left: 55vw; 16 | 17 | } 18 | .B{ 19 | position: absolute; 20 | top: 45vh; 21 | left: 55vw; 22 | 23 | } 24 | 25 | #appView{ 26 | position: relative; 27 | float: left; 28 | } -------------------------------------------------------------------------------- /app/scripts/views/boardView.js: -------------------------------------------------------------------------------- 1 | window.boardView = Backbone.View.extend({ 2 | template: _.template($('#boardViewTemplate').html()), 3 | //Boardview keeps track of the active robot, for scorekeeping and animation. 4 | initialize: function(){ 5 | this.model.on('change:activeRobot',this.newActiveRobot, this); 6 | }, 7 | render: function() { 8 | this.$el = this.template(this.model.attributes); 9 | return this.$el; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RicochetBackbone", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "author": "sdtsui", 11 | "license": "MIT", 12 | "dependencies": { 13 | "body-parser": "^1.12.0", 14 | "cors": "^2.5.3", 15 | "express": "^4.11.2", 16 | "nodemon": "^1.3.7" 17 | } 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Mac DS_Store files 2 | .DS_Store 3 | 4 | 5 | # Logs 6 | logs 7 | *.log 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # Commenting this out is preferred by some people, see 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 29 | node_modules 30 | 31 | # Users Environment Variables 32 | .lock-wscript 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ricochet-backbone", 3 | "version": "0.0.1", 4 | "description": "Ricochet Robots, built in Backbone.js", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/sdtsui/ricochet-backbone" 13 | }, 14 | "author": "Sze-Hung Daniel Tsui", 15 | "license": "ISC", 16 | "engines": { 17 | "node": "*" 18 | }, 19 | "homepage": "https://github.com/sdtsui/ricochet-backbone", 20 | "dependencies": { 21 | "backbone": "^1.1.2", 22 | "d3": "^3.5.5", 23 | "jquery": "^2.1.3", 24 | "twitter-bootstrap": "^2.1.1", 25 | "underscore": "^1.8.2", 26 | "vex-js": "^2.3.2", 27 | "body-parser": "^1.12.0", 28 | "cors": "^2.5.3", 29 | "express": "^4.11.2", 30 | "nodemon": "^1.3.7" 31 | }, 32 | "devDependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /app/scripts/models/robotModel.js: -------------------------------------------------------------------------------- 1 | window.robotModel = Backbone.Model.extend({ 2 | defaults: { 3 | color: undefined, 4 | loc: { 5 | row: undefined, 6 | col: undefined 7 | }, 8 | lastLoc: { 9 | row: undefined, 10 | col: undefined 11 | }, 12 | roundLoc: { 13 | row: undefined, 14 | col: undefined 15 | }, 16 | boardModel: undefined, 17 | boxSize: undefined, 18 | lastMoveDir: undefined //See issue #6. 19 | }, 20 | initialize: function(){ 21 | Backbone.Events.on('boardAssetsRendered', this.triggerMove, this); 22 | this.on('change:loc', function(){ 23 | Backbone.Events.trigger('robotLocChange'); 24 | }); 25 | }, 26 | setPosition: function(square){ 27 | //used to set a valid square 28 | this.set('loc', square); 29 | }, 30 | savePosition: function(){ 31 | var lastLoc = this.get('loc'); 32 | this.set('lastLoc', lastLoc); 33 | }, 34 | saveStartingPosition: function(){ 35 | var lastLoc = _.clone(this.get('loc')); 36 | this.set('roundLoc', lastLoc); 37 | }, 38 | resetPosition: function(){ 39 | var lastLoc = _.clone(this.get('roundLoc')); 40 | this.set('loc', lastLoc); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /app/scripts/collections/robots.js: -------------------------------------------------------------------------------- 1 | window.robots = Backbone.Collection.extend({ 2 | model: robotModel, 3 | initialize: function(){ 4 | Backbone.Events.on('robotLocChange', function(){ 5 | //This is an incomplete change. See issue #6. 6 | this.forEach(function(value, key, list){ 7 | value.set('lastMoveDir', undefined); 8 | }); 9 | }, this); 10 | Backbone.Events.on('resetPosition', function(){ 11 | //loop through array, move the robots back to their roundStart positions 12 | this.forEach(function(value, key){ 13 | value.resetPosition(); 14 | }) 15 | }, this); 16 | Backbone.Events.on('roundStart', function(){ 17 | //loop through the array, 18 | // save the current location of robots 19 | this.forEach(function(value, key){ 20 | value.saveStartingPosition(); 21 | }) 22 | }, this); 23 | }, 24 | /** 25 | * [Returns a boolean, true if the input square matches the location of a robot in collection] 26 | * @param {[obj]} square [has row and col properties] 27 | * @return {[type]} [description] 28 | */ 29 | squareHasConflict : function(square){ 30 | var foundConflict = false; 31 | this.forEach(function(val){ 32 | var loc = val.get('loc'); 33 | if (loc.row === square.row && loc.col === square.col){ 34 | foundConflict = true; 35 | } 36 | }); 37 | return foundConflict; 38 | } 39 | }); -------------------------------------------------------------------------------- /app/scripts/views/appView.js: -------------------------------------------------------------------------------- 1 | window.appView = Backbone.View.extend({ 2 | //Attaches main view to the assigned $el. 3 | render: function(num) { 4 | var board = this.model.get('boardModel'); 5 | $(this.$el).children().detach(); 6 | this.boardView = new boardView({model: board}); 7 | this.$el.append(this.boardView.render()); 8 | this.appendRobots(); 9 | Backbone.Events.trigger('boardAssetsRendered'); 10 | this.playersView = new playersView({ 11 | model :this.model.get('players'), 12 | el: $('#playersView') 13 | }); 14 | this.scoreView = new scoreView({ 15 | el: $('#scoreView'), 16 | model: this.model.get('scoreModel')}) 17 | this.scoreView.render(); 18 | }, 19 | //Adds a new html canvas on top of 20 | //Decision to go with a new HTML canvas influenced by original scope: only needed simple animations. 21 | //Made the most sense at the time spin the canvas elements using jQuery instead of creating 22 | //an animation function on the canvas. 23 | appendRobots: function(){ 24 | var robots = this.model.get('boardModel').get('robots'); 25 | for(var i = 0 ; i < robots.length; i++){ 26 | var newView = new robotView({model: robots.at(i)}); 27 | var newElement = newView.render(); 28 | $(this.$el[0]).append($(newElement)[0]); 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /app/scripts/models/playerModel.js: -------------------------------------------------------------------------------- 1 | window.playerModel = Backbone.Model.extend({ 2 | defaults: { 3 | newestBid: undefined, 4 | currentBid: undefined, 5 | tokensWon: [], //will be an array of strings, symbolizing the tokens won 6 | username: undefined, 7 | }, 8 | initialize: function(){ 9 | this.resetBids(); 10 | this.set('cid', this.cid); 11 | this.on('newBidEvent', this.handleNewBid, this); 12 | }, 13 | handleNewBid : function(bidData){ 14 | var oldBid = this.get('currentBid'); 15 | var newBid = bidData[0]; 16 | var bidNumber = rootModel.get('scoreModel').get('bidCounter'); 17 | if (oldBid.bidNumber === undefined){ 18 | this.set('currentBid', { 19 | moves: newBid, 20 | bidNumber: bidNumber 21 | }); 22 | } else if (newBid < oldBid.moves) { 23 | this.set('currentBid', { 24 | moves: newBid, 25 | bidNumber: bidNumber 26 | }); 27 | } 28 | this.set('newestBid', undefined);; 29 | Backbone.Events.trigger('newBid'); 30 | }, 31 | resetBids: function(){ 32 | this.set('currentBid', { 33 | moves: '-', 34 | bidNumber: undefined 35 | }); 36 | }, 37 | addPoint: function(newToken){ 38 | var oldTokens = _.clone(this.get('tokensWon')); 39 | oldTokens.push(newToken) 40 | this.set('tokensWon', oldTokens); 41 | }, 42 | removePoint: function(){ 43 | var tokens = _.clone(this.get('tokensWon')); 44 | tokens.sort(function(){return Math.random()-0.5;}); 45 | lostToken = tokens.shift(); 46 | 47 | this.set('tokensWon', tokens); 48 | return lostToken; 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ricochet-backbone 2 | 3 | Ricochet Robot is a board game invented by [Alex Randolph](http://en.wikipedia.org/wiki/Alex_Randolph), with art by [Franz Vohwinkel](http://en.wikipedia.org/wiki/Franz_Vohwinkel). It's åa fast paced multiplayer puzzle game. Vie for points by solving a complex puzzle faster than your friends. Check out the [blog post](http://sdtsui.com/2015/04/06/ricochet-robots-ii/) on this project for more information about the game. 4 | Built with Backbone.js, jQuery, and HTML5 Canvas. Intended for local play. 5 | 6 | ## Play the Webapp! 7 | (click the image below) 8 | [![screenshot](http://i.imgur.com/oR7ESXJ.gif)](https://ricochet.herokuapp.com) 9 | 10 | ##Getting Started 11 | 12 | - Find 1-4 other friends. 13 | - Load up the (web app)[https://ricochet.herokuapp.com]. 14 | - Set up your game by entering the number of players, and their names. 15 | - Get the right robot to the target square faster than your friends. 16 | - [Nerd Out after!](http://www-pr.informatik.uni-tuebingen.de/mitarbeiter/katharinazweig/downloads/ButkoLehmannRamenzoni.pdf) 17 | 18 | ##Instructions/Setup (Demo): 19 | [![Set-up Demo](http://i.imgur.com/tHKD4wm.gif)](https://ricochet.herokuapp.com) 20 | 21 | ## Contributing 22 | Accepting contributions! 23 | 24 | ### Next Planned Feature: 25 | Multiplayer with Socket.io. Interested in collaborating? 26 | Reach out if you have other ideas, either through the issues page, or by finding my contact information at [sdtsui.com](http://sdtsui.com). 27 | 28 | [Check out the style guide, contributing.md, and issues page for ideas.](https://github.com/sdtsui/ricochet-backbone/issues) 29 | 30 | ###To run locally: 31 | 32 | * Clone the repo 33 | * run `npm install` (requires backbone, vex, jquery, and some server dependencies) 34 | * run `nodemon server.js` 35 | * Navigate to localhost:8080 36 | -------------------------------------------------------------------------------- /app/scripts/views/playersView.js: -------------------------------------------------------------------------------- 1 | window.playersView = Backbone.View.extend({ 2 | el: $('playersView'), 3 | events: { 4 | 'click .bid-btn' : 'clickBidBtn', 5 | 'keypress :input': 'keyDown' 6 | }, 7 | /** 8 | * Following three functions associate the right user inputs with the right players' elements, by id, using a given element's siblings. 9 | */ 10 | clickBidBtn: function(e){ 11 | e.preventDefault(); 12 | var btn = $(e.currentTarget); 13 | var id = btn.data('id'); 14 | var bid = btn.siblings()[0].valueAsNumber; 15 | this.triggerBid(bid, id); 16 | }, 17 | keyDown: function(e){ 18 | if (e.keyCode === 13){ 19 | var input = $(e.currentTarget); 20 | var id = input.siblings().data('id'); 21 | var bid = input[0].valueAsNumber; 22 | this.triggerBid(bid, id); 23 | } 24 | }, 25 | triggerBid : function(bid, id){ 26 | if ((bid-bid === 0) && (bid <= 99) ){ 27 | Backbone.Events.trigger('newBidEvent', [bid]); 28 | var player = this.model.get(id); 29 | player.trigger('newBidEvent', [bid]); 30 | } 31 | }, 32 | playersViews : [], 33 | templStr : ['
'], 34 | scoreV: ['
'], 35 | scoreViews : [], 36 | initialize: function(){ 37 | this.render(); 38 | _.each(this.model.models, function(playerModel, idx){ 39 | var newView = new playerView({ 40 | model: playerModel, 41 | el: $('#player-'+playerModel.get('username')) 42 | }); 43 | this.playersViews.push(newView); 44 | }.bind(this)); 45 | }, 46 | render: function(){ 47 | this.$el.children().detach(); 48 | 49 | //make all the new views.. 50 | _.each(this.model.models, function(playerModel, idx){ 51 | var userName = playerModel.get('username'); 52 | var htmlString = this.templStr[0] + userName + this.templStr[2]; 53 | this.$el.append(htmlString); 54 | }.bind(this)); 55 | this.$el.append(this.scoreV); 56 | } 57 | }) -------------------------------------------------------------------------------- /app/init.js: -------------------------------------------------------------------------------- 1 | var findBoardDimensions = function(){ 2 | var windowWidth = $(window).width(); 3 | var windowHeight = $(window).height(); 4 | var boardWidth = 5 | (windowWidth <= windowHeight) ? windowWidth : windowHeight; 6 | boardWidth *= 0.95; 7 | return { 8 | windowWidth : windowWidth, 9 | windowHeight : windowHeight, 10 | boardWidth : boardWidth 11 | } 12 | } 13 | 14 | //instantiates the root models and associated views, starts initial user dialog. 15 | //adds jQuery input listeners 16 | $(document).on('ready', function(){ 17 | var boardDimensions = findBoardDimensions(); 18 | //Instantiate the main appModel instance. 19 | window.rootModel = new appModel({ 20 | windowWidth : boardDimensions.windowWidth, 21 | windowHeight : boardDimensions.windowHeight, 22 | boardWidth : boardDimensions.boardWidth 23 | }); 24 | var APPDIV = new appView({ 25 | model: rootModel, 26 | el: $('#appView') 27 | }); 28 | //**This can be abstracted back into the backbone model...** 29 | //Draw on canvas. 30 | window.boardDetails = new canvasDrawView({ 31 | model: rootModel 32 | }); 33 | 34 | // Last param of this function allows for fast rendering of 4 players 35 | startDialog([APPDIV, boardDetails], rootModel, false); 36 | 37 | }); 38 | 39 | //condense this into a clickhandler function to put into boardView eventually, 40 | //makes more sense there than in a random script 41 | $(document).on('ready', function(){ 42 | $(document.body).on('keydown', function(e){ 43 | //only act if there is an active player to win the point... 44 | if (rootModel.get('scoreModel').get('activePlayer') !== undefined){ 45 | var keyCode = e.keyCode; 46 | if ((e.keyCode === 38) ||(e.keyCode === 87)){ 47 | Backbone.Events.trigger('keyN'); 48 | } else if ((e.keyCode === 40) ||(e.keyCode === 83)) { 49 | Backbone.Events.trigger('keyS'); 50 | } else if ((e.keyCode === 39) ||(e.keyCode === 68)) { 51 | Backbone.Events.trigger('keyE'); 52 | } else if ((e.keyCode === 37) ||(e.keyCode === 65)) { 53 | Backbone.Events.trigger('keyW'); 54 | } 55 | } 56 | }); 57 | }) -------------------------------------------------------------------------------- /app/styles/buttonAndPlayerStyles.css: -------------------------------------------------------------------------------- 1 | .body{ 2 | background-color: #D4D7C7 !important; 3 | } 4 | 5 | .content_container{ 6 | border-top-left-radius: 3px; 7 | border-top-right-radius: 3px; 8 | } 9 | 10 | .card_header{ 11 | border-top-left-radius: 3px; 12 | border-top-right-radius: 3px; 13 | padding: 1px 1px; 14 | border-bottom: 1px solid transparent; 15 | } 16 | .card_bid_header{ 17 | margin-left: 20%; 18 | } 19 | 20 | .rotated { 21 | transition : 2.5s linear; 22 | -webkit-transform : rotate(2880deg); 23 | } 24 | 25 | #playersView{ 26 | position:relative; 27 | } 28 | 29 | .playerBox1{ 30 | background-color: #ABADA0; 31 | position: relative; 32 | border-top: 1px solid #ccd6dd; 33 | border-bottom: 1px solid #ccd6dd; 34 | border-left: 1px solid #e1e8ed; 35 | border-right: 1px solid #e1e8ed; 36 | width: 205; 37 | height: 140; 38 | border-width: 5px; 39 | } 40 | 41 | .scoreBox1{ 42 | background-color: #ABADA0; 43 | position: relative; 44 | border-top: 1px solid #ccd6dd; 45 | border-bottom: 1px solid #ccd6dd; 46 | border-left: 1px solid #e1e8ed; 47 | border-right: 1px solid #e1e8ed; 48 | width: 205; 49 | height: 155; 50 | border-width: 5px; 51 | 52 | } 53 | 54 | /*.leftText{ 55 | float:left; 56 | } 57 | .rightText{ 58 | float:left; 59 | text-align: right; 60 | right: 0px; 61 | 62 | }*/ 63 | 64 | .form{ 65 | display: block; 66 | margin-top: 0em; 67 | } 68 | 69 | .bidfield{ 70 | 71 | } 72 | .bid-btn{ 73 | width: 55px; 74 | height: 35px; 75 | } 76 | 77 | .game-btn{ 78 | width: 70px; 79 | height: 72px; 80 | } 81 | 82 | input{ 83 | box-shadow: inset 0 1px 0 #eee,#fff 0 1px 0; 84 | font-size: 6px; 85 | min-width: 0; 86 | width: auto; 87 | display: inline; 88 | min-height: 25; 89 | } 90 | 91 | .playerHolder{ 92 | position: absolute; 93 | } 94 | .scoreHolder{ 95 | position:block; 96 | float:left; 97 | } 98 | 99 | .p-sViewHolder{ 100 | float:left; 101 | } 102 | .round-timer{ 103 | position: relative; 104 | float:left; 105 | font-size: 85px; 106 | font-family: Garamond; 107 | text-align: center; 108 | top: 40%; 109 | display: block; 110 | height: 105; 111 | width: 125; 112 | } 113 | .pointDisplay{ 114 | font-size: 38; 115 | text-align: center; 116 | } 117 | 118 | .nameDisplay{ 119 | font-size: 22; 120 | text-align: center; 121 | } 122 | 123 | .footerText{ 124 | font-size: 16; 125 | font-family: Garamond; 126 | } -------------------------------------------------------------------------------- /app/scripts/models/appModel.js: -------------------------------------------------------------------------------- 1 | window.appModel = Backbone.Model.extend({ 2 | defaults: { 3 | boardWidth : undefined, //set upon instantiation 4 | numPlayers : undefined, 5 | playerCollection : undefined, 6 | gameRunning : false, 7 | roundRunning : false, 8 | winnerDeclared : false, 9 | bidManager : undefined, 10 | tokens : undefined, 11 | boardModel : undefined, 12 | scoreModel : undefined, //handles scores, bids, remaining tokens 13 | robots : undefined, 14 | players : undefined, 15 | newPlayerNames : undefined, 16 | colorHex: { 17 | brown: '#6C4E2F', 18 | R:'#F21018', 19 | G: '#39CC36', 20 | B: '#0C11CA', 21 | Y: '#EFEB1D', 22 | silverBorder : '#72736D', 23 | silverLight : '#ABADA0', //tiny xSquare dots 24 | lightYellow : '#E8E549', //background 25 | lightGrey : '#66665D', //center walls 26 | darkGrey : '#51514B', //center square 27 | background : '#E7D4B0', //grey, orange tint 28 | xSquareBorder : '#ABADA0', //tiny xSquare dots 29 | xSquareBg : '#D4D7C7',//offwhite 30 | xSquareOverlay : '#ECEDE6', //light, offwhite 31 | xSquareG : '#DBDBDB', 32 | xSquareY : '#EBE0A6' 33 | }, 34 | boxSize : undefined 35 | }, 36 | initialize: function(){ 37 | this.set({ 38 | boardModel : new boardModel({ 39 | boardWidth: this.get('boardWidth') 40 | }), 41 | scoreModel : new scoreModel() 42 | }); 43 | //Helper functions that provide data that child views' render functions will need. 44 | //boxSize Data 45 | this.on('change:boxSize', function(){ 46 | var boxSize = this.get('boxSize'); 47 | var robots = this.get('boardModel').get('robots'); 48 | _.each(robots.models, function(value, key, list){ 49 | value.set('boxSize', boxSize); 50 | }, this) 51 | }, this); 52 | 53 | //Username data 54 | this.on('change:numPlayers', function(){ 55 | var numPlayers = this.get('numPlayers') 56 | //define a set of hardcoded new players: allow for name input later.. 57 | var newPlayers = []; 58 | if (numPlayers > 0){ 59 | var newPlayerNames = this.get('newPlayerNames'); 60 | for (var i = 0 ; i < numPlayers; i++){ 61 | newPlayers.push(new playerModel({ 62 | username: newPlayerNames[i] 63 | })); 64 | } 65 | var finalPlayers = new players(newPlayers); 66 | this.set('players', finalPlayers); 67 | } 68 | }); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /app/scripts/models/quadrantHolderModel.js: -------------------------------------------------------------------------------- 1 | window.quadrantHolder = Backbone.Model.extend({ 2 | //All Quadrants: 3 | //Strings are hardcoded, and 4 two-sided quadrants that can be flipped and rearranged to form a board. 4 | //There are 96 different variations. 5 | //Converted from Python code written by Michael Fogleman: 6 | //https://github.com/fogleman/Ricochet/blob/master/model.py 7 | // 8 | //See boardModel.js, specifically constructBoard and rotateQuadrant, for information 9 | //on how these strings are formatted, converted, and then concatenated to create a playable baord. 10 | defaults: { 11 | Q1A : [ 12 | 'NW,N,N,N,NE,NW,N,N', 13 | 'W,S,X,X,X,X,SEYH,W', 14 | 'WE,NWGT,X,X,X,X,N,X', 15 | 'W,X,X,X,X,X,X,X', 16 | 'W,X,X,X,X,X,S,X', 17 | 'SW,X,X,X,X,X,NEBQ,W', 18 | 'NW,X,E,SWRC,X,X,X,S', 19 | 'W,X,X,N,X,X,E,NW' 20 | ], 21 | Q1B : [ 22 | 'NW,NE,NW,N,NS,N,N,N', 23 | 'W,S,X,E,NWRC,X,X,X', 24 | 'W,NEGT,W,X,X,X,X,X', 25 | 'W,X,X,X,X,X,SEYH,W', 26 | 'W,X,X,X,X,X,N,X', 27 | 'SW,X,X,X,X,X,X,X', 28 | 'NW,X,E,SWBQ,X,X,X,S', 29 | 'W,X,X,N,X,X,E,NW' 30 | ], 31 | Q2A : [ 32 | 'NW,N,N,NE,NW,N,N,N', 33 | 'W,X,X,X,X,E,SWBC,X', 34 | 'W,S,X,X,X,X,N,X', 35 | 'W,NEYT,W,X,X,S,X,X', 36 | 'W,X,X,X,E,NWGQ,X,X', 37 | 'W,X,SERH,W,X,X,X,X', 38 | 'SW,X,N,X,X,X,X,S', 39 | 'NW,X,X,X,X,X,E,NW' 40 | ], 41 | Q2B : [ 42 | 'NW,N,N,N,NE,NW,N,N', 43 | 'W,X,SERH,W,X,X,X,X', 44 | 'W,X,N,X,X,X,X,X', 45 | 'WE,SWGQ,X,X,X,X,S,X', 46 | 'SW,N,X,X,X,E,NWYT,X', 47 | 'NW,X,X,X,X,S,X,X', 48 | 'W,X,X,X,X,NEBC,W,S', 49 | 'W,X,X,X,X,X,E,NW' 50 | ], 51 | Q3A : [ 52 | 'NW,N,N,NE,NW,N,N,N', 53 | 'W,X,X,X,X,SEGH,W,X', 54 | 'WE,SWRQ,X,X,X,N,X,X', 55 | 'SW,N,X,X,X,X,S,X', 56 | 'NW,X,X,X,X,E,NWYC,X', 57 | 'W,X,S,X,X,X,X,X', 58 | 'W,X,NEBT,W,X,X,X,S', 59 | 'W,X,X,X,X,X,E,NW' 60 | ], 61 | Q3B : [ 62 | 'NW,N,NS,N,NE,NW,N,N', 63 | 'W,E,NWYC,X,X,X,X,X', 64 | 'W,X,X,X,X,X,X,X', 65 | 'W,X,X,X,X,E,SWBT,X', 66 | 'SW,X,X,X,S,X,N,X', 67 | 'NW,X,X,X,NERQ,W,X,X', 68 | 'W,SEGH,W,X,X,X,X,S', 69 | 'W,N,X,X,X,X,E,NW' 70 | ], 71 | Q4A : [ 72 | 'NW,N,N,NE,NW,N,N,N', 73 | 'W,X,X,X,X,X,X,X', 74 | 'W,X,X,X,X,SEBH,W,X', 75 | 'W,X,S,X,X,N,X,X', 76 | 'SW,X,NEGC,W,X,X,X,X', 77 | 'NW,S,X,X,X,X,E,SWRT', 78 | 'WE,NWYQ,X,X,X,X,X,NS', 79 | 'W,X,X,X,X,X,E,NW' 80 | ], 81 | Q4B : [ 82 | 'NW,N,N,NE,NW,N,N,N', 83 | 'WE,SWRT,X,X,X,X,S,X', 84 | 'W,N,X,X,X,X,NEGC,W', 85 | 'W,X,X,X,X,X,X,X', 86 | 'W,X,SEBH,W,X,X,X,S', 87 | 'SW,X,N,X,X,X,E,NWYQ', 88 | 'NW,X,X,X,X,X,X,S', 89 | 'W,X,X,X,X,X,E,NW' 90 | ] 91 | }, 92 | //Set the quadrants on the model 93 | initialize: function() { 94 | this.set('Q1', [this.get('Q1A'),this.get('Q1B')]); 95 | this.set('Q2', [this.get('Q2A'),this.get('Q2B')]); 96 | this.set('Q3', [this.get('Q3A'),this.get('Q3B')]); 97 | this.set('Q4', [this.get('Q4A'),this.get('Q4B')]); 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /app/scripts/dialogs/introDialogs.js: -------------------------------------------------------------------------------- 1 | 2 | //Core functionality in lines 69-80. 3 | var startDialog = function(viewsToRender, parentModel, test){ 4 | //Uses startDialog's closure to access parentModel, setting the players and names. 5 | //Also triggers render processes through viewsToRender. 6 | /** 7 | * Input, an object, 'data', for which data.players holds an array of strings. 8 | * @return {[type]} [description] 9 | */ 10 | var diagMessages = ['Let\'s play Ricochet Robots! \n How many players?', "What are your players' names?"]; 11 | var startGame = function(data) { 12 | var playerNames = []; 13 | for (var i = 0 ; i < data.players.length; i++){ 14 | playerNames.push(data.players[i]); 15 | } 16 | parentModel.set('newPlayerNames', playerNames); 17 | parentModel.set('numPlayers', data.players.length); 18 | 19 | //Render the app's views, now that all required player information is present. 20 | _.each(viewsToRender, function(view){ 21 | view.render(); 22 | }); 23 | test ? Backbone.Events.trigger('newGame', [true]) : Backbone.Events.trigger('newGame'); 24 | 25 | //Remove instructions, fill in background color for game. 26 | $('body').css('background-color', '#D4D7C7'); 27 | $('#credits').remove(); 28 | } 29 | 30 | //Asks for number of players, passes that number in data to cb. 31 | var askForNumPlayers = function(cb){ 32 | vex.dialog.open({ 33 | message: diagMessages[0], 34 | input: "", 35 | buttons: [ 36 | $.extend({}, vex.dialog.buttons.YES, { 37 | text: 'That many players!' 38 | }), $.extend({}, vex.dialog.buttons.NO, { 39 | text: '¯\\\(°_o)/¯ - How do I play?' 40 | }) 41 | ], 42 | callback: cb 43 | }); 44 | 45 | } 46 | 47 | //Asks for X player names, where X is specified in data. 48 | //Uses createNameInputForm. 49 | var askForPlayerNames = function(data) { 50 | if (data === false) { 51 | renderInstructions(); 52 | return; 53 | } 54 | var nameInput = createNameInputForm(data.players); 55 | 56 | //Open second dialogue, asking for names. 57 | vex.dialog.open({ 58 | message: diagMessages[1], 59 | input: nameInput, 60 | buttons: [ 61 | $.extend({}, vex.dialog.buttons.YES, { 62 | text: 'GO!!!' 63 | }) 64 | ], 65 | callback: startGame 66 | }); 67 | } 68 | 69 | //Test set of players, saving development time when drawing complex shapes on canvas 70 | if(test){ 71 | //Refactor into an invokation of startGame.... 72 | var testInput = { 73 | players : ["Zephanaiah", "Raghuvir", "Alpheus", "Sze-Hung"] 74 | }; 75 | startGame(testInput); 76 | 77 | } else{ 78 | //Open first Dialog, which either sends the player to an input screen for player names... 79 | askForNumPlayers(askForPlayerNames); 80 | } 81 | }; 82 | 83 | //Displays the instructions. 84 | var renderInstructions = function(){ 85 | var instructions = new instructionsView({ 86 | el: $('#instructions') 87 | }).render(); 88 | }; 89 | 90 | //Generates an HTML string for a vex dialoge to display. 91 | var createNameInputForm = function(players){ 92 | var inputTemplate = ['
']; 93 | var inputHTML = "" 94 | for (var i = 1 ; i <= players; i++){ 95 | inputHTML += inputTemplate[0] + i + inputTemplate[1]; 96 | } 97 | return inputHTML; 98 | } 99 | -------------------------------------------------------------------------------- /app/scripts/views/robotView.js: -------------------------------------------------------------------------------- 1 | window.robotView = Backbone.View.extend({ 2 | template: _.template( 3 | '' 4 | ), 5 | events: { 6 | }, 7 | initialize: function(){ 8 | this.model.on('change:boxSize', function(){ 9 | this.setup(); 10 | this.move(); 11 | }, this); 12 | this.model.on('change:loc', this.move, this); 13 | }, 14 | render: function(){ 15 | return this.template(this.model.attributes); 16 | }, 17 | //animate an activated robot... 18 | activate: function(){ 19 | var props = this.model.attributes; 20 | var robot = $('.'+props.color); 21 | robot.toggleClass('rotated'); 22 | setTimeout(function(){ 23 | this.toggleClass('rotated'); 24 | }.bind(robot),500); 25 | }, 26 | setup: function(){ 27 | var props = this.model.attributes; 28 | var boxSize = props.boxSize; 29 | 30 | //Model holds reference to the last clicked robot, to know which one to move. 31 | var robot = $('.'+props.color) 32 | robot.on('mousedown', function(selection){ 33 | var activeRobotColor = props.color; 34 | rootModel.get('boardModel').set('activeRobot', activeRobotColor); 35 | this.activate(); 36 | }.bind(this)); 37 | 38 | var colors = rootModel.get('colorHex'); 39 | var context = robot[0].getContext('2d'); 40 | this.drawRobot(context, colors, props); 41 | 42 | }, 43 | drawRobot: function(context, colors, props){ 44 | var drawEllipse = function(rotation){ 45 | context.beginPath(); 46 | for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) { 47 | var xPos = 20 - (10 * Math.sin(i)) * Math.sin(rotation * Math.PI) + (20 * Math.cos(i)) * Math.cos(rotation * Math.PI); 48 | var yPos = 20 + (20 * Math.cos(i)) * Math.sin(rotation * Math.PI) + (10 * Math.sin(i)) * Math.cos(rotation * Math.PI); 49 | 50 | if (i == 0) { 51 | context.moveTo(xPos, yPos); 52 | } else { 53 | context.lineTo(xPos, yPos); 54 | } 55 | } 56 | context.lineWidth = 2; 57 | context.strokeStyle = 'black'; 58 | context.fillStyle = colors[props.color]; 59 | context.closePath(); 60 | context.stroke(); 61 | context.fill(); 62 | } 63 | //Draw Ellipse one, outside the circular robot. 64 | drawEllipse(0.5); 65 | //Draw Ellipse two, also outside the circular robot. 66 | drawEllipse(1); 67 | //Draw main large circle that represents robot. 68 | context.beginPath(); 69 | context.fillStyle = colors[props.color]; 70 | context.arc(20,20,16,0,Math.PI*2); 71 | context.fill() 72 | context.lineWidth = 1.5; 73 | context.strokeStyle = 'black'; 74 | context.stroke(); 75 | 76 | //Draw small arcs inside main circle. 77 | context.beginPath(); 78 | context.arc(20,20,10,Math.PI*-.25,Math.PI*.25); 79 | context.lineWidth = 2; 80 | context.strokeStyle = 'black'; 81 | context.stroke(); 82 | 83 | context.beginPath(); 84 | context.arc(20,20,10,Math.PI*.75,Math.PI*1.25); 85 | context.stroke(); 86 | 87 | //draw a circle inside, draw arcs inside the smaller circle 88 | context.beginPath(); 89 | context.lineWidth = 1; 90 | context.arc(20,20,3,0,Math.PI*2); 91 | context.stroke(); 92 | }, 93 | /** 94 | * Moves the robot to a location on the board canvas element, using the board's loc property. 95 | */ 96 | move: function(){ 97 | var props = this.model.attributes; 98 | var boxSize = props.boxSize; 99 | var robot = $('.'+props.color) 100 | robot.animate({ 101 | top: (props.loc.row*boxSize+(boxSize*.1)+2), 102 | left: (props.loc.col*boxSize+(boxSize*.1)+2), 103 | width: boxSize*.8, 104 | height: boxSize*.8 105 | }, {duration: 150}, function(){}); 106 | } 107 | }); -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## General Workflow 4 | 5 | 1. Fork the repo 6 | 1. Cut a namespaced feature branch from master 7 | 1. Make commits to your feature branch. Prefix each commit like so: 8 | 1. When you've finished with your fix or feature, Rebase upstream changes into your branch. submit a [pull request][] 9 | directly to master. Include a description of your changes. 10 | 1. Fix any issues raised by your code reviwer, and push your fixes as a single 11 | new commit. 12 | 1. Once the pull request has been reviewed, it will be merged by another member of the team. Do not merge your own commits. 13 | 14 | ## Detailed Workflow 15 | 16 | ### Fork the repo 17 | 18 | Use github’s interface to make a fork of the repo, then add that repo as an upstream remote: 19 | 20 | ``` 21 | git remote add upstream https://github.com/sdtsui/.git 22 | ``` 23 | 24 | ### Cut a namespaced feature branch from master 25 | 26 | Your branch should follow this naming convention: 27 | - bug/... 28 | - feat/... 29 | - test/... 30 | - doc/... 31 | - refactor/... 32 | 33 | These commands will help you do this: 34 | 35 | ``` bash 36 | 37 | # Creates your branch and brings you there 38 | git checkout -b `your-branch-name` 39 | ``` 40 | 41 | ### Make commits to your feature branch. 42 | 43 | Prefix each commit like so 44 | - (feat) Added a new feature 45 | - (fix) Fixed inconsistent tests [Fixes #0] 46 | - (refactor) ... 47 | - (cleanup) ... 48 | - (test) ... 49 | - (doc) ... 50 | 51 | Make changes and commits on your branch, and make sure that you 52 | only make changes that are relevant to this branch. If you find 53 | yourself making unrelated changes, make a new branch for those 54 | changes. 55 | 56 | #### Commit Message Guidelines 57 | 58 | - Commit messages should be written in the present tense; e.g. "Fix continuous 59 | integration script". 60 | - The first line of your commit message should be a brief summary of what the 61 | commit changes. Aim for about 70 characters max. Remember: This is a summary, 62 | not a detailed description of everything that changed. 63 | - If you want to explain the commit in more depth, following the first line should 64 | be a blank line and then a more detailed description of the commit. This can be 65 | as detailed as you want, so dig into details here and keep the first line short. 66 | 67 | ### Rebase upstream changes into your branch 68 | 69 | Once you are done making changes, you can begin the process of getting 70 | your code merged into the main repo. Step 1 is to rebase upstream 71 | changes to the master branch into yours by running this command 72 | from your branch: 73 | 74 | ``` 75 | git pull --rebase upstream master 76 | ``` 77 | 78 | This will start the rebase process. You must commit all of your changes 79 | before doing this. If there are no conflicts, this should just roll all 80 | of your changes back on top of the changes from upstream, leading to a 81 | nice, clean, linear commit history. 82 | 83 | If there are conflicting changes, git will start yelling at you part way 84 | through the rebasing process. Git will pause rebasing to allow you to sort 85 | out the conflicts. You do this the same way you solve merge conflicts, 86 | by checking all of the files git says have been changed in both histories 87 | and picking the versions you want. Be aware that these changes will show 88 | up in your pull request, so try and incorporate upstream changes as much 89 | as possible. 90 | 91 | Once you are done fixing conflicts for a specific commit, run: 92 | 93 | ``` 94 | git rebase --continue 95 | ``` 96 | 97 | This will continue the rebasing process. Once you are done fixing all 98 | conflicts you should run the existing tests to make sure you didn’t break 99 | anything, then run your new tests (there are new tests, right?) and 100 | make sure they work also. 101 | 102 | If rebasing broke anything, fix it, then repeat the above process until 103 | you get here again and nothing is broken and all the tests pass. 104 | 105 | ### Make a pull request 106 | 107 | Make a clear pull request from your fork and branch to the upstream master 108 | branch, detailing exactly what changes you made and what feature this 109 | should add. The clearer your pull request is the faster you can get 110 | your changes incorporated into this repo. 111 | 112 | At least one other person MUST give your changes a code review, and once 113 | they are satisfied they will merge your changes into upstream. Alternatively, 114 | they may have some requested changes. You should make more commits to your 115 | branch to fix these, then follow this process again from rebasing onwards. 116 | 117 | Once you get back here, make a comment requesting further review and 118 | someone will look at your code again. If they like it, it will get merged, 119 | else, just repeat again. 120 | 121 | Thanks for contributing! 122 | 123 | ### Guidelines 124 | 125 | 1. Uphold the current code standard: 126 | - Keep your code [DRY][]. 127 | - Apply the [boy scout rule][]. 128 | - Follow [STYLE-GUIDE.md](STYLE-GUIDE.md) 129 | 1. Run the [tests][] before submitting a pull request. 130 | 1. Tests are very, very important. Submit tests if your pull request contains 131 | new, testable behavior. 132 | 1. Your pull request is comprised of a single ([squashed][]) commit. 133 | 134 | ## Checklist: 135 | 136 | This is just to help you organize your process 137 | 138 | - [ ] Did I cut my work branch off of master (don't cut new branches from existing feature brances)? 139 | - [ ] Did I follow the correct naming convention for my branch? 140 | - [ ] Is my branch focused on a single main change? 141 | - [ ] Do all of my changes directly relate to this change? 142 | - [ ] Did I rebase the upstream master branch after I finished all my 143 | work? 144 | - [ ] Did I write a clear pull request message detailing what changes I made? 145 | - [ ] Did I get a code review? 146 | - [ ] Did I make any requested changes from that code review? 147 | 148 | If you follow all of these guidelines and make good changes, you should have 149 | no problem getting your changes merged in. -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ricochet Backbone 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 67 | 94 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
116 |
117 |
118 | 119 |
120 |
121 |
122 |
123 |
124 |
Quick Start Instructions:
Bidders will be asked to move at the end of each round.
To activate a robot: click it!
To move an activated robot: use WASD or arrow keys.

For 2-5 players. Have fun!
125 |
126 | By: Daniel Tsui. Find me at sdtsui.com.
127 | Repo@ github.com/sdtsui/ricochet-backbone
128 | Please email bug reports and feedback to daniel.tsui at hackreactor dot com
129 |
130 |
131 | Original Author : Alex Randolph
132 | Original Graphics : Franz Vohwinkel 133 |
134 |
135 |
136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /app/scripts/models/scoreModel.js: -------------------------------------------------------------------------------- 1 | window.scoreModel = Backbone.Model.extend({ 2 | defaults : { 3 | tokensRemaining : [], 4 | targetToken : undefined, 5 | timerValue : 60, 6 | timeRemaining : true, 7 | bidQueue : [], 8 | activePlayer : undefined, 9 | activeBid : undefined, 10 | activeMoves : 0, 11 | interval : undefined, 12 | startTimerFn: function(){ 13 | if(this.get('timerValue') === 60){ 14 | this.set('interval', setInterval(function(){ 15 | this.set('timerValue', 16 | this.get('timerValue')-1); 17 | }.bind(this) 18 | ,1000) 19 | ); 20 | } 21 | var checkInterval = setInterval(function(){ 22 | if (this.get('timerValue') <= 0){ 23 | clearInterval(this.get('interval')); 24 | this.trigger('timeUp'); 25 | clearInterval(checkInterval); 26 | this.set('timerValue', 0); 27 | } 28 | }.bind(this), 1000); 29 | }, 30 | bidCounter: 0 31 | }, 32 | incrementBidCounter : function(){ 33 | this.set('bidCounter', this.get('bidCounter')+1); 34 | }, 35 | //Handles multiple sources of game logic. 36 | //4 main functions: 37 | //1) When robots are moved, scoreModel checks if a point is won, or the round is over. 38 | //2) When bids are placed, starts the timer when appropriate. 39 | //3) When timer reaches 0, gathers bids to create a queue, requesting moves from players. 40 | //4) Assigns points and resets board state when a round ends. 41 | initialize: function(){ 42 | //Functions 1 43 | Backbone.Events.on('robotMoved', this.robotMoved, this) 44 | Backbone.Events.on('robotArrived', this.robotArrived, this) 45 | 46 | //Functions 2/3 3 47 | Backbone.Events.on('newBidEvent', this.incrementBidCounter, this); 48 | this.on('change:timerValue', this.checkTimer, this); 49 | Backbone.Events.on('skipTimer', this.skipTimer, this); 50 | this.on('timeUp', this.timeUp, this); 51 | Backbone.Events.on('newBid', this.handleIncomingBid, this); 52 | 53 | //Function 4 54 | this.on('endRound', this.newRound, this); 55 | Backbone.Events.on('newGame', this.newRound, this); 56 | 57 | //See issue #7 58 | this.on('change:targetToken', this.drawCenter, this); 59 | 60 | //Function 4 61 | this.on('successRound failRound endRound newGame', this.resetActive, this); 62 | this.on('successRound', this.activeSuccess, this); 63 | this.on('failRound', this.activeFail, this); 64 | 65 | this.timerReset(this) 66 | }, 67 | resetActive: function(){ 68 | this.set('activePlayer', undefined); 69 | }, 70 | robotMoved: function(){ 71 | //increment active moves. 72 | //if active moves over latest bid, trigger fail event 73 | this.set('activeMoves', this.get('activeMoves')+1); 74 | if (this.get('activeMoves') >= this.get('activeBid').value ){ 75 | this.trigger('failRound', [this.get('activeBid')]); 76 | } 77 | }, 78 | robotArrived: function(){ 79 | //if active moves under or equal to latest bid, trigger success event 80 | //omg, Note to self: trigger can pass event callbacks. This changes things! 81 | //Can refactor a lot of events into groups.. 82 | this.trigger('successRound', [this.get('activeBid')]); 83 | 84 | }, 85 | activeSuccess: function(bid){ 86 | var bid = bid[0] 87 | var triggerEnd = function(){ 88 | this.trigger('endRound'); 89 | }.bind(this) 90 | //assign token object to active bid.player 91 | bid.playerModel.addPoint(this.get('targetToken')); 92 | vex.dialog.open({ 93 | message: bid.username + ' wins a point!', 94 | buttons: [ 95 | $.extend({}, vex.dialog.buttons.YES, { 96 | text: 'Sweet!' 97 | })], 98 | callback: triggerEnd 99 | }); 100 | //Note, this event, which is simply adding a token to playerModel, could be a responded to inside playerModel.. 101 | }, 102 | activeFail: function(bid){ 103 | var bid = bid[0] 104 | vex.dialog.open({ 105 | message: bid.username + ' failed!', 106 | callback: function(){ 107 | failRound(); 108 | } 109 | }); 110 | var failRound = function(){ 111 | var playerTokens = bid.playerModel.get('tokensWon'); 112 | if (playerTokens.length > 0){ 113 | this.get('tokensRemaining').push(bid.playerModel.removePoint()); 114 | this.trigger('change:tokensRemaining') 115 | } 116 | //decrement active players' points to min 0 117 | //if length >0 118 | //randomly select a token from active player's array of won tokens, 119 | // return them to remainingTokens 120 | 121 | if (this.get('bidQueue').length > 0){ 122 | Backbone.Events.trigger('resetPosition'); 123 | this.requestMove(); 124 | } else { 125 | this.get('tokensRemaining').push(this.get('targetToken')); 126 | Backbone.Events.trigger('resetPosition'); 127 | this.trigger('endRound'); 128 | } 129 | this.shuffleTokens(); 130 | }.bind(this); 131 | }, 132 | newRound: function(test){ 133 | var runRound = function(){ 134 | Backbone.Events.trigger('roundStart'); 135 | this.set('bidQueue', []); 136 | this.set('activeBid', undefined); 137 | this.set('activeMoves', 0); //this could be more modular, resetting functions 138 | //can be separate from the new token assignment functions 139 | ////See activeMoves on 134 (and all other instances of activeMoves) 140 | 141 | //remove token from tokenPool, trigger a new token so canvasDrawView 142 | var remTokens = this.get('tokensRemaining') 143 | if (remTokens.length === 0){ 144 | vex.dialog.open({ 145 | message: "Someone (calculate winner)" + " wins!" 146 | }); 147 | } else { 148 | this.set('targetToken', remTokens.shift()); 149 | this.timerReset(this); 150 | } 151 | }.bind(this); 152 | 153 | if (test){ 154 | runRound(); 155 | }else{ 156 | vex.dialog.open({ 157 | message: 'New Round! Ready for the next round?', 158 | buttons: [ 159 | $.extend({}, vex.dialog.buttons.YES, { 160 | text: 'GO!' 161 | }) 162 | ], 163 | callback: function() { 164 | runRound(); 165 | } 166 | }); 167 | } 168 | }, 169 | addToken: function(newToken){ 170 | var tokens = this.get('tokensRemaining') 171 | tokens.push(newToken); 172 | if(this.get('tokensRemaining').length === 16){ 173 | this.shuffleTokens(); 174 | } 175 | }, 176 | //Todo: See Issue #7. 177 | drawCenter: function(){ 178 | var target = this.get('targetToken'); 179 | var grid = { 180 | context : boardDetails.getContext(), 181 | box : boardDetails.getWidthAndSize(), 182 | grid : boardDetails 183 | }; 184 | grid.grid.drawShape( 185 | grid.context, 186 | grid.box.bsize, 187 | undefined, 188 | undefined, 189 | target.color, 190 | target.shape, 191 | true); 192 | }, 193 | shuffleTokens: function(){ 194 | this.get('tokensRemaining').sort(function(){return Math.random()-0.5;}); 195 | }, 196 | timerReset : function(ctx){ 197 | ctx.set('timerValue', 60); 198 | ctx.startTimer = _.once(this.get('startTimerFn').bind(this)); 199 | ctx.set('timeRemaining', false); 200 | 201 | }, 202 | skipTimer: function(){ 203 | this.set('timerValue', 0); 204 | }, 205 | checkTimer: function(){ 206 | if (this.get('timerValue') === 0){ 207 | if(this.get('timeRemaining')){ 208 | this.trigger('timeUp'); 209 | this.set('timeRemaining', false); 210 | } 211 | } 212 | }, 213 | requestMove: function(){ 214 | var activeBid = this.get('bidQueue').shift(); 215 | this.set('activeMoves', 0); 216 | this.set('activeBid', activeBid); 217 | this.set('activePlayer', activeBid.username); 218 | vex.dialog.open({ 219 | message: 'Your move, ' + activeBid.username + '. Your bid is '+ activeBid.value + ' moves.', 220 | buttons: [ 221 | $.extend({}, vex.dialog.buttons.YES, { 222 | text: 'I\'m ready.' 223 | }) 224 | ] 225 | }); 226 | }, 227 | timeUp: function(){ 228 | //collate bids. 229 | //requestMove.... 230 | this.collateBids(); 231 | this.requestMove(); 232 | }, 233 | collateBids: function(){ 234 | //uses createNewBid to take all the bid numbers, and order the players into the queue 235 | //use rootModel.collection...get all the bids 236 | var players = rootModel.get('players'); 237 | var bids = []; 238 | players.each(function(player, idx){ 239 | if(player.get('currentBid').moves !== '-'){ 240 | var newBid = this.createNewBid(player); 241 | bids.push(newBid); 242 | } 243 | }.bind(this)); 244 | bids.sort(function(bid1, bid2){ 245 | if(bid1.value === bid2.value){ 246 | return bid1.order - bid2.order; 247 | } 248 | return bid1.value - bid2.value; 249 | }) 250 | this.set('bidQueue', bids); 251 | }, 252 | createNewBid: function(player){ 253 | //creates a new bid object with the bid, and a reference to the player that made it 254 | return { 255 | username : player.get('username'), 256 | value : player.get('currentBid').moves, 257 | playerModel : player, 258 | order : player.get('currentBid').bidNumber 259 | }; 260 | }, 261 | dequeueBid: function(){ 262 | //pop off a bid from the queue, return object 263 | }, 264 | clearQueue: function(){ 265 | this.set('bidQueue', []); 266 | }, 267 | //non-DRY. See issue #7; 268 | handleIncomingBid: function(){ 269 | var x = $('.bidfield > input'); 270 | if (!this.get('activePlayer')){ 271 | this.startTimer(); 272 | } 273 | } 274 | }); 275 | -------------------------------------------------------------------------------- /app/scripts/models/boardModel.js: -------------------------------------------------------------------------------- 1 | window.boardModel = Backbone.Model.extend({ 2 | defaults: { 3 | boardWidth : undefined, 4 | quadrantArrangement : undefined, 5 | completeBoard : undefined, 6 | enteringMove : true, 7 | baseQuadrants: new quadrantHolder({}), 8 | rHash : { 9 | //hash for a 90 degree rotation 10 | N: "E", 11 | E: "S", 12 | S: "W", 13 | W: "N" 14 | }, 15 | dHash : { 16 | N: { 17 | row: -1, 18 | col: 0, 19 | opposite: 'S' 20 | }, 21 | S: { 22 | row: 1, 23 | col: 0, 24 | opposite: 'N' 25 | }, 26 | E: { 27 | row: 0, 28 | col: 1, 29 | opposite: 'W' 30 | }, 31 | W: { 32 | row: 0, 33 | col: -1, 34 | opposite: 'E' 35 | } 36 | }, 37 | robots : undefined, 38 | activeRobot: undefined 39 | }, 40 | initialize: function(){ 41 | this.set('quadrantArrangement', this.setQuads()); 42 | this.constructBoard(this.get('quadrantArrangement')); 43 | this.setRobots(); 44 | //N,S,E,W: 45 | Backbone.Events.on('all', function(n){ 46 | if (n.slice(0,3) === 'key'){ 47 | this.respondToKey(n); 48 | } 49 | }, this); 50 | }, 51 | respondToKey : function(keyName){ 52 | var activeRobot = this.get('activeRobot'); 53 | if (activeRobot && keyName.length === 4){ 54 | this.moveRobot(keyName[3], activeRobot); 55 | } 56 | }, 57 | moveRobot : function(dir, robot){ 58 | var robotToMove = this.get('robots').where({color: robot})[0]; 59 | var loc = robotToMove.get('loc'); 60 | var completeBoard = this.get('completeBoard'); 61 | /** 62 | * Control flow: 63 | * if a move in that direction is valid: 64 | * move in that direction, update position, call again 65 | * else 66 | * stop 67 | * 68 | * if moves === 0, do nothing, disregard click 69 | * else 70 | * savePosition, update lastMoveDir, 71 | * update loc...which should trigger a transition. 72 | */ 73 | var next = { 74 | nextSquare: undefined, 75 | lastValidSquare: undefined, 76 | moves: 0 77 | } 78 | next.nextSquare = this.checkMoveDirValid(loc, dir, robotToMove, completeBoard); 79 | //if valid, nextSquare is truthy 80 | while (next.nextSquare !== false){ 81 | next.moves++; 82 | next.lastValidSquare = next.nextSquare; 83 | next.nextSquare = this.checkMoveDirValid(next.nextSquare, dir, robotToMove, completeBoard); 84 | } 85 | if (next.moves === 0){ 86 | //'illegal move: nothing happens; should disregard keydown'; 87 | } else { 88 | robotToMove.savePosition(); 89 | robotToMove.set('lastMoveDir', dir); 90 | robotToMove.set('loc', next.lastValidSquare); 91 | 92 | // Checking for arrival. Look out: a little messy. Can be refactored. Currently not DRY. 93 | var activeRobot = this.get('robots').where({color: robot})[0]; 94 | var target = rootModel.get('scoreModel').get('targetToken'); 95 | if((activeRobot.get('loc').row === target.loc.row && activeRobot.get('loc').col === target.loc.col)//match position 96 | && 97 | (activeRobot.get('color') === target.color) // match type 98 | ){ 99 | Backbone.Events.trigger('robotArrived'); 100 | }else { 101 | Backbone.Events.trigger('robotMoved'); 102 | } 103 | } 104 | }, 105 | checkMoveDirValid : function(loc, dir, robot, completeBoard){ 106 | var dHash = this.get('dHash'); 107 | if (robot.get('lastMoveDir') === dHash[dir].opposite) { 108 | //moving back is illegal 109 | return false 110 | } 111 | var robotLoc = loc; 112 | if (completeBoard[robotLoc.row][robotLoc.col].indexOf(dir) !== -1){ 113 | //moving into a wall on this square is illegal 114 | return false; 115 | } 116 | var movement = dHash[dir] 117 | var nextSquare = { 118 | row: robotLoc.row + movement.row, 119 | col: robotLoc.col + movement.col 120 | } 121 | if (this.get('robots').squareHasConflict(nextSquare)){ 122 | return false; 123 | //ie, next square is occupied 124 | } 125 | //no conflicts, move is legal 126 | return nextSquare; 127 | }, 128 | //converts rowsStrings into arrays, for easy access by draw and position checking functions 129 | rowStringsToArrays : function(quad, size){ 130 | var convertedQuad = []; 131 | for (var i = 0 ; i < size; i++){ 132 | convertedQuad.push(quad[i].split(',')) 133 | } 134 | return convertedQuad; 135 | }, 136 | //Loops through each quadrant after a rotation, re-adjusting walls to conform to the new rotated quadrant. 137 | adjustWallsAfterRotation : function(quad, size){ 138 | var adjustedQuad = [] 139 | for (var i = 0; i < size; i++){ 140 | var newRow = []; 141 | for(var j =0 ; j < size; j++){ 142 | var oldString = quad[i][j]; 143 | var newString = ""; 144 | for (var k = 0 ; k < oldString.length; k++){ 145 | var oldChar = oldString.charAt(k); 146 | if(this.get('rHash').hasOwnProperty(oldChar)){ 147 | newString += this.get('rHash')[oldChar]; 148 | } else { 149 | newString += oldChar; 150 | } 151 | } 152 | newRow.push(newString); 153 | } 154 | adjustedQuad.push(newRow); 155 | } 156 | return adjustedQuad; 157 | }, 158 | rotateQuadrant : function(quad, size){ 159 | var newQuad = []; 160 | for (var i = 0 ; i < size; i++){ 161 | var newRow = []; 162 | for (var j = size-1; j >=0 ; j--){ 163 | newRow.push(quad[j][i]); 164 | } 165 | newQuad.push(newRow); 166 | } 167 | return this.adjustWallsAfterRotation(newQuad, 8); 168 | }, 169 | //A random quadrant is chosen, then a random side is chosen. Depending on what order it was chosen in, 170 | //each quadrant rotates a number of times so that all 4 quadrants create a square. 171 | //For example, the 3rd selected quadrant is rotated twice, since default orientation is all facing "north west". 172 | //3rd quadrant needs to be facing "north east", and is thus rotated twice. 173 | setQuads: function(){ 174 | var quadrants = [1,2,3,4] 175 | var arrangement = []; 176 | while (quadrants.length !== 0){ 177 | var nextQuad = _.random(0,quadrants.length-1); 178 | arrangement.push(quadrants.splice(nextQuad, 1)[0]); 179 | } 180 | quads = []; 181 | for (var i = 0; i < 4; i++){ 182 | var side = _.random(0,1); 183 | var newQuadrant = this.get('baseQuadrants').get('Q'+arrangement[i])[side]; 184 | newQuadrant = this.rowStringsToArrays(newQuadrant, 8); 185 | for (var j = i; j >0; j--){ 186 | newQuadrant = this.rotateQuadrant(newQuadrant,8) 187 | } 188 | quads.push(newQuadrant); 189 | } 190 | return quads; 191 | }, 192 | setRobots: function(){ 193 | var newRobots = []; 194 | var robotColors = ['R', 'Y', 'G', 'B']; 195 | //row, col 196 | //note: occupiedSquares is now only a helper variable. 197 | //no other instances should be in the app 198 | var occupiedSquares = [[7,7], [7,8], [8,7], [8,8]] 199 | while(newRobots.length <4){ 200 | var newCoords = [_.random(0,15), _.random(0,15)]; 201 | var row = newCoords[0]; 202 | var col = newCoords[1]; 203 | var match = false; 204 | for(var i = 0; i < occupiedSquares.length; i++){ 205 | if ( 206 | (occupiedSquares[i][0] === row && occupiedSquares[i][1] === col) 207 | || 208 | (this.get('completeBoard')[row][col].length > 2 ) 209 | ) 210 | { 211 | match = true; 212 | break; 213 | } 214 | } 215 | if (match){ 216 | //conflict, try to find a new square 217 | continue; 218 | } else { 219 | occupiedSquares.push(newCoords.slice()); 220 | newRobots.push(new robotModel({ 221 | color: robotColors.shift(), 222 | loc: { 223 | row: newCoords[0], 224 | col: newCoords[1] 225 | }, 226 | boxSize: this.get('boxSize'), 227 | boardModel: this 228 | })); 229 | } 230 | } 231 | this.set('robots', new robots(newRobots)); 232 | }, 233 | //Concatenates quadrants in position 1 and 4, with quadrants in positions 2 and 3. 234 | constructBoard: function(boardArray){ 235 | var newBoard = []; 236 | for (var i = 0; i < 8; i++){ 237 | boardArray[0][i] += ',' 238 | newBoard[i] = 239 | boardArray[0][i].concat(boardArray[1][i]).split(","); 240 | 241 | boardArray[3][i] += ',' 242 | newBoard[i+8] = 243 | boardArray[3][i].concat(boardArray[2][i]).split(","); 244 | } 245 | this.set('completeBoard', newBoard); 246 | } 247 | }); 248 | -------------------------------------------------------------------------------- /app/scripts/views/canvasDrawView.js: -------------------------------------------------------------------------------- 1 | window.canvasDrawView = Backbone.View.extend({ 2 | render: function() { 3 | var context = this.getContext(); 4 | var boardProps = this.getWidthAndSize(); 5 | var completeBoard = this.model.get('boardModel').get('completeBoard'); 6 | //start by filling board with background: 7 | var canvas = $('#boardCanvas'); 8 | var width = canvas.attr('width'); 9 | var height = canvas.attr('height'); 10 | 11 | var colorHash = this.model.get('colorHex'); 12 | context.fillStyle = colorHash['background']; 13 | context.rect(0,0, width, height); 14 | context.fill(); 15 | this.model.set('boxSize', boardProps.bsize); 16 | /** 17 | * Render the canvas first, which is a 16x16 grid of grey lines. 18 | * drawBoardProps on top of the canvas: 19 | * walls are thicker black lines 20 | * shapes inside the squares 21 | */ 22 | this.canvasRender(context, boardProps.bw, boardProps.bsize); 23 | this.drawBoardProps(context, boardProps.bw, boardProps.bsize, completeBoard); 24 | }, 25 | getContext: function(){ 26 | var canvas = document.getElementById('boardCanvas'); 27 | var context = canvas.getContext("2d"); 28 | return context; 29 | }, 30 | //gets Width and Size out of the model...much of this could be refactored to follow better practices, assigning properties directly to the view, and using Backbone's event system to update. 31 | getWidthAndSize: function(){ 32 | var bw = this.model.get('boardModel').get('boardWidth'); 33 | var bh = bw; 34 | var boxSize = (bw-5)/16; 35 | return { 36 | bsize: boxSize, 37 | bw: bw 38 | }; 39 | }, 40 | canvasRender: function(context, boardWidth, boxSize){ 41 | function drawBoard(){ 42 | var p = 2; 43 | for (var x = 0; x < boardWidth; x += boxSize) { 44 | context.moveTo(x + p, p); 45 | context.lineTo(x + p, boardWidth + p-5); 46 | } 47 | for (var x = 0; x < boardWidth; x += boxSize) { 48 | context.moveTo(p, x + p); 49 | context.lineTo(boardWidth + p-5, x + p); 50 | } 51 | context.strokeStyle = "grey"; 52 | context.stroke(); 53 | } 54 | drawBoard(); 55 | }, 56 | drawBoardProps: function(context, boardWidth, boxSize, completeBoard){ 57 | context.beginPath(); 58 | var p = 2; 59 | function drawBoardProps(viewCtx){ 60 | for (var row = 0; row < 16; row++){ 61 | for(var col = 0; col < 16; col++){ 62 | var x = p+(col*boxSize); 63 | var y = p+(row*boxSize); 64 | var squareProps = completeBoard[row][col]; 65 | 66 | //Draw shapes based on the propertyString. 67 | viewCtx.drawWalls(context, boxSize, x, y, squareProps); 68 | var colorIndex = viewCtx.indexOfColorOrShape(squareProps, "RGBY"); 69 | if (colorIndex !== -1){ 70 | var color = squareProps[colorIndex]; 71 | var shape = squareProps[viewCtx.indexOfColorOrShape(squareProps, "CTQH")]; 72 | 73 | //add the tokens to scoreModel, so a game can start. 74 | var newToken = { 75 | color : color, 76 | shape : shape, 77 | loc : { 78 | row : row, 79 | col : col 80 | } 81 | } 82 | rootModel.get('scoreModel').addToken(newToken); 83 | viewCtx.drawShape(context, boxSize, x, y, color, shape); 84 | } 85 | } 86 | } 87 | } 88 | function drawBackBoardProps(viewCtx){ 89 | for (var row = 0; row < 16; row++){ 90 | for(var col = 0; col < 16; col++){ 91 | var x = p+(col*boxSize); 92 | var y = p+(row*boxSize); 93 | var squareProps = completeBoard[row][col]; 94 | viewCtx.drawBackgroundX(context, boxSize, x, y); 95 | } 96 | } 97 | } 98 | var ctx = this 99 | drawBackBoardProps(ctx); 100 | drawBoardProps(ctx); 101 | }, 102 | indexOfColorOrShape: function(propString, searchString){ 103 | for(var i =0 ; i < searchString.length; i++){ 104 | var index = propString.indexOf(searchString[i]); 105 | if (index !== -1){ 106 | return index; 107 | } 108 | } 109 | return -1; 110 | }, 111 | /** 112 | * This function draws the background graphic on every tile of the board. 113 | * Note that it does this before any other drawings is done, so that the symbol stays underneath token symbols and robots. 114 | */ 115 | drawBackgroundX: function(context, boxSize, x, y){ 116 | var colorHex = this.model.get('colorHex'); 117 | /** 118 | * 8 grey dots 119 | * grey border square 120 | * darkgrey minisquare 121 | * yellow stripes moving diag..REALLY close to offwhite color, lil'yellow 122 | * 123 | * offwhite center sq...as huge border to block off missmatch from lines 124 | */ 125 | context.beginPath(); 126 | context.moveTo(x+boxSize*.2, y+boxSize*.2); 127 | context.lineTo(x+boxSize*.2, y+boxSize*.8); 128 | context.lineTo(x+boxSize*.8, y+boxSize*.8); 129 | context.lineTo(x+boxSize*.8, y+boxSize*.2); 130 | context.closePath(); 131 | context.lineWidth = .25; 132 | context.strokeStyle = '#ABADA0'; 133 | context.stroke(); 134 | //moveTo 135 | //draw a tiny circle at 8 points 136 | context.lineWidth = .5; 137 | context.strokeStyle = '#ABADA0'; 138 | 139 | context.beginPath(); 140 | context.arc(x+boxSize*.16, y+boxSize*.16, boxSize*.015, 0, 2*Math.PI,false) 141 | context.stroke(); 142 | 143 | context.beginPath(); 144 | context.arc(x+boxSize*.16, y+boxSize*.84, boxSize*.015, 0, 2*Math.PI,false) 145 | context.stroke(); 146 | 147 | context.beginPath(); 148 | context.arc(x+boxSize*.84, y+boxSize*.84, boxSize*.015, 0, 2*Math.PI,false) 149 | context.stroke(); 150 | 151 | context.beginPath(); 152 | context.arc(x+boxSize*.84, y+boxSize*.16, boxSize*.015, 0, 2*Math.PI,false) 153 | context.stroke(); 154 | 155 | context.beginPath(); 156 | context.arc(x+boxSize*.16, y+boxSize*.50, boxSize*.015, 0, 2*Math.PI,false) 157 | context.stroke(); 158 | 159 | context.beginPath(); 160 | context.arc(x+boxSize*.50, y+boxSize*.16, boxSize*.015, 0, 2*Math.PI,false) 161 | context.stroke(); 162 | 163 | context.beginPath(); 164 | context.arc(x+boxSize*.50, y+boxSize*.84, boxSize*.015, 0, 2*Math.PI,false) 165 | context.stroke(); 166 | 167 | context.beginPath(); 168 | context.arc(x+boxSize*.84, y+boxSize*.50, boxSize*.015, 0, 2*Math.PI,false) 169 | context.stroke(); 170 | //dark grey border 171 | //4 thick diagonal dark grey strokes 172 | //thick yellow-orange stroke: 173 | context.beginPath(); 174 | context.strokeStyle = colorHex['xSquareY']; 175 | context.lineWidth = boxSize*.23; 176 | context.moveTo(x+boxSize*.33, y+boxSize*.33); 177 | context.lineTo(x+boxSize*.75, y+boxSize*.75); 178 | context.stroke(); 179 | 180 | //thick diag dark grey strokes 181 | context.beginPath(); 182 | context.strokeStyle = colorHex['xSquareG']; 183 | context.lineWidth = boxSize*.09; 184 | context.moveTo(x+boxSize*.33, y+boxSize*.33); 185 | context.lineTo(x+boxSize*.75, y+boxSize*.75); 186 | context.stroke(); 187 | 188 | context.beginPath(); 189 | context.strokeStyle = colorHex['xSquareG']; 190 | context.lineWidth = boxSize*.08; 191 | 192 | context.moveTo(x+boxSize*.25, y+boxSize*.47); 193 | context.lineTo(x+boxSize*.50, y+boxSize*.73); 194 | context.stroke(); 195 | 196 | context.moveTo(x+boxSize*(1-.25), y+boxSize*(1-.47)); 197 | context.lineTo(x+boxSize*(1-.50), y+boxSize*(1-.73)); 198 | context.stroke(); 199 | 200 | context.beginPath(); 201 | context.strokeStyle = colorHex['xSquareOverlay'] 202 | context.lineWidth = boxSize*.13; 203 | context.moveTo(x+boxSize*.28, y+boxSize*.28); 204 | context.lineTo(x+boxSize*.28, y+boxSize*.72); 205 | context.lineTo(x+boxSize*.72, y+boxSize*.72); 206 | context.lineTo(x+boxSize*.72, y+boxSize*.28); 207 | context.closePath(); 208 | context.stroke(); 209 | }, 210 | drawWalls: function(context, boxSize, x, y, propString){ 211 | if (propString.indexOf("N") !== -1){ 212 | this.drawOneWall(context, boxSize, x, y, "N"); 213 | } 214 | if (propString.indexOf("W") !== -1){ 215 | this.drawOneWall(context, boxSize, x, y, "W"); 216 | } 217 | if (propString.indexOf("S") !== -1){ 218 | this.drawOneWall(context, boxSize, x, y, "S"); 219 | } 220 | if (propString.indexOf("E") !== -1){ 221 | this.drawOneWall(context, boxSize, x, y, "E"); 222 | } 223 | }, 224 | /** 225 | * [drawOneWall description] 226 | * @param {[]} context [canvas context] 227 | * @param {[]} boxSize [size in pixels of the box] 228 | * @param {[]} x [x coordinate on the board] 229 | * @param {[]} y [y coordinate on the board] 230 | * @param {[]} dir [cardinal direction the wall will be drawn on] 231 | * @return {[]} [none] 232 | */ 233 | drawOneWall: function(context, boxSize, x, y, dir){ 234 | context.moveTo(x,y); 235 | context.beginPath(); 236 | //ld, lineDetail hash to draw lines relative to passed-in x and y 237 | var ld = { 238 | N:{ 239 | start:{x: 0, y:0}, 240 | end:{x: boxSize, y: 0} 241 | }, 242 | S:{ 243 | start:{x:boxSize, y:boxSize}, 244 | end:{x:0, y:boxSize} 245 | }, 246 | W:{ 247 | start:{x: 0, y:0}, 248 | end:{x:0, y:boxSize} 249 | }, 250 | E:{ 251 | start:{x: boxSize, y: 0}, 252 | end:{x: boxSize, y: boxSize} 253 | } 254 | }; 255 | context.moveTo(x+ld[dir].start.x,y+ld[dir].start.y) 256 | context.lineWidth = 5; 257 | 258 | context.lineTo(x+ld[dir].end.x,y+ld[dir].end.y); 259 | context.strokeStyle = "#66665D"; 260 | context.stroke(); 261 | }, 262 | //There are two default runtimes/tasks for this function. 263 | //One involves drawing a specified shape, at the given location in the board. This draws token targets. 264 | //The second task, when center is true, modifies the inputs so that a scaled, larger image is drawn in the center. 265 | drawShape: function(context, boxSize, x, y, color, shape, center){ 266 | if (center){ 267 | var x = 7*boxSize; 268 | var y = 7*boxSize; 269 | boxSize *= 2; 270 | var p = 0.97 271 | var r = [boxSize*p, boxSize*(1-p)]; 272 | //clear the area where the new shape should go. 273 | context.beginPath(); 274 | context.clearRect(x+r[1], y+r[1], r[0], r[0]); 275 | context.fillStyle = '#51514B'; 276 | context.fillRect(x+r[1], y+r[1], r[0], r[0]); 277 | } 278 | var colorHex = this.model.get('colorHex'); 279 | 280 | //Draw a square of the right color. 281 | if (shape === "Q"){ 282 | context.fillStyle = colorHex[color]; 283 | context.beginPath(); 284 | context.moveTo(x+boxSize*.25, y+boxSize*.25); 285 | context.lineTo(x+boxSize*.75, y+boxSize*.25); 286 | context.lineTo(x+boxSize*.75, y+boxSize*.75); 287 | context.lineTo(x+boxSize*.25, y+boxSize*.75); 288 | context.closePath(); 289 | context.fill(); 290 | context.lineWidth = 3; 291 | context.strokeStyle = colorHex['silverBorder']; 292 | context.stroke(); 293 | 294 | context.fillStyle = colorHex['silverBorder']; 295 | context.beginPath(); 296 | context.arc( 297 | x+boxSize*.5, 298 | y+boxSize*.5, 299 | boxSize*.14, 300 | 0, 2*Math.PI, 301 | false 302 | ); 303 | context.fill(); 304 | 305 | context.beginPath(); 306 | 307 | for (var i = 0 * Math.PI; i < 2 * Math.PI; i += 0.01 ) { 308 | xPos = x+boxSize*.51 - (boxSize*.04 * Math.sin(i)) * Math.sin(0.76 * Math.PI) + (boxSize*.31 * Math.cos(i)) * Math.cos(0.76 * Math.PI); 309 | yPos = y+boxSize*.51 + (boxSize*.31 * Math.cos(i)) * Math.sin(0.76 * Math.PI) + (boxSize*.04 * Math.sin(i)) * Math.cos(0.76 * Math.PI); 310 | 311 | if (i == 0) { 312 | context.moveTo(xPos, yPos); 313 | } else { 314 | context.lineTo(xPos, yPos); 315 | } 316 | } 317 | context.lineWidth = 2; 318 | context.strokeStyle = colorHex['silverBorder']; 319 | context.stroke(); 320 | } else if (shape === "C"){ 321 | //Draw a circle of the right color. 322 | context.fillStyle = colorHex[color]; 323 | context.beginPath(); 324 | context.arc( 325 | x+boxSize*.5, 326 | y+boxSize*.5, 327 | boxSize*.25, 328 | 0, 2*Math.PI, 329 | false 330 | ) 331 | context.fill(); 332 | context.lineWidth = 3; 333 | context.strokeStyle = colorHex['silverBorder']; 334 | context.stroke(); 335 | context.beginPath(); 336 | var u = 15; 337 | var ap ={//arc points 338 | a1 : 0.37*Math.PI, 339 | a2 : 1.25*Math.PI 340 | } 341 | //See previous commits. 342 | //gave bezierCurves an honest shot; would prefer to draw a crescent. Going for half-circles instead. 343 | context.arc(x+boxSize*.5, y+boxSize*.5, boxSize*.16, ap.a1, ap.a2); 344 | context.lineWidth = 0.08*boxSize; 345 | context.strokeStyle = colorHex['silverBorder'] 346 | context.stroke(); 347 | context.fillStyle = colorHex['silverBorder']; 348 | context.fill() 349 | } else if (shape === "T"){ 350 | context.fillStyle = colorHex[color]; 351 | context.beginPath(); 352 | context.moveTo(x+boxSize*.25, y+boxSize*.75); 353 | context.lineTo(x+boxSize*.75, y+boxSize*.75); 354 | context.lineTo(x+boxSize*.5, y+boxSize*.25); 355 | context.closePath(); 356 | context.strokeStyle = colorHex['silverBorder']; 357 | context.lineWidth = 0.11 * boxSize; 358 | context.stroke(); 359 | context.fill(); 360 | 361 | context.strokeStyle = colorHex['silverBorder']; 362 | context.beginPath(); 363 | context.arc( 364 | x+boxSize*.5, 365 | y+boxSize*.59, 366 | boxSize*.14, 367 | 0, 2*Math.PI, 368 | false 369 | ) 370 | context.lineWidth = boxSize*0.05; 371 | context.stroke(); 372 | } else if (shape === "H"){ 373 | //Draw hexagonal shape of the right color. 374 | context.fillStyle = colorHex[color]; 375 | context.beginPath(); 376 | context.moveTo(x+boxSize*.2, y+boxSize*.7); //bottom left 377 | context.lineTo(x+boxSize*.2, y+boxSize*.3); //top left 378 | context.lineTo(x+boxSize*.5, y+boxSize*.15); //top top 379 | context.lineTo(x+boxSize*.8, y+boxSize*.3); //top right 380 | context.lineTo(x+boxSize*.8, y+boxSize*.7); //bottom right 381 | context.lineTo(x+boxSize*.5, y+boxSize*.85); //bottom bottom 382 | context.closePath(); 383 | context.fill(); 384 | context.strokeStyle = colorHex['silverBorder']; 385 | context.lineWidth = boxSize*.06; 386 | context.stroke(); 387 | 388 | var cp = {//centerpoint 389 | x: x+boxSize*.5, 390 | y: y+boxSize*.5,//x and y coords 391 | r1: boxSize*.06,//range 1 392 | r2: boxSize*.4,//range 2 393 | } 394 | context.fillStyle = colorHex['silverBorder']; 395 | context.strokeStyle = colorHex['silverBorder']; 396 | context.lineWidth = boxSize*.02; 397 | context.beginPath(); 398 | context.moveTo(cp.x-cp.r1, cp.y-cp.r1); //r1 top left 399 | context.lineTo(cp.x, cp.y-cp.r2); // toptoptop 400 | context.lineTo(cp.x+cp.r1, cp.y-cp.r1); // r1 top right 401 | context.lineTo(cp.x+cp.r2, cp.y); // right right 402 | context.lineTo(cp.x+cp.r1, cp.y+cp.r1); // r1 bot right 403 | context.lineTo(cp.x, cp.y+cp.r2); // botbotbot 404 | context.lineTo(cp.x-cp.r1, cp.y+cp.r1); // r1 bot left 405 | context.lineTo(cp.x-cp.r2, cp.y); // left left 406 | context.closePath(); 407 | context.fill() 408 | } 409 | } 410 | }); -------------------------------------------------------------------------------- /STYLE-GUIDE.md: -------------------------------------------------------------------------------- 1 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/airbnb/javascript?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 2 | 3 | # Airbnb JavaScript Style Guide() { 4 | 5 | *A mostly reasonable approach to JavaScript* 6 | 7 | 8 | ## Table of Contents 9 | 10 | 1. [Types](#types) 11 | 1. [Objects](#objects) 12 | 1. [Arrays](#arrays) 13 | 1. [Strings](#strings) 14 | 1. [Functions](#functions) 15 | 1. [Properties](#properties) 16 | 1. [Variables](#variables) 17 | 1. [Hoisting](#hoisting) 18 | 1. [Conditional Expressions & Equality](#conditional-expressions--equality) 19 | 1. [Blocks](#blocks) 20 | 1. [Comments](#comments) 21 | 1. [Whitespace](#whitespace) 22 | 1. [Commas](#commas) 23 | 1. [Semicolons](#semicolons) 24 | 1. [Type Casting & Coercion](#type-casting--coercion) 25 | 1. [Naming Conventions](#naming-conventions) 26 | 1. [Accessors](#accessors) 27 | 1. [Constructors](#constructors) 28 | 1. [Events](#events) 29 | 1. [Modules](#modules) 30 | 1. [jQuery](#jquery) 31 | 1. [ECMAScript 5 Compatibility](#ecmascript-5-compatibility) 32 | 1. [Testing](#testing) 33 | 1. [Performance](#performance) 34 | 1. [Resources](#resources) 35 | 1. [In the Wild](#in-the-wild) 36 | 1. [Translation](#translation) 37 | 1. [The JavaScript Style Guide Guide](#the-javascript-style-guide-guide) 38 | 1. [Chat With Us About Javascript](#chat-with-us-about-javascript) 39 | 1. [Contributors](#contributors) 40 | 1. [License](#license) 41 | 42 | ## Types 43 | 44 | - **Primitives**: When you access a primitive type you work directly on its value 45 | 46 | + `string` 47 | + `number` 48 | + `boolean` 49 | + `null` 50 | + `undefined` 51 | 52 | ```javascript 53 | var foo = 1; 54 | var bar = foo; 55 | 56 | bar = 9; 57 | 58 | console.log(foo, bar); // => 1, 9 59 | ``` 60 | - **Complex**: When you access a complex type you work on a reference to its value 61 | 62 | + `object` 63 | + `array` 64 | + `function` 65 | 66 | ```javascript 67 | var foo = [1, 2]; 68 | var bar = foo; 69 | 70 | bar[0] = 9; 71 | 72 | console.log(foo[0], bar[0]); // => 9, 9 73 | ``` 74 | 75 | **[⬆ back to top](#table-of-contents)** 76 | 77 | ## Objects 78 | 79 | - Use the literal syntax for object creation. 80 | 81 | ```javascript 82 | // bad 83 | var item = new Object(); 84 | 85 | // good 86 | var item = {}; 87 | ``` 88 | 89 | - Don't use [reserved words](http://es5.github.io/#x7.6.1) as keys. It won't work in IE8. [More info](https://github.com/airbnb/javascript/issues/61) 90 | 91 | ```javascript 92 | // bad 93 | var superman = { 94 | default: { clark: 'kent' }, 95 | private: true 96 | }; 97 | 98 | // good 99 | var superman = { 100 | defaults: { clark: 'kent' }, 101 | hidden: true 102 | }; 103 | ``` 104 | 105 | - Use readable synonyms in place of reserved words. 106 | 107 | ```javascript 108 | // bad 109 | var superman = { 110 | class: 'alien' 111 | }; 112 | 113 | // bad 114 | var superman = { 115 | klass: 'alien' 116 | }; 117 | 118 | // good 119 | var superman = { 120 | type: 'alien' 121 | }; 122 | ``` 123 | 124 | **[⬆ back to top](#table-of-contents)** 125 | 126 | ## Arrays 127 | 128 | - Use the literal syntax for array creation 129 | 130 | ```javascript 131 | // bad 132 | var items = new Array(); 133 | 134 | // good 135 | var items = []; 136 | ``` 137 | 138 | - If you don't know array length use Array#push. 139 | 140 | ```javascript 141 | var someStack = []; 142 | 143 | 144 | // bad 145 | someStack[someStack.length] = 'abracadabra'; 146 | 147 | // good 148 | someStack.push('abracadabra'); 149 | ``` 150 | 151 | - When you need to copy an array use Array#slice. [jsPerf](http://jsperf.com/converting-arguments-to-an-array/7) 152 | 153 | ```javascript 154 | var len = items.length; 155 | var itemsCopy = []; 156 | var i; 157 | 158 | // bad 159 | for (i = 0; i < len; i++) { 160 | itemsCopy[i] = items[i]; 161 | } 162 | 163 | // good 164 | itemsCopy = items.slice(); 165 | ``` 166 | 167 | - To convert an array-like object to an array, use Array#slice. 168 | 169 | ```javascript 170 | function trigger() { 171 | var args = Array.prototype.slice.call(arguments); 172 | ... 173 | } 174 | ``` 175 | 176 | **[⬆ back to top](#table-of-contents)** 177 | 178 | 179 | ## Strings 180 | 181 | - Use single quotes `''` for strings 182 | 183 | ```javascript 184 | // bad 185 | var name = "Bob Parr"; 186 | 187 | // good 188 | var name = 'Bob Parr'; 189 | 190 | // bad 191 | var fullName = "Bob " + this.lastName; 192 | 193 | // good 194 | var fullName = 'Bob ' + this.lastName; 195 | ``` 196 | 197 | - Strings longer than 80 characters should be written across multiple lines using string concatenation. 198 | - Note: If overused, long strings with concatenation could impact performance. [jsPerf](http://jsperf.com/ya-string-concat) & [Discussion](https://github.com/airbnb/javascript/issues/40) 199 | 200 | ```javascript 201 | // bad 202 | var errorMessage = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.'; 203 | 204 | // bad 205 | var errorMessage = 'This is a super long error that was thrown because \ 206 | of Batman. When you stop to think about how Batman had anything to do \ 207 | with this, you would get nowhere \ 208 | fast.'; 209 | 210 | // good 211 | var errorMessage = 'This is a super long error that was thrown because ' + 212 | 'of Batman. When you stop to think about how Batman had anything to do ' + 213 | 'with this, you would get nowhere fast.'; 214 | ``` 215 | 216 | - When programmatically building up a string, use Array#join instead of string concatenation. Mostly for IE: [jsPerf](http://jsperf.com/string-vs-array-concat/2). 217 | 218 | ```javascript 219 | var items; 220 | var messages; 221 | var length; 222 | var i; 223 | 224 | messages = [{ 225 | state: 'success', 226 | message: 'This one worked.' 227 | }, { 228 | state: 'success', 229 | message: 'This one worked as well.' 230 | }, { 231 | state: 'error', 232 | message: 'This one did not work.' 233 | }]; 234 | 235 | length = messages.length; 236 | 237 | // bad 238 | function inbox(messages) { 239 | items = ''; 246 | } 247 | 248 | // good 249 | function inbox(messages) { 250 | items = []; 251 | 252 | for (i = 0; i < length; i++) { 253 | items[i] = messages[i].message; 254 | } 255 | 256 | return ''; 257 | } 258 | ``` 259 | 260 | **[⬆ back to top](#table-of-contents)** 261 | 262 | 263 | ## Functions 264 | 265 | - Function expressions: 266 | 267 | ```javascript 268 | // anonymous function expression 269 | var anonymous = function() { 270 | return true; 271 | }; 272 | 273 | // named function expression 274 | var named = function named() { 275 | return true; 276 | }; 277 | 278 | // immediately-invoked function expression (IIFE) 279 | (function() { 280 | console.log('Welcome to the Internet. Please follow me.'); 281 | })(); 282 | ``` 283 | 284 | - Never declare a function in a non-function block (if, while, etc). Assign the function to a variable instead. Browsers will allow you to do it, but they all interpret it differently, which is bad news bears. 285 | - **Note:** ECMA-262 defines a `block` as a list of statements. A function declaration is not a statement. [Read ECMA-262's note on this issue](http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf#page=97). 286 | 287 | ```javascript 288 | // bad 289 | if (currentUser) { 290 | function test() { 291 | console.log('Nope.'); 292 | } 293 | } 294 | 295 | // good 296 | var test; 297 | if (currentUser) { 298 | test = function test() { 299 | console.log('Yup.'); 300 | }; 301 | } 302 | ``` 303 | 304 | - Never name a parameter `arguments`, this will take precedence over the `arguments` object that is given to every function scope. 305 | 306 | ```javascript 307 | // bad 308 | function nope(name, options, arguments) { 309 | // ...stuff... 310 | } 311 | 312 | // good 313 | function yup(name, options, args) { 314 | // ...stuff... 315 | } 316 | ``` 317 | 318 | **[⬆ back to top](#table-of-contents)** 319 | 320 | 321 | 322 | ## Properties 323 | 324 | - Use dot notation when accessing properties. 325 | 326 | ```javascript 327 | var luke = { 328 | jedi: true, 329 | age: 28 330 | }; 331 | 332 | // bad 333 | var isJedi = luke['jedi']; 334 | 335 | // good 336 | var isJedi = luke.jedi; 337 | ``` 338 | 339 | - Use subscript notation `[]` when accessing properties with a variable. 340 | 341 | ```javascript 342 | var luke = { 343 | jedi: true, 344 | age: 28 345 | }; 346 | 347 | function getProp(prop) { 348 | return luke[prop]; 349 | } 350 | 351 | var isJedi = getProp('jedi'); 352 | ``` 353 | 354 | **[⬆ back to top](#table-of-contents)** 355 | 356 | 357 | ## Variables 358 | 359 | - Always use `var` to declare variables. Not doing so will result in global variables. We want to avoid polluting the global namespace. Captain Planet warned us of that. 360 | 361 | ```javascript 362 | // bad 363 | superPower = new SuperPower(); 364 | 365 | // good 366 | var superPower = new SuperPower(); 367 | ``` 368 | 369 | - Use one `var` declaration per variable. 370 | It's easier to add new variable declarations this way, and you never have 371 | to worry about swapping out a `;` for a `,` or introducing punctuation-only 372 | diffs. 373 | 374 | ```javascript 375 | // bad 376 | var items = getItems(), 377 | goSportsTeam = true, 378 | dragonball = 'z'; 379 | 380 | // bad 381 | // (compare to above, and try to spot the mistake) 382 | var items = getItems(), 383 | goSportsTeam = true; 384 | dragonball = 'z'; 385 | 386 | // good 387 | var items = getItems(); 388 | var goSportsTeam = true; 389 | var dragonball = 'z'; 390 | ``` 391 | 392 | - Declare unassigned variables last. This is helpful when later on you might need to assign a variable depending on one of the previous assigned variables. 393 | 394 | ```javascript 395 | // bad 396 | var i, len, dragonball, 397 | items = getItems(), 398 | goSportsTeam = true; 399 | 400 | // bad 401 | var i; 402 | var items = getItems(); 403 | var dragonball; 404 | var goSportsTeam = true; 405 | var len; 406 | 407 | // good 408 | var items = getItems(); 409 | var goSportsTeam = true; 410 | var dragonball; 411 | var length; 412 | var i; 413 | ``` 414 | 415 | - Assign variables at the top of their scope. This helps avoid issues with variable declaration and assignment hoisting related issues. 416 | 417 | ```javascript 418 | // bad 419 | function() { 420 | test(); 421 | console.log('doing stuff..'); 422 | 423 | //..other stuff.. 424 | 425 | var name = getName(); 426 | 427 | if (name === 'test') { 428 | return false; 429 | } 430 | 431 | return name; 432 | } 433 | 434 | // good 435 | function() { 436 | var name = getName(); 437 | 438 | test(); 439 | console.log('doing stuff..'); 440 | 441 | //..other stuff.. 442 | 443 | if (name === 'test') { 444 | return false; 445 | } 446 | 447 | return name; 448 | } 449 | 450 | // bad 451 | function() { 452 | var name = getName(); 453 | 454 | if (!arguments.length) { 455 | return false; 456 | } 457 | 458 | return true; 459 | } 460 | 461 | // good 462 | function() { 463 | if (!arguments.length) { 464 | return false; 465 | } 466 | 467 | var name = getName(); 468 | 469 | return true; 470 | } 471 | ``` 472 | 473 | **[⬆ back to top](#table-of-contents)** 474 | 475 | 476 | ## Hoisting 477 | 478 | - Variable declarations get hoisted to the top of their scope, their assignment does not. 479 | 480 | ```javascript 481 | // we know this wouldn't work (assuming there 482 | // is no notDefined global variable) 483 | function example() { 484 | console.log(notDefined); // => throws a ReferenceError 485 | } 486 | 487 | // creating a variable declaration after you 488 | // reference the variable will work due to 489 | // variable hoisting. Note: the assignment 490 | // value of `true` is not hoisted. 491 | function example() { 492 | console.log(declaredButNotAssigned); // => undefined 493 | var declaredButNotAssigned = true; 494 | } 495 | 496 | // The interpreter is hoisting the variable 497 | // declaration to the top of the scope, 498 | // which means our example could be rewritten as: 499 | function example() { 500 | var declaredButNotAssigned; 501 | console.log(declaredButNotAssigned); // => undefined 502 | declaredButNotAssigned = true; 503 | } 504 | ``` 505 | 506 | - Anonymous function expressions hoist their variable name, but not the function assignment. 507 | 508 | ```javascript 509 | function example() { 510 | console.log(anonymous); // => undefined 511 | 512 | anonymous(); // => TypeError anonymous is not a function 513 | 514 | var anonymous = function() { 515 | console.log('anonymous function expression'); 516 | }; 517 | } 518 | ``` 519 | 520 | - Named function expressions hoist the variable name, not the function name or the function body. 521 | 522 | ```javascript 523 | function example() { 524 | console.log(named); // => undefined 525 | 526 | named(); // => TypeError named is not a function 527 | 528 | superPower(); // => ReferenceError superPower is not defined 529 | 530 | var named = function superPower() { 531 | console.log('Flying'); 532 | }; 533 | } 534 | 535 | // the same is true when the function name 536 | // is the same as the variable name. 537 | function example() { 538 | console.log(named); // => undefined 539 | 540 | named(); // => TypeError named is not a function 541 | 542 | var named = function named() { 543 | console.log('named'); 544 | } 545 | } 546 | ``` 547 | 548 | - Function declarations hoist their name and the function body. 549 | 550 | ```javascript 551 | function example() { 552 | superPower(); // => Flying 553 | 554 | function superPower() { 555 | console.log('Flying'); 556 | } 557 | } 558 | ``` 559 | 560 | - For more information refer to [JavaScript Scoping & Hoisting](http://www.adequatelygood.com/2010/2/JavaScript-Scoping-and-Hoisting) by [Ben Cherry](http://www.adequatelygood.com/) 561 | 562 | **[⬆ back to top](#table-of-contents)** 563 | 564 | 565 | 566 | ## Conditional Expressions & Equality 567 | 568 | - Use `===` and `!==` over `==` and `!=`. 569 | - Conditional expressions are evaluated using coercion with the `ToBoolean` method and always follow these simple rules: 570 | 571 | + **Objects** evaluate to **true** 572 | + **Undefined** evaluates to **false** 573 | + **Null** evaluates to **false** 574 | + **Booleans** evaluate to **the value of the boolean** 575 | + **Numbers** evaluate to **false** if **+0, -0, or NaN**, otherwise **true** 576 | + **Strings** evaluate to **false** if an empty string `''`, otherwise **true** 577 | 578 | ```javascript 579 | if ([0]) { 580 | // true 581 | // An array is an object, objects evaluate to true 582 | } 583 | ``` 584 | 585 | - Use shortcuts. 586 | 587 | ```javascript 588 | // bad 589 | if (name !== '') { 590 | // ...stuff... 591 | } 592 | 593 | // good 594 | if (name) { 595 | // ...stuff... 596 | } 597 | 598 | // bad 599 | if (collection.length > 0) { 600 | // ...stuff... 601 | } 602 | 603 | // good 604 | if (collection.length) { 605 | // ...stuff... 606 | } 607 | ``` 608 | 609 | - For more information see [Truth Equality and JavaScript](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/#more-2108) by Angus Croll 610 | 611 | **[⬆ back to top](#table-of-contents)** 612 | 613 | 614 | ## Blocks 615 | 616 | - Use braces with all multi-line blocks. 617 | 618 | ```javascript 619 | // bad 620 | if (test) 621 | return false; 622 | 623 | // good 624 | if (test) return false; 625 | 626 | // good 627 | if (test) { 628 | return false; 629 | } 630 | 631 | // bad 632 | function() { return false; } 633 | 634 | // good 635 | function() { 636 | return false; 637 | } 638 | ``` 639 | 640 | **[⬆ back to top](#table-of-contents)** 641 | 642 | 643 | ## Comments 644 | 645 | - Use `/** ... */` for multiline comments. Include a description, specify types and values for all parameters and return values. 646 | 647 | ```javascript 648 | // bad 649 | // make() returns a new element 650 | // based on the passed in tag name 651 | // 652 | // @param {String} tag 653 | // @return {Element} element 654 | function make(tag) { 655 | 656 | // ...stuff... 657 | 658 | return element; 659 | } 660 | 661 | // good 662 | /** 663 | * make() returns a new element 664 | * based on the passed in tag name 665 | * 666 | * @param {String} tag 667 | * @return {Element} element 668 | */ 669 | function make(tag) { 670 | 671 | // ...stuff... 672 | 673 | return element; 674 | } 675 | ``` 676 | 677 | - Use `//` for single line comments. Place single line comments on a newline above the subject of the comment. Put an empty line before the comment. 678 | 679 | ```javascript 680 | // bad 681 | var active = true; // is current tab 682 | 683 | // good 684 | // is current tab 685 | var active = true; 686 | 687 | // bad 688 | function getType() { 689 | console.log('fetching type...'); 690 | // set the default type to 'no type' 691 | var type = this._type || 'no type'; 692 | 693 | return type; 694 | } 695 | 696 | // good 697 | function getType() { 698 | console.log('fetching type...'); 699 | 700 | // set the default type to 'no type' 701 | var type = this._type || 'no type'; 702 | 703 | return type; 704 | } 705 | ``` 706 | 707 | - Prefixing your comments with `FIXME` or `TODO` helps other developers quickly understand if you're pointing out a problem that needs to be revisited, or if you're suggesting a solution to the problem that needs to be implemented. These are different than regular comments because they are actionable. The actions are `FIXME -- need to figure this out` or `TODO -- need to implement`. 708 | 709 | - Use `// FIXME:` to annotate problems 710 | 711 | ```javascript 712 | function Calculator() { 713 | 714 | // FIXME: shouldn't use a global here 715 | total = 0; 716 | 717 | return this; 718 | } 719 | ``` 720 | 721 | - Use `// TODO:` to annotate solutions to problems 722 | 723 | ```javascript 724 | function Calculator() { 725 | 726 | // TODO: total should be configurable by an options param 727 | this.total = 0; 728 | 729 | return this; 730 | } 731 | ``` 732 | 733 | **[⬆ back to top](#table-of-contents)** 734 | 735 | 736 | ## Whitespace 737 | 738 | - Use soft tabs set to 2 spaces 739 | 740 | ```javascript 741 | // bad 742 | function() { 743 | ∙∙∙∙var name; 744 | } 745 | 746 | // bad 747 | function() { 748 | ∙var name; 749 | } 750 | 751 | // good 752 | function() { 753 | ∙∙var name; 754 | } 755 | ``` 756 | 757 | - Place 1 space before the leading brace. 758 | 759 | ```javascript 760 | // bad 761 | function test(){ 762 | console.log('test'); 763 | } 764 | 765 | // good 766 | function test() { 767 | console.log('test'); 768 | } 769 | 770 | // bad 771 | dog.set('attr',{ 772 | age: '1 year', 773 | breed: 'Bernese Mountain Dog' 774 | }); 775 | 776 | // good 777 | dog.set('attr', { 778 | age: '1 year', 779 | breed: 'Bernese Mountain Dog' 780 | }); 781 | ``` 782 | 783 | - Set off operators with spaces. 784 | 785 | ```javascript 786 | // bad 787 | var x=y+5; 788 | 789 | // good 790 | var x = y + 5; 791 | ``` 792 | 793 | - End files with a single newline character. 794 | 795 | ```javascript 796 | // bad 797 | (function(global) { 798 | // ...stuff... 799 | })(this); 800 | ``` 801 | 802 | ```javascript 803 | // bad 804 | (function(global) { 805 | // ...stuff... 806 | })(this);↵ 807 | ↵ 808 | ``` 809 | 810 | ```javascript 811 | // good 812 | (function(global) { 813 | // ...stuff... 814 | })(this);↵ 815 | ``` 816 | 817 | - Use indentation when making long method chains. 818 | 819 | ```javascript 820 | // bad 821 | $('#items').find('.selected').highlight().end().find('.open').updateCount(); 822 | 823 | // good 824 | $('#items') 825 | .find('.selected') 826 | .highlight() 827 | .end() 828 | .find('.open') 829 | .updateCount(); 830 | 831 | // bad 832 | var leds = stage.selectAll('.led').data(data).enter().append('svg:svg').class('led', true) 833 | .attr('width', (radius + margin) * 2).append('svg:g') 834 | .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')') 835 | .call(tron.led); 836 | 837 | // good 838 | var leds = stage.selectAll('.led') 839 | .data(data) 840 | .enter().append('svg:svg') 841 | .class('led', true) 842 | .attr('width', (radius + margin) * 2) 843 | .append('svg:g') 844 | .attr('transform', 'translate(' + (radius + margin) + ',' + (radius + margin) + ')') 845 | .call(tron.led); 846 | ``` 847 | 848 | **[⬆ back to top](#table-of-contents)** 849 | 850 | ## Commas 851 | 852 | - Leading commas: **Nope.** 853 | 854 | ```javascript 855 | // bad 856 | var story = [ 857 | once 858 | , upon 859 | , aTime 860 | ]; 861 | 862 | // good 863 | var story = [ 864 | once, 865 | upon, 866 | aTime 867 | ]; 868 | 869 | // bad 870 | var hero = { 871 | firstName: 'Bob' 872 | , lastName: 'Parr' 873 | , heroName: 'Mr. Incredible' 874 | , superPower: 'strength' 875 | }; 876 | 877 | // good 878 | var hero = { 879 | firstName: 'Bob', 880 | lastName: 'Parr', 881 | heroName: 'Mr. Incredible', 882 | superPower: 'strength' 883 | }; 884 | ``` 885 | 886 | - Additional trailing comma: **Nope.** This can cause problems with IE6/7 and IE9 if it's in quirksmode. Also, in some implementations of ES3 would add length to an array if it had an additional trailing comma. This was clarified in ES5 ([source](http://es5.github.io/#D)): 887 | 888 | > Edition 5 clarifies the fact that a trailing comma at the end of an ArrayInitialiser does not add to the length of the array. This is not a semantic change from Edition 3 but some implementations may have previously misinterpreted this. 889 | 890 | ```javascript 891 | // bad 892 | var hero = { 893 | firstName: 'Kevin', 894 | lastName: 'Flynn', 895 | }; 896 | 897 | var heroes = [ 898 | 'Batman', 899 | 'Superman', 900 | ]; 901 | 902 | // good 903 | var hero = { 904 | firstName: 'Kevin', 905 | lastName: 'Flynn' 906 | }; 907 | 908 | var heroes = [ 909 | 'Batman', 910 | 'Superman' 911 | ]; 912 | ``` 913 | 914 | **[⬆ back to top](#table-of-contents)** 915 | 916 | 917 | ## Semicolons 918 | 919 | - **Yup.** 920 | 921 | ```javascript 922 | // bad 923 | (function() { 924 | var name = 'Skywalker' 925 | return name 926 | })() 927 | 928 | // good 929 | (function() { 930 | var name = 'Skywalker'; 931 | return name; 932 | })(); 933 | 934 | // good (guards against the function becoming an argument when two files with IIFEs are concatenated) 935 | ;(function() { 936 | var name = 'Skywalker'; 937 | return name; 938 | })(); 939 | ``` 940 | 941 | [Read more](http://stackoverflow.com/a/7365214/1712802). 942 | 943 | **[⬆ back to top](#table-of-contents)** 944 | 945 | 946 | ## Type Casting & Coercion 947 | 948 | - Perform type coercion at the beginning of the statement. 949 | - Strings: 950 | 951 | ```javascript 952 | // => this.reviewScore = 9; 953 | 954 | // bad 955 | var totalScore = this.reviewScore + ''; 956 | 957 | // good 958 | var totalScore = '' + this.reviewScore; 959 | 960 | // bad 961 | var totalScore = '' + this.reviewScore + ' total score'; 962 | 963 | // good 964 | var totalScore = this.reviewScore + ' total score'; 965 | ``` 966 | 967 | - Use `parseInt` for Numbers and always with a radix for type casting. 968 | 969 | ```javascript 970 | var inputValue = '4'; 971 | 972 | // bad 973 | var val = new Number(inputValue); 974 | 975 | // bad 976 | var val = +inputValue; 977 | 978 | // bad 979 | var val = inputValue >> 0; 980 | 981 | // bad 982 | var val = parseInt(inputValue); 983 | 984 | // good 985 | var val = Number(inputValue); 986 | 987 | // good 988 | var val = parseInt(inputValue, 10); 989 | ``` 990 | 991 | - If for whatever reason you are doing something wild and `parseInt` is your bottleneck and need to use Bitshift for [performance reasons](http://jsperf.com/coercion-vs-casting/3), leave a comment explaining why and what you're doing. 992 | 993 | ```javascript 994 | // good 995 | /** 996 | * parseInt was the reason my code was slow. 997 | * Bitshifting the String to coerce it to a 998 | * Number made it a lot faster. 999 | */ 1000 | var val = inputValue >> 0; 1001 | ``` 1002 | 1003 | - **Note:** Be careful when using bitshift operations. Numbers are represented as [64-bit values](http://es5.github.io/#x4.3.19), but Bitshift operations always return a 32-bit integer ([source](http://es5.github.io/#x11.7)). Bitshift can lead to unexpected behavior for integer values larger than 32 bits. [Discussion](https://github.com/airbnb/javascript/issues/109). Largest signed 32-bit Int is 2,147,483,647: 1004 | 1005 | ```javascript 1006 | 2147483647 >> 0 //=> 2147483647 1007 | 2147483648 >> 0 //=> -2147483648 1008 | 2147483649 >> 0 //=> -2147483647 1009 | ``` 1010 | 1011 | - Booleans: 1012 | 1013 | ```javascript 1014 | var age = 0; 1015 | 1016 | // bad 1017 | var hasAge = new Boolean(age); 1018 | 1019 | // good 1020 | var hasAge = Boolean(age); 1021 | 1022 | // good 1023 | var hasAge = !!age; 1024 | ``` 1025 | 1026 | **[⬆ back to top](#table-of-contents)** 1027 | 1028 | 1029 | ## Naming Conventions 1030 | 1031 | - Avoid single letter names. Be descriptive with your naming. 1032 | 1033 | ```javascript 1034 | // bad 1035 | function q() { 1036 | // ...stuff... 1037 | } 1038 | 1039 | // good 1040 | function query() { 1041 | // ..stuff.. 1042 | } 1043 | ``` 1044 | 1045 | - Use camelCase when naming objects, functions, and instances 1046 | 1047 | ```javascript 1048 | // bad 1049 | var OBJEcttsssss = {}; 1050 | var this_is_my_object = {}; 1051 | function c() {} 1052 | var u = new user({ 1053 | name: 'Bob Parr' 1054 | }); 1055 | 1056 | // good 1057 | var thisIsMyObject = {}; 1058 | function thisIsMyFunction() {} 1059 | var user = new User({ 1060 | name: 'Bob Parr' 1061 | }); 1062 | ``` 1063 | 1064 | - Use PascalCase when naming constructors or classes 1065 | 1066 | ```javascript 1067 | // bad 1068 | function user(options) { 1069 | this.name = options.name; 1070 | } 1071 | 1072 | var bad = new user({ 1073 | name: 'nope' 1074 | }); 1075 | 1076 | // good 1077 | function User(options) { 1078 | this.name = options.name; 1079 | } 1080 | 1081 | var good = new User({ 1082 | name: 'yup' 1083 | }); 1084 | ``` 1085 | 1086 | - Use a leading underscore `_` when naming private properties 1087 | 1088 | ```javascript 1089 | // bad 1090 | this.__firstName__ = 'Panda'; 1091 | this.firstName_ = 'Panda'; 1092 | 1093 | // good 1094 | this._firstName = 'Panda'; 1095 | ``` 1096 | 1097 | - When saving a reference to `this` use `_this`. 1098 | 1099 | ```javascript 1100 | // bad 1101 | function() { 1102 | var self = this; 1103 | return function() { 1104 | console.log(self); 1105 | }; 1106 | } 1107 | 1108 | // bad 1109 | function() { 1110 | var that = this; 1111 | return function() { 1112 | console.log(that); 1113 | }; 1114 | } 1115 | 1116 | // good 1117 | function() { 1118 | var _this = this; 1119 | return function() { 1120 | console.log(_this); 1121 | }; 1122 | } 1123 | ``` 1124 | 1125 | - Name your functions. This is helpful for stack traces. 1126 | 1127 | ```javascript 1128 | // bad 1129 | var log = function(msg) { 1130 | console.log(msg); 1131 | }; 1132 | 1133 | // good 1134 | var log = function log(msg) { 1135 | console.log(msg); 1136 | }; 1137 | ``` 1138 | 1139 | - **Note:** IE8 and below exhibit some quirks with named function expressions. See [http://kangax.github.io/nfe/](http://kangax.github.io/nfe/) for more info. 1140 | 1141 | **[⬆ back to top](#table-of-contents)** 1142 | 1143 | 1144 | ## Accessors 1145 | 1146 | - Accessor functions for properties are not required 1147 | - If you do make accessor functions use getVal() and setVal('hello') 1148 | 1149 | ```javascript 1150 | // bad 1151 | dragon.age(); 1152 | 1153 | // good 1154 | dragon.getAge(); 1155 | 1156 | // bad 1157 | dragon.age(25); 1158 | 1159 | // good 1160 | dragon.setAge(25); 1161 | ``` 1162 | 1163 | - If the property is a boolean, use isVal() or hasVal() 1164 | 1165 | ```javascript 1166 | // bad 1167 | if (!dragon.age()) { 1168 | return false; 1169 | } 1170 | 1171 | // good 1172 | if (!dragon.hasAge()) { 1173 | return false; 1174 | } 1175 | ``` 1176 | 1177 | - It's okay to create get() and set() functions, but be consistent. 1178 | 1179 | ```javascript 1180 | function Jedi(options) { 1181 | options || (options = {}); 1182 | var lightsaber = options.lightsaber || 'blue'; 1183 | this.set('lightsaber', lightsaber); 1184 | } 1185 | 1186 | Jedi.prototype.set = function(key, val) { 1187 | this[key] = val; 1188 | }; 1189 | 1190 | Jedi.prototype.get = function(key) { 1191 | return this[key]; 1192 | }; 1193 | ``` 1194 | 1195 | **[⬆ back to top](#table-of-contents)** 1196 | 1197 | 1198 | ## Constructors 1199 | 1200 | - Assign methods to the prototype object, instead of overwriting the prototype with a new object. Overwriting the prototype makes inheritance impossible: by resetting the prototype you'll overwrite the base! 1201 | 1202 | ```javascript 1203 | function Jedi() { 1204 | console.log('new jedi'); 1205 | } 1206 | 1207 | // bad 1208 | Jedi.prototype = { 1209 | fight: function fight() { 1210 | console.log('fighting'); 1211 | }, 1212 | 1213 | block: function block() { 1214 | console.log('blocking'); 1215 | } 1216 | }; 1217 | 1218 | // good 1219 | Jedi.prototype.fight = function fight() { 1220 | console.log('fighting'); 1221 | }; 1222 | 1223 | Jedi.prototype.block = function block() { 1224 | console.log('blocking'); 1225 | }; 1226 | ``` 1227 | 1228 | - Methods can return `this` to help with method chaining. 1229 | 1230 | ```javascript 1231 | // bad 1232 | Jedi.prototype.jump = function() { 1233 | this.jumping = true; 1234 | return true; 1235 | }; 1236 | 1237 | Jedi.prototype.setHeight = function(height) { 1238 | this.height = height; 1239 | }; 1240 | 1241 | var luke = new Jedi(); 1242 | luke.jump(); // => true 1243 | luke.setHeight(20); // => undefined 1244 | 1245 | // good 1246 | Jedi.prototype.jump = function() { 1247 | this.jumping = true; 1248 | return this; 1249 | }; 1250 | 1251 | Jedi.prototype.setHeight = function(height) { 1252 | this.height = height; 1253 | return this; 1254 | }; 1255 | 1256 | var luke = new Jedi(); 1257 | 1258 | luke.jump() 1259 | .setHeight(20); 1260 | ``` 1261 | 1262 | 1263 | - It's okay to write a custom toString() method, just make sure it works successfully and causes no side effects. 1264 | 1265 | ```javascript 1266 | function Jedi(options) { 1267 | options || (options = {}); 1268 | this.name = options.name || 'no name'; 1269 | } 1270 | 1271 | Jedi.prototype.getName = function getName() { 1272 | return this.name; 1273 | }; 1274 | 1275 | Jedi.prototype.toString = function toString() { 1276 | return 'Jedi - ' + this.getName(); 1277 | }; 1278 | ``` 1279 | 1280 | **[⬆ back to top](#table-of-contents)** 1281 | 1282 | 1283 | ## Events 1284 | 1285 | - When attaching data payloads to events (whether DOM events or something more proprietary like Backbone events), pass a hash instead of a raw value. This allows a subsequent contributor to add more data to the event payload without finding and updating every handler for the event. For example, instead of: 1286 | 1287 | ```js 1288 | // bad 1289 | $(this).trigger('listingUpdated', listing.id); 1290 | 1291 | ... 1292 | 1293 | $(this).on('listingUpdated', function(e, listingId) { 1294 | // do something with listingId 1295 | }); 1296 | ``` 1297 | 1298 | prefer: 1299 | 1300 | ```js 1301 | // good 1302 | $(this).trigger('listingUpdated', { listingId : listing.id }); 1303 | 1304 | ... 1305 | 1306 | $(this).on('listingUpdated', function(e, data) { 1307 | // do something with data.listingId 1308 | }); 1309 | ``` 1310 | 1311 | **[⬆ back to top](#table-of-contents)** 1312 | 1313 | 1314 | ## Modules 1315 | 1316 | - The module should start with a `!`. This ensures that if a malformed module forgets to include a final semicolon there aren't errors in production when the scripts get concatenated. [Explanation](https://github.com/airbnb/javascript/issues/44#issuecomment-13063933) 1317 | - The file should be named with camelCase, live in a folder with the same name, and match the name of the single export. 1318 | - Add a method called `noConflict()` that sets the exported module to the previous version and returns this one. 1319 | - Always declare `'use strict';` at the top of the module. 1320 | 1321 | ```javascript 1322 | // fancyInput/fancyInput.js 1323 | 1324 | !function(global) { 1325 | 'use strict'; 1326 | 1327 | var previousFancyInput = global.FancyInput; 1328 | 1329 | function FancyInput(options) { 1330 | this.options = options || {}; 1331 | } 1332 | 1333 | FancyInput.noConflict = function noConflict() { 1334 | global.FancyInput = previousFancyInput; 1335 | return FancyInput; 1336 | }; 1337 | 1338 | global.FancyInput = FancyInput; 1339 | }(this); 1340 | ``` 1341 | 1342 | **[⬆ back to top](#table-of-contents)** 1343 | 1344 | 1345 | ## jQuery 1346 | 1347 | - Prefix jQuery object variables with a `$`. 1348 | 1349 | ```javascript 1350 | // bad 1351 | var sidebar = $('.sidebar'); 1352 | 1353 | // good 1354 | var $sidebar = $('.sidebar'); 1355 | ``` 1356 | 1357 | - Cache jQuery lookups. 1358 | 1359 | ```javascript 1360 | // bad 1361 | function setSidebar() { 1362 | $('.sidebar').hide(); 1363 | 1364 | // ...stuff... 1365 | 1366 | $('.sidebar').css({ 1367 | 'background-color': 'pink' 1368 | }); 1369 | } 1370 | 1371 | // good 1372 | function setSidebar() { 1373 | var $sidebar = $('.sidebar'); 1374 | $sidebar.hide(); 1375 | 1376 | // ...stuff... 1377 | 1378 | $sidebar.css({ 1379 | 'background-color': 'pink' 1380 | }); 1381 | } 1382 | ``` 1383 | 1384 | - For DOM queries use Cascading `$('.sidebar ul')` or parent > child `$('.sidebar > ul')`. [jsPerf](http://jsperf.com/jquery-find-vs-context-sel/16) 1385 | - Use `find` with scoped jQuery object queries. 1386 | 1387 | ```javascript 1388 | // bad 1389 | $('ul', '.sidebar').hide(); 1390 | 1391 | // bad 1392 | $('.sidebar').find('ul').hide(); 1393 | 1394 | // good 1395 | $('.sidebar ul').hide(); 1396 | 1397 | // good 1398 | $('.sidebar > ul').hide(); 1399 | 1400 | // good 1401 | $sidebar.find('ul').hide(); 1402 | ``` 1403 | 1404 | **[⬆ back to top](#table-of-contents)** 1405 | 1406 | ## Testing 1407 | 1408 | - **Yup.** 1409 | 1410 | ```javascript 1411 | function() { 1412 | return true; 1413 | } 1414 | ``` 1415 | 1416 | **[⬆ back to top](#table-of-contents)** 1417 | 1418 | 1419 | 1420 | ## Contributors 1421 | 1422 | - [View Contributors](https://github.com/airbnb/javascript/graphs/contributors) 1423 | 1424 | 1425 | ## License 1426 | 1427 | (The MIT License) 1428 | 1429 | Copyright (c) 2014 Airbnb 1430 | 1431 | Permission is hereby granted, free of charge, to any person obtaining 1432 | a copy of this software and associated documentation files (the 1433 | 'Software'), to deal in the Software without restriction, including 1434 | without limitation the rights to use, copy, modify, merge, publish, 1435 | distribute, sublicense, and/or sell copies of the Software, and to 1436 | permit persons to whom the Software is furnished to do so, subject to 1437 | the following conditions: 1438 | 1439 | The above copyright notice and this permission notice shall be 1440 | included in all copies or substantial portions of the Software. 1441 | 1442 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 1443 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 1444 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 1445 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 1446 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 1447 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 1448 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 1449 | 1450 | **[⬆ back to top](#table-of-contents)** 1451 | 1452 | # }; 1453 | --------------------------------------------------------------------------------