C++ Windows Programming
上QQ阅读APP看书,第一时间看更新

The TetrisFigure class

In this application, there is the root figure class and one subclass for each type of falling figure. All figures can be moved sideways or rotated as a response to the user's requests. They are also moved downward by the timer.

There are seven figures, one for each color: red, brown, turquoise, green, yellow, blue, and purple. Each of them also has a unique shape. However, they all contain four squares. They can further be divided into three groups based on their ability to rotate. The red figure is the simplest one. It is a square and does not rotate at all. The brown, turquoise, and green figure can be rotated in vertical and horizontal directions, while the yellow, blue, and purple figures can be rotated in north, east, south, and west directions. For the red figure, it does not really matter since it does not rotate.

The row and col fields of the TetrisFigure class hold the center of the figure, which is marked by a cross in the illustrations of this section. The color field holds the color of the figure, and direction holds the current direction of the figure.

Finally, the direction array holds the relative positions of the three squares surrounding the marked square. There are four directions at most. Each direction holds three squares, which are the three remaining squares that are not the center of the figure. Each square holds two integers: the relative position of the center row and column.

The default constructor is needed to initialize the fallingFigure and nextFigure methods in the TetrisWindow class. The second constructor is protected since it is only called by its sub classes. Each figure has its own TetrisFigure subclass. Their constructors take a pointer to the color grid and define its color, start position, and figure patterns:

TetrisFigure.h

class TetrisFigure { 
  public: 
    TetrisFigure(); 
 
  protected: 
    TetrisFigure(Window* windowPtr, GameGrid* colorGridPtr, 
           Color color, int row, int col, Direction direction, 
           IntPair* northList, IntPair* eastList, 
           IntPair* southList, IntPair* westList); 
 
  public: 
    TetrisFigure& operator=(const TetrisFigure& figure); 

The TryMoveLeft, TryMoveRight, TryRotateClockwise, TryRotateClockwise, TryRotateAnticlockwise, and TryMoveDown methods all try to move the figure. They call the IsFigureValid method, which checks whether the new location is valid, that is, it is not located outside the game grid or at a location already occupied. The IsFigureValid method, in turn, calls the IsSquareValid method for each of its four squares:

    void TryMoveLeft(); 
    void TryMoveRight(); 
    void TryRotateClockwise(); 
    void TryRotateAnticlockwise(); 
    bool TryMoveDown(); 

There are two versions of the IsFigureValid method, where the first version is called by the TetrisWindow method and the other version is called by the preceding try methods in order to test whether a new location of the falling figure is valid:

    bool IsFigureValid();
    static bool IsFigureValid(int direction, int row, int col,
                  GameGrid* gameGridPtr, IntPair* figureInfo[]);
    static bool IsSquareValid(int row, int col,
                              GameGrid* gameGridPtr);

The AddToGrid method adds the four squares of the figure to the game grid:

    void AddToGrid();

The InvalidateFigure method invalidates the area occupied by the figure, and the DrawFigure method draws the figure:

    void InvalidateFigure(Size offsetSize = ZeroSize); 
    void DrawFigure(Graphics& graphics, 
                    Size offsetSize = ZeroSize) const; 

The gameGridPtr field is a pointer to the game grid, which we access when we try to move a figure in order to decide whether its new location is valid. The color field is the color of the figure (red, brown, turquoise, green, yellow, blue, or purple). The row, col, and direction fields hold the current location and direction of the figure.

The figureInfo field holds the shape of the figure. The figure can hold up to four directions: north, east, south, and west. Remember that row and col hold the location of the figures. More specifically, they hold the location of the center square of the four squares constituting the figure (marked by a cross in the following illustrations). The other three squares are defined by integer pairs holding their locations relative to the center square.

Technically, figureInfo is an array of four pointers (one each for the directions north, east, south, and west). Each pointer points at an array of three integer pairs, holding the locations of the three squares relative to the center square:

  protected: 
    Window* windowPtr; 
    GameGrid* gameGridPtr; 
    Color color; 
    int row, col; 
    Direction direction; 
    IntPair* figureInfo[4]; 
 }; 

The default constructor is necessary because fallingFigure and nextFigure are member objects of the TetrisWindow class. However, they do not need to be initialized since their values are assigned one of the seven figures in the figureList array:

TetrisFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h" 
#include "GameGrid.h" 
#include "TetrisFigure.h" 
#include "TetrisWindow.h" 
 
TetrisFigure::TetrisFigure() { 
  // Empty 
} 

The second constructor is called by the colored figure sub class constructor in order to initialize the figure. It takes a pointer to the main window and the game grid, the color of the figure, its start location and direction, and its location lists in the north, east, south, and west directions. Each of the lists holds three integer pairs representing the location of the squares relative to the center square:

TetrisFigure::TetrisFigure(Window*windowPtr, GameGrid*gameGridPtr, 
                           Color color, int row, int col, 
                           Direction direction, 
                           IntPair* northList, IntPair* eastList, 
                           IntPair* southList, IntPair* westList) 
 :windowPtr(windowPtr), 
  gameGridPtr(gameGridPtr), 
  color(color), 
  row(row), 
  col(col), 
  direction(direction) { 
  figureInfo[North] = northList; 
  figureInfo[East] = eastList; 
  figureInfo[South] = southList; 
  figureInfo[West] = westList; 
} 

The assignment operator is necessary because the fallingFigure and nextFigure methods in the TetrisWindow class are copied from the figure list:

TetrisFigure& TetrisFigure::operator=(const TetrisFigure& figure) { 
  if (this != &figure) { 
    windowPtr = figure.windowPtr; 
    gameGridPtr = figure.gameGridPtr; 
    color = figure.color; 
    row = figure.row; 
    col = figure.col; 
    direction = figure.direction; 
    figureInfo[North] = figure.figureInfo[North]; 
    figureInfo[East] = figure.figureInfo[East]; 
    figureInfo[South] = figure.figureInfo[South]; 
    figureInfo[West] = figure.figureInfo[West]; 
  } 
 
  return *this; 
} 

The TryMoveLeft, TryMoveRight, TryRotateClockwise, and TryRotateAnticlockwise methods are called when the user presses the arrow keys. They try to move the figure and invalidate its previous and current area if they succeed:

void TetrisFigure::TryMoveLeft() { 
  if (IsFigureValid(direction, row, col - 1
                    gameGridPtr, figureInfo)) { 
    windowPtr->Invalidate(Area()); 
    --col; 
    windowPtr->Invalidate(Area()); 
    windowPtr->UpdateWindow(); 
  } 
} 
 
void TetrisFigure::TryMoveRight() { 
  if (IsFigureValid(direction, row, col + 1
                     gameGridPtr, figureInfo)) { 
    windowPtr->Invalidate(Area()); 
    ++col; 
    windowPtr->Invalidate(Area()); 
    windowPtr->UpdateWindow(); 
  } 
} 
 

void TetrisFigure::TryRotateClockwise() { 
  Direction newDirection = (direction == West) ? North : 
                           ((Direction) (direction + 1)); 
 
  if (IsFigureValid(newDirection, row, col, 
                    gameGridPtr, figureInfo)) { 
    InvalidateFigure(); 
    direction = newDirection; 
    InvalidateFigure(); 
    windowPtr->UpdateWindow(); 
  } 
} 
 
void TetrisFigure::TryRotateAnticlockwise() { 
  Direction newDirection = (this->direction == North) ? West : 
                           ((Direction) (direction - 1)); 
 
  if (IsFigureValid(newDirection, row, col, 
                    gameGridPtr, figureInfo)) { 
    InvalidateFigure(); 
    direction = newDirection; 
    InvalidateFigure(); 
    windowPtr->UpdateWindow(); 
  } 
} 

The TryMoveDown method is called by the timer when the player presses the Space key. It is also called by the OnTimer method in the TetrisWindow class; it returns a Boolean value indicating whether the movement succeeded:

bool TetrisFigure::TryMoveDown() { 
  if (IsFigureValid(direction, row + 1, col
                     gameGridPtr, figureInfo)) { 
    windowPtr->Invalidate(Area()); 
    ++row; 
    windowPtr->Invalidate(Area()); 
    windowPtr->UpdateWindow(); 
    return true; 
  } 
 
  return false; 
} 

The first version of the IsFigureValid method is called by the TetrisWindow class and calls the second static version, with the current location and direction of the figure:

bool TetrisFigure::IsFigureValid() { 
  return IsFigureValid(direction, row, col
                     gameGridPtr, figureInfo); 
} 

