Skip to main content
2 of 4
edited tags; edited title
200_success
  • 144.2k
  • 22
  • 188
  • 473

NCurses-based Tetris game in C++

What sticks out and what would you have done better in this termina-tetris implementation? I do not intend to use namespaces or split it up into multiple files.

#include <ncurses.h>
#include <array>
#include <cstdlib>

// The tetris board
const int board_size_x = 15;
const int board_size_y = 20;

// In-game stats
int block_x = 0; // Position of the moving block inside the board
int block_y = 0;
int tick_force_down; // Ticks down. Force down block when < 0. Resets to 'level' every time the block moves down.
int level;  // Max ticks before block is forced down. Decreases during the game as score increases.
int score;  // Current score (number of lines taken)


// Where to print stuff
const int board_x = 10; // Tetris board
const int board_y = 3;
const int score_y = 1; // Scoreboard
const int score_x = 5;
const int next_block_x = 30; // Next block
const int next_block_y = 5;

const int blocksize = 3;
using block = std::array<std::array<int, blocksize>, blocksize>;

// Tetris board. 0 = empty
std::array<std::array<int, board_size_x>, board_size_y> board {};
block current_block {};
block next_block{};


// Draw a colored square
void drawsquare(int y, int x, int color){
    move(y,x);
    attron(COLOR_PAIR(color));
    addch(' ');
    attroff(COLOR_PAIR(color));
}

// Draw block
void drawblock(int row, int col, block & b){
    for(int y=0; y < blocksize; ++y)
        for(int x=0; x < blocksize; ++x)
            if(b[y][x])
                drawsquare(row + y, col + x, b[y][x]);
}

// Draw moving block
void drawmoving(){
    drawblock(board_y+1+block_y, board_x+1+block_x, current_block);
}


// Draw next block
void drawnext(){
    mvprintw(next_block_y, next_block_x, "Next: ");
    drawblock(next_block_y+1, next_block_x+1, next_block);
}


// Randomize next block
void newnext(){
    int c = 1 + rand()%7;   // Color. 1-7 as initialized for ncurses.
    switch(rand()%7){
        case 0:
            next_block = {0,c,0,  0,c,0,  c,c,c};
            break;
        case 1:
            next_block = {c,c,c,  c,c,c,  c,c,c};
            break;
        case 2:
            next_block = {c,c,0,  0,c,c,  0,0,0};
            break;
        case 3:
            next_block = {0,c,0,  0,c,0,  0,c,c};
            break;
        case 4:
            next_block = {0,c,0,  0,c,0,  0,c,0};
            break;
        case 5:
            next_block = {0,c,c,  c,c,0,  0,0,0};
            break;
        case 6:
            next_block = {0,c,0,  0,c,0,  c,c,0};           
            break;
    }

}


// Crystalizes moving block into the tetris board
void raster(){
    for(int y=0; y < blocksize; ++y)
    for(int x=0; x < blocksize; ++x){
        if(!  current_block[y][x])
            continue;
        board[block_y+y][block_x+x] = current_block[y][x];
    }   
}

// block is inside another rasterized block or outside the board?
bool collide(int row, int col, const block & b){
    for(int y=0; y < blocksize; ++y)
    for(int x=0; x < blocksize; ++x){
        if(! b[y][x] )
            continue;

        int y_on_board = row + y;
        int x_on_board = col + x;

        if(x_on_board < 0 || x_on_board >= board_size_x || y_on_board >= board_size_y)
            return true;

        if(board[y_on_board][x_on_board])
            return true;
    }

    return false;
}

// Drops the next block, makes a new next. False on collide.
bool drop(){
    block_y = 1;
    block_x = board_size_x/2 - 1;
    current_block = next_block;
    newnext();

    return !collide(block_y, block_x, current_block);
}

// Rotated right if possible
void rotright(){
    block rot;
    for(int ny=0; ny < blocksize; ++ny)
        for(int nx=0; nx < blocksize; ++nx)
            rot[ny][nx] = current_block[blocksize-1-nx][ny];
    if(collide(block_y, block_x, rot))
        return;
    current_block = rot;
}

