Tetris

  CU_MDX_FourBlocksOfFun.zip (172.1 KiB, 3,587 hits)


  CUnitFramework.zip (101.1 KiB, 20,514 hits)

Extract the C-Unit Framework
inside the project (not solution) directory.

This tutorial uses the October 2005 SDK version. Microsoft changed the functionality after this version and I never got around to updating the code.

Tetris is the game that everyone says to create_ for your first game. It’s got everything a basic game should have: user-interaction, time management, basic collision detection, etc. We’ve already learned everything about Managed DirectX that we need to know to create_ Tetris, it’s just a matter of implementing the code.

Tetri...I mean Four Blocks of Fun

Tetris’ main idea is to align shapes. If you fill up a row completely, it disappears and you score. There are many different kinds of shapes but they all share the same basic functionality. Therefore, we will create_ a Shape class that all the different types of shapes will inherit from. Every Shape in Tetris is composed of 4 blocks, the basic unit of Tetris. We will create_ a Block class and each Shape will have an array of 4 Blocks.

using System;
using System.Collections.Generic;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace CUnit
{
/// Each Shape is composed of 4 Blocks
class Block : ICloneable
{
public static Texture BlockTexture;
public static Rectangle BlockRectangle = new Rectangle( 0, 0, 15, 15 );
public static int BlockWidth = 16;
private Vector3 m_position;
private bool m_active;

/// Creates a new Block
/// X position
/// Y position
public Block( float x, float y ) Toggle
{
m_position = new Vector3( x, y, 0f );
m_active = true;
}


/// Renders the block
/// Sprite to render with
/// Color of the block
public void Render( Sprite sprite, Color color ) Toggle
{
if ( m_active )
{
sprite.Draw( BlockTexture, BlockRectangle, Vector3.Empty, m_position, color );
}
}


/// Gets and sets whether the block is active
public bool Active Toggle
{
get { return m_active; }
set { m_active = value; }
}


/// Gets and sets the Blocks X position
public float X Toggle
{
get { return m_position.X; }
set { m_position.X = value; }
}


/// Gets and sets the Blocks Y position
public float Y Toggle
{
get { return m_position.Y; }
set { m_position.Y = value; }
}


/// Clones the block
public object Clone() Toggle
{
Block block = new Block( X, Y );
block.Active = Active;
return block;
}

}
}

This is the Block class. Each block has a position and a method to render itself. We will be using Sprites to render our game, so render the Block with Sprite.Draw. Notice that the Texture, Rectangle and BlockWidth variables are static. This is because every Block in Tetris shares these values so there is no point allocating the memory for new values.

