diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index bbf9c1f3b..a22e43ee3 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -3,10 +3,10 @@ LevelLoader = require 'lib/LevelLoader' GoalManager = require 'lib/world/GoalManager' module.exports = class Simulator + constructor: -> @retryDelayInSeconds = 10 - @taskURL = "/queue/scoring" - + @taskURL = '/queue/scoring' fetchAndSimulateTask: => $.ajax @@ -15,48 +15,45 @@ module.exports = class Simulator error: @handleFetchTaskError success: @setupSimulationAndLoadLevel - cleanupSimulation: -> - - handleFetchTaskError: (errorData) => console.log "There were no games to score. Error: #{JSON.stringify errorData}" console.log "Retrying in #{@retryDelayInSeconds}" - _.delay @fetchAndSimulateTask, @retryDelayInSeconds * 1000 + @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 e - console.log "There was an error in simulation. Trying again in #{retryDelayInSeconds} seconds" - console.log "Error #{e}" - _.delay @fetchAndSimulateTask, @retryDelayInSeconds * 1000 + 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() @@ -66,7 +63,6 @@ module.exports = class Simulator @god.createWorld() Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @ - processResults: (simulationResults) -> taskResults = @formTaskResultsObject simulationResults sendResultsBackToServer taskResults @@ -90,7 +86,7 @@ module.exports = class Simulator @cleanupSimulation() @fetchAndSimulateTask() - + cleanupSimulation: -> formTaskResultsObject: (simulationResults) -> taskResults = diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index a663e7ecc..50f88c192 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -21,6 +21,8 @@ LevelHandler = class LevelHandler extends Handler 'icon' ] + postEditableProperties: ['name'] + getByRelationship: (req, res, args...) -> return @getSession(req, res, args[0]) if args[1] is 'session' 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 @sendNotFoundError(res) - getSession: (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 - } - - # 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) -> + 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, '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()) 'level.original': level.original.toString() 'level.majorVersion': level.version.major - } Feedback.findOne(feedbackQuery).exec (err, doc) => return @sendDatabaseError(res, err) if err return @sendNotFoundError(res) unless doc? @sendSuccess(res, doc) - return - - postEditableProperties: ['name'] module.exports = new LevelHandler() diff --git a/server/levels/sessions/level_session_schema.coffee b/server/levels/sessions/level_session_schema.coffee index 8f1a6beaa..4073d25ef 100644 --- a/server/levels/sessions/level_session_schema.coffee +++ b/server/levels/sessions/level_session_schema.coffee @@ -1,73 +1,138 @@ c = require '../../commons/schemas' -LevelSessionPlayerSchema = c.object { - id: c.objectId(links: [{rel: 'extra', href: "/db/user/{($)}"}]) - time: { type: 'Number' } - changes: { type: 'Number' } -} +LevelSessionPlayerSchema = c.object + id: c.objectId + links: [ + { + 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({}) - majorVersion: {type: 'integer', minimum: 0, default: 0}} + majorVersion: + type: 'integer' + minimum: 0 + default: 0 -LevelSessionSchema = c.object { + +LevelSessionSchema = c.object title: "Session" description: "A single session for a given level." -} + _.extend LevelSessionSchema.properties, # denormalization - creatorName: { type: 'string' } - levelName: { type: 'string' } - levelID: { type: 'string' } - multiplayer: { type: 'boolean' } + creatorName: + type: 'string' + levelName: + 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() level: LevelSessionLevelSchema - screenshot: { type: 'string' } - state: c.object {}, { - complete: { type: 'boolean' } - scripts: c.object {}, { - ended: { type: 'object', additionalProperties: { type: 'number' }} - currentScript: { type: ['null', 'string']} - currentScriptOffset: { type: 'number' }} - selected: { type: ['null', 'string'] } - playing: { type: 'boolean' } - frame: { type: 'number' } - thangs: { type: 'object', additionalProperties: { - title: 'Thang' + + screenshot: + type: 'string' + + state: c.object {}, + complete: + type: 'boolean' + scripts: c.object {}, + ended: + type: 'object' + additionalProperties: + type: 'number' + currentScript: + type: [ + 'null' + 'string' + ] + currentScriptOffset: + type: 'number' + + selected: + type: [ + 'null' + 'string' + ] + playing: + type: 'boolean' + frame: + type: 'number' + thangs: type: 'object' - properties: { - methods: { type: 'object', additionalProperties: { - title: 'Thang Method' - type: 'object' - properties: { - metrics: { type: 'object' } - source: { type: 'string' } - } - }} - } - }} - } + additionalProperties: + title: 'Thang' + type: 'object' + properties: + methods: + type: 'object' + additionalProperties: + title: 'Thang Method' + type: 'object' + properties: + metrics: + type: 'object' + source: + type: 'string' # TODO: specify this more - code: { type: 'object' } + code: + type: 'object' + teamSpells: type: 'object' additionalProperties: type: 'array' + players: + type: 'object' - players: { type: 'object' } - chat: { type: 'array' } + chat: + type: 'array' - meanStrength: {type: 'number', default: 25} - standardDeviation: {type:'number', default:25/3, minimum: 0} - totalScore: {type: 'number', default: 10} - submitted: {type: 'boolean', default: false, index:true} + meanStrength: + type: 'number' + default: 25 + + 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.extendPermissionsProperties LevelSessionSchema, 'level.session'