log = require 'winston' async = require 'async' bayes = new (require 'bayesian-battle')() LevelSession = require '../../levels/sessions/LevelSession' User = require '../../users/User' perfmon = require '../../commons/perfmon' SIMULATOR_VERSION = 3 module.exports.scoringTaskTimeoutInSeconds = 600 module.exports.scoringTaskQueue = null module.exports.simulatorIsTooOld = (req, res) -> clientSimulator = req.body.simulator return false if clientSimulator?.version >= SIMULATOR_VERSION message = "Old simulator version #{clientSimulator?.version}, need to clear cache and get version #{SIMULATOR_VERSION}." log.debug "400: #{message}" res.send 400, message res.end() true module.exports.sendResponseObject = (res, object) -> res.setHeader('Content-Type', 'application/json') res.send(object) res.end() module.exports.formatSessionInformation = (session) -> sessionID: session._id team: session.team ? 'No team' transpiledCode: session.transpiledCode submittedCodeLanguage: session.submittedCodeLanguage teamSpells: session.teamSpells ? {} levelID: session.levelID creatorName: session.creatorName creator: session.creator totalScore: session.totalScore submitDate: session.submitDate shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague module.exports.calculateSessionScores = (callback) -> sessionIDs = _.map @clientResponseObject.sessions, 'sessionID' async.map sessionIDs, retrieveOldSessionData.bind(@), (err, oldScores) => if err? then return callback err, {error: 'There was an error retrieving the old scores'} try oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores newScoreArray = updatePlayerSkills oldScoreArray createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray callback err, newScoreArray catch e callback e retrieveOldSessionData = (sessionID, callback) -> formatOldScoreObject = (session) => oldScoreObject = standardDeviation: session.standardDeviation ? 25/3 meanStrength: session.meanStrength ? 25 totalScore: session.totalScore ? (25 - 1.8*(25/3)) 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 = [] for league in session.leagues oldScoreObject.leagues.push leagueID: league.leagueID stats: id: sessionID standardDeviation: league.stats.standardDeviation ? 25/3 meanStrength: league.stats.meanStrength ? 25 totalScore: league.stats.totalScore ? (25 - 1.8*(25/3)) oldScoreObject return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again query = _id: sessionID 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 putRankingFromMetricsIntoScoreObject = (taskObject, scoreObject) -> scoreObject = _.indexBy scoreObject, 'id' sharedLeagueIDs = (league.leagueID for league in (taskObject.sessions[0].leagues ? []) when _.find(taskObject.sessions[1].leagues, leagueID: league.leagueID)) for session in taskObject.sessions scoreObject[session.sessionID].gameRanking = session.metrics.rank for league in (session.leagues ? []) when league.leagueID in sharedLeagueIDs # We will also score any shared leagues, and we indicate that by assigning a non-null gameRanking to them. _.find(scoreObject[session.sessionID].leagues, leagueID: league.leagueID).stats.gameRanking = session.metrics.rank return scoreObject updatePlayerSkills = (oldScoreArray) -> newScoreArray = bayes.updatePlayerSkills oldScoreArray scoreObjectA = newScoreArray[0] scoreObjectB = newScoreArray[1] for leagueA in (scoreObjectA.leagues ? []) when leagueA.stats.gameRanking? leagueB = _.find scoreObjectB.leagues, leagueID: leagueA.leagueID [leagueA.stats, leagueB.stats] = bayes.updatePlayerSkills [leagueA.stats, leagueB.stats] leagueA.stats.updated = leagueB.stats.updated = true newScoreArray createSessionScoreUpdate = (scoreObject) -> newTotalScore = scoreObject.meanStrength - 1.8 * scoreObject.standardDeviation scoreHistoryAddition = [Date.now(), newTotalScore] @levelSessionUpdates ?= {} @levelSessionUpdates[scoreObject.id] = meanStrength: scoreObject.meanStrength standardDeviation: scoreObject.standardDeviation totalScore: newTotalScore $push: {scoreHistory: {$each: [scoreHistoryAddition], $slice: -1000}} randomSimulationIndex: Math.random() for league, leagueIndex in (scoreObject.leagues ? []) continue unless league.stats.updated newTotalScore = league.stats.meanStrength - 1.8 * league.stats.standardDeviation scoreHistoryAddition = [scoreHistoryAddition[0], newTotalScore] leagueSetPrefix = "leagues.#{leagueIndex}.stats." 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) -> newScoresObject = _.indexBy newScoreArray, 'id' @newScoresObject = newScoresObject callback null, newScoresObject module.exports.addMatchToSessionsAndUpdate = (newScoreObject, callback) -> matchObject = {} matchObject.date = new Date() matchObject.opponents = {} for session in @clientResponseObject.sessions sessionID = session.sessionID matchObject.opponents[sessionID] = match = {} match.sessionID = sessionID match.userID = session.creator match.name = session.name match.totalScore = session.totalScore match.metrics = {} match.metrics.rank = Number(newScoreObject[sessionID]?.gameRanking ? 0) match.codeLanguage = newScoreObject[sessionID].submittedCodeLanguage #log.info "Match object computed, result: #{JSON.stringify(matchObject, null, 2)}" #log.info 'Writing match object to database...' #use bind with async to do the writes sessionIDs = _.map @clientResponseObject.sessions, 'sessionID' async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) -> callback err ladderBenchmarkAIs = '564ba6cea33967be1312ae59': 0 '564ba830a33967be1312ae61': 1 '564ba91aa33967be1312ae65': 2 '564ba95ca33967be1312ae69': 3 '564ba9b7a33967be1312ae6d': 4 updateMatchesInSession = (matchObject, sessionID, callback) -> currentMatchObject = {} currentMatchObject.date = matchObject.date currentMatchObject.metrics = matchObject.opponents[sessionID].metrics opponentsClone = _.cloneDeep matchObject.opponents opponentsClone = _.omit opponentsClone, sessionID opponentsArray = _.toArray opponentsClone currentMatchObject.opponents = opponentsArray currentMatchObject.codeLanguage = matchObject.opponents[opponentsArray[0].sessionID].codeLanguage # TODO: we have our opponent code language in twice, do we maybe want our own code language instead? #currentMatchObject.simulator = @clientResponseObject.simulator # Uncomment when actively debugging simulation mismatches #currentMatchObject.randomSeed = parseInt(@clientResponseObject.randomSeed or 0, 10) # Uncomment when actively debugging simulation mismatches sessionUpdateObject = @levelSessionUpdates[sessionID] sessionUpdateObject.$push.matches = {$each: [currentMatchObject], $slice: -200} if currentMatchObject.metrics.rank is 0 and defeatedAI = ladderBenchmarkAIs[currentMatchObject.opponents[0].userID] mySession = _.find @clientResponseObject.sessions, sessionID: sessionID newLadderAchievementDifficulty = Math.max defeatedAI, mySession.ladderAchievementDifficulty || 0 if newLadderAchievementDifficulty isnt mySession.ladderAchievementDifficulty sessionUpdateObject.ladderAchievementDifficulty = newLadderAchievementDifficulty myScoreObject = @newScoresObject[sessionID] opponentSession = _.find @clientResponseObject.sessions, (session) -> session.sessionID isnt sessionID for league, leagueIndex in myScoreObject.leagues ? [] continue unless league.stats.updated opponentLeagueTotalScore = _.find(opponentSession.leagues, leagueID: league.leagueID).stats.totalScore ? (25 - 1.8*(25/3)) 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 module.exports.updateUserSimulationCounts = (reqUserID, callback) -> incrementUserSimulationCount reqUserID, 'simulatedBy', (err) => if err? then return callback err #console.log 'Incremented user simulation count!' perfmon.client.increment 'simulations' unless @isRandomMatch incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback else callback null incrementUserSimulationCount = (userID, type, callback) => return callback null unless userID inc = {} inc[type] = 1 User.update {_id: userID}, {$inc: inc}, (err, result) -> log.error "Error incrementing #{type} for #{userID}: #{err}" if err callback err module.exports.calculateOpposingTeam = (sessionTeam) -> teams = ['ogres', 'humans'] opposingTeams = _.pull teams, sessionTeam return opposingTeams[0] module.exports.sendEachTaskPairToTheQueue = (taskPairs, callback) -> async.each taskPairs, sendTaskPairToQueue, callback sendTaskPairToQueue = (taskPair, callback) -> module.exports.scoringTaskQueue.sendMessage {sessions: taskPair}, 5, (err, data) -> callback? err, data module.exports.generateTaskPairs = (submittedSessions, sessionToScore) -> taskPairs = [] for session in submittedSessions if session.toObject? session = session.toObject() teams = ['ogres', 'humans'] opposingTeams = _.pull teams, sessionToScore.team if String(session._id) isnt String(sessionToScore._id) and session.team in opposingTeams #console.log 'Adding game to taskPairs!' taskPairs.push [sessionToScore._id, String session._id] return taskPairs module.exports.addPairwiseTaskToQueue = (taskPair, cb) -> LevelSession.findOne(_id: taskPair[0]).lean().exec (err, firstSession) => if err? then return cb err LevelSession.find(_id: taskPair[1]).exec (err, secondSession) => if err? then return cb err try taskPairs = module.exports.generateTaskPairs(secondSession, firstSession) catch e if e then return cb e module.exports.sendEachTaskPairToTheQueue taskPairs, (taskPairError) -> if taskPairError? then return cb taskPairError cb null