// Rotated left if possible
void rotleft(){
    block rot;
    for(int ny=0; ny < blocksize; ++ny)
        for(int nx=0; nx < blocksize; ++nx)
            rot[ny][nx] = current_block[nx][blocksize-1-ny];
    if(collide(block_y, block_x, rot))
        return;
    current_block = rot;
}


// false and refuse on collide
bool movedown(){
    if(collide(block_y+1, block_x, current_block))
        return false;
    ++block_y;
    return true;
}

void moveleft(){
    if(collide(block_y, block_x-1, current_block))
        return;
    --block_x;  
}

void moveright(){
    if(collide(block_y, block_x+1, current_block))
        return;
    ++block_x;
}



void textout(int y, int x, const char* str){
    mvprintw(y, x, str);
}

// Returns number of cleared lines
int clearlines(){
    int cleared = 0;
    for(int y=0; y < board_size_y; ++ y){
        int squares = 0;
        for(int x=0; x < board_size_x; ++ x){
            if(board[y][x])
                ++squares;
        }


        // Drop down all the above lines        
        if(squares == board_size_x){
            ++cleared;
            for(int xc=0; xc < board_size_x; ++xc) // Clear line. Important for row 0.
                board[y][xc]=0;

            for(int y2 = y; y2 > 0; --y2) // The line we're moving to
            for(int x2 = 0; x2 < board_size_x; ++x2)
                board[y2][x2] = board[y2-1][x2]; // Move above line to this line
        }
        
        
    }
    return cleared;
}

void drawboard(){
    // Draw a box around the tetris board
    mvaddch(board_y, board_x, ACS_ULCORNER);
    mvaddch(board_y, board_x + board_size_x + 1, ACS_URCORNER);
    mvaddch(board_y + board_size_y + 1, board_x, ACS_LLCORNER);
    mvaddch(board_y + board_size_y + 1, board_x + board_size_x + 1, ACS_LRCORNER);

    for(int i = 1; i <= board_size_x; ++i){
        mvaddch(board_y, board_x + i , ACS_HLINE);
        mvaddch(board_y + board_size_y + 1, board_x + i, ACS_HLINE);
    }

    for(int i = 1; i <= board_size_y; ++i){
        mvaddch(board_y + i, board_x, ACS_VLINE);
        mvaddch(board_y + i, board_x + board_size_x + 1, ACS_VLINE);
    }

    // Draw the filled board squares
    for(int y=0; y < board_size_y; ++y)
        for(int x=0; x < board_size_x; ++x)
            drawsquare(board_y + y + 1, board_x + x + 1, board[y][x]);
}

// Init a new game
void newgame(){
    newnext();
    drop();
    level = 300;
    tick_force_down = level;
    score = 0;
}

bool lost = false;
bool ingame_loop(){
    int c=getch();

    if(c == 'q' || c == 'Q')
        return false;

    if(lost){
        mvprintw(0,0,"You lost. Press q to quit.");
        refresh();
        return true;
    }

    bool down = false;

    if(--tick_force_down < 0){
        tick_force_down = level;
        down = true;
    }

    switch(c){
        case ' ':
            down = true;
            while(movedown())
                ;
            break;
        case 'z':
        case 'Z':
            rotleft();
            break;

        case 'x':
        case 'X':
        case KEY_UP:
            rotright();
            break;

        case KEY_LEFT:
            moveleft();
            break;

        case KEY_RIGHT:
            moveright();
            break;

        case KEY_DOWN:
            down = true;
            break;
    }

    if(down){
        tick_force_down = level;
        if(!movedown()){
            raster();
            if(!drop())
                lost = true;
            else{
                int lines = clearlines();
                level -= lines;
                score += lines;
            }
        }
    }

    // Update the screen
    clear();
    drawboard();
    drawmoving();
    drawnext();
    mvprintw(score_y, score_x, "Score: %d", score);
    refresh();

    return true;
}

int main()
{   
    // Init ncurses
    initscr();
    start_color();
    curs_set(0);
    cbreak();
    noecho();
    keypad(stdscr,TRUE);
    for(int i=1; i <= 7; ++i) // man init_pair
        init_pair(i, COLOR_BLACK, i);
    timeout(1);

    newgame();
    while(ingame_loop())
        ;

    endwin();
    return 0;
}