8
\$\begingroup\$

Let me start by saying that until very recently procedural was the paradigm of choice for about 100% of my programming activity, and I was a complete stranger to C++ and OOP concepts. Since a few weeks ago, I have been studying C++ and today I decided to take some random procedural code and translate it to object oriented design as an exercise. The code in question was an implementation of the classical game Tetris for Windows console.

#include <iostream>
using namespace std;

#include <Windows.h>
#include <thread>
#include <vector>

#define XPADDING 34
#define YPADDING 5

// Screen buffer class
//==============================================================

class Screen
{
public:

    Screen(int, int);

    const int screenWidth;
    const int screenHeight;


    wchar_t *screen;

    HANDLE hConsole;
    DWORD dwBytesWritten;   
};

Screen::Screen(int screenWidth, int screenHeight)
    : screenWidth(screenWidth), screenHeight(screenHeight)
{
    screen = new wchar_t[screenWidth * screenHeight];
    for (int i = 0; i < screenWidth * screenHeight; i++) screen[i] = L' ';
    hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
    SetConsoleActiveScreenBuffer(hConsole);
    dwBytesWritten = 0;
}

// Tetromino Class
//==============================================================

class Tetromino
{
public:
    Tetromino(wstring, int, int, int);

    int y;
    int x;
    int rotation;

    wstring layout;

    int rotate(int, int);
};

Tetromino::Tetromino(wstring layout, int startingX, int startingY, int startingRotation)
    : layout(layout), y(startingY), x(startingX), rotation(startingRotation)
{}

int Tetromino::rotate(int x, int y)
{
    /*
    * Rotates piece layout
    * string based on given angle 
    * 'rotation'
    */
    switch (rotation % 4) {
        case 0: return y * 4 + x;          // 0 degress
        case 1: return 12 + y - (x * 4);   // 90 degress
        case 2: return 15 - (y * 4) - x;   // 180 degress
        case 3: return 3 - y + (x * 4);    // 270 degress
    }

    return 0;
}

// Playing Field Class
//==============================================================

class PlayingField
{
public:
    PlayingField(int, int);

    const int fieldWidth;
    const int fieldHeight;

    unsigned char *pField;

    bool doesPieceFit(Tetromino*, int, int, int);
};

PlayingField::PlayingField(int fieldWidth, int fieldHeight)
    : fieldWidth(fieldWidth), fieldHeight(fieldHeight), pField(nullptr)
{
    // Creating play field buffer
    pField = new unsigned char[fieldHeight * fieldWidth];
    for (int x = 0; x < fieldWidth; x++)
        for (int y = 0; y < fieldHeight; y++)
            // 0 characters are spaces and 9 are borders
            pField[y * fieldWidth + x] = (x == 0 || x == fieldWidth - 1 || y == fieldHeight - 1) ? 9 : 0;
}

bool PlayingField::doesPieceFit(Tetromino *tetromino, int rotation, int x, int y)
{
    for (int px = 0; px < 4; px++)
        for (int py = 0; py < 4; py++) {
            int pi = tetromino->rotate(px, py);
            int fi = (y + py) * fieldWidth + (x + px);
            if (x + px >= 0 && x + px < fieldWidth)
                if (y + py >= 0 && y + py < fieldHeight)
                    // if cell value != 0, it's occupied
                    if (tetromino->layout[pi] == L'X' && pField[fi] != 0)
                        return false;
        }
    return true;
}

// Game class
//==============================================================

class Tetris
{
public:
    Tetris(Screen*, PlayingField*, int);

    bool gameOver;

    int score;

    void draw();
    void checkLines();
    void computeNextState();
    void lockPieceOnField();
    void processInput();
    void synchronizeMovement();

private:
    int lines;
    int speed;
    int nextPiece;
    int pieceCount;
    int currentPiece;
    int speedCounter;

    bool key[4];
    bool forceDown;
    bool rotateHold;

    Screen *screenBuffer;
    Tetromino *tetromino[7];
    PlayingField *playingField;

    vector<int> fullLines;

};

Tetris::Tetris(Screen *screenBuffer, PlayingField *playingField, int speed) 
    : speed(speed), screenBuffer(screenBuffer), playingField(playingField)
{
    // Set game initial state
    score = 0;
    lines = 0;
    pieceCount = 0;
    speedCounter = 0;
    gameOver = false;
    forceDown = false;
    nextPiece = rand() % 7;
    currentPiece = rand() % 7;

    // Generate pieces
    int startingPieceX = playingField->fieldWidth / 2;
    tetromino[0] = new Tetromino(L"..X...X...X...X.", startingPieceX, 0, 0);
    tetromino[1] = new Tetromino(L"..X..XX...X.....", startingPieceX, 0, 0);
    tetromino[2] = new Tetromino(L".....XX..XX.....", startingPieceX, 0, 0);
    tetromino[3] = new Tetromino(L"..X..XX..X......", startingPieceX, 0, 0);
    tetromino[4] = new Tetromino(L".X...XX...X.....", startingPieceX, 0, 0);
    tetromino[5] = new Tetromino(L".X...X...XX.....", startingPieceX, 0, 0);
    tetromino[6] = new Tetromino(L"..X...X..XX.....", startingPieceX, 0, 0);

    rotateHold = true;
}

