codecombat/app/lib/simulator/Simulator.coffee

425 lines
16 KiB
CoffeeScript
Raw Normal View History

2014-02-14 13:49:16 -05:00
SuperModel = require 'models/SuperModel'
CocoClass = require 'core/CocoClass'
2014-02-14 13:49:16 -05:00
LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
God = require 'lib/God'
2014-08-30 16:43:56 -04:00
{createAetherOptions} = require 'lib/aether_utils'
2014-05-08 14:43:00 -04:00
2016-05-05 16:22:30 -04:00
SIMULATOR_VERSION = 4
simulatorInfo = {}
if $.browser
simulatorInfo['desktop'] = $.browser.desktop if $.browser.desktop
simulatorInfo['name'] = $.browser.name if $.browser.name
simulatorInfo['platform'] = $.browser.platform if $.browser.platform
simulatorInfo['version'] = $.browser.versionNumber if $.browser.versionNumber
module.exports = class Simulator extends CocoClass
constructor: (@options) ->
@options ?= {}
simulatorType = if @options.headlessClient then 'headless' else 'browser'
@simulator =
type: simulatorType
version: SIMULATOR_VERSION
info: simulatorInfo
_.extend @, Backbone.Events
@trigger 'statusUpdate', 'Starting simulation!'
@retryDelayInSeconds = 2
@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-05-20 11:00:44 -04:00
2014-05-19 13:11:20 -04:00
fetchAndSimulateOneGame: (humanGameID, ogresGameID) =>
return if @destroyed
$.ajax
2014-06-30 22:16:26 -04:00
url: '/queue/scoring/getTwoGames'
type: 'POST'
2014-05-19 13:11:20 -04:00
parse: true
data:
humansGameID: humanGameID
ogresGameID: ogresGameID
simulator: @simulator
background: Boolean(@options.background)
levelID: @options.levelID
leagueID: @options.leagueID
2014-05-19 13:11:20 -04:00
error: (errorData) ->
console.warn "There was an error fetching two games! #{JSON.stringify errorData}"
if errorData?.responseText?.indexOf("Old simulator") isnt -1
noty {
text: errorData.responseText
layout: 'center'
type: 'error'
}
2014-05-19 13:11:20 -04:00
success: (taskData) =>
2014-05-20 11:00:44 -04:00
return if @destroyed
unless taskData
@retryDelayInSeconds = 10
@trigger 'statusUpdate', "No games to simulate. Trying another game in #{@retryDelayInSeconds} seconds."
@simulateAnotherTaskAfterDelay()
return
@trigger 'statusUpdate', 'Setting up simulation...'
2014-05-19 13:11:20 -04:00
#refactor this
@task = new SimulationTask(taskData)
2014-05-20 11:00:44 -04:00
2014-05-19 13:11:20 -04:00
@supermodel ?= new SuperModel()
@supermodel.resetProgress()
@stopListening @supermodel, 'loaded-all'
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @task.getLevelName(), sessionID: @task.getFirstSessionID(), opponentSessionID: @task.getSecondSessionID(), headless: true
2014-05-20 11:00:44 -04:00
2014-05-19 13:11:20 -04:00
if @supermodel.finished()
@simulateSingleGame()
else
@listenToOnce @supermodel, 'loaded-all', @simulateSingleGame
2014-05-20 11:00:44 -04:00
2014-05-19 13:11:20 -04:00
simulateSingleGame: ->
return if @destroyed
@assignWorldAndLevelFromLevelLoaderAndDestroyIt()
@trigger 'statusUpdate', 'Simulating...'
2014-05-19 13:11:20 -04:00
@setupGod()
try
@commenceSingleSimulation()
catch error
@handleSingleSimulationError error
2014-05-20 11:00:44 -04:00
2014-05-19 13:11:20 -04:00
commenceSingleSimulation: ->
@listenToOnce @god, 'infinite-loop', @handleSingleSimulationInfiniteLoop
@listenToOnce @god, 'goals-calculated', @processSingleGameResults
2014-05-24 00:24:50 -04:00
@god.createWorld @generateSpellsObject()
2014-05-20 11:00:44 -04:00
handleSingleSimulationError: (error) ->
2014-06-30 22:16:26 -04:00
console.error 'There was an error simulating a single game!', error
return if @destroyed
if @options.headlessClient and @options.simulateOnlyOneGame
2014-06-30 22:16:26 -04:00
console.log 'GAMERESULT:tie'
2014-05-19 13:11:20 -04:00
process.exit(0)
@cleanupAndSimulateAnotherTask()
2014-05-20 11:00:44 -04:00
handleSingleSimulationInfiniteLoop: (e) ->
2014-06-30 22:16:26 -04:00
console.log 'There was an infinite loop in the single game!'
return if @destroyed
if @options.headlessClient and @options.simulateOnlyOneGame
2014-06-30 22:16:26 -04:00
console.log 'GAMERESULT:tie'
2014-05-19 13:11:20 -04:00
process.exit(0)
@cleanupAndSimulateAnotherTask()
2014-05-20 11:00:44 -04:00
2014-05-19 13:11:20 -04:00
processSingleGameResults: (simulationResults) ->
try
taskResults = @formTaskResultsObject simulationResults
catch error
console.log "Failed to form task results:", error
return @cleanupAndSimulateAnotherTask()
2014-05-19 13:11:20 -04:00
humanSessionRank = taskResults.sessions[0].metrics.rank
ogreSessionRank = taskResults.sessions[1].metrics.rank
2014-06-06 15:18:00 -04:00
if @options.headlessClient and @options.simulateOnlyOneGame
2014-05-19 13:11:20 -04:00
if humanSessionRank is ogreSessionRank
2014-06-30 22:16:26 -04:00
console.log 'GAMERESULT:tie'
2014-05-19 13:11:20 -04:00
else if humanSessionRank < ogreSessionRank
2014-06-30 22:16:26 -04:00
console.log 'GAMERESULT:humans'
2014-05-19 13:11:20 -04:00
else if ogreSessionRank < humanSessionRank
2014-06-30 22:16:26 -04:00
console.log 'GAMERESULT:ogres'
2014-05-19 13:11:20 -04:00
process.exit(0)
else
@sendSingleGameBackToServer(taskResults)
2014-05-20 11:00:44 -04:00
sendSingleGameBackToServer: (results) ->
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
2014-05-20 11:00:44 -04:00
$.ajax
2014-06-30 22:16:26 -04:00
url: '/queue/scoring/recordTwoGames'
data: results
2014-06-30 22:16:26 -04:00
type: 'PUT'
parse: true
success: @handleTaskResultsTransferSuccess
error: @handleTaskResultsTransferError
complete: @cleanupAndSimulateAnotherTask
2014-05-20 11:00:44 -04:00
2014-02-14 13:49:16 -05:00
fetchAndSimulateTask: =>
2014-03-16 23:36:02 -04:00
return if @destroyed
# Because there's some bug where the chained rankings don't work, let's just do getTwoGames until we fix it.
return @fetchAndSimulateOneGame()
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.
2014-06-30 22:16:26 -04:00
console.log 'Writing snapshot.'
@options.heapdump.writeSnapshot()
@dumpThisTime = true if @options.heapdump
2014-05-05 23:07:34 -04:00
if @options.testing
2014-06-30 22:16:26 -04:00
_.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
2014-06-30 22:16:26 -04:00
type: 'GET'
parse: true
2014-02-14 13:49:16 -05:00
error: @handleFetchTaskError
success: @setupSimulationAndLoadLevel
cache: false
2014-02-14 13:49:16 -05:00
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-05-20 11:00:44 -04:00
handleNoGamesResponse: ->
@noTasks = true
info = 'Finding game to simulate...'
2014-05-05 23:07:34 -04:00
console.log info
@trigger 'statusUpdate', info
@fetchAndSimulateOneGame()
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()
@stopListening @supermodel, 'loaded-all'
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), opponentSessionID: @task.getSecondSessionID(), 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
2014-02-14 13:49:16 -05:00
@assignWorldAndLevelFromLevelLoaderAndDestroyIt()
@trigger 'statusUpdate', info, @task.getSessions()
2014-02-14 13:49:16 -05:00
@setupGod()
2014-02-14 13:49:16 -05:00
try
@commenceSimulationAndSetupCallback()
catch err
console.error 'There was an error in simulation:', err, err.stack, "-- trying again in #{@retryDelayInSeconds} seconds"
@simulateAnotherTaskAfterDelay()
2014-02-14 13:49:16 -05:00
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
@world = @levelLoader.world
2014-05-15 19:43:16 -04:00
@task.setWorld(@world)
2014-02-14 13:49:16 -05:00
@level = @levelLoader.level
@session = @levelLoader.session
@otherSession = @levelLoader.opponentSession
2014-02-14 13:49:16 -05:00
@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, @session, @otherSession, headless: true, sessionless: false}
2014-05-20 11:00:44 -04:00
@god.setLevelSessionIDs (session.sessionID 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'), null, {headless: true}
humanFlagHistory = _.filter @session.get('state')?.flagHistory ? [], (event) => event.source isnt 'code' and event.team is (@session.get('team') ? 'humans')
ogreFlagHistory = _.filter @otherSession.get('state')?.flagHistory ? [], (event) => event.source isnt 'code' and event.team is (@otherSession.get('team') ? 'ogres')
@god.lastFlagHistory = humanFlagHistory.concat ogreFlagHistory
#console.log 'got flag history', @god.lastFlagHistory, 'from', humanFlagHistory, ogreFlagHistory, @session.get('state'), @otherSession.get('state')
@god.lastSubmissionCount = 0 # TODO: figure out how to combine submissionCounts from both players so we can use submissionCount random seeds again.
@god.lastDifficulty = 0
2014-05-05 23:07:34 -04:00
2014-02-14 13:49:16 -05:00
commenceSimulationAndSetupCallback: ->
@listenToOnce @god, 'infinite-loop', @onInfiniteLoop
@listenToOnce @god, 'goals-calculated', @processResults
2014-05-24 00:24:50 -04:00
@god.createWorld @generateSpellsObject()
2014-02-14 13:49:16 -05:00
2014-06-30 22:16:26 -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
2014-06-30 22:16:26 -04:00
console.log 'Setting leak callbacks.'
2014-05-05 23:07:34 -04:00
@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) =>
2014-06-30 22:16:26 -04:00
console.warn 'stats callback: ' + stats
2014-05-05 23:07:34 -04:00
diff = @hd.end()
console.warn "HeapDiff:\n" + JSON.stringify(diff)
if @options.exitOnLeak
2014-06-30 22:16:26 -04:00
console.warn 'Exiting because of Leak.'
2014-05-05 23:07:34 -04:00
process.exit()
@hd = new @memwatch.HeapDiff()
onInfiniteLoop: (e) ->
return if @destroyed
2014-06-30 22:16:26 -04:00
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) ->
try
taskResults = @formTaskResultsObject simulationResults
catch error
console.log "Failed to form task results:", error
return @cleanupAndSimulateAnotherTask()
unless taskResults.taskID
console.error "*** Error: taskResults has no taskID ***\ntaskResults:", taskResults
@cleanupAndSimulateAnotherTask()
else
@sendResultsBackToServer taskResults
2014-02-14 13:49:16 -05:00
sendResultsBackToServer: (results) ->
status = 'Recording:'
for session in results.sessions
states = ['wins', if _.find(results.sessions, (s) -> s.metrics.rank is 0) then 'loses' else 'draws']
status += " #{session.name} #{states[session.metrics.rank]}"
@trigger 'statusUpdate', status
2014-06-30 22:16:26 -04:00
console.log 'Sending result back to server:'
2014-06-06 15:18:00 -04:00
console.log JSON.stringify 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-06-30 22:16:26 -04:00
url: '/queue/scoring'
2014-02-14 13:49:16 -05:00
data: results
2014-06-30 22:16:26 -04:00
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-05-20 11:00:44 -04:00
return if @destroyed
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
@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) =>
2014-05-20 11:00:44 -04:00
return if @destroyed
@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: =>
2014-05-20 11:00:44 -04:00
return if @destroyed
@cleanupSimulation()
if @options.background or @noTasks
@fetchAndSimulateOneGame()
else
@fetchAndSimulateTask()
2014-02-14 13:49:16 -05:00
cleanupSimulation: ->
@stopListening @god
@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: []
simulator: @simulator
randomSeed: @task.world.randomSeed
2014-02-14 13:49:16 -05:00
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
name: session.creatorName
totalScore: session.totalScore
2014-02-14 13:49:16 -05:00
metrics:
rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap()
shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague
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-06-30 22:16:26 -04:00
else if ogresWon and teamSessionMap['ogres'] is sessionID
2014-02-14 13:49:16 -05:00
return 0
2014-06-30 22:16:26 -04:00
else if ogresWon and teamSessionMap['ogres'] isnt sessionID
2014-02-14 13:49:16 -05:00
return 1
2014-06-30 22:16:26 -04:00
else if humansWon and teamSessionMap['humans'] is sessionID
2014-02-14 13:49:16 -05:00
return 0
else
return 1
generateSpellsObject: ->
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
2014-02-14 13:49:16 -05:00
class SimulationTask
constructor: (@rawData) ->
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?
2014-06-30 22:16:26 -04:00
@throwMalformedTaskError 'The level name couldn\'t be deduced from the task.'
2014-02-14 13:49:16 -05:00
generateTeamToSessionMap: ->
teamSessionMap = {}
for session in @rawData.sessions
2014-06-30 22:16:26 -04:00
@throwMalformedTaskError 'Two players share the same team' if teamSessionMap[session.team]?
2014-02-14 13:49:16 -05:00
teamSessionMap[session.team] = session.sessionID
teamSessionMap
throwMalformedTaskError: (errorString) ->
throw new Error "The task was malformed, reason: #{errorString}"
getFirstSessionID: -> @rawData.sessions[0].sessionID
getSecondSessionID: -> @rawData.sessions[1].sessionID
2014-02-14 13:49:16 -05:00
getTaskID: -> @rawData.taskID
getReceiptHandle: -> @rawData.receiptHandle
getSessions: -> @rawData.sessions
setWorld: (@world) ->