mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Created headless-client and an alternative implementation of god
This commit is contained in:
parent
82529c781b
commit
c9bb488794
16 changed files with 1236 additions and 51 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -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
|
||||
|
||||
### If you add something here, copy it to the end of .npmignore, too. ###
|
||||
# local settings
|
||||
login.coffee
|
||||
|
||||
# debugging
|
||||
*.heapsnapshot
|
||||
|
||||
### If you add something here, copy it to the end of .npmignore, too. ###
|
||||
|
|
11
.npmignore
11
.npmignore
|
@ -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
|
||||
|
||||
|
|
243
app/lib/Buddha.coffee
Normal file
243
app/lib/Buddha.coffee
Normal file
|
@ -0,0 +1,243 @@
|
|||
#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..."
|
||||
@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
|
||||
|
||||
#TODO: self.world.totalFrames??
|
||||
#TODO: Don't show arguments.
|
|
@ -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
|
||||
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', @onWorkerMessage
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
@ -255,7 +278,7 @@ class Angel
|
|||
|
||||
testWorker: =>
|
||||
unless @worker.initialized
|
||||
console.warn "Worker", @id, "hadn't even loaded the scripts yet after", @infiniteLoopIntervalDuration, "ms."
|
||||
console.warn "Worker", @id, " hadn't even loaded the scripts yet after", @infiniteLoopIntervalDuration, "ms."
|
||||
return
|
||||
@worker.postMessage {func: 'reportIn'}
|
||||
@condemnTimeout = _.delay @condemnWorker, @infiniteLoopTimeoutDuration
|
||||
|
@ -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'
|
||||
|
|
|
@ -103,10 +103,8 @@ module.exports.getConflicts = (headDeltas, pendingDeltas) ->
|
|||
pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas)
|
||||
paths = _.keys(headPathMap).concat(_.keys(pendingPathMap))
|
||||
|
||||
# Here's my thinking:
|
||||
# A) Conflicts happen when one delta path is a substring of another delta path
|
||||
# B) A delta from one self-consistent group cannot conflict with another
|
||||
# So, sort the paths, which will naturally make conflicts adjacent,
|
||||
# Here's my thinking: conflicts happen when one delta path is a substring of another delta path
|
||||
# So, sort paths from both deltas together, which will naturally make conflicts adjacent,
|
||||
# and if one is identified, one path is from the headDeltas, the other is from pendingDeltas
|
||||
# This is all to avoid an O(nm) brute force search.
|
||||
|
||||
|
@ -141,7 +139,27 @@ groupDeltasByAffectingPaths = (deltas) ->
|
|||
delta: delta
|
||||
path: (item.toString() for item in path).join('/')
|
||||
}
|
||||
_.groupBy metaDeltas, 'path'
|
||||
|
||||
map = _.groupBy metaDeltas, 'path'
|
||||
|
||||
# Turns out there are cases where a single delta can include paths
|
||||
# that 'conflict' with each other, ie one is a substring of the other
|
||||
# because of moved indices. To handle this case, go through and prune
|
||||
# out all deeper paths that conflict with more shallow paths, so
|
||||
# getConflicts path checking works properly.
|
||||
|
||||
paths = _.keys(map)
|
||||
return map unless paths.length
|
||||
paths.sort()
|
||||
prunedMap = {}
|
||||
previousPath = paths[0]
|
||||
for path, i in paths
|
||||
continue if i is 0
|
||||
continue if path.startsWith previousPath
|
||||
prunedMap[path] = map[path]
|
||||
previousPath = path
|
||||
|
||||
prunedMap
|
||||
|
||||
module.exports.pruneConflictsFromDelta = (delta, conflicts) ->
|
||||
# the jsondiffpatch delta mustn't include any dangling nodes,
|
||||
|
|
|
@ -2,7 +2,7 @@ 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
|
||||
|
||||
|
@ -53,7 +53,8 @@ module.exports = class Simulator extends CocoClass
|
|||
return
|
||||
|
||||
@supermodel ?= new SuperModel()
|
||||
@god = new God maxWorkerPoolSize: 1, maxAngels: 1 # Start loading worker.
|
||||
|
||||
@god = new God maxAngels: 2 # Start loading worker.
|
||||
|
||||
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true
|
||||
if @supermodel.finished()
|
||||
|
@ -81,15 +82,18 @@ 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'
|
||||
goalManager = new GoalManager @world
|
||||
goalManager.goals = @god.level.goals
|
||||
goalManager.goalStates = @manuallyGenerateGoalStates()
|
||||
@god.setGoalManager goalManager
|
||||
|
||||
commenceSimulationAndSetupCallback: ->
|
||||
@god.createWorld()
|
||||
@god.createWorld @generateSpellsObject()
|
||||
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @
|
||||
Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @
|
||||
|
||||
|
@ -174,10 +178,6 @@ module.exports = class Simulator extends CocoClass
|
|||
else
|
||||
return 1
|
||||
|
||||
setupGodSpells: ->
|
||||
@generateSpellsObject()
|
||||
@god.spells = @spells
|
||||
|
||||
generateSpellsObject: ->
|
||||
@currentUserCodeMap = @task.generateSpellKeyToSourceMap()
|
||||
@spells = {}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
@ -83,7 +84,7 @@ class CocoModel extends Backbone.Model
|
|||
if @type() is 'ThangType'
|
||||
@_revertAttributes = _.clone @attributes # No deep clones for these!
|
||||
else
|
||||
@_revertAttributes = $.extend(true, {}, @attributes)
|
||||
@_revertAttributes = _.cloneDeep(@attributes)
|
||||
|
||||
revert: ->
|
||||
@set(@_revertAttributes, {silent: true}) if @_revertAttributes
|
||||
|
@ -96,7 +97,8 @@ class CocoModel extends Backbone.Model
|
|||
not _.isEqual @attributes, @_revertAttributes
|
||||
|
||||
cloneNewMinorVersion: ->
|
||||
newData = $.extend(null, {}, @attributes)
|
||||
newData = _.clone @attributes # needs to be deep?
|
||||
|
||||
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')
|
||||
|
|
|
@ -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,10 @@ 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")
|
||||
#@listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded)
|
||||
#@listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged)
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
|
@ -174,7 +176,7 @@ module.exports = class PlayLevelView extends View
|
|||
@initSurface()
|
||||
@initGoalManager()
|
||||
@initScriptManager()
|
||||
@insertSubviews()
|
||||
@insertSubviews ladderGame: (@level.get('type') is "ladder")
|
||||
@initVolume()
|
||||
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
|
||||
@originalSessionState = $.extend(true, {}, @session.get('state'))
|
||||
|
|
|
@ -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
|
||||
|
|
574
headless_client.coffee
Normal file
574
headless_client.coffee
Normal file
|
@ -0,0 +1,574 @@
|
|||
###
|
||||
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
|
||||
debug = false # Enable logging of ajax calls mainly
|
||||
testing = false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting.
|
||||
leaktest = false # Install callback that tries to find leaks automatically
|
||||
exitOnLeak = false # Exit if leak is found. Only useful if leaktest is set to true, obviously.
|
||||
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'
|
||||
]
|
||||
|
||||
bowerComponents = "./bower_components/"
|
||||
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.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 is 'lib/god'
|
||||
# console.log 'I choose you, SimpleGod.'
|
||||
# request = './headless_client/SimpleGod'
|
||||
#else
|
||||
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'
|
||||
|
||||
God = require 'lib/Buddha'
|
||||
|
||||
workerCode = require headlessClient + 'worker_world'
|
||||
|
||||
SuperModel = require 'models/SuperModel'
|
||||
|
||||
log = require 'winston'
|
||||
|
||||
CocoClass = require 'lib/CocoClass'
|
||||
|
||||
class Simulator extends CocoClass
|
||||
|
||||
constructor: ->
|
||||
_.extend @, Backbone.Events
|
||||
@trigger 'statusUpdate', 'Starting simulation!'
|
||||
@retryDelayInSeconds = 10
|
||||
@taskURL = 'queue/scoring'
|
||||
@simulatedByYou = 0
|
||||
|
||||
@god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: workerCode # Start loading worker.
|
||||
|
||||
destroy: ->
|
||||
@off()
|
||||
@cleanupSimulation()
|
||||
super()
|
||||
|
||||
fetchAndSimulateTask: =>
|
||||
return if @destroyed
|
||||
|
||||
if testing
|
||||
test = require headlessClient + 'test.js'
|
||||
console.log test
|
||||
_.delay @setupSimulationAndLoadLevel, 0, test, "Testing...", status: 400
|
||||
return
|
||||
|
||||
if @ranonce and heapdump
|
||||
console.log "Writing snapshot."
|
||||
heapdump.writeSnapshot()
|
||||
@ranonce = true
|
||||
|
||||
@trigger 'statusUpdate', 'Fetching simulation data!'
|
||||
$.ajax
|
||||
url: @taskURL
|
||||
type: "GET"
|
||||
parse: true
|
||||
error: @handleFetchTaskError
|
||||
success: @setupSimulationAndLoadLevel
|
||||
|
||||
handleFetchTaskError: (errorData) =>
|
||||
console.error "There was a horrible Error: #{JSON.stringify errorData}"
|
||||
@trigger 'statusUpdate', 'There was an error fetching games to simulate. Retrying in 10 seconds.'
|
||||
@simulateAnotherTaskAfterDelay()
|
||||
|
||||
handleNoGamesResponse: ->
|
||||
console.log "Nothing to do."
|
||||
@trigger 'statusUpdate', 'There were no games to simulate--nice. Retrying in 10 seconds.'
|
||||
@simulateAnotherTaskAfterDelay()
|
||||
|
||||
simulateAnotherTaskAfterDelay: =>
|
||||
console.log "Retrying..."
|
||||
console.log "Retrying in #{@retryDelayInSeconds}"
|
||||
retryDelayInMilliseconds = @retryDelayInSeconds * 1000
|
||||
_.delay @fetchAndSimulateTask, retryDelayInMilliseconds
|
||||
|
||||
setupSimulationAndLoadLevel: (taskData, textStatus, jqXHR) =>
|
||||
return @handleNoGamesResponse() if jqXHR.status is 204
|
||||
@trigger 'statusUpdate', 'Setting up simulation!'
|
||||
|
||||
@task = new SimulationTask(taskData)
|
||||
try
|
||||
levelID = @task.getLevelName()
|
||||
catch err
|
||||
console.error err
|
||||
@trigger 'statusUpdate', "Error simulating game: #{err}. Trying another game in #{@retryDelayInSeconds} seconds."
|
||||
@simulateAnotherTaskAfterDelay()
|
||||
return
|
||||
|
||||
@supermodel ?= new SuperModel()
|
||||
|
||||
#console.log "Creating loader with levelID: " + levelID + " and SessionID: " + @task.getFirstSessionID() + " - task: " + JSON.stringify(@task)
|
||||
|
||||
@levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true
|
||||
|
||||
console.log "Waiting for loaded game"
|
||||
|
||||
@listenToOnce(@levelLoader, 'loaded-all', @simulateGame)
|
||||
|
||||
simulateGame: ->
|
||||
console.warn "Simulate game."
|
||||
return if @destroyed
|
||||
@trigger 'statusUpdate', 'All resources loaded, simulating!', @task.getSessions()
|
||||
console.log "assignWorld"
|
||||
@assignWorldAndLevelFromLevelLoaderAndDestroyIt()
|
||||
console.log "SetupGod"
|
||||
@setupGod()
|
||||
try
|
||||
@commenceSimulationAndSetupCallback()
|
||||
catch err
|
||||
console.log "There was an error in simulation(#{err}). Trying again in #{@retryDelayInSeconds} seconds"
|
||||
|
||||
#TODO: Comment out.
|
||||
throw err
|
||||
|
||||
@simulateAnotherTaskAfterDelay()
|
||||
|
||||
assignWorldAndLevelFromLevelLoaderAndDestroyIt: ->
|
||||
console.log "Assigning world and level"
|
||||
@world = @levelLoader.world
|
||||
@level = @levelLoader.level
|
||||
@levelLoader.destroy()
|
||||
@levelLoader = null
|
||||
|
||||
setupGod: ->
|
||||
@god.level = @level.serialize @supermodel
|
||||
@god.setWorldClassMap @world.classMap
|
||||
@setupGoalManager()
|
||||
|
||||
setupGoalManager: ->
|
||||
goalManager = new GoalManager @world
|
||||
goalManager.goals = @god.level.goals
|
||||
goalManager.goalStates = @manuallyGenerateGoalStates()
|
||||
@god.setGoalManager goalManager
|
||||
|
||||
commenceSimulationAndSetupCallback: ->
|
||||
console.log "Creating World."
|
||||
@god.createWorld(@generateSpellsObject())
|
||||
Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @
|
||||
Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @
|
||||
|
||||
#Search for leaks
|
||||
if 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."
|
||||
_.delay @cleanupAndSimulateAnotherTask, @retryDelayInSeconds * 1000
|
||||
|
||||
processResults: (simulationResults) ->
|
||||
|
||||
console.log "Processing Results"
|
||||
|
||||
taskResults = @formTaskResultsObject simulationResults
|
||||
console.warn taskResults
|
||||
@sendResultsBackToServer taskResults
|
||||
|
||||
sendResultsBackToServer: (results) =>
|
||||
@trigger 'statusUpdate', 'Simulation completed, sending results back to server!'
|
||||
console.log "Sending result back to server"
|
||||
|
||||
if testing
|
||||
return @fetchAndSimulateTask()
|
||||
|
||||
$.ajax
|
||||
url: "queue/scoring"
|
||||
data: results
|
||||
parse: true
|
||||
type: "PUT"
|
||||
success: @handleTaskResultsTransferSuccess
|
||||
error: @handleTaskResultsTransferError
|
||||
complete: @cleanupAndSimulateAnotherTask
|
||||
|
||||
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++
|
||||
|
||||
handleTaskResultsTransferError: (error) =>
|
||||
@trigger 'statusUpdate', 'There was an error sending the results back to the server.'
|
||||
console.log "Task registration error: #{JSON.stringify error}"
|
||||
|
||||
cleanupAndSimulateAnotherTask: =>
|
||||
#@cleanupSimulation() Not needed for Buddha.
|
||||
@fetchAndSimulateTask()
|
||||
|
||||
cleanupSimulation: ->
|
||||
@god?.destroy()
|
||||
@god = null
|
||||
@world = null
|
||||
@level = null
|
||||
|
||||
formTaskResultsObject: (simulationResults) ->
|
||||
taskResults =
|
||||
taskID: @task.getTaskID()
|
||||
receiptHandle: @task.getReceiptHandle()
|
||||
originalSessionID: @task.getFirstSessionID()
|
||||
originalSessionRank: -1
|
||||
calculationTime: 500
|
||||
sessions: []
|
||||
|
||||
for session in @task.getSessions()
|
||||
|
||||
sessionResult =
|
||||
sessionID: session.sessionID
|
||||
submitDate: session.submitDate
|
||||
creator: session.creator
|
||||
metrics:
|
||||
rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap()
|
||||
if session.sessionID is taskResults.originalSessionID
|
||||
taskResults.originalSessionRank = sessionResult.metrics.rank
|
||||
taskResults.originalSessionTeam = session.team
|
||||
taskResults.sessions.push sessionResult
|
||||
|
||||
return taskResults
|
||||
|
||||
calculateSessionRank: (sessionID, goalStates, teamSessionMap) ->
|
||||
humansDestroyed = goalStates["destroy-humans"].status is "success"
|
||||
ogresDestroyed = goalStates["destroy-ogres"].status is "success"
|
||||
if humansDestroyed is ogresDestroyed
|
||||
return 0
|
||||
else if humansDestroyed and teamSessionMap["ogres"] is sessionID
|
||||
return 0
|
||||
else if humansDestroyed and teamSessionMap["ogres"] isnt sessionID
|
||||
return 1
|
||||
else if ogresDestroyed and teamSessionMap["humans"] is sessionID
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
|
||||
manuallyGenerateGoalStates: ->
|
||||
goalStates =
|
||||
"destroy-humans":
|
||||
keyFrame: 0
|
||||
killed:
|
||||
"Human Base": false
|
||||
status: "incomplete"
|
||||
"destroy-ogres":
|
||||
keyFrame:0
|
||||
killed:
|
||||
"Ogre Base": false
|
||||
status: "incomplete"
|
||||
|
||||
generateSpellsObject: ->
|
||||
@currentUserCodeMap = @task.generateSpellKeyToSourceMap()
|
||||
@spells = {}
|
||||
for thang in @level.attributes.thangs
|
||||
continue if @thangIsATemplate thang
|
||||
@generateSpellKeyToSourceMapPropertiesFromThang thang
|
||||
|
||||
thangIsATemplate: (thang) ->
|
||||
for component in thang.components
|
||||
continue unless @componentHasProgrammableMethods component
|
||||
for methodName, method of component.config.programmableMethods
|
||||
return true if @methodBelongsToTemplateThang method
|
||||
|
||||
return false
|
||||
|
||||
componentHasProgrammableMethods: (component) -> component.config? and _.has component.config, 'programmableMethods'
|
||||
|
||||
methodBelongsToTemplateThang: (method) -> typeof method is 'string'
|
||||
|
||||
generateSpellKeyToSourceMapPropertiesFromThang: (thang) =>
|
||||
for component in thang.components
|
||||
continue unless @componentHasProgrammableMethods component
|
||||
for methodName, method of component.config.programmableMethods
|
||||
spellKey = @generateSpellKeyFromThangIDAndMethodName thang.id, methodName
|
||||
|
||||
@createSpellAndAssignName spellKey, methodName
|
||||
@createSpellThang thang, method, spellKey
|
||||
@transpileSpell thang, spellKey, methodName
|
||||
|
||||
generateSpellKeyFromThangIDAndMethodName: (thang, methodName) ->
|
||||
spellKeyComponents = [thang, methodName]
|
||||
spellKeyComponents[0] = _.string.slugify spellKeyComponents[0]
|
||||
spellKey = spellKeyComponents.join '/'
|
||||
spellKey
|
||||
|
||||
|
||||
createSpellAndAssignName: (spellKey, spellName) ->
|
||||
@spells[spellKey] ?= {}
|
||||
@spells[spellKey].name = spellName
|
||||
|
||||
createSpellThang: (thang, method, spellKey) ->
|
||||
@spells[spellKey].thangs ?= {}
|
||||
@spells[spellKey].thangs[thang.id] ?= {}
|
||||
@spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method
|
||||
|
||||
transpileSpell: (thang, spellKey, methodName) ->
|
||||
slugifiedThangID = _.string.slugify thang.id
|
||||
source = @currentUserCodeMap[[slugifiedThangID,methodName].join '/'] ? ""
|
||||
aether = @spells[spellKey].thangs[thang.id].aether
|
||||
try
|
||||
aether.transpile source
|
||||
catch e
|
||||
console.log "Couldn't transpile #{spellKey}:\n#{source}\n", e
|
||||
aether.transpile ''
|
||||
|
||||
createAether: (methodName, method) ->
|
||||
aetherOptions =
|
||||
functionName: methodName
|
||||
protectAPI: true
|
||||
includeFlow: false
|
||||
requiresThis: true
|
||||
yieldConditionally: false
|
||||
problems:
|
||||
jshint_W040: {level: "ignore"}
|
||||
jshint_W030: {level: "ignore"} # aether_NoEffect instead
|
||||
aether_MissingThis: {level: 'error'}
|
||||
if methodName is 'hear'
|
||||
aetherOptions.functionParameters = ['speaker', 'message', 'data']
|
||||
#console.log "creating aether with options", aetherOptions
|
||||
|
||||
return new Aether aetherOptions
|
||||
|
||||
class SimulationTask
|
||||
constructor: (@rawData) ->
|
||||
#console.log 'Simulating sessions', (session for session in @getSessions())
|
||||
|
||||
getLevelName: ->
|
||||
levelName = @rawData.sessions?[0]?.levelID
|
||||
return levelName if levelName?
|
||||
@throwMalformedTaskError "The level name couldn't be deduced from the task."
|
||||
|
||||
generateTeamToSessionMap: ->
|
||||
teamSessionMap = {}
|
||||
for session in @rawData.sessions
|
||||
@throwMalformedTaskError "Two players share the same team" if teamSessionMap[session.team]?
|
||||
teamSessionMap[session.team] = session.sessionID
|
||||
|
||||
teamSessionMap
|
||||
|
||||
throwMalformedTaskError: (errorString) ->
|
||||
throw new Error "The task was malformed, reason: #{errorString}"
|
||||
|
||||
getFirstSessionID: -> @rawData.sessions[0].sessionID
|
||||
|
||||
getTaskID: -> @rawData.taskID
|
||||
|
||||
getReceiptHandle: -> @rawData.receiptHandle
|
||||
|
||||
getSessions: -> @rawData.sessions
|
||||
|
||||
generateSpellKeyToSourceMap: ->
|
||||
spellKeyToSourceMap = {}
|
||||
for session in @rawData.sessions
|
||||
teamSpells = session.teamSpells[session.team]
|
||||
teamCode = {}
|
||||
for thangName, thangSpells of session.code
|
||||
for spellName, spell of thangSpells
|
||||
fullSpellName = [thangName,spellName].join '/'
|
||||
if _.contains(teamSpells, fullSpellName)
|
||||
teamCode[fullSpellName]=spell
|
||||
|
||||
_.merge spellKeyToSourceMap, teamCode
|
||||
commonSpells = session.teamSpells["common"]
|
||||
_.merge spellKeyToSourceMap, _.pick(session.code, commonSpells) if commonSpells?
|
||||
|
||||
|
||||
spellKeyToSourceMap
|
||||
|
||||
sim = new Simulator()
|
||||
|
||||
|
||||
sim.fetchAndSimulateTask()
|
87
headless_client/test.js
Normal file
87
headless_client/test.js
Normal 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"
|
||||
}
|
209
headless_client/worker_world.coffee
Normal file
209
headless_client/worker_world.coffee
Normal 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)
|
10
package.json
10
package.json
|
@ -60,9 +60,13 @@
|
|||
"gridfs-stream": "0.4.x",
|
||||
"stream-buffers": "0.2.x",
|
||||
"sendwithus": "2.0.x",
|
||||
"aws-sdk":"~2.0.0",
|
||||
"bayesian-battle":"0.0.x",
|
||||
"redis": ""
|
||||
"aws-sdk": "~2.0.0",
|
||||
"bayesian-battle": "0.0.x",
|
||||
"redis": "",
|
||||
"webworker-threads": "~0.4.11",
|
||||
"node-gyp": "~0.13.0",
|
||||
"aether": "~0.1.18",
|
||||
"JASON": "~0.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jade": "0.33.x",
|
||||
|
|
|
@ -15,7 +15,7 @@ module.exports.connect = () ->
|
|||
|
||||
|
||||
module.exports.generateMongoConnectionString = ->
|
||||
if config.mongo.mongoose_replica_string
|
||||
if not testing and config.mongo.mongoose_replica_string
|
||||
address = config.mongo.mongoose_replica_string
|
||||
else
|
||||
dbName = config.mongo.db
|
||||
|
@ -25,4 +25,4 @@ module.exports.generateMongoConnectionString = ->
|
|||
address = config.mongo.username + ":" + config.mongo.password + "@" + address
|
||||
address = "mongodb://#{address}/#{dbName}"
|
||||
|
||||
return address
|
||||
return address
|
||||
|
|
|
@ -108,7 +108,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
|
|||
context =
|
||||
email_id: sendwithus.templates.ladder_update_email
|
||||
recipient:
|
||||
address: if DEBUGGING then 'nick@codecombat.com' else user.email
|
||||
address: if DEBUGGING then 'nick@codecombat.com' else user.get('email')
|
||||
name: name
|
||||
email_data:
|
||||
name: name
|
||||
|
|
Loading…
Reference in a new issue