diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index 3e43d22ae..d17ea91bc 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -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 diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee index 1343aff15..56fde06a8 100644 --- a/app/schemas/models/level_session.coffee +++ b/app/schemas/models/level_session.coffee @@ -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' diff --git a/app/views/ladder/LadderView.coffee b/app/views/ladder/LadderView.coffee index da050f1f4..d5674a32d 100644 --- a/app/views/ladder/LadderView.coffee +++ b/app/views/ladder/LadderView.coffee @@ -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. diff --git a/app/views/ladder/SimulateTabView.coffee b/app/views/ladder/SimulateTabView.coffee index 8d6cfeac6..02751e4bf 100644 --- a/app/views/ladder/SimulateTabView.coffee +++ b/app/views/ladder/SimulateTabView.coffee @@ -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 diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 6f2f49a8d..8687266a7 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -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 diff --git a/server/queues/scoring/createNewTask.coffee b/server/queues/scoring/createNewTask.coffee index 1ee2a22a0..0318e45a3 100644 --- a/server/queues/scoring/createNewTask.coffee +++ b/server/queues/scoring/createNewTask.coffee @@ -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 diff --git a/server/queues/scoring/getTwoGames.coffee b/server/queues/scoring/getTwoGames.coffee index 9e660ee6c..eb7ee6d79 100644 --- a/server/queues/scoring/getTwoGames.coffee +++ b/server/queues/scoring/getTwoGames.coffee @@ -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 diff --git a/server/queues/scoring/scoringUtils.coffee b/server/queues/scoring/scoringUtils.coffee index eaa5cec1b..a25bb826b 100644 --- a/server/queues/scoring/scoringUtils.coffee +++ b/server/queues/scoring/scoringUtils.coffee @@ -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