3
\$\begingroup\$

The original source code by falgon.

For kbhit() and getch() in Linux/UNIX is written by Chris Giese.

Previous question:

Text-based Tetris game - follow-up

Summary of improvements:

  • More "magic numbers" eliminated
  • Deleted unnecessary classes
  • Implemented the elimination of a complete row if formed

How can I improve this code further?

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>

#ifdef __linux__ 
/*****************************************************************************
kbhit() and getch() for Linux/UNIX
Chris Giese <geezer@execpc.com> http://my.execpc.com/~geezer
Release date: ?
This code is public domain (no copyright).
You can do whatever you want with it.
****************************************************************************/
#include <sys/time.h> /* struct timeval, select() */
/* ICANON, ECHO, TCSANOW, struct termios */
#include <termios.h> /* tcgetattr(), tcsetattr() */
#include <stdlib.h> /* atexit(), exit() */
#include <unistd.h> /* read() */
#include <stdio.h> /* printf() */

static struct termios g_old_kbd_mode;
/*****************************************************************************
*****************************************************************************/
static void cooked(void)
{
    tcsetattr(0, TCSANOW, &g_old_kbd_mode);
}
/*****************************************************************************
*****************************************************************************/
static void raw(void)
{
    static char init;
    /**/
    struct termios new_kbd_mode;

    if (init)
        return;
    /* put keyboard (stdin, actually) in raw, unbuffered mode */
    tcgetattr(0, &g_old_kbd_mode);
    memcpy(&new_kbd_mode, &g_old_kbd_mode, sizeof(struct termios));
    new_kbd_mode.c_lflag &= ~(ICANON | ECHO);
    new_kbd_mode.c_cc[VTIME] = 0;
    new_kbd_mode.c_cc[VMIN] = 1;
    tcsetattr(0, TCSANOW, &new_kbd_mode);
    /* when we exit, go back to normal, "cooked" mode */
    atexit(cooked);

    init = 1;
}
/*****************************************************************************
*****************************************************************************/
static int _kbhit(void)
{
    struct timeval timeout;
    fd_set read_handles;
    int status;

    raw();
    /* check stdin (fd 0) for activity */
    FD_ZERO(&read_handles);
    FD_SET(0, &read_handles);
    timeout.tv_sec = timeout.tv_usec = 0;
    status = select(0 + 1, &read_handles, NULL, NULL, &timeout);
    if (status < 0)
    {
        printf("select() failed in kbhit()\n");
        exit(1);
    }
    return status;
}
/*****************************************************************************
*****************************************************************************/
static int _getch(void)
{
    unsigned char temp;

    raw();
    /* stdin = fd 0 */
    if (read(0, &temp, 1) != 1)
        return 0;
    return temp;
}
struct COORD { short X; short Y; };
bool gotoxy(unsigned short x = 1, unsigned short y = 1) {
    if ((x == 0) || (y == 0))
        return false;
    std::cout << "\x1B[" << y << ";" << x << "H";
}

void clearScreen(bool moveToStart = true) {
    std::cout << "\x1B[2J";
    if (moveToStart)
        gotoxy(1, 1);
}
inline
void print(const std::string& str, COORD& coord)
{
    gotoxy(coord.X, coord.Y);
    std::cout << str << std::flush;
}
inline
void print(const char* str, COORD& coord){
    gotoxy(coord.X, coord.Y);
    std::cout << str << std::flush;
}
inline
void print(char c, COORD& coord){
    gotoxy(coord.X, coord.Y);
    std::cout << c << std::flush;
}

#elif _WIN32

#include <conio.h> /* kbhit(), getch() */
#include <Windows.h>
#include <tchar.h>
static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
static CONSOLE_SCREEN_BUFFER_INFO csbi;
void clearScreen()
{
    DWORD                      count;
    DWORD                      cellCount;
    COORD                      homeCoords = { 0, 0 };

    if (!GetConsoleScreenBufferInfo(hStdOut, &csbi))
        std::cerr << "ERROR GetConsoleScreenBufferInfo - clearScreen : "
                  << GetLastError() << std::endl;

    cellCount = csbi.dwSize.X *csbi.dwSize.Y;

    FillConsoleOutputCharacter( hStdOut, (TCHAR) ' ',cellCount, homeCoords, &count);

    FillConsoleOutputAttribute( hStdOut, csbi.wAttributes, cellCount, homeCoords, &count);

    SetConsoleCursorPosition(hStdOut, homeCoords);
}
void gotoxy(int x, int y)
{
    COORD coord;
    coord.X = x;
    coord.Y = y;
    SetConsoleCursorPosition(hStdOut, coord);
}

