Improve simulation game logic, and re-enable automatic simulations under certain conditions, better targeted toward the matches the player cares about

This commit is contained in:
Nick Winter 2015-12-06 09:20:30 -08:00
parent 45418dbfe3
commit 1187390fd0
8 changed files with 65 additions and 24 deletions

View file

@ -46,6 +46,8 @@ module.exports = class Simulator extends CocoClass
ogresGameID: ogresGameID
simulator: @simulator
background: Boolean(@options.background)
levelID: @options.levelID
leagueID: @options.leagueID
error: (errorData) ->
console.warn "There was an error fetching two games! #{JSON.stringify errorData}"
if errorData?.responseText?.indexOf("Old simulator") isnt -1
@ -349,6 +351,7 @@ module.exports = class Simulator extends CocoClass
totalScore: session.totalScore
metrics:
rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap()
shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague
if session.sessionID is taskResults.originalSessionID
taskResults.originalSessionRank = sessionResult.metrics.rank
taskResults.originalSessionTeam = session.team

View file

@ -304,6 +304,7 @@ _.extend LevelSessionSchema.properties,
c.object {},
leagueID: {type: 'string', description: 'The _id of a Clan or CourseInstance the user belongs to.'}
stats: c.object {description: 'Multiplayer match statistics corresponding to this entry in the league.'}
lastOpponentSubmitDate: c.date {description: 'The submitDate of the last league session we selected to play against (for playing through league opponents in order).'}
LevelSessionSchema.properties.leagues.items.properties.stats.properties = _.pick LevelSessionSchema.properties, 'meanStrength', 'standardDeviation', 'totalScore', 'numberOfWinsAndTies', 'numberOfLosses', 'scoreHistory', 'matches'

View file

@ -91,7 +91,7 @@ module.exports = class LadderView extends RootView
return unless @supermodel.finished()
@insertSubView(@ladderTab = new LadderTabView({league: @league}, @level, @sessions))
@insertSubView(@myMatchesTab = new MyMatchesTabView({league: @league}, @level, @sessions))
@insertSubView(@simulateTab = new SimulateTabView(league: @league))
@insertSubView(@simulateTab = new SimulateTabView(league: @league, level: @level, leagueID: @leagueID))
highLoad = true
@refreshDelay = switch
when not application.isProduction() then 10 # Refresh very quickly in develompent.

View file

@ -26,7 +26,7 @@ module.exports = class SimulateTabView extends CocoView
onLoaded: ->
super()
@render()
if document.location.hash is '#simulate' and not @simulator
if (document.location.hash is '#simulate' or @options.level.get('type') is 'course-ladder') and not @simulator
@startSimulating()
getRenderData: ->
@ -59,7 +59,7 @@ module.exports = class SimulateTabView extends CocoView
simulateNextGame: ->
unless @simulator
@simulator = new Simulator()
@simulator = new Simulator levelID: @options.level.get('slug'), leagueID: @options.leagueID
@listenTo @simulator, 'statusUpdate', @updateSimulationStatus
# Work around simulator getting super slow on Chrome
fetchAndSimulateTaskOriginal = @simulator.fetchAndSimulateTask

View file

@ -410,7 +410,9 @@ module.exports = class PlayLevelView extends RootView
simulateNextGame: ->
return @simulator.fetchAndSimulateOneGame() if @simulator
@simulator = new Simulator background: true
simulatorOptions = background: true, leagueID: @courseInstanceID
simulatorOptions.levelID = @level.get('slug') if @level.get('type', true) in ['course-ladder', 'hero-ladder']
@simulator = new Simulator simulatorOptions
# Crude method of mitigating Simulator memory leak issues
fetchAndSimulateOneGameOriginal = @simulator.fetchAndSimulateOneGame
@simulator.fetchAndSimulateOneGame = =>
@ -424,8 +426,8 @@ module.exports = class PlayLevelView extends RootView
@simulator.fetchAndSimulateOneGame()
shouldSimulate: ->
return @getQueryVariable('simulate') is true # Performance is too bad right now, gotta fix it first.
# Crude heuristics are crude.
return true if @getQueryVariable('simulate') is true
stillBuggy = true # Keep this true while we still haven't fixed the zombie worker problem when simulating the more difficult levels on Chrome
defaultCores = 2
cores = window.navigator.hardwareConcurrency or defaultCores # Available on Chrome/Opera, soon Safari
defaultHeapLimit = 793000000
@ -440,14 +442,17 @@ module.exports = class PlayLevelView extends RootView
if levelType is 'course'
return false
else if levelType is 'hero' and gamesSimulated
return false if stillBuggy
return false if cores < 8
return false if heapLimit < defaultHeapLimit
return false if @loadDuration > 10000
else if levelType is 'hero-ladder' and gamesSimulated
return false if stillBuggy
return false if cores < 4
return false if heapLimit < defaultHeapLimit
return false if @loadDuration > 15000
else if levelType is 'hero-ladder' and not gamesSimulated
return false if stillBuggy
return false if cores < 8
return false if heapLimit <= defaultHeapLimit
return false if @loadDuration > 20000
@ -457,6 +462,7 @@ module.exports = class PlayLevelView extends RootView
return false if @loadDuration > 18000
else
console.warn "Unwritten level type simulation heuristics; fill these in for new level type #{levelType}?"
return false if stillBuggy
return false if cores < 8
return false if heapLimit < defaultHeapLimit
return false if @loadDuration > 10000

