├── .ackrc ├── .editorconfig ├── .gitignore ├── Gruntfile.js ├── MIT-LICENSE.txt ├── README.md ├── TODO.md ├── node_modules └── .gitkeep ├── package.json ├── public ├── css │ ├── alertify.css │ ├── bootstrap.css │ ├── github.css │ └── style.css ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ └── glyphicons-halflings-regular.woff ├── img │ ├── code.png │ ├── computer.png │ ├── favicon.ico │ ├── launchpad.png │ ├── loading.gif │ └── rockets.png └── js │ ├── admin.js │ ├── game.js │ ├── libs │ ├── alertify.min.js │ ├── bootstrap.js │ ├── favico.min.js │ ├── highlight.min.js │ ├── jquery-1.10.1.min.js │ ├── knockout-2.3.0.min.js │ ├── lodash-1.3.1.min.js │ ├── moment-2.1.0.min.js │ ├── mousetrap.min.js │ └── parsley.js │ ├── lobby.js │ ├── main.js │ └── plugins.js ├── runserver.js ├── src ├── config.js ├── db.js ├── eventnet.js ├── models.js ├── routes.js ├── server.js ├── settings.js.example.js └── sockets.js └── views ├── about.jade ├── admin.jade ├── footer.jade ├── game.jade ├── index.jade ├── layout.jade ├── lobby.jade ├── nav.jade └── signup.jade /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=highlight.js 2 | --ignore-dir=public/js/libs 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [package.json] 11 | indent_size = 2 12 | 13 | [Gruntfile.js] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | backup 3 | src/settings.js 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | module.exports = function(grunt) { 5 | var jsFiles = ['Gruntfile.js', 'public/**/*.js', '!public/js/libs/**/*.js', 6 | '!public/js/plugins.js', 'src/**/*.js']; 7 | 8 | grunt.initConfig({ 9 | pkg: grunt.file.readJSON('package.json'), 10 | jshint: { 11 | options: { 12 | newcap: false, 13 | node: true 14 | }, 15 | all: jsFiles 16 | }, 17 | watch: { 18 | test: { 19 | files: jsFiles, 20 | tasks: ['usetheforce_on', 'test', 'usetheforce_off'] 21 | } 22 | } 23 | }); 24 | 25 | grunt.loadNpmTasks('grunt-contrib-jshint'); 26 | grunt.loadNpmTasks('grunt-contrib-watch'); 27 | 28 | // Workaround to force continuation when encountering errors 29 | // during development cycle (for watch / livereload) 30 | grunt.registerTask('usetheforce_on', '// force the force option', function() { 31 | if (!grunt.option('force')) { 32 | grunt.config.set('usetheforce', true); 33 | grunt.option('force', true); 34 | } 35 | }); 36 | grunt.registerTask('usetheforce_off', '// remove the force option', function() { 37 | if (grunt.config.get('usetheforce')) { 38 | grunt.option('force', false); 39 | } 40 | }); 41 | 42 | grunt.registerTask('test', 'Lint source files', 43 | ['jshint']); 44 | grunt.registerTask('watchtest', 'Watch for changes and lint and test source files', 45 | ['usetheforce_on', 'test', 'watch:test', 'usetheforce_off']); 46 | 47 | grunt.registerTask('add-admin', 'Add an administrator user into the database', function() { 48 | var done = this.async(); 49 | 50 | var prompt = require('prompt'); 51 | prompt.start(); 52 | 53 | prompt.get([{ 54 | name: 'username', 55 | required: true 56 | }, { 57 | name: 'password', 58 | hidden: true, 59 | required: true, 60 | conform: function(value) { 61 | return true; 62 | } 63 | }], function(err, result) { 64 | // Setup DB connection 65 | var SwiftCODEConfig = require('./src/config'); 66 | var db = require('./src/db'); 67 | var models = require('./src/models'); 68 | db.setupConnection(new SwiftCODEConfig()); 69 | 70 | var user = new models.User({ 71 | username: result.username, 72 | password: result.password, 73 | isAdmin: true 74 | }); 75 | user.save(function(err, saved) { 76 | if (err) { 77 | console.log(err); 78 | } 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | grunt.registerTask('default', ['test']); 85 | 86 | }; 87 | })(); 88 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Zaven Muradyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftCODE 2 | 3 | SwiftCODE is a multiplayer, interactive, realtime typing speed game 4 | for coders. 5 | 6 | ## How to play 7 | 8 | The app is built around 2 main pages. 9 | 10 | - After logging in, or choosing to be anonymous, the user is redirected to the 11 | lobby, where the currently active games are shown. From here, they can choose 12 | to join a game or create their own (either single-player or multiplayer) from 13 | a set of preconfigured programming languages. 14 | 15 | - Once the user has created or joined a game, they are redirected to the game 16 | screen where the code snippet loads. There they wait until another user joins 17 | their game, after which the countdown begins. Each game can have a maximum of 18 | 4 players. Once the game starts, players must type the provided code as fast 19 | as possible. Throughout this, the players' current positions are shown to 20 | each other in real-time, and at the end some statistics are shown. 21 | 22 | ## Vision 23 | 24 | As programmers, we rely on many tools while coding. A keyboard is usually the 25 | most basic and most important of all such tools. Of course, there are far more 26 | important skills that a developer must have than typing speed. That being said, 27 | however, it's still great fun to hear the keys clicking away as you furiously 28 | write out some code. In fact, it's so fun, why not make it into a game? And a 29 | multiplayer one at that! 30 | 31 | In the past, I've enjoyed typing games, but there is a large difference between 32 | typing natural language, and typing code (even the choice of programming 33 | language can make a significant difference!). I had found 34 | [Typing.io](http://typing.io/) a while back, which is great fun - but 35 | unfortunately doesn't support any kind of multiplayer. 36 | 37 | The goal of this project was to fill that gap - to create a multiplayer, 38 | interactive, typing game for developers! I envisioned multiple players 39 | simultaneously receiving a piece of code, getting ready, and then racing each 40 | other to type out the code, all while streaming the progress of each player to 41 | his opponents, and animating their progress via multiple cursors on the 42 | player's screen. 43 | 44 | ## Installation 45 | 46 | ### Requirements 47 | 48 | - Node.js and NPM (v0.10) 49 | - MongoDB (v2.4) 50 | 51 | ### Download 52 | 53 | Grab the source code and the NPM dependencies. 54 | 55 | git clone https://github.com/voithos/swiftcode.git 56 | cd swiftcode 57 | npm install 58 | 59 | ### Configure 60 | 61 | Create a database in MongoDB for SwiftCODE, and create a MongoDB user 62 | account that SwiftCODE will use to connect. 63 | 64 | mongo 65 | > use swiftcode 66 | > db.addUser({ user: 'swiftcodeuser', pwd: 'password', roles: ['readWrite'] }) 67 | 68 | Copy the sample `settings.js.example.js` file to `settings.js`, and fill out 69 | the settings as desired (specifically, you must provide the database settings 70 | to your MongoDB). 71 | 72 | cd src 73 | cp settings.js.example.js settings.js 74 | vim settings.js 75 | 76 | SwiftCODE does not come with code exercises preloaded, but does have a simple 77 | admin interface which allows for the definition of new languages, projects, and 78 | exercises. An admin user is required to access the interface, which can be 79 | created through grunt (note, this requires the database settings to be in 80 | place). 81 | 82 | grunt add-admin 83 | 84 | The following prompt is for a username and a password (must be >= 8 characters). 85 | Once an admin user has been added, the admin interface can be accessed after 86 | logging in, using the link in the drop-down in the top-right corner of the 87 | page. 88 | 89 | ### Run 90 | 91 | At this point, SwiftCODE should be fully set up, and runnable. 92 | 93 | ./runserver.js 94 | 95 | Success! 96 | 97 | ## Open Source 98 | 99 | Without open source technologies and libraries, this project would not be 100 | possible. A great thanks goes to all of their creators. Listed in no particular 101 | order: 102 | 103 | - Node.js 104 | - Express 105 | - Socket.IO 106 | - MongoDB and Mongoose 107 | - Bootstrap 108 | - jQuery 109 | - Knockout.js 110 | - Highlight.js 111 | - Lo-dash 112 | - Moment.js 113 | - Jade 114 | - Passport 115 | - Cheerio 116 | - Helmet 117 | - Alertify 118 | - Favico.js 119 | - Mousetrap 120 | - And of course, HTML5 itself! 121 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ### Core features 4 | * Spam protection in login process 5 | * Add automated tests to compare server-side typeable-detection with client-side typeable-to-DOM matching algorithm 6 | * Better anti-cheat 7 | * Add method to kick out inactive users 8 | * Allow single user to play multi-player, to allow multi-player to get started when few users are online. 9 | 10 | ### Bugs 11 | * Error occurs after unjoining (playerCursor set to null, never set back?) 12 | * Docstrings are not classified seperately by highlight.js, thus not excluded 13 | * Certain keys don't work under Opera (for example, underscore is treated as '-') 14 | * When multiple players join around the same time, some may not get "ingame:join" updates 15 | * Foreign keyboard don't work (Mousetrap problem?) 16 | 17 | ### Possible future features 18 | * Learning mode? (collaborate with 'Learn X in Y' project?) 19 | * Profile page, where user can see stats, tweak settings 20 | * Names / passwords / invitation-only option for game rooms 21 | * Training mode? ("beat your own time") 22 | * Badges / leaderboard? 23 | * Show statistics of opponents, after they win? 24 | -------------------------------------------------------------------------------- /node_modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/node_modules/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SwiftCODE", 3 | "version": "1.0.11", 4 | "description": "Realtime typing speed game for coders", 5 | "keywords": [ 6 | "typing", 7 | "game", 8 | "realtime" 9 | ], 10 | "author": { 11 | "name": "Zaven Muradyan", 12 | "email": "megalivoithos@gmail.com" 13 | }, 14 | "homepage": "http://swiftcode.herokuapp.com/", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/voithos/swiftcode" 18 | }, 19 | "engines": { 20 | "node": "8.x" 21 | }, 22 | "dependencies": { 23 | "bcrypt": "1.0.3", 24 | "cheerio": "0.12.1", 25 | "connect": "2.10.1", 26 | "connect-flash": "0.1.1", 27 | "debug": "2.2.0", 28 | "express": "3.3.5", 29 | "helmet": "0.1.0", 30 | "highlight.js": "7.5.0", 31 | "jade": "0.34.1", 32 | "lodash": "1.3.1", 33 | "moment": "2.1.0", 34 | "mongoose": "5.5.11", 35 | "passport": "0.1.17", 36 | "passport-local": "0.1.6", 37 | "passport-local-mongoose": "5.0.1", 38 | "session-mongoose": "0.5.2", 39 | "socket.io": "2.2.0" 40 | }, 41 | "devDependencies": { 42 | "grunt": "~0.4.1", 43 | "grunt-contrib-jshint": "~0.6.3", 44 | "grunt-contrib-watch": "~0.5.2", 45 | "prompt": "~0.2.11", 46 | "forever-monitor": "~1.2.3" 47 | }, 48 | "bundleDependencies": [], 49 | "private": true, 50 | "main": "src/server.js", 51 | "scripts": { 52 | "start": "node src/server.js" 53 | }, 54 | "subdomain": "swiftcode" 55 | } 56 | -------------------------------------------------------------------------------- /public/css/alertify.css: -------------------------------------------------------------------------------- 1 | .alertify, 2 | .alertify-show, 3 | .alertify-log { 4 | -webkit-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 5 | -moz-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 6 | -ms-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 7 | -o-transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); 8 | transition: all 500ms cubic-bezier(0.175, 0.885, 0.320, 1.275); /* easeOutBack */ 9 | } 10 | .alertify-hide { 11 | -webkit-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 12 | -moz-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 13 | -ms-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 14 | -o-transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 15 | transition: all 250ms cubic-bezier(0.600, -0.280, 0.735, 0.045); /* easeInBack */ 16 | } 17 | .alertify-log-hide { 18 | -webkit-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 19 | -moz-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 20 | -ms-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 21 | -o-transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); 22 | transition: all 500ms cubic-bezier(0.600, -0.280, 0.735, 0.045); /* easeInBack */ 23 | } 24 | .alertify-cover { 25 | position: fixed; z-index: 99999; 26 | top: 0; right: 0; bottom: 0; left: 0; 27 | background-color:white; 28 | filter:alpha(opacity=0); 29 | opacity:0; 30 | } 31 | .alertify-cover-hidden { 32 | display: none; 33 | } 34 | .alertify { 35 | position: fixed; z-index: 99999; 36 | top: 50px; left: 50%; 37 | width: 550px; 38 | margin-left: -275px; 39 | opacity: 1; 40 | } 41 | .alertify-hidden { 42 | -webkit-transform: translate(0,-150px); 43 | -moz-transform: translate(0,-150px); 44 | -ms-transform: translate(0,-150px); 45 | -o-transform: translate(0,-150px); 46 | transform: translate(0,-150px); 47 | opacity: 0; 48 | display: none; 49 | } 50 | /* overwrite display: none; for everything except IE6-8 */ 51 | :root *> .alertify-hidden { 52 | display: block; 53 | visibility: hidden; 54 | } 55 | .alertify-logs { 56 | position: fixed; 57 | z-index: 5000; 58 | bottom: 10px; 59 | right: 10px; 60 | width: 300px; 61 | } 62 | .alertify-logs-hidden { 63 | display: none; 64 | } 65 | .alertify-log { 66 | display: block; 67 | margin-top: 10px; 68 | position: relative; 69 | right: -300px; 70 | opacity: 0; 71 | } 72 | .alertify-log-show { 73 | right: 0; 74 | opacity: 1; 75 | } 76 | .alertify-log-hide { 77 | -webkit-transform: translate(300px, 0); 78 | -moz-transform: translate(300px, 0); 79 | -ms-transform: translate(300px, 0); 80 | -o-transform: translate(300px, 0); 81 | transform: translate(300px, 0); 82 | opacity: 0; 83 | } 84 | .alertify-dialog { 85 | padding: 25px; 86 | } 87 | .alertify-resetFocus { 88 | border: 0; 89 | clip: rect(0 0 0 0); 90 | height: 1px; 91 | margin: -1px; 92 | overflow: hidden; 93 | padding: 0; 94 | position: absolute; 95 | width: 1px; 96 | } 97 | .alertify-inner { 98 | text-align: center; 99 | } 100 | .alertify-text { 101 | margin-bottom: 15px; 102 | width: 100%; 103 | -webkit-box-sizing: border-box; 104 | -moz-box-sizing: border-box; 105 | box-sizing: border-box; 106 | font-size: 100%; 107 | } 108 | .alertify-buttons { 109 | } 110 | .alertify-button, 111 | .alertify-button:hover, 112 | .alertify-button:active, 113 | .alertify-button:visited { 114 | background: none; 115 | text-decoration: none; 116 | border: none; 117 | /* line-height and font-size for input button */ 118 | line-height: 1.5; 119 | font-size: 100%; 120 | display: inline-block; 121 | cursor: pointer; 122 | margin-left: 5px; 123 | } 124 | 125 | @media only screen and (max-width: 680px) { 126 | .alertify, 127 | .alertify-logs { 128 | width: 90%; 129 | -webkit-box-sizing: border-box; 130 | -moz-box-sizing: border-box; 131 | box-sizing: border-box; 132 | } 133 | .alertify { 134 | left: 5%; 135 | margin: 0; 136 | } 137 | } 138 | 139 | 140 | /** 141 | * Twitter Bootstrap Look and Feel 142 | * Based on http://twitter.github.com/bootstrap/ 143 | */ 144 | .alertify, 145 | .alertify-log { 146 | font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; 147 | } 148 | .alertify { 149 | background: #FFF; 150 | border: 1px solid #8E8E8E; /* browsers that don't support rgba */ 151 | border: 1px solid rgba(0,0,0,.3); 152 | border-radius: 6px; 153 | box-shadow: 0 3px 7px rgba(0,0,0,.3); 154 | -webkit-background-clip: padding; /* Safari 4? Chrome 6? */ 155 | -moz-background-clip: padding; /* Firefox 3.6 */ 156 | background-clip: padding-box; /* Firefox 4, Safari 5, Opera 10, IE 9 */ 157 | } 158 | .alertify-dialog { 159 | padding: 0; 160 | } 161 | .alertify-inner { 162 | text-align: left; 163 | } 164 | .alertify-message { 165 | font-size: 20px; 166 | padding: 15px; 167 | margin: 0; 168 | } 169 | .alertify-text-wrapper { 170 | padding: 0 15px; 171 | } 172 | .alertify-text { 173 | color: #555; 174 | border-radius: 4px; 175 | padding: 8px; 176 | background-color: #FFF; 177 | border: 1px solid #CCC; 178 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075); 179 | } 180 | .alertify-text:focus { 181 | border-color: rgba(82,168,236,.8); 182 | outline: 0; 183 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6); 184 | } 185 | 186 | .alertify-buttons { 187 | padding: 14px 15px 15px; 188 | background: #F5F5F5; 189 | border-top: 1px solid #DDD; 190 | border-radius: 0 0 6px 6px; 191 | text-align: right; 192 | } 193 | .alertify-button, 194 | .alertify-button:hover, 195 | .alertify-button:focus, 196 | .alertify-button:active { 197 | margin-left: 10px; 198 | border-radius: 4px; 199 | font-weight: normal; 200 | padding: 4px 12px; 201 | text-decoration: none; 202 | } 203 | .alertify-button:focus { 204 | outline: none; 205 | } 206 | .alertify-button:active { 207 | position: relative; 208 | } 209 | .alertify-button-cancel, 210 | .alertify-button-cancel:hover, 211 | .alertify-button-cancel:focus, 212 | .alertify-button-cancel:active { 213 | text-shadow: 0 -1px 0 rgba(255,255,255,.75); 214 | background-color: #E6E6E6; 215 | border: 1px solid #BBB; 216 | color: #333; 217 | } 218 | .alertify-button-cancel:hover, 219 | .alertify-button-cancel:focus, 220 | .alertify-button-cancel:active { 221 | background: #E6E6E6; 222 | } 223 | .alertify-button-ok, 224 | .alertify-button-ok:hover, 225 | .alertify-button-ok:focus, 226 | .alertify-button-ok:active { 227 | background-color: #18bc9c; 228 | border: 1px solid #18bc9c; 229 | color: #FFF; 230 | } 231 | .alertify-button-ok:hover, 232 | .alertify-button-ok:focus, 233 | .alertify-button-ok:active { 234 | background-color: #13987e; 235 | border: 1px solid #11866f; 236 | } 237 | 238 | .alertify-log { 239 | background: #D9EDF7; 240 | padding: 8px 14px; 241 | border-radius: 4px; 242 | color: #3A8ABF; 243 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 244 | border: 1px solid #BCE8F1; 245 | } 246 | .alertify-log-error { 247 | color: #B94A48; 248 | background: #F2DEDE; 249 | border: 1px solid #EED3D7; 250 | } 251 | .alertify-log-success { 252 | color: #468847; 253 | background: #DFF0D8; 254 | border: 1px solid #D6E9C6; 255 | } 256 | 257 | .alertify-error .alertify-message { 258 | color: #e74c3c; 259 | } 260 | 261 | .alertify-error .alertify-button { 262 | background-color: #e74c3c; 263 | border: 1px solid #e74c3c; 264 | } 265 | 266 | .alertify-error .alertify-button:hover, 267 | .alertify-error .alertify-button:focus, 268 | .alertify-error .alertify-button:active { 269 | background-color: #df2e1b; 270 | border: 1px solid #cd2a19; 271 | } 272 | -------------------------------------------------------------------------------- /public/css/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | color: #333; 10 | background: #f8f8ff 11 | } 12 | 13 | pre .comment, 14 | pre .template_comment, 15 | pre .diff .header, 16 | pre .javadoc { 17 | color: #998; 18 | font-style: italic 19 | } 20 | 21 | pre .keyword, 22 | pre .css .rule .keyword, 23 | pre .winutils, 24 | pre .javascript .title, 25 | pre .nginx .title, 26 | pre .subst, 27 | pre .request, 28 | pre .status { 29 | color: #333; 30 | font-weight: bold 31 | } 32 | 33 | pre .number, 34 | pre .hexcolor, 35 | pre .ruby .constant { 36 | color: #099; 37 | } 38 | 39 | pre .string, 40 | pre .tag .value, 41 | pre .phpdoc, 42 | pre .tex .formula { 43 | color: #d14 44 | } 45 | 46 | pre .title, 47 | pre .id { 48 | color: #900; 49 | font-weight: bold 50 | } 51 | 52 | pre .javascript .title, 53 | pre .lisp .title, 54 | pre .clojure .title, 55 | pre .subst { 56 | font-weight: normal 57 | } 58 | 59 | pre .class .title, 60 | pre .haskell .type, 61 | pre .vhdl .literal, 62 | pre .tex .command { 63 | color: #458; 64 | font-weight: bold 65 | } 66 | 67 | pre .tag, 68 | pre .tag .title, 69 | pre .rules .property, 70 | pre .django .tag .keyword { 71 | color: #000080; 72 | font-weight: normal 73 | } 74 | 75 | pre .attribute, 76 | pre .variable, 77 | pre .lisp .body { 78 | color: #008080 79 | } 80 | 81 | pre .regexp { 82 | color: #009926 83 | } 84 | 85 | pre .class { 86 | color: #458; 87 | font-weight: bold 88 | } 89 | 90 | pre .symbol, 91 | pre .ruby .symbol .string, 92 | pre .lisp .keyword, 93 | pre .tex .special, 94 | pre .prompt { 95 | color: #990073 96 | } 97 | 98 | pre .built_in, 99 | pre .lisp .title, 100 | pre .clojure .built_in { 101 | color: #0086b3 102 | } 103 | 104 | pre .preprocessor, 105 | pre .pi, 106 | pre .doctype, 107 | pre .shebang, 108 | pre .cdata { 109 | color: #999; 110 | font-weight: bold 111 | } 112 | 113 | pre .deletion { 114 | background: #fdd 115 | } 116 | 117 | pre .addition { 118 | background: #dfd 119 | } 120 | 121 | pre .diff .change { 122 | background: #0086b3 123 | } 124 | 125 | pre .chunk { 126 | color: #aaa 127 | } 128 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | /* Overrides for navbar-form */ 2 | .navbar { 3 | border-radius: 0; 4 | } 5 | 6 | .navbar-form { 7 | padding: 10px 15px; 8 | margin-top: 8px; 9 | margin-right: -15px; 10 | margin-bottom: 8px; 11 | margin-left: -15px; 12 | border-top: 1px solid #e6e6e6; 13 | border-bottom: 1px solid #e6e6e6; 14 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 15 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 16 | } 17 | @media (min-width: 768px) { 18 | .navbar-form { 19 | width: auto; 20 | padding-top: 0; 21 | padding-bottom: 0; 22 | margin-right: 0; 23 | margin-left: 0; 24 | border: 0; 25 | -webkit-box-shadow: none; 26 | box-shadow: none; 27 | } 28 | 29 | .navbar-form .form-group { 30 | display: inline-block; 31 | margin-bottom: 0; 32 | margin-left: 3px; 33 | margin-right: 3px; 34 | vertical-align: middle; 35 | } 36 | 37 | .navbar-form .form-group.has-success, 38 | .navbar-form .form-group.has-error { 39 | margin-left: 3px; 40 | margin-right: 3px; 41 | } 42 | 43 | .navbar-form .form-control { 44 | display: inline-block; 45 | padding: 10px 15px; 46 | } 47 | 48 | .navbar-form button { 49 | margin: 0 5px; 50 | } 51 | } 52 | 53 | /* General styles */ 54 | .logo { 55 | padding: 15px; 56 | } 57 | 58 | .logo .inverse { 59 | position: relative; 60 | display: inline-block; 61 | background-color: #000000; 62 | padding: 4px 8px; 63 | margin: 0 4px; 64 | border-radius: 14px; 65 | } 66 | 67 | .logo:hover .inverse { 68 | background-color: #ffffff; 69 | } 70 | 71 | .footer { 72 | text-align: center; 73 | margin-top: 50px; 74 | } 75 | 76 | .center-parent { 77 | text-align: center; 78 | } 79 | 80 | .center-parent:before { 81 | content: ''; 82 | display: inline-block; 83 | height: 100%; 84 | vertical-align: middle; 85 | margin-right: -0.25em; 86 | } 87 | 88 | .center-child { 89 | display: inline-block; 90 | vertical-align: middle; 91 | } 92 | 93 | /* Index page */ 94 | .banner { 95 | background: url("/img/rockets.png") no-repeat top center #023251; 96 | margin-top: -21px; 97 | border-bottom: 5px solid #7dc3e9; 98 | } 99 | 100 | .banner .container { 101 | height: 550px; 102 | } 103 | 104 | .banner-intro { 105 | text-align: center; 106 | margin-top: 110px; 107 | } 108 | 109 | .banner-heading { 110 | font-size: 50px; 111 | font-weight: bold; 112 | color: #149c82; 113 | } 114 | 115 | .banner-phrase { 116 | color: #ffffff; 117 | } 118 | 119 | .banner-buttons { 120 | } 121 | 122 | .banner-buttons .playnow-button { 123 | margin: 15px 0 10px 0; 124 | font-size: 30px; 125 | width: 300px; 126 | } 127 | 128 | .banner-buttons .signup-section { 129 | font-size: 20px; 130 | color: #ffffff; 131 | } 132 | 133 | .featurette-heading { 134 | margin-top: 125px; 135 | } 136 | 137 | /* Signup page */ 138 | .form-signup { 139 | max-width: 330px; 140 | padding: 15px; 141 | margin: 0 auto; 142 | } 143 | 144 | /* Lobby page */ 145 | .lobby { 146 | margin-bottom: 0; 147 | } 148 | 149 | .lobby .panel-body { 150 | height: 400px; 151 | } 152 | 153 | .lobby .game-row { 154 | height: 50px; 155 | } 156 | 157 | .lobby .game-row td { 158 | vertical-align: middle; 159 | } 160 | 161 | .lobby .game-row.no-games { 162 | text-align: center; 163 | } 164 | 165 | .join-choice[disabled] { 166 | background-color: #13987e; 167 | border-color: #11866f; 168 | opacity: 1.0; 169 | } 170 | 171 | .loading-container { 172 | display: block; 173 | height: 285px; 174 | text-align: center; 175 | line-height: 285px; 176 | } 177 | 178 | .slide-panel-container { 179 | position: relative; 180 | } 181 | 182 | .slide-panel { 183 | position: absolute; 184 | width: 100%; 185 | } 186 | 187 | .lang-container { 188 | display: none; 189 | } 190 | 191 | .lang { 192 | width: 90%; 193 | margin: 0 auto; 194 | } 195 | 196 | .lang-choice[disabled] { 197 | background-color: #df2e1b; 198 | border-color: #cd2a19; 199 | opacity: 1.0; 200 | } 201 | 202 | /* Game page */ 203 | .control-panel { 204 | height: 147px; 205 | } 206 | 207 | .control-panel .panel-body { 208 | height: 85%; 209 | } 210 | 211 | .control-panel-go { 212 | margin: 0 auto; 213 | } 214 | 215 | .control-panel .status { 216 | display: inline-block; 217 | margin-top: 10px; 218 | margin-bottom: 10px; 219 | } 220 | 221 | .control-panel .timer-parent { 222 | line-height: 48px; 223 | } 224 | 225 | .control-panel .timer { 226 | font-size: 25px; 227 | } 228 | 229 | .code { 230 | font-family: 'Source Code Pro', monospace; 231 | } 232 | 233 | .code-char.mistake-path { 234 | color: #df2e1b; 235 | } 236 | 237 | .code-char.opponent1 { 238 | background-color: #3498db; 239 | /* outline: 1px solid #3498db; */ 240 | color: #ffffff; 241 | } 242 | 243 | .code-char.opponent2 { 244 | background-color: #f39c12; 245 | /* outline: 1px solid #f39c12; */ 246 | color: #ffffff; 247 | } 248 | 249 | .code-char.opponent3 { 250 | background-color: #563d7c; 251 | /* outline: 1px solid #e74c3c; */ 252 | color: #ffffff; 253 | } 254 | 255 | .code-char.player { 256 | background-color: #1AC7A4; 257 | /* outline: 1px solid #16B696; */ 258 | color: #ffffff; 259 | } 260 | 261 | .code-char.mistake { 262 | background-color: #df2e1b; 263 | color: #ffffff; 264 | } 265 | 266 | .code-char.player.mistaken { 267 | background-color: #7f9293; 268 | color: #ffffff; 269 | } 270 | 271 | .comment { 272 | opacity: 0.7; 273 | } 274 | 275 | .code-char.untyped { 276 | opacity: 0.7; 277 | } 278 | 279 | .code-char.typed { 280 | opacity: 1; 281 | } 282 | 283 | .code-char.return-char { 284 | text-align: center; 285 | display: inline-block; 286 | width: 20px; 287 | visibility: hidden; 288 | } 289 | 290 | .code-char.return-char.mistake, 291 | .code-char.return-char.mistaken-path { 292 | visibility: visible; 293 | } 294 | 295 | .code-char.return-char:before { 296 | content: '\23CE'; 297 | } 298 | 299 | .code-char.return-char.opponent1 { 300 | visibility: visible; 301 | } 302 | 303 | .code-char.return-char.opponent2 { 304 | visibility: visible; 305 | } 306 | 307 | .code-char.return-char.opponent3 { 308 | visibility: visible; 309 | } 310 | 311 | .code-char.return-char.player { 312 | visibility: visible; 313 | } 314 | 315 | .code-char.backspace-char { 316 | } 317 | 318 | .code-char.backspace-char:before { 319 | content: '\232B'; 320 | } 321 | 322 | .completion-header { 323 | font-weight: bold; 324 | text-align: center; 325 | } 326 | 327 | .completion-table { 328 | font-size: 34px; 329 | } 330 | 331 | .completion-table .muted { 332 | font-size: 24px; 333 | } 334 | 335 | .completion-table .row { 336 | opacity: 0; 337 | position: relative; 338 | left: -55px; 339 | } 340 | 341 | .panel-default { 342 | border-color: #95a5a6; 343 | } 344 | 345 | .players-panel .panel-body { 346 | padding: 10px; 347 | } 348 | 349 | .progress.progress-labeled { 350 | width: 200px; 351 | height: 20px; 352 | margin: 3px auto; 353 | } 354 | 355 | .progress.progress-labeled .progress-label { 356 | width: 200px; 357 | height: 20px; 358 | font-size: 14px; 359 | position: absolute; 360 | text-align: center; 361 | } 362 | 363 | .progress.progress-labeled .progress-label.player0 { 364 | color: #18bc9c; 365 | } 366 | 367 | .progress.progress-labeled .progress-label.player1 { 368 | color: #3498db; 369 | } 370 | 371 | .progress.progress-labeled .progress-label.player2 { 372 | color: #f39c12; 373 | } 374 | 375 | .progress.progress-labeled .progress-label.player3 { 376 | color: #563d7c; 377 | } 378 | 379 | .players-panel .progress.player0 { 380 | border: 1px solid #18bc9c; 381 | } 382 | 383 | .players-panel .progress.player1 { 384 | border: 1px solid #3498db; 385 | } 386 | 387 | .players-panel .progress.player2 { 388 | border: 1px solid #f39c12; 389 | } 390 | 391 | .players-panel .progress.player3 { 392 | border: 1px solid #563d7c; 393 | } 394 | 395 | /* Special case for opponent3 - custom purple color (not part of theme) */ 396 | .progress.player3 .progress-bar { 397 | background-color: #563d7c; 398 | } 399 | 400 | .progress-bar.progress-labeled { 401 | overflow: hidden; 402 | position: relative; 403 | } 404 | 405 | .progress-bar.progress-labeled .progress-label { 406 | left: 0; 407 | color: #ffffff; 408 | } 409 | 410 | /* Admin Page */ 411 | .form-group-tooltip input, 412 | .form-group-tooltip textarea { 413 | max-width: 70%; 414 | } 415 | 416 | /* About page */ 417 | .about-projects .project-container { 418 | background-color: #fafafa; 419 | border: 1px solid #dadada; 420 | position: relative; 421 | } 422 | 423 | .about-projects .project-name { 424 | font-size: 26px; 425 | margin: 5px 0; 426 | display: block; 427 | } 428 | 429 | .about-projects .lang { 430 | font-size: 15px; 431 | margin-left: 20px; 432 | } 433 | 434 | .about-projects .link-group { 435 | margin-top: 7px; 436 | padding-bottom: 7px; 437 | } 438 | 439 | .about-projects .project-link { 440 | width: 75%; 441 | margin-top: 2px; 442 | margin-bottom: 2px; 443 | } 444 | 445 | /* Footer */ 446 | .forkme { 447 | margin-left: 15px; 448 | } 449 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/img/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/img/code.png -------------------------------------------------------------------------------- /public/img/computer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/img/computer.png -------------------------------------------------------------------------------- /public/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/img/favicon.ico -------------------------------------------------------------------------------- /public/img/launchpad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/img/launchpad.png -------------------------------------------------------------------------------- /public/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/img/loading.gif -------------------------------------------------------------------------------- /public/img/rockets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/voithos/swiftcode/3692087b6692631b8fb61e12040ddf961e62d6c6/public/img/rockets.png -------------------------------------------------------------------------------- /public/js/admin.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var Project = function() { 4 | viewModel.projectCount(viewModel.projectCount() + 1); 5 | viewModel.exerciseCount(viewModel.exerciseCount() + 1); 6 | 7 | this.exercises = ko.observableArray([{ absIndex: viewModel.exerciseCount() - 1 }]); 8 | this.addExercise = function() { 9 | viewModel.exerciseCount(viewModel.exerciseCount() + 1); 10 | this.exercises.push({ absIndex: viewModel.exerciseCount() - 1 }); 11 | }.bind(this); 12 | }; 13 | 14 | var viewModel = { 15 | projects: ko.observableArray(), 16 | projectCount: ko.observable(0), 17 | exerciseCount: ko.observable(0), 18 | addProject: function() { 19 | this.projects.push(new Project()); 20 | }, 21 | reinitExercises: function() { 22 | $.ajax({ 23 | type: 'POST', 24 | url: '/admin/reinit-exercises', 25 | success: function(data) { 26 | if (data.success) { 27 | showAlert('All exercises successfully reinitialized.'); 28 | } else { 29 | showAlert('There was an error during reinitialization.', true); 30 | } 31 | }, 32 | dataType: 'json' 33 | }); 34 | } 35 | }; 36 | 37 | swiftcode.viewModel = viewModel; 38 | ko.applyBindings(viewModel); 39 | viewModel.addProject(); 40 | })(); 41 | -------------------------------------------------------------------------------- /public/js/game.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | hljs.tabReplace = ' '; 3 | 4 | var socket = io.connect(getSocketUrl() + '/game'); 5 | 6 | var GameState = function() { 7 | this.gameStatus = ko.observable('Loading...'); 8 | this.gameStatusCss = ko.observable(''); 9 | this.timer = ko.observable(''); 10 | this.timerCss = ko.observable(''); 11 | this.timerRunning = ko.observable(false); 12 | this.started = ko.observable(false); 13 | this.gamecode = ko.observable(''); 14 | this.projectName = ko.observable(''); 15 | this.langCss = ko.observable(''); 16 | this.isMultiplayer = ko.observable(false); 17 | this.players = ko.observableArray(); 18 | }; 19 | 20 | var playerMapping = { 21 | player0: 'success', 22 | player1: 'info', 23 | player2: 'warning', 24 | player3: 'danger' 25 | }; 26 | 27 | var Player = function(id, numId, name) { 28 | this.id = id; // Used for removal 29 | this.name = ko.observable(name); 30 | this.percentage = ko.observable(0); 31 | this.cssClass = ko.observable('player' + numId); 32 | this.colorClass = ko.observable(playerMapping[this.cssClass()]); 33 | }; 34 | 35 | Player.prototype.formattedName = function(n) { 36 | return this.name().length > n ? 37 | this.name().substr(0, n-1) + '…' : 38 | this.name(); 39 | }; 40 | 41 | var viewModel = { 42 | loaded: ko.observable(false), 43 | loading: ko.observable(false), 44 | completionText: ko.observable(''), 45 | stats: { 46 | time: ko.observable(''), 47 | speed: ko.observable(0), 48 | typeables: ko.observable(0), 49 | keystrokes: ko.observable(0), 50 | percentUnproductive: ko.observable(0), 51 | mistakes: ko.observable(0) 52 | }, 53 | game: new GameState(), 54 | hideCompletionDialog: function() { 55 | $('#completion-dialog').modal('hide'); 56 | }, 57 | submitHighlightingReport: function() { 58 | console.log('emit report:highlightingerror'); 59 | socket.emit('report:highlightingerror', { 60 | exercise: game.exercise 61 | }); 62 | 63 | $('.highlight-flag').hide('slide', { direction: 'up' }); 64 | alertify.log("Thanks! We'll look into it.", 'success', 3000); 65 | } 66 | }; 67 | 68 | ko.applyBindings(viewModel); 69 | 70 | var $gamecode = null; 71 | 72 | var game = null; 73 | var exercise = null; 74 | var nonTypeables = null; 75 | 76 | /** 77 | * Represents a player or opponent's cursor 78 | */ 79 | var CodeCursor = function(cfg) { 80 | this.playerId = cfg.playerId; 81 | this.playerName = cfg.playerName; 82 | this.cursor = cfg.cursor; 83 | this.code = cfg.code; 84 | this.codeLength = cfg.code.length; 85 | this.pos = 0; 86 | this.keystrokes = 0; 87 | this.isMistaken = false; 88 | this.mistakePathLength = 0; 89 | this.mistakes = 0; 90 | this.mistakePositions = []; 91 | 92 | this.isMainPlayer = cfg.isMainPlayer || false; 93 | 94 | this.onCorrectKey = cfg.onCorrectKey || function() {}; 95 | this.onAdvanceCursor = cfg.onAdvanceCursor || function() {}; 96 | this.onRetreatCursor = cfg.onRetreatCursor || function() {}; 97 | this.onGameComplete = cfg.onGameComplete || function() {}; 98 | 99 | this.cursor.addClass(this.playerName); 100 | }; 101 | 102 | CodeCursor.prototype.processKey = function(key) { 103 | if (this.isMistaken) { 104 | this.mistakePathKey(); 105 | } else if (key === this.code.charAt(this.pos)) { 106 | this.correctKey(); 107 | } else { 108 | this.incorrectKey(); 109 | } 110 | }; 111 | 112 | CodeCursor.prototype.advanceCursor = function() { 113 | this.advanceCursorWithClass(this.playerName); 114 | }; 115 | 116 | CodeCursor.prototype.advanceCursorWithClass = function(curClass, trailingClass) { 117 | this.keystrokes++; 118 | this.pos++; 119 | 120 | this.cursor.removeClass(curClass); 121 | if (this.isMainPlayer) { 122 | this.cursor.removeClass('untyped'); 123 | this.cursor.addClass('typed'); 124 | } 125 | this.cursor.addClass(trailingClass); 126 | 127 | this.cursor = this.cursor.nextAll('.code-char').first(); 128 | this.cursor.addClass(curClass); 129 | 130 | this.onAdvanceCursor.call(this, this); 131 | }; 132 | 133 | CodeCursor.prototype.retreatCursor = function() { 134 | this.retreatCursorWithClass(this.playerName); 135 | }; 136 | 137 | CodeCursor.prototype.retreatCursorWithClass = function(curClass, trailingClass) { 138 | this.keystrokes++; 139 | this.pos--; 140 | this.mistakePathLength--; 141 | 142 | this.cursor.removeClass(curClass); 143 | this.cursor = this.cursor.prevAll('.code-char').first(); 144 | 145 | this.cursor.removeClass(trailingClass); 146 | if (this.isMainPlayer) { 147 | this.cursor.removeClass('typed'); 148 | this.cursor.addClass('untyped'); 149 | } 150 | this.cursor.addClass(curClass); 151 | 152 | this.onRetreatCursor.call(this, this); 153 | }; 154 | 155 | CodeCursor.prototype.correctKey = function() { 156 | this.advanceCursorWithClass(this.playerName); 157 | 158 | this.onCorrectKey.call(this, this); 159 | if (this.pos === this.codeLength) { 160 | this.onGameComplete.call(this, this); 161 | } 162 | }; 163 | 164 | CodeCursor.prototype.incorrectKey = function() { 165 | // We must *not* be at the final character of the code if we want to 166 | // create a mistake path, so check for it 167 | if (this.pos < this.codeLength - 1) { 168 | this.isMistaken = true; 169 | this.mistakes++; 170 | this.mistakePositions.push(this.pos); 171 | this.advanceCursorWithClass(this.playerName, 'mistake'); 172 | this.mistakePathLength++; 173 | } 174 | // But we do want to highlight a mistake, even if we're at the end 175 | // of the code 176 | this.cursor.addClass('mistaken'); 177 | }; 178 | 179 | CodeCursor.prototype.mistakePathKey = function() { 180 | if (this.pos < this.codeLength - 1) { 181 | if (this.mistakePathLength < 10) { 182 | this.advanceCursorWithClass(this.playerName + ' mistaken', 'mistake-path'); 183 | this.mistakePathLength++; 184 | } 185 | } 186 | }; 187 | 188 | CodeCursor.prototype.backspaceKey = function() { 189 | if (this.isMistaken) { 190 | this.retreatCursorWithClass(this.playerName + ' mistaken', 'mistake-path mistake'); 191 | 192 | if (this.mistakePathLength === 0) { 193 | this.isMistaken = false; 194 | this.cursor.removeClass('mistaken'); 195 | } 196 | } 197 | }; 198 | 199 | CodeCursor.prototype.destroy = function() { 200 | this.cursor.removeClass(this.playerName); 201 | }; 202 | 203 | 204 | /** 205 | * Current game state 206 | */ 207 | var state = { 208 | time: null, 209 | startTime: null, 210 | code: null, 211 | playerCursor: null, 212 | opponents: 0, 213 | opponentCursors: {} 214 | }; 215 | 216 | /** 217 | * Extract game code, manipulate references, remove non-typeables, 218 | * and wrap each character is a specific span tag 219 | */ 220 | var bindCodeCharacters = function() { 221 | $gamecode = $('#gamecode'); 222 | 223 | var codemap = []; 224 | var $contents = $gamecode.contents(); 225 | 226 | // Loop through contents of code, and add all non-comment 227 | // blocks into the codemap, keeping track of their positions 228 | // and elements 229 | _.each($contents, function(elem, elIdx) { 230 | var $elem = $(elem); 231 | 232 | if ($elem.is(nonTypeables)) { 233 | // Handle special case of end-of-line comment 234 | var $prev = $($contents.get(elIdx - 1)), 235 | $next = $($contents.get(elIdx + 1)); 236 | 237 | if ($prev && $next) { 238 | // End-of-line comment is preceded by non-newline and 239 | // followed by newline 240 | var isEndOfLineComment = 241 | !$prev.text().match(/\n\s*$/) && 242 | $next.text().charAt(0) === '\n'; 243 | 244 | if (isEndOfLineComment) { 245 | // Add the return at the end of the previous 246 | // element 247 | codemap.push({ 248 | char: '\n', 249 | beforeComment: true, 250 | idx: $prev.text().search(/\s*$/), 251 | elIdx: elIdx - 1, 252 | el: $prev 253 | }); 254 | } 255 | } 256 | return; 257 | } 258 | 259 | var text = $elem.text(); 260 | _.each(text, function(s, i) { 261 | codemap.push({ 262 | char: s, 263 | beforeComment: false, 264 | idx: i, 265 | elIdx: elIdx, 266 | el: $elem 267 | }); 268 | }); 269 | }); 270 | 271 | /** 272 | * Reusable filter method that keeps track of indices 273 | * marked for removal, with custom criteria functions 274 | */ 275 | var iterativeFilter = function(collection, state, loopFn) { 276 | var indices = {}; 277 | var addSection = function(lastIdx, curIdx) { 278 | var start = lastIdx + 1, 279 | howMany = curIdx - start; 280 | 281 | if (howMany > 0) { 282 | for (var i = start; i < start + howMany; i++) { 283 | indices[i] = true; 284 | } 285 | } 286 | }; 287 | 288 | _.each(collection, function(piece, i) { 289 | loopFn.call(state, piece, i, addSection); 290 | }); 291 | 292 | // Remove the collected indices 293 | return _.filter(collection, function(piece, i) { 294 | return !indices[i]; 295 | }); 296 | }; 297 | 298 | // Loop through the codemap and remove occurrences of leading and 299 | // trailing whitespace 300 | codemap = iterativeFilter(codemap, { 301 | leadingSearch: true, 302 | trailingSearch: false, 303 | lastNewline: -1, 304 | lastTypeable: -1, 305 | setMode: function(mode) { 306 | this.leadingSearch = mode === 'leading'; 307 | this.trailingSearch = mode === 'trailing'; 308 | } 309 | }, function(piece, i, addSection) { 310 | if (piece.char === ' ' || piece.char === '\t') { 311 | // Skip over 312 | return; 313 | } else if (piece.char === '\n') { 314 | // New line 315 | if (this.trailingSearch) { 316 | this.setMode('leading'); 317 | addSection(this.lastTypeable, i); 318 | } 319 | this.lastNewline = i; 320 | } else { 321 | // Typeable 322 | if (this.leadingSearch) { 323 | this.setMode('trailing'); 324 | addSection(this.lastNewline, i); 325 | } 326 | this.lastTypeable = i; 327 | } 328 | }); 329 | 330 | // Finally, remove contiguous blocks of newline+whitespace, 331 | // as well as globally leading whitespace 332 | codemap = iterativeFilter(codemap, { 333 | firstTypeableFound: false, 334 | newlineFound: false, 335 | typeableFound: false, 336 | lastRelevantNewline: -1, 337 | setFound: function(found) { 338 | this.newlineFound = found === 'newline'; 339 | this.typeableFound = found === 'typeable'; 340 | if (found === 'typeable') { 341 | this.firstTypeableFound = true; 342 | } 343 | } 344 | }, function(piece, i, addSection) { 345 | if (piece.char === ' ' || piece.char === '\t') { 346 | // Skip over 347 | return; 348 | } else if (piece.char === '\n') { 349 | // Newline 350 | if (this.firstTypeableFound && !this.newlineFound) { 351 | this.lastRelevantNewline = i; 352 | } 353 | this.setFound('newline'); 354 | } else { 355 | // Typeable 356 | if (this.newlineFound) { 357 | addSection(this.lastRelevantNewline, i); 358 | } 359 | this.setFound('typeable'); 360 | } 361 | }); 362 | 363 | var isTextNode = function(el) { 364 | return el.get(0).nodeType === 3; 365 | }; 366 | 367 | // Group remaining code chars by original element, and loop through 368 | // every element group and replace the element's text content with the 369 | // wrapped code chars 370 | var groupedCodemap = _.groupBy(codemap, function(piece) { return piece.elIdx; }); 371 | _.each(groupedCodemap, function(codeGroup) { 372 | var $elem = codeGroup[0].el, 373 | text = $elem.text(); 374 | 375 | var collapseCodeGroup = function(codeGroup, text) { 376 | var chunks = [], 377 | idx = 0; 378 | 379 | _.each(codeGroup, function(piece) { 380 | chunks.push(text.slice(idx, piece.idx)); 381 | idx = piece.idx + 1; 382 | 383 | if (piece.char === '\n') { 384 | chunks.push(''); 385 | if (!piece.beforeComment) { 386 | chunks.push('\n'); 387 | } 388 | } else { 389 | chunks.push('' + piece.char + ''); 390 | } 391 | }); 392 | 393 | chunks.push(text.slice(idx, text.length)); 394 | return chunks.join(''); 395 | }; 396 | 397 | if (isTextNode($elem)) { 398 | $elem.replaceWith(collapseCodeGroup(codeGroup, text)); 399 | } else { 400 | // Re-add highlighting classes to the new spans 401 | var oldClass = $elem.attr('class'); 402 | var $newContent = $(collapseCodeGroup(codeGroup, text)); 403 | $elem.replaceWith($newContent); 404 | $newContent.addClass(oldClass); 405 | } 406 | }); 407 | 408 | // Attach boundcode 409 | swiftcode.boundCode = _.map(codemap, function(piece) { return piece.char; }).join(''); 410 | 411 | // Set all code characters to untyped 412 | $gamecode.find('.code-char').addClass('untyped'); 413 | }; 414 | 415 | var checkGameState = function() { 416 | viewModel.game.timerRunning(game.starting || game.started); 417 | viewModel.game.started(game.started); 418 | addRemoveOpponents(); 419 | 420 | if (game.started) { 421 | setStarting(); 422 | startGame(); 423 | } else if (game.starting) { 424 | setStarting(); 425 | } else { 426 | resetStarting(); 427 | } 428 | }; 429 | 430 | var fullyStarted = false; 431 | var startGame = function() { 432 | if (fullyStarted) { 433 | return; 434 | } 435 | state.startTime = moment(); 436 | viewModel.game.gameStatus('Go!'); 437 | viewModel.game.gameStatusCss('text-info control-panel-go'); 438 | fullyStarted = true; 439 | }; 440 | 441 | var setStarting = function() { 442 | if (fullyStarted) { 443 | return; 444 | } 445 | viewModel.game.gameStatus('Get ready... '); 446 | 447 | updateTime(); 448 | if (!state.playerCursor) { 449 | state.playerCursor = new CodeCursor({ 450 | isMainPlayer: true, 451 | playerId: user._id, 452 | playerName: 'player', 453 | cursor: $gamecode.find('.code-char').first(), 454 | code: state.code, 455 | onAdvanceCursor: onPlayerAdvanceCursor, 456 | onRetreatCursor: emitCursorRetreat, 457 | onGameComplete: completeGame 458 | }); 459 | } 460 | }; 461 | 462 | var addRemoveOpponents = function() { 463 | // Remove opponents that are no longer in-game 464 | _.each(state.opponentCursors, function(cursor, opponentId) { 465 | if (!_.contains(game.players, opponentId)) { 466 | removeOpponent(opponentId); 467 | } 468 | }); 469 | 470 | // Add new opponents that are not in the list yet 471 | _.each(game.players, function(player, i) { 472 | // Do not add self as an opponent 473 | if (player != user._id && !(player in state.opponentCursors)) { 474 | addOpponent(player, game.playerNames[i]); 475 | } 476 | }); 477 | }; 478 | 479 | var addOpponent = function(opponentId, opponentName) { 480 | state.opponents++; 481 | state.opponentCursors[opponentId] = new CodeCursor({ 482 | playerId: opponentId, 483 | playerName: 'opponent' + state.opponents, 484 | cursor: $gamecode.find('.code-char').first(), 485 | code: state.code, 486 | onAdvanceCursor: updatePlayerProgress 487 | }); 488 | viewModel.game.players.push(new Player(opponentId, state.opponents, opponentName)); 489 | }; 490 | 491 | var removeOpponent = function(opponentId) { 492 | state.opponents--; 493 | if (opponentId in state.opponentCursors) { 494 | state.opponentCursors[opponentId].destroy(); 495 | delete state.opponentCursors[opponentId]; 496 | } 497 | 498 | var match = ko.utils.arrayFirst(viewModel.game.players(), function(player) { 499 | return player.id == opponentId; 500 | }); 501 | if (match) { 502 | viewModel.game.players.remove(match); 503 | } 504 | }; 505 | 506 | var updatePlayerProgress = function(cursor) { 507 | var match = ko.utils.arrayFirst(viewModel.game.players(), function(player) { 508 | return player.id == cursor.playerId; 509 | }); 510 | if (match) { 511 | match.percentage((cursor.pos / cursor.codeLength * 100) | 0); 512 | } 513 | }; 514 | 515 | var onPlayerAdvanceCursor = function(cursor) { 516 | scrollToCursor(cursor); 517 | updatePlayerProgress(cursor); 518 | emitCursorAdvance(); 519 | }; 520 | 521 | var scrollToCursor = function(cursor) { 522 | // Make sure the cursor DOM element exists 523 | if (cursor.cursor.length) { 524 | var windowHeight = $(window).height(), 525 | isAnimating = $('html, body').is(':animated'), 526 | cursorPos = cursor.cursor.offset().top, 527 | windowPos = $(window).scrollTop() + windowHeight; 528 | 529 | // Begin scrolling when 25% from the bottom 530 | if (windowPos - cursorPos < windowHeight * 0.25 && !isAnimating) { 531 | $('html, body').animate({ 532 | // Move to 25% from top 533 | scrollTop: cursorPos - windowHeight * 0.25 534 | }, 1000); 535 | } 536 | } 537 | }; 538 | 539 | var addInitialPlayer = function() { 540 | viewModel.game.players.push(new Player(user._id, 0, user.username)); 541 | }; 542 | 543 | var emitCursorAdvance = function() { 544 | socket.emit('ingame:advancecursor', { 545 | game: game._id, 546 | player: user._id 547 | }); 548 | }; 549 | 550 | var emitCursorRetreat = function() { 551 | socket.emit('ingame:retreatcursor', { 552 | game: game._id, 553 | player: user._id 554 | }); 555 | }; 556 | 557 | var completeGame = function(cursor) { 558 | game.isComplete = true; 559 | clearTimeout(timeId); 560 | lastTimestamp = null; 561 | 562 | console.log('emit ingame:complete'); 563 | socket.emit('ingame:complete', { 564 | time: state.time, 565 | keystrokes: cursor.keystrokes, 566 | mistakes: cursor.mistakes 567 | }); 568 | }; 569 | 570 | 571 | var timeId = null; 572 | var lastTimestamp = null; 573 | var updateTime = function() { 574 | if (game.starting && !game.isComplete) { 575 | // Synchronize with the time since start 576 | if (state.startTime) { 577 | state.time = moment().diff(state.startTime); 578 | } 579 | var t = moment.duration(state.time); 580 | var minutes = t.minutes(); 581 | var seconds = t.seconds(); 582 | seconds = state.time < 0 ? -seconds + 1 : seconds; 583 | 584 | viewModel.game.timer(sprintf('%s%d:%02d', 585 | state.time < 0 ? 'T-' : '', minutes, seconds)); 586 | viewModel.game.timerCss(state.time < 0 ? 'label-warning' : 'label-info'); 587 | 588 | // Increment with smaller granularity for the cruicial starting time 589 | if (lastTimestamp && !state.startTime) { 590 | state.time += moment().diff(lastTimestamp); 591 | } 592 | lastTimestamp = moment(); 593 | 594 | // Schedule the start of the game if close enough 595 | if (state.time > -2500 && state.time < 0) { 596 | setTimeout(startGame, -state.time); 597 | } 598 | 599 | timeId = setTimeout(updateTime, 100); 600 | } 601 | }; 602 | 603 | var resetStarting = function() { 604 | viewModel.game.gameStatus('Waiting for players...'); 605 | if (state.playerCursor) { 606 | state.playerCursor.destroy(); 607 | state.playerCursor = null; 608 | } 609 | 610 | clearTimeout(timeId); 611 | lastTimestamp = null; 612 | }; 613 | 614 | var wrapFullyStarted = function(fn) { 615 | return function() { 616 | if (fullyStarted) { 617 | fn.apply(this, arguments); 618 | } 619 | }; 620 | }; 621 | 622 | // Bind key events 623 | var keys = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; 624 | keys = keys.concat(_.map(keys, function(k) { return k.toUpperCase(); })); 625 | keys = keys.concat(['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']); 626 | keys = keys.concat(['`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=', '+', '[', '{', ']', '}', '\\', '|', '\'', '"', ';', ':', '/', '?', '.', '>', ',', '<']); 627 | keys = keys.concat(['enter', 'space', 'shift+space', 'shift+enter']); 628 | 629 | Mousetrap.bind(keys, wrapFullyStarted(function(e, key) { 630 | e.preventDefault(); 631 | 632 | key = _.contains(['space', 'shift+space'], key) ? ' ' : 633 | _.contains(['enter', 'shift+enter'], key) ? '\n' : 634 | key; 635 | 636 | state.playerCursor.processKey(key); 637 | })); 638 | 639 | Mousetrap.bind(['backspace', 'shift+backspace'], wrapFullyStarted(function(e, key) { 640 | e.preventDefault(); 641 | state.playerCursor.backspaceKey(); 642 | })); 643 | 644 | 645 | socket.on('ingame:ready:res', function(data) { 646 | console.log('received ingame:ready:res'); 647 | 648 | if (!data.success) { 649 | showAlert('Error: ' + data.err, true, function() { 650 | redirect('/lobby'); 651 | }); 652 | return; 653 | } 654 | 655 | game = data.game; 656 | exercise = data.exercise; 657 | state.code = data.exercise.typeableCode; 658 | state.time = data.timeLeft; 659 | nonTypeables = data.nonTypeables; 660 | viewModel.loaded(true); 661 | viewModel.loading(false); 662 | viewModel.game.isMultiplayer(!data.game.isSinglePlayer); 663 | viewModel.game.gameStatus('Waiting for players...'); 664 | viewModel.game.gamecode(data.exercise.code); 665 | viewModel.game.projectName(data.exercise.projectName); 666 | viewModel.game.langCss('language-' + data.game.lang); 667 | 668 | hljs.initHighlighting(); 669 | bindCodeCharacters(); 670 | addInitialPlayer(); 671 | checkGameState(); 672 | }); 673 | 674 | socket.on('ingame:update', function(data) { 675 | console.log('received ingame:update'); 676 | game = data.game; 677 | state.time = data.timeLeft; 678 | checkGameState(); 679 | }); 680 | 681 | socket.on('ingame:complete:res', function(data) { 682 | console.log('received ingame:complete:res'); 683 | game = data.game; 684 | 685 | var message; 686 | 687 | if (game.isSinglePlayer) { 688 | message = 'You completed the code! Well done!'; 689 | } else { 690 | if (game.winner === user._id) { 691 | message = 'Congratulations! You got 1st place!'; 692 | } else { 693 | message = 'Nicely done!'; 694 | } 695 | } 696 | 697 | viewModel.completionText(message); 698 | viewModel.stats.time(moment(data.stats.time).format('mm:ss')); 699 | viewModel.stats.speed(data.stats.speed | 0); 700 | viewModel.stats.typeables(data.stats.typeables | 0); 701 | viewModel.stats.keystrokes(data.stats.keystrokes | 0); 702 | viewModel.stats.percentUnproductive((data.stats.percentUnproductive * 100).toFixed(2)); 703 | viewModel.stats.speed(data.stats.speed | 0); 704 | viewModel.stats.mistakes(data.stats.mistakes | 0); 705 | 706 | var $dialog = $('#completion-dialog'); 707 | $dialog.on('shown.bs.modal', function() { 708 | var rows = $('#completion-dialog .row'), 709 | animIdx = 0; 710 | 711 | var animateSingle = function(row) { 712 | $(row).animate({ 713 | opacity: 1, 714 | left: 0 715 | }, { 716 | queue: true, 717 | duration: 200, 718 | complete: function() { 719 | enqueueAnimation(); 720 | } 721 | }); 722 | }; 723 | 724 | var enqueueAnimation = function() { 725 | if (animIdx < rows.length) { 726 | animateSingle(rows[animIdx++]); 727 | } 728 | }; 729 | 730 | enqueueAnimation(); 731 | }); 732 | $dialog.modal('show'); 733 | }); 734 | 735 | socket.on('ingame:advancecursor', function(data) { 736 | var opponent = data.player; 737 | if (opponent in state.opponentCursors) { 738 | state.opponentCursors[opponent].advanceCursor(); 739 | } 740 | }); 741 | 742 | socket.on('ingame:retreatcursor', function(data) { 743 | var opponent = data.player; 744 | if (opponent in state.opponentCursors) { 745 | state.opponentCursors[opponent].retreatCursor(); 746 | } 747 | }); 748 | 749 | console.log('emit ingame:ready'); 750 | socket.emit('ingame:ready', { player: user._id }); 751 | viewModel.loading(true); 752 | })(); 753 | -------------------------------------------------------------------------------- /public/js/libs/alertify.min.js: -------------------------------------------------------------------------------- 1 | /*! alertify - v0.3.11 - 2013-10-08 */ 2 | !function(a,b){"use strict";var c,d=a.document;c=function(){var c,e,f,g,h,i,j,k,l,m,n,o,p,q={},r={},s=!1,t={ENTER:13,ESC:27,SPACE:32},u=[];return r={buttons:{holder:'',submit:'',ok:'',cancel:''},input:'
',message:'

{{message}}

',log:'
{{message}}
'},p=function(){var a,c,e=!1,f=d.createElement("fakeelement"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"otransitionend",transition:"transitionend"};for(a in g)if(f.style[a]!==b){c=g[a],e=!0;break}return{type:c,supported:e}},c=function(a){return d.getElementById(a)},q={labels:{ok:"OK",cancel:"Cancel"},delay:5e3,buttonReverse:!1,buttonFocus:"ok",transition:b,addListeners:function(a){var b,c,i,j,k,l="undefined"!=typeof f,m="undefined"!=typeof e,n="undefined"!=typeof o,p="",q=this;b=function(b){return"undefined"!=typeof b.preventDefault&&b.preventDefault(),i(b),"undefined"!=typeof o&&(p=o.value),"function"==typeof a&&("undefined"!=typeof o?a(!0,p):a(!0)),!1},c=function(b){return"undefined"!=typeof b.preventDefault&&b.preventDefault(),i(b),"function"==typeof a&&a(!1),!1},i=function(){q.hide(),q.unbind(d.body,"keyup",j),q.unbind(g,"focus",k),l&&q.unbind(f,"click",b),m&&q.unbind(e,"click",c)},j=function(a){var d=a.keyCode;(d===t.SPACE&&!n||n&&d===t.ENTER)&&b(a),d===t.ESC&&m&&c(a)},k=function(){n?o.focus():!m||q.buttonReverse?f.focus():e.focus()},this.bind(g,"focus",k),this.bind(h,"focus",k),l&&this.bind(f,"click",b),m&&this.bind(e,"click",c),this.bind(d.body,"keyup",j),this.transition.supported||this.setFocus()},bind:function(a,b,c){"function"==typeof a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent&&a.attachEvent("on"+b,c)},handleErrors:function(){if("undefined"!=typeof a.onerror){var b=this;return a.onerror=function(a,c,d){b.error("["+a+" on line "+d+" of "+c+"]",0)},!0}return!1},appendButtons:function(a,b){return this.buttonReverse?b+a:a+b},build:function(a){var b="",c=a.type,d=a.message,e=a.cssClass||"";switch(b+='
',b+='Reset Focus',"none"===q.buttonFocus&&(b+=''),"prompt"===c&&(b+='
'),b+='
',b+=r.message.replace("{{message}}",d),"prompt"===c&&(b+=r.input),b+=r.buttons.holder,b+="
","prompt"===c&&(b+="
"),b+='Reset Focus',b+="
",c){case"confirm":b=b.replace("{{buttons}}",this.appendButtons(r.buttons.cancel,r.buttons.ok)),b=b.replace("{{ok}}",this.labels.ok).replace("{{cancel}}",this.labels.cancel);break;case"prompt":b=b.replace("{{buttons}}",this.appendButtons(r.buttons.cancel,r.buttons.submit)),b=b.replace("{{ok}}",this.labels.ok).replace("{{cancel}}",this.labels.cancel);break;case"alert":b=b.replace("{{buttons}}",r.buttons.ok),b=b.replace("{{ok}}",this.labels.ok)}return l.className="alertify alertify-"+c+" "+e,k.className="alertify-cover",b},close:function(a,b){var c,d,e=b&&!isNaN(b)?+b:this.delay,f=this;this.bind(a,"click",function(){c(a)}),d=function(a){a.stopPropagation(),f.unbind(this,f.transition.type,d),m.removeChild(this),m.hasChildNodes()||(m.className+=" alertify-logs-hidden")},c=function(a){"undefined"!=typeof a&&a.parentNode===m&&(f.transition.supported?(f.bind(a,f.transition.type,d),a.className+=" alertify-log-hide"):(m.removeChild(a),m.hasChildNodes()||(m.className+=" alertify-logs-hidden")))},0!==b&&setTimeout(function(){c(a)},e)},dialog:function(a,b,c,e,f){j=d.activeElement;var g=function(){m&&null!==m.scrollTop&&k&&null!==k.scrollTop||g()};if("string"!=typeof a)throw new Error("message must be a string");if("string"!=typeof b)throw new Error("type must be a string");if("undefined"!=typeof c&&"function"!=typeof c)throw new Error("fn must be a function");return this.init(),g(),u.push({type:b,message:a,callback:c,placeholder:e,cssClass:f}),s||this.setup(),this},extend:function(a){if("string"!=typeof a)throw new Error("extend method must have exactly one paramter");return function(b,c){return this.log(b,a,c),this}},hide:function(){var a,b=this;u.splice(0,1),u.length>0?this.setup(!0):(s=!1,a=function(c){c.stopPropagation(),b.unbind(l,b.transition.type,a)},this.transition.supported?(this.bind(l,this.transition.type,a),l.className="alertify alertify-hide alertify-hidden"):l.className="alertify alertify-hide alertify-hidden alertify-isHidden",k.className="alertify-cover alertify-cover-hidden",j.focus())},init:function(){d.createElement("nav"),d.createElement("article"),d.createElement("section"),null==c("alertify-cover")&&(k=d.createElement("div"),k.setAttribute("id","alertify-cover"),k.className="alertify-cover alertify-cover-hidden",d.body.appendChild(k)),null==c("alertify")&&(s=!1,u=[],l=d.createElement("section"),l.setAttribute("id","alertify"),l.className="alertify alertify-hidden",d.body.appendChild(l)),null==c("alertify-logs")&&(m=d.createElement("section"),m.setAttribute("id","alertify-logs"),m.className="alertify-logs alertify-logs-hidden",d.body.appendChild(m)),d.body.setAttribute("tabindex","0"),this.transition=p()},log:function(a,b,c){var d=function(){m&&null!==m.scrollTop||d()};return this.init(),d(),m.className="alertify-logs",this.notify(a,b,c),this},notify:function(a,b,c){var e=d.createElement("article");e.className="alertify-log"+("string"==typeof b&&""!==b?" alertify-log-"+b:""),e.innerHTML=a,m.appendChild(e),setTimeout(function(){e.className=e.className+" alertify-log-show"},50),this.close(e,c)},set:function(a){var b;if("object"!=typeof a&&a instanceof Array)throw new Error("args must be an object");for(b in a)a.hasOwnProperty(b)&&(this[b]=a[b])},setFocus:function(){o?(o.focus(),o.select()):i.focus()},setup:function(a){var d,j=u[0],k=this;s=!0,d=function(a){a.stopPropagation(),k.setFocus(),k.unbind(l,k.transition.type,d)},this.transition.supported&&!a&&this.bind(l,this.transition.type,d),l.innerHTML=this.build(j),g=c("alertify-resetFocus"),h=c("alertify-resetFocusBack"),f=c("alertify-ok")||b,e=c("alertify-cancel")||b,i="cancel"===q.buttonFocus?e:"none"===q.buttonFocus?c("alertify-noneFocus"):f,o=c("alertify-text")||b,n=c("alertify-form")||b,"string"==typeof j.placeholder&&""!==j.placeholder&&(o.value=j.placeholder),a&&this.setFocus(),this.addListeners(j.callback)},unbind:function(a,b,c){"function"==typeof a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent&&a.detachEvent("on"+b,c)}},{alert:function(a,b,c){return q.dialog(a,"alert",b,"",c),this},confirm:function(a,b,c){return q.dialog(a,"confirm",b,"",c),this},extend:q.extend,init:q.init,log:function(a,b,c){return q.log(a,b,c),this},prompt:function(a,b,c,d){return q.dialog(a,"prompt",b,c,d),this},success:function(a,b){return q.log(a,"success",b),this},error:function(a,b){return q.log(a,"error",b),this},set:function(a){q.set(a)},labels:q.labels,debug:q.handleErrors}},"function"==typeof define?define([],function(){return new c}):"undefined"==typeof a.alertify&&(a.alertify=new c)}(this); -------------------------------------------------------------------------------- /public/js/libs/favico.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license MIT 3 | * @fileOverview Favico animations 4 | * @author Miroslav Magda, http://blog.ejci.net 5 | * @version 0.3.4 6 | */ 7 | !function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||w)return!1;try{f.clearRect(0,0,h,s),f.drawImage(e,0,0,h,s)}catch(o){}p=setTimeout(t,k.duration,e),R.setIcon(c)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function i(){return document.hidden||document.msHidden||document.webkitHidden||document.mozHidden}e=e?e:{};var r,a,s,h,c,f,l,d,u,y,g,w,m,x,p,b={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1};m={},m.ff=/firefox/i.test(navigator.userAgent.toLowerCase()),m.chrome=/chrome/i.test(navigator.userAgent.toLowerCase()),m.opera=/opera/i.test(navigator.userAgent.toLowerCase()),m.ie=/msie/i.test(navigator.userAgent.toLowerCase())||/trident/i.test(navigator.userAgent.toLowerCase()),m.supported=m.chrome||m.ff||m.opera;var v=[];g=function(){},d=w=!1;var C=function(){r=n(b,e),r.bgColor=o(r.bgColor),r.textColor=o(r.textColor),r.position=r.position.toLowerCase(),r.animation=k.types[""+r.animation]?r.animation:b.animation;var t=r.position.indexOf("up")>-1,i=r.position.indexOf("left")>-1;if(t||i)for(var d=0;d0?l.height:32,h=l.width>0?l.width:32,c.height=s,c.width=h,f=c.getContext("2d"),M.ready()}):(l.setAttribute("src",""),s=32,h=32,l.height=s,l.width=h,c.height=s,c.width=h,f=c.getContext("2d"),M.ready())}catch(y){throw"Error initializing favico. Message: "+y.message}},M={};M.ready=function(){d=!0,M.reset(),g()},M.reset=function(){d&&(v=[],u=!1,f.clearRect(0,0,h,s),f.drawImage(l,0,0,h,s),R.setIcon(c),window.clearTimeout(x),window.clearTimeout(p))},M.start=function(){if(d&&!y){var e=function(){u=v[0],y=!1,v.length>0&&(v.shift(),M.start())};if(v.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in v[0].options&&(r[e]=v[0].options[e])}),k.run(v[0].options,function(){e()},!1)};u?k.run(u.options,function(){t()},!0):t()}}};var I={},A=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=h*e.x,e.y=s*e.y,e.w=h*e.w,e.h=s*e.h,e.len=(""+e.n).length,e};I.circle=function(e){e=A(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,h,s),f.drawImage(l,0,0,h,s),f.beginPath(),f.font=r.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+r.fontFamily,f.textAlign="center",t?(f.moveTo(e.x+e.w/2,e.y),f.lineTo(e.x+e.w-e.h/2,e.y),f.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),f.lineTo(e.x+e.w,e.y+e.h-e.h/2),f.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),f.lineTo(e.x+e.h/2,e.y+e.h),f.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),f.lineTo(e.x,e.y+e.h/2),f.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):f.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),f.fillStyle="rgba("+r.bgColor.r+","+r.bgColor.g+","+r.bgColor.b+","+e.o+")",f.fill(),f.closePath(),f.beginPath(),f.stroke(),f.fillStyle="rgba("+r.textColor.r+","+r.textColor.g+","+r.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()},I.rectangle=function(e){e=A(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,h,s),f.drawImage(l,0,0,h,s),f.beginPath(),f.font="bold "+Math.floor(e.h*(e.n>99?.9:1))+"px sans-serif",f.textAlign="center",f.fillStyle="rgba("+r.bgColor.r+","+r.bgColor.g+","+r.bgColor.b+","+e.o+")",f.fillRect(e.x,e.y,e.w,e.h),f.fillStyle="rgba("+r.textColor.r+","+r.textColor.g+","+r.textColor.b+","+e.o+")","number"==typeof e.n&&e.len>3?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()};var E=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},g=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&k.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&I[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),v.push(n),v.length>100)throw"Too many badges requests in queue.";M.start()}else M.reset()}catch(i){throw"Error setting badge. Message: "+i.message}},d&&g()},T=function(e){g=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),i=o/s>t/h?t/h:o/s;n.setAttribute("src",e.getAttribute("src")),n.height=o/i,n.width=t/i,f.clearRect(0,0,h,s),f.drawImage(n,0,0,h,s),R.setIcon(c)}catch(r){throw"Error setting image. Message: "+r.message}},d&&g()},L=function(e){g=function(){try{if("stop"===e)return w=!0,M.reset(),void(w=!1);e.addEventListener("play",function(){t(this)},!1)}catch(o){throw"Error setting video. Message: "+o.message}},d&&g()},U=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),m.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,g=function(){try{if("stop"===e)return w=!0,M.reset(),void(w=!1);o=document.createElement("video"),o.width=h,o.height=s,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw"Error setting webcam. Message: "+n.message}},d&&g()}},R={};R.getIcon=function(){var e=!1,t="",o=function(){for(var e=document.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};if(r.elementId?(e=document.getElementById(r.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=o(),e===!1&&(e=document.createElement("link"),e.setAttribute("rel","icon"),document.getElementsByTagName("head")[0].appendChild(e))),t=r.elementId?e.src:e.href,-1===t.indexOf(document.location.hostname))throw new Error("Error setting favicon. Favicon image is on different domain (Icon: "+t+", Domain: "+document.location.hostname+")");return e.setAttribute("type","image/png"),e},R.setIcon=function(e){var t=e.toDataURL("image/png");if(r.elementId)document.getElementById(r.elementId).setAttribute("src",t);else if(m.ff||m.opera){var o=a;a=document.createElement("link"),m.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),document.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),o.parentNode&&o.parentNode.removeChild(o)}else a.setAttribute("href",t)};var k={};return k.duration=40,k.types={},k.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.run=function(e,t,o,a){var s=k.types[i()?"none":r.animation];return a=o===!0?"undefined"!=typeof a?a:s.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a=0?(I[r.type](n(e,s[a])),x=setTimeout(function(){o?a-=1:a+=1,k.run(e,t,o,a)},k.duration),R.setIcon(c),void 0):void t()},C(),{badge:E,video:L,image:T,webcam:U,reset:M.reset}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}(); 8 | -------------------------------------------------------------------------------- /public/js/libs/lodash-1.3.1.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Lo-Dash 1.3.1 (Custom Build) lodash.com/license 4 | * Build: `lodash modern -o ./dist/lodash.js` 5 | * Underscore.js 1.4.4 underscorejs.org/LICENSE 6 | */ 7 | ;!function(n){function t(n,t,e){e=(e||0)-1;for(var r=n.length;++et||typeof n=="undefined")return 1;if(ne?0:e);++re?_e(0,a+e):e)||0,a&&typeof a=="number"?o=-1<(ht(n)?n.indexOf(t,e):u(n,t,e)):d(n,function(n){return++ra&&(a=i) 21 | }}else t=!t&&ht(n)?u:tt.createCallback(t,e),wt(n,function(n,e,u){e=t(n,e,u),e>r&&(r=e,a=n)});return a}function Ot(n,t){var e=-1,r=n?n.length:0;if(typeof r=="number")for(var u=Mt(r);++earguments.length;t=tt.createCallback(t,r,4);var a=-1,o=n.length;if(typeof o=="number")for(u&&(e=n[++a]);++aarguments.length; 22 | if(typeof u!="number")var o=Se(n),u=o.length;return t=tt.createCallback(t,r,4),wt(n,function(r,i,f){i=o?o[--u]:--u,e=a?(a=b,n[i]):t(e,n[i],i,f)}),e}function It(n,t,e){var r;t=tt.createCallback(t,e),e=-1;var u=n?n.length:0;if(typeof u=="number")for(;++e=w&&u===t;if(l){var c=o(i);c?(u=e,i=c):l=b}for(;++ru(i,c)&&f.push(c); 23 | return l&&p(i),f}function Nt(n,t,e){if(n){var r=0,u=n.length;if(typeof t!="number"&&t!=h){var a=-1;for(t=tt.createCallback(t,e);++ar?_e(0,u+r):r||0}else if(r)return r=Ft(n,e),n[r]===e?r:-1;return n?t(n,e,r):-1}function Bt(n,t,e){if(typeof t!="number"&&t!=h){var r=0,u=-1,a=n?n.length:0;for(t=tt.createCallback(t,e);++u>>1,e(n[r])e?0:e);++tl&&(i=n.apply(f,o));else{var e=new Vt;!s&&!m&&(c=e);var r=p-(e-c);0/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:N,variable:"",imports:{_:tt}};var Ee=he,Se=de?function(n){return gt(n)?de(n):[]}:Z,Ie={"&":"&","<":"<",">":">",'"':""","'":"'"},Ae=pt(Ie),Ut=ot(function $e(n,t,e){for(var r=-1,u=n?n.length:0,a=[];++r=w&&i===t,g=u||v?f():s;if(v){var y=o(g);y?(i=e,g=y):(v=b,g=u?g:(c(g),s))}for(;++ai(g,h))&&((u||v)&&g.push(h),s.push(y))}return v?(c(g.b),p(g)):u&&c(g),s});return Ht&&Y&&typeof pe=="function"&&(zt=qt(pe,r)),pe=8==je(B+"08")?je:function(n,t){return je(ht(n)?n.replace(F,""):n,t||0)},tt.after=function(n,t){return 1>n?t():function(){return 1>--n?t.apply(this,arguments):void 0 29 | }},tt.assign=X,tt.at=function(n){for(var t=-1,e=ae.apply(Zt,Ce.call(arguments,1)),r=e.length,u=Mt(r);++t=w&&o(a?r[a]:y)}n:for(;++l(b?e(b,h):s(y,h))){for(a=u,(b||y).push(h);--a;)if(b=i[a],0>(b?e(b,h):s(r[a],h)))continue n;g.push(h)}}for(;u--;)(b=i[u])&&p(b);return c(i),c(y),g 33 | },tt.invert=pt,tt.invoke=function(n,t){var e=Ce.call(arguments,2),r=-1,u=typeof t=="function",a=n?n.length:0,o=Mt(typeof a=="number"?a:0);return wt(n,function(n){o[++r]=(u?t:n[t]).apply(n,e)}),o},tt.keys=Se,tt.map=Ct,tt.max=xt,tt.memoize=function(n,t){function e(){var r=e.cache,u=j+(t?t.apply(this,arguments):arguments[0]);return le.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}return e.cache={},e},tt.merge=bt,tt.min=function(n,t,e){var r=1/0,a=r;if(!t&&Ee(n)){e=-1;for(var o=n.length;++er(o,e))&&(a[e]=n)}),a},tt.once=function(n){var t,e;return function(){return t?e:(t=y,e=n.apply(this,arguments),n=h,e)}},tt.pairs=function(n){for(var t=-1,e=Se(n),r=e.length,u=Mt(r);++te?_e(0,r+e):ke(e,r-1))+1);r--;)if(n[r]===t)return r;return-1},tt.mixin=Pt,tt.noConflict=function(){return r._=te,this},tt.parseInt=pe,tt.random=function(n,t){n==h&&t==h&&(t=1),n=+n||0,t==h?(t=n,n=0):t=+t||0; 42 | var e=we();return n%1||t%1?n+ke(e*(t-n+parseFloat("1e-"+((e+"").length-1))),t):n+oe(e*(t-n+1))},tt.reduce=Et,tt.reduceRight=St,tt.result=function(n,t){var e=n?n[t]:g;return vt(e)?n[t]():e},tt.runInContext=v,tt.size=function(n){var t=n?n.length:0;return typeof t=="number"?t:Se(n).length},tt.some=It,tt.sortedIndex=Ft,tt.template=function(n,t,e){var r=tt.templateSettings;n||(n=""),e=Q({},e,r);var u,a=Q({},e.imports,r.imports),r=Se(a),a=mt(a),o=0,f=e.interpolate||R,l="__p+='",f=Qt((e.escape||R).source+"|"+f.source+"|"+(f===N?I:R).source+"|"+(e.evaluate||R).source+"|$","g"); 43 | n.replace(f,function(t,e,r,a,f,c){return r||(r=a),l+=n.slice(o,c).replace(q,i),e&&(l+="'+__e("+e+")+'"),f&&(u=y,l+="';"+f+";__p+='"),r&&(l+="'+((__t=("+r+"))==null?'':__t)+'"),o=c+t.length,t}),l+="';\n",f=e=e.variable,f||(e="obj",l="with("+e+"){"+l+"}"),l=(u?l.replace(x,""):l).replace(O,"$1").replace(E,"$1;"),l="function("+e+"){"+(f?"":e+"||("+e+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}";try{var c=Gt(r,"return "+l).apply(g,a) 44 | }catch(p){throw p.source=l,p}return t?c(t):(c.source=l,c)},tt.unescape=function(n){return n==h?"":Xt(n).replace(S,ft)},tt.uniqueId=function(n){var t=++_;return Xt(n==h?"":n)+t},tt.all=_t,tt.any=It,tt.detect=jt,tt.findWhere=jt,tt.foldl=Et,tt.foldr=St,tt.include=dt,tt.inject=Et,d(tt,function(n,t){tt.prototype[t]||(tt.prototype[t]=function(){var t=[this.__wrapped__];return ce.apply(t,arguments),n.apply(tt,t)})}),tt.first=Nt,tt.last=function(n,t,e){if(n){var r=0,u=n.length;if(typeof t!="number"&&t!=h){var a=u; 45 | for(t=tt.createCallback(t,e);a--&&t(n[a],a,n);)r++}else if(r=t,r==h||e)return n[u-1];return s(n,_e(0,u-r))}},tt.take=Nt,tt.head=Nt,d(tt,function(n,t){tt.prototype[t]||(tt.prototype[t]=function(t,e){var r=n(this.__wrapped__,t,e);return t==h||e&&typeof t!="function"?r:new et(r)})}),tt.VERSION="1.3.1",tt.prototype.toString=function(){return Xt(this.__wrapped__)},tt.prototype.value=Kt,tt.prototype.valueOf=Kt,wt(["join","pop","shift"],function(n){var t=Zt[n];tt.prototype[n]=function(){return t.apply(this.__wrapped__,arguments) 46 | }}),wt(["push","reverse","sort","unshift"],function(n){var t=Zt[n];tt.prototype[n]=function(){return t.apply(this.__wrapped__,arguments),this}}),wt(["concat","slice","splice"],function(n){var t=Zt[n];tt.prototype[n]=function(){return new et(t.apply(this.__wrapped__,arguments))}}),tt}var g,y=!0,h=null,b=!1,m=[],d=[],_=0,k={},j=+new Date+"",w=75,C=40,x=/\b__p\+='';/g,O=/\b(__p\+=)''\+/g,E=/(__e\(.*?\)|\b__t\))\+'';/g,S=/&(?:amp|lt|gt|quot|#39);/g,I=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,A=/\w*$/,N=/<%=([\s\S]+?)%>/g,$=($=/\bthis\b/)&&$.test(v)&&$,B=" \t\x0B\f\xa0\ufeff\n\r\u2028\u2029\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000",F=RegExp("^["+B+"]*0+(?=.$)"),R=/($^)/,T=/[&<>"']/g,q=/['\n\r\t\u2028\u2029\\]/g,D="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setImmediate setTimeout".split(" "),z="[object Arguments]",W="[object Array]",P="[object Boolean]",K="[object Date]",M="[object Function]",U="[object Number]",V="[object Object]",G="[object RegExp]",H="[object String]",J={}; 47 | J[M]=b,J[z]=J[W]=J[P]=J[K]=J[U]=J[V]=J[G]=J[H]=y;var L={"boolean":b,"function":y,object:y,number:b,string:b,undefined:b},Q={"\\":"\\","'":"'","\n":"n","\r":"r","\t":"t","\u2028":"u2028","\u2029":"u2029"},X=L[typeof exports]&&exports,Y=L[typeof module]&&module&&module.exports==X&&module,Z=L[typeof global]&&global;!Z||Z.global!==Z&&Z.window!==Z||(n=Z);var nt=v();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(n._=nt, define(function(){return nt})):X&&!X.nodeType?Y?(Y.exports=nt)._=nt:X._=nt:n._=nt 48 | }(this); 49 | -------------------------------------------------------------------------------- /public/js/libs/moment-2.1.0.min.js: -------------------------------------------------------------------------------- 1 | // moment.js 2 | // version : 2.1.0 3 | // author : Tim Wood 4 | // license : MIT 5 | // momentjs.com 6 | !function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+36e5*r,this._days=i+7*s,this._months=n+12*e,this._data={},this._bubble()}function a(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function o(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e){for(var n=t+"";n.lengthn;n++)~~t[n]!==~~e[n]&&r++;return r+i}function f(t){return t?ie[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function l(t,e){return e.abbr=t,x[t]||(x[t]=new s),x[t].set(e),x[t]}function _(t){if(!t)return H.fn._lang;if(!x[t]&&A)try{require("./lang/"+t)}catch(e){return H.fn._lang}return x[t]}function m(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,n,s=t.match(E);for(e=0,n=s.length;n>e;e++)s[e]=ue[s[e]]?ue[s[e]]:m(s[e]);return function(i){var r="";for(e=0;n>e;e++)r+=s[e]instanceof Function?s[e].call(i,t):s[e];return r}}function M(t,e){function n(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&N.test(e);)e=e.replace(N,n);return re[e]||(re[e]=y(e)),re[e](t)}function g(t,e){switch(t){case"DDDD":return V;case"YYYY":return X;case"YYYYY":return $;case"S":case"SS":case"SSS":case"DDD":return I;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return R;case"a":case"A":return _(e._l)._meridiemParse;case"X":return B;case"Z":case"ZZ":return j;case"T":return q;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return J;default:return new RegExp(t.replace("\\",""))}}function p(t){var e=(j.exec(t)||[])[0],n=(e+"").match(ee)||["-",0,0],s=+(60*n[1])+~~n[2];return"+"===n[0]?-s:s}function D(t,e,n){var s,i=n._a;switch(t){case"M":case"MM":i[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=_(n._l).monthsParse(e),null!=s?i[1]=s:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(i[2]=~~e);break;case"YY":i[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":i[0]=~~e;break;case"a":case"A":n._isPm=_(n._l).isPM(e);break;case"H":case"HH":case"h":case"hh":i[3]=~~e;break;case"m":case"mm":i[4]=~~e;break;case"s":case"ss":i[5]=~~e;break;case"S":case"SS":case"SSS":i[6]=~~(1e3*("0."+e));break;case"X":n._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":n._useUTC=!0,n._tzm=p(e)}null==e&&(n._isValid=!1)}function Y(t){var e,n,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),n=new Date(0),t._useUTC?(n.setUTCFullYear(s[0],s[1],s[2]),n.setUTCHours(s[3],s[4],s[5],s[6])):(n.setFullYear(s[0],s[1],s[2]),n.setHours(s[3],s[4],s[5],s[6])),t._d=n}}function w(t){var e,n,s=t._f.match(E),i=t._i;for(t._a=[],e=0;eo&&(u=o,s=n);a(t,s)}function v(t){var e,n=t._i,s=K.exec(n);if(s){for(t._f="YYYY-MM-DD"+(s[2]||" "),e=0;4>e;e++)if(te[e][1].exec(n)){t._f+=te[e][0];break}j.exec(n)&&(t._f+=" Z"),w(t)}else t._d=new Date(n)}function T(e){var n=e._i,s=G.exec(n);n===t?e._d=new Date:s?e._d=new Date(+s[1]):"string"==typeof n?v(e):d(n)?(e._a=n.slice(0),Y(e)):e._d=n instanceof Date?new Date(+n):new Date(n)}function b(t,e,n,s,i){return i.relativeTime(e||1,!!n,t,s)}function S(t,e,n){var s=W(Math.abs(t)/1e3),i=W(s/60),r=W(i/60),a=W(r/24),o=W(a/365),u=45>s&&["s",s]||1===i&&["m"]||45>i&&["mm",i]||1===r&&["h"]||22>r&&["hh",r]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",W(a/30)]||1===o&&["y"]||["yy",o];return u[2]=e,u[3]=t>0,u[4]=n,b.apply({},u)}function F(t,e,n){var s,i=n-e,r=n-t.day();return r>i&&(r-=7),i-7>r&&(r+=7),s=H(t).add("d",r),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function O(t){var e=t._i,n=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=_().preparse(e)),H.isMoment(e)?(t=a({},e),t._d=new Date(+e._d)):n?d(n)?k(t):w(t):T(t),new i(t))}function z(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var n=this._isUTC?"UTC":"";return null!=t?(this._d["set"+n+e](t),H.updateOffset(this),this):this._d["get"+n+e]()}}function C(t){H.duration.fn[t]=function(){return this._data[t]}}function L(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,P,U="2.1.0",W=Math.round,x={},A="undefined"!=typeof module&&module.exports,G=/^\/?Date\((\-?\d+)/i,Z=/(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,E=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,N=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,J=/\d\d?/,I=/\d{1,3}/,V=/\d{3}/,X=/\d{1,4}/,$=/[+\-]?\d{1,6}/,R=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,j=/Z|[\+\-]\d\d:?\d\d/i,q=/T/i,B=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,Q="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ne="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ie={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",M:"month",y:"year"},re={},ae="DDD w W M D d".split(" "),oe="M D H h m s w W".split(" "),ue={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return u(~~(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(t/60),2)+":"+u(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ae.length;)P=ae.pop(),ue[P+"o"]=n(ue[P],P);for(;oe.length;)P=oe.pop(),ue[P+P]=e(ue[P],2);for(ue.DDDD=e(ue.DDD,3),s.prototype={set:function(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,n,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(n=H([2e3,e]),s="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,n,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(n=H([2e3,1]).day(e),s="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase()[0]},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var n=this._calendar[t];return"function"==typeof n?n.apply(e):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,n,s){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,s):i.replace(/%d/i,t)},pastFuture:function(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return F(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}},H=function(t,e,n){return O({_i:t,_f:e,_l:n,_isUTC:!1})},H.utc=function(t,e,n){return O({_useUTC:!0,_isUTC:!0,_l:n,_i:t,_f:e})},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var n,s,i=H.isDuration(t),a="number"==typeof t,o=i?t._input:a?{}:t,u=Z.exec(t);return a?e?o[e]=t:o.milliseconds=t:u&&(n="-"===u[1]?-1:1,o={y:0,d:~~u[2]*n,h:~~u[3]*n,m:~~u[4]*n,s:~~u[5]*n,ms:~~u[6]*n}),s=new r(o),i&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},H.version=U,H.defaultFormat=Q,H.updateOffset=function(){},H.lang=function(t,e){return t?(e?l(t,e):x[t]||_(t),H.duration.fn._lang=H.fn._lang=_(t),void 0):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),_(t)},H.isMoment=function(t){return t instanceof i},H.isDuration=function(t){return t instanceof r},H.fn=i.prototype={clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return M(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!c(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=M(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,1),this},subtract:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,-1),this},diff:function(t,e,n){var s,i,r=this._isUTC?H(t).zone(this._offset||0):H(t).local(),a=6e4*(this.zone()-r.zone());return e=f(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+r.daysInMonth()),i=12*(this.year()-r.year())+(this.month()-r.month()),i+=(this-H(this).startOf("month")-(r-H(r).startOf("month")))/s,i-=6e4*(this.zone()-H(this).startOf("month").zone()-(r.zone()-H(r).startOf("month").zone()))/s,"year"===e&&(i/=12)):(s=this-r,i="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),n?i:o(i)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=p(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&h(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=F(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=F(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=F(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},lang:function(e){return e===t?this._lang:(this._lang=_(e),this)}},P=0;Pf||k.hasOwnProperty(f)&&(q[k[f]]=f)}e=q[c]?"keydown":"keypress"}"keypress"==e&&g.length&&(e="keydown");return{key:d,modifiers:g,action:e}}function E(a,b,c,d,e){r[a+":"+c]=b;a=a.replace(/\s+/g," ");var g=a.split(" ");1":".","?":"/","|":"\\"},F={option:"alt",command:"meta","return":"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},q,m={},r={},p={},C,y=!1,H=!1,u=!1,h=1;20>h;++h)k[111+h]="f"+h;for(h=0;9>=h;++h)k[h+96]=h;s(document,"keypress",x);s(document,"keydown",x);s(document,"keyup",x);var n={bind:function(a,b,c){a=a instanceof Array?a:[a];for(var d=0;d 1) { 487 | game.starting = true; 488 | game.startTime = moment().add(GAME_MULTI_PLAYER_WAIT_TIME, 'seconds').toDate(); 489 | game.setupTiming(); 490 | } 491 | } 492 | } 493 | }; 494 | 495 | GameSchema.methods.setupTiming = function() { 496 | var game = this; 497 | var timeLeft = moment(game.startTime).diff(moment()); 498 | if (game.isJoinable) { 499 | if (timeLeft > GAME_TIME_JOIN_CUTOFF_MS) { 500 | gameTimeouts.create(game.id, timeLeft - GAME_TIME_JOIN_CUTOFF_MS + 1, function() { 501 | Game.findById(game.id, function(err, game) { 502 | if (err) { 503 | util.log(err); 504 | } 505 | game.isJoinable = false; 506 | game.updateGameState(); 507 | }); 508 | }); 509 | } else { 510 | game.isJoinable = false; 511 | } 512 | } else { 513 | if (timeLeft > 0) { 514 | gameTimeouts.create(game.id, timeLeft + 1, function() { 515 | Game.findById(game.id, function(err, game) { 516 | if (err) { 517 | util.log(err); 518 | } 519 | game.start(); 520 | game.updateGameState(); 521 | }); 522 | }); 523 | } else { 524 | game.start(); 525 | } 526 | } 527 | }; 528 | 529 | GameSchema.methods.setStatus = function(status) { 530 | var bindings = { 531 | 'waiting': 'Waiting', 532 | 'ingame': 'In game' 533 | }; 534 | this.status = status; 535 | this.statusText = bindings[status]; 536 | }; 537 | 538 | GameSchema.methods.getJoinError = function(player) { 539 | var game = this; 540 | if (!game.isJoinable && !game.isSinglePlayer) { 541 | return 'this game is full, or has been removed'; 542 | } 543 | var playerIds = _.map(game.players, function(p) { 544 | return p.toHexString(); 545 | }); 546 | if (_.contains(playerIds, player.id)) { 547 | return 'you are already inside another game!'; 548 | } 549 | return undefined; 550 | }; 551 | 552 | GameSchema.methods.addPlayer = function(player, callback) { 553 | var game = this; 554 | 555 | game.players.push(player._id); 556 | game.playerNames.push(player.username); 557 | game.numPlayers += 1; 558 | game.updateGameState(callback); 559 | }; 560 | 561 | GameSchema.methods.removePlayer = function(player, callback) { 562 | var game = this; 563 | game.players.remove(player._id); 564 | game.playerNames.remove(player.username); 565 | game.numPlayers = game.numPlayers <= 0 ? 0 : game.numPlayers - 1; 566 | game.updateGameState(callback); 567 | }; 568 | 569 | GameSchema.methods.start = function() { 570 | var game = this; 571 | game.started = true; 572 | game.startingPlayers = game.players.slice(); 573 | game.setStatus('ingame'); 574 | game.isJoinable = false; 575 | }; 576 | 577 | GameSchema.methods.finish = function() { 578 | var game = this; 579 | game.isComplete = true; 580 | game.isViewable = false; 581 | game.isJoinable = false; 582 | gameTimeouts.remove(game.id); 583 | }; 584 | 585 | GameSchema.methods.updateStatistics = function(stats, callback) { 586 | var game = this; 587 | if (game.winnerTime !== stats.time && 588 | Math.min(game.winnerTime || Infinity, stats.time || Infinity) === stats.time) { 589 | 590 | game.winner = stats.player; 591 | game.winnerTime = stats.time; 592 | game.winnerSpeed = stats.speed; 593 | 594 | game.save(function(err) { 595 | if (err) { 596 | util.log(err); 597 | return callback('error saving game'); 598 | } 599 | return callback(null, game); 600 | }); 601 | } else { 602 | return callback(null, game); 603 | } 604 | }; 605 | 606 | GameSchema.statics.resetIncomplete = function() { 607 | var games = this; 608 | games.update({ 609 | $or: [{ isComplete: false }, { isViewable: true }] 610 | }, { 611 | isComplete: true, 612 | isViewable: false, 613 | numPlayers: 0, 614 | players: [], 615 | isJoinable: false, 616 | wasReset: true 617 | }, { 618 | multi: true 619 | }, function(err) { 620 | if (err) { 621 | util.log(err); 622 | } 623 | util.log('games reset'); 624 | }); 625 | }; 626 | 627 | var StatsSchema = new Schema({ 628 | player: { type: Schema.ObjectId, required: true }, 629 | game: { type: Schema.ObjectId, required: true }, 630 | time: { type: Number }, 631 | speed: { type: Number }, 632 | typeables: { type: Number }, 633 | keystrokes: { type: Number }, 634 | percentUnproductive: { type: Number }, 635 | mistakes: { type: Number } 636 | }, { usePushEach: true }); 637 | 638 | StatsSchema.methods.updateStatistics = function(callback) { 639 | var stats = this; 640 | 641 | User.findById(stats.player, function(err, user) { 642 | if (err) { 643 | util.log(err); 644 | return callback(err); 645 | } 646 | Game.findById(stats.game, function(err, game) { 647 | if (err) { 648 | util.log(err); 649 | return callback(err); 650 | } 651 | if (game) { 652 | Exercise.findById(game.exercise, function(err, exercise) { 653 | if (err) { 654 | util.log(err); 655 | return callback(err); 656 | } 657 | 658 | // Clamp input to the real time difference, if it is beyond 659 | // a threshold 660 | var realTime = moment().diff(game.startTime, 'milliseconds'); 661 | if (Math.abs(realTime - stats.time) > STATISTICS_VALIDATION_THRESHOLD_MS) { 662 | stats.time = realTime; 663 | } 664 | 665 | stats.typeables = exercise.typeables; 666 | stats.speed = (stats.typeables / CHARACTERS_PER_WORD) * 667 | (1 / (stats.time / MILLISECONDS_PER_MINUTE)); 668 | stats.percentUnproductive = 1 - stats.typeables / stats.keystrokes; 669 | 670 | stats.save(function(err) { 671 | if (err) { 672 | util.log(err); 673 | } 674 | }); 675 | game.updateStatistics(stats, function(err) { 676 | if (err) { 677 | util.log(err); 678 | return callback(err); 679 | } 680 | user.updateStatistics(stats, game, function(err) { 681 | if (err) { 682 | util.log(err); 683 | return callback(err); 684 | } 685 | return callback(null, stats, user, game); 686 | }); 687 | }); 688 | }); 689 | } 690 | }); 691 | }); 692 | }; 693 | 694 | var User = mongoose.model('User', UserSchema); 695 | var Lang = mongoose.model('Lang', LangSchema); 696 | var Project = mongoose.model('Project', ProjectSchema); 697 | var Exercise = mongoose.model('Exercise', ExerciseSchema); 698 | var Game = mongoose.model('Game', GameSchema); 699 | var Stats = mongoose.model('Stats', StatsSchema); 700 | 701 | module.exports.User = User; 702 | module.exports.Lang = Lang; 703 | module.exports.Project = Project; 704 | module.exports.Exercise = Exercise; 705 | module.exports.Game = Game; 706 | module.exports.Stats = Stats; 707 | 708 | module.exports.NON_TYPEABLES = NON_TYPEABLES; 709 | module.exports.NON_TYPEABLE_CLASSES = NON_TYPEABLE_CLASSES; 710 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | var _ = require('lodash'); 5 | var models = require('./models'); 6 | 7 | /* 8 | * GET playnow. 9 | */ 10 | exports.playnow = function(req, res) { 11 | if (req.isAuthenticated()) { 12 | res.redirect('/lobby'); 13 | return; 14 | } 15 | 16 | var maybeGenerateAnonymous = function() { 17 | var user = new models.User({ 18 | isAnonymous: true, 19 | username: models.User.generateAnonymousUsername() 20 | }); 21 | user.save(function(err, user) { 22 | if (err) { 23 | util.log(err); 24 | return maybeGenerateAnonymous(); 25 | } 26 | req.logIn(user, function(err) { 27 | return res.redirect('/lobby'); 28 | }); 29 | }); 30 | }; 31 | 32 | maybeGenerateAnonymous(); 33 | }; 34 | 35 | /* 36 | * GET lobby page. 37 | */ 38 | 39 | exports.lobby = function(req, res) { 40 | models.Lang.find({}, 'key name', { sort: { order: 1 } }, function(err, docs) { 41 | if (err) { 42 | util.log(err); 43 | util.log('Langs not found'); return; 44 | } 45 | res.render('lobby', { 46 | title: 'Lobby', 47 | langs: docs 48 | }); 49 | }); 50 | }; 51 | 52 | 53 | /* 54 | * GET game page. 55 | */ 56 | 57 | exports.game = function(req, res) { 58 | if (!req.user.isAllowedIngame) { 59 | return res.redirect('/lobby'); 60 | } 61 | 62 | req.user.isAllowedIngame = false; 63 | req.user.save(); 64 | 65 | res.render('game', { 66 | title: 'Game' 67 | }); 68 | }; 69 | 70 | /* 71 | * GET about page. 72 | */ 73 | 74 | exports.about = function(req, res) { 75 | models.Project.find({}, null, { sort: { name: 1 } }, function(err, docs) { 76 | if (err) { 77 | util.log(err); 78 | util.log('Projects not found'); return; 79 | } 80 | res.render('about', { 81 | title: 'About', 82 | projects: docs 83 | }); 84 | }); 85 | }; 86 | 87 | /* 88 | * GET signup page. 89 | */ 90 | 91 | exports.signup = function(req, res) { 92 | if (req.isAuthenticated()) { 93 | res.redirect('/lobby'); 94 | return; 95 | } 96 | 97 | res.render('signup', { 98 | title: 'Signup', 99 | error: req.flash('error') 100 | }); 101 | }; 102 | 103 | /* 104 | * POST create-account. 105 | */ 106 | 107 | exports.createAccount = function(req, res) { 108 | var reportError = function(msg) { 109 | req.flash('error', msg); 110 | return res.redirect('/signup'); 111 | }; 112 | var username = req.body.username, 113 | password = req.body.password; 114 | 115 | if (!username || !password) { 116 | return reportError('Both username and password are required.'); 117 | } else if (username.length < 2 || username.length > 32) { 118 | return reportError('The username must be between 2 and 32 characters long.'); 119 | } else if (password.length < 8) { 120 | return reportError('The password must be at least 8 characters long.'); 121 | } 122 | 123 | models.User.findOne({ username: username }, function(err, user) { 124 | if (err) { 125 | util.log(err); 126 | return reportError('An error occurred on the server.'); 127 | } 128 | // Create a new user if none exists 129 | if (!user) { 130 | user = new models.User({ username: username, password: password }); 131 | user.save(function(err, saved) { 132 | req.logIn(user, function(err) { 133 | return res.redirect('/lobby'); 134 | }); 135 | }); 136 | } else { 137 | return reportError('That username is already in use.'); 138 | } 139 | }); 140 | }; 141 | 142 | /* 143 | * GET admin page. 144 | */ 145 | 146 | exports.admin = function(req, res) { 147 | res.render('admin', { 148 | title: 'Admin', 149 | error: req.flash('error') 150 | }); 151 | }; 152 | 153 | /* 154 | * POST admin/add-lang. 155 | */ 156 | 157 | exports.addLang = function(req, res) { 158 | var done = function(err) { 159 | if (err) { 160 | req.flash('error', err); 161 | } 162 | res.redirect('/admin'); 163 | }; 164 | 165 | var eachZipped = function(zipped, fn) { 166 | for (var i = 0, l = zipped.length; i < l; i++) { 167 | fn.apply(zipped, zipped[i]); 168 | } 169 | }; 170 | 171 | var getRequestArray = function(key, l) { 172 | var collection = [], 173 | i, v; 174 | for (i = 0; i < l; i++) { 175 | v = req.body[key + i]; 176 | if (v) { 177 | collection.push(v); 178 | } 179 | } 180 | return collection; 181 | }; 182 | 183 | var allSame = function(array) { 184 | if (array.length > 0) { 185 | for (var i = 0; i < array.length; i++) { 186 | if (array[i] !== array[0]) { 187 | return false; 188 | } 189 | } 190 | } 191 | return true; 192 | }; 193 | 194 | var langKey = req.body.key, 195 | langName = req.body.name, 196 | order = req.body.order, 197 | projectCount = parseInt(req.body.projectCount, 10), 198 | exerciseCount = parseInt(req.body.exerciseCount, 10), 199 | projectKey = getRequestArray('projectKey', projectCount), 200 | projectName = getRequestArray('projectName', projectCount), 201 | projectUrl = getRequestArray('projectUrl', projectCount), 202 | projectCodeUrl = getRequestArray('projectCodeUrl', projectCount), 203 | projectLicenseUrl = getRequestArray('projectLicenseUrl', projectCount), 204 | exerciseProject = getRequestArray('exerciseProject', exerciseCount), 205 | exerciseName = getRequestArray('exerciseName', exerciseCount), 206 | code = getRequestArray('code', exerciseCount); 207 | 208 | if (_.all([langKey, langName, order, projectKey, projectName, projectUrl, projectCodeUrl, projectLicenseUrl, exerciseProject, exerciseName, code]) && 209 | allSame(_.pluck([projectKey, projectName, projectUrl, projectCodeUrl, projectLicenseUrl], 'length')) && 210 | allSame(_.pluck([exerciseProject, exerciseName, code], 'length'))) { 211 | 212 | var lang = new models.Lang({ 213 | key: langKey, 214 | name: langName, 215 | order: order 216 | }); 217 | 218 | var exercises = []; 219 | eachZipped(_.zip(exerciseName, code), function(exerciseName, code) { 220 | exercises.push({ 221 | lang: langKey, 222 | exerciseName: exerciseName, 223 | code: code 224 | }); 225 | }); 226 | 227 | var projects = []; 228 | eachZipped(_.zip(projectKey, projectName, projectUrl, projectCodeUrl, projectLicenseUrl), function(key, name, url, codeUrl, licenseUrl) { 229 | projects.push({ 230 | key: key, 231 | name: name, 232 | url: url, 233 | codeUrl: codeUrl, 234 | licenseUrl: licenseUrl, 235 | lang: langKey, 236 | langName: langName 237 | }); 238 | }); 239 | 240 | models.Project.create(projects, function(err) { 241 | if (err) { 242 | util.log(err); 243 | util.log('addLang error'); 244 | return done(err); 245 | } 246 | 247 | var projects = Array.prototype.slice.call(arguments, 1); 248 | _.each(_.map(exerciseProject, function(p) { return parseInt(p, 10); }), function(project, i) { 249 | exercises[i].project = projects[project]._id; 250 | exercises[i].projectName = projects[project].name; 251 | }); 252 | 253 | models.Exercise.create(exercises, function(err) { 254 | if (err) { 255 | util.log(err); 256 | util.log('addLang error'); 257 | return done(err); 258 | } 259 | _.each(Array.prototype.slice.call(arguments, 1), function(exercise) { 260 | lang.exercises.push(exercise._id); 261 | }); 262 | 263 | lang.save(function(err) { 264 | return done(err); 265 | }); 266 | }); 267 | }); 268 | } else { 269 | return done('Did not pass validation.'); 270 | } 271 | }; 272 | 273 | /* 274 | * POST admin/reinit-exercises. 275 | */ 276 | 277 | exports.reinitExercises = function(req, res) { 278 | var done = function(err) { 279 | var result = { 280 | success: !err 281 | }; 282 | res.write(JSON.stringify(result)); 283 | res.end(); 284 | }; 285 | 286 | models.Exercise.find({}, function(err, exercises) { 287 | if (err) { 288 | util.log(err); 289 | util.log('reinitLang error'); 290 | return done(err); 291 | } 292 | var total = exercises.length, 293 | saveCount = 0; 294 | 295 | _.each(exercises, function(exercise) { 296 | exercise.initialize(); 297 | exercise.save(function(err, saved) { 298 | if (err) { 299 | util.log(err); 300 | util.log('reinitLang error'); 301 | } 302 | saveCount++; 303 | if (saveCount === total) { 304 | return done(); 305 | } 306 | }); 307 | }); 308 | }); 309 | }; 310 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var express = require('express'); 5 | var io = require('socket.io'); 6 | var http = require('http'); 7 | var helmet = require('helmet'); 8 | 9 | var path = require('path'); 10 | var util = require('util'); 11 | var _ = require('lodash'); 12 | var fs = require('fs'); 13 | 14 | var settings; 15 | try { 16 | settings = require('./settings'); 17 | } catch (e) { 18 | settings = {}; 19 | } 20 | 21 | var db = require('./db'); 22 | var routes = require('./routes'); 23 | var models = require('./models'); 24 | var sockets = require('./sockets'); 25 | 26 | var SwiftCODEConfig = require('./config'); 27 | 28 | var mongoose = require('mongoose'); 29 | 30 | var SessionStore = require('session-mongoose')(express); 31 | 32 | // Auth libs 33 | var passport = require('passport'); 34 | var LocalStrategy = require('passport-local').Strategy; 35 | var flash = require('connect-flash'); 36 | 37 | var requireHTTPS = function(req, res, next) { 38 | if (req.headers['x-forwarded-proto'] != 'https') { 39 | return res.redirect(301, 'https://' + req.get('host') + req.url); 40 | } 41 | next(); 42 | }; 43 | 44 | var ensureAuthenticated = function(url, admin) { 45 | return function(req, res, next) { 46 | if (!req.isAuthenticated || !req.isAuthenticated()) { 47 | return res.redirect(url); 48 | } else if (admin && (!req.user || !req.user.isAdmin)) { 49 | return res.redirect(url); 50 | } 51 | next(); 52 | }; 53 | }; 54 | 55 | var SwiftCODE = function() { 56 | var self = this; 57 | 58 | /** 59 | * Listen on the configured port and IP 60 | */ 61 | self.listen = function() { 62 | self.server = http.createServer(self.app); 63 | 64 | // Socket.IO server needs to listen in the same block as the HTTP 65 | // server, or you'll get listen EACCES errors (due to Node's context 66 | // switching?) 67 | self.io = io(self.server); 68 | self.server.listen(self.config.port, self.config.ipaddress); 69 | self.sockets.listen(self.io); 70 | 71 | util.log('Listening at ' + self.config.ipaddress + ':' + self.config.port); 72 | }; 73 | 74 | self._initialize = function() { 75 | self._setupConfig(); 76 | self._setupDb(); 77 | self._setupSession(); 78 | self._setupAuth(); 79 | self._setupApp(); 80 | self._setupRoutes(); 81 | self._setupSockets(); 82 | }; 83 | 84 | self._setupConfig = function() { 85 | self.config = new SwiftCODEConfig(); 86 | }; 87 | 88 | /** 89 | * Setup database connection 90 | */ 91 | self._setupDb = function() { 92 | db.setupConnection(self.config); 93 | 94 | models.Game.resetIncomplete(); 95 | models.User.resetCurrentGames(); 96 | models.User.resetAnonymous(); 97 | models.User.setupAnonymous(); 98 | }; 99 | 100 | self._setupSession = function() { 101 | self.sessionstore = new SessionStore({ 102 | interval: 120000, 103 | connection: mongoose.connection 104 | }); 105 | }; 106 | 107 | /** 108 | * Setup authentication and user config 109 | */ 110 | self._setupAuth = function() { 111 | passport.use(new LocalStrategy(function(username, password, done) { 112 | models.User.findOne({ username: username }, function(err, user) { 113 | if (err) { 114 | util.log(err); 115 | return done(err); 116 | } 117 | // Respond with a message if no such user exists 118 | if (!user) { 119 | return done(null, false, { message: 'No such user exists.' }); 120 | } 121 | // Otherwise check the password 122 | user.comparePassword(password, function(err, isMatch) { 123 | if (err) { 124 | util.log(err); 125 | return done(err); 126 | } 127 | if (!isMatch) { 128 | return done(null, false, { message: 'Looks like that was an incorrect password.' }); 129 | } 130 | return done(null, user); 131 | }); 132 | }); 133 | })); 134 | 135 | passport.serializeUser(function(user, done) { 136 | done(null, user.id); 137 | }); 138 | 139 | passport.deserializeUser(function(id, done) { 140 | models.User.findById(id, function(err, user) { 141 | done(null, user); 142 | }); 143 | }); 144 | }; 145 | 146 | /** 147 | * Setup app configuration 148 | */ 149 | self._setupApp = function() { 150 | self.app = express(); 151 | 152 | // Environment-specific configuration 153 | self.app.configure('development', function() { 154 | self.app.locals.pretty = true; 155 | }); 156 | 157 | self.app.configure('production', function() { 158 | // Force redirection to HTTPS 159 | self.app.use(requireHTTPS); 160 | }); 161 | 162 | // General configuration 163 | self.app.configure(function() { 164 | self.app.set('views', path.join(self.config.repo, 'views')); 165 | self.app.set('view engine', 'jade'); 166 | 167 | // Use HTTP Strict Transport Security, to require compliant 168 | // user agents to communicate by HTTPS only 169 | self.app.use(helmet.hsts()); 170 | 171 | self.app.use(express.favicon(path.join(self.config.repo, 'public/img/favicon.ico'))); 172 | self.app.use(express.json()); 173 | 174 | // Do not use bodyParser, which includes express.multipart, which 175 | // has a problem with creating tmp files on every request 176 | self.app.use(express.urlencoded()); 177 | self.app.use(express.methodOverride()); 178 | 179 | self.app.use(express.cookieParser()); 180 | self.app.use(express.session({ 181 | store: self.sessionstore, 182 | secret: self.config.sessionSecret, 183 | cookie: { 184 | maxAge: 60 * 60 * 1000 // 1 hour 185 | } 186 | })); 187 | self.app.use(flash()); 188 | 189 | self.app.use(passport.initialize()); 190 | self.app.use(passport.session()); 191 | 192 | // The default Connect session does not function as a normal 193 | // rolling session (i.e. session timeout cookies are not updated 194 | // per request - only when session is modified) 195 | self.app.use(function(req, res, next) { 196 | if (req.method === 'HEAD' || req.method === 'OPTIONS') { 197 | return next(); 198 | } 199 | // Force express to generate a new session timestamp 200 | req.session._noop = new Date().getTime(); 201 | req.session.touch(); 202 | next(); 203 | }); 204 | 205 | // Template default available attributes 206 | self.app.use(function(req, res, next) { 207 | res.locals({ 208 | user: req.user, 209 | path: req.path 210 | }); 211 | next(); 212 | }); 213 | 214 | self.app.use(self.app.router); 215 | self.app.use(express.static(path.join(self.config.repo, 'public'))); 216 | }); 217 | }; 218 | 219 | /** 220 | * Setup routing table 221 | */ 222 | self._setupRoutes = function() { 223 | var authMiddleware = ensureAuthenticated('/'); 224 | var adminMiddleware = ensureAuthenticated('/', true); 225 | 226 | // Use custom authentication handler in order to redirect back 227 | // to the referer 228 | self.app.post('/login', function(req, res, next) { 229 | passport.authenticate('local', function(err, user, info) { 230 | if (err) { return next(err); } 231 | if (!user) { 232 | req.flash('error', info.message); 233 | return res.redirect(req.get('referer')); 234 | } 235 | req.login(user, function(err) { 236 | if (err) { return next(err); } 237 | return res.redirect('/lobby'); 238 | }); 239 | })(req, res, next); 240 | }); 241 | 242 | self.app.get('/logout', function(req, res) { 243 | var deletionId; 244 | 245 | if (req.user) { 246 | if (req.user.isAnonymous) { 247 | deletionId = req.user._id; 248 | } 249 | req.user.quitCurrentGame(); 250 | } 251 | 252 | req.logout(); 253 | 254 | if (deletionId) { 255 | models.User.remove({ _id: deletionId }, function(err) { 256 | if (err) { 257 | util.log(err); 258 | } 259 | }); 260 | } 261 | res.redirect('/'); 262 | }); 263 | 264 | self.app.get('/', function(req, res) { 265 | if (req.isAuthenticated()) { 266 | res.redirect('/lobby'); 267 | return; 268 | } 269 | 270 | res.render('index', { 271 | title: 'Home', 272 | error: req.flash('error') 273 | }); 274 | }); 275 | 276 | 277 | self.app.get('/signup', routes.signup); 278 | self.app.get('/playnow', routes.playnow); 279 | self.app.post('/create-account', routes.createAccount); 280 | self.app.get('/lobby', authMiddleware, routes.lobby); 281 | self.app.get('/game', authMiddleware, routes.game); 282 | self.app.get('/admin', adminMiddleware, routes.admin); 283 | self.app.post('/admin/add-lang', adminMiddleware, routes.addLang); 284 | self.app.post('/admin/reinit-exercises', adminMiddleware, routes.reinitExercises); 285 | self.app.get('/about', routes.about); 286 | }; 287 | 288 | /** 289 | * Setup the realtime sockets 290 | */ 291 | self._setupSockets = function() { 292 | self.sockets = new sockets(); 293 | }; 294 | 295 | self._initialize(); 296 | }; 297 | 298 | if (require.main === module) { 299 | var app = new SwiftCODE(); 300 | app.listen(); 301 | } 302 | -------------------------------------------------------------------------------- /src/settings.js.example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var settings = {}; 4 | 5 | // Note: most of these configuration options can be overridden 6 | // with specific environment variables. See the README for details 7 | 8 | // HTTP server's (and WebSocket server's) settings 9 | // NOTE: Generally, you'll want to set the ipaddress to '0.0.0.0' when in 10 | // a production environment (search for INADDR_ANY for more info) 11 | settings.ipaddress = '127.0.0.1'; 12 | settings.port = 8080; 13 | 14 | // The secret salt used to generate session tokens. 15 | // Set this to a falsy value (i.e. null) 16 | // to have it be auto-generated on startup. 17 | settings.sessionSecret = ''; 18 | 19 | // Database settings 20 | settings.dbname = 'swiftcode'; 21 | settings.dbhost = 'localhost'; 22 | settings.dbport = 27017; // Default MongoDB port 23 | settings.dbusername = ''; 24 | settings.dbpassword = ''; 25 | 26 | // Database connection string, for convenience (if this is not falsy, it 27 | // will OVERRIDE the previous DB settings) 28 | settings.dbconnectionstring = null; 29 | 30 | module.exports = settings; 31 | -------------------------------------------------------------------------------- /src/sockets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var io = require('socket.io'); 4 | var moment = require('moment'); 5 | var util = require('util'); 6 | 7 | var models = require('./models'); 8 | var enet = require('./eventnet'); 9 | 10 | var SwiftCODESockets = function() { 11 | var self = this; 12 | 13 | self.listen = function(server) { 14 | self.io = server; 15 | self.io.set('log level', 1); 16 | 17 | self.setupListeners(); 18 | }; 19 | 20 | /** 21 | * Setup socket listeners 22 | */ 23 | self.setupListeners = function() { 24 | var lobbyCount = 0; 25 | 26 | var lobbySockets = self.io.of('/lobby') 27 | .on('connection', function(socket) { 28 | socket.on('games:fetch', function(data) { 29 | models.Game.find({ isViewable: true }, null, { sort: { isJoinable: -1 } }, function(err, docs) { 30 | socket.emit('games:fetch:res', docs); 31 | }); 32 | }); 33 | 34 | socket.on('games:join', function(data) { 35 | models.User.findById(data.player, function(err, user) { 36 | if (err) { 37 | util.log(err); 38 | util.log('games:join error'); return; 39 | } 40 | if (user) { 41 | user.prepareIngameAction('join', { 42 | game: data.game 43 | }, function(err) { 44 | if (err) { 45 | util.log(err); 46 | } 47 | socket.emit('games:join:res', { success: !err }); 48 | }); 49 | } 50 | }); 51 | }); 52 | 53 | socket.on('games:createnew', function(data) { 54 | models.User.findById(data.player, function(err, user) { 55 | if (err) { 56 | util.log(err); 57 | util.log('games:join error'); return; 58 | } 59 | if (user) { 60 | user.prepareIngameAction('createnew', { 61 | lang: data.key, 62 | isSinglePlayer: data.gameType === 'single' 63 | }, function(err) { 64 | if (err) { 65 | util.log(err); 66 | } 67 | socket.emit('games:createnew:res', { success: !err }); 68 | }); 69 | } 70 | }); 71 | }); 72 | 73 | socket.on('disconnect', function() { 74 | lobbyCount--; 75 | lobbySockets.emit('lobbycount', { count: lobbyCount }); 76 | }); 77 | 78 | lobbyCount++; 79 | lobbySockets.emit('lobbycount', { count: lobbyCount }); 80 | }); 81 | 82 | var gameSockets = self.io.of('/game') 83 | .on('connection', function(socket) { 84 | socket.on('ingame:ready', function(data) { 85 | socket.player = data.player; 86 | models.User.findById(data.player, function(err, user) { 87 | if (err) { 88 | util.log(err); 89 | util.log('ingame:ready error'); 90 | return socket.emit('ingame:ready:res', { 91 | success: false, 92 | err: err 93 | }); 94 | } 95 | if (user) { 96 | user.performIngameAction(function(err, success, game) { 97 | if (!success) { 98 | return socket.emit('ingame:ready:res', { 99 | success: false, 100 | err: err 101 | }); 102 | } 103 | 104 | socket.game = game.id; 105 | models.Exercise.findById(game.exercise, 'code projectName typeableCode typeables', function(err, exercise) { 106 | if (exercise) { 107 | // Join a room and broadcast join 108 | socket.join('game-' + game.id); 109 | 110 | socket.emit('ingame:ready:res', { 111 | success: true, 112 | game: game, 113 | timeLeft: game.starting ? moment().diff(game.startTime) : undefined, 114 | exercise: exercise, 115 | nonTypeables: models.NON_TYPEABLE_CLASSES 116 | }); 117 | } else { 118 | util.log('exercise not found'); 119 | socket.emit('ingame:ready:res', { 120 | success: false, 121 | err: 'exercise not found' 122 | }); 123 | } 124 | }); 125 | }); 126 | } 127 | }); 128 | }); 129 | 130 | socket.on('ingame:complete', function(data) { 131 | if (socket.player == undefined) { 132 | util.log('could not find player'); 133 | return; 134 | } 135 | if (socket.game == undefined) { 136 | util.log('could not find game'); 137 | return; 138 | } 139 | 140 | var stats = new models.Stats({ 141 | player: socket.player, 142 | game: socket.game, 143 | time: data.time, 144 | keystrokes: data.keystrokes, 145 | mistakes: data.mistakes 146 | }); 147 | 148 | stats.updateStatistics(function(err, stats, user, game) { 149 | if (err) { 150 | util.log(err); 151 | return; 152 | } 153 | socket.emit('ingame:complete:res', { 154 | stats: stats, 155 | game: game 156 | }); 157 | }); 158 | }); 159 | 160 | socket.on('ingame:advancecursor', function(data) { 161 | socket.broadcast.to('game-' + data.game).emit('ingame:advancecursor', { 162 | player: data.player, 163 | game: data.game 164 | }); 165 | }); 166 | 167 | socket.on('ingame:retreatcursor', function(data) { 168 | socket.broadcast.to('game-' + data.game).emit('ingame:retreatcursor', { 169 | player: data.player, 170 | game: data.game 171 | }); 172 | }); 173 | 174 | socket.on('report:highlightingerror', function(data) { 175 | models.Exercise.findById(data.exercise, function(err, exercise) { 176 | if (exercise) { 177 | exercise.highlightingErrorReports++; 178 | exercise.save(); 179 | } 180 | }); 181 | }); 182 | 183 | socket.on('disconnect', function() { 184 | if (socket.player == undefined) { 185 | util.log('could not find player'); 186 | return; 187 | } 188 | if (socket.game == undefined) { 189 | util.log('could not find game'); 190 | return; 191 | } 192 | models.User.findById(socket.player, function(err, user) { 193 | if (err) { 194 | util.log(err); 195 | util.log('ingame:exit error'); return; 196 | } 197 | if (user) { 198 | user.quitCurrentGame(function(err, game) { 199 | if (err) { 200 | util.log(err); 201 | util.log('ingame:exit error'); return; 202 | } 203 | }); 204 | } 205 | }); 206 | }); 207 | }); 208 | 209 | enet.on('games:new', function(game) { 210 | if (game.isViewable) { 211 | lobbySockets.emit('games:new', game); 212 | } 213 | }); 214 | 215 | enet.on('games:update', function(game) { 216 | if (game.isViewable) { 217 | lobbySockets.emit('games:update', game); 218 | } 219 | gameSockets.in('game-' + game.id).emit('ingame:update', { 220 | game: game, 221 | timeLeft: moment().diff(game.startTime) 222 | }); 223 | }); 224 | 225 | enet.on('games:remove', function(game) { 226 | // By definition, a game must be removed when it isn't viewable 227 | lobbySockets.emit('games:remove', game); 228 | }); 229 | 230 | }; 231 | }; 232 | 233 | module.exports = SwiftCODESockets; 234 | -------------------------------------------------------------------------------- /views/about.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .container 5 | h1= title 6 | p.lead. 7 | SwiftCODE was envisioned and (mostly) created during 8 | Pandacodium 2013. It took a 9 | few more months of on-the-side development before it began to 10 | take shape, but at long last it has been completed. Importantly, 11 | it is designed to be a game and nothing more. So, don't 12 | give "typing skill" more importance than it needs, and of course, 13 | have fun! 14 | p.lead. 15 | SwiftCODE would not be possible were it not for the multitude of 16 | open source projects that are freely available to learn from. Below 17 | is the list of source projects from which typeable code has been 18 | taken, along with links to the code and licenses. All snippets are 19 | copyright to their respective owners. 20 | .col-md-1 21 | .about-projects.col-md-10 22 | each p in projects 23 | .col-md-6 24 | .project-container.well 25 | .col-md-8 26 | a.project-name(href='#{p.url}') #{p.name} 27 | span.lang #{p.langName} 28 | .link-group.col-md-4 29 | .row 30 | a.btn.btn-info.btn-xs.project-link(href='#{p.codeUrl}') Code 31 | .row 32 | a.btn.btn-info.btn-xs.project-link(href='#{p.licenseUrl}') License 33 | .clearfix 34 | .col-md-1 35 | -------------------------------------------------------------------------------- /views/admin.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block styles 4 | link(rel='stylesheet', href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,700', type='text/css') 5 | 6 | block scripts 7 | script(src='/js/libs/knockout-2.3.0.min.js') 8 | script(src='/js/admin.js') 9 | 10 | block content 11 | .container 12 | form.form-horizontal.form-parsley(action='/admin/add-lang', method='POST', role='form') 13 | fieldset 14 | legend New Language 15 | .row 16 | .col-md-10 17 | .col-md-2 18 | button.btn.btn-info.btn-xs(type='button', data-bind='click: reinitExercises') Reinitialize Exercises 19 | .form-group.form-group-tooltip 20 | label.col-md-2.control-label(for='keyInput') Key 21 | .col-md-4 22 | input.form-control(id='keyInput', type='text', name='key', data-required='true', data-placement='right', autofocus) 23 | .form-group.form-group-tooltip 24 | label.col-md-2.control-label(for='nameInput') Language Name 25 | .col-md-6 26 | input.form-control(id='nameInput', type='text', name='name', data-required='true', data-placement='right') 27 | .form-group.form-group-tooltip 28 | label.col-md-2.control-label(for='orderInput') Order 29 | .col-md-6 30 | input.form-control(id='orderInput', type='text', name='order', data-required='true', data-type='number', data-placement='right') 31 | 32 | fieldset.code-projects 33 | legend Source Projects 34 | input(type='hidden', name='projectCount', data-bind='value: $root.projectCount') 35 | input(type='hidden', name='exerciseCount', data-bind='value: $root.exerciseCount') 36 | div(data-bind="template: { name: 'projectsTemplate', foreach: projects }") 37 | 38 | script(id='projectsTemplate', type='text/html') 39 | .form-group.form-group-tooltip 40 | label.col-md-2.control-label(data-bind="attr: { 'for': 'projectKeyInput' + $index() }") Project Key 41 | .col-md-4 42 | input.form-control(data-bind="attr: { 'id': 'projectKeyInput' + $index(), 'name': 'projectKey' + $index() }", type='text', data-required='true', data-placement='right') 43 | .form-group.form-group-tooltip 44 | label.col-md-2.control-label(data-bind="attr: { 'for': 'projectNameInput' + $index() }") Project Name 45 | .col-md-6 46 | input.form-control(data-bind="attr: { 'id': 'projectNameInput' + $index(), 'name': 'projectName' + $index() }", type='text', data-required='true', data-placement='right') .form-group.form-group-tooltip 47 | .form-group.form-group-tooltip 48 | label.col-md-2.control-label(data-bind="attr: { 'for': 'projectUrlInput' + $index() }") Project URL 49 | .col-md-6 50 | input.form-control(data-bind="attr: { 'id': 'projectUrlInput' + $index(), 'name': 'projectUrl' + $index() }", type='text', data-required='true', data-type='url', data-placement='right') 51 | .form-group.form-group-tooltip 52 | label.col-md-2.control-label(data-bind="attr: { 'for': 'projectCodeUrlInput' + $index() }") Project Code URL 53 | .col-md-6 54 | input.form-control(data-bind="attr: { 'id': 'projectCodeUrlInput' + $index(), 'name': 'projectCodeUrl' + $index() }", type='text', data-required='true', data-type='url', data-placement='right') 55 | .form-group.form-group-tooltip 56 | label.col-md-2.control-label(data-bind="attr: { 'for': 'projectLicenseUrlInput' + $index() }") Project License URL 57 | .col-md-6 58 | input.form-control(data-bind="attr: { 'id': 'projectLicenseUrlInput' + $index(), 'name': 'projectLicenseUrl' + $index() }", type='text', data-required='true', data-type='url', data-placement='right') 59 | 60 | fieldset.code-samples 61 | legend Exercises 62 | div(data-bind="foreach: exercises") 63 | input(type='hidden', name='exerciseProject', data-bind="attr: { 'name': 'exerciseProject' + absIndex }, value: $parentContext.$index") 64 | .form-group.form-group-tooltip 65 | label.col-md-2.control-label(data-bind="attr: { 'for': 'exerciseNameInput' + $parentContext.$index() + '-' + $index() }") Exercise Name 66 | .col-md-4 67 | input.form-control(data-bind="attr: { 'id': 'exerciseNameInput' + $parentContext.$index() + '-' + $index(), name: 'exerciseName' + absIndex }, value: 'exercise' + $parentContext.$index() + '-' + $index()", type='text', data-required='true', data-placement='right') 68 | .form-group.form-group-tooltip 69 | label.col-md-2.control-label(data-bind="attr: { 'for': 'codeInput' + $parentContext.$index() + '-' + $index() }") Code 70 | .col-md-10 71 | textarea.form-control.code(data-bind="attr: { 'id': 'codeInput' + $parentContext.$index() + '-' + $index(), name: 'code' + absIndex }", rows='10', data-required='true', data-placement='right') 72 | .form-group 73 | .col-md-2 74 | .col-md-8 75 | button.btn.btn-large(type='button', data-bind='click: addExercise') Add Exercise 76 | 77 | .form-group 78 | .col-md-2 79 | .col-md-8 80 | button.btn.btn-large(type='button', data-bind='click: addProject') Add Project 81 | button.btn.btn-large.btn-primary(type='submit') Submit 82 | -------------------------------------------------------------------------------- /views/footer.jade: -------------------------------------------------------------------------------- 1 | .footer 2 | .container 3 | p.text-muted Made in 2014. Created for Pandacodium 2013. Graciously hosted by Heroku. 4 | a(href='https://github.com/voithos/swiftcode') 5 | span.forkme.label.label-success Fork me on GitHub 6 | -------------------------------------------------------------------------------- /views/game.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block styles 4 | link(rel='stylesheet', href='/css/github.css') 5 | link(rel='stylesheet', href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400,700', type='text/css') 6 | 7 | block scripts 8 | script(src='/js/libs/knockout-2.3.0.min.js') 9 | script(src='/js/libs/highlight.min.js') 10 | script(src='/js/libs/moment-2.1.0.min.js') 11 | script(src='/js/libs/mousetrap.min.js') 12 | script(src='/socket.io/socket.io.js') 13 | script(src='/js/game.js') 14 | 15 | block content 16 | .container 17 | .row 18 | .col-md-4 19 | .col-md-4 20 | .panel.panel-danger.control-panel 21 | .panel-heading 22 | .panel-body.center-parent(data-bind="template: 'controlPanelTemplate'") 23 | script(id='controlPanelTemplate', type='text/html') 24 | .row.center-child 25 | .col-md-7 26 | h3.status(data-bind='text: game.gameStatus, css: game.gameStatusCss') 27 | .col-md-5 28 | div(data-bind='if: game.timerRunning') 29 | div.timer-parent 30 | span.timer.label(data-bind='text: game.timer, css: game.timerCss') 31 | .col-md-3 32 | div(data-bind="template: 'playersPanelTemplate'") 33 | script(id='playersPanelTemplate', type='text/html') 34 | div(data-bind="if: game.isMultiplayer") 35 | .label.label-default Players 36 | .panel.panel-default.players-panel 37 | div(data-bind="if: loaded() && game.players().length > 0") 38 | .panel-body(data-bind="foreach: game.players") 39 | .row 40 | .progress.progress-striped.progress-labeled.active(data-bind="css: cssClass") 41 | .progress-label(data-bind="text: formattedName(20), css: cssClass") 42 | .progress-bar.progress-labeled(data-bind="css: 'progress-bar-' + colorClass(), style: { width: percentage() + '%' }") 43 | .progress-label(data-bind="text: formattedName(20)") 44 | div(data-bind='if: loaded() && game.players().length == 0') 45 | .panel-body 46 | .row 47 | .progress.progress-labeled 48 | .progress-label.text-primary None yet 49 | .col-md-1 50 | .row 51 | .col-md-1 52 | .col-md-10 53 | .row 54 | pre 55 | code.code(data-bind='css: game.langCss') 56 | span#gamecode(data-bind='text: game.gamecode') 57 | span.loading-container(data-bind='visible: loading') 58 | img(src='/img/loading.gif') 59 | .row 60 | .col-md-3(data-bind="if: game.projectName") 61 | span.label.label-info(data-bind="text: 'Code taken from ' + game.projectName()") 62 | .col-md-3.pull-right 63 | a.pull-right(href='', data-bind='click: submitHighlightingReport') 64 | span.highlight-flag.label.label-default Syntax highlighting doesn't look right? 65 | .col-md-1 66 | #completion-dialog.completion-dialog.modal.fade(tabindex='-1', role='dialog', data-backdrop='static', data-keyboard='false') 67 | .modal-dialog 68 | .modal-content 69 | .modal-header 70 | h3.completion-header.text-info(data-bind='text: completionText') 71 | .modal-body 72 | .col-md-1 73 | .col-md-10.completion-table 74 | .row 75 | .col-md-8 Time: 76 | .col-md-4 77 | span(data-bind='text: stats.time') 78 | .row 79 | .col-md-8 Speed: 80 | .col-md-4 81 | span(data-bind='text: stats.speed') 82 | span.muted wpm 83 | .row 84 | .col-md-8 Keystrokes: 85 | .col-md-4 86 | span(data-bind='text: stats.keystrokes') 87 | .row 88 | .col-md-8 Characters: 89 | .col-md-4 90 | span(data-bind='text: stats.typeables') 91 | .row 92 | .col-md-8 Unproductive: 93 | .col-md-4 94 | span(data-bind='text: stats.percentUnproductive') 95 | span.muted % 96 | .row 97 | .col-md-8 Mistakes: 98 | .col-md-4 99 | span(data-bind='text: stats.mistakes') 100 | .col-md-1 101 | .clearfix 102 | .modal-footer 103 | a.btn.btn-primary(href='/lobby') Back to lobby 104 | // ko if: game.isMultiplayer 105 | a.btn.btn-default(data-bind='click: hideCompletionDialog') Continue watching 106 | // /ko 107 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | .banner 5 | .container 6 | .row 7 | .col-md-6 8 | .col-md-6.banner-intro.pull-right 9 | h1.banner-heading How Swift Are You? 10 | h2.banner-phrase SwiftCODE is a multiplayer typing speed game for programmers. 11 | .banner-buttons 12 | a.btn.btn-lg.btn-success.playnow-button(href='/playnow') Play Now 13 | .signup-section or sign up to keep track of your progress 14 | .container 15 | hr.featurette-divider 16 | .row.featurette 17 | .col-md-6 18 | h2.featurette-heading It's just you and your keyboard.
19 | span.text-muted Use the source, Luke. 20 | p.lead. 21 | Do you enjoy hearing the clicking of the keyboard as you 22 | furiously type out code? The keyboard is the most basic 23 | tool in a programmer's arsenal; to master such a tool 24 | requires training. Enter SwiftCODE. Prepare yourself, find 25 | focus, and dive in. 26 | .col-md-6 27 | img.featurette-image.img-circle.img-responsive.pull-right(src='/img/computer.png') 28 | .row.featurette 29 | .col-md-6 30 | img.featurette-image.img-circle.img-responsive(src='/img/launchpad.png') 31 | .col-md-6 32 | h2.featurette-heading Play by yourself, or with friends.
33 | span.text-muted See who's swiftest. 34 | p.lead. 35 | Hone your skills in single-player mode. Train to improve on 36 | typing speed and accuracy, making every keystroke count. 37 | Then, enter the multiplayer fray and challenge up to 3 38 | others in a real-time battle of swiftness. 39 | .row.featurette 40 | .col-md-6 41 | h2.featurette-heading Try out the syntax of new languages.
42 | span.text-muted Go supersonic. 43 | p.lead. 44 | Play with your favorite language, or try a new one, whether 45 | it's object-oriented, functional, declarative, or something 46 | else altogether. Type code taken from real open-source 47 | projects. Practice the hard-to-reach and quirky characters 48 | that the language uses. 49 | .col-md-6 50 | img.featurette-image.img-circle.img-responsive.pull-right(src='/img/code.png') 51 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | !!! 2 | html 3 | head 4 | meta(charset='utf-8') 5 | title= title + ' | SwiftCODE' 6 | link(rel='shortcut icon', href='favicon.ico') 7 | link(rel='stylesheet', href='/css/bootstrap.css') 8 | link(rel='stylesheet', href='/css/alertify.css') 9 | link(rel='stylesheet', href='/css/style.css') 10 | block styles 11 | body 12 | include nav 13 | block content 14 | include footer 15 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js') 16 | script. 17 | if (!('jQuery' in window)) { 18 | document.write('