Skip to content

Corg-Labs/blocks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Repository files navigation

Tetris

Tetris in the terminal, written in C.

A small, self-contained demo written in pure C — no external libraries, just the standard library and POSIX. Part of the Corg-Labs collection of single-file C programs.


How It Works

  1. Seven tetromino shapes are stored as 4x4 rotation bitmasks
  2. Collision is checked against walls, floor and settled blocks
  3. A gravity tick drops the active piece on a timer
  4. Full rows are cleared and scored

Tutorial

This tutorial walks through the core ideas in blocks.c — how the board is represented, how pieces are encoded, how collision works, and how the game loop ties everything together.

1. The Board

The playing field is a fixed 2-D array of ints:

#define W 12
#define H 22

static int board[H][W];

Every cell is either 0 (empty) or 1 (settled block). The board starts zeroed with memset(board, 0, sizeof(board)) and is never resized.

2. Tetromino Encoding as Bitmasks

All seven tetrominoes — including their four rotations — are packed into a single constant table of 16-bit integers:

static const int PIECES[7][4]={
 {0x0F00,0x2222,0x0F00,0x2222}, /* I */
 {0x0660,0x0660,0x0660,0x0660}, /* O */
 {0x0E40,0x4C40,0x4E00,0x4640}, /* T */
 {0x06C0,0x8C40,0x6C00,0x4620}, /* S */
 {0x0C60,0x4C80,0xC600,0x2640}, /* Z */
 {0x44C0,0x8E00,0xC880,0x0E20}, /* L */
 {0x88C0,0x0E80,0xC440,0x2E00}  /* J */
};

Each value represents a 4×4 grid of cells packed into the high 16 bits of an int. Bit 15 is the top-left cell, bit 12 is the top-right, bit 0 is the bottom-right. A helper extracts a single cell by index:

static int cell(int p, int r, int x, int y){
    return (PIECES[p][r] >> (15 - (y*4 + x))) & 1;
}

p is the piece index (0–6), r is the rotation (0–3), and (x, y) is the column/row within the 4×4 bounding box.

3. Collision Detection

Before any move is committed the game checks whether the piece would overlap a wall, the floor, or a settled cell:

static int collide(int p, int r, int px, int py){
    for(int y=0;y<4;y++) for(int x=0;x<4;x++) if(cell(p,r,x,y)){
        int bx=px+x, by=py+y;
        if(bx<0 || bx>=W || by>=H) return 1;
        if(by>=0 && board[by][bx]) return 1;
    }
    return 0;
}

It iterates over the 4×4 bounding box, skips empty cells, and fails if any filled cell lands out-of-bounds or on a non-zero board cell. Every movement attempt (left, right, down, rotate, hard-drop) calls collide first and only applies the move when it returns 0.

4. Input Handling and Raw Mode

The program switches the terminal into raw, non-blocking mode at startup so keystrokes are available immediately without pressing Enter:

static void raw_on(void){
    tcgetattr(0, &orig);
    struct termios t = orig;
    t.c_lflag &= ~(ICANON|ECHO);
    tcsetattr(0, TCSANOW, &t);
    fcntl(0, F_SETFL, fcntl(0, F_GETFL) | O_NONBLOCK);
}

Inside the game loop, read(0, &c, 1) is called in a tight loop that drains all pending keystrokes each frame. Arrow-key escape sequences (\033[A etc.) are also handled by reading the two follow-up bytes when the first byte is \033.

5. The Gravity Tick and Lock-Down

The game loop runs at roughly 30 fps (usleep(33000)). A simple counter drives gravity:

if(++tick >= 8){ tick = 0;
    if(!collide(p, r, px, py+1)) py++;
    else{
        /* lock the piece into the board */
        for(int y=0;y<4;y++) for(int x=0;x<4;x++)
            if(cell(p,r,x,y)){ int by=py+y, bx=px+x; if(by>=0) board[by][bx]=1; }
        /* spawn next piece */
        p=rand()%7; r=0; px=W/2-2; py=0;
        if(collide(p,r,px,py)) run=0;  /* game over */
    }
}

Every 8 frames (≈ 267 ms) the active piece tries to move down one row. If it cannot, each of its cells is written into board, a new random piece spawns at the top, and an immediate collision there ends the game.

6. Row Clearing and Scoring

After a piece locks, every row is checked from bottom to top:

for(int y=H-1; y>=0; y--){
    int fullrow=1;
    for(int x=0;x<W;x++) if(!board[y][x]) fullrow=0;
    if(fullrow){
        score += 100;
        for(int yy=y; yy>0; yy--)
            memcpy(board[yy], board[yy-1], sizeof(board[0]));
        memset(board[0], 0, sizeof(board[0]));
        y++;   /* re-check same index after shifting */
    }
}

A full row earns 100 points and is removed by shifting every row above it down one position with memcpy, then clearing the top row. The index y is incremented so the same row position is re-examined after the shift, correctly handling multiple simultaneous clears.

7. Rendering

Each frame the cursor is moved to the home position (\033[H) and the board is redrawn in-place — no flicker from clearing the screen:

printf("\033[H");
for(int y=0;y<H;y++){
    putchar('<');
    for(int x=0;x<W;x++){
        int on = board[y][x];
        if(!on) for(int yy=0;yy<4;yy++) for(int xx=0;xx<4;xx++)
            if(cell(p,r,xx,yy) && px+xx==x && py+yy==y) on=2;
        printf(on ? (on==2 ? "[]" : "##") : "  ");
    }
    printf(">\n");
}

Settled cells print ##, the active piece prints [], and empty cells are two spaces. Border walls are drawn as < and > characters. Because every frame overwrites the exact same terminal cells, no ANSI erase sequence is needed beyond the initial \033[2J at startup.


Build

gcc blocks.c -o blocks

Run

./blocks

Controls

a/d move, w rotate, s soft-drop, space hard-drop, q quit.

About

► Tetris in the terminal, written in C.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages