Added league-based ladder game simulation.

This commit is contained in:
Nick Winter 2015-08-15 05:38:47 -07:00
parent f757f220be
commit bda2483738
9 changed files with 190 additions and 78 deletions

View file

@ -294,6 +294,14 @@ _.extend LevelSessionSchema.properties,
simulator: {type: 'object', description: 'Holds info on who simulated the match, and with what tools.'}
randomSeed: {description: 'Stores the random seed that was used during this match.'}
leagues:
c.array {description: 'Multiplayer data for the league corresponding to Clans and CourseInstances the player is a part of.'},
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.'}
LevelSessionSchema.properties.leagues.items.properties.stats.properties = _.pick LevelSessionSchema.properties, 'meanStrength', 'standardDeviation', 'totalScore', 'numberOfWinsAndTies', 'numberOfLosses', 'scoreHistory', 'matches'
c.extendBasicProperties LevelSessionSchema, 'level.session'
c.extendPermissionsProperties LevelSessionSchema, 'level.session'

View file

@ -28,6 +28,7 @@ LevelSessionSchema.index({submitted: 1, team: 1, level:1, totalScore: -1}, {name
LevelSessionSchema.index({levelID: 1, submitted:1, team: 1}, {name: 'get all scores index', sparse: true})
LevelSessionSchema.index({submitted: 1, team: 1, levelID: 1, submitDate: -1}, {name: 'matchmaking index', sparse: true})
LevelSessionSchema.index({submitted: 1, team: 1, levelID: 1, randomSimulationIndex: -1}, {name: 'matchmaking random index', sparse: true})
LevelSessionSchema.index({'leagues.leagueID': 1, submitted: 1, levelID: 1, team: 1, randomSimulationIndex: -1}, {name: 'league-based matchmaking random index', sparse: true}) # Really need MongoDB 3.2 for partial indexes for this and several others: https://jira.mongodb.org/browse/SERVER-785
LevelSessionSchema.plugin(plugins.PermissionsPlugin)
LevelSessionSchema.plugin(AchievablePlugin)

View file

@ -36,7 +36,7 @@ module.exports.addPairwiseTaskToQueueFromRequest = (req, res) ->
taskPair = req.body.sessions
scoringUtils.addPairwiseTaskToQueue req.body.sessions, (err, success) ->
if err? then return errors.serverError res, "There was an error adding pairwise tasks: #{err}"
scoringUtils.sendResponseObject req, res, {message: 'All task pairs were succesfully sent to the queue'}
scoringUtils.sendResponseObject res, {message: 'All task pairs were succesfully sent to the queue'}
module.exports.getTwoGames = getTwoGames

View file

@ -3,6 +3,7 @@ async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
Level = require '../../levels/Level'
module.exports = createNewTask = (req, res) ->
requestSessionID = req.body.session
@ -16,12 +17,12 @@ module.exports = createNewTask = (req, res) ->
validatePermissions.bind(yetiGuru, req, requestSessionID)
fetchAndVerifyLevelType.bind(yetiGuru, currentLevelID)
fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID)
updateSessionToSubmit.bind(yetiGuru, transpiledCode)
updateSessionToSubmit.bind(yetiGuru, transpiledCode, req.user)
fetchInitialSessionsToRankAgainst.bind(yetiGuru, requestLevelMajorVersion, originalLevelID)
generateAndSendTaskPairsToTheQueue
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error submitting the game to the queue:#{err}"
scoringUtils.sendResponseObject req, res, successMessageObject
scoringUtils.sendResponseObject res, successMessageObject
validatePermissions = (req, sessionID, callback) ->
@ -45,22 +46,37 @@ fetchAndVerifyLevelType = (levelID, cb) ->
cb null
fetchSessionObjectToSubmit = (sessionID, callback) ->
LevelSession.findOne({_id: sessionID}).select('team code').exec (err, session) ->
LevelSession.findOne({_id: sessionID}).select('team code leagues').exec (err, session) ->
callback err, session?.toObject()
updateSessionToSubmit = (transpiledCode, sessionToUpdate, callback) ->
updateSessionToSubmit = (transpiledCode, user, sessionToUpdate, callback) ->
sessionUpdateObject =
submitted: true
submittedCode: sessionToUpdate.code
transpiledCode: transpiledCode
submitDate: new Date()
#meanStrength: 25 # Let's try not resetting the score on resubmission
standardDeviation: 25/3
standardDeviation: 25 / 3
#totalScore: 10 # Let's try not resetting the score on resubmission
numberOfWinsAndTies: 0
numberOfLosses: 0
isRanking: true
randomSimulationIndex: Math.random()
# Reset all league stats as well, and enter the session into any leagues the user is currently part of (not retroactive when joining new leagues)
leagueIDs = user.get('clans') or []
#leagueIDs = leagueIDs.concat user.get('courseInstances') or []
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.stats ?= {}
league.stats.standardDeviation = 25 / 3
league.stats.numberOfWinsAndTies = 0
league.stats.numberOfLosses = 0
newLeagues.push(league)
unless _.isEqual newLeagues, sessionToUpdate.leagues
sessionUpdateObject.leagues = sessionToUpdate.leagues = newLeagues
LevelSession.update {_id: sessionToUpdate._id}, sessionUpdateObject, (err, result) ->
callback err, sessionToUpdate

View file

@ -22,7 +22,7 @@ module.exports = dispatchTaskToConsumer = (req, res) ->
return res.end()
else
return errors.serverError res, "There was an error dispatching the task: #{err}"
scoringUtils.sendResponseObject req, res, taskObjectToSend
scoringUtils.sendResponseObject res, taskObjectToSend
checkSimulationPermissions = (req, cb) ->

View file

@ -5,42 +5,89 @@ scoringUtils = require './scoringUtils'
LevelSession = require '../../levels/sessions/LevelSession'
module.exports = getTwoGames = (req, res) ->
#if isUserAnonymous req then return errors.unauthorized(res, 'You need to be logged in to get games.')
humansGameID = req.body.humansGameID
ogresGameID = req.body.ogresGameID
#return errors.unauthorized(res, 'You need to be logged in to get games.') unless req.user?.get('email')
return if scoringUtils.simulatorIsTooOld req, res
#ladderGameIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span'] # Let's not give any extra simulations to old ladders.
ladderGameIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland', 'zero-sum']
levelID = _.sample ladderGameIDs
unless ogresGameID and humansGameID
recentHumans = 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: recentHumans}, {levelID: levelID, team: 'ogres', favorRecent: not recentHumans}], findRandomSession, (err, sessions) ->
if err then return errors.serverError(res, "Couldn't get two games to simulate for #{levelID}.")
unless sessions.length is 2
res.send(204, 'No games to score.')
return res.end()
taskObject = messageGenerated: Date.now(), sessions: (scoringUtils.formatSessionInformation session for session in sessions)
#console.log 'Dispatching random game between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName
scoringUtils.sendResponseObject req, res, taskObject
else
#console.log "Directly simulating #{humansGameID} vs. #{ogresGameID}."
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
LevelSession.findOne(_id: humansGameID).select(selection).lean().exec (err, humanSession) =>
if err? then return errors.serverError(res, 'Couldn\'t find the human game')
LevelSession.findOne(_id: ogresGameID).select(selection).lean().exec (err, ogreSession) =>
if err? then return errors.serverError(res, 'Couldn\'t find the ogre game')
taskObject = messageGenerated: Date.now(), sessions: (scoringUtils.formatSessionInformation session for session in [humanSession, ogreSession])
scoringUtils.sendResponseObject req, res, taskObject
humansSessionID = req.body.humansGameID
ogresSessionID = req.body.ogresGameID
return getSpecificSessions res, humansSessionID, ogresSessionID if humansSessionID and ogresSessionID
getRandomSessions req.user, sendSessionsResponse(res)
sessionSelectionString = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate leagues'
earliestSubmissionCache = {}
findEarliestSubmission = (queryParams, callback) ->
cacheKey = JSON.stringify queryParams
return callback null, cached if cached = earliestSubmissionCache[cacheKey]
LevelSession.findOne(queryParams).sort(submitDate: 1).lean().exec (err, earliest) ->
sendSessionsResponse = (res) ->
(err, sessions) ->
if err then return errors.serverError res, "Couldn't get two games to simulate: #{err}"
unless sessions.length is 2
console.log 'No games to score.', sessions.length
res.send 204, 'No games to score.'
return res.end()
taskObject = messageGenerated: Date.now(), sessions: (scoringUtils.formatSessionInformation session for session in sessions)
#console.log 'Dispatching ladder game simulation between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName
scoringUtils.sendResponseObject res, taskObject
getSpecificSessions = (res, humansSessionID, ogresSessionID) ->
async.map [humansSessionID, ogresSessionID], getSpecificSession, sendSessionsResponse(res)
getSpecificSession = (sessionID, callback) ->
LevelSession.findOne(_id: sessionID).select(sessionSelectionString).lean().exec (err, session) ->
if err? then return callback "Couldn\'t find target simulation session #{sessionID}"
callback null, session
getRandomSessions = (user, 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 callback unless leagueIDs.length and Math.random() > 1 / leagueIDs.length
leagueID = _.sample leagueIDs
findRandomSession {'leagues.leagueID': leagueID, favorRecent: true}, (err, session) ->
if err then return callback err
unless session then return sampleByLevel callback
otherTeam = scoringUtils.calculateOpposingTeam session.team
queryParameters = team: otherTeam, levelID: session.levelID
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) ->
if err then return callback err
if otherSession then return callback null, [session, otherSession]
# No opposing league session found; try to play an external match
delete queryParameters['leagues.leagueID']
findRandomSession queryParameters, (err, otherSession) ->
if err then return callback err
callback null, [session, otherSession]
else
# Play what will probably end up being an external match
findRandomSession queryParameters, (err, otherSession) ->
if err then return callback err
callback null, [session, otherSession]
# Sampling by level: we pick a level, then find a human and ogre session for that level, one at random, one biased towards recent submissions.
#ladderLevelIDs = ['greed', 'criss-cross', 'brawlwood', 'dungeon-arena', 'gold-rush', 'sky-span'] # Let's not give any extra simulations to old ladders.
ladderLevelIDs = ['dueling-grounds', 'cavern-survival', 'multiplayer-treasure-grove', 'harrowland', 'zero-sum']
sampleByLevel = (callback) ->
levelID = _.sample 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
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
favorRecent = queryParams.favorRecent
delete queryParams.favorRecent
if favorRecent
return findRecentRandomSession queryParams, callback
queryParams.randomSimulationIndex = $lte: Math.random()
sort = randomSimulationIndex: -1
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
result = earliestSubmissionCache[cacheKey] = earliest?.submitDate
callback null, result
return callback null, session if session
delete queryParams.randomSimulationIndex # Just find the highest-indexed session, if our randomSimulationIndex was lower than the lowest one.
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
callback null, session
findRecentRandomSession = (queryParams, callback) ->
# We pick a random submitDate between the first submit date for the level and now, then do a $lt fetch to find a session to simulate.
@ -51,26 +98,17 @@ findRecentRandomSession = (queryParams, callback) ->
interval = now - startDate
cutoff = new Date now - Math.pow(Math.random(), 4) * interval
queryParams.submitDate = $gte: startDate, $lt: cutoff
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
LevelSession.findOne(queryParams).sort(submitDate: -1).select(selection).lean().exec (err, session) ->
LevelSession.findOne(queryParams).sort(submitDate: -1).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
callback null, session
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
favorRecent = queryParams.favorRecent
delete queryParams.favorRecent
if favorRecent
return findRecentRandomSession queryParams, callback
queryParams.randomSimulationIndex = $lte: Math.random()
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
sort = randomSimulationIndex: -1
LevelSession.findOne(queryParams).sort(sort).select(selection).lean().exec (err, session) ->
earliestSubmissionCache = {}
findEarliestSubmission = (queryParams, callback) ->
cacheKey = JSON.stringify queryParams
return callback null, cached if cached = earliestSubmissionCache[cacheKey]
LevelSession.findOne(queryParams).sort(submitDate: 1).lean().exec (err, earliest) ->
return callback err if err
return callback null, session if session
delete queryParams.randomSimulationIndex # Just find the highest-indexed session, if our randomSimulationIndex was lower than the lowest one.
LevelSession.findOne(queryParams).sort(sort).select(selection).lean().exec (err, session) ->
return callback err if err
callback null, session
result = earliestSubmissionCache[cacheKey] = earliest?.submitDate
callback null, result

