/*
(c) CopyLeft 2003, 2004 Benn Herrera benn@bennherrera.com
Permission granted to copy, use, and modify this code so long as this notice is left intact.
If this code is used in any commercial product, appropriate attribution is required.
Full terms at http://www.gnu.org/copyleft/gpl.html
*/

//
// This file implements the game behind the UI
// The game is composed of 3 classes
// JSTetris - the main class that controls the game as a whole
// JSTBoard - an abstraction of the board on which the game
//            is displayed
// JSTBlock - an abstraction of the shaped blocks that fall
//            as the game progresses
//
//
//

//########################################################
// Global Utility Routines:
// simple set of routines for handling
// 2 signed 16 bit values stored in 1 32bit integer
//#########################################################
function makeXY( x, y ) {
	// masks out x values top 16 bits
	// to prevent negative x from interfering
	// w/ y value
	return (y << 16)|(x & 0xffff);
}

function getX( pos ) {
	// returns sign-extended low 16 bits
	return (pos  << 16) >> 16;
}

function getY( pos ) {
	// returns sign-extended high 16 bits
	return pos >> 16;
}

function setX( pos, x ) {
	return makeXY( x, getY( pos ) );
}

function setY( pos, y ) {
	return makeXY( getX( pos ), y );
}

function incX( pos ) {
	return setX( pos, getX( pos ) + 1 );
}

function decX( pos ) {
	return setX( pos, getX( pos ) - 1 );
}

function incY( pos ) {
	return setY( pos, getY( pos ) + 1 );
}

function decY( pos ) {
	return setY( pos, getY( pos ) - 1 );
}

// for debugging
function posToStr( pos ) {
	return "(" + getX( pos ) + ", " + getY( pos ) + ")";
}


// ###################################################
//  JSTBlock - class representing a tetris block
// composed of four tiles.
// ###################################################

// updates the positions this block would occupy on
// the tetris board - does not update the actual displayed
// tile positions
function updateBoardPositions() {
	//document.getElementById( "zeroImgTxt" ).innerHTML = "[o=" + this.orientation + "]";
	
	var px = getX( this.position );
	var py = getY( this.position );
	var x, y;

	// loop through the relative positions for each tile
	for( var i = 0; i < 4; i++ ) {
		x = getX( this.offsets[this.orientation][i] ) + px;
		y = getY( this.offsets[this.orientation][i] ) + py

		// set the board positions
		this.boardPositions[i] = makeXY( x, y );
	}
}

// change the orientation by 'direction' 90 degree turns clockwise
// direction may be positive or negative
// note: does not change display position - that is done by commitMove()
function rotateBlock( direction ) {
	this.previousPosition = this.position;
	this.previousOrientation = this.orientation;

	this.orientation += direction;
	if( this.orientation < 0 )		this.orientation = 3;
	else if( this.orientation > 3 )	this.orientation = 0;
	
	this.updateBoardPositions();
}

// change the horizontal position by 'direction' tiles to the right
// direction may be positive or negative
// note: does not change display position - that is done by commitMove() 
function moveBlock( direction ) {
	this.previousPosition = this.position;
	this.previousOrientation = this.orientation;

	this.position = setX( this.position, getX( this.position ) + direction );
	this.updateBoardPositions();
}

// drop the vertical position by 1 tile
// note: does not change display position - that is done by commitMove() 
function stepDown() {
	this.previousPosition = this.position;
	this.previousOrientation = this.orientation;

	this.position = incY( this.position );
	this.updateBoardPositions();
}

// a collision case was encountered and we want to undo the last
// movement/rotation instead of committing it.
function undoMovement() {
	this.position = this.previousPosition;
	this.orientation = this.previousOrientation;

	this.updateBoardPositions();
}

// updates the tile positions to reflect the board positions
function displayMovement() {
	for( var sx, sy, bx, by, pbx = -1, pby = -1, i = 0; i < 4; i++, pbx = bx, pby = by ) {
 		bx = getX( this.boardPositions[i] );
		by = getY( this.boardPositions[i] );

		if( bx != pbx )
			sx = ( bx * TileSize ) + BoardCellLeft;

		if( by != pby )
			sy = ( by * TileSize ) + BoardCellTop;

		this.tiles[i].style.left = 	sx + "px";
		this.tiles[i].style.top  =	sy + "px";
	}
}