using System;
using System.Collections;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace CUnit
{
/// Base Shape class
class Shape : ICloneable
{
public static Color[] s_Colors = Toggle
{
Color.Red, Color.ForestGreen, Color.SkyBlue, Color.Purple,
Color.Yellow, Color.Blue, Color.Orange
}
;
public enum ShapeType { Straight, Box, T, Zig, Zag, L, J };
protected Block[] m_blocks;
protected Color m_color;
protected int m_state;
protected ShapeType m_type;

/// Creates a new Shape
public Shape() Toggle
{
m_blocks = new Block[4];
m_state = 0;

// Make random color
Random random = new Random( DateTime.Now.Millisecond );
int index = random.Next( 0, s_Colors.Length );
m_color = s_Colors[index];
}


/// Moves each block down 1 unit
public void MoveDown() Toggle
{
foreach ( Block b in m_blocks )
{
b.Y += Block.BlockWidth;
}
}


/// Moves each block right 1 unit
public void MoveRight() Toggle
{
foreach ( Block b in m_blocks )
{
b.X += Block.BlockWidth;
}
}


/// Moves each block left 1 unit
public void MoveLeft() Toggle
{
foreach ( Block b in m_blocks )
{
b.X -= Block.BlockWidth;
}
}


/// Renders the shape
/// Sprite to render with
public void Render( Sprite sprite ) Toggle
{
foreach ( Block b in m_blocks )
{
b.Render( sprite, m_color );
}
}


/// Deactivates the blocks in a given row
/// Row to deactivate
public void DeactivateBlocks( int row ) Toggle
{
for ( int i = 0; i < 4; i++ )
{
if ( m_blocks[i].Y == row )
{
m_blocks[i].Active = false;
}
}
}


/// Determines if a Shape is blocked on the bottom.
/// Bottom border
/// Shapes that are settled
/// true if the Shape is blocked, false otherwise.
public bool TouchingBottom( int bottomBorder, ArrayList shapes ) Toggle
{
// See if any of the blocks are touching the bottom border
// or if they are touching the blocks of other shapes
foreach ( Block b1 in m_blocks )
{
if ( b1.Y >= bottomBorder )
{
return true;
}
foreach ( Shape s in shapes )
{
foreach ( Block b2 in s.Blocks )
{
if ( b2.Active && ( b1.X == b2.X ) && ( b1.Y + Block.BlockWidth == b2.Y ) )
{
return true;
}
}
}
}
return false;
}


/// Determines if a Shape is blocked on the right.
/// Right border
/// Shapes that are settled
/// true if the Shape is blocked, false otherwise.
public bool TouchingRight( int rightBorder, ArrayList shapes ) Toggle
{
// See if any of the blocks are touching the right border
// or if they are touching the blocks of other shapes
foreach ( Block b1 in m_blocks )
{
if ( b1.X >= rightBorder )
{
return true;
}
foreach ( Shape s in shapes )
{
foreach ( Block b2 in s.Blocks )
{
if ( b2.Active && ( b1.X + Block.BlockWidth == b2.X ) && ( b1.Y == b2.Y ) )
{
return true;
}
}
}
}
return false;
}


/// Determines if a Shape is blocked on the left.
/// Left border
/// Shapes that are settled
/// true if the Shape is blocked, false otherwise.
public bool TouchingLeft( int leftBorder, ArrayList shapes ) Toggle
{
// See if any of the blocks are touching the left border
// or if they are touching the blocks of other shapes
foreach ( Block b1 in m_blocks )
{
if ( b1.X <= leftBorder )
{
return true;
}
foreach ( Shape s in shapes )
{
foreach ( Block b2 in s.Blocks )
{
if ( b2.Active && ( b1.X == b2.X + Block.BlockWidth ) && ( b1.Y == b2.Y ) )
{
return true;
}
}
}
}
return false;
}


/// Determines if a Shape is intersecting another Shape or any of the borders.
/// Shapes that are settled
/// Left border
/// Right border
/// Bottom border
/// true if the Shape is intersecting, false otherwise.
public bool IntersectingShapesOrBorders( ArrayList shapes, int leftBorder, int rightBorder, int bottomBorder ) Toggle
{
foreach ( Block b1 in m_blocks )
{
// Intersecting border?
if ( b1.X < leftBorder || b1.X > rightBorder || b1.Y > bottomBorder )
{
return true;
}
// Intersecting another Shape?
foreach ( Shape s in shapes )
{
foreach ( Block b2 in s.Blocks )
{
if ( b2.Active && ( b1.X == b2.X ) && ( b1.Y == b2.Y ) )
{
return true;
}
}
}
}
return false;
}


/// Moves blocks down if a row has been deactivated
/// Deactivated row
public void MoveBlocksAboveRowDown( int row ) Toggle
{
for ( int i = 0; i < 4; i++ )
{
if ( m_blocks[i].Y < row )
{
m_blocks[i].Y += Block.BlockWidth;
}
}
}


/// Checks if all the blocks of the Shape are inactive
/// true if all the blocks are inactive, false otherwise
public bool AllInactive() Toggle
{
foreach ( Block b in m_blocks )
{
if ( b.Active )
{
return false;
}
}
return true;
}


/// Rotates the shape
public virtual void Rotate() Toggle
{
}


/// Gets and sets the Shape’s blocks.
public Block[] Blocks Toggle
{
get { return m_blocks; }
set { m_blocks = value; }
}


/// Gets and sets the Shape’s Color.
public Color Color Toggle
{
get { return m_color; }
set { m_color = value; }
}


/// Gets and sets the Shape’s rotation state.
public int State Toggle
{
get { return m_state; }
set { m_state = value; }
}


/// Clones the Shape.
public object Clone() Toggle
{
   Shape newShape = null;
   switch ( m_type )
   {
       case ShapeType.Straight:
           newShape = new ShapeStraight();
           break;
       case ShapeType.Box:
           newShape = new ShapeBox();
           break;
       case ShapeType.T:
           newShape = new ShapeT();
           break;
       case ShapeType.Zig:
           newShape = new ShapeZig();
           break;
       case ShapeType.Zag:
           newShape = new ShapeZag();
           break;
       case ShapeType.L:
           newShape = new ShapeL();
           break;
       case ShapeType.J:
           newShape = new ShapeJ();
           break;
       default:
           throw new Exception( “Invalid type.” );
   }
   Block[] blocks = new Block[4];
   for ( int i = 0; i < 4; i++ )
   {
       blocks[i] = (Block)m_blocks[i].Clone();
   }
   newShape.Blocks = blocks;
   newShape.Color = m_color;
   newShape.State = m_state;
   return newShape;
}
}
}