View file

@ -29,16 +29,16 @@ module.exports = processTaskResult = (req, res) ->
], (err, results) ->
if err is 'shouldn\'t continue'
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return scoringUtils.sendResponseObject req, res, {'error': 'There was an error marking the session as done ranking'}
scoringUtils.sendResponseObject req, res, {message: 'The scores were updated successfully, person lost so no more games are being inserted!'}
if err? then return scoringUtils.sendResponseObject res, {'error': 'There was an error marking the session as done ranking'}
scoringUtils.sendResponseObject res, {message: 'The scores were updated successfully, person lost so no more games are being inserted!'}
else if err is 'no session was found'
markSessionAsDoneRanking originalSessionID, (err) ->
if err? then return scoringUtils.sendResponseObject req, res, {'error': 'There was an error marking the session as done ranking'}
scoringUtils.sendResponseObject req, res, {message: 'There were no more games to rank (game is at top)!'}
if err? then return scoringUtils.sendResponseObject res, {'error': 'There was an error marking the session as done ranking'}
scoringUtils.sendResponseObject res, {message: 'There were no more games to rank (game is at top)!'}
else if err?
errors.serverError res, "There was an error:#{err}"
else
scoringUtils.sendResponseObject req, res, {message: 'The scores were updated successfully and more games were sent to the queue!'}
scoringUtils.sendResponseObject res, {message: 'The scores were updated successfully and more games were sent to the queue!'}
catch e
errors.serverError res, 'There was an error processing the task result!'
@ -70,7 +70,7 @@ deleteQueueMessage = (callback) ->
callback err
fetchLevelSession = (callback) ->
selectString = 'submitDate creator level standardDeviation meanStrength totalScore submittedCodeLanguage'
selectString = 'submitDate creator level standardDeviation meanStrength totalScore submittedCodeLanguage leagues'
LevelSession.findOne(_id: @clientResponseObject.originalSessionID).select(selectString).lean().exec (err, session) =>
@levelSession = session
callback err

