﻿//
//  game.js
//
//  Main game logic
//

var lvlNum;
var score;
var bricksPoppedThisLevel;
var repopDelay;
var nextRepop = 0;
var magnetCol;
var magnetLoaded = 0;
var colorLoaded = -1;
var gameState;
var bricksOnscreen;
var fPaused = false;
var tickCnt = 0;
var fAnimInProgress = false;
var animTimeout = -1;

var GameState_Play = 1;
var GameState_Dying = 2;
var GameState_Leveling = 3;
var GameState_LevelPrompt = 4;
var GameState_Score = 5;
var GameState_PlayAgainPrompt = 6;

//
//                      startGame
//
function startGame()
{
    lvlNum = 1;
    score = 1000;
    findName( 'cnvMagBeam' ).Opacity = 1;
}


//
//                      startLevel
//
function startLevel()
{
    clearTileMap();

    // init scores
    bricksPoppedThisLevel = 0;

    // figure out how fast to add new rows
    repopDelay = 31 - lvlNum * 2;
    if (repopDelay < 6)
        repopDelay = 6;
    nextRepop = Now + repopDelay;

    // deal out some rows
    deal();

    // init misc state vars
    magnetCol = MAGNET_STARTING_COL;
    findName( 'anMagBeam' ).stop();
    findName( 'cnvMagBeam' ).SetValue( 'Canvas.Left', magnetCol * CELLWIDTH);

    // put play area background back to normal color
    findName( 'rctGameboardBg' ).Fill = '#80000000';

    magnetLoaded = 0;
    drawScore();
    drawLevel();
    drawMeter();
    
    var snd = findName( 'sndNewLevel' );
    snd.stop();
    snd.Position = "0:0:0";
    snd.play();
    
    gameState = GameState_Play;
}

//
//                      drawMeter
//
function drawMeter()
{
    var h = bricksPoppedThisLevel * METER_PIXELS_PER_BRICK;
    h = h > METER_HEIGHT ? METER_HEIGHT : h;
    var y = METER_Y + METER_HEIGHT - h;
    var meter = findName( 'rctMeter' );
    meter.SetValue( 'Canvas.Top', y );
    meter.Height = h;
}

//
//                      drawLevel
//
//  Set level label text and centers
//
function drawLevel()
{
    var tb = findName( 'tbLevel' );
    tb.Text = 'Level ' + lvlNum;
    var offset = -tb.ActualWidth / 2;
    tb.SetValue( 'Canvas.Left', LEVEL_X_CENTER + offset );   
}

//
//                      drawScore
//
//  Set score label text and centers
//
function drawScore()
{
    var tb = findName( 'tbScore' );
    tb.Text = '' + score;
    var offset = -tb.ActualWidth / 2;
    tb.SetValue( 'Canvas.Left', SCORE_X_CENTER + offset );   
}

//
//                      clearTileMap
//
//  Resets tile maps and releases all existing tiles.
//
function clearTileMap()
{
    var r, c, b;
    var gb = findName( 'cnvGameGrid' );

    for ( r = 0; r < EXTGRIDHEIGHT; r++ )
    {
        for ( c = 0; c < GRIDWIDTH; c++)
        {
            b = g_aBlocks[r][c];
            if (b != null)
            {
                g_aBlocks[r][c] = null;
                b.destroy();
            }
        }
    }
}