This is the base Shape class. It contains all the methods we need to manipulate the Shape. I’ll now give a brief overview of each method. Each Shape is assigned a random color as shown in the constructor using the Random class. The MoveDown, MoveRight, and MoveLeft methods do just what their names imply and are used to move the shapes with the keyboard. The Render method loops through all 4 of its Blocks and Renders them. We call the DeactivateBlocks method when a row has been completely filled up. This deactivates all the Blocks of a given row so they will no longer be rendered or accounted for.

The TouchingBottom, TouchingRight, and TouchingLeft methods are the collision detection methods. They determine when a Shape can no longer move down, right, or left. This will happen when another Shape is in the way or the Shape has reached one of the borders of the play board.

The IntersectingShapesOrBorders method is used to see if a Shape is currently intersecting a border or another Shape. When a Shape is rotated, some of its Blocks may be moved to a location where they intersect another Shape or are out of bounds, which is bad. Therefore, when we rotate a Shape, we will first Clone the Shape, rotate the clone and see if the clone is intersecting any Shapes or borders. If it isn’t then we can rotate the original Shape.

The MoveBlocksAboveRowDown is called when a row has been deactivated. This method moves all Blocks above a given row down one unit to take the place of the deactivated row. The AllInactive method checks if all of the Blocks in the Shape are not active. We will use this method to remove the Shape from our main ArrayList of Shapes, since we don’t need to keep around a Shape if all of its Blocks are inactive.

The Rotate method is dependant upon the type of Shape and will be implemented in the inherited Shapes. When we rotate, however, we need to keep track of the Shape’s current rotation state so we know how to make our next rotation. This is what the m_state variable keeps track of.

Finally, the Clone method is the implementation of the ICloneable interface. We use this method in conjuction with the IntersectingShapesOrBorders method as explained above.

using System;

namespace CUnit
{
   /// Straight shape
   class ShapeStraight : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeStraight() Toggle
       {
           m_blocks[0] = new Block( 0, 0 );
           m_blocks[1] = new Block( Block.BlockWidth, 0 );
           m_blocks[2] = new Block( 2 * Block.BlockWidth, 0 );
           m_blocks[3] = new Block( 3 * Block.BlockWidth, 0 );
           m_type = ShapeType.Straight;
       }


