Merge branch 'master' of https://github.com/codecombat/codecombat
This commit is contained in:
commit
fdf1e21e72
4 changed files with 470 additions and 294 deletions
app
server/levels
237
app/lib/simulator/Simulator.coffee
Normal file
237
app/lib/simulator/Simulator.coffee
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
SuperModel = require 'models/SuperModel'
|
||||||
|
LevelLoader = require 'lib/LevelLoader'
|
||||||
|
GoalManager = require 'lib/world/GoalManager'
|
||||||
|
God = require 'lib/God'
|
||||||
|
|
||||||
|
module.exports = class Simulator
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@retryDelayInSeconds = 10
|
||||||
|
@taskURL = '/queue/scoring'
|
||||||
|
|
||||||
|
fetchAndSimulateTask: =>
|
||||||
|
$.ajax
|
||||||
|
url: @taskURL
|
||||||
|
type: "GET"
|
||||||
|
error: @handleFetchTaskError
|
||||||
|
success: @setupSimulationAndLoadLevel
|
||||||
|
|
||||||
|
handleFetchTaskError: (errorData) =>
|
||||||
|
console.log "There were no games to score. Error: #{JSON.stringify errorData}"
|
||||||
|
console.log "Retrying in #{@retryDelayInSeconds}"
|
||||||
|
|
||||||
|
@simulateAnotherTaskAfterDelay()
|
||||||
|
|
||||||
|
simulateAnotherTaskAfterDelay: =>
|
||||||
|
retryDelayInMilliseconds = @retryDelayInSeconds * 1000
|
||||||
|
_.delay @fetchAndSimulateTask, retryDelayInMilliseconds
|
||||||
|
|
||||||
|
setupSimulationAndLoadLevel: (taskData) =>
|
||||||
|
@task = new SimulationTask(taskData)
|
||||||
|
@supermodel = new SuperModel()
|
||||||
|
@god = new God()
|
||||||
|
|
||||||
|
@levelLoader = new LevelLoader @task.getLevelName(), @supermodel, @task.getFirstSessionID()
|
||||||
|
@levelLoader.once 'loaded-all', @simulateGame
|
||||||
|
|
||||||
|
simulateGame: =>
|
||||||
|
@assignWorldAndLevelFromLevelLoaderAndDestroyIt()
|
||||||
|
@setupGod()
|
||||||
|
|
||||||
|
try
|
||||||
|
@commenceSimulationAndSetupCallback()
|
||||||
|
catch err
|
||||||
|
console.log "There was an error in simulation(#{err}). Trying again in #{retryDelayInSeconds} seconds"
|
||||||
|
@simulateAnotherTaskAfterDelay()
|
||||||
|
|
||||||
|
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
|
||||||
|
@world = @levelLoader.world
|
||||||
|
@level = @levelLoader.level
|
||||||
|
@levelLoader.destroy()
|
||||||
|
|
||||||
|
setupGod: ->
|
||||||
|
@god.level = @level.serialize @supermodel
|
||||||
|
@god.worldClassMap = @world.classMap
|
||||||
|
@setupGoalManager()
|
||||||
|
@setupGodSpells()
|
||||||
|
|
||||||
|
setupGoalManager: ->
|
||||||
|
@god.goalManager = new GoalManager @world
|
||||||
|
@god.goalManager.goals = @fetchGoalsFromWorldNoteChain()
|
||||||
|
@god.goalManager.goalStates = @manuallyGenerateGoalStates()
|
||||||
|
|
||||||
|
commenceSimulationAndSetupCallback: ->
|
||||||
|
@god.createWorld()
|
||||||
|
Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @
|
||||||
|
|
||||||
|
processResults: (simulationResults) ->
|
||||||
|
taskResults = @formTaskResultsObject simulationResults
|
||||||
|
@sendResultsBackToServer taskResults
|
||||||
|
|
||||||
|
sendResultsBackToServer: (results) =>
|
||||||
|
$.ajax
|
||||||
|
url: @taskURL
|
||||||
|
data: results
|
||||||
|
type: "PUT"
|
||||||
|
success: @handleTaskResultsTransferSuccess
|
||||||
|
error: @handleTaskResultsTransferError
|
||||||
|
complete: @cleanupAndSimulateAnotherTask()
|
||||||
|
|
||||||
|
handleTaskResultsTransferSuccess: (result) ->
|
||||||
|
console.log "Task registration result: #{JSON.stringify result}"
|
||||||
|
|
||||||
|
handleTaskResultsTransferError: (error) ->
|
||||||
|
console.log "Task registration error: #{JSON.stringify error}"
|
||||||
|
|
||||||
|
cleanupAndSimulateAnotherTask: =>
|
||||||
|
@cleanupSimulation()
|
||||||
|
@fetchAndSimulateTask()
|
||||||
|
|
||||||
|
cleanupSimulation: ->
|
||||||
|
|
||||||
|
formTaskResultsObject: (simulationResults) ->
|
||||||
|
taskResults =
|
||||||
|
taskID: @task.getTaskID()
|
||||||
|
receiptHandle: @task.getReceiptHandle()
|
||||||
|
calculationTime: 500
|
||||||
|
sessions: []
|
||||||
|
|
||||||
|
for session in @task.getSessions()
|
||||||
|
sessionResult =
|
||||||
|
sessionID: session.sessionID
|
||||||
|
sessionChangedTime: session.sessionChangedTime
|
||||||
|
metrics:
|
||||||
|
rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap()
|
||||||
|
|
||||||
|
taskResults.sessions.push sessionResult
|
||||||
|
|
||||||
|
return taskResults
|
||||||
|
|
||||||
|
calculateSessionRank: (sessionID, goalStates, teamSessionMap) ->
|
||||||
|
humansDestroyed = goalStates["destroy-humans"].status is "success"
|
||||||
|
ogresDestroyed = goalStates["destroy-ogres"].status is "success"
|
||||||
|
if humansDestroyed is ogresDestroyed
|
||||||
|
return 0
|
||||||
|
else if humansDestroyed and teamSessionMap["ogres"] is sessionID
|
||||||
|
return 0
|
||||||
|
else if humansDestroyed and teamSessionMap["ogres"] isnt sessionID
|
||||||
|
return 1
|
||||||
|
else if ogresDestroyed and teamSessionMap["humans"] is sessionID
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
|
||||||
|
fetchGoalsFromWorldNoteChain: -> return @god.goalManager.world.scripts[0].noteChain[0].goals.add
|
||||||
|
|
||||||
|
manuallyGenerateGoalStates: ->
|
||||||
|
goalStates =
|
||||||
|
"destroy-humans":
|
||||||
|
keyFrame: 0
|
||||||
|
killed:
|
||||||
|
"Human Base": false
|
||||||
|
status: "incomplete"
|
||||||
|
"destroy-ogres":
|
||||||
|
keyFrame:0
|
||||||
|
killed:
|
||||||
|
"Ogre Base": false
|
||||||
|
status: "incomplete"
|
||||||
|
|
||||||
|
setupGodSpells: ->
|
||||||
|
@generateSpellsObject()
|
||||||
|
@god.spells = @spells
|
||||||
|
|
||||||
|
generateSpellsObject: ->
|
||||||
|
@currentUserCodeMap = @task.generateSpellKeyToSourceMap()
|
||||||
|
@spells = {}
|
||||||
|
for thang in @level.attributes.thangs
|
||||||
|
continue if @thangIsATemplate thang
|
||||||
|
@generateSpellKeyToSourceMapPropertiesFromThang thang
|
||||||
|
|
||||||
|
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.id, methodName]
|
||||||
|
spellKeyComponents[0] = _.string.slugify spellKeyComponents[0]
|
||||||
|
spellKeyComponents.join '/'
|
||||||
|
|
||||||
|
createSpellAndAssignName: (spellKey, spellName) ->
|
||||||
|
@spells[spellKey] ?= {}
|
||||||
|
@spells[spellKey].name = spellName
|
||||||
|
|
||||||
|
createSpellThang: (thang, method, spellKey) ->
|
||||||
|
@spells[spellKey].thangs ?= {}
|
||||||
|
@spells[spellKey].thangs[thang.id] ?= {}
|
||||||
|
@spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method
|
||||||
|
|
||||||
|
transpileSpell: (thang, spellKey, methodName) ->
|
||||||
|
slugifiedThangID = _.string.slugify thang.id
|
||||||
|
source = @currentUserCodeMap[slugifiedThangID]?[methodName] ? ""
|
||||||
|
@spells[spellKey].thangs[thang.id].aether.transpile source
|
||||||
|
|
||||||
|
createAether: (methodName, method) ->
|
||||||
|
aetherOptions =
|
||||||
|
functionName: methodName
|
||||||
|
protectAPI: false
|
||||||
|
includeFlow: false
|
||||||
|
return new Aether aetherOptions
|
||||||
|
|
||||||
|
class SimulationTask
|
||||||
|
constructor: (@rawData) ->
|
||||||
|
|
||||||
|
getLevelName: ->
|
||||||
|
levelName = @rawData.sessions?[0]?.levelID
|
||||||
|
return levelName if levelName?
|
||||||
|
@throwMalformedTaskError "The level name couldn't be deduced from the task."
|
||||||
|
|
||||||
|
generateTeamToSessionMap: ->
|
||||||
|
teamSessionMap = {}
|
||||||
|
for session in @rawData.sessions
|
||||||
|
@throwMalformedTaskError "Two players share the same team" if teamSessionMap[session.team]?
|
||||||
|
teamSessionMap[session.team] = session.sessionID
|
||||||
|
|
||||||
|
teamSessionMap
|
||||||
|
|
||||||
|
throwMalformedTaskError: (errorString) ->
|
||||||
|
throw new Error "The task was malformed, reason: #{errorString}"
|
||||||
|
|
||||||
|
getFirstSessionID: -> @rawData.sessions[0].sessionID
|
||||||
|
|
||||||
|
getTaskID: -> @rawData.taskID
|
||||||
|
|
||||||
|
getReceiptHandle: -> @rawData.receiptHandle
|
||||||
|
|
||||||
|
getSessions: -> @rawData.sessions
|
||||||
|
|
||||||
|
generateSpellKeyToSourceMap: ->
|
||||||
|
spellKeyToSourceMap = {}
|
||||||
|
|
||||||
|
for session in @rawData.sessions
|
||||||
|
teamSpells = session.teamSpells[session.team]
|
||||||
|
_.merge spellKeyToSourceMap, _.pick(session.code, teamSpells)
|
||||||
|
|
||||||
|
commonSpells = session.teamSpells["common"]
|
||||||
|
_.merge spellKeyToSourceMap, _.pick(session.code, commonSpells) if commonSpells?
|
||||||
|
|
||||||
|
spellKeyToSourceMap
|
||||||
|
|
|
@ -2,10 +2,7 @@ View = require 'views/kinds/RootView'
|
||||||
template = require 'templates/home'
|
template = require 'templates/home'
|
||||||
WizardSprite = require 'lib/surface/WizardSprite'
|
WizardSprite = require 'lib/surface/WizardSprite'
|
||||||
ThangType = require 'models/ThangType'
|
ThangType = require 'models/ThangType'
|
||||||
LevelLoader = require 'lib/LevelLoader'
|
Simulator = require 'lib/simulator/Simulator'
|
||||||
God = require 'lib/God'
|
|
||||||
|
|
||||||
GoalManager = require 'lib/world/GoalManager'
|
|
||||||
|
|
||||||
module.exports = class HomeView extends View
|
module.exports = class HomeView extends View
|
||||||
id: 'home-view'
|
id: 'home-view'
|
||||||
|
@ -105,156 +102,5 @@ module.exports = class HomeView extends View
|
||||||
@wizardSprite?.destroy()
|
@wizardSprite?.destroy()
|
||||||
|
|
||||||
onSimulateButtonClick: (e) =>
|
onSimulateButtonClick: (e) =>
|
||||||
@alreadyPostedResults = false
|
simulator = new Simulator()
|
||||||
console.log "Simulating world!"
|
simulator.fetchAndSimulateTask()
|
||||||
$.ajax
|
|
||||||
url: "/queue/scoring"
|
|
||||||
type: "GET"
|
|
||||||
error: (data) =>
|
|
||||||
console.log "There are no games to score. Error: #{JSON.stringify data}"
|
|
||||||
console.log "Retrying in ten seconds..."
|
|
||||||
_.delay @onSimulateButtonClick, 10000
|
|
||||||
success: (data) =>
|
|
||||||
console.log data
|
|
||||||
levelName = data.sessions[0].levelID
|
|
||||||
#TODO: Refactor. So much refactor.
|
|
||||||
@taskData = data
|
|
||||||
@teamSessionMap = @generateTeamSessionMap data
|
|
||||||
world = {}
|
|
||||||
god = new God()
|
|
||||||
levelLoader = new LevelLoader(levelName, @supermodel, data.sessions[0].sessionID)
|
|
||||||
levelLoader.once 'loaded-all', =>
|
|
||||||
world = levelLoader.world
|
|
||||||
level = levelLoader.level
|
|
||||||
levelLoader.destroy()
|
|
||||||
god.level = level.serialize @supermodel
|
|
||||||
god.worldClassMap = world.classMap
|
|
||||||
god.goalManager = new GoalManager(world)
|
|
||||||
#move goals in here
|
|
||||||
goalsToAdd = god.goalManager.world.scripts[0].noteChain[0].goals.add
|
|
||||||
god.goalManager.goals = goalsToAdd
|
|
||||||
god.goalManager.goalStates =
|
|
||||||
"destroy-humans":
|
|
||||||
keyFrame: 0
|
|
||||||
killed:
|
|
||||||
"Human Base": false
|
|
||||||
status: "incomplete"
|
|
||||||
"destroy-ogres":
|
|
||||||
keyFrame:0
|
|
||||||
killed:
|
|
||||||
"Ogre Base": false
|
|
||||||
status: "incomplete"
|
|
||||||
god.spells = @filterProgrammableComponents level.attributes.thangs, @generateSpellToSourceMap data.sessions
|
|
||||||
god.createWorld()
|
|
||||||
@god = god
|
|
||||||
Backbone.Mediator.subscribeOnce 'god:new-world-created', @onWorldCreated, @
|
|
||||||
|
|
||||||
onWorldCreated: (data) ->
|
|
||||||
return if @alreadyPostedResults
|
|
||||||
taskResults = @translateGoalStatesIntoTaskResults data.goalStates
|
|
||||||
@god?.destroy()
|
|
||||||
|
|
||||||
$.ajax
|
|
||||||
url: "/queue/scoring"
|
|
||||||
data: taskResults
|
|
||||||
type: 'PUT'
|
|
||||||
success: (result) =>
|
|
||||||
console.log "TASK REGISTRATION RESULT:#{JSON.stringify result}"
|
|
||||||
error: (error) =>
|
|
||||||
console.log "TASK REGISTRATION ERROR:#{JSON.stringify error}"
|
|
||||||
complete: (result) =>
|
|
||||||
@alreadyPostedResults = true
|
|
||||||
@onSimulateButtonClick()
|
|
||||||
|
|
||||||
|
|
||||||
translateGoalStatesIntoTaskResults: (goalStates) =>
|
|
||||||
taskResults = {}
|
|
||||||
taskResults =
|
|
||||||
taskID: @taskData.taskID
|
|
||||||
receiptHandle: @taskData.receiptHandle
|
|
||||||
calculationTime: 500
|
|
||||||
sessions: []
|
|
||||||
|
|
||||||
for session in @taskData.sessions
|
|
||||||
sessionResult =
|
|
||||||
sessionID: session.sessionID
|
|
||||||
sessionChangedTime: session.sessionChangedTime
|
|
||||||
metrics:
|
|
||||||
rank: @calculateSessionRank session.sessionID, goalStates
|
|
||||||
taskResults.sessions.push sessionResult
|
|
||||||
taskResults
|
|
||||||
|
|
||||||
calculateSessionRank: (sessionID, goalStates) ->
|
|
||||||
humansDestroyed = goalStates["destroy-humans"].status is "success"
|
|
||||||
ogresDestroyed = goalStates["destroy-ogres"].status is "success"
|
|
||||||
console.log "Humans destroyed:#{humansDestroyed}"
|
|
||||||
console.log "Ogres destroyed:#{ogresDestroyed}"
|
|
||||||
console.log "Team Session Map: #{JSON.stringify @teamSessionMap}"
|
|
||||||
if humansDestroyed is ogresDestroyed
|
|
||||||
return 0
|
|
||||||
else if humansDestroyed and @teamSessionMap["ogres"] is sessionID
|
|
||||||
return 0
|
|
||||||
else if humansDestroyed and @teamSessionMap["ogres"] isnt sessionID
|
|
||||||
return 1
|
|
||||||
else if ogresDestroyed and @teamSessionMap["humans"] is sessionID
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
generateTeamSessionMap: (task) ->
|
|
||||||
teamSessionMap = {}
|
|
||||||
for session in @taskData.sessions
|
|
||||||
teamSessionMap[session.team] = session.sessionID
|
|
||||||
teamSessionMap
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
filterProgrammableComponents: (thangs, spellToSourceMap) =>
|
|
||||||
spells = {}
|
|
||||||
for thang in thangs
|
|
||||||
isTemplate = false
|
|
||||||
for component in thang.components
|
|
||||||
if component.config? and _.has component.config,'programmableMethods'
|
|
||||||
for methodName, method of component.config.programmableMethods
|
|
||||||
if typeof method is 'string'
|
|
||||||
isTemplate = true
|
|
||||||
break
|
|
||||||
|
|
||||||
pathComponents = [thang.id,methodName]
|
|
||||||
pathComponents[0] = _.string.slugify pathComponents[0]
|
|
||||||
spellKey = pathComponents.join '/'
|
|
||||||
spells[spellKey] ?= {}
|
|
||||||
spells[spellKey].thangs ?= {}
|
|
||||||
spells[spellKey].name = methodName
|
|
||||||
thangID = _.string.slugify thang.id
|
|
||||||
spells[spellKey].thangs[thang.id] ?= {}
|
|
||||||
spells[spellKey].thangs[thang.id].aether = @createAether methodName, method
|
|
||||||
if spellToSourceMap[thangID]? then source = spellToSourceMap[thangID][methodName] else source = ""
|
|
||||||
spells[spellKey].thangs[thang.id].aether.transpile source
|
|
||||||
if isTemplate
|
|
||||||
break
|
|
||||||
|
|
||||||
spells
|
|
||||||
|
|
||||||
createAether : (methodName, method) ->
|
|
||||||
aetherOptions =
|
|
||||||
functionName: methodName
|
|
||||||
protectAPI: false
|
|
||||||
includeFlow: false
|
|
||||||
return new Aether aetherOptions
|
|
||||||
|
|
||||||
generateSpellToSourceMap: (sessions) ->
|
|
||||||
spellKeyToSourceMap = {}
|
|
||||||
spellSources = {}
|
|
||||||
for session in sessions
|
|
||||||
teamSpells = session.teamSpells[session.team]
|
|
||||||
_.merge spellSources, _.pick(session.code, teamSpells)
|
|
||||||
|
|
||||||
#merge common ones, this overwrites until the last session
|
|
||||||
commonSpells = session.teamSpells["common"]
|
|
||||||
if commonSpells?
|
|
||||||
_.merge spellSources, _.pick(session.code, commonSpells)
|
|
||||||
|
|
||||||
spellSources
|
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ LevelHandler = class LevelHandler extends Handler
|
||||||
'icon'
|
'icon'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
postEditableProperties: ['name']
|
||||||
|
|
||||||
getByRelationship: (req, res, args...) ->
|
getByRelationship: (req, res, args...) ->
|
||||||
return @getSession(req, res, args[0]) if args[1] is 'session'
|
return @getSession(req, res, args[0]) if args[1] is 'session'
|
||||||
return @getLeaderboard(req, res, args[0]) if args[1] is 'leaderboard'
|
return @getLeaderboard(req, res, args[0]) if args[1] is 'leaderboard'
|
||||||
|
@ -28,108 +30,134 @@ LevelHandler = class LevelHandler extends Handler
|
||||||
return @getFeedback(req, res, args[0]) if args[1] is 'feedback'
|
return @getFeedback(req, res, args[0]) if args[1] is 'feedback'
|
||||||
return @sendNotFoundError(res)
|
return @sendNotFoundError(res)
|
||||||
|
|
||||||
getSession: (req, res, id) ->
|
fetchLevelByIDAndHandleErrors: (id, req, res, callback) ->
|
||||||
@getDocumentForIdOrSlug id, (err, level) =>
|
|
||||||
return @sendDatabaseError(res, err) if err
|
|
||||||
return @sendNotFoundError(res) unless level?
|
|
||||||
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, level)
|
|
||||||
|
|
||||||
sessionQuery = {
|
|
||||||
level: {original: level.original.toString(), majorVersion: level.version.major}
|
|
||||||
creator: req.user.id
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: generalize this for levels that need teams
|
|
||||||
team = req.query.team
|
|
||||||
team ?= 'humans' if level.name is 'Project DotA'
|
|
||||||
sessionQuery.team = team if team
|
|
||||||
|
|
||||||
Session.findOne(sessionQuery).exec (err, doc) =>
|
|
||||||
return @sendDatabaseError(res, err) if err
|
|
||||||
if doc
|
|
||||||
@sendSuccess(res, doc)
|
|
||||||
return
|
|
||||||
|
|
||||||
initVals = sessionQuery
|
|
||||||
initVals.state = {complete:false, scripts:{currentScript:null}} # will not save empty objects
|
|
||||||
initVals.permissions = [{target:req.user.id, access:'owner'}, {target:'public', access:'write'}]
|
|
||||||
initVals.team = team if team
|
|
||||||
session = new Session(initVals)
|
|
||||||
session.save (err) =>
|
|
||||||
return @sendDatabaseError(res, err) if err
|
|
||||||
@sendSuccess(res, @formatEntity(req, session))
|
|
||||||
# TODO: tying things like @formatEntity and saveChangesToDocument don't make sense
|
|
||||||
# associated with the handler, because the handler might return a different type
|
|
||||||
# of model, like in this case. Refactor to move that logic to the model instead.
|
|
||||||
|
|
||||||
getAllSessions: (req, res, id) ->
|
|
||||||
@getDocumentForIdOrSlug id, (err, level) =>
|
|
||||||
return @sendDatabaseError(res, err) if err
|
|
||||||
return @sendNotFoundError(res) unless level?
|
|
||||||
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, level)
|
|
||||||
|
|
||||||
sessionQuery = {
|
|
||||||
level: {original: level.original.toString(), majorVersion: level.version.major}
|
|
||||||
#creator: req.user.id
|
|
||||||
submitted: true
|
|
||||||
}
|
|
||||||
Session.find sessionQuery, '_id totalScore submitted team creatorName', (err, results) =>
|
|
||||||
return @sendDatabaseError(res, err) if err
|
|
||||||
res.send(results)
|
|
||||||
res.end()
|
|
||||||
|
|
||||||
getLeaderboard: (req, res, id) ->
|
|
||||||
# stub handler
|
|
||||||
# [original, version] = id.split('.')
|
|
||||||
# version = parseInt version
|
|
||||||
# console.log 'get leaderboard for', original, version, req.query
|
|
||||||
[original, version] = id.split '.'
|
|
||||||
version = parseInt(version) or 0
|
|
||||||
|
|
||||||
req.query.order = parseInt(req.query.order) or -1
|
|
||||||
req.query.scoreOffset = parseInt(req.query.scoreOffset) ? 100000
|
|
||||||
req.query.team ?= 'humans'
|
|
||||||
req.query.limit ?= parseInt(req.query.limit) or 20
|
|
||||||
|
|
||||||
|
|
||||||
if parseInt(req.query.order) is 1
|
|
||||||
scoreQuery = {"$gte":parseFloat req.query.scoreOffset}
|
|
||||||
else
|
|
||||||
scoreQuery = {"$lte": parseFloat req.query.scoreOffset}
|
|
||||||
|
|
||||||
sessionsQuery =
|
|
||||||
level: {original: original, majorVersion: version}
|
|
||||||
team: req.query.team
|
|
||||||
totalScore: scoreQuery
|
|
||||||
submitted: true
|
|
||||||
|
|
||||||
sortObject =
|
|
||||||
"totalScore": parseInt(req.query.order)
|
|
||||||
query = Session.find(sessionsQuery).sort(sortObject).limit(parseInt req.query.limit)
|
|
||||||
Session.find sessionsQuery, 'totalScore creatorName creator', (err, resultSessions) =>
|
|
||||||
return @sendDatabaseError(res, err) if err
|
|
||||||
if resultSessions
|
|
||||||
return @sendSuccess res, resultSessions
|
|
||||||
res.send([])
|
|
||||||
|
|
||||||
getFeedback: (req, res, id) ->
|
|
||||||
@getDocumentForIdOrSlug id, (err, level) =>
|
@getDocumentForIdOrSlug id, (err, level) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendNotFoundError(res) unless level?
|
return @sendNotFoundError(res) unless level?
|
||||||
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, level, 'get')
|
return @sendUnauthorizedError(res) unless @hasAccessToDocument(req, level, 'get')
|
||||||
|
callback err, level
|
||||||
|
|
||||||
feedbackQuery = {
|
getSession: (req, res, id) ->
|
||||||
|
@fetchLevelByIDAndHandleErrors id, req, res, (err, level) =>
|
||||||
|
sessionQuery =
|
||||||
|
level:
|
||||||
|
original: level.original.toString()
|
||||||
|
majorVersion: level.version.major
|
||||||
|
creator: req.user.id
|
||||||
|
|
||||||
|
# TODO: generalize this for levels that need teams
|
||||||
|
if req.query.team?
|
||||||
|
sessionQuery.team = req.query.team
|
||||||
|
else if level.name is 'Project DotA'
|
||||||
|
sessionQuery.team = 'humans'
|
||||||
|
|
||||||
|
Session.findOne(sessionQuery).exec (err, doc) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
return @sendSuccess(res, doc) if doc?
|
||||||
|
@createAndSaveNewSession sessionQuery, req, res
|
||||||
|
|
||||||
|
|
||||||
|
createAndSaveNewSession: (sessionQuery, req, res) =>
|
||||||
|
initVals = sessionQuery
|
||||||
|
|
||||||
|
initVals.state =
|
||||||
|
complete:false
|
||||||
|
scripts:
|
||||||
|
currentScript:null # will not save empty objects
|
||||||
|
|
||||||
|
initVals.permissions = [
|
||||||
|
{
|
||||||
|
target:req.user.id
|
||||||
|
access:'owner'
|
||||||
|
}
|
||||||
|
{
|
||||||
|
target:'public'
|
||||||
|
access:'write'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
session = new Session(initVals)
|
||||||
|
|
||||||
|
session.save (err) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
@sendSuccess(res, @formatEntity(req, session))
|
||||||
|
# TODO: tying things like @formatEntity and saveChangesToDocument don't make sense
|
||||||
|
# associated with the handler, because the handler might return a different type
|
||||||
|
# of model, like in this case. Refactor to move that logic to the model instead.
|
||||||
|
|
||||||
|
getAllSessions: (req, res, id) ->
|
||||||
|
@fetchLevelByIDAndHandleErrors id, req, res, (err, level) =>
|
||||||
|
sessionQuery =
|
||||||
|
level:
|
||||||
|
original: level.original.toString()
|
||||||
|
majorVersion: level.version.major
|
||||||
|
submitted: true
|
||||||
|
|
||||||
|
propertiesToReturn = [
|
||||||
|
'_id'
|
||||||
|
'totalScore'
|
||||||
|
'submitted'
|
||||||
|
'team'
|
||||||
|
'creatorName'
|
||||||
|
]
|
||||||
|
|
||||||
|
query = Session
|
||||||
|
.find(sessionQuery)
|
||||||
|
.select(propertiesToReturn.join ' ')
|
||||||
|
|
||||||
|
query.exec (err, results) =>
|
||||||
|
if err then @sendDatabaseError(res, err) else @sendSuccess res, results
|
||||||
|
|
||||||
|
getLeaderboard: (req, res, id) ->
|
||||||
|
@validateLeaderboardRequestParameters req
|
||||||
|
[original, version] = id.split '.'
|
||||||
|
version = parseInt(version) ? 0
|
||||||
|
scoreQuery = {}
|
||||||
|
scoreQuery[if req.query.order is 1 then "$gte" else "$lte"] = req.query.scoreOffset
|
||||||
|
|
||||||
|
sessionsQueryParameters =
|
||||||
|
level:
|
||||||
|
original: original
|
||||||
|
majorVersion: version
|
||||||
|
team: req.query.team
|
||||||
|
totalScore: scoreQuery
|
||||||
|
submitted: true
|
||||||
|
|
||||||
|
sortParameters =
|
||||||
|
"totalScore": req.query.order
|
||||||
|
|
||||||
|
selectProperties = [
|
||||||
|
'totalScore'
|
||||||
|
'creatorName'
|
||||||
|
'creator'
|
||||||
|
]
|
||||||
|
|
||||||
|
query = Session
|
||||||
|
.find(sessionsQueryParameters)
|
||||||
|
.limit(req.query.limit)
|
||||||
|
.sort(sortParameters)
|
||||||
|
.select(selectProperties.join ' ')
|
||||||
|
|
||||||
|
query.exec (err, resultSessions) =>
|
||||||
|
return @sendDatabaseError(res, err) if err
|
||||||
|
resultSessions ?= []
|
||||||
|
@sendSuccess res, resultSessions
|
||||||
|
|
||||||
|
validateLeaderboardRequestParameters: (req) ->
|
||||||
|
req.query.order = parseInt(req.query.order) ? -1
|
||||||
|
req.query.scoreOffset = parseFloat(req.query.scoreOffset) ? 100000
|
||||||
|
req.query.team ?= 'humans'
|
||||||
|
req.query.limit = parseInt(req.query.limit) ? 20
|
||||||
|
|
||||||
|
getFeedback: (req, res, id) ->
|
||||||
|
@fetchLevelByIDAndHandleErrors id, req, res, (err, level) =>
|
||||||
|
feedbackQuery =
|
||||||
creator: mongoose.Types.ObjectId(req.user.id.toString())
|
creator: mongoose.Types.ObjectId(req.user.id.toString())
|
||||||
'level.original': level.original.toString()
|
'level.original': level.original.toString()
|
||||||
'level.majorVersion': level.version.major
|
'level.majorVersion': level.version.major
|
||||||
}
|
|
||||||
|
|
||||||
Feedback.findOne(feedbackQuery).exec (err, doc) =>
|
Feedback.findOne(feedbackQuery).exec (err, doc) =>
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendNotFoundError(res) unless doc?
|
return @sendNotFoundError(res) unless doc?
|
||||||
@sendSuccess(res, doc)
|
@sendSuccess(res, doc)
|
||||||
return
|
|
||||||
|
|
||||||
postEditableProperties: ['name']
|
|
||||||
|
|
||||||
module.exports = new LevelHandler()
|
module.exports = new LevelHandler()
|
||||||
|
|
|
@ -1,73 +1,138 @@
|
||||||
c = require '../../commons/schemas'
|
c = require '../../commons/schemas'
|
||||||
|
|
||||||
LevelSessionPlayerSchema = c.object {
|
LevelSessionPlayerSchema = c.object
|
||||||
id: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])
|
id: c.objectId
|
||||||
time: { type: 'Number' }
|
links: [
|
||||||
changes: { type: 'Number' }
|
{
|
||||||
}
|
rel: 'extra'
|
||||||
|
href: "/db/user/{($)}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
time:
|
||||||
|
type: 'Number'
|
||||||
|
changes:
|
||||||
|
type: 'Number'
|
||||||
|
|
||||||
LevelSessionLevelSchema = c.object {required: ['original', 'majorVersion']}, {
|
|
||||||
|
LevelSessionLevelSchema = c.object {required: ['original', 'majorVersion']},
|
||||||
original: c.objectId({})
|
original: c.objectId({})
|
||||||
majorVersion: {type: 'integer', minimum: 0, default: 0}}
|
majorVersion:
|
||||||
|
type: 'integer'
|
||||||
|
minimum: 0
|
||||||
|
default: 0
|
||||||
|
|
||||||
LevelSessionSchema = c.object {
|
|
||||||
|
LevelSessionSchema = c.object
|
||||||
title: "Session"
|
title: "Session"
|
||||||
description: "A single session for a given level."
|
description: "A single session for a given level."
|
||||||
}
|
|
||||||
|
|
||||||
_.extend LevelSessionSchema.properties,
|
_.extend LevelSessionSchema.properties,
|
||||||
# denormalization
|
# denormalization
|
||||||
creatorName: { type: 'string' }
|
creatorName:
|
||||||
levelName: { type: 'string' }
|
type: 'string'
|
||||||
levelID: { type: 'string' }
|
levelName:
|
||||||
multiplayer: { type: 'boolean' }
|
type: 'string'
|
||||||
|
levelID:
|
||||||
|
type: 'string'
|
||||||
|
multiplayer:
|
||||||
|
type: 'boolean'
|
||||||
|
creator: c.objectId
|
||||||
|
links:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
rel: 'extra'
|
||||||
|
href: "/db/user/{($)}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
created: c.date
|
||||||
|
title: 'Created'
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
|
changed: c.date
|
||||||
|
title: 'Changed'
|
||||||
|
readOnly: true
|
||||||
|
|
||||||
creator: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}])
|
|
||||||
created: c.date( { title: 'Created', readOnly: true })
|
|
||||||
changed: c.date( { title: 'Changed', readOnly: true })
|
|
||||||
team: c.shortString()
|
team: c.shortString()
|
||||||
level: LevelSessionLevelSchema
|
level: LevelSessionLevelSchema
|
||||||
screenshot: { type: 'string' }
|
|
||||||
state: c.object {}, {
|
screenshot:
|
||||||
complete: { type: 'boolean' }
|
type: 'string'
|
||||||
scripts: c.object {}, {
|
|
||||||
ended: { type: 'object', additionalProperties: { type: 'number' }}
|
state: c.object {},
|
||||||
currentScript: { type: ['null', 'string']}
|
complete:
|
||||||
currentScriptOffset: { type: 'number' }}
|
type: 'boolean'
|
||||||
selected: { type: ['null', 'string'] }
|
scripts: c.object {},
|
||||||
playing: { type: 'boolean' }
|
ended:
|
||||||
frame: { type: 'number' }
|
type: 'object'
|
||||||
thangs: { type: 'object', additionalProperties: {
|
additionalProperties:
|
||||||
title: 'Thang'
|
type: 'number'
|
||||||
|
currentScript:
|
||||||
|
type: [
|
||||||
|
'null'
|
||||||
|
'string'
|
||||||
|
]
|
||||||
|
currentScriptOffset:
|
||||||
|
type: 'number'
|
||||||
|
|
||||||
|
selected:
|
||||||
|
type: [
|
||||||
|
'null'
|
||||||
|
'string'
|
||||||
|
]
|
||||||
|
playing:
|
||||||
|
type: 'boolean'
|
||||||
|
frame:
|
||||||
|
type: 'number'
|
||||||
|
thangs:
|
||||||
type: 'object'
|
type: 'object'
|
||||||
properties: {
|
additionalProperties:
|
||||||
methods: { type: 'object', additionalProperties: {
|
title: 'Thang'
|
||||||
title: 'Thang Method'
|
type: 'object'
|
||||||
type: 'object'
|
properties:
|
||||||
properties: {
|
methods:
|
||||||
metrics: { type: 'object' }
|
type: 'object'
|
||||||
source: { type: 'string' }
|
additionalProperties:
|
||||||
}
|
title: 'Thang Method'
|
||||||
}}
|
type: 'object'
|
||||||
}
|
properties:
|
||||||
}}
|
metrics:
|
||||||
}
|
type: 'object'
|
||||||
|
source:
|
||||||
|
type: 'string'
|
||||||
|
|
||||||
# TODO: specify this more
|
# TODO: specify this more
|
||||||
code: { type: 'object' }
|
code:
|
||||||
|
type: 'object'
|
||||||
|
|
||||||
teamSpells:
|
teamSpells:
|
||||||
type: 'object'
|
type: 'object'
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: 'array'
|
type: 'array'
|
||||||
|
|
||||||
|
players:
|
||||||
|
type: 'object'
|
||||||
|
|
||||||
players: { type: 'object' }
|
chat:
|
||||||
chat: { type: 'array' }
|
type: 'array'
|
||||||
|
|
||||||
meanStrength: {type: 'number', default: 25}
|
meanStrength:
|
||||||
standardDeviation: {type:'number', default:25/3, minimum: 0}
|
type: 'number'
|
||||||
totalScore: {type: 'number', default: 10}
|
default: 25
|
||||||
submitted: {type: 'boolean', default: false, index:true}
|
|
||||||
|
standardDeviation:
|
||||||
|
type:'number'
|
||||||
|
default:25/3
|
||||||
|
minimum: 0
|
||||||
|
|
||||||
|
totalScore:
|
||||||
|
type: 'number'
|
||||||
|
default: 10
|
||||||
|
|
||||||
|
submitted:
|
||||||
|
type: 'boolean'
|
||||||
|
default: false
|
||||||
|
index:true
|
||||||
|
|
||||||
c.extendBasicProperties LevelSessionSchema, 'level.session'
|
c.extendBasicProperties LevelSessionSchema, 'level.session'
|
||||||
c.extendPermissionsProperties LevelSessionSchema, 'level.session'
|
c.extendPermissionsProperties LevelSessionSchema, 'level.session'
|
||||||
|
|
Reference in a new issue