Merge branch 'master' of git://github.com/domenukk/codecombat

This commit is contained in:
Nick Winter 2014-05-06 08:00:39 -07:00
commit 1f3cc8a004
13 changed files with 903 additions and 52 deletions

9
.gitignore vendored
View file

@ -28,6 +28,9 @@ Thumbs.db
*.sublime-project
*.sublime-workspace
# IntelliJ/WebStorm
*.iml
# NPM packages folder.
node_modules/
bower_components/
@ -77,4 +80,10 @@ bin/mongo/
# windows
/SCOCODE.bat
# local settings
login.coffee
# debugging
*.heapsnapshot
### If you add something here, copy it to the end of .npmignore, too. ###

View file

@ -53,6 +53,9 @@ Thumbs.db
*.sublime-project
*.sublime-workspace
# IntelliJ/WebStorm
*.iml
# NPM packages folder.
node_modules/
@ -89,6 +92,12 @@ mongo/
bin/node/
bin/mongo/
# Karma coverage
coverage/
# local settings
login.coffee
# debugging
*.heapsnapshot

241
app/lib/Buddha.coffee Normal file
View file

@ -0,0 +1,241 @@
#Sane rewrite of God (a thread pool)
{now} = require 'lib/world/world_utils'
World = require 'lib/world/world'
###
Every Angel has exactly one WebWorker attached to it.
It will call methods inside the webwrker and kill it if it times out.
###
class Angel
@cyanide: 0xDEADBEEF
infiniteLoopIntervalDuration: 7500 # check this often (must be more than the others added)
infiniteLoopTimeoutDuration: 10000 # wait this long when we check
abortTimeoutDuration: 500 # give in-process or dying workers this long to give up
constructor: (@id, @shared) ->
console.log @id + ": Creating Angel"
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
@initialized = false
@running = false
@hireWorker()
@shared.angels.push @
testWorker: =>
if @initialized
@worker.postMessage {func: 'reportIn'}
# Are there any errors when webworker isn't loaded properly?
onWorkerMessage: (event) =>
#console.log JSON.stringify event
if @aborting and not
event.data.type is 'abort'
console.log id + " is currently aborting old work."
return
switch event.data.type
when 'start-load-frames'
clearTimeout(@condemnTimeout)
@condemnTimeout = _.delay @infinitelyLooped, @infiniteLoopTimeoutDuration
when 'end-load-frames'
console.log @id + ': No condemn this time.'
clearTimeout(@condemnTimeout)
when 'worker-initialized'
unless @initialized
console.log @id + ": Worker initialized after", ((new Date()) - @worker.creationTime), "ms"
@initialized = true
@doWork()
when 'new-world'
@beholdWorld event.data.serialized, event.data.goalStates
when 'world-load-progress-changed'
Backbone.Mediator.publish 'god:world-load-progress-changed', event.data
when 'console-log'
console.log "|" + @id + "|", event.data.args...
when 'user-code-problem'
Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem
when 'abort'
console.log @id, "aborted."
clearTimeout @abortTimeout
@aborting = false
@running = false
@shared.busyAngels.pop @
@doWork()
when 'reportIn'
clearTimeout @condemnTimeout
else
console.log @id + " received unsupported message:", event.data
beholdWorld: (serialized, goalStates) ->
return if @aborting
unless serialized
# We're only interested in goalStates. (Simulator)
@latestGoalStates = goalStates;
Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates)
@running = false
@shared.busyAngels.pop @
# console.warn "Goal states: " + JSON.stringify(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.deserialize serialized, @shared.worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld(goalStates)
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
finishBeholdingWorld: (goalStates) => (world) =>
return if @aborting
world.findFirstChangedFrame @shared.world
@shared.world = world
errorCount = (t for t in @shared.world.thangs when t.errorsOut).length
Backbone.Mediator.publish('god:new-world-created', world: world, firstWorld: @shared.firstWorld, errorCount: errorCount, goalStates: goalStates)
for scriptNote in @shared.world.scriptNotes
Backbone.Mediator.publish scriptNote.channel, scriptNote.event
@shared.goalManager?.world = world
@running = false
@shared.busyAngels.pop @
@shared.firstWorld = false;
@doWork()
infinitelyLooped: =>
unless @aborting
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: @shared.firstWorld
@fireWorker()
workIfIdle: ->
@doWork() unless @running
doWork: =>
#console.log "work."
return if @aborted
console.log @id + " ready and looking for work. WorkQueue lenght is " + @shared.workQueue.length
if @initialized and @shared.workQueue.length
work = @shared.workQueue.pop()
if work is Angel.cyanide # Kill all other Angels, too
console.log @id + ": 'work is poison'"
@shared.workQueue.push Angel.cyanide
@free()
else
console.log @id + ": Sending the worker to work."
@running = true
@shared.busyAngels.push @
console.log "Running world..."
#console.error "worker.postMessage: " + @worker.postMessage + ", work: " + work
@worker.postMessage func: 'runWorld', args: work
console.log @id + ": Setting interval."
clearTimeout @purgatoryTimer
@purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration
else
console.log "No work for " + @id
@hireWorker()
abort: =>
if @worker and @running
console.log "Aborting " + @id
@running = false
@shared.busyAngels.pop @
@abortTimeout = _.delay @terminate, @fireWorker, @abortTimeoutDuration
@worker.postMessage func: 'abort'
@aborting = true
@work = null
fireWorker: (rehire=true) =>
@aborting = false
@running = false
@shared.busyAngels.pop @
@worker?.removeEventListener 'message', @onWorkerMessage
@worker?.terminate()
@worker = null
clearTimeout @condemnTimeout
clearInterval @purgatoryTimer
console.log "Fired worker."
@initialized = false
@work = null
@hireWorker() if rehire
hireWorker: ->
unless @worker
console.log @id + ": Hiring worker."
@worker = new Worker @shared.workerCode
@worker.addEventListener 'message', @onWorkerMessage
@worker.creationTime = new Date()
#@worker.postMessage func: 'initialized' else
kill: ->
@fireWorker false
@shared.angels.pop @
clearTimeout @condemnTimeout
clearTimeout @purgatoryTimer
@purgatoryTimer = null
@condemnTimeout = null
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']
nextID: ->
@lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length
@ids[@lastID]
# Charlie's Angels are all given access to this.
angelsShare: {
workerCode: '/javascripts/workers/worker_world.js' # Either path or function
workQueue: []
firstWorld: true
world: undefined
goalManager: undefined
worldClassMap: undefined
angels: []
busyAngels: [] # Busy angels will automatically register here.
}
constructor: (options) ->
options ?= {}
@angelsShare.workerCode = options.workerCode if options.workerCode
# ~20MB per idle worker + angel overhead - in this implementation, every Angel maps to 1 worker
angelCount = options.maxAngels ? options.maxWorkerPoolSize ? 2 # How many concurrent Angels/web workers to use at a time
_.delay (=>new Angel @nextID(), @angelsShare), 250 * i for i in [0...angelCount] # Don't generate all Angels at once.
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
onTomeCast: (e) ->
@createWorld e.spells
setGoalManager: (goalManager) =>
@angelsShare.goalManager = goalManager
setWorldClassMap: (worldClassMap) =>
@angelsShare.worldClassMap = worldClassMap
getUserCodeMap: (spells) ->
userCodeMap = {}
for spellKey, spell of spells
for thangID, spellThang of spell.thangs
(userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize()
#console.log userCodeMap
userCodeMap
createWorld: (spells) =>
angel.abort() for angel in @angelsShare.busyAngels # We really only ever want one world calculated per God
#console.log "Level: " + @level
@angelsShare.workQueue.push
worldName: @level.name
userCodeMap: @getUserCodeMap(spells)
level: @level
goals: @angelsShare.goalManager?.getGoals()
angel.workIfIdle() for angel in @angelsShare.angels
destroy: =>
console.log "Destroying Buddha"
@createWorld = -> console.log "CreateWorld already gone."
@angelsShare.workQueue.push Angel.cyanide
angel.kill for angel in @angelsShare.busyAngels
Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @)
@angelsShare.goalManager?.destroy()
@angelsShare.goalManager = null
@angelsShare = null

