codecombat/app/lib/God.coffee

376 lines
14 KiB
CoffeeScript
Raw Normal View History

{now} = require 'lib/world/world_utils'
World = require 'lib/world/world'
2014-01-03 13:32:13 -05:00
## Uncomment to imitate IE9 (and in world_utils.coffee)
#window.Worker = null
#window.Float32Array = null
# Also uncomment vendor_with_box2d.js in index.html if you want Collision to run and things to move.
2014-01-03 13:32:13 -05:00
module.exports = class God
@ids: ['Athena', 'Baldr', 'Crom', 'Dagr', 'Eris', 'Freyja', 'Great Gish', 'Hades', 'Ishtar', 'Janus', 'Khronos', 'Loki', 'Marduk', 'Negafook', 'Odin', 'Poseidon', 'Quetzalcoatl', 'Ra', 'Shiva', 'Thor', 'Umvelinqangi', 'Týr', 'Vishnu', 'Wepwawet', 'Xipe Totec', 'Yahweh', 'Zeus', '上帝', 'Tiamat', '盘古', 'Phoebe', 'Artemis', 'Osiris', "嫦娥", 'Anhur', 'Teshub', 'Enlil', 'Perkele', 'Chaos', 'Hera', 'Iris', 'Theia', 'Uranus', 'Stribog', 'Sabazios', 'Izanagi', 'Ao', 'Tāwhirimātea', 'Tengri', 'Inmar', 'Torngarsuk', 'Centzonhuitznahua', 'Hunab Ku', 'Apollo', 'Helios', 'Thoth', 'Hyperion', 'Alectrona', 'Eos', 'Mitra', 'Saranyu', 'Freyr', 'Koyash', 'Atropos', 'Clotho', 'Lachesis', 'Tyche', 'Skuld', 'Urðr', 'Verðandi', 'Camaxtli', 'Huhetotl', 'Set', 'Anu', 'Allah', 'Anshar', 'Hermes', 'Lugh', 'Brigit', 'Manannan Mac Lir', 'Persephone', 'Mercury', 'Venus', 'Mars', 'Azrael', 'He-Man', 'Anansi', 'Issek', 'Mog', 'Kos', 'Amaterasu Omikami', 'Raijin', 'Susanowo', 'Blind Io', 'The Lady', 'Offler', 'Ptah', 'Anubis', 'Ereshkigal', 'Nergal', 'Thanatos', 'Macaria', 'Angelos', 'Erebus', 'Hecate', 'Hel', 'Orcus', 'Ishtar-Deela Nakh', 'Prometheus', 'Hephaestos', 'Sekhmet', 'Ares', 'Enyo', 'Otrera', 'Pele', 'Hadúr', 'Hachiman', 'Dayisun Tngri', 'Ullr', 'Lua', 'Minerva']
2014-01-03 13:32:13 -05:00
@nextID: ->
@lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length
@ids[@lastID]
worldWaiting: false # whether we're waiting for a worker to free up and run the world
constructor: (options) ->
2014-01-03 13:32:13 -05:00
@id = God.nextID()
2014-02-15 20:38:45 -05:00
options ?= {}
@maxAngels = options.maxAngels ? 2 # How many concurrent web workers to use; if set past 8, make up more names
@maxWorkerPoolSize = options.maxWorkerPoolSize ? 2 # ~20MB per idle worker
@workerCode = options.workerCode if options.workerCode?
2014-01-03 13:32:13 -05:00
@angels = []
@firstWorld = true
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
@retriveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
Backbone.Mediator.subscribe 'tome:spell-debug-value-request', @retrieveValueFromFrame, @
@fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false
@fillWorkerPool()
#TODO: have this as a constructor option
@debugWorker = @createDebugWorker()
2014-05-01 14:12:44 -04:00
@currentUserCodeMap = {}
2014-01-03 13:32:13 -05:00
workerCode: '/javascripts/workers/worker_world.js' #Can be a string or a function.
2014-01-03 13:32:13 -05:00
onTomeCast: (e) ->
return if @dead
@createWorld e.spells
2014-01-03 13:32:13 -05:00
fillWorkerPool: =>
return unless Worker and not @dead
@workerPool ?= []
if @workerPool.length < @maxWorkerPoolSize
@workerPool.push @createWorker()
if @workerPool.length < @maxWorkerPoolSize
@fillWorkerPool()
getWorker: ->
@fillWorkerPool()
worker = @workerPool?.shift()
return worker if worker
@createWorker()
createWorker: ->
worker = new Worker @workerCode
worker.creationTime = new Date()
worker.addEventListener 'message', @onWorkerMessage(worker)
worker
createDebugWorker: ->
2014-05-08 12:47:01 -04:00
worker = new Worker '/javascripts/workers/worker_world.js'
worker.creationTime = new Date()
worker.addEventListener 'message', @onDebugWorkerMessage
worker
onWorkerMessage: (worker) =>
unless worker.onMessage?
worker.onMessage = (event) =>
if event.data.type is 'worker-initialized'
console.log @id, "worker initialized after", ((new Date()) - worker.creationTime), "ms (before it was needed)"
worker.initialized = true
worker.removeEventListener 'message', worker.onMessage
else
console.warn "Received strange word from God: #{event.data.type}"
worker.onMessage
onDebugWorkerMessage: (event) =>
worker = event.target
switch event.data.type
when "worker-initialized"
worker.initialized = true
when 'new-debug-world'
2014-05-01 14:12:44 -04:00
console.log "New Debug world!"
when 'console-log'
2014-05-09 17:30:33 -04:00
console.log "|" + @id + "'s debugger|", event.data.args...
when 'debug-value-return'
Backbone.Mediator.publish 'god:debug-value-return', event.data.serialized
2014-01-03 13:32:13 -05:00
getAngel: ->
freeAngel = null
2014-01-03 13:32:13 -05:00
for angel in @angels
if angel.busy
angel.abort()
else
freeAngel ?= angel
return freeAngel.enslave() if freeAngel
2014-01-03 13:32:13 -05:00
maxedOut = @angels.length is @maxAngels
if not maxedOut
angel = new Angel @
@angels.push angel
return angel.enslave()
null
angelInfinitelyLooped: (angel) ->
return if @dead
problem = type: "runtime", level: "error", id: "runtime_InfiniteLoop", message: "Code never finished. It's either really slow or has an infinite loop."
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @firstWorld
angelAborted: (angel) ->
return unless @worldWaiting and not @dead
@createWorld()
angelUserCodeProblem: (angel, problem) ->
return if @dead
2014-05-08 14:43:00 -04:00
#console.log "UserCodeProblem:", '"' + problem.message + '"', "for", problem.userInfo.thangID, "-", problem.userInfo.methodName, 'at', problem.range?[0]
2014-01-03 13:32:13 -05:00
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
createWorld: (@spells) ->
2014-01-03 13:32:13 -05:00
#console.log @id + ': "Let there be light upon', @world.name + '!"'
unless Worker? # profiling world simulation is easier on main thread, or we are IE9
setTimeout @simulateWorld, 1
return
angel = @getAngel()
if angel
@worldWaiting = false
else
@worldWaiting = true
return
#console.log "going to run world with code", @getUserCodeMap()
2014-01-03 13:32:13 -05:00
angel.worker.postMessage {func: 'runWorld', args: {
worldName: @level.name
userCodeMap: @getUserCodeMap(spells)
2014-01-03 13:32:13 -05:00
level: @level
firstWorld: @firstWorld
goals: @goalManager?.getGoals()
}}
retrieveValueFromFrame: (args) ->
if not args.thangID or not args.spellID or not args.variableChain then return
args.frame ?= @world.age / @world.dt
@debugWorker.postMessage
2014-05-05 16:26:37 -04:00
func: 'retrieveValueFromFrame'
args:
worldName: @level.name
userCodeMap: @currentUserCodeMap
level: @level
goals: @goalManager?.getGoals()
frame: args.frame
currentThangID: args.thangID
currentSpellID: args.spellID
variableChain: args.variableChain
#Coffeescript needs getters and setters.
setGoalManager: (@goalManager) =>
setWorldClassMap: (@worldClassMap) =>
2014-01-03 13:32:13 -05:00
beholdWorld: (angel, serialized, goalStates) ->
unless serialized
# We're only interested in goalStates.
@latestGoalStates = goalStates
Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates, team: me.team)
unless _.find @angels, 'busy'
@spells = null # Don't hold onto old spells; memory leaks
return
console.log "Beholding world."
2014-01-03 13:32:13 -05:00
worldCreation = angel.started
angel.free()
return if @latestWorldCreation? and worldCreation < @latestWorldCreation
@latestWorldCreation = worldCreation
@latestGoalStates = goalStates
console.warn "Goal states: " + JSON.stringify(goalStates)
2014-01-03 13:32:13 -05:00
window.BOX2D_ENABLED = false # Flip this off so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment
World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld
2014-01-03 13:32:13 -05:00
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
2014-02-12 12:55:19 -05:00
finishBeholdingWorld: (newWorld) =>
2014-01-03 13:32:13 -05:00
newWorld.findFirstChangedFrame @world
@world = newWorld
@currentUserCodeMap = @filterUserCodeMapWhenFromWorld @world.userCodeMap
2014-01-03 13:32:13 -05:00
errorCount = (t for t in @world.thangs when t.errorsOut).length
2014-02-15 18:44:45 -05:00
Backbone.Mediator.publish('god:new-world-created', world: @world, firstWorld: @firstWorld, errorCount: errorCount, goalStates: @latestGoalStates, team: me.team)
2014-01-03 13:32:13 -05:00
for scriptNote in @world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@goalManager?.world = newWorld
2014-01-03 13:32:13 -05:00
@firstWorld = false
@testWorld = null
unless _.find @angels, 'busy'
@spells = null # Don't hold onto old spells; memory leaks
2014-01-03 13:32:13 -05:00
filterUserCodeMapWhenFromWorld: (worldUserCodeMap) ->
newUserCodeMap = {}
for thangName, thang of worldUserCodeMap
newUserCodeMap[thangName] = {}
for spellName,aether of thang
shallowFilteredObject = _.pick aether, ['raw','pure','originalOptions']
newUserCodeMap[thangName][spellName] = _.cloneDeep shallowFilteredObject
newUserCodeMap[thangName][spellName] = _.defaults newUserCodeMap[thangName][spellName],
flow: {}
metrics: {}
problems:
errors: []
infos: []
warnings: []
style: {}
newUserCodeMap
2014-01-03 13:32:13 -05:00
getUserCodeMap: ->
userCodeMap = {}
for spellKey, spell of @spells
for thangID, spellThang of spell.thangs
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
userCodeMap
destroy: ->
worker.removeEventListener 'message', @onWorkerMessage for worker in @workerPool ? []
2014-01-03 13:32:13 -05:00
angel.destroy() for angel in @angels
@dead = true
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
@goalManager?.destroy()
@debugWorker?.terminate()
@debugWorker?.removeEventListener 'message', @onDebugWorkerMessage
@debugWorker ?= null
@currentUserCodeMap = null
2014-01-03 13:32:13 -05:00
@goalManager = null
2014-02-12 15:41:41 -05:00
@fillWorkerPool = null
@simulateWorld = null
@onWorkerMessage = null
2014-01-03 13:32:13 -05:00
#### Bad code for running worlds on main thread (profiling / IE9) ####
simulateWorld: =>
if Worker?
console?.profile? "World Generation #{(Math.random() * 1000).toFixed(0)}"
@t0 = now()
@testWorld = new @world.constructor @world.name, @getUserCodeMap()
@testWorld.loadFromLevel @level
if @goalManager
@testGM = new @goalManager.constructor @testWorld
@testGM.setGoals @goalManager.getGoals()
@testGM.setCode @getUserCodeMap()
@testGM.worldGenerationWillBegin()
@testWorld.setGoalManager @testGM
@doSimulateWorld()
if Worker?
console?.profileEnd?()
console.log "Construction:", (@t1 - @t0).toFixed(0), "ms. Simulation:", (@t2 - @t1).toFixed(0), "ms --", ((@t2 - @t1) / @testWorld.frames.length).toFixed(3), "ms per frame, profiled."
# If performance was really a priority in IE9, we would rework things to be able to skip this step.
@latestGoalStates = @testGM?.getGoalStates()
serialized = @testWorld.serialize().serializedWorld
window.BOX2D_ENABLED = false
World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld
2014-01-03 13:32:13 -05:00
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
doSimulateWorld: ->
@t1 = now()
Math.random = @testWorld.rand.randf # so user code is predictable
i = 0
while i < @testWorld.totalFrames
frame = @testWorld.getFrame i++
@testWorld.ended = true
system.finish @testWorld.thangs for system in @testWorld.systems
@t2 = now()
#### End bad testing code ####
class Angel
@ids: ['Archer', 'Lana', 'Cyril', 'Pam', 'Cheryl', 'Woodhouse', 'Ray', 'Krieger']
@nextID: ->
@lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length
@ids[@lastID]
# https://github.com/codecombat/codecombat/issues/81 -- TODO: we need to wait for worker initialization first
infiniteLoopIntervalDuration: 7500 # check this often (must be more than the others added)
infiniteLoopTimeoutDuration: 2500 # wait this long when we check
2014-01-03 13:32:13 -05:00
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
constructor: (@god) ->
@id = Angel.nextID()
if (navigator.userAgent or navigator.vendor or window.opera).search("MSIE") isnt -1
@infiniteLoopIntervalDuration *= 20 # since it's so slow to serialize without transferable objects, we can't trust it
@infiniteLoopTimeoutDuration *= 20
@abortTimeoutDuration *= 10
@spawnWorker()
spawnWorker: ->
@worker = @god.getWorker()
2014-01-03 13:32:13 -05:00
@listen()
enslave: ->
@busy = true
@started = new Date()
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
@spawnWorker() unless @worker
2014-01-03 13:32:13 -05:00
@
free: ->
@busy = false
@started = null
clearInterval @purgatoryTimer
@purgatoryTimer = null
if @worker
worker = @worker
onWorkerMessage = @onWorkerMessage
_.delay ->
worker.terminate()
worker.removeEventListener 'message', onWorkerMessage
, 3000
@worker = null
2014-01-03 13:32:13 -05:00
@
abort: ->
2014-02-11 14:30:47 -05:00
return unless @worker
2014-01-03 13:32:13 -05:00
@abortTimeout = _.delay @terminate, @abortTimeoutDuration
@worker.postMessage {func: 'abort'}
terminate: =>
2014-02-06 20:31:08 -05:00
@worker?.terminate()
2014-02-12 15:41:41 -05:00
@worker?.removeEventListener 'message', @onWorkerMessage
@worker = null
2014-01-03 13:32:13 -05:00
return if @dead
@free()
@god.angelAborted @
destroy: ->
@dead = true
2014-02-12 12:55:19 -05:00
@finishBeholdingWorld = null
2014-01-03 13:32:13 -05:00
@abort()
2014-02-12 15:41:41 -05:00
@terminate = null
@testWorker = null
@condemnWorker = null
@onWorkerMessage = null
2014-01-03 13:32:13 -05:00
testWorker: =>
unless @worker.initialized
console.warn "Worker", @id, " hadn't even loaded the scripts yet after", @infiniteLoopIntervalDuration, "ms."
return
2014-01-03 13:32:13 -05:00
@worker.postMessage {func: 'reportIn'}
@condemnTimeout = _.delay @condemnWorker, @infiniteLoopTimeoutDuration
condemnWorker: =>
@god.angelInfinitelyLooped @
@abort()
listen: ->
2014-02-12 15:41:41 -05:00
@worker.addEventListener 'message', @onWorkerMessage
onWorkerMessage: (event) =>
switch event.data.type
when 'worker-initialized'
console.log "Worker", @id, "initialized after", ((new Date()) - @worker.creationTime), "ms (we had been waiting for it)"
@worker.initialized = true
2014-02-12 15:41:41 -05:00
when 'new-world'
@god.beholdWorld @, event.data.serialized, event.data.goalStates
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data unless @dead
when 'console-log'
console.log "|" + @god.id + "'s " + @id + "|", event.data.args...
when 'user-code-problem'
@god.angelUserCodeProblem @, event.data.problem
when 'abort'
#console.log @id, "aborted."
clearTimeout @abortTimeout
@free()
@god.angelAborted @
when 'reportIn'
clearTimeout @condemnTimeout
else
console.log "Unsupported message:", event.data