codecombat/app/lib/world/GoalManager.coffee
2016-07-15 00:40:32 -07:00

332 lines
14 KiB
CoffeeScript

CocoClass = require 'core/CocoClass'
utils = require 'core/utils'
module.exports = class GoalManager extends CocoClass
# The Goal Manager is created both on the main thread and
# each time the world is generated. The one in world generation
# records which code and world related goals
# are completed or failed, and then the results are sent back
# and saved to the main thread instance.
# The main instance handles goals based on UI notifications,
# and keeps track of what the goals are at any given point.
# Goals can only have only one goal property. Otherwise who knows what will happen.
# If you want weird goals or hybrid goals, make a custom goal.
nextGoalID: 0
nicks: ['GoalManager']
constructor: (@world, @initialGoals, @team, options) ->
super()
@options = options or {}
@init()
init: ->
@goals = []
@goalStates = {} # goalID -> object (complete, frameCompleted)
@userCodeMap = {} # @userCodeMap.thangID.methodName.aether.raw = codeString
@thangTeams = {}
@initThangTeams()
@addGoal goal for goal in @initialGoals if @initialGoals
initThangTeams: ->
return unless @world
for thang in @world.thangs when thang.team and thang.isAttackable
continue unless thang.team
@thangTeams[thang.team] = [] unless @thangTeams[thang.team]
@thangTeams[thang.team].push(thang.id)
subscriptions:
'god:new-world-created': 'onNewWorldCreated'
'level:restarted': 'onLevelRestarted'
#'tome:html-updated': 'onHTMLUpdated'
backgroundSubscriptions:
'world:thang-died': 'onThangDied'
'world:thang-touched-goal': 'onThangTouchedGoal'
'world:thang-left-map': 'onThangLeftMap'
'world:thang-collected-item': 'onThangCollectedItem'
'world:user-code-problem': 'onUserCodeProblem'
'world:lines-of-code-counted': 'onLinesOfCodeCounted'
onLevelRestarted: ->
@goals = []
@goalStates = {}
@userCodeMap = {}
@notifyGoalChanges()
@addGoal goal for goal in @initialGoals if @initialGoals
# INTERFACE AND LIFETIME OVERVIEW
# world generator gets current goals from the main instance
getGoals: -> @goals
# background instance created by world generator,
# gets these goals and code, and is told to be all ears during world gen
setGoals: (@goals) ->
setCode: (@userCodeMap) -> @updateCodeGoalStates()
worldGenerationWillBegin: ->
@initGoalStates()
@checkForInitialUserCodeProblems()
# World generator feeds world events to the goal manager to keep track
submitWorldGenerationEvent: (channel, event, frameNumber) ->
func = @backgroundSubscriptions[channel]
func = utils.normalizeFunc(func, @)
return unless func
func.call(@, event, frameNumber)
# after world generation, generated goal states
# are grabbed to send back to main instance
worldGenerationEnded: (finalFrame) -> @wrapUpGoalStates(finalFrame)
getGoalStates: -> @goalStates
# main instance gets them and updates their existing goal states,
# passes the word along
onNewWorldCreated: (e) ->
@world = e.world
@updateGoalStates(e.goalStates) if e.goalStates?
updateGoalStates: (newGoalStates) ->
for goalID, goalState of newGoalStates
continue unless @goalStates[goalID]?
@goalStates[goalID] = goalState
@notifyGoalChanges()
# IMPLEMENTATION DETAILS
addGoal: (goal) ->
goal = $.extend(true, {}, goal)
goal.id = @nextGoalID++ if not goal.id
return if @goalStates[goal.id]?
@goals.push(goal)
goal.isPositive = @goalIsPositive goal.id
@goalStates[goal.id] = {status: 'incomplete', keyFrame: 0, team: goal.team}
@notifyGoalChanges()
return unless goal.notificationGoal
f = (channel) => (event) => @onNote(channel, event)
channel = goal.notificationGoal.channel
@addNewSubscription(channel, f(channel))
notifyGoalChanges: ->
return if @options.headless
overallStatus = @checkOverallStatus()
event =
goalStates: @goalStates
goals: @goals
overallStatus: overallStatus
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) ->
overallStatus = null
goals = if @goalStates then _.values @goalStates else []
goals = (g for g in goals when not g.optional)
goals = (g for g in goals when g.team in [undefined, @team]) if @team
statuses = (goal.status for goal in goals)
overallStatus = 'success' if statuses.length > 0 and _.every(statuses, (s) -> s is 'success' or (ignoreIncomplete and s is null))
overallStatus = 'failure' if statuses.length > 0 and 'failure' in statuses
#console.log 'got overallStatus', overallStatus, 'from goals', goals, 'goalStates', @goalStates, 'statuses', statuses
overallStatus
# WORLD GOAL TRACKING
initGoalStates: ->
@goalStates = {}
return unless @goals
for goal in @goals
state = {
status: null # should eventually be either 'success', 'failure', or 'incomplete'
keyFrame: 0 # when it became a 'success' or 'failure'
team: goal.team
optional: goal.optional
}
@initGoalState(state, [goal.killThangs, goal.saveThangs], 'killed')
for getTo in goal.getAllToLocations ? []
@initGoalState(state, [getTo.getToLocation?.who, []], 'arrived')
for keepFrom in goal.keepAllFromLocations ? []
@initGoalState(state, [[], keepFrom.keepFromLocation?.who], 'arrived')
@initGoalState(state, [goal.getToLocations?.who, goal.keepFromLocations?.who], 'arrived')
@initGoalState(state, [goal.leaveOffSides?.who, goal.keepFromLeavingOffSides?.who], 'left')
@initGoalState(state, [goal.collectThangs?.targets, goal.keepFromCollectingThangs?.targets], 'collected')
@initGoalState(state, [goal.codeProblems], 'problems')
@initGoalState(state, [_.keys(goal.linesOfCode ? {})], 'lines')
@goalStates[goal.id] = state
checkForInitialUserCodeProblems: ->
# There might have been some user code problems reported before the goal manager started listening.
return unless @world
for thang in @world.thangs when thang.isProgrammable
for message, problem of thang.publishedUserCodeProblems
@onUserCodeProblem {thang: thang, problem: problem}, 0
onThangDied: (e, frameNumber) ->
for goal in @goals ? []
@checkKillThangs(goal.id, goal.killThangs, e.thang, frameNumber) if goal.killThangs?
@checkKillThangs(goal.id, goal.saveThangs, e.thang, frameNumber) if goal.saveThangs?
checkKillThangs: (goalID, targets, thang, frameNumber) ->
return unless thang.id in targets or thang.team in targets
@updateGoalState(goalID, thang.id, 'killed', frameNumber)
onThangTouchedGoal: (e, frameNumber) ->
for goal in @goals ? []
@checkArrived(goal.id, goal.getToLocations.who, goal.getToLocations.targets, e.actor, e.touched.id, frameNumber) if goal.getToLocations?
if goal.getAllToLocations?
for getTo in goal.getAllToLocations
@checkArrived(goal.id, getTo.getToLocation.who, getTo.getToLocation.targets, e.actor, e.touched.id, frameNumber)
@checkArrived(goal.id, goal.keepFromLocations.who, goal.keepFromLocations.targets, e.actor, e.touched.id, frameNumber) if goal.keepFromLocations?
if goal.keepAllFromLocations?
for keepFrom in goal.keepAllFromLocations
@checkArrived(goal.id, keepFrom.keepFromLocation.who , keepFrom.keepFromLocation.targets, e.actor, e.touched.id, frameNumber )
checkArrived: (goalID, who, targets, thang, touchedID, frameNumber) ->
return unless touchedID in targets
return unless thang.id in who or thang.team in who
@updateGoalState(goalID, thang.id, 'arrived', frameNumber)
onThangLeftMap: (e, frameNumber) ->
for goal in @goals ? []
@checkLeft(goal.id, goal.leaveOffSides.who, goal.leaveOffSides.sides, e.thang, e.side, frameNumber) if goal.leaveOffSides?
@checkLeft(goal.id, goal.keepFromLeavingOffSides.who, goal.keepFromLeavingOffSides.sides, e.thang, e.side, frameNumber) if goal.keepFromLeavingOffSides?
checkLeft: (goalID, who, sides, thang, side, frameNumber) ->
return if sides and side and not (side in sides)
return unless thang.id in who or thang.team in who
@updateGoalState(goalID, thang.id, 'left', frameNumber)
onThangCollectedItem: (e, frameNumber) ->
for goal in @goals ? []
@checkCollected(goal.id, goal.collectThangs.who, goal.collectThangs.targets, e.actor, e.item.id, frameNumber) if goal.collectThangs?
@checkCollected(goal.id, goal.keepFromCollectingThangs.who, goal.keepFromCollectingThangs.targets, e.actor, e.item.id, frameNumber) if goal.keepFromCollectingThangs?
checkCollected: (goalID, who, targets, thang, itemID, frameNumber) ->
return unless itemID in targets
return unless thang.id in who or thang.team in who
@updateGoalState(goalID, itemID, 'collected', frameNumber)
onUserCodeProblem: (e, frameNumber) ->
for goal in @goals ? [] when goal.codeProblems
@checkCodeProblem goal.id, goal.codeProblems, e.thang, frameNumber
checkCodeProblem: (goalID, who, thang, frameNumber) ->
return unless thang.id in who or thang.team in who
@updateGoalState goalID, thang.id, 'problems', frameNumber
onLinesOfCodeCounted: (e, frameNumber) ->
for goal in @goals ? [] when goal.linesOfCode
@checkLinesOfCode goal.id, goal.linesOfCode, e.thang, e.linesUsed, frameNumber
checkLinesOfCode: (goalID, who, thang, linesUsed, frameNumber) ->
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
if @goalIsPositive(goalID)
state.status = 'incomplete'
else
state.status = 'success'
state.keyFrame = 'end' # special case for objective UI to handle
# UI EVENT GOAL TRACKING
onNote: (channel, e) ->
# TODO for UI event related goals
# HELPER FUNCTIONS
# It's a pretty similar pattern for each of the above goals.
# Once you determine a thang has done the thing, you mark it done in the progress object.
initGoalState: (state, whos, progressObjectName) ->
# 'whos' is an array of goal 'who' values.
# This inits the progress object for the goal tracking.
arrays = (prop for prop in whos when prop?.length)
return unless arrays.length
state[progressObjectName] ?= {}
for array in arrays
for thang in array
if @thangTeams[thang]?
for t in @thangTeams[thang]
state[progressObjectName][t] = false
else
state[progressObjectName][thang] = false
getGoalState: (goalID) ->
@goalStates[goalID].status
setGoalState: (goalID, status) ->
state = @goalStates[goalID]
state.status = status
if overallStatus = @checkOverallStatus true
matchedGoals = (_.find(@goals, {id: goalID}) for goalID, goalState of @goalStates when goalState.status is overallStatus)
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
victory = overallStatus is 'success'
tentative = overallStatus is 'success'
@world?.endWorld victory, mostEagerGoal.worldEndsAfter, tentative if mostEagerGoal isnt Infinity
updateGoalState: (goalID, thangID, progressObjectName, frameNumber) ->
# A thang has done something related to the goal!
# Mark it down and update the goal state.
goal = _.find @goals, {id: goalID}
state = @goalStates[goalID]
stateThangs = state[progressObjectName]
stateThangs[thangID] = true
success = @goalIsPositive goalID
if success
numNeeded = goal.howMany ? Math.max(1, _.size stateThangs)
else
# saveThangs: by default we would want to save all the Thangs, which means that we would want none of them to be 'done'
numNeeded = _.size(stateThangs) - Math.max((goal.howMany ? 1), _.size stateThangs) + 1
numDone = _.filter(stateThangs).length
#console.log 'needed', numNeeded, 'done', numDone, 'of total', _.size(stateThangs), 'with how many', goal.howMany, 'and stateThangs', stateThangs, 'for', goalID, thangID, 'on frame', frameNumber, 'all Thangs', _.keys(stateThangs), _.values(stateThangs)
return unless numDone >= numNeeded
return if state.status and not success # already failed it; don't wipe keyframe
state.status = if success then 'success' else 'failure'
state.keyFrame = frameNumber
#console.log goalID, 'became', success, 'on frame', frameNumber, 'with overallStatus', @checkOverallStatus true
if overallStatus = @checkOverallStatus true
matchedGoals = (_.find(@goals, {id: goalID}) for goalID, goalState of @goalStates when goalState.status is overallStatus)
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'
victory = overallStatus is 'success'
tentative = overallStatus is 'success'
@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)
# Negative goals fail when any are true (keep all these thangs from being killed)
goal = _.find(@goals, {id: goalID}) ? {}
return false for prop of goal when @positiveGoalMap[prop] is 0
true
positiveGoalMap:
killThangs: 1
saveThangs: 0
getToLocations: 1
getAllToLocations: 1
keepFromLocations: 0
keepAllFromLocations: 0
leaveOffSides: 1
keepFromLeavingOffSides: 0
collectThangs: 1
keepFromCollectingThangs: 0
linesOfCode: 0
codeProblems: 0
#onHTMLUpdated: (e) ->
# @checkHTML goal, e.html for goal in @goals when goal.html
updateCodeGoalStates: ->
# TODO
# teardown
destroy: ->
super()