       /// Rotates the shape
       public override void Rotate() Toggle
       {
           switch ( m_state )
           {
               case 0:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].X -= 2 * Block.BlockWidth;
                   m_blocks[3].Y += 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 1:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].X += 2 * Block.BlockWidth;
                   m_blocks[3].Y -= 2 * Block.BlockWidth;
                   m_state–;
                   break;
               default:
                   break;
           }
       }

   }

   /// Box shape
   class ShapeBox : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeBox() Toggle
       {
           m_blocks[0] = new Block( 0, 0 );
           m_blocks[1] = new Block( Block.BlockWidth, 0 );
           m_blocks[2] = new Block( 0, Block.BlockWidth );
           m_blocks[3] = new Block( Block.BlockWidth, Block.BlockWidth );
           m_type = ShapeType.Box;
       }

   }

   /// T shape
   class ShapeT : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeT() Toggle
       {
           m_blocks[0] = new Block( 0, Block.BlockWidth );
           m_blocks[1] = new Block( Block.BlockWidth, Block.BlockWidth );
           m_blocks[2] = new Block( Block.BlockWidth, 0 );
           m_blocks[3] = new Block( 2 * Block.BlockWidth, Block.BlockWidth );
           m_type = ShapeType.T;
       }


       /// Rotates the shape
       public override void Rotate() Toggle
       {
           switch ( m_state )
           {
               case 0:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].X -= Block.BlockWidth;
                   m_blocks[3].Y += Block.BlockWidth;
                   m_state++;
                   break;
               case 1:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].X -= Block.BlockWidth;
                   m_blocks[3].Y -= Block.BlockWidth;
                   m_state++;
                   break;
               case 2:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].X += Block.BlockWidth;
                   m_blocks[3].Y -= Block.BlockWidth;
                   m_state++;
                   break;
               case 3:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].X += Block.BlockWidth;
                   m_blocks[3].Y += Block.BlockWidth;
                   m_state = 0;
                   break;
               default:
                   break;
           }
       }


   }

   /// Zig shape
   class ShapeZig : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeZig() Toggle
       {
           m_blocks[0] = new Block( 0, 0 );
           m_blocks[1] = new Block( 0, Block.BlockWidth );
           m_blocks[2] = new Block( Block.BlockWidth, Block.BlockWidth );
           m_blocks[3] = new Block( Block.BlockWidth, 2 * Block.BlockWidth );
           m_type = ShapeType.Zig;
       }


       /// Rotates the shape
       public override void Rotate() Toggle
       {
           switch ( m_state )
           {
               case 0:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].X -= 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 1:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].X += 2 * Block.BlockWidth;
                   m_state–;
                   break;
               default:
                   break;
           }
       }

   }

   /// Zag shape
   class ShapeZag : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeZag() Toggle
       {
           m_blocks[0] = new Block( Block.BlockWidth, 0 );
           m_blocks[1] = new Block( Block.BlockWidth, Block.BlockWidth );
           m_blocks[2] = new Block( 0, Block.BlockWidth );
           m_blocks[3] = new Block( 0, 2 * Block.BlockWidth );
           m_type = ShapeType.Zag;
       }


       /// Rotates the shape
       public override void Rotate() Toggle
       {
           switch ( m_state )
           {
               case 0:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y += 2 * Block.BlockWidth;
                   m_blocks[1].Y += Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[3].Y -= Block.BlockWidth;
                   m_state++;
                   break;
               case 1:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y -= 2 * Block.BlockWidth;
                   m_blocks[1].Y -= Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[3].Y += Block.BlockWidth;
                   m_state–;
                   break;
               default:
                   break;
           }
       }

   }

   /// L shape
   class ShapeL : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeL() Toggle
       {
           m_blocks[0] = new Block( 0, 0 );
           m_blocks[1] = new Block( 0, Block.BlockWidth );
           m_blocks[2] = new Block( 0, 2 *Block.BlockWidth );
           m_blocks[3] = new Block( Block.BlockWidth, 2 * Block.BlockWidth );
           m_type = ShapeType.L;
       }


       /// Rotates the shape
       public override void Rotate() Toggle
       {
           switch ( m_state )
           {
               case 0:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].X -= 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 1:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].Y -= 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 2:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].X += 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 3:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].Y += 2 * Block.BlockWidth;
                   m_state = 0;
                   break;
               default:
                   break;
           }
       }

   }

   /// J shape
   class ShapeJ : Shape
   {
       /// Initializes the blocks of the shape
       public ShapeJ() Toggle
       {
           m_blocks[0] = new Block( Block.BlockWidth, 0 );
           m_blocks[1] = new Block( Block.BlockWidth, Block.BlockWidth );
           m_blocks[2] = new Block( Block.BlockWidth, 2 * Block.BlockWidth );
           m_blocks[3] = new Block( 0, 2 * Block.BlockWidth );
           m_type = ShapeType.J;
       }


       /// Rotates the shape
       public override void Rotate() Toggle
       {
           switch ( m_state )
           {
               case 0:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].Y -= 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 1:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y += Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y -= Block.BlockWidth;
                   m_blocks[3].X += 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 2:
                   m_blocks[0].X -= Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X += Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].Y += 2 * Block.BlockWidth;
                   m_state++;
                   break;
               case 3:
                   m_blocks[0].X += Block.BlockWidth;
                   m_blocks[0].Y -= Block.BlockWidth;
                   m_blocks[2].X -= Block.BlockWidth;
                   m_blocks[2].Y += Block.BlockWidth;
                   m_blocks[3].X -= 2 * Block.BlockWidth;
                   m_state = 0;
                   break;
               default:
                   break;
           }
       }

   }
}

