mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 17:45:40 -05:00
commit
da0b7d3e64
25 changed files with 937 additions and 791 deletions
|
@ -104,6 +104,7 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'multiplayer': go('MultiplayerView')
|
||||
|
||||
'play': go('play/CampaignView')
|
||||
'play/ladder/:levelID/:leagueType/:leagueID': go('ladder/LadderView')
|
||||
'play/ladder/:levelID': go('ladder/LadderView')
|
||||
'play/ladder': go('ladder/MainLadderView')
|
||||
'play/level/:levelID': go('play/level/PlayLevelView')
|
||||
|
|
|
@ -820,6 +820,7 @@
|
|||
latest_achievement: "Latest Achievement"
|
||||
playtime: "Playtime"
|
||||
last_played: "Last played"
|
||||
leagues_explanation: "Play in a league against other clan members in these multiplayer arena instances."
|
||||
|
||||
classes:
|
||||
archmage_title: "Archmage"
|
||||
|
@ -1009,6 +1010,7 @@
|
|||
my_matches: "My Matches"
|
||||
simulate: "Simulate"
|
||||
simulation_explanation: "By simulating games you can get your game ranked faster!"
|
||||
simulation_explanation_leagues: "You will mainly help simulate games for allied players in your clans and courses."
|
||||
simulate_games: "Simulate Games!"
|
||||
simulate_all: "RESET AND SIMULATE GAMES"
|
||||
games_simulated_by: "Games simulated by you:"
|
||||
|
@ -1059,6 +1061,7 @@
|
|||
tournament_blurb_blog: "on our blog"
|
||||
rules: "Rules"
|
||||
winners: "Winners"
|
||||
league: "League"
|
||||
|
||||
user:
|
||||
stats: "Stats"
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ block content
|
|||
button.btn(data-dismiss='modal', data-i18n="modal.close") Close
|
||||
button.btn.edit-description-save-btn(data-i18n="common.save_changes") Save changes
|
||||
|
||||
.row
|
||||
.col-lg-6
|
||||
if clan
|
||||
h1 #{clan.get('name')}
|
||||
if clan.get('type') === 'private'
|
||||
|
@ -73,6 +75,14 @@ block content
|
|||
input.join-clan-link(type="text", readonly, value="#{joinClanLink}")
|
||||
.small(data-i18n="clans.invite_2") *Invite players to this Clan by sending them this link.
|
||||
|
||||
if arenas && arenas.length
|
||||
.col-lg-6
|
||||
h2(data-i18n="play.campaign_multiplayer")
|
||||
p(data-i18n="clans.leagues_explanation")
|
||||
for arena in arenas
|
||||
h3
|
||||
a(href="/play/ladder/#{arena.slug}/clan/#{clan.id}")= i18n(arena, 'name')
|
||||
|
||||
if members
|
||||
h3
|
||||
span.spr(data-i18n="clans.members") Members
|
||||
|
|
|
@ -23,12 +23,13 @@ div#columns.row
|
|||
- if(!showJustTop && topSessions.length == 20) topSessions = topSessions.slice(0, 10);
|
||||
for session, rank in topSessions
|
||||
- var myRow = session.get('creator') == me.id
|
||||
- var sessionStats = league ? (_.find(session.get('leagues') || [], {leagueID: league.id}) || {}).stats || {} : session.attributes;
|
||||
tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id)
|
||||
td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)" title=capitalize(session.get('submittedCodeLanguage')))
|
||||
if level.get('type', true) == 'hero-ladder'
|
||||
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
|
||||
td.rank-cell= rank + 1
|
||||
td.score-cell= Math.round(session.get('totalScore') * 100)
|
||||
td.score-cell= Math.round(sessionStats.totalScore * 100)
|
||||
td.name-col-cell= session.get('creatorName') || "Anonymous"
|
||||
td.fight-cell
|
||||
a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}")
|
||||
|
@ -41,12 +42,13 @@ div#columns.row
|
|||
td(colspan=4).ellipsis-row ...
|
||||
for session in team.leaderboard.nearbySessions()
|
||||
- var myRow = session.get('creator') == me.id
|
||||
- var sessionStats = league ? (_.find(session.get('leagues'), {leagueID: league.id}) || {}).stats || {} : session.attributes;
|
||||
tr(class=myRow ? "success" : "", data-player-id=session.get('creator'), data-session-id=session.id)
|
||||
td.code-language-cell(style="background-image: url(/images/common/code_languages/" + session.get('submittedCodeLanguage') + "_icon.png)")
|
||||
if level.get('type', true) == 'hero-ladder'
|
||||
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{(session.get('heroConfig') || {}).thangType || '529ffbf1cf1818f2be000001'}/portrait.png)")
|
||||
td.rank-cell= session.rank
|
||||
td.score-cell= Math.round(session.get('totalScore') * 100)
|
||||
td.score-cell= Math.round(sessionStats.totalScore * 100)
|
||||
td.name-col-cell= session.get('creatorName') || "Anonymous"
|
||||
td.fight-cell
|
||||
a(href="/play/level/#{level.get('slug') || level.id}?team=#{team.otherTeam}&opponent=#{session.id}")
|
||||
|
|
|
@ -10,6 +10,11 @@ block content
|
|||
else
|
||||
h1= level.get('name')
|
||||
|
||||
if league
|
||||
h1
|
||||
a(href="/#{leagueType == 'clan' ? 'clans' : leagueType}/#{league.id}")= league.get('name')
|
||||
span.spl(data-i18n="ladder.league") League
|
||||
|
||||
if level.get('name') == 'Greed'
|
||||
.tournament-blurb
|
||||
h2
|
||||
|
|
|
@ -3,6 +3,7 @@ p(id="simulation-status-text")
|
|||
| #{simulationStatus}
|
||||
else
|
||||
span(data-i18n="ladder.simulation_explanation") By simulating games you can get your game ranked faster!
|
||||
span.spl(data-i18n="ladder.simulation_explanation_leagues") You will mainly help simulate games for allied players in your clans and courses.
|
||||
p
|
||||
button(data-i18n="ladder.simulate_games").btn.btn-warning.btn-lg.highlight#simulate-button Simulate Games!
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ LevelSession = require 'models/LevelSession'
|
|||
SubscribeModal = require 'views/core/SubscribeModal'
|
||||
ThangType = require 'models/ThangType'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
|
||||
# TODO: Add message for clan not found
|
||||
# TODO: Progress visual for premium levels?
|
||||
|
@ -60,7 +61,7 @@ module.exports = class ClanDetailsView extends RootView
|
|||
@listenTo @memberAchievements, 'sync', @onMemberAchievementsSync
|
||||
@listenTo @memberSessions, 'sync', @onMemberSessionsSync
|
||||
|
||||
@supermodel.loadModel @campaigns, 'clan', cache: false
|
||||
@supermodel.loadModel @campaigns, 'campaigns', cache: false
|
||||
@supermodel.loadModel @clan, 'clan', cache: false
|
||||
@supermodel.loadCollection(@members, 'members', {cache: false})
|
||||
@supermodel.loadCollection(@memberAchievements, 'member_achievements', {cache: false})
|
||||
|
@ -120,6 +121,8 @@ module.exports = class ClanDetailsView extends RootView
|
|||
context.lastUserCampaignLevelMap = lastUserCampaignLevelMap
|
||||
context.showExpandedProgress = maxLastUserCampaignLevel <= 30 or @showExpandedProgress
|
||||
context.userConceptsMap = userConceptsMap
|
||||
context.arenas = @arenas
|
||||
context.i18n = utils.i18n
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
@ -179,21 +182,24 @@ module.exports = class ClanDetailsView extends RootView
|
|||
return unless @campaigns.loaded
|
||||
@campaignLevelProgressions = []
|
||||
@conceptsProgression = []
|
||||
@arenas = []
|
||||
for campaign in @campaigns.models
|
||||
continue if campaign.get('slug') is 'auditions'
|
||||
campaignLevelProgression =
|
||||
ID: campaign.id
|
||||
slug: campaign.get('slug')
|
||||
name: campaign.get('fullName') or campaign.get('name')
|
||||
name: utils.i18n(campaign.attributes, 'fullName') or utils.i18n(campaign.attributes, 'name')
|
||||
levels: []
|
||||
for levelID, level of campaign.get('levels')
|
||||
campaignLevelProgression.levels.push
|
||||
ID: levelID
|
||||
slug: level.slug
|
||||
name: level.name
|
||||
name: utils.i18n level, 'name'
|
||||
if level.concepts?
|
||||
for concept in level.concepts
|
||||
@conceptsProgression.push concept unless concept in @conceptsProgression
|
||||
if level.type == 'hero-ladder'
|
||||
@arenas.push level
|
||||
@campaignLevelProgressions.push campaignLevelProgression
|
||||
@render?()
|
||||
|
||||
|
|
|
@ -43,11 +43,14 @@ module.exports = class LadderPlayModal extends ModalView
|
|||
|
||||
# PART 1: Load challengers from the db unless some are in the matches
|
||||
startLoadingChallengersMaybe: ->
|
||||
if @options.league
|
||||
matches = _.find(@session?.get('leagues'), leagueID: @options.league.id)?.stats.matches
|
||||
else
|
||||
matches = @session?.get('matches')
|
||||
if matches?.length then @loadNames() else @loadChallengers()
|
||||
|
||||
loadChallengers: ->
|
||||
@challengersCollection = new ChallengersData(@level, @team, @otherTeam, @session)
|
||||
@challengersCollection = new ChallengersData(@level, @team, @otherTeam, @session, @options.league)
|
||||
@listenTo(@challengersCollection, 'sync', @loadNames)
|
||||
|
||||
# PART 2: Loading the names of the other users
|
||||
|
@ -156,7 +159,10 @@ module.exports = class LadderPlayModal extends ModalView
|
|||
mediumInfo = @challengeInfoFromSession(@challengersCollection.mediumPlayer.models[0])
|
||||
hardInfo = @challengeInfoFromSession(@challengersCollection.hardPlayer.models[0])
|
||||
else
|
||||
matches = @session.get('matches')
|
||||
if @options.league
|
||||
matches = _.find(@session?.get('leagues'), leagueID: @options.league.id)?.stats.matches
|
||||
else
|
||||
matches = @session?.get('matches')
|
||||
won = (m for m in matches when m.metrics.rank < m.opponents[0].metrics.rank)
|
||||
lost = (m for m in matches when m.metrics.rank > m.opponents[0].metrics.rank)
|
||||
tied = (m for m in matches when m.metrics.rank is m.opponents[0].metrics.rank)
|
||||
|
@ -195,18 +201,26 @@ module.exports = class LadderPlayModal extends ModalView
|
|||
}
|
||||
|
||||
class ChallengersData
|
||||
constructor: (@level, @team, @otherTeam, @session) ->
|
||||
constructor: (@level, @team, @otherTeam, @session, @league) ->
|
||||
_.extend @, Backbone.Events
|
||||
score = @session?.get('totalScore') or 25
|
||||
@easyPlayer = new LeaderboardCollection(@level, {order: 1, scoreOffset: score - 5, limit: 1, team: @otherTeam})
|
||||
@easyPlayer.fetch cache: false
|
||||
@listenToOnce(@easyPlayer, 'sync', @challengerLoaded)
|
||||
@mediumPlayer = new LeaderboardCollection(@level, {order: 1, scoreOffset: score, limit: 1, team: @otherTeam})
|
||||
@mediumPlayer.fetch cache: false
|
||||
@listenToOnce(@mediumPlayer, 'sync', @challengerLoaded)
|
||||
@hardPlayer = new LeaderboardCollection(@level, {order: -1, scoreOffset: score + 5, limit: 1, team: @otherTeam})
|
||||
@hardPlayer.fetch cache: false
|
||||
@listenToOnce(@hardPlayer, 'sync', @challengerLoaded)
|
||||
if @league
|
||||
score = _.find(@session?.get('leagues'), leagueID: @league.id)?.stats?.totalScore or 10
|
||||
else
|
||||
score = @session?.get('totalScore') or 10
|
||||
for player in [
|
||||
{type: 'easyPlayer', order: 1, scoreOffset: score - 5}
|
||||
{type: 'mediumPlayer', order: 1, scoreOffset: score}
|
||||
{type: 'hardPlayer', order: -1, scoreOffset: score + 5}
|
||||
]
|
||||
playerResource = @[player.type] = new LeaderboardCollection(@level, @collectionParameters(order: player.order, scoreOffset: player.scoreOffset))
|
||||
playerResource.fetch cache: false
|
||||
@listenToOnce playerResource, 'sync', @challengerLoaded
|
||||
|
||||
collectionParameters: (parameters) ->
|
||||
parameters.team = @otherTeam
|
||||
parameters.limit = 1
|
||||
parameters['leagues.leagueID'] = @league.id if @league
|
||||
parameters
|
||||
|
||||
challengerLoaded: ->
|
||||
if @allLoaded()
|
||||
|
|
|
@ -154,7 +154,7 @@ module.exports = class LadderTabView extends CocoView
|
|||
@supermodel.removeModelResource oldLeaderboard
|
||||
oldLeaderboard.destroy()
|
||||
teamSession = _.find @sessions.models, (session) -> session.get('team') is team.id
|
||||
@leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit)
|
||||
@leaderboards[team.id] = new LeaderboardData(@level, team.id, teamSession, @ladderLimit, @options.league)
|
||||
@leaderboardRes = @supermodel.addModelResource(@leaderboards[team.id], 'leaderboard', {cache: false}, 3)
|
||||
@leaderboardRes.load()
|
||||
|
||||
|
@ -166,7 +166,9 @@ module.exports = class LadderTabView extends CocoView
|
|||
team = _.find @teams, name: histogramWrapper.data('team-name')
|
||||
histogramData = null
|
||||
$.when(
|
||||
$.get "/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}", {cache: false}, (data) -> histogramData = data
|
||||
url = "/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}"
|
||||
url += '&leagues.leagueID=' + @options.league.id if @options.league
|
||||
$.get url, {cache: false}, (data) -> histogramData = data
|
||||
).then =>
|
||||
@generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) unless @destroyed
|
||||
|
||||
|
@ -181,6 +183,8 @@ module.exports = class LadderTabView extends CocoView
|
|||
ctx.onFacebook = @facebookStatus is 'connected'
|
||||
ctx.onGPlus = application.gplusHandler.loggedIn
|
||||
ctx.capitalize = _.string.capitalize
|
||||
ctx.league = @options.league
|
||||
ctx._ = _
|
||||
ctx
|
||||
|
||||
generateHistogram: (histogramElement, histogramData, teamName) ->
|
||||
|
@ -227,8 +231,11 @@ module.exports = class LadderTabView extends CocoView
|
|||
.attr('x', 1)
|
||||
.attr('width', width/20)
|
||||
.attr('height', (d) -> height - y(d.y))
|
||||
if @leaderboards[teamName].session?
|
||||
playerScore = @leaderboards[teamName].session.get('totalScore') * 100
|
||||
if session = @leaderboards[teamName].session
|
||||
if @options.league
|
||||
playerScore = (_.find(session.get('leagues'), {leagueID: @options.league.id})?.stats.totalScore or 10) * 100
|
||||
else
|
||||
playerScore = session.get('totalScore') * 100
|
||||
scorebar = svg.selectAll('.specialbar')
|
||||
.data([playerScore])
|
||||
.enter().append('g')
|
||||
|
@ -301,24 +308,35 @@ module.exports.LeaderboardData = LeaderboardData = class LeaderboardData extends
|
|||
Consolidates what you need to load for a leaderboard into a single Backbone Model-like object.
|
||||
###
|
||||
|
||||
constructor: (@level, @team, @session, @limit) ->
|
||||
constructor: (@level, @team, @session, @limit, @league) ->
|
||||
super()
|
||||
|
||||
collectionParameters: (parameters) ->
|
||||
parameters.team = @team
|
||||
parameters['leagues.leagueID'] = @league.id if @league
|
||||
parameters
|
||||
|
||||
fetch: ->
|
||||
console.warn 'Already have top players on', @ if @topPlayers
|
||||
@topPlayers = new LeaderboardCollection(@level, {order: -1, scoreOffset: HIGHEST_SCORE, team: @team, limit: @limit})
|
||||
|
||||
@topPlayers = new LeaderboardCollection(@level, @collectionParameters(order: -1, scoreOffset: HIGHEST_SCORE, limit: @limit))
|
||||
promises = []
|
||||
promises.push @topPlayers.fetch cache: false
|
||||
|
||||
if @session
|
||||
if @league
|
||||
score = _.find(@session.get('leagues'), {leagueID: @league.id})?.stats.totalScore or 10
|
||||
else
|
||||
score = @session.get('totalScore') or 10
|
||||
@playersAbove = new LeaderboardCollection(@level, {order: 1, scoreOffset: score, limit: 4, team: @team})
|
||||
@playersAbove = new LeaderboardCollection(@level, @collectionParameters(order: 1, scoreOffset: score, limit: 4))
|
||||
promises.push @playersAbove.fetch cache: false
|
||||
@playersBelow = new LeaderboardCollection(@level, {order: -1, scoreOffset: score, limit: 4, team: @team})
|
||||
@playersBelow = new LeaderboardCollection(@level, @collectionParameters(order: -1, scoreOffset: score, limit: 4))
|
||||
promises.push @playersBelow.fetch cache: false
|
||||
level = "#{@level.get('original')}.#{@level.get('version').major}"
|
||||
success = (@myRank) =>
|
||||
promises.push $.ajax("/db/level/#{level}/leaderboard_rank?scoreOffset=#{@session.get('totalScore')}&team=#{@team}", cache: false, success: success)
|
||||
loadURL = "/db/level/#{level}/leaderboard_rank?scoreOffset=#{score}&team=#{@team}"
|
||||
loadURL += '&leagues.leagueID=' + @league.id if @league
|
||||
promises.push $.ajax(loadURL, cache: false, success: success)
|
||||
@promise = $.when(promises...)
|
||||
@promise.then @onLoad
|
||||
@promise.fail @onFail
|
||||
|
|
|
@ -12,6 +12,9 @@ SimulateTabView = require './SimulateTabView'
|
|||
LadderPlayModal = require './LadderPlayModal'
|
||||
CocoClass = require 'core/CocoClass'
|
||||
|
||||
Clan = require 'models/Clan'
|
||||
#CourseInstance = require 'models/CourseInstance'
|
||||
|
||||
HIGHEST_SCORE = 1000000
|
||||
|
||||
class LevelSessionsCollection extends CocoCollection
|
||||
|
@ -35,12 +38,19 @@ module.exports = class LadderView extends RootView
|
|||
'click a:not([data-toggle])': 'onClickedLink'
|
||||
'click .spectate-button': 'onClickSpectateButton'
|
||||
|
||||
constructor: (options, @levelID) ->
|
||||
constructor: (options, @levelID, @leagueType, @leagueID) ->
|
||||
super(options)
|
||||
@level = @supermodel.loadModel(new Level(_id: @levelID), 'level').model
|
||||
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(@levelID), 'your_sessions', {cache: false}).model
|
||||
|
||||
@teams = []
|
||||
@loadLeague()
|
||||
|
||||
loadLeague: ->
|
||||
@leagueID = @leagueType = null unless @leagueType in ['clan'] #, 'course']
|
||||
return unless @leagueID
|
||||
modelClass = if @leagueType is 'clan' then Clan else null# else CourseInstance
|
||||
resourceString = if @leagueType is 'clan' then 'clans.clan' else null# else 'courses.course'
|
||||
@league = @supermodel.loadModel(new modelClass(_id: @leagueID), resourceString).model
|
||||
|
||||
onLoaded: ->
|
||||
@teams = teamDataFromLevel @level
|
||||
|
@ -53,6 +63,8 @@ module.exports = class LadderView extends RootView
|
|||
ctx.teams = @teams
|
||||
ctx.levelID = @levelID
|
||||
ctx.levelDescription = marked(@level.get('description')) if @level.get('description')
|
||||
ctx.leagueType = @leagueType
|
||||
ctx.league = @league
|
||||
ctx._ = _
|
||||
if tournamentEndDate = {greed: 1402444800000, 'criss-cross': 1410912000000, 'zero-sum': 1428364800000}[@levelID]
|
||||
ctx.tournamentTimeLeft = moment(new Date(tournamentEndDate)).fromNow()
|
||||
|
@ -64,9 +76,9 @@ module.exports = class LadderView extends RootView
|
|||
afterRender: ->
|
||||
super()
|
||||
return unless @supermodel.finished()
|
||||
@insertSubView(@ladderTab = new LadderTabView({}, @level, @sessions))
|
||||
@insertSubView(@myMatchesTab = new MyMatchesTabView({}, @level, @sessions))
|
||||
@insertSubView(@simulateTab = new SimulateTabView())
|
||||
@insertSubView(@ladderTab = new LadderTabView({league: @league}, @level, @sessions))
|
||||
@insertSubView(@myMatchesTab = new MyMatchesTabView({league: @league}, @level, @sessions))
|
||||
@insertSubView(@simulateTab = new SimulateTabView(league: @league))
|
||||
@refreshInterval = setInterval(@fetchSessionsAndRefreshViews.bind(@), 60 * 1000)
|
||||
hash = document.location.hash[1..] if document.location.hash
|
||||
if hash and not (hash in ['my-matches', 'simulate', 'ladder', 'prizes', 'rules', 'winners'])
|
||||
|
@ -101,7 +113,7 @@ module.exports = class LadderView extends RootView
|
|||
|
||||
showPlayModal: (teamID) ->
|
||||
session = (s for s in @sessions.models when s.get('team') is teamID)[0]
|
||||
modal = new LadderPlayModal({}, @level, session, teamID)
|
||||
modal = new LadderPlayModal({league: @league}, @level, session, teamID)
|
||||
@openModalView modal
|
||||
|
||||
onClickedLink: (e) ->
|
||||
|
|
|
@ -24,7 +24,8 @@ module.exports = class MyMatchesTabView extends CocoView
|
|||
# Only fetch the names for the userIDs we don't already have in @nameMap
|
||||
ids = []
|
||||
for session in @sessions.models
|
||||
for match in (session.get('matches') or [])
|
||||
matches = @statsFromSession(session).matches or []
|
||||
for match in matches
|
||||
id = match.opponents[0].userID
|
||||
unless id
|
||||
console.error 'Found bad opponent ID in malformed match:', match, 'from session', session
|
||||
|
@ -37,7 +38,8 @@ module.exports = class MyMatchesTabView extends CocoView
|
|||
success = (nameMap) =>
|
||||
return if @destroyed
|
||||
for session in @sessions.models
|
||||
for match in session.get('matches') or []
|
||||
matches = @statsFromSession(session).matches or []
|
||||
for match in matches
|
||||
opponent = match.opponents[0]
|
||||
continue if @nameMap[opponent.userID]
|
||||
opponentUser = nameMap[opponent.userID]
|
||||
|
@ -88,15 +90,16 @@ module.exports = class MyMatchesTabView extends CocoView
|
|||
|
||||
for team in @teams
|
||||
team.session = (s for s in @sessions.models when s.get('team') is team.id)[0]
|
||||
stats = @statsFromSession team.session
|
||||
team.readyToRank = team.session?.readyToRank()
|
||||
team.isRanking = team.session?.get('isRanking')
|
||||
team.matches = (convertMatch(match, team.session.get('submitDate')) for match in team.session?.get('matches') or [])
|
||||
team.matches = (convertMatch(match, team.session.get('submitDate')) for match in (stats?.matches or []))
|
||||
team.matches.reverse()
|
||||
team.score = (team.session?.get('totalScore') or 10).toFixed(2)
|
||||
team.score = (stats?.totalScore ? 10).toFixed(2)
|
||||
team.wins = _.filter(team.matches, {state: 'win', stale: false}).length
|
||||
team.ties = _.filter(team.matches, {state: 'tie', stale: false}).length
|
||||
team.losses = _.filter(team.matches, {state: 'loss', stale: false}).length
|
||||
scoreHistory = team.session?.get('scoreHistory')
|
||||
scoreHistory = stats?.scoreHistory
|
||||
if scoreHistory?.length > 1
|
||||
team.scoreHistory = scoreHistory
|
||||
|
||||
|
@ -123,6 +126,12 @@ module.exports = class MyMatchesTabView extends CocoView
|
|||
|
||||
@$el.find('tr.fresh').removeClass('fresh', 5000)
|
||||
|
||||
statsFromSession: (session) ->
|
||||
return null unless session
|
||||
if @options.league
|
||||
return _.find(session.get('leagues') or [], leagueID: @options.league.id)?.stats ? {}
|
||||
session.attributes
|
||||
|
||||
generateScoreLineChart: (wrapperID, scoreHistory, teamName) =>
|
||||
margin =
|
||||
top: 20
|
||||
|
|
|
@ -98,19 +98,6 @@ module.exports = class SimulateTabView extends CocoView
|
|||
link = if @simulationSpectateLink then "<a href=#{@simulationSpectateLink}>#{_.string.escapeHTML(@simulationMatchDescription)}</a>" else ''
|
||||
$('#simulation-status-text').html "<h3>#{@simulationStatus}</h3>#{link}"
|
||||
|
||||
resimulateAllSessions: ->
|
||||
postData =
|
||||
originalLevelID: @level.get('original')
|
||||
levelMajorVersion: @level.get('version').major
|
||||
console.log postData
|
||||
|
||||
$.ajax
|
||||
url: '/queue/scoring/resimulateAllSessions'
|
||||
method: 'POST'
|
||||
data: postData
|
||||
complete: (jqxhr) ->
|
||||
console.log jqxhr.responseText
|
||||
|
||||
destroy: ->
|
||||
clearTimeout @simulationPageRefreshTimeout
|
||||
@simulator?.destroy()
|
||||
|
|
|
@ -160,16 +160,18 @@ LevelHandler = class LevelHandler extends Handler
|
|||
creator: req.user._id+''
|
||||
|
||||
query = Session.find(sessionQuery).select('-screenshot -transpiledCode')
|
||||
# TODO: take out "code" as well, since that can get huge containing the transpiled code for the lat hero, and find another way of having the LadderSubmissionViews in the MyMatchesTab determine rankin readiness
|
||||
# TODO: take out "code" as well, since that can get huge containing the transpiled code for the lat hero, and find another way of having the LadderSubmissionViews in the MyMatchesTab determine ranking readiness
|
||||
query.exec (err, results) =>
|
||||
if err then @sendDatabaseError(res, err) else @sendSuccess res, results
|
||||
|
||||
getHistogramData: (req, res, slug) ->
|
||||
match = levelID: slug, submitted: true, team: req.query.team
|
||||
match['leagues.leagueID'] = league if league = req.query['leagues.leagueID']
|
||||
aggregate = Session.aggregate [
|
||||
{$match: {'levelID': slug, 'submitted': true, 'team': req.query.team}}
|
||||
{$match: match}
|
||||
{$project: {totalScore: 1, _id: 0}}
|
||||
]
|
||||
aggregate.cache()
|
||||
aggregate.cache() unless league
|
||||
|
||||
aggregate.exec (err, data) =>
|
||||
if err? then return @sendDatabaseError res, err
|
||||
|
@ -198,7 +200,7 @@ LevelHandler = class LevelHandler extends Handler
|
|||
|
||||
sortParameters =
|
||||
'totalScore': req.query.order
|
||||
selectProperties = ['totalScore', 'creatorName', 'creator', 'submittedCodeLanguage', 'heroConfig']
|
||||
selectProperties = ['totalScore', 'creatorName', 'creator', 'submittedCodeLanguage', 'heroConfig', 'leagues.leagueID', 'leagues.stats.totalScore']
|
||||
|
||||
query = Session
|
||||
.find(sessionsQueryParameters)
|
||||
|
@ -232,6 +234,7 @@ LevelHandler = class LevelHandler extends Handler
|
|||
team: req.query.team
|
||||
totalScore: scoreQuery
|
||||
submitted: true
|
||||
query['leagues.leagueID'] = league if league = req.query['leagues.leagueID']
|
||||
query
|
||||
|
||||
validateLeaderboardRequestParameters: (req) ->
|
||||
|
|
|
@ -17,6 +17,7 @@ LevelSessionSchema.index({levelID: 1})
|
|||
LevelSessionSchema.index({'level.majorVersion': 1})
|
||||
LevelSessionSchema.index({'level.original': 1}, {name: 'Level Original'})
|
||||
LevelSessionSchema.index({'level.original': 1, 'level.majorVersion': 1, 'creator': 1, 'team': 1})
|
||||
LevelSessionSchema.index({creator: 1, level: 1}) # Looks like the ones operating on level as two separate fields might not be working, and sometimes this query uses the "level" index instead of the "creator" index.
|
||||
LevelSessionSchema.index({playtime: 1}, {name: 'Playtime'})
|
||||
LevelSessionSchema.index({submitDate: 1})
|
||||
LevelSessionSchema.index({submitted: 1}, {sparse: true})
|
||||
|
@ -24,10 +25,13 @@ LevelSessionSchema.index({team: 1}, {sparse: true})
|
|||
LevelSessionSchema.index({totalScore: 1}, {sparse: true})
|
||||
LevelSessionSchema.index({user: 1, changed: -1}, {name: 'last played index', sparse: true})
|
||||
LevelSessionSchema.index({'level.original': 1, 'state.topScores.type': 1, 'state.topScores.date': -1, 'state.topScores.score': -1}, {name: 'top scores index', sparse: true})
|
||||
LevelSessionSchema.index({submitted: 1, team: 1, level:1, totalScore: -1}, {name: 'rank counting index', sparse: true})
|
||||
LevelSessionSchema.index({levelID: 1, submitted:1, team: 1}, {name: 'get all scores index', sparse: true})
|
||||
LevelSessionSchema.index({submitted: 1, team: 1, level: 1, totalScore: -1}, {name: 'rank counting index', sparse: true})
|
||||
#LevelSessionSchema.index({level: 1, 'leagues.leagueID': 1, submitted: 1, team: 1, totalScore: -1}, {name: 'league rank counting index', sparse: true}) # needed for league leaderboards?
|
||||
LevelSessionSchema.index({levelID: 1, submitted: 1, team: 1}, {name: 'get all scores index', sparse: true})
|
||||
#LevelSessionSchema.index({levelID: 1, 'leagues.leagueID': 1, submitted: 1, team: 1}, {name: 'league get all scores index', sparse: true}) # needed for league histograms?
|
||||
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)
|
||||
|
|
|
@ -9,25 +9,24 @@ queues = require '../commons/queue'
|
|||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
Level = require '../levels/Level'
|
||||
User = require '../users/User'
|
||||
TaskLog = require './task/ScoringTask'
|
||||
bayes = new (require 'bayesian-battle')()
|
||||
TaskLog = require './scoring/ScoringTask'
|
||||
scoringUtils = require './scoring/scoringUtils'
|
||||
getTwoGames = require './scoring/getTwoGames'
|
||||
recordTwoGames = require './scoring/recordTwoGames'
|
||||
createNewTask = require './scoring/createNewTask'
|
||||
dispatchTaskToConsumer = require './scoring/dispatchTaskToConsumer'
|
||||
processTaskResult = require './scoring/processTaskResult'
|
||||
|
||||
scoringTaskQueue = undefined
|
||||
scoringTaskTimeoutInSeconds = 600
|
||||
|
||||
SIMULATOR_VERSION = 3
|
||||
|
||||
module.exports.setup = (app) -> connectToScoringQueue()
|
||||
|
||||
connectToScoringQueue = ->
|
||||
module.exports.setup = (app) ->
|
||||
# Connect to scoring queue
|
||||
queues.initializeQueueClient ->
|
||||
queues.queueClient.registerQueue 'scoring', {}, (error, data) ->
|
||||
if error? then throw new Error "There was an error registering the scoring queue: #{error}"
|
||||
scoringTaskQueue = data
|
||||
scoringUtils.scoringTaskQueue = data
|
||||
#log.info 'Connected to scoring task queue!'
|
||||
|
||||
module.exports.messagesInQueueCount = (req, res) ->
|
||||
scoringTaskQueue.totalMessagesInQueue (err, count) ->
|
||||
scoringUtils.scoringTaskQueue.totalMessagesInQueue (err, count) ->
|
||||
if err? then return errors.serverError res, "There was an issue finding the Mongoose count:#{err}"
|
||||
response = String(count)
|
||||
res.send(response)
|
||||
|
@ -35,673 +34,13 @@ module.exports.messagesInQueueCount = (req, res) ->
|
|||
|
||||
module.exports.addPairwiseTaskToQueueFromRequest = (req, res) ->
|
||||
taskPair = req.body.sessions
|
||||
addPairwiseTaskToQueue req.body.sessions, (err, success) ->
|
||||
scoringUtils.addPairwiseTaskToQueue req.body.sessions, (err, success) ->
|
||||
if err? then return errors.serverError res, "There was an error adding pairwise tasks: #{err}"
|
||||
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'}
|
||||
|
||||
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 = generateTaskPairs(secondSession, firstSession)
|
||||
catch e
|
||||
if e then return cb e
|
||||
|
||||
sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
|
||||
if taskPairError? then return cb taskPairError
|
||||
cb null
|
||||
|
||||
# We should rip these out, probably
|
||||
module.exports.resimulateAllSessions = (req, res) ->
|
||||
unless isUserAdmin req then return errors.unauthorized res, 'Unauthorized. Even if you are authorized, you shouldn\'t do this'
|
||||
|
||||
originalLevelID = req.body.originalLevelID
|
||||
levelMajorVersion = parseInt(req.body.levelMajorVersion)
|
||||
|
||||
findParameters =
|
||||
submitted: true
|
||||
level:
|
||||
original: originalLevelID
|
||||
majorVersion: levelMajorVersion
|
||||
|
||||
query = LevelSession
|
||||
.find(findParameters)
|
||||
.lean()
|
||||
|
||||
query.exec (err, result) ->
|
||||
if err? then return errors.serverError res, err
|
||||
result = _.sample result, 10
|
||||
async.each result, resimulateSession.bind(@, originalLevelID, levelMajorVersion), (err) ->
|
||||
if err? then return errors.serverError res, err
|
||||
sendResponseObject req, res, {message: 'All task pairs were succesfully sent to the queue'}
|
||||
|
||||
resimulateSession = (originalLevelID, levelMajorVersion, session, cb) =>
|
||||
sessionUpdateObject =
|
||||
submitted: true
|
||||
submitDate: new Date()
|
||||
meanStrength: 25
|
||||
standardDeviation: 25/3
|
||||
totalScore: 10
|
||||
numberOfWinsAndTies: 0
|
||||
numberOfLosses: 0
|
||||
isRanking: true
|
||||
LevelSession.update {_id: session._id}, sessionUpdateObject, (err, updatedSession) ->
|
||||
if err? then return cb err, null
|
||||
opposingTeam = calculateOpposingTeam(session.team)
|
||||
fetchInitialSessionsToRankAgainst levelMajorVersion, originalLevelID, opposingTeam, (err, sessionsToRankAgainst) ->
|
||||
if err? then return cb err, null
|
||||
|
||||
taskPairs = generateTaskPairs(sessionsToRankAgainst, session)
|
||||
sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
|
||||
if taskPairError? then return cb taskPairError, null
|
||||
cb null
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
selection = 'team totalScore transpiledCode submittedCodeLanguage teamSpells levelID creatorName creator submitDate'
|
||||
LevelSession.findOne(queryParams).sort(submitDate: -1).select(selection).lean().exec (err, session) ->
|
||||
return callback err if err
|
||||
callback null, session
|
||||
|
||||
findRandomSession = (queryParams, callback) ->
|
||||
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) ->
|
||||
return callback err if err
|
||||
return callback null, session if session
|
||||
delete queryParams.randomSimulationIndex # Effectively switch to $gt, 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
|
||||
|
||||
formatSessionInformation = (session) ->
|
||||
sessionID: session._id
|
||||
team: session.team ? 'No team'
|
||||
transpiledCode: session.transpiledCode
|
||||
submittedCodeLanguage: session.submittedCodeLanguage
|
||||
teamSpells: session.teamSpells ? {}
|
||||
levelID: session.levelID
|
||||
creatorName: session.creatorName
|
||||
creator: session.creator
|
||||
totalScore: session.totalScore
|
||||
|
||||
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 if 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: (formatSessionInformation session for session in sessions)
|
||||
#console.log 'Dispatching random game between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName
|
||||
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: (formatSessionInformation session for session in [humanSession, ogreSession])
|
||||
sendResponseObject req, res, taskObject
|
||||
|
||||
module.exports.recordTwoGames = (req, res) ->
|
||||
sessions = req.body.sessions
|
||||
#console.log 'Recording non-chained result of', sessions?[0]?.name, sessions[0]?.metrics?.rank, 'and', sessions?[1]?.name, sessions?[1]?.metrics?.rank
|
||||
return if simulatorIsTooOld req, res
|
||||
req.body?.simulator?.user = '' + req.user?._id
|
||||
|
||||
yetiGuru = clientResponseObject: req.body, isRandomMatch: true
|
||||
async.waterfall [
|
||||
calculateSessionScores.bind(yetiGuru) # Fetches a few small properties from both sessions, prepares @levelSessionUpdates with the score part
|
||||
indexNewScoreArray.bind(yetiGuru) # Creates and returns @newScoresObject, no query
|
||||
addMatchToSessionsAndUpdate.bind(yetiGuru) # Adds matches to the session updates and does the writes
|
||||
updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
|
||||
], (err, successMessageObject) ->
|
||||
if err? then return errors.serverError res, "There was an error recording the single game: #{err}"
|
||||
sendResponseObject req, res, {message: 'The single game was submitted successfully!'}
|
||||
|
||||
module.exports.createNewTask = (req, res) ->
|
||||
requestSessionID = req.body.session
|
||||
originalLevelID = req.body.originalLevelID
|
||||
currentLevelID = req.body.levelID
|
||||
transpiledCode = req.body.transpiledCode
|
||||
requestLevelMajorVersion = parseInt(req.body.levelMajorVersion)
|
||||
|
||||
yetiGuru = {}
|
||||
async.waterfall [
|
||||
validatePermissions.bind(yetiGuru, req, requestSessionID)
|
||||
fetchAndVerifyLevelType.bind(yetiGuru, currentLevelID)
|
||||
fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID)
|
||||
updateSessionToSubmit.bind(yetiGuru, transpiledCode)
|
||||
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}"
|
||||
sendResponseObject req, res, successMessageObject
|
||||
|
||||
validatePermissions = (req, sessionID, callback) ->
|
||||
if isUserAnonymous req then return callback 'You are unauthorized to submit that game to the simulator'
|
||||
if isUserAdmin req then return callback null
|
||||
|
||||
findParameters =
|
||||
_id: sessionID
|
||||
selectString = 'creator submittedCode code'
|
||||
query = LevelSession
|
||||
.findOne(findParameters)
|
||||
.select(selectString)
|
||||
.lean()
|
||||
|
||||
query.exec (err, retrievedSession) ->
|
||||
if err? then return callback err
|
||||
userHasPermissionToSubmitCode = retrievedSession.creator is req.user?.id and
|
||||
not _.isEqual(retrievedSession.code, retrievedSession.submittedCode)
|
||||
unless userHasPermissionToSubmitCode then return callback 'You are unauthorized to submit that game to the simulator'
|
||||
callback null
|
||||
|
||||
fetchAndVerifyLevelType = (levelID, cb) ->
|
||||
findParameters =
|
||||
_id: levelID
|
||||
selectString = 'type'
|
||||
|
||||
query = Level
|
||||
.findOne(findParameters)
|
||||
.select(selectString)
|
||||
.lean()
|
||||
query.exec (err, levelWithType) ->
|
||||
if err? then return cb err
|
||||
if not levelWithType.type or not (levelWithType.type in ['ladder', 'hero-ladder']) then return cb 'Level isn\'t of type "ladder"'
|
||||
cb null
|
||||
|
||||
fetchSessionObjectToSubmit = (sessionID, callback) ->
|
||||
findParameters =
|
||||
_id: sessionID
|
||||
selectString = 'team code'
|
||||
|
||||
query = LevelSession
|
||||
.findOne(findParameters)
|
||||
.select(selectString)
|
||||
|
||||
query.exec (err, session) ->
|
||||
callback err, session?.toObject()
|
||||
|
||||
updateSessionToSubmit = (transpiledCode, 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
|
||||
#totalScore: 10 # Let's try not resetting the score on resubmission
|
||||
numberOfWinsAndTies: 0
|
||||
numberOfLosses: 0
|
||||
isRanking: true
|
||||
randomSimulationIndex: Math.random()
|
||||
LevelSession.update {_id: sessionToUpdate._id}, sessionUpdateObject, (err, result) ->
|
||||
callback err, sessionToUpdate
|
||||
|
||||
fetchInitialSessionsToRankAgainst = (levelMajorVersion, levelID, submittedSession, callback) ->
|
||||
opposingTeam = calculateOpposingTeam(submittedSession.team)
|
||||
|
||||
findParameters =
|
||||
'level.original': levelID
|
||||
'level.majorVersion': levelMajorVersion
|
||||
submitted: true
|
||||
team: opposingTeam
|
||||
|
||||
sortParameters =
|
||||
totalScore: 1
|
||||
|
||||
limitNumber = 1
|
||||
query = LevelSession.aggregate [
|
||||
{$match: findParameters}
|
||||
{$sort: sortParameters}
|
||||
{$limit: limitNumber}
|
||||
]
|
||||
|
||||
query.exec (err, sessionToRankAgainst) ->
|
||||
callback err, sessionToRankAgainst, submittedSession
|
||||
|
||||
generateAndSendTaskPairsToTheQueue = (sessionToRankAgainst, submittedSession, callback) ->
|
||||
taskPairs = generateTaskPairs(sessionToRankAgainst, submittedSession)
|
||||
sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
|
||||
if taskPairError? then return callback taskPairError
|
||||
#console.log 'Sent task pairs to the queue!'
|
||||
#console.log taskPairs
|
||||
callback null, {message: 'All task pairs were succesfully sent to the queue'}
|
||||
|
||||
module.exports.dispatchTaskToConsumer = (req, res) ->
|
||||
yetiGuru = {}
|
||||
async.waterfall [
|
||||
checkSimulationPermissions.bind(yetiGuru, req)
|
||||
receiveMessageFromSimulationQueue
|
||||
changeMessageVisibilityTimeout
|
||||
parseTaskQueueMessage
|
||||
constructTaskObject
|
||||
constructTaskLogObject.bind(yetiGuru, getUserIDFromRequest(req))
|
||||
processTaskObject
|
||||
], (err, taskObjectToSend) ->
|
||||
if err?
|
||||
if typeof err is 'string' and err.indexOf 'No more games in the queue' isnt -1
|
||||
res.send(204, 'No games to score.')
|
||||
return res.end()
|
||||
else
|
||||
return errors.serverError res, "There was an error dispatching the task: #{err}"
|
||||
sendResponseObject req, res, taskObjectToSend
|
||||
|
||||
checkSimulationPermissions = (req, cb) ->
|
||||
if isUserAnonymous req
|
||||
cb 'You need to be logged in to simulate games'
|
||||
else
|
||||
cb null
|
||||
|
||||
receiveMessageFromSimulationQueue = (cb) ->
|
||||
scoringTaskQueue.receiveMessage (err, message) ->
|
||||
if err? then return cb "No more games in the queue, error:#{err}"
|
||||
if messageIsInvalid(message) then return cb 'Message received from queue is invalid'
|
||||
cb null, message
|
||||
|
||||
changeMessageVisibilityTimeout = (message, cb) ->
|
||||
message.changeMessageVisibilityTimeout scoringTaskTimeoutInSeconds, (err) -> cb err, message
|
||||
|
||||
parseTaskQueueMessage = (message, cb) ->
|
||||
try
|
||||
if typeof message.getBody() is 'object'
|
||||
messageBody = message.getBody()
|
||||
else
|
||||
messageBody = JSON.parse message.getBody()
|
||||
cb null, messageBody, message
|
||||
catch e
|
||||
cb "There was an error parsing the task.Error: #{e}"
|
||||
|
||||
constructTaskObject = (taskMessageBody, message, callback) ->
|
||||
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
|
||||
if err? then return callback err
|
||||
taskObject = messageGenerated: Date.now(), sessions: (formatSessionInformation session for session in sessions)
|
||||
callback null, taskObject, message
|
||||
|
||||
constructTaskLogObject = (calculatorUserID, taskObject, message, callback) ->
|
||||
taskLogObject = new TaskLog
|
||||
'createdAt': new Date()
|
||||
'calculator': calculatorUserID
|
||||
'sentDate': Date.now()
|
||||
'messageIdentifierString': message.getReceiptHandle()
|
||||
taskLogObject.save (err) -> callback err, taskObject, taskLogObject, message
|
||||
|
||||
processTaskObject = (taskObject, taskLogObject, message, cb) ->
|
||||
taskObject.taskID = taskLogObject._id
|
||||
taskObject.receiptHandle = message.getReceiptHandle()
|
||||
cb null, taskObject
|
||||
|
||||
getSessionInformation = (sessionIDString, callback) ->
|
||||
findParameters =
|
||||
_id: sessionIDString
|
||||
selectString = 'submitDate team submittedCode teamSpells levelID creator creatorName transpiledCode submittedCodeLanguage totalScore'
|
||||
query = LevelSession
|
||||
.findOne(findParameters)
|
||||
.select(selectString)
|
||||
.lean()
|
||||
|
||||
query.exec (err, session) ->
|
||||
if err? then return callback err, {'error': 'There was an error retrieving the session.'}
|
||||
callback null, session
|
||||
|
||||
module.exports.processTaskResult = (req, res) ->
|
||||
return if simulatorIsTooOld req, res
|
||||
originalSessionID = req.body?.originalSessionID
|
||||
req.body?.simulator?.user = '' + req.user?._id
|
||||
yetiGuru = {}
|
||||
try
|
||||
async.waterfall [
|
||||
verifyClientResponse.bind(yetiGuru, req.body)
|
||||
fetchTaskLog.bind(yetiGuru)
|
||||
checkTaskLog.bind(yetiGuru)
|
||||
deleteQueueMessage.bind(yetiGuru)
|
||||
fetchLevelSession.bind(yetiGuru)
|
||||
checkSubmissionDate.bind(yetiGuru)
|
||||
logTaskComputation.bind(yetiGuru)
|
||||
calculateSessionScores.bind(yetiGuru)
|
||||
indexNewScoreArray.bind(yetiGuru)
|
||||
addMatchToSessionsAndUpdate.bind(yetiGuru)
|
||||
updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
|
||||
determineIfSessionShouldContinueAndUpdateLog.bind(yetiGuru)
|
||||
findNearestBetterSessionID.bind(yetiGuru)
|
||||
addNewSessionsToQueue.bind(yetiGuru)
|
||||
], (err, results) ->
|
||||
if err is 'shouldn\'t continue'
|
||||
markSessionAsDoneRanking originalSessionID, (err) ->
|
||||
if err? then return sendResponseObject req, res, {'error': 'There was an error marking the session as done ranking'}
|
||||
sendResponseObject req, 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 sendResponseObject req, res, {'error': 'There was an error marking the session as done ranking'}
|
||||
sendResponseObject req, 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
|
||||
sendResponseObject req, 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!'
|
||||
|
||||
verifyClientResponse = (responseObject, callback) ->
|
||||
#TODO: better verification
|
||||
if typeof responseObject isnt 'object' or responseObject?.originalSessionID?.length isnt 24
|
||||
callback 'The response to that query is required to be a JSON object.'
|
||||
else
|
||||
@clientResponseObject = responseObject
|
||||
callback null, responseObject
|
||||
|
||||
fetchTaskLog = (responseObject, callback) ->
|
||||
TaskLog.findOne(_id: responseObject.taskID).lean().exec (err, taskLog) =>
|
||||
return callback new Error("Couldn't find TaskLog for _id #{responseObject.taskID}!") unless taskLog
|
||||
@taskLog = taskLog
|
||||
callback err, taskLog
|
||||
|
||||
checkTaskLog = (taskLog, callback) ->
|
||||
if taskLog.calculationTimeMS then return callback 'That computational task has already been performed'
|
||||
if hasTaskTimedOut taskLog.sentDate then return callback 'The task has timed out'
|
||||
callback null
|
||||
|
||||
deleteQueueMessage = (callback) ->
|
||||
scoringTaskQueue.deleteMessage @clientResponseObject.receiptHandle, (err) ->
|
||||
callback err
|
||||
|
||||
fetchLevelSession = (callback) ->
|
||||
LevelSession.findOne(_id: @clientResponseObject.originalSessionID).select('submitDate creator level standardDeviation meanStrength totalScore submittedCodeLanguage').lean().exec (err, session) =>
|
||||
@levelSession = session
|
||||
callback err
|
||||
|
||||
checkSubmissionDate = (callback) ->
|
||||
supposedSubmissionDate = new Date(@clientResponseObject.sessions[0].submitDate)
|
||||
if Number(supposedSubmissionDate) isnt Number(@levelSession.submitDate)
|
||||
callback 'The game has been resubmitted. Removing from queue...'
|
||||
else
|
||||
callback null
|
||||
|
||||
logTaskComputation = (callback) ->
|
||||
@taskLog.set('calculationTimeMS', @clientResponseObject.calculationTimeMS)
|
||||
@taskLog.set('sessions') # Huh?
|
||||
@taskLog.calculationTimeMS = @clientResponseObject.calculationTimeMS
|
||||
@taskLog.sessions = @clientResponseObject.sessions
|
||||
@taskLog.save (err, saved) ->
|
||||
callback err
|
||||
|
||||
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'}
|
||||
try
|
||||
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
|
||||
newScoreArray = bayes.updatePlayerSkills oldScoreArray
|
||||
createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray
|
||||
callback err, newScoreArray
|
||||
catch e
|
||||
callback e
|
||||
|
||||
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()
|
||||
|
||||
indexNewScoreArray = (newScoreArray, callback) ->
|
||||
newScoresObject = _.indexBy newScoreArray, 'id'
|
||||
@newScoresObject = newScoresObject
|
||||
callback null, newScoresObject
|
||||
|
||||
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 = _.pluck @clientResponseObject.sessions, 'sessionID'
|
||||
async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) ->
|
||||
callback err
|
||||
|
||||
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
|
||||
#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)}"
|
||||
LevelSession.update {_id: sessionID}, sessionUpdateObject, callback
|
||||
|
||||
updateUserSimulationCounts = (reqUserID, callback) ->
|
||||
incrementUserSimulationCount reqUserID, 'simulatedBy', (err) =>
|
||||
if err? then return callback err
|
||||
#console.log 'Incremented user simulation count!'
|
||||
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, affected) ->
|
||||
log.error "Error incrementing #{type} for #{userID}: #{err}" if err
|
||||
callback err
|
||||
|
||||
determineIfSessionShouldContinueAndUpdateLog = (cb) ->
|
||||
sessionID = @clientResponseObject.originalSessionID
|
||||
sessionRank = parseInt @clientResponseObject.originalSessionRank
|
||||
|
||||
queryParameters = _id: sessionID
|
||||
updateParameters = '$inc': {}
|
||||
|
||||
if sessionRank is 0
|
||||
updateParameters['$inc'] = {numberOfWinsAndTies: 1}
|
||||
else
|
||||
updateParameters['$inc'] = {numberOfLosses: 1}
|
||||
|
||||
LevelSession.findOneAndUpdate queryParameters, updateParameters, {select: 'numberOfWinsAndTies numberOfLosses', lean: true}, (err, updatedSession) ->
|
||||
if err? then return cb err, updatedSession
|
||||
|
||||
totalNumberOfGamesPlayed = updatedSession.numberOfWinsAndTies + updatedSession.numberOfLosses
|
||||
if totalNumberOfGamesPlayed < 10
|
||||
#console.log 'Number of games played is less than 10, continuing...'
|
||||
cb null
|
||||
else
|
||||
ratio = (updatedSession.numberOfLosses) / (totalNumberOfGamesPlayed)
|
||||
if ratio > 0.33
|
||||
cb 'shouldn\'t continue'
|
||||
#console.log "Ratio(#{ratio}) is bad, ending simulation"
|
||||
else
|
||||
#console.log "Ratio(#{ratio}) is good, so continuing simulations"
|
||||
cb null
|
||||
|
||||
findNearestBetterSessionID = (cb) ->
|
||||
try
|
||||
levelOriginalID = @levelSession.level.original
|
||||
levelMajorVersion = @levelSession.level.majorVersion
|
||||
sessionID = @clientResponseObject.originalSessionID
|
||||
sessionTotalScore = @newScoresObject[sessionID].totalScore
|
||||
opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID)
|
||||
opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore
|
||||
opposingTeam = calculateOpposingTeam(@clientResponseObject.originalSessionTeam)
|
||||
catch e
|
||||
cb e
|
||||
|
||||
retrieveAllOpponentSessionIDs sessionID, (err, opponentSessionIDs) ->
|
||||
if err? then return cb err, null
|
||||
|
||||
queryParameters =
|
||||
totalScore:
|
||||
$gt: opponentSessionTotalScore
|
||||
_id:
|
||||
$nin: opponentSessionIDs
|
||||
'level.original': levelOriginalID
|
||||
'level.majorVersion': levelMajorVersion
|
||||
submitted: true
|
||||
team: opposingTeam
|
||||
|
||||
if opponentSessionTotalScore < 30
|
||||
# Don't play a ton of matches at low scores--skip some in proportion to how close to 30 we are.
|
||||
# TODO: this could be made a lot more flexible.
|
||||
queryParameters['totalScore']['$gt'] = opponentSessionTotalScore + 2 * (30 - opponentSessionTotalScore) / 20
|
||||
|
||||
limitNumber = 1
|
||||
|
||||
sortParameters =
|
||||
totalScore: 1
|
||||
|
||||
selectString = '_id totalScore'
|
||||
|
||||
query = LevelSession.findOne(queryParameters)
|
||||
.sort(sortParameters)
|
||||
.limit(limitNumber)
|
||||
.select(selectString)
|
||||
.lean()
|
||||
|
||||
#console.log "Finding session with score near #{opponentSessionTotalScore}"
|
||||
query.exec (err, session) ->
|
||||
if err? then return cb err, session
|
||||
unless session then return cb 'no session was found'
|
||||
#console.log "Found session with score #{session.totalScore}"
|
||||
cb err, session._id
|
||||
|
||||
retrieveAllOpponentSessionIDs = (sessionID, cb) ->
|
||||
query = LevelSession.findOne({_id: sessionID})
|
||||
.select('matches.opponents.sessionID matches.date submitDate')
|
||||
.lean()
|
||||
query.exec (err, session) ->
|
||||
if err? then return cb err, null
|
||||
opponentSessionIDs = (match.opponents[0].sessionID for match in session.matches when match.date > session.submitDate)
|
||||
cb err, opponentSessionIDs
|
||||
|
||||
calculateOpposingTeam = (sessionTeam) ->
|
||||
teams = ['ogres', 'humans']
|
||||
opposingTeams = _.pull teams, sessionTeam
|
||||
return opposingTeams[0]
|
||||
|
||||
addNewSessionsToQueue = (sessionID, callback) ->
|
||||
sessions = [@clientResponseObject.originalSessionID, sessionID]
|
||||
addPairwiseTaskToQueue sessions, callback
|
||||
|
||||
messageIsInvalid = (message) -> (not message?) or message.isEmpty()
|
||||
|
||||
sendEachTaskPairToTheQueue = (taskPairs, callback) -> async.each taskPairs, sendTaskPairToQueue, callback
|
||||
|
||||
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
|
||||
|
||||
sendTaskPairToQueue = (taskPair, callback) ->
|
||||
scoringTaskQueue.sendMessage {sessions: taskPair}, 5, (err, data) -> callback? err, data
|
||||
|
||||
getUserIDFromRequest = (req) -> if req.user? then return req.user._id else return null
|
||||
|
||||
isUserAnonymous = (req) -> if req.user? then return req.user.get('anonymous') else return true
|
||||
|
||||
isUserAdmin = (req) -> return Boolean(req.user?.isAdmin())
|
||||
|
||||
sendResponseObject = (req, res, object) ->
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(object)
|
||||
res.end()
|
||||
|
||||
hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutInSeconds * 1000 < Date.now()
|
||||
|
||||
handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, 'The results weren\'t provided within the timeout'
|
||||
|
||||
putRankingFromMetricsIntoScoreObject = (taskObject, scoreObject) ->
|
||||
scoreObject = _.indexBy scoreObject, 'id'
|
||||
scoreObject[session.sessionID].gameRanking = session.metrics.rank for session in taskObject.sessions
|
||||
return scoreObject
|
||||
|
||||
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
|
||||
|
||||
return formatOldScoreObject @levelSession if sessionID is @levelSession?._id # No need to fetch again
|
||||
|
||||
query = _id: sessionID
|
||||
selection = 'standardDeviation meanStrength totalScore submittedCodeLanguage'
|
||||
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
|
||||
|
||||
markSessionAsDoneRanking = (sessionID, cb) ->
|
||||
#console.log 'Marking session as done ranking...'
|
||||
LevelSession.update {_id: sessionID}, {isRanking: false}, cb
|
||||
|
||||
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.getTwoGames = getTwoGames
|
||||
module.exports.recordTwoGames = recordTwoGames
|
||||
module.exports.createNewTask = createNewTask
|
||||
module.exports.dispatchTaskToConsumer = dispatchTaskToConsumer
|
||||
module.exports.processTaskResult = processTaskResult
|
||||
|
|
107
server/queues/scoring/createNewTask.coffee
Normal file
107
server/queues/scoring/createNewTask.coffee
Normal file
|
@ -0,0 +1,107 @@
|
|||
log = require 'winston'
|
||||
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
|
||||
originalLevelID = req.body.originalLevelID
|
||||
currentLevelID = req.body.levelID
|
||||
transpiledCode = req.body.transpiledCode
|
||||
requestLevelMajorVersion = parseInt(req.body.levelMajorVersion)
|
||||
|
||||
yetiGuru = {}
|
||||
async.waterfall [
|
||||
validatePermissions.bind(yetiGuru, req, requestSessionID)
|
||||
fetchAndVerifyLevelType.bind(yetiGuru, currentLevelID)
|
||||
fetchSessionObjectToSubmit.bind(yetiGuru, requestSessionID)
|
||||
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 res, successMessageObject
|
||||
|
||||
|
||||
validatePermissions = (req, sessionID, callback) ->
|
||||
return callback 'You are unauthorized to submit that game to the simulator' unless req.user?.get('email')
|
||||
return callback null if req.user?.isAdmin()
|
||||
|
||||
findParameters = _id: sessionID
|
||||
selectString = 'creator submittedCode code'
|
||||
LevelSession.findOne(findParameters).select(selectString).lean().exec (err, retrievedSession) ->
|
||||
if err? then return callback err
|
||||
userHasPermissionToSubmitCode = retrievedSession.creator is req.user?.id and
|
||||
not _.isEqual(retrievedSession.code, retrievedSession.submittedCode)
|
||||
unless userHasPermissionToSubmitCode then return callback 'You are unauthorized to submit that game to the simulator'
|
||||
callback null
|
||||
|
||||
|
||||
fetchAndVerifyLevelType = (levelID, cb) ->
|
||||
Level.findOne(_id: levelID).select('type').lean().exec (err, levelWithType) ->
|
||||
if err? then return cb err
|
||||
if not levelWithType.type or not (levelWithType.type in ['ladder', 'hero-ladder', 'course-ladder']) then return cb 'Level isn\'t of type "ladder"'
|
||||
cb null
|
||||
|
||||
fetchSessionObjectToSubmit = (sessionID, callback) ->
|
||||
LevelSession.findOne({_id: sessionID}).select('team code leagues').exec (err, session) ->
|
||||
callback err, session?.toObject()
|
||||
|
||||
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
|
||||
#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
|
||||
|
||||
fetchInitialSessionsToRankAgainst = (levelMajorVersion, levelID, submittedSession, callback) ->
|
||||
opposingTeam = scoringUtils.calculateOpposingTeam(submittedSession.team)
|
||||
findParameters =
|
||||
'level.original': levelID
|
||||
'level.majorVersion': levelMajorVersion
|
||||
submitted: true
|
||||
team: opposingTeam
|
||||
sortParameters = totalScore: 1
|
||||
limitNumber = 1
|
||||
query = LevelSession.aggregate [
|
||||
{$match: findParameters}
|
||||
{$sort: sortParameters}
|
||||
{$limit: limitNumber}
|
||||
]
|
||||
|
||||
query.exec (err, sessionToRankAgainst) ->
|
||||
callback err, sessionToRankAgainst, submittedSession
|
||||
|
||||
generateAndSendTaskPairsToTheQueue = (sessionToRankAgainst, submittedSession, callback) ->
|
||||
taskPairs = scoringUtils.generateTaskPairs(sessionToRankAgainst, submittedSession)
|
||||
scoringUtils.sendEachTaskPairToTheQueue taskPairs, (taskPairError) ->
|
||||
if taskPairError? then return callback taskPairError
|
||||
#console.log 'Sent task pairs to the queue!'
|
||||
#console.log taskPairs
|
||||
callback null, {message: 'All task pairs were succesfully sent to the queue'}
|
80
server/queues/scoring/dispatchTaskToConsumer.coffee
Normal file
80
server/queues/scoring/dispatchTaskToConsumer.coffee
Normal file
|
@ -0,0 +1,80 @@
|
|||
log = require 'winston'
|
||||
async = require 'async'
|
||||
errors = require '../../commons/errors'
|
||||
scoringUtils = require './scoringUtils'
|
||||
LevelSession = require '../../levels/sessions/LevelSession'
|
||||
TaskLog = require './ScoringTask'
|
||||
|
||||
module.exports = dispatchTaskToConsumer = (req, res) ->
|
||||
yetiGuru = {}
|
||||
async.waterfall [
|
||||
checkSimulationPermissions.bind(yetiGuru, req)
|
||||
receiveMessageFromSimulationQueue
|
||||
changeMessageVisibilityTimeout
|
||||
parseTaskQueueMessage
|
||||
constructTaskObject
|
||||
constructTaskLogObject.bind(yetiGuru, getUserIDFromRequest(req))
|
||||
processTaskObject
|
||||
], (err, taskObjectToSend) ->
|
||||
if err?
|
||||
if typeof err is 'string' and err.indexOf 'No more games in the queue' isnt -1
|
||||
res.send(204, 'No games to score.')
|
||||
return res.end()
|
||||
else
|
||||
return errors.serverError res, "There was an error dispatching the task: #{err}"
|
||||
scoringUtils.sendResponseObject res, taskObjectToSend
|
||||
|
||||
|
||||
checkSimulationPermissions = (req, cb) ->
|
||||
if req.user?.get('email')
|
||||
cb null
|
||||
else
|
||||
cb 'You need to be logged in to simulate games'
|
||||
|
||||
receiveMessageFromSimulationQueue = (cb) ->
|
||||
scoringUtils.scoringTaskQueue.receiveMessage (err, message) ->
|
||||
if err? then return cb "No more games in the queue, error: #{err}"
|
||||
if not message? or message.isEmpty() then return cb 'Message received from queue is invalid'
|
||||
cb null, message
|
||||
|
||||
changeMessageVisibilityTimeout = (message, cb) ->
|
||||
message.changeMessageVisibilityTimeout scoringUtils.scoringTaskTimeoutInSeconds, (err) ->
|
||||
cb err, message
|
||||
|
||||
parseTaskQueueMessage = (message, cb) ->
|
||||
try
|
||||
messageBody = message.getBody()
|
||||
unless typeof messageBody is 'object'
|
||||
messageBody = JSON.parse messageBody
|
||||
cb null, messageBody, message
|
||||
catch e
|
||||
cb "There was an error parsing the task. Error: #{e}"
|
||||
|
||||
constructTaskObject = (taskMessageBody, message, callback) ->
|
||||
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
|
||||
if err? then return callback err
|
||||
taskObject = messageGenerated: Date.now(), sessions: (scoringUtils.formatSessionInformation session for session in sessions)
|
||||
callback null, taskObject, message
|
||||
|
||||
getSessionInformation = (sessionIDString, callback) ->
|
||||
selectString = 'submitDate team submittedCode teamSpells levelID creator creatorName transpiledCode submittedCodeLanguage totalScore'
|
||||
LevelSession.findOne(_id: sessionIDString).select(selectString).lean().exec (err, session) ->
|
||||
if err? then return callback err, {'error': 'There was an error retrieving the session.'}
|
||||
callback null, session
|
||||
|
||||
constructTaskLogObject = (calculatorUserID, taskObject, message, callback) ->
|
||||
taskLogObject = new TaskLog
|
||||
createdAt: new Date()
|
||||
calculator: calculatorUserID
|
||||
sentDate: Date.now()
|
||||
messageIdentifierString: message.getReceiptHandle()
|
||||
taskLogObject.save (err) ->
|
||||
callback err, taskObject, taskLogObject, message
|
||||
|
||||
getUserIDFromRequest = (req) ->
|
||||
if req.user? then return req.user._id else return null
|
||||
|
||||
processTaskObject = (taskObject, taskLogObject, message, cb) ->
|
||||
taskObject.taskID = taskLogObject._id
|
||||
taskObject.receiptHandle = message.getReceiptHandle()
|
||||
cb null, taskObject
|
114
server/queues/scoring/getTwoGames.coffee
Normal file
114
server/queues/scoring/getTwoGames.coffee
Normal file
|
@ -0,0 +1,114 @@
|
|||
log = require 'winston'
|
||||
async = require 'async'
|
||||
errors = require '../../commons/errors'
|
||||
scoringUtils = require './scoringUtils'
|
||||
LevelSession = require '../../levels/sessions/LevelSession'
|
||||
|
||||
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
|
||||
getRandomSessions req.user, 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 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
|
||||
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
|
||||
|
||||
|
172
server/queues/scoring/processTaskResult.coffee
Normal file
172
server/queues/scoring/processTaskResult.coffee
Normal file
|
@ -0,0 +1,172 @@
|
|||
log = require 'winston'
|
||||
async = require 'async'
|
||||
errors = require '../../commons/errors'
|
||||
scoringUtils = require './scoringUtils'
|
||||
LevelSession = require '../../levels/sessions/LevelSession'
|
||||
TaskLog = require './ScoringTask'
|
||||
|
||||
module.exports = processTaskResult = (req, res) ->
|
||||
return if scoringUtils.simulatorIsTooOld req, res
|
||||
originalSessionID = req.body?.originalSessionID
|
||||
req.body?.simulator?.user = '' + req.user?._id
|
||||
yetiGuru = {}
|
||||
try
|
||||
async.waterfall [
|
||||
verifyClientResponse.bind(yetiGuru, req.body)
|
||||
fetchTaskLog.bind(yetiGuru)
|
||||
checkTaskLog.bind(yetiGuru)
|
||||
deleteQueueMessage.bind(yetiGuru)
|
||||
fetchLevelSession.bind(yetiGuru)
|
||||
checkSubmissionDate.bind(yetiGuru)
|
||||
logTaskComputation.bind(yetiGuru)
|
||||
scoringUtils.calculateSessionScores.bind(yetiGuru)
|
||||
scoringUtils.indexNewScoreArray.bind(yetiGuru)
|
||||
scoringUtils.addMatchToSessionsAndUpdate.bind(yetiGuru)
|
||||
scoringUtils.updateUserSimulationCounts.bind(yetiGuru, req.user?._id)
|
||||
determineIfSessionShouldContinueAndUpdateLog.bind(yetiGuru)
|
||||
findNearestBetterSessionID.bind(yetiGuru)
|
||||
addNewSessionsToQueue.bind(yetiGuru)
|
||||
], (err, results) ->
|
||||
if err is 'shouldn\'t continue'
|
||||
markSessionAsDoneRanking originalSessionID, (err) ->
|
||||
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 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 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!'
|
||||
|
||||
|
||||
verifyClientResponse = (responseObject, callback) ->
|
||||
# TODO: better verification
|
||||
if typeof responseObject isnt 'object' or responseObject?.originalSessionID?.length isnt 24
|
||||
callback 'The response to that query is required to be a JSON object.'
|
||||
else
|
||||
@clientResponseObject = responseObject
|
||||
callback null, responseObject
|
||||
|
||||
fetchTaskLog = (responseObject, callback) ->
|
||||
TaskLog.findOne(_id: responseObject.taskID).lean().exec (err, taskLog) =>
|
||||
return callback new Error("Couldn't find TaskLog for _id #{responseObject.taskID}!") unless taskLog
|
||||
@taskLog = taskLog
|
||||
callback err, taskLog
|
||||
|
||||
checkTaskLog = (taskLog, callback) ->
|
||||
if taskLog.calculationTimeMS then return callback 'That computational task has already been performed'
|
||||
if hasTaskTimedOut taskLog.sentDate then return callback 'The task has timed out'
|
||||
callback null
|
||||
|
||||
hasTaskTimedOut = (taskSentTimestamp) ->
|
||||
taskSentTimestamp + scoringUtils.scoringTaskTimeoutInSeconds * 1000 < Date.now()
|
||||
|
||||
deleteQueueMessage = (callback) ->
|
||||
scoringUtils.scoringTaskQueue.deleteMessage @clientResponseObject.receiptHandle, (err) ->
|
||||
callback err
|
||||
|
||||
fetchLevelSession = (callback) ->
|
||||
selectString = 'submitDate creator level standardDeviation meanStrength totalScore submittedCodeLanguage leagues'
|
||||
LevelSession.findOne(_id: @clientResponseObject.originalSessionID).select(selectString).lean().exec (err, session) =>
|
||||
@levelSession = session
|
||||
callback err
|
||||
|
||||
checkSubmissionDate = (callback) ->
|
||||
supposedSubmissionDate = new Date(@clientResponseObject.sessions[0].submitDate)
|
||||
if Number(supposedSubmissionDate) isnt Number(@levelSession.submitDate)
|
||||
callback 'The game has been resubmitted. Removing from queue...'
|
||||
else
|
||||
callback null
|
||||
|
||||
logTaskComputation = (callback) ->
|
||||
@taskLog.set('calculationTimeMS', @clientResponseObject.calculationTimeMS)
|
||||
@taskLog.set('sessions') # Huh?
|
||||
@taskLog.calculationTimeMS = @clientResponseObject.calculationTimeMS
|
||||
@taskLog.sessions = @clientResponseObject.sessions
|
||||
@taskLog.save (err, saved) ->
|
||||
callback err
|
||||
|
||||
determineIfSessionShouldContinueAndUpdateLog = (cb) ->
|
||||
sessionID = @clientResponseObject.originalSessionID
|
||||
sessionRank = parseInt @clientResponseObject.originalSessionRank
|
||||
update = '$inc': {}
|
||||
if sessionRank is 0
|
||||
update['$inc'] = {numberOfWinsAndTies: 1}
|
||||
else
|
||||
update['$inc'] = {numberOfLosses: 1}
|
||||
LevelSession.findOneAndUpdate {_id: sessionID}, update, {select: 'numberOfWinsAndTies numberOfLosses', lean: true}, (err, updatedSession) ->
|
||||
if err? then return cb err, updatedSession
|
||||
totalNumberOfGamesPlayed = updatedSession.numberOfWinsAndTies + updatedSession.numberOfLosses
|
||||
if totalNumberOfGamesPlayed < 10
|
||||
#console.log 'Number of games played is less than 10, continuing...'
|
||||
cb null
|
||||
else
|
||||
ratio = (updatedSession.numberOfLosses) / (totalNumberOfGamesPlayed)
|
||||
if ratio > 0.33
|
||||
cb 'shouldn\'t continue'
|
||||
#console.log "Ratio(#{ratio}) is bad, ending simulation"
|
||||
else
|
||||
#console.log "Ratio(#{ratio}) is good, so continuing simulations"
|
||||
cb null
|
||||
|
||||
findNearestBetterSessionID = (cb) ->
|
||||
try
|
||||
levelOriginalID = @levelSession.level.original
|
||||
levelMajorVersion = @levelSession.level.majorVersion
|
||||
sessionID = @clientResponseObject.originalSessionID
|
||||
sessionTotalScore = @newScoresObject[sessionID].totalScore
|
||||
opponentSessionID = _.pull(_.keys(@newScoresObject), sessionID)
|
||||
opponentSessionTotalScore = @newScoresObject[opponentSessionID].totalScore
|
||||
opposingTeam = scoringUtils.calculateOpposingTeam(@clientResponseObject.originalSessionTeam)
|
||||
catch e
|
||||
cb e
|
||||
|
||||
retrieveAllOpponentSessionIDs sessionID, (err, opponentSessionIDs) ->
|
||||
if err? then return cb err, null
|
||||
queryParameters =
|
||||
totalScore:
|
||||
$gt: opponentSessionTotalScore
|
||||
_id:
|
||||
$nin: opponentSessionIDs
|
||||
'level.original': levelOriginalID
|
||||
'level.majorVersion': levelMajorVersion
|
||||
submitted: true
|
||||
team: opposingTeam
|
||||
if opponentSessionTotalScore < 30
|
||||
# Don't play a ton of matches at low scores--skip some in proportion to how close to 30 we are.
|
||||
# TODO: this could be made a lot more flexible.
|
||||
queryParameters['totalScore']['$gt'] = opponentSessionTotalScore + 2 * (30 - opponentSessionTotalScore) / 20
|
||||
|
||||
limitNumber = 1
|
||||
sortParameters = totalScore: 1
|
||||
selectString = '_id totalScore'
|
||||
query = LevelSession.findOne(queryParameters)
|
||||
.sort(sortParameters)
|
||||
.limit(limitNumber)
|
||||
.select(selectString)
|
||||
.lean()
|
||||
#console.log "Finding session with score near #{opponentSessionTotalScore}"
|
||||
query.exec (err, session) ->
|
||||
if err? then return cb err, session
|
||||
unless session then return cb 'no session was found'
|
||||
#console.log "Found session with score #{session.totalScore}"
|
||||
cb err, session._id
|
||||
|
||||
retrieveAllOpponentSessionIDs = (sessionID, cb) ->
|
||||
selectString = 'matches.opponents.sessionID matches.date submitDate'
|
||||
LevelSession.findOne({_id: sessionID}).select(selectString).lean().exec (err, session) ->
|
||||
if err? then return cb err, null
|
||||
opponentSessionIDs = (match.opponents[0].sessionID for match in session.matches when match.date > session.submitDate)
|
||||
cb err, opponentSessionIDs
|
||||
|
||||
addNewSessionsToQueue = (sessionID, callback) ->
|
||||
sessions = [@clientResponseObject.originalSessionID, sessionID]
|
||||
scoringUtils.addPairwiseTaskToQueue sessions, callback
|
||||
|
||||
markSessionAsDoneRanking = (sessionID, cb) ->
|
||||
#console.log 'Marking session as done ranking...'
|
||||
LevelSession.update {_id: sessionID}, {isRanking: false}, cb
|
21
server/queues/scoring/recordTwoGames.coffee
Normal file
21
server/queues/scoring/recordTwoGames.coffee
Normal file
|
@ -0,0 +1,21 @@
|
|||
log = require 'winston'
|
||||
async = require 'async'
|
||||
errors = require '../../commons/errors'
|
||||
scoringUtils = require './scoringUtils'
|
||||
LevelSession = require '../../levels/sessions/LevelSession'
|
||||
|
||||
module.exports = recordTwoGames = (req, res) ->
|
||||
sessions = req.body.sessions
|
||||
#console.log 'Recording non-chained result of', sessions?[0]?.name, sessions[0]?.metrics?.rank, 'and', sessions?[1]?.name, sessions?[1]?.metrics?.rank
|
||||
return if scoringUtils.simulatorIsTooOld req, res
|
||||
req.body?.simulator?.user = '' + req.user?._id
|
||||
|
||||
yetiGuru = clientResponseObject: req.body, isRandomMatch: true
|
||||
async.waterfall [
|
||||
scoringUtils.calculateSessionScores.bind(yetiGuru) # Fetches a few small properties from both sessions, prepares @levelSessionUpdates with the score part
|
||||
scoringUtils.indexNewScoreArray.bind(yetiGuru) # Creates and returns @newScoresObject, no query
|
||||
scoringUtils.addMatchToSessionsAndUpdate.bind(yetiGuru) # Adds matches to the session updates and does the writes
|
||||
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 res, {message: 'The single game was submitted successfully!'}
|
236
server/queues/scoring/scoringUtils.coffee
Normal file
236
server/queues/scoring/scoringUtils.coffee
Normal file
|
@ -0,0 +1,236 @@
|
|||
log = require 'winston'
|
||||
async = require 'async'
|
||||
bayes = new (require 'bayesian-battle')()
|
||||
LevelSession = require '../../levels/sessions/LevelSession'
|
||||
User = require '../../users/User'
|
||||
|
||||
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) ->
|
||||
sessionID: session._id
|
||||
team: session.team ? 'No team'
|
||||
transpiledCode: session.transpiledCode
|
||||
submittedCodeLanguage: session.submittedCodeLanguage
|
||||
teamSpells: session.teamSpells ? {}
|
||||
levelID: session.levelID
|
||||
creatorName: session.creatorName
|
||||
creator: session.creator
|
||||
totalScore: session.totalScore
|
||||
|
||||
|
||||
module.exports.calculateSessionScores = (callback) ->
|
||||
sessionIDs = _.pluck @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'}
|
||||
try
|
||||
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject @clientResponseObject, oldScores
|
||||
newScoreArray = updatePlayerSkills oldScoreArray
|
||||
createSessionScoreUpdate.call @, scoreObject for scoreObject in newScoreArray
|
||||
callback err, newScoreArray
|
||||
catch e
|
||||
callback e
|
||||
|
||||
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
|
||||
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'
|
||||
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."
|
||||
@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) ->
|
||||
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 = _.pluck @clientResponseObject.sessions, 'sessionID'
|
||||
async.each sessionIDs, updateMatchesInSession.bind(@, matchObject), (err) ->
|
||||
callback err
|
||||
|
||||
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}
|
||||
|
||||
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
|
||||
|
||||
|
||||
module.exports.updateUserSimulationCounts = (reqUserID, callback) ->
|
||||
incrementUserSimulationCount reqUserID, 'simulatedBy', (err) =>
|
||||
if err? then return callback err
|
||||
#console.log 'Incremented user simulation count!'
|
||||
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, affected) ->
|
||||
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
|
|
@ -13,10 +13,6 @@ module.exports.setup = (app) ->
|
|||
handler = loadQueueHandler 'scoring'
|
||||
handler.messagesInQueueCount req, res
|
||||
|
||||
app.post '/queue/scoring/resimulateAllSessions', (req, res) ->
|
||||
handler = loadQueueHandler 'scoring'
|
||||
handler.resimulateAllSessions req, res
|
||||
|
||||
app.post '/queue/scoring/getTwoGames', (req, res) ->
|
||||
handler = loadQueueHandler 'scoring'
|
||||
handler.getTwoGames req, res
|
||||
|
|
|
@ -51,10 +51,8 @@ UserSchema.methods.isAnonymous = ->
|
|||
@get 'anonymous'
|
||||
|
||||
UserSchema.methods.getUserInfo = ->
|
||||
info =
|
||||
id : @get('_id')
|
||||
email : if @get('anonymous') then 'Unregistered User' else @get('email')
|
||||
return info
|
||||
id: @get('_id')
|
||||
email: if @get('anonymous') then 'Unregistered User' else @get('email')
|
||||
|
||||
UserSchema.methods.trackActivity = (activityName, increment) ->
|
||||
now = new Date()
|
||||
|
|
Loading…
Reference in a new issue