void Tetris::synchronizeMovement()
{
    // Timing game ticks
    this_thread::sleep_for(50ms);
    speedCounter++;
    forceDown = (speed == speedCounter);
}

void Tetris::processInput()
{
    // x27 = right arrow key
    // x25 = left arrow key
    // x28 = down arrow key
    for (int k = 0; k < 4; k++)
        key[k] = (0x8000 & GetAsyncKeyState((unsigned char) ("\x27\x25\x28Z"[k]))) != 0;

    // Handling input
    Tetromino *currentTetromino = tetromino[currentPiece];
    currentTetromino->x += (key[0] && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x + 1, currentTetromino->y)) ? 1 : 0;
    currentTetromino->x -= (key[1] && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x - 1, currentTetromino->y)) ? 1 : 0;
    currentTetromino->y += (key[2] && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x, currentTetromino->y + 1)) ? 1 : 0;

    if (key[3]) {
        currentTetromino->rotation += (rotateHold && playingField->doesPieceFit(currentTetromino, currentTetromino->rotation + 1, currentTetromino->x, currentTetromino->y)) ? 1 : 0;
        rotateHold = false;
    } else {
        rotateHold = true;
    }
}

void Tetris::computeNextState()
{
    if (forceDown) {
        Tetromino *currentTetromino = tetromino[currentPiece];
        if (playingField->doesPieceFit(currentTetromino, currentTetromino->rotation, currentTetromino->x, currentTetromino->y + 1)) {
            currentTetromino->y++;
        } else {
            lockPieceOnField();

            // Set up new piece
            currentPiece = nextPiece;
            nextPiece = rand() % 7;
            tetromino[currentPiece]->rotation = 0;
            tetromino[currentPiece]->y = 0;
            tetromino[currentPiece]->x = playingField->fieldWidth / 2;

            // Increse game speed every 10 tics
            pieceCount++;
            if (pieceCount % 10 == 0)
                if (speed >= 10) speed--;

            checkLines();

            score += 25;
            if (!fullLines.empty()) score += (1 << fullLines.size()) * 100;

            // Game over if it doesn't fit
            gameOver = !playingField->doesPieceFit(tetromino[currentPiece], tetromino[currentPiece]->rotation, tetromino[currentPiece]->x, tetromino[currentPiece]->y);

        }
        speedCounter = 0;
    }
}

void Tetris::lockPieceOnField()
{
    Tetromino *currentTetromino = tetromino[currentPiece];
    for (int px = 0; px < 4; px++)
        for (int py = 0; py < 4; py++)
            if (currentTetromino->layout[currentTetromino->rotate(px, py)] == L'X')
                // nCurrentPiece + 1 because 0 means empty spots in the playing field
                playingField->pField[(currentTetromino->y + py) * playingField->fieldWidth + (currentTetromino->x + px)] = currentPiece + 1;
}

void Tetris::checkLines()
{
    Tetromino *currentTetromino = tetromino[currentPiece];
    for (int py = 0; py < 4; py++) {
        if (currentTetromino->y + py < playingField->fieldHeight - 1) {
            bool bLine = true;
            for (int px = 1; px < playingField->fieldWidth; px++)
                // if any cell is empty, line isn't complete
                bLine &= (playingField->pField[(currentTetromino->y + py) * playingField->fieldWidth + px]) != 0;
            if (bLine) {
                // draw '=' symbols
                for (int px = 1; px < playingField->fieldWidth - 1; px++)
                    playingField->pField[(currentTetromino->y + py) * playingField->fieldWidth + px] = 8;
                fullLines.push_back(currentTetromino->y + py);
                lines++;
            }
        }
    }
}