//
//                      deal
//
//  Deal out starting block formation.
//
function deal()
{
    bricksOnscreen = 0;
    var heights;
    var numColors = 4;

    switch (lvlNum)
    {
        case 1:
            heights = new Array( 3, 3, 3, 3, 3, 3, 3, 3, 3 );
            break;
        case 2:
            heights = new Array( 1, 2, 3, 4, 4, 4, 3, 2, 1 );
            break;
        case 3:
            heights = new Array( 5, 4, 3, 2, 1, 2, 3, 4, 5 );
            break;
        case 4:
            heights = new Array( 1, 2, 3, 4, 5, 4, 3, 2, 1 );
            break;

        case 5:
            heights = new Array( 8, 7, 6, 5, 4, 3, 2, 1, 0 );
            break;

        case 6:
            numColors = 5;
            heights = new Array( 0, 1, 2, 3, 4, 5, 6, 7, 8 );
            break;

        case 7:
            numColors = 5;
            heights = new Array( 7, 1, 6, 1, 5, 1, 4, 1, 3 );
            break;

        case 8:
            numColors = 5;
            heights = new Array( 4, 1, 5, 1, 6, 1, 7, 1, 8 );
            break;

        case 9:
            numColors = 5;
            heights = new Array( 8, 8, 6, 6, 4, 4, 2, 2, 0 );
            break;

        default:
            numColors = 5;
            heights = new Array( random(9), random(9), random(9),
                                 random(9), random(9), random(9),
                                 random(9), random(9), random(9)
                                 );
            break;
    }
    var c, r;
    for (c = 0; c < GRIDWIDTH; c++)
    {
        for (r = 0; r < heights[c]; r++)
        {
            var blk;
            while( true )
            {
                blk = genRandomBlock();
                g_aBlocks[r][c] = blk;
                if( checkFor4(c, r) == true )
                {
                    // throw it back and try again
                    blk.destroy();
                }
                else
                {
                    break;
                }
            }

            // got valid one, position it in the UI
            blk.handle.SetValue( 'Canvas.Left', c * CELLWIDTH);
            blk.handle.SetValue( 'Canvas.Top', r * CELLHEIGHT );
            bricksOnscreen++;
        }
    }
}

//
//                      checkFor4
//
//  Looks for 4 adjacent bricks of same color to see if we have poppable blocks.
//
function checkFor4(col, row)
{
    var cntToExamine;
    var cntFound = 0;
    var toExamineCol = new Array(0,0,0,0);
    var toExamineRow = new Array(0,0,0,0);
    var foundCol = new Array(0,0,0,0);
    var foundRow = new Array(0,0,0,0);

    var b = g_aBlocks[row][col]; ;
    if (b == null)
    {
        return false;
    }
    var c = b.color;

    // count ourselves
    foundCol[0] = toExamineCol[0] = col;
    foundRow[0] = toExamineRow[0] = row;
    cntToExamine = cntFound = 1;
    b.color = BLOCK_MARKER;

    // do work
    while (cntToExamine != 0 && cntFound != 4)
    {
        cntToExamine--;
        col = toExamineCol[cntToExamine];
        row = toExamineRow[cntToExamine];

        // look behind us
        var newrow, newcol;
        var blkTest;
        if (row != 0)
        {
            newcol = col;
            newrow = row - 1;
            blkTest = g_aBlocks[newrow][newcol];
            if (blkTest != null && blkTest.color == c)
            {
                blkTest.color = BLOCK_MARKER;
                toExamineCol[cntToExamine] = foundCol[cntFound] = newcol;
                toExamineRow[cntToExamine] = foundRow[cntFound] = newrow;
                cntToExamine++;
                cntFound++;
                if (cntFound == 4)
                    continue;
            }
        }

        // look in front of us
        if (row < GRIDHEIGHT - 1)
        {
            newcol = col;
            newrow = row + 1;
            blkTest = g_aBlocks[newrow][newcol];
            if (blkTest != null && blkTest.color == c)
            {
                blkTest.color = BLOCK_MARKER;
                toExamineCol[cntToExamine] = foundCol[cntFound] = newcol;
                toExamineRow[cntToExamine] = foundRow[cntFound] = newrow;
                cntToExamine++;
                cntFound++;
                if (cntFound == 4)
                    continue;
            }
        }

        // look to the left
        if (col != 0)
        {
            newcol = col - 1;
            newrow = row;
            blkTest = g_aBlocks[newrow][newcol];
            if (blkTest != null && blkTest.color == c)
            {
                blkTest.color = BLOCK_MARKER;
                toExamineCol[cntToExamine] = foundCol[cntFound] = newcol;
                toExamineRow[cntToExamine] = foundRow[cntFound] = newrow;
                cntToExamine++;
                cntFound++;
                if (cntFound == 4)
                    continue;
            }
        }

        // look to the right
        if (col < GRIDWIDTH - 1)
        {
            newcol = col + 1;
            newrow = row;
            blkTest = g_aBlocks[newrow][newcol];
            if (blkTest != null && blkTest.color == c)
            {
                blkTest.color = BLOCK_MARKER;
                toExamineCol[cntToExamine] = foundCol[cntFound] = newcol;
                toExamineRow[cntToExamine] = foundRow[cntFound] = newrow;
                cntToExamine++;
                cntFound++;
                if (cntFound == 4)
                    continue;
            }
        }
    }

    // found all we could, set 'em back
    var i;
    for (i = 0; i < cntFound; i++)
    {
        g_aBlocks[foundRow[i]][foundCol[i]].color = c;
    }

    // return the result
    return cntFound >= 4;
}

