<?xml version="1.0"?>

<!-- SVG Tetris for SVG-enabled Mozilla       -->
<!-- (c)2004 alex fritze <alex@croczilla.com> -->
<!--                                          -->
<!-- For infos about Mozilla/SVG see          -->
<!-- http://www.mozilla.org/projects/svg      -->

<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:html="http://www.w3.org/1999/xhtml"
     viewBox="0 0 21 25">

<!-- use html:script because svg:script isn't working yet -->
<html:script>
<![CDATA[

//----------------------------------------------------------------------
// static data

const SVG_NS ="http://www.w3.org/2000/svg";

const ROWS = 20;
const COLS = 10;

const HPAD = 2; // horizontal padding used in laying out grids
const VPAD = 2; // ditto for vertical

const SHAPE_DESCRIPTORS = [
  { color: "red",   orientations: [ [[0,0],[1,0],[0,1],[1,1]] ] },
  { color: "blue",  orientations: [ [[0,0],[1,0],[2,0],[2,1]],
                                    [[1,0],[1,1],[1,2],[0,2]],
                                    [[0,0],[0,1],[1,1],[2,1]],
                                    [[0,0],[1,0],[0,1],[0,2]] ] },
  { color: "magenta",orientations: [ [[0,0],[0,1],[1,0],[2,0]],
                                     [[0,0],[1,0],[1,1],[1,2]],
                                     [[0,1],[1,1],[2,1],[2,0]],
                                     [[0,0],[0,1],[0,2],[1,2]] ] },
  { color: "green", orientations: [ [[0,0],[1,0],[2,0],[3,0]],
                                    [[0,0],[0,1],[0,2],[0,3]] ] },
  { color: "cyan",  orientations: [ [[1,0],[2,0],[0,1],[1,1]],
                                    [[0,0],[0,1],[1,1],[1,2]] ] },
  { color: "yellow",orientations: [ [[1,0],[1,1],[0,1],[0,2]],
                                    [[0,0],[1,0],[1,1],[2,1]] ] },
  { color: "purple",orientations: [ [[0,1],[1,1],[2,1],[1,0]],
                                    [[0,0],[0,1],[0,2],[1,1]],
                                    [[0,0],[1,0],[2,0],[1,1]],
                                    [[1,0],[1,1],[1,2],[0,1]] ] }
];

//----------------------------------------------------------------------
// Helper functions

// map a function over an array; accumulate output in new array:
function mapcar(f, a) {
  var res = new Array(a.length);
  for (var i=0;i<a.length;++i) {
    res[i] = f(a[i]);
  }
  return res;
}

// map a function over an array; don't accumulate output:
function mapc(f, a) {
  for (var i=0;i<a.length;++i) {
    f(a[i]);
  }
}

// return true if predicate p is true for every element of the array:
function every(p, a) {
  for (var i=0;i<a.length;++i) {
    if (!p(a[i])) return false;
  }
  return true;
}

//----------------------------------------------------------------------
// Shape class

function Shape(position) {
  // create a new shape, randomly picking a descriptor & orientation:
  this._descriptor = SHAPE_DESCRIPTORS[Math.round(Math.random()*(SHAPE_DESCRIPTORS.length-1))];
  this._orientation = Math.round(Math.random()*(this._descriptor.orientations.length-1));
  this._pos = position;
}

Shape.prototype = {
  get cellArray() {
    var s = this;
    return mapcar(function(coord) { return [coord[0]+s._pos[0],coord[1]+s._pos[1]]; },
               this._descriptor.orientations[this._orientation]);
  },
  get color() {
    return this._descriptor.color;
  },
  move : function(dx,dy) {
    this._pos[0] += dx; this._pos[1] += dy;
  },
  rotate : function(dOrient) {
    this._orientation = (this._orientation+dOrient) % this._descriptor.orientations.length;
    if (this._orientation<0) this._orientation += this._descriptor.orientations.length;
  }
};

//----------------------------------------------------------------------
// Grid class

function Grid(cols, rows, color, bordercolor, x, y, width, height, node) {
  // Create a cols*rows grid with a 1/2-cell border.
  // Scale to fit width*height user pixels (including border).
  // Place at x,y user pixel coords.

  this._cols = cols;
  this._rows = rows;
  this._color = color;

  node.setAttribute("transform", "translate("+x+","+y+") scale("+
                                 (width/(cols+1))+","+(height/(rows+1))+") translate(0.5,0.5)");

  this._border = document.createElementNS(SVG_NS, "rect");
  this._border.setAttribute("fill", bordercolor);
  this._border.setAttribute("width", cols+1);
  this._border.setAttribute("height", rows+1);
  this._border.setAttribute("x", "-0.5");
  this._border.setAttribute("y", "-0.5");

  this._background = document.createElementNS(SVG_NS, "rect");
  this._background.setAttribute("fill", color);
  this._background.setAttribute("width", cols);
  this._background.setAttribute("height", rows);

  this._rowArray = document.createElementNS(SVG_NS, "g");
  for (var r=0;r<rows;++r) {
    var row_group = document.createElementNS(SVG_NS, "g");
    row_group.setAttribute("transform", "translate(0,"+r+")");

    for (var c=0;c<cols;++c) {
      var cell = document.createElementNS(SVG_NS, "rect");
      cell.setAttribute("x", c);
      cell.setAttribute("width", "1");
      cell.setAttribute("height", "1");
      cell.setAttribute("stroke", "grey");
      cell.setAttribute("fill", "none");
      cell.occupied = false;
      row_group.appendChild(cell);
    }
    this._rowArray.appendChild(row_group);
  }
  
  node.appendChild(this._border);
  node.appendChild(this._background);
  node.appendChild(this._rowArray);
}

Grid.prototype = {
  colorCell : function(coord, color) { try {
    this._rowArray.childNodes[coord[1]].childNodes[coord[0]].setAttribute("fill", color);  
}catch(e) {
alert("error: coord="+coord);
}
  },
  clearCell : function(coord) {
    this.colorCell(coord, this._color);
  },
  occupyCell : function(coord) {
    this._rowArray.childNodes[coord[1]].childNodes[coord[0]].occupied = true;
  },
  unoccupyCell : function(coord) {
    this._rowArray.childNodes[coord[1]].childNodes[coord[0]].occupied = false;
  },
  cellInBounds : function(coord) {
    return (coord[0]>=0 && coord[1]>=0 && coord[0]<this._cols && coord[1]<this._rows);
  },
  cellOccupied : function(coord) {
    return this._rowArray.childNodes[coord[1]].childNodes[coord[0]].occupied;
  },
  eliminateFullRows : function() {
    var g = this;
    function rowFull(r) {
      for (var c=0;c<g._cols;++c) {
        if (!g.cellOccupied([c,r])) return false;
      }
      return true;
    }

    function moveCellDown(c,r) {
      var src = g._rowArray.childNodes[r].childNodes[c];
      var dest = g._rowArray.childNodes[r+1].childNodes[c];
      dest.setAttribute("fill", src.getAttribute("fill"));
      dest.occupied = src.occupied;
      src.occupied = false;
      src.setAttribute("fill", g._color);
    }
    
    function eliminateRow(row) {
      var id = document.documentElement.suspendRedraw(0);
      for (var c=0;c<g._cols;++c) {
        g.clearCell([c,row]);
        g.unoccupyCell([c,row]);
      }
      for (var r=row-1;r>=0;--r) {
        for (c=0;c<g._cols;++c) {
          if (g.cellOccupied([c,r])) 
            moveCellDown(c,r);
        }
      }
      document.documentElement.unsuspendRedraw(id);
    }

    for (var r=0;r<this._rows;++r) {
      if (rowFull(r)) 
        eliminateRow(r);
    }
  }
};

//----------------------------------------------------------------------
// message class

function Message(txt, position, style) {
  this._node = document.createElementNS(SVG_NS, "text");
  this._node.setAttribute("style", style);
  this._node.setAttribute("x", position[0]);
  this._node.setAttribute("y", position[1]);
  this._node.appendChild(document.createTextNode(txt));
}

Message.prototype = {
  show : function() {
    document.documentElement.suspendRedraw(0);
    document.documentElement.appendChild(this._node);
    document.documentElement.unsuspendRedraw(0);
  },
  hide : function() {
    document.documentElement.removeChild(this._node);
  }
};

//----------------------------------------------------------------------
// grid <---> shape operations

function canPlace(shape, grid) {
  // can only place if all cells in shape are in bounds and not occupied:
  return every(function(coord){ return grid.cellInBounds(coord) &&
                                       !grid.cellOccupied(coord); },
                shape.cellArray);
}

function show(shape, grid) {
  var id = document.documentElement.suspendRedraw(0);
  mapc(function(coord){ grid.colorCell(coord, shape.color); },
       shape.cellArray);
  document.documentElement.unsuspendRedraw(id);
}

function hide(shape, grid) {
  var id = document.documentElement.suspendRedraw(0);
  mapc(function(coord){ grid.clearCell(coord); },
       shape.cellArray);
  document.documentElement.unsuspendRedraw(id);
}

function occupy(shape, grid) {
  mapc(function(coord){ grid.occupyCell(coord); },
       shape.cellArray);
}

function move(shape, grid, dx, dy) {
  shape.move(dx,dy);
  if (!canPlace(shape, grid)) {
    shape.move(-dx,-dy);
    return false;
  }
  var id = document.documentElement.suspendRedraw(0);
  shape.move(-dx, -dy);
  hide(shape, grid);
  shape.move(dx,dy);
  show(shape, grid);
  document.documentElement.unsuspendRedraw(id);
  return true;
}

function rotate(shape, grid, dOrient) {
  shape.rotate(dOrient);
  if (!canPlace(shape, grid)) {
    shape.rotate(-dOrient);
    return false;
  }
  var id = document.documentElement.suspendRedraw(0);
  shape.rotate(-dOrient);
  hide(shape, grid);
  shape.rotate(dOrient);
  show(shape, grid);
  document.documentElement.unsuspendRedraw(id);
  return true;
}

function drop(shape, grid) {
  var id = document.documentElement.suspendRedraw(0);
  while (move(shape, grid, 0, 1))
    /**/;
  document.documentElement.unsuspendRedraw(id);
}

//----------------------------------------------------------------------
// the game:

var board;   // grid where the action is
var preview; // grid where the next shape will be previewed
var currentShape; 
var nextShape; 
var gameState; // "stopped", "running"

function startNewGame() {
  // XXX clear grids

  var id = document.documentElement.suspendRedraw(0);
  currentShape = new Shape([3,0]);
  show(currentShape, board);
  nextShape = new Shape([0,0]);
  show(nextShape, preview);
  document.documentElement.unsuspendRedraw(id);
  gameState = "running";
  tick();
}

function runNextShape() {
  occupy(currentShape, board);
  board.eliminateFullRows();

  var id = document.documentElement.suspendRedraw(0);
  currentShape = nextShape;
  hide(nextShape, preview);
  currentShape.move(3,0);
  if (!canPlace(currentShape, board)) {
    document.documentElement.unsuspendRedraw(id);
    return false; // game over!
  }
  show(currentShape, board);
  nextShape = new Shape([0,0]);
  show(nextShape, preview);
  document.documentElement.unsuspendRedraw(id);
  return true;
}

function tick() {
  if (gameState != "running") return;

  if (!move(currentShape, board, 0, 1)) {
    if(!runNextShape()) {
      var m = new Message("Game Over!", [2,10], "stroke:none;font-size:3px;fill:red;fill-opacity:0.5;");
      m.show();
      return; // Game over
    }
  }
  setTimeout(tick, 300);
}

function pause() {
  gameState = "stopped";
}

function resume() {
  if (gameState == "stopped") {
    gameState = "running";
    tick();
  }
}

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

function init() {
  // page layout:

  // XXX looks like there is a bug with the gdi+ renderer which means
  // that dynamic canvas sizing by setting viewBox doesn't take effect
  // until the user resizes the window...  
  document.documentElement.setAttribute("viewBox", "0 0 "+(3*HPAD+(COLS+1)+5)+" "+(2*VPAD+(ROWS+1)));

  board = new Grid(COLS, ROWS, "black", "grey", HPAD, VPAD, COLS+1, ROWS+1, document.getElementById("board"));
  preview = new Grid(4,4, "black", "grey", 2*HPAD+COLS+1, VPAD, 5, 5, document.getElementById("preview"));

  startNewGame();

  // initialize event processing:
  document.documentElement.addEventListener("keypress", keyHandler, true);
}

//----------------------------------------------------------------------
// user input handler

function keyHandler(event) { //alert(event.charCode);
  event.preventDefault();
  switch (event.charCode) {
    case 32: /* space */
      if (gameState == "running")
        drop(currentShape, board);
      break;
    case 104: /* h */
      pause();
      alert("Keys:\n"+
            "-------------------------------------\n\n"+
            "h : Display this help\n"+
            "n : Start new game\n"+
            "p : Toggle pause game\n"+
            "up    : Rotate piece counterclockwise\n"+
            "down  : Rotate piece clocwise\n"+
            "left  : Move piece left\n"+
            "right : Move piece right\n"+
            "space : Drop piece\n");
      resume();
      break;
    case 112: /* p */
      if (gameState == "running") 
        pause();
      else
        resume();
      break;
    case 0: 
      switch (event.keyCode) {
        case 38: /* up */
          if (gameState == "running")
            rotate(currentShape, board, -1);
          break;
        case 40: /* down */
          if (gameState == "running")
            rotate(currentShape, board, 1);
          break;
        case 37: /* left */
          if (gameState == "running")
            move(currentShape, board, -1, 0);
          break;
        case 39: /* right */
          if (gameState == "running")
            move(currentShape, board, 1, 0);
          break;
      }
      break;
  }
}

]]>
</html:script>

<!-- html:body is here purely for the side-effect of starting 'init()' -->
<html:body onload="init();"/>
<text x="1" y="1" font-size="1px">Mozilla SVG Tetris - Press 'h' for help.</text>
<g id="preview" stroke-width="0.02"/>
<g id="board" stroke-width="0.02"/>

</svg>