The second version of the IsFigureValid method is called by the preceding try methods and checks if the figure is valid by calling the IsSquareValid method for each square in the figure. In order to do so, it needs to look up the relative positions of the included squares in the figureInfo method. The first value of the integer pairs is the row, and the second value is the column:

bool TetrisFigure::IsFigureValid(int direction, int row, int col, 
                                 GameGrid* gameGridPtr, 
                                 IntPair* figureInfo[]) { 
  int relRow0 = row + figureInfo[direction][0].first, 
      relCol0 = col + figureInfo[direction][0].second, 
      relRow1 = row + figureInfo[direction][1].first, 
      relCol1 = col + figureInfo[direction][1].second, 
      relRow2 = row + figureInfo[direction][2].first, 
      relCol2 = col + figureInfo[direction][2].second; 
 
  return IsSquareValid(row, col, gameGridPtr) && 
         IsSquareValid(relRow0, relCol0, gameGridPtr) && 
         IsSquareValid(relRow1, relCol1, gameGridPtr) && 
         IsSquareValid(relRow2, relCol2, gameGridPtr); 
} 

The IsSquareValid method returns true if the given square is located inside the game grid and not already occupied. A square on the game board is considered unoccupied if it is white:

bool TetrisFigure::IsSquareValid(int row, int col,
                                 GameGrid* gameGridPtr) { 
  return (row >= 0) && (row < Rows) && 
         (col >= 0) && (col < Cols) && 
         ((*gameGridPtr)[row][col] == White); 
} 

When the falling figure has reached its final position, it is added to the game grid. It is performed by setting the figure's color to the squares in the game grid at its current location. A falling figure has reached its final position when it cannot fall any longer without colliding with an earlier figure or has reached the game grid's lower bound:

void TetrisFigure::AddToGrid() { 
  (*gameGridPtr)[row][col] = color; 
 

  { int relRow = row + figureInfo[direction][0].first, 
        relCol = col + figureInfo[direction][0].second; 
    (*gameGridPtr)[relRow][relCol] = color; 
  } 
 

  { int relRow = row + figureInfo[direction][1].first, 
        relCol = col + figureInfo[direction][1].second; 
    (*gameGridPtr)[relRow][relCol] = color; 
  } 
 
  { int relRow = row + figureInfo[direction][2].first, 
        relCol = col + figureInfo[direction][2].second; 
    (*gameGridPtr)[relRow][relCol] = color; 
  } 
} 

When a figure has been moved, we need to redraw it. In order to avoid dazzle, we want to invalidate only its area, which is done by the InvalidateFigure method. We look up the rows and columns of the figure's four squares and call the InvalidateSquare method in the game grid for each of them:

void TetrisFigure::InvalidateFigure(Size offsetSize/*=ZeroSize*/){ 
  gameGridPtr->InvalidateSquare(windowPtr, row, col, offsetSize); 
 
  { int relRow = row + figureInfo[direction][0].first, 
        relCol = col + figureInfo[direction][0].second; 
    gameGridPtr->InvalidateSquare(windowPtr, relRow, 
                                  relCol, offsetSize); 
  }  
  { int relRow = row + figureInfo[direction][1].first, 
        relCol = col + figureInfo[direction][1].second; 
    gameGridPtr->InvalidateSquare(windowPtr, relRow, 
                                  relCol, offsetSize); 
  } 
  { int relRow = row + figureInfo[direction][2].first, 
        relCol = col + figureInfo[direction][2].second; 
    gameGridPtr->InvalidateSquare(windowPtr, relRow, 
                                  relCol, offsetSize); 
  } 
} 

When drawing the figure, we need to look up the locations of the squares of the figure before we draw them in a way similar to the InvalidateFigure method:

void TetrisFigure::DrawFigure(Graphics& graphics,Size offsetSize) 
                              const { 
  gameGridPtr->DrawSquare(graphics, row, col, 
                          Black, color, offsetSize); 
 
  { int relRow = row + figureInfo[direction][0].first, 
        relCol = col + figureInfo[direction][0].second; 
    gameGridPtr->DrawSquare(graphics, relRow, relCol, 
                            Black, color, offsetSize); 
  } 
 
  { int relRow = row + figureInfo[direction][1].first, 
        relCol = col + figureInfo[direction][1].second; 
    gameGridPtr->DrawSquare(graphics, relRow, relCol, 
                            Black, color, offsetSize); 
  } 
 
  { int relRow = row + figureInfo[direction][2].first, 
        relCol = col + figureInfo[direction][2].second; 
    gameGridPtr->DrawSquare(graphics, relRow, relCol, 
                            Black, color, offsetSize); 
  } 
} 

