Questo browser non supporta i canvas
DiDiLaV, Sala Giochi, Breakout, Codice
//=============================================================================
//
// We need some ECMAScript 5 methods but we need to implement them ourselves
// for older browsers (compatibility: http://kangax.github.com/es5-compat-table/)
//
//  Function.bind:        https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
//  Object.create:        http://javascript.crockford.com/prototypal.html
//  Object.extend:        (defacto standard like jquery $.extend or prototype's Object.extend)
//
//  Object.construct:     our own wrapper around Object.create that ALSO calls
//                        an initialize constructor method if one exists
//
//=============================================================================

if (!Function.prototype.bind) {
  Function.prototype.bind = function(obj) {
    var slice = [].slice,
        args  = slice.call(arguments, 1),
        self  = this,
        nop   = function () {},
        bound = function () {
          return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));   
        };
    nop.prototype   = self.prototype;
    bound.prototype = new nop();
    return bound;
  };
}

if (!Object.create) {
  Object.create = function(base) {
    function F() {};
    F.prototype = base;
    return new F();
  }
}

if (!Object.construct) {
  Object.construct = function(base) {
    var instance = Object.create(base);
    if (instance.initialize)
      instance.initialize.apply(instance, [].slice.call(arguments, 1));
    return instance;
  }
}

if (!Object.extend) {
  Object.extend = function(destination, source) {
    for (var property in source) {
      if (source.hasOwnProperty(property))
        destination[property] = source[property];
    }
    return destination;
  };
}

/* NOT READY FOR PRIME TIME
if (!window.requestAnimationFrame) {// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
  window.requestAnimationFrame = window.webkitRequestAnimationFrame || 
                                 window.mozRequestAnimationFrame    || 
                                 window.oRequestAnimationFrame      || 
                                 window.msRequestAnimationFrame     || 
                                 function(callback, element) {
                                   window.setTimeout(callback, 1000 / 60);
                                 }
}
*/

//=============================================================================
// Minimal DOM Library ($)
//=============================================================================

Element = function() {

  var instance = {

    _extended: true,

    showIf: function(on)      { if (on) this.show(); else this.hide(); },
    show:   function()        { this.style.display = '';      },
    hide:   function()        { this.style.display = 'none';  },
    update: function(content) { this.innerHTML     = content; },

    hasClassName:    function(name)     { return (new RegExp("(^|\s*)" + name + "(\s*|$)")).test(this.className) },
    addClassName:    function(name)     { this.toggleClassName(name, true);  },
    removeClassName: function(name)     { this.toggleClassName(name, false); },
    toggleClassName: function(name, on) {
      var classes = this.className.split(' ');
      var n = classes.indexOf(name);
      on = (typeof on == 'undefined') ? (n < 0) : on;
      if (on && (n < 0))
        classes.push(name);
      else if (!on && (n >= 0))
        classes.splice(n, 1);
      this.className = classes.join(' ');
    }
  };

  var get = function(ele) {
    if (typeof ele == 'string')
      ele = document.getElementById(ele);
    if (!ele._extended)
      Object.extend(ele, instance);
    return ele;
  };

  return get;

}();

$ = Element;

//=============================================================================
// State Machine
//=============================================================================

StateMachine = {

  //---------------------------------------------------------------------------

  create: function(cfg) {

    var target  = cfg.target  || {};
    var events  = cfg.events;

    var n, event, name, can = {};
    for(n = 0 ; n < events.length ; n++) {
      event = events[n];
      name  = event.name;
      can[name] = (can[name] || []).concat(event.from);
      target[name] = this.buildEvent(name, event.from, event.to, target);
    }

    target.current = 'none';
    target.is      = function(state) { return this.current == state; };
    target.can     = function(event) { return can[event].indexOf(this.current) >= 0; };
    target.cannot  = function(event) { return !this.can(event); };

    if (cfg.initial) { // see "initial" qunit tests for examples
      var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow single string to represent initial state, or complex object to configure { state: 'first', event: 'init', defer: true|false }
      name = initial.event || 'startup';
      can[name] = ['none'];
      event = this.buildEvent(name, 'none', initial.state, target);
      if (initial.defer)
        target[name] = event; // allow caller to trigger initial transition event
      else
        event.call(target);
    }

    return target;
  },

  //---------------------------------------------------------------------------

  buildEvent: function(name, from, to, target) {

    return function() {

      if (this.cannot(name))
        throw "event " + name + " innapropriate in current state " + this.current;

      var beforeEvent = this['onbefore' + name];
      if (beforeEvent && (false === beforeEvent.apply(this, arguments)))
        return;

      if (this.current != to) {

        var exitState = this['onleave'  + this.current];
        if (exitState)
          exitState.apply(this, arguments);

        this.current = to;

        var enterState = this['onenter' + to] || this['on' + to];
        if (enterState)
          enterState.apply(this, arguments);
      }

      var afterEvent = this['onafter'  + name] || this['on' + name];
      if (afterEvent)
        afterEvent.apply(this, arguments);
    }

  }

  //---------------------------------------------------------------------------

};

//=============================================================================
// GAME
//=============================================================================