These are the different types of Shapes found in Tetris. The only difference between them is the layout of their Blocks. Since they all have different Block layouts, they all need to be rotated differently. This took some quick pen-and-paper work to figure out the rotations.

using System;

namespace CUnit
{
   ///
   /// Used to generate random Shapes
   ///

   class ShapeGenerator
   {
       private Random m_random;

       /// Creates a new ShapeGenerator
       public ShapeGenerator() Toggle
       {
           m_random = new Random( DateTime.Now.Millisecond );
       }


       /// Gets a random new Shape
       /// The random Shape
       public Shape GetNextShape() Toggle
       {
           int value = m_random.Next( 7 );
           switch ( value )
           {
               case 0:
                   return new ShapeStraight();
               case 1:
                   return new ShapeBox();
               case 2:
                   return new ShapeT();
               case 3:
                   return new ShapeZig();
               case 4:
                   return new ShapeZag();
               case 5:
                   return new ShapeL();
               case 6:
                   return new ShapeJ();
               default:
                   return new ShapeBox();
           }
       }

   }
}

The ShapeGenerator class is used to generate random Shapes. This is what determines which Shape we are currently playing with. All we do is use the Random class to pick a Shape.

using System;
using System.Collections;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using DirectInput = Microsoft.DirectX.DirectInput;