// create an image tile for the tetris block.
// this being tetris, there are always 4, so this
// gets called 4 times.
function createTile( img ) {
	var tile = new Image( TileSize, TileSize );

	tile.src = img;
	tile.style.position = "absolute";
	tile.style.width	= TileSize	+ "px";
	tile.style.height	= TileSize + "px";

	BoardCell.appendChild( tile );

	return tile;
}

// used to generate preview of next block
var OPEN_PVW_TABLE       = "<table class=\"nextBlockDisplayTable\" cellpadding=\"0\" cellspacing=\"0\">";
var CLOSE_PVW_TABLE      = "</table>";
var OPEN_PVW_ROW         = "<tr class=\"nextBlockDisplayRow\">";
var CLOSE_PVW_ROW        = "</tr>";
var OPEN_PVW_CELL        = "<td class=\"nextBlockDisplayCell\">";
var CLOSE_PVW_CELL       = "</td>";
var EMPTY_PVW_CELL       = OPEN_PVW_CELL + "&nbsp;" + CLOSE_PVW_CELL;

function genPreviewCell( idx ) {
    return  OPEN_PVW_CELL +
            "<img src=\"" + peekNextImg( idx + 1 ) + "\" width=\"" + TileSize + "\" height=\"" + TileSize + "\">" +
            CLOSE_PVW_CELL;
}

function genLinePreview() {  
    return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + genPreviewCell( 0 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 1 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 2 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 3 ) + CLOSE_PVW_ROW +
            CLOSE_PVW_TABLE;
}

function genBlockPreview() {   
    return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + genPreviewCell( 0 ) + genPreviewCell( 1 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 3 ) + genPreviewCell( 2 ) + CLOSE_PVW_ROW +
            CLOSE_PVW_TABLE;
}

function genRightEllPreview() {
    return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + genPreviewCell( 0 ) + EMPTY_PVW_CELL      + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 1 ) + EMPTY_PVW_CELL      + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 2 ) + genPreviewCell( 3 ) + CLOSE_PVW_ROW + 
            CLOSE_PVW_TABLE;
}

function genLeftEllPreview() {
    return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + EMPTY_PVW_CELL      + genPreviewCell( 0 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + EMPTY_PVW_CELL      + genPreviewCell( 1 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 3 ) + genPreviewCell( 2 ) + CLOSE_PVW_ROW +
            CLOSE_PVW_TABLE;
}

function genTeePreview() {
    return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + EMPTY_PVW_CELL      + genPreviewCell( 2 ) + EMPTY_PVW_CELL + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 0 ) + genPreviewCell( 1 ) + genPreviewCell( 3 )   + CLOSE_PVW_ROW +
            CLOSE_PVW_TABLE;
}

function genLeftEnPreview() {
     return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + genPreviewCell( 0 ) + EMPTY_PVW_CELL      + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 1 ) + genPreviewCell( 2 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + EMPTY_PVW_CELL      + genPreviewCell( 3 ) + CLOSE_PVW_ROW +
            CLOSE_PVW_TABLE;
}

function genRightEnPreview() { 
    return  OPEN_PVW_TABLE +
            OPEN_PVW_ROW   + EMPTY_PVW_CELL      + genPreviewCell( 0 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 2 ) + genPreviewCell( 1 ) + CLOSE_PVW_ROW +
            OPEN_PVW_ROW   + genPreviewCell( 3 ) + EMPTY_PVW_CELL      + CLOSE_PVW_ROW +
            CLOSE_PVW_TABLE;
}

function genShapePreview( shape ) {
    switch ( shape ) {
        case 0: return genLinePreview();
        case 1: return genBlockPreview();
        case 2: return genRightEllPreview();
        case 3: return genLeftEllPreview();
        case 4: return genTeePreview();
        case 5: return genLeftEnPreview();
        case 6: return genRightEnPreview();
    }
    
    return "&nbsp;";
}

function resizeBlock() {
	for( var sx, sy, bx, by, pbx = -1, pby = -1, i = 0; i < 4; i++, pbx = bx, pby = by ) {
 		bx = getX( this.boardPositions[i] );
		by = getY( this.boardPositions[i] );

		if( bx != pbx )
			sx = ( bx * TileSize ) + BoardCellLeft;

		if( by != pby )
			sy = ( by * TileSize ) + BoardCellTop;

		this.tiles[i].style.left 	= 	sx			+ "px";
		this.tiles[i].style.top  	=	sy			+ "px";
		this.tiles[i].style.width	=	TileSize	+ "px";
		this.tiles[i].style.height	=	TileSize	+ "px";
	}
}

