Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-11-29 12:33:16 -08:00
commit e8665f7299
23 changed files with 230 additions and 75 deletions

View file

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

View file

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

View file

@ -93,11 +93,13 @@ class AudioPlayer extends CocoClass
return defaults[message.length % defaults.length]
preloadInterfaceSounds: (names) ->
return unless me.get 'volume'
for name in names
filename = "/file/interface/#{name}#{@ext}"
@preloadSound filename, name
playInterfaceSound: (name, volume=1) ->
return unless volume and me.get 'volume'
filename = "/file/interface/#{name}#{@ext}"
if @hasLoadedSound filename
@playSound name, volume
@ -107,6 +109,7 @@ class AudioPlayer extends CocoClass
playSound: (name, volume=1, delay=0, pos=null) ->
return console.error 'Trying to play empty sound?' unless name
return unless volume and me.get 'volume'
audioOptions = {volume: volume, delay: delay}
filename = if _.string.startsWith(name, '/file/') then name else '/file/' + name
unless @hasLoadedSound filename
@ -120,9 +123,8 @@ class AudioPlayer extends CocoClass
return false unless createjs.Sound.loadComplete filename
true
# TODO: load Interface sounds somehow, somewhere, somewhen
preloadSoundReference: (sound) ->
return unless me.get 'volume'
return unless name = @nameForSoundReference sound
filename = '/file/' + name
@preloadSound filename, name

View file

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

View file

@ -418,20 +418,23 @@ module.exports = class LevelLoader extends CocoClass
# Initial Sound Loading
playJingle: ->
return if @headless
return if @headless or not me.get('volume')
volume = 0.5
if me.level() < 3
volume = 0.25 # Start softly, since they may not be expecting it
# Apparently the jingle, when it tries to play immediately during all this loading, you can't hear it.
# Add the timeout to fix this weird behavior.
f = ->
jingles = ['ident_1', 'ident_2']
AudioPlayer.playInterfaceSound jingles[Math.floor Math.random() * jingles.length]
AudioPlayer.playInterfaceSound jingles[Math.floor Math.random() * jingles.length], volume
setTimeout f, 500
loadAudio: ->
return if @headless
return if @headless or not me.get('volume')
AudioPlayer.preloadInterfaceSounds ['victory']
loadLevelSounds: ->
return if @headless
return if @headless or not me.get('volume')
scripts = @level.get 'scripts'
return unless scripts

View file

@ -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()
if @options.background or @noTasks
@fetchAndSimulateOneGame()
else
@fetchAndSimulateTask()
cleanupSimulation: ->
@stopListening @god
@world = null
@level = null

View file

@ -16,6 +16,7 @@ module.exports = class MusicPlayer extends CocoClass
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
'music-player:enter-menu': 'onEnterMenu'
'music-player:exit-menu': 'onExitMenu'
'level:set-volume': 'onSetVolume'
constructor: ->
super arguments...
@ -26,6 +27,9 @@ module.exports = class MusicPlayer extends CocoClass
onPlayMusic: (e) ->
return if application.isIPadApp # Hard to measure, but just guessing this will save memory.
unless me.get 'volume'
@lastMusicEventIgnoredWhileMuted = e
return
src = e.file
src = "/file#{src}#{AudioPlayer.ext}"
if (not e.file) or src is @currentMusic?.src
@ -97,6 +101,11 @@ module.exports = class MusicPlayer extends CocoClass
@currentMusic = @previousMusic
@restartCurrentMusic()
onSetVolume: (e) ->
return unless e.volume and @lastMusicEventIgnoredWhileMuted
@onPlayMusic @lastMusicEventIgnoredWhileMuted
@lastMusicEventIgnoredWhileMuted = null
destroy: ->
me.off 'change:music', @onMusicSettingChanged, @
@fadeOutCurrentMusic()

View file

@ -49,7 +49,6 @@ module.exports = Surface = class Surface extends CocoClass
navigateToSelection: true
choosing: false # 'point', 'region', 'ratio-region'
coords: null # use world defaults, or set to false/true to override
playJingle: false
showInvisible: false
frameRate: 30 # Best as a divisor of 60, like 15, 30, 60, with RAF_SYNCHED timing.

View file

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

View file

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

View file

@ -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: {}

View file

@ -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']},

View file

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

View file

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

View file

@ -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
@ -196,7 +196,7 @@ module.exports = class SpectateLevelView extends RootView
initSurface: ->
webGLSurface = $('canvas#webgl-surface', @$el)
normalSurface = $('canvas#normal-surface', @$el)
@surface = new Surface @world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview, spectateGame: true, playerNames: @findPlayerNames()
@surface = new Surface @world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), spectateGame: true, playerNames: @findPlayerNames()
worldBounds = @world.getBounds()
bounds = [{x:worldBounds.left, y:worldBounds.top}, {x:worldBounds.right, y:worldBounds.bottom}]
@surface.camera.setBounds(bounds)

