5
\$\begingroup\$

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 my RotateSquareArray() 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.

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
    }
}
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Let's start with small stuff.

Casing is indeed wrong - it should be lower (Pascal) case for variables, parameters and private fields.

So, eg. instead of

for (int ColNum = 0; ColNum < columns; ColNum++)

should be:

for (int colNum = 0; colNum < columns; colNum++)

Refer to https://msdn.microsoft.com/en-us/library/ms229043%28v=vs.100%29.aspx for more info.

In C# it is also a bit alien to use Get... methods, because C# has a thing called properties. More info: https://msdn.microsoft.com/en-us/library/x9fsa0sw.aspx

Properties allow to replace

private string fullName;

public string GetFullName()
{
    return FullName;
}

with:

public string FullName
{
    // "get" is implicitly public, because FullName itself is public
    get;

    // this mirrors your implementation, but you can use any access modifiers in properties
    private set; 
}

That's idiomatic C#.

You are correct that constants could be replaced with enums.

I have to say I don't particularly like the design of TetrisPiece. It feels very wrong to me that the shape is chosen at random shape in the constructor. TetrisPiece represents a single piece - and randomizing pieces isn't a responsibility of a single piece. This violates Single Responsibility Principle and Open/Closed Principle.

Here's how I would tackle it, thinking out loud - obviously there isn't one right (or wrong) solution, I'll just try to sort of pitch my OOD approach to you.

I'd make TetrisPiece an abstract class, in which Shape, FullName and OneLetterName are abstract properties.

Then I would define concrete pieces (such as Stick) as its subclasses, and create a multiton - https://en.wikipedia.org/wiki/Multiton_pattern - containing all possible pieces.

(If you used Java, you could use an enum - Java enums are more powerful than their C# counterpart, see https://stackoverflow.com/questions/469287/c-sharp-vs-java-enum-for-those-new-to-c - but we can emulate it in C# with multiton pattern).

I would keep them immutable (read-only): these are our Platonic ideas of pieces, if you will, they themselves don't ever change. A stick is always the same stick. Only a single copy of a Stick is ever allowed to exist.

Let's rename it from TetrisPiece to TetrisShape to acknowledge that.

Now, pieces in the sense of multiple pieces located on the screen, these can be numerous. That would be our TetrisPieces. Each TetrisPiece has a readonly Shape property, set to one of predefined TetrisShapes.

A TetrisPiece knows its Shape, and it knows its Location and Roration. The former two can be mutable.

And the TetrisPiece exposes a Sprite matrix (as in https://en.wikipedia.org/wiki/Sprite_%28computer_graphics%29). The Sprite takes the matrix from the Shape, and transforms it accordingly to the piece's Rotation value.

The result could be cached, or always calculated on demand - it's not computationally expensive in this case.

It kind of solves your deep-copying problems. Shapes don't need to be copied: there's only 7 of them in existence (the multiton), and each piece only points to one of them.

I would move randomization of pieces into another class - let's call it PieceProvider. Objects galore, but, well, that's consistent with Single Responsibility Principle. What if one day you wanted to adjust probabilities for different shapes? You shouldn't have to meddle with the implementation of the TetrisPiece itself. What if you wanted these probabilities different for various difficulty levels, for instance? Would you then supply the TetrisPiece constructor with yet another parameter? That's an increasingly clever piece we have, whereas it would be much simplier to just switch between different PieceProviders.

And one final remark: note I didn't name it TetrisPieceProvider. Indeed, I would drop "Tetris" from the all class names.

It's not so jarring when you only have one class (in which, in my opinion, you've shoved too many responsibilities), but it's redundant anyway. We're in the "MillenniumTetris" namespace already - obviously we're not talking chess pieces here.

See http://blog.codinghorror.com/new-programming-jargon/ :)

Smurf Naming Convention

When almost every class has the same prefix. IE, when a user clicks on the button, a SmurfAccountView passes a SmurfAccountDTO to the SmurfAccountController. The SmurfID is used to fetch a SmurfOrderHistory which is passed to the SmurfHistoryMatch before forwarding to either SmurfHistoryReviewView or SmurfHistoryReportingView. If a SmurfErrorEvent occurs it is logged by SmurfErrorLogger to ${app}/smurf/log/smurf/smurflog.log

So it's Piece, Shape, PieceProvider(Dealer?) etc. that they're Tetris-related goes without saying at this point : )

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the feedback! 1) I will implement your suggestions on get/set, enums, and camelCase. 2) I use deep copy not to create an identical piece on the game board, but to copy the piece into a TestPiece whose position I change, and then test against the board state to see if it's a legal move. If it's a legal move, I replace CurrentPiece with TestPiece. If not, I throw it away. 3) I'm not sold on the multiton pattern yet. It seems disorganized to turn one neat class into 8 classes. Perhaps as I keep programming I will more clearly see the benefits of doing this. \$\endgroup\$ Apr 3, 2016 at 16:26
  • 1
    \$\begingroup\$ Sure thing, glad I could be of some help! As for 3, my point is that it isn't really neat, because it's a know-it-all type of class that performs more than one responsibility. The tendency of preferring 1 heavy-weight class to several light-weight ones is known as en.wikipedia.org/wiki/God_object anti-pattern, and yes the bigger the project, the more this approach starts to hurt \$\endgroup\$ Apr 3, 2016 at 17:42
  • 1
    \$\begingroup\$ 1) I'm going to refactor my code and give the multiton pattern a try. After thinking about it, it does seem a little more organized, since each piece gets an IntelliType name instead of just a string name. 2) What type of data structure do you recommend for storing the Tetris board? I am currently using a char array. Should I create a Square class and use a Square array? Should I use some kind of linked list? Should I use something else? \$\endgroup\$ Apr 12, 2016 at 18:22
  • \$\begingroup\$ @AdmiralAdama happy to hear that, it's always best to try things by yourself. As for the board - a 2D array seems like the most natural fit, it's fixed, it's a matrix accessible by coordinates. If you're ditching text-based names of pieces, I guess you could turn it into an array of enum values rather than char values. It's also possible to wrap it in a custom class, and with indexers you could still "pretend" it's an array (I mean access it with [x,y] syntax), and possibly implement some board-related logic on that very class, then? Certainly an option. Best of luck with the project! \$\endgroup\$ Apr 13, 2016 at 9:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.