2014-07-23 10:02:45 -04:00
|
|
|
SpellView = require './SpellView'
|
|
|
|
SpellListTabEntryView = require './SpellListTabEntryView'
|
2014-11-28 20:49:41 -05:00
|
|
|
{me} = require 'core/auth'
|
2014-08-30 16:43:56 -04:00
|
|
|
{createAetherOptions} = require 'lib/aether_utils'
|
2014-05-08 14:43:00 -04:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
module.exports = class Spell
|
|
|
|
loaded: false
|
|
|
|
view: null
|
|
|
|
entryView: null
|
|
|
|
|
2014-01-28 18:24:08 -05:00
|
|
|
constructor: (options) ->
|
|
|
|
@spellKey = options.spellKey
|
|
|
|
@pathComponents = options.pathComponents
|
|
|
|
@session = options.session
|
2014-06-20 20:19:18 -04:00
|
|
|
@otherSession = options.otherSession
|
2014-05-15 18:18:15 -04:00
|
|
|
@spectateView = options.spectateView
|
2014-09-18 11:12:46 -04:00
|
|
|
@spectateOpponentCodeLanguage = options.spectateOpponentCodeLanguage
|
2015-01-31 13:04:02 -05:00
|
|
|
@observing = options.observing
|
2014-01-28 18:24:08 -05:00
|
|
|
@supermodel = options.supermodel
|
|
|
|
@skipProtectAPI = options.skipProtectAPI
|
2014-02-17 20:38:49 -05:00
|
|
|
@worker = options.worker
|
2014-08-14 14:55:43 -04:00
|
|
|
@levelID = options.levelID
|
2014-10-08 01:28:53 -04:00
|
|
|
@levelType = options.level.get('type', true)
|
2016-02-17 14:33:50 -05:00
|
|
|
@level = options.level
|
2014-01-28 18:24:08 -05:00
|
|
|
|
2014-06-18 01:17:44 -04:00
|
|
|
p = options.programmableMethod
|
2014-10-29 00:15:41 -04:00
|
|
|
@commentI18N = p.i18n
|
|
|
|
@commentContext = p.context
|
2014-06-18 01:17:44 -04:00
|
|
|
@languages = p.languages ? {}
|
|
|
|
@languages.javascript ?= p.source
|
2014-01-03 13:32:13 -05:00
|
|
|
@name = p.name
|
|
|
|
@permissions = read: p.permissions?.read ? [], readwrite: p.permissions?.readwrite ? [] # teams
|
2014-06-20 20:19:18 -04:00
|
|
|
if @canWrite()
|
|
|
|
@setLanguage options.language
|
2014-06-20 21:45:57 -04:00
|
|
|
else if @isEnemySpell()
|
2014-09-18 11:12:46 -04:00
|
|
|
@setLanguage @otherSession?.get('submittedCodeLanguage') ? @spectateOpponentCodeLanguage
|
2014-06-20 20:19:18 -04:00
|
|
|
else
|
|
|
|
@setLanguage 'javascript'
|
2014-06-18 01:17:44 -04:00
|
|
|
@useTranspiledCode = @shouldUseTranspiledCode()
|
2015-09-08 19:30:18 -04:00
|
|
|
#console.log 'Spell', @spellKey, 'is using transpiled code (should only happen if it\'s an enemy/spectate writable method).' if @useTranspiledCode
|
2014-06-18 01:17:44 -04:00
|
|
|
|
|
|
|
@source = @originalSource
|
2014-04-26 17:21:26 -04:00
|
|
|
@parameters = p.parameters
|
|
|
|
if @permissions.readwrite.length and sessionSource = @session.getSourceFor(@spellKey)
|
2014-09-23 11:58:23 -04:00
|
|
|
if sessionSource isnt '// Should fill in some default source\n' # TODO: figure out why session is getting this default source in there and stop it
|
|
|
|
@source = sessionSource
|
2014-10-19 15:44:58 -04:00
|
|
|
if p.aiSource and not @otherSession and not @canWrite()
|
|
|
|
@source = @originalSource = p.aiSource
|
2014-01-03 13:32:13 -05:00
|
|
|
@thangs = {}
|
2014-08-25 00:52:33 -04:00
|
|
|
if @canRead() # We can avoid creating these views if we'll never use them.
|
2016-04-25 21:56:15 -04:00
|
|
|
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel}
|
2014-08-25 00:39:34 -04:00
|
|
|
@view.render() # Get it ready and code loaded in advance
|
2014-09-24 19:29:28 -04:00
|
|
|
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level
|
2014-08-25 00:39:34 -04:00
|
|
|
@tabView.render()
|
2014-06-30 22:16:26 -04:00
|
|
|
@team = @permissions.readwrite[0] ? 'common'
|
2014-02-10 16:18:39 -05:00
|
|
|
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-02-11 15:10:21 -05:00
|
|
|
destroy: ->
|
2014-08-25 00:52:33 -04:00
|
|
|
@view?.destroy()
|
|
|
|
@tabView?.destroy()
|
2014-02-11 18:38:36 -05:00
|
|
|
@thangs = null
|
2014-02-17 20:38:49 -05:00
|
|
|
@worker = null
|
2014-02-11 16:10:59 -05:00
|
|
|
|
2014-06-18 01:17:44 -04:00
|
|
|
setLanguage: (@language) ->
|
2014-09-24 19:29:28 -04:00
|
|
|
#console.log 'setting language to', @language, 'so using original source', @languages[language] ? @languages.javascript
|
2015-01-30 10:51:57 -05:00
|
|
|
@originalSource = @languages[@language] ? @languages.javascript
|
2016-02-17 14:33:50 -05:00
|
|
|
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
|
2015-11-12 14:00:51 -05:00
|
|
|
|
2014-10-29 00:15:41 -04:00
|
|
|
# Translate comments chosen spoken language.
|
|
|
|
return unless @commentContext
|
|
|
|
context = $.extend true, {}, @commentContext
|
|
|
|
if @commentI18N
|
|
|
|
spokenLanguage = me.get 'preferredLanguage'
|
|
|
|
while spokenLanguage
|
|
|
|
spokenLanguage = spokenLanguage.substr 0, spokenLanguage.lastIndexOf('-') if fallingBack?
|
|
|
|
if spokenLanguageContext = @commentI18N[spokenLanguage]?.context
|
|
|
|
context = _.merge context, spokenLanguageContext
|
|
|
|
break
|
|
|
|
fallingBack = true
|
2014-10-29 15:08:03 -04:00
|
|
|
try
|
|
|
|
@originalSource = _.template @originalSource, context
|
|
|
|
catch e
|
|
|
|
console.error "Couldn't create example code template of", @originalSource, "\nwith context", context, "\nError:", e
|
2014-06-18 01:17:44 -04:00
|
|
|
|
2015-11-12 14:00:51 -05:00
|
|
|
if /loop/.test(@originalSource) and @levelType in ['course', 'course-ladder']
|
|
|
|
# Temporary hackery to make it look like we meant while True: in our sample code until we can update everything
|
|
|
|
@originalSource = switch @language
|
|
|
|
when 'python' then @originalSource.replace /loop:/, 'while True:'
|
|
|
|
when 'javascript' then @originalSource.replace /loop {/, 'while (true) {'
|
|
|
|
when 'clojure' then @originalSource.replace /dotimes \[n 1000\]/, '(while true'
|
|
|
|
when 'lua' then @originalSource.replace /loop\n/, 'while true then\n'
|
|
|
|
when 'coffeescript' then @originalSource
|
|
|
|
when 'io' then @originalSource.replace /loop\n/, 'while true,\n'
|
|
|
|
else @originalSource
|
|
|
|
|
2016-02-17 14:33:50 -05:00
|
|
|
addPicoCTFProblem: ->
|
2016-03-03 20:18:17 -05:00
|
|
|
return @originalSource unless problem = @level.picoCTFProblem
|
2016-02-17 14:33:50 -05:00
|
|
|
description = """
|
|
|
|
-- #{problem.name} --
|
|
|
|
#{problem.description}
|
|
|
|
""".replace /<p>(.*?)<\/p>/gi, '$1'
|
|
|
|
("// #{line}" for line in description.split('\n')).join('\n') + '\n' + @originalSource
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
addThang: (thang) ->
|
2014-02-06 17:00:27 -05:00
|
|
|
if @thangs[thang.id]
|
|
|
|
@thangs[thang.id].thang = thang
|
|
|
|
else
|
|
|
|
@thangs[thang.id] = {thang: thang, aether: @createAether(thang), castAether: null}
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-02-05 18:16:59 -05:00
|
|
|
removeThangID: (thangID) ->
|
|
|
|
delete @thangs[thangID]
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
canRead: (team) ->
|
|
|
|
(team ? me.team) in @permissions.read or (team ? me.team) in @permissions.readwrite
|
|
|
|
|
|
|
|
canWrite: (team) ->
|
|
|
|
(team ? me.team) in @permissions.readwrite
|
|
|
|
|
|
|
|
getSource: ->
|
2014-08-25 00:52:33 -04:00
|
|
|
@view?.getSource() ? @source
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
transpile: (source) ->
|
|
|
|
if source
|
|
|
|
@source = source
|
|
|
|
else
|
|
|
|
source = @getSource()
|
2014-02-17 20:38:49 -05:00
|
|
|
[pure, problems] = [null, null]
|
2014-05-15 18:18:15 -04:00
|
|
|
if @useTranspiledCode
|
|
|
|
transpiledCode = @session.get('code')
|
2014-02-17 20:38:49 -05:00
|
|
|
for thangID, spellThang of @thangs
|
|
|
|
unless pure
|
2014-05-23 15:04:42 -04:00
|
|
|
if @useTranspiledCode and transpiledSpell = transpiledCode[@spellKey.split('/')[0]]?[@name]
|
2014-05-15 18:18:15 -04:00
|
|
|
spellThang.aether.pure = transpiledSpell
|
|
|
|
else
|
|
|
|
pure = spellThang.aether.transpile source
|
|
|
|
problems = spellThang.aether.problems
|
2014-06-30 22:16:26 -04:00
|
|
|
#console.log 'aether transpiled', source.length, 'to', spellThang.aether.pure.length, 'for', thangID, @spellKey
|
2014-02-17 20:38:49 -05:00
|
|
|
else
|
2014-07-19 23:26:13 -04:00
|
|
|
spellThang.aether.raw = source
|
2014-02-17 20:38:49 -05:00
|
|
|
spellThang.aether.pure = pure
|
|
|
|
spellThang.aether.problems = problems
|
2014-06-30 22:16:26 -04:00
|
|
|
#console.log 'aether reused transpilation for', thangID, @spellKey
|
2014-02-17 20:38:49 -05:00
|
|
|
null
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
hasChanged: (newSource=null, currentSource=null) ->
|
|
|
|
(newSource ? @originalSource) isnt (currentSource ? @source)
|
|
|
|
|
2014-04-22 11:54:35 -04:00
|
|
|
hasChangedSignificantly: (newSource=null, currentSource=null, cb) ->
|
2014-01-03 13:32:13 -05:00
|
|
|
for thangID, spellThang of @thangs
|
|
|
|
aether = spellThang.aether
|
|
|
|
break
|
|
|
|
unless aether
|
2014-06-30 22:16:26 -04:00
|
|
|
console.error @toString(), 'couldn\'t find a spellThang with aether of', @thangs
|
2014-04-22 14:04:56 -04:00
|
|
|
cb false
|
2014-09-29 02:24:18 -04:00
|
|
|
if @worker
|
|
|
|
workerMessage =
|
|
|
|
function: 'hasChangedSignificantly'
|
|
|
|
a: (newSource ? @originalSource)
|
|
|
|
spellKey: @spellKey
|
|
|
|
b: (currentSource ? @source)
|
|
|
|
careAboutLineNumbers: true
|
|
|
|
careAboutLint: true
|
|
|
|
@worker.addEventListener 'message', (e) =>
|
|
|
|
workerData = JSON.parse e.data
|
|
|
|
if workerData.function is 'hasChangedSignificantly' and workerData.spellKey is @spellKey
|
|
|
|
@worker.removeEventListener 'message', arguments.callee, false
|
|
|
|
cb(workerData.hasChanged)
|
|
|
|
@worker.postMessage JSON.stringify(workerMessage)
|
|
|
|
else
|
|
|
|
cb(aether.hasChangedSignificantly((newSource ? @originalSource), (currentSource ? @source), true, true))
|
2014-04-26 17:21:26 -04:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
createAether: (thang) ->
|
2014-05-15 00:54:36 -04:00
|
|
|
writable = @permissions.readwrite.length > 0
|
2014-08-30 16:43:56 -04:00
|
|
|
skipProtectAPI = @skipProtectAPI or not writable
|
2014-10-14 17:02:31 -04:00
|
|
|
problemContext = @createProblemContext thang
|
2015-07-24 20:37:42 -04:00
|
|
|
includeFlow = (@levelType in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']) and not skipProtectAPI
|
2015-10-19 19:46:53 -04:00
|
|
|
aetherOptions = createAetherOptions
|
|
|
|
functionName: @name
|
|
|
|
codeLanguage: @language
|
|
|
|
functionParameters: @parameters
|
|
|
|
skipProtectAPI: skipProtectAPI
|
|
|
|
includeFlow: includeFlow
|
|
|
|
problemContext: problemContext
|
2014-01-03 13:32:13 -05:00
|
|
|
aether = new Aether aetherOptions
|
2014-09-29 02:24:18 -04:00
|
|
|
if @worker
|
|
|
|
workerMessage =
|
|
|
|
function: 'createAether'
|
|
|
|
spellKey: @spellKey
|
|
|
|
options: aetherOptions
|
|
|
|
@worker.postMessage JSON.stringify workerMessage
|
2014-01-03 13:32:13 -05:00
|
|
|
aether
|
|
|
|
|
2014-05-15 00:54:36 -04:00
|
|
|
updateLanguageAether: (@language) ->
|
2014-03-16 21:14:04 -04:00
|
|
|
for thangId, spellThang of @thangs
|
2014-05-15 00:54:36 -04:00
|
|
|
spellThang.aether?.setLanguage @language
|
2014-03-16 21:14:04 -04:00
|
|
|
spellThang.castAether = null
|
2014-06-26 01:56:39 -04:00
|
|
|
Backbone.Mediator.publish 'tome:spell-changed-language', spell: @, language: @language
|
2014-09-29 02:24:18 -04:00
|
|
|
if @worker
|
|
|
|
workerMessage =
|
|
|
|
function: 'updateLanguageAether'
|
|
|
|
newLanguage: @language
|
|
|
|
@worker.postMessage JSON.stringify workerMessage
|
2014-03-16 21:14:04 -04:00
|
|
|
@transpile()
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
toString: ->
|
|
|
|
"<Spell: #{@spellKey}>"
|
2014-06-18 01:17:44 -04:00
|
|
|
|
2014-06-20 20:19:18 -04:00
|
|
|
isEnemySpell: ->
|
2014-06-20 21:45:57 -04:00
|
|
|
return false unless @permissions.readwrite.length
|
2014-09-18 11:12:46 -04:00
|
|
|
return false unless @otherSession or @spectateView
|
2014-06-20 20:19:18 -04:00
|
|
|
teamSpells = @session.get('teamSpells')
|
|
|
|
team = @session.get('team') ? 'humans'
|
|
|
|
teamSpells and not _.contains(teamSpells[team], @spellKey)
|
|
|
|
|
2014-06-18 01:17:44 -04:00
|
|
|
shouldUseTranspiledCode: ->
|
|
|
|
# Determine whether this code has already been transpiled, or whether it's raw source needing transpilation.
|
|
|
|
return true if @spectateView # Use transpiled code for both teams if we're just spectating.
|
2014-06-20 20:19:18 -04:00
|
|
|
return true if @isEnemySpell() # Use transpiled for enemy spells.
|
2014-06-18 01:17:44 -04:00
|
|
|
# Players without permissions can't view the raw code.
|
2015-07-24 20:37:42 -04:00
|
|
|
return false if @observing and @levelType in ['hero', 'course']
|
2014-09-01 12:11:10 -04:00
|
|
|
return true if @session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions', true))
|
2014-06-18 01:17:44 -04:00
|
|
|
false
|
Real-time multiplayer initial commit
Simple matchmaking, synchronous multiplayer PVP, flags!
Rough matchmaking is under the game menu multiplayer tab, for ladder
games only. After creating a 2-person game there, you can exit that
modal and real-time cast to play against each other.
If you’re the first person to cast, you’ll sit at the real-time level
playback view waiting until the other player casts. When they do, you
both should start the real-time playback (and start placing flags like
crazy people).
If in a multiplayer session, the real-time simulation runs the players’
code against each other. Your multiplayer opponent’s name should be up
near the level name.
Multiplayer sessions are stored completely in Firebase for now, and
removed if both players leave the game. There’s plenty of bugs,
synchronization issues, and minimal polish to add before we push it to
master.
2014-08-29 02:34:07 -04:00
|
|
|
|
2014-10-14 17:02:31 -04:00
|
|
|
createProblemContext: (thang) ->
|
|
|
|
# Create problemContext Aether can use to craft better error messages
|
|
|
|
# stringReferences: values that should be referred to as a string instead of a variable (e.g. "Brak", not Brak)
|
|
|
|
# thisMethods: methods available on the 'this' object
|
|
|
|
# thisProperties: properties available on the 'this' object
|
2014-10-14 20:53:17 -04:00
|
|
|
# commonThisMethods: methods that are available sometimes, but not awlays
|
2014-10-14 17:02:31 -04:00
|
|
|
|
|
|
|
# NOTE: Assuming the first createProblemContext call has everything we need, and we'll use that forevermore
|
|
|
|
return @problemContext if @problemContext?
|
|
|
|
|
|
|
|
@problemContext = { stringReferences: [], thisMethods: [], thisProperties: [] }
|
2014-10-14 20:53:17 -04:00
|
|
|
# TODO: These should be read from the database
|
2015-02-12 19:06:27 -05:00
|
|
|
@problemContext.commonThisMethods = ['moveRight', 'moveLeft', 'moveUp', 'moveDown', 'attack', 'findNearestEnemy', 'buildXY', 'moveXY', 'say', 'move', 'distance', 'findEnemies', 'findFriends', 'addFlag', 'findFlag', 'removeFlag', 'findFlags', 'attackRange', 'cast', 'buildTypes', 'jump', 'jumpTo', 'attackXY']
|
2014-10-14 17:02:31 -04:00
|
|
|
return @problemContext unless thang?
|
|
|
|
|
|
|
|
# Populate stringReferences
|
|
|
|
for key, value of thang.world?.thangMap
|
|
|
|
if (value.isAttackable or value.isSelectable) and value.id not in @problemContext.stringReferences
|
|
|
|
@problemContext.stringReferences.push value.id
|
|
|
|
|
|
|
|
# Populate thisMethods and thisProperties
|
|
|
|
if thang.programmableProperties?
|
|
|
|
for prop in thang.programmableProperties
|
|
|
|
if _.isFunction(thang[prop])
|
|
|
|
@problemContext.thisMethods.push prop
|
|
|
|
else
|
|
|
|
@problemContext.thisProperties.push prop
|
|
|
|
|
|
|
|
# TODO: See SpellPaletteView.createPalette() for other interesting contextual properties
|
|
|
|
|
|
|
|
@problemContext
|