2048 sliding-tile game in the terminal, 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.
- Tiles slide and merge equal values toward the pressed direction
- Each move spawns a new 2 (or occasionally a 4)
- Merges add their value to the score
- Rows/columns are transposed to reuse one slide routine
This section walks through every moving part of tiles.c so you can understand
(or extend) the implementation from top to bottom.
The entire game board is a single 4×4 array of int, with a separate int for
the running score. Both are file-scope globals so every function can reach them
without parameter threading:
static int g[4][4], score;A cell value of 0 means the cell is empty. All non-zero values are powers of
two (2, 4, 8 … 2048).
The game needs to read individual keystrokes without waiting for Enter and without echoing characters to the screen. This is done by switching the terminal to raw mode at startup and restoring it on exit:
static struct termios orig;
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);
}
static void raw_off(void){ tcsetattr(0, TCSANOW, &orig); printf("\033[?25h"); }O_NONBLOCK makes read() return immediately with −1 when no key is
buffered, so the game loop can call usleep(10000) and poll again rather than
blocking.
After every successful move (and at the start of the game), one empty cell is chosen at random and filled with 2 (90 % chance) or 4 (10 % chance):
static void spawn(void){
int e[16], n = 0;
for(int i = 0; i < 16; i++) if(!g[i/4][i%4]) e[n++] = i;
if(!n) return;
int p = e[rand() % n];
g[p/4][p%4] = (rand() % 10 == 0) ? 4 : 2;
}The flat index i is decoded back to (row, col) via i/4 and i%4.
slide() operates on a single int r[4] row passed by pointer. It performs
three steps in sequence:
- Compact — pack all non-zero values to the front of a scratch array.
- Merge — scan left-to-right; when two adjacent equal values are found,
double the left one, add it to
score, and zero the right one. - Re-compact — pack the merged result into the output row.
static int slide(int *r){
int moved = 0, tmp[4] = {0,0,0,0}, k = 0;
for(int i = 0; i < 4; i++) if(r[i]) tmp[k++] = r[i];
for(int i = 0; i < 3; i++)
if(tmp[i] && tmp[i] == tmp[i+1]){ tmp[i] *= 2; score += tmp[i]; tmp[i+1] = 0; }
int out[4] = {0,0,0,0}; k = 0;
for(int i = 0; i < 4; i++) if(tmp[i]) out[k++] = tmp[i];
for(int i = 0; i < 4; i++){ if(r[i] != out[i]) moved = 1; r[i] = out[i]; }
return moved;
}The function returns 1 if any cell changed, so the caller knows whether to
spawn a new tile.
Rather than writing four separate slide routines, move() remaps each of the
four directions to a virtual row and calls the same slide():
static int move(int dir){ /* 0=left 1=right 2=up 3=down */
int moved = 0;
for(int i = 0; i < 4; i++){
int r[4];
for(int j = 0; j < 4; j++){
if(dir == 0) r[j] = g[i][j];
else if(dir == 1) r[j] = g[i][3-j];
else if(dir == 2) r[j] = g[j][i];
else r[j] = g[3-j][i];
}
if(slide(r)) moved = 1;
/* write results back with the same index mapping */
for(int j = 0; j < 4; j++){
if(dir == 0) g[i][j] = r[j];
else if(dir == 1) g[i][3-j] = r[j];
else if(dir == 2) g[j][i] = r[j];
else g[3-j][i] = r[j];
}
}
return moved;
}- Left (0): row
i, columns left-to-right — identical to the natural layout. - Right (1): row
i, columns reversed — slide pushes tiles to the high end. - Up (2): column
iread top-to-bottom as a virtual row. - Down (3): column
iread bottom-to-top as a virtual row.
The same index expression is used for the write-back, so the transposition is automatically undone.
The renderer uses ANSI escape sequences to rewrite the screen in-place on every frame, avoiding flicker:
printf("\033[H 2048 score: %d\n\n", score); /* move cursor to top-left */
for(int i = 0; i < 4; i++){
for(int j = 0; j < 4; j++){
if(g[i][j]) printf("%6d", g[i][j]);
else printf(" .");
}
printf("\n\n");
}\033[2J (sent once at startup) clears the screen. Subsequent frames use
\033[H to home the cursor and overwrite the same lines, so there is no
scrolling or blinking. \033[?25l hides the cursor during play; \033[?25h
(in raw_off) restores it on exit.
The loop renders the board, then spins in an inner polling loop until a valid key arrives:
while(run){
/* render ... */
int did = 0; char c;
while(!did){
if(read(0, &c, 1) == 1){
int d = -1;
if(c=='a') d=0; else if(c=='d') d=1;
else if(c=='w') d=2; else if(c=='s') d=3;
else if(c=='q'){ run=0; break; }
else if(c=='\033'){ /* arrow-key escape sequence */
char a, b;
if(read(0,&a,1)==1 && read(0,&b,1)==1){
if(b=='D') d=0; else if(b=='C') d=1;
else if(b=='A') d=2; else if(b=='B') d=3;
} }
if(d >= 0){ if(move(d)) spawn(); did=1; }
} else usleep(10000);
}
}Arrow keys arrive as three-byte sequences \033 [ <letter>, where the letter
encodes the direction (A=up, B=down, C=right, D=left). Because stdin is in
non-blocking mode the two follow-up bytes are read immediately without risk of
hanging.
gcc tiles.c -o tiles
./tiles
WASD or arrow keys to move, q to quit.