├── server ├── roomQueueRedis │ ├── roomFilling.js │ └── redisQueue.js ├── .DS_Store ├── public │ ├── img │ │ ├── battle.png │ │ ├── logo_50.png │ │ ├── logo_full.png │ │ ├── training.png │ │ ├── github_logo.png │ │ ├── description-staging.png │ │ ├── description-challenge.png │ │ └── description-practice.png │ ├── index.html │ ├── styles │ │ ├── reset.css │ │ ├── profile.css │ │ └── styles.css │ └── addProblemsSolutions.html ├── sockets │ ├── findRoom.js │ └── socketsChallengeArena.js ├── challenges │ ├── challengeModel.js │ ├── challengeController.js │ └── challenges.csv ├── users │ ├── userModel.js │ ├── adminPrivilege.js │ └── userController.js ├── solutions │ ├── solutionModel.js │ └── solutionController.js ├── matches │ ├── matchModel.js │ └── matchController.js ├── responseRedis │ ├── redisQueue.js │ └── responseRedisRunner.js ├── helpers │ ├── psConfig.js │ └── dbConfig.js ├── server.js └── routes.js ├── app ├── actions │ ├── leaderboardActions.js │ ├── index.js │ ├── stagingActions.js │ ├── loginActions.js │ ├── arenaActions.js │ └── navActions.js ├── reducers │ ├── index.js │ ├── viewReducers.js │ ├── userReducers.js │ └── arenaReducers.js ├── components │ ├── index.js │ ├── ErrorList.js │ ├── Login.js │ ├── DelaySplash.js │ ├── NavBar.js │ ├── Leaderboard.js │ ├── SoloStaging.js │ ├── Staging.js │ ├── SoloArena.js │ ├── PairArena.js │ ├── Profile.js │ └── ChallengeArena.js ├── index.js ├── store │ └── configureStore.js ├── tests │ └── reducers │ │ ├── arenaReducerTestAsync.js │ │ ├── viewReducerTest.js │ │ ├── userReducerTest.js │ │ └── arenaReducerTest.js ├── constants.js ├── sockets │ └── socket-helper.js └── containers │ └── App.js ├── .gitignore ├── docker-compose.yml ├── worker ├── queue.js └── solutionTester.js ├── Dockerfile-server ├── Dockerfile-worker ├── github.md ├── package.json ├── test ├── matchControllerSpec.js ├── userControllerSpec.js ├── challengeControllerSpec.js └── solutionControllerSpec.js ├── README.md ├── PRESS_RELEASE.md └── webpack.config.js /server/roomQueueRedis/roomFilling.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/.DS_Store -------------------------------------------------------------------------------- /server/public/img/battle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/battle.png -------------------------------------------------------------------------------- /app/actions/leaderboardActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var actions = require('../constants').action; 3 | module.exports = {} 4 | -------------------------------------------------------------------------------- /server/public/img/logo_50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/logo_50.png -------------------------------------------------------------------------------- /server/public/img/logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/logo_full.png -------------------------------------------------------------------------------- /server/public/img/training.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/training.png -------------------------------------------------------------------------------- /server/public/img/github_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/github_logo.png -------------------------------------------------------------------------------- /server/public/img/description-staging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/description-staging.png -------------------------------------------------------------------------------- /server/public/img/description-challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/description-challenge.png -------------------------------------------------------------------------------- /server/public/img/description-practice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dexterous-Rambutan/battle-code/HEAD/server/public/img/description-practice.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/bundle.js 3 | server/lib/apiKey.js 4 | server/lib/apiKey-dev.js 5 | server/lib/apiKey-demo.js 6 | server/public/bundle.js 7 | dump.rdb 8 | .DS_Store 9 | logfile 10 | test/npm-debug.log 11 | -------------------------------------------------------------------------------- /server/sockets/findRoom.js: -------------------------------------------------------------------------------- 1 | module.exports = function(socket) { 2 | var room; 3 | for(var key in socket.rooms){ 4 | if(key[0] !== '/'){ 5 | room = key; 6 | } 7 | } 8 | return room; 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var combineReducers = require('redux').combineReducers; 3 | 4 | 5 | var reducers = { 6 | arena: require('./arenaReducers.js'), 7 | 8 | user: require('./userReducers.js'), 9 | 10 | view: require('./viewReducers.js'), 11 | 12 | } 13 | 14 | module.exports = combineReducers(reducers); 15 | -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | navActions: require('./navActions.js'), 5 | 6 | loginActions: require('./loginActions.js'), 7 | 8 | stagingActions: require('./stagingActions.js'), 9 | 10 | arenaActions: require('./arenaActions.js'), 11 | 12 | leaderboardActions: require('./leaderboardActions.js'), 13 | 14 | } 15 | -------------------------------------------------------------------------------- /server/challenges/challengeModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../helpers/dbConfig'); 2 | var Solution = require('../solutions/solutionModel'); 3 | 4 | var Challenge = db.Model.extend({ 5 | tableName: 'challenges', 6 | hasTimestamps: true, 7 | solutions: function() { 8 | return this.hasMany(Solution); 9 | } 10 | }); 11 | 12 | module.exports = Challenge; 13 | -------------------------------------------------------------------------------- /app/actions/stagingActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var socket = require('../sockets/socket-helper'); 3 | var actions = require('../constants').action; 4 | 5 | 6 | var createSocket = function(){ 7 | return { 8 | type: actions.CREATE_SOCKET, 9 | payload: socket 10 | } 11 | }; 12 | 13 | 14 | module.exports = { 15 | createSocket: createSocket 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | creator: 2 | build: . 3 | dockerfile: Dockerfile-server 4 | ports: 5 | - 80:3000 6 | environment: 7 | DEPLOYED: "true" 8 | links: 9 | - redis 10 | - postgres 11 | 12 | solution: 13 | build: . 14 | dockerfile: Dockerfile-worker 15 | environment: 16 | DEPLOYED: "true" 17 | links: 18 | - redis 19 | - postgres 20 | 21 | redis: 22 | image: redis 23 | 24 | postgres: 25 | image: postgres -------------------------------------------------------------------------------- /server/users/userModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../helpers/dbConfig'); 2 | 3 | var User = db.Model.extend({ 4 | tableName: 'users', 5 | hasTimestamps: true, 6 | solutions: function() { 7 | var Solution = require('../solutions/solutionModel'); 8 | return this.hasMany(Solution); 9 | }, 10 | matches: function() { 11 | var Match = require('../matches/matchModel'); 12 | return this.hasMany(Match); 13 | } 14 | }); 15 | 16 | module.exports = User; 17 | 18 | -------------------------------------------------------------------------------- /server/solutions/solutionModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../helpers/dbConfig'); 2 | var User = require('../users/userModel'); 3 | 4 | var Solution = db.Model.extend({ 5 | tableName: 'solutions', 6 | user: function() { 7 | return this.belongsTo(User, 'user_id'); 8 | }, 9 | challenge: function() { 10 | var Challenge = require('../challenges/challengeModel'); 11 | return this.belongsTo(Challenge, 'challenge_id'); 12 | } 13 | }); 14 | 15 | module.exports = Solution; 16 | -------------------------------------------------------------------------------- /server/matches/matchModel.js: -------------------------------------------------------------------------------- 1 | var db = require('../helpers/dbConfig'); 2 | var Challenge = require('../challenges/challengeModel'); 3 | var User = require('../users/userModel'); 4 | 5 | var Match = db.Model.extend({ 6 | tableName: 'matches', 7 | hasTimestamps: true, 8 | user: function(){ 9 | return this.belongsTo(User, 'user_id'); 10 | }, 11 | challenge: function(){ 12 | return this.belongsTo(Challenge, 'challenge_id'); 13 | } 14 | }); 15 | 16 | module.exports = Match; 17 | -------------------------------------------------------------------------------- /app/components/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | ChallengeArena: require('./ChallengeArena.js'), 5 | ErrorList: require('./ErrorList.js'), 6 | Leaderboard: require('./Leaderboard.js'), 7 | Login: require('./Login.js'), 8 | NavBar: require('./NavBar.js'), 9 | Profile: require('./Profile.js'), 10 | SoloArena: require('./SoloArena.js'), 11 | SoloStaging: require('./SoloStaging.js'), 12 | Staging: require('./Staging.js'), 13 | PairArena: require('./PairArena.js') 14 | } 15 | -------------------------------------------------------------------------------- /app/components/ErrorList.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var ErrorList = React.createClass({ 4 | render: function() { 5 | var errors = this.props.errors.map(function(error){ 6 | return ( 7 |
8 | Row: {error.row + 1} - {error.text} 9 |
10 | ) 11 | 12 | }) 13 | return ( 14 |
15 | {this.props.syntaxMessage} 16 | {errors} 17 |
18 | ) 19 | } 20 | }); 21 | 22 | module.exports = ErrorList; 23 | -------------------------------------------------------------------------------- /server/users/adminPrivilege.js: -------------------------------------------------------------------------------- 1 | var admins = ['kweng2', 'alanzfu', 'puzzlehe4d', 'hahnbi']; 2 | module.exports = function (req, res, next) { 3 | if (req.session.passport) { 4 | var isAdmin = admins.indexOf(req.session.passport.user.username) !== -1; 5 | if (isAdmin) { 6 | next(); 7 | } else { 8 | res.status(401).end('Requires admin privilege
Log In'); 9 | } 10 | } else { 11 | res.status(401).end('Requires admin privilege
Log In'); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /worker/queue.js: -------------------------------------------------------------------------------- 1 | function Queue (name, client) { 2 | this.name = name; 3 | this.client = client; 4 | this.timeout = 0; 5 | } 6 | 7 | Queue.prototype.push = function (data, callback) { 8 | console.log('Pushing to:', this.client.address, this.name); 9 | this.client.rpush([this.name, data], function(err, replies){ 10 | console.log('Finished pushing:', err, replies); 11 | callback(); 12 | }.bind(this)); 13 | }; 14 | 15 | Queue.prototype.pop = function (callback) { 16 | this.client.blpop(this.name, this.timeout, callback); 17 | }; 18 | 19 | module.exports = Queue; 20 | -------------------------------------------------------------------------------- /Dockerfile-server: -------------------------------------------------------------------------------- 1 | FROM node 2 | RUN mkdir server 3 | 4 | # This command makes `/server/` the current working directory. You can assume you are 'inside' that directory for all following commands 5 | WORKDIR server 6 | 7 | # TODO: ADD all the application code into /server 8 | ADD ./server /server 9 | ADD package.json /server 10 | ADD ./node_modules /server/node_modules 11 | 12 | # TODO: RUN `npm install` 13 | # RUN npm install 14 | 15 | EXPOSE 3000 16 | # This command allows us to access the web server port from outside the container 17 | 18 | CMD ["node", "server.js"] # `package.json` already provides this command 19 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var render = require('react-dom').render; 5 | var Provider = require('react-redux').Provider; 6 | 7 | var App = require('./containers/App'); 8 | 9 | var configureStore = require('./store/configureStore'); 10 | 11 | 12 | 13 | 14 | var store = configureStore(); 15 | // store.subscribe(() => { 16 | // console.log('state changed',store.getState()); 17 | // }); 18 | 19 | 20 | window.store = store; 21 | store.getState(); 22 | 23 | require('./sockets/socket-helper'); 24 | 25 | render( 26 | 27 | 28 | , 29 | document.getElementById('app') 30 | ); 31 | -------------------------------------------------------------------------------- /app/actions/loginActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var actions = require('../constants').action; 3 | 4 | 5 | 6 | var checkLoggedIn = function(){ 7 | return function(dispatch){ 8 | $.ajax({ 9 | method: 'GET', 10 | url: '/auth-verify', 11 | dataType: 'json', 12 | cache: false, 13 | success: function(data){ 14 | dispatch({ 15 | type: actions.IS_LOGGED_IN, 16 | payload: data 17 | }) 18 | }, 19 | error: function(error){ 20 | dispatch({ 21 | type: actions.IS_LOGGED_OUT 22 | }); 23 | } 24 | }) 25 | } 26 | }; 27 | 28 | 29 | module.exports = { 30 | checkLoggedIn: checkLoggedIn 31 | } 32 | -------------------------------------------------------------------------------- /app/components/Login.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Login = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |

Login with GitHub

17 |
18 |
19 |
20 |
21 | ) 22 | } 23 | }); 24 | 25 | module.exports = Login; 26 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createStore = require('redux').createStore; 4 | var applyMiddleware = require('redux').applyMiddleware; 5 | var thunk = require('redux-thunk'); 6 | var reducer = require('../reducers'); 7 | 8 | 9 | var createStoreWithMiddleware = applyMiddleware(thunk)(createStore); 10 | 11 | function configureStore(initialState) { 12 | var store = createStoreWithMiddleware(reducer, initialState) 13 | 14 | // IF YOU WANT TO USE HOT RELOADING 15 | // if (module.hot) { 16 | // // Enable Webpack hot module replacement for reducers 17 | // module.hot.accept('../reducers', function(){ 18 | // var nextReducer = require('../reducers'); 19 | // store.replaceReducer(nextReducer); 20 | // }); 21 | // } 22 | 23 | return store; 24 | } 25 | 26 | module.exports = configureStore; 27 | -------------------------------------------------------------------------------- /app/reducers/viewReducers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | //view strings 5 | var views = require('../constants').view 6 | var actions = require('../constants').action; 7 | 8 | 9 | 10 | 11 | function viewReducer (state, action){ 12 | state = state || views.STAGING; 13 | switch (action.type){ 14 | case actions.NAV_STAGING: 15 | return views.STAGING; 16 | case actions.NAV_SOLO_STAGING: 17 | return views.SOLO_STAGING; 18 | case actions.NAV_SOLO_ARENA: 19 | return views.SOLO_ARENA; 20 | case actions.NAV_CHALLENGE_ARENA: 21 | return views.CHALLENGE_ARENA; 22 | case actions.LOGOUT: 23 | return views.LOGIN; 24 | case actions.NAV_PROFILE: 25 | return views.PROFILE; 26 | default: 27 | return state; 28 | } 29 | }; 30 | 31 | module.exports = viewReducer; 32 | -------------------------------------------------------------------------------- /server/responseRedis/redisQueue.js: -------------------------------------------------------------------------------- 1 | function Queue (name, client) { 2 | this.name = name; 3 | // `client`, when passed in, is expected be a Redis client connection instance 4 | this.client = client; 5 | this.timeout = 0; 6 | } 7 | 8 | Queue.prototype.push = function (data) { 9 | // Pushes `data` onto the tail of the list at key `this.name` 10 | console.log('Pushing to', this.name, 'at', this.client.address); 11 | this.client.rpush(this.name, data, function (err, replies) { 12 | if (err) throw new Error(err); 13 | console.log('Successfully pushed to', this.name, 'at', this.client.address, replies); 14 | }.bind(this)); 15 | }; 16 | 17 | Queue.prototype.pop = function (callback) { 18 | // Pops, or indefinitely waits to pop off the head of the list at `this.name` 19 | this.client.blpop(this.name, this.timeout, callback); 20 | }; 21 | 22 | module.exports = Queue; 23 | -------------------------------------------------------------------------------- /server/roomQueueRedis/redisQueue.js: -------------------------------------------------------------------------------- 1 | function Queue (name, client) { 2 | this.name = name; 3 | // `client`, when passed in, is expected be a Redis client connection instance 4 | this.client = client; 5 | this.timeout = 0; 6 | } 7 | 8 | Queue.prototype.push = function (data) { 9 | // Pushes `data` onto the tail of the list at key `this.name` 10 | console.log('Pushing to', this.name, 'at', this.client.address); 11 | this.client.rpush(this.name, data, function (err, replies) { 12 | if (err) throw new Error(err); 13 | console.log('Successfully pushed to', this.name, 'at', this.client.address, replies); 14 | }.bind(this)); 15 | }; 16 | 17 | Queue.prototype.pop = function (callback) { 18 | // Pops, or indefinitely waits to pop off the head of the list at `this.name` 19 | this.client.blpop(this.name, this.timeout, callback); 20 | }; 21 | 22 | module.exports = Queue; 23 | -------------------------------------------------------------------------------- /Dockerfile-worker: -------------------------------------------------------------------------------- 1 | FROM node 2 | RUN mkdir worker 3 | 4 | # This command makes `/worker/` the current working directory. You can assume you are 'inside' that directory for all following commands 5 | WORKDIR worker 6 | 7 | # TODO: ADD all the application code into /worker 8 | ADD ./worker /worker 9 | ADD package.json /worker 10 | ADD ./node_modules /worker/node_modules 11 | ADD ./server/solutions/solutionModel.js /worker/solutions/solutionModel.js 12 | ADD ./server/challenges/challengeModel.js /worker/challenges/challengeModel.js 13 | ADD ./server/users/userModel.js /worker/users/userModel.js 14 | ADD ./server/helpers/dbConfig.js /worker/helpers/dbConfig.js 15 | 16 | # TODO: RUN `npm install` 17 | RUN npm install forever -g 18 | 19 | # EXPOSE 3000 20 | # This command allows us to access the web server port from outside the container 21 | 22 | CMD ["forever", "solutionTester.js"] # `package.json` already provides this command 23 | -------------------------------------------------------------------------------- /app/tests/reducers/arenaReducerTestAsync.js: -------------------------------------------------------------------------------- 1 | //file does not work; boiler plate setup for testing async actions with mock store 2 | var configureMockStore = require('redux-mock-store'); 3 | var thunk = require('redux-thunk'); 4 | var expect = require('expect'); 5 | var arenaReducer = require('../../reducers/arenaReducers.js'); 6 | var actions = require('../../constants').action; 7 | var navActions = require('../../actions/navActions.js'); 8 | var nock = require('nock'); 9 | var middlewares = [ thunk ]; 10 | var mockStore = configureMockStore(middlewares); 11 | 12 | describe('async actions', function(){ 13 | afterEach(function() { 14 | nock.cleanAll() 15 | }) 16 | 17 | 18 | 19 | it('fired STORE_SOLO_PROBLEM & NAV_SOLO_ARENA with navActions.spoofSolo', function(done){ 20 | nock('http://127.0.0.1:3000') 21 | .get('/api/challenges/1') 22 | .reply(200) 23 | 24 | var expectedActions = [ 25 | { type: actions.STORE_SOLO_PROBLEM } , 26 | { type: actions.NAV_SOLO_ARENA } 27 | ] 28 | var store = mockStore({}, expectedActions, done) 29 | store.dispatch(navActions.spoofSolo()) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Battle Code 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 |
15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/helpers/psConfig.js: -------------------------------------------------------------------------------- 1 | var apiKey; 2 | var callbackURL; 3 | var port = process.env.PORT || 3000; 4 | 5 | if (process.env.DEPLOYED) { 6 | callbackURL = "http://battlecode.tech/login/callback"; 7 | apiKey = require('../lib/apiKey-demo.js'); 8 | } else { 9 | callbackURL = "http://127.0.0.1:3000/login/callback"; 10 | apiKey = require('../lib/apiKey-dev.js'); 11 | } 12 | 13 | ///////////////// API key for GitHub OAuth ///////////// 14 | var GITHUB_CLIENT_ID = apiKey.GITHUB_CLIENT_ID; 15 | var GITHUB_CLIENT_SECRET = apiKey.GITHUB_CLIENT_SECRET; 16 | 17 | ////////// Passport and github passport required ////// 18 | var GitHubStrategy = require('passport-github').Strategy; 19 | var passport = require('passport'); 20 | 21 | passport.serializeUser(function(user, done) { 22 | done(null, user); 23 | }); 24 | passport.deserializeUser(function(obj, done) { 25 | done(null, obj); 26 | }); 27 | 28 | passport.use(new GitHubStrategy({ 29 | clientID: GITHUB_CLIENT_ID, 30 | clientSecret: GITHUB_CLIENT_SECRET, 31 | callbackURL: callbackURL 32 | }, 33 | function(accessToken, refreshToken, profile, done) { 34 | process.nextTick(function () { 35 | return done(null, profile); 36 | }); 37 | } 38 | )); 39 | 40 | module.exports = passport; -------------------------------------------------------------------------------- /app/components/DelaySplash.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var actions = require('../constants').action; 3 | var DelaySplash = React.createClass({ 4 | componentDidUpdate: function () { 5 | if(this.props.arena.delay > 0){ 6 | setTimeout(this.props.arenaActions.countdown, 1000) 7 | } 8 | }, 9 | render: function() { 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |
17 |

{this.props.user.github_handle}

18 |
19 |
20 |
21 |
22 |

{this.props.arena.opponent_info.github_handle}

23 |
24 |
25 | 26 |
27 |
28 |
29 | {"Starting in " + this.props.arena.delay + "..."} 30 |
31 |
32 | ) 33 | } 34 | }); 35 | 36 | module.exports = DelaySplash; 37 | -------------------------------------------------------------------------------- /server/public/styles/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /github.md: -------------------------------------------------------------------------------- 1 | ## Dexterous-Rambutan Git Workflow 2 | 3 | 1. **GITHUB**: Fork Dexterous-Rambutan/battle-code 4 | 1. **LOCAL**: git clone user/battle-code 5 | 1. **LOCAL**: git remote add upstream Dexterous-Rambutan/battle-code 6 | 7 |
8 | 9 | ### Development Worklow 10 | 1. Find issue that you were assigned 11 | a. Or Assign issue to yourself 12 | 1. **LOCAL** || git status to make sure you are on master 13 | 1. **LOCAL** || git pull --rebase usptream master => makes sure master is up to date with truth 14 | 1. **LOCAL** || git checkout -b feature-IssueThatYouWereAssigned 15 | 1. **GITHUB** || pull request Create initial pull request with Task list of issues 16 | 1. **LOCAL** || git status => to check branch 17 | 1. Cyclical 18 | a. make edits 19 | b. git add 20 | c. git commit 21 | 1. **LOCAL** || git pull --rebase upstream master 22 | 1. **LOCAL** || git push origin feature-IssueThatYouWereAssigned 23 | 1. **GITHUB** || pull request finalize and assign 24 | 1. **GITHUB** || fill out git-splainin within pull request comment field 25 | 1. *IF MAKING Changes to pull request* 26 | 1. **LOCAL** || make changes locally 27 | 1. **LOCAL** || git add 28 | 1. **LOCAL** || git commit 29 | 1. **LOCAL** || git pull --rebase upstream master 30 | 1. **LOCAL** || git push origin feature-IssueThatYouWereAssigned 31 | 32 | 33 | ### SCRUM MASTER // SECOND SET OF EYES 34 | 1. review changes and merge 35 | 1. merge request 36 | -------------------------------------------------------------------------------- /app/components/NavBar.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Nav = React.createClass({ 4 | navProfile: function () { 5 | this.props.navActions.navProfile(this.props.user.github_handle); 6 | }, 7 | 8 | render: function () { 9 | var showNav = (this.props.user.isLoggedIn && (this.props.view !== 'CHALLENGE_ARENA' && this.props.view !== 'SOLO_ARENA')); 10 | var showLeave = (this.props.user.isLoggedIn && (this.props.view === 'CHALLENGE_ARENA' || this.props.view === 'SOLO_ARENA')); 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 |
    21 | {this.props.arena.opponent_info.github_handle ?
  • You are playing against: {this.props.arena.opponent_info.github_handle}
  • : null} 22 | {showNav ?
  • Profile
  • : null} 23 | {showNav ?
  • Logout
  • : null} 24 | {showLeave ?
  • LEAVE
  • : null} 25 |
