Created headless-client and an alternative implementation of god

This commit is contained in:
Dominik Maier 2014-05-06 02:37:14 +02:00
parent 82529c781b
commit c9bb488794
16 changed files with 1236 additions and 51 deletions

11
.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
### 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. ###

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

243
app/lib/Buddha.coffee Normal file
View 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.

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
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'

View file

@ -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,

View file

@ -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 = {}

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
@ -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')

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,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'))

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

574
headless_client.coffee Normal file
View 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
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

@ -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",

View file

@ -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

View file

@ -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