Game = {

  compatible: function() {
    return Object.create &&
           Object.extend &&
           Function.bind &&
           document.addEventListener && // HTML5 standard, all modern browsers that support canvas should also support add/removeEventListener
           Game.ua.hasCanvas
  },

  start: function(id, game, cfg) {
    if (Game.compatible())
      return Game.current = Object.construct(Game.Runner, id, game, cfg).game; // return the game instance, not the runner (caller can always get at the runner via game.runner)
  },

  ua: function() { // should avoid user agent sniffing... but sometimes you just gotta do what you gotta do
    var ua  = navigator.userAgent.toLowerCase();
    var key =        ((ua.indexOf("opera")   > -1) ? "opera"   : null);
        key = key || ((ua.indexOf("firefox") > -1) ? "firefox" : null);
        key = key || ((ua.indexOf("chrome")  > -1) ? "chrome"  : null);
        key = key || ((ua.indexOf("safari")  > -1) ? "safari"  : null);
        key = key || ((ua.indexOf("msie")    > -1) ? "ie"      : null);

    try {
      var re      = (key == "ie") ? "msie (\\d)" : key + "\\/(\\d\\.\\d)"
      var matches = ua.match(new RegExp(re, "i"));
      var version = matches ? parseFloat(matches[1]) : null;
    } catch (e) {}

    return {
      full:      ua, 
      name:      key + (version ? " " + version.toString() : ""),
      version:   version,
      isFirefox: (key == "firefox"),
      isChrome:  (key == "chrome"),
      isSafari:  (key == "safari"),
      isOpera:   (key == "opera"),
      isIE:      (key == "ie"),
      hasCanvas: (document.createElement('canvas').getContext),
      hasAudio:  (typeof(Audio) != 'undefined'),
      hasTouch:  ('ontouchstart' in window)
    }
  }(),

  addEvent:    function(obj, type, fn) { $(obj).addEventListener(type, fn, false);    },
  removeEvent: function(obj, type, fn) { $(obj).removeEventListener(type, fn, false); },

  windowWidth:  function() { return window.innerWidth  || /* ie */ document.documentElement.offsetWidth;  },
  windowHeight: function() { return window.innerHeight || /* ie */ document.documentElement.offsetHeight; },

  ready: function(fn) {
    if (Game.compatible())
      Game.addEvent(document, 'DOMContentLoaded', fn);
  },

  renderToCanvas: function(width, height, render, canvas) { // http://kaioa.com/node/103
    canvas = canvas || document.createElement('canvas');
    canvas.width  = width;
    canvas.height = height;
    render(canvas.getContext('2d'));
    return canvas;
  },

  loadScript: function(src, cb) {
    var head = document.getElementsByTagName('head')[0];
    var s = document.createElement('script');
    head.appendChild(s);
    if (Game.ua.isIE) {
      s.onreadystatechange = function(e) {
        if (e.currentTarget.readyState == 'loaded')
          cb(e.currentTarget);
      }
    }
    else {
      s.onload = function(e) { cb(e.currentTarget); }
    }
    s.type = 'text/javascript';
    s.src = src;
  },

  loadImages: function(sources, callback) { /* load multiple images and callback when ALL have finished loading */
    var images = {};
    var count = sources ? sources.length : 0;
    if (count == 0) {
      callback(images);
    }
    else {
      for(var n = 0 ; n < sources.length ; n++) {
        var source = sources[n];
        var image = document.createElement('img');
        images[source] = image;
        Game.addEvent(image, 'load', function() { if (--count == 0) callback(images); });
        image.src = source;
      }
    }
  },

  loadSounds: function(cfg) {
    cfg = cfg || {};
    if (typeof soundManager == 'undefined') {
      var path = cfg.path || 'sound/soundmanager2-nodebug-jsmin.js';
      var swf  = cfg.swf  || 'sound/swf';
      window.SM2_DEFER = true;
      Game.loadScript(path, function() {
        window.soundManager = new SoundManager();
        soundManager.useHighPerformance = true;
        soundManager.useFastPolling = true;
        soundManager.url = swf;
        soundManager.defaultOptions.volume = 50; // shhh!
        soundManager.onready(function() {
          Game.loadSounds(cfg);
        });
        soundManager.beginDelayedInit();
      });
    }
    else {
      var sounds = [];
      for(var id in cfg.sounds) {
        sounds.push(soundManager.createSound({id: id, url: cfg.sounds[id]}));
      }
      if (cfg.onload)
        cfg.onload(sounds);
    }
  },

  random: function(min, max) {
    return (min + (Math.random() * (max - min)));
  },

  randomChoice: function(choices) {
    return choices[Math.round(Game.random(0, choices.length-1))];
  },

  randomBool: function() {
    return Game.randomChoice([true, false]);
  },

  timestamp: function() { 
    return new Date().getTime();
  },

  THREESIXTY: Math.PI * 2,

  KEY: {
    BACKSPACE: 8,
    TAB:       9,
    RETURN:   13,
    ESC:      27,
    SPACE:    32,
    LEFT:     37,
    UP:       38,
    RIGHT:    39,
    DOWN:     40,
    DELETE:   46,
    HOME:     36,
    END:      35,
    PAGEUP:   33,
    PAGEDOWN: 34,
    INSERT:   45,
    ZERO:     48,
    ONE:      49,
    TWO:      50,
    A:        65,
    D:        68,
    L:        76,
    P:        80,
    Q:        81,
    TILDA:    192
  },

  //-----------------------------------------------------------------------------

  Math: {

    bound: function(box) {
      if (box.radius) {
        box.w      = 2 * box.radius;
        box.h      = 2 * box.radius;
        box.left   = box.x - box.radius;
        box.right  = box.x + box.radius;
        box.top    = box.y - box.radius;
        box.bottom = box.y + box.radius;
      }
      else {
        box.left   = box.x;
        box.right  = box.x + box.w;
        box.top    = box.y;
        box.bottom = box.y + box.h;
      }
      return box;
    },

    overlap: function(box1, box2, returnOverlap) {
      if ((box1.right < box2.left)   ||
          (box1.left  > box2.right)  ||
          (box1.top   > box2.bottom) ||
          (box1.bottom < box2.top)) {
        return false;
      }
      else {
        if (returnOverlap) {
          var left   = Math.max(box1.left,  box2.left);
          var right  = Math.min(box1.right, box2.right);
          var top    = Math.max(box1.top,   box2.top);
          var bottom = Math.min(box1.bottom, box2.bottom);
          return {x: left, y: top, w: right-left, h: bottom-top, left: left, right: right, top: top, bottom: bottom };
        }
        else {
          return true;
        }
      }
    },

    normalize: function(vec, m) {
      vec.m = this.magnitude(vec.x, vec.y);
      if (vec.m == 0) {
        vec.x = vec.y = vec.m = 0;
      }
      else {
        vec.m = vec.m / (m || 1);
        vec.x = vec.x / vec.m;
        vec.y = vec.y / vec.m;
        vec.m = vec.m / vec.m;
      }
      return vec; 
    },

    magnitude: function(x, y) {
      return Math.sqrt(x*x + y*y);
    },

    move: function(x, y, dx, dy, dt) {
      var nx = dx * dt;
      var ny = dy * dt;
      return { x: x + nx, y: y + ny, dx: dx, dy: dy, nx: nx, ny: ny };
    },

    accelerate: function(x, y, dx, dy, accel, dt) {
      var x2  = x + (dt * dx) + (accel * dt * dt * 0.5);
      var y2  = y + (dt * dy) + (accel * dt * dt * 0.5);
      var dx2 = dx + (accel * dt) * (dx > 0 ? 1 : -1);
      var dy2 = dy + (accel * dt) * (dy > 0 ? 1 : -1);
      return { nx: (x2-x), ny: (y2-y), x: x2, y: y2, dx: dx2, dy: dy2 };
    },

    intercept: function(x1, y1, x2, y2, x3, y3, x4, y4, d) {
      var denom = ((y4-y3) * (x2-x1)) - ((x4-x3) * (y2-y1));
      if (denom != 0) {
        var ua = (((x4-x3) * (y1-y3)) - ((y4-y3) * (x1-x3))) / denom;
        if ((ua >= 0) && (ua <= 1)) {
          var ub = (((x2-x1) * (y1-y3)) - ((y2-y1) * (x1-x3))) / denom;
          if ((ub >= 0) && (ub <= 1)) {
            var x = x1 + (ua * (x2-x1));
            var y = y1 + (ua * (y2-y1));
            return { x: x, y: y, d: d};
          }
        }
      }
      return null;
    },

    ballIntercept: function(ball, rect, nx, ny) {
      var pt;
      if (nx < 0) {
        pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, 
                                 rect.right  + ball.radius, 
                                 rect.top    - ball.radius, 
                                 rect.right  + ball.radius, 
                                 rect.bottom + ball.radius, 
                                 "right");
      }
      else if (nx > 0) {
        pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, 
                                 rect.left   - ball.radius, 
                                 rect.top    - ball.radius, 
                                 rect.left   - ball.radius, 
                                 rect.bottom + ball.radius,
                                 "left");
      }
      if (!pt) {
        if (ny < 0) {
          pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, 
                                   rect.left   - ball.radius, 
                                   rect.bottom + ball.radius, 
                                   rect.right  + ball.radius, 
                                   rect.bottom + ball.radius,
                                   "bottom");
        }
        else if (ny > 0) {
          pt = Game.Math.intercept(ball.x, ball.y, ball.x + nx, ball.y + ny, 
                                   rect.left   - ball.radius, 
                                   rect.top    - ball.radius, 
                                   rect.right  + ball.radius, 
                                   rect.top    - ball.radius,
                                   "top");
        }
      }
      return pt;
    }

  },

  //-----------------------------------------------------------------------------

  Runner: {

    initialize: function(id, game, cfg) {
      this.cfg          = Object.extend(game.Defaults || {}, cfg || {}); // use game defaults (if any) and extend with custom cfg (if any)
      this.fps          = this.cfg.fps || 60;
      this.interval     = 1000.0 / this.fps;
      this.canvas       = $(id);
      this.bounds       = this.canvas.getBoundingClientRect();
      this.width        = this.cfg.width  || this.canvas.offsetWidth;
      this.height       = this.cfg.height || this.canvas.offsetHeight;
      this.front        = this.canvas;
      this.front.width  = this.width;
      this.front.height = this.height;
      this.front2d      = this.front.getContext('2d');
      this.addEvents();
      this.resetStats();
      this.resize();

      this.game = Object.construct(game, this, this.cfg); // finally construct the game object itself

      if (this.cfg.state)
        StateMachine.create(Object.extend({target: this.game}, this.cfg.state));

      this.initCanvas();
    },

    start: function() { // game instance should call runner.start() when its finished initializing and is ready to start the game loop
      this.lastFrame = Game.timestamp();
      this.timer     = setInterval(this.loop.bind(this), this.interval);
    },

    stop: function() {
      clearInterval(this.timer);
    },

    loop: function() {
      this._start  = Game.timestamp(); this.update((this._start - this.lastFrame)/1000.0); // send dt as seconds
      this._middle = Game.timestamp(); this.draw();
      this._end    = Game.timestamp();
      this.updateStats(this._middle - this._start, this._end - this._middle);
      this.lastFrame = this._start;
    },

    initCanvas: function() {
      if (this.game && this.game.initCanvas)
        this.game.initCanvas(this.front2d);
    },

    update: function(dt) {
      this.game.update(dt);
    },

    draw: function() {
      this.game.draw(this.front2d);
      this.drawStats(this.front2d);
    },

    resetStats: function() {
      this.stats = {
        count:  0,
        fps:    0,
        update: 0,
        draw:   0, 
        frame:  0  // update + draw
      };
    },

    updateStats: function(update, draw) {
      if (this.cfg.stats) {
        this.stats.update = Math.max(1, update);
        this.stats.draw   = Math.max(1, draw);
        this.stats.frame  = this.stats.update + this.stats.draw;
        this.stats.count  = this.stats.count == this.fps ? 0 : this.stats.count + 1;
        this.stats.fps    = Math.min(this.fps, 1000 / this.stats.frame);
      }
    },

    strings: {
      frame:  "frame: ",
      fps:    "fps: ",
      update: "update: ",
      draw:   "draw: ",
      ms:     "ms"  
    },

    drawStats: function(ctx) {
      if (this.cfg.stats) {
        ctx.fillText(this.strings.frame  + Math.round(this.stats.count),                    this.width - 100, this.height - 60);
        ctx.fillText(this.strings.fps    + Math.round(this.stats.fps),                      this.width - 100, this.height - 50);
        ctx.fillText(this.strings.update + Math.round(this.stats.update) + this.strings.ms, this.width - 100, this.height - 40);
        ctx.fillText(this.strings.draw   + Math.round(this.stats.draw)   + this.strings.ms, this.width - 100, this.height - 30);
      }
    },

    addEvents: function() {
      Game.addEvent(document, 'keydown', this.onkeydown.bind(this));
      Game.addEvent(document, 'keyup',   this.onkeyup.bind(this));
      Game.addEvent(window,   'resize',  this.onresize.bind(this));
    },

    onresize: function() {
      this.stop();
      if (this.onresizeTimer)
        clearTimeout(this.onresizeTimer);
      this.onresizeTimer = setTimeout(this.onresizeend.bind(this), 50); // dont fire resize event until 50ms after user has stopped resizing (avoid flickering)
    },

    onresizeend: function() {
      this.resize();
      this.start();
    },

    resize: function() {
      if ((this.width != this.canvas.offsetWidth) || (this.height != this.front.offsetHeight)) {
        // console.log("CANVAS RESIZED " + this.front.offsetWidth + ", " + this.front.offsetHeight);
        this.width  = this.front.width  = this.front.offsetWidth;
        this.height = this.front.height = this.front.offsetHeight;
        if (this.game && this.game.onresize)
          this.game.onresize(this.width, this.height);
        this.initCanvas(); // when canvas is really resized, its state is reset so we need to re-initialize
      }
    },

    onkeydown: function(ev) {
      if (this.game.onkeydown)
        return this.game.onkeydown(ev.keyCode);
      else if (this.cfg.keys)
        return this.onkey(ev.keyCode, 'down');
    },

    onkeyup: function(ev) {
      if (this.game.onkeyup)
        return this.game.onkeyup(ev.keyCode);
      else if (this.cfg.keys)
        return this.onkey(ev.keyCode, 'up');
    },

    onkey: function(keyCode, mode) {
      var n, k, i, state = this.game.current; // avoid same key event triggering in 2 different states by remembering current state so that even if an earlier keyhandler changes state, the later keyhandler wont kick in.
      for(n = 0 ; n < this.cfg.keys.length ; n++) {
        k = this.cfg.keys[n];
        k.mode = k.mode || 'up';
        if ((k.key == keyCode) || (k.keys && (k.keys.indexOf(keyCode) >= 0))) {
          if (!k.state || (k.state == state)) {
            if (k.mode == mode) {
              k.action.call(this.game);
            }
          }
        }
      }
    },

    storage: function() {
      try {
        return this.localStorage = this.localStorage || window.localStorage || {};
      }
      catch(e) { // IE localStorage throws exceptions when using non-standard port (e.g. during development)
        return this.localStorage = {};
      }
    },

    alert: function(msg) {
      this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update
      result = window.alert(msg);
      this.start();
      return result;
    },

    confirm: function(msg) {
      this.stop(); // alert blocks thread, so need to stop game loop in order to avoid sending huge dt values to next update
      result = window.confirm(msg);
      this.start();
      return result;
    }

    //-------------------------------------------------------------------------

  } // Game.Runner
} // Game


