mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
e8665f7299
23 changed files with 230 additions and 75 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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,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()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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