static DWORD cCharsWritten;

inline
void print(const std::string& str, COORD& coord)
{
    WriteConsoleOutputCharacter(hStdOut, str.c_str(), str.length(), coord, &cCharsWritten);
}
inline
void print(const TCHAR* str, COORD& coord)
{
    WriteConsoleOutputCharacter(hStdOut, str, _tcslen(str), coord, &cCharsWritten);
}
inline
void print(TCHAR c, COORD& coord, bool dummy = true)
{
    FillConsoleOutputCharacter(hStdOut, c, 1, coord, &cCharsWritten);
}
#else
#error "OS not supported!"
#endif

using Matrix = std::vector<std::vector<int>>;

constexpr std::initializer_list<size_t> il = 
{
    0, 1, 2, 3 
};

constexpr std::initializer_list<size_t> ilBoard = 
{
    0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
};

constexpr std::initializer_list<size_t> ilBoardRow =
{
    0,1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
};

static std::vector<Matrix> blockList =
{
    {
        { 0, 1, 0, 0 },
        { 0, 1, 0, 0 },
        { 0, 1, 0, 0 },
        { 0, 1, 0, 0 }
    },
    {
        { 0, 0, 0, 0 },
        { 0, 1, 1, 0 },
        { 0, 1, 0, 0 },
        { 0, 1, 0, 0 }
    },
    {
        { 0, 0, 1, 0 },
        { 0, 1, 1, 0 },
        { 0, 1, 0, 0 },
        { 0, 0, 0, 0 }
    },
    {
        { 0, 1, 0, 0 },
        { 0, 1, 1, 0 },
        { 0, 0, 1, 0 },
        { 0, 0, 0, 0 }
    },
    {
        { 0, 0, 0, 0 },
        { 0, 1, 0, 0 },
        { 1, 1, 1, 0 },
        { 0, 0, 0, 0 }
    },
    {
        { 0, 0, 0, 0 },
        { 0, 1, 1, 0 },
        { 0, 1, 1, 0 },
        { 0, 0, 0, 0 }
    },
    {
        { 0, 0, 0, 0 },
        { 0, 1, 1, 0 },
        { 0, 0, 1, 0 },
        { 0, 0, 1, 0 }
    }
};

class NonCopyable
{
public:
    NonCopyable() = default;
private:
    NonCopyable(const NonCopyable &) = delete;
    NonCopyable(const NonCopyable &&) = delete;
    NonCopyable& operator = (const NonCopyable&) = delete;
};


class Random : private NonCopyable
{
public:
    Random(int min, int max)
        : mUniformDistribution(min, max)
    {}

    int operator()() 
    {
        return mUniformDistribution(mEngine);
    }
private:
    std::default_random_engine mEngine{ std::random_device()() };
    std::uniform_int_distribution<int> mUniformDistribution;
};

class Tetris : private NonCopyable
{
public:
    Tetris() 
    {
        mBlock.resize(il.size(), std::vector<int>(il.size(), 0));
        mBoard.resize(ilBoard.size(), std::vector<int>(ilBoardRow.size(), 0));
        mStage.resize(ilBoard.size(), std::vector<int>(ilBoardRow.size(), 0));
    };

private:
    bool makeBlocks();
    void moveBlock(int, int);
    bool isCollide(int, int);
    bool rotateBlock();
    void isFull();
    void checkLine();

    virtual void display(){};
    virtual void GameOverScreen() {};

    Random getRandom{ 0, (blockList.size() - 1) };
    Matrix mStage;
    Matrix mBlock;

    int y = 0;
    int x = il.size();

protected:
    void initField();
    void spawnBlock();
    void userInput();

    bool gameOver = false;
    Matrix mBoard;
};