26 |
27 |
28 |
29 | ); 30 | } 31 | }); 32 | 33 | module.exports = Nav; 34 | -------------------------------------------------------------------------------- /app/components/Leaderboard.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Leaderboard = React.createClass({ 4 | getLeaderBoard: function(){ 5 | var problem_id = this.props.arena.problem_id; 6 | this.props.arenaActions.getLeaderBoard(problem_id); 7 | }, 8 | exit: function(){ 9 | this.props.arenaActions.exitSplash(); 10 | }, 11 | render: function() { 12 | var solutions = this.props.arena.leaderBoard.map(function(element, index){ 13 | return ( 14 | 15 | 16 | {index + 1}. {element.github_handle} {element.total_time/1000} seconds 17 | 18 | ) 19 | }) 20 | if (solutions.length > 10) { 21 | solutions = solutions.slice(0, 10); 22 | } 23 | 24 | 25 | return ( 26 |
27 |
28 | {this.props.arena.status === 'YOU WON!' ?
{this.props.arena.status}
:
CHALLENGE COMPLETE
} 29 |
30 |
31 | 32 | {solutions} 33 |
34 |
35 |
36 | ) 37 | } 38 | }); 39 | 40 | module.exports = Leaderboard; 41 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var port = process.env.PORT || 3000; 2 | 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | var passport = require('./helpers/psConfig.js'); 6 | var session = require('express-session'); 7 | var redis = require('redis'); 8 | var client; 9 | if (process.env.DEPLOYED) { 10 | client = redis.createClient(6379, 'redis'); 11 | } else { 12 | client = redis.createClient(); 13 | } 14 | 15 | var app = express(); 16 | app.use(bodyParser.urlencoded({extended: true})); 17 | app.use(bodyParser.json()); 18 | 19 | //////////// SESSION SECRETS //////////////////// 20 | app.use(session({ 21 | key: 'app.testS', 22 | secret: 'SEKR37', 23 | saveUninitialized:false, 24 | resave:false 25 | })); 26 | 27 | app.use(passport.initialize()); 28 | app.use(passport.session()); 29 | 30 | //////////////////////////////////////////////// 31 | require('./routes.js')(app, client); 32 | app.use(express.static(__dirname + '/public')); 33 | //////////////////////////////////////////////// 34 | 35 | // Start server 36 | var server = app.listen(port, function () { 37 | console.log('Server listening at port ', port); 38 | }); 39 | 40 | // Start socket listener 41 | var io = require('socket.io').listen(server); 42 | 43 | 44 | // Start redisQueue listener for evaluated solutions 45 | var solutionEvalResponse = require('./responseRedis/responseRedisRunner.js'); 46 | solutionEvalResponse(io); 47 | 48 | 49 | require('./sockets/socketsChallengeArena.js')(io); 50 | 51 | -------------------------------------------------------------------------------- /app/reducers/userReducers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var actions = require('../constants').action; 3 | 4 | 5 | var _ = require('lodash'); 6 | 7 | var initial = { 8 | isLoggedIn: false, 9 | github_handle: "", 10 | github_display_name: '', 11 | github_profile_url: '', 12 | github_avatar_url: '', 13 | user_problems: [], 14 | elo_rating: '', 15 | user_match_history: [] 16 | }; 17 | 18 | function userReducer (state, action){ 19 | state = state || initial; 20 | switch(action.type){ 21 | case actions.IS_LOGGED_IN: 22 | return _.extend({}, state, { 23 | isLoggedIn: true, 24 | github_handle: action.payload.github_handle, 25 | github_display_name: action.payload.github_display_name, 26 | github_profile_url: action.payload.github_profile_url, 27 | github_avatar_url: action.payload.github_avatar_url 28 | }); 29 | case actions.IS_LOGGED_OUT: 30 | return _.extend({}, state, { 31 | isLoggedIn: false, 32 | github_handle: "", 33 | github_display_name: '', 34 | github_profile_url: '', 35 | github_avatar_url: '' 36 | }); 37 | case actions.STORE_USER_PROBLEMS: 38 | return _.extend({}, state, { 39 | user_problems: action.payload 40 | }); 41 | case actions.STORE_MATCH_HISTORY: 42 | return _.extend({}, state, { 43 | user_match_history: action.payload 44 | }); 45 | case actions.GET_ELO: 46 | return _.extend({}, state, { 47 | elo_rating: action.payload 48 | }); 49 | default: 50 | return state; 51 | } 52 | return state; 53 | } 54 | 55 | module.exports = userReducer; 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "battle-code", 3 | "version": "0.0.1", 4 | "description": "head to head programming arena", 5 | "main": "/public/index.html", 6 | "dependencies": { 7 | "babel": "^6.3.26", 8 | "babel-core": "^6.4.0", 9 | "babel-loader": "^6.2.1", 10 | "babel-preset-es2015": "^6.3.13", 11 | "babel-preset-react": "^6.3.13", 12 | "bluebird": "^3.2.2", 13 | "body-parser": "^1.14.2", 14 | "bookshelf": "~0.9.1", 15 | "chai": "~3.4.1", 16 | "elo-rank": "^1.0.0", 17 | "express": "^4.13.3", 18 | "express-session": "^1.13.0", 19 | "history": "^1.17.0", 20 | "knex": "~0.9.0", 21 | "lodash": "^4.0.0", 22 | "mocha": "~2.3.4", 23 | "moment": "^2.11.2", 24 | "passport": "^0.3.2", 25 | "passport-github": "^1.0.0", 26 | "pg": "^4.4.3", 27 | "pg-copy-streams": "^0.3.0", 28 | "react": "^0.14.6", 29 | "react-dom": "^0.14.6", 30 | "react-redux": "^4.0.6", 31 | "react-router": "^2.0.0-rc5", 32 | "redis": "~2.4.2", 33 | "redux": "^3.0.5", 34 | "redux-simple-router": "^2.0.4", 35 | "redux-thunk": "^1.0.3", 36 | "rest": "^1.3.1", 37 | "socket.io": "^1.4.5", 38 | "webpack": "^1.12.10", 39 | "webpack-dev-server": "^1.14.0" 40 | }, 41 | "devDependencies": { 42 | "webpack-dev-server": "^1.14.0", 43 | "request": "~2.69.0", 44 | "socket.io-client": "~1.4.5" 45 | }, 46 | "scripts": { 47 | "test": "mocha test/challengeControllerSpec.js && mocha test/solutionControllerSpec.js && mocha test/userControllerSpec.js", 48 | "start": "webpack-dev-server --inline --hot --content-base public/" 49 | }, 50 | "author": "", 51 | "license": "ISC" 52 | } 53 | -------------------------------------------------------------------------------- /app/components/SoloStaging.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var moment = require('moment'); 3 | 4 | var SoloStaging = React.createClass({ 5 | render: function () { 6 | 7 | var listOfProblems = this.props.user.user_problems.map(function (problem) { 8 | var linkToProblem = function(){ 9 | this.props.navActions.navSoloArena(problem); 10 | }.bind(this); 11 | return ( 12 |
13 |
14 | 15 |
16 |
17 | {problem.challenge_name} 18 |
19 | Seen: {moment(problem.start_time).format('l')} 20 |
21 |
22 | ) 23 | }.bind(this)); 24 | 25 | return ( 26 |
27 |
28 | { 29 | listOfProblems.length > 0 ? 30 |
31 |
32 | 33 |
34 |
35 | {listOfProblems} 36 |
37 |
: 38 |
39 |
40 | 41 |
42 |
43 | Sorry, you do not have any problems to practice on. Please play challenge mode to unlock more problems. 44 |
45 |
46 | } 47 |
48 | ) 49 | } 50 | }); 51 | 52 | module.exports = SoloStaging; 53 | -------------------------------------------------------------------------------- /app/tests/reducers/viewReducerTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect'); 2 | var viewReducer = require('../../reducers/viewReducers.js'); 3 | var actions = require('../../constants').action; 4 | var views = require('../../constants').view; 5 | describe('View Reducer', function(){ 6 | 7 | describe('#viewReducer (immutability of state)', function(){ 8 | // inital state for Arena Reducer 9 | var initialViewReducer = views.STAGING; 10 | 11 | Object.freeze(initialViewReducer); 12 | 13 | it('should return the initial state (STAGING)', function(){ 14 | expect( 15 | viewReducer(undefined, {}) 16 | ).toEqual(initialViewReducer) 17 | }) 18 | 19 | it('should store STAGING view on state', function(){ 20 | var state = viewReducer(initialViewReducer, { 21 | type: actions.NAV_STAGING 22 | }) 23 | expect(state).toEqual(views.STAGING) 24 | }) 25 | 26 | it('should store SOLO_STAGING view on state', function(){ 27 | var state = viewReducer(initialViewReducer, { 28 | type: actions.NAV_SOLO_STAGING 29 | }) 30 | expect(state).toEqual(views.SOLO_STAGING) 31 | }) 32 | 33 | it('should store SOLO_ARENA view on state', function(){ 34 | var state = viewReducer(initialViewReducer, { 35 | type: actions.NAV_SOLO_ARENA 36 | }) 37 | expect(state).toEqual(views.SOLO_ARENA) 38 | }) 39 | 40 | it('should store CHALLENGE_ARENA view on state', function(){ 41 | var state = viewReducer(initialViewReducer, { 42 | type: actions.NAV_CHALLENGE_ARENA 43 | }) 44 | expect(state).toEqual(views.CHALLENGE_ARENA) 45 | }) 46 | 47 | it('should store LOGIN view on state when LOGOUT action is dispatched', function(){ 48 | var state = viewReducer(initialViewReducer, { 49 | type: actions.LOGOUT 50 | }) 51 | expect(state).toEqual(views.LOGIN) 52 | }) 53 | 54 | it('should store PROFILE view on state', function(){ 55 | var state = viewReducer(initialViewReducer, { 56 | type: actions.NAV_PROFILE 57 | }) 58 | expect(state).toEqual(views.PROFILE) 59 | }) 60 | 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/matchControllerSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var request = require('request'); 3 | 4 | var matchController = require('../server/matches/matchController.js'); 5 | 6 | describe('matchController', function () { 7 | before(function (done) { 8 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 9 | done(); 10 | }); 11 | }); 12 | 13 | describe('addForBoth', function () { 14 | it('should be a function', function () { 15 | assert.isFunction(matchController.addForBoth); 16 | }); 17 | it('should insert two matches for 2 users', function (done) { 18 | matchController.addForBoth('alanzfu', 'kweng2', 1, function (match2) { 19 | assert.isNumber(match2.get('challenge_id')); 20 | assert.equal(match2.get('user_github_handle'), 'kweng2'); 21 | done(); 22 | }) 23 | }) 24 | }); 25 | 26 | describe('editOneWhenValid', function () { 27 | it('should be a function', function() { 28 | assert.isFunction(matchController.editOneWhenValid); 29 | }); 30 | it('should edit an entry that exists', function (done) { 31 | matchController.editOneWhenValid({ 32 | github_handle: 'hahnbi', 33 | challenge_id: 3 34 | }, function (userMatch) { 35 | assert.equal(userMatch.get('win'), true); 36 | assert.equal(userMatch.get('user_github_handle'), 'hahnbi'); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('getAllByUser', function () { 43 | it('should be a function', function() { 44 | assert.isFunction(matchController.getAllByUser); 45 | }); 46 | it('should get all matches by a user', function (done) { 47 | request({ 48 | url: 'http://127.0.0.1:3000/api/users/alanzfu/matches', 49 | method: 'GET' 50 | }, function (err, response, body) { 51 | assert.equal(JSON.parse(body).length, 3); 52 | assert.equal(JSON.parse(body)[0].opponent_github_handle, 'hahnbi'); 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | after(function (done) { 59 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 60 | done(); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /server/responseRedis/responseRedisRunner.js: -------------------------------------------------------------------------------- 1 | var redis = require('redis'); 2 | var redisQueue = require('./redisQueue.js'); 3 | var solutionController = require('../solutions/solutionController.js'); 4 | var matchController = require('../matches/matchController.js'); 5 | 6 | var client; 7 | if (process.env.DEPLOYED) { 8 | client = redis.createClient(6379, 'redis'); 9 | } else { 10 | client = redis.createClient(); 11 | } 12 | 13 | var responseQueue = new redisQueue('rQueue', client); 14 | 15 | var responds = function (io) { 16 | // Wait for responses to arrive in `responseQueue`, then pop them out. 17 | // Afterwards, resume waiting for more responses to arrive 18 | responseQueue.pop(function (err, replies) { 19 | if (err) throw new Error(err); 20 | 21 | console.log('Successfully popped from', replies[0], 'with message: ', JSON.parse(replies[1]).message); 22 | 23 | // assume replies is a JSON object 24 | var reply = JSON.parse(replies[1]); 25 | var toSocket = reply.socket_id; 26 | var challenge_id = reply.challenge_id; 27 | var github_handle = reply.github_handle; 28 | var soln_str = reply.soln_str; 29 | var message = reply.message; 30 | var type = reply.type; 31 | var stdout = reply.stdout; 32 | 33 | if ( message === 'victory!') { 34 | solutionController.addSolution({ 35 | content: soln_str, 36 | challenge_id: challenge_id, 37 | github_handle: github_handle, 38 | type: type 39 | }); 40 | if(type === 'battle'){ 41 | matchController.editOneWhenValid({ 42 | challenge_id: challenge_id, 43 | github_handle: github_handle 44 | }, matchController.assignEloRating); 45 | } 46 | } 47 | var socketMessage = { 48 | message: message, 49 | challenge_id: challenge_id, 50 | github_handle: github_handle, 51 | stdout: stdout 52 | }; 53 | // Send evaluated response to socket 54 | if (type === 'battle' || type === 'solo') { 55 | io.to('/#'+toSocket).emit('eval', socketMessage); 56 | } else if (type === 'pair') { 57 | io.to('/#'+toSocket).emit('pair_eval', socketMessage); 58 | } 59 | 60 | // Keep listening 61 | responds(io); 62 | }); 63 | }; 64 | 65 | module.exports = responds; 66 | -------------------------------------------------------------------------------- /app/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | //ACTION CONSTANTS 5 | action: { 6 | //NavBar 7 | LOGOUT: 'LOGOUT', 8 | NAV_LOGIN: 'NAV_LOGIN', 9 | NAV_PROFILE: 'NAV_PROFILE', 10 | 11 | 12 | //Login 13 | LOGIN: 'LOGIN', 14 | NAV_STAGING: 'NAV_STAGING', 15 | IS_LOGGED_IN: 'IS_LOGGED_IN', 16 | IS_LOGGED_OUT: 'IS_LOGGED_OUT', 17 | 18 | //Staging 19 | NAV_SOLO_ARENA: 'NAV_SOLO_ARENA', 20 | NAV_CHALLENGE_ARENA: 'NAV_CHALLENGE_ARENA', 21 | CREATE_SOCKET: 'CREATE_SOCKET', 22 | NAV_SOLO_STAGING: 'NAV_SOLO_STAGING', 23 | STORE_USER_PROBLEMS: 'STORE_USER_PROBLEMS', 24 | STORE_MATCH_HISTORY: 'STORE_MATCH_HISTORY', 25 | 26 | //Arena 27 | GET_PROBLEM: 'GET_PROBLEM', 28 | GET_PROBLEM_SUCCESS: 'GET_PROBLEM_SUCCESS', 29 | GET_PROBLEM_ERROR: 'GET_PROBLEM_ERROR', 30 | SUBMIT_PROBLEM: 'SUBMIT_PROBLEM', 31 | SUBMIT_PROBLEM_WRONG: 'SUBMIT_PROBLEM_WRONG', 32 | SUBMIT_PROBLEM_SUCCESS: 'SUBMIT_PROBLEM_SUCCESS', 33 | NAV_LEADERBOARD: 'NAV_LEADERBOARD', 34 | STORE_EDITOR: 'STORE_EDITOR', 35 | STORE_EDITOR_OPPONENT: 'STORE_EDITOR_OPPONENT', 36 | SYNTAX_ERROR: 'SYNTAX_ERROR', 37 | NO_SYNTAX_ERROR: 'NO_SYNTAX_ERROR', 38 | STORE_SOLO_PROBLEM: 'STORE_SOLO_PROBLEM', 39 | CLEAR_INFO: 'CLEAR_INFO', 40 | COMPLETE_CHALLENGE: 'COMPLETE_CHALLENGE', 41 | LOST_CHALLENGE: 'LOST_CHALLENGE', 42 | PLAYER_LEAVE: 'PLAYER_LEAVE', 43 | GOT_OPPONENT_HANDLE: 'GOT_OPPONENT_HANDLE', 44 | DELAY_START: 'DELAY_START', 45 | COUNTDOWN: 'COUNTDOWN', 46 | RESET_PROMPT: 'RESET_PROMPT', 47 | EXIT_SPLASH: 'EXIT_SPLASH', 48 | LEAVING: 'LEAVING', 49 | 50 | 51 | 52 | //Arena - Sockets 53 | ENTER_ROOM: 'ENTER_ROOM', 54 | PROBLEM_START: 'PROBLEM_START', 55 | EXIT_ROOM: 'EXIT_ROOM', 56 | OPPONENT_FINISH: 'OPPONENT_FINISH', 57 | OPPONENT_KEYSTROKE: 'OPPONENT_KEYSTROKE', 58 | 59 | //Leaderboard 60 | GET_LEADERBOARD: 'GET_LEADERBOARD', 61 | GET_LEADERBOARD_SUCCESS: 'GET_LEADERBOARD_SUCCESS', 62 | GET_LEADERBOARD_ERROR: 'GET_LEADERBOARD_ERROR', 63 | GET_ELO: 'GET_ELO' 64 | }, 65 | 66 | //VIEW CONSTANTS 67 | view: { 68 | LOGIN: 'LOGIN', 69 | STAGING: 'STAGING', 70 | SOLO_ARENA: 'SOLO_ARENA', 71 | SOLO_STAGING: 'SOLO_STAGING', 72 | CHALLENGE_ARENA: 'CHALLENGE_ARENA', 73 | LEADERBOARD: 'LEADERBOARD', 74 | PROFILE: 'PROFILE' 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/sockets/socket-helper.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io-client'); 2 | var arenaAction = require('../actions/arenaActions'); 3 | var actions = require('../constants').action; 4 | //var stagingActions = require('../actions/stagingActions'); 5 | 6 | var socket = io(); 7 | // should be stored on state herestore.dispatch(stagingActions.createSocket()); 8 | 9 | socket.on('start', function (data) { 10 | var player = { 11 | github_handle: store.getState().user.github_handle, 12 | github_display_name: store.getState().user.github_display_name, 13 | github_profile_url: store.getState().user.github_profile_url, 14 | github_avatar_url: store.getState().user.github_avatar_url 15 | }; 16 | socket.emit('playerId', player); 17 | store.dispatch({ 18 | type: actions.DELAY_START 19 | }) 20 | setTimeout(function(){ 21 | return store.dispatch({ 22 | type: actions.GET_PROBLEM_SUCCESS, 23 | payload: data 24 | }); 25 | }, 5000); 26 | 27 | }); 28 | 29 | socket.on('keypress', function (data) { 30 | var array = data.split(''); 31 | var obf = []; 32 | for(var i =0; i 38 |
39 |
40 |
41 | 42 |
43 |
44 |

TRAINING

45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 |
53 |

ARENA

