SuperModel = require 'models/SuperModel' CocoClass = require 'core/CocoClass' LevelLoader = require 'lib/LevelLoader' GoalManager = require 'lib/world/GoalManager' God = require 'lib/God' {createAetherOptions} = require 'lib/aether_utils' 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' @simulatedByYou = 0 @god = new God maxAngels: 1, workerCode: @options.workerCode, headless: true # Start loading worker. destroy: -> @off() @cleanupSimulation() @god?.destroy() super() fetchAndSimulateOneGame: (humanGameID, ogresGameID) => return if @destroyed $.ajax url: '/queue/scoring/getTwoGames' type: 'POST' parse: true data: humansGameID: humanGameID ogresGameID: ogresGameID simulator: @simulator background: Boolean(@options.background) levelID: @options.levelID leagueID: @options.leagueID 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' } success: (taskData) => 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...' #refactor this @task = new SimulationTask(taskData) @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 if @supermodel.finished() @simulateSingleGame() else @listenToOnce @supermodel, 'loaded-all', @simulateSingleGame simulateSingleGame: -> return if @destroyed @assignWorldAndLevelFromLevelLoaderAndDestroyIt() @trigger 'statusUpdate', 'Simulating...' @setupGod() try @commenceSingleSimulation() catch error @handleSingleSimulationError error commenceSingleSimulation: -> @listenToOnce @god, 'infinite-loop', @handleSingleSimulationInfiniteLoop @listenToOnce @god, 'goals-calculated', @processSingleGameResults @god.createWorld @generateSpellsObject() handleSingleSimulationError: (error) -> console.error 'There was an error simulating a single game!', error return if @destroyed if @options.headlessClient and @options.simulateOnlyOneGame console.log 'GAMERESULT:tie' process.exit(0) @cleanupAndSimulateAnotherTask() handleSingleSimulationInfiniteLoop: (e) -> console.log 'There was an infinite loop in the single game!' return if @destroyed if @options.headlessClient and @options.simulateOnlyOneGame console.log 'GAMERESULT:tie' process.exit(0) @cleanupAndSimulateAnotherTask() processSingleGameResults: (simulationResults) -> try taskResults = @formTaskResultsObject simulationResults catch error console.log "Failed to form task results:", error return @cleanupAndSimulateAnotherTask() console.log 'Processing results:', taskResults humanSessionRank = taskResults.sessions[0].metrics.rank ogreSessionRank = taskResults.sessions[1].metrics.rank if @options.headlessClient and @options.simulateOnlyOneGame if humanSessionRank is ogreSessionRank console.log 'GAMERESULT:tie' else if humanSessionRank < ogreSessionRank console.log 'GAMERESULT:humans' else if ogreSessionRank < humanSessionRank console.log 'GAMERESULT:ogres' process.exit(0) else @sendSingleGameBackToServer(taskResults) sendSingleGameBackToServer: (results) -> @trigger 'statusUpdate', 'Simulation completed, sending results back to server!' $.ajax url: '/queue/scoring/recordTwoGames' data: results type: 'PUT' parse: true success: @handleTaskResultsTransferSuccess error: @handleTaskResultsTransferError complete: @cleanupAndSimulateAnotherTask fetchAndSimulateTask: => 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() if @options.headlessClient if @dumpThisTime # The first heapdump would be useless to find leaks. console.log 'Writing snapshot.' @options.heapdump.writeSnapshot() @dumpThisTime = true if @options.heapdump if @options.testing _.delay @setupSimulationAndLoadLevel, 0, @options.testFile, 'Testing...', status: 400 return @trigger 'statusUpdate', 'Fetching simulation data!' $.ajax url: @taskURL type: 'GET' parse: true error: @handleFetchTaskError success: @setupSimulationAndLoadLevel cache: false 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() handleNoGamesResponse: -> @noTasks = true info = 'Finding game to simulate...' console.log info @trigger 'statusUpdate', info @fetchAndSimulateOneGame() simulateAnotherTaskAfterDelay: => console.log "Retrying in #{@retryDelayInSeconds}" retryDelayInMilliseconds = @retryDelayInSeconds * 1000 _.delay @fetchAndSimulateTask, retryDelayInMilliseconds setupSimulationAndLoadLevel: (taskData, textStatus, jqXHR) => return @handleNoGamesResponse() if jqXHR.status is 204 @trigger 'statusUpdate', 'Setting up simulation!' @task = new SimulationTask(taskData) 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 simulateGame: -> return if @destroyed info = 'All resources loaded, simulating!' console.log info @assignWorldAndLevelFromLevelLoaderAndDestroyIt() @trigger 'statusUpdate', info, @task.getSessions() @setupGod() try @commenceSimulationAndSetupCallback() catch err console.error 'There was an error in simulation:', err, err.stack, "-- trying again in #{@retryDelayInSeconds} seconds" @simulateAnotherTaskAfterDelay() assignWorldAndLevelFromLevelLoaderAndDestroyIt: -> @world = @levelLoader.world @task.setWorld(@world) @level = @levelLoader.level @session = @levelLoader.session @otherSession = @levelLoader.opponentSession @levelLoader.destroy() @levelLoader = null setupGod: -> @god.setLevel @level.serialize(@supermodel, @session, @otherSession) @god.setLevelSessionIDs (session.sessionID for session in @task.getSessions()) @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 commenceSimulationAndSetupCallback: -> @listenToOnce @god, 'infinite-loop', @onInfiniteLoop @listenToOnce @god, 'goals-calculated', @processResults @god.createWorld @generateSpellsObject() # Search for leaks, headless-client only. if @options.headlessClient and @options.leakTest and not @memwatch? 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 console.warn 'Exiting because of Leak.' process.exit() @hd = new @memwatch.HeapDiff() onInfiniteLoop: (e) -> return if @destroyed console.warn 'Skipping infinitely looping game.' @trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds." _.delay @cleanupAndSimulateAnotherTask, @retryDelayInSeconds * 1000 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 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 console.log 'Sending result back to server:' console.log JSON.stringify results if @options.headlessClient and @options.testing return @fetchAndSimulateTask() $.ajax url: '/queue/scoring' data: results type: 'PUT' parse: true success: @handleTaskResultsTransferSuccess error: @handleTaskResultsTransferError complete: @cleanupAndSimulateAnotherTask handleTaskResultsTransferSuccess: (result) => return if @destroyed console.log "Task registration result: #{JSON.stringify result}" @trigger 'statusUpdate', 'Results were successfully sent back to server!' @simulatedByYou++ unless @options.headlessClient simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1 $('#simulated-by-you').text(simulatedBy) handleTaskResultsTransferError: (error) => return if @destroyed @trigger 'statusUpdate', 'There was an error sending the results back to the server.' console.log "Task registration error: #{JSON.stringify error}" cleanupAndSimulateAnotherTask: => return if @destroyed @cleanupSimulation() if @options.background or @noTasks @fetchAndSimulateOneGame() else @fetchAndSimulateTask() cleanupSimulation: -> @stopListening @god @world = null @level = null formTaskResultsObject: (simulationResults) -> taskResults = taskID: @task.getTaskID() receiptHandle: @task.getReceiptHandle() originalSessionID: @task.getFirstSessionID() originalSessionRank: -1 calculationTime: 500 sessions: [] simulator: @simulator randomSeed: @task.world.randomSeed for session in @task.getSessions() sessionResult = sessionID: session.sessionID submitDate: session.submitDate creator: session.creator name: session.creatorName totalScore: session.totalScore metrics: rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap() shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague if session.sessionID is taskResults.originalSessionID taskResults.originalSessionRank = sessionResult.metrics.rank taskResults.originalSessionTeam = session.team taskResults.sessions.push sessionResult return taskResults calculateSessionRank: (sessionID, goalStates, teamSessionMap) -> 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 return 0 else if ogresWon and teamSessionMap['ogres'] is sessionID return 0 else if ogresWon and teamSessionMap['ogres'] isnt sessionID return 1 else if humansWon and teamSessionMap['humans'] is sessionID return 0 else return 1 generateSpellsObject: -> @currentUserCodeMap = @task.generateSpellKeyToSourceMap() @spells = {} for thang in @level.attributes.thangs continue if @thangIsATemplate thang @generateSpellKeyToSourceMapPropertiesFromThang thang @spells 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, methodName] spellKeyComponents[0] = _.string.slugify spellKeyComponents[0] spellKey = spellKeyComponents.join '/' spellKey createSpellAndAssignName: (spellKey, spellName) -> @spells[spellKey] ?= {} @spells[spellKey].name = spellName 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 useProtectAPI = false else spellSession = _.filter(@task.getSessions(), {team: spellTeam})[0] unless codeLanguage = spellSession?.submittedCodeLanguage console.warn 'Session', spellSession.creatorName, spellSession.team, 'didn\'t have submittedCodeLanguage, just:', spellSession @spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method, useProtectAPI, codeLanguage ? 'javascript' transpileSpell: (thang, spellKey, methodName) -> slugifiedThangID = _.string.slugify thang.id generatedSpellKey = [slugifiedThangID,methodName].join '/' source = @currentUserCodeMap[generatedSpellKey] ? '' aether = @spells[spellKey].thangs[thang.id].aether #unless _.contains(@task.spellKeysToTranspile, generatedSpellKey) try aether.transpile source catch e console.log "Couldn't transpile #{spellKey}:\n#{source}\n", e aether.transpile '' createAether: (methodName, method, useProtectAPI, codeLanguage) -> aetherOptions = createAetherOptions functionName: methodName, codeLanguage: codeLanguage, skipProtectAPI: not useProtectAPI, useInterpreter: true return new Aether aetherOptions class SimulationTask constructor: (@rawData) -> @spellKeyToTeamMap = {} 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 getSecondSessionID: -> @rawData.sessions[1].sessionID getTaskID: -> @rawData.taskID getReceiptHandle: -> @rawData.receiptHandle getSessions: -> @rawData.sessions getSpellKeyToTeamMap: -> @spellKeyToTeamMap getPlayerTeams: -> _.pluck @rawData.sessions, 'team' setWorld: (@world) -> generateSpellKeyToSourceMap: -> # TODO: we always now only have hero-placeholder/plan vs. hero-placeholder-1/plan on humans vs. ogres, always just have to retranspile for Esper, and never need to transpile for NPCs or other methods, so we can get rid of almost all of this stuff. playerTeams = _.pluck @rawData.sessions, 'team' spellKeyToSourceMap = {} for session in @rawData.sessions teamSpells = session.teamSpells[session.team] allTeams = _.keys session.teamSpells for team in allTeams for spell in session.teamSpells[team] @spellKeyToTeamMap[spell] = team teamCode = {} for thangName, thangSpells of session.submittedCode for spellName, spell of thangSpells fullSpellName = [thangName, spellName].join '/' if _.contains(teamSpells, fullSpellName) teamCode[fullSpellName] = LZString.decompressFromUTF16 spell _.merge spellKeyToSourceMap, teamCode spellKeyToSourceMap