// constructor
// takes one value - shape [0->6]
function JSTBlock( shape ) {
	// methods
	this.createTile				= createTile;
	this.displayMovement		= displayMovement;
	this.move					= moveBlock;
	this.resize					= resizeBlock;
	this.rotate					= rotateBlock;
	this.stepDown				= stepDown;
	this.undoMovement			= undoMovement;
	this.updateBoardPositions	= updateBoardPositions;
 
	// tiles that make up the block
	this.tiles			= new Array( 4 );
	
	// array of position offset arrays from current position of the block on the board.
	// the offsets place the tiles so as to lay out the shape.
	// 4 entries are for 4 orientations. (up, right, down, left)
	this.offsets		= new Array( 4 );
	
	// tile positions in board space
	this.boardPositions = new Array( 4 );
	
	var boardCenterX	= Math.floor( BoardWidth / 2 ) - 1;
	var boardTopY		= 1;
	
	// shapes and offsets
	//     0(up)   1(right) 2(down)  3(left)
	switch( shape ) {
	case 0:
		// [1]                 [4]
		// [2]   [4][3][2][1]  [3]   [1][2][3][4]
		// [3]                 [2]
		// [4]                 [1]
		this.offsets[0] = [ makeXY(  0, -1 ), makeXY( 0, 0 ), makeXY(  0,  1 ), makeXY(  0,  2 ) ];
		this.offsets[1] = [ makeXY(  1,  0 ), makeXY( 0, 0 ), makeXY( -1,  0 ), makeXY( -2,  0 ) ];
		this.offsets[2] = [ makeXY(  0,  1 ), makeXY( 0, 0 ), makeXY(  0, -1 ), makeXY(  0, -2 ) ];
		this.offsets[3] = [ makeXY( -1,  0 ), makeXY( 0, 0 ), makeXY(  1,  0 ), makeXY(  2,  0 ) ];
		break;
	case 1:
		// [1][2]   [4][1]   [3][4]   [2][3]
		// [4][3]   [3][2]   [2][1]   [1][4]
		this.offsets[0] = [ makeXY( 0, -1 ), makeXY( 0,  0 ), makeXY( 1,  0 ), makeXY( 1, -1 ) ];
		this.offsets[1] = [ makeXY( 1, -1 ), makeXY( 0, -1 ), makeXY( 0,  0 ), makeXY( 1,  0 ) ];
		this.offsets[2] = [ makeXY( 1,  0 ), makeXY( 1, -1 ), makeXY( 0, -1 ), makeXY( 0,  0 ) ];
		this.offsets[3] = [ makeXY( 0,  0 ), makeXY( 1,  0 ), makeXY( 1, -1 ), makeXY( 0, -1 ) ];
		break;
	case 2:
		// [1]      [3][2][1]   [4][3]         [4]
		// [2]      [4]            [2]   [1][2][3]
		// [3][4]                  [1]
		this.offsets[0] = [ makeXY(  0, -1 ), makeXY( 0, 0 ), makeXY(  0,  1 ), makeXY(  1,  1 ) ];
		this.offsets[1] = [ makeXY(  1,  0 ), makeXY( 0, 0 ), makeXY( -1,  0 ), makeXY( -1,  1 ) ];
		this.offsets[2] = [ makeXY(  0,  1 ), makeXY( 0, 0 ), makeXY(  0, -1 ), makeXY( -1, -1 ) ];
		this.offsets[3] = [ makeXY( -1,  0 ), makeXY( 0, 0 ), makeXY(  1,  0 ), makeXY(  1, -1 ) ];
		break;
	case 3:
		//   [1]   [4]         [3][4]   [1][2][3]
		//   [2]   [3][2][1]   [2]            [4]
		//[4][3]               [1]
		this.offsets[0] = [ makeXY(  0, -1 ), makeXY( 0, 0 ), makeXY(  0,  1 ), makeXY( -1,  1 ) ];
		this.offsets[1] = [ makeXY(  1,  0 ), makeXY( 0, 0 ), makeXY( -1,  0 ), makeXY( -1, -1 ) ];
		this.offsets[2] = [ makeXY(  0,  1 ), makeXY( 0, 0 ), makeXY(  0, -1 ), makeXY(  1, -1 ) ];
		this.offsets[3] = [ makeXY( -1,  0 ), makeXY( 0, 0 ), makeXY(  1,  0 ), makeXY(  1,  1 ) ];
		boardCenterX++;
		break;
	case 4:
		//   [3]      [1]      [4][2][1]      [4]
		//[1][2][4]   [2][3]      [3]      [3][2]
		//            [4]                     [1]
		this.offsets[0] = [ makeXY( -1,  0 ), makeXY( 0, 0 ), makeXY(  0, -1 ), makeXY(  1,  0 ) ];
		this.offsets[1] = [ makeXY(  0, -1 ), makeXY( 0, 0 ), makeXY(  1,  0 ), makeXY(  0,  1 ) ];
		this.offsets[2] = [ makeXY(  1,  0 ), makeXY( 0, 0 ), makeXY(  0,  1 ), makeXY( -1,  0 ) ];
		this.offsets[3] = [ makeXY(  0,  1 ), makeXY( 0, 0 ), makeXY( -1,  0 ), makeXY(  0, -1 ) ];
		break;
	case 5:
		// [1]         [2][1]   [4]         [3][4]
		// [2][3]   [4][3]      [3][2]   [1][2]
		//    [4]                  [1]
		this.offsets[0] = [ makeXY(  0, -1 ), makeXY( 0, 0 ), makeXY(  1,  0 ), makeXY(  1,  1 ) ];
		this.offsets[1] = [ makeXY(  1,  0 ), makeXY( 0, 0 ), makeXY(  0,  1 ), makeXY( -1,  1 ) ];
		this.offsets[2] = [ makeXY(  0,  1 ), makeXY( 0, 0 ), makeXY( -1,  0 ), makeXY( -1, -1 ) ];
		this.offsets[3] = [ makeXY( -1,  0 ), makeXY( 0, 0 ), makeXY(  0, -1 ), makeXY(  1, -1 ) ];
		break;
	case 6:
		//    [1]   [4][2]         [4]   [1][3]
		// [2][3]      [3][1]   [3][2]      [2][4]
		// [4]                  [1]
		this.offsets[0] = [ makeXY(  1, -1 ), makeXY( 0, 0 ), makeXY(  1,  0 ), makeXY(  0,  1 ) ];
		this.offsets[1] = [ makeXY(  1,  1 ), makeXY( 0, 0 ), makeXY(  0,  1 ), makeXY( -1,  0 ) ];
		this.offsets[2] = [ makeXY( -1,  1 ), makeXY( 0, 0 ), makeXY( -1,  0 ), makeXY(  0, -1 ) ];
		this.offsets[3] = [ makeXY( -1, -1 ), makeXY( 0, 0 ), makeXY(  0, -1 ), makeXY(  1,  0 ) ];
		// the other shapes have their center of rotation on the left, this one is on the right
		// so adjust to make the piece appear in the middle properly
		break;
	}
	
	// 0: up
	// 1: rotated 90 degrees clockwise (right)
	// 2: rotated 180 degrees (down)
	// 3: rotated 90 degrees  counter-clockwise (left)
	this.orientation	= 0; // always start the shape upright
	this.position		= makeXY( boardCenterX, boardTopY ); // always start top/center of the board

	// our undo values for position and orientation start
	// out as the initial position/orientation
	this.previousPosition		= this.position;
	this.previousOrientation	= this.orientation;

	// create the tiles that make up this block
	for( var i = 0; i < 4; i++ )
		this.tiles[i] = this.createTile( getNextImg() );

	// initializes w/o changing rotation
	this.move( 0 );

	// display our block on the screen
	this.displayMovement();
}

