codecombat/app/lib/simulator/Simulator.coffee

357 lines
12 KiB
CoffeeScript
Raw Normal View History

2014-02-14 13:49:16 -05:00
SuperModel = require 'models/SuperModel'
CocoClass = require 'lib/CocoClass'
2014-02-14 13:49:16 -05:00
LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
God = require 'lib/God'
2014-02-14 13:49:16 -05:00
2014-05-08 14:43:00 -04:00
Aether.addGlobal 'Vector', require 'lib/world/vector'
Aether.addGlobal '_', _
module.exports = class Simulator extends CocoClass
constructor: (@options) ->
@options ?= {}
_.extend @, Backbone.Events
@trigger 'statusUpdate', 'Starting simulation!'
2014-02-14 13:49:16 -05:00
@retryDelayInSeconds = 10
@taskURL = '/queue/scoring'
2014-05-05 23:07:34 -04:00
@simulatedByYou = 0
@god = new God maxAngels: 1, workerCode: @options.workerCode, headless: true # Start loading worker.
destroy: ->
@off()
@cleanupSimulation()
@god?.destroy()
super()
2014-02-14 13:49:16 -05:00
fetchAndSimulateTask: =>
2014-03-16 23:36:02 -04:00
return if @destroyed
2014-05-05 23:07:34 -04:00
if @options.headlessClient
2014-05-05 23:07:34 -04:00
if @dumpThisTime # The first heapdump would be useless to find leaks.
console.log "Writing snapshot."
@options.heapdump.writeSnapshot()
@dumpThisTime = true if @options.heapdump
2014-05-05 23:07:34 -04:00
if @options.testing
_.delay @setupSimulationAndLoadLevel, 0, @options.testFile, "Testing...", status: 400
2014-05-05 23:07:34 -04:00
return
@trigger 'statusUpdate', 'Fetching simulation data!'
2014-02-14 13:49:16 -05:00
$.ajax
url: @taskURL
type: "GET"
parse: true
2014-02-14 13:49:16 -05:00
error: @handleFetchTaskError
success: @setupSimulationAndLoadLevel
handleFetchTaskError: (errorData) =>
console.error "There was a horrible Error: #{JSON.stringify errorData}"
@trigger 'statusUpdate', 'There was an error fetching games to simulate. Retrying in 10 seconds.'
@simulateAnotherTaskAfterDelay()
2014-02-14 13:49:16 -05:00
handleNoGamesResponse: ->
2014-05-05 23:07:34 -04:00
info = 'There were no games to simulate--all simulations are done or in process. Retrying in 10 seconds.'
console.log info
@trigger 'statusUpdate', info
@simulateAnotherTaskAfterDelay()
simulateAnotherTaskAfterDelay: =>
console.log "Retrying in #{@retryDelayInSeconds}"
retryDelayInMilliseconds = @retryDelayInSeconds * 1000
_.delay @fetchAndSimulateTask, retryDelayInMilliseconds
2014-02-14 13:49:16 -05:00
setupSimulationAndLoadLevel: (taskData, textStatus, jqXHR) =>
return @handleNoGamesResponse() if jqXHR.status is 204
@trigger 'statusUpdate', 'Setting up simulation!'
2014-02-14 13:49:16 -05:00
@task = new SimulationTask(taskData)
2014-03-26 15:12:43 -04:00
try
levelID = @task.getLevelName()
catch err
console.error err
@trigger 'statusUpdate', "Error simulating game: #{err}. Trying another game in #{@retryDelayInSeconds} seconds."
@simulateAnotherTaskAfterDelay()
return
@supermodel ?= new SuperModel()
@supermodel.resetProgress()
2014-03-26 15:12:43 -04:00
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true
if @supermodel.finished()
@simulateGame()
else
@listenToOnce @supermodel, 'loaded-all', @simulateGame
2014-02-14 13:49:16 -05:00
2014-03-24 12:58:34 -04:00
simulateGame: ->
return if @destroyed
2014-05-05 23:07:34 -04:00
info = 'All resources loaded, simulating!'
console.log info
@trigger 'statusUpdate', info, @task.getSessions()
2014-02-14 13:49:16 -05:00
@assignWorldAndLevelFromLevelLoaderAndDestroyIt()
@setupGod()
2014-02-14 13:49:16 -05:00
try
@commenceSimulationAndSetupCallback()
catch err
2014-02-15 18:45:53 -05:00
console.log "There was an error in simulation(#{err}). Trying again in #{@retryDelayInSeconds} seconds"
@simulateAnotherTaskAfterDelay()
2014-02-14 13:49:16 -05:00
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
@world = @levelLoader.world
@level = @levelLoader.level
@levelLoader.destroy()
2014-02-15 18:44:45 -05:00
@levelLoader = null
2014-02-14 13:49:16 -05:00
setupGod: ->
@god.setLevel @level.serialize @supermodel
@god.setLevelSessionIDs (session.id for session in @task.getSessions())
2014-05-05 23:07:34 -04:00
@god.setWorldClassMap @world.classMap
@god.setGoalManager new GoalManager(@world, @level.get 'goals')
2014-02-14 13:49:16 -05:00
commenceSimulationAndSetupCallback: ->
@god.createWorld @generateSpellsObject()
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @
Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @
2014-02-14 13:49:16 -05:00
2014-05-05 23:07:34 -04:00
#Search for leaks, headless-client only.
if @options.headlessClient and @options.leakTest and not @memwatch?
2014-05-05 23:07:34 -04:00
leakcount = 0
maxleakcount = 0
console.log "Setting leak callbacks."
@memwatch = require 'memwatch'
@memwatch.on 'leak', (info) =>
console.warn "LEAK!!\n" + JSON.stringify(info)
unless @hd?
if (leakcount++ is maxleakcount)
@hd = new @memwatch.HeapDiff()
@memwatch.on 'stats', (stats) =>
console.warn "stats callback: " + stats
diff = @hd.end()
console.warn "HeapDiff:\n" + JSON.stringify(diff)
if @options.exitOnLeak
2014-05-05 23:07:34 -04:00
console.warn "Exiting because of Leak."
process.exit()
@hd = new @memwatch.HeapDiff()
onInfiniteLoop: ->
console.warn "Skipping infinitely looping game."
2014-03-26 15:12:43 -04:00
@trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds."
2014-03-26 15:34:45 -04:00
_.delay @cleanupAndSimulateAnotherTask, @retryDelayInSeconds * 1000
2014-02-14 13:49:16 -05:00
processResults: (simulationResults) ->
taskResults = @formTaskResultsObject simulationResults
@sendResultsBackToServer taskResults
2014-02-14 13:49:16 -05:00
sendResultsBackToServer: (results) ->
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
console.log "Sending result back to server!", results
if @options.headlessClient and @options.testing
2014-05-05 23:07:34 -04:00
return @fetchAndSimulateTask()
2014-02-14 13:49:16 -05:00
$.ajax
2014-02-18 14:46:14 -05:00
url: "/queue/scoring"
2014-02-14 13:49:16 -05:00
data: results
type: "PUT"
parse: true
2014-02-14 13:49:16 -05:00
success: @handleTaskResultsTransferSuccess
error: @handleTaskResultsTransferError
2014-02-14 19:53:34 -05:00
complete: @cleanupAndSimulateAnotherTask
2014-02-14 13:49:16 -05:00
handleTaskResultsTransferSuccess: (result) =>
2014-02-14 13:49:16 -05:00
console.log "Task registration result: #{JSON.stringify result}"
@trigger 'statusUpdate', 'Results were successfully sent back to server!'
2014-05-05 23:07:34 -04:00
console.log "Simulated by you: " + @simulatedByYou
@simulatedByYou++
unless @options.headlessClient
2014-05-05 23:07:34 -04:00
simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1
$('#simulated-by-you').text(simulatedBy)
2014-02-14 13:49:16 -05:00
handleTaskResultsTransferError: (error) =>
@trigger 'statusUpdate', 'There was an error sending the results back to the server.'
2014-02-14 13:49:16 -05:00
console.log "Task registration error: #{JSON.stringify error}"
cleanupAndSimulateAnotherTask: =>
@cleanupSimulation()
2014-02-14 13:49:16 -05:00
@fetchAndSimulateTask()
cleanupSimulation: ->
@world = null
@level = null
2014-02-14 13:49:16 -05:00
formTaskResultsObject: (simulationResults) ->
taskResults =
taskID: @task.getTaskID()
receiptHandle: @task.getReceiptHandle()
2014-02-26 15:14:02 -05:00
originalSessionID: @task.getFirstSessionID()
originalSessionRank: -1
2014-02-14 13:49:16 -05:00
calculationTime: 500
sessions: []
for session in @task.getSessions()
2014-02-14 13:49:16 -05:00
sessionResult =
sessionID: session.sessionID
2014-02-18 14:46:14 -05:00
submitDate: session.submitDate
creator: session.creator
2014-02-14 13:49:16 -05:00
metrics:
rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap()
2014-02-26 15:14:02 -05:00
if session.sessionID is taskResults.originalSessionID
taskResults.originalSessionRank = sessionResult.metrics.rank
taskResults.originalSessionTeam = session.team
2014-02-14 13:49:16 -05:00
taskResults.sessions.push sessionResult
return taskResults
calculateSessionRank: (sessionID, goalStates, teamSessionMap) ->
2014-04-08 17:58:34 -04:00
ogreGoals = (goalState for key, goalState of goalStates when goalState.team is 'ogres')
humanGoals = (goalState for key, goalState of goalStates when goalState.team is 'humans')
ogresWon = _.all ogreGoals, {status: 'success'}
humansWon = _.all humanGoals, {status: 'success'}
if ogresWon is humansWon
2014-02-14 13:49:16 -05:00
return 0
2014-04-08 17:58:34 -04:00
else if ogresWon and teamSessionMap["ogres"] is sessionID
2014-02-14 13:49:16 -05:00
return 0
2014-04-08 17:58:34 -04:00
else if ogresWon and teamSessionMap["ogres"] isnt sessionID
2014-02-14 13:49:16 -05:00
return 1
2014-04-08 17:58:34 -04:00
else if humansWon and teamSessionMap["humans"] is sessionID
2014-02-14 13:49:16 -05:00
return 0
else
return 1
generateSpellsObject: ->
@currentUserCodeMap = @task.generateSpellKeyToSourceMap()
2014-02-14 13:49:16 -05:00
@spells = {}
for thang in @level.attributes.thangs
continue if @thangIsATemplate thang
@generateSpellKeyToSourceMapPropertiesFromThang thang
@spells
2014-02-14 13:49:16 -05:00
thangIsATemplate: (thang) ->
for component in thang.components
continue unless @componentHasProgrammableMethods component
for methodName, method of component.config.programmableMethods
return true if @methodBelongsToTemplateThang method
2014-02-14 13:49:16 -05:00
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
2014-02-26 20:45:08 -05:00
2014-02-14 13:49:16 -05:00
createSpellAndAssignName: (spellKey, spellName) ->
@spells[spellKey] ?= {}
@spells[spellKey].name = spellName
2014-02-14 13:49:16 -05:00
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 then useProtectAPI = false
@spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method, useProtectAPI
2014-02-14 13:49:16 -05:00
transpileSpell: (thang, spellKey, methodName) ->
2014-02-14 13:49:16 -05:00
slugifiedThangID = _.string.slugify thang.id
source = @currentUserCodeMap[[slugifiedThangID,methodName].join '/'] ? ""
aether = @spells[spellKey].thangs[thang.id].aether
try
aether.transpile source
catch e
console.log "Couldn't transpile #{spellKey}:\n#{source}\n", e
aether.transpile ''
2014-02-14 13:49:16 -05:00
createAether: (methodName, method, useProtectAPI) ->
2014-02-14 13:49:16 -05:00
aetherOptions =
functionName: methodName
protectAPI: useProtectAPI
2014-02-14 13:49:16 -05:00
includeFlow: false
yieldConditionally: methodName is "plan"
2014-05-08 14:43:00 -04:00
globals: ['Vector', '_']
problems:
jshint_W040: {level: "ignore"}
jshint_W030: {level: "ignore"} # aether_NoEffect instead
aether_MissingThis: {level: 'error'}
#functionParameters: # TODOOOOO
if methodName is 'hear'
aetherOptions.functionParameters = ['speaker', 'message', 'data']
#console.log "creating aether with options", aetherOptions
2014-02-14 13:49:16 -05:00
return new Aether aetherOptions
2014-02-14 13:49:16 -05:00
class SimulationTask
constructor: (@rawData) ->
@spellKeyToTeamMap = {}
2014-02-14 13:49:16 -05:00
getLevelName: ->
2014-04-26 17:21:26 -04:00
levelName = @rawData.sessions?[0]?.levelID
2014-02-14 13:49:16 -05:00
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
getSpellKeyToTeamMap: -> @spellKeyToTeamMap
getPlayerTeams: -> _.pluck @rawData.sessions, 'team'
2014-02-14 13:49:16 -05:00
generateSpellKeyToSourceMap: ->
playerTeams = _.pluck @rawData.sessions, 'team'
2014-02-14 13:49:16 -05:00
spellKeyToSourceMap = {}
for session in @rawData.sessions
teamSpells = session.teamSpells[session.team]
allTeams = _.keys session.teamSpells
nonPlayerTeams = _.difference allTeams, playerTeams
for team in allTeams
for spell in session.teamSpells[team]
@spellKeyToTeamMap[spell] = team
for nonPlayerTeam in nonPlayerTeams
teamSpells = teamSpells.concat(session.teamSpells[nonPlayerTeam])
teamCode = {}
for thangName, thangSpells of session.code
for spellName, spell of thangSpells
fullSpellName = [thangName,spellName].join '/'
if _.contains(teamSpells, fullSpellName)
teamCode[fullSpellName]=spell
_.merge spellKeyToSourceMap, teamCode
2014-02-26 20:45:08 -05:00
2014-02-14 13:49:16 -05:00
spellKeyToSourceMap