View file

@ -18,4 +18,4 @@ module.exports = recordTwoGames = (req, res) ->
scoringUtils.updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
], (err, successMessageObject) ->
if err? then return errors.serverError res, "There was an error recording the single game: #{err}"
scoringUtils.sendResponseObject req, res, {message: 'The single game was submitted successfully!'}
scoringUtils.sendResponseObject res, {message: 'The single game was submitted successfully!'}

View file

@ -20,7 +20,7 @@ module.exports.simulatorIsTooOld = (req, res) ->
true
module.exports.sendResponseObject = (req, res, object) ->
module.exports.sendResponseObject = (res, object) ->
res.setHeader('Content-Type', 'application/json')
res.send(object)
res.end()
@ -40,37 +40,65 @@ module.exports.formatSessionInformation = (session) ->
module.exports.calculateSessionScores = (callback) ->
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
async.map sessionIDs, retrieveOldSessionData, (err, oldScores) =>
if err? then callback err, {error: 'There was an error retrieving the old scores'}
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 = bayes.updatePlayerSkills oldScoreArray
newScoreArray = updatePlayerSkills oldScoreArray
createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray
callback err, newScoreArray
catch e
callback e
retrieveOldSessionData = (sessionID, callback) ->
formatOldScoreObject = (session) ->
standardDeviation: session.standardDeviation ? 25/3
meanStrength: session.meanStrength ? 25
totalScore: session.totalScore ? (25 - 1.8*(25/3))
id: sessionID
submittedCodeLanguage: session.submittedCodeLanguage
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
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'
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues'
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'
scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions
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]
@ -81,6 +109,17 @@ createSessionScoreUpdate = (scoreObject) ->
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."
@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}
module.exports.indexNewScoreArray = (newScoreArray, callback) ->
@ -119,12 +158,22 @@ updateMatchesInSession = (matchObject, sessionID, callback) ->
opponentsClone = _.omit opponentsClone, sessionID
opponentsArray = _.toArray opponentsClone
currentMatchObject.opponents = opponentsArray
currentMatchObject.codeLanguage = matchObject.opponents[opponentsArray[0].sessionID].codeLanguage
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}
#log.info "Update is #{JSON.stringify(sessionUpdateObject, null, 2)}"
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}
#log.info "Update for #{sessionID} is #{JSON.stringify(sessionUpdateObject, null, 2)}"
LevelSession.update {_id: sessionID}, sessionUpdateObject, callback