//
//                      onTick
//
// This is the main game engine here.  This is a regular tick every 1/20 second and contains
//  all the logic for checking for keys and moving stuff.
//
function onTick()
{
    switch (gameState)
    {
        case GameState_Play:
            runLevel();
            break;
            
        case GameState_Leveling:
        case GameState_Dying:
            endLevel();
            break;
            
        case GameState_LevelPrompt:
        case GameState_PlayAgainPrompt:
            waitForEnter();
            break;
    }
    setTimeout( "onTick()", 50 );
}

//
//                      runLevel
//
function runLevel()
{
    // if we're paused, keep the newRow time at bay
    if (fPaused)
    {
        nextRepop = Now + 60;
        return;
    }

    // DEBUG
    if ((tickCnt++ % 100) == 0)
    {
        //dumpBlocks();
    }

    // first look for queued deferred UI commands
    runDeferredUI();

    // if we're still animating or processing deferred UI commands, ignore keys and new rows
    if (fAnimInProgress || cmdq.length != 0)
    {
        return;
    }

    // check for need to repop bricks
    if (bricksOnscreen < 27)
    {
        newRepop = Now + 2;
        if (nextRepop > newRepop)
            nextRepop = newRepop;
    }
    if (Now > nextRepop)
    {
        newRow();
    }

    // nothing else preempting, check for keys
    var key = checkKey();
    switch (key)
    {
        case KEY_LEFT:
            if (magnetCol != 0)
            {
                var an = findName( 'anMagBeamX' );
                an.From = magnetCol * CELLWIDTH;
                magnetCol--;
                an.To = magnetCol * CELLWIDTH;
                an.Duration = "0:0:0.15";
                findName( 'anMagBeam' ).begin();

                var i;
                for (i = 0; i < magnetLoaded; i++)
                {
                    var blk = g_aAnimBlocks[i];
                    blk.animate( null, an.From, blk.Y, an.To, blk.Y, 0.15 );
                }
                if( fAnimInProgress )
                {
                    // cancel existing animation timer
                    clearTimeout( animTimeout );
                }
                animTimeout = setTimeout( "animDone()", 150 );
                fAnimInProgress = true;
                
                // make a sound
                var snd = findName( 'sndMoveLeftRight' );
                snd.stop();
                snd.Position = "0:0:0";
                snd.play();
            }
            break;

        case KEY_RIGHT:
            if (magnetCol != GRIDWIDTH - 1)
            {
                var an = findName( 'anMagBeamX' );
                an.From = magnetCol * CELLWIDTH;
                magnetCol++;
                an.To = magnetCol * CELLWIDTH;
                an.Duration = "0:0:0.15";
                findName( 'anMagBeam' ).begin();
                var i;
                for (i = 0; i < magnetLoaded; i++)
                {
                    var blk = g_aAnimBlocks[i];
                    blk.animate( null, an.From, blk.Y, an.To, blk.Y, 0.15 );
                }
                if( fAnimInProgress )
                {
                    // cancel existing animation timer
                    clearTimeout( animTimeout );
                }
                animTimeout = setTimeout( "animDone()", 150 );
                fAnimInProgress = true;
                var snd = findName( 'sndMoveLeftRight' );
                snd.stop();
                snd.Position = "0:0:0";
                snd.play();
            }
            break;

        case KEY_DOWN:
            attractTiles();
            break;

        case KEY_UP:
            if (magnetLoaded != 0)
            {
                releaseTiles();
            }
            break;

    }
}