View file

@ -18,16 +18,18 @@ module.exports = class God
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?
@angels = []
@firstWorld = true
Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @
@fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false
@fillWorkerPool()
workerCode: '/javascripts/workers/worker_world.js' #Can be a string or a function.
onTomeCast: (e) ->
return if @dead
@spells = e.spells
@createWorld()
@createWorld e.spells
fillWorkerPool: =>
return unless Worker and not @dead
@ -44,17 +46,21 @@ module.exports = class God
@createWorker()
createWorker: ->
worker = new Worker '/javascripts/workers/worker_world.js'
worker = new Worker @workerCode
worker.creationTime = new Date()
worker.addEventListener 'message', @onWorkerMessage
worker.addEventListener 'message', @onWorkerMessage(worker)
worker
onWorkerMessage: (event) =>
worker = event.target
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)"
console.log @id, "worker initialized after", ((new Date()) - worker.creationTime), "ms (before it was needed)"
worker.initialized = true
worker.removeEventListener 'message', @onWorkerMessage
worker.removeEventListener 'message', worker.onMessage
else
console.warn "Received strange word from God: #{event.data.type}"
worker.onMessage
getAngel: ->
freeAngel = null
@ -86,7 +92,7 @@ module.exports = class God
#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: ->
createWorld: (spells) ->
#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
@ -101,20 +107,37 @@ module.exports = class God
#console.log "going to run world with code", @getUserCodeMap()
angel.worker.postMessage {func: 'runWorld', args: {
worldName: @level.name
userCodeMap: @getUserCodeMap()
userCodeMap: @getUserCodeMap(spells)
level: @level
firstWorld: @firstWorld
goals: @goalManager?.getGoals()
}}
#Coffeescript needs getters and setters.
setGoalManager: (@goalManager) =>
setWorldClassMap: (@worldClassMap) =>
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."
worldCreation = angel.started
angel.free()
return if @latestWorldCreation? and worldCreation < @latestWorldCreation
@latestWorldCreation = worldCreation
@latestGoalStates = goalStates
console.warn "Goal states: " + JSON.stringify(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.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, worldCreation, @finishBeholdingWorld
World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
@ -171,7 +194,7 @@ module.exports = class God
@latestGoalStates = @testGM?.getGoalStates()
serialized = @testWorld.serialize().serializedWorld
window.BOX2D_ENABLED = false
World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @t0, @finishBeholdingWorld
World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld
window.BOX2D_ENABLED = true
@lastSerializedWorldFrames = serialized.frames
@ -271,6 +294,7 @@ class Angel
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
when 'new-world'
@god.beholdWorld @, event.data.serialized, event.data.goalStates
when 'world-load-progress-changed'

View file

@ -2,15 +2,20 @@ SuperModel = require 'models/SuperModel'
CocoClass = require 'lib/CocoClass'
LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
God = require 'lib/God'
God = require 'lib/Buddha'
module.exports = class Simulator extends CocoClass
constructor: ->
constructor: (workerCode) ->
_.extend @, Backbone.Events
@trigger 'statusUpdate', 'Starting simulation!'
@retryDelayInSeconds = 10
@taskURL = '/queue/scoring'
@simulatedByYou = 0
if workerCode
@god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: workerCode # Start loading worker.
else
@god = new God maxWorkerPoolSize: 1, maxAngels: 1
destroy: ->
@off()
@ -19,6 +24,17 @@ module.exports = class Simulator extends CocoClass
fetchAndSimulateTask: =>
return if @destroyed
if headless
if @dumpThisTime # The first heapdump would be useless to find leaks.
console.log "Writing snapshot."
heapdump.writeSnapshot()
@dumpThisTime = true if heapdump
if testing
_.delay @setupSimulationAndLoadLevel, 0, testFile, "Testing...", status: 400
return
@trigger 'statusUpdate', 'Fetching simulation data!'
$.ajax
url: @taskURL
@ -32,7 +48,9 @@ module.exports = class Simulator extends CocoClass
@simulateAnotherTaskAfterDelay()
handleNoGamesResponse: ->
@trigger 'statusUpdate', 'There were no games to simulate--all simulations are done or in process. Retrying in 10 seconds.'
info = 'There were no games to simulate--all simulations are done or in process. Retrying in 10 seconds.'
console.log info
@trigger 'statusUpdate', info
@simulateAnotherTaskAfterDelay()
simulateAnotherTaskAfterDelay: =>
@ -53,7 +71,6 @@ module.exports = class Simulator extends CocoClass
return
@supermodel ?= new SuperModel()
@god = new God maxWorkerPoolSize: 1, maxAngels: 1 # Start loading worker.
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true
if @supermodel.finished()
@ -63,7 +80,9 @@ module.exports = class Simulator extends CocoClass
simulateGame: ->
return if @destroyed
@trigger 'statusUpdate', 'All resources loaded, simulating!', @task.getSessions()
info = 'All resources loaded, simulating!'
console.log info
@trigger 'statusUpdate', info, @task.getSessions()
@assignWorldAndLevelFromLevelLoaderAndDestroyIt()
@setupGod()
@ -74,6 +93,7 @@ module.exports = class Simulator extends CocoClass
@simulateAnotherTaskAfterDelay()
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
console.log "Assigning world and level"
@world = @levelLoader.world
@level = @levelLoader.level
@levelLoader.destroy()
@ -81,18 +101,45 @@ module.exports = class Simulator extends CocoClass
setupGod: ->
@god.level = @level.serialize @supermodel
@god.worldClassMap = @world.classMap
@god.setWorldClassMap @world.classMap
@setupGoalManager()
@setupGodSpells()
setupGoalManager: ->
@god.goalManager = new GoalManager @world, @level.get 'goals'
@god.setGoalManager new GoalManager(@world, @level.get 'goals')
commenceSimulationAndSetupCallback: ->
@god.createWorld()
@god.createWorld @generateSpellsObject()
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @
Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @
#Search for leaks, headless-client only.
if headless and leaktest and not @memwatch?
leakcount = 0
maxleakcount = 0
console.log "Setting leak callbacks."
@memwatch = require 'memwatch'
@memwatch.on 'leak', (info) =>
console.warn "LEAK!!\n" + JSON.stringify(info)
unless @hd?
if (leakcount++ is maxleakcount)
@hd = new @memwatch.HeapDiff()
@memwatch.on 'stats', (stats) =>
console.warn "stats callback: " + stats
diff = @hd.end()
console.warn "HeapDiff:\n" + JSON.stringify(diff)
if exitOnLeak
console.warn "Exiting because of Leak."
process.exit()
@hd = new @memwatch.HeapDiff()
onInfiniteLoop: ->
console.warn "Skipping infinitely looping game."
@trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds."
@ -106,6 +153,9 @@ module.exports = class Simulator extends CocoClass
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
console.log "Sending result back to server!"
if headless and testing
return @fetchAndSimulateTask()
$.ajax
url: "/queue/scoring"
data: results
@ -117,6 +167,9 @@ module.exports = class Simulator extends CocoClass
handleTaskResultsTransferSuccess: (result) =>
console.log "Task registration result: #{JSON.stringify result}"
@trigger 'statusUpdate', 'Results were successfully sent back to server!'
console.log "Simulated by you: " + @simulatedByYou
@simulatedByYou++
if not headless
simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1
$('#simulated-by-you').text(simulatedBy)
@ -125,15 +178,8 @@ module.exports = class Simulator extends CocoClass
console.log "Task registration error: #{JSON.stringify error}"
cleanupAndSimulateAnotherTask: =>
@cleanupSimulation()
@fetchAndSimulateTask()
cleanupSimulation: ->
@god?.destroy()
@god = null
@world = null
@level = null
formTaskResultsObject: (simulationResults) ->
taskResults =
taskID: @task.getTaskID()
@ -144,7 +190,6 @@ module.exports = class Simulator extends CocoClass
sessions: []
for session in @task.getSessions()
sessionResult =
sessionID: session.sessionID
submitDate: session.submitDate

View file

@ -72,7 +72,7 @@ module.exports = class World
(@runtimeErrors ?= []).push error
(@unhandledRuntimeErrors ?= []).push error
loadFrames: (loadedCallback, errorCallback, loadProgressCallback) ->
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading) ->
return if @aborted
unless @thangs.length
console.log "Warning: loadFrames called on empty World (no thangs)."
@ -96,7 +96,11 @@ module.exports = class World
if t2 - @t0 > 1000
console.log(' Loaded', i, 'of', @totalFrames, "(+" + (t2 - @t0).toFixed(0) + "ms)")
@t0 = t2
setTimeout((=> @loadFrames(loadedCallback, errorCallback, loadProgressCallback)), 0)
continueFn = => @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading)
if skipDeferredLoading
continueFn()
else
setTimeout(continueFn, 0)
return
@ended = true
system.finish @thangs for system in @systems
@ -336,7 +340,7 @@ module.exports = class World
console.log "Whoa, serializing a lot of WorldScriptNotes here:", o.scriptNotes.length
{serializedWorld: o, transferableObjects: [o.storageBuffer]}
@deserialize: (o, classMap, oldSerializedWorldFrames, worldCreationTime, finishedWorldCallback) ->
@deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback) ->
# Code hotspot; optimize it
#console.log "Deserializing", o, "length", JSON.stringify(o).length
#console.log JSON.stringify(o)

