mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
Improve simulation game logic, and re-enable automatic simulations under certain conditions, better targeted toward the matches the player cares about
This commit is contained in:
parent
45418dbfe3
commit
1187390fd0
8 changed files with 65 additions and 24 deletions
|
@ -46,6 +46,8 @@ module.exports = class Simulator extends CocoClass
|
|||
ogresGameID: ogresGameID
|
||||
simulator: @simulator
|
||||
background: Boolean(@options.background)
|
||||
levelID: @options.levelID
|
||||
leagueID: @options.leagueID
|
||||
error: (errorData) ->
|
||||
console.warn "There was an error fetching two games! #{JSON.stringify errorData}"
|
||||
if errorData?.responseText?.indexOf("Old simulator") isnt -1
|
||||
|
@ -349,6 +351,7 @@ module.exports = class Simulator extends CocoClass
|
|||
totalScore: session.totalScore
|
||||
metrics:
|
||||
rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap()
|
||||
shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague
|
||||
if session.sessionID is taskResults.originalSessionID
|
||||
taskResults.originalSessionRank = sessionResult.metrics.rank
|
||||
taskResults.originalSessionTeam = session.team
|
||||
|
|
|
@ -304,6 +304,7 @@ _.extend LevelSessionSchema.properties,
|
|||
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.'}
|
||||
lastOpponentSubmitDate: c.date {description: 'The submitDate of the last league session we selected to play against (for playing through league opponents in order).'}
|
||||
|
||||
LevelSessionSchema.properties.leagues.items.properties.stats.properties = _.pick LevelSessionSchema.properties, 'meanStrength', 'standardDeviation', 'totalScore', 'numberOfWinsAndTies', 'numberOfLosses', 'scoreHistory', 'matches'
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ module.exports = class LadderView extends RootView
|
|||
return unless @supermodel.finished()
|
||||
@insertSubView(@ladderTab = new LadderTabView({league: @league}, @level, @sessions))
|
||||
@insertSubView(@myMatchesTab = new MyMatchesTabView({league: @league}, @level, @sessions))
|
||||
@insertSubView(@simulateTab = new SimulateTabView(league: @league))
|
||||
@insertSubView(@simulateTab = new SimulateTabView(league: @league, level: @level, leagueID: @leagueID))
|
||||
highLoad = true
|
||||
@refreshDelay = switch
|
||||
when not application.isProduction() then 10 # Refresh very quickly in develompent.
|
||||
|
|
|
@ -26,7 +26,7 @@ module.exports = class SimulateTabView extends CocoView
|
|||
onLoaded: ->
|
||||
super()
|
||||
@render()
|
||||
if document.location.hash is '#simulate' and not @simulator
|
||||
if (document.location.hash is '#simulate' or @options.level.get('type') is 'course-ladder') and not @simulator
|
||||
@startSimulating()
|
||||
|
||||
getRenderData: ->
|
||||
|
@ -59,7 +59,7 @@ module.exports = class SimulateTabView extends CocoView
|
|||
|
||||
simulateNextGame: ->
|
||||
unless @simulator
|
||||
@simulator = new Simulator()
|
||||
@simulator = new Simulator levelID: @options.level.get('slug'), leagueID: @options.leagueID
|
||||
@listenTo @simulator, 'statusUpdate', @updateSimulationStatus
|
||||
# Work around simulator getting super slow on Chrome
|
||||
fetchAndSimulateTaskOriginal = @simulator.fetchAndSimulateTask
|
||||
|
|
|
@ -410,7 +410,9 @@ module.exports = class PlayLevelView extends RootView
|
|||
|
||||
simulateNextGame: ->
|
||||
return @simulator.fetchAndSimulateOneGame() if @simulator
|
||||
@simulator = new Simulator background: true
|
||||
simulatorOptions = background: true, leagueID: @courseInstanceID
|
||||
simulatorOptions.levelID = @level.get('slug') if @level.get('type', true) in ['course-ladder', 'hero-ladder']
|
||||
@simulator = new Simulator simulatorOptions
|
||||
# Crude method of mitigating Simulator memory leak issues
|
||||
fetchAndSimulateOneGameOriginal = @simulator.fetchAndSimulateOneGame
|
||||
@simulator.fetchAndSimulateOneGame = =>
|
||||
|
@ -424,8 +426,8 @@ module.exports = class PlayLevelView extends RootView
|
|||
@simulator.fetchAndSimulateOneGame()
|
||||
|
||||
shouldSimulate: ->
|
||||
return @getQueryVariable('simulate') is true # Performance is too bad right now, gotta fix it first.
|
||||
# Crude heuristics are crude.
|
||||
return true if @getQueryVariable('simulate') is true
|
||||
stillBuggy = true # Keep this true while we still haven't fixed the zombie worker problem when simulating the more difficult levels on Chrome
|
||||
defaultCores = 2
|
||||
cores = window.navigator.hardwareConcurrency or defaultCores # Available on Chrome/Opera, soon Safari
|
||||
defaultHeapLimit = 793000000
|
||||
|
@ -440,14 +442,17 @@ module.exports = class PlayLevelView extends RootView
|
|||
if levelType is 'course'
|
||||
return false
|
||||
else if levelType is 'hero' and gamesSimulated
|
||||
return false if stillBuggy
|
||||
return false if cores < 8
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 10000
|
||||
else if levelType is 'hero-ladder' and gamesSimulated
|
||||
return false if stillBuggy
|
||||
return false if cores < 4
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 15000
|
||||
else if levelType is 'hero-ladder' and not gamesSimulated
|
||||
return false if stillBuggy
|
||||
return false if cores < 8
|
||||
return false if heapLimit <= defaultHeapLimit
|
||||
return false if @loadDuration > 20000
|
||||
|
@ -457,6 +462,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
return false if @loadDuration > 18000
|
||||
else
|
||||
console.warn "Unwritten level type simulation heuristics; fill these in for new level type #{levelType}?"
|
||||
return false if stillBuggy
|
||||
return false if cores < 8
|
||||
return false if heapLimit < defaultHeapLimit
|
||||
return false if @loadDuration > 10000
|
||||
|
|
|
@ -72,13 +72,14 @@ updateSessionToSubmit = (transpiledCode, user, sessionToUpdate, callback) ->
|
|||
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 = _.clone(_.find(sessionToUpdate.leagues, leagueID: leagueID) ? leagueID: leagueID)
|
||||
league.stats ?= {}
|
||||
league.stats.standardDeviation = 25 / 3
|
||||
league.stats.numberOfWinsAndTies = 0
|
||||
league.stats.numberOfLosses = 0
|
||||
league.stats.meanStrength ?= 25
|
||||
league.stats.totalScore ?= 10
|
||||
delete league.lastOpponentSubmitDate
|
||||
newLeagues.push(league)
|
||||
unless _.isEqual newLeagues, sessionToUpdate.leagues
|
||||
sessionUpdateObject.leagues = sessionToUpdate.leagues = newLeagues
|
||||
|
|
|
@ -12,6 +12,8 @@ module.exports = getTwoGames = (req, res) ->
|
|||
return getSpecificSessions res, humansSessionID, ogresSessionID if humansSessionID and ogresSessionID
|
||||
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'
|
||||
|
@ -39,12 +41,15 @@ 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.
|
||||
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
|
||||
findRandomSession {'leagues.leagueID': leagueID}, (err, session) ->
|
||||
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
|
||||
|
@ -52,11 +57,15 @@ getRandomSessions = (user, options, callback) ->
|
|||
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) ->
|
||||
findNextLeagueOpponent session, queryParameters, (err, otherSession) ->
|
||||
if err then return callback err
|
||||
if otherSession then return callback null, [session, otherSession]
|
||||
if otherSession
|
||||
console.log 'start off with it man', leagueID
|
||||
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]
|
||||
|
@ -71,10 +80,26 @@ getRandomSessions = (user, options, callback) ->
|
|||
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 = _.sample(if options.background then backgroundLadderLevelIDs else ladderLevelIDs)
|
||||
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
|
||||
console.log "Making query", queryParams
|
||||
LevelSession.findOne(queryParams).sort(sort).select(sessionSelectionString).lean().exec (err, otherSession) ->
|
||||
return callback err if err
|
||||
if otherSession and otherSession.creator + '' is session.creator + ''
|
||||
console.log 'Do not play a league match against ourselves', queryParams.submitDate.$lt, typeof queryParams.submitDate.$lt
|
||||
queryParams.submitDate.$lt = new Date(new Date(queryParams.submitDate.$lt) - 1)
|
||||
console.log ' ', queryParams.submitDate, '-- is that better?'
|
||||
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
|
||||
|
|
|
@ -36,7 +36,8 @@ module.exports.formatSessionInformation = (session) ->
|
|||
creatorName: session.creatorName
|
||||
creator: session.creator
|
||||
totalScore: session.totalScore
|
||||
|
||||
submitDate: session.submitDate
|
||||
shouldUpdateLastOpponentSubmitDateForLeague: session.shouldUpdateLastOpponentSubmitDateForLeague
|
||||
|
||||
module.exports.calculateSessionScores = (callback) ->
|
||||
sessionIDs = _.pluck @clientResponseObject.sessions, 'sessionID'
|
||||
|
@ -59,6 +60,7 @@ retrieveOldSessionData = (sessionID, callback) ->
|
|||
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 = []
|
||||
|
@ -75,7 +77,7 @@ retrieveOldSessionData = (sessionID, callback) ->
|
|||
return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again
|
||||
|
||||
query = _id: sessionID
|
||||
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage leagues ladderAchievementDifficulty'
|
||||
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
|
||||
|
@ -115,12 +117,13 @@ createSessionScoreUpdate = (scoreObject) ->
|
|||
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}
|
||||
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) ->
|
||||
|
@ -185,6 +188,8 @@ updateMatchesInSession = (matchObject, sessionID, callback) ->
|
|||
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
|
||||
|
|
Loading…
Reference in a new issue