//
//                      animDone
//
//  Called approx when an animation should have completed
//
function animDone()
{
    animTimeout = -1;
    fAnimInProgress = false;
    
    // TODO do whatever
}    

//
//                      waitForEnter
//
//  Wait for user to hit enter and then clear the current modal message.
//
function waitForEnter()
{
    if (checkKey() == KEY_ENTER)
    {
        findName( 'cnvMessage' ).Opacity = 0;
        if (gameState == GameState_PlayAgainPrompt)
        {
            startGame();
        }
        gameState = GameState_Play;
        startLevel();
        findName( 'anTinyStars' ).resume();
    }
}


//
//                      runDeferredUI
//
function runDeferredUI()
{
    while (!fAnimInProgress && cmdq.length != 0)
    {
        // pop off a command
        var qc = cmdq.shift();

        // see if it's an end-of-tick indication
        if (qc.Code == QcWait)
        {
            // that's all we do this tick
            break;
        }

        // decode the command
        switch (qc.Code)
        {
            case QcMove:
                {
                    var x = qc.Arg1;
                    var y = qc.Arg2;
                    var blk = qc.Object;
                    blk.handle.SetValue( 'Canvas.Left', x );
                    blk.handle.SetValue( 'Canvas.Top', y );
                    
                    // this is the secret trick for detaching all previous animations
                    //img.BeginAnimation(Canvas.TopProperty, null);
                    //img.BeginAnimation(Canvas.LeftProperty, null);
                }
                break;
            case QcPop:
                {
                    var blk = qc.Object;
                    var x = blk.handle.GetValue( 'Canvas.Left' );
                    var y = blk.handle.GetValue( 'Canvas.Top' );

                    // and show some fireworks in this position
                    launchFirework(x, y);

                    // scrub the block
                    blk.destroy();
                    
                    // play sound
                    var snd = findName( 'sndPop' );
                    snd.stop();
                    snd.Position = "0:0:0";
                    snd.play();
                }
                break;
                
            case QcTurnWhite:
                {
                    var blk = qc.Object;
                    blk.handle.Source = 'images/white.png';
                }
                break;
        }
    }
}

var starIndex = 0;

//
//                      launchFirework
//
function launchFirework(x, y)
{
    var star;
    star = g_aStars[starIndex];
    starIndex = (starIndex + 1) % g_aStars.length;
    star.animate( x, y );
    
    // pull star from the canvas and add again to move to top of Z
    var children = findName( 'cnvGameGrid' ).children;
    children.remove( star.handle );
    children.add( star.handle );
}

//
//                      newRow
//
//  Adds a row of bricks to the grid.
//
function newRow()
{
    // move entire map up
    var c, r;
    for (c = 0; c < GRIDWIDTH; c++)
    {
        for (r = GRIDHEIGHT - 1; r > 0; r--)
        {
            var blk = g_aBlocks[r - 1][c];
            g_aBlocks[r][c] = blk;
            g_aBlocks[r - 1][c] = null;
            if (blk != null)
            {
                blk.handle.SetValue( 'Canvas.Top', CELLHEIGHT * r);

                // this is the secret trick to detach prev animations so that the above
                // "set" will stick
                // TODO blk.Image.BeginAnimation(Canvas.TopProperty, null);
            }
        }
    }

    // now gen tiles
    for (c = 0; c < GRIDWIDTH; c++)
    {
        var b;
        while( true )
        {
            b = g_aBlocks[0][c] = genRandomBlock();
            if(checkFor4(c, 0))
            {
                // throw it back and try again
                b.destroy();
            }
            else
            {
                // this works, break out
                break;
            }
        }
        b.handle.SetValue( 'Canvas.Top', 0 );
        b.handle.SetValue( 'Canvas.Left', CELLWIDTH * c);
        bricksOnscreen++;
    }

    // see if we crossed the line
    for (c = 0; c < GRIDWIDTH; c++)
    {
        var blk = g_aBlocks[WARNINGROW][c];
        if (blk != null)
        {
            gameState = GameState_Dying;
            break;
        }
    }

    // reset repop time
    nextRepop = Now + repopDelay;
}