void Tetris::initField()
{
    for (auto i = ilBoard.begin(); i != ilBoard.end() - 1; ++i)
    {
        for (auto j = ilBoardRow.begin(); j != ilBoardRow.end() - 1; ++j)
        {
            if ((*j == 0) || (*j ==  ilBoardRow.size() - 2) || (*i == ilBoard.size() - 2))
            {
                mBoard[*i][*j] = mStage[*i][*j] = 9;
            }
            else
            {
                mBoard[*i][*j] = mStage[*i][*j] = 0;
            }
        }
    }

    makeBlocks();

    display();
}

bool Tetris::makeBlocks()
{
    x = il.size();
    y = 0;

    int blockType = getRandom();

    for (auto i : il)
    {
        for (auto j : il)
        {
            mBlock[i][j] = blockList[blockType][i][j];
        }
    }

    for (auto i : il)
    {
        for (auto j : il)
        {
            mBoard[i][j + il.size()] = mStage[i][j + il.size()] + mBlock[i][j];
        }
    }
    return false;
}


void Tetris::isFull()
{
    for (auto i : il)
    {
        for (auto j : il)
        {       
            if(mBoard[i][j + il.size()] > 1)
            {
                gameOver = true;
            }
        }
    }
}

void Tetris::moveBlock(int x2, int y2)
{

    //Remove block
    for (auto i : il)
    {
        for (auto j : il)
        {
            mBoard[y + i][x + j] -= mBlock[i][j];
        }
    }

    //Update coordinates
    x = x2;
    y = y2;

    // assign a block with the updated value
    for (auto i : il)
    {
        for (auto j : il)
        {
            mBoard[y + i][x + j] += mBlock[i][j];
        }
    }

    display();
}

void Tetris::checkLine()
{
    std::copy(mBoard.begin(), mBoard.end(), mStage.begin());

    for (auto i = ilBoard.begin() + 1; i != ilBoard.end() - 2; ++i)
    {
        bool isCompeteLine = true;

        for (auto j = ilBoardRow.begin() + 1; j != ilBoardRow.end() - 1; ++j)
        {
            if (mStage[*i][*j] == 0)
            {
                isCompeteLine = false;
            }
        }

        if (isCompeteLine)
        {
            for (auto j : il)
                std::copy(mStage[*i - 1 - j].begin(), mStage[*i - 1 - j].end(), mStage[*i - j].begin());
        }
    }

    std::copy(mStage.begin(), mStage.end(), mBoard.begin());
}

bool Tetris::isCollide(int x2, int y2)
{
    for (auto i : il)
    {
        for (auto j : il)
        {
            if (mBlock[i][j] && mStage[y2 + i][ x2 + j] != 0)
            {
                return true;
            }
        }
    }
    return false;
}

void Tetris::userInput()
{
    char key;

    key = _getch();

    switch (key)
    {
        case 'd':
            if (!isCollide(x + 1, y))
            {
                moveBlock(x + 1, y);
            }
            break;
        case 'a':
            if (!isCollide(x - 1, y))
            {
                moveBlock(x - 1, y);
            }
            break;
        case 's':
            if (!isCollide(x, y + 1))
            {
                moveBlock(x, y + 1);
            }
            break;
        case ' ':
            rotateBlock();
    }
}

bool Tetris::rotateBlock()
{
    Matrix temp(il.size(), std::vector<int>(il.size(), 0));

    std::copy(mBlock.begin(), mBlock.end(), temp.begin());

    // Rotate 
    for (auto i : il)
    {
        for (auto j : il)
        {
            if (i < j)
            {
                std::swap(mBlock[i][j], mBlock[j][i]);
            }
        }

        std::reverse(mBlock[i].begin(), mBlock[i].end());
    }

    if (isCollide(x, y))
    { // And stop if it overlaps not be rotated
        std::copy(temp.begin(), temp.end(), mBlock.begin());
        return true;
    }

    for (auto i : il)
    {
        for (auto j : il)
        {
            mBoard[y + i][x + j] -= temp[i][j];
            mBoard[y + i][x + j] += mBlock[i][j];
        }
    }

    display();

    return false;
}

void Tetris::spawnBlock()
{
    if (!isCollide(x, y + 1))
    {
        moveBlock(x, y + 1);
    }
    else
    {
        checkLine();
        makeBlocks();
        isFull();
        display();
    }
}