View file

@ -1,6 +1,5 @@
storage = require 'lib/storage'
deltasLib = require 'lib/deltas'
auth = require 'lib/auth'
class CocoModel extends Backbone.Model
idAttribute: "_id"
@ -9,6 +8,8 @@ class CocoModel extends Backbone.Model
saveBackups: false
@schema: null
getMe: -> @me or @me = require('lib/auth').me
initialize: ->
super()
if not @constructor.className
@ -96,7 +97,8 @@ class CocoModel extends Backbone.Model
not _.isEqual @attributes, @_revertAttributes
cloneNewMinorVersion: ->
newData = $.extend(null, {}, @attributes)
newData = _.clone @attributes
clone = new @constructor(newData)
clone
@ -136,7 +138,7 @@ class CocoModel extends Backbone.Model
hasReadAccess: (actor) ->
# actor is a User object
actor ?= auth.me
actor ?= @getMe()
return true if actor.isAdmin()
if @get('permissions')?
for permission in @get('permissions')
@ -148,7 +150,7 @@ class CocoModel extends Backbone.Model
hasWriteAccess: (actor) ->
# actor is a User object
actor ?= auth.me
actor ?= @getMe()
return true if actor.isAdmin()
if @get('permissions')?
for permission in @get('permissions')

View file