namespace CUnit
{
/// Tetr…I mean Four Blocks of Fun!
class FourBlocksOfFun
{
private Timer m_gameTimer;
private float m_timePassed;
private float m_shapeFallInterval;
private float m_minFallInterval;
private int m_level;
private ShapeGenerator m_shapeGenerator;
private Shape m_shape;
private ArrayList m_settledShapes;
private Sprite m_sprite = null;
private Surface m_background = null;
private Font m_font = null;
private int m_bottomBorder;
private int m_leftBorder;
private int m_rightBorder;
private int m_numColumns;
private int m_numRows;
private int m_score;
private bool m_endGame;

/// Initialize the game.
public FourBlocksOfFun() Toggle
{
m_settledShapes = new ArrayList();
m_shapeGenerator = new ShapeGenerator();
m_shape = m_shapeGenerator.GetNextShape();
CenterShape( m_shape );
m_timePassed = 0.0f;
m_score = 0;
m_level = 1;
m_shapeFallInterval = 1.0f;
m_minFallInterval = 0.05f;
m_bottomBorder = 480 – Block.BlockWidth;
m_leftBorder = 13 * Block.BlockWidth;
m_rightBorder = 64014 * Block.BlockWidth;
m_numColumns = ( m_rightBorder – m_leftBorder ) / Block.BlockWidth + 1;
m_numRows = 480 / Block.BlockWidth;
m_endGame = false;

m_gameTimer = new Timer();
m_gameTimer.Start();
}


/// Create resources
/// D3D Device
public void OnCreateDevice( Device device ) Toggle
{
// Load Block texture
Block.BlockTexture = TextureLoader.FromFile( device, Utility.GetMediaFile( “block.dds” ) );

// Create Sprite
m_sprite = new Sprite( device );

// Create font
if ( m_font != null )
{
m_font.Dispose();
}
m_font = new Font( device, “Arial”, 18, true, false );
}


/// Release default resources.
public void OnLostDevice() Toggle
{
if ( m_sprite != null )
{
m_sprite.OnLostDevice();
}
if ( m_font != null )
{
m_font.OnLostDevice();
}
if ( m_background != null )
{
m_background.Dispose();
m_background = null;
}
}


/// Create default resources.
/// D3D Device
/// Framework
public void OnResetDevice( Device device, Framework framework ) Toggle
{
if ( m_font != null )
{
m_font.OnResetDevice();
}
if ( m_sprite != null )
{
m_sprite.OnResetDevice();
}

// Create background surface
m_background = device.CreateOffscreenPlainSurface( 640, 480,
framework.CurrentSettings.AdapterFormat, Pool.Default );
SurfaceLoader.FromFile( m_background, Utility.GetMediaFile( “background.jpg” ), Filter.None, 0 );

}


/// Release managed resources.
public void OnDestroyDevice() Toggle
{
if ( Block.BlockTexture != null )
{
Block.BlockTexture.Dispose();
Block.BlockTexture = null;
}
if ( m_font != null )
{
m_font.Dispose();
m_font = null;
}
if ( m_sprite != null )
{
m_sprite.Dispose();
m_sprite = null;
}
}


/// Update the game.
public void Update() Toggle
{
if ( m_endGame )
{
return;
}
m_gameTimer.Update();
m_timePassed += m_gameTimer.ElapsedTime;
if ( m_timePassed >= m_shapeFallInterval )
{
m_timePassed -= m_shapeFallInterval;
if ( m_shape.TouchingBottom( m_bottomBorder, m_settledShapes ) )
{
// Check end game
foreach ( Block b in m_shape.Blocks )
{
if ( b.Y == 0 )
{
m_endGame = true;
break;
}
}
AddToScore( 1 );
m_settledShapes.Add( m_shape );
m_shape = m_shapeGenerator.GetNextShape();
CenterShape( m_shape );

// Check for row completion
CheckRowCompletion();
}
else
{
m_shape.MoveDown();
}
}
}


/// Render the game.
/// D3D Device
public void Render( Device device ) Toggle
{
// Render background
Surface backbuffer = device.GetBackBuffer( 0, 0, BackBufferType.Mono );
device.StretchRectangle( m_background, new Rectangle( 0, 0, 640, 480 ), backbuffer,
new Rectangle( 0, 0, backbuffer.Description.Width, backbuffer.Description.Height ),
TextureFilter.None );
backbuffer.Dispose();

m_sprite.Begin( SpriteFlags.AlphaBlend | SpriteFlags.SortTexture );
m_shape.Render( m_sprite );
foreach ( Shape s in m_settledShapes )
{
s.Render( m_sprite );
}
// Score
m_font.Print( m_sprite, “Level:”, 440, 80, 0, 0, Color.Black.ToArgb(), Font.Alignment.Left );
m_font.Print( m_sprite, m_level.ToString(), 500, 80, 130, 0, Color.Black.ToArgb(), Font.Alignment.Right );
m_font.Print( m_sprite, “Score:”, 440, 100, 0, 0, Color.Black.ToArgb(), Font.Alignment.Left );
m_font.Print( m_sprite, m_score.ToString(), 500, 100, 130, 0, Color.Black.ToArgb(), Font.Alignment.Right );

if ( m_endGame )
{
m_font.Print( m_sprite, “GAME OVER”, 0, 240, 640, 0, Color.Black.ToArgb(), Font.Alignment.Center );
}

m_sprite.End();
}


/// Process input.
/// Array of pressed keys
/// Framework so we can lock keys
public void ProcessInput( DirectInput.Key[] pressedKeys, Framework framework ) Toggle
{
   if ( m_endGame )
   {
       return;
   }
   foreach ( DirectInput.Key k in pressedKeys )
   {
       switch ( k )
       {
           case DirectInput.Key.Left:
               framework.LockKey( DirectInput.Key.Left );
               // Stop at collision
               if ( !m_shape.TouchingLeft( m_leftBorder, m_settledShapes ) )
               {
                   m_shape.MoveLeft();
               }
               break;
           case DirectInput.Key.Right:
               framework.LockKey( DirectInput.Key.Right );
               // Stop at collision
               if ( !m_shape.TouchingRight( m_rightBorder, m_settledShapes ) )
               {
                   m_shape.MoveRight();
               }
               break;
           case DirectInput.Key.Down:
               framework.LockKey( DirectInput.Key.Down );
               // Stop at collision
               if ( !m_shape.TouchingBottom( m_bottomBorder, m_settledShapes ) )
               {
                   m_shape.MoveDown();
                   if ( m_shape.TouchingBottom( m_bottomBorder, m_settledShapes ) )
                   {
                       AddToScore( 1 );
                       // Ready for next shape
                       m_settledShapes.Add( m_shape );
                       m_shape = m_shapeGenerator.GetNextShape();
                       CenterShape( m_shape );
                       m_timePassed = 0.0f;

                       // Check for row completion
                       CheckRowCompletion();
                   }
               }
               break;
           case DirectInput.Key.Up:
               framework.LockKey( DirectInput.Key.Up );
               Shape tempShape = (Shape)m_shape.Clone();
               tempShape.Rotate();
               if ( !tempShape.IntersectingShapesOrBorders( m_settledShapes, m_leftBorder,
                   m_rightBorder, m_bottomBorder ) )
               {
                   m_shape.Rotate();
               }

               break;
       }
   }
}


/// Moves a shape over to the center.
/// Shape to center
public void CenterShape( Shape shape ) Toggle
{
for ( int i = 0; i < 19; i++ )
{
shape.MoveRight();
}
}


/// Checks if a row has been filled up and deactivates blocks on a complete row.
public void CheckRowCompletion() Toggle
{
int columnsFilled = 0;
int rowPosition = 0;
ArrayList rowsToRemove = new ArrayList();
int scoreMultiplier = 1;

// See if a row is completely filled up.
for ( int row = 0; row < m_numRows; row++ )
{
rowPosition = row * Block.BlockWidth;
foreach ( Shape s in m_settledShapes )
{
foreach ( Block b in s.Blocks )
{
if ( b.Active && ( b.Y == rowPosition ) )
{
columnsFilled++;
if ( columnsFilled == m_numColumns )
{
// Mark row for deactivation
rowsToRemove.Add( rowPosition );
scoreMultiplier *= 2;
columnsFilled = 0;
}
}
}
}
columnsFilled = 0;
}

// Update score
if ( scoreMultiplier != 1 )
{
AddToScore( 5 * scoreMultiplier );
}

// Remove the blocks from the rows that are marked for removal
ArrayList shapesToRemove = new ArrayList();
foreach ( int row in rowsToRemove )
{
for ( int i = 0; i < m_settledShapes.Count; i++ )
{
( (Shape)m_settledShapes[i] ).DeactivateBlocks( row );
( (Shape)m_settledShapes[i] ).MoveBlocksAboveRowDown( row );
if ( ( (Shape)m_settledShapes[i] ).AllInactive() )
{
// Mark for removal. Can’t remove here because we’re
// iterating through it.
shapesToRemove.Add( m_settledShapes[i] );
}
}
}

// Remove Shapes that are totally inactive from ArrayList
for ( int i = 0; i < shapesToRemove.Count; i++ )
{
m_settledShapes.Remove( shapesToRemove[i] );
}
}


/// Add to score and increment level if score threshold is met.
/// Amount to add to score.
public void AddToScore( int amount ) Toggle
{
int scoreModBefore = m_score % 100;
m_score += amount;
int scoreModAfter = m_score % 100;

// Increment level every 100 points
if ( scoreModBefore > scoreModAfter )
{
m_level++;
if ( m_shapeFallInterval – 0.1f >= m_minFallInterval )
{
m_shapeFallInterval -= 0.1f;
}
}
}

}
}