View file

@ -72,13 +72,14 @@ updateSessionToSubmit = (transpiledCode, user, sessionToUpdate, callback) ->
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to save them as strings.
newLeagues = []
for leagueID in leagueIDs
league = _.find(sessionToUpdate.leagues, leagueID: leagueID) ? leagueID: leagueID
league = _.clone(_.find(sessionToUpdate.leagues, leagueID: leagueID) ? leagueID: leagueID)
league.stats ?= {}
league.stats.standardDeviation = 25 / 3
league.stats.numberOfWinsAndTies = 0
league.stats.numberOfLosses = 0
league.stats.meanStrength ?= 25
league.stats.totalScore ?= 10
delete league.lastOpponentSubmitDate
newLeagues.push(league)
unless _.isEqual newLeagues, sessionToUpdate.leagues
sessionUpdateObject.leagues = sessionToUpdate.leagues = newLeagues

View file

@ -12,6 +12,8 @@ module.exports = getTwoGames = (req, res) ->
return getSpecificSessions res, humansSessionID, ogresSessionID if humansSessionID and ogresSessionID
options =
background: req.body.background
levelID: req.body.levelID
leagueID: req.body.leagueID
getRandomSessions req.user, options, sendSessionsResponse(res)
sessionSelectionString = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate leagues'
@ -39,12 +41,15 @@ getRandomSessions = (user, options, callback) ->
# Determine whether to play a random match, an internal league match, or an external league match.
# Only people in a league will end up simulating internal league matches (for leagues they're in) except by dumb chance.
# If we don't like that, we can rework sampleByLevel to have an opportunity to switch to internal leagues if the first session had a league affiliation.
leagueIDs = user?.get('clans') or []
leagueIDs = leagueIDs.concat user?.get('courseInstances') or []
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to fetch them as strings.
return sampleByLevel options, callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
leagueID = _.sample leagueIDs
findRandomSession {'leagues.leagueID': leagueID}, (err, session) ->
if not leagueID = options.leagueID
leagueIDs = user?.get('clans') or []
leagueIDs = leagueIDs.concat user?.get('courseInstances') or []
leagueIDs = (leagueID + '' for leagueID in leagueIDs) # Make sure to fetch them as strings.
return sampleByLevel options, callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
leagueID = _.sample leagueIDs
queryParameters = {'leagues.leagueID': leagueID}
queryParameters.levelID = options.levelID if options.levelID
findRandomSession queryParameters, (err, session) ->
if err then return callback err
unless session then return sampleByLevel options, callback
otherTeam = scoringUtils.calculateOpposingTeam session.team
@ -52,11 +57,15 @@ getRandomSessions = (user, options, callback) ->
if Math.random() < 0.5
# Try to play a match on the internal league ladder for this level
queryParameters['leagues.leagueID'] = leagueID
findRandomSession queryParameters, (err, otherSession) ->
findNextLeagueOpponent session, queryParameters, (err, otherSession) ->
if err then return callback err
if otherSession then return callback null, [session, otherSession]
if otherSession
console.log 'start off with it man', leagueID
session.shouldUpdateLastOpponentSubmitDateForLeague = leagueID
return callback null, [session, otherSession]
# No opposing league session found; try to play an external match
delete queryParameters['leagues.leagueID']
delete queryParameters.submitDate
findRandomSession queryParameters, (err, otherSession) ->
if err then return callback err
callback null, [session, otherSession]
@ -71,10 +80,26 @@ getRandomSessions = (user, options, callback) ->
ladderLevelIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland', 'zero-sum', 'ace-of-coders', 'wakka-maul']
backgroundLadderLevelIDs = _.without ladderLevelIDs, 'zero-sum', 'ace-of-coders'
sampleByLevel = (options, callback) ->
levelID = _.sample(if options.background then backgroundLadderLevelIDs else ladderLevelIDs)
levelID = options.levelID or _.sample(if options.background then backgroundLadderLevelIDs else ladderLevelIDs)
favorRecentHumans = Math.random() < 0.5 # We pick one session favoring recent submissions, then find another one uniformly to play against
async.map [{levelID: levelID, team: 'humans', favorRecent: favorRecentHumans}, {levelID: levelID, team: 'ogres', favorRecent: not favorRecentHumans}], findRandomSession, callback
findNextLeagueOpponent = (session, queryParams, callback) ->
queryParams.submitted = true
league = _.find session.leagues, leagueID: queryParams['leagues.leagueID']
lastOpponentSubmitDate = league.lastOpponentSubmitDate or new Date()
queryParams.submitDate = $lt: lastOpponentSubmitDate
sort = submitDate: -1
console.log "Making query", queryParams
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, otherSession) ->
return callback err if err
if otherSession and otherSession.creator + '' is session.creator + ''
console.log 'Do not play a league match against ourselves', queryParams.submitDate.$lt, typeof queryParams.submitDate.$lt
queryParams.submitDate.$lt = new Date(new Date(queryParams.submitDate.$lt) - 1)
console.log ' ', queryParams.submitDate, '-- is that better?'
return LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec callback
callback null, otherSession
findRandomSession = (queryParams, callback) ->
# In MongoDB 3.2, we will be able to easily get a random document with aggregate $sample: https://jira.mongodb.org/browse/SERVER-533
queryParams.submitted = true

