I am writing a Tetris program in C# for programming practice, and I've got it working. This will be the first post in a multi-part series. Future posts will cover the TetrisBoard
(the game logic), the WinForm, the AI, and maybe a graphical board and sounds that I will code later.
Design
Some design decisions worth mentioning before you read the code include:
- I chose to use a 5x5 char array to represent each piece. 0 is empty, 1 is filled.
- For piece rotation, the center square of the 5x5 array is used as the pivot point.
- The top two rows of the 5x5 array start hanging off the top of the board, and are only used if the piece gets rotated.
- I particularly like the simple solution I came up with to rotate the 5x5 board in the
RotateSquareArray()
method. I was reading another answer on StackOverflow and they were using trigonometry (sine, cosine) to rotate array contents. That just seemed way too complicated. If you plot out the coordinates in a table, the number changing pattern is clear, and has been captured in myRotateSquareArray()
method. - The piece letters are short for their colors. I had the colors in here before, but I moved them to the graphics class so that the graphics class can choose the color of each piece.
Feedback
Specific areas that might need improvement include:
- I'm still getting used to C# styling conventions. Things like camelCase vs PascalCase, and if ( conditional ) vs if(conditional).
- Those CLOCKWISE and RIGHT/LEFT constants might need a better data structure. Enum?
- Any advice on the code in
TetrisPiece(TetrisPiece PieceToCopy)
that deep copies the class? I'm surprised there isn't a "copy" keyword or something in the C# language.- I might delete the Orientation variable. The
TetrisBoard
class never needs it or uses it.
- I might delete the Orientation variable. The
using System;
namespace MillenniumTetris
{
public class TetrisPiece
{
#region Variables, Constants, Literals
private string FullName;
private char OneLetterName;
private int[,] Shape;
private int[] Location;
// TODO: Delete orientation variable. The TetrisBoard class doesn't need it.
private int Orientation;
public const int NO_ROTATION = 0;
public const int CLOCKWISE = 1;
public const int ONE_HUNDRED_EIGHTY_DEGREES = 2;
public const int COUNTER_CLOCKWISE = 3;
public const int LEFT = 1;
public const int RIGHT = 2;
public const int DOWN = 3;
public const int UP = 4;
#endregion
#region Public Methods
public TetrisPiece()
{
Random rnd = new Random();
int PieceShape = rnd.Next(1, 7);
Location = new int[] { 0, 0 };
Orientation = NO_ROTATION;
switch (PieceShape)
{
// The center of the 5x5 grid is the pivot point.
// The top two rows of the 5x5 grid will hang off the top of the board.
// Therefore they should be left empty, and will only be used after a rotation.
case 1:
FullName = "Stick";
OneLetterName = 'O';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,1,1,1,1},
{0,0,0,0,0},
{0,0,0,0,0}
};
break;
case 2:
FullName = "L";
OneLetterName = 'B';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,1,1,1,0},
{0,1,0,0,0},
{0,0,0,0,0}
};
break;
case 3:
FullName = "BackwardsL";
OneLetterName = 'P';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,1,1,1,0},
{0,0,0,1,0},
{0,0,0,0,0}
};
break;
case 4:
FullName = "Trapezoid";
OneLetterName = 'Y';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,0,1,0,0},
{0,1,1,1,0},
{0,0,0,0,0}
};
break;
case 5:
FullName = "RightLeaningParallelogram";
OneLetterName = 'L';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,0,1,1,0},
{0,1,1,0,0},
{0,0,0,0,0}
};
break;
case 6:
FullName = "LeftLeaningParallelogram";
OneLetterName = 'G';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,1,1,0,0},
{0,0,1,1,0},
{0,0,0,0,0}
};
break;
case 7:
FullName = "Square";
OneLetterName = 'R';
Shape = new int[5, 5] {
{0,0,0,0,0},
{0,0,0,0,0},
{0,0,1,1,0},
{0,0,1,1,0},
{0,0,0,0,0}
};
break;
}
}
public TetrisPiece(TetrisPiece PieceToCopy)
{
// deep copy
// I can't believe this is so hard in C# :/
FullName = String.Copy(PieceToCopy.GetFullName()); // string
OneLetterName = PieceToCopy.GetOneLetterName(); // char
Shape = new int[5, 5];
Array.Copy(PieceToCopy.GetShape(), Shape, 25); // int[,]
Location = new int[2];
Array.Copy(PieceToCopy.GetLocation(), Location, 2); // int[]
Orientation = PieceToCopy.GetOrientation(); // int
}
public void ChangeLocation(int[] NewLocation)
{
// it's up to the game board to check for ArgumentExceptions since it depends on game
// board size
Location[0] = NewLocation[0];
Location[1] = NewLocation[1];
}
public string GetFullName()
{
return FullName;
}
public char GetOneLetterName()
{
return OneLetterName;
}
public int[,] GetShape()
{
return Shape;
}
public int[] GetLocation()
{
return Location;
}
public int GetOrientation()
{
return Orientation;
}
public void MoveOneSquare(int Direction)
{
if (Direction == DOWN)
{
Location[0]++;
}
else if (Direction == UP)
{
Location[0]--;
}
else if (Direction == LEFT)
{
Location[1]--;
}
else if (Direction == RIGHT)
{
Location[1]++;
}
else
{
throw new ArgumentException("To move the piece one square, the direction must be correctly specified.");
}
}
public void Rotate(int TypeOfRotation)
{
// TODO: check for ArgumentException?
Shape = RotateSquareArray(Shape, TypeOfRotation);
Orientation = (Orientation + TypeOfRotation) % 4;
}
#endregion
#region Private Methods
private int[,] RotateSquareArray(int[,] OldArray, int TypeOfRotation)
{
int rows = OldArray.GetLength(0);
int columns = OldArray.GetLength(1);
if ( rows != columns )
{
throw new ArgumentException("In order to be rotated, the array must be square. The number of columns must equal the number of rows.");
}
int[,] NewArray = new int[rows, columns];
int NumberOfClockwiseRotations = TypeOfRotation;
for (int RotationNum = 1; RotationNum <= NumberOfClockwiseRotations; RotationNum++)
{
for (int RowNum = 0; RowNum < rows; RowNum++)
{
for (int ColNum = 0; ColNum < columns; ColNum++)
{
NewArray[ColNum, columns - 1 - RowNum] = OldArray[RowNum, ColNum];
}
}
}
return NewArray;
}
#endregion
}
}