class Game : public Tetris
{
public:
    Game() = default;
    int menu();
    void gameLoop();
private:
    void introScreen();

    virtual void display();
    virtual void gameOverScreen();

    size_t GAMESPEED = 20000;
};

void Game::gameOverScreen()
{
    COORD coord = { 0, 0 };
    coord.Y += 2;
    print(" #####     #    #     # ####### ####### #     # ####### ######", coord); coord.Y++;
    print("#     #   # #   ##   ## #       #     # #     # #       #     #", coord);coord.Y++;
    print("#        #   #  # # # # #       #     # #     # #       #     #", coord);coord.Y++;
    print("#  #### #     # #  #  # #####   #     # #     # #####   ######", coord); coord.Y++;
    print("#     # ####### #     # #       #     #  #   #  #       #   #", coord);  coord.Y++;
    print("#     # #     # #     # #       #     #   # #   #       #    #", coord); coord.Y++;
    print(" #####  #     # #     # ####### #######    #    ####### #     #", coord);coord.Y += 2;

    print("Press any key and enter", coord);

    gotoxy((coord.X = strlen("Press any key and enter") + 1), coord.Y++);

    char a;
    std::cin >> a;
}

void Game::gameLoop()
{
    size_t time = 0;

    initField();

    while (!gameOver)
    {
        if (_kbhit())
        {
            userInput();
        }

        if (time < GAMESPEED)
        {
            time++;
        }
        else
        {
            spawnBlock();
            time = 0;
        }
    }

}

int Game::menu()
{
    introScreen();

    int select_num = 0;

    std::cin >> select_num;

    switch (select_num)
    {
        case 1:
        case 2:
        case 3:
            break;
        default:
            select_num = 0;
            break;
    }

    return select_num;
}

void Game::introScreen()
{
    clearScreen();
    COORD coord = { 0, 0 };
    print("#==============================================================================#", coord);   coord.Y += 2;
    print("####### ####### ####### ######    ###    #####", coord);                                     coord.Y++;
    print("   #    #          #    #     #    #    #     #", coord);                                    coord.Y++;
    print("   #    #          #    #     #    #    #", coord);                                          coord.Y++;
    print("   #    #####      #    ######     #     #####", coord);                                     coord.Y++;
    print("   #    #          #    #   #      #          #", coord);                                    coord.Y++;
    print("   #    #          #    #    #     #    #     #", coord);                                    coord.Y++;
    print("   #    #######    #    #     #   ###    #####          made for fun ", coord);              coord.Y += 5;

    print("     <Menu>", coord);                                                                        coord.Y++;
    print("     1: Start Game", coord);                                                                 coord.Y++;
    print("     2: Quit", coord);                                                                       coord.Y += 2;
    print("#==============================================================================#", coord);   coord.Y++;
    print("Choose >> ", coord);

    coord.X = strlen("Choose >> ");
    gotoxy(coord.X, coord.Y);
}

void Game::display()
{
    clearScreen();

    for (auto i : ilBoard)
    {
        for (auto j : ilBoardRow)
        {
            switch (mBoard[i][j])
            {
                case 0:
                    std::cout << ' ';
                    break;
                case 9:
                    std::cout << '@';
                    break;
                default:
                    std::cout << '#';
                    break;
            }
        }
        std::cout << std::endl;
    }

    COORD coord = { 0, ilBoard.size() };

    print("     A: left     S: down     D: right            Rotation[Space]", coord);

    if (gameOver)
    {
        clearScreen();
        gameOverScreen();
    }
}

int main()
{
    Game game;
    switch (game.menu())
    {
        case 1:
            game.gameLoop();
            break;
        case 2:
            return 0;
        default:
            COORD coord = { 0, 0 };
            print("Choose 1~2", coord);
            return -1;
    }
    return 0;
}
\$\endgroup\$
4
\$\begingroup\$

Well, with a few minor changes, the game almost worked on my Mac Laptop, but for some reason the text was all messed up, like if the line breaks where missing. You might want to investigate that, I'll be glad to test again in the future.

About the platform specific code:

The platform-specific code under the __linux__ block is compatible with any other Unix system it seems. So you can enable that path for other similar platform as well. This is what I did to get it to compile on a Mac:

#if defined(__linux__) || defined(__APPLE__)

But that large block of platform specific functionality is really taking the attention from the relevant parts of the code, the game itself, so it think it is very important that you move all that to a separate file.

Also, instead of using the ugly Windows names like _getch() and _kbhit(), which are reserved, create your own wrapper functions that call those on Windows and do something else on Linux/Mac/etc.

There is actually some global state involved in the platform code, so you might actually want to turn that into a class Terminal or Console that your game instantiates during initialization.

Other problems I faced trying to compile with Clang:

Clang doesn't like the constexpr in those global initializer_lists. Changing all of them to const works fine:

const std::initializer_list<size_t> il = ...

Though its not clear to me why the choice for an initializer_list. An std::array seems more adequate in this case.

Also that name, il is mighty vague. What's that list for?


The gotoxy() function for Unix is missing a return for the normal case:

bool gotoxy(unsigned short x = 1, unsigned short y = 1) {
     if ((x == 0) || (y == 0))
        return false;
    std::cout << "\x1B[" << y << ";" << x << "H";
    return true // <--- Was missing this one!
}

Clang also complained about a few missing type casts, such as here:

Random getRandom{ 0, static_cast<int>(blockList.size() - 1) };

And here:

COORD coord = { 0, static_cast<short>(ilBoard.size()) };

Without the casts, it failed to compile.


I'm pretty sure blockList should be const.

You should initialize this variable on declaration:

void Tetris::userInput()
{
    char key;

    key = _getch();

    ...

Just

const char key = _getch();

Would be better. In such cases, I think also making it const can help emphasising that it is only initialized once.

\$\endgroup\$
  • 1
    \$\begingroup\$ i tried to use ANSI Escape sequences but i had similar result like what you have. probably your laptop doesn't support ANSI Escape sequences like mine \$\endgroup\$ – MORTAL Dec 22 '14 at 20:58
2
\$\begingroup\$

Throughout your code you consistently refuse to use braces ({}) around one-line if statements until you reach void Tetris::isFull() like this:

static int _getch(void)
{
    unsigned char temp;

    raw();
    /* stdin = fd 0 */
    if (read(0, &temp, 1) != 1)
        return 0;
    return temp;
}

AND

bool gotoxy(unsigned short x = 1, unsigned short y = 1) {
    if ((x == 0) || (y == 0))
        return false;
    std::cout << "\x1B[" << y << ";" << x << "H";
}

Then you use braces ({}) around your if statements.

void Tetris::isFull()
{
    for (auto i : il)
    {
        for (auto j : il)
        {       
            if(mBoard[i][j + il.size()] > 1)
            {
                gameOver = true;
            }
        }
    }
}

You are also using braces({}) around one-line for loops but not if statements. This confuses me and is not a consistent coding style, it could lead to bugs.

Here is the only place that you don't use Braces ({}) around a for loop in your code

void Tetris::checkLine()
{
    std::copy(mBoard.begin(), mBoard.end(), mStage.begin());

    for (auto i = ilBoard.begin() + 1; i != ilBoard.end() - 2; ++i)
    {
        bool isCompeteLine = true;

        for (auto j = ilBoardRow.begin() + 1; j != ilBoardRow.end() - 1; ++j)
        {
            if (mStage[*i][*j] == 0)
            {
                isCompeteLine = false;
            }
        }

        if (isCompeteLine)
        {
            for (auto j : il)
                std::copy(mStage[*i - 1 - j].begin(), mStage[*i - 1 - j].end(), mStage[*i - j].begin());
        }
    }

    std::copy(mStage.begin(), mStage.end(), mBoard.begin());
}

It looks like you fixed the bracing on the code after that method, or someone else wrote the code from that point.

Make sure that you are consistent with your coding style.

Always use braces. there is plenty of Memory for them.

\$\endgroup\$
  • \$\begingroup\$ i post the original source code where i got the start point to work out in C++. but i didn't get what you mean by braces \$\endgroup\$ – MORTAL Dec 22 '14 at 15:02
  • \$\begingroup\$ @MORTAL I clarified with some code snippets \$\endgroup\$ – Malachi Dec 22 '14 at 15:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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