mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-25 06:52:21 -05:00
258 lines
12 KiB
CoffeeScript
258 lines
12 KiB
CoffeeScript
log = require 'winston'
|
|
async = require 'async'
|
|
bayes = new (require 'bayesian-battle')()
|
|
LevelSession = require '../../models/LevelSession'
|
|
User = require '../../models/User'
|
|
perfmon = require '../../commons/perfmon'
|
|
LZString = require 'lz-string'
|
|
|
|
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) ->
|
|
heroID = if session.team is 'ogres' then 'hero-placeholder-1' else 'hero-placeholder'
|
|
submittedCode = {}
|
|
submittedCode[heroID] = plan: LZString.compressToUTF16(session.submittedCode?[heroID]?.plan ? '')
|
|
|
|
_id: session._id
|
|
sessionID: session._id
|
|
team: session.team ? 'No team'
|
|
submittedCode: submittedCode
|
|
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'}
|
|
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
|
|
newScoreArray = updatePlayerSkills oldScoreArray
|
|
createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray
|
|
callback null, newScoreArray
|
|
|
|
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
|