2014-07-23 10:02:45 -04:00
|
|
|
SpellView = require './SpellView'
|
2016-07-15 01:43:25 -04:00
|
|
|
SpellTopBarView = require './SpellTopBarView'
|
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'
|
2016-05-04 01:26:48 -04:00
|
|
|
utils = require 'core/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
|
2016-07-15 01:43:25 -04:00
|
|
|
topBarView: null
|
2014-01-03 13:32:13 -05:00
|
|
|
|
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
|
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
|
2016-05-24 16:54:49 -04:00
|
|
|
@team = @permissions.readwrite[0] ? 'common'
|
2014-06-20 20:19:18 -04:00
|
|
|
if @canWrite()
|
|
|
|
@setLanguage options.language
|
2016-05-24 16:54:49 -04:00
|
|
|
else if @otherSession and @team is @otherSession.get 'team'
|
|
|
|
@setLanguage @otherSession.get('submittedCodeLanguage') or @otherSession.get('codeLanguage')
|
2014-06-20 20:19:18 -04:00
|
|
|
else
|
|
|
|
@setLanguage 'javascript'
|
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
|
2016-06-09 20:59:19 -04:00
|
|
|
@isAISource = true
|
2014-08-25 00:52:33 -04:00
|
|
|
if @canRead() # We can avoid creating these views if we'll never use them.
|
2016-07-14 11:58:43 -04:00
|
|
|
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel, levelID: options.levelID}
|
2014-08-25 00:39:34 -04:00
|
|
|
@view.render() # Get it ready and code loaded in advance
|
2016-07-15 01:43:25 -04:00
|
|
|
@topBarView = new SpellTopBarView
|
2016-05-26 20:46:49 -04:00
|
|
|
hintsState: options.hintsState
|
|
|
|
spell: @
|
|
|
|
supermodel: @supermodel
|
|
|
|
codeLanguage: @language
|
|
|
|
level: options.level
|
2016-07-16 01:14:25 -04:00
|
|
|
session: options.session
|
|
|
|
courseID: options.courseID
|
2016-07-15 01:43:25 -04:00
|
|
|
@topBarView.render()
|
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()
|
2016-07-15 01:43:25 -04:00
|
|
|
@topBarView?.destroy()
|
|
|
|
@thang = 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) ->
|
2016-07-14 15:34:22 -04:00
|
|
|
@language = 'html' if @level.isType('web-dev')
|
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
|
|
|
|
2016-07-14 21:07:36 -04:00
|
|
|
if @level.isType('web-dev')
|
2016-07-17 03:53:17 -04:00
|
|
|
# Pull apart the structural wrapper code and the player code, remember the wrapper code, and strip indentation on player code.
|
2016-07-14 21:07:36 -04:00
|
|
|
playerCode = @originalSource.match(/<playercode>\n([\s\S]*)\n *<\/playercode>/)[1]
|
|
|
|
playerCodeLines = playerCode.split('\n')
|
|
|
|
indentation = playerCodeLines[0].length - playerCodeLines[0].trim().length
|
|
|
|
playerCode = (line.substr(indentation) for line in playerCodeLines).join('\n')
|
2016-07-17 03:53:17 -04:00
|
|
|
@wrapperCode = @originalSource.replace /<playercode>[\s\S]*<\/playercode>/, '☃' # ☃ serves as placeholder for constructHTML
|
2016-07-14 21:07:36 -04:00
|
|
|
@originalSource = playerCode
|
|
|
|
|
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
|
|
|
|
2016-07-14 11:58:43 -04:00
|
|
|
if /loop/.test(@originalSource) and @level.isType('course', 'course-ladder')
|
2015-11-12 14:00:51 -05:00
|
|
|
# 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 'lua' then @originalSource.replace /loop\n/, 'while true then\n'
|
|
|
|
when 'coffeescript' then @originalSource
|
|
|
|
else @originalSource
|
|
|
|
|
2016-07-14 21:07:36 -04:00
|
|
|
constructHTML: (source) ->
|
|
|
|
@wrapperCode.replace '☃', source
|
|
|
|
|
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) ->
|
2016-07-15 01:43:25 -04:00
|
|
|
if @thang?.thang.id is thang.id
|
|
|
|
@thang.thang = thang
|
2014-02-06 17:00:27 -05:00
|
|
|
else
|
2016-07-15 01:43:25 -04:00
|
|
|
@thang = {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) ->
|
2016-07-15 01:43:25 -04:00
|
|
|
@thang = null if @thang?.thang.id is thangID
|
2014-02-05 18:16:59 -05:00
|
|
|
|
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()
|
2016-07-15 01:43:25 -04:00
|
|
|
unless @language is 'html'
|
|
|
|
@thang?.aether.transpile source
|
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) ->
|
2016-07-15 01:43:25 -04:00
|
|
|
unless aether = @thang?.aether
|
|
|
|
console.error @toString(), 'couldn\'t find a spellThang with aether', @thang
|
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) ->
|
2016-06-09 20:59:19 -04:00
|
|
|
writable = @permissions.readwrite.length > 0 and not @isAISource
|
2016-07-14 11:58:43 -04:00
|
|
|
skipProtectAPI = @skipProtectAPI or not writable or @level.isType('game-dev')
|
2014-10-14 17:02:31 -04:00
|
|
|
problemContext = @createProblemContext thang
|
2016-07-14 11:58:43 -04:00
|
|
|
includeFlow = @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') 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
|
2016-05-24 15:00:04 -04:00
|
|
|
useInterpreter: true
|
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) ->
|
2016-07-15 01:43:25 -04:00
|
|
|
@thang?.aether?.setLanguage @language
|
|
|
|
@thang?.castAether = null
|
|
|
|
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-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
|