//########################################################
//
// JSTBoard - abstracts the board on which the blocks fall
// 
//########################################################

// check for a collision with this set of positions
// and currently occupied tiles on the board
function collisionTest( positions ) {
	for( var px, py, d = 0; d < positions.length; d++ ) {
		px = getX( positions[d] );
		py = getY( positions[d] );

		if( px < 0 || px >= BoardWidth || py >= BoardHeight )
			return true;

		if( this.tiles[ ( py * BoardWidth ) + px ] )
			return true;
	}

	return false;
}

// following a line being cleared by completion,
// settle all blocks above 'completedLine' down by
// one row.
function settle( completedLine ) {
	if( !completedLine )
		return;

	// start from the line just above the completed line
	var y = completedLine - 1;
	// start the index at the right end of the line above the completed one
	var i = ( y * BoardWidth ) + ( BoardWidth - 1 );
	var iBelow, x, anyTilesOnThisLine;

	// while we have board above
	for( ; y >= 0; y-- ) {
		// reset the tiles check boolean and start at the end of the line working
		// right to left
		for( anyTilesOnThisLine = false, x = BoardWidth - 1; x >= 0 ; x--, i-- ) {
			// skip empty tiles
			if( !this.tiles[i] )
				continue;

			// move the display position of this tile down one row
			this.tiles[i].style.top = ( ( ( y + 1 ) * TileSize ) + BoardCellTop ) + "px";

			// move the tile down one row in the board array
			this.tiles[i + BoardWidth] = this.tiles[i];

			// mark this tile as unoccupied
			this.tiles[i] = null;

			// keep track of whether or not there were any tiles to deal with
			anyTilesOnThisLine = true;
		}

		// if we ran through an empty line, there is nothing above us, so we can stop now.
		if( !anyTilesOnThisLine )
			return;
	}
}