//
//                      endLevel
//
//  Level is ending, run out any queued cmds and then prompt the user.
//
function endLevel()
{
    runDeferredUI();

    // see if we're done
    if (cmdq.length == 0 && !fAnimInProgress)
    {
        var bonus = Math.round( lvlNum * score / 12 );
        if (gameState == GameState_Dying)
        {
            // show 'em the level completion screen
            messageBox("Game Over", "Bonus: " + bonus);
            gameState = GameState_PlayAgainPrompt;
        }
        else
        {
            // show 'em the level completion screen
            messageBox("Level " + lvlNum + " Complete", "Bonus: " + bonus);
            gameState = GameState_LevelPrompt;
        }

        // play sound
        var snd = findName( 'sndEndLevel' );
        snd.stop();
        snd.Position = "0:0:0";
        snd.play();
        
        score += bonus;
        lvlNum++;
    }
}

//
//                         messageBox
//
//  Put up a sorta modal prompt for the user
//
function messageBox(s1, s2)
{
    var MsgLeft = 60;

    var obj = findName( 'tbS1' );
    obj.Text = s1;
    var w = obj.ActualWidth;
    obj.SetValue( 'Canvas.Left', (300 - w) / 2 );
    
    obj = findName( 'tbS2' );
    obj.Text = s2;
    w = obj.ActualWidth;
    obj.SetValue( 'Canvas.Left', (300 - w) / 2 );

    obj = findName( 'tbOK' );
    w = obj.ActualWidth;
    obj.SetValue( 'Canvas.Left', (300 - w) / 2 );
    
    findName( 'cnvMessage' ).Opacity = 1;    
    
    // pause the star field
    findName( 'anTinyStars' ).pause();
}


//
//                      attractTiles
//
//  Attract files to the magnet
//
function attractTiles()
{
    // check for magnet full
    if( magnetLoaded == MAGNET_MAX_LOAD )
        return;
        
    // find closest tile
    var blk = null;
    var row;
    for (row = WARNINGROW; row >= 0; row--)
    {
        blk = g_aBlocks[row][magnetCol];
        if (blk != null)
        {
            break;
        }
    }

    // make sure we got a tile
    if (blk == null)
    {
        // empty row
        return;
    }

    // see what color we're doing
    var bc = blk.color;

    // see if we've already partially loaded the magnet
    if( magnetLoaded != 0 )
    {
        // make sure block we found matches color we're working with
        if( colorLoaded != bc )
            return;
    }
    else
    {
        // starting anew, clear out magnet blocks and loaded count
        var i;
        for (i = 0; i < magnetBlocks.length; i++)
        {
            magnetBlocks[i] = null;
        }
        magnetLoaded = 0;
    }
    
    // see how many we can grab
    var cnt = 0;
    while (row >= 0 && magnetLoaded < MAGNET_MAX_LOAD )
    {
        if (g_aBlocks[row][magnetCol].color == bc)
        {
            magnetBlocks[magnetLoaded + cnt++] = g_aBlocks[row][magnetCol];
            g_aBlocks[row][magnetCol] = null;
            row--;
        }
        else
        {
            break;
        }
    }
    row++;

    // calc the amount we'll move 'em in Y
    var steps = MAGNETHEIGHT - row - cnt - magnetLoaded;

    // now set the animators
    var extraDelay = 0.050 * cnt;
    var canvasLeft = magnetCol * CELLWIDTH;
    animSetCallback( onAttractDone );
    for (i = 0; i < cnt; i++)
    {
        // set up an animation to move
        blk = magnetBlocks[i + magnetLoaded];
        var img = blk.img;
        
        // make the real block invis
        blk.handle.Opacity = 0;
        
        // set up the animated block as a stand-in
        blk = g_aAnimBlocks[i + magnetLoaded];
        var from = CELLHEIGHT * (row + i);
        blk.animate( img,
                     canvasLeft,
                     from,
                     canvasLeft,
                     from + CELLHEIGHT * steps,
                     0.050 + extraDelay );
    }

    // mark the magnet as loaded (or update the loaded count if this is a multiple load)
    magnetLoaded += cnt;
    colorLoaded = bc;

    // and turn off beam
    findName( 'anBeamOpacity' ).stop();
    findName( 'rctBeam' ).Opacity = 0;
    fAnimInProgress = true;
    
    // make sound
    var snd = findName( 'sndAttract' );
    snd.stop();
    snd.Position = "0:0:0";
    snd.play();
}