This is the main control point of our game, Four Blocks of Fun. Since we’ll be plugging this into our GameApp class, it has the usual On*Device methods to handle resource allocation and deallocation. It also has methods to update_ and render the frame.

The main challenge of Tetris is the speed at which the Shapes fall. This will be determined with the m_shapeFallInterval variable. As the level increases, this value will decrease making the Shapes fall faster. We also have m_minFallInterval so the Shapes don’t keep falling faster and faster forever, which would be way too hard. Our game time will be managed by the Timer class that we wrote earlier.

In OnCreateDevice, we allocate our Block Texture, Sprite and game Font using the Font class we wrote earlier. In OnResetDevice, we create_ the off-screen surface to display the background, which we learned about in the previous tutorial. The OnLostDevice and OnDestroyDevice methods deallocate our game resources.

The Update method updates our game state. It updates the timer and see’s if enough time has passed to move the Shape down one unit. If enough time has passed, we first check to see if the Shape is touching anything on its bottom with Shape.TouchingBottom. If it isn’t touching anything we move the Shape down. If it is touching something, we first check if the game should be over. The game is over when the Shape is both touching a Shape below and touching the top of the screen. If the game isn’t over, it means that the Shape has landed. When the Shape lands, we increase the score with AddToScore, add the shape to our ArrayList of settled Shapes, and generate a new shape with the ShapeGenerator class. Once the Shape has been added to the ArrayList of settled Shapes, we check if any of the rows are complete with CheckRowCompletion.