54 |
55 | 56 |
57 | 58 |
59 |
60 | {this.hoverPractice ?
: null} 61 | {this.hoverChallenge ?
: null} 62 |
63 | 64 | ) 65 | } 66 | }); 67 | 68 | module.exports = Staging; 69 | -------------------------------------------------------------------------------- /test/userControllerSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var request = require('request'); 3 | 4 | var userController = require('../server/users/userController.js'); 5 | 6 | describe('userController', function () { 7 | before(function (done) { 8 | console.log('Resetting DBs') 9 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 10 | done(); 11 | }); 12 | }); 13 | 14 | describe('getUserById', function () { 15 | it('should be a function', function () { 16 | assert.isFunction(userController.getUserById); 17 | }); 18 | it('should get a user by github handle', function (done) { 19 | request({ 20 | url: 'http://127.0.0.1:3000/api/users/puzzlehe4d', 21 | method: 'GET', 22 | json: {} 23 | }, function (err, response, body) { 24 | assert.equal(body.github_display_name, 'Harun Davood'); 25 | assert.equal(body.id, 2); 26 | assert.equal(response.statusCode, 200); 27 | done(); 28 | }); 29 | }); 30 | it('should return 404/null if user does not exist', function (done) { 31 | request({ 32 | url: 'http://127.0.0.1:3000/api/users/kweng3', 33 | method: 'GET', 34 | json: {} 35 | }, function (err, response, body) { 36 | assert.equal(response.statusCode, 404); 37 | assert.equal(body, null); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | describe('addUser', function () { 43 | it('should be a function', function () { 44 | assert.isFunction(userController.addUser); 45 | }); 46 | it('should add a new user', function (done) { 47 | request({ 48 | url: 'http://127.0.0.1:3000/api/users', 49 | method: 'POST', 50 | json: { 51 | github_handle: 'battlecoderbot' 52 | } 53 | }, function (err, response, body) { 54 | assert.equal(body.github_handle, 'battlecoderbot'); 55 | assert.equal(response.statusCode, 201); 56 | done(); 57 | }); 58 | }); 59 | it('should not add a new user if user exists', function (done) { 60 | request({ 61 | url: 'http://127.0.0.1:3000/api/users', 62 | method: 'POST', 63 | json: { 64 | github_handle: 'kweng2' 65 | } 66 | }, function (err, response, body) { 67 | assert.equal(body.github_handle, 'kweng2'); 68 | assert.equal(response.statusCode, 200); 69 | done(); 70 | }); 71 | }); 72 | }); 73 | 74 | after(function (done) { 75 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 76 | done(); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /app/tests/reducers/userReducerTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect'); 2 | var userReducer = require('../../reducers/userReducers.js'); 3 | var actions = require('../../constants').action; 4 | var navActions = require('../../actions/navActions.js'); 5 | var loginActions = require('../../actions/loginActions.js'); 6 | describe('User Reducer', function(){ 7 | var initialUserReducer = { 8 | isLoggedIn: false, 9 | github_handle: "", 10 | github_display_name: '', 11 | github_profile_url: '', 12 | github_avatar_url: '', 13 | user_problems: [], 14 | user_match_history: [] 15 | } 16 | 17 | Object.freeze(initialUserReducer); 18 | 19 | describe('#userReducer (immutability of state)', function(){ 20 | // inital state for Arena Reducer 21 | 22 | 23 | it('should return the initial state', function(){ 24 | expect( 25 | userReducer(undefined, {}) 26 | ).toEqual(initialUserReducer) 27 | }) 28 | 29 | it('should store user information when logged in', function(){ 30 | var state = userReducer(initialUserReducer, { 31 | type: actions.IS_LOGGED_IN, 32 | payload: { 33 | github_handle: 'puzzlehead', 34 | github_display_name:'seymour butts', 35 | github_profile_url:'http://example.com', 36 | github_avatar_url:'http://example.com' 37 | } 38 | }) 39 | expect(state.github_handle).toEqual('puzzlehead'); 40 | expect(state.github_display_name).toEqual('seymour butts'); 41 | expect(state.github_profile_url).toEqual('http://example.com'); 42 | expect(state.github_avatar_url).toEqual('http://example.com'); 43 | expect(state.isLoggedIn).toEqual(true); 44 | 45 | }) 46 | 47 | it('should delete user information when logged out', function(){ 48 | var state = userReducer(initialUserReducer, { 49 | type: actions.IS_LOGGED_IN, 50 | payload: { 51 | github_handle: 'puzzlehead', 52 | github_display_name:'seymour butts', 53 | github_profile_url:'http://example.com', 54 | github_avatar_url:'http://example.com' 55 | } 56 | }) 57 | state = userReducer(state, { 58 | type: actions.IS_LOGGED_OUT, 59 | }) 60 | 61 | expect(state.github_handle).toEqual(''); 62 | expect(state.github_display_name).toEqual(''); 63 | expect(state.github_profile_url).toEqual(''); 64 | expect(state.github_avatar_url).toEqual(''); 65 | 66 | }) 67 | 68 | it('should store user problems that user has completed', function(){ 69 | var state = userReducer(initialUserReducer, { 70 | type: actions.STORE_USER_PROBLEMS, 71 | payload: [] 72 | }) 73 | expect(state.user_problems).toBeA('array'); 74 | }) 75 | 76 | }) 77 | 78 | 79 | }) 80 | -------------------------------------------------------------------------------- /app/components/SoloArena.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var io = require('socket.io-client'); 3 | 4 | var ErrorList = require('./ErrorList'); 5 | var socket = require('../sockets/socket-helper'); 6 | 7 | var selfEditorOptions = { 8 | theme: "ace/theme/dawn", 9 | mode: "ace/mode/javascript", 10 | useSoftTabs: true, 11 | tabSize: 2, 12 | wrap: true, 13 | showPrintMargin: false, 14 | fontSize: 14, 15 | }; 16 | 17 | var SoloArena = React.createClass({ 18 | componentDidMount: function(){ 19 | var editor = ace.edit("editor"); 20 | editor.focus(); 21 | editor.setOptions(selfEditorOptions); 22 | editor.$blockScrolling = Infinity; 23 | 24 | // Key binding to enable Command-Enter or Ctrl-Enter to submit problem 25 | editor.commands.addCommand({ 26 | name: "replace", 27 | bindKey: {win: "Ctrl-Enter", mac: "Command-Enter"}, 28 | exec: function(editor) { 29 | this.submitProblem(); 30 | }.bind(this) 31 | }); 32 | 33 | this.props.arenaActions.storeEditor(editor); 34 | }, 35 | componentDidUpdate: function(){ 36 | this.props.arena.editorSolo.setValue(this.props.arena.content,1); 37 | var pos = this.props.arena.editorSolo.selection.getCursor(); 38 | this.props.arena.editorSolo.moveCursorTo(pos.row - 1, 2); 39 | }, 40 | 41 | submitProblem: function(){ 42 | var errors = this.props.arena.editorSolo.getSession().getAnnotations(); 43 | var content = this.props.arena.editorSolo.getSession().getValue(); 44 | this.props.arenaActions.submitProblem(errors, content, this.props.arena.socket.id, this.props.arena.problem_id, this.props.user.github_handle, 'solo'); 45 | }, 46 | 47 | render: function() { 48 | 49 | var submissionMessage; 50 | if (this.props.arena.submissionMessage === "Nothing passing so far...(From initial arena reducer)") { 51 | submissionMessage = null; 52 | } else if (this.props.arena.submissionMessage === "Solution passed all tests!") { 53 | submissionMessage =
{this.props.arena.submissionMessage}
54 | } else { 55 | submissionMessage =
{this.props.arena.submissionMessage}
56 | } 57 | return ( 58 |
59 | {this.props.arena.spinner ? : null} 60 |
61 |
62 |
63 | 64 | 65 |
66 |
{this.props.arena.stdout} 67 | {this.props.arena.syntaxMessage !== '' ? : null} 68 | {submissionMessage} 69 |
70 | 71 |
72 |
73 | ); 74 | } 75 | }); 76 | 77 | module.exports = SoloArena; 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Battle-Code 2 | Battle your peers head-to-head with intense coding challenges. 3 | 4 |
5 | 6 | ## Team 7 | 8 | - __Product Owner__: Hahnbi Sun 9 | - __Scrum Master__: Harun Davood 10 | - __Development Team Members__: [Harun Davood](https://github.com/puzzlehe4d), [Alan Fu](https://github.com/alanzfu), [Hahnbi Sun](https://github.com/hahnbi), [Kevin Weng](https://github.com/kweng2) 11 | 12 |
13 | 14 | ## Table of Contents 15 | 16 | 1. [Usage](#Usage) 17 | 1. [Requirements](#requirements) 18 | 1. [Development](#development) 19 | 1. [Installing Dependencies](#installing-dependencies) 20 | 1. [Roadmap](#roadmap) 21 | 1. [Contributing](#contributing) 22 | 1. [Architecture](#architecture) 23 | 1. [API](#api) 24 | 1. [Deployment](#deployment) 25 | 26 |
27 | 28 | ## Usage 29 | 30 | Load up the arena and try to complete as many of the test cases for the coding challenge as possible before your opponent. 31 |
32 | 33 | ## Requirements 34 | 35 | - Node 0.10.x 36 | - Express - 37 | - Postgresql 9.1.x 38 | - ORM 39 | - React - 40 | - Redux - 41 | 42 |
43 | 44 | ## Development 45 | 46 | ### Installing Dependencies 47 | 48 | From within the root directory: 49 | 50 | ```sh 51 | sudo npm install -g bower 52 | npm install 53 | bower install 54 | ``` 55 | 56 | ### Roadmap 57 | 58 | View the project roadmap [here](https://github.com/Dexterous-Rambutan/battle-code/issues) 59 | 60 |
61 | 62 | ## Contributing + Github Workflow 63 | 64 | See [github.md](github.md) for contribution and github workflow guidelines. 65 | 66 |
67 | 68 | ## Architecture 69 | 70 | ### High Level Architecture 71 | 72 | ![](http://i.imgur.com/mqdnWVh.png) 73 | 74 | ### Database Schema 75 | 76 | Database in Postgres, using Bookshelf and Knex 77 | ![](http://i.imgur.com/xyi07Rv.png) 78 | 79 | ### Socket Interactions 80 | 81 | ![](http://i.imgur.com/7s7RKSD.png) 82 | 83 | ![](http://i.imgur.com/w3Qfhy7.png) 84 | 85 | ![](http://i.imgur.com/6437Led.png) 86 | 87 | ![](http://i.imgur.com/1N1vi5h.png) 88 | 89 | ## API 90 | 91 | ##### Public End Points 92 | 93 | |Request|URL|Response| 94 | |---|---|---| 95 | |Log-in|/auth/github| | 96 | |Log-out|/logout| | 97 | |Verify User|/auth-verify|userObj| 98 | |Get User|/api/users/:githubHandle|userObj| 99 | |Get Match History|/api/users/:githubHandle/matches/|[matchObj...]| 100 | |Get User's Solutions|/api/solutions/user/:gitubHandle|[solutionObj...]| 101 | |Get Solution|/api/solutions/:solutionId|solutionObj| 102 | |Get Leaderboard|/api/solutions/:challenge_id/top|[solutionObj...]| 103 | |Get Challenge|/api/challenges/:challengeId|[challengeObj...]| 104 | 105 | ##### Admin Only 106 | 107 | |Request|URL|Response| 108 | |---|---|---| 109 | |GUI Access to Database|/addProblemsSolutions.html| | 110 | |Add Challenge|/api/challenges|challengeObj| 111 | |Get Random Challenge|/api/challenges|challengeObj| 112 | |Add User|/api/users|userObj| 113 | |Empty Database|/api/resetDB| | 114 | |Reseed Database|/api/resetDBWithData| | 115 | |Reseed Challenges Table|/api/resetChallenges| | 116 | 117 | 118 | [comment]: <> (|Get Leaderboard|/api/solutions/:challengeId|[solutionObj...]|) 119 | 120 | 121 | ## Deployment 122 | This has been deployed onto Digital Ocean using Docker containers. The backend architecture allows horizontal scaling of the solution worker to handle higher loads 123 | -------------------------------------------------------------------------------- /app/components/PairArena.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var io = require('socket.io-client'); 3 | var socket = require('../sockets/socket-helper'); 4 | var _ = require('lodash'); 5 | var ErrorList = require('./ErrorList'); 6 | 7 | var selfEditorOptions = { 8 | theme: "ace/theme/solarized_light", 9 | mode: "ace/mode/javascript", 10 | useSoftTabs: true, 11 | tabSize: 2, 12 | wrap: true 13 | }; 14 | var challengerEditorOptions = _.create(selfEditorOptions, { 15 | theme: "ace/theme/solarized_dark", 16 | readOnly: true, 17 | highlightActiveLine: false, 18 | highlightGutterLine: false 19 | }); 20 | 21 | var PairArena = React.createClass({ 22 | componentDidMount: function() { 23 | //setting up pair editor 24 | var editor = ace.edit('editor'); 25 | editor.setOptions(selfEditorOptions); 26 | editor.$blockScrolling = Infinity; 27 | this.props.arenaActions.storeEditor(editor); 28 | }, 29 | ready: function () { 30 | this.props.arenaActions.ready(); 31 | this.props.arena.socket.emit('ready'); 32 | }, 33 | emitSocket: function () { 34 | /////////////////////////////////////////////////////////////// 35 | //////// CHANGE THIS SECTION TO REFLECT PAIR MODE ///////////// 36 | /////////////////////////////////////////////////////////////// 37 | // if(this.props.arena.editorSolo.getSession().getValue()){ 38 | // this.props.arena.socket.emit('update', this.props.arena.editorSolo.getSession().getValue()) 39 | // } 40 | }, 41 | submitProblem: function(){ 42 | var errors = this.props.arena.editorSolo.getSession().getAnnotations(); 43 | var content = this.props.arena.editorSolo.getSession().getValue(); 44 | // if (errors.length > 0) { 45 | this.props.arena.socket.emit('syntaxErrors', errors); 46 | // } 47 | this.props.arenaActions.submitProblem(errors, content, this.props.arena.socket.id, this.props.arena.problem_id, this.props.user.github_handle, 'pair'); 48 | }, 49 | render: function() { 50 | 51 | return ( 52 |
53 |
54 | {this.props.arena.content ?
: null} 55 | {!this.props.arena.iAmReady ?
: null} 56 |
57 | {!!this.props.arena.opponent_info.github_handle ?
OPPONENT: {this.props.arena.opponent_info.github_handle}
: null} 58 | {this.props.arena.syntaxMessage !== '' ? : null} 59 | {this.props.arena.submissionMessage !== "Nothing passing so far...(From initial arena reducer)" ?
SUBMISSION RESPONSE: {this.props.arena.submissionMessage}
: null} 60 | {this.props.arena.stdout !== '' ?
Console:
{this.props.arena.stdout}
: null } 61 | {this.props.arena.opponentStatus !== '' ?
{this.props.arena.opponentStatus}
: null} 62 | {this.props.arena.status !== '' ?
{this.props.arena.status}
: null} 63 |
64 |
65 | ) 66 | }, 67 | 68 | componentDidUpdate: function(){ 69 | this.props.arena.editorSolo.setValue(this.props.arena.content,1); 70 | } 71 | }); 72 | 73 | module.exports = PairArena; 74 | -------------------------------------------------------------------------------- /test/challengeControllerSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var request = require('request'); 3 | 4 | var challengeController = require('../server/challenges/challengeController.js'); 5 | 6 | describe('challengeController', function () { 7 | before(function (done) { 8 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 9 | done(); 10 | }); 11 | }); 12 | 13 | describe('getChallenge', function () { 14 | it('should be a function', function () { 15 | assert.isFunction(challengeController.getChallenge); 16 | }); 17 | it('should get a random by challengeID', function (done) { 18 | request({ 19 | url: 'http://127.0.0.1:3000/api/challenges', 20 | method: 'GET', 21 | json: {} 22 | }, function (err, response, body) { 23 | assert.isNumber(body.id); 24 | assert.equal(response.statusCode, 200); 25 | done(); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('getChallengeMultiplayer', function () { 31 | it('should be a function', function() { 32 | assert.isFunction(challengeController.getChallengeMultiplayer); 33 | }); 34 | it('should get a random challenge that two players have not seen', function (done) { 35 | challengeController.getChallengeMultiplayer({ 36 | body: { 37 | player1_github_handle: 'kweng2', 38 | player2_github_handle: 'alanzfu' 39 | } 40 | }, function (challenge) { 41 | assert.isAbove(challenge.id, 3); 42 | assert.isBelow(challenge.id, 6); 43 | done(); 44 | }) 45 | }); 46 | }); 47 | 48 | describe('getChallengeById', function () { 49 | it('should be a function', function () { 50 | assert.isFunction(challengeController.getChallengeById); 51 | }); 52 | it('should get a challenge by challengeID', function (done) { 53 | request({ 54 | url: 'http://127.0.0.1:3000/api/challenges/3', 55 | method: 'GET', 56 | json: {} 57 | }, function (err, response, body) { 58 | assert.equal(body.id, 3); 59 | assert.equal(body.name, "Test function three") 60 | assert.equal(response.statusCode, 200); 61 | done(); 62 | }); 63 | }); 64 | it('should return 404/null if challenge does not exist', function (done) { 65 | request({ 66 | url: 'http://127.0.0.1:3000/api/challenges/15', 67 | method: 'GET', 68 | json: {} 69 | }, function (err, response, body) { 70 | assert.equal(body, null); 71 | assert.equal(response.statusCode, 404); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | describe('addChallenge', function () { 77 | it('should be a function', function () { 78 | assert.isFunction(challengeController.addChallenge); 79 | }); 80 | it('should add a new challenge', function (done) { 81 | request({ 82 | url: 'http://127.0.0.1:3000/api/challenges', 83 | method: 'POST', 84 | json: { 85 | name: 'testing_name', 86 | prompt: 'testing_prompt', 87 | test_suite: 'testing_test_suite' 88 | } 89 | }, function (err, response, body) { 90 | assert.equal(response.statusCode, 201); 91 | assert.equal(body.name, 'testing_name'); 92 | assert.equal(body.prompt, 'testing_prompt'); 93 | assert.equal(body.test_suite, 'testing_test_suite'); 94 | done(); 95 | }); 96 | }); 97 | }); 98 | 99 | after(function (done) { 100 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 101 | done(); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /server/users/userController.js: -------------------------------------------------------------------------------- 1 | var User = require('./userModel.js'); 2 | 3 | var userController = {}; 4 | 5 | // Try to fetch that user via route: /api/users/:github_handle, and 6 | // return {user object} 7 | userController.getUserById = function ( req, res ) { 8 | var github_handle = req.params.githubHandle; 9 | new User({github_handle: github_handle}).fetch() 10 | .then(function(user) { 11 | if (user) { 12 | res.status(200).json(user); 13 | } else { 14 | res.status(404).json(null); 15 | } 16 | }).catch(function (err) { 17 | res.status(500).json({error: true, data: {message: err.message}}); 18 | }); 19 | }; 20 | 21 | userController.getEloByUser = function (req, res) { 22 | 23 | var github_handle = req.params.githubHandle; 24 | new User ({github_handle: github_handle}).fetch() 25 | .then(function(user){ 26 | res.status(200).json(user.get('elo_rating')); 27 | }) 28 | .catch(function(err){ 29 | res.status(500).json({error: true, data: {message: err.message}}); 30 | }); 31 | }; 32 | // retrieve user object if it exists 33 | // otherwise ask DB to create user 34 | userController.addUser = function ( req, res ) { 35 | var github_handle = req.body.github_handle; 36 | var userAttr = { 37 | github_handle: req.body.github_handle, 38 | github_display_name: req.body.github_display_name, 39 | github_avatar_url: req.body.github_avatar_url, 40 | github_profile_url: req.body.github_profile_url, 41 | elo_rating: 1000, 42 | email: req.body.email 43 | }; 44 | // Construct a new user, and see if it already exists 45 | new User({github_handle: github_handle}).fetch() 46 | .then(function(user) { 47 | // if so, return that user object 48 | if (user) { 49 | if (res) { 50 | res.status(200).json(user); 51 | } 52 | // if not, add that user to the DB 53 | } else { 54 | User.forge(userAttr).save().then(function(newUser){ 55 | if (res) { 56 | res.status(201).json(newUser); 57 | } 58 | }).catch(function (err) { 59 | res.status(500).json({error: true, data: {message: err.message}}); 60 | }); 61 | } 62 | }); 63 | }; 64 | 65 | userController.resetWithData = function() { 66 | return User.forge({ 67 | github_handle: 'alanzfu', 68 | github_display_name: 'Alan Fu', 69 | github_avatar_url: 'https://avatars0.githubusercontent.com/u/7851211?v=3&s=460', 70 | github_profile_url: 'https://github.com/alanzfu', 71 | elo_rating:1000, 72 | email: null 73 | }).save().then(function() { 74 | return User.forge({ 75 | github_handle: 'puzzlehe4d', 76 | github_display_name: 'Harun Davood', 77 | github_avatar_url: 'https://avatars2.githubusercontent.com/u/12518929?v=3&s=460', 78 | github_profile_url: 'https://github.com/puzzlehe4d', 79 | elo_rating:1000, 80 | email: null 81 | }).save(); 82 | }).then(function() { 83 | return User.forge({ 84 | github_handle: 'kweng2', 85 | github_display_name: 'Kevin Weng', 86 | github_avatar_url: 'https://avatars2.githubusercontent.com/u/13741053?v=3&s=460', 87 | github_profile_url: 'https://github.com/kweng2', 88 | elo_rating:1000, 89 | email: null 90 | }).save(); 91 | }).then(function() { 92 | return User.forge({ 93 | github_handle: 'hahnbi', 94 | github_display_name: 'Hahnbi Sun', 95 | github_avatar_url: 'https://avatars3.githubusercontent.com/u/12260923?v=3&s=460', 96 | github_profile_url: 'https://github.com/hahnbi', 97 | elo_rating:1000, 98 | email: null 99 | }).save(); 100 | }) 101 | } 102 | 103 | module.exports = userController; 104 | -------------------------------------------------------------------------------- /app/containers/App.js: -------------------------------------------------------------------------------- 1 | //react & redux require 2 | var _ = require('lodash'); 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var { bindActionCreators } = require('redux'); 6 | var { connect } = require('react-redux'); 7 | var io = require('socket.io-client'); 8 | 9 | 10 | //actions require 11 | var actions = require('../actions'); 12 | 13 | //component require 14 | var components = require('../components'); 15 | 16 | //constant requires (views) 17 | var views = require('../constants').view; 18 | 19 | 20 | var contextType = { 21 | redux: React.PropTypes.object 22 | }; 23 | 24 | var App = React.createClass({ 25 | componentWillMount: function(){ 26 | this.props.loginActions.checkLoggedIn(); 27 | }, 28 | 29 | render: function(){ 30 | if(this.props.user.isLoggedIn){ 31 | switch(this.props.view) { 32 | case views.STAGING: 33 | //history.pushState(store.getState(), 'Staging', "staging"); 34 | return ( 35 |
36 | 37 | 38 |
39 | ); 40 | case views.SOLO_ARENA: 41 | //history.pushState(store.getState(), 'Arena', "arena"); 42 | return ( 43 |
44 | 45 | 46 |
47 | ); 48 | case views.CHALLENGE_ARENA: 49 | //history.pushState(store.getState(), 'Arena', "arena"); 50 | return ( 51 |
52 | 53 | 54 |
55 | ); 56 | case views.LEADERBOARD: 57 | //history.pushState(store.getState(), 'Leaderboard', "leadboard"); 58 | return ( 59 |
60 | 61 | 62 |
63 | ); 64 | case views.PROFILE: 65 | //history.pushState(store.getState(), 'Leaderboard', "leadboard"); 66 | return ( 67 |
68 | 69 | 70 | 71 |
72 | ); 73 | case views.SOLO_STAGING: 74 | //history.pushState(store.getState(), 'Leaderboard', "leadboard"); 75 | return ( 76 |
77 | 78 | 79 | 80 |
81 | ); 82 | } 83 | } else { 84 | //history.pushState(store.getState(), 'Login', "login"); 85 | return ( 86 |
87 | 88 |
89 | ); 90 | } 91 | 92 | } 93 | }); 94 | 95 | function mapStateToProps(state) { 96 | // instantiate empty object 97 | // keys currently are: user, view, newRace, activeRace 98 | var mapping = {}; 99 | 100 | for (var k in state){ 101 | mapping[k] = state[k]; 102 | } 103 | 104 | return mapping; 105 | } 106 | 107 | function mapDispatchToProps(dispatch) { 108 | // console.log("THE MAPPED ACTIONS", actions); 109 | var actionsObj = {} 110 | for(var key in actions) { 111 | actionsObj[key] = bindActionCreators(actions[key], dispatch); 112 | } 113 | return actionsObj; 114 | } 115 | 116 | module.exports = connect(mapStateToProps, mapDispatchToProps)(App); 117 | -------------------------------------------------------------------------------- /PRESS_RELEASE.md: -------------------------------------------------------------------------------- 1 | # BattleCode # 2 | 3 | 18 | 19 | ## Heading ## 20 | BattleCode 21 | 22 | ## Sub-Heading ## 23 | Put your coding skills to the test in a one-on-one programming challenge! 24 | 25 | ## Summary ## 26 | This product allows users to compete against each other in solving coding problems. 27 | 28 | ## Problem ## 29 | Practicing coding is often a dry and grinding experience. 30 | 31 | ## Solution ## 32 | BattleCode combines coding with the spirit of competition, which inspires users to want to improve their own coding. 33 | 34 | ## Quote from You ## 35 | "With BattleCode, people who want to improve their programming skills can do so in a fun and competitive environment, providing the opportunity to test skills against other inspiring coders." 36 | 37 | ## How to Get Started ## 38 | Head over to (deployed link) to sign up for an account to get started! 39 | 40 | ## Customer Quote ## 41 | "BattleCode makes me want to code." 42 | 43 | ## Closing and Call to Action ## 44 | Many call themselves coders or programmers. See how your skills match up to the rest of them. 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js exports a configuration object in the CommonJS pattern. 2 | module.exports = { 3 | 4 | // `entry` gives a path to the file that is the "root" of the dependency 5 | // tree, since Webpack walks through your files and builds a bundle by 6 | // reading `require` statements in each of them. So think of `entry` as 7 | // the top-level file that then `requires` some other files, which then 8 | // `require` some other files, etc. Webpack pulls these all into a modularized 9 | // bundle. 10 | entry: './app/index.js', 11 | 12 | // `output` is an object with options for the bundle that Webpack creates 13 | // out of your source files. 14 | output: { 15 | 16 | // `path` is a path to the directory where your bundle will be written. 17 | path: 'server/public', 18 | 19 | // `publicPath` is optional. It allows you to set a separate path that will 20 | // be used by any lazy-loading in your Webpack scripts to load more chunks 21 | // from the server. Basically, `path` sets where in your project's file structure 22 | // your bundle will be written, while `publicPath` tells your Webpack modules 23 | // where your bundle can be requested from the server. In this repo, `publicPath` 24 | // tells the webpack-dev-server that it's ok to serve the files in the dist folder. 25 | publicPath: 'public', 26 | 27 | // `filename` tells Webpack what to call the file/files it outputs. 28 | filename: 'bundle.js', 29 | }, 30 | 31 | // `module` is an object with options for how Webpack processes the files it loads 32 | // when it scans a `require` statement. 99% of the time, `loaders` will be the only 33 | // thing you specify inside of `module`. 34 | module: { 35 | 36 | // `loaders` lets you plug in programs that will transform your source files 37 | // when Webpack loads them with a `require` statement. A lot of the magic of 38 | // Webpack is done using loaders. In this example, there's one loader declared 39 | // to use Babel to transform ES6 and JSX into ES5. 40 | // 41 | // `loaders` is an array of objects. 42 | loaders: [ 43 | { 44 | // `test` is a test condition that causes the loader to be applied when a 45 | // filename passes. In this case, when any filename contains either `.js` or `.jsx` 46 | // as its terminating characters, this loader will be applied. 47 | test: /\.jsx?$/, 48 | 49 | // `exclude` lets you specify tests that, when passed by a filename, cause those 50 | // files to *not* be transformed by the loader. There's also an `include` option 51 | // that works in the inverse way. 52 | exclude: /(node_modules|bower_components)/, 53 | 54 | // `loader` names the actual loader that is to be applied. In this case, 55 | // this object requires 'babel-loader' to do the transformation. 56 | // We could actually apply multiple loaders here by using the property `loaders` 57 | // instead of `loader`, which takes an array of loader names. 58 | // 59 | // When you're declaring loaders in this field, you can leave off the `-loader` part 60 | // of the package name. Webpack will interpret `babel` as `babel-loader` here, 61 | // `coffee` as `coffee-loader`, etc. But you can also just write out `babel-loader`, 62 | // if you prefer. 63 | loader: 'babel', 64 | 65 | // `query` lets you pass options to the loader's process. The options that a loader takes 66 | // are specific to each loader. In this case, `babel-loader` is being told to use the 'react' 67 | // and 'es2015' presets when it transforms files. `query` becomes a query string, similar 68 | // to what you see in request URLs, and the same thing could be achieved by writing this above: 69 | // loader: 'babel?presets[]=react,presets[]=es2015' 70 | query: { 71 | presets: ['react', 'es2015'], 72 | } 73 | }, 74 | ] 75 | } 76 | 77 | }; 78 | -------------------------------------------------------------------------------- /server/public/styles/profile.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | position:fixed; 3 | top:0; 4 | left:0; 5 | height:100%; 6 | width:100%; 7 | z-index:1; 8 | background-color:#444444; 9 | opacity:0.5; 10 | filter:alpha(opacity=50); 11 | } 12 | 13 | .exit { 14 | position: absolute; 15 | right: -20px; 16 | top: -20px; 17 | z-index:4; 18 | height: 40px; 19 | width: 40px; 20 | border-radius: 20px; 21 | font-weight: bolder; 22 | background-color: rgb(180,180,180); 23 | } 24 | 25 | .exit:hover { 26 | cursor: pointer; 27 | } 28 | 29 | .profile { 30 | display: flex; 31 | padding: 20px 20px; 32 | position: fixed; 33 | border-radius: 4px; 34 | z-index:3; 35 | background-color: #efefef; 36 | top: 50px; 37 | left: 50%; 38 | width: 800px; 39 | margin-left: -400px; 40 | height: 570px; 41 | } 42 | 43 | .profile-top-row { 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | .profile-card { 49 | padding: 0px; 50 | margin: 10px 10px; 51 | flex-direction: column; 52 | } 53 | 54 | .basic-info { 55 | width: 150px; 56 | height: 200px; 57 | } 58 | 59 | .profile-img img{ 60 | width: 150px; 61 | border-radius: 5px; 62 | } 63 | 64 | .profile-img { 65 | margin: 0 auto; 66 | } 67 | 68 | .profile-name { 69 | margin: 10px 0px 0px 10px; 70 | font-size: 1em; 71 | } 72 | 73 | .profile-github { 74 | margin-left: 10px; 75 | margin-bottom: 10px; 76 | } 77 | 78 | .profile-github a { 79 | text-decoration: none; 80 | color: #8b1919; 81 | font-size: 0.9em; 82 | } 83 | 84 | .match-stats { 85 | flex-direction: column; 86 | justify-content: center; 87 | clear: right; 88 | height: 180px; 89 | width: 150px; 90 | } 91 | 92 | .win-loss-record { 93 | text-align: center; 94 | } 95 | 96 | .profile-record { 97 | text-align: center; 98 | font-size: 0.9em; 99 | } 100 | 101 | .elo-rating { 102 | padding: 10px; 103 | } 104 | 105 | .elo-rating h2 { 106 | margin-top: -10px; 107 | font-size: 1em; 108 | font-weight: 300; 109 | } 110 | 111 | .elo-rating-content { 112 | text-align: center; 113 | font-size: 2.5em; 114 | color: #8b1919; 115 | } 116 | 117 | .profile-matches { 118 | margin: 10px; 119 | height: 510px; 120 | } 121 | 122 | .match-grid-header { 123 | margin: 20px 30px 5px 30px; 124 | font-size: 1.5em; 125 | font-weight: 300; 126 | } 127 | 128 | .profile-match-grid{ 129 | display: flex; 130 | flex-wrap: wrap; 131 | flex-direction: row; 132 | justify-content: center; 133 | align-content: flex-start; 134 | overflow-y: auto; 135 | width: 570px; 136 | height: 430px; 137 | } 138 | 139 | .opponent-profile-image { 140 | height: 75px; 141 | } 142 | 143 | .match-result { 144 | display: flex; 145 | justify-content: center; 146 | align-items: center;; 147 | flex-direction: column; 148 | font-size: 2em; 149 | font-weight: 600; 150 | width: 75px; 151 | } 152 | 153 | .match-won { 154 | color: rgba(0,51,0,0.8); 155 | } 156 | 157 | .match-lost { 158 | color: rgba(128,0,0,0.8); 159 | } 160 | 161 | .match-detail-opponent { 162 | width: 170px; 163 | } 164 | 165 | .match-detail-challenge-title { 166 | font-size: 12px; 167 | width: 120px; 168 | } 169 | 170 | .match-detail-date { 171 | font-size: 12px; 172 | width: 0px; 173 | } 174 | 175 | .match-opponent-url { 176 | margin-bottom: 3px; 177 | } 178 | 179 | .match-card-container { 180 | margin: 10px; 181 | } 182 | 183 | .match-profile-card { 184 | margin: 10px; 185 | text-decoration: none; 186 | color: rgb(127, 127, 127); 187 | width: 520px; 188 | height: 75px; 189 | display: flex; 190 | align-items: center; 191 | } 192 | 193 | .match-profile-card-blank { 194 | width: 500px; 195 | height: 75px; 196 | margin: 10px; 197 | } 198 | 199 | div.match-profile-card:hover { 200 | border-right: 10px solid #8b1919; 201 | } 202 | 203 | .profile-offset-card { 204 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 2px 5px rgba(0,0,0,0.24); 205 | margin: 0; 206 | } 207 | 208 | canvas{ 209 | width: 80% !important; 210 | max-width: 800px; 211 | height: auto !important; 212 | } 213 | -------------------------------------------------------------------------------- /server/sockets/socketsChallengeArena.js: -------------------------------------------------------------------------------- 1 | // Socket matchmaking system here: 2 | var challengeController = require('../challenges/challengeController.js'); 3 | var solutionController = require('../solutions/solutionController.js'); 4 | var matchController = require('../matches/matchController.js'); 5 | var findRoom = require('./findRoom.js'); 6 | 7 | module.exports = function (io) { 8 | 9 | var openQ = []; 10 | var roomCounter = 0; 11 | io.on('connection', function (socket) { 12 | socket.on('playerId', function(data){ 13 | socket.to(findRoom(socket)).broadcast.emit('otherPlayer', data) 14 | }); 15 | socket.on('won', function(data){ 16 | socket.to(findRoom(socket)).broadcast.emit('won', data); 17 | }); 18 | socket.on('update', function (data) { 19 | console.log('room is:', socket.rooms); 20 | socket.to(findRoom(socket)).broadcast.emit('keypress', data); 21 | }); 22 | 23 | console.log('socketsChallengeArena.js, line-25, Socket connected:', socket.id, socket.rooms); 24 | socket.on('arena', function (github_handle) { 25 | // if there aren't any open room, create a room and join it 26 | if (openQ.length === 0) { 27 | // create a room 28 | roomCounter++; 29 | console.log('socketsChallengeArena.js, line-31, Creating and joining new room', roomCounter); 30 | socket.join(String(roomCounter)); 31 | // add this room to the openQ 32 | //require redis here 33 | // 34 | 35 | openQ.push({ 36 | name: roomCounter, 37 | players: [github_handle], 38 | socket_id: [socket.id] 39 | }); 40 | // Otherwise, there is an open room, join that one 41 | } else { 42 | var existingRoom = openQ.shift(); 43 | // join the first existing room 44 | console.log('socketsChallengeArena.js, line-46, Joining existing room:', existingRoom.name); 45 | socket.join(String(existingRoom.name)); 46 | // remove this room from the openQ and add to inProgressRooms 47 | // find all players in the room and find a challenge neither player has seen 48 | var otherPlayer = existingRoom.players[0]; 49 | challengeController.getChallengeMultiplayer({ 50 | body: { 51 | player1_github_handle: otherPlayer, 52 | player2_github_handle: github_handle, 53 | type: 'battle' 54 | } 55 | }, function (challenge) { 56 | if (challenge !== null) { 57 | //initialize the solutions so that there is record of attempt 58 | solutionController.initializeChallengeSolutions(otherPlayer, github_handle, challenge.id, 'battle'); 59 | matchController.addForBoth(otherPlayer, github_handle, challenge.id); 60 | // emit start event to this entire room 61 | io.to(String(existingRoom.name)).emit('start', challenge); 62 | } else { 63 | //initialize the solutions so that there is record of attempt 64 | challenge = { 65 | id: null, 66 | name: null, 67 | prompt: '/*Sorry we ran out of problems! \nPlease exit and re-enter the room to try again*/' 68 | }; 69 | io.to(String(existingRoom.name)).emit('start', challenge); 70 | } 71 | }); 72 | } 73 | }); 74 | socket.on('leaveArena', function (data) { 75 | var foundRoom = findRoom(socket); 76 | console.log('server.js line 117, Leaving room: ', foundRoom); 77 | socket.leave(foundRoom); 78 | socket.to(foundRoom).broadcast.emit('playerLeave', data); 79 | if(openQ.length !== 0 && foundRoom == openQ[0].name) { 80 | openQ.shift(); 81 | } 82 | }); 83 | socket.on('disconnect', function () { 84 | if (openQ[0]) { 85 | if (openQ[0]['socket_id'][0] == socket.id) { 86 | console.log('Client disconnected prior to starting a challenge,', socket.id); 87 | openQ.shift(); 88 | } 89 | } else { 90 | console.log('Client disconnected after having started a challenge', socket.id); 91 | } 92 | }); 93 | 94 | }); 95 | 96 | }; 97 | -------------------------------------------------------------------------------- /app/actions/arenaActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var actions = require('../constants').action; 3 | 4 | var storeEditor = function(payload) { 5 | return { 6 | type: actions.STORE_EDITOR, 7 | payload: payload 8 | }; 9 | }; 10 | 11 | var storeEditorOpponent = function (payload) { 12 | return { 13 | type: actions.STORE_EDITOR_OPPONENT, 14 | payload: payload 15 | }; 16 | }; 17 | 18 | var countdown = function () { 19 | return ({ 20 | type: actions.COUNTDOWN 21 | }); 22 | }; 23 | 24 | var resetPrompt = function () { 25 | return ({ 26 | type: actions.RESET_PROMPT 27 | }); 28 | }; 29 | 30 | var getProblem = function (payload) { 31 | return function(dispatch){ 32 | $.ajax({ 33 | method: 'GET', 34 | url: '/api/challenges/:' + payload.challenge_id, 35 | dataType: 'json', 36 | cache: false, 37 | success: function (data) { 38 | dispatch({ 39 | type: actions.GET_PROBLEM_SUCCESS, 40 | payload: data 41 | }); 42 | }, 43 | error: function (error) { 44 | dispatch({ 45 | type: actions.GET_PROBLEM_ERROR 46 | }); 47 | } 48 | }); 49 | }; 50 | }; 51 | 52 | var getLeaderBoard = function(id) { 53 | return function(dispatch) { 54 | $.ajax({ 55 | method: 'GET', 56 | url: '/api/solutions/' + id + '/top', 57 | dataType: 'json', 58 | cache: false, 59 | success: function (data) { 60 | 61 | dispatch({ 62 | type: actions.GET_LEADERBOARD_SUCCESS, 63 | payload:data 64 | }) 65 | }, 66 | 67 | error: function(error) { 68 | console.log('error getting leaderboard', error); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | var lostChallenge = function (payload) { 75 | return { 76 | type: actions.LOST_CHALLENGE, 77 | payload: payload 78 | }; 79 | }; 80 | 81 | var playerLeave = function (payload) { 82 | return { 83 | type: actions.PLAYER_LEAVE, 84 | payload: payload 85 | }; 86 | }; 87 | 88 | var exitSplash = function () { 89 | console.log('here') 90 | return { 91 | type: actions.EXIT_SPLASH 92 | }; 93 | }; 94 | 95 | var submitProblem = function (errors, solution_str, socket_id, problem_id, user_handle, type) { 96 | 97 | if(errors.length === 0) { 98 | return function (dispatch) { 99 | dispatch({ 100 | type: actions.NO_SYNTAX_ERROR, 101 | payload: solution_str 102 | }); 103 | $.ajax({ 104 | method:'POST', 105 | url: '/api/solutions/' + problem_id, 106 | contentType: 'application/json', 107 | data: JSON.stringify({ 108 | soln_str: solution_str, 109 | user_handle: user_handle, 110 | socket_id: socket_id, 111 | type: type 112 | }) 113 | }); 114 | }; 115 | } else { 116 | return { 117 | type: actions.SYNTAX_ERROR, 118 | payload: { 119 | errors: errors, 120 | solution_str: solution_str 121 | } 122 | }; 123 | } 124 | }; 125 | 126 | var handleSubmissionResponse = function (payload) { 127 | return function (dispatch) { 128 | if (payload.message === 'victory!') { 129 | // inform of submission success 130 | dispatch({ 131 | type: actions.SUBMIT_PROBLEM_SUCCESS, 132 | payload: { 133 | stdout: payload.stdout 134 | } 135 | }); 136 | dispatch({ 137 | type: actions.COMPLETE_CHALLENGE 138 | }); 139 | } else { 140 | dispatch({ 141 | type: actions.SUBMIT_PROBLEM_WRONG, 142 | payload: { 143 | message: payload.message, 144 | stdout: payload.stdout 145 | } 146 | }); 147 | } 148 | }; 149 | }; 150 | 151 | 152 | module.exports = { 153 | getProblem: getProblem, 154 | submitProblem: submitProblem, 155 | handleSubmissionResponse: handleSubmissionResponse, 156 | storeEditor: storeEditor, 157 | storeEditorOpponent: storeEditorOpponent, 158 | lostChallenge: lostChallenge, 159 | playerLeave: playerLeave, 160 | countdown: countdown, 161 | resetPrompt: resetPrompt, 162 | getLeaderBoard: getLeaderBoard, 163 | exitSplash: exitSplash 164 | }; 165 | -------------------------------------------------------------------------------- /app/components/Profile.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var moment = require('moment'); 3 | 4 | var Profile = React.createClass({ 5 | componentDidUpdate: function () { 6 | var wins = 0; 7 | var loss = 0; 8 | var matchHistory = this.props.user.user_match_history; 9 | for ( var i = 0; i < matchHistory.length; i ++ ) { 10 | wins += matchHistory[i].win ? 1 : 0; 11 | loss += matchHistory[i].win ? 0 : 1; 12 | } 13 | console.log('Wins:', wins, 'Losses:', loss); 14 | var options = { 15 | animationSteps:40, 16 | animationEasing: "easeOutBack" 17 | }; 18 | var ctx = document.getElementById("myChart").getContext("2d"); 19 | var data = [{ 20 | value: wins, 21 | color:"rgba(0,51,0,0.6)", 22 | highlight: "rgba(0,51,0,0.8)", 23 | label: "Wins" 24 | },{ 25 | value: loss, 26 | color: "rgba(128,0,0,0.8)", 27 | highlight: "rgba(128,0,0,1)", 28 | label: "Losses" 29 | }]; 30 | var myPieChart = new Chart(ctx).Pie(data,options); 31 | }, 32 | 33 | render: function() { 34 | var wins = 0; 35 | var loss = 0; 36 | this.props.user.user_match_history.forEach(function (match) { 37 | wins += match.win ? 1 : 0; 38 | loss += match.win ? 0 : 1; 39 | }); 40 | 41 | var matchHistory = this.props.user.user_match_history.map(function (match) { 42 | var opponent = match.opponent_github_handle; 43 | var opponentURL = "http://github.com/" + opponent; 44 | var date = match.created_at; 45 | date = moment(date).format("l"); 46 | var linkToProblem = function(){ 47 | this.props.navActions.navSoloArena(match); 48 | }.bind(this); 49 | return ( 50 | 51 |
52 |
53 | {match.win ?
W
:
L
} 54 |
{opponent}
55 |
{match.challenge_name}
56 |
{date}
57 |
58 |
59 | ) 60 | }.bind(this)); 61 | 62 | return ( 63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 | 72 |
73 |
74 | {this.props.user.github_display_name} 75 |
76 | 79 |
80 | 81 |
82 |
83 | 84 |
85 |
86 | Wins: {wins}   87 | Losses: {loss} 88 |
89 |
90 | 91 |
92 |

ELO RATING

93 |
{this.props.user.elo_rating}
94 |
95 |
96 | 97 |
98 |

MATCH HISTORY

99 | {/*listOfProblems*/} 100 |
101 | {matchHistory} 102 |
103 | 104 |
105 |
106 |
107 | ) 108 | } 109 | }); 110 | 111 | module.exports = Profile; 112 | -------------------------------------------------------------------------------- /worker/solutionTester.js: -------------------------------------------------------------------------------- 1 | var vm = require('vm'); 2 | var util = require('util'); 3 | var redis = require('redis'); 4 | var fs = require('fs'); 5 | var Queue = require('./queue.js'); 6 | var Solution; 7 | var Challenge; 8 | var client; 9 | if (process.env.DEPLOYED) { 10 | client = redis.createClient(6379, 'redis'); 11 | Solution = require('./solutions/solutionModel.js'); 12 | Challenge = require('./challenges/challengeModel.js'); 13 | } else { 14 | client = redis.createClient(); 15 | Solution = require('../server/solutions/solutionModel.js'); 16 | Challenge = require('../server/challenges/challengeModel.js'); 17 | } 18 | 19 | var testQueue = new Queue('testQueue', client); 20 | var responseQueue = new Queue('rQueue', client); 21 | 22 | runTest(); 23 | 24 | function runTest() { 25 | console.log('Listening for items to enter testQueue...'); 26 | 27 | // Pop from testQueue any solutions waiting to be tested 28 | testQueue.pop(function (err, results) { 29 | if (err) { 30 | fs.appendFile('ERROR_LOG.txt', JSON.stringify(err), function(er) { 31 | if (er) throw er; 32 | console.log('Redis list blocking pop threw an error, check ERROR_LOG!'); 33 | }); 34 | throw new Error(err); 35 | } 36 | console.log('successfully popped from testQueue'); 37 | 38 | // Parse the solution string from testQueue 39 | var solutionInfo = JSON.parse(results[1]); 40 | var challenge = Challenge.forge({ 41 | id: solutionInfo.challenge_id 42 | }); 43 | 44 | // Fetch the challenge test suite 45 | challenge.fetch() 46 | .then(function (challenge) { 47 | console.log('Successfully fetched challenge', challenge); 48 | // create a sandbox and load chai into that context 49 | var context = new vm.createContext(); 50 | // Load test suite into the context 51 | var testText = challenge.get('test_suite'); 52 | var testScript = new vm.Script(testText); 53 | var solutionText = solutionInfo.soln_str; 54 | var stdout = ''; 55 | 56 | // Try to run solution string against the test suite 57 | try { 58 | // hooking into node's stdout 59 | context.console = console; 60 | function hook_stdout(callback) { 61 | var old_write = process.stdout.write 62 | process.stdout.write = (function(write) { 63 | return function(string, encoding, fd) { 64 | write.apply(process.stdout, arguments) 65 | callback(string, encoding, fd) 66 | } 67 | })(process.stdout.write) 68 | return function() { 69 | process.stdout.write = old_write 70 | } 71 | } 72 | var unhook = hook_stdout(function(string, encoding, fd) { 73 | stdout += string; 74 | }) 75 | 76 | // Try to load the solution string into the context 77 | var solutionScript = new vm.Script(solutionText); 78 | solutionScript.runInContext(context, {timeout:2000}); 79 | 80 | // Try to run tests 81 | context.assert = require('chai').assert; 82 | testScript.runInContext(context,{timeout:2000}); 83 | 84 | // unhook console.log 85 | unhook(); 86 | 87 | // Successful evaluation, add response to rQueue 88 | console.log('Successfully evaluated the solution!'); 89 | responseQueue.push(JSON.stringify({ 90 | socket_id: solutionInfo.socket_id, 91 | challenge_id: challenge.get('id'), 92 | github_handle: solutionInfo.user_handle, 93 | soln_str: solutionText, 94 | type: solutionInfo.type, 95 | message: 'victory!', 96 | stdout: stdout 97 | }), runTest); 98 | } catch (e) { 99 | // Failed evaluation, add response to rQueue 100 | unhook(); 101 | console.log('Failed while evaluating the solution', e.message); 102 | responseQueue.push(JSON.stringify({ 103 | socket_id: solutionInfo.socket_id, 104 | challenge_id: challenge.get('id'), 105 | github_handle: solutionInfo.user_handle, 106 | soln_str: solutionText, 107 | type: solutionInfo.type, 108 | message: e.message, 109 | stdout: stdout 110 | }), runTest); 111 | } 112 | }) 113 | // Potential errors include: no such challenge 114 | .catch(function (err) { 115 | console.log('got an error despite the try catch block', err); 116 | return new Error(err); 117 | }); 118 | }); 119 | } 120 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | var challengeController = require('./challenges/challengeController.js'); 2 | var userController = require('./users/userController.js'); 3 | var solutionController = require('./solutions/solutionController.js'); 4 | var matchController = require('./matches/matchController.js'); 5 | var passport = require('./helpers/psConfig.js'); 6 | var db = require('./helpers/dbConfig.js'); 7 | var adminPrivilege = require('./users/adminPrivilege.js'); 8 | 9 | module.exports = function (app, redisClient) { 10 | 11 | // Try to authenticate via github 12 | app.get('/auth/github', passport.authenticate('github')); 13 | 14 | // once authentication completes, redirect to / 15 | app.get('/login/callback', 16 | passport.authenticate('github', { failureRedirect: '/login' }), 17 | function(req, res) { 18 | req.session.loggedIn = true; 19 | console.log('Successfully logged in as: ', req.session.passport.user.displayName); 20 | 21 | // Add user to DB 22 | req.body.github_handle = req.session.passport.user.username; 23 | req.body.github_display_name = req.session.passport.user.displayName; 24 | req.body.github_avatar_url = req.session.passport.user._json.avatar_url; 25 | req.body.github_profile_url = req.session.passport.user.profileUrl; 26 | req.body.elo_rating = 1000; 27 | req.body.email = req.session.passport.user.emails ? req.session.passport.user.emails[0].value : ''; 28 | userController.addUser(req); 29 | 30 | // Once complete, redirect to home page 31 | res.redirect('/'); 32 | }); 33 | app.get('/logout', function (req, res) { 34 | req.session.loggedIn = false; 35 | res.redirect('/'); 36 | }); 37 | 38 | // When client needs to verify if they are authenticated 39 | app.get('/auth-verify', function(req, res) { 40 | console.log('GET request to /auth-verify', req.session.loggedIn); 41 | if (req.session.loggedIn) { 42 | req.params.githubHandle = req.session.passport.user.username || ''; 43 | userController.getUserById(req, res); 44 | } else { 45 | res.status(403).json(null); 46 | } 47 | }); 48 | 49 | app.get('/api/users/:githubHandle', userController.getUserById); 50 | app.get('/api/users/:githubHandle/elo', userController.getEloByUser); 51 | app.get('/api/users/:githubHandle/matches', matchController.getAllByUser); 52 | 53 | // Used by solo/practice mode 54 | app.get('/api/challenges/:challengeId', challengeController.getChallengeById); 55 | 56 | app.get('/api/solutions/:solutionId', solutionController.getSolutionById); 57 | app.get('/api/solutions/user/:githubHandle', solutionController.getAllSolutionsForUser); 58 | app.get('/api/solutions/:challenge_id/top', solutionController.getTopSolutions); 59 | app.post('/api/solutions/:challengeId', function (req, res) { 60 | solutionController.testSolution(req, res, redisClient); 61 | }); 62 | 63 | // Add user to the db, require admin privilige. Normal users go through /auth/login 64 | app.post('/api/users', adminPrivilege, userController.addUser); 65 | 66 | // Get a random challenge, disabled for the public 67 | app.get('/api/challenges', adminPrivilege, challengeController.getChallenge); 68 | 69 | // GUI to add solutions and challenges to the DB directly 70 | app.get('/addProblemsSolutions.html', adminPrivilege); 71 | 72 | // Add challenge to the db, admin privilige required 73 | app.post('/api/challenges', adminPrivilege, challengeController.addChallenge); 74 | 75 | // Update a challenge to the db 76 | app.put('/api/challenges/:challengeId', adminPrivilege, challengeController.update); 77 | 78 | // Reset the database to be blank 79 | app.get('/api/resetDB', adminPrivilege, db.resetEverything); 80 | 81 | // Reset the datbabase with some seed data 82 | app.get('/api/resetDBWithData', adminPrivilege, function (req, res) { 83 | db.resetEverythingPromise() 84 | .then(function() { 85 | return userController.resetWithData(); 86 | }) 87 | .then(function() { 88 | return challengeController.resetWithData(); 89 | }) 90 | .then(function() { 91 | return solutionController.resetWithData(); 92 | }) 93 | .then(function() { 94 | return matchController.resetWithData(); 95 | }) 96 | .then(function() { 97 | res.status(201).end(); 98 | return; 99 | }) 100 | .catch(function(err) { 101 | res.status(500).send(err); 102 | return; 103 | }) 104 | }); 105 | 106 | // Reset the challenges table with challenges.csv 107 | app.get('/api/resetChallenges', adminPrivilege, challengeController.repopulateTable); 108 | }; 109 | -------------------------------------------------------------------------------- /app/components/ChallengeArena.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var io = require('socket.io-client'); 3 | var socket = require('../sockets/socket-helper'); 4 | var _ = require('lodash'); 5 | var ErrorList = require('./ErrorList'); 6 | var DelaySplash = require('./DelaySplash'); 7 | var Leaderboard = require('./Leaderboard'); 8 | 9 | var selfEditorOptions = { 10 | theme: "ace/theme/dawn", 11 | mode: "ace/mode/javascript", 12 | useSoftTabs: true, 13 | tabSize: 2, 14 | wrap: true, 15 | showPrintMargin: false, 16 | fontSize: 14, 17 | }; 18 | 19 | var challengerEditorOptions = { 20 | theme: "ace/theme/pastel_on_dark", 21 | useSoftTabs: true, 22 | tabSize: 2, 23 | wrap: true, 24 | showPrintMargin: false, 25 | fontSize: 14, 26 | readOnly: true, 27 | highlightActiveLine: false, 28 | highlightGutterLine: false, 29 | }; 30 | 31 | var ChallengeArena = React.createClass({ 32 | componentDidMount: function() { 33 | //setting up solo (player) editor 34 | var editor = ace.edit('editor'); 35 | editor.focus(); 36 | editor.setOptions(selfEditorOptions); 37 | editor.$blockScrolling = Infinity; 38 | 39 | // Key binding to enable Command-Enter or Ctrl-Enter to submit problem 40 | editor.commands.addCommand({ 41 | name: "replace", 42 | bindKey: {win: "Ctrl-Enter", mac: "Command-Enter"}, 43 | exec: function(editor) { 44 | this.submitProblem(); 45 | }.bind(this) 46 | }); 47 | 48 | this.props.arenaActions.storeEditor(editor); 49 | 50 | //setting up opponnent editor 51 | var editor2 = ace.edit('editor2'); 52 | editor2.setOptions(challengerEditorOptions); 53 | editor2.$blockScrolling = Infinity; 54 | this.props.arenaActions.storeEditorOpponent(editor2); 55 | }, 56 | emitSocket: function () { 57 | if(this.props.arena.editorSolo.getSession().getValue()){ 58 | this.props.arena.socket.emit('update', this.props.arena.editorSolo.getSession().getValue()) 59 | } 60 | }, 61 | submitProblem: function(){ 62 | var errors = this.props.arena.editorSolo.getSession().getAnnotations(); 63 | var content = this.props.arena.editorSolo.getSession().getValue(); 64 | this.props.arenaActions.submitProblem(errors, content, this.props.arena.socket.id, this.props.arena.problem_id, this.props.user.github_handle, 'battle'); 65 | }, 66 | render: function() { 67 | var submissionMessage; 68 | if (this.props.arena.submissionMessage === "Nothing passing so far...(From initial arena reducer)") { 69 | submissionMessage = null; 70 | } else if (this.props.arena.submissionMessage === "Solution passed all tests!") { 71 | submissionMessage =
{this.props.arena.submissionMessage}
72 | } else { 73 | submissionMessage =
{this.props.arena.submissionMessage}
74 | } 75 | 76 | return ( 77 |
78 | {this.props.arena.spinner ? : null} 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | 87 | {this.props.arena.problem_id !== null ? : null} 88 |
89 |
90 |
91 |
92 | {this.props.arena.opponentStatus !== '' ?
{this.props.arena.opponentStatus}
: null} 93 | {this.props.arena.status !== '' ?
{this.props.arena.status}
: null} 94 | {this.props.arena.stdout} 95 | {this.props.arena.syntaxMessage !== '' ? : null} 96 | {submissionMessage} 97 |
98 | {this.props.arena.opponentStatus === 'Player has joined. Challenge starting soon...' ?
: null} 99 | {this.props.arena.submitted ?
: null} 100 |
101 |
102 | ) 103 | }, 104 | 105 | componentDidUpdate: function(){ 106 | this.props.arena.editorSolo.setValue(this.props.arena.content,1); 107 | var pos = this.props.arena.editorSolo.selection.getCursor(); 108 | this.props.arena.editorSolo.moveCursorTo(pos.row - 1, 2); 109 | } 110 | }); 111 | 112 | module.exports = ChallengeArena; 113 | -------------------------------------------------------------------------------- /app/actions/navActions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var actions = require('../constants').action; 3 | 4 | var socket = require('../sockets/socket-helper'); 5 | 6 | var navStaging = function () { 7 | return { 8 | type: actions.NAV_STAGING 9 | }; 10 | }; 11 | 12 | // Update the view with challenges the user has either attempted or completed 13 | var updateChallenges = function (dispatch, github_handle, action) { 14 | $.ajax({ 15 | method: 'GET', 16 | dataType: 'json', 17 | url:'/api/solutions/user/' + github_handle, 18 | success: function (data) { 19 | dispatch({ 20 | type: actions.STORE_USER_PROBLEMS, 21 | payload: data 22 | }); 23 | dispatch({ 24 | type: action 25 | }); 26 | }, 27 | error: function (err) { 28 | dispatch({ 29 | type: action 30 | }); 31 | } 32 | }); 33 | }; 34 | 35 | // Update user match history 36 | var updateMatchHistory = function (dispatch, github_handle, action) { 37 | $.ajax({ 38 | method: 'GET', 39 | dataType: 'json', 40 | url:'/api/users/' + github_handle + '/matches', 41 | success: function (data) { 42 | dispatch({ 43 | type: actions.STORE_MATCH_HISTORY, 44 | payload: data 45 | }); 46 | dispatch({ 47 | type: action 48 | }); 49 | }, 50 | error: function (err) { 51 | dispatch({ 52 | type: action 53 | }); 54 | } 55 | }); 56 | }; 57 | 58 | var getElo = function (dispatch, github_handle, action) { 59 | $.ajax({ 60 | method: 'GET', 61 | dataType: 'json', 62 | url:'/api/users/' + github_handle + '/elo', 63 | success: function (data) { 64 | console.log('data', data) 65 | dispatch({ 66 | type: action, 67 | payload: data 68 | }); 69 | }, 70 | error: function (err) { 71 | dispatch({ 72 | type: action 73 | }); 74 | } 75 | }); 76 | }; 77 | 78 | var navSoloStaging = function (github_handle) { 79 | return function (dispatch) { 80 | updateChallenges(dispatch, github_handle, actions.NAV_SOLO_STAGING); 81 | }; 82 | }; 83 | 84 | var navSoloArena = function (payload) { 85 | return function (dispatch) { 86 | $.ajax({ 87 | method: 'GET', 88 | url: '/api/challenges/'+payload.challenge_id, 89 | dataType: 'json', 90 | success: function (results) { 91 | dispatch({ 92 | type: actions.CLEAR_INFO 93 | }); 94 | dispatch({ 95 | type: actions.STORE_SOLO_PROBLEM, 96 | payload: results 97 | }); 98 | dispatch({ 99 | type: actions.NAV_SOLO_ARENA 100 | }); 101 | }, 102 | error: function (error) { 103 | console.log('error fetching problem', error); 104 | dispatch({ 105 | type: actions.NAV_SOLO_STAGING 106 | }); 107 | } 108 | }); 109 | }; 110 | }; 111 | 112 | var spoofSolo = function () { 113 | return function (dispatch) { 114 | $.ajax({ 115 | method:'GET', 116 | url: '/api/challenges/1', 117 | dataType: 'json', 118 | success: function (data) { 119 | dispatch({ 120 | type: actions.STORE_SOLO_PROBLEM, 121 | payload: data 122 | }); 123 | dispatch({ 124 | type: actions.NAV_SOLO_ARENA 125 | }); 126 | }, 127 | error: function (err) { 128 | console.log('Change the url to the spoofed challenge_id from the db',err); 129 | } 130 | }); 131 | }; 132 | }; 133 | 134 | var navChallengeArena = function (github_handle) { 135 | return function (dispatch) { 136 | socket.emit('arena', github_handle); 137 | dispatch({ 138 | type: actions.CLEAR_INFO 139 | }); 140 | dispatch({ 141 | type: actions.NAV_CHALLENGE_ARENA 142 | }); 143 | }; 144 | }; 145 | 146 | var navAwayFromArena = function () { 147 | socket.emit('leaveArena'); 148 | return function (dispatch) { 149 | dispatch({ 150 | type: actions.NAV_STAGING 151 | }); 152 | dispatch({ 153 | type: actions.LEAVING 154 | }); 155 | }; 156 | }; 157 | 158 | //Currently Not used 159 | var navLogout = function () { 160 | //need to send request to route to LOGOUT 161 | //on success, dispatch LOGOUT statement 162 | return { 163 | type: actions.LOGOUT 164 | }; 165 | }; 166 | 167 | var navProfile = function (github_handle) { 168 | return function (dispatch) { 169 | updateChallenges(dispatch, github_handle, actions.NAV_PROFILE); 170 | updateMatchHistory(dispatch, github_handle, actions.NAV_PROFILE); 171 | getElo(dispatch, github_handle, actions.GET_ELO); 172 | }; 173 | }; 174 | 175 | 176 | module.exports = { 177 | navStaging: navStaging, 178 | navLogout: navLogout, 179 | navSoloArena: navSoloArena, 180 | navSoloStaging: navSoloStaging, 181 | navChallengeArena: navChallengeArena, 182 | navAwayFromArena: navAwayFromArena, 183 | navProfile: navProfile, 184 | spoofSolo: spoofSolo, 185 | getElo: getElo 186 | }; 187 | -------------------------------------------------------------------------------- /server/public/addProblemsSolutions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 88 | 89 | 90 |
91 |
92 | 93 |
94 |
95 |
96 |
97 |
98 |
99 | 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | 109 | 110 |
111 | 112 |
113 |
114 |
115 | 116 |
117 |
118 |
119 |
120 |
121 |
122 | 123 |
; 124 |
125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /app/tests/reducers/arenaReducerTest.js: -------------------------------------------------------------------------------- 1 | var expect = require('expect'); 2 | var arenaReducer = require('../../reducers/arenaReducers.js'); 3 | var actions = require('../../constants').action; 4 | describe('Arena Reducer', function(){ 5 | 6 | describe('#arenaReducer (immutability of state); tests the storing of state variables when dispatch actions are fired', function(){ 7 | // inital state for Arena Reducer 8 | var initialArenaReducer = { 9 | problem_id: 0, 10 | content: "", 11 | opponentStatus: "waiting for other player... when propmt appears, you may begin hacking. be ready.", 12 | status: '', 13 | opponent_content: "", 14 | submissionMessage: "Nothing passing so far...(From initial arena reducer)", 15 | socket: {}, 16 | editorSolo: {}, 17 | editorOpponent: {}, 18 | syntaxMessage: '', 19 | errors: [] 20 | } 21 | 22 | Object.freeze(initialArenaReducer); 23 | 24 | it('should return the initial state', function(){ 25 | expect( 26 | arenaReducer(undefined, {}) 27 | ).toEqual(initialArenaReducer) 28 | }) 29 | 30 | it('should store a solo problem', function(){ 31 | var state = arenaReducer(initialArenaReducer, { 32 | type: actions.STORE_SOLO_PROBLEM, 33 | payload: { 34 | id: 1, 35 | prompt:'test problem prompt' 36 | } 37 | }) 38 | expect(state.content).toEqual('test problem prompt'); 39 | expect(state.problem_id).toEqual(1); 40 | }) 41 | 42 | it('should store syntax errors', function(){ 43 | var state = arenaReducer(initialArenaReducer, { 44 | type: actions.SYNTAX_ERROR, 45 | payload: { 46 | solution_str: 'test string', 47 | errors: ['error1', 'error2'] 48 | } 49 | }) 50 | expect(state.syntaxMessage).toBeA('string'); 51 | expect(state.errors).toEqual(['error1', 'error2']); 52 | }) 53 | 54 | it('should delete syntax errors when fixed', function(){ 55 | var state = arenaReducer(initialArenaReducer, { 56 | type: actions.NO_SYNTAX_ERROR, 57 | }) 58 | expect(state.syntaxMessage).toEqual(''); 59 | expect(state.errors).toEqual([]); 60 | }) 61 | 62 | it('should store ace editor on state', function(){ 63 | var state = arenaReducer(initialArenaReducer, { 64 | type: actions.STORE_EDITOR, 65 | payload: {} 66 | }) 67 | expect(state.editorSolo.length).toNotEqual(0); 68 | }) 69 | 70 | it('should store submission message on successful solution post', function(){ 71 | var state = arenaReducer(initialArenaReducer, { 72 | type: actions.SUBMIT_PROBLEM_SUCCESS, 73 | }) 74 | expect(state.submissionMessage).toBeA('string').toEqual("solution submitted successfully with passing results..."); 75 | }) 76 | 77 | it('should store error message on failing test', function(){ 78 | var state = arenaReducer(initialArenaReducer, { 79 | type: actions.SUBMIT_PROBLEM_WRONG, 80 | payload:'example error' 81 | }) 82 | expect(state.submissionMessage).toBeA('string').toEqual('example error'); 83 | }) 84 | 85 | it('should store problem content, id and empty opponentStatus', function(){ 86 | var state = arenaReducer(initialArenaReducer, { 87 | type: actions.GET_PROBLEM_SUCCESS, 88 | payload: { 89 | prompt:'test problem prompt', 90 | id: 15 91 | } 92 | }) 93 | expect(state.content).toEqual('test problem prompt'); 94 | expect(state.opponentStatus).toEqual(''); 95 | expect(state.problem_id).toEqual(15); 96 | }) 97 | 98 | it('should store socket', function(){ 99 | var state = arenaReducer(initialArenaReducer, { 100 | type: actions.CREATE_SOCKET, 101 | payload: {} 102 | }) 103 | expect(state.socket).toBeA('object'); 104 | }) 105 | 106 | it('should clear content and player status and reset opponentStatus and submissionMessage', function(){ 107 | var state = arenaReducer(initialArenaReducer, { 108 | type: actions.CLEAR_INFO 109 | }) 110 | expect(state.content).toEqual(''); 111 | expect(state.status).toEqual(''); 112 | expect(state.opponentStatus).toEqual("waiting for other player... when propmt appears, you may begin hacking. be ready."); 113 | expect(state.submissionMessage).toEqual('Nothing passing so far...(From initial arena reducer)'); 114 | }) 115 | 116 | it('should update winners status on win', function(){ 117 | var state = arenaReducer(initialArenaReducer, { 118 | type: actions.COMPLETE_CHALLENGE 119 | }) 120 | expect(state.status).toEqual('YOU WON!'); 121 | }) 122 | 123 | it('should update losers status on loss', function(){ 124 | var state = arenaReducer(initialArenaReducer, { 125 | type: actions.LOST_CHALLENGE 126 | }) 127 | expect(state.status).toEqual('YOU LOST :('); 128 | }) 129 | 130 | it('should update opponentStatus when opponent leaves room', function(){ 131 | var state = arenaReducer(initialArenaReducer, { 132 | type: actions.PLAYER_LEAVE 133 | }) 134 | expect(state.opponentStatus).toEqual('The other player has left the room.'); 135 | }) 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /server/helpers/dbConfig.js: -------------------------------------------------------------------------------- 1 | var knex; 2 | if (process.env.DEPLOYED) { 3 | knex = require('knex')({ 4 | client: 'pg', 5 | connection: { 6 | host: 'postgres', 7 | user: 'postgres', 8 | password: 'mysecretpassword', 9 | database : 'postgres', 10 | charset : 'utf8' 11 | } 12 | }); 13 | } else { 14 | knex = require('knex')({ 15 | client: 'pg', 16 | connection: { 17 | host: 'localhost', 18 | database: 'myDB', 19 | charset: 'utf8' 20 | } 21 | }); 22 | } 23 | 24 | var db = require('bookshelf')(knex); 25 | 26 | // Shortcut function to create the users table 27 | var createUsersTable = function () { 28 | return db.knex.schema.createTable('users', function (user) { 29 | user.increments('id').primary(); 30 | user.string('github_handle', 255); 31 | user.string('github_display_name', 255); 32 | user.string('github_avatar_url', 255); 33 | user.string('github_profile_url', 255); 34 | user.integer('elo_rating'); 35 | user.string('email', 255); 36 | user.timestamps(); 37 | }).then(function (table) { 38 | console.log('Created user Table', table); 39 | }); 40 | }; 41 | 42 | // Shortcut function to create the solutions table 43 | var createSolutionsTable = function () { 44 | return db.knex.schema.createTable('solutions', function (solution) { 45 | solution.increments('id').primary(); 46 | solution.dateTime('start_time'); 47 | solution.dateTime('end_time'); 48 | solution.integer('total_time'); 49 | solution.string('content', 80000); // user input solution string, might be large! 50 | solution.integer('user_id'); 51 | solution.string('github_handle'); 52 | solution.integer('challenge_id'); 53 | solution.boolean('valid'); 54 | }).then(function (table) { 55 | console.log('Created solutions Table', table); 56 | }); 57 | }; 58 | 59 | // Shortcut function to create the challenges table 60 | var createChallengesTable = function () { 61 | return db.knex.schema.createTable('challenges', function (challenge) { 62 | challenge.increments('id').primary(); 63 | challenge.string('name', 255); 64 | challenge.string('prompt', 1000); 65 | challenge.string('test_suite', 8000); // test code that we will write per challenge 66 | challenge.string('type', 50); 67 | challenge.timestamps(); 68 | }).then(function (table) { 69 | console.log('Created challenges Table', table); 70 | }); 71 | }; 72 | 73 | var createMatchesTable = function () { 74 | return db.knex.schema.createTable('matches', function (match) { 75 | match.increments('id').primary(); 76 | match.integer('user_id'); 77 | match.string('user_github_handle', 50); 78 | match.string('opponent_github_handle', 50); 79 | match.string('opponent_avatar', 150); 80 | match.boolean('win'); 81 | match.integer('challenge_id'); 82 | match.timestamps(); 83 | }).then(function (table) { 84 | console.log('Created matches Table', table); 85 | }); 86 | }; 87 | 88 | // Shortcut function to reset users 89 | var resetUsersTable = function () { 90 | return db.knex.schema.dropTable('users').then(createUsersTable); 91 | }; 92 | 93 | // Shortcut function to reset solutions 94 | var resetSolutionsTable = function () { 95 | return db.knex.schema.dropTable('solutions').then(createSolutionsTable); 96 | }; 97 | 98 | // Shortcut function to reset challenges 99 | var resetChallengesTable = function () { 100 | return db.knex.schema.dropTable('challenges').then(createChallengesTable); 101 | }; 102 | 103 | // Shortcut function to reset matches 104 | var resetMatchesTable = function () { 105 | return db.knex.schema.dropTable('matches').then(createMatchesTable); 106 | }; 107 | 108 | // Exposed function that resets the entire database 109 | db.resetEverything = function (req, res) { 110 | resetUsersTable().then(function() { 111 | resetSolutionsTable(); 112 | }).then(function() { 113 | resetChallengesTable(); 114 | }).then(function() { 115 | resetMatchesTable(); 116 | }).then(function() { 117 | res.status(201).end(); 118 | }); 119 | }; 120 | 121 | db.resetEverythingPromise = function () { 122 | return resetUsersTable().then(function() { 123 | return resetChallengesTable(); 124 | }).then(function() { 125 | return resetSolutionsTable(); 126 | }).then(function() { 127 | return resetMatchesTable(); 128 | }).catch(function(e) { 129 | console.log(e); 130 | }); 131 | }; 132 | 133 | // Create users table with id, github_handle 134 | db.knex.schema.hasTable('users').then(function (exists) { 135 | if (!exists) { 136 | createUsersTable(); 137 | } 138 | }); 139 | 140 | // Create solutions table with id, start_time, end_time, total_time, content, user_id, and challenge_id 141 | db.knex.schema.hasTable('solutions').then(function (exists) { 142 | if (!exists) { 143 | createSolutionsTable(); 144 | } 145 | }); 146 | 147 | // Create challenges table with id, name, prompt, and test_suite 148 | db.knex.schema.hasTable('challenges').then(function(exists) { 149 | if (!exists) { 150 | createChallengesTable(); 151 | } 152 | }); 153 | 154 | // Create challenges table with id, user_id, opponent_id, win 155 | db.knex.schema.hasTable('matches').then(function(exists) { 156 | if (!exists) { 157 | createMatchesTable(); 158 | } 159 | }); 160 | 161 | module.exports = db; 162 | -------------------------------------------------------------------------------- /app/reducers/arenaReducers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var actions = require('../constants').action; 6 | 7 | 8 | var initial = { 9 | problem_id: null, 10 | prompt: '', 11 | problem_name: '', 12 | content: "", 13 | status: '', 14 | spinner: false, 15 | submitted: false, 16 | delay: 5, 17 | opponent_info: {}, 18 | submissionMessage: "Nothing passing so far...(From initial arena reducer)", 19 | socket: {}, 20 | editorSolo: {}, 21 | opponentStatus: "Waiting for another player...", 22 | editorOpponent: {}, 23 | syntaxMessage: '', 24 | leaderBoard: [], 25 | errors: [], 26 | stdout: '' 27 | }; 28 | 29 | function arenaReducer (state, action){ 30 | state = state || initial; 31 | switch(action.type){ 32 | case actions.CREATE_SOCKET: 33 | return _.extend({},state,{ 34 | socket: action.payload 35 | }); 36 | case actions.GET_PROBLEM_SUCCESS: 37 | return _.extend({}, state, { 38 | problem_name: action.payload.name, 39 | content: action.payload.prompt, 40 | prompt: action.payload.prompt, 41 | opponentStatus: '', 42 | problem_id: action.payload.id 43 | }); 44 | case actions.DELAY_START: 45 | return _.extend({}, state, { 46 | opponentStatus: 'Player has joined. Challenge starting soon...', 47 | delay: 5 48 | }); 49 | case actions.COUNTDOWN: 50 | var newDelay = state.delay-1; 51 | return _.extend({}, state, { 52 | delay: newDelay 53 | }); 54 | case actions.SUBMIT_PROBLEM_WRONG: 55 | return _.extend({}, state, { 56 | submissionMessage: action.payload.message, 57 | stdout: action.payload.stdout, 58 | spinner: false 59 | }); 60 | case actions.GET_LEADERBOARD_SUCCESS: 61 | return _.extend({}, state, { 62 | leaderBoard: action.payload 63 | }); 64 | case actions.SUBMIT_PROBLEM_SUCCESS: 65 | return _.extend({}, state, { 66 | submissionMessage: "Solution passed all tests!", 67 | stdout: action.payload.stdout, 68 | spinner: false, 69 | submitted: true 70 | }); 71 | case actions.STORE_EDITOR: 72 | return _.extend({}, state, { 73 | editorSolo: action.payload, 74 | }); 75 | case actions.EXIT_SPLASH: 76 | return _.extend({}, state, { 77 | submitted: false, 78 | leaderBoard: [] 79 | }); 80 | case actions.STORE_EDITOR_OPPONENT: 81 | return _.extend({}, state, { 82 | editorOpponent: action.payload, 83 | }); 84 | case actions.SYNTAX_ERROR: 85 | return _.extend({}, state, { 86 | syntaxMessage: 'There are syntax errors in your code. Please fix them and re-submit.', 87 | content: action.payload.solution_str, 88 | errors: action.payload.errors, 89 | submissionMessage: "Nothing passing so far...(From initial arena reducer)", 90 | stdout: '' 91 | }); 92 | case actions.NO_SYNTAX_ERROR: 93 | return _.extend({}, state, { 94 | content: action.payload, 95 | syntaxMessage: '', 96 | errors: [], 97 | spinner: true, 98 | submissionMessage: "Nothing passing so far...(From initial arena reducer)" 99 | }); 100 | case actions.STORE_SOLO_PROBLEM: 101 | return _.extend({}, state, { 102 | content: action.payload.prompt, 103 | prompt: action.payload.prompt, 104 | problem_id: action.payload.id 105 | }); 106 | case actions.CLEAR_INFO: 107 | return _.extend({}, state, { 108 | content: '', 109 | prompt: '', 110 | syntaxMessage: '', 111 | problem_name: '', 112 | problem_id: null, 113 | leaderBoard: [], 114 | status: '', 115 | submitted: false, 116 | delay: 5, 117 | opponentStatus: "Waiting for another player...", 118 | submissionMessage: 'Nothing passing so far...(From initial arena reducer)', 119 | stdout: '', 120 | opponent_info: {} 121 | }); 122 | case actions.COMPLETE_CHALLENGE: 123 | if(state.status !== 'YOU LOST :('){ 124 | return _.extend({}, state, { 125 | status: 'YOU WON!' 126 | }); 127 | } else { 128 | return state; 129 | } 130 | case actions.LOST_CHALLENGE: 131 | if(state.status !== 'YOU WON!') { 132 | return _.extend({}, state, { 133 | status: 'YOU LOST :(', 134 | content: action.payload 135 | }); 136 | } else { 137 | return state; 138 | } 139 | case actions.PLAYER_LEAVE: 140 | return _.extend({}, state, { 141 | opponentStatus: 'The other player has left the room.', 142 | opponent_info: {}, 143 | content: action.payload 144 | }); 145 | case actions.LEAVING: 146 | return _.extend({}, state, { 147 | opponent_info: {} 148 | }); 149 | case actions.GOT_OPPONENT_HANDLE: 150 | return _.extend({}, state, { 151 | opponent_info: action.payload 152 | }); 153 | case actions.RESET_PROMPT: 154 | console.log(state.prompt); 155 | return _.extend({}, state, { 156 | content: state.prompt, 157 | syntaxMessage: '', 158 | leaderBoard: [], 159 | submissionMessage: 'Nothing passing so far...(From initial arena reducer)', 160 | stdout: '', 161 | }); 162 | default: 163 | return state; 164 | } 165 | 166 | }; 167 | 168 | module.exports = arenaReducer; 169 | -------------------------------------------------------------------------------- /test/solutionControllerSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var request = require('request'); 3 | var io = require('socket.io-client'); 4 | 5 | var solutionController = require('../server/solutions/solutionController.js'); 6 | var Solution = require('../server/solutions/solutionModel.js'); 7 | 8 | describe('solutionControllerTest', function () { 9 | before(function (done) { 10 | console.log('Resetting DBs') 11 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 12 | done(); 13 | }); 14 | }); 15 | 16 | describe('getSolutionById', function () { 17 | it('should be a function', function () { 18 | assert.isFunction(solutionController.getSolutionById); 19 | }); 20 | it('should get a solution by solutionID', function (done) { 21 | request({ 22 | url: 'http://127.0.0.1:3000/api/solutions/1', 23 | method: 'GET', 24 | json: {} 25 | }, function (err, response, body) { 26 | assert.equal(body.content, 'solved!'); 27 | assert.equal(body.user_id, 3); 28 | assert.equal(body.challenge_id, 1); 29 | assert.equal(response.statusCode, 200); 30 | done(); 31 | }); 32 | }); 33 | it('should return 404/null if solution does not exist', function (done) { 34 | request({ 35 | url: 'http://127.0.0.1:3000/api/solutions/100', 36 | method: 'GET', 37 | json: {} 38 | }, function (err, response, body) { 39 | assert.equal(response.statusCode, 404); 40 | assert.equal(body, null) 41 | done(); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('getAllSolutionsForUser', function () { 47 | it('should be a function', function () { 48 | assert.isFunction(solutionController.getAllSolutionsForUser); 49 | }); 50 | it('should get all solutions for a user', function (done) { 51 | request({ 52 | url: 'http://127.0.0.1:3000/api/solutions/user/kweng2', 53 | method: 'GET', 54 | json: {} 55 | }, function (err, response, body) { 56 | assert.equal(body.length, 3); 57 | assert.equal(body[1].challenge_id, 2); 58 | assert.equal(response.statusCode, 200); 59 | done(); 60 | }); 61 | }); 62 | it('should return 404/null if user does not exist', function (done) { 63 | request({ 64 | url: 'http://127.0.0.1:3000/api/solutions/user/kweng3', 65 | method: 'GET', 66 | json: {} 67 | }, function (err, response, body) { 68 | assert.equal(response.statusCode, 404); 69 | assert.equal(body, null); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | describe('testSolution', function () { 75 | it('should be a function', function () { 76 | assert.isFunction(solutionController.testSolution); 77 | }); 78 | it('should return success for a valid solution if user has already completed challenge', function (done) { 79 | var socket = io('http://127.0.0.1:3000'); 80 | socket.on('connect', function () { 81 | request({ 82 | url: 'http://127.0.0.1:3000/api/solutions/2', 83 | method: 'POST', 84 | json: { 85 | socket_id: socket.id, 86 | soln_str: "function two() {return 2;}", 87 | user_handle: 'kweng2', 88 | type: 'battle' 89 | } 90 | }, function (err, response, body) { 91 | assert.equal(response.statusCode, 201); 92 | }); 93 | 94 | socket.on('eval', function(message) { 95 | assert.equal(message, 'victory!'); 96 | done(); 97 | }); 98 | }); 99 | }); 100 | it('should not persist valid solutions in database if user has already completed the challenge', function (done) { 101 | request({ 102 | url: 'http://127.0.0.1:3000/api/solutions/11', 103 | method: 'GET', 104 | json: {} 105 | }, function (err, response, body) { 106 | assert.equal(response.statusCode, 404); 107 | done(); 108 | }); 109 | }); 110 | it('should return success for a valid solution if user has not completed challenge', function (done) { 111 | var socket = io('http://127.0.0.1:3000'); 112 | socket.on('connect', function () { 113 | solutionController.initializeChallengeSolutions('kweng2','alanzfu', 4, function () { 114 | request({ 115 | url: 'http://127.0.0.1:3000/api/solutions/4', 116 | method: 'POST', 117 | json: { 118 | socket_id: socket.id, 119 | soln_str: "function four() {return 4;}", 120 | user_handle: 'kweng2', 121 | type: 'battle' 122 | } 123 | }, function (err, response, body) { 124 | assert.equal(response.statusCode, 201); 125 | }); 126 | 127 | }) 128 | 129 | socket.on('eval', function(message) { 130 | assert.equal(message, 'victory!'); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | it('should return failure for an invalid solution', function (done) { 136 | var socket = io('http://127.0.0.1:3000'); 137 | socket.on('connect', function () { 138 | request({ 139 | url: 'http://127.0.0.1:3000/api/solutions/2', 140 | method: 'POST', 141 | json: { 142 | socket_id: socket.id, 143 | soln_str: "test solution", 144 | user_handle: 'kweng2' 145 | } 146 | }, function (err, response, body) { 147 | assert.equal(response.statusCode, 201); 148 | }); 149 | 150 | socket.on('eval', function(message) { 151 | assert.equal(message, 'Unexpected identifier'); 152 | done(); 153 | }) 154 | }); 155 | }); 156 | it('should not persist invalid solutons in database', function (done) { 157 | request({ 158 | url: 'http://127.0.0.1:3000/api/solutions/11', 159 | method: 'GET', 160 | json: {} 161 | }, function (err, response, body) { 162 | assert.equal(response.statusCode, 404); 163 | done(); 164 | }); 165 | }); 166 | }); 167 | 168 | after(function (done) { 169 | request('http://127.0.0.1:3000/api/resetDBWithData', function () { 170 | done(); 171 | }); 172 | }); 173 | 174 | }); 175 | -------------------------------------------------------------------------------- /server/solutions/solutionController.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var db = require('../helpers/dbConfig'); 3 | var Solution = require('./solutionModel.js'); 4 | var User = require('../users/userModel.js'); 5 | var Challenge = require('../challenges/challengeModel.js'); 6 | var Queue = require('../responseRedis/redisQueue.js'); 7 | 8 | module.exports = { 9 | // GET /api/solutions/:solutionId 10 | getSolutionById: function (req, res) { 11 | var solutionId = req.params.solutionId; 12 | Solution.forge({ 13 | id: solutionId 14 | }) 15 | .fetch() 16 | .then(function (solution) { 17 | if (solution) { 18 | res.json(solution); 19 | } else { 20 | res.status(404).json(null); 21 | } 22 | }).catch(function (err) { 23 | res.status(500).json({error: true, data: {message: err.message}}); 24 | }); 25 | }, 26 | 27 | // GET /api/solutions/user/:githubHandle 28 | getAllSolutionsForUser: function (req, res) { 29 | var github_handle = req.params.githubHandle; 30 | User.forge({ 31 | github_handle: github_handle 32 | }) 33 | .fetch() 34 | .then(function (user) { 35 | if (user) { 36 | return Solution.where({ 37 | user_id: user.get('id') 38 | }).fetchAll(); 39 | } 40 | else { 41 | res.status(404).json(null); 42 | } 43 | }) 44 | .then(function (solutions) { 45 | // Decorate the solution objects with "challenge_name" field 46 | var decorateSolution = function (solutions, count) { 47 | if (count <= 0) { 48 | res.status(201).json(solutions); 49 | return solutions; 50 | } else { 51 | Challenge.forge({ 52 | id: solutions.models[count-1].get('challenge_id') 53 | }).fetch().then(function (challenge) { 54 | solutions.models[count-1].set('challenge_name', challenge.get('name')); 55 | }).then(function () { 56 | return decorateSolution(solutions, count-1); 57 | }).catch(function (err) { 58 | console.log("Errored while recursively decorating solution models with challenge_name,", err); 59 | }); 60 | } 61 | }; 62 | var count = solutions.models.length; 63 | return decorateSolution(solutions, count); 64 | }) 65 | .catch(function (err) { 66 | console.log("Got an error while retrieving all solutions for a user", err); 67 | res.status(500).json({error: true, data: {message: err.message}}); 68 | }); 69 | 70 | }, 71 | 72 | 73 | // POST /api/solutions/:challengeId 74 | testSolution: function (req, res, redisClient) { 75 | var solutionAttr = { 76 | soln_str: req.body.soln_str, 77 | user_handle: req.body.user_handle, 78 | socket_id: req.body.socket_id, 79 | challenge_id: req.params.challengeId, 80 | type: req.body.type 81 | }; 82 | //prevents submission in event of 83 | if(solutionAttr.challenge_id !== 'null') { 84 | var jobQueue = new Queue('testQueue', redisClient); 85 | jobQueue.push(JSON.stringify(solutionAttr)); 86 | } 87 | res.status(201).end(); 88 | }, 89 | 90 | findTimeElapsed: function(dateStart, dateEnd){ 91 | var time1 = Date.parse(dateStart); 92 | var time2 = Date.parse(dateEnd); 93 | return time2 - time1; 94 | }, 95 | 96 | // POST /api/solutions/:solutionId 97 | addSolution: function (solutionAttr) { 98 | console.log("LOOKING FOR USER WITH GITHUB HANDLE: ", solutionAttr.github_handle); 99 | new User({github_handle: solutionAttr.github_handle}) 100 | .fetch().then(function(user) { 101 | return user.get('id'); 102 | }).then(function(userId){ 103 | delete solutionAttr.github_handle; 104 | solutionAttr.user_id = userId; 105 | 106 | return Solution.forge({ 107 | challenge_id: solutionAttr.challenge_id, 108 | user_id: solutionAttr.user_id 109 | }).fetch(); 110 | }).then(function (solution) { 111 | 112 | if(solutionAttr.type === 'battle' && solution.get('valid') === false) { 113 | solutionAttr['valid'] = true; 114 | solutionAttr['end_time'] = new Date(Date.now()); 115 | solutionAttr['total_time'] = module.exports.findTimeElapsed(solution.attributes.start_time,solutionAttr.end_time); 116 | delete solutionAttr.type; 117 | solution.set(solutionAttr).save(); 118 | } 119 | return; 120 | }) 121 | .catch(function (err) { 122 | console.log('addSolution error: ', err); 123 | }); 124 | }, 125 | 126 | //GET api/solutions/:challenge_id/top 127 | getTopSolutions: function(req, res) { 128 | 129 | db.knex('solutions').where('challenge_id', req.params.challenge_id).whereNot('total_time',null).orderBy('total_time') 130 | .then(function (orderedSolutions) { 131 | res.json(orderedSolutions); 132 | }); 133 | }, 134 | 135 | 136 | //Internally invoked when two players enter a room and a challenge ID is assigned 137 | initializeChallengeSolutions: function(player1_github_handle, player2_github_handle, challenge_id, type, callback){ 138 | var playerIds = {}; 139 | 140 | User.forge({github_handle: player1_github_handle}).fetch() 141 | .then(function(player1) { 142 | playerIds.player1_id = player1.get('id'); 143 | return player1.get('id'); 144 | }) 145 | .then(function(player1_id){ 146 | return User.forge({github_handle: player2_github_handle}).fetch(); 147 | }) 148 | .then(function(player2) { 149 | playerIds.player2_id = player2.get('id'); 150 | return player2.get('id'); 151 | }) 152 | .then(function () { 153 | return Solution.forge({ 154 | start_time: new Date(Date.now()), 155 | end_time: new Date(Date.now()), 156 | total_time: null, 157 | content: 'Initial Value', 158 | user_id: playerIds.player1_id, 159 | github_handle: player1_github_handle, 160 | challenge_id: challenge_id, 161 | valid: false 162 | }).save(); 163 | }) 164 | .then(function () { 165 | return Solution.forge({ 166 | start_time: new Date(Date.now()), 167 | end_time: new Date(Date.now()), 168 | total_time: null, 169 | content: 'Initial Value', 170 | user_id: playerIds.player2_id, 171 | github_handle: player2_github_handle, 172 | challenge_id: challenge_id, 173 | valid: false 174 | }).save(); 175 | }).then(function (solution) { 176 | if (callback) { 177 | callback(solution); 178 | } 179 | }) 180 | .catch(function(error) { 181 | console.log('error initializing solutions', error) 182 | }); 183 | }, 184 | 185 | resetWithData: function () { 186 | return Solution.forge({ 187 | start_time: new Date(Date.now() - 152*60*60*1000), 188 | end_time: new Date(Date.now() - 149*60*60*1000), 189 | total_time: 3000, 190 | content: 'solved!', 191 | user_id: 1, 192 | github_handle: 'alanzfu', 193 | challenge_id: 2, 194 | valid: true 195 | }).save() 196 | .then(function () { 197 | return Solution.forge({ 198 | start_time: new Date(Date.now() - 150*60*60*1000), 199 | end_time: new Date(Date.now() - 142*60*60*1000), 200 | total_time: 4000, 201 | content: 'solved!', 202 | user_id: 4, 203 | github_handle: 'hahnbi', 204 | challenge_id: 2, 205 | valid: true 206 | }).save(); 207 | }).then(function() { 208 | return Solution.forge({ 209 | start_time: new Date(Date.now() - 88*60*60*1000), 210 | end_time: new Date(Date.now() - 73*60*60*1000), 211 | total_time: 4500, 212 | content: 'solved!', 213 | user_id: 4, 214 | github_handle: 'hahnbi', 215 | challenge_id: 3, 216 | valid: true 217 | }).save(); 218 | }).then(function() { 219 | return Solution.forge({ 220 | start_time: new Date(Date.now() - 82*60*60*1000), 221 | end_time: new Date(Date.now() - 80*60*60*1000), 222 | total_time: 5000, 223 | content: 'solved!', 224 | user_id: 3, 225 | github_handle: 'kweng2', 226 | challenge_id: 3, 227 | valid: true 228 | }).save(); 229 | }).then(function() { 230 | return Solution.forge({ 231 | start_time: new Date(Date.now() - 49*60*60*1000), 232 | end_time: new Date(Date.now() - 38*60*60*1000), 233 | total_time: 5500, 234 | content: 'solved!', 235 | user_id: 2, 236 | github_handle: 'puzzlehe4d', 237 | challenge_id: 1, 238 | valid: true 239 | }).save(); 240 | }).then(function() { 241 | return Solution.forge({ 242 | start_time: new Date(Date.now() - 49*60*60*1000), 243 | end_time: new Date(Date.now() - 38*60*60*1000), 244 | total_time: 6000, 245 | content: 'solved!', 246 | user_id: 1, 247 | github_handle: 'alanzfu', 248 | challenge_id: 1, 249 | valid: true 250 | }).save(); 251 | }) 252 | } 253 | }; 254 | -------------------------------------------------------------------------------- /server/matches/matchController.js: -------------------------------------------------------------------------------- 1 | var User = require('../users/userModel.js'); 2 | var Match = require('../matches/matchModel.js'); 3 | var Challenge = require('../challenges/challengeModel.js'); 4 | var elo = require('elo-rank')(32); 5 | var Promise = require('bluebird'); 6 | module.exports = { 7 | 8 | //Internally Invoked when a room is filled 9 | addForBoth: function(user_github_handle, opponent_github_handle, challenge_id, callback){ 10 | User.forge({ 11 | github_handle: user_github_handle 12 | }) 13 | .fetch() 14 | .then(function (player1) { 15 | return User.forge({ 16 | github_handle: opponent_github_handle 17 | }).fetch().then(function (opponent) { 18 | return Match.forge({ 19 | user_id: player1.get('id'), 20 | user_github_handle: user_github_handle, 21 | opponent_github_handle: opponent_github_handle, 22 | opponent_avatar: opponent.get('github_avatar_url'), 23 | challenge_id: challenge_id, 24 | win: false 25 | }).save(); 26 | }); 27 | }) 28 | .then(function(){ 29 | return User.forge({ 30 | github_handle: opponent_github_handle 31 | }).fetch(); 32 | }) 33 | .then(function(player2){ 34 | return User.forge({ 35 | github_handle: user_github_handle 36 | }).fetch().then(function (opponent) { 37 | return Match.forge({ 38 | user_id: player2.get('id'), 39 | user_github_handle: opponent_github_handle, 40 | opponent_github_handle: user_github_handle, 41 | opponent_avatar: opponent.get('github_avatar_url'), 42 | challenge_id: challenge_id, 43 | win: false 44 | }).save(); 45 | }); 46 | }) 47 | .then(function(match){ 48 | if(callback) { 49 | callback(match); 50 | } 51 | }) 52 | .catch(function(err){ 53 | console.log(err, 'error initiating match entries'); 54 | return err; 55 | }); 56 | }, 57 | 58 | /* 59 | var toSocket = reply.socket_id; 60 | var challenge_id = reply.challenge_id; 61 | var github_handle = reply.github_handle; 62 | var soln_str = reply.soln_str; 63 | var message = reply.message; 64 | */ 65 | //Internally invoked when a valid solution arrives from redisQueue 66 | editOneWhenValid: function(checkedSolutions, callback){ 67 | var challenge_id = checkedSolutions.challenge_id; 68 | var github_handle = checkedSolutions.github_handle; 69 | //define vairable for winner & loser 70 | var winner = ''; 71 | var loser = ''; 72 | return User.forge({ 73 | github_handle: github_handle 74 | }).fetch() 75 | .then(function(user){ 76 | return Match.forge({ 77 | user_github_handle: user.get('github_handle'), 78 | challenge_id: challenge_id 79 | }).fetch(); 80 | }) 81 | .then(function(userMatchEntry){ 82 | return Match.forge({ 83 | user_github_handle: userMatchEntry.get('opponent_github_handle'), 84 | challenge_id: checkedSolutions.challenge_id 85 | }).fetch(); 86 | }) 87 | .then(function(opponentMatchEntry) { 88 | if (opponentMatchEntry) { 89 | if (opponentMatchEntry.get('win') === false) { 90 | //set winner 91 | winner = opponentMatchEntry.get('opponent_github_handle'); 92 | Match.forge({ 93 | user_github_handle: opponentMatchEntry.get('opponent_github_handle'), 94 | challenge_id: opponentMatchEntry.get('challenge_id') 95 | }).fetch() 96 | .then(function (userMatchEntry) { 97 | //set loser 98 | loser = userMatchEntry.get('opponent_github_handle'); 99 | return userMatchEntry.set('win', true).save(); 100 | }) 101 | .then(function () { 102 | if(callback) { 103 | //callback assignEloRating(winner, loser) 104 | callback(winner, loser); 105 | } 106 | }); 107 | } 108 | } 109 | }) 110 | .catch(function(err) { 111 | console.log('what',err); 112 | return err; 113 | }); 114 | }, 115 | assignEloRating: function(winner, loser){ 116 | //make promise for winnerUser 117 | var winnerUser = function(winner) { 118 | return User.forge({ 119 | github_handle: winner 120 | }).fetch(); 121 | } 122 | 123 | //make Promise for loserUser 124 | var loserUser = function(loser) { 125 | return User.forge({ 126 | github_handle: loser 127 | }).fetch(); 128 | } 129 | //join promisese with .all 130 | Promise.all([winnerUser(winner), loserUser(loser)]).then(function(players){ 131 | var winner = players[0]; 132 | var loser = players[1]; 133 | // elo rating calculations 134 | var expectedWinnerScore = elo.getExpected(winner.get('elo_rating'), loser.get('elo_rating')); 135 | var expectedLoserScore = elo.getExpected(loser.get('elo_rating'), winner.get('elo_rating')); 136 | winner.set('elo_rating', elo.updateRating(expectedWinnerScore, 1 , winner.get('elo_rating'))).save(); 137 | loser.set('elo_rating', elo.updateRating(expectedLoserScore, 0, loser.get('elo_rating'))).save(); 138 | }).catch(function(err){ 139 | console.log(err, 'error getting players'); 140 | }) 141 | }, 142 | //Gets match history by user 143 | getAllByUser: function(req, res, callback){ 144 | User.forge({github_handle: req.params.githubHandle}) 145 | .fetch({withRelated: ['matches']}) 146 | .then(function (user) { 147 | var matches = user.related('matches'); 148 | 149 | // Decorate matches with challenge name 150 | var decorateMatches = function (matches, count) { 151 | if (count <= 0) { 152 | res.status(201).json(matches); 153 | return matches; 154 | } else { 155 | Challenge.forge({ 156 | id: matches.models[count-1].get('challenge_id') 157 | }).fetch().then(function (challenge) { 158 | matches.models[count-1].set('challenge_name', challenge.get('name')); 159 | }).then(function () { 160 | return decorateMatches(matches, count-1); 161 | }).catch(function (err) { 162 | console.log("Errored while recursively decorating matches models with challenge_name,", err); 163 | }); 164 | } 165 | }; 166 | var count = matches.models.length; 167 | return decorateMatches(matches, count); 168 | }) 169 | // .then(function (matches) { 170 | // console.log('got here', matches); 171 | // if(callback) { 172 | // console.log('not getting here'); 173 | // callback(matches); 174 | // } 175 | // }) 176 | .catch(function (err) { 177 | console.log("Errored retrieving matches for user", err); 178 | res.status(404).end(); 179 | }); 180 | }, 181 | 182 | resetWithData: function() { 183 | return Match.forge({ 184 | user_id: 1, 185 | user_github_handle: 'alanzfu', 186 | opponent_github_handle: 'hahnbi', 187 | opponent_avatar: "https://avatars1.githubusercontent.com/u/12260923?v=3&s=400", 188 | win: true, 189 | challenge_id: 2 190 | }).save().then(function() { 191 | return Match.forge({ 192 | user_id: 4, 193 | user_github_handle: 'hahnbi', 194 | opponent_github_handle: 'alanzfu', 195 | opponent_avatar: "https://avatars2.githubusercontent.com/u/7851211?v=3&s=400", 196 | win: false, 197 | challenge_id: 2 198 | }).save(); 199 | }).then(function() { 200 | return Match.forge({ 201 | user_id: 4, 202 | user_github_handle: 'hahnbi', 203 | opponent_github_handle: 'kweng2', 204 | opponent_avatar: "https://avatars2.githubusercontent.com/u/13741053?v=3&s=460", 205 | win: true, 206 | challenge_id: 3 207 | }).save(); 208 | }).then(function() { 209 | return Match.forge({ 210 | user_id: 3, 211 | user_github_handle: 'kweng2', 212 | opponent_github_handle: 'hahnbi', 213 | opponent_avatar: "https://avatars1.githubusercontent.com/u/12260923?v=3&s=400", 214 | win: false, 215 | challenge_id: 3 216 | }).save(); 217 | }) 218 | .then(function(){ 219 | return Match.forge({ 220 | user_id: 2, 221 | user_github_handle: 'puzzlehe4d', 222 | opponent_github_handle: 'alanzfu', 223 | opponent_avatar: "https://avatars2.githubusercontent.com/u/7851211?v=3&s=400", 224 | win: true, 225 | challenge_id: 1 226 | }).save(); 227 | }) 228 | .then(function() { 229 | return Match.forge({ 230 | user_id: 1, 231 | user_github_handle: 'alanzfu', 232 | opponent_github_handle: 'puzzlehe4d', 233 | opponent_avatar: "https://avatars0.githubusercontent.com/u/12518929?v=3&s=400", 234 | win: false, 235 | challenge_id: 1 236 | }).save(); 237 | }) 238 | .catch(function(err){ 239 | console.log(err); 240 | }); 241 | } 242 | }; 243 | -------------------------------------------------------------------------------- /server/challenges/challengeController.js: -------------------------------------------------------------------------------- 1 | var db = require('../helpers/dbConfig'); 2 | var Challenge = require('./challengeModel.js'); 3 | var User = require('../users/userModel.js'); 4 | var Solution = require('../solutions/solutionModel.js'); 5 | var _ = require('lodash'); 6 | 7 | module.exports = { 8 | // GET /api/challenges 9 | getChallenge: function (req, res) { 10 | Challenge.fetchAll() 11 | .then(function (challenges) { 12 | var i = Math.floor(Math.random() * challenges.size()); 13 | res.status(200).json(challenges.at(i)); 14 | }).catch(function (err) { 15 | res.status(500).json({error: true, data: {message: err.message}}); 16 | }); 17 | }, 18 | 19 | // NOT an HTTP route, will be called by sockets 20 | getChallengeMultiplayer: function (req, callback) { 21 | //req has to have type 22 | var p1 = req.body.player1_github_handle; 23 | var p2 = req.body.player2_github_handle; 24 | var type = req.body.type; 25 | var completedChallenges = { 26 | p1: [], 27 | p2: [] 28 | }; 29 | var completed = []; 30 | var total = []; 31 | 32 | // Get list of challenges completed by player1 33 | User.forge({ 34 | github_handle: p1 35 | }).fetch({withRelated: ['solutions']}) 36 | .then(function (user) { 37 | var solutions = user.related('solutions'); 38 | return solutions.fetch({withRelated: ['challenge']}); 39 | }) 40 | .then(function (solutions) { 41 | var challenges = solutions.filter(function (s) { 42 | return s.related('challenge').get('type') === type; 43 | }); 44 | completedChallenges.p1 = challenges.map(function (s) { 45 | return s.get('challenge_id'); 46 | }); 47 | // Get list of challenges completed by player2 48 | return User.forge({ 49 | github_handle: p2 50 | }).fetch({withRelated: ['solutions']}); 51 | }) 52 | .then(function (user) { 53 | var solutions = user.related('solutions'); 54 | return solutions.fetch({withRelated: ['challenge']}); 55 | }) 56 | .then(function (solutions) { 57 | var challenges = solutions.filter(function (s) { 58 | return s.related('challenge').get('type') === type; 59 | }); 60 | completedChallenges.p2 = challenges.map(function (s) { 61 | return s.get('challenge_id'); 62 | }); 63 | return Challenge.forge({type: type}).fetchAll(); 64 | }) 65 | .then(function (challenges) { 66 | // return a challenge neither player has seen 67 | completed = _.union(completedChallenges.p1, completedChallenges.p2); 68 | total = challenges.filter(function (c) { 69 | return c.get('type') === type; 70 | }).map(function (c) { 71 | return c.get('id'); 72 | }); 73 | var available = _.difference(total, completed); 74 | // if no challenges are available 75 | if (available.length === 0) { 76 | return null; 77 | } 78 | // otherwise continue to fetch challenge 79 | else { 80 | var challenge = _.sample(available); 81 | return challenge; 82 | } 83 | }) 84 | .then(function (id) { 85 | if (id === null) { 86 | return null; 87 | } 88 | return Challenge.forge({id: id, type:type}).fetch(); 89 | }) 90 | .then(function (challenge) { 91 | if (challenge !== null) { 92 | var attrs = { 93 | id: challenge.get('id'), 94 | name: challenge.get('name'), 95 | prompt: challenge.get('prompt') 96 | }; 97 | if (callback) { 98 | callback(attrs); 99 | } 100 | } else { 101 | if (callback) { 102 | callback(null); 103 | } 104 | } 105 | }); 106 | }, 107 | 108 | // GET /api/challenges/:challengeId 109 | getChallengeById: function (req, res) { 110 | var challengeId = req.params.challengeId; 111 | Challenge.forge({ 112 | id: challengeId 113 | }) 114 | .fetch() 115 | .then(function (challenge) { 116 | if (challenge) { 117 | res.status(200).json(challenge); 118 | } else { 119 | res.status(404).json(null); 120 | } 121 | }).catch(function (err) { 122 | res.status(500).json({error: true, data: {message: err.message}}); 123 | }); 124 | }, 125 | // POST /api/challenges 126 | addChallenge: function (req, res) { 127 | var challengeAttr = { 128 | name: req.body.name, 129 | prompt: req.body.prompt, 130 | test_suite: req.body.test_suite, 131 | type: req.body.type 132 | }; 133 | console.log(challengeAttr); 134 | 135 | Challenge.forge(challengeAttr).save() 136 | .then(function (challenge) { 137 | res.status(201).json(challenge); 138 | }).catch(function (err) { 139 | res.status(500).json({error: true, data: {message: err.message}}); 140 | }); 141 | }, 142 | 143 | update: function (req, res) { 144 | var challengeAttr = { 145 | prompt: req.body.prompt, 146 | test_suite: req.body.test_suite 147 | }; 148 | var challengeId = req.params.challengeId; 149 | Challenge.forge({ 150 | id: challengeId 151 | }).fetch() 152 | .then(function (challenge) { 153 | challenge.set(challengeAttr).save(); 154 | }).then(function (challenge) { 155 | res.status(201).json(challenge); 156 | }); 157 | }, 158 | 159 | resetWithData: function () { 160 | return Challenge.forge({ 161 | name: "One", 162 | prompt: "/*Write a function one() that returns the value 1.\n\nExample usage: \none(1); \/\/ => 1\none(2); \/\/ => 1*/\n\nvar one = function (n) {\n \n};", 163 | test_suite: "assert.equal(one(1),1);\nassert.equal(one(2),1);", 164 | type:'battle' 165 | }).save() 166 | .then(function () { 167 | return Challenge.forge({ 168 | name: "FizzBuzz", 169 | prompt: "/*Write a function that accepts an integer input, n, and returns \"FIZZ\" when n is divisible by 3, \"BUZZ\" when n is divisible by 5, and \"FIZZBUZZ\" when n is divisible by both 5 and 3\n\nExample usage:\n\nfizzBuzz(1) => 1\nfizzBuzz(15) => \"FIZZBUZZ\"\nfizzBuzz(20) => \"BUZZ\" */\n\nvar fizzBuzz = function (n) {\n \n};", 170 | test_suite: "assert.equal(fizzBuzz(1), 1);\nassert.equal(fizzBuzz(3), \"FIZZ\");\nassert.equal(fizzBuzz(5), \"BUZZ\");\nassert.equal(fizzBuzz(12), \"FIZZ\");\nassert.equal(fizzBuzz(30), \"FIZZBUZZ\");\nassert.equal(fizzBuzz(100), \"BUZZ\");\nassert.equal(fizzBuzz(300000), \"FIZZBUZZ\");\nassert.equal(fizzBuzz(123457), 123457);\nassert.equal(fizzBuzz(14), 14);", 171 | type:'battle' 172 | }).save(); 173 | }) 174 | .then(function () { 175 | return Challenge.forge({ 176 | name: "stringReverse", 177 | prompt: "/*Write a function that reverses a string, str\n\nExample usage:\nstringReverse(\"Foo bar!\") => \"!rab ooF\"\nstringReverse(\"Hello World\") => \"dlroW olleH\"*/\n\nvar stringReverse = function (str) {\n \n};", 178 | test_suite: "assert.equal(stringReverse(\"hi\"), \"ih\");\nassert.equal(stringReverse(\"a\"), \"a\");\nassert.equal(stringReverse(\"Foo bar!\"), \"!rab ooF\");\nassert.equal(stringReverse(\"1234\"), \"4321\");\nassert.equal(stringReverse(\"stringReverse\"), \"esreveRgnirts\");\nassert.equal(stringReverse(\"b b b\"), \"b b b\");", 179 | type:'battle' 180 | }).save(); 181 | }) 182 | .then(function () { 183 | return Challenge.forge({ 184 | name: "missingNumber", 185 | prompt: "/*Write a function that returns the missing number in an input array, arr.\nThe input array will always start at 0, and all following entries are 1 larger than the previous entry, except for the missing entry.\n\nExample usage:\nmissingNumber([0,1,2,4,5,6]) => 3\nmissingNumber([1,2,3,4,5]) => 0\nmissingNumber([0,1,2,3,4,5]) => null*/\n\nvar missingNumber = function (arr) {\n \n};", 186 | test_suite: "assert.equal(missingNumber([0,2]), 1);\nassert.equal(missingNumber([0]), null);\nassert.equal(missingNumber([1,2]), 0);\nassert.equal(missingNumber([0,1,2,3,5]), 4);\nassert.equal(missingNumber([0,1,3]), 2);\nassert.equal(missingNumber([0,1,2,3]), null);\nassert.equal(missingNumber([0,1,2,3,4]), null);\nassert.equal(missingNumber([0,1,2,4]), 3);\nassert.equal(missingNumber([0,1,2,3,4,5,6,8]), 7);", 187 | type:'battle' 188 | }).save(); 189 | }) 190 | .then(function() { 191 | return Challenge.forge({ 192 | name: "nth Fibonacci", 193 | prompt: "/*A Fibonacci sequence is a list of numbers that begins with 0 and 1, and each subsequent number is the sum of the previous two.\n\nFor example, the first five Fibonacci numbers are:\n\n 0 1 1 2 3\n\nIf n were 4, your function should return 3; for 5, it should return 5.\n\nWrite a function that accepts a number, n, and returns the nth Fibonacci number. Use a recursive solution to this problem; if you finish with time left over, implement an iterative solution.\n\nExample usage:\nnthFibonacci(2); \/\/ => 1\nnthFibonacci(3); \/\/ => 2\nnthFibonacci(4); \/\/ => 3\netc...*/\n\nvar nthFibonacci = function (n) {\n \n};", 194 | test_suite: "assert.equal(nthFibonacci(1), 1);\nassert.equal(nthFibonacci(2), 1);\nassert.equal(nthFibonacci(3), 2);\nassert.equal(nthFibonacci(4), 3);\nassert.equal(nthFibonacci(5), 5);\nassert.equal(nthFibonacci(10), 55);\nassert.equal(nthFibonacci(20), 6765);", 195 | type:'battle' 196 | }).save(); 197 | }); 198 | }, 199 | 200 | repopulateTable: function (req, res) { 201 | var fs = require('fs'); 202 | var pg = require('pg'); 203 | var copyFrom = require('pg-copy-streams').from; 204 | 205 | var DB_CONN_STR = "postgres://localhost:5432/myDB"; 206 | var DB_CSV_PATH = "./server/challenges/challenges.csv"; 207 | if (process.env.DEPLOYED) { 208 | DB_CSV_PATH = "./challenges/challenges.csv"; 209 | DB_CONN_STR = "postgres://postgres:mysecretpassword@postgres/postgres"; 210 | } 211 | 212 | // Delete elements from the challenges table, then reinsert them 213 | db.knex('challenges').truncate() 214 | .then(function() { 215 | pg.connect(DB_CONN_STR, function(err, client, done) { 216 | var stream = client.query(copyFrom("COPY challenges FROM STDIN WITH DELIMITER ',' CSV HEADER")); 217 | var fileStream = fs.createReadStream(DB_CSV_PATH); 218 | fileStream.on('error', done); 219 | fileStream.pipe(stream).on('finish', function () { 220 | res.end(); 221 | done(); 222 | }).on('error', done); 223 | }); 224 | }); 225 | } 226 | }; 227 | -------------------------------------------------------------------------------- /server/public/styles/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | *, *:before, *:after { 8 | -webkit-box-sizing: inherit; 9 | -moz-box-sizing: inherit; 10 | box-sizing: inherit; 11 | } 12 | 13 | body { 14 | background-color: #ffffff; 15 | font-family: Roboto, sans-serif; 16 | color: #7f7f7f; 17 | } 18 | 19 | .content { 20 | width: 100%; 21 | max-width: 1440px; 22 | height: 100%; 23 | margin: 0 auto; 24 | background-color: #efefef; 25 | display: flex; 26 | flex-direction: row; 27 | flex-wrap: wrap; 28 | align-content: center; 29 | position: relative; 30 | z-index: 0; 31 | padding-top: 50px; 32 | box-shadow: 0 1px 3px rgba(0,0,0,0.24), 0 1px 2px rgba(0,0,0,0.36); 33 | } 34 | 35 | .content-header { 36 | width: 100%; 37 | height: 40px; 38 | display: flex; 39 | align-items: center; 40 | } 41 | 42 | .content-header h2 { 43 | font-size: 16px; 44 | font-weight: 300; 45 | text-transform: uppercase; 46 | } 47 | 48 | .content-header-handle { 49 | width: 40px; 50 | overflow: hidden; 51 | } 52 | 53 | .content-header-handle img { 54 | height: 40px; 55 | width: auto; 56 | } 57 | 58 | .card { 59 | background-color: #ffffff; 60 | border-radius: 2px; 61 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 62 | transition: border 0.2s ease-in-out; 63 | border-right: 0px solid #8b1919; 64 | } 65 | 66 | .card-clickable:hover { 67 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 68 | cursor: pointer; 69 | border-right: 20px solid #8b1919; 70 | transition: border 0.2s ease-in-out; 71 | } 72 | 73 | .card-vertical { 74 | border-bottom: 0px solid #8b1919; 75 | transition: all 0.2s ease-in-out; 76 | } 77 | 78 | .card-vertical-clickable:hover { 79 | box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); 80 | cursor: pointer; 81 | border-bottom: 10px solid #8b1919; 82 | transition: all 0.2s ease-in-out; 83 | 84 | } 85 | 86 | .card-content { 87 | padding: 20px; 88 | 89 | } 90 | 91 | button { 92 | font-family: Roboto, sans-serif; 93 | font-size: 14px; 94 | padding: 10px; 95 | cursor: pointer; 96 | box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.24); 97 | transition: all 0.2s ease-in-out; 98 | border: 0px; 99 | } 100 | 101 | button:hover { 102 | box-shadow: 0 5px 10px rgba(0,0,0,0.19), 0 5px 10px rgba(0,0,0,0.23); 103 | transition: all 0.2s ease-in-out; 104 | } 105 | 106 | .submit { 107 | background-color: #8b1919; 108 | color: #ffffff; 109 | font-weight: 700; 110 | 111 | } 112 | 113 | .reset { 114 | background-color: #555555; 115 | color: #ffffff; 116 | font-weight: 700; 117 | } 118 | 119 | 120 | /* Splash page */ 121 | .splash { 122 | background-color: #efefef; 123 | width: 100%; 124 | height: 100%; 125 | display: flex; 126 | flex-direction: column; 127 | align-items: center; 128 | justify-content: center; 129 | } 130 | 131 | .splash a { 132 | margin-top: 40px; 133 | color: #7f7f7f; 134 | text-decoration: none; 135 | } 136 | 137 | .splash a:hover { 138 | color: #8b1919; 139 | } 140 | 141 | .splash a:hover .card { 142 | border-right: 10px solid #8b1919 143 | } 144 | 145 | .splash-logo { 146 | width: 100%; 147 | } 148 | 149 | .splash-logo img { 150 | display: block; 151 | width: 60%; 152 | margin-left: auto; 153 | margin-right: auto; 154 | opacity: 0.75 155 | } 156 | 157 | .github-login { 158 | background-color: #ffffff; 159 | width: 360px; 160 | height: 80px; 161 | margin: 0px 0px; 162 | display: flex; 163 | flex-direction: row; 164 | align-items: center; 165 | position: relative; 166 | } 167 | 168 | .github-login-handle { 169 | width: 80px; 170 | overflow: hidden; 171 | position: relative; 172 | } 173 | 174 | .github-login-handle img { 175 | position: relative; 176 | height: 80px 177 | } 178 | 179 | .splash a:hover .github-login-handle img { 180 | left: -80px; 181 | } 182 | 183 | .github-login .card-content { 184 | font-size: 24px; 185 | text-transform: uppercase; 186 | width: 270px; 187 | display: flex; 188 | justify-content: center; 189 | } 190 | 191 | /* Navigation bar */ 192 | .nav-bar { 193 | background-color: #ffffff; 194 | height: 50px; 195 | box-shadow: 0 2px 4px rgba(0,0,0,0.16), 0 2px 4px rgba(0,0,0,0.13); 196 | position: fixed; 197 | top: 0; 198 | z-index: 1; 199 | display: flex; 200 | justify-content: center; 201 | align-items: center; 202 | width: 100%; 203 | } 204 | 205 | .nav-content { 206 | width: 100%; 207 | max-width: 1440px; 208 | padding: 0 25px; 209 | display: flex; 210 | justify-content: space-between; 211 | align-items: center; 212 | } 213 | 214 | .nav-logo img { 215 | height: 30px; 216 | } 217 | 218 | .nav-links ul li { 219 | display: inline 220 | } 221 | 222 | .nav-links a { 223 | font-size: 16px; 224 | font-weight: 700; 225 | text-transform: uppercase; 226 | text-decoration: none; 227 | color: #7f7f7f; 228 | padding: 16px 15px 13px 15px; 229 | border-bottom: 3px solid transparent; 230 | } 231 | 232 | .nav-links a:hover { 233 | background-color: #ffffff; 234 | border-bottom: 3px solid #8b1919; 235 | color: #8b1919; 236 | } 237 | 238 | /* Staging */ 239 | .staging { 240 | display: flex; 241 | width: 100%; 242 | flex-direction: row; 243 | justify-content: center; 244 | align-self: center; 245 | } 246 | 247 | 248 | 249 | .mode { 250 | background-color: #ffffff; 251 | width: 300px; 252 | height: 400px; 253 | margin: 0px 20px; 254 | display: flex; 255 | flex-direction: column; 256 | align-items: center; 257 | position: relative; 258 | } 259 | 260 | .mode-handle { 261 | width: 300px; 262 | overflow: hidden; 263 | } 264 | 265 | .mode-handle img { 266 | height: 300px; 267 | width: auto; 268 | } 269 | 270 | .mode:hover img { 271 | position: relative; 272 | left: -300px; 273 | } 274 | 275 | .mode h3 { 276 | font-size: 40px; 277 | font-family: Roboto; 278 | font-weight: 300; 279 | letter-spacing: 4px; 280 | padding-top: 5px 281 | } 282 | 283 | .mode:hover h3 { 284 | color: #8b1919; 285 | } 286 | 287 | .mode-description { 288 | position: absolute; 289 | top: 0px; 290 | left: 0px; 291 | background-color: black; 292 | color: #ffffff; 293 | padding: 10px; 294 | width: 300px; 295 | height: 300px; 296 | opacity: 1; 297 | } 298 | 299 | .mode:hover .mode-description { 300 | } 301 | 302 | 303 | /* Solo staging */ 304 | .challenge-list { 305 | display: flex; 306 | flex-direction: column; 307 | flex-wrap: wrap; 308 | justify-content: center; 309 | align-items: center; 310 | width: 470px; 311 | z-index: 3; 312 | position: fixed; 313 | max-height: 90%; 314 | left: 50%; 315 | margin-left: -235px; 316 | top: 50px; 317 | } 318 | 319 | .challenge-list-container { 320 | width: 470px; 321 | padding: 25px; 322 | display: flex; 323 | flex-wrap: wrap; 324 | justify-content: center; 325 | position: relative; 326 | background-color: #efefef; 327 | border-radius: 4px; 328 | overflow-y: auto; 329 | } 330 | 331 | .challenge-card { 332 | width: 400px; 333 | height: 80px; 334 | margin: 10px; 335 | display: flex; 336 | } 337 | 338 | .challenge-card:hover { 339 | border-right: 10px solid #8b1919; 340 | } 341 | 342 | .challenge-card-handle { 343 | width: 80px; 344 | color: #ffffff; 345 | font-size: 30px; 346 | overflow: hidden; 347 | } 348 | 349 | .challenge-card-handle img { 350 | height: 80px; 351 | position: relative; 352 | } 353 | 354 | .challenge-card:hover .challenge-card-handle img { 355 | right: 80px; 356 | } 357 | 358 | .challenge-card .card-content { 359 | width: 310px; 360 | display: flex; 361 | flex-direction: column; 362 | justify-content: space-around; 363 | } 364 | 365 | .challenge-card:hover .card-content { 366 | color: #8b1919; 367 | } 368 | 369 | .challenge-card .challenge-date { 370 | font-size: 12px; 371 | margin-top: -10px; 372 | } 373 | 374 | .challenge-card-blank { 375 | width: 400px; 376 | height: 80px; 377 | margin: 10px; 378 | } 379 | 380 | .staging-exit-container { 381 | position: absolute; 382 | top: -20px; 383 | right: 50%; 384 | z-index: 4; 385 | margin-right: -255px; 386 | height: 20px; 387 | display: flex; 388 | justify-content: center; 389 | } 390 | 391 | .staging-exit { 392 | height: 40px; 393 | width: 40px; 394 | border-radius: 20px; 395 | font-weight: bolder; 396 | background-color: rgb(180,180,180); 397 | position: relative; 398 | z-index: 3; 399 | } 400 | 401 | /* Arena */ 402 | 403 | /* Solo arena */ 404 | .arena { 405 | width: 100%; 406 | height: 100%; 407 | display: flex; 408 | flex-direction: column 409 | } 410 | 411 | .solo-editor { 412 | width: 100%; 413 | height: 75%; 414 | position: relative; 415 | z-index: 1; 416 | border-bottom: 1px solid #aaa 417 | } 418 | 419 | .arena-buttons, .challenge-arena-buttons { 420 | position: relative; 421 | z-index: 2; 422 | } 423 | .splash-buttons { 424 | margin:5px; 425 | width: 400px; 426 | } 427 | .submit-challenge { 428 | position: absolute; 429 | right: 30px; 430 | top: -60px; 431 | border-radius: 2px; 432 | } 433 | 434 | .reset-challenge { 435 | position: absolute; 436 | right: 120px; 437 | top: -60px; 438 | border-radius: 2px; 439 | } 440 | 441 | .console { 442 | height: 25%; 443 | width: 100%; 444 | font-family: Courier, monospace; 445 | font-size: 20px; 446 | background-color: #fafa; 447 | color: #4f4f4f; 448 | padding: 10px; 449 | white-space: pre; 450 | position: relative; 451 | overflow: auto; 452 | 453 | } 454 | 455 | .success-messages { 456 | color: #00bb00; 457 | } 458 | 459 | .error-messages { 460 | color: #ff3333; 461 | } 462 | 463 | /* Challenge arena */ 464 | .editors { 465 | display: flex; 466 | z-index: 0; 467 | height: 75%; 468 | } 469 | .player-editor, .opponent-editor { 470 | height: 100%; 471 | width:50%; 472 | } 473 | 474 | .challenge-arena-buttons { 475 | width: 50%; 476 | } 477 | 478 | .backdrop { 479 | position: fixed; 480 | top: 0px; 481 | left: 0px; 482 | height: 100%; 483 | width: 100%; 484 | background-color: rgba(0,0,0,0.75); 485 | z-index: 2; 486 | display: flex; 487 | justify-content: center; 488 | align-items: center; 489 | } 490 | 491 | .delaysplash { 492 | white-space: pre; 493 | } 494 | 495 | .player-card { 496 | width: 300px; 497 | height: 75px; 498 | margin: 10px; 499 | display: flex; 500 | } 501 | 502 | .player-card-handle { 503 | width: 75px; 504 | background-color: #3f3f3f; 505 | color: #ffffff; 506 | font-size: 30px; 507 | display: flex; 508 | align-items: center; 509 | } 510 | 511 | .player-card-handle img { 512 | height: 75px; 513 | } 514 | 515 | .player-card .card-content { 516 | width: 225px; 517 | display: flex; 518 | align-items: center; 519 | font-size: 24px 520 | } 521 | 522 | .user-card { 523 | justify-content: flex-start; 524 | } 525 | 526 | .opponent-card { 527 | justify-content: flex-end; 528 | } 529 | 530 | .opponent-card .card-content { 531 | justify-content: flex-end; 532 | } 533 | 534 | .match-opponent-url { 535 | margin-bottom: 6px; 536 | } 537 | 538 | .challenge-title { 539 | margin-top: 3px; 540 | } 541 | 542 | .countdown-card { 543 | font-size: 16px; 544 | padding: 15px; 545 | width: 300px; 546 | background-color: #8b1919; 547 | color: white; 548 | display: flex; 549 | justify-content: center; 550 | align-items: center; 551 | margin: 0 auto; 552 | text-transform: uppercase; 553 | } 554 | 555 | .spinner { 556 | position: absolute; 557 | width: 50px; 558 | height: 50px; 559 | top: 200px; 560 | left: 50%; 561 | margin-left: -25px; 562 | z-index: 10; 563 | } 564 | .challenge-status { 565 | font-size: 50px; 566 | color: #FFFFFF; 567 | 568 | } 569 | 570 | .leaderboard { 571 | color: #FFFFFF; 572 | font-size: 15px; 573 | height: 20px; 574 | width: 400px; 575 | margin-top: 10px; 576 | } 577 | 578 | .right { 579 | text-align:right; 580 | } 581 | 582 | .left { 583 | text-align:left; 584 | } 585 | 586 | .description-staging { 587 | margin-left: auto; 588 | margin-right: auto; 589 | text-align: center; 590 | position:relative; 591 | margin-top:30px; 592 | height: 100px; 593 | width: 600px; 594 | } 595 | .description-challenge{ 596 | opacity: 1; 597 | webkit-animation: fadein 1s; /* Safari, Chrome and Opera > 12.1 */ 598 | -moz-animation: fadein 1s; /* Firefox < 16 */ 599 | -ms-animation: fadein 1s; /* Internet Explorer */ 600 | -o-animation: fadein 1s; /* Opera < 12.1 */ 601 | animation: fadein 1s; 602 | } 603 | .description-practice{ 604 | opacity: 1; 605 | webkit-animation: fadein 1s; /* Safari, Chrome and Opera > 12.1 */ 606 | -moz-animation: fadein 1s; /* Firefox < 16 */ 607 | -ms-animation: fadein 1s; /* Internet Explorer */ 608 | -o-animation: fadein 1s; /* Opera < 12.1 */ 609 | animation: fadein 1s; 610 | } 611 | 612 | @keyframes fadein { 613 | from { opacity: 0; } 614 | to { opacity: 1; } 615 | } 616 | 617 | /* Firefox < 16 */ 618 | @-moz-keyframes fadein { 619 | from { opacity: 0; } 620 | to { opacity: 1; } 621 | } 622 | 623 | /* Safari, Chrome and Opera > 12.1 */ 624 | @-webkit-keyframes fadein { 625 | from { opacity: 0; } 626 | to { opacity: 1; } 627 | } 628 | 629 | /* Internet Explorer */ 630 | @-ms-keyframes fadein { 631 | from { opacity: 0; } 632 | to { opacity: 1; } 633 | } 634 | 635 | /* Opera < 12.1 */ 636 | @-o-keyframes fadein { 637 | from { opacity: 0; } 638 | to { opacity: 1; } 639 | } 640 | 641 | .github-star { 642 | position: fixed; 643 | bottom: 10px; 644 | right: 10px; 645 | z-index: 4; 646 | } 647 | -------------------------------------------------------------------------------- /server/challenges/challenges.csv: -------------------------------------------------------------------------------- 1 | id,name,prompt,test_suite,type,created_at,updated_at 2 | 1,One,"/*Write a function one() that returns the value 1. 3 | Example usage: 4 | one(1); => 1 5 | one(2); => 1*/ 6 | 7 | var one = function (n) { 8 | 9 | };","assert.equal(one(1),1); 10 | assert.equal(one(2),1);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 11 | 2,fizzBuzz,"/*Write a function that accepts an integer input, n, and returns ""FIZZ"" when n is divisible by 3, ""BUZZ"" when n is divisible by 5, and ""FIZZBUZZ"" when n is divisible by both 5 and 3 12 | 13 | Example usage: 14 | fizzBuzz(1) => 1 15 | fizzBuzz(15) => ""FIZZBUZZ"" 16 | fizzBuzz(20) => ""BUZZ"" */ 17 | 18 | var fizzBuzz = function (n) { 19 | 20 | };","assert.equal(fizzBuzz(1), 1); 21 | assert.equal(fizzBuzz(3), ""FIZZ""); 22 | assert.equal(fizzBuzz(5), ""BUZZ""); 23 | assert.equal(fizzBuzz(12), ""FIZZ""); 24 | assert.equal(fizzBuzz(30), ""FIZZBUZZ""); 25 | assert.equal(fizzBuzz(100), ""BUZZ""); 26 | assert.equal(fizzBuzz(300000), ""FIZZBUZZ""); 27 | assert.equal(fizzBuzz(123457), 123457); 28 | assert.equal(fizzBuzz(14), 14);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 29 | 3,stringReverse,"/*Write a function that reverses a string, str 30 | 31 | Example usage: 32 | stringReverse(""Foo bar!"") => ""!rab ooF"" 33 | stringReverse(""Hello World"") => ""dlroW olleH""*/ 34 | 35 | var stringReverse = function (str) { 36 | 37 | };","assert.equal(stringReverse(""hi""), ""ih""); 38 | assert.equal(stringReverse(""a""), ""a""); 39 | assert.equal(stringReverse(""Foo bar!""), ""!rab ooF""); 40 | assert.equal(stringReverse(""1234""), ""4321""); 41 | assert.equal(stringReverse(""stringReverse""), ""esreveRgnirts""); 42 | assert.equal(stringReverse(""b b b""), ""b b b"");",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 43 | 4,missingNumber,"/*Write a function that returns the missing number in an input array, arr. 44 | The input array will always start at 0, and all following entries are 1 larger than the previous entry, except for the missing entry. 45 | 46 | Example usage: 47 | missingNumber([0,1,2,4,5,6]) => 3 48 | missingNumber([1,2,3,4,5]) => 0 49 | missingNumber([0,1,2,3,4,5]) => null*/ 50 | 51 | var missingNumber = function (arr) { 52 | 53 | };","assert.equal(missingNumber([0,2]), 1); 54 | assert.equal(missingNumber([0]), null); 55 | assert.equal(missingNumber([1,2]), 0); 56 | assert.equal(missingNumber([0,1,2,3,5]), 4); 57 | assert.equal(missingNumber([0,1,3]), 2); 58 | assert.equal(missingNumber([0,1,2,3]), null); 59 | assert.equal(missingNumber([0,1,2,3,4]), null); 60 | assert.equal(missingNumber([0,1,2,4]), 3); 61 | assert.equal(missingNumber([0,1,2,3,4,5,6,8]), 7);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 62 | 5,nthFibonacci,"/*A Fibonacci sequence is a list of numbers that begins with 0 and 1, and each subsequent number is the sum of the previous two. 63 | 64 | For example, the first five Fibonacci numbers are: 0 1 1 2 3 65 | If n were 4, your function should return 3; for 5, it should return 5. 66 | 67 | Write a function that accepts a number, n, and returns the nth Fibonacci number. Use a recursive solution to this problem; if you finish with time left over, implement an iterative solution. 68 | 69 | Example usage: 70 | nthFibonacci(2); => 1 71 | nthFibonacci(3); => 2 72 | nthFibonacci(4); => 3 73 | etc...*/ 74 | 75 | var nthFibonacci = function (n) { 76 | 77 | };","assert.equal(nthFibonacci(1), 1); 78 | assert.equal(nthFibonacci(2), 1); 79 | assert.equal(nthFibonacci(3), 2); 80 | assert.equal(nthFibonacci(4), 3); 81 | assert.equal(nthFibonacci(5), 5); 82 | assert.equal(nthFibonacci(10), 55); 83 | assert.equal(nthFibonacci(20), 6765);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 84 | 6,nthRowPascal,"/*A pascal triangle looks like this: 85 | 1 86 | 1 1 87 | 1 2 1 88 | 1 3 3 1 89 | 90 | It has the property that the following row starts and ends with 1's, and every middle entry is the sum of the two nearst entries from the row above. 91 | 92 | Write a function that accepts an input number, n, and returns the nth row of a pascal triangle as an array. Assume non-negative integer inputs 93 | 94 | Example usage: 95 | nthRowPascal(0) => [1] 96 | nthRowPascal(3) => [1, 3, 3, 1]*/ 97 | 98 | var nthRowPascal = function (n) { 99 | 100 | };","assert.deepEqual(nthRowPascal(0), [1]); 101 | assert.deepEqual(nthRowPascal(1), [1,1]); 102 | assert.deepEqual(nthRowPascal(3), [1,3,3,1]); 103 | assert.deepEqual(nthRowPascal(10), [1,10,45,120,210,252,210,120,45,10,1]); 104 | ",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 105 | 7,stringSum,"/*Write a function that accepts an input string, str, and returns the numeric sum of each character. Assume only positive integers 106 | 107 | Example usage: 108 | stringSum(""12345"") => 15 109 | stringSum(""82"") => 10 110 | stringSum(""0101"") => 2*/ 111 | 112 | var stringSum = function (str) { 113 | 114 | };","assert.equal(stringSum(""12345""), 15); 115 | assert.equal(stringSum(""82""), 10); 116 | assert.equal(stringSum(""010101""), 3); 117 | assert.equal(stringSum(""0""), 0); 118 | assert.equal(stringSum(""11111111111111111111""), 20);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 119 | 8,Two,"/*Write a function one() that returns the value 1. 120 | Example usage: 121 | two(1); => 2 122 | two(2); => 2*/ 123 | 124 | var two = function (n) { 125 | 126 | };","assert.equal(two(1),2); 127 | assert.equal(two(2),2);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 128 | 9,isPrime,"/*Write a function that check to see if an input integer, n, is a prime number. Assume positive non-zero inputs 129 | 130 | Example usage: 131 | isPrime(2) => true 132 | isPrime(17) => true 133 | isPrime(100) => false*/ 134 | 135 | var isPrime = function (n) { 136 | 137 | };","assert.equal(isPrime(2), true); 138 | assert.equal(isPrime(113), true); 139 | assert.equal(isPrime(163), true); 140 | assert.equal(isPrime(1000000), false); 141 | assert.equal(isPrime(23452), false); 142 | assert.equal(isPrime(1234567654321), false);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 143 | 10,nthPrime,"/*Write a function that takes an input integer, n, and returns the nth prime number. Assume positive inputs 144 | 145 | Example usage: 146 | nthPrime(0) => 2 147 | nthPrime(1) => 3 148 | nthPrime(2) => 5*/ 149 | 150 | var nthPrime = function (n) { 151 | 152 | };","assert.equal(nthPrime(0),2); 153 | assert.equal(nthPrime(1),3); 154 | assert.equal(nthPrime(2),5); 155 | assert.equal(nthPrime(3),7); 156 | assert.equal(nthPrime(4),11); 157 | assert.equal(nthPrime(99),541); 158 | assert.equal(nthPrime(999),7919); 159 | assert.equal(nthPrime(344),2333);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 160 | 11,balancedParens,"/*Write a function that determines if an input string, str, has balanced parenthesis 161 | 162 | Example usage: 163 | balancedParens(""( ( ) )"") => true 164 | balancedParens(""() (())"") => true 165 | balancedParens(""((( ) ( )"") => false 166 | balancedParens("")("") = > false*/ 167 | 168 | var balancedParens = function (str) { 169 | 170 | };","assert.equal(balancedParens(""()()()()()()()""), true); 171 | assert.equal(balancedParens(""( ().. (b)a )"" ), true); 172 | assert.equal(balancedParens(""(()())((())())""), true); 173 | assert.equal(balancedParens(""()aaa(""), false); 174 | assert.equal(balancedParens(""sdjsf))((""), false); 175 | assert.equal(balancedParens("")()()(""), false); 176 | assert.equal(balancedParens(""((((((((""), false); 177 | assert.equal(balancedParens(""(((((((((())))))))))""), true);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 178 | 12,maxProfit,"/*Write a function that accepts an array of integers, arr, representing a stock’s price over time and returns the maximum achievable profit under the constraint that you must buy before you sell, and that you can buy and sell at most once. Assume input array only contains positive integers. 179 | 180 | Example usage: 181 | maxProfit([1]) => 0 182 | maxProfit([10,9,8]) => 0 183 | maxProfit([3,10,1,5]) => 7*/ 184 | 185 | var maxProfit = function (arr) { 186 | 187 | };","assert.equal(maxProfit([10,9,8,7,6,5,4,3,2,1]), 0); 188 | assert.equal(maxProfit([1,2,3,4,5,6,7,8,9,10]), 9); 189 | assert.equal(maxProfit([1]), 0); 190 | assert.equal(maxProfit([]), 0); 191 | assert.equal(maxProfit([3,10,1,5]), 7); 192 | assert.equal(maxProfit([7,4,7,8,0,1,3]), 4); 193 | assert.equal(maxProfit([1,10,1,10]), 9); 194 | assert.equal(maxProfit([1,100]), 99);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 195 | 13,Three,"/*Write a function three() that returns the value 3. 196 | Example usage: 197 | three(1); => 3 198 | three(2); => 3*/ 199 | 200 | var three = function (n) { 201 | 202 | };","assert.equal(three(1),3); 203 | assert.equal(three(2),3);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 204 | 14,isPalindrome,"/*Write a function that determines if an input string, str, is a palindrome 205 | 206 | Example usage: 207 | isPalindrome(""racecar"") => true 208 | isPalindrome(""peanut"") => false*/ 209 | 210 | var isPalindrome = function (str) { 211 | 212 | };","assert.equal(isPalindrome(""hannah""), true); 213 | assert.equal(isPalindrome(""racecar""), true); 214 | assert.equal(isPalindrome(""a""), true); 215 | assert.equal(isPalindrome(""""), true); 216 | assert.equal(isPalindrome(""Hannah""), false); 217 | assert.equal(isPalindrome(""hahaha""), false); 218 | assert.equal(isPalindrome(""oh no""), false);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 219 | 15,Four,"/*Write a function four() that returns the value 4. 220 | Example usage: 221 | four(1); => 4 222 | four(2); => 4*/ 223 | 224 | var four = function (n) { 225 | 226 | };","assert.equal(four(1),4); 227 | assert.equal(four(2),4);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 228 | 16,moveZeros,"/*Write a function that moves all zeros in an input array, arr, to the end of the array, while maintaining the relative order of the non-zero elements. 229 | Move the numbers in-place without making a copy of the input array 230 | 231 | Example usage: 232 | var arr = [0, 1, 0, 3, 12]; 233 | moveZeros(arr); 234 | arr => [1, 3, 12, 0, 0]*/ 235 | 236 | var moveZeros = function (arr) { 237 | 238 | };","var arr = [0,1,0,3,12]; 239 | var arr2 = [0,0,1]; 240 | var arr3 = [1]; 241 | var arr4 = [1,1,0,4,0,3,0,2]; 242 | moveZeros(arr); 243 | moveZeros(arr2); 244 | moveZeros(arr3); 245 | moveZeros(arr4); 246 | assert.deepEqual(arr, [1,3,12,0,0]); 247 | assert.deepEqual(arr2, [1,0,0]); 248 | assert.deepEqual(arr3, [1]); 249 | assert.deepEqual(arr4, [1,1,4,3,2,0,0,0]);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 250 | 17,colToNum,"/*Write a function that converts an input string, str, representing spreadsheet column headers to numbers like so: 251 | 252 | colToNum(""A"") => 1 253 | colToNum(""B"") => 2 254 | colToNum(""AA"") => 27 255 | 256 | Assume only uppercase letters in the input string. ""A"".charCodeAt(0) = 65 257 | */ 258 | 259 | var colToNum = function (str) { 260 | 261 | };","assert.equal(colToNum(""A""),1); 262 | assert.equal(colToNum(""Z""),26); 263 | assert.equal(colToNum(""AA""), 27); 264 | assert.equal(colToNum(""BC""), 55); 265 | assert.equal(colToNum(""AAA""), 703); 266 | assert.equal(colToNum(""ZAZ""), 17628); 267 | assert.equal(colToNum(""BBBBB""), 950510);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 268 | 18,addDigits,"/*Write a function that takes an input integer and sums its digits until the sum is single digit 269 | 270 | Example usage: 271 | addDigital(38) => 2 ==> 3+8=11 ==> 1+1=2 ==> return 2*/ 272 | 273 | var addDigits = function (num) { 274 | 275 | };","assert.equal(addDigits(38), 2); 276 | assert.equal(addDigits(111), 3); 277 | assert.equal(addDigits(803), 2); 278 | assert.equal(addDigits(888), 6); 279 | assert.equal(addDigits(0), 0); 280 | assert.equal(addDigits(999), 9);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 281 | 19,compressString,"/*Write a function that compresses an input string, str, while maintaining order of appearance, into this form: 282 | 283 | compressString(""banana"") => ""b1a3n2"" 284 | compressString(""mississippi"") => ""m1i4s4p2""*/ 285 | 286 | var compressString = function (str) { 287 | 288 | };","assert.equal(compressString(""banana""), ""b1a3n2""); 289 | assert.equal(compressString(""mississippi""), ""m1i4s4p2""); 290 | assert.equal(compressString(""abc""), ""a1b1c1""); 291 | assert.equal(compressString(""hey !""), ""h1e1y1 1!1""); 292 | assert.equal(compressString(""Hannah""), ""H1a2n2h1""); 293 | assert.equal(compressString(""hahnbi""), ""h2a1n1b1i1"");",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 294 | 20,stepSum,"/*Write a function that returns number of possible ways to reach the top of a staircase containing n steps, given that you can take 1 or 2 steps per move. 295 | 296 | Example usage: 297 | stepSum(1) => 1 298 | stepSum(4) => 5*/ 299 | 300 | var stepSum = function () { 301 | 302 | };","assert.equal(stepSum(1),1); 303 | assert.equal(stepSum(5),8); 304 | assert.equal(stepSum(7),21); 305 | assert.equal(stepSum(10),81); 306 | assert.equal(stepSum(20),10946);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 307 | 21,findSingleNum,"/*Given an input array of integers, arr, in which every element appears twice, except for one element, which appears only once. Write a function that finds that element 308 | 309 | Example usage: 310 | findSingleSum([1,2,3,4,1,2,3]) => 4 311 | findSingleSum([9,9,5,5,1]) => 1*/ 312 | 313 | var findSingleSum = function (arr) { 314 | 315 | };","assert.equal(findSingleSum([1,2,3,4,3,2,1],4)); 316 | assert.equal(findSingleSum([2,2,3,3,9,9,1],1)); 317 | assert.equal(findSingleSum([2,2,3],3)); 318 | assert.equal(findSingleSum([9,8,7,6,7,9,6],8)); 319 | assert.equal(findSingleSum([1,0,0,5,1],5)); 320 | assert.equal(findSingleSum([1,2,3,4,5,6,7,8,9,10,9,8,7,6,5,4,3,2,1],10));",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 321 | 22,romanNum,"/*Write a function that interprets roman numeral, str, into an integer, assuming that the input str in integer form will not be larger than 3999. 322 | I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1,000*/ 323 | 324 | var romanNum = function (str) { 325 | 326 | };","assert.equal(romanNum(""LXXVII""),77); 327 | assert.equal(romanNum(""III""),3); 328 | assert.equal(romanNum(""VI""),6); 329 | assert.equal(romanNum(""IV""),4); 330 | assert.equal(romanNum(""IX""),9); 331 | assert.equal(romanNum(""XLIII""),43); 332 | assert.equal(romanNum(""MDCCC""),1800); 333 | assert.equal(romanNum(""MDCCCXLIII""),1843); 334 | assert.equal(romanNum(""MCMXCVI""),1996);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 335 | 23,isAnagram,"/*Write a function that given two input strings, str1 and st2, determins if str1 is an anagram of str2, ignoring spaces and capitalization 336 | 337 | Example usage: 338 | isAnagram(""Hi!"", ""!iH"") => true 339 | isAnagram(""Hello"", ""hello"") => true*/ 340 | 341 | var isAnagram = function (str1, str2) { 342 | 343 | };","assert.equal(isAnagram("""",""""),true); 344 | assert.equal(isAnagram(""abc"",""bac""),true); 345 | assert.equal(isAnagram(""Hi !"",""!ih""),true); 346 | assert.equal(isAnagram(""Hello"",""hello""),true); 347 | assert.equal(isAnagram(""Male chauvinism"",""Im such a vile man""),true); 348 | assert.equal(isAnagram(""bat"",""cat""),false);",battle,2016-02-02 20:42:35.743-08,2016-02-02 20:42:35.743-08 349 | --------------------------------------------------------------------------------