@ -12,7 +12,7 @@ Surface = require 'lib/surface/Surface'
God = require 'lib/God'
GoalManager = require 'lib/world/GoalManager'
ScriptManager = require 'lib/scripts/ScriptManager'
LevelBus = require('lib/LevelBus')
LevelBus = require 'lib/LevelBus'
LevelLoader = require 'lib/LevelLoader'
LevelSession = require 'models/LevelSession'
Level = require 'models/Level'
@ -112,8 +112,8 @@ module.exports = class PlayLevelView extends View
load: ->
@loadStartTime = new Date()
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team")
@god = new God()
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team")
getRenderData: ->
c = super()
@ -169,7 +169,7 @@ module.exports = class PlayLevelView extends View
team = @getQueryVariable("team") ? @world.teamForPlayer(0)
@loadOpponentTeam(team)
@god.level = @level.serialize @supermodel
@god.worldClassMap = @world.classMap
@god.setWorldClassMap @world.classMap
@setTeam team
@initSurface()
@initGoalManager()
@ -427,7 +427,7 @@ module.exports = class PlayLevelView extends View
initGoalManager: ->
@goalManager = new GoalManager(@world, @level.get('goals'))
@god.goalManager = @goalManager
@god.setGoalManager @goalManager
initScriptManager: ->
@scriptManager = new ScriptManager({scripts: @world.scripts or [], view:@, session: @session})

View file

@ -8,7 +8,7 @@ World = require 'lib/world/world'
# tools
Surface = require 'lib/surface/Surface'
God = require 'lib/God'
God = require 'lib/Buddha' # 'lib/God'
GoalManager = require 'lib/world/GoalManager'
ScriptManager = require 'lib/scripts/ScriptManager'
LevelLoader = require 'lib/LevelLoader'
@ -156,7 +156,7 @@ module.exports = class SpectateLevelView extends View
team = @world.teamForPlayer(0)
@loadOpponentTeam(team)
@god.level = @level.serialize @supermodel
@god.worldClassMap = @world.classMap
@god.setWorldClassMap @world.classMap
@setTeam team
@initSurface()
@initGoalManager()
@ -387,7 +387,7 @@ module.exports = class SpectateLevelView extends View
initGoalManager: ->
@goalManager = new GoalManager(@world, @level.get('goals'))
@god.goalManager = @goalManager
@god.setGoalManager @goalManager
initScriptManager: ->
if @world.scripts

217
headless_client.coffee Normal file
View file

