This commit is contained in:
Nick Winter 2014-02-14 16:52:54 -08:00
commit fdf1e21e72
4 changed files with 470 additions and 294 deletions

View 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

View file

@ -2,10 +2,7 @@ View = require 'views/kinds/RootView'
template = require 'templates/home'
WizardSprite = require 'lib/surface/WizardSprite'
ThangType = require 'models/ThangType'
LevelLoader = require 'lib/LevelLoader'
God = require 'lib/God'
GoalManager = require 'lib/world/GoalManager'
Simulator = require 'lib/simulator/Simulator'
module.exports = class HomeView extends View
id: 'home-view'
@ -105,156 +102,5 @@ module.exports = class HomeView extends View
@wizardSprite?.destroy()
onSimulateButtonClick: (e) =>
@alreadyPostedResults = false
console.log "Simulating world!"
$.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
simulator = new Simulator()
simulator.fetchAndSimulateTask()

View file

@ -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()

View file

@ -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'