mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-12 00:31:21 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
76113fd5ab
9 changed files with 327 additions and 97 deletions
|
@ -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')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,3 +11,6 @@ block content
|
|||
div
|
||||
a(href="/artisans/solution-problems")
|
||||
|Solution Problems
|
||||
div
|
||||
a(href="/artisans/level-concepts")
|
||||
|Level Concept Map
|
24
app/templates/artisans/concept-map-view.jade
Normal file
24
app/templates/artisans/concept-map-view.jade
Normal file
|
@ -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
|
173
app/views/artisans/LevelConceptMap.coffee
Normal file
173
app/views/artisans/LevelConceptMap.coffee
Normal file
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
105
scripts/analytics/analyzeClassCompletionTimes.js
Normal file
105
scripts/analytics/analyzeClassCompletionTimes.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
if ( typeof password === 'undefined' ) {
|
||||
throw "Please specify the coco user password on the commandline as --eval 'password \"<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, ' '));
|
||||
|
Loading…
Reference in a new issue