From 2b2fea30924a56e2eda9eadb781de84f748e3435 Mon Sep 17 00:00:00 2001 From: Roy Xue <xljroy@gmail.com> Date: Mon, 5 May 2014 21:33:01 +0800 Subject: [PATCH 01/12] Update zh.coffee --- app/locale/zh.coffee | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/app/locale/zh.coffee b/app/locale/zh.coffee index b3c8ac1df..36f24fc34 100644 --- a/app/locale/zh.coffee +++ b/app/locale/zh.coffee @@ -17,7 +17,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra retry: "重试" # watch: "Watch" # unwatch: "Unwatch" -# submit_patch: "Submit Patch" + submit_patch: "提交补丁" units: second: "秒" @@ -36,7 +36,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra nav: play: "玩" -# community: "Community" + community: "社区" editor: "编辑" blog: "博客" forum: "论坛" @@ -53,7 +53,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra versions: save_version_title: "保存新版本" new_major_version: "最新主要版本" -# cla_prefix: "To save changes, first you must agree to our" + cla_prefix: "要保存更改, 首先你必须要统一我们的" # cla_url: "CLA" # cla_suffix: "." cla_agree: "我同意" @@ -61,7 +61,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra login: sign_up: "注册" log_in: "登录" -# logging_in: "Logging In" + logging_in: "登录中..." log_out: "登出" recover: "找回账户" @@ -78,7 +78,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra creating: "账户在创新中" sign_up: "注册" log_in: "以密码登录" -# social_signup: "Or, you can sign up through Facebook or G+:" + social_signup: "或者, 你可以通过Facebook 或者 G+ 注册:" home: slogan: "通过游戏学习Javascript脚本语言" @@ -131,20 +131,20 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra # subscribe_as_diplomat: "Subscribe as a Diplomat" # wizard_settings: -# title: "Wizard Settings" -# customize_avatar: "Customize Your Avatar" -# active: "Active" -# color: "Color" -# group: "Group" -# clothes: "Clothes" -# trim: "Trim" -# cloud: "Cloud" -# team: "Team" -# spell: "Spell" -# boots: "Boots" -# hue: "Hue" -# saturation: "Saturation" -# lightness: "Lightness" + title: "巫师设定" + customize_avatar: "设置你的头像" + active: "启用" + color: "颜色" + group: "类别" + clothes: "衣服" + trim: "条纹" + cloud: "云" + team: "队伍" + spell: "魔法球" + boots: "鞋子" + hue: "色彩" + saturation: "饱和度" + lightness: "亮度" # account_settings: # title: "Account Settings" From 93d117100ab36a92330ef7c5b9b6ad81e69a0bff Mon Sep 17 00:00:00 2001 From: gosnat <gosnat@gmail.com> Date: Mon, 5 May 2014 13:22:57 -0500 Subject: [PATCH 02/12] Update GoalManager.coffee For compound goals like GetAllToLocation, you should continue adding to the existing list of thang states. --- app/lib/world/GoalManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee index 073e87b05..cc31a61bf 100644 --- a/app/lib/world/GoalManager.coffee +++ b/app/lib/world/GoalManager.coffee @@ -204,7 +204,7 @@ module.exports = class GoalManager extends CocoClass arrays = (prop for prop in whos when prop?.length) return unless arrays.length - state[progressObjectName] = {} + state[progressObjectName] = state[progressObjectName] ? {} for array in arrays for thang in array if @thangTeams[thang]? From 939509849cf24fd06c957f168b85eff418e24757 Mon Sep 17 00:00:00 2001 From: gosnat <gosnat@gmail.com> Date: Mon, 5 May 2014 14:08:41 -0500 Subject: [PATCH 03/12] Update GoalManager.coffee Justification: For a negative goal like saveThangs, let's say you have 5 thangs on the save list. As things stand, if you don't have a HowMany defined, then the number of needed deaths to fail is numNeeded = _.size(stateThangs) - Math.min((goal.howMany ? 1), _.size stateThangs) + 1 numNeeded = 5 - Math.min(1, 5) + 1 numNeeded = 5 So you would only fail the goal if all 5 thangs you were supposed to save died. This is contrary to the comment right above this line: # saveThangs: by default we would want to save all the Thangs, which means that we would want none of them to be "done" Therefore, I think it should be Math.max rather than Math.min. numNeeded = _.size(stateThangs) - Math.max((goal.howMany ? 1), _.size stateThangs) + 1 numNeeded = 5 - Math.max(1, 5) + 1 numNeeded = 1 So any of the Thangs on the save list dying is enough to fail the goal. As a double check, what if the level designer designated a HowMany of 5? numNeeded = _.size(stateThangs) - Math.max((goal.howMany ? 1), _.size stateThangs) + 1 numNeeded = 5 - Math.max(5, 5) + 1 numNeeded = 1 So this is consistent. --- app/lib/world/GoalManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee index 073e87b05..7aa53c132 100644 --- a/app/lib/world/GoalManager.coffee +++ b/app/lib/world/GoalManager.coffee @@ -235,7 +235,7 @@ module.exports = class GoalManager extends CocoClass numNeeded = goal.howMany ? Math.max(1, _.size stateThangs) else # saveThangs: by default we would want to save all the Thangs, which means that we would want none of them to be "done" - numNeeded = _.size(stateThangs) - Math.min((goal.howMany ? 1), _.size stateThangs) + 1 + numNeeded = _.size(stateThangs) - Math.max((goal.howMany ? 1), _.size stateThangs) + 1 numDone = _.filter(stateThangs).length console.log "needed", numNeeded, "done", numDone, "of total", _.size(stateThangs), "with how many", goal.howMany, "and stateThangs", stateThangs return unless numDone >= numNeeded From 0e0ca785d9cece8aeb85398b6dbb841421115dcb Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Mon, 5 May 2014 16:59:12 -0700 Subject: [PATCH 04/12] Moved ready-to-rank logic to LevelSession where it can be shared across victory and multiplayer modals and My Matches tab. --- app/locale/en.coffee | 3 +- app/models/LevelSession.coffee | 12 ++++++ .../play/level/modal/multiplayer.jade | 7 ++-- app/views/play/ladder/my_matches_tab.coffee | 41 +++++-------------- app/views/play/level/control_bar_view.coffee | 1 - .../play/level/modal/multiplayer_modal.coffee | 27 +++++++++--- .../play/level/modal/victory_modal.coffee | 5 +-- 7 files changed, 51 insertions(+), 45 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 17c07b1cd..abf1a2fd1 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -246,6 +246,7 @@ multiplayer_hint_label: "Hint:" multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." multiplayer_coming_soon: "More multiplayer features to come!" + multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guide" tome_minion_spells: "Your Minions' Spells" tome_read_only_spells: "Read-Only Spells" @@ -710,4 +711,4 @@ user_names: "User Names" files: "Files" top_simulators: "Top Simulators" - source_document: "Source Document" \ No newline at end of file + source_document: "Source Document" diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee index fade84cd1..90ff84260 100644 --- a/app/models/LevelSession.coffee +++ b/app/models/LevelSession.coffee @@ -24,3 +24,15 @@ module.exports = class LevelSession extends CocoModel code = @get('code') parts = spellKey.split '/' code?[parts[0]]?[parts[1]] + + readyToRank: -> + return false unless @get('levelID') # If it hasn't been denormalized, then it's not ready. + return false unless c1 = @get('code') + return false unless team = @get('team') + return true unless c2 = @get('submittedCode') + thangSpellArr = (s.split("/") for s in @get('teamSpells')[team]) + for item in thangSpellArr + thang = item[0] + spell = item[1] + return true if c1[thang][spell] isnt c2[thang][spell] + false diff --git a/app/templates/play/level/modal/multiplayer.jade b/app/templates/play/level/modal/multiplayer.jade index 2f19a008b..5a3413e3c 100644 --- a/app/templates/play/level/modal/multiplayer.jade +++ b/app/templates/play/level/modal/multiplayer.jade @@ -27,10 +27,11 @@ block modal-body-content if ladderGame if me.get('anonymous') - p Sign in or create an account and get your solution on the leaderboard! + p(data-i18n="play_level.multiplayer_sign_in_leaderboard") Sign in or create an account and get your solution on the leaderboard. + else if readyToRank + button.btn.btn-success.rank-game-button(data-i18n="play_level.victory_rank_my_game") Rank My Game else - a#go-to-leaderboard-button.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches") Go to the leaderboard! - p You can submit your game to be ranked from the leaderboard page. + a.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches", data-i18n="play_level.victory_go_ladder") Return to Ladder block modal-footer-content a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="modal.close").btn.btn-primary Close diff --git a/app/views/play/ladder/my_matches_tab.coffee b/app/views/play/ladder/my_matches_tab.coffee index a725a1b16..0e9bd4cbb 100644 --- a/app/views/play/ladder/my_matches_tab.coffee +++ b/app/views/play/ladder/my_matches_tab.coffee @@ -72,7 +72,7 @@ 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] - team.readyToRank = @readyToRank(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.reverse() @@ -84,7 +84,7 @@ module.exports = class MyMatchesTabView extends CocoView if scoreHistory?.length > 1 team.scoreHistory = scoreHistory scoreHistory = _.last scoreHistory, 100 # Chart URL needs to be under 2048 characters for GET - + team.currentScore = Math.round scoreHistory[scoreHistory.length - 1][1] * 100 team.chartColor = team.primaryColor.replace '#', '' #times = (s[0] for s in scoreHistory) @@ -108,36 +108,35 @@ module.exports = class MyMatchesTabView extends CocoView sessionID = button.data('session-id') session = _.find @sessions.models, {id: sessionID} rankingState = 'unavailable' - if @readyToRank session + if session.readyToRank() rankingState = 'rank' else if session.get 'isRanking' rankingState = 'ranking' @setRankingButtonText button, rankingState - + @$el.find('.score-chart-wrapper').each (i, el) => scoreWrapper = $(el) team = _.find @teams, name: scoreWrapper.data('team-name') @generateScoreLineChart(scoreWrapper.attr('id'), team.scoreHistory, team.name) - generateScoreLineChart: (wrapperID, scoreHistory,teamName) => - margin = + margin = top: 20 right: 20 bottom: 30 left: 50 - + width = 450 - margin.left - margin.right height = 125 x = d3.time.scale().range([0,width]) y = d3.scale.linear().range([height,0]) - + xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(4).outerTickSize(0) yAxis = d3.svg.axis().scale(y).orient("left").ticks(4).outerTickSize(0) - + line = d3.svg.line().x(((d) -> x(d.date))).y((d) -> y(d.close)) selector = "#" + wrapperID - + svg = d3.select(selector).append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) @@ -150,12 +149,10 @@ module.exports = class MyMatchesTabView extends CocoView date: time close: d[1] * 100 } - + x.domain(d3.extent(data, (d) -> d.date)) y.domain(d3.extent(data, (d) -> d.close)) - - - + svg.append("g") .attr("class", "y axis") .call(yAxis) @@ -172,21 +169,6 @@ module.exports = class MyMatchesTabView extends CocoView .datum(data) .attr("class",lineClass) .attr("d",line) - - - - - readyToRank: (session) -> - return false unless session?.get('levelID') # If it hasn't been denormalized, then it's not ready. - return false unless c1 = session.get('code') - return false unless team = session.get('team') - return true unless c2 = session.get('submittedCode') - thangSpellArr = (s.split("/") for s in session.get('teamSpells')[team]) - for item in thangSpellArr - thang = item[0] - spell = item[1] - return true if c1[thang][spell] isnt c2[thang][spell] - return false rankSession: (e) -> button = $(e.target).closest('.rank-button') @@ -202,7 +184,6 @@ module.exports = class MyMatchesTabView extends CocoView @setRankingButtonText(button, 'failed') ajaxData = {session: sessionID, levelID: @level.id, originalLevelID: @level.attributes.original, levelMajorVersion: @level.attributes.version.major} - console.log "Posting game for ranking from My Matches view." $.ajax '/queue/scoring', { type: 'POST' data: ajaxData diff --git a/app/views/play/level/control_bar_view.coffee b/app/views/play/level/control_bar_view.coffee index a0a9407bb..f2502ba6f 100644 --- a/app/views/play/level/control_bar_view.coffee +++ b/app/views/play/level/control_bar_view.coffee @@ -56,7 +56,6 @@ module.exports = class ControlBarView extends View c.multiplayerEnabled = @session.get('multiplayer') c.ladderGame = @level.get('type') is 'ladder' c.spectateGame = @spectateGame - console.log "level type is", @level.get('type') if @level.get('type') in ['ladder', 'ladder-tutorial'] c.homeLink = '/play/ladder/' + @level.get('slug').replace /\-tutorial$/, '' else diff --git a/app/views/play/level/modal/multiplayer_modal.coffee b/app/views/play/level/modal/multiplayer_modal.coffee index 973bc8b47..2a34354d9 100644 --- a/app/views/play/level/modal/multiplayer_modal.coffee +++ b/app/views/play/level/modal/multiplayer_modal.coffee @@ -9,7 +9,7 @@ module.exports = class MultiplayerModal extends View events: 'click textarea': 'onClickLink' 'change #multiplayer': 'updateLinkSection' - + 'click .rank-game-button': 'onRankGame' constructor: (options) -> super(options) @@ -17,20 +17,20 @@ module.exports = class MultiplayerModal extends View @level = options.level @listenTo(@session, 'change:multiplayer', @updateLinkSection) @playableTeams = options.playableTeams - @ladderGame = options.ladderGame - console.log 'ladder game is', @ladderGame getRenderData: -> c = super() c.joinLink = (document.location.href.replace(/\?.*/, '').replace('#', '') + '?session=' + @session.id) - c.multiplayer = @session.get('multiplayer') + c.multiplayer = @session.get 'multiplayer' c.team = @session.get 'team' - c.levelSlug = @level?.get('slug') + c.levelSlug = @level?.get 'slug' c.playableTeams = @playableTeams - c.ladderGame = @ladderGame # For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet. + if @level?.get('type') is 'ladder' + c.ladderGame = true + c.readyToRank = @session?.readyToRank() c afterRender: -> @@ -50,5 +50,20 @@ module.exports = class MultiplayerModal extends View multiplayer = Boolean(@$el.find('#multiplayer').prop('checked')) @session.set('multiplayer', multiplayer) + onRankGame: (e) -> + button = @$el.find('.rank-game-button') + button.text($.i18n.t('play_level.victory_ranking_game', defaultValue: 'Submitting...')) + button.prop 'disabled', true + ajaxData = session: @session.id, levelID: @level.id, originalLevelID: @level.get('original'), levelMajorVersion: @level.get('version').major + ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches" + goToLadder = -> Backbone.Mediator.publish 'router:navigate', route: ladderURL + $.ajax '/queue/scoring', + type: 'POST' + data: ajaxData + success: goToLadder + failure: (response) -> + console.error "Couldn't submit game for ranking:", response + goToLadder() + destroy: -> super() diff --git a/app/views/play/level/modal/victory_modal.coffee b/app/views/play/level/modal/victory_modal.coffee index 3fe539b99..7ba691c8e 100644 --- a/app/views/play/level/modal/victory_modal.coffee +++ b/app/views/play/level/modal/victory_modal.coffee @@ -65,7 +65,6 @@ module.exports = class VictoryModal extends View ajaxData = session: @session.id, levelID: @level.id, originalLevelID: @level.get('original'), levelMajorVersion: @level.get('version').major ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches" goToLadder = -> Backbone.Mediator.publish 'router:navigate', route: ladderURL - console.log "Posting game for ranking from victory modal." $.ajax '/queue/scoring', type: 'POST' data: ajaxData @@ -82,9 +81,7 @@ module.exports = class VictoryModal extends View c.levelName = utils.i18n @level.attributes, 'name' c.level = @level if c.level.get('type') is 'ladder' - c1 = @session?.get('code') - c2 = @session?.get('submittedCode') - c.readyToRank = @session.get('levelID') and c1 and not _.isEqual(c1, c2) + c.readyToRank = @session.readyToRank() if me.get 'hourOfCode' # Show the Hour of Code "I'm Done" tracking pixel after they played for 30 minutes elapsed = (new Date() - new Date(me.get('dateCreated'))) From c9bb4887944e3bc8ec84dc55219def8869ea1baf Mon Sep 17 00:00:00 2001 From: Dominik Maier <domenukk@gmail.com> Date: Tue, 6 May 2014 02:37:14 +0200 Subject: [PATCH 05/12] Created headless-client and an alternative implementation of god --- .gitignore | 11 +- .npmignore | 11 +- app/lib/Buddha.coffee | 243 ++++++++++++ app/lib/God.coffee | 54 ++- app/lib/deltas.coffee | 28 +- app/lib/simulator/Simulator.coffee | 18 +- app/lib/world/world.coffee | 10 +- app/models/CocoModel.coffee | 12 +- app/views/play/level_view.coffee | 8 +- app/views/play/spectate_view.coffee | 6 +- headless_client.coffee | 574 ++++++++++++++++++++++++++++ headless_client/test.js | 87 +++++ headless_client/worker_world.coffee | 209 ++++++++++ package.json | 10 +- server/commons/database.coffee | 4 +- server/routes/mail.coffee | 2 +- 16 files changed, 1236 insertions(+), 51 deletions(-) create mode 100644 app/lib/Buddha.coffee create mode 100644 headless_client.coffee create mode 100644 headless_client/test.js create mode 100644 headless_client/worker_world.coffee diff --git a/.gitignore b/.gitignore index c3df101ed..44a78773d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ Thumbs.db *.sublime-project *.sublime-workspace +# IntelliJ/WebStorm +*.iml + # NPM packages folder. node_modules/ bower_components/ @@ -77,4 +80,10 @@ bin/mongo/ # windows /SCOCODE.bat -### If you add something here, copy it to the end of .npmignore, too. ### \ No newline at end of file +# local settings +login.coffee + +# debugging +*.heapsnapshot + +### If you add something here, copy it to the end of .npmignore, too. ### diff --git a/.npmignore b/.npmignore index ae193b37d..5d0542980 100644 --- a/.npmignore +++ b/.npmignore @@ -53,6 +53,9 @@ Thumbs.db *.sublime-project *.sublime-workspace +# IntelliJ/WebStorm +*.iml + # NPM packages folder. node_modules/ @@ -89,6 +92,12 @@ mongo/ bin/node/ bin/mongo/ - # Karma coverage coverage/ + +# local settings +login.coffee + +# debugging +*.heapsnapshot + diff --git a/app/lib/Buddha.coffee b/app/lib/Buddha.coffee new file mode 100644 index 000000000..15787c12d --- /dev/null +++ b/app/lib/Buddha.coffee @@ -0,0 +1,243 @@ +#Sane rewrite of God (a thread pool) +{now} = require 'lib/world/world_utils' +World = require 'lib/world/world' + +### + Every Angel has exactly one WebWorker attached to it. + It will call methods inside the webwrker and kill it if it times out. +### +class Angel + @cyanide: 0xDEADBEEF + + infiniteLoopIntervalDuration: 7500 # check this often (must be more than the others added) + infiniteLoopTimeoutDuration: 10000 # wait this long when we check + abortTimeoutDuration: 500 # give in-process or dying workers this long to give up + + constructor: (@id, @shared) -> + console.log @id + ": Creating Angel" + if (navigator.userAgent or navigator.vendor or window.opera).search("MSIE") isnt -1 + @infiniteLoopIntervalDuration *= 20 # since it's so slow to serialize without transferable objects, we can't trust it + @infiniteLoopTimeoutDuration *= 20 + @abortTimeoutDuration *= 10 + @initialized = false + @running = false + @hireWorker() + @shared.angels.push @ + + testWorker: => + if @initialized + @worker.postMessage {func: 'reportIn'} + # Are there any errors when webworker isn't loaded properly? + + onWorkerMessage: (event) => + #console.log JSON.stringify event + if @aborting and not + event.data.type is 'abort' + console.log id + " is currently aborting old work." + return + + switch event.data.type + when 'start-load-frames' + clearTimeout(@condemnTimeout) + @condemnTimeout = _.delay @infinitelyLooped, @infiniteLoopTimeoutDuration + when 'end-load-frames' + console.log @id + ': No condemn this time.' + clearTimeout(@condemnTimeout) + when 'worker-initialized' + unless @initialized + console.log @id + ": Worker initialized after", ((new Date()) - @worker.creationTime), "ms" + @initialized = true + @doWork() + when 'new-world' + @beholdWorld event.data.serialized, event.data.goalStates + when 'world-load-progress-changed' + Backbone.Mediator.publish 'god:world-load-progress-changed', event.data + when 'console-log' + console.log "|" + @id + "|", event.data.args... + when 'user-code-problem' + Backbone.Mediator.publish 'god:user-code-problem', problem: event.data.problem + when 'abort' + console.log @id, "aborted." + clearTimeout @abortTimeout + @aborting = false + @running = false + @shared.busyAngels.pop @ + @doWork() + when 'reportIn' + clearTimeout @condemnTimeout + else + console.log @id + " received unsupported message:", event.data + + beholdWorld: (serialized, goalStates) -> + return if @aborting + unless serialized + # We're only interested in goalStates. (Simulator) + @latestGoalStates = goalStates; + Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates) + @running = false + @shared.busyAngels.pop @ + + # console.warn "Goal states: " + JSON.stringify(goalStates) + + window.BOX2D_ENABLED = false # Flip this off so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment + World.deserialize serialized, @shared.worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld(goalStates) + window.BOX2D_ENABLED = true + @lastSerializedWorldFrames = serialized.frames + + finishBeholdingWorld: (goalStates) => (world) => + return if @aborting + world.findFirstChangedFrame @shared.world + @shared.world = world + errorCount = (t for t in @shared.world.thangs when t.errorsOut).length + Backbone.Mediator.publish('god:new-world-created', world: world, firstWorld: @shared.firstWorld, errorCount: errorCount, goalStates: goalStates) + for scriptNote in @shared.world.scriptNotes + Backbone.Mediator.publish scriptNote.channel, scriptNote.event + @shared.goalManager?.world = world + @running = false + @shared.busyAngels.pop @ + @shared.firstWorld = false; + @doWork() + + infinitelyLooped: => + unless @aborting + problem = type: "runtime", level: "error", id: "runtime_InfiniteLoop", message: "Code never finished. It's either really slow or has an infinite loop." + Backbone.Mediator.publish 'god:user-code-problem', problem: problem + Backbone.Mediator.publish 'god:infinite-loop', firstWorld: @shared.firstWorld + @fireWorker() + + workIfIdle: -> + @doWork() unless @running + + doWork: => + #console.log "work." + return if @aborted + console.log @id + " ready and looking for work. WorkQueue lenght is " + @shared.workQueue.length + if @initialized and @shared.workQueue.length + work = @shared.workQueue.pop() + if work is Angel.cyanide # Kill all other Angels, too + console.log @id + ": 'work is poison'" + @shared.workQueue.push Angel.cyanide + @free() + else + console.log @id + ": Sending the worker to work." + @running = true + @shared.busyAngels.push @ + + console.log "Running world..." + @worker.postMessage func: 'runWorld', args: work + console.log @id + ": Setting interval." + clearTimeout @purgatoryTimer + @purgatoryTimer = setInterval @testWorker, @infiniteLoopIntervalDuration + else + console.log "No work for " + @id + @hireWorker() + + abort: => + if @worker and @running + console.log "Aborting " + @id + @running = false + @shared.busyAngels.pop @ + @abortTimeout = _.delay @terminate, @fireWorker, @abortTimeoutDuration + @worker.postMessage func: 'abort' + @aborting = true + @work = null + + fireWorker: (rehire=true) => + @aborting = false + @running = false + @shared.busyAngels.pop @ + @worker?.removeEventListener 'message', @onWorkerMessage + @worker?.terminate() + @worker = null + clearTimeout @condemnTimeout + clearInterval @purgatoryTimer + console.log "Fired worker." + @initialized = false + @work = null + @hireWorker() if rehire + + hireWorker: -> + unless @worker + console.log @id + ": Hiring worker." + @worker = new Worker @shared.workerCode + @worker.addEventListener 'message', @onWorkerMessage + @worker.creationTime = new Date() + #@worker.postMessage func: 'initialized' else + + kill: -> + @fireWorker false + @shared.angels.pop @ + clearTimeout @condemnTimeout + clearTimeout @purgatoryTimer + @purgatoryTimer = null + @condemnTimeout = null + +module.exports = class God + ids: ['Athena', 'Baldr', 'Crom', 'Dagr', 'Eris', 'Freyja', 'Great Gish', 'Hades', 'Ishtar', 'Janus', 'Khronos', 'Loki', 'Marduk', 'Negafook', 'Odin', 'Poseidon', 'Quetzalcoatl', 'Ra', 'Shiva', 'Thor', 'Umvelinqangi', 'Týr', 'Vishnu', 'Wepwawet', 'Xipe Totec', 'Yahweh', 'Zeus', '上帝', 'Tiamat', '盘古', 'Phoebe', 'Artemis', 'Osiris', "嫦娥", 'Anhur', 'Teshub', 'Enlil', 'Perkele', 'Chaos', 'Hera', 'Iris', 'Theia', 'Uranus', 'Stribog', 'Sabazios', 'Izanagi', 'Ao', 'Tāwhirimātea', 'Tengri', 'Inmar', 'Torngarsuk', 'Centzonhuitznahua', 'Hunab Ku', 'Apollo', 'Helios', 'Thoth', 'Hyperion', 'Alectrona', 'Eos', 'Mitra', 'Saranyu', 'Freyr', 'Koyash', 'Atropos', 'Clotho', 'Lachesis', 'Tyche', 'Skuld', 'Urðr', 'Verðandi', 'Camaxtli', 'Huhetotl', 'Set', 'Anu', 'Allah', 'Anshar', 'Hermes', 'Lugh', 'Brigit', 'Manannan Mac Lir', 'Persephone', 'Mercury', 'Venus', 'Mars', 'Azrael', 'He-Man', 'Anansi', 'Issek', 'Mog', 'Kos', 'Amaterasu Omikami', 'Raijin', 'Susanowo', 'Blind Io', 'The Lady', 'Offler', 'Ptah', 'Anubis', 'Ereshkigal', 'Nergal', 'Thanatos', 'Macaria', 'Angelos', 'Erebus', 'Hecate', 'Hel', 'Orcus', 'Ishtar-Deela Nakh', 'Prometheus', 'Hephaestos', 'Sekhmet', 'Ares', 'Enyo', 'Otrera', 'Pele', 'Hadúr', 'Hachiman', 'Dayisun Tngri', 'Ullr', 'Lua', 'Minerva'] + nextID: -> + @lastID = (if @lastID? then @lastID + 1 else Math.floor(@ids.length * Math.random())) % @ids.length + @ids[@lastID] + + # Charlie's Angels are all given access to this. + angelsShare: { + workerCode: '/javascripts/workers/worker_world.js' # Either path or function + workQueue: [] + firstWorld: true + world: undefined + goalManager: undefined + worldClassMap: undefined + angels: [] + busyAngels: [] # Busy angels will automatically register here. + } + + constructor: (options) -> + options ?= {} + + @angelsShare.workerCode = options.workerCode if options.workerCode + + # ~20MB per idle worker + angel overhead - in this implementation, every Angel maps to 1 worker + angelCount = options.maxAngels ? options.maxWorkerPoolSize ? 2 # How many concurrent Angels/web workers to use at a time + + _.delay (=>new Angel @nextID(), @angelsShare), 250 * i for i in [0...angelCount] # Don't generate all Angels at once. + Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @ + + onTomeCast: (e) -> + @createWorld e.spells + + setGoalManager: (goalManager) => + @angelsShare.goalManager = goalManager + + setWorldClassMap: (worldClassMap) => + @angelsShare.worldClassMap = worldClassMap + + getUserCodeMap: (spells) -> + userCodeMap = {} + for spellKey, spell of spells + for thangID, spellThang of spell.thangs + (userCodeMap[thangID] ?= {})[spell.name] = spellThang.aether.serialize() + + #console.log userCodeMap + userCodeMap + + createWorld: (spells) => + angel.abort() for angel in @angelsShare.busyAngels # We really only ever want one world calculated per God + #console.log "Level: " + @level + @angelsShare.workQueue.push + worldName: @level.name + userCodeMap: @getUserCodeMap(spells) + level: @level + goals: @angelsShare.goalManager?.getGoals() + angel.workIfIdle() for angel in @angelsShare.angels + + destroy: => + console.log "Destroying Buddha" + @createWorld = -> console.log "CreateWorld already gone." + @angelsShare.workQueue.push Angel.cyanide + angel.kill for angel in @angelsShare.busyAngels + Backbone.Mediator.unsubscribe('tome:cast-spells', @onTomeCast, @) + @angelsShare.goalManager?.destroy() + @angelsShare.goalManager = null + @angelsShare = null + + #TODO: self.world.totalFrames?? + #TODO: Don't show arguments. diff --git a/app/lib/God.coffee b/app/lib/God.coffee index 6fe758391..3bca0b1d8 100644 --- a/app/lib/God.coffee +++ b/app/lib/God.coffee @@ -18,16 +18,18 @@ module.exports = class God options ?= {} @maxAngels = options.maxAngels ? 2 # How many concurrent web workers to use; if set past 8, make up more names @maxWorkerPoolSize = options.maxWorkerPoolSize ? 2 # ~20MB per idle worker + @workerCode = options.workerCode if options.workerCode? @angels = [] @firstWorld = true Backbone.Mediator.subscribe 'tome:cast-spells', @onTomeCast, @ @fillWorkerPool = _.throttle @fillWorkerPool, 3000, leading: false @fillWorkerPool() + workerCode: '/javascripts/workers/worker_world.js' #Can be a string or a function. + onTomeCast: (e) -> return if @dead - @spells = e.spells - @createWorld() + @createWorld e.spells fillWorkerPool: => return unless Worker and not @dead @@ -44,17 +46,21 @@ module.exports = class God @createWorker() createWorker: -> - worker = new Worker '/javascripts/workers/worker_world.js' + worker = new Worker @workerCode worker.creationTime = new Date() - worker.addEventListener 'message', @onWorkerMessage + worker.addEventListener 'message', @onWorkerMessage(worker) worker - onWorkerMessage: (event) => - worker = event.target - if event.data.type is 'worker-initialized' - #console.log @id, "worker initialized after", ((new Date()) - worker.creationTime), "ms (before it was needed)" - worker.initialized = true - worker.removeEventListener 'message', @onWorkerMessage + onWorkerMessage: (worker) => + unless worker.onMessage? + worker.onMessage = (event) => + if event.data.type is 'worker-initialized' + console.log @id, "worker initialized after", ((new Date()) - worker.creationTime), "ms (before it was needed)" + worker.initialized = true + worker.removeEventListener 'message', worker.onMessage + else + console.warn "Received strange word from God: #{event.data.type}" + worker.onMessage getAngel: -> freeAngel = null @@ -86,7 +92,7 @@ module.exports = class God #console.log "UserCodeProblem:", '"' + problem.message + '"', "for", problem.userInfo.thangID, "-", problem.userInfo.methodName, 'at line', problem.ranges?[0][0][0], 'column', problem.ranges?[0][0][1] Backbone.Mediator.publish 'god:user-code-problem', problem: problem - createWorld: -> + createWorld: (spells) -> #console.log @id + ': "Let there be light upon', @world.name + '!"' unless Worker? # profiling world simulation is easier on main thread, or we are IE9 setTimeout @simulateWorld, 1 @@ -101,20 +107,37 @@ module.exports = class God #console.log "going to run world with code", @getUserCodeMap() angel.worker.postMessage {func: 'runWorld', args: { worldName: @level.name - userCodeMap: @getUserCodeMap() + userCodeMap: @getUserCodeMap(spells) level: @level firstWorld: @firstWorld goals: @goalManager?.getGoals() }} + #Coffeescript needs getters and setters. + setGoalManager: (@goalManager) => + + setWorldClassMap: (@worldClassMap) => + beholdWorld: (angel, serialized, goalStates) -> + unless serialized + # We're only interested in goalStates. + @latestGoalStates = goalStates; + Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates, team: me.team) + unless _.find @angels, 'busy' + @spells = null # Don't hold onto old spells; memory leaks + return + + console.log "Beholding world." worldCreation = angel.started angel.free() return if @latestWorldCreation? and worldCreation < @latestWorldCreation @latestWorldCreation = worldCreation @latestGoalStates = goalStates + + console.warn "Goal states: " + JSON.stringify(goalStates) + window.BOX2D_ENABLED = false # Flip this off so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment - World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, worldCreation, @finishBeholdingWorld + World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld window.BOX2D_ENABLED = true @lastSerializedWorldFrames = serialized.frames @@ -171,7 +194,7 @@ module.exports = class God @latestGoalStates = @testGM?.getGoalStates() serialized = @testWorld.serialize().serializedWorld window.BOX2D_ENABLED = false - World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @t0, @finishBeholdingWorld + World.deserialize serialized, @worldClassMap, @lastSerializedWorldFrames, @finishBeholdingWorld window.BOX2D_ENABLED = true @lastSerializedWorldFrames = serialized.frames @@ -255,7 +278,7 @@ class Angel testWorker: => unless @worker.initialized - console.warn "Worker", @id, "hadn't even loaded the scripts yet after", @infiniteLoopIntervalDuration, "ms." + console.warn "Worker", @id, " hadn't even loaded the scripts yet after", @infiniteLoopIntervalDuration, "ms." return @worker.postMessage {func: 'reportIn'} @condemnTimeout = _.delay @condemnWorker, @infiniteLoopTimeoutDuration @@ -271,6 +294,7 @@ class Angel switch event.data.type when 'worker-initialized' console.log "Worker", @id, "initialized after", ((new Date()) - @worker.creationTime), "ms (we had been waiting for it)" + @worker.initialized = true when 'new-world' @god.beholdWorld @, event.data.serialized, event.data.goalStates when 'world-load-progress-changed' diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee index e817551d1..e97d659e9 100644 --- a/app/lib/deltas.coffee +++ b/app/lib/deltas.coffee @@ -103,10 +103,8 @@ module.exports.getConflicts = (headDeltas, pendingDeltas) -> pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas) paths = _.keys(headPathMap).concat(_.keys(pendingPathMap)) - # Here's my thinking: - # A) Conflicts happen when one delta path is a substring of another delta path - # B) A delta from one self-consistent group cannot conflict with another - # So, sort the paths, which will naturally make conflicts adjacent, + # Here's my thinking: conflicts happen when one delta path is a substring of another delta path + # So, sort paths from both deltas together, which will naturally make conflicts adjacent, # and if one is identified, one path is from the headDeltas, the other is from pendingDeltas # This is all to avoid an O(nm) brute force search. @@ -141,7 +139,27 @@ groupDeltasByAffectingPaths = (deltas) -> delta: delta path: (item.toString() for item in path).join('/') } - _.groupBy metaDeltas, 'path' + + map = _.groupBy metaDeltas, 'path' + + # Turns out there are cases where a single delta can include paths + # that 'conflict' with each other, ie one is a substring of the other + # because of moved indices. To handle this case, go through and prune + # out all deeper paths that conflict with more shallow paths, so + # getConflicts path checking works properly. + + paths = _.keys(map) + return map unless paths.length + paths.sort() + prunedMap = {} + previousPath = paths[0] + for path, i in paths + continue if i is 0 + continue if path.startsWith previousPath + prunedMap[path] = map[path] + previousPath = path + + prunedMap module.exports.pruneConflictsFromDelta = (delta, conflicts) -> # the jsondiffpatch delta mustn't include any dangling nodes, diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index 067317512..758d05028 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -2,7 +2,7 @@ SuperModel = require 'models/SuperModel' CocoClass = require 'lib/CocoClass' LevelLoader = require 'lib/LevelLoader' GoalManager = require 'lib/world/GoalManager' -God = require 'lib/God' +God = require 'lib/Buddha' module.exports = class Simulator extends CocoClass @@ -53,7 +53,8 @@ module.exports = class Simulator extends CocoClass return @supermodel ?= new SuperModel() - @god = new God maxWorkerPoolSize: 1, maxAngels: 1 # Start loading worker. + + @god = new God maxAngels: 2 # Start loading worker. @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true if @supermodel.finished() @@ -81,15 +82,18 @@ module.exports = class Simulator extends CocoClass setupGod: -> @god.level = @level.serialize @supermodel - @god.worldClassMap = @world.classMap + @god.setWorldClassMap = @world.classMap @setupGoalManager() @setupGodSpells() setupGoalManager: -> - @god.goalManager = new GoalManager @world, @level.get 'goals' + goalManager = new GoalManager @world + goalManager.goals = @god.level.goals + goalManager.goalStates = @manuallyGenerateGoalStates() + @god.setGoalManager goalManager commenceSimulationAndSetupCallback: -> - @god.createWorld() + @god.createWorld @generateSpellsObject() Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @ Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @ @@ -174,10 +178,6 @@ module.exports = class Simulator extends CocoClass else return 1 - setupGodSpells: -> - @generateSpellsObject() - @god.spells = @spells - generateSpellsObject: -> @currentUserCodeMap = @task.generateSpellKeyToSourceMap() @spells = {} diff --git a/app/lib/world/world.coffee b/app/lib/world/world.coffee index f209f82d3..d9ba75179 100644 --- a/app/lib/world/world.coffee +++ b/app/lib/world/world.coffee @@ -72,7 +72,7 @@ module.exports = class World (@runtimeErrors ?= []).push error (@unhandledRuntimeErrors ?= []).push error - loadFrames: (loadedCallback, errorCallback, loadProgressCallback) -> + loadFrames: (loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading) -> return if @aborted unless @thangs.length console.log "Warning: loadFrames called on empty World (no thangs)." @@ -96,7 +96,11 @@ module.exports = class World if t2 - @t0 > 1000 console.log(' Loaded', i, 'of', @totalFrames, "(+" + (t2 - @t0).toFixed(0) + "ms)") @t0 = t2 - setTimeout((=> @loadFrames(loadedCallback, errorCallback, loadProgressCallback)), 0) + continueFn = => @loadFrames(loadedCallback, errorCallback, loadProgressCallback, skipDeferredLoading) + if skipDeferredLoading + continueFn() + else + setTimeout(continueFn, 0) return @ended = true system.finish @thangs for system in @systems @@ -336,7 +340,7 @@ module.exports = class World console.log "Whoa, serializing a lot of WorldScriptNotes here:", o.scriptNotes.length {serializedWorld: o, transferableObjects: [o.storageBuffer]} - @deserialize: (o, classMap, oldSerializedWorldFrames, worldCreationTime, finishedWorldCallback) -> + @deserialize: (o, classMap, oldSerializedWorldFrames, finishedWorldCallback) -> # Code hotspot; optimize it #console.log "Deserializing", o, "length", JSON.stringify(o).length #console.log JSON.stringify(o) diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index a7d408adf..b81ac1571 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -1,6 +1,5 @@ storage = require 'lib/storage' deltasLib = require 'lib/deltas' -auth = require 'lib/auth' class CocoModel extends Backbone.Model idAttribute: "_id" @@ -9,6 +8,8 @@ class CocoModel extends Backbone.Model saveBackups: false @schema: null + getMe: -> @me or @me = require('lib/auth').me + initialize: -> super() if not @constructor.className @@ -83,7 +84,7 @@ class CocoModel extends Backbone.Model if @type() is 'ThangType' @_revertAttributes = _.clone @attributes # No deep clones for these! else - @_revertAttributes = $.extend(true, {}, @attributes) + @_revertAttributes = _.cloneDeep(@attributes) revert: -> @set(@_revertAttributes, {silent: true}) if @_revertAttributes @@ -96,7 +97,8 @@ class CocoModel extends Backbone.Model not _.isEqual @attributes, @_revertAttributes cloneNewMinorVersion: -> - newData = $.extend(null, {}, @attributes) + newData = _.clone @attributes # needs to be deep? + clone = new @constructor(newData) clone @@ -136,7 +138,7 @@ class CocoModel extends Backbone.Model hasReadAccess: (actor) -> # actor is a User object - actor ?= auth.me + actor ?= @getMe() return true if actor.isAdmin() if @get('permissions')? for permission in @get('permissions') @@ -148,7 +150,7 @@ class CocoModel extends Backbone.Model hasWriteAccess: (actor) -> # actor is a User object - actor ?= auth.me + actor ?= @getMe() return true if actor.isAdmin() if @get('permissions')? for permission in @get('permissions') diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index d1fef01f6..4545a15aa 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -12,7 +12,7 @@ Surface = require 'lib/surface/Surface' God = require 'lib/God' GoalManager = require 'lib/world/GoalManager' ScriptManager = require 'lib/scripts/ScriptManager' -LevelBus = require('lib/LevelBus') +LevelBus = require 'lib/LevelBus' LevelLoader = require 'lib/LevelLoader' LevelSession = require 'models/LevelSession' Level = require 'models/Level' @@ -112,8 +112,10 @@ module.exports = class PlayLevelView extends View load: -> @loadStartTime = new Date() - @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team") @god = new God() + @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team") + #@listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded) + #@listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged) getRenderData: -> c = super() @@ -174,7 +176,7 @@ module.exports = class PlayLevelView extends View @initSurface() @initGoalManager() @initScriptManager() - @insertSubviews() + @insertSubviews ladderGame: (@level.get('type') is "ladder") @initVolume() @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @originalSessionState = $.extend(true, {}, @session.get('state')) diff --git a/app/views/play/spectate_view.coffee b/app/views/play/spectate_view.coffee index 4a14f76c4..8c72f742b 100644 --- a/app/views/play/spectate_view.coffee +++ b/app/views/play/spectate_view.coffee @@ -8,7 +8,7 @@ World = require 'lib/world/world' # tools Surface = require 'lib/surface/Surface' -God = require 'lib/God' +God = require 'lib/Buddha' # 'lib/God' GoalManager = require 'lib/world/GoalManager' ScriptManager = require 'lib/scripts/ScriptManager' LevelLoader = require 'lib/LevelLoader' @@ -156,7 +156,7 @@ module.exports = class SpectateLevelView extends View team = @world.teamForPlayer(0) @loadOpponentTeam(team) @god.level = @level.serialize @supermodel - @god.worldClassMap = @world.classMap + @god.setWorldClassMap @world.classMap @setTeam team @initSurface() @initGoalManager() @@ -387,7 +387,7 @@ module.exports = class SpectateLevelView extends View initGoalManager: -> @goalManager = new GoalManager(@world, @level.get('goals')) - @god.goalManager = @goalManager + @god.setGoalManager @goalManager initScriptManager: -> if @world.scripts diff --git a/headless_client.coffee b/headless_client.coffee new file mode 100644 index 000000000..b446cf0ca --- /dev/null +++ b/headless_client.coffee @@ -0,0 +1,574 @@ +### +This file will simulate games on node.js by emulating the browser environment. +At some point, most of the code can be merged with Simulator.coffee +### + +# SETTINGS +debug = false # Enable logging of ajax calls mainly +testing = false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. +leaktest = false # Install callback that tries to find leaks automatically +exitOnLeak = false # Exit if leak is found. Only useful if leaktest is set to true, obviously. +heapdump = false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. + +server = if testing then "http://127.0.0.1:3000" else "http://codecombat.com" + +# Disabled modules +disable = [ + 'lib/AudioPlayer' + 'locale/locale' + '../locale/locale' +] + +bowerComponents = "./bower_components/" +headlessClient = "./headless_client/" + + +# Start of the actual code. Setting up the enivronment to match the environment of the browser +heapdump = require('heapdump') if heapdump + +# the path used for the loader. __dirname is module dependent. +path = __dirname + +m = require 'module' +request = require 'request' + +originalLoader = m._load + +unhook = () -> + m._load = originalLoader + +hook = () -> + m._load = hookedLoader + + +JASON = require 'jason' + +# Global emulated stuff +GLOBAL.window = GLOBAL +GLOBAL.Worker = require('webworker-threads').Worker +Worker::removeEventListener = (what) -> + if what is 'message' + @onmessage = -> #This webworker api has only one event listener at a time. + +GLOBAL.tv4 = require('tv4').tv4 + +GLOBAL.marked = setOptions: -> + +GLOBAL.navigator = +# userAgent: "nodejs" + platform: "headless_client" + vendor: "codecombat" + opera: false + +store = {} +GLOBAL.localStorage = + getItem: (key) => store[key] + setItem: (key, s) => store[key] = s + removeItem: (key) => delete store[key] + +# Hook node.js require. See https://github.com/mfncooper/mockery/blob/master/mockery.js +# The signature of this function *must* match that of Node's Module._load, +# since it will replace that. +# (Why is there no easier way?) +hookedLoader = (request, parent, isMain) -> + #if request is 'lib/god' + # console.log 'I choose you, SimpleGod.' + # request = './headless_client/SimpleGod' + #else + if request in disable or ~request.indexOf('templates') + console.log 'Ignored ' + request if debug + return class fake + else if '/' in request and not (request[0] is '.') or request is 'application' + request = path + '/app/' + request + else if request is 'underscore' + request = 'lodash' + + console.log "loading " + request if debug + originalLoader request, parent, isMain + + +#jQuery wrapped for compatibility purposes. Poorly. +GLOBAL.$ = GLOBAL.jQuery = (input) -> + console.log 'Ignored jQuery: ' + input if debug + append: (input)-> exports: ()-> + +cookies = request.jar() + +$.ajax = (options) -> + responded = false + url = options.url + if url.indexOf('http') + url = '/' + url unless url[0] is '/' + url = server + url + + data = options.data + + + #if (typeof data) is 'object' + #console.warn JSON.stringify data + #data = JSON.stringify data + + console.log "Requesting: " + JSON.stringify options if debug + console.log "URL: " + url if debug + request + url: url + jar: cookies + json: options.parse + method: options.type + body: data + , (error, response, body) -> + console.log "HTTP Request:" + JSON.stringify options if debug and not error + + if responded + console.log "\t↳Already returned before." if debug + return + + if (error) + console.warn "\t↳Returned: error: #{error}" + options.error(error) if options.error? + else + console.log "\t↳Returned: statusCode #{response.statusCode}: #{if options.parse then JSON.stringify body else body}" if debug + options.success(body, response, status: response.statusCode) if options.success? + + + statusCode = response.statusCode if response? + options.complete(status: statusCode) if options.complete? + responded = true + +$.extend = (deep, into, from) -> + copy = _.clone(from, deep); + if into + _.assign into, copy + copy = into + copy + +$.isArray = (object) -> + _.isArray object + +$.isPlainObject = (object) -> + _.isPlainObject object + + +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.string = _.str + _.mixin _.str.exports() + + +# load Backbone. Needs hooked loader to reroute underscore to lodash. +hook() +GLOBAL.Backbone = require bowerComponents + 'backbone/backbone' +unhook() +Backbone.$ = $ + +require bowerComponents + 'validated-backbone-mediator/backbone-mediator' +# Instead of mediator, dummy might be faster yet suffice? +#Mediator = class Mediator +# publish: (id, object) -> +# console.Log "Published #{id}: #{object}" +# @subscribe: () -> +# @unsubscribe: () -> + +GLOBAL.Aether = require 'aether' + +# Set up new loader. +hook() + +login = require './login.coffee' #should contain an object containing they keys 'username' and 'password' + + +#Login user and start the code. +$.ajax + url: '/auth/login' + type: "POST" + data: login + parse: true + error: (error) -> "Bad Error. Can't connect to server or something. " + error + success: (response) -> + console.log "User: " + response + GLOBAL.window.userObject = response # JSON.parse response + + User = require 'models/User' + + World = require 'lib/world/world' + LevelLoader = require 'lib/LevelLoader' + GoalManager = require 'lib/world/GoalManager' + + God = require 'lib/Buddha' + + workerCode = require headlessClient + 'worker_world' + + SuperModel = require 'models/SuperModel' + + log = require 'winston' + + CocoClass = require 'lib/CocoClass' + + class Simulator extends CocoClass + + constructor: -> + _.extend @, Backbone.Events + @trigger 'statusUpdate', 'Starting simulation!' + @retryDelayInSeconds = 10 + @taskURL = 'queue/scoring' + @simulatedByYou = 0 + + @god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: workerCode # Start loading worker. + + destroy: -> + @off() + @cleanupSimulation() + super() + + fetchAndSimulateTask: => + return if @destroyed + + if testing + test = require headlessClient + 'test.js' + console.log test + _.delay @setupSimulationAndLoadLevel, 0, test, "Testing...", status: 400 + return + + if @ranonce and heapdump + console.log "Writing snapshot." + heapdump.writeSnapshot() + @ranonce = true + + @trigger 'statusUpdate', 'Fetching simulation data!' + $.ajax + url: @taskURL + type: "GET" + parse: true + error: @handleFetchTaskError + success: @setupSimulationAndLoadLevel + + handleFetchTaskError: (errorData) => + console.error "There was a horrible Error: #{JSON.stringify errorData}" + @trigger 'statusUpdate', 'There was an error fetching games to simulate. Retrying in 10 seconds.' + @simulateAnotherTaskAfterDelay() + + handleNoGamesResponse: -> + console.log "Nothing to do." + @trigger 'statusUpdate', 'There were no games to simulate--nice. Retrying in 10 seconds.' + @simulateAnotherTaskAfterDelay() + + simulateAnotherTaskAfterDelay: => + console.log "Retrying..." + console.log "Retrying in #{@retryDelayInSeconds}" + retryDelayInMilliseconds = @retryDelayInSeconds * 1000 + _.delay @fetchAndSimulateTask, retryDelayInMilliseconds + + setupSimulationAndLoadLevel: (taskData, textStatus, jqXHR) => + return @handleNoGamesResponse() if jqXHR.status is 204 + @trigger 'statusUpdate', 'Setting up simulation!' + + @task = new SimulationTask(taskData) + try + levelID = @task.getLevelName() + catch err + console.error err + @trigger 'statusUpdate', "Error simulating game: #{err}. Trying another game in #{@retryDelayInSeconds} seconds." + @simulateAnotherTaskAfterDelay() + return + + @supermodel ?= new SuperModel() + + #console.log "Creating loader with levelID: " + levelID + " and SessionID: " + @task.getFirstSessionID() + " - task: " + JSON.stringify(@task) + + @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true + + console.log "Waiting for loaded game" + + @listenToOnce(@levelLoader, 'loaded-all', @simulateGame) + + simulateGame: -> + console.warn "Simulate game." + return if @destroyed + @trigger 'statusUpdate', 'All resources loaded, simulating!', @task.getSessions() + console.log "assignWorld" + @assignWorldAndLevelFromLevelLoaderAndDestroyIt() + console.log "SetupGod" + @setupGod() + try + @commenceSimulationAndSetupCallback() + catch err + console.log "There was an error in simulation(#{err}). Trying again in #{@retryDelayInSeconds} seconds" + + #TODO: Comment out. + throw err + + @simulateAnotherTaskAfterDelay() + + assignWorldAndLevelFromLevelLoaderAndDestroyIt: -> + console.log "Assigning world and level" + @world = @levelLoader.world + @level = @levelLoader.level + @levelLoader.destroy() + @levelLoader = null + + setupGod: -> + @god.level = @level.serialize @supermodel + @god.setWorldClassMap @world.classMap + @setupGoalManager() + + setupGoalManager: -> + goalManager = new GoalManager @world + goalManager.goals = @god.level.goals + goalManager.goalStates = @manuallyGenerateGoalStates() + @god.setGoalManager goalManager + + commenceSimulationAndSetupCallback: -> + console.log "Creating World." + @god.createWorld(@generateSpellsObject()) + Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @ + Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @ + + #Search for leaks + if leaktest and not @memwatch? + leakcount = 0 + maxleakcount = 0 + console.log "Setting leak callbacks." + @memwatch = require 'memwatch' + + @memwatch.on 'leak', (info) => + console.warn "LEAK!!\n" + JSON.stringify(info) + + unless @hd? + if (leakcount++ is maxleakcount) + @hd = new @memwatch.HeapDiff() + + @memwatch.on 'stats', (stats) => + console.warn "stats callback: " + stats + diff = @hd.end() + console.warn "HeapDiff:\n" + JSON.stringify(diff) + + if exitOnLeak + console.warn "Exiting because of Leak." + process.exit() + @hd = new @memwatch.HeapDiff() + + + + onInfiniteLoop: -> + console.warn "Skipping infinitely looping game." + @trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds." + _.delay @cleanupAndSimulateAnotherTask, @retryDelayInSeconds * 1000 + + processResults: (simulationResults) -> + + console.log "Processing Results" + + taskResults = @formTaskResultsObject simulationResults + console.warn taskResults + @sendResultsBackToServer taskResults + + sendResultsBackToServer: (results) => + @trigger 'statusUpdate', 'Simulation completed, sending results back to server!' + console.log "Sending result back to server" + + if testing + return @fetchAndSimulateTask() + + $.ajax + url: "queue/scoring" + data: results + parse: true + type: "PUT" + success: @handleTaskResultsTransferSuccess + error: @handleTaskResultsTransferError + complete: @cleanupAndSimulateAnotherTask + + handleTaskResultsTransferSuccess: (result) => + console.log "Task registration result: #{JSON.stringify result}" + @trigger 'statusUpdate', 'Results were successfully sent back to server!' + console.log "Simulated by you: " + @simulatedByYou + @simulatedByYou++ + + handleTaskResultsTransferError: (error) => + @trigger 'statusUpdate', 'There was an error sending the results back to the server.' + console.log "Task registration error: #{JSON.stringify error}" + + cleanupAndSimulateAnotherTask: => + #@cleanupSimulation() Not needed for Buddha. + @fetchAndSimulateTask() + + cleanupSimulation: -> + @god?.destroy() + @god = null + @world = null + @level = null + + formTaskResultsObject: (simulationResults) -> + 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 + + calculateSessionRank: (sessionID, goalStates, teamSessionMap) -> + humansDestroyed = goalStates["destroy-humans"].status is "success" + ogresDestroyed = goalStates["destroy-ogres"].status is "success" + if humansDestroyed is ogresDestroyed + return 0 + else if humansDestroyed and teamSessionMap["ogres"] is sessionID + return 0 + else if humansDestroyed and teamSessionMap["ogres"] isnt sessionID + return 1 + else if ogresDestroyed and teamSessionMap["humans"] is sessionID + return 0 + else + return 1 + + manuallyGenerateGoalStates: -> + goalStates = + "destroy-humans": + keyFrame: 0 + killed: + "Human Base": false + status: "incomplete" + "destroy-ogres": + keyFrame:0 + killed: + "Ogre Base": false + status: "incomplete" + + generateSpellsObject: -> + @currentUserCodeMap = @task.generateSpellKeyToSourceMap() + @spells = {} + for thang in @level.attributes.thangs + continue if @thangIsATemplate thang + @generateSpellKeyToSourceMapPropertiesFromThang thang + + thangIsATemplate: (thang) -> + for component in thang.components + continue unless @componentHasProgrammableMethods component + for methodName, method of component.config.programmableMethods + return true if @methodBelongsToTemplateThang method + + return false + + componentHasProgrammableMethods: (component) -> component.config? and _.has component.config, 'programmableMethods' + + methodBelongsToTemplateThang: (method) -> typeof method is 'string' + + generateSpellKeyToSourceMapPropertiesFromThang: (thang) => + for component in thang.components + continue unless @componentHasProgrammableMethods component + for methodName, method of component.config.programmableMethods + spellKey = @generateSpellKeyFromThangIDAndMethodName thang.id, methodName + + @createSpellAndAssignName spellKey, methodName + @createSpellThang thang, method, spellKey + @transpileSpell thang, spellKey, methodName + + generateSpellKeyFromThangIDAndMethodName: (thang, methodName) -> + spellKeyComponents = [thang, methodName] + spellKeyComponents[0] = _.string.slugify spellKeyComponents[0] + spellKey = spellKeyComponents.join '/' + spellKey + + + createSpellAndAssignName: (spellKey, spellName) -> + @spells[spellKey] ?= {} + @spells[spellKey].name = spellName + + createSpellThang: (thang, method, spellKey) -> + @spells[spellKey].thangs ?= {} + @spells[spellKey].thangs[thang.id] ?= {} + @spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method + + transpileSpell: (thang, spellKey, methodName) -> + slugifiedThangID = _.string.slugify thang.id + source = @currentUserCodeMap[[slugifiedThangID,methodName].join '/'] ? "" + aether = @spells[spellKey].thangs[thang.id].aether + try + aether.transpile source + catch e + console.log "Couldn't transpile #{spellKey}:\n#{source}\n", e + aether.transpile '' + + createAether: (methodName, method) -> + aetherOptions = + functionName: methodName + protectAPI: true + includeFlow: false + requiresThis: true + yieldConditionally: false + problems: + jshint_W040: {level: "ignore"} + jshint_W030: {level: "ignore"} # aether_NoEffect instead + aether_MissingThis: {level: 'error'} + if methodName is 'hear' + aetherOptions.functionParameters = ['speaker', 'message', 'data'] + #console.log "creating aether with options", aetherOptions + + return new Aether aetherOptions + + class SimulationTask + constructor: (@rawData) -> + #console.log 'Simulating sessions', (session for session in @getSessions()) + + getLevelName: -> + levelName = @rawData.sessions?[0]?.levelID + return levelName if levelName? + @throwMalformedTaskError "The level name couldn't be deduced from the task." + + generateTeamToSessionMap: -> + teamSessionMap = {} + for session in @rawData.sessions + @throwMalformedTaskError "Two players share the same team" if teamSessionMap[session.team]? + teamSessionMap[session.team] = session.sessionID + + teamSessionMap + + throwMalformedTaskError: (errorString) -> + throw new Error "The task was malformed, reason: #{errorString}" + + getFirstSessionID: -> @rawData.sessions[0].sessionID + + getTaskID: -> @rawData.taskID + + getReceiptHandle: -> @rawData.receiptHandle + + getSessions: -> @rawData.sessions + + generateSpellKeyToSourceMap: -> + spellKeyToSourceMap = {} + for session in @rawData.sessions + teamSpells = session.teamSpells[session.team] + teamCode = {} + for thangName, thangSpells of session.code + for spellName, spell of thangSpells + 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 + + sim = new Simulator() + + + sim.fetchAndSimulateTask() \ No newline at end of file diff --git a/headless_client/test.js b/headless_client/test.js new file mode 100644 index 000000000..fdd51bf83 --- /dev/null +++ b/headless_client/test.js @@ -0,0 +1,87 @@ +module.exports = { + "messageGenerated": 1396792689279, + "sessions": [ + { + "sessionID": "533a2c4893b95d9319a58049", + "submitDate": "2014-04-06T06:31:11.806Z", + "team": "humans", + "code": { + "ogre-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// Choose your hero! You can only build one hero.\nvar hero;\n//hero = 'ironjaw'; // A leaping juggernaut hero, type 'brawler'.\nhero = 'yugargen'; // A devious spellcaster hero, type 'shaman'.\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Munchkins are weak melee units with 1.25s build cooldown.\n// Throwers are fragile, deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['munchkin', 'thrower', 'munchkin', 'thrower', 'munchkin', 'thrower'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);\n//this.say(\"Move\", {to:{x:20, y:30}});{x: 68, y: 29}{x: 70, y: 30}" + }, + "programmable-shaman": { + "chooseAction": "if (this.hero !== undefined) {\n this.hero = this.getNearest(enemy);\n}\n// Shamans are spellcasters with a weak magic attack\n// and three spells: 'shrink', 'grow', and 'poison-cloud'.\n// Shrink: target has 2/3 health, 1.5x speed for 5s.\n// Grow: target has double health, half speed for 5s.\n// Once per match, she can cast poison cloud, which does\n// 5 poison dps for 10s to enemies in a 10m radius.\nvar right = 0;\nif(right === 0){this.move({x: 70, y: 40});\n}\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\nif(this.canCast('shrink', enemy)) \n{\n this.castShrink(enemy);\n}\nelse\n{\n this.castGrow(friend);\n}\n\nvar enemiesinpoisonrange = 0;\nfor (var i = 0; i < enemies.lenght; ++i) {\n var enemi = enemies[i];\n if (this.distance(enemi) <= 10) {\n enemiesinpoisonrange++;\n }\n}\nif (enemiesinpoisonrange >= 7) {\n this.castPoisonCloud(enemy);\n}\n//if (this.distance(ogrebase) > 10) {\n// this.move({x: 70, y: 30});\n//}\n//this.say(\"Defend!\", {targetPos: {x: 45, y: 30}});\n\n//this.say(\"Defend!\", {targetPos: {x: 35, y: 30}});\n\n//this.say(\"Defend!\", {targetPos: {x: 25, y: 30}});\n\n//this.say(\"Attack!\", {to:{x:20, y:30}});\n\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('shrink', enemy)) this.castShrink(enemy);\n//if(this.canCast('grow', friend)) this.castGrow(friend);\n//if(this.canCast('poison-cloud', enemy)) this.castPoisonCloud(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 45, y: 30}});\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});" + }, + "programmable-brawler": { + "chooseAction": "// The Brawler is a huge melee hero with mighty mass.\n// this.throw() hurls an enemy behind him.\n// this.jumpTo() leaps to a target within 20m every 10s.\n// this.stomp() knocks everyone away, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('jump')) this.jumpTo(enemy.pos);\n//if(!this.getCooldown('stomp') && this.distance(enemy) < 10) this.stomp();\n//if(!this.getCooldown('throw')) this.throw(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;{x: 68, y: 29}{x: 70, y: 30}" + }, + "programmable-librarian": { + "chooseAction": "var enemies = this.getEnemies();\nif (enemies.length === 0)\n return;\nvar enemy = this.getNearest(enemies);\nvar friends = this.getFriends();\nvar friend = this.getNearest(friends);\nvar archer = this.getFriends(type, \"archer\");\nvar soldier = this.getFriends(type, \"soldier\");\nvar hero = this.getFriends(type, \"hushbaum\");\nvar rand = Math.random();\nvar xmove;\nvar ymove;\nfor (var i = 0; i < enemies.length / 3; i += 1) {\n var e = enemies[i];\n var ehealth = Math.floor(e.health);\n if (this.canCast(\"haste\", friend)) {\n this.say(\"Godspeed \" + friend.id + \"!\");\n this.castHaste(friend);\n }\n if (this.canCast(\"haste\", this)) {\n this.say(\"I am Godspeed!\");\n this.castHaste(this);\n }\n if (this.canCast(\"slow\", e)) {\n this.say(\"Chill Out \" + e.id + \"!\");\n this.castSlow(e);\n }\n if (this.distance(e) < 45) {\n this.attack(e);\n this.say(\"Attacking \" + e.id + \" life is \" + ehealth + \".\");\n }\n if (this.health < this.maxHealth * 0.75) {\n if (this.pos.x > 20) {\n this.move({\n x: this.pos.x - 20,\n y: this.pos.y\n });\n } else {\n this.move({\n x: this.pos.x + 20,\n y: this.pos.y\n });\n }\n }\n if (this.canCast(\"regen\", this)) {\n this.castRegen(this);\n this.say(\"I won't die today bitch!\");\n }\n if (friend.health < friend.maxHealth * 0.5) {\n if (this.canCast(\"regen\", friend)) {\n this.say(\"You won't die today \" + friend.id + \".\");\n this.castRegen(friend);\n }\n }\n}\n;" + }, + "programmable-tharin": { + "chooseAction": "// Tharin is a melee fighter with shield, warcry, and terrify skills.\n// this.shield() lets him take one-third damage while defending.\n// this.warcry() gives allies within 10m 30% haste for 5s, every 10s.\n// this.terrify() sends foes within 30m fleeing for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('warcry')) this.warcry();\n//if(!this.getCooldown('terrify')) this.terrify();\n//this.shield();\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 40, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;" + }, + "human-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// CHOOSE YOUR HERO! You can only build one hero.\nvar hero;\n//hero = 'tharin'; // A fierce knight with battlecry abilities.\nhero = 'hushbaum'; // A fiery spellcaster hero.\n\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Soldiers are hard-to-kill, low damage melee units with 2s build cooldown.\n// Archers are fragile but deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['soldier', 'soldier', 'archer', 'archer', 'soldier', 'soldier'];\nvar type = buildOrder[this.built.length % buildOrder.length];\nthis.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);\n\n " + } + }, + "teamSpells": { + "ogres": [ + "programmable-brawler/chooseAction", + "programmable-shaman/chooseAction", + "ogre-base/chooseAction" + ], + "humans": [ + "programmable-librarian/chooseAction", + "programmable-tharin/chooseAction", + "human-base/chooseAction" + ] + }, + "levelID": "dungeon-arena", + "creator": "5338c38c4811eff221de2347", + "creatorName": "iC0DE" + }, + { + "sessionID": "532a777c2042708b711a6c29", + "submitDate": "2014-03-20T05:45:54.691Z", + "team": "ogres", + "code": { + "ogre-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// Choose your hero! You can only build one hero.\nvar hero;\n//hero = 'ironjaw'; // A leaping juggernaut hero, type 'brawler'.\nhero = 'yugargen'; // A devious spellcaster hero, type 'shaman'.\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Munchkins are weak melee units with 1.25s build cooldown.\n// Throwers are fragile, deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['munchkin', 'munchkin', 'munchkin', 'thrower'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);" + }, + "programmable-shaman": { + "chooseAction": "// Shamans are spellcasters with a weak magic attack\n// and three spells: 'shrink', 'grow', and 'poison-cloud'.\n// Shrink: target has 2/3 health, 1.5x speed for 5s.\n// Grow: target has double health, half speed for 5s.\n// Once per match, she can cast poison cloud, which does\n// 5 poison dps for 10s to enemies in a 10m radius.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) {\n return; // Chill if all enemies are dead.\n}\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\nif (enemies.length > 5) {\n if(this.canCast('poison-cloud', enemy)) {\n this.castPoisonCloud(enemy);\n return;\n }\n}\n\nif (friends.length > 4) {\n this.attack(enemy); \n}\nfor (var i = 0; i < friends.length; ++i) {\n if (friends[i].health < 0) {\n continue;\n }\n if(friends[i].type == \"thrower\" && this.canCast('shrink', friends[i])) {\n this.castShrink(friends[i]);\n return;\n } \n if(friends[i].type == \"munchkin\" && this.canCast('grow', friends[i])) {\n this.castGrow(friends[i]);\n return;\n } \n}\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('shrink', enemy)) this.castShrink(enemy);\n//if(this.canCast('grow', friend)) this.castGrow(friend);\n//if(this.canCast('poison-cloud', enemy)) this.castPoisonCloud(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});" + }, + "programmable-brawler": { + "chooseAction": "// The Brawler is a huge melee hero with mighty mass.\n// this.throw() hurls an enemy behind him.\n// this.jumpTo() leaps to a target within 20m every 10s.\n// this.stomp() knocks everyone away, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('jump')) this.jumpTo(enemy.pos);\n//if(!this.getCooldown('stomp') && this.distance(enemy) < 10) this.stomp();\n//if(!this.getCooldown('throw')) this.throw(enemy);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 60, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;" + }, + "human-base": { + "chooseAction": "// This is the code for your base. Decide which unit to build each frame.\n// Units you build will go into the this.built array.\n// Destroy the enemy base within 60 seconds!\n// Check out the Guide at the top for more info.\n\n// CHOOSE YOUR HERO! You can only build one hero.\nvar hero;\nhero = 'tharin'; // A fierce knight with battlecry abilities.\n//hero = 'hushbaum'; // A fiery spellcaster hero.\n\nif(hero && !this.builtHero) {\n this.builtHero = this.build(hero);\n return;\n}\n\n// Soldiers are hard-to-kill, low damage melee units with 2s build cooldown.\n// Archers are fragile but deadly ranged units with 2.5s build cooldown.\nvar buildOrder = ['archer', 'archer', 'soldier', 'archer', 'soldier'];\nvar type = buildOrder[this.built.length % buildOrder.length];\n//this.say('Unit #' + this.built.length + ' will be a ' + type);\nthis.build(type);" + }, + "programmable-tharin": { + "chooseAction": "this.findTypeInRange = function(units, type) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if (unit.type === type && this.distance(unit) < 20)\n return unit;\n }\n return null;\n};\n\nthis.findType = function(units, type) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if (unit.type === type)\n return unit;\n }\n return null;\n};\n\nthis.findHeroInRange = function(units, range) {\n for (var i = 0; i < units.length; ++i) {\n var unit = units[i];\n if ((unit.type === 'shaman' || unit.type === 'brawler') && this.distance(unit) < range)\n return unit;\n }\n return null;\n};\n\n// Tharin is a melee fighter with shield, warcry, and terrify skills.\n// this.shield() lets him take one-third damage while defending.\n// this.warcry() gives allies within 10m 30% haste for 5s, every 10s.\n// this.terrify() sends foes within 30m fleeing for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\n\n//Enemies\nvar enemyBase = this.findType(enemies, 'base');\nvar brawler = this.findTypeInRange(enemies, 'brawler');\nvar shaman = this.findTypeInRange(enemies, 'shaman');\n\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(!this.getCooldown('warcry')) this.warcry();\n//if(!this.getCooldown('terrify')) this.terrify();\n//this.shield();\n\nif((brawler || shaman) && !this.attackTime)\n{\n this.attackTime = true;\n if(brawler)\n this.say(\"Attack!\", {target: brawler});\n else if(shaman)\n this.say(\"Attack!\", {target: shaman});\n}\nelse if(this.health < 15 && this.getCooldown('terrify'))\n{\n this.terrify();\n}\nelse if(this.findHeroInRange(enemies, 30) && this.getCooldown('terrify'))\n{\n this.terrify();\n}\nelse if(this.health < 25)\n{\n this.shield();\n}\nelse if(brawler && this.distance(brawler) <=10)\n{\n this.attack(brawler);\n}\nelse\n{\n this.attack(enemy);\n}\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 40, y: 40});\n\n// You can store state on this across frames:\n//this.lastHealth = this.health;" + }, + "programmable-librarian": { + "chooseAction": "// The Librarian is a spellcaster with a fireball attack\n// plus three useful spells: 'slow', 'regen', and 'haste'.\n// Slow makes a target move and attack at half speed for 5s.\n// Regen makes a target heal 10 hp/s for 10s.\n// Haste speeds up a target by 4x for 5s, once per match.\n\nvar friends = this.getFriends();\nvar enemies = this.getEnemies();\nif (enemies.length === 0) return; // Chill if all enemies are dead.\nvar enemy = this.getNearest(enemies);\nvar friend = this.getNearest(friends);\n\n// Which one do you do at any given time? Only the last called action happens.\n//if(this.canCast('slow', enemy)) this.castSlow(enemy);\n//if(this.canCast('regen', friend)) this.castRegen(friend);\n//if(this.canCast('haste', friend)) this.castHaste(friend);\n//this.attack(enemy);\n\n// You can also command your troops with this.say():\n//this.say(\"Defend!\", {targetPos: {x: 30, y: 30}}));\n//this.say(\"Attack!\", {target: enemy});\n//this.say(\"Move!\", {targetPos: {x: 50, y: 40});" + } + }, + "teamSpells": { + "ogres": [ + "programmable-brawler/chooseAction", + "programmable-shaman/chooseAction", + "ogre-base/chooseAction" + ], + "humans": [ + "programmable-librarian/chooseAction", + "programmable-tharin/chooseAction", + "human-base/chooseAction" + ] + }, + "levelID": "dungeon-arena", + "creator": "53291a80b112e7240f324667", + "creatorName": "Imbal Oceanrage" + } +], + "taskID": "53415d71942d00aa43dbf3e9", + "receiptHandle": "cd50e44db7dbd4cc0bcce047aa822ba2fe3556cf" +} \ No newline at end of file diff --git a/headless_client/worker_world.coffee b/headless_client/worker_world.coffee new file mode 100644 index 000000000..c4ccf32d8 --- /dev/null +++ b/headless_client/worker_world.coffee @@ -0,0 +1,209 @@ +# function to use inside a webworker. +# This function needs to run inside an environment that has a 'self'. +# This specific worker is targeted towards the node.js headless_client environment. + +JASON = require 'jason' +fs = require 'fs' + +betterConsole = () -> + + self.logLimit = 200; + self.logsLogged = 0; + + self.transferableSupported = () -> true + + self.console = log: -> + if self.logsLogged++ is self.logLimit + self.postMessage + type: "console-log" + args: ["Log limit " + self.logLimit + " reached; shutting up."] + id: self.workerID + + else if self.logsLogged < self.logLimit + args = [].slice.call(arguments) + i = 0 + + while i < args.length + args[i] = args[i].toString() if args[i].constructor.className is "Thang" or args[i].isComponent if args[i] and args[i].constructor + ++i + try + self.postMessage + type: "console-log" + args: args + id: self.workerID + + catch error + self.postMessage + type: "console-log" + args: [ + "Could not post log: " + args + error.toString() + error.stack + error.stackTrace + ] + id: self.workerID + + # so that we don't crash when debugging statements happen + self.console.error = self.console.info = self.console.log + GLOBAL.console = console = self.console + self.console + + +work = () -> + console.log "starting..." + + console.log = -> + + World = self.require('lib/world/world'); + GoalManager = self.require('lib/world/GoalManager'); + + self.cleanUp = -> + self.world = null + self.goalManager = null + self.postedErrors = {} + self.t0 = null + self.logsLogged = 0 + + self.runWorld = (args) -> + console.log "Running world inside worker." + self.postedErrors = {} + self.t0 = new Date() + self.postedErrors = false + self.logsLogged = 0 + + try + self.world = new World(args.worldName, args.userCodeMap) + self.world.loadFromLevel args.level, true if args.level + self.goalManager = new GoalManager(self.world) + self.goalManager.setGoals args.goals + self.goalManager.setCode args.userCodeMap + self.goalManager.worldGenerationWillBegin() + self.world.setGoalManager self.goalManager + catch error + console.log "There has been an error inside the worker." + self.onWorldError error + return + Math.random = self.world.rand.randf # so user code is predictable + console.log "Loading frames." + + self.postMessage type: "start-load-frames" + + + self.world.loadFrames self.onWorldLoaded, self.onWorldError, self.onWorldLoadProgress, true + + + self.onWorldLoaded = onWorldLoaded = -> + self.postMessage type: "end-load-frames" + + self.goalManager.worldGenerationEnded() + t1 = new Date() + diff = t1 - self.t0 + transferableSupported = self.transferableSupported() + try + serialized = serializedWorld: undefined # self.world.serialize() + transferableSupported = false + catch error + console.log "World serialization error:", error.toString() + "\n" + error.stack or error.stackTrace + t2 = new Date() + + # console.log("About to transfer", serialized.serializedWorld.trackedPropertiesPerThangValues, serialized.transferableObjects); + try + if transferableSupported + self.postMessage + type: "new-world" + serialized: serialized.serializedWorld + goalStates: self.goalManager.getGoalStates() + , serialized.transferableObjects + else + self.postMessage + type: "new-world" + serialized: serialized.serializedWorld + goalStates: self.goalManager.getGoalStates() + + catch error + console.log "World delivery error:", error.toString() + "\n" + error.stack or error.stackTrace + t3 = new Date() + console.log "And it was so: (" + (diff / self.world.totalFrames).toFixed(3) + "ms per frame,", self.world.totalFrames, "frames)\nSimulation :", diff + "ms \nSerialization:", (t2 - t1) + "ms\nDelivery :", (t3 - t2) + "ms" + self.cleanUp() + + + self.onWorldError = onWorldError = (error) -> + self.postMessage type: "end-load-frames" + if error instanceof Aether.problems.UserCodeProblem + #console.log "Aether userCodeProblem occured." + unless self.postedErrors[error.key] + problem = error.serialize() + self.postMessage + type: "user-code-problem" + problem: problem + + self.postedErrors[error.key] = problem + else + console.log "Non-UserCodeError:", error.toString() + "\n" + error.stack or error.stackTrace + self.cleanUp() + + self.onWorldLoadProgress = onWorldLoadProgress = (progress) -> + #console.log "Worker onWorldLoadProgress" + self.postMessage + type: "world-load-progress-changed" + progress: progress + + self.abort = abort = -> + #console.log "Abort called for worker." + if self.world and self.world.name + #console.log "About to abort:", self.world.name, typeof self.world.abort + self.world.abort() if typeof self.world isnt "undefined" + self.world = null + self.postMessage type: "abort" + self.cleanUp() + + self.reportIn = reportIn = -> + console.log "Reporting in." + self.postMessage type: "reportIn" + + self.addEventListener "message", (event) -> + #console.log JSON.stringify event + self[event.data.func] event.data.args + + self.postMessage type: "worker-initialized" + +world = fs.readFileSync "./public/javascripts/world.js", 'utf8' + + +#window.BOX2D_ENABLED = true; + +newConsole = "newConsole = #{}JASON.stringify newConsole}()"; + +ret = """ + + GLOBAL = root = window = self; + GLOBAL.window = window; + + self.workerID = "Worker"; + + console = #{JASON.stringify betterConsole}(); + + try { + // the world javascript file + #{world}; + + // Don't let user generated code access stuff from our file system! + self.importScripts = importScripts = null; + self.native_fs_ = native_fs_ = null; + + // the actual function + #{JASON.stringify work}(); + }catch (error) { + self.postMessage({"type": "console-log", args: ["An unhandled error occured: ", error.toString(), error.stack], id: -1}); + } +""" + + +#console = #{JASON.stringify createConsole}(); +# +# console.error = console.info = console.log; +#self.console = console; +#GLOBAL.console = console; + + +module.exports = new Function(ret) diff --git a/package.json b/package.json index d4c3fb6c9..eccaacc3f 100644 --- a/package.json +++ b/package.json @@ -60,9 +60,13 @@ "gridfs-stream": "0.4.x", "stream-buffers": "0.2.x", "sendwithus": "2.0.x", - "aws-sdk":"~2.0.0", - "bayesian-battle":"0.0.x", - "redis": "" + "aws-sdk": "~2.0.0", + "bayesian-battle": "0.0.x", + "redis": "", + "webworker-threads": "~0.4.11", + "node-gyp": "~0.13.0", + "aether": "~0.1.18", + "JASON": "~0.1.3" }, "devDependencies": { "jade": "0.33.x", diff --git a/server/commons/database.coffee b/server/commons/database.coffee index 6356fe7dd..d664725c0 100644 --- a/server/commons/database.coffee +++ b/server/commons/database.coffee @@ -15,7 +15,7 @@ module.exports.connect = () -> module.exports.generateMongoConnectionString = -> - if config.mongo.mongoose_replica_string + if not testing and config.mongo.mongoose_replica_string address = config.mongo.mongoose_replica_string else dbName = config.mongo.db @@ -25,4 +25,4 @@ module.exports.generateMongoConnectionString = -> address = config.mongo.username + ":" + config.mongo.password + "@" + address address = "mongodb://#{address}/#{dbName}" - return address \ No newline at end of file + return address diff --git a/server/routes/mail.coffee b/server/routes/mail.coffee index 8b4f979fa..e7c2ee361 100644 --- a/server/routes/mail.coffee +++ b/server/routes/mail.coffee @@ -108,7 +108,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) -> context = email_id: sendwithus.templates.ladder_update_email recipient: - address: if DEBUGGING then 'nick@codecombat.com' else user.email + address: if DEBUGGING then 'nick@codecombat.com' else user.get('email') name: name email_data: name: name From 81634190ef909942a8bd8e135bb1bb05039d5533 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Mon, 5 May 2014 19:14:38 -0700 Subject: [PATCH 06/12] Added i18n for translating specific articles now, too. --- app/schemas/models/level.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 0daf000a2..444ac24c6 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -4,6 +4,7 @@ ThangComponentSchema = require './thang_component' SpecificArticleSchema = c.object() c.extendNamedProperties SpecificArticleSchema # name first SpecificArticleSchema.properties.body = { type: 'string', title: 'Content', description: "The body content of the article, in Markdown.", format: 'markdown' } +SpecificArticleSchema.properties.i18n = {type: "object", format: 'i18n', props: ['name', 'body'], description: "Help translate this article"} SpecificArticleSchema.displayProperty = 'name' side = {title: "Side", description: "A side.", type: 'string', 'enum': ['left', 'right', 'top', 'bottom']} From 6d244e85604ed4c993accfbf20457bef56008912 Mon Sep 17 00:00:00 2001 From: Dominik Maier <domenukk@gmail.com> Date: Tue, 6 May 2014 05:07:34 +0200 Subject: [PATCH 07/12] + Merged Simulators --- app/lib/Buddha.coffee | 4 +- app/lib/simulator/Simulator.coffee | 85 +++++-- app/models/CocoModel.coffee | 4 +- app/views/play/level_view.coffee | 8 +- headless_client.coffee | 393 ++--------------------------- 5 files changed, 89 insertions(+), 405 deletions(-) diff --git a/app/lib/Buddha.coffee b/app/lib/Buddha.coffee index 15787c12d..33af9d742 100644 --- a/app/lib/Buddha.coffee +++ b/app/lib/Buddha.coffee @@ -124,6 +124,7 @@ class Angel @shared.busyAngels.push @ console.log "Running world..." + #console.error "worker.postMessage: " + @worker.postMessage + ", work: " + work @worker.postMessage func: 'runWorld', args: work console.log @id + ": Setting interval." clearTimeout @purgatoryTimer @@ -238,6 +239,3 @@ module.exports = class God @angelsShare.goalManager?.destroy() @angelsShare.goalManager = null @angelsShare = null - - #TODO: self.world.totalFrames?? - #TODO: Don't show arguments. diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index 758d05028..7293c9f7f 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -6,11 +6,16 @@ God = require 'lib/Buddha' module.exports = class Simulator extends CocoClass - constructor: -> + constructor: (workerCode) -> _.extend @, Backbone.Events @trigger 'statusUpdate', 'Starting simulation!' @retryDelayInSeconds = 10 @taskURL = '/queue/scoring' + @simulatedByYou = 0 + if workerCode + @god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: workerCode # Start loading worker. + else + @god = new God maxWorkerPoolSize: 1, maxAngels: 1 destroy: -> @off() @@ -19,6 +24,17 @@ module.exports = class Simulator extends CocoClass fetchAndSimulateTask: => return if @destroyed + + if headless + if @dumpThisTime # The first heapdump would be useless to find leaks. + console.log "Writing snapshot." + heapdump.writeSnapshot() + @dumpThisTime = true if heapdump + + if testing + _.delay @setupSimulationAndLoadLevel, 0, testFile, "Testing...", status: 400 + return + @trigger 'statusUpdate', 'Fetching simulation data!' $.ajax url: @taskURL @@ -32,7 +48,9 @@ module.exports = class Simulator extends CocoClass @simulateAnotherTaskAfterDelay() handleNoGamesResponse: -> - @trigger 'statusUpdate', 'There were no games to simulate--all simulations are done or in process. Retrying in 10 seconds.' + info = 'There were no games to simulate--all simulations are done or in process. Retrying in 10 seconds.' + console.log info + @trigger 'statusUpdate', info @simulateAnotherTaskAfterDelay() simulateAnotherTaskAfterDelay: => @@ -54,8 +72,6 @@ module.exports = class Simulator extends CocoClass @supermodel ?= new SuperModel() - @god = new God maxAngels: 2 # Start loading worker. - @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true if @supermodel.finished() @simulateGame() @@ -64,7 +80,9 @@ module.exports = class Simulator extends CocoClass simulateGame: -> return if @destroyed - @trigger 'statusUpdate', 'All resources loaded, simulating!', @task.getSessions() + info = 'All resources loaded, simulating!' + console.log info + @trigger 'statusUpdate', info, @task.getSessions() @assignWorldAndLevelFromLevelLoaderAndDestroyIt() @setupGod() @@ -75,6 +93,7 @@ module.exports = class Simulator extends CocoClass @simulateAnotherTaskAfterDelay() assignWorldAndLevelFromLevelLoaderAndDestroyIt: -> + console.log "Assigning world and level" @world = @levelLoader.world @level = @levelLoader.level @levelLoader.destroy() @@ -82,21 +101,45 @@ module.exports = class Simulator extends CocoClass setupGod: -> @god.level = @level.serialize @supermodel - @god.setWorldClassMap = @world.classMap + @god.setWorldClassMap @world.classMap @setupGoalManager() @setupGodSpells() + setupGoalManager: -> - goalManager = new GoalManager @world - goalManager.goals = @god.level.goals - goalManager.goalStates = @manuallyGenerateGoalStates() - @god.setGoalManager goalManager + @god.setGoalManager new GoalManager(@world, @level.get 'goals') + commenceSimulationAndSetupCallback: -> @god.createWorld @generateSpellsObject() Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @ Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @ + #Search for leaks, headless-client only. + if headless and leaktest and not @memwatch? + leakcount = 0 + maxleakcount = 0 + console.log "Setting leak callbacks." + @memwatch = require 'memwatch' + + @memwatch.on 'leak', (info) => + console.warn "LEAK!!\n" + JSON.stringify(info) + + unless @hd? + if (leakcount++ is maxleakcount) + @hd = new @memwatch.HeapDiff() + + @memwatch.on 'stats', (stats) => + console.warn "stats callback: " + stats + diff = @hd.end() + console.warn "HeapDiff:\n" + JSON.stringify(diff) + + if exitOnLeak + console.warn "Exiting because of Leak." + process.exit() + @hd = new @memwatch.HeapDiff() + + onInfiniteLoop: -> console.warn "Skipping infinitely looping game." @trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds." @@ -110,6 +153,9 @@ module.exports = class Simulator extends CocoClass @trigger 'statusUpdate', 'Simulation completed, sending results back to server!' console.log "Sending result back to server!" + if headless and testing + return @fetchAndSimulateTask() + $.ajax url: "/queue/scoring" data: results @@ -121,23 +167,19 @@ module.exports = class Simulator extends CocoClass handleTaskResultsTransferSuccess: (result) => console.log "Task registration result: #{JSON.stringify result}" @trigger 'statusUpdate', 'Results were successfully sent back to server!' - simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1 - $('#simulated-by-you').text(simulatedBy) + console.log "Simulated by you: " + @simulatedByYou + @simulatedByYou++ + if not headless + simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1 + $('#simulated-by-you').text(simulatedBy) handleTaskResultsTransferError: (error) => @trigger 'statusUpdate', 'There was an error sending the results back to the server.' console.log "Task registration error: #{JSON.stringify error}" cleanupAndSimulateAnotherTask: => - @cleanupSimulation() @fetchAndSimulateTask() - cleanupSimulation: -> - @god?.destroy() - @god = null - @world = null - @level = null - formTaskResultsObject: (simulationResults) -> taskResults = taskID: @task.getTaskID() @@ -148,7 +190,6 @@ module.exports = class Simulator extends CocoClass sessions: [] for session in @task.getSessions() - sessionResult = sessionID: session.sessionID submitDate: session.submitDate @@ -178,6 +219,10 @@ module.exports = class Simulator extends CocoClass else return 1 + setupGodSpells: -> + @generateSpellsObject() + @god.spells = @spells + generateSpellsObject: -> @currentUserCodeMap = @task.generateSpellKeyToSourceMap() @spells = {} diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index b81ac1571..2413ebe3d 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -84,7 +84,7 @@ class CocoModel extends Backbone.Model if @type() is 'ThangType' @_revertAttributes = _.clone @attributes # No deep clones for these! else - @_revertAttributes = _.cloneDeep(@attributes) + @_revertAttributes = $.extend(true, {}, @attributes) revert: -> @set(@_revertAttributes, {silent: true}) if @_revertAttributes @@ -97,7 +97,7 @@ class CocoModel extends Backbone.Model not _.isEqual @attributes, @_revertAttributes cloneNewMinorVersion: -> - newData = _.clone @attributes # needs to be deep? + newData = _.clone @attributes clone = new @constructor(newData) clone diff --git a/app/views/play/level_view.coffee b/app/views/play/level_view.coffee index 4545a15aa..df66d44f6 100644 --- a/app/views/play/level_view.coffee +++ b/app/views/play/level_view.coffee @@ -114,8 +114,6 @@ module.exports = class PlayLevelView extends View @loadStartTime = new Date() @god = new God() @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @getQueryVariable('opponent'), team: @getQueryVariable("team") - #@listenToOnce(@levelLoader, 'loaded-all', @onLevelLoaderLoaded) - #@listenTo(@levelLoader, 'progress', @onLevelLoaderProgressChanged) getRenderData: -> c = super() @@ -171,12 +169,12 @@ module.exports = class PlayLevelView extends View team = @getQueryVariable("team") ? @world.teamForPlayer(0) @loadOpponentTeam(team) @god.level = @level.serialize @supermodel - @god.worldClassMap = @world.classMap + @god.setWorldClassMap @world.classMap @setTeam team @initSurface() @initGoalManager() @initScriptManager() - @insertSubviews ladderGame: (@level.get('type') is "ladder") + @insertSubviews() @initVolume() @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged) @originalSessionState = $.extend(true, {}, @session.get('state')) @@ -429,7 +427,7 @@ module.exports = class PlayLevelView extends View initGoalManager: -> @goalManager = new GoalManager(@world, @level.get('goals')) - @god.goalManager = @goalManager + @god.setGoalManager @goalManager initScriptManager: -> @scriptManager = new ScriptManager({scripts: @world.scripts or [], view:@, session: @session}) diff --git a/headless_client.coffee b/headless_client.coffee index b446cf0ca..686851893 100644 --- a/headless_client.coffee +++ b/headless_client.coffee @@ -4,11 +4,11 @@ At some point, most of the code can be merged with Simulator.coffee ### # SETTINGS -debug = false # Enable logging of ajax calls mainly -testing = false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. -leaktest = false # Install callback that tries to find leaks automatically -exitOnLeak = false # Exit if leak is found. Only useful if leaktest is set to true, obviously. -heapdump = false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. +GLOBAL.debug = false # Enable logging of ajax calls mainly +GLOBAL.testing = false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. +GLOBAL.leaktest = false # Install callback that tries to find leaks automatically +GLOBAL.exitOnLeak = false # Exit if leak is found. Only useful if leaktest is set to true, obviously. +GLOBAL.heapdump = false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. server = if testing then "http://127.0.0.1:3000" else "http://codecombat.com" @@ -19,8 +19,8 @@ disable = [ '../locale/locale' ] -bowerComponents = "./bower_components/" -headlessClient = "./headless_client/" +GLOBAL.bowerComponents = "./bower_components/" +GLOBAL.headlessClient = "./headless_client/" # Start of the actual code. Setting up the enivronment to match the environment of the browser @@ -45,6 +45,10 @@ JASON = require 'jason' # Global emulated stuff GLOBAL.window = GLOBAL +GLOBAL.headless = true +GLOBAL.document = location: pathname: "headless_client" +GLOBAL.console.debug = console.log + GLOBAL.Worker = require('webworker-threads').Worker Worker::removeEventListener = (what) -> if what is 'message' @@ -71,10 +75,9 @@ GLOBAL.localStorage = # since it will replace that. # (Why is there no easier way?) hookedLoader = (request, parent, isMain) -> - #if request is 'lib/god' - # console.log 'I choose you, SimpleGod.' - # request = './headless_client/SimpleGod' - #else + if request == 'lib/God' + request = 'lib/Buddha' + if request in disable or ~request.indexOf('templates') console.log 'Ignored ' + request if debug return class fake @@ -195,7 +198,7 @@ $.ajax LevelLoader = require 'lib/LevelLoader' GoalManager = require 'lib/world/GoalManager' - God = require 'lib/Buddha' + workerCode = require headlessClient + 'worker_world' @@ -205,370 +208,10 @@ $.ajax CocoClass = require 'lib/CocoClass' - class Simulator extends CocoClass + Simulator = require 'lib/simulator/Simulator' - constructor: -> - _.extend @, Backbone.Events - @trigger 'statusUpdate', 'Starting simulation!' - @retryDelayInSeconds = 10 - @taskURL = 'queue/scoring' - @simulatedByYou = 0 - - @god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: workerCode # Start loading worker. - - destroy: -> - @off() - @cleanupSimulation() - super() - - fetchAndSimulateTask: => - return if @destroyed - - if testing - test = require headlessClient + 'test.js' - console.log test - _.delay @setupSimulationAndLoadLevel, 0, test, "Testing...", status: 400 - return - - if @ranonce and heapdump - console.log "Writing snapshot." - heapdump.writeSnapshot() - @ranonce = true - - @trigger 'statusUpdate', 'Fetching simulation data!' - $.ajax - url: @taskURL - type: "GET" - parse: true - error: @handleFetchTaskError - success: @setupSimulationAndLoadLevel - - handleFetchTaskError: (errorData) => - console.error "There was a horrible Error: #{JSON.stringify errorData}" - @trigger 'statusUpdate', 'There was an error fetching games to simulate. Retrying in 10 seconds.' - @simulateAnotherTaskAfterDelay() - - handleNoGamesResponse: -> - console.log "Nothing to do." - @trigger 'statusUpdate', 'There were no games to simulate--nice. Retrying in 10 seconds.' - @simulateAnotherTaskAfterDelay() - - simulateAnotherTaskAfterDelay: => - console.log "Retrying..." - console.log "Retrying in #{@retryDelayInSeconds}" - retryDelayInMilliseconds = @retryDelayInSeconds * 1000 - _.delay @fetchAndSimulateTask, retryDelayInMilliseconds - - setupSimulationAndLoadLevel: (taskData, textStatus, jqXHR) => - return @handleNoGamesResponse() if jqXHR.status is 204 - @trigger 'statusUpdate', 'Setting up simulation!' - - @task = new SimulationTask(taskData) - try - levelID = @task.getLevelName() - catch err - console.error err - @trigger 'statusUpdate', "Error simulating game: #{err}. Trying another game in #{@retryDelayInSeconds} seconds." - @simulateAnotherTaskAfterDelay() - return - - @supermodel ?= new SuperModel() - - #console.log "Creating loader with levelID: " + levelID + " and SessionID: " + @task.getFirstSessionID() + " - task: " + JSON.stringify(@task) - - @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), headless: true - - console.log "Waiting for loaded game" - - @listenToOnce(@levelLoader, 'loaded-all', @simulateGame) - - simulateGame: -> - console.warn "Simulate game." - return if @destroyed - @trigger 'statusUpdate', 'All resources loaded, simulating!', @task.getSessions() - console.log "assignWorld" - @assignWorldAndLevelFromLevelLoaderAndDestroyIt() - console.log "SetupGod" - @setupGod() - try - @commenceSimulationAndSetupCallback() - catch err - console.log "There was an error in simulation(#{err}). Trying again in #{@retryDelayInSeconds} seconds" - - #TODO: Comment out. - throw err - - @simulateAnotherTaskAfterDelay() - - assignWorldAndLevelFromLevelLoaderAndDestroyIt: -> - console.log "Assigning world and level" - @world = @levelLoader.world - @level = @levelLoader.level - @levelLoader.destroy() - @levelLoader = null - - setupGod: -> - @god.level = @level.serialize @supermodel - @god.setWorldClassMap @world.classMap - @setupGoalManager() - - setupGoalManager: -> - goalManager = new GoalManager @world - goalManager.goals = @god.level.goals - goalManager.goalStates = @manuallyGenerateGoalStates() - @god.setGoalManager goalManager - - commenceSimulationAndSetupCallback: -> - console.log "Creating World." - @god.createWorld(@generateSpellsObject()) - Backbone.Mediator.subscribeOnce 'god:infinite-loop', @onInfiniteLoop, @ - Backbone.Mediator.subscribeOnce 'god:goals-calculated', @processResults, @ - - #Search for leaks - if leaktest and not @memwatch? - leakcount = 0 - maxleakcount = 0 - console.log "Setting leak callbacks." - @memwatch = require 'memwatch' - - @memwatch.on 'leak', (info) => - console.warn "LEAK!!\n" + JSON.stringify(info) - - unless @hd? - if (leakcount++ is maxleakcount) - @hd = new @memwatch.HeapDiff() - - @memwatch.on 'stats', (stats) => - console.warn "stats callback: " + stats - diff = @hd.end() - console.warn "HeapDiff:\n" + JSON.stringify(diff) - - if exitOnLeak - console.warn "Exiting because of Leak." - process.exit() - @hd = new @memwatch.HeapDiff() - - - - onInfiniteLoop: -> - console.warn "Skipping infinitely looping game." - @trigger 'statusUpdate', "Infinite loop detected; grabbing a new game in #{@retryDelayInSeconds} seconds." - _.delay @cleanupAndSimulateAnotherTask, @retryDelayInSeconds * 1000 - - processResults: (simulationResults) -> - - console.log "Processing Results" - - taskResults = @formTaskResultsObject simulationResults - console.warn taskResults - @sendResultsBackToServer taskResults - - sendResultsBackToServer: (results) => - @trigger 'statusUpdate', 'Simulation completed, sending results back to server!' - console.log "Sending result back to server" - - if testing - return @fetchAndSimulateTask() - - $.ajax - url: "queue/scoring" - data: results - parse: true - type: "PUT" - success: @handleTaskResultsTransferSuccess - error: @handleTaskResultsTransferError - complete: @cleanupAndSimulateAnotherTask - - handleTaskResultsTransferSuccess: (result) => - console.log "Task registration result: #{JSON.stringify result}" - @trigger 'statusUpdate', 'Results were successfully sent back to server!' - console.log "Simulated by you: " + @simulatedByYou - @simulatedByYou++ - - handleTaskResultsTransferError: (error) => - @trigger 'statusUpdate', 'There was an error sending the results back to the server.' - console.log "Task registration error: #{JSON.stringify error}" - - cleanupAndSimulateAnotherTask: => - #@cleanupSimulation() Not needed for Buddha. - @fetchAndSimulateTask() - - cleanupSimulation: -> - @god?.destroy() - @god = null - @world = null - @level = null - - formTaskResultsObject: (simulationResults) -> - 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 - - calculateSessionRank: (sessionID, goalStates, teamSessionMap) -> - humansDestroyed = goalStates["destroy-humans"].status is "success" - ogresDestroyed = goalStates["destroy-ogres"].status is "success" - if humansDestroyed is ogresDestroyed - return 0 - else if humansDestroyed and teamSessionMap["ogres"] is sessionID - return 0 - else if humansDestroyed and teamSessionMap["ogres"] isnt sessionID - return 1 - else if ogresDestroyed and teamSessionMap["humans"] is sessionID - return 0 - else - return 1 - - manuallyGenerateGoalStates: -> - goalStates = - "destroy-humans": - keyFrame: 0 - killed: - "Human Base": false - status: "incomplete" - "destroy-ogres": - keyFrame:0 - killed: - "Ogre Base": false - status: "incomplete" - - generateSpellsObject: -> - @currentUserCodeMap = @task.generateSpellKeyToSourceMap() - @spells = {} - for thang in @level.attributes.thangs - continue if @thangIsATemplate thang - @generateSpellKeyToSourceMapPropertiesFromThang thang - - thangIsATemplate: (thang) -> - for component in thang.components - continue unless @componentHasProgrammableMethods component - for methodName, method of component.config.programmableMethods - return true if @methodBelongsToTemplateThang method - - return false - - componentHasProgrammableMethods: (component) -> component.config? and _.has component.config, 'programmableMethods' - - methodBelongsToTemplateThang: (method) -> typeof method is 'string' - - generateSpellKeyToSourceMapPropertiesFromThang: (thang) => - for component in thang.components - continue unless @componentHasProgrammableMethods component - for methodName, method of component.config.programmableMethods - spellKey = @generateSpellKeyFromThangIDAndMethodName thang.id, methodName - - @createSpellAndAssignName spellKey, methodName - @createSpellThang thang, method, spellKey - @transpileSpell thang, spellKey, methodName - - generateSpellKeyFromThangIDAndMethodName: (thang, methodName) -> - spellKeyComponents = [thang, methodName] - spellKeyComponents[0] = _.string.slugify spellKeyComponents[0] - spellKey = spellKeyComponents.join '/' - spellKey - - - createSpellAndAssignName: (spellKey, spellName) -> - @spells[spellKey] ?= {} - @spells[spellKey].name = spellName - - createSpellThang: (thang, method, spellKey) -> - @spells[spellKey].thangs ?= {} - @spells[spellKey].thangs[thang.id] ?= {} - @spells[spellKey].thangs[thang.id].aether = @createAether @spells[spellKey].name, method - - transpileSpell: (thang, spellKey, methodName) -> - slugifiedThangID = _.string.slugify thang.id - source = @currentUserCodeMap[[slugifiedThangID,methodName].join '/'] ? "" - aether = @spells[spellKey].thangs[thang.id].aether - try - aether.transpile source - catch e - console.log "Couldn't transpile #{spellKey}:\n#{source}\n", e - aether.transpile '' - - createAether: (methodName, method) -> - aetherOptions = - functionName: methodName - protectAPI: true - includeFlow: false - requiresThis: true - yieldConditionally: false - problems: - jshint_W040: {level: "ignore"} - jshint_W030: {level: "ignore"} # aether_NoEffect instead - aether_MissingThis: {level: 'error'} - if methodName is 'hear' - aetherOptions.functionParameters = ['speaker', 'message', 'data'] - #console.log "creating aether with options", aetherOptions - - return new Aether aetherOptions - - class SimulationTask - constructor: (@rawData) -> - #console.log 'Simulating sessions', (session for session in @getSessions()) - - getLevelName: -> - levelName = @rawData.sessions?[0]?.levelID - return levelName if levelName? - @throwMalformedTaskError "The level name couldn't be deduced from the task." - - generateTeamToSessionMap: -> - teamSessionMap = {} - for session in @rawData.sessions - @throwMalformedTaskError "Two players share the same team" if teamSessionMap[session.team]? - teamSessionMap[session.team] = session.sessionID - - teamSessionMap - - throwMalformedTaskError: (errorString) -> - throw new Error "The task was malformed, reason: #{errorString}" - - getFirstSessionID: -> @rawData.sessions[0].sessionID - - getTaskID: -> @rawData.taskID - - getReceiptHandle: -> @rawData.receiptHandle - - getSessions: -> @rawData.sessions - - generateSpellKeyToSourceMap: -> - spellKeyToSourceMap = {} - for session in @rawData.sessions - teamSpells = session.teamSpells[session.team] - teamCode = {} - for thangName, thangSpells of session.code - for spellName, spell of thangSpells - 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 - - sim = new Simulator() + GLOBAL.testFile = require headlessClient + 'test.js' + sim = new Simulator workerCode sim.fetchAndSimulateTask() \ No newline at end of file From af5b878166e21d77cdb14dcce81db20038ce8900 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Tue, 6 May 2014 06:56:14 -0700 Subject: [PATCH 08/12] Fixed wizard setting things. --- app/locale/zh.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/locale/zh.coffee b/app/locale/zh.coffee index 36f24fc34..f2d506f57 100644 --- a/app/locale/zh.coffee +++ b/app/locale/zh.coffee @@ -130,7 +130,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra # learn_more: "Learn more about being a Diplomat" # subscribe_as_diplomat: "Subscribe as a Diplomat" -# wizard_settings: + wizard_settings: title: "巫师设定" customize_avatar: "设置你的头像" active: "启用" From 9e40dcd2e312c5596ce3f72636529e22da32042e Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Tue, 6 May 2014 06:58:20 -0700 Subject: [PATCH 09/12] Added a couple i18n tags. --- app/locale/ar.coffee | 2 ++ app/locale/bg.coffee | 2 ++ app/locale/ca.coffee | 2 ++ app/locale/cs.coffee | 2 ++ app/locale/da.coffee | 2 ++ app/locale/de-AT.coffee | 2 ++ app/locale/de-CH.coffee | 2 ++ app/locale/de-DE.coffee | 2 ++ app/locale/de.coffee | 2 ++ app/locale/el.coffee | 2 ++ app/locale/en-AU.coffee | 2 ++ app/locale/en-GB.coffee | 2 ++ app/locale/en-US.coffee | 2 ++ app/locale/es-419.coffee | 2 ++ app/locale/es-ES.coffee | 2 ++ app/locale/es.coffee | 2 ++ app/locale/fa.coffee | 2 ++ app/locale/fi.coffee | 2 ++ app/locale/fr.coffee | 2 ++ app/locale/he.coffee | 2 ++ app/locale/hi.coffee | 2 ++ app/locale/hu.coffee | 2 ++ app/locale/id.coffee | 2 ++ app/locale/it.coffee | 2 ++ app/locale/ja.coffee | 2 ++ app/locale/ko.coffee | 2 ++ app/locale/lt.coffee | 2 ++ app/locale/ms.coffee | 2 ++ app/locale/nb.coffee | 2 ++ app/locale/nl-BE.coffee | 2 ++ app/locale/nl-NL.coffee | 2 ++ app/locale/nl.coffee | 6 ++++-- app/locale/nn.coffee | 2 ++ app/locale/no.coffee | 2 ++ app/locale/pl.coffee | 2 ++ app/locale/pt-BR.coffee | 2 ++ app/locale/pt-PT.coffee | 2 ++ app/locale/pt.coffee | 2 ++ app/locale/ro.coffee | 2 ++ app/locale/ru.coffee | 2 ++ app/locale/sk.coffee | 2 ++ app/locale/sl.coffee | 2 ++ app/locale/sr.coffee | 2 ++ app/locale/sv.coffee | 2 ++ app/locale/th.coffee | 2 ++ app/locale/tr.coffee | 2 ++ app/locale/uk.coffee | 2 ++ app/locale/ur.coffee | 2 ++ app/locale/vi.coffee | 2 ++ app/locale/zh-HANS.coffee | 2 ++ app/locale/zh-HANT.coffee | 2 ++ app/locale/zh-WUU-HANS.coffee | 2 ++ app/locale/zh-WUU-HANT.coffee | 2 ++ app/locale/zh.coffee | 2 ++ 54 files changed, 110 insertions(+), 2 deletions(-) diff --git a/app/locale/ar.coffee b/app/locale/ar.coffee index 42b9b617e..60847fffd 100644 --- a/app/locale/ar.coffee +++ b/app/locale/ar.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "العربية", englishDescription: "Arabi # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "العربية", englishDescription: "Arabi # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/bg.coffee b/app/locale/bg.coffee index 32f930ab4..712b83d4c 100644 --- a/app/locale/bg.coffee +++ b/app/locale/bg.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "български език", englishDescri # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "български език", englishDescri # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ca.coffee b/app/locale/ca.coffee index 763a73766..a609dd265 100644 --- a/app/locale/ca.coffee +++ b/app/locale/ca.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Català", englishDescription: "Catalan", tr # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/cs.coffee b/app/locale/cs.coffee index 0568b5128..c06ba4be2 100644 --- a/app/locale/cs.coffee +++ b/app/locale/cs.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr multiplayer_hint_label: "Tip:" multiplayer_hint: " Klikněte na odkaz pro jeho výběr, poté stiskněte ⌘-C nebo Ctrl-C pro kopírování odkazu." multiplayer_coming_soon: "Další vlastnosti multiplayeru jsou na cestě!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Průvodce" tome_minion_spells: "Vaše oblíbená kouzla" tome_read_only_spells: "Kouzla jen pro čtení" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/da.coffee b/app/locale/da.coffee index 01c6f1b79..b7f579fb8 100644 --- a/app/locale/da.coffee +++ b/app/locale/da.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans multiplayer_hint_label: "Tip:" multiplayer_hint: " Klik på linket for markere alt; tryk derefter ⌘-C eller Ctrl-C tfr at kopiere linket." multiplayer_coming_soon: "Yderligere flerspillermuligheder er på vej!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Instruktioner" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/de-AT.coffee b/app/locale/de-AT.coffee index 80df6de2f..b1bca6e27 100644 --- a/app/locale/de-AT.coffee +++ b/app/locale/de-AT.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Deutsch (Österreich)", englishDescription: # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/de-CH.coffee b/app/locale/de-CH.coffee index 6203a96a9..46694a416 100644 --- a/app/locale/de-CH.coffee +++ b/app/locale/de-CH.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Deutsch (Schweiz)", englishDescription: "Ge # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Deutsch (Schweiz)", englishDescription: "Ge # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/de-DE.coffee b/app/locale/de-DE.coffee index 91a82ba98..51faa5412 100644 --- a/app/locale/de-DE.coffee +++ b/app/locale/de-DE.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription: multiplayer_hint_label: "Hinweis:" multiplayer_hint: " Klick den Link, um alles auszuwählen, dann drück ⌘-C oder Strg-C um den Link zu kopieren." multiplayer_coming_soon: "Mehr Multiplayerfeatures werden kommen!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Anleitung" tome_minion_spells: "Die Zaubersprüche Deiner Knechte" tome_read_only_spells: "Nur-lesen Zauberspüche" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription: # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/de.coffee b/app/locale/de.coffee index 5a6ad4372..8874f2477 100644 --- a/app/locale/de.coffee +++ b/app/locale/de.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Deutsch", englishDescription: "German", tra multiplayer_hint_label: "Hinweis:" multiplayer_hint: " Klick den Link, um alles auszuwählen, dann drück ⌘-C oder Strg-C um den Link zu kopieren." multiplayer_coming_soon: "Mehr Multiplayerfeatures werden kommen!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Anleitung" tome_minion_spells: "Die Zaubersprüche Deiner Knechte" tome_read_only_spells: "Nur-lesen Zauberspüche" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Deutsch", englishDescription: "German", tra # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/el.coffee b/app/locale/el.coffee index 7ccc8b35c..86645355d 100644 --- a/app/locale/el.coffee +++ b/app/locale/el.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "ελληνικά", englishDescription: "Gre multiplayer_hint_label: "Συμβουλή:" multiplayer_hint: " Κάντε κλικ στο σύνδεσμο για να επιλέξετε όλα, στη συνέχεια, πατήστε την Apple-C ή Ctrl-C για να αντιγράψετε το σύνδεσμο." multiplayer_coming_soon: "Περισσότερα multiplayer χαρακτιριστηκα προσεχως!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Οδηγός" tome_minion_spells: "Ξόρκια για τα τσιράκια σας" tome_read_only_spells: "Ξορκια μονο για αναγνωση" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "ελληνικά", englishDescription: "Gre # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/en-AU.coffee b/app/locale/en-AU.coffee index 9d73b3a95..a9574e5ad 100644 --- a/app/locale/en-AU.coffee +++ b/app/locale/en-AU.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "English (AU)", englishDescription: "English # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "English (AU)", englishDescription: "English # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/en-GB.coffee b/app/locale/en-GB.coffee index cd8995418..f3dc42c6d 100644 --- a/app/locale/en-GB.coffee +++ b/app/locale/en-GB.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "English (UK)", englishDescription: "English # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "English (UK)", englishDescription: "English # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/en-US.coffee b/app/locale/en-US.coffee index 640cbee96..874cd9063 100644 --- a/app/locale/en-US.coffee +++ b/app/locale/en-US.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "English (US)", englishDescription: "English # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "English (US)", englishDescription: "English # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 660719a22..a9e351132 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "español (América Latina)", englishDescrip multiplayer_hint_label: "Consejo:" multiplayer_hint: " Cliquea el enlace para seleccionar todo, luego presiona ⌘-C o Ctrl-C para copiar el enlace." multiplayer_coming_soon: "¡Más características de multijugador por venir!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guía" tome_minion_spells: "Hechizos de tus Secuaces" tome_read_only_spells: "Hechizos de Sólo Lectura" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "español (América Latina)", englishDescrip # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/es-ES.coffee b/app/locale/es-ES.coffee index ee6166771..632d9aeec 100644 --- a/app/locale/es-ES.coffee +++ b/app/locale/es-ES.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis multiplayer_hint_label: "Pista:" multiplayer_hint: " Haz un click en el link para que se seleccione, después utiliza Ctrl-C o ⌘-C para copiar el link." multiplayer_coming_soon: "¡Más opciones de Multijugador están por venir!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guía" tome_minion_spells: "Los hechizos de tus súbditos" tome_read_only_spells: "Hechizos de solo lectura" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/es.coffee b/app/locale/es.coffee index d13ab8697..19a13448f 100644 --- a/app/locale/es.coffee +++ b/app/locale/es.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "español", englishDescription: "Spanish", t multiplayer_hint_label: "Consejo:" multiplayer_hint: " Cliquea el enlace para seleccionar todo, luego presiona ⌘-C o Ctrl-C para copiar el enlace." multiplayer_coming_soon: "¡Más características de multijugador por venir!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guía" tome_minion_spells: "Hechizos de tus Secuaces" tome_read_only_spells: "Hechizos de Sólo Lectura" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "español", englishDescription: "Spanish", t # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/fa.coffee b/app/locale/fa.coffee index 404fb28cd..e733bf95e 100644 --- a/app/locale/fa.coffee +++ b/app/locale/fa.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "فارسی", englishDescription: "Persian", # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "فارسی", englishDescription: "Persian", # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/fi.coffee b/app/locale/fi.coffee index 448cf6899..974751c43 100644 --- a/app/locale/fi.coffee +++ b/app/locale/fi.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "suomi", englishDescription: "Finnish", tran # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "suomi", englishDescription: "Finnish", tran # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/fr.coffee b/app/locale/fr.coffee index 2b510dd04..1c76f3ccd 100644 --- a/app/locale/fr.coffee +++ b/app/locale/fr.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t multiplayer_hint_label: "Astuce:" multiplayer_hint: " Cliquez sur le lien pour tout sélectionner, puis appuyer sur Pomme-C ou Ctrl-C pour copier le lien." multiplayer_coming_soon: "Plus de fonctionnalités multijoueurs sont à venir" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guide" tome_minion_spells: "Les sorts de vos soldats" tome_read_only_spells: "Sorts en lecture-seule" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "français", englishDescription: "French", t # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/he.coffee b/app/locale/he.coffee index 77f17163c..2df5729cc 100644 --- a/app/locale/he.coffee +++ b/app/locale/he.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "עברית", englishDescription: "Hebrew", # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "עברית", englishDescription: "Hebrew", # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/hi.coffee b/app/locale/hi.coffee index dd0fc9ea0..d8d8ee3af 100644 --- a/app/locale/hi.coffee +++ b/app/locale/hi.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "मानक हिन्दी", englishDe # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "मानक हिन्दी", englishDe # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/hu.coffee b/app/locale/hu.coffee index 9f1258214..0c2c6d55c 100644 --- a/app/locale/hu.coffee +++ b/app/locale/hu.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t multiplayer_hint_label: "Tipp:" multiplayer_hint: " Kattints a linkre, és Ctrl+C-vel (vagy ⌘+C-vel) másold a vágólapra!" # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Útmutató" tome_minion_spells: "Egységeid varázslatai" tome_read_only_spells: "Csak olvasható varázslatok" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/id.coffee b/app/locale/id.coffee index 7de47c188..93fb972d3 100644 --- a/app/locale/id.coffee +++ b/app/locale/id.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Bahasa Indonesia", englishDescription: "Ind # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Bahasa Indonesia", englishDescription: "Ind # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/it.coffee b/app/locale/it.coffee index 1073eeb5d..6ca663b43 100644 --- a/app/locale/it.coffee +++ b/app/locale/it.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t multiplayer_hint_label: "Suggerimento:" multiplayer_hint: " Clicca il link per selezionare tutto, quindi premi CMD-C o Ctrl-C per copiare il link." multiplayer_coming_soon: "Ulteriori aggiunte per il multigiocatore in arrivo!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guida" tome_minion_spells: "Incantesimi dei tuoi seguaci" tome_read_only_spells: "Incantesimi in sola lettura" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Italiano", englishDescription: "Italian", t # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ja.coffee b/app/locale/ja.coffee index c0933ed0f..bd68e1aa9 100644 --- a/app/locale/ja.coffee +++ b/app/locale/ja.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese", multiplayer_hint_label: "ヒント:" multiplayer_hint: " リンクを選択後、 ⌘-C(MacOS) or Ctrl-C(Windows) でリンクをコピーできます。" multiplayer_coming_soon: "今後より多くのマルチプレイ機能が追加されます。" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "ガイド" tome_minion_spells: "操作できるキャラクターの呪文" tome_read_only_spells: "読込専用の呪文" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese", # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ko.coffee b/app/locale/ko.coffee index 3b0ba73d7..e4145bc72 100644 --- a/app/locale/ko.coffee +++ b/app/locale/ko.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "한국어", englishDescription: "Korean", t multiplayer_hint_label: "힌트:" multiplayer_hint: " 모두 선택하려면 링크를 클릭하세요, 그리고 ⌘-C 또는 Ctrl-C 를 눌러서 링크를 복사하세요." multiplayer_coming_soon: "곧 좀 더 다양한 멀티플레이어 모드가 업데이트 됩니다!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "가이드" tome_minion_spells: "당신 미니언의' 마법" tome_read_only_spells: "읽기 전용 마법" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "한국어", englishDescription: "Korean", t # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/lt.coffee b/app/locale/lt.coffee index 64864930f..31749c727 100644 --- a/app/locale/lt.coffee +++ b/app/locale/lt.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "lietuvių kalba", englishDescription: "Lith # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "lietuvių kalba", englishDescription: "Lith # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ms.coffee b/app/locale/ms.coffee index 3fbc839f7..1316bbe2a 100644 --- a/app/locale/ms.coffee +++ b/app/locale/ms.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Bahasa Melayu", englishDescription: "Bahasa # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Bahasa Melayu", englishDescription: "Bahasa # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/nb.coffee b/app/locale/nb.coffee index 7f7604c7a..8ce394170 100644 --- a/app/locale/nb.coffee +++ b/app/locale/nb.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Norsk Bokmål", englishDescription: "Norweg multiplayer_hint_label: "Hint:" multiplayer_hint: " Klikk lenken for å velge alle, så trykker du Apple-C eller Ctrl-C for å kopiere lenken." multiplayer_coming_soon: "Det kommer flere flerspillsmuligheter!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guide" tome_minion_spells: "Din Minions' Trylleformularer" tome_read_only_spells: "Kun-Lesbare Trylleformularer" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Norsk Bokmål", englishDescription: "Norweg # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/nl-BE.coffee b/app/locale/nl-BE.coffee index 7c65bdb53..e89cbd30e 100644 --- a/app/locale/nl-BE.coffee +++ b/app/locale/nl-BE.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription: multiplayer_hint_label: "Hint:" multiplayer_hint: " Klik de link om alles te selecteren, druk dan op Apple-C of Ctrl-C om de link te kopiëren." multiplayer_coming_soon: "Binnenkort komen er meer Multiplayermogelijkheden!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Handleiding" tome_minion_spells: "Jouw Minions' Spreuken" tome_read_only_spells: "Read-Only Spreuken" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Nederlands (België)", englishDescription: # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index 66ebdec3f..4ce7aa839 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription multiplayer_hint_label: "Hint:" multiplayer_hint: " Klik de link om alles te selecteren, druk dan op Apple-C of Ctrl-C om de link te kopiëren." multiplayer_coming_soon: "Binnenkort komen er meer Multiplayermogelijkheden!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Handleiding" tome_minion_spells: "Jouw Minions' Spreuken" tome_read_only_spells: "Read-Only Spreuken" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/nl.coffee b/app/locale/nl.coffee index f749e910f..8fbf7075b 100644 --- a/app/locale/nl.coffee +++ b/app/locale/nl.coffee @@ -16,7 +16,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t play: "Spelen" retry: "Probeer opnieuw" watch: "Volgen" - unwatch: "Ontvolgen" + unwatch: "Ontvolgen" submit_patch: "Correctie Opsturen" units: @@ -199,7 +199,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t employers: want_to_hire_our_players: "Wil je expert CodeCombat spelers aanwerven? " - see_candidates: "Klik om je kandidaten te zien" + see_candidates: "Klik om je kandidaten te zien" candidates_count_prefix: "Momenteel hebben we " candidates_count_many: "veel" candidates_count_suffix: "zeer getalenteerde en ervaren ontwikkelaars die werk zoeken." @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t multiplayer_hint_label: "Hint:" multiplayer_hint: " Klik de link om alles te selecteren, druk dan op Apple-C of Ctrl-C om de link te kopiëren." multiplayer_coming_soon: "Binnenkort komen er meer Multiplayermogelijkheden!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Handleiding" tome_minion_spells: "Jouw Minions' Spreuken" tome_read_only_spells: "Read-Only Spreuken" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t user_names: "Gebruikersnamen" files: "Bestanden" top_simulators: "Top Simulatoren" +# source_document: "Source Document" diff --git a/app/locale/nn.coffee b/app/locale/nn.coffee index 5a6fabbc3..7dcf52989 100644 --- a/app/locale/nn.coffee +++ b/app/locale/nn.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Norwegian Nynorsk", englishDescription: "No # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Norwegian Nynorsk", englishDescription: "No # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/no.coffee b/app/locale/no.coffee index 6f8e0c22c..3d4984583 100644 --- a/app/locale/no.coffee +++ b/app/locale/no.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Norsk", englishDescription: "Norwegian", tr multiplayer_hint_label: "Hint:" multiplayer_hint: " Klikk lenken for å velge alle, så trykker du Apple-C eller Ctrl-C for å kopiere lenken." multiplayer_coming_soon: "Det kommer flere flerspillsmuligheter!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guide" tome_minion_spells: "Din Minions' Trylleformularer" tome_read_only_spells: "Kun-Lesbare Trylleformularer" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Norsk", englishDescription: "Norwegian", tr # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/pl.coffee b/app/locale/pl.coffee index d035da233..8e4e0dc89 100644 --- a/app/locale/pl.coffee +++ b/app/locale/pl.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "język polski", englishDescription: "Polish multiplayer_hint_label: "Podpowiedź:" multiplayer_hint: "Kliknij link by zaznaczyć wszystko, potem wciśnij Cmd-C lub Ctrl-C by skopiować ten link." multiplayer_coming_soon: "Wkrótce więcej opcji multiplayer" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Przewodnik" tome_minion_spells: "Czary twojego podopiecznego" tome_read_only_spells: "Czary tylko do odczytu" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "język polski", englishDescription: "Polish # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index 53dc7cd4f..0d30c42c8 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "português do Brasil", englishDescription: multiplayer_hint_label: "Dica:" multiplayer_hint: " Clique no link para selecionar tudo, então dê Ctrl+C ou ⌘+C para copiar o link. " multiplayer_coming_soon: "Mais novidades no multiplayer estão chegando!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guia" tome_minion_spells: "Magias dos seus subordinados" tome_read_only_spells: "Magias não editáveis" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "português do Brasil", englishDescription: # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/pt-PT.coffee b/app/locale/pt-PT.coffee index 3a7a84bac..26be267f8 100644 --- a/app/locale/pt-PT.coffee +++ b/app/locale/pt-PT.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Português europeu", englishDescription: "P multiplayer_hint_label: "Dica:" multiplayer_hint: " Carrega no link para seleccionar tudp, depois pressiona ⌘-C ou Ctrl-C para copiar o link." multiplayer_coming_soon: "Mais funcionalidades de multiplayer brevemente!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guia" tome_minion_spells: "Feitiços dos teus Minions" tome_read_only_spells: "Feitiços apenas de leitura" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Português europeu", englishDescription: "P # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/pt.coffee b/app/locale/pt.coffee index de356491c..1d742cd0d 100644 --- a/app/locale/pt.coffee +++ b/app/locale/pt.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "português", englishDescription: "Portugues multiplayer_hint_label: "Dica:" multiplayer_hint: " Clique no link para selecionar tudo, então dê Ctrl+C ou ⌘+C para copiar o link. " multiplayer_coming_soon: "Mais novidades no multiplayer estão chegando!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guia" tome_minion_spells: "Magias dos seus subordinados" tome_read_only_spells: "Magias não editáveis" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "português", englishDescription: "Portugues # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ro.coffee b/app/locale/ro.coffee index 1bb1a7342..d8701339f 100644 --- a/app/locale/ro.coffee +++ b/app/locale/ro.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman multiplayer_hint_label: "Hint:" multiplayer_hint: " Apasă pe link pentru a selecta tot, apoi apasă ⌘-C sau Ctrl-C pentru a copia link-ul." multiplayer_coming_soon: "Mai multe feature-uri multiplayer în curând!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Ghid" tome_minion_spells: "Vrăjile Minion-ilor tăi" tome_read_only_spells: "Vrăji Read-Only" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "limba română", englishDescription: "Roman # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index fc3c6e5a4..2b3bfd5fb 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi multiplayer_hint_label: "Подсказка: " multiplayer_hint: "кликните на ссылку, чтобы выделить её, затем нажмите ⌘-С или Ctrl-C, чтобы скопировать." multiplayer_coming_soon: "Больше возможностей мультиплеера на подходе!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Руководство" tome_minion_spells: "Заклинания ваших миньонов" tome_read_only_spells: "Заклинания только для чтения" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/sk.coffee b/app/locale/sk.coffee index decaf66c2..c4148ea16 100644 --- a/app/locale/sk.coffee +++ b/app/locale/sk.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak", # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "slovenčina", englishDescription: "Slovak", # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/sl.coffee b/app/locale/sl.coffee index d56856f20..aaea22579 100644 --- a/app/locale/sl.coffee +++ b/app/locale/sl.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "slovenščina", englishDescription: "Sloven # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "slovenščina", englishDescription: "Sloven # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/sr.coffee b/app/locale/sr.coffee index b309b5d0a..264f7f92b 100644 --- a/app/locale/sr.coffee +++ b/app/locale/sr.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian multiplayer_hint_label: "Мала помоћ" multiplayer_hint: " Кликни на линк да обележиш све, затим притисни Apple-C или Ctrl-C да копираш линк." multiplayer_coming_soon: "Стиже још нових карактеристика!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Водич" tome_minion_spells: "Чини твојих поданика" tome_read_only_spells: "Чини које се могу само гледати" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "српски", englishDescription: "Serbian # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/sv.coffee b/app/locale/sv.coffee index f37b7d57b..c783521f5 100644 --- a/app/locale/sv.coffee +++ b/app/locale/sv.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Svenska", englishDescription: "Swedish", tr multiplayer_hint_label: "Tips:" multiplayer_hint: " Klicka på länken för att välja allt, tryck sedan på Cmd-C eller Ctrl-C för att kopiera länken." multiplayer_coming_soon: "Fler flerspelarlägen kommer!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Guide" tome_minion_spells: "Dina soldaters förmågor" tome_read_only_spells: "Skrivskyddade förmågor" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Svenska", englishDescription: "Swedish", tr # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/th.coffee b/app/locale/th.coffee index 49b2b5285..513bae855 100644 --- a/app/locale/th.coffee +++ b/app/locale/th.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "ไทย", englishDescription: "Thai", tra multiplayer_hint_label: "คำใบ้" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "ไทย", englishDescription: "Thai", tra # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/tr.coffee b/app/locale/tr.coffee index e4b9b9496..8b1ef047a 100644 --- a/app/locale/tr.coffee +++ b/app/locale/tr.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t multiplayer_hint_label: "İpucu:" multiplayer_hint: " Kopyalamak için önce linke tıklayın, ardından CTRL+C veya ⌘+C kombinasyonuna basın." multiplayer_coming_soon: "Daha bir çok çoklu oyuncu özelliği eklenecek!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Rehber" tome_minion_spells: "Minyonlarınızın Büyüleri" tome_read_only_spells: "Salt Okunur Büyüler" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Türkçe", englishDescription: "Turkish", t # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/uk.coffee b/app/locale/uk.coffee index 752827cde..b2d544530 100644 --- a/app/locale/uk.coffee +++ b/app/locale/uk.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "українська мова", englishDesc multiplayer_hint_label: "Підказка:" multiplayer_hint: "Натисніть на посилання, щоб обрати всіх, та натисніть Apple-C або Ctrl-C, щоб скопіювати посилання." multiplayer_coming_soon: "Скоро - більше можливостей у мультиплеєрі!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "Посібник" tome_minion_spells: "Закляття ваших міньонів" tome_read_only_spells: "Закляття тільки для читання" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "українська мова", englishDesc # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/ur.coffee b/app/locale/ur.coffee index dea4a72a2..ce51b341d 100644 --- a/app/locale/ur.coffee +++ b/app/locale/ur.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "اُردُو", englishDescription: "Urdu", # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "اُردُو", englishDescription: "Urdu", # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/vi.coffee b/app/locale/vi.coffee index 898291cfb..3a243c5d7 100644 --- a/app/locale/vi.coffee +++ b/app/locale/vi.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "Tiếng Việt", englishDescription: "Vietn # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/zh-HANS.coffee b/app/locale/zh-HANS.coffee index 364483ca9..73198457c 100644 --- a/app/locale/zh-HANS.coffee +++ b/app/locale/zh-HANS.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese multiplayer_hint_label: "提示:" multiplayer_hint: " 点击全选,然后按 Apple-C(苹果电脑)或 Ctrl-C 复制链接。" multiplayer_coming_soon: "多人游戏的更多特性!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "指南" tome_minion_spells: "助手的咒语" tome_read_only_spells: "只读的咒语" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/zh-HANT.coffee b/app/locale/zh-HANT.coffee index 0972a07f8..ed6c76e07 100644 --- a/app/locale/zh-HANT.coffee +++ b/app/locale/zh-HANT.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese multiplayer_hint_label: "提示:" multiplayer_hint: " 點擊全選,然後按 ⌘-C 或 Ctrl-C 複製連結。" multiplayer_coming_soon: "請期待更多的多人關卡!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "指南" tome_minion_spells: "助手的咒語" tome_read_only_spells: "唯讀的咒語" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "繁体中文", englishDescription: "Chinese # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/zh-WUU-HANS.coffee b/app/locale/zh-WUU-HANS.coffee index 33070e062..04f6deb48 100644 --- a/app/locale/zh-WUU-HANS.coffee +++ b/app/locale/zh-WUU-HANS.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "吴语", englishDescription: "Wuu (Simplifi # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "吴语", englishDescription: "Wuu (Simplifi # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/zh-WUU-HANT.coffee b/app/locale/zh-WUU-HANT.coffee index b7dd0f40a..410d45285 100644 --- a/app/locale/zh-WUU-HANT.coffee +++ b/app/locale/zh-WUU-HANT.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "吳語", englishDescription: "Wuu (Traditio multiplayer_hint_label: "提醒:" multiplayer_hint: " 點牢全選,再捺 Apple-C(蘋果電腦)要勿 Ctrl-C 複製鏈接。" multiplayer_coming_soon: "多人遊戲還多特性!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." guide_title: "指南" tome_minion_spells: "下手個咒語" tome_read_only_spells: "只讀個咒語" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "吳語", englishDescription: "Wuu (Traditio # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" diff --git a/app/locale/zh.coffee b/app/locale/zh.coffee index f2d506f57..22e226cd7 100644 --- a/app/locale/zh.coffee +++ b/app/locale/zh.coffee @@ -246,6 +246,7 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra # multiplayer_hint_label: "Hint:" # multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link." # multiplayer_coming_soon: "More multiplayer features to come!" +# multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard." # guide_title: "Guide" # tome_minion_spells: "Your Minions' Spells" # tome_read_only_spells: "Read-Only Spells" @@ -710,3 +711,4 @@ module.exports = nativeDescription: "中文", englishDescription: "Chinese", tra # user_names: "User Names" # files: "Files" # top_simulators: "Top Simulators" +# source_document: "Source Document" From acccee33a37103a8664e66dd9371fd83b5fa324b Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Tue, 6 May 2014 07:22:09 -0700 Subject: [PATCH 10/12] Cleaner conditional init syntax. --- app/lib/world/GoalManager.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee index cc31a61bf..ea13e4188 100644 --- a/app/lib/world/GoalManager.coffee +++ b/app/lib/world/GoalManager.coffee @@ -204,7 +204,7 @@ module.exports = class GoalManager extends CocoClass arrays = (prop for prop in whos when prop?.length) return unless arrays.length - state[progressObjectName] = state[progressObjectName] ? {} + state[progressObjectName] ?= {} for array in arrays for thang in array if @thangTeams[thang]? From 2e6bec545488bad6cd19c851fd1daea57e2011f7 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Tue, 6 May 2014 09:49:04 -0700 Subject: [PATCH 11/12] Some fixes for my accidental merge of #970. --- app/lib/Buddha.coffee | 6 +-- app/lib/God.coffee | 4 +- app/lib/LevelLoader.coffee | 1 + app/lib/simulator/Simulator.coffee | 33 +++++++------ app/views/play/ladder/simulate_tab.coffee | 2 +- headless_client.coffee | 57 +++++++++++------------ 6 files changed, 53 insertions(+), 50 deletions(-) diff --git a/app/lib/Buddha.coffee b/app/lib/Buddha.coffee index 33af9d742..25ac3e21e 100644 --- a/app/lib/Buddha.coffee +++ b/app/lib/Buddha.coffee @@ -72,7 +72,7 @@ class Angel return if @aborting unless serialized # We're only interested in goalStates. (Simulator) - @latestGoalStates = goalStates; + @latestGoalStates = goalStates Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates) @running = false @shared.busyAngels.pop @ @@ -95,7 +95,7 @@ class Angel @shared.goalManager?.world = world @running = false @shared.busyAngels.pop @ - @shared.firstWorld = false; + @shared.firstWorld = false @doWork() infinitelyLooped: => @@ -111,7 +111,7 @@ class Angel doWork: => #console.log "work." return if @aborted - console.log @id + " ready and looking for work. WorkQueue lenght is " + @shared.workQueue.length + console.log @id + " ready and looking for work. WorkQueue length is " + @shared.workQueue.length if @initialized and @shared.workQueue.length work = @shared.workQueue.pop() if work is Angel.cyanide # Kill all other Angels, too diff --git a/app/lib/God.coffee b/app/lib/God.coffee index 3bca0b1d8..b036b2467 100644 --- a/app/lib/God.coffee +++ b/app/lib/God.coffee @@ -92,7 +92,7 @@ module.exports = class God #console.log "UserCodeProblem:", '"' + problem.message + '"', "for", problem.userInfo.thangID, "-", problem.userInfo.methodName, 'at line', problem.ranges?[0][0][0], 'column', problem.ranges?[0][0][1] Backbone.Mediator.publish 'god:user-code-problem', problem: problem - createWorld: (spells) -> + createWorld: (@spells) -> #console.log @id + ': "Let there be light upon', @world.name + '!"' unless Worker? # profiling world simulation is easier on main thread, or we are IE9 setTimeout @simulateWorld, 1 @@ -121,7 +121,7 @@ module.exports = class God beholdWorld: (angel, serialized, goalStates) -> unless serialized # We're only interested in goalStates. - @latestGoalStates = goalStates; + @latestGoalStates = goalStates Backbone.Mediator.publish('god:goals-calculated', goalStates: goalStates, team: me.team) unless _.find @angels, 'busy' @spells = null # Don't hold onto old spells; memory leaks diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee index a276d0364..ac1e77601 100644 --- a/app/lib/LevelLoader.coffee +++ b/app/lib/LevelLoader.coffee @@ -155,6 +155,7 @@ module.exports = class LevelLoader extends CocoClass # Building sprite sheets buildSpriteSheetsForThangType: (thangType) -> + return if @headless @grabThangTypeTeams() unless @thangTypeTeams for team in @thangTypeTeams[thangType.get('original')] ? [null] spriteOptions = {resolutionFactor: 4, async: false} diff --git a/app/lib/simulator/Simulator.coffee b/app/lib/simulator/Simulator.coffee index 7293c9f7f..b27266f8f 100644 --- a/app/lib/simulator/Simulator.coffee +++ b/app/lib/simulator/Simulator.coffee @@ -6,16 +6,14 @@ God = require 'lib/Buddha' module.exports = class Simulator extends CocoClass - constructor: (workerCode) -> + constructor: (@options) -> + @options ?= {} _.extend @, Backbone.Events @trigger 'statusUpdate', 'Starting simulation!' @retryDelayInSeconds = 10 @taskURL = '/queue/scoring' @simulatedByYou = 0 - if workerCode - @god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: workerCode # Start loading worker. - else - @god = new God maxWorkerPoolSize: 1, maxAngels: 1 + @god = new God maxWorkerPoolSize: 1, maxAngels: 1, workerCode: @options.workerCode # Start loading worker. destroy: -> @off() @@ -25,14 +23,14 @@ module.exports = class Simulator extends CocoClass fetchAndSimulateTask: => return if @destroyed - if headless + if @options.headlessClient if @dumpThisTime # The first heapdump would be useless to find leaks. console.log "Writing snapshot." - heapdump.writeSnapshot() - @dumpThisTime = true if heapdump + @options.heapdump.writeSnapshot() + @dumpThisTime = true if @options.heapdump - if testing - _.delay @setupSimulationAndLoadLevel, 0, testFile, "Testing...", status: 400 + if @options.testing + _.delay @setupSimulationAndLoadLevel, 0, @options.testFile, "Testing...", status: 400 return @trigger 'statusUpdate', 'Fetching simulation data!' @@ -116,7 +114,7 @@ module.exports = class Simulator extends CocoClass Backbone.Mediator.subscribeOnce 'god:new-world-created', @processResults, @ #Search for leaks, headless-client only. - if headless and leaktest and not @memwatch? + if @options.headlessClient and @options.leakTest and not @memwatch? leakcount = 0 maxleakcount = 0 console.log "Setting leak callbacks." @@ -134,7 +132,7 @@ module.exports = class Simulator extends CocoClass diff = @hd.end() console.warn "HeapDiff:\n" + JSON.stringify(diff) - if exitOnLeak + if @options.exitOnLeak console.warn "Exiting because of Leak." process.exit() @hd = new @memwatch.HeapDiff() @@ -153,7 +151,7 @@ module.exports = class Simulator extends CocoClass @trigger 'statusUpdate', 'Simulation completed, sending results back to server!' console.log "Sending result back to server!" - if headless and testing + if @options.headlessClient and @options.testing return @fetchAndSimulateTask() $.ajax @@ -169,7 +167,7 @@ module.exports = class Simulator extends CocoClass @trigger 'statusUpdate', 'Results were successfully sent back to server!' console.log "Simulated by you: " + @simulatedByYou @simulatedByYou++ - if not headless + unless @options.headlessClient simulatedBy = parseInt($('#simulated-by-you').text(), 10) + 1 $('#simulated-by-you').text(simulatedBy) @@ -178,8 +176,15 @@ module.exports = class Simulator extends CocoClass console.log "Task registration error: #{JSON.stringify error}" cleanupAndSimulateAnotherTask: => + @cleanupSimulation() @fetchAndSimulateTask() + cleanupSimulation: -> + @god?.destroy() + @god = null + @world = null + @level = null + formTaskResultsObject: (simulationResults) -> taskResults = taskID: @task.getTaskID() diff --git a/app/views/play/ladder/simulate_tab.coffee b/app/views/play/ladder/simulate_tab.coffee index 418838b43..645a4597a 100644 --- a/app/views/play/ladder/simulate_tab.coffee +++ b/app/views/play/ladder/simulate_tab.coffee @@ -38,7 +38,7 @@ module.exports = class SimulateTabView extends CocoView # Simulations onSimulateButtonClick: (e) -> - $("#simulate-button").prop "disabled",true + $("#simulate-button").prop "disabled", true $("#simulate-button").text "Simulating..." @simulator.fetchAndSimulateTask() diff --git a/headless_client.coffee b/headless_client.coffee index 686851893..01041a68a 100644 --- a/headless_client.coffee +++ b/headless_client.coffee @@ -3,14 +3,22 @@ This file will simulate games on node.js by emulating the browser environment. At some point, most of the code can be merged with Simulator.coffee ### -# SETTINGS -GLOBAL.debug = false # Enable logging of ajax calls mainly -GLOBAL.testing = false # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. -GLOBAL.leaktest = false # Install callback that tries to find leaks automatically -GLOBAL.exitOnLeak = false # Exit if leak is found. Only useful if leaktest is set to true, obviously. -GLOBAL.heapdump = false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. +bowerComponentsPath = "./bower_components/" +headlessClientPath = "./headless_client/" -server = if testing then "http://127.0.0.1:3000" else "http://codecombat.com" +# SETTINGS +options = + workerCode: require headlessClientPath + 'worker_world' + debug: false # Enable logging of ajax calls mainly + testing: true # Instead of simulating 'real' games, use the same one over and over again. Good for leak hunting. + testFile: require headlessClientPath + 'test.js' + leakTest: false # Install callback that tries to find leaks automatically + exitOnLeak: false # Exit if leak is found. Only useful if leaktest is set to true, obviously. + heapdump: false # Dumps the whole heap after every pass. The heap dumps can then be viewed in Chrome browser. + headlessClient: true + +options.heapdump = require('heapdump') if options.heapdump +server = if options.testing then "http://127.0.0.1:3000" else "http://codecombat.com" # Disabled modules disable = [ @@ -19,12 +27,8 @@ disable = [ '../locale/locale' ] -GLOBAL.bowerComponents = "./bower_components/" -GLOBAL.headlessClient = "./headless_client/" - # Start of the actual code. Setting up the enivronment to match the environment of the browser -heapdump = require('heapdump') if heapdump # the path used for the loader. __dirname is module dependent. path = __dirname @@ -45,7 +49,6 @@ JASON = require 'jason' # Global emulated stuff GLOBAL.window = GLOBAL -GLOBAL.headless = true GLOBAL.document = location: pathname: "headless_client" GLOBAL.console.debug = console.log @@ -79,20 +82,20 @@ hookedLoader = (request, parent, isMain) -> request = 'lib/Buddha' if request in disable or ~request.indexOf('templates') - console.log 'Ignored ' + request if debug + console.log 'Ignored ' + request if options.debug return class fake else if '/' in request and not (request[0] is '.') or request is 'application' request = path + '/app/' + request else if request is 'underscore' request = 'lodash' - console.log "loading " + request if debug + console.log "loading " + request if options.debug originalLoader request, parent, isMain #jQuery wrapped for compatibility purposes. Poorly. GLOBAL.$ = GLOBAL.jQuery = (input) -> - console.log 'Ignored jQuery: ' + input if debug + console.log 'Ignored jQuery: ' + input if options.debug append: (input)-> exports: ()-> cookies = request.jar() @@ -111,8 +114,8 @@ $.ajax = (options) -> #console.warn JSON.stringify data #data = JSON.stringify data - console.log "Requesting: " + JSON.stringify options if debug - console.log "URL: " + url if debug + console.log "Requesting: " + JSON.stringify options if options.debug + console.log "URL: " + url if options.debug request url: url jar: cookies @@ -120,17 +123,17 @@ $.ajax = (options) -> method: options.type body: data , (error, response, body) -> - console.log "HTTP Request:" + JSON.stringify options if debug and not error + console.log "HTTP Request:" + JSON.stringify options if options.debug and not error if responded - console.log "\t↳Already returned before." if debug + console.log "\t↳Already returned before." if options.debug return if (error) console.warn "\t↳Returned: error: #{error}" options.error(error) if options.error? else - console.log "\t↳Returned: statusCode #{response.statusCode}: #{if options.parse then JSON.stringify body else body}" if debug + console.log "\t↳Returned: statusCode #{response.statusCode}: #{if options.parse then JSON.stringify body else body}" if options.debug options.success(body, response, status: response.statusCode) if options.success? @@ -161,11 +164,11 @@ do (setupLodash = this) -> # load Backbone. Needs hooked loader to reroute underscore to lodash. hook() -GLOBAL.Backbone = require bowerComponents + 'backbone/backbone' +GLOBAL.Backbone = require bowerComponentsPath + 'backbone/backbone' unhook() Backbone.$ = $ -require bowerComponents + 'validated-backbone-mediator/backbone-mediator' +require bowerComponentsPath + 'validated-backbone-mediator/backbone-mediator' # Instead of mediator, dummy might be faster yet suffice? #Mediator = class Mediator # publish: (id, object) -> @@ -198,10 +201,6 @@ $.ajax LevelLoader = require 'lib/LevelLoader' GoalManager = require 'lib/world/GoalManager' - - - workerCode = require headlessClient + 'worker_world' - SuperModel = require 'models/SuperModel' log = require 'winston' @@ -210,8 +209,6 @@ $.ajax Simulator = require 'lib/simulator/Simulator' - GLOBAL.testFile = require headlessClient + 'test.js' + sim = new Simulator options - sim = new Simulator workerCode - - sim.fetchAndSimulateTask() \ No newline at end of file + sim.fetchAndSimulateTask() From ca8d5c1f3bcd12052ffe882d8f4066b76436bdf3 Mon Sep 17 00:00:00 2001 From: Nick Winter <livelily@gmail.com> Date: Tue, 6 May 2014 11:02:53 -0700 Subject: [PATCH 12/12] Stop bobbing and more marks when world ended. --- app/lib/surface/CocoSprite.coffee | 6 +++++- app/lib/surface/SpriteBoss.coffee | 4 ++-- app/views/play/spectate_view.coffee | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/lib/surface/CocoSprite.coffee b/app/lib/surface/CocoSprite.coffee index 55a1982e4..225c6b308 100644 --- a/app/lib/surface/CocoSprite.coffee +++ b/app/lib/surface/CocoSprite.coffee @@ -168,10 +168,12 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass stop: -> @imageObject?.stop?() mark.stop() for name, mark of @marks + @stopped = true play: -> @imageObject?.play?() mark.play() for name, mark of @marks + @stopped = false update: (frameChanged) -> # Gets the sprite to reflect what the current state of the thangs and surface are @@ -222,7 +224,8 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass getBobOffset: -> return 0 unless @thang.bobHeight - @thang.bobHeight * (1 + Math.sin(@age * Math.PI / @thang.bobTime)) + return @lastBobOffset if @stopped + return @lastBobOffset = @thang.bobHeight * (1 + Math.sin(@age * Math.PI / @thang.bobTime)) getWorldPosition: -> p1 = if @possessed then @shadow.pos else @thang.pos @@ -495,6 +498,7 @@ module.exports = CocoSprite = class CocoSprite extends CocoClass updateEffectMarks: -> return if _.isEqual @thang.effectNames, @previousEffectNames + return if @stopped for effect in @thang.effectNames mark = @addMark effect, @options.floatingLayer, effect mark.statusEffect = true diff --git a/app/lib/surface/SpriteBoss.coffee b/app/lib/surface/SpriteBoss.coffee index 03e05e0c3..1c7916817 100644 --- a/app/lib/surface/SpriteBoss.coffee +++ b/app/lib/surface/SpriteBoss.coffee @@ -221,12 +221,12 @@ module.exports = class SpriteBoss extends CocoClass onCastSpells: -> @stop() play: -> - sprite.imageObject.play() for sprite in @spriteArray + sprite.play() for sprite in @spriteArray @selectionMark?.play() @targetMark?.play() stop: -> - sprite.imageObject.stop() for sprite in @spriteArray + sprite.stop() for sprite in @spriteArray @selectionMark?.stop() @targetMark?.stop() diff --git a/app/views/play/spectate_view.coffee b/app/views/play/spectate_view.coffee index 8c72f742b..97bcc0a02 100644 --- a/app/views/play/spectate_view.coffee +++ b/app/views/play/spectate_view.coffee @@ -8,7 +8,7 @@ World = require 'lib/world/world' # tools Surface = require 'lib/surface/Surface' -God = require 'lib/Buddha' # 'lib/God' +God = require 'lib/God' # 'lib/Buddha' GoalManager = require 'lib/world/GoalManager' ScriptManager = require 'lib/scripts/ScriptManager' LevelLoader = require 'lib/LevelLoader'