// place a set of tiles on the board
// from the current block which was falling
// but just hit bottom or other blocks on the board
// returns true if a tile has been placed on the top line
// (ending the game) and false if not
function placeTiles( tiles, positions ) {
	var topTouched = false;

	for( var px, py, t = 0; t < positions.length; t++ ) {
		px = getX( positions[t] );
		py = getY( positions[t] );

		// is this tile on the top row?
		if( py == 0 )
			topTouched = true;

		this.tiles[ ( py * BoardWidth ) + px ] = tiles[t];
	}

	return topTouched;
}

// check for completed lines on the rows represented
// in the array of positions.  if a complete line
// is detected, handle it.
// returns number of completed lines
function handleCompleteRows( positions ) {
	var fullRows = new Array();
	// for checking if we already tested this row
	// as BoardHeight is not very large, this is not a major issue
	var testedRows = new Array( BoardHeight );

	var y, x, p, i, t, c;

	// run through the positions that were just placed on the board
	// and check each of those rows for a complete fill.
	// if filled, settle the board above the completed lines.
	for( p = 0; p < positions.length; p++ ) {
		// get the row value
		y = getY( positions[p] );

		// did we already check this one?
		if( testedRows[y] == true )
			continue;

		// mark this one as tested
		testedRows[y] = true;

		// go through the row, counting set tiles
		for( x = 0, i = ( y * BoardWidth ), c = 0; x < BoardWidth; x++, i++ ) {
			if( this.tiles[i] )
				c++;
		}

		// if we have a full row, hang on to that value
		if( c == BoardWidth )
			fullRows.push( y );
	}

	// settle the board from top to bottom - minimizes
	// work required to do this.
	fullRows.sort( function( a, b ) { return a - b; } );

	// settle the board down above these full rows.
	for( c = 0; c < fullRows.length; c++ ) {
		// the completed row to settle onto
		y = fullRows[c];

		// clear the tiles from that row
		for( i = ( y * BoardWidth ), x = 0; x < BoardWidth; x++, i++ ) {
			BoardCell.removeChild( this.tiles[i] );
			this.tiles[i] = null;
		}

		// settle the rows above onto this space
		this.settle( y );
	}

	// return the number of complete lines we found
	return fullRows.length;
}

// clear the board visually and representationally
function clear() {
	// empty the displayed board
	BoardCell.innerHTML = "";

	// clear the tile entries
	for( var t = 0; t < this.tiles.length; this.tiles[t++] = null );
}

function resizeBoard() {
	for( var y = 0, i = 0, x; y < BoardHeight; y++ ) {
		for( x = 0; x < BoardWidth; x++, i++ ) {
			if( !this.tiles[i] )
				continue;
			this.tiles[i].style.left	= ( ( x * TileSize ) + BoardCellLeft )	+ "px";
			this.tiles[i].style.top		= ( ( y * TileSize ) + BoardCellTop )	+ "px";
			this.tiles[i].style.width	= TileSize + "px";
			this.tiles[i].style.height	= TileSize + "px";
		}
	}
}

