diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee index 287110f98..18985716b 100644 --- a/app/lib/LevelBus.coffee +++ b/app/lib/LevelBus.coffee @@ -113,7 +113,6 @@ module.exports = class LevelBus extends Bus @changedSessionProperties.teamSpells = true @session.set({'teamSpells': @teamSpellMap}) @saveSession() - console.log spellTeam, me.team, e.spell.spellKey if spellTeam is me.team @onSpellChanged e # Save the new spell to the session, too. diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index 8afc6577a..3d1aa8115 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -110,16 +110,19 @@ module.exports = class LevelLoader extends CocoClass @updateCompleted = true denormalizeSession: -> - return if @session.get 'levelName' + return if @sessionDenormalized patch = 'levelName': @level.get('name') 'levelID': @level.get('slug') or @level.id if me.id is @session.get 'creator' patch.creatorName = me.get('name') - - @session.set key, value for key, value of patch - tempSession = new LevelSession _id: @session.id - tempSession.save(patch, {patch: true}) + for key, value of patch + if @session.get(key) is value + delete patch[key] + unless _.isEmpty patch + @session.set key, value for key, value of patch + tempSession = new LevelSession _id: @session.id + tempSession.save(patch, {patch: true}) @sessionDenormalized = true # World init diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index cbaf95c4a..521e810c7 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -64,7 +64,7 @@ module.exports = class Simulator setupGoalManager: -> @god.goalManager = new GoalManager @world - @god.goalManager.goals = @fetchGoalsFromWorldNoteChain() + @god.goalManager.goals = @god.level.goals @god.goalManager.goalStates = @manuallyGenerateGoalStates() commenceSimulationAndSetupCallback: -> @@ -108,17 +108,22 @@ module.exports = class Simulator taskResults = taskID: @task.getTaskID() receiptHandle: @task.getReceiptHandle() + originalSessionID: @task.getFirstSessionID() + originalSessionRank: -1 calculationTime: 500 sessions: [] for session in @task.getSessions() + sessionResult = sessionID: session.sessionID submitDate: session.submitDate creator: session.creator metrics: rank: @calculateSessionRank session.sessionID, simulationResults.goalStates, @task.generateTeamToSessionMap() - + if session.sessionID is taskResults.originalSessionID + taskResults.originalSessionRank = sessionResult.metrics.rank + taskResults.originalSessionTeam = session.team taskResults.sessions.push sessionResult return taskResults @@ -137,8 +142,6 @@ module.exports = class Simulator else return 1 - fetchGoalsFromWorldNoteChain: -> return @god.goalManager.world.scripts[0].noteChain[0].goals.add - manuallyGenerateGoalStates: -> goalStates = "destroy-humans": @@ -190,7 +193,7 @@ module.exports = class Simulator spellKeyComponents[0] = _.string.slugify spellKeyComponents[0] spellKey = spellKeyComponents.join '/' spellKey - + createSpellAndAssignName: (spellKey, spellName) -> @spells[spellKey] ?= {} @@ -262,10 +265,10 @@ class SimulationTask fullSpellName = [thangName,spellName].join '/' if _.contains(teamSpells, fullSpellName) teamCode[fullSpellName]=spell - + _.merge spellKeyToSourceMap, teamCode commonSpells = session.teamSpells["common"] _.merge spellKeyToSourceMap, _.pick(session.code, commonSpells) if commonSpells? - + spellKeyToSourceMap diff --git a/app/lib/surface/PointChooser.coffee b/app/lib/surface/PointChooser.coffee index 470b10c5f..c55b1a134 100644 --- a/app/lib/surface/PointChooser.coffee +++ b/app/lib/surface/PointChooser.coffee @@ -5,6 +5,7 @@ module.exports = class PointChooser extends CocoClass super() @buildShape() @options.stage.addEventListener 'stagemousedown', @onMouseDown + @options.camera.dragDisabled = true destroy: -> @options.stage.removeEventListener 'stagemousedown', @onMouseDown diff --git a/app/lib/surface/RegionChooser.coffee b/app/lib/surface/RegionChooser.coffee index 14eb47035..d0dbdc82a 100644 --- a/app/lib/surface/RegionChooser.coffee +++ b/app/lib/surface/RegionChooser.coffee @@ -7,6 +7,7 @@ module.exports = class RegionChooser extends CocoClass @options.stage.addEventListener 'stagemousedown', @onMouseDown @options.stage.addEventListener 'stagemousemove', @onMouseMove @options.stage.addEventListener 'stagemouseup', @onMouseUp + @options.camera.dragDisabled = true destroy: -> @options.stage.removeEventListener 'stagemousedown', @onMouseDown diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee index b5adf175c..130d017b7 100644 --- a/app/lib/surface/SpriteBoss.coffee +++ b/app/lib/surface/SpriteBoss.coffee @@ -25,6 +25,7 @@ module.exports = class SpriteBoss extends CocoClass constructor: (@options) -> super() + @dragged = 0 @options ?= {} @camera = @options.camera @surfaceLayer = @options.surfaceLayer @@ -238,11 +239,12 @@ module.exports = class SpriteBoss extends CocoClass @selectThang e.thangID, e.spellName onCameraDragged: -> - @dragged = true + @dragged += 1 onSpriteMouseUp: (e) -> return if key.shift and @options.choosing - return @dragged = false if @dragged + return @dragged = 0 if @dragged > 3 + @dragged = 0 sprite = if e.sprite?.thang?.isSelectable then e.sprite else null @selectSprite e, sprite diff --git a/app/styles/play/level/modal/docs.sass b/app/styles/play/level/modal/docs.sass new file mode 100644 index 000000000..3c25f0389 --- /dev/null +++ b/app/styles/play/level/modal/docs.sass @@ -0,0 +1,2 @@ +#docs-modal .modal-dialog + width: 800px \ No newline at end of file diff --git a/app/templates/admin.jade b/app/templates/admin.jade index 43d6769c6..feb9d9347 100644 --- a/app/templates/admin.jade +++ b/app/templates/admin.jade @@ -2,6 +2,18 @@ extends /templates/base block content + h3 Espionage mode + h5 Please enter the email/username of the person you want to spy on + .form + .form-group + label.control-label Email + input#user-email + .form-group + label.control-label Username + input#user-username + + button.btn.btn-primary.btn-large#enter-espionage-mode 007 + h3(data-i18n="admin.av_title") Admin Views h4(data-i18n="admin.av_entities_sub_title") Entities diff --git a/app/templates/play/level/control_bar.jade b/app/templates/play/level/control_bar.jade index 59d378bc8..1cc3af07d 100644 --- a/app/templates/play/level/control_bar.jade +++ b/app/templates/play/level/control_bar.jade @@ -1,12 +1,12 @@ h4.home - a(href="/") + a(href=homeLink || "/") i.icon-home.icon-white span(data-i18n="play_level.home") Home h4.title #{worldName} -button.btn.btn-xs.btn-inverse.banner#docs-button(title="Show level instructions", data-i18n="play_level.guide") Guide +button.btn.btn-xs.btn-success.banner#docs-button(title="Show level instructions", data-i18n="play_level.guide") Guide if ladderGame button.btn.btn-xs.btn-inverse.banner#multiplayer-button(title="Leaderboard", data-i18n="play_level.leaderboard") Leaderboard diff --git a/app/treema-ext.coffee b/app/treema-ext.coffee index ec105749b..9112abaa6 100644 --- a/app/treema-ext.coffee +++ b/app/treema-ext.coffee @@ -38,12 +38,13 @@ class LiveEditingMarkup extends TreemaNode.nodeMap.ace url: InkBlob.url filename: InkBlob.filename mimetype: InkBlob.mimetype - description: '' - createdFor: [] + path: @settings.filePath + + @uploadingPath = [@settings.filePath, InkBlob.filename].join('/') $.ajax('/file', { type: 'POST', data: body, success: @onFileUploaded }) onFileUploaded: (e) => - @editor.insert "" + @editor.insert "" onEditorChange: => @saveChanges() diff --git a/app/views/admin_view.coffee b/app/views/admin_view.coffee index 941136574..e71c9acaf 100644 --- a/app/views/admin_view.coffee +++ b/app/views/admin_view.coffee @@ -1,6 +1,35 @@ +{backboneFailure, genericFailure} = require 'lib/errors' View = require 'views/kinds/RootView' template = require 'templates/admin' +storage = require 'lib/storage' module.exports = class AdminView extends View id: "admin-view" template: template + + events: + 'click #enter-espionage-mode': 'enterEspionageMode' + + enterEspionageMode: -> + userEmail = $("#user-email").val().toLowerCase() + username = $("#user-username").val().toLowerCase() + + userIdentifier = userEmail || username + postData = + usernameLower: username + emailLower: userEmail + + $.ajax + type: "POST", + url: "/auth/spy" + data: postData + success: @espionageSuccess + error: @espionageFailure + + espionageSuccess: (model) -> + storage.save('whoami',model) + window.location.reload() + espionageFailure: (jqxhr, status,error)-> + console.log "There was an error entering espionage mode: #{error}" + + \ No newline at end of file diff --git a/app/views/editor/article/edit.coffee b/app/views/editor/article/edit.coffee index f462afd40..05df5291d 100644 --- a/app/views/editor/article/edit.coffee +++ b/app/views/editor/article/edit.coffee @@ -35,6 +35,7 @@ module.exports = class ArticleEditView extends View data = $.extend(true, {}, @article.attributes) options = data: data + filePath: "db/thang.type/#{@article.get('original')}" schema: Article.schema.attributes callbacks: change: @pushChangesToPreview diff --git a/app/views/play/level/control_bar_view.coffee b/app/views/play/level/control_bar_view.coffee index 7c5b22d53..685452d81 100644 --- a/app/views/play/level/control_bar_view.coffee +++ b/app/views/play/level/control_bar_view.coffee @@ -47,12 +47,17 @@ module.exports = class ControlBarView extends View text += " (#{numPlayers})" if numPlayers > 1 $('#multiplayer-button', @$el).text(text) - getRenderData: (context={}) -> - super context - context.worldName = @worldName - context.multiplayerEnabled = @session.get('multiplayer') - context.ladderGame = @ladderGame - context + getRenderData: (c={}) -> + super c + c.worldName = @worldName + c.multiplayerEnabled = @session.get('multiplayer') + c.ladderGame = @ladderGame + c.homeLink = "/" + levelID = @level.get('slug') + if levelID in ["project-dota", "brawlwood", "ladder-tutorial"] + levelID = 'project-dota' if levelID is 'ladder-tutorial' + c.homeLink = "/play/ladder/" + levelID + c showGuideModal: -> options = {docs: @level.get('documentation'), supermodel: @supermodel} diff --git a/app/views/play/level/hud_view.coffee b/app/views/play/level/hud_view.coffee index 09059d20b..cc62fb047 100644 --- a/app/views/play/level/hud_view.coffee +++ b/app/views/play/level/hud_view.coffee @@ -270,7 +270,7 @@ module.exports = class HUDView extends View if prop is "rotation" return (val * 180 / Math.PI).toFixed(0) + "˚" if typeof val is 'number' - if Math.round(val) == val then return val.toFixed(0) # int + if Math.round(val) == val or prop is 'gold' then return val.toFixed(0) # int if -10 < val < 10 then return val.toFixed(2) if -100 < val < 100 then return val.toFixed(1) return val.toFixed(0) diff --git a/app/views/play/level/modal/docs_modal.coffee b/app/views/play/level/modal/docs_modal.coffee index fc975b84a..c7b77a287 100644 --- a/app/views/play/level/modal/docs_modal.coffee +++ b/app/views/play/level/modal/docs_modal.coffee @@ -6,6 +6,7 @@ Article = require 'models/Article' module.exports = class DocsModal extends View template: template + id: 'docs-modal' shortcuts: 'enter': 'hide' diff --git a/app/views/play/level/tome/spell_debug_view.coffee b/app/views/play/level/tome/spell_debug_view.coffee index f59b84501..5f4cfdd30 100644 --- a/app/views/play/level/tome/spell_debug_view.coffee +++ b/app/views/play/level/tome/spell_debug_view.coffee @@ -82,11 +82,20 @@ module.exports = class DebugView extends View else @$el.hide() if @variableChain?.length is 2 - Backbone.Mediator.publish 'tome:spell-debug-property-hovered', property: @variableChain[1], owner: @variableChain[0] + clearTimeout @hoveredPropertyTimeout if @hoveredPropertyTimeout + @hoveredPropertyTimeout = _.delay @notifyPropertyHovered, 500 else - Backbone.Mediator.publish 'tome:spell-debug-property-hovered', property: null + @notifyPropertyHovered() @updateMarker() + notifyPropertyHovered: => + clearTimeout @hoveredPropertyTimeout if @hoveredPropertyTimeout + @hoveredPropertyTimeout = null + oldHoveredProperty = @hoveredProperty + @hoveredProperty = if @variableChain?.length is 2 then owner: @variableChain[0], property: @variableChain[1] else {} + unless _.isEqual oldHoveredProperty, @hoveredProperty + Backbone.Mediator.publish 'tome:spell-debug-property-hovered', @hoveredProperty + updateMarker: -> if @marker @ace.getSession().removeMarker @marker diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index 060db8630..e49b2d2b9 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -141,17 +141,13 @@ module.exports = class PlayLevelView extends View otherSession = @levelLoader.opponentSession opponentCode = otherSession?.get('submittedCode') or {} - console.log "otherSession", otherSession, "opponentSpells", opponentSpells myCode = @session.get('code') or {} for spell in opponentSpells [thang, spell] = spell.split '/' c = opponentCode[thang]?[spell] - console.log "Got opponent code", c, "for", spell, "and had my code", myCode[spell] myCode[thang] ?= {} if c then myCode[thang][spell] = c else delete myCode[thang][spell] - console.log "Going to set session code from", _.cloneDeep(myCode) @session.set('code', myCode) - console.log "Just set session code to", _.cloneDeep(@session.get('code')) if @session.get('multiplayer') and otherSession? # For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet. @session.set 'multiplayer', false diff --git a/server/levels/sessions/level_session_schema.coffee b/server/levels/sessions/level_session_schema.coffee index da97b8ce5..290422c10 100644 --- a/server/levels/sessions/level_session_schema.coffee +++ b/server/levels/sessions/level_session_schema.coffee @@ -139,6 +139,13 @@ _.extend LevelSessionSchema.properties, submittedCode: type: 'object' + + numberOfWinsAndTies: + type: 'number' + default: 0 + numberOfLosses: + type: 'number' + default: 0 matches: type: 'array' diff --git a/server/queues/scoring.coffee b/server/queues/scoring.coffee index 4592ec74c..40a6b2f21 100644 --- a/server/queues/scoring.coffee +++ b/server/queues/scoring.coffee @@ -12,7 +12,7 @@ TaskLog = require './task/ScoringTask' bayes = new (require 'bayesian-battle')() scoringTaskQueue = undefined -scoringTaskTimeoutInSeconds = 120 +scoringTaskTimeoutInSeconds = 180 module.exports.setup = (app) -> connectToScoringQueue() @@ -24,24 +24,27 @@ connectToScoringQueue = -> scoringTaskQueue = data log.info "Connected to scoring task queue!" -module.exports.addPairwiseTaskToQueue = (req, res) -> +module.exports.addPairwiseTaskToQueueFromRequest = (req, res) -> taskPair = req.body.sessions - #unless isUserAdmin req then return errors.forbidden res, "You do not have the permissions to submit that game to the leaderboard" - #fetch both sessions + 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"} + + +addPairwiseTaskToQueue = (taskPair, cb) -> LevelSession.findOne(_id:taskPair[0]).lean().exec (err, firstSession) => - if err? then return errors.serverError res, "There was an error fetching the first session in the pair" + if err? then return cb err, false LevelSession.find(_id:taskPair[1]).exec (err, secondSession) => - if err? then return errors.serverError res, "There was an error fetching the second session" + if err? then return cb err, false try taskPairs = generateTaskPairs(secondSession, firstSession) catch e - if e then return errors.serverError res, "There was an error generating the task pairs" - - sendEachTaskPairToTheQueue taskPairs, (taskPairError) -> - if taskPairError? then return errors.serverError res, "There was an error sending the task pairs to the queue" + if e then return cb e, false - sendResponseObject req, res, {"message":"All task pairs were succesfully sent to the queue"} - + sendEachTaskPairToTheQueue taskPairs, (taskPairError) -> + if taskPairError? then return cb taskPairError,false + cb null, true + module.exports.createNewTask = (req, res) -> requestSessionID = req.body.session @@ -56,8 +59,8 @@ module.exports.createNewTask = (req, res) -> updateSessionToSubmit sessionToSubmit, (err, data) -> if err? then return errors.serverError res, "There was an error updating the session" - - fetchSessionsToRankAgainst (err, sessionsToRankAgainst) -> + opposingTeam = calculateOpposingTeam(sessionToSubmit.team) + fetchInitialSessionsToRankAgainst opposingTeam, (err, sessionsToRankAgainst) -> if err? then return errors.serverError res, "There was an error fetching the sessions to rank against" taskPairs = generateTaskPairs(sessionsToRankAgainst, sessionToSubmit) @@ -114,9 +117,102 @@ module.exports.processTaskResult = (req, res) -> addMatchToSessions clientResponseObject, newScoresObject, (err, data) -> if err? then return errors.serverError res, "There was an error updating the sessions with the match! #{JSON.stringify err}" - console.log "Sending response object" - sendResponseObject req, res, {"message":"The scores were updated successfully!"} + + originalSessionID = clientResponseObject.originalSessionID + originalSessionTeam = clientResponseObject.originalSessionTeam + originalSessionRank = parseInt clientResponseObject.originalSessionRank + + determineIfSessionShouldContinueAndUpdateLog originalSessionID, originalSessionRank, (err, sessionShouldContinue) -> + if err? then return errors.serverError res, "There was an error determining if the session should continue, #{err}" + + if sessionShouldContinue + opposingTeam = calculateOpposingTeam(originalSessionTeam) + opponentID = _.pull(_.keys(newScoresObject), originalSessionID) + sessionNewScore = newScoresObject[originalSessionID].totalScore + opponentNewScore = newScoresObject[opponentID].totalScore + findNearestBetterSessionID sessionNewScore, opponentNewScore, opponentID ,opposingTeam, (err, opponentSessionID) -> + if err? then return errors.serverError res, "There was an error finding the nearest sessionID!" + unless opponentSessionID then return sendResponseObject req, res, {"message":"There were no more games to rank(game is at top!"} + + addPairwiseTaskToQueue [originalSessionID, opponentSessionID], (err, success) -> + if err? then return errors.serverError res, "There was an error sending the pairwise tasks to the queue!" + sendResponseObject req, res, {"message":"The scores were updated successfully and more games were sent to the queue!"} + else + console.log "Player lost, achieved rank #{originalSessionRank}" + sendResponseObject req, res, {"message":"The scores were updated successfully, person lost so no more games are being inserted!"} + +determineIfSessionShouldContinueAndUpdateLog = (sessionID, sessionRank, cb) -> + 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'}, (err, updatedSession) -> + if err? then return cb err, updatedSession + updatedSession = updatedSession.toObject() + + totalNumberOfGamesPlayed = updatedSession.numberOfWinsAndTies + updatedSession.numberOfLosses + if totalNumberOfGamesPlayed < 5 + console.log "Number of games played is less than 5, continuing..." + cb null, true + else if totalNumberOfGamesPlayed > 15 + console.log "Too many games played, ending..." + cb null, false + else + ratio = (updatedSession.numberOfLosses - 5) / (totalNumberOfGamesPlayed) + if ratio > 0.66 + cb null, false + console.log "Ratio(#{ratio}) is bad, ending simulation" + else + console.log "Ratio(#{ratio}) is good, so continuing simulations" + cb null, true + + +findNearestBetterSessionID = (sessionTotalScore, opponentSessionTotalScore, opponentSessionID, opposingTeam, cb) -> + queryParameters = + totalScore: + $gt:opponentSessionTotalScore + 0.5 + _id: + $ne: opponentSessionID + levelID: "project-dota" + submitted: true + submittedCode: + $exists: true + team: opposingTeam + + 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 err, null + console.log "Found session with score #{session.totalScore}" + cb err, session._id + +calculateOpposingTeam = (sessionTeam) -> + teams = ['ogres','humans'] + opposingTeams = _.pull teams, sessionTeam + return opposingTeams[0] + + validatePermissions = (req, sessionID, callback) -> if isUserAnonymous req then return callback null, false if isUserAdmin req then return callback null, true @@ -177,15 +273,30 @@ updateSessionToSubmit = (sessionToUpdate, callback) -> meanStrength: 25 standardDeviation: 25/3 totalScore: 10 + numberOfWinsAndTies: 0 + numberOfLosses: 0 LevelSession.update {_id: sessionToUpdate._id}, sessionUpdateObject, callback -fetchSessionsToRankAgainst = (callback) -> - submittedSessionsQuery = +fetchInitialSessionsToRankAgainst = (opposingTeam, callback) -> + console.log "Fetching sessions to rank against for opposing team #{opposingTeam}" + findParameters = levelID: "project-dota" submitted: true submittedCode: $exists: true - LevelSession.find submittedSessionsQuery, callback + team: opposingTeam + + sortParameters = + totalScore: 1 + + limitNumber = 1 + + query = LevelSession.find(findParameters) + .sort(sortParameters) + .limit(limitNumber) + + + query.exec callback generateTaskPairs = (submittedSessions, sessionToScore) -> taskPairs = [] diff --git a/server/routes/auth.coffee b/server/routes/auth.coffee index e5d3f9367..2e6dbf72d 100644 --- a/server/routes/auth.coffee +++ b/server/routes/auth.coffee @@ -28,7 +28,30 @@ module.exports.setup = (app) -> return done(null, user) ) )) - + app.post '/auth/spy', (req, res, next) -> + if req?.user?.isAdmin() + + username = req.body.usernameLower + emailLower = req.body.emailLower + if emailLower + query = {"emailLower":emailLower} + else if username + query = {"nameLower":username} + else + return errors.badInput res, "You need to supply one of emailLower or username" + + User.findOne query, (err, user) -> + if err? then return errors.serverError res, "There was an error finding the specified user" + + unless user then return errors.badInput res, "The specified user couldn't be found" + + req.logIn user, (err) -> + if err? then return errors.serverError res, "There was an error logging in with the specified" + res.send(UserHandler.formatEntity(req, user)) + return res.end() + else + return errors.unauthorized res, "You must be an admin to enter espionage mode" + app.post('/auth/login', (req, res, next) -> authentication.authenticate('local', (err, user, info) -> return next(err) if err