//
//                      onAttractDone
//
function onAttractDone()
{
    fAnimInProgress = false;
}

//
//                      releaseTiles
//
//  Let tiles drop back onto grid column.
//
function releaseTiles()
{
    // find first availabe row in this col
    var row;
    for (row = WARNINGROW; row >= 0; row--)
    {
        if (g_aBlocks[row][magnetCol] != null)
        {
            row++;
            break;
        }
    }
    if (row < 0)
        row = 0;

    // set up for the animation
    var cnt = magnetLoaded;
    var steps = MAGNETHEIGHT - row - cnt;

    // set up the animations
    var extraDelay = 0;
    animSetCallback( onReleaseDone );
    var i;
    for (i = 0; i < magnetLoaded; i++)
    {
        // set up an animation to move the surrogate animblock
        var blk = g_aAnimBlocks[i];
        var to = (row + i) * CELLHEIGHT;
        blk.animate( null, 
                     blk.X, 
                     to + CELLHEIGHT * steps,
                     blk.X,
                     to,
                     0.050 + extraDelay );
        fAnimInProgress = true;
        extraDelay += 0.050;
    }

    // now install tiles in the slots
    for (i = 0; i < magnetLoaded; i++)
    {
        if (row + i > WARNINGROW)
        {
            gameState = GameState_Dying;
        }
        g_aBlocks[row + i][magnetCol] = magnetBlocks[i];
    }
    
    // play sound
    var snd = findName( 'sndRelease' );
    snd.stop();
    snd.Position = "0:0:0";
    snd.play();
}

//
//                      onReleaseDone
//
function onReleaseDone()
{
    var i;

    fAnimInProgress = false;

    // hide the anim blocks and make the real blocks visible again and put in right position
    for( i = 0; i < magnetBlocks.length; i++ )
    {
        var blk = magnetBlocks[i];
        if( blk == null )
            continue;
        var ablk = g_aAnimBlocks[i];
        blk.handle.Opacity = 1;
        blk.handle.SetValue( 'Canvas.Left' ) = ablk.X;
        blk.handle.SetValue( 'Canvas.Top' ) = ablk.Y;
        blk = g_aAnimBlocks[i].handle.Opacity = 0;
    }
    // find first availabe row in this col
    var row;
    for (row = WARNINGROW; row >= 0; row--)
    {
        if (g_aBlocks[row][magnetCol] != null)
        {
            break;
        }
    }
    if (row < 0)
        row = 0;

    // re-enable beam
    magnetLoaded = 0;
    findName( 'anBeamOpacity' ).begin();

    // now let's look for stuff to pop
    if (checkFor4(magnetCol, row))
    {
        // capture initial pop count
        var origPopped = bricksPoppedThisLevel;

        // pop stuff
        pop(magnetCol, row);

        // compact the map if necessary
        while (compact())
            ;

        // update score
        drawScore();
        drawMeter();


        // see if they get a special bonus
        var delta = bricksPoppedThisLevel - origPopped;
        // if more than 7
        if (delta > 7)
        {
            doCoin(5000);
        }
        // if more than 5
        else if (delta > 5)
        {
            doCoin(1000);
        }

        // see if we finished a level
        if (bricksPoppedThisLevel > BRICKS_PER_LEVEL)
        {
            // brighten up the background
            findName( 'rctGameboardBg' ).Fill = '#40F0F0F0';
            gameState = GameState_Leveling;
        }
    }
}

