mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-18 03:23:42 -05:00
266 lines
10 KiB
CoffeeScript
266 lines
10 KiB
CoffeeScript
|
CocoClass = require 'lib/CocoClass'
|
||
|
utils = require 'lib/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
|
||
|
|
||
|
constructor: (@world) ->
|
||
|
super()
|
||
|
@init()
|
||
|
|
||
|
init: ->
|
||
|
@goals = []
|
||
|
@goalStates = {} # goalID -> object (complete, frameCompleted)
|
||
|
@userCodeMap = {} # @userCodeMap.thangID.methodName.aether.raw = codeString
|
||
|
@thangTeams = {}
|
||
|
@initThangTeams()
|
||
|
|
||
|
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:
|
||
|
'level-add-goals': 'onAddGoals'
|
||
|
'level-remove-goals': 'onRemoveGoals'
|
||
|
'god:new-world-created': 'onNewWorldCreated'
|
||
|
'level:restarted': 'onLevelRestarted'
|
||
|
|
||
|
backgroundSubscriptions:
|
||
|
'world:thang-died': 'onThangDied'
|
||
|
'world:thang-touched-goal': 'onThangTouchedGoal'
|
||
|
'world:thang-left-map': 'onThangLeftMap'
|
||
|
'world:thang-collected-item': 'onThangCollectedItem'
|
||
|
'world:ended': 'onWorldEnded'
|
||
|
|
||
|
onLevelRestarted: =>
|
||
|
@goals = []
|
||
|
@goalStates = {}
|
||
|
@userCodeMap = {}
|
||
|
@notifyGoalChanges()
|
||
|
|
||
|
# INTERFACE AND LIFETIME OVERVIEW
|
||
|
|
||
|
# main instance receives goal updates from the script manager
|
||
|
onAddGoals: (e) =>
|
||
|
return unless e.worldName is @world.name
|
||
|
goals = e.goals
|
||
|
@addGoal(goal) for goal in goals
|
||
|
|
||
|
onRemoveGoals: (e) =>
|
||
|
if e.goal in @goals
|
||
|
@goals.remove(e.goal)
|
||
|
delete @goalStates[e.goal]
|
||
|
|
||
|
# 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()
|
||
|
|
||
|
# 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(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) =>
|
||
|
@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.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}
|
||
|
@notifyGoalChanges()
|
||
|
return unless goal.notificationGoal
|
||
|
f = (channel) => (event) => @onNote(channel, event)
|
||
|
channel = goal.notificationGoal.channel
|
||
|
@addNewSubscription(channel, f(channel))
|
||
|
|
||
|
notifyGoalChanges: ->
|
||
|
overallStatus = @checkOverallStatus()
|
||
|
event = {goalStates: @goalStates, goals: @goals, overallStatus: overallStatus}
|
||
|
#console.log JSON.stringify(event), "new goal states"
|
||
|
Backbone.Mediator.publish('goal-manager:new-goal-states', event)
|
||
|
|
||
|
checkOverallStatus: (ignoreIncomplete=false) ->
|
||
|
overallStatus = null
|
||
|
statuses = if @goalStates then (val.status for key, val of @goalStates) else []
|
||
|
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
|
||
|
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'
|
||
|
}
|
||
|
|
||
|
@initGoalState(state, [goal.killThangs, goal.saveThangs], 'killed')
|
||
|
@initGoalState(state, [goal.getToLocations?.who, goal.keepFromLocations?.who], 'arrived')
|
||
|
@initGoalState(state, [goal.leaveOffSides?.who, goal.keepFromLeavingOffSides?.who], 'left')
|
||
|
@initGoalState(state, [goal.collectThangs?.who, goal.keepFromCollectingThangs?.who], 'collected')
|
||
|
@goalStates[goal.id] = state
|
||
|
|
||
|
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.id, e.touched.id, frameNumber) if goal.getToLocations?
|
||
|
@checkArrived(goal.id, goal.keepFromLocations.who, goal.keepFromLocations.targets, e.actor.id, e.touched.id, frameNumber) if goal.keepFromLocations?
|
||
|
|
||
|
checkArrived: (goalID, who, targets, thangID, touchedID, frameNumber) =>
|
||
|
return unless touchedID in targets
|
||
|
return unless thangID in who
|
||
|
@updateGoalState(goalID, thangID, 'arrived', frameNumber)
|
||
|
|
||
|
onThangLeftMap: (e, frameNumber) =>
|
||
|
for goal in @goals ? []
|
||
|
@checkLeft(goal.id, goal.leaveOffSides.who, goal.leaveOffSides.sides, e.thang.id, e.side, frameNumber) if goal.leaveOffSides?
|
||
|
@checkLeft(goal.id, goal.keepFromLeavingOffSides.who, goal.keepFromLeavingOffSides.sides, e.thang.id, e.side, frameNumber) if goal.keepFromLeavingOffSides?
|
||
|
|
||
|
checkLeft: (goalID, who, sides, thangID, side, frameNumber) =>
|
||
|
return if sides and side and not (side in sides)
|
||
|
return unless thangID in who
|
||
|
@updateGoalState(goalID, thangID, 'left', frameNumber)
|
||
|
|
||
|
onThangCollectedItem: (e, frameNumber) =>
|
||
|
for goal in @goals ? []
|
||
|
@checkCollected(goal.id, goal.collectThangs.who, goal.collectThangs.targets, e.actor.id, e.item.id, frameNumber) if goal.collectThangs?
|
||
|
@checkCollected(goal.id, goal.keepFromCollectingThangs.who, goal.keepFromCollectingThangs.targets, e.actor.id, e.item.id, frameNumber) if goal.keepFromCollectingThangs?
|
||
|
|
||
|
checkCollected: (goalID, who, targets, thangID, itemID, frameNumber) =>
|
||
|
return unless itemID in targets
|
||
|
return unless thangID in who
|
||
|
@updateGoalState(goalID, thangID, 'collected', frameNumber)
|
||
|
|
||
|
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
|
||
|
|
||
|
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 = Math.max((goal.howMany ? 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.min((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
|
||
|
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
|
||
|
keepFromLocations: 0
|
||
|
leaveOffSides: 1
|
||
|
keepFromLeavingOffSides: 0
|
||
|
collectThangs: 1
|
||
|
keepFromCollectingThangs: 0
|
||
|
|
||
|
updateCodeGoalStates: ->
|
||
|
# TODO
|
||
|
|
||
|
# teardown
|
||
|
|
||
|
destroy: ->
|
||
|
super()
|