void Tetris::draw()
{
    // Draw playing field
    for (int x = 0; x < playingField->fieldWidth; x++)
        for (int y = 0; y < playingField->fieldHeight; y++)
            //mapping playing field (' ', 1,..., 9) to Screen characters (' ', A,...,#)
            screenBuffer->screen[(y + YPADDING) * screenBuffer->screenWidth + (x + XPADDING)] = L" ABCDEFG=#"[playingField->pField[y * playingField->fieldWidth + x]];

    // Draw pieces
    for (int px = 0; px < 4; px++)
        for (int py = 0; py < 4; py++) {
            if (tetromino[currentPiece]->layout[tetromino[currentPiece]->rotate(px, py)] == L'X')
                // Drawing current piece ( n + ASCII code of character 'A') 0 -> A, 1 - > B, ...
                screenBuffer->screen[(tetromino[currentPiece]->y + py + YPADDING) * screenBuffer->screenWidth + (tetromino[currentPiece]->x + px + XPADDING)] = currentPiece + 65;
            if (tetromino[nextPiece]->layout[tetromino[nextPiece]->rotate(px, py)] == L'X')
                // Drawing next piece ( n + ASCII code of character 'A') 0 -> A, 1 - > B, ...
                screenBuffer->screen[(YPADDING + 3 + py) * screenBuffer->screenWidth + (XPADDING / 2 + px + 3)] = nextPiece + 65;
            else
                screenBuffer->screen[(YPADDING + 3 + py) * screenBuffer->screenWidth + (XPADDING / 2 + px + 3)] = ' ';

        }

    swprintf_s(&screenBuffer->screen[YPADDING * screenBuffer->screenWidth + XPADDING / 4], 16, L"SCORE: %8d", score);
    swprintf_s(&screenBuffer->screen[(YPADDING + 1) * screenBuffer->screenWidth + XPADDING / 4], 16, L"LINES: %8d", lines);
    swprintf_s(&screenBuffer->screen[(YPADDING + 4) * screenBuffer->screenWidth + XPADDING / 4], 13, L"NEXT PIECE: ");

    if (!fullLines.empty()) {
        WriteConsoleOutputCharacter(screenBuffer->hConsole, screenBuffer->screen, screenBuffer->screenWidth * screenBuffer->screenHeight, {0,0}, &screenBuffer->dwBytesWritten);
        this_thread::sleep_for(400ms);
        for (auto &v : fullLines)
            for (int px = 1; px < playingField->fieldWidth - 1; px++) {
                for (int py = v; py > 0; py--)
                    // clear line, moving lines above one unit down
                    playingField->pField[py * playingField->fieldWidth + px] = playingField->pField[(py - 1) * playingField->fieldWidth + px];
                playingField->pField[px] = 0;
            }
        fullLines.clear();
    }

    // Display Frame
    WriteConsoleOutputCharacter(screenBuffer->hConsole, screenBuffer->screen, screenBuffer->screenWidth * screenBuffer->screenHeight, {0,0}, &screenBuffer->dwBytesWritten);
}

int main(void){

    Screen *screenBuffer = new Screen(80, 30);
    PlayingField *playingField = new PlayingField(12, 18);
    Tetris *tetrisGame = new Tetris(screenBuffer, playingField, 20);

    // Main game loop
    while (!tetrisGame->gameOver) {
        // Timing
        tetrisGame->synchronizeMovement();
        // Input
        tetrisGame->processInput();
        // Logic
        tetrisGame->computeNextState();
        //Render Output
        tetrisGame->draw();
    }

    CloseHandle(screenBuffer->hConsole);
    cout << "Game Over! Score:" << tetrisGame->score << endl;
    system("pause");                                       

    return 0;
}

Some doubts I had while coding:

  • Overall code logistics. What would be the best (advised) way of interrelating my class objects? Should I pass references around as member variables (the way I did with my Tetris class, it has pointers to screenBuffer and playingField objects) and make most of the game functionality internal to my objects or make them as independent of one another as possible, bringing all together in my program's main function by accessing each object when needed (essentially pulling some of the programs functionality out of my objects)?

  • I'm using the this keyword a lot. It sure clutters the code a little bit. I'll go ahead and not use it at all. I wonder if this is ok.

  • Most of these classes don't have anything private. Should I use structures instead?

  • I should probably split this code into multiple files, one for each class definition.

