2015-08-13 14:17:38 -04:00
log = require ' winston '
async = require ' async '
bayes = new ( require ' bayesian-battle ' ) ( )
2016-04-06 13:56:06 -04:00
LevelSession = require ' ../../models/LevelSession '
User = require ' ../../models/User '
2015-12-09 09:22:40 -05:00
perfmon = require ' ../../commons/perfmon '
2016-05-05 16:22:30 -04:00
LZString = require ' lz-string '
2015-08-13 14:17:38 -04:00
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
2015-08-15 08:38:47 -04:00
module.exports.sendResponseObject = (res, object) ->
2015-08-13 14:17:38 -04:00
res . setHeader ( ' Content-Type ' , ' application/json ' )
res . send ( object )
res . end ( )
module.exports.formatSessionInformation = (session) ->
2016-05-05 16:22:30 -04:00
heroID = if session . team is ' ogres ' then ' hero-placeholder-1 ' else ' hero-placeholder '
submittedCode = { }
2016-05-24 16:42:37 -04:00
submittedCode [ heroID ] = plan: LZString . compressToUTF16 ( session . submittedCode ? [ heroID ] ? . plan ? ' ' )
2016-05-05 16:22:30 -04:00
_id: session . _id
2015-08-13 14:17:38 -04:00
sessionID: session . _id
team: session . team ? ' No team '
2016-05-05 16:22:30 -04:00
submittedCode: submittedCode
2015-08-13 14:17:38 -04:00
submittedCodeLanguage: session . submittedCodeLanguage
teamSpells: session . teamSpells ? { }
levelID: session . levelID
creatorName: session . creatorName
creator: session . creator
totalScore: session . totalScore
2015-12-06 12:20:30 -05:00
submitDate: session . submitDate
shouldUpdateLastOpponentSubmitDateForLeague: session . shouldUpdateLastOpponentSubmitDateForLeague
2015-08-13 14:17:38 -04:00
module.exports.calculateSessionScores = (callback) ->
2016-01-18 19:09:19 -05:00
sessionIDs = _ . map @ clientResponseObject . sessions , ' sessionID '
2015-08-15 08:38:47 -04:00
async . map sessionIDs , retrieveOldSessionData . bind ( @ ) , (err, oldScores) =>
if err ? then return callback err , { error: ' There was an error retrieving the old scores ' }
2016-01-20 12:34:25 -05:00
oldScoreArray = _ . toArray putRankingFromMetricsIntoScoreObject @ clientResponseObject , oldScores
newScoreArray = updatePlayerSkills oldScoreArray
createSessionScoreUpdate . call @ , scoreObject for scoreObject in newScoreArray
callback null , newScoreArray
2015-08-13 14:17:38 -04:00
retrieveOldSessionData = (sessionID, callback) ->
2015-08-15 08:38:47 -04:00
formatOldScoreObject = (session) =>
2015-11-17 18:23:35 -05:00
oldScoreObject =
2015-08-15 08:38:47 -04:00
standardDeviation: session . standardDeviation ? 25 / 3
meanStrength: session . meanStrength ? 25
totalScore: session . totalScore ? ( 25 - 1.8 * ( 25 / 3 ) )
id: sessionID
submittedCodeLanguage: session . submittedCodeLanguage
2015-11-17 18:23:35 -05:00
ladderAchievementDifficulty: session . ladderAchievementDifficulty
2015-12-06 12:20:30 -05:00
submitDate: session . submitDate
2015-08-15 08:38:47 -04:00
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
2015-08-13 14:17:38 -04:00
return formatOldScoreObject @ levelSession if sessionID is @ levelSession ? . _id # No need to fetch again
query = _id: sessionID
2015-12-06 12:20:30 -05:00
selection = ' standardDeviation meanStrength totalScore submittedCodeLanguage leagues ladderAchievementDifficulty submitDate '
2015-08-13 14:17:38 -04:00
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 '
2015-08-15 08:38:47 -04:00
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
2015-08-13 14:17:38 -04:00
return scoreObject
2015-08-15 08:38:47 -04:00
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
2015-08-13 14:17:38 -04:00
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 ( )
2015-08-15 08:38:47 -04:00
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. "
2015-12-06 12:20:30 -05:00
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 }
2015-08-13 14:17:38 -04:00
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
2016-01-18 19:09:19 -05:00
sessionIDs = _ . map @ clientResponseObject . sessions , ' sessionID '
2015-08-13 14:17:38 -04:00
async . each sessionIDs , updateMatchesInSession . bind ( @ , matchObject ) , (err) ->
callback err
2015-11-17 18:23:35 -05:00
ladderBenchmarkAIs =
' 564ba6cea33967be1312ae59 ' : 0
' 564ba830a33967be1312ae61 ' : 1
' 564ba91aa33967be1312ae65 ' : 2
' 564ba95ca33967be1312ae69 ' : 3
' 564ba9b7a33967be1312ae6d ' : 4
2015-08-13 14:17:38 -04:00
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
2015-08-15 08:38:47 -04:00
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?
2015-08-13 14:17:38 -04:00
#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 }
2015-11-17 18:23:35 -05:00
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
2015-08-15 08:38:47 -04:00
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 }
2015-12-06 12:20:30 -05:00
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?
2015-08-15 08:38:47 -04:00
#log.info "Update for #{sessionID} is #{JSON.stringify(sessionUpdateObject, null, 2)}"
2015-08-13 14:17:38 -04:00
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!'
2015-12-09 09:22:40 -05:00
perfmon . client . increment ' simulations '
2015-08-13 14:17:38 -04:00
unless @ isRandomMatch
incrementUserSimulationCount @ levelSession . creator , ' simulatedFor ' , callback
else
callback null
incrementUserSimulationCount = (userID, type, callback) =>
return callback null unless userID
inc = { }
inc [ type ] = 1
2015-12-09 14:56:18 -05:00
User . update { _id: userID } , { $inc: inc } , (err, result) ->
2015-08-13 14:17:38 -04:00
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