//=============================================================================
// Breakout
//=============================================================================

Breakout = {

  Defaults: {

    fps: 60,
    stats: false,

    score: {
      lives: {
        initial: 3,
        max: 5
      }
    },

    court: {
      xchunks: 30,
      ychunks: 25
    },

    ball: {
      radius:  0.3,
      speed:   15,
      labels: {
        3: { text: 'pronti...', fill: '#D82800', stroke: 'black', font: 'bold 28pt arial' },
        2: { text: 'a posto..',    fill: '#FC9838', stroke: 'black', font: 'bold 28pt arial' },
        1: { text: 'via!',      fill: '#80D010', stroke: 'black', font: 'bold 28pt arial' }
      }
    },

    paddle: {
      width:  6,
      height: 1,
      speed:  20
    },

    color: {
      background: 'rgba(200, 200, 200, 0.5)',
      foreground: 'green',
      border:     '#222',
      wall:       '#333',
      ball:       'black',
      paddle:     'rgb(245,111,37)',
      score:      "#EFD279",
      highscore:  "#AFD775"
    },

    state: {
      initial: 'menu',
      events: [
        { name: 'play',    from: 'menu', to: 'game' },
        { name: 'abandon', from: 'game', to: 'menu' },
        { name: 'lose',    from: 'game', to: 'menu' }
    ]},

    keys: [
      { keys: [Game.KEY.LEFT,  Game.KEY.A],      mode: 'down',  action: function() { this.paddle.moveLeft();          } },
      { keys: [Game.KEY.RIGHT, Game.KEY.D],      mode: 'down',  action: function() { this.paddle.moveRight();         } },
      { keys: [Game.KEY.LEFT,  Game.KEY.A],                     action: function() { this.paddle.stopMovingLeft();    } },
      { keys: [Game.KEY.RIGHT, Game.KEY.D],                     action: function() { this.paddle.stopMovingRight();   } },
      { keys: [Game.KEY.SPACE, Game.KEY.RETURN], state: 'menu', action: function() { this.play();                     } },
      { keys: [Game.KEY.SPACE, Game.KEY.RETURN], state: 'game', action: function() { this.ball.launchNow();           } },
      { key:  Game.KEY.ESC,                      state: 'game', action: function() { this.abandon();                  } },
      { key:  Game.KEY.UP,                       state: 'menu', action: function() { this.nextLevel();                } },
      { key:  Game.KEY.DOWN,                     state: 'menu', action: function() { this.prevLevel();                } }
    ],

    sounds: {
      brick:    '/sound/breakout/brick.mp3',
      paddle:   '/sound/breakout/paddle.mp3',
      go:       '/sound/breakout/go.mp3',
      levelup:  '/sound/breakout/levelup.mp3',
      loselife: '/sound/breakout/loselife.mp3',
      gameover: '/sound/breakout/gameover.mp3'
    }

  },

  //-----------------------------------------------------------------------------

  initialize: function(runner, cfg) {
    this.cfg     = cfg;
    this.runner  = runner;
    this.width   = runner.width;
    this.height  = runner.height;
    this.storage = runner.storage();
    this.color   = cfg.color;
    this.sound   = false;
    this.court   = Object.construct(Breakout.Court,  this, cfg.court);
    this.paddle  = Object.construct(Breakout.Paddle, this, cfg.paddle);
    this.ball    = Object.construct(Breakout.Ball,   this, cfg.ball);
    this.score   = Object.construct(Breakout.Score,  this, cfg.score);
    Game.loadSounds({sounds: cfg.sounds});
  },

  onstartup: function() { // the event that fires the initial state transition occurs when Game.Runner constructs our StateMachine
    this.addEvents();
    this.runner.start(); // start the 60fps update/draw game loop
  },

  addEvents: function() {
    Game.addEvent('prev',  'click',  this.prevLevel.bind(this, false));
    Game.addEvent('next',  'click',  this.nextLevel.bind(this, false));
    Game.addEvent('sound', 'change', this.toggleSound.bind(this, false));

    Game.addEvent('instructions',     'touchstart', this.play.bind(this));
    Game.addEvent(this.runner.canvas, 'touchmove',  this.ontouchmove.bind(this));
    Game.addEvent(document.body,      'touchmove',  function(event) { event.preventDefault(); }); // prevent ipad bouncing up and down when finger scrolled
  },

  toggleSound: function() {
    this.storage.sound = this.sound = !this.sound;
  },

  update: function(dt) {
    this.court.update(dt);
    this.paddle.update(dt);
    this.ball.update(dt);
    this.score.update(dt);
  },

  draw: function(ctx) {
    ctx.save();
    ctx.clearRect(0, 0, this.width, this.height);
    ctx.fillStyle = this.color.background;
    ctx.fillRect(0, 0, this.width, this.height);
    this.court.draw(ctx);
    this.paddle.draw(ctx);
    this.ball.draw(ctx);
    this.score.draw(ctx);
    ctx.restore();
  },

  onresize: function(width, height) {
    this.width  = width;
    this.height = height;
    this.court.resize();
    this.paddle.reset();
    this.ball.reset();
  },

  onmenu: function() {
    this.resetLevel();
    this.paddle.reset();
    this.ball.reset();
    this.refreshDOM();
  },

  ongame: function() {
    this.refreshDOM();
    this.score.reset();
    this.ball.reset({launch: true});
  },

  onlose: function() {
    this.playSound('gameover');
  },

  onleavegame: function() {
    this.score.save();
    this.score.resetLives();
  },

  onbeforeabandon: function() {
    return this.runner.confirm("Abandon game?")
  },

  loseBall: function() {
    this.playSound('loselife');
    if (this.score.loseLife())
      this.lose();
    else {
      this.ball.reset({launch: true});
    }
  },

  winLevel: function() {
    this.playSound('levelup');
    this.score.gainLife();
    this.nextLevel(true);
    this.ball.reset({launch: true});
  },

  hitBrick: function(brick) {
    this.playSound('brick');
    this.court.remove(brick);
    this.score.increase(brick.score);
    this.ball.speed += 10 * (1 - (this.ball.speed / this.ball.maxspeed)); // decay curve - speed increases less the faster the ball is (otherwise game becomes impossible)
    if (this.court.empty())
      this.winLevel();
  },

  resetLevel: function() { this.setLevel(); },
  setLevel: function(level) {
    level = (typeof level == 'undefined') ? (this.storage.level ? parseInt(this.storage.level) : 0) : level;
    level = level < Breakout.Levels.length ? level : 0;
    this.court.reset(level);
    this.storage.level = this.level = level;
    this.refreshDOM();
  },

  canPrevLevel: function()      { return this.is('menu') && (this.level > 0);                          },
  canNextLevel: function()      { return this.is('menu') && (this.level < (Breakout.Levels.length-1)); },
  prevLevel:    function(force) { if (force || this.canPrevLevel()) this.setLevel(this.level - 1);     },
  nextLevel:    function(force) { if (force || this.canNextLevel()) this.setLevel(this.level + 1);     },

  initCanvas: function(ctx) { // called by Game.Runner whenever the canvas is reset (on init and on resize)
    ctx.fillStyle    = this.color.foreground;
    ctx.strokeStyle  = this.color.foreground;
    ctx.lineWidth    = 1;
    this.score.measure(ctx);  // score needs to measure itself
  },

  refreshDOM: function() {
    $('instructions').className = Game.ua.hasTouch ? 'touch' : 'keyboard';
    $('instructions').showIf(this.is('menu'));
    $('prev').toggleClassName('disabled', !this.canPrevLevel());
    $('next').toggleClassName('disabled', !this.canNextLevel());
    $('level').update(this.level + 1);
    $('sound').checked = this.sound;
  },

  playSound: function(id) {
    if (soundManager && this.sound) {
      soundManager.play(id);
    }
  },

  ontouchmove: function(ev) {
    if (ev.targetTouches.length == 1) {
      this.paddle.place(ev.targetTouches[0].pageX - this.runner.bounds.left - this.paddle.w/2); // clientX only works in ios, not on android - must use pageX - yuck
    }
  },

  //=============================================================================

  Score: {

    initialize: function(game, cfg) {
      this.game = game;
      this.cfg  = cfg;
      this.load();
      this.reset();
    },

    reset:    function()  { this.set(0); this.resetLives(); },
    set:      function(n) { this.score = this.vscore = n; this.rerender = true; },
    increase: function(n) { this.score = this.score + n;  this.rerender = true; },
    format:   function(n) { return ("0000000" + n).slice(-7); },
    load:     function()  { this.highscore = this.game.storage.highscore ? parseInt(this.game.storage.highscore) : 1000; },
    save:     function()  { if (this.score > this.highscore) this.game.storage.highscore = this.highscore = this.score;  },

    resetLives: function()  { this.setLives(this.cfg.lives.initial);                       }, 
    setLives:   function(n) { this.lives = n; this.rerender = true;                        },
    gainLife:   function()  { this.setLives(Math.min(this.cfg.lives.max, this.lives + 1)); },
    loseLife:   function()  { this.setLives(this.lives-1); return (this.lives == 0);       },
 
    update: function(dt) {
      if (this.vscore < this.score) {
        this.vscore = Math.min(this.score, this.vscore + 10);
        this.rerender = true;
      }
    },

    measure: function(ctx) {
      this.left   = this.game.court.left;
      this.top    = this.game.court.top - this.game.court.wall.size*2;
      this.width  = this.game.court.width;
      this.height = this.game.court.wall.size*2;
      this.scorefont = "bold " + Math.max(9, this.game.court.wall.size - 2) + "pt arial";
      this.highfont  = ""      + Math.max(9, this.game.court.wall.size - 8) + "pt arial";
      ctx.save();
      ctx.font = this.scorefont;
      this.scorewidth = ctx.measureText(this.format(0)).width;
      ctx.font = this.highfont;
      this.highwidth  = ctx.measureText("HIGH SCORE: " + this.format(0)).width;
      ctx.restore();
      this.rerender = true;
    },

    draw: function(ctx) {
      if (this.rerender) {
        this.canvas = Game.renderToCanvas(this.width, this.height, this.render.bind(this), this.canvas);
        this.rerender = false;
      }
      ctx.drawImage(this.canvas, this.left, this.top);
    },

    render: function(ctx) {
      var text, width, paddle;
      var ishigh = this.game.is('game') && (this.score > this.highscore);

      ctx.textBaseline = "middle";
      ctx.fillStyle    = this.game.color.score;
      ctx.font         = this.scorefont;
      text             = this.format(this.vscore);
      ctx.fillText(text, 0, this.height/2);

      ctx.fillStyle = ishigh ? this.game.color.score : this.game.color.highscore;
      text          = " Punteggio Massimo: " + this.format(ishigh ? this.score : this.highscore);
      ctx.font      = this.highfont;
      width         = ctx.measureText(text).width;
      ctx.fillText(text, this.width - width, this.height/2);

      paddle = {
        game: this.game,
        w:    this.game.court.chunk * 1.5,
        h:    this.game.court.chunk * 2/3
      }
      ctx.translate(this.scorewidth + 20, (this.height-paddle.h) / 2);
      for(var n = 0 ; n < this.lives ; n++) {
        this.game.paddle.render.call(paddle, ctx);
        ctx.translate(paddle.w + 5, 0);
      }

    }

  },

  //=============================================================================

  Court: {

    initialize: function(game, cfg) {
      this.game = game;
      this.cfg  = cfg;
    },
    
    reset: function(level) {
      var layout = Breakout.Levels[level];
      var line, brick, score, c, n, x, y, nx, ny = Math.min(layout.bricks.length, this.cfg.ychunks);
      this.bricks = [];
      for(y = 0 ; y < ny ; y++) {
        score = (this.cfg.ychunks - y) * 5;
        line  = layout.bricks[y] + " "; // extra space simplifies loop
        brick = null;
        nx = Math.min(line.length, this.cfg.xchunks + 1);
        for(x = 0 ; x < nx ; x++) {
          c = line[x];
          if (brick && (brick.c == c)) {
            brick.pos.x2 = x;
          }
          else if (brick && (brick.c != c)) {
             this.bricks.push(brick);
            brick = null;
          }

          if (!brick && (c != ' '))
            brick = { isbrick: true, hit: false, c: c, pos: { x1: x, x2: x, y: y }, score: score, color: layout.colors[c.toLowerCase()] };
        }
      }
      this.numbricks = this.bricks.length;
      this.numhits   = 0;
      this.resize();
    },

    resize: function() {

      this.chunk  = Math.floor(Math.min(this.game.width, this.game.height) / (Math.max(this.cfg.xchunks, this.cfg.ychunks) + 4)); // room for court plus 2 chunk wall either side
      this.width  = this.cfg.xchunks * this.chunk;
      this.height = this.cfg.ychunks * this.chunk;
      this.left   = Math.floor((this.game.width  - this.width)  / 2);
      this.top    = Math.floor((this.game.height - this.height) / 2);
      this.right  = this.left + this.width;
      this.bottom = this.top  + this.height;

      this.wall = {}
      this.wall.size  = this.chunk;
      this.wall.top   = Game.Math.bound({x: this.left - this.wall.size, y: this.top - this.wall.size*2, w: this.width + this.wall.size*2, h: this.wall.size*2               });
      this.wall.left  = Game.Math.bound({x: this.left - this.wall.size, y: this.top - this.wall.size*2, w: this.wall.size,                h: this.wall.size*2 + this.height });
      this.wall.right = Game.Math.bound({x: this.right,                 y: this.top - this.wall.size*2, w: this.wall.size,                h: this.wall.size*2 + this.height });

      for(n = 0 ; n < this.numbricks ; n++) {
        brick = this.bricks[n];
        brick.x = this.left + (brick.pos.x1 * this.chunk);
        brick.y = this.top  + (brick.pos.y  * this.chunk);
        brick.w = (brick.pos.x2 - brick.pos.x1 + 1) * this.chunk;
        brick.h = this.chunk;
        Game.Math.bound(brick);
      }

      this.rerender = true;
    },

    update: function(dt) {
    },

    draw: function(ctx) {
      if (this.rerender) {
        this.canvas = Game.renderToCanvas(this.game.width, this.game.height, this.render.bind(this), this.canvas);
        this.rerender = false;
      }
      ctx.drawImage(this.canvas, 0, 0);
    },

    render: function(ctx) {
      var n, brick;

      ctx.translate(0.5, 0.5); // crisp 1px lines for the brick borders
      ctx.strokeStyle = this.game.color.border;
      ctx.lineWidth = 1;
      for(n = 0 ; n < this.numbricks ; n++) {
        brick = this.bricks[n];
        if (!brick.hit) {
          ctx.fillStyle = brick.color;
          ctx.fillRect(brick.x, brick.y, brick.w, brick.h); 
          ctx.strokeRect(brick.x, brick.y, brick.w, brick.h);
        }
      }

      ctx.fillStyle = this.game.color.wall;
      ctx.lineWidth = 2;
      ctx.beginPath();
      ctx.moveTo(this.wall.top.left,     this.wall.top.top);
      ctx.lineTo(this.wall.top.right,    this.wall.top.top);
      ctx.lineTo(this.wall.top.right,    this.wall.right.bottom);
      ctx.lineTo(this.wall.right.left,   this.wall.right.bottom);
      ctx.lineTo(this.wall.right.left,   this.wall.top.bottom);
      ctx.lineTo(this.wall.left.right,   this.wall.top.bottom);
      ctx.lineTo(this.wall.left.right,   this.wall.left.bottom);
      ctx.lineTo(this.wall.left.left,    this.wall.left.bottom);
      ctx.lineTo(this.wall.top.left,     this.wall.top.top);
      ctx.fill();
      ctx.stroke();
      ctx.closePath();
    },

    remove: function(brick) {
      brick.hit = true;
      this.numhits++;
      this.rerender = true;
    },

    empty: function() {
      return (this.numhits == this.numbricks);
    }

  },

  //=============================================================================

  Ball: {

    initialize: function(game, cfg) {
      this.game = game;
      this.cfg  = cfg;
    },

    reset: function(options) {
      this.radius   = this.cfg.radius * this.game.court.chunk;
      this.speed    = this.cfg.speed  * this.game.court.chunk;
      this.maxspeed = this.speed * 1.5;
      this.color    = this.game.color.ball;
      this.moveToPaddle();
      this.setdir(0, 0);
      this.clearLaunch();
      this.hitTargets = [
        this.game.paddle,
        this.game.court.wall.top,
        this.game.court.wall.left,
        this.game.court.wall.right,
      ].concat(this.game.court.bricks);
      if (options && options.launch)
        this.launch();
    },

    moveToPaddle: function() {
      this.setpos(this.game.paddle.left + (this.game.paddle.w/2), this.game.court.bottom - this.game.paddle.h - this.radius);
    },

    setpos: function(x, y) {
      this.x = x;
      this.y = y;
      Game.Math.bound(this);
    },

    setdir: function(dx, dy) {
      var dir = Game.Math.normalize({ x: dx, y: dy });
      this.dx = dir.x;
      this.dy = dir.y;
      this.moving = dir.m != 0;
    },

    launch: function() {
      if (!this.moving || this.countdown) {
        this.countdown = (typeof this.countdown == 'undefined') || (this.countdown == null) ? 3 : this.countdown - 1;
        if (this.countdown > 0) {
          this.label = this.launchLabel(this.countdown);
          this.delayTimer = setTimeout(this.launch.bind(this), 1000);
          if (this.countdown == 1)
            this.setdir(1, -1); // launch on 'go'
        }
        else {
          this.clearLaunch();
        }
      }
    },

    launchNow: function() { // <space> key can override countdown launch
      if (!this.moving) {
        this.clearLaunch();
        this.setdir(1, -1);
      }
    },

    launchLabel: function(count) {
      var label       = this.cfg.labels[count];
      var ctx         = this.game.runner.front2d; // dodgy getting the context this way, should probably have a Game.Runner.ctx() method ?
      ctx.save();
      ctx.font        = label.font;
      ctx.fillStyle   = label.fill;
      ctx.strokeStyle = label.stroke;
      ctx.lineWidth   = 0.5;
      var width       = ctx.measureText(label.text).width;
      ctx.restore();
      label.x         = this.game.court.left +   (this.game.court.width - width)/2;
      label.y         = this.game.paddle.top - 60;
      return label;
    },

    clearLaunch: function() {
      if (this.delayTimer) {
        clearTimeout(this.delayTimer);
        this.delayTimer = this.label = this.countdown = null;
      }
    },

    update: function(dt) {

      if (!this.moving)
        return this.moveToPaddle();

      var p2 = Game.Math.move(this.x, this.y, this.dx * this.speed, this.dy * this.speed, dt);

      var mCurrent, mClosest = Infinity, point, item, closest = null;
      for (var n = 0 ; n < this.hitTargets.length ; n++) {
        item = this.hitTargets[n];
        if (!item.hit) {
          point = Game.Math.ballIntercept(this, item, p2.nx, p2.ny);
          if (point) {
            mCurrent = Game.Math.magnitude(point.x - this.x, point.y - this.y);
            if (mCurrent < mClosest) {
              mClosest = mCurrent;
              closest = {item: item, point: point};
            }
          }
        }
      }

      if (closest) {

        if (closest.item.isbrick) {
          this.game.hitBrick(closest.item);
          if (!this.moving) // if hitBrick caused game to end we dont want to continue updating our state
            return;
        }

        if ((closest.item == this.game.paddle) && (closest.point.d == 'top')) {
          p2.dx = this.speed * (closest.point.x - (this.game.paddle.left + this.game.paddle.w/2)) / (this.game.paddle.w/2);
          this.game.playSound('paddle');
        }

        this.setpos(closest.point.x, closest.point.y);

        switch(closest.point.d) {
          case 'left':
          case 'right':
            this.setdir(-p2.dx, p2.dy);
            break;

          case 'top':
          case 'bottom':
            this.setdir(p2.dx, -p2.dy);
            break;
        }

        var udt = dt * (mClosest / Game.Math.magnitude(p2.nx, p2.ny)); // how far along did we get before intercept ?
        return this.update(dt - udt);                                  // so we can update for time remaining
      }

      if ((p2.x < 0) || (p2.y < 0) || (p2.x > this.game.width) || (p2.y > this.game.height)) {
        this.game.loseBall();
      }
      else {
        this.setpos(p2.x,  p2.y);
        this.setdir(p2.dx, p2.dy);
      }

    },

    draw: function(ctx) {
      ctx.fillStyle = this.color;
      ctx.beginPath();
      ctx.arc(this.x, this.y, this.radius, 0, Game.THREESIXTY, true);
      ctx.fill();
      ctx.stroke();
      ctx.closePath();

      if (this.label) {
        ctx.font = this.label.font;
        ctx.fillStyle = this.label.fill;
        ctx.strokeStyle = this.label.stroke;
        ctx.lineWidth = 0.5;
        ctx.fillText(this.label.text,   this.label.x, this.label.y);
        ctx.strokeText(this.label.text, this.label.x, this.label.y);
      }
    }

  },

  //=============================================================================

  Paddle: {
    initialize: function(game, cfg) {
      this.game = game;
      this.cfg  = cfg;
    },

    reset: function() {
      this.speed  = this.cfg.speed  * this.game.court.chunk;
      this.w      = this.cfg.width  * this.game.court.chunk;
      this.h      = this.cfg.height * this.game.court.chunk;
      this.minX   = this.game.court.left;
      this.maxX   = this.game.court.right - this.w;
      this.setpos(Game.random(this.minX, this.maxX), this.game.court.bottom - this.h);
      this.setdir(0);
      this.rerender = true;
    },

    setpos: function(x, y) {
      this.x      = x;
      this.y      = y;
      Game.Math.bound(this);
    },

    setdir: function(dx) {
      this.dleft  = (dx < 0 ? -dx : 0);
      this.dright = (dx > 0 ?  dx : 0);
    },

    place: function(x) {
      this.setpos(Math.min(this.maxX, Math.max(this.minX, x)), this.y);
    },

    update: function(dt) {
      var amount = this.dright - this.dleft;
      if (amount != 0)
        this.place(this.x + (amount * dt * this.speed));
    },

    draw: function(ctx) {
      if (this.rerender) {
        this.canvas = Game.renderToCanvas(this.w, this.h, this.render.bind(this));
        this.rerender = false;
      }
      ctx.drawImage(this.canvas, this.x, this.y);
    },

    render: function(ctx) {

      var gradient = ctx.createLinearGradient(0, this.h, 0, 0);
      gradient.addColorStop(0.36, 'rgb(245,111,37)');
      gradient.addColorStop(0.68, 'rgb(255,145,63)');
      gradient.addColorStop(0.84, 'rgb(255,174,95)');

      var r = this.h/2;

      ctx.fillStyle = gradient;
      ctx.strokeStyle = this.game.color.border;
      ctx.beginPath();
      ctx.moveTo(r,  0);
      ctx.lineTo(this.w - r, 0);
      ctx.arcTo(this.w, 0, this.w, r, r);
      ctx.lineTo(this.w, this.h - r);
      ctx.arcTo(this.w, this.h, this.w - r, this.h, r);
      ctx.lineTo(r, this.h);
      ctx.arcTo(0, this.h, 0, this.h - r, r);
      ctx.lineTo(0, r);
      ctx.arcTo(0, 0, r, 0, r);
      ctx.fill();
      ctx.stroke();
      ctx.closePath();

    },

    moveLeft:        function() { this.dleft  = 1; },
    moveRight:       function() { this.dright = 1; },  
    stopMovingLeft:  function() { this.dleft  = 0; },
    stopMovingRight: function() { this.dright = 0; }

  }

  //=============================================================================

}; // Breakout


