├── 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 |
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 |
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 |
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 | 
73 |
74 | ### Database Schema
75 |
76 | Database in Postgres, using Bookshelf and Knex
77 | 
78 |
79 | ### Socket Interactions
80 |
81 | 
82 |
83 | 
84 |
85 | 
86 |
87 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------