Previous question:
https://codereview.stackexchange.com/questions/74677/text-based-tetris-game-follow-up-final
Summary of improvements:
- Implementation of a
Drawable
class - Separate functionality, input moves to
Game
class - Implementation of
Cloneable
class by CRTP - Removed global variables
- Added
Block
class - Improving the game loop timer by implementing
<chrono>
How can I improve this code further?
Tetris.cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <memory>
#include <chrono>
#include "utility.h"
using Matrix = std::vector<std::vector<int>>;
class Shape
{
public:
virtual ~Shape() = default;
virtual Shape *clone() const = 0;
virtual int getDrived(std::size_t i, std::size_t j) const = 0;
};
template <typename Derived>
struct Clonable : public Shape
{
virtual Shape *clone() const
{
return new Derived(static_cast<const Derived&>(*this));
}
};
class O : public Clonable<O>
{
public:
O() = default;
virtual ~O() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 0, 0, 0 },
{ 0, 1, 1, 0 },
{ 0, 1, 1, 0 },
{ 0, 0, 0, 0 }
}
};
};
class L : public Clonable<L>
{
public:
L() = default;
virtual ~L() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 0, 0, 0 },
{ 0, 1, 1, 0 },
{ 0, 0, 1, 0 },
{ 0, 0, 1, 0 }
}
};
};
class N : public Clonable<N>
{
public:
N() = default;
virtual ~N() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 1, 0, 0 },
{ 0, 1, 1, 0 },
{ 0, 0, 1, 0 },
{ 0, 0, 0, 0 }
}
};
};
class M : public Clonable<M>
{
public:
M() = default;
virtual ~M() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 0, 1, 0 },
{ 0, 1, 1, 0 },
{ 0, 1, 0, 0 },
{ 0, 0, 0, 0 }
}
};
};
class T : public Clonable<T>
{
public:
T() = default;
virtual ~T() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 0, 0, 0 },
{ 0, 1, 0, 0 },
{ 1, 1, 1, 0 },
{ 0, 0, 0, 0 }
}
};
};
class I : public Clonable<I>
{
public:
I() = default;
virtual ~I() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 1, 0, 0 },
{ 0, 1, 0, 0 },
{ 0, 1, 0, 0 },
{ 0, 1, 0, 0 }
}
};
};
class S : public Clonable<S>
{
public:
S() = default;
virtual ~S() = default;
virtual int getDrived(std::size_t i, std::size_t j) const
{
return shape[i][j];
}
private:
Matrix shape
{
{
{ 0, 0, 0, 0 },
{ 0, 1, 1, 0 },
{ 0, 1, 0, 0 },
{ 0, 1, 0, 0 }
}
};
};
class NonCopyable
{
public:
NonCopyable() = default;
virtual ~NonCopyable() = default;
private:
NonCopyable(const NonCopyable &) = delete;
NonCopyable(const NonCopyable &&) = delete;
NonCopyable& operator = (const NonCopyable&) = delete;
};
struct Drawable
{
virtual void draw(std::ostream& stream) const = 0;
};
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 Block : private NonCopyable
{
public:
using Ptr = std::unique_ptr<Shape>;
Block();
protected:
void createBlock();
void rotateBlock();
std::size_t size() const
{
return ilBlock.size();
}
Matrix mBlock;
static const std::initializer_list<size_t> ilBlock;
private:
// shapes
Ptr t;
Ptr m;
Ptr n;
Ptr i;
Ptr o;
Ptr l;
Ptr s;
std::vector<Ptr> shapes;
const int shapeCounter = 7;
Random getRandom{ 0, shapeCounter - 1 };
};
const std::initializer_list<size_t> Block::ilBlock =
{
0, 1, 2, 3
};
Block::Block()
: t(std::make_unique<T>())
, m(std::make_unique<M>())
, n(std::make_unique<N>())
, i(std::make_unique<I>())
, o(std::make_unique<O>())
, l(std::make_unique<L>())
, s(std::make_unique<S>())
{
mBlock.resize(ilBlock.size(), std::vector<int>(ilBlock.size(), 0));
shapes.emplace_back(std::move(t->clone()));
shapes.emplace_back(std::move(m->clone()));
shapes.emplace_back(std::move(n->clone()));
shapes.emplace_back(std::move(i->clone()));
shapes.emplace_back(std::move(o->clone()));
shapes.emplace_back(std::move(l->clone()));
shapes.emplace_back(std::move(s->clone()));
createBlock();
}
void Block::createBlock()
{
int blockType = getRandom();
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
mBlock[i][j] = shapes[blockType]->getDrived(i, j);
}
}
}
void Block::rotateBlock()
{
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
if (i < j)
{
std::swap(mBlock[i][j], mBlock[j][i]);
}
}
std::reverse(mBlock[i].begin(), mBlock[i].end());
}
}
class Tetris : public Block, public Drawable
{
public:
Tetris();
void moveBlock(int, int);
bool isCollide(int, int);
void spawnBlock();
bool applyRotate();
bool isFull();
COORD getPosition()
{
return position;
}
private:
void initField();
void makeBlocks();
void checkLine();
Matrix mStage;
COORD position;
virtual void draw(std::ostream& stream) const;
friend std::ostream& operator<<(std::ostream& stream, const Tetris& self)
{
self.draw(stream);
return stream;
}
int mScore = 0;
Matrix mBoard;
static const std::initializer_list<size_t> ilBoard;
static const std::initializer_list<size_t> ilBoardRow;
};
Tetris::Tetris()
{
mBoard.resize(ilBoard.size(), std::vector<int>(ilBoardRow.size(), 0));
mStage.resize(ilBoard.size(), std::vector<int>(ilBoardRow.size(), 0));
initField();
}
const std::initializer_list<size_t> Tetris::ilBoard =
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
};
const std::initializer_list<size_t> Tetris::ilBoardRow =
{
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
};
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();
}
void Tetris::makeBlocks()
{
position.X = ilBlock.size();
position.Y = 0;
createBlock();
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
mBoard[i][j + size()] += mBlock[i][j];
}
}
}
bool Tetris::isFull()
{
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
if (mBoard[i][j + size()] > 1)
{
return true;
}
}
}
return false;
}
void Tetris::moveBlock(int x2, int y2)
{
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
mBoard[position.Y + i][position.X + j] -= mBlock[i][j];
}
}
position.X = x2;
position.Y = y2;
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
mBoard[position.Y + i][position.X + j] += mBlock[i][j];
}
}
}
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)
{
mScore += 10;
for (auto k : ilBlock)
{
std::copy(mStage[*i - 1 - k].begin(), mStage[*i - 1 - k].end(), mStage[*i - k].begin());
}
}
}
std::copy(mStage.begin(), mStage.end(), mBoard.begin());
}
bool Tetris::isCollide(int x, int y)
{
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
if (mBlock[i][j] && mStage[y + i][x + j] != 0)
{
return true;
}
}
}
return false;
}
bool Tetris::applyRotate()
{
Matrix temp(ilBlock.size(), std::vector<int>(ilBlock.size(), 0));
std::copy(mBlock.begin(), mBlock.end(), temp.begin());
rotateBlock();
if (isCollide(position.X, position.Y))
{
std::copy(temp.begin(), temp.end(), mBlock.begin());
return true;
}
for (auto i : ilBlock)
{
for (auto j : ilBlock)
{
mBoard[position.Y + i][position.X + j] -= temp[i][j];
mBoard[position.Y + i][position.X + j] += mBlock[i][j];
}
}
return false;
}
void Tetris::spawnBlock()
{
if (!isCollide(position.X, position.Y + 1))
{
moveBlock(position.X, position.Y + 1);
}
else
{
checkLine();
makeBlocks();
}
}
void Tetris::draw(std::ostream& stream) const
{
for (auto i : ilBoard)
{
for (auto j : ilBoardRow)
{
switch (mBoard[i][j])
{
case 0:
stream << ' ';
break;
case 9:
stream << '@';
break;
default:
stream << '#';
break;
}
}
stream << '\n';
}
stream << "Score : " << mScore << "\n\n\tA: left\tS: down\tD: right \t Rotation[Space]";
}
class Game : private NonCopyable
{
public:
int menu();
void gameLoop();
private:
void introScreen();
void userInput();
void display();
void gameOverScreen();
Tetris tetris;
};
void Game::gameOverScreen()
{
std::cout << "\n"
" ##### # # # ####### ####### # # ####### ######\n"
"# # # # ## ## # # # # # # # #\n"
"# # # # # # # # # # # # # # #\n"
"# #### # # # # # ##### # # # # ##### ######\n"
"# # ####### # # # # # # # # # #\n"
"# # # # # # # # # # # # # #\n"
" ##### # # # # ####### ####### # ####### # #\n"
"\n\nPress any key and enter\n";
std::cin.ignore();
std::cin.get();
}
void Game::gameLoop()
{
auto start = std::chrono::high_resolution_clock::now();
while (!tetris.isFull())
{
auto end = std::chrono::high_resolution_clock::now();
double timeTakenInSeconds = (end - start).count()
* (static_cast<double>(std::chrono::high_resolution_clock::period::num)
/ std::chrono::high_resolution_clock::period::den);
if (_kbhit())
{
userInput();
}
if(timeTakenInSeconds > .3)
{
tetris.spawnBlock();
display();
start = std::chrono::high_resolution_clock::now();
}
}
clearScreen();
gameOverScreen();
}
int Game::menu()
{
introScreen();
int select_num = 0;
std::cin >> select_num;
switch (select_num)
{
case 1:
case 2:
break;
default:
select_num = 0;
break;
}
return select_num;
}
void Game::introScreen()
{
clearScreen();
std::cout << "#==============================================================================#\n"
"####### ####### ####### ###### ### #####\n"
" # # # # # # # #\n"
" # # # # # # #\n"
" # ##### # ###### # #####\n"
" # # # # # # #\n"
" # # # # # # # #\n"
" # ####### # # # ### #####\t\tmade for fun \n"
"\n\n\n\n"
"\t<Menu>\n"
"\t1: Start Game\n\t2: Quit\n\n"
"#==============================================================================#\n"
"Choose >> ";
}
void Game::display()
{
clearScreen();
std::cout << tetris;
}
void Game::userInput()
{
switch (_getch())
{
case 77:
if (!tetris.isCollide(tetris.getPosition().X + 1, tetris.getPosition().Y))
{
tetris.moveBlock(tetris.getPosition().X + 1, tetris.getPosition().Y);
}
break;
case 75:
if (!tetris.isCollide(tetris.getPosition().X - 1, tetris.getPosition().Y))
{
tetris.moveBlock(tetris.getPosition().X - 1, tetris.getPosition().Y);
}
break;
case 80:
if (!tetris.isCollide(tetris.getPosition().X, tetris.getPosition().Y + 1))
{
tetris.moveBlock(tetris.getPosition().X, tetris.getPosition().Y + 1);
}
break;
case 72:
tetris.applyRotate();
}
}
int main()
{
Game game;
switch (game.menu())
{
case 1:
game.gameLoop();
break;
case 2:
return 0;
default:
std::cerr << "Choose 1~2" << std::endl;
return -1;
}
}
utility.h
#if defined(__linux__) || defined(__APPLE__)
#include <sys/time.h>
#include <termios.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
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;
}
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);
atexit(cooked);
init = 1;
}
static int _kbhit(void)
{
struct timeval timeout;
fd_set read_handles;
int status;
raw();
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();
if (read(0, &temp, 1) != 1)
{
return 0;
}
return temp;
}
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
}
void clearScreen(bool moveToStart = true)
{
std::cout << "\x1B[2J";
if (moveToStart)
{
gotoxy(1, 1);
}
}
#elif _WIN32
#include <conio.h>
#include <Windows.h>
#include <tchar.h>
namespace
{
HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO csbi;
};
void clearScreen()
{
static DWORD count;
static 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);
}
#else
#error "OS not supported!"
#endif