//
//  Constructor for the JSTBoard object.
//
function JSTBoard() {
	this.clear					= clear;
	this.collisionTest			= collisionTest;
	this.handleCompleteRows		= handleCompleteRows;
	this.placeTiles				= placeTiles;
	this.resize					= resizeBoard;
	this.settle					= settle;

	this.tiles = new Array( BoardWidth * BoardHeight );
	this.clear();
}

//####################################################################
//
// The JSTetris game class - the main class of the game
//
//####################################################################

// move the falling block to the left or right ( -1 = left, 1 = right )
function move( direction ) {
	if( !this.isRunning )
		return;

	// only updates board positions, not displayed position
	this.curBlock.move( direction );

	//don't allow movement into collisions
	if( this.board.collisionTest( this.curBlock.boardPositions ) ) {
		// undo the collided movement
		this.curBlock.undoMovement();
		return;
	}

	// no collision - commit the move to the screen
	this.curBlock.displayMovement();
}

// rotate the falling block counterclockwise (-1) or clockwise (1)
// by 90 degrees
function rotate( direction ) {
	if( !this.isRunning )
		return;

	// only updates board positions, not displayed orientation
	this.curBlock.rotate( direction );

	//don't allow movement into collisions
	if( this.board.collisionTest( this.curBlock.boardPositions ) ) {
		this.curBlock.undoMovement();
		return;
	}

	// no collision - commit the movement to the screen
	this.curBlock.displayMovement();
}

// generate a preview of the next block (a table)
// and show it in the preview area on the ui.
function previewNextBlock() {
    displayNextBlock( genShapePreview( this.nextShape ) );
}

// turns on the 'rush' flag
// which drops the currently falling
// block at maximum speed
function rush() {
	if(!this.isRunning)
		return;
	
	this.isRushing = true;
}

// start a new game
function start() {
	// already running - bail.
	if( this.isRunning )
		return;

	// create a new board object
	this.board = new JSTBoard();

	// reset level and score values
	this.level = 0;
	this.score = 0;

	// initialize the displayed score to match
	// (the level will be set in increaseLevel()
	ScoreTxt.innerHTML = this.score;
	
	// initialize our 'isRushing' boolean to false
	// (piece should fall at normal rate)
	this.isRushing = false;

	// increase game level from 0 (not running)
	// to 1 (first level)
	this.increaseLevel();

	// we keep track of how many times the game has
	// "stepped".  The number of steps per second is constant.
	// the number of steps that must pass before a block falls one line
	// varies as the levels increase.  This is to preserve response time
	// to the controls.
	this.stepCount = 1;
	
	// create our first falling block
	this.curBlock = new JSTBlock( this.getNextShape() );
    
    // show the user what the next block will be.
    this.previewNextBlock();

	// mark this game as running (not paused)
	this.isRunning = true;

	// kick off our first step
	this.step();
}

// get the next 0-6 value.
function getNextShape() {
	// there are 7 possible shapes - see jstBlock.js for details
    if ( this.nextShape < 0 )
        this.nextShape = this.rng.nextRandom( 7 );
    
    var shape = this.nextShape;
    
	this.nextShape = this.rng.nextRandom( 7 );

	return shape;
}

// handle one cycle of interaction/behavior/movement
function step() {
	// if we are paused, bail.
	if( !this.isRunning )
		return;

	// do we need to do step the block down?
	// we do if enough steps have passed that it is time to step the falling block
	// down a row or if we are rushing, in which case we step the block down at
	// maximum speed (1 row per step)
	if( this.isRushing || ( this.stepCount == this.stepsPerLine ) ) {
		// drop the block down by 1 line
		this.curBlock.stepDown();

		// check to see if the step down caused an overlapping placement
		if( this.board.collisionTest( this.curBlock.boardPositions ) ) {
			// undo the overlap
			this.curBlock.undoMovement();

			// place the block's tiles on the board before we nuke this block
			// and make a new one.  as we place the tiles, we check to see if one of them
			// went on the top row (ending the game)
			var topTouched		= this.board.placeTiles( this.curBlock.tiles, this.curBlock.boardPositions );

			// if we have completed lines, eliminate them and drop
			// the tiles above by 1 row
			var completeRows	= this.board.handleCompleteRows( this.curBlock.boardPositions );

			// increment the score
			this.score += completeRows * this.pointsPerLine;
			this.score += this.pointsPerBlock;

			// update the score display
			ScoreTxt.innerHTML = this.score;

			// if we have passed the next level threshold
			// increase the speed and adjust the scoring
			if( this.score >= this.nextLevelScore )
				this.increaseLevel();

			// if the game is over, end it
			if( topTouched ) {
				this.gameOver();
				return;
			}
            
			// make a new block
			this.curBlock = new JSTBlock( this.getNextShape() );
            
            // show the player what the next block will be
            this.previewNextBlock();
            
			// starting over with a new block,
			// don't continue previous rushing behavior
			this.isRushing = false;
		} else {
			// block was moved down with no incident
			// so commit the board positions to the display
			this.curBlock.displayMovement();
		}

		// reset our step count for the next set of steps
		this.stepCount = 1;
	} else {
		// nothing else to do but keep counting steps until
		// we hit the threshold of action
		this.stepCount++;
	}

	// set up another timer for the next step
	this.stepTimer = window.setTimeout( "JSTInstance.step()", StepMilliSeconds );
}