Breakout.Colors = {

  arkanoid: {
    w: "#FCFCFC", // white
    o: "#FC7460", // orange
    l: "#3CBCFC", // light blue
    g: "#80D010", // green
    r: "#D82800", // red
    b: "#0070EC", // blue
    p: "#FC74B4", // pink
    y: "#FC9838", // yellow
    s: "#BCBCBC", // silver
    d: "#F0BC3C"  // gold
  },

  pastel: {
    y: "#FFF7A5", // yellow
    p: "#FFA5E0", // pink
    b: "#A5B3FF", // blue
    g: "#BFFFA5", // green
    o: "#FFCBA5"  // orange
  },

  vintage: {
    a: "#EFD279", // yellow
    b: "#95CBE9", // light blue
    c: "#024769", // dark blue
    d: "#AFD775", // light green
    e: "#2C5700", // grass
    f: "#DE9D7F", // red
    g: "#7F9DDE", // purple
    h: "#00572C", // dark green
    i: "#75D7AF", // mint
    j: "#694702", // brown
    k: "#E9CB95", // peach
    l: "#79D2EF"  // blue
  },

  liquidplanner: {
    a: '#62C4E7', // light blue
    b: '#00A5DE', // dark  blue
    x: '#969699', // light gray
    y: '#7B797E'  // dark  gray
  },


};

