From c44c16e5d2cb28ddd1977a4cce027694d8abdfc5 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Fri, 15 Jul 2016 00:40:32 -0700 Subject: [PATCH] Started implementing web-dev goals --- app/assets/javascripts/web-dev-listener.js | 76 +++++++++++++++++++++- app/lib/world/GoalManager.coffee | 15 ++++- app/schemas/models/level.coffee | 3 + app/views/play/level/PlayLevelView.coffee | 2 +- app/views/play/level/WebSurfaceView.coffee | 4 +- 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js index 41ce0788b..03842912c 100644 --- a/app/assets/javascripts/web-dev-listener.js +++ b/app/assets/javascripts/web-dev-listener.js @@ -14,18 +14,20 @@ function receiveMessage(event) { switch (event.data.type) { case 'create': create(event.data.dom); + checkGoals(event.data.goals); break; case 'update': if (virtualDOM) update(event.data.dom); else - create(event.data.dom); + create(event.data.dom); + checkGoals(event.data.goals); break; case 'log': console.log(event.data.text); break; default: - console.log('Unknown message type:', event.data.type); + console.log('Unknown message type:', event.data.type); } //event.source.postMessage("hi there yourself! the secret response is: rheeeeet!", event.origin); @@ -45,3 +47,73 @@ function update(dom) { changes.reduce(deku.dom.update(dispatch, context), concreteDOM) // Rerender virtualDOM = event.data.dom; } + +function checkGoals(goals) { + goals.forEach(function(goal) { + var $result = $(goal.html.selector); + //console.log('ran selector', goal.html.selector, 'to find element(s)', $result); + var success = true; + goal.html.valueChecks.forEach(function(check) { + //console.log(' ... and should make sure that the value of', check.eventProps, 'is', _.omit(check, 'eventProps'), '?', matchesCheck($result, check)) + success = success && matchesCheck($result, check) + }); + console.log('HTML', goal.id, '-', goal.name, '- succeeds?', success) + }); +} + +function downTheChain(obj, keyChain) { + if (!obj) + return null; + if (!_.isArray(keyChain)) + return obj[keyChain]; + var value = obj; + while (keyChain.length && value) { + if (keyChain[0].match(/\(.*\)$/)) { + var args, argsString = keyChain[0].match(/\((.*)\)$/)[1]; + if (argsString) + args = eval(argsString).split(/, ?/g).filter(function(x) { return x !== ""; }); // TODO: can/should we avoid eval here? + else + args = []; + value = value[keyChain[0].split('(')[0]].apply(value, args); // value.text(), value.css('background-color'), etc. + } + else + value = value[keyChain[0]]; + keyChain = keyChain.slice(1); + } + return value; +}; + +function matchesCheck(value, check) { + var v = downTheChain(value, check.eventProps); + if ((check.equalTo != null) && v !== check.equalTo) { + return false; + } + if ((check.notEqualTo != null) && v === check.notEqualTo) { + return false; + } + if ((check.greaterThan != null) && !(v > check.greaterThan)) { + return false; + } + if ((check.greaterThanOrEqualTo != null) && !(v >= check.greaterThanOrEqualTo)) { + return false; + } + if ((check.lessThan != null) && !(v < check.lessThan)) { + return false; + } + if ((check.lessThanOrEqualTo != null) && !(v <= check.lessThanOrEqualTo)) { + return false; + } + if ((check.containingString != null) && (!v || v.search(check.containingString) === -1)) { + return false; + } + if ((check.notContainingString != null) && (v != null ? v.search(check.notContainingString) : void 0) !== -1) { + return false; + } + if ((check.containingRegexp != null) && (!v || v.search(new RegExp(check.containingRegexp)) === -1)) { + return false; + } + if ((check.notContainingRegexp != null) && (v != null ? v.search(new RegExp(check.notContainingRegexp)) : void 0) !== -1) { + return false; + } + return true; +} diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee index 378068507..4d331dca3 100644 --- a/app/lib/world/GoalManager.coffee +++ b/app/lib/world/GoalManager.coffee @@ -39,6 +39,7 @@ module.exports = class GoalManager extends CocoClass subscriptions: 'god:new-world-created': 'onNewWorldCreated' 'level:restarted': 'onLevelRestarted' + #'tome:html-updated': 'onHTMLUpdated' backgroundSubscriptions: 'world:thang-died': 'onThangDied' @@ -114,7 +115,7 @@ module.exports = class GoalManager extends CocoClass goalStates: @goalStates goals: @goals overallStatus: overallStatus - timedOut: @world.totalFrames is @world.maxTotalFrames and overallStatus not in ['success', 'failure'] + timedOut: @world? and (@world.totalFrames is @world.maxTotalFrames and overallStatus not in ['success', 'failure']) Backbone.Mediator.publish('goal-manager:new-goal-states', event) checkOverallStatus: (ignoreIncomplete=false) -> @@ -220,6 +221,11 @@ module.exports = class GoalManager extends CocoClass return unless linesAllowed = who[thang.id] ? who[thang.team] @updateGoalState goalID, thang.id, 'lines', frameNumber if linesUsed > linesAllowed + #checkHTML: (goal, html) -> + # console.log 'should run selector', goal.html.selector, 'to find element(s)' + # console.log ' ... and should make sure that the value of', check.eventProps, 'is', _.omit(check, 'eventProps') for check in goal.html.valueChecks + # console.log 'should do it with cheerio', window.cheerio + wrapUpGoalStates: (finalFrame) -> for goalID, state of @goalStates if state.status is null @@ -264,7 +270,7 @@ module.exports = class GoalManager extends CocoClass mostEagerGoal = _.min matchedGoals, 'worldEndsAfter' victory = overallStatus is 'success' tentative = overallStatus is 'success' - @world.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity + @world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity updateGoalState: (goalID, thangID, progressObjectName, frameNumber) -> # A thang has done something related to the goal! @@ -291,7 +297,7 @@ module.exports = class GoalManager extends CocoClass mostEagerGoal = _.min matchedGoals, 'worldEndsAfter' victory = overallStatus is 'success' tentative = overallStatus is 'success' - @world.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity + @world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity goalIsPositive: (goalID) -> # Positive goals are completed when all conditions are true (kill all these thangs) @@ -314,6 +320,9 @@ module.exports = class GoalManager extends CocoClass linesOfCode: 0 codeProblems: 0 + #onHTMLUpdated: (e) -> + # @checkHTML goal, e.html for goal in @goals when goal.html + updateCodeGoalStates: -> # TODO diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 15d928dfa..d71d48f8e 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -114,6 +114,9 @@ GoalSchema = c.object {title: 'Goal', description: 'A goal that the player can a targets: c.array {title: 'Targets', description: 'The target items which the Thangs must not collect.', minItems: 1}, thang codeProblems: c.array {title: 'Code Problems', description: 'A list of Thang IDs that should not have any code problems, or team names.', uniqueItems: true, minItems: 1, 'default': ['humans']}, thang linesOfCode: {title: 'Lines of Code', description: 'A mapping of Thang IDs or teams to how many many lines of code should be allowed (well, statements).', type: 'object', default: {humans: 10}, additionalProperties: {type: 'integer', description: 'How many lines to allow for this Thang.'}} + html: c.object {title: 'HTML', description: 'A jQuery selector and what its result should be'}, + selector: {type: 'string', description: 'jQuery selector to run on the user HTML, like "h1:first-child"'} + valueChecks: c.array {title: 'Value checks', description: 'Logical checks on the resulting value for this goal to pass.', format: 'event-prereqs'}, EventPrereqSchema ResponseSchema = c.object {title: 'Dialogue Button', description: 'A button to be shown to the user with the dialogue.', required: ['text']}, text: {title: 'Title', description: 'The text that will be on the button', 'default': 'Okay', type: 'string', maxLength: 30} diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 9fd28e1f6..05c0bdaa2 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -269,7 +269,7 @@ module.exports = class PlayLevelView extends RootView @insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.isType('hero-ladder', 'course-ladder') @insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID} @insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view') - @insertSubView @webSurface = new WebSurfaceView level: @level if @level.isType('web-dev') + @insertSubView @webSurface = new WebSurfaceView {level: @level, @goalManager} if @level.isType('web-dev') #_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick' initVolume: -> diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee index 4b0bdc9ab..47e8939b6 100644 --- a/app/views/play/level/WebSurfaceView.coffee +++ b/app/views/play/level/WebSurfaceView.coffee @@ -12,6 +12,8 @@ module.exports = class WebSurfaceView extends CocoView initialize: (options) -> @state = new State blah: 'blah' + @goals = (goal for goal in options.goalManager.goals when goal.html) + # Consider https://www.npmjs.com/package/css-select to do this on virtualDOM instead of in iframe on concreteDOM super(options) afterRender: -> @@ -34,7 +36,7 @@ module.exports = class WebSurfaceView extends CocoView # TODO: pull out the actual scripts, styles, and body/elements they are doing so we can merge them with our initial structure on the other side virtualDOM = @dekuify html messageType = if e.create or not @virtualDOM then 'create' else 'update' - @iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM}, '*' + @iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM, goals: @goals}, '*' @virtualDOM = virtualDOM checkGoals: (dom) ->