View file

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

View file

@ -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'
@ -49,7 +50,7 @@ module.exports = class PlayLevelView extends RootView
isEditorPreview: false
subscriptions:
'level:set-volume': (e) -> createjs.Sound.setVolume(if e.volume is 1 then 0.6 else e.volume) # Quieter for now until individual sound FX controls work again.
'level:set-volume': 'onSetVolume'
'level:show-victory': 'onShowVictory'
'level:restart': 'onRestartLevel'
'level:highlight-dom': 'onHighlightDOM'
@ -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']
@ -321,7 +324,7 @@ module.exports = class PlayLevelView extends RootView
initSurface: ->
webGLSurface = $('canvas#webgl-surface', @$el)
normalSurface = $('canvas#normal-surface', @$el)
@surface = new Surface(@world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), playJingle: not @isEditorPreview, observing: @observing, playerNames: @findPlayerNames())
@surface = new Surface(@world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), observing: @observing, playerNames: @findPlayerNames())
worldBounds = @world.getBounds()
bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
@surface.camera.setBounds(bounds)
@ -365,10 +368,17 @@ 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
onSetVolume: (e) ->
createjs.Sound.setVolume(if e.volume is 1 then 0.6 else e.volume) # Quieter for now until individual sound FX controls work again.
if e.volume and not @ambientSound
@playAmbientSound()
playAmbientSound: ->
return if @destroyed
return if @ambientSound
return unless me.get 'volume'
return unless file = {Dungeon: 'ambient-dungeon', Grass: 'ambient-grass'}[@level.get('terrain')]
src = "/file/interface/#{file}#{AudioPlayer.ext}"
unless AudioPlayer.getStatus(src)?.loaded
@ -384,6 +394,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 +526,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 +626,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()

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,53 @@
// Adds up all concept statistics for all non-anoymous users.
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
var alreadyCompleted = 0; // For simple skipping and restarting
addConceptStatsToUsers();
function addConceptStatsToUsers() {
print("Adding concept stats to all non-anonymous users...");
var levels = db.levels.find({slug: {$exists: true}}, {campaign: 1, slug: 1, original: 1, concepts: 1, 'state.complete': 1}).toArray();
levels = levels.filter(function(level) { return level.campaign && level.concepts && level.concepts.length; });
var conceptMap = {};
levels.forEach(function(level) { conceptMap[level.original + ''] = level.concepts || []; });
print("Got concept map for", levels.length, "levels.");
var users = db.users.find({emailLower: {$exists: true}});
var usersTotal = users.count();
var usersDone = 0;
var t0 = null;
users.forEach(function(user) {
if (usersDone < alreadyCompleted) return ++usersDone;
if (!t0) t0 = new Date(); // Started processing users, so start the timer.
user.stats = user.stats || {};
user.stats.concepts = {};
var sessions = db.level.sessions.find({creator: user._id + ''});
sessions.forEach(function(session) {
if (!session.state || !session.state.complete) return;
var concepts = conceptMap[session.level.original + ''];
if (!concepts) return;
concepts.forEach(function(concept) {
user.stats.concepts[concept] = (user.stats.concepts[concept] || 0) + 1;
});
});
//print("Would say", user.name, user.email, "learned concepts", JSON.stringify(user.stats.concepts));
db.users.save(user);
if (++usersDone % 100 == 0) {
var t1 = new Date();
var elapsedSeconds = Math.round((t1 - t0) / 1000);
var remainingSeconds = Math.round((usersTotal - usersDone) / ((usersDone - alreadyCompleted) / elapsedSeconds));
print(usersDone, "\t/", usersTotal, "\t-- ", (100 * usersDone / usersTotal).toFixed(4) + "%\tElapsed:", toHHMMSS(elapsedSeconds), "\tRemaining:", toHHMMSS(remainingSeconds));
}
});
}
function toHHMMSS(rawSeconds) {
var hours = Math.floor(rawSeconds / 3600);
var minutes = Math.floor((rawSeconds - (hours * 3600)) / 60);
var seconds = rawSeconds - (hours * 3600) - (minutes * 60);
if (hours < 10) hours = "0" + hours;
if (minutes < 10) minutes = "0" + minutes;
if (seconds < 10) seconds = "0" + seconds;
return hours + ':' + minutes + ':' + seconds;
}

View file

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