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.
- Seven tetromino shapes are stored as 4x4 rotation bitmasks
- Collision is checked against walls, floor and settled blocks
- A gravity tick drops the active piece on a timer
- Full rows are cleared and scored
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.
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.
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.
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.
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.
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.
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.
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.
gcc blocks.c -o blocks
./blocks
a/d move, w rotate, s soft-drop, space hard-drop, q quit.