diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 0c0d8dd64..51b2e2789 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -530,11 +530,20 @@ module.exports = Surface = class Surface extends CocoClass
       newWidth = 0.55 * pageWidth
       newHeight = newWidth / aspectRatio
     return unless newWidth > 0 and newHeight > 0
-    return if newWidth is oldWidth and newHeight is oldHeight and not @options.spectateGame
-    return if newWidth < 200 or newHeight < 200
+
     #scaleFactor = if application.isIPadApp then 2 else 1  # Retina
     scaleFactor = 1
-    @normalCanvas.add(@webGLCanvas).attr width: newWidth * scaleFactor, height: newHeight * scaleFactor
+    if @options.stayVisible
+      availableHeight = window.innerHeight
+      availableHeight -= $('.ad-container').outerHeight() 
+      availableHeight -= $('#game-area').outerHeight() - $('#canvas-wrapper').outerHeight()
+      scaleFactor = availableHeight / newHeight if availableHeight < newHeight
+    newWidth *= scaleFactor
+    newHeight *= scaleFactor
+
+    return if newWidth is oldWidth and newHeight is oldHeight and not @options.spectateGame
+    return if newWidth < 200 or newHeight < 200
+    @normalCanvas.add(@webGLCanvas).attr width: newWidth, height: newHeight
 
     # Cannot do this to the webGLStage because it does not use scaleX/Y.
     # Instead the LayerAdapter scales webGL-enabled layers.
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index e1b26dd8b..6f12fe859 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -502,6 +502,7 @@
     feature5: "Video tutorials"
     feature6: "Premium email support"
     feature7: "Private <strong>Clans</strong>"
+    feature8: "<strong>No ads!</strong>"
     free: "Free"
     month: "month"
     must_be_logged: "You must be logged in first. Please create an account or log in from the menu above."
diff --git a/app/models/User.coffee b/app/models/User.coffee
index 356fa46fe..8ea24ea5c 100644
--- a/app/models/User.coffee
+++ b/app/models/User.coffee
@@ -77,7 +77,7 @@ module.exports = class User extends CocoModel
 
   # y = a * ln(1/b * (x + c)) + 1
   @levelFromExp: (xp) ->
-    if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + c))) + 1 else 1
+    if xp > 0 then Math.floor(a * Math.log((1 / b) * (xp + c))) + 1 else 1
 
   # x = b * e^((y-1)/a) - c
   @expForLevel: (level) ->
@@ -137,6 +137,16 @@ module.exports = class User extends CocoModel
     application.tracker.identify announcesActionAudioGroup: @announcesActionAudioGroup unless me.isAdmin()
     @announcesActionAudioGroup
 
+  getCampaignAdsGroup: ->
+    return @campaignAdsGroup if @campaignAdsGroup
+    group = me.get('testGroupNumber') % 2
+    @campaignAdsGroup = switch group
+      when 0 then 'no-ads'
+      when 1 then 'leaderboard-ads'
+    @campaignAdsGroup = 'no-ads' if me.isAdmin()
+    application.tracker.identify campaignAdsGroup: @campaignAdsGroup unless me.isAdmin()
+    @campaignAdsGroup
+
   getHomepageGroup: ->
     # Only testing on en-US so localization issues are not a factor
     return 'new-home-student' unless _.string.startsWith(me.get('preferredLanguage', true) or 'en-US', 'en')
diff --git a/app/styles/modal/subscribe-modal.sass b/app/styles/modal/subscribe-modal.sass
index 1d588e8f2..4a9efb066 100644
--- a/app/styles/modal/subscribe-modal.sass
+++ b/app/styles/modal/subscribe-modal.sass
@@ -111,7 +111,7 @@
         text-align: center
       tr
         td
-          padding: 3px
+          padding: 2px
           border-width: 0px
           border-top-width: 1px
           border-color: rgba(85, 85, 85, 0.1)
diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass
index 8fbd22a7f..fd2bddcd0 100644
--- a/app/styles/play/campaign-view.sass
+++ b/app/styles/play/campaign-view.sass
@@ -628,6 +628,14 @@ $gameControlMargin: 30px
     img
       height: 30px
 
+  .ad-container
+    width: 100%
+    height: 90px
+    text-align: center
+
+  .gameplay-container
+    position: absolute
+
 body.ipad #campaign-view
   // iPad only supports up to Kithgard Gates for now.
   .campaign-switch
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index ad2a515c5..fb3f236bc 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -66,6 +66,7 @@ $level-resize-transition-time: 0.5s
 
   .level-content
     position: relative
+    background-color: black
 
   #canvas-wrapper
     top: 50px
@@ -83,14 +84,14 @@ $level-resize-transition-time: 0.5s
   canvas#webgl-surface
     background-color: #333
     z-index: 1
-    
+
   canvas#normal-surface
     z-index: 1
     position: absolute
     top: 0
     left: 0
     pointer-events: none
-    
+
   canvas#webgl-surface, canvas#normal-surface
     display: block
     z-index: 2
@@ -259,6 +260,10 @@ $level-resize-transition-time: 0.5s
     right: 15px
     font-size: 30px
 
+  .ad-container
+    width: 100%
+    height: 90px
+    text-align: center
 
 html.fullscreen-editor
   #level-view
diff --git a/app/templates/core/subscribe-modal.jade b/app/templates/core/subscribe-modal.jade
index fec7039f7..b0eca0241 100644
--- a/app/templates/core/subscribe-modal.jade
+++ b/app/templates/core/subscribe-modal.jade
@@ -68,18 +68,26 @@
               span.glyphicon.glyphicon-ok
           tr
             td.feature-description
-              span(data-i18n="subscribe.feature6")
+              span(data-i18n="[html]subscribe.feature7")
             if !me.isOnPremiumServer()
               td.free-cell
             td.center-ok
               span.glyphicon.glyphicon-ok
           tr
             td.feature-description
-              span(data-i18n="[html]subscribe.feature7")
+              span(data-i18n="subscribe.feature6")
             if !me.isOnPremiumServer()
               td.free-cell
             td.center-ok
               span.glyphicon.glyphicon-ok
+          if me.getCampaignAdsGroup() === 'leaderboard-ads'
+            tr
+              td.feature-description
+                span(data-i18n="[html]subscribe.feature8")
+              if !me.isOnPremiumServer()
+                td.free-cell
+              td.center-ok
+                span.glyphicon.glyphicon-ok
       #parents-info(data-i18n="subscribe.parents")
       #payment-methods-info(data-i18n="subscribe.payment_methods")
 
diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade
index b2b0aa87b..96b3bfe32 100644
--- a/app/templates/play/campaign-view.jade
+++ b/app/templates/play/campaign-view.jade
@@ -1,165 +1,182 @@
-a(href="/").picoctf-hide
-  img.small-nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
-
-.picoctf-show
-  a(href="http://staging.picoctf.com").picoctf-logo
-    img.small-nav-logo(src="http://picoctf.com/img/2014_logo_blue2.svg", title="picoCTF home", alt="picoCTF home")
-  a(href="http://codecombat.com").picoctf-powered-by
-    em.spr powered by
-    img(src="/images/pages/base/logo.png", title="Powered by CodeCombat - Learn how to code by playing a game ", alt="Powered by CodeCombat")
-
-if campaign
-  .map
-    .gradient.horizontal-gradient.top-gradient
-    .gradient.vertical-gradient.right-gradient
-    .gradient.horizontal-gradient.bottom-gradient
-    .gradient.vertical-gradient.left-gradient
-    .map-background(alt="", draggable="false")
-
-    each level in levels
-      if !level.hidden
-        div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + (levelStatusMap[level.slug] || ""), data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
-          if level.unlocksHero && (!level.purchasedHero || editorMode)
-            img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png")
-          a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
-          if level.slug == 'lost-viking'
-            img.star(src="/file/db/thang.type/5441c3144e9aeb727cc97111/portrait.png")
-          else if level.requiresSubscription
-            img.star(src="/images/pages/play/star.png")
-          if levelStatusMap[level.slug] === 'complete'
-            img.banner(src="/images/pages/play/level-banner-complete.png")
-          if levelStatusMap[level.slug] === 'started'
-            img.banner(src="/images/pages/play/level-banner-started.png")
-          if levelDifficultyMap[level.slug]
-            .level-difficulty-banner-text= levelDifficultyMap[level.slug]
-        div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + (levelStatusMap[level.slug] || ""))
-        .level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
-          - var playCount = levelPlayCountMap[level.slug]
-          .progress.progress-striped.active.hide
-            .progress-bar(style="width: 100%")
-          - var showsLeaderboard = levelStatusMap[level.slug] === 'complete' && ((level.scoreTypes && level.scoreTypes.length) || ['hero-ladder', 'course-ladder'].indexOf(level.type) !== -1);
-
-          div(class="level-info " + (levelStatusMap[level.slug] || "") + (level.requiresSubscription ? " premium" : "") + (showsLeaderboard ? " shows-leaderboard" : ""))
-            .level-status
-            h3= i18n(level, 'name') + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
-            - var description = i18n(level, 'description') || level.description || ""
-            .level-description!= marked(description, {sanitize: !picoCTF})
-            if level.disabled
-              p
-                span.spr(data-i18n="play.awaiting_levels_adventurer_prefix") We release five levels per week.
-                a.spr(href="/contribute/adventurer")
-                  strong(data-i18n="play.awaiting_levels_adventurer") Sign up as an Adventurer
-                span.spl(data-i18n="play.awaiting_levels_adventurer_suffix") to be the first to play new levels.
-            if level.displayConcepts && level.displayConcepts.length
-              p
-                for concept in level.displayConcepts
-                  kbd(data-i18n="concepts." + concept)
-
-            if !level.disabled && !level.locked
-              if playCount && playCount.sessions
-                .play-counts.hidden
-                  span.spl.spr= playCount.sessions
-                  span(data-i18n="play.players") players
-                  span.spr , #{Math.round(playCount.playtime / 3600)}
-                  span(data-i18n="play.hours_played") hours played
-              if showsLeaderboard
-                button.btn.btn-warning.btn.btn-lg.btn-illustrated.view-solutions(data-level-slug=level.slug)
-                  span(data-i18n="leaderboard.scores")
-              button.btn.btn-success.btn.btn-lg.btn-illustrated.start-level(data-i18n="common.play") Play
-              if me.get('courseInstances') && me.get('courseInstances').length
-                .course-version.hidden(data-level-original=level.original)
-                  em(data-i18n="general.or")
-                  | ...
-                  br
-                  button.btn.btn-primary.btn.btn-lg.btn-illustrated
-                    span(data-i18n="play.play_classroom_version") Play Classroom Version
-      else if level.unlocksHero && !level.purchasedHero
-        img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;")
-
-    for adjacentCampaign in adjacentCampaigns
-      a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug)
-        span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
-
-else
-  .portal
-    .portals
-      for campaignSlug in ['dungeon', 'forest', 'desert', 'mountain', 'glacier', 'volcano']
-        - var campaign = campaigns[campaignSlug];
-        - var godmode = me.get('permissions', true).indexOf('godmode') != -1;
-        div(class="campaign #{campaignSlug}" + (campaign ? "" : " silhouette") + (campaign && campaign.locked && !godmode ? " locked" : ""), data-campaign-slug=campaignSlug)
-          .campaign-label
-            h2.campaign-name
-              if campaign
-                span= i18n(campaign.attributes, 'fullName')
-              else
-                span ???
-            if campaign && campaign.levelsTotal
-              h3.levels-completed
-                span= campaign.levelsCompleted
-                | /
-                span= campaign.levelsTotal
-            if campaign && campaign.locked && !godmode
-              h3.campaign-locked(data-i18n="play.locked") Locked
-            else if campaign
-              btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button
-            if campaign && campaign.get('description')
-              p.campaign-description
-                span= i18n(campaign.attributes, 'description')
-
-.game-controls.header-font.picoctf-hide
-  button.btn.poll.hidden(data-i18n="[title]play.poll")
-  a.btn.clans(href="/clans", data-i18n="[title]clans.clans")
-  button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
-  button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
-  button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
-  if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp
-    button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
-  if !me.get('anonymous', true)
-    button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
-  //if me.isAdmin()
-  //  button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
-  if me.get('anonymous', true)
-    button.btn.settings(data-toggle='coco-modal', data-target='core/CreateAccountModal', data-i18n="[title]play.settings")
-
-.user-status.header-font.picoctf-hide
-  .user-status-line
-    span.gem.gem-30
-    span#gems-count.spr= me.gems()
-    span.level-indicator(data-i18n="general.player_level")
-    span.player-level.spr= me.level()
-    span.player-hero-icon
-    if me.get('anonymous')
-      span.player-name.spr(data-i18n="play.anonymous") Anonymous Player
-      button.btn.btn-illustrated.login-button.btn-warning(data-i18n="login.log_in")
-      button.btn.btn-illustrated.signup-button.btn-danger(data-i18n="signup.sign_up")
+if view.showAds()
+  // TODO: loading this multiple times yields script error:
+  // Uncaught TagError: adsbygoogle.push() error: All ins elements in the DOM with class=adsbygoogle already have ads in them.
+  .ad-container
+    if campaign
+      script(async, src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js")
+      ins.adsbygoogle(style="display:inline-block;width:728px;height:90px", data-ad-client="ca-pub-6640930638193614", data-ad-slot="4924994487")
+      script.
+        (adsbygoogle = window.adsbygoogle || []).push({});
     else
-      a(data-toggle="coco-modal", data-target="play/modal/PlayAccountModal").player-name.spr= me.get('name')
-      button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out
-      if me.isPremium()
-        button.btn.btn-illustrated.btn-primary(data-i18n="nav.contact", data-toggle="coco-modal", data-target="core/ContactModal") Contact
+      script(async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js")
+      ins.adsbygoogle(style="display:inline-block;width:728px;height:90px", data-ad-client="ca-pub-6640930638193614", data-ad-slot="4469166082")
+      script.
+        (adsbygoogle = window.adsbygoogle || []).push({});
 
-button.btn.btn-lg.btn-inverse.campaign-control-button.picoctf-hide#volume-button(data-i18n="[title]play.adjust_volume", title="Adjust volume")
-  .glyphicon.glyphicon-volume-off
-  .glyphicon.glyphicon-volume-down
-  .glyphicon.glyphicon-volume-up
+// TODO: .gameplay-container causes world map buttons to briefly appear in top left of screen
+.gameplay-container
+  a(href="/").picoctf-hide
+    img.small-nav-logo(src="/images/pages/base/logo.png", title="CodeCombat - Learn how to code by playing a game", alt="CodeCombat")
 
-if campaign && !editorMode
-  button.btn.btn-lg.btn-inverse.campaign-control-button.picoctf-hide#back-button(data-i18n="[title]resources.campaigns", title="Campaigns")
-    .glyphicon.glyphicon-globe
+  .picoctf-show
+    a(href="http://staging.picoctf.com").picoctf-logo
+      img.small-nav-logo(src="http://picoctf.com/img/2014_logo_blue2.svg", title="picoCTF home", alt="picoCTF home")
+    a(href="http://codecombat.com").picoctf-powered-by
+      em.spr powered by
+      img(src="/images/pages/base/logo.png", title="Powered by CodeCombat - Learn how to code by playing a game ", alt="Powered by CodeCombat")
 
-if editorMode
-  button.btn.btn-lg.btn-inverse.campaign-control-button#clear-storage-button(data-i18n="[title]editor.clear_storage", title="Clear your local changes")
-    .glyphicon.glyphicon-refresh
+  if campaign
+    .map
+      .gradient.horizontal-gradient.top-gradient
+      .gradient.vertical-gradient.right-gradient
+      .gradient.horizontal-gradient.bottom-gradient
+      .gradient.vertical-gradient.left-gradient
+      .map-background(alt="", draggable="false")
 
-if campaign && campaign.loaded
-  h1#campaign-status.picoctf-hide
-    .campaign-status-background
-      .campaign-name
-        - var fullName = i18n(campaign.attributes, 'fullName')
-        if (me.get('preferredLanguage', true) || 'en-US').split('-')[0] == 'en' || fullName != campaign.get('fullName')
-          // We have a translation.
-          span= fullName
-      .levels-completed
-        span= levelsCompleted
-        | /
-        span= levelsTotal
+      each level in levels
+        if !level.hidden
+          div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + (levelStatusMap[level.slug] || ""), data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
+            if level.unlocksHero && (!level.purchasedHero || editorMode)
+              img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png")
+            a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
+            if level.slug == 'lost-viking'
+              img.star(src="/file/db/thang.type/5441c3144e9aeb727cc97111/portrait.png")
+            else if level.requiresSubscription
+              img.star(src="/images/pages/play/star.png")
+            if levelStatusMap[level.slug] === 'complete'
+              img.banner(src="/images/pages/play/level-banner-complete.png")
+            if levelStatusMap[level.slug] === 'started'
+              img.banner(src="/images/pages/play/level-banner-started.png")
+            if levelDifficultyMap[level.slug]
+              .level-difficulty-banner-text= levelDifficultyMap[level.slug]
+          div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + (levelStatusMap[level.slug] || ""))
+          .level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
+            - var playCount = levelPlayCountMap[level.slug]
+            .progress.progress-striped.active.hide
+              .progress-bar(style="width: 100%")
+            - var showsLeaderboard = levelStatusMap[level.slug] === 'complete' && ((level.scoreTypes && level.scoreTypes.length) || ['hero-ladder', 'course-ladder'].indexOf(level.type) !== -1);
+
+            div(class="level-info " + (levelStatusMap[level.slug] || "") + (level.requiresSubscription ? " premium" : "") + (showsLeaderboard ? " shows-leaderboard" : ""))
+              .level-status
+              h3= i18n(level, 'name') + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
+              - var description = i18n(level, 'description') || level.description || ""
+              .level-description!= marked(description, {sanitize: !picoCTF})
+              if level.disabled
+                p
+                  span.spr(data-i18n="play.awaiting_levels_adventurer_prefix") We release five levels per week.
+                  a.spr(href="/contribute/adventurer")
+                    strong(data-i18n="play.awaiting_levels_adventurer") Sign up as an Adventurer
+                  span.spl(data-i18n="play.awaiting_levels_adventurer_suffix") to be the first to play new levels.
+              if level.displayConcepts && level.displayConcepts.length
+                p
+                  for concept in level.displayConcepts
+                    kbd(data-i18n="concepts." + concept)
+
+              if !level.disabled && !level.locked
+                if playCount && playCount.sessions
+                  .play-counts.hidden
+                    span.spl.spr= playCount.sessions
+                    span(data-i18n="play.players") players
+                    span.spr , #{Math.round(playCount.playtime / 3600)}
+                    span(data-i18n="play.hours_played") hours played
+                if showsLeaderboard
+                  button.btn.btn-warning.btn.btn-lg.btn-illustrated.view-solutions(data-level-slug=level.slug)
+                    span(data-i18n="leaderboard.scores")
+                button.btn.btn-success.btn.btn-lg.btn-illustrated.start-level(data-i18n="common.play") Play
+                if me.get('courseInstances') && me.get('courseInstances').length
+                  .course-version.hidden(data-level-original=level.original)
+                    em(data-i18n="general.or")
+                    | ...
+                    br
+                    button.btn.btn-primary.btn.btn-lg.btn-illustrated
+                      span(data-i18n="play.play_classroom_version") Play Classroom Version
+        else if level.unlocksHero && !level.purchasedHero
+          img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;")
+
+      for adjacentCampaign in adjacentCampaigns
+        a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug)
+          span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
+
+  else
+    .portal
+      .portals
+        for campaignSlug in ['dungeon', 'forest', 'desert', 'mountain', 'glacier', 'volcano']
+          - var campaign = campaigns[campaignSlug];
+          - var godmode = me.get('permissions', true).indexOf('godmode') != -1;
+          div(class="campaign #{campaignSlug}" + (campaign ? "" : " silhouette") + (campaign && campaign.locked && !godmode ? " locked" : ""), data-campaign-slug=campaignSlug)
+            .campaign-label
+              h2.campaign-name
+                if campaign
+                  span= i18n(campaign.attributes, 'fullName')
+                else
+                  span ???
+              if campaign && campaign.levelsTotal
+                h3.levels-completed
+                  span= campaign.levelsCompleted
+                  | /
+                  span= campaign.levelsTotal
+              if campaign && campaign.locked && !godmode
+                h3.campaign-locked(data-i18n="play.locked") Locked
+              else if campaign
+                btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button
+              if campaign && campaign.get('description')
+                p.campaign-description
+                  span= i18n(campaign.attributes, 'description')
+
+  .game-controls.header-font.picoctf-hide
+    button.btn.poll.hidden(data-i18n="[title]play.poll")
+    a.btn.clans(href="/clans", data-i18n="[title]clans.clans")
+    button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
+    button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
+    button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
+    if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp
+      button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
+    if !me.get('anonymous', true)
+      button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
+    //if me.isAdmin()
+    //  button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
+    if me.get('anonymous', true)
+      button.btn.settings(data-toggle='coco-modal', data-target='core/CreateAccountModal', data-i18n="[title]play.settings")
+
+  .user-status.header-font.picoctf-hide
+    .user-status-line
+      span.gem.gem-30
+      span#gems-count.spr= me.gems()
+      span.level-indicator(data-i18n="general.player_level")
+      span.player-level.spr= me.level()
+      span.player-hero-icon
+      if me.get('anonymous')
+        span.player-name.spr(data-i18n="play.anonymous") Anonymous Player
+        button.btn.btn-illustrated.login-button.btn-warning(data-i18n="login.log_in")
+        button.btn.btn-illustrated.signup-button.btn-danger(data-i18n="signup.sign_up")
+      else
+        a(data-toggle="coco-modal", data-target="play/modal/PlayAccountModal").player-name.spr= me.get('name')
+        button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out
+        if me.isPremium()
+          button.btn.btn-illustrated.btn-primary(data-i18n="nav.contact", data-toggle="coco-modal", data-target="core/ContactModal") Contact
+
+  button.btn.btn-lg.btn-inverse.campaign-control-button.picoctf-hide#volume-button(data-i18n="[title]play.adjust_volume", title="Adjust volume")
+    .glyphicon.glyphicon-volume-off
+    .glyphicon.glyphicon-volume-down
+    .glyphicon.glyphicon-volume-up
+
+  if campaign && !editorMode
+    button.btn.btn-lg.btn-inverse.campaign-control-button.picoctf-hide#back-button(data-i18n="[title]resources.campaigns", title="Campaigns")
+      .glyphicon.glyphicon-globe
+
+  if editorMode
+    button.btn.btn-lg.btn-inverse.campaign-control-button#clear-storage-button(data-i18n="[title]editor.clear_storage", title="Clear your local changes")
+      .glyphicon.glyphicon-refresh
+
+  if campaign && campaign.loaded
+    h1#campaign-status.picoctf-hide
+      .campaign-status-background
+        .campaign-name
+          - var fullName = i18n(campaign.attributes, 'fullName')
+          if (me.get('preferredLanguage', true) || 'en-US').split('-')[0] == 'en' || fullName != campaign.get('fullName')
+            // We have a translation.
+            span= fullName
+        .levels-completed
+          span= levelsCompleted
+          | /
+          span= levelsTotal
diff --git a/app/templates/play/level.jade b/app/templates/play/level.jade
index 02bc0c9b7..5bf5a8efe 100644
--- a/app/templates/play/level.jade
+++ b/app/templates/play/level.jade
@@ -1,49 +1,59 @@
-#level-loading-view
+if view.showAds() 
+  // TODO: loading this multiple times yields script error:
+  // Uncaught TagError: adsbygoogle.push() error: All ins elements in the DOM with class=adsbygoogle already have ads in them.
+  .ad-container
+    script(async, src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js")
+    ins.adsbygoogle(style="display:inline-block;width:728px;height:90px", data-ad-client="ca-pub-6640930638193614", data-ad-slot="5527096883")
+    script.
+      (adsbygoogle = window.adsbygoogle || []).push({});
 
-.level-content
-  #control-bar-view
+.game-container
+  #level-loading-view
 
-  #fullscreen-editor-background-screen(title="Click to minimize the code editor")
+  .level-content
+    #control-bar-view
 
-  #code-area
-    #code-area-gradient.gradient
-    #tome-view
+    #fullscreen-editor-background-screen(title="Click to minimize the code editor")
 
-  #game-area
+    #code-area
+      #code-area-gradient.gradient
+      #tome-view
 
-    #canvas-wrapper
-      canvas(width=924, height=589)#webgl-surface
-      canvas(width=924, height=589)#normal-surface
-      #ascii-surface
-      #canvas-left-gradient.gradient
-      #canvas-top-gradient.gradient
-      #goals-view
+    #game-area
 
-    #level-flags-view
+      #canvas-wrapper
+        canvas(width=924, height=589)#webgl-surface
+        canvas(width=924, height=589)#normal-surface
+        #ascii-surface
+        #canvas-left-gradient.gradient
+        #canvas-top-gradient.gradient
+        #goals-view
 
-    #gold-view
+      #level-flags-view
 
-    #problem-alert-view
+      #gold-view
 
-    #level-chat-view
+      #problem-alert-view
 
-    #multiplayer-status-view
+      #level-chat-view
 
-    #duel-stats-view
+      #multiplayer-status-view
 
-    #playback-view
+      #duel-stats-view
 
-    #thang-hud
+      #playback-view
 
-    #level-dialogue-view
+      #thang-hud
 
-  button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip
+      #level-dialogue-view
 
-#level-footer-shadow
-#level-footer-background
+    button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip
 
-if !me.get('anonymous')
-  #play-footer(class=me.isPremium() ? "premium" : "")
-    p(class='footer-link-text').picoctf-hide
-      a.contact-link(title='Send CodeCombat a message', tabindex=-1, data-i18n="nav.contact") Contact
+  #level-footer-shadow
+  #level-footer-background
+
+  if !me.get('anonymous')
+    #play-footer(class=me.isPremium() ? "premium" : "")
+      p(class='footer-link-text').picoctf-hide
+        a.contact-link(title='Send CodeCombat a message', tabindex=-1, data-i18n="nav.contact") Contact
 
diff --git a/app/templates/play/modal/buy-gems-modal.jade b/app/templates/play/modal/buy-gems-modal.jade
index c67c31ca0..261fa9e41 100644
--- a/app/templates/play/modal/buy-gems-modal.jade
+++ b/app/templates/play/modal/buy-gems-modal.jade
@@ -22,7 +22,7 @@
         .product
           h4.subscription-gem-amount x{{gems}} / mo
           h3(data-i18n="account.subscription")
-          if me.isPremium()
+          if me.hasSubscription()
             button.disabled.start-subscription-button.btn.btn-lg.btn-illustrated.btn-success
               | ✓ 
               span(data-i18n="account.subscribed")
diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee
index f2fbfee90..ffb0da0b6 100644
--- a/app/views/play/CampaignView.coffee
+++ b/app/views/play/CampaignView.coffee
@@ -266,6 +266,11 @@ module.exports = class CampaignView extends RootView
     authModal.mode = 'signup'
     @openModalView authModal
 
+  showAds: ->
+    if application.isProduction() && !me.isPremium() && !me.isTeacher() && !window.serverConfig.picoCTF
+      return me.getCampaignAdsGroup() is 'leaderboard-ads'
+    false
+
   annotateLevel: (level) ->
     level.position ?= { x: 10, y: 10 }
     level.locked = not me.ownsLevel level.original
@@ -554,6 +559,7 @@ module.exports = class CampaignView extends RootView
     aspectRatio = mapWidth / mapHeight
     pageWidth = @$el.width()
     pageHeight = @$el.height()
+    pageHeight -= adContainerHeight if adContainerHeight = $('.ad-container').outerHeight()
     widthRatio = pageWidth / mapWidth
     heightRatio = pageHeight / mapHeight
     # Make sure we can see the whole map, fading to background in one dimension.
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index b9a4590e1..71c134ca8 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -148,6 +148,13 @@ module.exports = class PlayLevelView extends RootView
       application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: @loadDuration
       application.tracker?.trackTiming @loadDuration, 'Level Load Time', @levelID, @levelID
 
+  isCourseMode: -> @courseID and @courseInstanceID
+
+  showAds: ->
+    if application.isProduction() && !me.isPremium() && !me.isTeacher() && !window.serverConfig.picoCTF && !@isCourseMode()
+      return me.getCampaignAdsGroup() is 'leaderboard-ads'
+    false
+
   # CocoView overridden methods ###############################################
 
   getRenderData: ->
@@ -326,7 +333,13 @@ module.exports = class PlayLevelView extends RootView
   initSurface: ->
     webGLSurface = $('canvas#webgl-surface', @$el)
     normalSurface = $('canvas#normal-surface', @$el)
-    @surface = new Surface(@world, normalSurface, webGLSurface, thangTypes: @supermodel.getModels(ThangType), observing: @observing, playerNames: @findPlayerNames(), levelType: @level.get('type', true))
+    surfaceOptions =
+      thangTypes: @supermodel.getModels(ThangType)
+      observing: @observing
+      playerNames: @findPlayerNames()
+      levelType: @level.get('type', true)
+      stayVisible: @showAds()
+    @surface = new Surface(@world, normalSurface, webGLSurface, surfaceOptions)
     worldBounds = @world.getBounds()
     bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
     @surface.camera.setBounds(bounds)
@@ -499,7 +512,8 @@ module.exports = class PlayLevelView extends RootView
         break
     Backbone.Mediator.publish 'tome:cast-spell', {}
 
-  onWindowResize: (e) => @endHighlight()
+  onWindowResize: (e) => 
+    @endHighlight()
 
   onDisableControls: (e) ->
     return if e.controls and not ('level' in e.controls)
@@ -535,7 +549,7 @@ module.exports = class PlayLevelView extends RootView
     @endHighlight()
     options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world}
     ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder'] then HeroVictoryModal else VictoryModal
-    ModalClass = CourseVictoryModal if @courseID and @courseInstanceID
+    ModalClass = CourseVictoryModal if @isCourseMode()
     ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
     victoryModal = new ModalClass(options)
     @openModalView(victoryModal)