codecombat/server/queues/scoring/getTwoGames.coffee

140 lines
8.1 KiB
CoffeeScript

log = require 'winston'
async = require 'async'
errors = require '../../commons/errors'
scoringUtils = require './scoringUtils'
LevelSession = require '../../models/LevelSession'
Mandate = require '../../models/Mandate'
module.exports = getTwoGames = (req, res) ->
#return errors.unauthorized(res, 'You need to be logged in to get games.') unless req.user?.get('email')
return if scoringUtils.simulatorIsTooOld req, res
humansSessionID = req.body.humansGameID
ogresSessionID = req.body.ogresGameID
return getSpecificSessions res, humansSessionID, ogresSessionID if humansSessionID and ogresSessionID
Mandate.findOne({}).cache(5 * 60 * 1000).exec (err, mandate) ->
if err then return errors.serverError res, "Error fetching our Mandate: #{err}"
if (throughputRatio = mandate?.get 'simulationThroughputRatio')? and Math.random() > throughputRatio
return sendSessionsResponse(res)(null, [])
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'
sendSessionsResponse = (res) ->
(err, sessions) ->
if err then return errors.serverError res, "Couldn't get two games to simulate: #{err}"
unless _.filter(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 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, 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.
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
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
findNextLeagueOpponent session, queryParameters, (err, otherSession) ->
if err then return callback err
if otherSession
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]
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', 'ace-of-coders', 'wakka-maul']
backgroundLadderLevelIDs = _.without ladderLevelIDs, 'zero-sum', 'ace-of-coders'
sampleByLevel = (options, callback) ->
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
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, otherSession) ->
return callback err if err
if otherSession and otherSession.creator + '' is session.creator + ''
queryParams.submitDate.$lt = new Date(new Date(queryParams.submitDate.$lt) - 1)
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
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
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.
# We bias it towards recently submitted sessions.
findEarliestSubmission queryParams, (err, startDate) ->
return callback err, null unless startDate
now = new Date()
interval = now - startDate
cutoff = new Date now - Math.pow(Math.random(), 4) * interval
queryParams.submitDate = $gte: startDate, $lt: cutoff
LevelSession.findOne(queryParams).sort(submitDate: -1).select(sessionSelectionString).lean().exec (err, session) ->
return callback err if err
callback null, 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
result = earliestSubmissionCache[cacheKey] = earliest?.submitDate
callback null, result