//
//                      doCoin
//
function doCoin( amt )
{
    var grid = findName( 'cnvGameGrid' );
    var cnv = findName( 'cnvCoin' );
    var tb = findName( 'tbCoin' );
    tb.Text = '' + amt;
    
    // pull off of game grid and put back to move to top of Z
    grid.children.remove( cnv );
    grid.children.add( cnv );
    
    // pick a location near the magnet but not on it
    var x;
    if( magnetCol >= 4 )
    {
        x = (magnetCol - 2) * 45;
    }
    else
    {
        x = (magnetCol + 2) * 45;
    }
    cnv.SetValue( 'Canvas.Left', x );
    score += amt;
    var an = findName( 'anCoin' );
    an.stop();
    an.begin();
}

//
//                      pop
//
function pop( col, row)
{
    var bc = g_aBlocks[row][col].color;

    // queue up the deferred UI stuff
    var qc = new QCmd(QcTurnWhite, g_aBlocks[row][col]);
    cmdq.push(qc);
    var wqc = new QCmd(QcWait);
    cmdq.push(wqc);
    
    qc = new QCmd(QcPop, g_aBlocks[row][col]);
    cmdq.push(qc);
    cmdq.push(wqc);

    // remove the one we're popping
    g_aBlocks[row][col] = null;

    // look left
    var blk;
    if (col != 0)
    {
        blk = g_aBlocks[row][col - 1];
        if (blk != null && blk.color == bc)
        {
            pop(col - 1, row);
        }
    }

    // look right
    if (col < GRIDWIDTH - 1)
    {
        blk = g_aBlocks[row][col + 1];
        if (blk != null && blk.color == bc)
        {
            pop(col + 1, row);
        }
    }

    // look down
    if (row < WARNINGROW)
    {
        blk = g_aBlocks[row + 1][col];
        if (blk != null && blk.color == bc)
        {
            pop(col, row + 1);
        }
    }

    // look up
    if (row > 0)
    {
        blk = g_aBlocks[row - 1][col];
        if (blk != null && blk.color == bc)
        {
            pop(col, row - 1);
        }
    }

    // bookkeeping
    score += 50 * lvlNum;
    bricksPoppedThisLevel++;
    bricksOnscreen--;
}

//
//                      compact
//
// Compacts blocks after popping has left voids
//
function compact()
{
    var didSomething = false;
    var c, r;
    for (c = 0; c < GRIDWIDTH; c++)
    {
        var inEmpty = false;
        for (r = 0; r < WARNINGROW; r++)
        {
            if (g_aBlocks[r][c] != null)
            {
                if (inEmpty)
                {
                    compactRow(c, r);
                    return true;
                }
            }
            else
            {
                inEmpty = true;
            }
        }
    }
    return didSomething;
}

//
//                      compactRow
//
//  'row' is where we re-encountered tiles so we figure out how far to move them down
// 	 and then try popping some stuff.
function compactRow(col, row)
{
    // walk back down to the beginning of the unoccupied region
    var r = row - 1;
    while (r >= 0)
    {
        if (g_aBlocks[r][col] != null)
            break;
        r--;
    }
    r++;
    var emptyStart = r;
    var destRow = emptyStart;
    var srcRow = row;
    var rowsToMove = row - emptyStart;

    // now slide the tiles down
    var blk;
    var cnt = 0;
    while (srcRow < GRIDHEIGHT && (blk = g_aBlocks[srcRow][col]) != null)
    {
        g_aBlocks[destRow][col] = blk;
        g_aBlocks[srcRow][col] = null;
        var qc = new QCmd(QcMove,
            blk,
            col * CELLWIDTH,
            destRow * CELLHEIGHT);
        cmdq.push(qc);
        srcRow++;
        destRow++;
        cnt++;
    }

    // check for pops
    var i;
    for (i = 0; i < cnt; i++)
    {
        if (checkFor4(col, emptyStart + i))
        {
            pop(col, emptyStart + i);
        }
    }
}

//
//                      launch
//
function launch()
{
    gameState = GameState_PlayAgainPrompt;
    messageBox( "Use arrow keys.", "Explode 4 or more blocks!" );   
}