@ -0,0 +1,217 @@
###
This file will simulate games on node.js by emulating the browser environment.
At some point, most of the code can be merged with Simulator.coffee
###
# SETTINGS
GLOBAL.debug = false # Enable logging of ajax calls mainly
GLOBAL.testing = false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting.
GLOBAL.leaktest = false # Install callback that tries to find leaks automatically
GLOBAL.exitOnLeak = false # Exit if leak is found. Only useful if leaktest is set to true, obviously.
GLOBAL.heapdump = false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser.
server = if testing then "http://127.0.0.1:3000" else "http://codecombat.com"
# Disabled modules
disable = [
'lib/AudioPlayer'
'locale/locale'
'../locale/locale'
]
GLOBAL.bowerComponents = "./bower_components/"
GLOBAL.headlessClient = "./headless_client/"
# Start of the actual code. Setting up the enivronment to match the environment of the browser
heapdump = require('heapdump') if heapdump
# the path used for the loader. __dirname is module dependent.
path = __dirname
m = require 'module'
request = require 'request'
originalLoader = m._load
unhook = () ->
m._load = originalLoader
hook = () ->
m._load = hookedLoader
JASON = require 'jason'
# Global emulated stuff
GLOBAL.window = GLOBAL
GLOBAL.headless = true
GLOBAL.document = location: pathname: "headless_client"
GLOBAL.console.debug = console.log
GLOBAL.Worker = require('webworker-threads').Worker
Worker::removeEventListener = (what) ->
if what is 'message'
@onmessage = -> #This webworker api has only one event listener at a time.
GLOBAL.tv4 = require('tv4').tv4
GLOBAL.marked = setOptions: ->
GLOBAL.navigator =
# userAgent: "nodejs"
platform: "headless_client"
vendor: "codecombat"
opera: false
store = {}
GLOBAL.localStorage =
getItem: (key) => store[key]
setItem: (key, s) => store[key] = s
removeItem: (key) => delete store[key]
# Hook node.js require. See https://github.com/mfncooper/mockery/blob/master/mockery.js
# The signature of this function *must* match that of Node's Module._load,
# since it will replace that.
# (Why is there no easier way?)
hookedLoader = (request, parent, isMain) ->
if request == 'lib/God'
request = 'lib/Buddha'
if request in disable or ~request.indexOf('templates')
console.log 'Ignored ' + request if debug
return class fake
else if '/' in request and not (request[0] is '.') or request is 'application'
request = path + '/app/' + request
else if request is 'underscore'
request = 'lodash'
console.log "loading " + request if debug
originalLoader request, parent, isMain
#jQuery wrapped for compatibility purposes. Poorly.
GLOBAL.$ = GLOBAL.jQuery = (input) ->
console.log 'Ignored jQuery: ' + input if debug
append: (input)-> exports: ()->
cookies = request.jar()
$.ajax = (options) ->
responded = false
url = options.url
if url.indexOf('http')
url = '/' + url unless url[0] is '/'
url = server + url
data = options.data
#if (typeof data) is 'object'
#console.warn JSON.stringify data
#data = JSON.stringify data
console.log "Requesting: " + JSON.stringify options if debug
console.log "URL: " + url if debug
request
url: url
jar: cookies
json: options.parse
method: options.type
body: data
, (error, response, body) ->
console.log "HTTP Request:" + JSON.stringify options if debug and not error
if responded
console.log "\t↳Already returned before." if debug
return
if (error)
console.warn "\t↳Returned: error: #{error}"
options.error(error) if options.error?
else
console.log "\t↳Returned: statusCode #{response.statusCode}: #{if options.parse then JSON.stringify body else body}" if debug
options.success(body, response, status: response.statusCode) if options.success?
statusCode = response.statusCode if response?
options.complete(status: statusCode) if options.complete?
responded = true
$.extend = (deep, into, from) ->
copy = _.clone(from, deep);
if into
_.assign into, copy
copy = into
copy
$.isArray = (object) ->
_.isArray object
$.isPlainObject = (object) ->
_.isPlainObject object
do (setupLodash = this) ->
GLOBAL._ = require 'lodash'
_.str = require 'underscore.string'
_.string = _.str
_.mixin _.str.exports()
# load Backbone. Needs hooked loader to reroute underscore to lodash.
hook()
GLOBAL.Backbone = require bowerComponents + 'backbone/backbone'
unhook()
Backbone.$ = $
require bowerComponents + 'validated-backbone-mediator/backbone-mediator'
# Instead of mediator, dummy might be faster yet suffice?
#Mediator = class Mediator
# publish: (id, object) ->
# console.Log "Published #{id}: #{object}"
# @subscribe: () ->
# @unsubscribe: () ->
GLOBAL.Aether = require 'aether'
# Set up new loader.
hook()
login = require './login.coffee' #should contain an object containing they keys 'username' and 'password'
#Login user and start the code.
$.ajax
url: '/auth/login'
type: "POST"
data: login
parse: true
error: (error) -> "Bad Error. Can't connect to server or something. " + error
success: (response) ->
console.log "User: " + response
GLOBAL.window.userObject = response # JSON.parse response
User = require 'models/User'
World = require 'lib/world/world'
LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
workerCode = require headlessClient + 'worker_world'
SuperModel = require 'models/SuperModel'
log = require 'winston'
CocoClass = require 'lib/CocoClass'
Simulator = require 'lib/simulator/Simulator'
GLOBAL.testFile = require headlessClient + 'test.js'
sim = new Simulator workerCode
sim.fetchAndSimulateTask()

87
headless_client/test.js Normal file
View file

