mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-01 03:16:56 -05:00
225 lines
9.4 KiB
CoffeeScript
225 lines
9.4 KiB
CoffeeScript
{now} = require './world/world_utils'
|
|
|
|
## Uncomment to imitate IE9 (and in world_utils.coffee)
|
|
#window.Worker = null
|
|
#window.Float32Array = null
|
|
# (except that we won't have included vendor_with_box2d.js, so Collision won't run, things won't move, etc.)
|
|
|
|
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', 'Aether', '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']
|
|
@nextID: ->
|
|
@lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length
|
|
@ids[@lastID]
|
|
|
|
maxAngels: 2 # how many concurrent web workers to use; if set past 8, make up more names
|
|
worldWaiting: false # whether we're waiting for a worker to free up and run the world
|
|
constructor: (@world, @level) ->
|
|
@id = God.nextID()
|
|
@angels = []
|
|
@firstWorld = true
|
|
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
|
|
|
|
onTomeCast: (e) ->
|
|
return if @dead
|
|
@spells = e.spells
|
|
@createWorld()
|
|
|
|
getAngel: ->
|
|
for angel in @angels
|
|
return angel.enslave() unless angel.busy
|
|
maxedOut = @angels.length is @maxAngels
|
|
if not maxedOut
|
|
angel = new Angel @
|
|
@angels.push angel
|
|
return angel.enslave()
|
|
oldestAngel = {started: new Date(2099, 1, 1)}
|
|
for angel in @angels
|
|
oldestAngel = angel if angel.started < oldestAngel.started
|
|
oldestAngel.abort()
|
|
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
|
|
#console.log "UserCodeProblem:", '"' + problem.message + '"', "for", problem.userInfo.thangID, "-", problem.userInfo.methodName, 'at line', problem.ranges?[0][0][0], 'column', problem.ranges?[0][0][1]
|
|
Backbone.Mediator.publish 'god:user-code-problem', problem: problem
|
|
|
|
createWorld: ->
|
|
#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
|
|
angel.worker.postMessage {func: 'runWorld', args: {
|
|
worldName: @world.name
|
|
userCodeMap: @getUserCodeMap()
|
|
level: @level
|
|
firstWorld: @firstWorld
|
|
goals: @goalManager?.getGoals()
|
|
}}
|
|
|
|
beholdWorld: (angel, serialized, goalStates) ->
|
|
worldCreation = angel.started
|
|
angel.free()
|
|
return if @latestWorldCreation? and worldCreation < @latestWorldCreation
|
|
@latestWorldCreation = worldCreation
|
|
@latestGoalStates = goalStates
|
|
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.constructor.deserialize serialized, @world.classMap, @lastSerializedWorldFrames, worldCreation, @finishBeholdingWorld
|
|
window.BOX2D_ENABLED = true
|
|
@lastSerializedWorldFrames = serialized.frames
|
|
|
|
finishBeholdingWorld: (newWorld) =>
|
|
newWorld.findFirstChangedFrame @world
|
|
@world = newWorld
|
|
errorCount = (t for t in @world.thangs when t.errorsOut).length
|
|
Backbone.Mediator.publish('god:new-world-created', world: @world, firstWorld: @firstWorld, errorCount: errorCount, goalStates: @latestGoalStates)
|
|
for scriptNote in @world.scriptNotes
|
|
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
|
|
@firstWorld = false
|
|
|
|
getUserCodeMap: ->
|
|
userCodeMap = {}
|
|
for spellKey, spell of @spells
|
|
for thangID, spellThang of spell.thangs
|
|
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
|
|
userCodeMap
|
|
|
|
destroy: ->
|
|
angel.destroy() for angel in @angels
|
|
@dead = true
|
|
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
|
|
@goalManager = null
|
|
|
|
#### 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
|
|
@testWorld.constructor.deserialize serialized, @world.classMap, @lastSerializedWorldFrames, @t0, @finishBeholdingWorld
|
|
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: 15000 # check this often (must be more than the others added)
|
|
infiniteLoopTimeoutDuration: 1500 # wait this long when we check
|
|
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 = new Worker '/javascripts/workers/worker_world.js'
|
|
@listen()
|
|
|
|
enslave: ->
|
|
@busy = true
|
|
@started = new Date()
|
|
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
|
|
@
|
|
|
|
free: ->
|
|
@busy = false
|
|
@started = null
|
|
clearInterval @purgatoryTimer
|
|
@purgatoryTimer = null
|
|
@
|
|
|
|
abort: ->
|
|
@abortTimeout = _.delay @terminate, @abortTimeoutDuration
|
|
@worker.postMessage {func: 'abort'}
|
|
|
|
terminate: =>
|
|
@worker.terminate()
|
|
return if @dead
|
|
@spawnWorker()
|
|
@free()
|
|
@god.angelAborted @
|
|
|
|
destroy: ->
|
|
@dead = true
|
|
@abort()
|
|
|
|
testWorker: =>
|
|
@worker.postMessage {func: 'reportIn'}
|
|
@condemnTimeout = _.delay @condemnWorker, @infiniteLoopTimeoutDuration
|
|
|
|
condemnWorker: =>
|
|
@god.angelInfinitelyLooped @
|
|
@abort()
|
|
|
|
listen: ->
|
|
@worker.addEventListener 'message', (event) =>
|
|
switch event.data.type
|
|
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 @
|
|
@worker.terminate() if @god.dead
|
|
when 'reportIn'
|
|
clearTimeout @condemnTimeout
|
|
else
|
|
console.log "Unsupported message:", event.data
|