diff --git a/app/core/Router.coffee b/app/core/Router.coffee index b80e7f671..3b9359c98 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -51,6 +51,7 @@ module.exports = class CocoRouter extends Backbone.Router 'artisans/level-tasks': go('artisans/LevelTasksView') 'artisans/solution-problems': go('artisans/SolutionProblemsView') 'artisans/thang-tasks': go('artisans/ThangTasksView') + 'artisans/level-concepts': go('artisans/LevelConceptMap') 'beta': go('HomeView') diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index 16e48dd6a..7c26e6c08 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -114,7 +114,6 @@ module.exports = class Simulator extends CocoClass catch error console.log "Failed to form task results:", error return @cleanupAndSimulateAnotherTask() - console.log 'Processing results:', taskResults humanSessionRank = taskResults.sessions[0].metrics.rank ogreSessionRank = taskResults.sessions[1].metrics.rank if @options.headlessClient and @options.simulateOnlyOneGame @@ -377,78 +376,24 @@ module.exports = class Simulator extends CocoClass return 1 generateSpellsObject: -> - @currentUserCodeMap = @task.generateSpellKeyToSourceMap() - @spells = {} - for thang in @level.attributes.thangs - continue if @thangIsATemplate thang - @generateSpellKeyToSourceMapPropertiesFromThang thang - @spells + spells = {} + for {hero, team} in [{hero: 'Hero Placeholder', team: 'humans'}, {hero: 'Hero Placeholder 1', team: 'ogres'}] + sessionInfo = _.filter(@task.getSessions(), {team: team})[0] + fullSpellName = _.string.slugify(hero) + '/plan' + submittedCodeLanguage = sessionInfo?.submittedCodeLanguage ? 'javascript' + submittedCode = LZString.decompressFromUTF16 sessionInfo?.submittedCode?[_.string.slugify(hero)]?.plan ? '' + aether = new Aether createAetherOptions functionName: 'plan', codeLanguage: submittedCodeLanguage, skipProtectAPI: false + try + aether.transpile submittedCode + catch e + console.log "Couldn't transpile #{fullSpellName}:\n#{submittedCode}\n", e + aether.transpile '' + spells[fullSpellName] = name: 'plan', team: team, thang: {thang: {id: hero}, aether: aether} + spells - thangIsATemplate: (thang) -> - for component in thang.components - continue unless @componentHasProgrammableMethods component - for methodName, method of component.config.programmableMethods - return true if @methodBelongsToTemplateThang method - - return false - - componentHasProgrammableMethods: (component) -> component.config? and _.has component.config, 'programmableMethods' - - methodBelongsToTemplateThang: (method) -> typeof method is 'string' - - generateSpellKeyToSourceMapPropertiesFromThang: (thang) => - for component in thang.components - continue unless @componentHasProgrammableMethods component - for methodName, method of component.config.programmableMethods - spellKey = @generateSpellKeyFromThangIDAndMethodName thang.id, methodName - - @createSpellAndAssignName spellKey, methodName - @createSpellThang thang, method, spellKey - @transpileSpell thang, spellKey, methodName - - generateSpellKeyFromThangIDAndMethodName: (thang, methodName) -> - spellKeyComponents = [thang, methodName] - spellKeyComponents[0] = _.string.slugify spellKeyComponents[0] - spellKey = spellKeyComponents.join '/' - spellKey - - createSpellAndAssignName: (spellKey, spellName) -> - @spells[spellKey] ?= {} - @spells[spellKey].name = spellName - - createSpellThang: (thang, method, spellKey) -> - @spells[spellKey].thangs ?= {} - @spells[spellKey].thangs[thang.id] ?= {} - spellTeam = @task.getSpellKeyToTeamMap()[spellKey] - playerTeams = @task.getPlayerTeams() - useProtectAPI = true - if spellTeam not in playerTeams - useProtectAPI = false - else - spellSession = _.filter(@task.getSessions(), {team: spellTeam})[0] - unless codeLanguage = spellSession?.submittedCodeLanguage - console.warn 'Session', spellSession.creatorName, spellSession.team, 'didn\'t have submittedCodeLanguage, just:', spellSession - @spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method, useProtectAPI, codeLanguage ? 'javascript' - - transpileSpell: (thang, spellKey, methodName) -> - slugifiedThangID = _.string.slugify thang.id - generatedSpellKey = [slugifiedThangID,methodName].join '/' - source = @currentUserCodeMap[generatedSpellKey] ? '' - aether = @spells[spellKey].thangs[thang.id].aether - #unless _.contains(@task.spellKeysToTranspile, generatedSpellKey) - try - aether.transpile source - catch e - console.log "Couldn't transpile #{spellKey}:\n#{source}\n", e - aether.transpile '' - - createAether: (methodName, method, useProtectAPI, codeLanguage) -> - aetherOptions = createAetherOptions functionName: methodName, codeLanguage: codeLanguage, skipProtectAPI: not useProtectAPI - return new Aether aetherOptions class SimulationTask constructor: (@rawData) -> - @spellKeyToTeamMap = {} getLevelName: -> levelName = @rawData.sessions?[0]?.levelID @@ -476,30 +421,4 @@ class SimulationTask getSessions: -> @rawData.sessions - getSpellKeyToTeamMap: -> @spellKeyToTeamMap - - getPlayerTeams: -> _.pluck @rawData.sessions, 'team' - setWorld: (@world) -> - - generateSpellKeyToSourceMap: -> - # TODO: we always now only have hero-placeholder/plan vs. hero-placeholder-1/plan on humans vs. ogres, always just have to retranspile for Esper, and never need to transpile for NPCs or other methods, so we can get rid of almost all of this stuff. - playerTeams = _.pluck @rawData.sessions, 'team' - spellKeyToSourceMap = {} - for session in @rawData.sessions - teamSpells = session.teamSpells[session.team] - allTeams = _.keys session.teamSpells - for team in allTeams - for spell in session.teamSpells[team] - @spellKeyToTeamMap[spell] = team - teamCode = {} - - for thangName, thangSpells of session.submittedCode - for spellName, spell of thangSpells - fullSpellName = [thangName, spellName].join '/' - if _.contains(teamSpells, fullSpellName) - teamCode[fullSpellName] = LZString.decompressFromUTF16 spell - - _.merge spellKeyToSourceMap, teamCode - - spellKeyToSourceMap diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 9aee76dfa..46d02551c 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -798,6 +798,7 @@ phoenix_title: "Software Engineer" nolan_title: "Territory Manager" elliot_title: "Partnership Manager" + elliot_blurb: "Mindreader" lisa_title: "Market Development Rep" retrostyle_title: "Illustration" retrostyle_blurb: "RetroStyle Games" diff --git a/app/templates/about.jade b/app/templates/about.jade index b7f3d81ac..a0be8a7fe 100644 --- a/app/templates/about.jade +++ b/app/templates/about.jade @@ -137,7 +137,7 @@ block content .team-bio h6.label.team-name Elliot Okiwelu small(data-i18n="about.elliot_title") - br + small(data-i18n="about.elliot_blurb") li img(src="/images/pages/about/lisa_small.png").img-thumbnail diff --git a/app/templates/artisans/artisans-view.jade b/app/templates/artisans/artisans-view.jade index c2b5b3215..2f6321319 100644 --- a/app/templates/artisans/artisans-view.jade +++ b/app/templates/artisans/artisans-view.jade @@ -10,4 +10,7 @@ block content |Level Tasks div a(href="/artisans/solution-problems") - |Solution Problems \ No newline at end of file + |Solution Problems + div + a(href="/artisans/level-concepts") + |Level Concept Map \ No newline at end of file diff --git a/app/templates/artisans/concept-map-view.jade b/app/templates/artisans/concept-map-view.jade new file mode 100644 index 000000000..50368de79 --- /dev/null +++ b/app/templates/artisans/concept-map-view.jade @@ -0,0 +1,24 @@ +// DNT +extends /templates/base + +block content + #solution-problems-view + div + a(href='/artisans') + span.glyphicon.glyphicon-chevron-left + span Artisans Home + br + for course, k in (view.data || {}) + h3= k + table.table.table-striped + tr + th No + th Level Name + th Concepts Detected + for level in course + tr + td= level.seqNo + td(style="width:10%")= level.get('name') + td + for tag in (level.tags || []) + span.label.label-primary(style='margin-right: 10px; float: left; line-height: 20px; margin-bottom: 10px')= tag \ No newline at end of file diff --git a/app/views/artisans/LevelConceptMap.coffee b/app/views/artisans/LevelConceptMap.coffee new file mode 100644 index 000000000..13c1183c2 --- /dev/null +++ b/app/views/artisans/LevelConceptMap.coffee @@ -0,0 +1,173 @@ +RootView = require 'views/core/RootView' +template = require 'templates/artisans/concept-map-view' + +Level = require 'models/Level' +Campaign = require 'models/Campaign' + +CocoCollection = require 'collections/CocoCollection' +Campaigns = require 'collections/Campaigns' +Levels = require 'collections/Levels' +parser = new esper().realm.parser + +module.exports = class LevelConceptMap extends RootView + template: template + id: 'solution-problems-view' + excludedCampaigns = [ + # Misc. campaigns + 'picoctf', 'auditions' + + # Campaign-version campaigns + #'dungeon', 'forest', 'desert', 'mountain', 'glacier' + + # Test campaigns + 'dungeon-branching-test', 'forest-branching-test', 'desert-branching-test' + + # Course-version campaigns + #'intro', 'course-2', 'course-3', 'course-4', 'course-5', 'course-6' + ] + + includedLanguages = [ + 'javascript' + ] + + excludedLevelSnippets = [ + 'treasure', 'brawl', 'siege' + ] + + unloadedCampaigns: 0 + campaignLevels: {} + loadedLevels: {} + data: {} + problemCount: 0 + + initialize: -> + @campaigns = new Campaigns([]) + @listenTo(@campaigns, 'sync', @onCampaignsLoaded) + @supermodel.trackRequest(@campaigns.fetch( + data: + project:'slug' + )) + + onCampaignsLoaded: (campCollection) -> + for campaign in campCollection.models + campaignSlug = campaign.get('slug') + continue if campaignSlug in excludedCampaigns + @unloadedCampaigns++ + + @campaignLevels[campaignSlug] = new Levels() + @listenTo(@campaignLevels[campaignSlug], 'sync', @onLevelsLoaded.bind @, campaignSlug) + @supermodel.trackRequest(@campaignLevels[campaignSlug].fetchForCampaign(campaignSlug, + data: + project: 'thangs,name,slug,campaign' + )) + + onLevelsLoaded: (campaignSlug, lvlCollection) -> + for level, k in lvlCollection.models + level.campaign = campaignSlug + @loadedLevels[campaignSlug] = {} unless @loadedLevels[campaignSlug]? + ll = {} unless ll? + level.seqNo = lvlCollection.models.length - k + @loadedLevels[campaignSlug][level.get('slug')] = level + if --@unloadedCampaigns is 0 + @onAllLevelsLoaded() + + onAllLevelsLoaded: -> + for campaignSlug, campaign of @loadedLevels + for levelSlug, level of campaign + unless level? + console.error 'Level Slug doesn\'t have associated Level', levelSlug + continue + + isBad = false + for word in excludedLevelSnippets + if levelSlug.indexOf(word) isnt -1 + isBad = true + continue if isBad + thangs = level.get 'thangs' + component = null + thangs = _.filter(thangs, (elem) -> + return _.findWhere(elem.components, (elem2) -> + if elem2.config?.programmableMethods? + component = elem2 + return true + ) + ) + + if thangs.length > 1 + console.warn 'Level has more than 1 programmableMethod Thangs', levelSlug + continue + + unless component? + console.error 'Level doesn\'t have programmableMethod Thang', levelSlug + continue + + plan = component.config.programmableMethods.plan + level.tags = @tagLevel _.find plan.solutions, (s) -> s.language is 'javascript' + @data[campaignSlug] = _.sortBy _.values(@loadedLevels[campaignSlug]), 'seqNo' + + console.log @render, @loadedLevels + @render() + + tagLevel: (src) -> + return [] if not src?.source? + try + ast = parser(src.source) + catch e + return ['parse error: ' + e.message] + + tags = {} + process = (n) -> + return unless n? + switch n.type + when "Program", "BlockStatement" + process(n) for n in n.body + when "FunctionDeclaration" + tags['function-def'] = true + if n.params > 0 + tags['function-params:' + n.params.length] = true + process(n.body) + when "ExpressionStatement" + process(n.expression) + when "CallExpression" + process(n.callee) + when "MemberExpression" + if n.object?.name is 'hero' + tags["hero." + n.property.name] = true + when "WhileStatement" + if n.test.type is 'Literal' and n.test.value is true + tags['while-true'] = true + else + tags['while'] = true + process(n.test) + process(n.body) + when "ForStatement" + tags['for'] = true + process(n.init) + process(n.test) + process(n.update) + process(n.body) + when "IfStatement" + tags['if'] = true + process(n.test) + process(n.consequent) + process(n.alternate) + when "Literal" + if n.value is true + tags['true'] = true + else + tags['literal:' + typeof n.value] = true + when "BinaryExpression","LogicalExpression" + process(n.left) + process(n.right) + tags[n.operator] = true + when "AssignmentExpression" + tags['assign:' + n.operator] = true + process(n.right) + else + tags[n.type] = true + + + + process ast + Object.keys(tags) + diff --git a/app/views/core/RootView.coffee b/app/views/core/RootView.coffee index 75ed205a3..8e29b9e12 100644 --- a/app/views/core/RootView.coffee +++ b/app/views/core/RootView.coffee @@ -30,6 +30,7 @@ module.exports = class RootView extends CocoView 'click a': 'onClickAnchor' 'click button': 'toggleModal' 'click li': 'toggleModal' + 'treema-error': 'onTreemaError' subscriptions: 'achievements:new': 'handleNewAchievements' @@ -184,3 +185,6 @@ module.exports = class RootView extends CocoView navigateToAdmin: -> if window.amActually or me.isAdmin() application.router.navigate('/admin', {trigger: true}) + + onTreemaError: (e) -> + noty text: e.message, layout: 'topCenter', type: 'error', killer: false, timeout: 5000, dismissQueue: true diff --git a/scripts/analytics/analyzeClassCompletionTimes.js b/scripts/analytics/analyzeClassCompletionTimes.js new file mode 100644 index 000000000..151d24923 --- /dev/null +++ b/scripts/analytics/analyzeClassCompletionTimes.js @@ -0,0 +1,105 @@ +if ( typeof password === 'undefined' ) { + throw "Please specify the coco user password on the commandline as --eval 'password \"\"'"; +} + +if ( typeof _ === 'undefined' ) { + throw "Please include underscore/lodash on the commandline before this script."; +} + + +var main = connect('ec2-52-4-223-77.compute-1.amazonaws.com:27017/coco', 'coco', password); +var ls = connect('ec2-54-236-64-198.compute-1.amazonaws.com:27017/coco', 'coco', password); +var users = main.users.find( + {role: 'student', + //birthday: {$regex: /^20/} + birthday: {$exists: 1} + }, + {_id: 1, birthday: 1, email: 1} +).toArray(); +print("Found " + users.length + " users in age range"); + +var names = { + "5411cb3769152f1707be029c": "Dungeons of Kithgard", + "54173c90844506ae0195a0b4": "Gems in the Deep", + "54174347844506ae0195a0b8": "Shadow Guard", + "54ca592de4983255055a5478": "Enemy Mine", + "541875da4c16460000ab990f": "True Names", + "5604169b60537b8705386a59": "Kithgard Librarian", + "55ca293b9bc1892c835b0136": "Fire Dancing", + "565ce2291b940587057366dd": "Loop Da Loop", + "545a5914d820eb0000f6dc0a": "Haunted Kithmaze", + "5418cf256bae62f707c7e1c3": "The Second Kithmaze", + "5418d40f4c16460000ab9ac2": "Dread Door", + "54e0cdefe308cb510555a7f5": "Cupboards of Kithgard", + "54f0e074a375e47f055d619c": "Breakout", + "5452adea57e83800009730ee": "Known Enemy", + "5452c3ce57e83800009730f7": "Master of Names", + "55ca29439bc1892c835b0137": "A Mayhem of Munchkins", + "5452d8b906a59e000067e4fa": "The Gauntlet", + "541b434e1ccc8eaae19f3c33": "The Final Kithmaze", + "541c9a30c6362edfb0f34479": "Kithgard Gates", + "5630eab0c0fcbd86057cc2f8": "Wakka Maul" +}; + +var samples = 0; +var excluded = 0; +var buckets = { + 30: 0, + 45: 0, + 60: 0, + 75: 0, + 90: 0, + 120: 0, + 'Infinity': 0 +}; + +var years = { + +}; + +_.shuffle(users.slice(0,1000)).forEach(function(user, idx) { + print("Scan " + user.email + ' / ' + user.birthday); + var totalPlayTime = 0; + var sessions = ls.level.sessions.find( + {code: {$exists: 1}, creator: user._id.valueOf()}, + {'created': 1, 'level': 1, playtime: 1, 'state.complete': 1} + ).toArray(); + sessions = _.sortBy(sessions, 'created'); + var success = false; + for ( var i = 0; i < sessions.length; ++i ) { + var s = sessions[i]; + var name = names[s.level.original]; + if ( !name ) { + ++excluded; + return; + } + totalPlayTime += s.playtime; + if ( name == 'Known Enemy' && s.state.complete ) { + success = true; + break; + } + + //print(s.created, name, s.state.complete, totalPlayTime / 60); + } + if ( !success && totalPlayTime < 60 * 90 ) return; + var by = ISODate(user.birthday).getYear(); + if ( !years[by] ) years[by] = 1; + else ++years[by]; + ++samples; + if ( success ) { + for ( var bracket in buckets ) { + if ( Number(bracket) * 60 >= totalPlayTime ) { + buckets[bracket]++; + } + } + } + //print(JSON.stringify(sessions, null, ' ')); + +}); +print("Excluded " + excluded + "\tSample Size:" + samples ); +_.forEach(buckets, function(v,k) { + print(k + 'm\t', (v * 100 / samples) + '%'); +}); + +//print(JSON.stringify(years, null, ' ')); +