@ -0,0 +1,87 @@
module.exports = {
"messageGenerated": 1396792689279,
"sessions": [
{
"sessionID": "533a2c4893b95d9319a58049",
"submitDate": "2014-04-06T06:31:11.806Z",
"team": "humans",
"code": {
"ogre-base": {
"chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// Choose your hero! You can only build one hero.\nvar hero;\n//hero = 'ironjaw'; // A leaping juggernaut hero, type 'brawler'.\nhero = 'yugargen'; // A devious spellcaster hero, type 'shaman'.\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Munchkins are weak melee units with 1.25s build cooldown.\n// Throwers are fragile, deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['munchkin', 'thrower', 'munchkin', 'thrower', 'munchkin', 'thrower'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);\n//this.say(\"Move\", {to:{x:20, y:30}});{x: 68, y: 29}{x: 70, y: 30}"
},
"programmable-shaman": {
"chooseAction": "if (this.hero !== undefined) {\n this.hero = this.getNearest(enemy);\n}\n// Shamans are spellcasters with a weak magic attack\n// and three spells: 'shrink', 'grow', and 'poison-cloud'.\n// Shrink: target has 2/3 health, 1.5x speed for 5s.\n// Grow: target has double health, half speed for 5s.\n// Once per match, she can cast poison cloud, which does\n// 5 poison dps for 10s to enemies in a 10m radius.\nvar right = 0;\nif(right === 0){this.move({x: 70, y: 40});\n}\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\nif(this.canCast('shrink', enemy)) \n{\n this.castShrink(enemy);\n}\nelse\n{\n this.castGrow(friend);\n}\n\nvar enemiesinpoisonrange = 0;\nfor (var i = 0; i < enemies.lenght; ++i) {\n var enemi = enemies[i];\n if (this.distance(enemi) <= 10) {\n enemiesinpoisonrange++;\n }\n}\nif (enemiesinpoisonrange >= 7) {\n this.castPoisonCloud(enemy);\n}\n//if (this.distance(ogrebase) > 10) {\n// this.move({x: 70, y: 30});\n//}\n//this.say(\"Defend!\", {targetPos: {x: 45, y: 30}});\n\n//this.say(\"Defend!\", {targetPos: {x: 35, y: 30}});\n\n//this.say(\"Defend!\", {targetPos: {x: 25, y: 30}});\n\n//this.say(\"Attack!\", {to:{x:20, y:30}});\n\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('shrink', enemy)) this.castShrink(enemy);\n//if(this.canCast('grow', friend)) this.castGrow(friend);\n//if(this.canCast('poison-cloud', enemy)) this.castPoisonCloud(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 45, y: 30}});\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});"
},
"programmable-brawler": {
"chooseAction": "// The Brawler is a huge melee hero with mighty mass.\n// this.throw() hurls an enemy behind him.\n// this.jumpTo() leaps to a target within 20m every 10s.\n// this.stomp() knocks everyone away, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('jump')) this.jumpTo(enemy.pos);\n//if(!this.getCooldown('stomp') && this.distance(enemy) < 10) this.stomp();\n//if(!this.getCooldown('throw')) this.throw(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;{x: 68, y: 29}{x: 70, y: 30}"
},
"programmable-librarian": {
"chooseAction": "var enemies = this.getEnemies();\nif (enemies.length === 0)\n return;\nvar enemy = this.getNearest(enemies);\nvar friends = this.getFriends();\nvar friend = this.getNearest(friends);\nvar archer = this.getFriends(type, \"archer\");\nvar soldier = this.getFriends(type, \"soldier\");\nvar hero = this.getFriends(type, \"hushbaum\");\nvar rand = Math.random();\nvar xmove;\nvar ymove;\nfor (var i = 0; i < enemies.length / 3; i += 1) {\n var e = enemies[i];\n var ehealth = Math.floor(e.health);\n if (this.canCast(\"haste\", friend)) {\n this.say(\"Godspeed \" + friend.id + \"!\");\n this.castHaste(friend);\n }\n if (this.canCast(\"haste\", this)) {\n this.say(\"I am Godspeed!\");\n this.castHaste(this);\n }\n if (this.canCast(\"slow\", e)) {\n this.say(\"Chill Out \" + e.id + \"!\");\n this.castSlow(e);\n }\n if (this.distance(e) < 45) {\n this.attack(e);\n this.say(\"Attacking \" + e.id + \" life is \" + ehealth + \".\");\n }\n if (this.health < this.maxHealth * 0.75) {\n if (this.pos.x > 20) {\n this.move({\n x: this.pos.x - 20,\n y: this.pos.y\n });\n } else {\n this.move({\n x: this.pos.x + 20,\n y: this.pos.y\n });\n }\n }\n if (this.canCast(\"regen\", this)) {\n this.castRegen(this);\n this.say(\"I won't die today bitch!\");\n }\n if (friend.health < friend.maxHealth * 0.5) {\n if (this.canCast(\"regen\", friend)) {\n this.say(\"You won't die today \" + friend.id + \".\");\n this.castRegen(friend);\n }\n }\n}\n;"
},
"programmable-tharin": {
"chooseAction": "// Tharin is a melee fighter with shield, warcry, and terrify skills.\n// this.shield() lets him take one-third damage while defending.\n// this.warcry() gives allies within 10m 30% haste for 5s, every 10s.\n// this.terrify() sends foes within 30m fleeing for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('warcry')) this.warcry();\n//if(!this.getCooldown('terrify')) this.terrify();\n//this.shield();\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 40, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;"
},
"human-base": {
"chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// CHOOSE YOUR HERO! You can only build one hero.\nvar hero;\n//hero = 'tharin'; // A fierce knight with battlecry abilities.\nhero = 'hushbaum'; // A fiery spellcaster hero.\n\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Soldiers are hard-to-kill, low damage melee units with 2s build cooldown.\n// Archers are fragile but deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['soldier', 'soldier', 'archer', 'archer', 'soldier', 'soldier'];\nvar type = buildOrder[this.built.length % buildOrder.length];\nthis.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);\n\n "
}
},
"teamSpells": {
"ogres": [
"programmable-brawler/chooseAction",
"programmable-shaman/chooseAction",
"ogre-base/chooseAction"
],
"humans": [
"programmable-librarian/chooseAction",
"programmable-tharin/chooseAction",
"human-base/chooseAction"
]
},
"levelID": "dungeon-arena",
"creator": "5338c38c4811eff221de2347",
"creatorName": "iC0DE"
},
{
"sessionID": "532a777c2042708b711a6c29",
"submitDate": "2014-03-20T05:45:54.691Z",
"team": "ogres",
"code": {
"ogre-base": {
"chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// Choose your hero! You can only build one hero.\nvar hero;\n//hero = 'ironjaw'; // A leaping juggernaut hero, type 'brawler'.\nhero = 'yugargen'; // A devious spellcaster hero, type 'shaman'.\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Munchkins are weak melee units with 1.25s build cooldown.\n// Throwers are fragile, deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['munchkin', 'munchkin', 'munchkin', 'thrower'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);"
},
"programmable-shaman": {
"chooseAction": "// Shamans are spellcasters with a weak magic attack\n// and three spells: 'shrink', 'grow', and 'poison-cloud'.\n// Shrink: target has 2/3 health, 1.5x speed for 5s.\n// Grow: target has double health, half speed for 5s.\n// Once per match, she can cast poison cloud, which does\n// 5 poison dps for 10s to enemies in a 10m radius.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) {\n return; // Chill if all enemies are dead.\n}\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\nif (enemies.length > 5) {\n if(this.canCast('poison-cloud', enemy)) {\n this.castPoisonCloud(enemy);\n return;\n }\n}\n\nif (friends.length > 4) {\n this.attack(enemy); \n}\nfor (var i = 0; i < friends.length; ++i) {\n if (friends[i].health < 0) {\n continue;\n }\n if(friends[i].type == \"thrower\" && this.canCast('shrink', friends[i])) {\n this.castShrink(friends[i]);\n return;\n } \n if(friends[i].type == \"munchkin\" && this.canCast('grow', friends[i])) {\n this.castGrow(friends[i]);\n return;\n } \n}\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('shrink', enemy)) this.castShrink(enemy);\n//if(this.canCast('grow', friend)) this.castGrow(friend);\n//if(this.canCast('poison-cloud', enemy)) this.castPoisonCloud(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});"
},
"programmable-brawler": {
"chooseAction": "// The Brawler is a huge melee hero with mighty mass.\n// this.throw() hurls an enemy behind him.\n// this.jumpTo() leaps to a target within 20m every 10s.\n// this.stomp() knocks everyone away, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('jump')) this.jumpTo(enemy.pos);\n//if(!this.getCooldown('stomp') && this.distance(enemy) < 10) this.stomp();\n//if(!this.getCooldown('throw')) this.throw(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;"
},
"human-base": {
"chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// CHOOSE YOUR HERO! You can only build one hero.\nvar hero;\nhero = 'tharin'; // A fierce knight with battlecry abilities.\n//hero = 'hushbaum'; // A fiery spellcaster hero.\n\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Soldiers are hard-to-kill, low damage melee units with 2s build cooldown.\n// Archers are fragile but deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['archer', 'archer', 'soldier', 'archer', 'soldier'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);"
},
"programmable-tharin": {
"chooseAction": "this.findTypeInRange = function(units, type) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if (unit.type === type && this.distance(unit) < 20)\n return unit;\n }\n return null;\n};\n\nthis.findType = function(units, type) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if (unit.type === type)\n return unit;\n }\n return null;\n};\n\nthis.findHeroInRange = function(units, range) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if ((unit.type === 'shaman' || unit.type === 'brawler') && this.distance(unit) < range)\n return unit;\n }\n return null;\n};\n\n// Tharin is a melee fighter with shield, warcry, and terrify skills.\n// this.shield() lets him take one-third damage while defending.\n// this.warcry() gives allies within 10m 30% haste for 5s, every 10s.\n// this.terrify() sends foes within 30m fleeing for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\n\n//Enemies\nvar enemyBase = this.findType(enemies, 'base');\nvar brawler = this.findTypeInRange(enemies, 'brawler');\nvar shaman = this.findTypeInRange(enemies, 'shaman');\n\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('warcry')) this.warcry();\n//if(!this.getCooldown('terrify')) this.terrify();\n//this.shield();\n\nif((brawler || shaman) && !this.attackTime)\n{\n this.attackTime = true;\n if(brawler)\n this.say(\"Attack!\", {target: brawler});\n else if(shaman)\n this.say(\"Attack!\", {target: shaman});\n}\nelse if(this.health < 15 && this.getCooldown('terrify'))\n{\n this.terrify();\n}\nelse if(this.findHeroInRange(enemies, 30) && this.getCooldown('terrify'))\n{\n this.terrify();\n}\nelse if(this.health < 25)\n{\n this.shield();\n}\nelse if(brawler && this.distance(brawler) <=10)\n{\n this.attack(brawler);\n}\nelse\n{\n this.attack(enemy);\n}\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 40, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;"
},
"programmable-librarian": {
"chooseAction": "// The Librarian is a spellcaster with a fireball attack\n// plus three useful spells: 'slow', 'regen', and 'haste'.\n// Slow makes a target move and attack at half speed for 5s.\n// Regen makes a target heal 10 hp/s for 10s.\n// Haste speeds up a target by 4x for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('slow', enemy)) this.castSlow(enemy);\n//if(this.canCast('regen', friend)) this.castRegen(friend);\n//if(this.canCast('haste', friend)) this.castHaste(friend);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});"
}
},
"teamSpells": {
"ogres": [
"programmable-brawler/chooseAction",
"programmable-shaman/chooseAction",
"ogre-base/chooseAction"
],
"humans": [
"programmable-librarian/chooseAction",
"programmable-tharin/chooseAction",
"human-base/chooseAction"
]
},
"levelID": "dungeon-arena",
"creator": "53291a80b112e7240f324667",
"creatorName": "Imbal Oceanrage"
}
],
"taskID": "53415d71942d00aa43dbf3e9",
"receiptHandle": "cd50e44db7dbd4cc0bcce047aa822ba2fe3556cf"
}