View file

@ -36,7 +36,8 @@ module.exports.formatSessionInformation = (session) ->
creatorName: session.creatorName
creator: session.creator
totalScore: session.totalScore
submitDate: session.submitDate
shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague
module.exports.calculateSessionScores = (callback) ->
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
@ -59,6 +60,7 @@ retrieveOldSessionData = (sessionID, callback) ->
id: sessionID
submittedCodeLanguage: session.submittedCodeLanguage
ladderAchievementDifficulty: session.ladderAchievementDifficulty
submitDate: session.submitDate
if session.leagues?.length
_.find(@clientResponseObject.sessions, sessionID: sessionID).leagues = session.leagues
oldScoreObject.leagues = []
@ -75,7 +77,7 @@ retrieveOldSessionData = (sessionID, callback) ->
return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again
query = _id: sessionID
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues ladderAchievementDifficulty'
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues ladderAchievementDifficulty submitDate'
LevelSession.findOne(query).select(selection).lean().exec (err, session) ->
return callback err, {'error': 'There was an error retrieving the session.'} if err?
callback err, formatOldScoreObject session
@ -115,12 +117,13 @@ createSessionScoreUpdate = (scoreObject) ->
newTotalScore = league.stats.meanStrength - 1.8 * league.stats.standardDeviation
scoreHistoryAddition = [scoreHistoryAddition[0], newTotalScore]
leagueSetPrefix = "leagues.#{leagueIndex}.stats."
@levelSessionUpdates[scoreObject.id].$set ?= {}
@levelSessionUpdates[scoreObject.id].$push ?= {}
@levelSessionUpdates[scoreObject.id].$set[leagueSetPrefix + 'meanStrength'] = league.stats.meanStrength
@levelSessionUpdates[scoreObject.id].$set[leagueSetPrefix + 'standardDeviation'] = league.stats.standardDeviation
@levelSessionUpdates[scoreObject.id].$set[leagueSetPrefix + 'totalScore'] = newTotalScore
@levelSessionUpdates[scoreObject.id].$push[leagueSetPrefix + 'scoreHistory'] = {$each: [scoreHistoryAddition], $slice: -1000}
sessionUpdateObject = @levelSessionUpdates[scoreObject.id]
sessionUpdateObject.$set ?= {}
sessionUpdateObject.$push ?= {}
sessionUpdateObject.$set[leagueSetPrefix + 'meanStrength'] = league.stats.meanStrength
sessionUpdateObject.$set[leagueSetPrefix + 'standardDeviation'] = league.stats.standardDeviation
sessionUpdateObject.$set[leagueSetPrefix + 'totalScore'] = newTotalScore
sessionUpdateObject.$push[leagueSetPrefix + 'scoreHistory'] = {$each: [scoreHistoryAddition], $slice: -1000}
module.exports.indexNewScoreArray = (newScoreArray, callback) ->
@ -185,6 +188,8 @@ updateMatchesInSession = (matchObject, sessionID, callback) ->
leagueMatch = _.cloneDeep currentMatchObject
leagueMatch.opponents[0].totalScore = opponentLeagueTotalScore
sessionUpdateObject.$push["leagues.#{leagueIndex}.stats.matches"] = {$each: [leagueMatch], $slice: -200}
if _.find(@clientResponseObject.sessions, sessionID: sessionID).shouldUpdateLastOpponentSubmitDateForLeague is league.leagueID
sessionUpdateObject.$set["leagues.#{leagueIndex}.lastOpponentSubmitDate"] = new Date(opponentSession.submitDate) # TODO: somewhere, if these are already the same, don't record the match, since we likely just recorded the same match?
#log.info "Update for #{sessionID} is #{JSON.stringify(sessionUpdateObject, null, 2)}"
LevelSession.update {_id: sessionID}, sessionUpdateObject, callback