mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 23:58:02 -05:00
Simulate games in background while playing levels if we think there's extra compute power
This commit is contained in:
parent
9d21f87345
commit
95c703a7df
18 changed files with 148 additions and 65 deletions
|
@ -71,7 +71,7 @@ setUpBackboneMediator = ->
|
|||
if false # Debug which events are being fired
|
||||
originalPublish = Backbone.Mediator.publish
|
||||
Backbone.Mediator.publish = ->
|
||||
console.log 'Publishing event:', arguments...
|
||||
console.log 'Publishing event:', arguments... unless /(tick|frame-changed)/.test(arguments[0])
|
||||
originalPublish.apply Backbone.Mediator, arguments
|
||||
|
||||
setUpMoment = ->
|
||||
|
|
|
@ -25,9 +25,10 @@ module.exports = class Angel extends CocoClass
|
|||
super()
|
||||
@say 'Got my wings.'
|
||||
isIE = window.navigator and (window.navigator.userAgent.search('MSIE') isnt -1 or window.navigator.appName is 'Microsoft Internet Explorer')
|
||||
if isIE or @shared.headless
|
||||
# Since IE is so slow to serialize without transferable objects, we can't trust it.
|
||||
# We also noticed the headless_client simulator needing more time. (This does both Simulators, though.)
|
||||
slowerSimulations = isIE #or @shared.headless
|
||||
# Since IE is so slow to serialize without transferable objects, we can't trust it.
|
||||
# We also noticed the headless_client simulator needing more time. (This does both Simulators, though.) If we need to use lots of headless clients, enable this.
|
||||
if slowerSimulations
|
||||
@infiniteLoopIntervalDuration *= 10
|
||||
@infiniteLoopTimeoutDuration *= 10
|
||||
@abortTimeoutDuration *= 10
|
||||
|
@ -89,7 +90,7 @@ module.exports = class Angel extends CocoClass
|
|||
|
||||
# We have to abort like an infinite loop if we see one of these; they're not really recoverable
|
||||
when 'non-user-code-problem'
|
||||
Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem
|
||||
Backbone.Mediator.publish 'god:non-user-code-problem', problem: event.data.problem, god: @shared.god
|
||||
if @shared.firstWorld
|
||||
@infinitelyLooped(false, true) # For now, this should do roughly the right thing if it happens during load.
|
||||
else
|
||||
|
@ -108,9 +109,9 @@ module.exports = class Angel extends CocoClass
|
|||
when 'console-log'
|
||||
@log event.data.args...
|
||||
when 'user-code-problem'
|
||||
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
|
||||
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem, god: @shared.god
|
||||
when 'world-load-progress-changed'
|
||||
Backbone.Mediator.publish 'god:world-load-progress-changed', progress: event.data.progress
|
||||
Backbone.Mediator.publish 'god:world-load-progress-changed', progress: event.data.progress, god: @shared.god
|
||||
unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializationQueue.length or (@shared.firstWorld and not @shared.spectate)
|
||||
@worker.postMessage func: 'serializeFramesSoFar' # Stream it!
|
||||
|
||||
|
@ -126,7 +127,8 @@ module.exports = class Angel extends CocoClass
|
|||
|
||||
beholdGoalStates: (goalStates, overallStatus, preload=false) ->
|
||||
return if @aborting
|
||||
Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus
|
||||
Backbone.Mediator.publish 'god:goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus, god: @shared.god
|
||||
@shared.god.trigger 'goals-calculated', goalStates: goalStates, preload: preload, overallStatus: overallStatus
|
||||
@finishWork() if @shared.headless
|
||||
|
||||
beholdWorld: (serialized, goalStates, startFrame, endFrame, streamingWorld) ->
|
||||
|
@ -176,8 +178,9 @@ module.exports = class Angel extends CocoClass
|
|||
return if @aborting
|
||||
problem = type: 'runtime', level: 'error', id: 'runtime_InfiniteLoop', message: 'Code never finished. It\'s either really slow or has an infinite loop.'
|
||||
problem.message = 'Escape pressed; code aborted.' if escaped
|
||||
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
|
||||
Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @shared.firstWorld, nonUserCodeProblem: nonUserCodeProblem
|
||||
Backbone.Mediator.publish 'god:user-code-problem', problem: problem, god: @shared.god
|
||||
Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @shared.firstWorld, nonUserCodeProblem: nonUserCodeProblem, god: @shared.god
|
||||
@shared.god.trigger 'infinite-loop', firstWorld: @shared.firstWorld, nonUserCodeProblem: nonUserCodeProblem # For Simulator. TODO: refactor all the god:* Mediator events to be local events.
|
||||
@reportLoadError() if nonUserCodeProblem
|
||||
@fireWorker()
|
||||
|
||||
|
@ -308,7 +311,7 @@ module.exports = class Angel extends CocoClass
|
|||
i = 0
|
||||
while i < work.testWorld.totalFrames
|
||||
frame = work.testWorld.getFrame i++
|
||||
Backbone.Mediator.publish 'god:world-load-progress-changed', progress: 1
|
||||
Backbone.Mediator.publish 'god:world-load-progress-changed', progress: 1, god: @shared.god
|
||||
work.testWorld.ended = true
|
||||
system.finish work.testWorld.thangs for system in work.testWorld.systems
|
||||
work.t2 = now()
|
||||
|
|
|
@ -25,6 +25,7 @@ module.exports = class God extends CocoClass
|
|||
workerCode: options.workerCode or '/javascripts/workers/worker_world.js' # Either path or function
|
||||
headless: options.headless # Whether to just simulate the goals, or to deserialize all simulation results
|
||||
spectate: options.spectate
|
||||
god: @
|
||||
godNick: @nick
|
||||
workQueue: []
|
||||
firstWorld: true
|
||||
|
@ -61,6 +62,7 @@ module.exports = class God extends CocoClass
|
|||
setWorldClassMap: (worldClassMap) -> @angelsShare.worldClassMap = worldClassMap
|
||||
|
||||
onTomeCast: (e) ->
|
||||
return unless e.god is @
|
||||
@lastSubmissionCount = e.submissionCount
|
||||
@lastFlagHistory = (flag for flag in e.flagHistory when flag.source isnt 'code')
|
||||
@lastDifficulty = e.difficulty
|
||||
|
@ -142,9 +144,9 @@ module.exports = class God extends CocoClass
|
|||
when 'console-log'
|
||||
console.log "|#{@nick}'s debugger|", event.data.args...
|
||||
when 'debug-value-return'
|
||||
Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized
|
||||
Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized, god: @
|
||||
when 'debug-world-load-progress-changed'
|
||||
Backbone.Mediator.publish 'god:debug-world-load-progress-changed', progress: event.data.progress
|
||||
Backbone.Mediator.publish 'god:debug-world-load-progress-changed', progress: event.data.progress, god: @
|
||||
|
||||
onNewWorldCreated: (e) ->
|
||||
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld e.world.userCodeMap
|
||||
|
|
|
@ -45,6 +45,7 @@ module.exports = class Simulator extends CocoClass
|
|||
humansGameID: humanGameID
|
||||
ogresGameID: ogresGameID
|
||||
simulator: @simulator
|
||||
background: Boolean(@options.background)
|
||||
error: (errorData) ->
|
||||
console.warn "There was an error fetching two games! #{JSON.stringify errorData}"
|
||||
if errorData?.responseText?.indexOf("Old simulator") isnt -1
|
||||
|
@ -84,8 +85,8 @@ module.exports = class Simulator extends CocoClass
|
|||
@handleSingleSimulationError error
|
||||
|
||||
commenceSingleSimulation: ->
|
||||
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @handleSingleSimulationInfiniteLoop, @
|
||||
Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processSingleGameResults, @
|
||||
@listenToOnce @god, 'infinite-loop', @handleSingleSimulationInfiniteLoop
|
||||
@listenToOnce @god, 'goals-calculated', @processSingleGameResults
|
||||
@god.createWorld @generateSpellsObject()
|
||||
|
||||
handleSingleSimulationError: (error) ->
|
||||
|
@ -96,7 +97,7 @@ module.exports = class Simulator extends CocoClass
|
|||
process.exit(0)
|
||||
@cleanupAndSimulateAnotherTask()
|
||||
|
||||
handleSingleSimulationInfiniteLoop: ->
|
||||
handleSingleSimulationInfiniteLoop: (e) ->
|
||||
console.log 'There was an infinite loop in the single game!'
|
||||
return if @destroyed
|
||||
if @options.headlessClient and @options.simulateOnlyOneGame
|
||||
|
@ -105,7 +106,6 @@ module.exports = class Simulator extends CocoClass
|
|||
@cleanupAndSimulateAnotherTask()
|
||||
|
||||
processSingleGameResults: (simulationResults) ->
|
||||
return console.error "Weird, we destroyed the Simulator before it processed results?" if @destroyed
|
||||
try
|
||||
taskResults = @formTaskResultsObject simulationResults
|
||||
catch error
|
||||
|
@ -165,6 +165,7 @@ module.exports = class Simulator extends CocoClass
|
|||
@simulateAnotherTaskAfterDelay()
|
||||
|
||||
handleNoGamesResponse: ->
|
||||
@noTasks = true
|
||||
info = 'Finding game to simulate...'
|
||||
console.log info
|
||||
@trigger 'statusUpdate', info
|
||||
|
@ -223,7 +224,7 @@ module.exports = class Simulator extends CocoClass
|
|||
@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')
|
||||
@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
|
||||
|
@ -232,8 +233,8 @@ module.exports = class Simulator extends CocoClass
|
|||
@god.lastDifficulty = 0
|
||||
|
||||
commenceSimulationAndSetupCallback: ->
|
||||
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @
|
||||
Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @
|
||||
@listenToOnce @god, 'infinite-loop', @onInfiniteLoop
|
||||
@listenToOnce @god, 'goals-calculated', @processResults
|
||||
@god.createWorld @generateSpellsObject()
|
||||
|
||||
# Search for leaks, headless-client only.
|
||||
|
@ -260,14 +261,13 @@ module.exports = class Simulator extends CocoClass
|
|||
process.exit()
|
||||
@hd = new @memwatch.HeapDiff()
|
||||
|
||||
onInfiniteLoop: ->
|
||||
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) ->
|
||||
return console.error "Weird, we destroyed the Simulator before it processed results?" if @destroyed
|
||||
try
|
||||
taskResults = @formTaskResultsObject simulationResults
|
||||
catch error
|
||||
|
@ -317,9 +317,13 @@ module.exports = class Simulator extends CocoClass
|
|||
cleanupAndSimulateAnotherTask: =>
|
||||
return if @destroyed
|
||||
@cleanupSimulation()
|
||||
@fetchAndSimulateTask()
|
||||
if @options.background or @noTasks
|
||||
@fetchAndSimulateOneGame()
|
||||
else
|
||||
@fetchAndSimulateTask()
|
||||
|
||||
cleanupSimulation: ->
|
||||
@stopListening @god
|
||||
@world = null
|
||||
@level = null
|
||||
|
||||
|
|
|
@ -16,8 +16,9 @@ module.exports = class GoalManager extends CocoClass
|
|||
nextGoalID: 0
|
||||
nicks: ['GoalManager']
|
||||
|
||||
constructor: (@world, @initialGoals, @team) ->
|
||||
constructor: (@world, @initialGoals, @team, options) ->
|
||||
super()
|
||||
@options = options or {}
|
||||
@init()
|
||||
|
||||
init: ->
|
||||
|
@ -107,6 +108,7 @@ module.exports = class GoalManager extends CocoClass
|
|||
@addNewSubscription(channel, f(channel))
|
||||
|
||||
notifyGoalChanges: ->
|
||||
return if @options.headless
|
||||
overallStatus = @checkOverallStatus()
|
||||
event =
|
||||
goalStates: @goalStates
|
||||
|
|
|
@ -199,7 +199,6 @@ module.exports = class SuperModel extends Backbone.Model
|
|||
@progress = newProg
|
||||
@trigger('update-progress', @progress)
|
||||
@trigger('loaded-all') if @finished()
|
||||
Backbone.Mediator.publish 'supermodel:load-progress-changed', progress: @progress
|
||||
|
||||
setMaxProgress: (@maxProgress) ->
|
||||
resetProgress: -> @progress = 0
|
||||
|
|
|
@ -27,13 +27,16 @@ worldUpdatedEventSchema = c.object {required: ['world', 'firstWorld', 'goalState
|
|||
finished: {type: 'boolean'}
|
||||
|
||||
module.exports =
|
||||
'god:user-code-problem': c.object {required: ['problem']},
|
||||
'god:user-code-problem': c.object {required: ['problem', 'god']},
|
||||
god: {type: 'object'}
|
||||
problem: {type: 'object'}
|
||||
|
||||
'god:non-user-code-problem': c.object {required: ['problem']},
|
||||
'god:non-user-code-problem': c.object {required: ['problem', 'god']},
|
||||
god: {type: 'object'}
|
||||
problem: {type: 'object'}
|
||||
|
||||
'god:infinite-loop': c.object {required: ['firstWorld']},
|
||||
'god:infinite-loop': c.object {required: ['firstWorld', 'god']},
|
||||
god: {type: 'object'}
|
||||
firstWorld: {type: 'boolean'}
|
||||
nonUserCodeProblem: {type: 'boolean'}
|
||||
|
||||
|
@ -41,17 +44,21 @@ module.exports =
|
|||
|
||||
'god:streaming-world-updated': worldUpdatedEventSchema
|
||||
|
||||
'god:goals-calculated': c.object {required: ['goalStates']},
|
||||
'god:goals-calculated': c.object {required: ['goalStates', 'god']},
|
||||
god: {type: 'object'}
|
||||
goalStates: goalStatesSchema
|
||||
preload: {type: 'boolean'}
|
||||
overallStatus: {type: ['string', 'null'], enum: ['success', 'failure', 'incomplete', null]}
|
||||
|
||||
'god:world-load-progress-changed': c.object {required: ['progress']},
|
||||
'god:world-load-progress-changed': c.object {required: ['progress', 'god']},
|
||||
god: {type: 'object'}
|
||||
progress: {type: 'number', minimum: 0, maximum: 1}
|
||||
|
||||
'god:debug-world-load-progress-changed': c.object {required: ['progress']},
|
||||
'god:debug-world-load-progress-changed': c.object {required: ['progress', 'god']},
|
||||
god: {type: 'object'}
|
||||
progress: {type: 'number', minimum: 0, maximum: 1}
|
||||
|
||||
'god:debug-value-return': c.object {required: ['key']},
|
||||
'god:debug-value-return': c.object {required: ['key', 'god']},
|
||||
god: {type: 'object'}
|
||||
key: {type: 'string'}
|
||||
value: {}
|
||||
|
|
|
@ -48,9 +48,6 @@ module.exports =
|
|||
session: {type: 'object'}
|
||||
level: {type: 'object'}
|
||||
|
||||
'supermodel:load-progress-changed': c.object {required: ['progress']},
|
||||
progress: {type: 'number', minimum: 0, maximum: 1}
|
||||
|
||||
'buy-gems-modal:update-products': { }
|
||||
|
||||
'buy-gems-modal:purchase-initiated': c.object {required: ['productID']},
|
||||
|
|
|
@ -7,13 +7,14 @@ module.exports =
|
|||
preload: {type: 'boolean'}
|
||||
realTime: {type: 'boolean'}
|
||||
|
||||
'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory', 'difficulty']},
|
||||
spells: [type: 'object']
|
||||
preload: [type: 'boolean']
|
||||
realTime: [type: 'boolean']
|
||||
submissionCount: [type: 'integer']
|
||||
flagHistory: [type: 'array']
|
||||
difficulty: [type: 'integer']
|
||||
'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory', 'difficulty', 'god']},
|
||||
spells: {type: 'object'}
|
||||
preload: {type: 'boolean'}
|
||||
realTime: {type: 'boolean'}
|
||||
submissionCount: {type: 'integer'}
|
||||
flagHistory: {type: 'array'}
|
||||
difficulty: {type: 'integer'}
|
||||
god: {type: 'object'}
|
||||
|
||||
'tome:manual-cast': c.object {title: 'Manually Cast Spells', description: 'Published when you wish to manually recast all spells', required: []},
|
||||
realTime: {type: 'boolean'}
|
||||
|
|
|
@ -16,12 +16,7 @@ module.exports = class SimulateTabView extends CocoView
|
|||
@simulatorsLeaderboardData = new SimulatorsLeaderboardData(me)
|
||||
@simulatorsLeaderboardDataRes = @supermodel.addModelResource(@simulatorsLeaderboardData, 'top_simulators', {cache: false})
|
||||
@simulatorsLeaderboardDataRes.load()
|
||||
require 'vendor/aether-javascript'
|
||||
require 'vendor/aether-python'
|
||||
require 'vendor/aether-coffeescript'
|
||||
require 'vendor/aether-lua'
|
||||
require 'vendor/aether-clojure'
|
||||
require 'vendor/aether-io'
|
||||
require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'clojure', 'io']
|
||||
|
||||
onLoaded: ->
|
||||
super()
|
||||
|
|
|
@ -187,7 +187,7 @@ module.exports = class SpectateLevelView extends RootView
|
|||
# callbacks
|
||||
|
||||
onInfiniteLoop: (e) ->
|
||||
return unless e.firstWorld
|
||||
return unless e.firstWorld and e.god is @god
|
||||
@openModalView new InfiniteLoopModal()
|
||||
window.tracker?.trackEvent 'Saw Initial Infinite Loop', level: @world.name, label: @world.name
|
||||
|
||||
|
|
|
@ -44,12 +44,14 @@ module.exports = class LevelLoadingView extends CocoView
|
|||
@$el.addClass('manually-sized').css('height', newHeight)
|
||||
|
||||
onLevelLoaded: (e) ->
|
||||
return if @level
|
||||
@level = e.level
|
||||
@prepareGoals e
|
||||
@prepareTip()
|
||||
@prepareIntro()
|
||||
|
||||
onSessionLoaded: (e) ->
|
||||
return if @session
|
||||
@session = e.session if e.session.get('creator') is me.id
|
||||
|
||||
prepareGoals: (e) ->
|
||||
|
|
|
@ -19,6 +19,7 @@ LevelComponent = require 'models/LevelComponent'
|
|||
Article = require 'models/Article'
|
||||
Camera = require 'lib/surface/Camera'
|
||||
AudioPlayer = require 'lib/AudioPlayer'
|
||||
Simulator = require 'lib/simulator/Simulator'
|
||||
|
||||
# subviews
|
||||
LevelLoadingView = require './LevelLoadingView'
|
||||
|
@ -139,11 +140,11 @@ module.exports = class PlayLevelView extends RootView
|
|||
trackLevelLoadEnd: ->
|
||||
return if @isEditorPreview
|
||||
@loadEndTime = new Date()
|
||||
loadDuration = @loadEndTime - @loadStartTime
|
||||
console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s"
|
||||
@loadDuration = @loadEndTime - @loadStartTime
|
||||
console.debug "Level unveiled after #{(@loadDuration / 1000).toFixed(2)}s"
|
||||
unless @observing
|
||||
application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: loadDuration
|
||||
application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID
|
||||
application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: @loadDuration
|
||||
application.tracker?.trackTiming @loadDuration, 'Level Load Time', @levelID, @levelID
|
||||
|
||||
# CocoView overridden methods ###############################################
|
||||
|
||||
|
@ -195,6 +196,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@worldLoadFakeResources.push @supermodel.addSomethingResource "world_simulation_#{percent}%", 1
|
||||
|
||||
onWorldLoadProgressChanged: (e) ->
|
||||
return unless e.god is @god
|
||||
return unless @worldLoadFakeResources
|
||||
@lastWorldLoadPercent ?= 0
|
||||
worldLoadPercent = Math.floor 100 * e.progress
|
||||
|
@ -240,7 +242,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@god.setGoalManager @goalManager
|
||||
|
||||
insertSubviews: ->
|
||||
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing, courseID: @courseID, courseInstanceID: @courseInstanceID
|
||||
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing, courseID: @courseID, courseInstanceID: @courseInstanceID, god: @god
|
||||
@insertSubView new LevelPlaybackView session: @session, level: @level
|
||||
@insertSubView new GoalsView {}
|
||||
@insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags'
|
||||
|
@ -273,6 +275,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
# Load Completed Setup ######################################################
|
||||
|
||||
onSessionLoaded: (e) ->
|
||||
return if @session
|
||||
Backbone.Mediator.publish "ipad:language-chosen", language: e.session.get('codeLanguage') ? "python"
|
||||
# Just the level and session have been loaded by the level loader
|
||||
if e.level.get('slug') is 'zero-sum'
|
||||
|
@ -288,7 +291,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
e.session.set 'heroConfig', {"thangType":raider,"inventory":{}}
|
||||
else if e.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] and not _.size e.session.get('heroConfig')?.inventory ? {}
|
||||
@setupManager?.destroy()
|
||||
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, courseID: @courseID, courseInstanceID: @courseInstanceID})
|
||||
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: e.level, levelID: @levelID, parent: @, session: e.session, courseID: @courseID, courseInstanceID: @courseInstanceID})
|
||||
@setupManager.open()
|
||||
|
||||
@onRealTimeMultiplayerLevelLoaded e.session if e.level.get('type') in ['hero-ladder', 'course-ladder']
|
||||
|
@ -365,6 +368,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
# TODO: Is it possible to create a Mongoose ObjectId for 'ls', instead of the string returned from get()?
|
||||
application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id') unless @observing
|
||||
$(window).trigger 'resize'
|
||||
_.delay (=> @perhapsStartSimulating?()), 10 * 1000
|
||||
|
||||
playAmbientSound: ->
|
||||
return if @destroyed
|
||||
|
@ -384,6 +388,65 @@ module.exports = class PlayLevelView extends RootView
|
|||
Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
|
||||
@surface.focusOnHero()
|
||||
|
||||
perhapsStartSimulating: ->
|
||||
return unless @shouldSimulate()
|
||||
require "vendor/aether-#{codeLanguage}" for codeLanguage in ['javascript', 'python', 'coffeescript', 'lua', 'clojure', 'io']
|
||||
@simulateNextGame()
|
||||
|
||||
simulateNextGame: ->
|
||||
return @simulator.fetchAndSimulateOneGame() if @simulator
|
||||
@simulator = new Simulator background: true
|
||||
# Crude method of mitigating Simulator memory leak issues
|
||||
fetchAndSimulateOneGameOriginal = @simulator.fetchAndSimulateOneGame
|
||||
@simulator.fetchAndSimulateOneGame = =>
|
||||
if @simulator.simulatedByYou >= 10
|
||||
console.log '------------------- Destroying Simulator and making a new one -----------------'
|
||||
@simulator.destroy()
|
||||
@simulator = null
|
||||
@simulateNextGame()
|
||||
else
|
||||
fetchAndSimulateOneGameOriginal.apply @simulator
|
||||
@simulator.fetchAndSimulateOneGame()
|
||||
|
||||
shouldSimulate: ->
|
||||
# Crude heuristics are crude.
|
||||
defaultCores = 2
|
||||
cores = window.navigator.hardwareConcurrency or defaultCores # Available on Chrome/Opera, soon Safari
|
||||
defaultHeapLimit = 793000000
|
||||
heapLimit = window.performance?.memory?.jsHeapSizeLimit or defaultHeapLimit # Only available on Chrome, basically just says 32- vs. 64-bit
|
||||
levelType = @level.get 'type', true
|
||||
gamesSimulated = me.get('simulatedBy')
|
||||
console.debug "Should we start simulating? Cores:", window.navigator.hardwareConcurrency, "Heap limit:", window.performance?.memory?.jsHeapSizeLimit, "Load duration:", @loadDuration
|
||||
return false unless $.browser?.desktop
|
||||
return false if $.browser?.msie or $.browser?.msedge
|
||||
return false if $.browser.linux
|
||||
return false if me.level() < 8
|
||||
if levelType is 'course'
|
||||
return false
|
||||
else if levelType is 'hero' and gamesSimulated
|
||||
return false if cores < 8
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 10000
|
||||
else if levelType is 'hero-ladder' and gamesSimulated
|
||||
return false if cores < 4
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 15000
|
||||
else if levelType is 'hero-ladder' and not gamesSimulated
|
||||
return false if cores < 8
|
||||
return false if heapLimit <= defaultHeapLimit
|
||||
return false if @loadDuration > 20000
|
||||
else if levelType is 'course-ladder'
|
||||
return false if cores <= defaultCores
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 18000
|
||||
else
|
||||
console.warn "Unwritten level type simulation heuristics; fill these in for new level type #{levelType}?"
|
||||
return false if cores < 8
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 10000
|
||||
console.debug "We should have the power. Begin background ladder simulation."
|
||||
true
|
||||
|
||||
# callbacks
|
||||
|
||||
onCtrlS: (e) ->
|
||||
|
@ -457,7 +520,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing
|
||||
|
||||
onInfiniteLoop: (e) ->
|
||||
return unless e.firstWorld
|
||||
return unless e.firstWorld and e.god is @god
|
||||
@openModalView new InfiniteLoopModal nonUserCodeProblem: e.nonUserCodeProblem
|
||||
application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing
|
||||
|
||||
|
@ -557,6 +620,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@goalManager?.destroy()
|
||||
@scriptManager?.destroy()
|
||||
@setupManager?.destroy()
|
||||
@simulator?.destroy()
|
||||
if ambientSound = @ambientSound
|
||||
# Doesn't seem to work; stops immediately.
|
||||
createjs.Tween.get(ambientSound).to({volume: 0.0}, 1500).call -> ambientSound.stop()
|
||||
|
|
|
@ -123,6 +123,7 @@ module.exports = class CastButtonView extends CocoView
|
|||
|
||||
onGoalsCalculated: (e) ->
|
||||
# When preloading, with real-time playback enabled, we highlight the submit button when we think they'll win.
|
||||
return unless e.god is @god
|
||||
return unless e.preload
|
||||
return if @options.level.get 'hidesRealTimePlayback'
|
||||
return if @options.level.get('slug') in ['course-thornbush-farm', 'thornbush-farm'] # Don't show it until they actually win for this first one.
|
||||
|
|
|
@ -47,7 +47,7 @@ module.exports = class Spell
|
|||
@source = @originalSource = p.aiSource
|
||||
@thangs = {}
|
||||
if @canRead() # We can avoid creating these views if we'll never use them.
|
||||
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker}
|
||||
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god}
|
||||
@view.render() # Get it ready and code loaded in advance
|
||||
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level
|
||||
@tabView.render()
|
||||
|
|
|
@ -883,6 +883,7 @@ module.exports = class SpellView extends CocoView
|
|||
@spellHasChanged = false
|
||||
|
||||
onUserCodeProblem: (e) ->
|
||||
return unless e.god is @options.god
|
||||
return @onInfiniteLoop e if e.problem.id is 'runtime_InfiniteLoop'
|
||||
return unless e.problem.userInfo.methodName is @spell.name
|
||||
return unless spellThang = _.find @spell.thangs, (spellThang, thangID) -> thangID is e.problem.userInfo.thangID
|
||||
|
@ -893,6 +894,7 @@ module.exports = class SpellView extends CocoView
|
|||
@updateAether false, false
|
||||
|
||||
onNonUserCodeProblem: (e) ->
|
||||
return unless e.god is @options.god
|
||||
return unless @spellThang
|
||||
problem = @spellThang.aether.createUserCodeProblem type: 'runtime', kind: 'Unhandled', message: "Unhandled error: #{e.problem.message}"
|
||||
@spellThang.aether.addProblem problem
|
||||
|
|
|
@ -62,7 +62,7 @@ module.exports = class TomeView extends CocoView
|
|||
@createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
|
||||
unless @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
|
||||
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
|
||||
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session
|
||||
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god
|
||||
@teamSpellMap = @generateTeamSpellMap(@spells)
|
||||
unless programmableThangs.length
|
||||
@cast()
|
||||
|
@ -136,6 +136,7 @@ module.exports = class TomeView extends CocoView
|
|||
observing: @options.observing
|
||||
levelID: @options.levelID
|
||||
level: @options.level
|
||||
god: @options.god
|
||||
|
||||
for thangID, spellKeys of @thangSpells
|
||||
thang = world.getThangByID thangID
|
||||
|
@ -168,7 +169,7 @@ module.exports = class TomeView extends CocoView
|
|||
difficulty = sessionState.difficulty ? 0
|
||||
if @options.observing
|
||||
difficulty = Math.max 0, difficulty - 1 # Show the difficulty they won, not the next one.
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty
|
||||
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: difficulty, god: @options.god
|
||||
|
||||
onToggleSpellList: (e) ->
|
||||
@spellList?.rerenderEntries()
|
||||
|
|
|
@ -10,7 +10,9 @@ module.exports = getTwoGames = (req, res) ->
|
|||
humansSessionID = req.body.humansGameID
|
||||
ogresSessionID = req.body.ogresGameID
|
||||
return getSpecificSessions res, humansSessionID, ogresSessionID if humansSessionID and ogresSessionID
|
||||
getRandomSessions req.user, sendSessionsResponse(res)
|
||||
options =
|
||||
background: req.body.background
|
||||
getRandomSessions req.user, options, sendSessionsResponse(res)
|
||||
|
||||
sessionSelectionString = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate leagues'
|
||||
|
||||
|
@ -33,18 +35,18 @@ getSpecificSession = (sessionID, callback) ->
|
|||
if err? then return callback "Couldn\'t find target simulation session #{sessionID}"
|
||||
callback null, session
|
||||
|
||||
getRandomSessions = (user, callback) ->
|
||||
getRandomSessions = (user, options, callback) ->
|
||||
# Determine whether to play a random match, an internal league match, or an external league match.
|
||||
# Only people in a league will end up simulating internal league matches (for leagues they're in) except by dumb chance.
|
||||
# If we don't like that, we can rework sampleByLevel to have an opportunity to switch to internal leagues if the first session had a league affiliation.
|
||||
leagueIDs = user?.get('clans') or []
|
||||
leagueIDs = leagueIDs.concat user?.get('courseInstances') or []
|
||||
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to fetch them as strings.
|
||||
return sampleByLevel callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
|
||||
return sampleByLevel options, callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
|
||||
leagueID = _.sample leagueIDs
|
||||
findRandomSession {'leagues.leagueID': leagueID}, (err, session) ->
|
||||
if err then return callback err
|
||||
unless session then return sampleByLevel callback
|
||||
unless session then return sampleByLevel options, callback
|
||||
otherTeam = scoringUtils.calculateOpposingTeam session.team
|
||||
queryParameters = team: otherTeam, levelID: session.levelID
|
||||
if Math.random() < 0.5
|
||||
|
@ -67,8 +69,9 @@ getRandomSessions = (user, callback) ->
|
|||
# Sampling by level: we pick a level, then find a human and ogre session for that level, one at random, one biased towards recent submissions.
|
||||
#ladderLevelIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span'] # Let's not give any extra simulations to old ladders.
|
||||
ladderLevelIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland', 'zero-sum', 'ace-of-coders', 'wakka-maul']
|
||||
sampleByLevel = (callback) ->
|
||||
levelID = _.sample ladderLevelIDs
|
||||
backgroundLadderLevelIDs = _.without ladderLevelIDs, 'zero-sum', 'ace-of-coders'
|
||||
sampleByLevel = (options, callback) ->
|
||||
levelID = _.sample(if options.background then backgroundLadderLevelIDs else ladderLevelIDs)
|
||||
favorRecentHumans = Math.random() < 0.5 # We pick one session favoring recent submissions, then find another one uniformly to play against
|
||||
async.map [{levelID: levelID, team: 'humans', favorRecent: favorRecentHumans}, {levelID: levelID, team: 'ogres', favorRecent: not favorRecentHumans}], findRandomSession, callback
|
||||
|
||||
|
|
Loading…
Reference in a new issue