The red figure

The red figure is one large square, built up by four smaller regular squares. It the simplest figure of the game since it does not change shape when rotating. This implies that we just need to look at one figure, shown as follows:

This also implies that it is enough to define the squares for one direction and this to define the shape of the figure in all four directions:

RedFigure.h

class RedFigure : public TetrisFigure { 
  public: 
    static IntPair GenericList[]; 
    RedFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

RedFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h" 
#include "GameGrid.h" 
#include "TetrisFigure.h" 
#include "RedFigure.h" 

IntPair RedFigure::GenericList[] =
                 {IntPair(0,1), IntPair(1,0), IntPair(1,1)};

RedFigure::RedFigure(Window* windowPtr, GameGrid* gameGridPtr)
 :TetrisFigure(windowPtr, gameGridPtr, Red, 1, (Cols / 2) - 1,
                 North, GenericList, GenericList, GenericList, 
                 GenericList) {
 // Empty.
}

The first integer pair (rel row 0, rel col 1) of the generic list represents the square to the right of the marked square, the second integer pair (rel row 1, rel col 0) represents the square below the marked square, and the third integer pair (rel row 1, rel col 1) represents the square below and to the right of the marked square. Note that the rows increase downward and the columns increase to the right.

The brown figure

The brown figure can be oriented in a horizontal or vertical direction. It is initialized to vertical mode, as it can only be rotated into two directions. The north and south arrays are initialized with the vertical array and the east and west arrays are initialized with the horizontal array, as shown in the following image:

Since the row numbers increase downward and the column numbers increase to the right, the topmost square in the vertical direction (and the leftmost square in the horizontal direction) are represented by negative values:

BrownFigure.h

class BrownFigure : public TetrisFigure { 
  public: 
    static IntPair HorizontalList[], VerticalList[]; 
    BrownFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

BrownFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h" 
#include "GameGrid.h" 
#include "TetrisFigure.h" 
#include "BrownFigure.h"

IntPair BrownFigure::HorizontalList[] =
                     {IntPair(-1,0), IntPair(1,0), IntPair(2,0)},
        BrownFigure::VerticalList[] =
                     {IntPair(0,-1), IntPair(0,1), IntPair(0,2)};

BrownFigure::BrownFigure(Window* windowPtr, GameGrid* gameGridPtr)
  :TetrisFigure(windowPtr, gameGridPtr, Brown, 1, (Cols / 2) - 1,
                North, HorizontalList, VerticalList,
                HorizontalList, VerticalList) {
 // Empty.
}

The turquoise figure

Similar to the brown figure, the turquoise figure can be rotated in a vertical and horizontal direction, as shown in the following figure:

TurquoiseFigure.h

class TurquoiseFigure : public TetrisFigure { 
  public: 
    static IntPair HorizontalList[], VerticalList[]; 
    TurquoiseFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

TurquoiseFigure cpp

#include "..\\SmallWindows\\SmallWindows.h"
#include "GameGrid.h"
#include "TetrisFigure.h"
#include "TurquoiseFigure.h"

IntPair TurquoiseFigure::HorizontalList[] =
                 {IntPair(-1,0), IntPair(0,1), IntPair(1,1)},
        TurquoiseFigure::VerticalList[] =
                 {IntPair(1,-1), IntPair(1,0), IntPair(0,1)};

TurquoiseFigure::TurquoiseFigure(Window* windowPtr,
                                 GameGrid* gameGridPtr)
 :TetrisFigure(windowPtr, gameGridPtr, Turquoise, 1, (Cols/2) - 1,
               North, HorizontalList, VerticalList,
               HorizontalList, VerticalList) {
  // Empty.
}

The green figure

The green figure is mirrored in relation to the turquoise figure, shown as follows:

GreenFigure.h

class GreenFigure : public TetrisFigure { 
  public: 
    static IntPair HorizontalList[], VerticalList[]; 
    GreenFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

GreenFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h"
#include "GameGrid.h"
#include "TetrisFigure.h"
#include "GreenFigure.h"

IntPair GreenFigure::HorizontalList[] =
                  {IntPair(1,-1), IntPair(0,-1), IntPair(-1,0)}, GreenFigure::VerticalList[] = 
                  {IntPair(0,-1), IntPair(1,0), IntPair(1,1)}; 

GreenFigure::GreenFigure(Window* windowPtr, GameGrid* gameGridPtr)
 :TetrisFigure(windowPtr, gameGridPtr, Green, 1, Cols / 2, 
               North, HorizontalList, VerticalList,
               HorizontalList, VerticalList) {
 // Empty.
}

The yellow figure

The yellow figure can be rotated in a north, east, south, and west direction. It is initialized to the south, as shown in the following figure:

YellowFigure.h

class YellowFigure : public TetrisFigure { 
  public: 
    static IntPair NorthList[], EastList[], 
                   SouthList[], WestList[]; 
    YellowFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

YellowFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h"
#include "GameGrid.h"
#include "TetrisFigure.h"
#include "YellowFigure.h"
IntPair YellowFigure::NorthList[] =
              {IntPair(0,-1), IntPair(-1,0), IntPair(0,1)},
        YellowFigure::EastList[] =
              {IntPair(-1,0),IntPair(0,1),IntPair(1,0)}, 
        YellowFigure::SouthList[] = 
              {IntPair(0,-1),IntPair(1,0),IntPair(0,1)},
        YellowFigure::WestList[] = 
              {IntPair(-1,0),IntPair(0,-1),IntPair(1,0)};






YellowFigure::YellowFigure(Window* windowPtr,
                           GameGrid* gameGridPtr)
 :TetrisFigure(windowPtr, gameGridPtr, Yellow, 1, (Cols / 2) - 1,
               South, NorthList, EastList, SouthList, WestList) {
  // Empty. 
}

The blue figure

The blue figure can also be directed in all four directions. It is initialized to the south, as shown in the following figure:

BlueFigure.h

class BlueFigure : public TetrisFigure { 
  public: 
    static IntPair NorthList[], EastList[], 
           SouthList[], WestList[]; 
    BlueFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

BlueFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h"
#include "GameGrid.h"
#include "TetrisFigure.h"
#include "BlueFigure.h"




IntPair BlueFigure::NorthList[] =
                    {IntPair(0,-2),IntPair(0,-1),IntPair(-1,0)},
        BlueFigure::EastList[] =
                    {IntPair(-2,0), IntPair(-1,0), IntPair(0,1)},
        BlueFigure::SouthList[] =
                    {IntPair(1,0), IntPair(0,1), IntPair(0,2)},
        BlueFigure::WestList[] =
                    {IntPair(0,-1), IntPair(1,0), IntPair(2,0)};

BlueFigure::BlueFigure(Window* windowPtr, GameGrid* gameGridPtr)
 :TetrisFigure(windowPtr, gameGridPtr, Blue, 1, (Cols / 2) - 1,
              South, NorthList, EastList, SouthList, WestList) {
  // Empty. 
}

The purple figure

Finally, the purple figure is mirrored in relation to the blue figure and also initialized to the south, as shown in the following image:

PurpleFigure.h

class PurpleFigure : public TetrisFigure { 
  public: 
    static IntPair NorthList[], EastList[], 
                   SouthList[], WestList[]; 
    PurpleFigure(Window* windowPtr, GameGrid* gameGridPtr); 
};

PurpleFigure.cpp

#include "..\\SmallWindows\\SmallWindows.h"
#include "GameGrid.h"
#include "TetrisFigure.h"
#include "PurpleFigure.h"

IntPair PurpleFigure::NorthList[] =
              {IntPair(-1,0),IntPair(0,1),IntPair(0,2)},
        PurpleFigure::EastList[] =
              {IntPair(1,0), IntPair(2,0), IntPair(0,1)},
        PurpleFigure::SouthList[] =
              {IntPair(0,-2),IntPair(0,-1),IntPair(1,0)},
        PurpleFigure::WestList[] =
              {IntPair(0,-1),IntPair(-2,0),IntPair(-1,0)}; 



PurpleFigure::PurpleFigure(Window* windowPtr,
                           GameGrid* gameGridPtr)
 :TetrisFigure(windowPtr, gameGridPtr, Purple, 1, Cols / 2, South,
               NorthList, EastList, SouthList, WestList) {
  // Empty. 
}