The Render method simply renders everything to the screen. Since the FourBlocksOfFun class is plugged into the GameApp class, we don’t need to call Device.BeginScene or Device.EndScene, since those are called in GameApp.

The ProcessInput method processes user input. The Down, Left, and Right arrow keys move the Shape. When we move the Shape we check for collisions so we know if we can move the Shape in that direction. When we move down, there’s a chance that the Shape has landed so we do the same thing we did in the Update method: add the Shape to the settled shapes ArrayList and generate a new Shape. When we rotate the Shape with the Up key we have to check if the rotated Shape intersects anything. As explained earlier, we Clone the Shape and see if the clone intersects anything by calling Shape.IntersectingShapesOrBorders. If it isn’t we can rotate the Shape.

Since Shapes are generated at coordinates (0,0), the CenterShape method simply moves it right a few times so it starts in the center.

The CheckRowCompletion method is called everytime a Shape lands. To see if a row is complete, we loop through each row and count the number of blocks on the row. If this number is equal to the total number of columns in a row, the row is completely filled up. When a row is completely filled up, we add the row to an ArrayList so we can deactivate it later on. We don’t deactivate here because we are currently iterating through the rows and deactivating the row would mess up the iteration. We also increase a score multiplier. The score multiplier doubles for each row completed on a given Shape-landing. So for a single row completion, we would get ten points, while a completion of four rows would give us 80 points. Once we’ve found all the rows that need deactivation, we loop through each Shape and deactivate the Blocks on the given row with Shape.DeactivateBlocks. We also move all the higher Blocks down one unit with Shape.MoveBlocksAboveRowDown. Once we deactivate some Blocks, we need to check if all the Blocks of a given Shape are deactivated with Shape.AllInactive. If all the Blocks are inactive, there is no point in keeping the Shape around so we remove the Shape from our settled Shapes ArrayList.

Finally, the AddToScore method adds a given value to the player’s score. Every 100 points we increment the level and lower the shape fall interval so the blocks fall faster.

And there you have it: Tetri…I mean Four Blocks of Fun.