\$\endgroup\$
8
  • \$\begingroup\$ Hm, that's unexpected. It builds perfectly for me on my Windows 10/Visual Studio environment, not a single warning. I have pushed my whole solution to github.com/northernSage/ConsoleTetris so you can just clone it to your machine and try building it again with the same project properties I'm using, try building it on release mode though (I'm assuming you are on windows platform since this project has platform dependent code). Thanks for taking the time to look into it... \$\endgroup\$
    – sage
    Commented May 27, 2019 at 18:07
  • \$\begingroup\$ Okay I spent a few seconds figuring it out. I blame _UNICODE. Of course this makes your application implementation defined. \$\endgroup\$
    – Summer
    Commented May 27, 2019 at 18:21
  • 2
    \$\begingroup\$ That already makes one thing I hadn't thought about that came up on this thread, I'm glad you mentioned it. It seems the trouble maker is my wchar_t *screen console buffer pointer, I wonder what is the advised way of getting around this problem... \$\endgroup\$
    – sage
    Commented May 28, 2019 at 17:59
  • 2
    \$\begingroup\$ There is one huge overarching problem in the code, and that is your excessive use of new. You should not need any new in the code. Simple values like Screen screenBuffer(80, 30); and std::vector for some other places would improve this code a lot in terms of fixing the memory leaks, making the syntax nicer and the program faster. \$\endgroup\$
    – nwp
    Commented May 29, 2019 at 13:18
  • \$\begingroup\$ @Chipster Your points would make a great answer so please consider answering instead of commenting. \$\endgroup\$
    – yuri
    Commented May 30, 2019 at 11:52

1 Answer 1

7
\$\begingroup\$

Overall code logistics. What would be the best (advised) way of interrelating my class objects? Should I pass references around as member variables (the way I did with my Tetris class, it has pointers to screenBuffer and playingField objects) and make most of the game functionality internal to my objects or make them as independent of one another as possible, bringing all together in my program's main function by accessing each object when needed (essentially pulling some of the programs functionality out of my objects)?

I don't feel like an authoritative source enough to answer this one specifically, but if you want my opinion, I'm going to say make them separate. That way, because the objects don't overlap, you have more control at the upper level to do with them what you want. If you want to change how these objects interact, you can change the upper level without messing with the internal representation. If you need a new way for them to interact, you can just make a new method, and then you can keep the old way too, if you want, much easier.

I'm using the this keyword a lot. It sure clutters the code a little bit. I'll go ahead and not use it at all. I wonder if this is ok.

Yeah, that should be okay. The this keyword is generally to avoid confusion for data member names. So if you had, for example:

class foo {
    private:
        int x;
    public:
    void bar() {
        int x = 0;
        x = 5; // the function version of x
        this->x = 5; // the data member "x"
    }
};

If you don't have any data members the same name as other variables in your function, you should be good. this is unnecessary in that case.

Most of these classes don't have anything private. Should I use structures instead?

Private data members don't usually have anything to do with structures, so I'm not sure I understand the question correctly. However, I will say that structures are not a replacement for private data members. I think you misunderstand how the OOP model works.

Generally, private data members are reserved for data that no one else needs to see or access. Only the class will access these members via its own methods. If for some reason you need to access or manipulate these members, you need to make a public interface for them, that is, make special methods tasked with adjusting those private data members.

If I'm understanding you right by "Should I use structures instead?", meaning should you copy your data members to a special structure to pass around, the answer is no. Just pass the object itself around, and let other functions use the public interface you defined for it instead.

I should probably split this code into multiple files, one for each class definition.

This isn't necessary, per say, but yes, it would probably be good to do eventually.

\$\endgroup\$
6
  • \$\begingroup\$ What I mean by the third question is: Since the difference between a class and a struct in C++ are that structs have default public members and bases and classes have default private members and bases, if I don't have anything private in my class, wouldn't it be more natural to use a struct which is public by default? But now that I read it again, I used quite a shady choice of words for that 3rd question, sorry about that. The aspects you pointed make a lot of sense, specially the idea of not overlapping, it would also make my code easier to understand now that I think about it. \$\endgroup\$
    – sage
    Commented May 31, 2019 at 3:27
  • 2
    \$\begingroup\$ Oh. I got you now. I don't know if I can answer that. I prefer classes still. in case I want to change it later, but I would imagine that's just a style choice. \$\endgroup\$
    – Chipster
    Commented Jun 1, 2019 at 20:15
  • \$\begingroup\$ Final product Console Tetris. After pulling all code responsible for interrelating objects out of my classes, I was left with four simple classes with no overlap and seven or so helper methods responsible for bringing the objects together into the main execution flow of the program. I bundled all these helper methods into a single utility class (this class has internal references for each relevant object), so to make them available under the same namespace, it seemed to be the most organised thing to do (feel free to correct me here). \$\endgroup\$
    – sage
    Commented Jun 2, 2019 at 21:39
  • \$\begingroup\$ First of all, really cool. You did some really good cleaning up and organization. Second of all, if you're looking for more feed back after taking suggestions, you can ask another question with the revised code if you like. It will make things easier so no one ends up reviewing two different revisions at once. There will be less confusion that way. \$\endgroup\$
    – Chipster
    Commented Jun 2, 2019 at 22:29
  • 3
    \$\begingroup\$ @NorthernSage stackoverflow.com/a/54596/5416291 \$\endgroup\$
    – Summer
    Commented Jun 3, 2019 at 14:24

Your Answer

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

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