View file

@ -0,0 +1,209 @@
# function to use inside a webworker.
# This function needs to run inside an environment that has a 'self'.
# This specific worker is targeted towards the node.js headless_client environment.
JASON = require 'jason'
fs = require 'fs'
betterConsole = () ->
self.logLimit = 200;
self.logsLogged = 0;
self.transferableSupported = () -> true
self.console = log: ->
if self.logsLogged++ is self.logLimit
self.postMessage
type: "console-log"
args: ["Log limit " + self.logLimit + " reached; shutting up."]
id: self.workerID
else if self.logsLogged < self.logLimit
args = [].slice.call(arguments)
i = 0
while i < args.length
args[i] = args[i].toString() if args[i].constructor.className is "Thang" or args[i].isComponent if args[i] and args[i].constructor
++i
try
self.postMessage
type: "console-log"
args: args
id: self.workerID
catch error
self.postMessage
type: "console-log"
args: [
"Could not post log: " + args
error.toString()
error.stack
error.stackTrace
]
id: self.workerID
# so that we don't crash when debugging statements happen
self.console.error = self.console.info = self.console.log
GLOBAL.console = console = self.console
self.console
work = () ->
console.log "starting..."
console.log = ->
World = self.require('lib/world/world');
GoalManager = self.require('lib/world/GoalManager');
self.cleanUp = ->
self.world = null
self.goalManager = null
self.postedErrors = {}
self.t0 = null
self.logsLogged = 0
self.runWorld = (args) ->
console.log "Running world inside worker."
self.postedErrors = {}
self.t0 = new Date()
self.postedErrors = false
self.logsLogged = 0
try
self.world = new World(args.worldName, args.userCodeMap)
self.world.loadFromLevel args.level, true if args.level
self.goalManager = new GoalManager(self.world)
self.goalManager.setGoals args.goals
self.goalManager.setCode args.userCodeMap
self.goalManager.worldGenerationWillBegin()
self.world.setGoalManager self.goalManager
catch error
console.log "There has been an error inside the worker."
self.onWorldError error
return
Math.random = self.world.rand.randf # so user code is predictable
console.log "Loading frames."
self.postMessage type: "start-load-frames"
self.world.loadFrames self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress, true
self.onWorldLoaded = onWorldLoaded = ->
self.postMessage type: "end-load-frames"
self.goalManager.worldGenerationEnded()
t1 = new Date()
diff = t1 - self.t0
transferableSupported = self.transferableSupported()
try
serialized = serializedWorld: undefined # self.world.serialize()
transferableSupported = false
catch error
console.log "World serialization error:", error.toString() + "\n" + error.stack or error.stackTrace
t2 = new Date()
# console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects);
try
if transferableSupported
self.postMessage
type: "new-world"
serialized: serialized.serializedWorld
goalStates: self.goalManager.getGoalStates()
, serialized.transferableObjects
else
self.postMessage
type: "new-world"
serialized: serialized.serializedWorld
goalStates: self.goalManager.getGoalStates()
catch error
console.log "World delivery error:", error.toString() + "\n" + error.stack or error.stackTrace
t3 = new Date()
console.log "And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms"
self.cleanUp()
self.onWorldError = onWorldError = (error) ->
self.postMessage type: "end-load-frames"
if error instanceof Aether.problems.UserCodeProblem
#console.log "Aether userCodeProblem occured."
unless self.postedErrors[error.key]
problem = error.serialize()
self.postMessage
type: "user-code-problem"
problem: problem
self.postedErrors[error.key] = problem
else
console.log "Non-UserCodeError:", error.toString() + "\n" + error.stack or error.stackTrace
self.cleanUp()
self.onWorldLoadProgress = onWorldLoadProgress = (progress) ->
#console.log "Worker onWorldLoadProgress"
self.postMessage
type: "world-load-progress-changed"
progress: progress
self.abort = abort = ->
#console.log "Abort called for worker."
if self.world and self.world.name
#console.log "About to abort:", self.world.name, typeof self.world.abort
self.world.abort() if typeof self.world isnt "undefined"
self.world = null
self.postMessage type: "abort"
self.cleanUp()
self.reportIn = reportIn = ->
console.log "Reporting in."
self.postMessage type: "reportIn"
self.addEventListener "message", (event) ->
#console.log JSON.stringify event
self[event.data.func] event.data.args
self.postMessage type: "worker-initialized"
world = fs.readFileSync "./public/javascripts/world.js", 'utf8'
#window.BOX2D_ENABLED = true;
newConsole = "newConsole = #{}JASON.stringify newConsole}()";
ret = """
GLOBAL = root = window = self;
GLOBAL.window = window;
self.workerID = "Worker";
console = #{JASON.stringify betterConsole}();
try {
// the world javascript file
#{world};
// Don't let user generated code access stuff from our file system!
self.importScripts = importScripts = null;
self.native_fs_ = native_fs_ = null;
// the actual function
#{JASON.stringify work}();
}catch (error) {
self.postMessage({"type": "console-log", args: ["An unhandled error occured: ", error.toString(), error.stack], id: -1});
}
"""
#console = #{JASON.stringify createConsole}();
#
# console.error = console.info = console.log;
#self.console = console;
#GLOBAL.console = console;
module.exports = new Function(ret)

View file

@ -62,7 +62,11 @@
"sendwithus": "2.0.x",
"aws-sdk": "~2.0.0",
"bayesian-battle": "0.0.x",
"redis": ""
"redis": "",
"webworker-threads": "~0.4.11",
"node-gyp": "~0.13.0",
"aether": "~0.1.18",
"JASON": "~0.1.3"
},
"devDependencies": {
"jade": "0.33.x",