// turn off the 'isRunning' flag
// to stop the steps from occurring
function pause() {
	this.isRunning = false;
}

// resume game play
function resume() {
	if( this.isRunning )
		return;

	// turn the 'isRunning' flag back on
	// and kick off the stepping loop
	this.isRunning = true;
	this.step();
}

// increase the speed at which the blocks fall
// and update the level text in the header
function increaseLevel() {
	switch( this.level ) {
		case 0:
			this.level = 1;
			this.pointsPerLine = 50;
			this.pointsPerBlock = 10;
			this.nextLevelDelta = 750;
			this.nextLevelScore = 750;
			this.stepsPerLine = 10;
			break;
		default:
			this.level++;
			this.pointsPerLine += 50;
			this.pointsPerBlock += 10;
			this.nextLevelDelta = Math.floor( this.nextLevelDelta * 1.5 );
			this.nextLevelScore += this.nextLevelDelta;
			if( this.stepsPerLine > 1 )
				this.stepsPerLine--;
			break;
	}

	// should we clear the board at each new level?
	// this.board.clear();

	LevelTxt.innerHTML = this.level;
}

// ends execution of the game
function gameOver() {
	window.clearTimeout( this.stepTimer );
	this.stepTimer = null;
	this.isRunning = false;
	endGame();
}

function resize() {
	this.board.resize();
	this.curBlock.resize();
}

// constructor for the game object
function JSTetris() {
    this.rng              = new JSTRandom();
	this.increaseLevel	  = increaseLevel;
	this.gameOver		  = gameOver;
	this.getNextShape	  = getNextShape;
    this.previewNextBlock = previewNextBlock;
	this.move			  = move;
	this.pause			  = pause;
	this.resize			  = resize;
	this.resume			  = resume;
	this.rotate			  = rotate;
	this.rush			  = rush;
	this.start			  = start;
	this.step			  = step;
    this.curBlock         = 0;
    this.nextShape        = -1;
}

// ********************************************************
// simple lagged fibonacci sequence random number generator
// ********************************************************
function nextRandom() {
    var nextIdx = ( this.lastIdx + 1 ) % this.lag;
    
    this.history[nextIdx] += this.history[this.lastIdx];
    
    this.lastIdx = nextIdx;
    
    if ( arguments )
        return this.history[nextIdx] % arguments[0];
    
    return this.history[nextIdx];
}

function JSTRandom() {
    this.nextRandom = nextRandom;
    
    this.history    = new Array();
    this.lag        = 53;
    this.lastIdx    = 0;
    
    var twoGigs = 1024 * 1024 * 1024 * 2;
    
    // initialize the history with the so-so random numbers from
    // the built-in rng. (it is too streaky for my taste)
    for ( var i = 0; i < this.lag; i++ )
        this.history[i] = Math.floor( Math.random() * twoGigs );
    
    // grind through the history a few times to get to some decently
    // pseudo-random numbers.
    for ( var j = 0; j <= this.lag * 13; j++ )
        nextRandom();
    
    /*
    var rStr = "";
    
    for ( var k = 0; k < this.lag; k++ ) {
        rStr = rStr + (this.nextRandom() % 100).toString() + " " + (this.nextRandom() % 100).toString() + "\n";
    }
    
    alert( rStr );
    */
}




