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 <[email protected]> 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;
}
-
\$\begingroup\$ UPDATED code gist.github.com/MORTAL2000/e9adc83b3d9c59d2ba71 \$\endgroup\$MORTAL– MORTAL2014年12月22日 19:17:30 +00:00Commented Dec 22, 2014 at 19:17
2 Answers 2
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_list
s.
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.
-
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– MORTAL2014年12月22日 20:58:52 +00:00Commented Dec 22, 2014 at 20:58
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.
-
\$\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– MORTAL2014年12月22日 15:02:51 +00:00Commented Dec 22, 2014 at 15:02 -
\$\begingroup\$ @MORTAL I clarified with some code snippets \$\endgroup\$Malachi– Malachi2014年12月22日 15:12:07 +00:00Commented Dec 22, 2014 at 15:12