Breakout.Levels = [

  { colors: Breakout.Colors.pastel,
    bricks: [
      "", "", "", "", "", "",
      "yyyyyYYYYYyyyyyYYYYYyyyyyYYYYY",
      "pppppPPPPPpppppPPPPPpppppPPPPP",
      "bbbbbBBBBBbbbbbBBBBBbbbbbBBBBB",
      "gggggGGGGGgggggGGGGGgggggGGGGG",
      "oooooOOOOOoooooOOOOOoooooOOOOO"
    ]
  },

  { colors: Breakout.Colors.arkanoid,
    bricks: [
      "", "",
      "          yy      yy          ",
      "            yy  yy            ",
      "            yy  yy            ", 
      "          ssSSssSSss          ",
      "          ssSSssSSss          ",
      "        SSsswwsswwssSS        ",
      "        SSsswwsswwssSS        ",
      "      ssSSssSSssSSssSSss      ",
      "      ssSSssSSssSSssSSss      ",
      "      ss  ssSSssSSss  ss      ",
      "      ss  ss      ss  ss      ",
      "      ss  ss      ss  ss      ",
      "            ss  ss            ",
      "            ss  ss            ",
    ]
  },

  { colors: Breakout.Colors.arkanoid,
    bricks: [
      "",
      "oo",
      "ooll",
      "oollgg",
      "oollggbb",
      "oollggbbrr",
      "oollggbbrroo",
      "oollggbbrrooll",
      "oollggbbrroollgg",
      "oollggbbrroollggbb",
      "oollggbbrroollggbbrr",
      "oollggbbrroollggbbrroo",
      "oollggbbrroollggbbrrooll",
      "oollggbbrroollggbbrroollgg",
      "oollggbbrroollggbbrroollggbb",
      "ssSSssSSssSSssSSssSSssSSssSSrr"
    ]
  },

  { colors: Breakout.Colors.arkanoid,
    bricks: [
      "", "",
      "              ss              ",
      "          bbBBssggGG          ",
      "        BBbbWWwwWWGGgg        ",
      "      bbBBwwWWwwWWwwggGG      ",
      "      bbBBwwWWwwWWwwggGG      ",
      "      bbBBwwWWwwWWwwggGG      ",
      "      ss  ss  ss  ss  ss      ",
      "              ss              ",
      "              ss              ",
      "          oo  oo              ",
      "          ooOOoo              ",
      "            OO                "
    ]
  },

  { colors: Breakout.Colors.pastel,
    bricks: [
      "", "",
      "  yyYYyyYYyyYY  YYyyYYyyYYyy  ",
      "  bbBBbbBBbbBB  BBbbBBbbBBbb  ",
      "  ggGGggGGggGG  GGggGGggGGgg  ",
      "  ooOOooOOooOO  OOooOOooOOoo  ",
      "", "",
      "  yyYYyyYYyyYY  YYyyYYyyYYyy  ",
      "  bbBBbbBBbbBB  BBbbBBbbBBbb  ",
      "  ggGGggGGggGG  GGggGGggGGgg  ",
      "  ooOOooOOooOO  OOooOOooOOoo  ",
      "", "",
      "  yyYYyyYYyyYY  YYyyYYyyYYyy  ",
      "  bbBBbbBBbbBB  BBbbBBbbBBbb  ",
      "  ggGGggGGggGG  GGggGGggGGgg  ",
      "  ooOOooOOooOO  OOooOOooOOoo  "
    ]
  },

  { colors: Breakout.Colors.vintage,
    bricks: [
      "", "", "",
      "   AAaaAAaaAAaaAAaaAAaaAAaa   ",
      "    BBbbBBbbBBbbBBbbBBbbBB    ",
      "     CCccCCccCCccCCccCCcc     ",
      "      DDddDDddDDddDDddDD      ",
      "       EEeeEEeeEEeeEEee       ",
      "        FFffFFffFFffFF        ",
      "         GGggGGggGGgg         ",
      "          HHhhHHhhHH          ",
      "           IIiiIIii           ",
      "            JJjjJJ            ",
      "             KKkk             ",
      "              LL              "
    ]
  },

  { colors: Breakout.Colors.vintage,
    bricks: [
      "", "",
      "  aabbccddeeffggFFEEDDCCBBAA  ",
      "   aabbccddeeffFFEEDDCCBBAA   ",
      "    aabbccddeeffEEDDCCBBAA    ",
      "     aabbccddeeEEDDCCBBAA     ",
      "      aabbccddeeDDCCBBAA      ",
      "       aabbccddDDCCBBAA       ",
      "        aabbccddCCBBAA        ",
      "         aabbccCCBBAA         ",
      "          aabbccBBAA          ",
      "      hh   aabbCCAA   hh      ",
      "     hhHH   aabbAA   hhHH     ",
      "    hhiiHH   aaAA   hhiiHH    ",
      "   hhiiIIHH   aa   hhiiIIHH   ",
      "  hhiijjIIHH      hhiijjIIHH  ",
      " hhiijjJJIIHH    hhiijjJJIIHH "
    ]
  },

  { colors: Breakout.Colors.pastel,
    bricks: [
      "                              ",
      "                              ",
      "  bbBBbbBBbbBBbbBBbbBBbbBBbb  ",
      "  ooggGGggGGggGGggGGggGGggoo  ",
      "  ooggGGggGGggGGggGGggGGggoo  ",
      "  ooppPPppPPppPPppPPppPPppoo  ",
      "  ooppPPppPPppBBppPPppPPppoo  ",
      "  ooppPPppPPbbBBbbPPppPPppoo  ",
      "  ooppPPppBBbbOObbBBppPPppoo  ",
      "  ooppPPbbBBooOOooBBbbPPppoo  ",
      "  ooppBBbbOOooYYooOObbBBppoo  ",
      "  oobbBBOOooyyYYyyooOOBBbboo  ",
      "  oobbooOOYYyyYYyyYYOOoobboo  ",
      "  ooOOooyyYYyyYYyyYYyyooOOoo  ",
      "  ooOOYYyyYYyyYYyyYYyyYYOOoo  ",
      "  ooyyYYyyYYyyYYyyYYyyYYyyoo  ",
      "  ooyyYYyyYYyyYYyyYYyyYYyyoo  ",
      "  bbBBbbBBbbBBbbBBbbBBbbBBbb  "
    ]
  },

  { colors: {
      b: '#111111', // black,
      w: '#EEEEEE', // white,
      c: '#EC7150', // cherry,
      s: '#B33A2F'  // shadow,
    },

    bricks: [
      "",
      "       bBb                    ",
      "      BcCcB                   ",
      "     bCwCcsb  b               ",
      "     bCcCcsb b                ",
      "      BcCsB B                 ",
      "    BbBsSsBbB       bBb       ",
      "   bcCcbBbcCcb     BcCcB      ",
      "  bcwcCsbcwcCsb   bCwCcsb  b  ",
      "  bcCcCsbcCcCsb   bCcCcsb b   ",
      "  bcCcsSbcCcsSb    BcCsB B    ",
      "   bsSsb bsSsb   BbBsSsBbB    ",
      "    bBb   bBb   bcCcbBbcCcb   ",
      "               bcwcCsbcwcCsb  ",
      "               bcCcCsbcCcCsb  ",
      "               bcCcsSbcCcsSb  ",
      "                bsSsb bsSsb   ",
      "                 bBb   bBb    ",
      "                              ",
      "                              ",
      "                              ",
      "                              ",
    ]
  },

  { colors: {
      r: '#D80000', // red
      b: '#706800', // brown
      o: '#F8AB00', // orange
      f: '#F83800', // fire
      w: '#FFFFFF', // white
      e: '#FFE0A8'  // beige
    },

    bricks: [
      "",
      "    rRrRr                     ",
      "   RrRrRrRrR                  ",
      "   BbBoObo                    ",
      "  boboOoboOo       F    f   f ",
      "  bobBoOoboOo     f e         ",
      "  bBoOoObBbB       F  f     e ",
      "    oOoOoOo        Ff      E  ",
      "   bBrbBb        E  f fF F  f ",
      "  bBbrbBrbBb       FfFfFf  F  ",
      " bBbBrRrRbBbB     fFeFeFfFf   ",
      " oObrorRorboO    FfEeEeEfF    ",
      " oOorRrRrRoOo    FeEeWwEeFf   ",
      " oOrRrRrRrRoO   fFeFwWfEeFf   ",
      "   rRr  RrR     fFeFwWfEeFf   ",
      "  bBb    bBb    fFeEwWeEeFf   ",
      " bBbB    bBbB   fFfEeEeEfF    ",
      "                 FfFfFfFfF    ",
      "                   FfFfF      "
    ]
  }


];