Skip to content

Instantly share code, notes, and snippets.

@chebert
Created June 30, 2018 17:42
Show Gist options
  • Save chebert/0443fa343804fe5cc203d35ddcbd1155 to your computer and use it in GitHub Desktop.
Save chebert/0443fa343804fe5cc203d35ddcbd1155 to your computer and use it in GitHub Desktop.
Tetris in C using NCURSES
/*
map <leader>g :!gcc % -o %:r -lncurses && ./%:r <CR>
gcc tetris.c -o tetris -lncurses
./tetris
*/
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <ncurses.h>
#include <sys/time.h>
#define internal static
#define global_variable static
#define local_persist static
#define UNUSED(x) (void)(x);
#define MIN(x,y) ((x) < (y) ? (x) : (y))
#define PRINT(fmt, ...)\
printf(fmt "\n", ##__VA_ARGS__);
#define ARRAY_LENGTH(arr)\
(sizeof(arr) / sizeof(arr[0]))
#define for_array(index, arr)\
for (index; index < ARRAY_LENGTH(arr); ++index)
typedef int8_t int8;
typedef uint8_t uint8;
typedef int16_t int16;
typedef uint16_t uint16;
typedef int32_t int32;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
typedef float real32;
typedef double real64;
typedef int32_t bool32;
typedef char* c_str;
typedef struct {
int32 x, y;
} v2;
uint32
time_in_us() {
struct timeval t;
gettimeofday(&t, NULL);
return t.tv_sec * 1000000 + t.tv_usec;
}
#define NUM_ROTATIONS 4
#define NUM_BLOCKS 4
typedef v2 shape_t[NUM_BLOCKS];
typedef shape_t piece_t[NUM_ROTATIONS];
typedef enum {
NO_TETR = -1,
T_TETR,
J_TETR,
L_TETR,
O_TETR,
S_TETR,
Z_TETR,
I_TETR,
NUM_TETRS
} tetr_t;
#define play_area_cols 10
#define play_area_rows 22
global_variable
tetr_t play_area[play_area_rows][play_area_cols] = { 0 };
global_variable
char tetr_chars[NUM_TETRS] = "TJLOSZI";
global_variable
bool32 has_color_terminal;
global_variable
piece_t pieces[NUM_TETRS] = {
/* T_TETR */
{
{ { 0, 1 }, { 1, 1 }, { 2, 1 }, { 1, 2 } },
{ { 1, 0 }, { 0, 1 }, { 1, 1 }, { 1, 2 } },
{ { 0, 1 }, { 1, 1 }, { 2, 1 }, { 1, 0 } },
{ { 1, 0 }, { 2, 1 }, { 1, 1 }, { 1, 2 } }
},
/*J_TETR*/
{
{ { 0, 1 }, { 1, 1 }, { 2, 1 }, { 2, 2 } },
{ { 1, 0 }, { 1, 1 }, { 1, 2 }, { 0, 2 } },
{ { 0, 1 }, { 1, 1 }, { 2, 1 }, { 0, 0 } },
{ { 1, 0 }, { 1, 1 }, { 1, 2 }, { 2, 0 } },
},
/*L_TETR*/
{
{ { 0, 1 }, { 1, 1 }, { 2, 1 }, { 0, 2 } },
{ { 1, 0 }, { 1, 1 }, { 1, 2 }, { 0, 0 } },
{ { 0, 1 }, { 1, 1 }, { 2, 1 }, { 2, 0 } },
{ { 1, 0 }, { 1, 1 }, { 1, 2 }, { 2, 2 } },
},
/*O_TETR*/
{
{ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, 1 } },
{ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, 1 } },
{ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, 1 } },
{ { 0, 0 }, { 0, 1 }, { 1, 0 }, { 1, 1 } },
},
/*S_TETR*/
{
{ { 0, 2 }, { 1, 2 }, { 1, 1 }, { 2, 1 } },
{ { 1, 0 }, { 1, 1 }, { 2, 1 }, { 2, 2 } },
{ { 0, 2 }, { 1, 2 }, { 1, 1 }, { 2, 1 } },
{ { 1, 0 }, { 1, 1 }, { 2, 1 }, { 2, 2 } },
},
/*Z_TETR*/
{
{ { 0, 1 }, { 1, 1 }, { 1, 2 }, { 2, 2 } },
{ { 1, 1 }, { 1, 2 }, { 2, 0 }, { 2, 1 } },
{ { 0, 1 }, { 1, 1 }, { 1, 2 }, { 2, 2 } },
{ { 1, 1 }, { 1, 2 }, { 2, 0 }, { 2, 1 } },
},
/*I_TETR*/
{
{ { 0, 2 }, { 1, 2 }, { 2, 2 }, { 3, 2 } },
{ { 2, 0 }, { 2, 1 }, { 2, 2 }, { 2, 3 } },
{ { 0, 2 }, { 1, 2 }, { 2, 2 }, { 3, 2 } },
{ { 2, 0 }, { 2, 1 }, { 2, 2 }, { 2, 3 } },
}
};
internal int
get_score(int level, int num_lines) {
int modifier = 0;
switch (num_lines) {
case 1:
modifier = 40;
break;
case 2:
modifier = 100;
break;
case 3:
modifier = 300;
break;
case 4:
modifier = 1200;
break;
}
return modifier * (level + 1);
}
internal int
get_frames_per_cell(int level) {
const int fpcs[] = {
48, 43, 38, 33, 28, 23, 18, 13, 8, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3
};
if (level >= 19) {
return level <= 28 ? 2 : 1;
}
return fpcs[level];
}
internal bool32
soft_drop_enabled(int level) {
return level < 19;
}
internal bool32
check_collision(v2* shape, int x, int y) {
int i;
for (i = 0; i < NUM_BLOCKS; ++i) {
int posx = shape[i].x + x;
int posy = shape[i].y + y;
if (posx < 0 || posx >= play_area_cols || posy >= play_area_rows || play_area[posy][posx] != NO_TETR) {
return TRUE;
}
}
return FALSE;
}
#define MS(x) (1000*(x))
#define SECONDS(x) (MS(1000*(x)))
#define LINES_PER_LEVEL 10
#define SOFT_DROP_TIME 2
typedef enum {
PLAYING,
CLEARING_LINES,
CLEARING_AREA,
GAME_OVER
} program_state_t;
typedef struct {
uint32 level;
uint32 score;
uint32 lines_cleared;
uint32 lines_until_next_level;
uint32 drop_time;
uint32 drop_timer;
int soft_drop_counter;
bool32 soft_dropping;
tetr_t next_tetr;
tetr_t tetr;
shape_t* piece_shape;
v2 pos;
uint32 rotation;
} game_t;
#define PLAY_AREA_TOP 2
#define PLAY_AREA_LEFT 5
global_variable game_t zero_game = {};
internal
v2 tetr_offset(tetr_t tetr) {
v2 new_piece_pos = { -1, 1 }, new_i_piece_pos = { -2, 0 },
new_o_piece_pos = { -1, 2 };
if (tetr == I_TETR) {
return new_i_piece_pos;
} else if (tetr == O_TETR) {
return new_o_piece_pos;
} else {
return new_piece_pos;
}
}
internal void
start_new_piece(game_t* game) {
v2 pos = tetr_offset(game->tetr);
pos.x += play_area_cols / 2;
game->piece_shape = pieces[game->tetr];
game->pos = pos;
game->soft_dropping = FALSE;
game->soft_drop_counter = 0;
game->rotation = 0;
}
internal tetr_t
random_tetr() {
return rand() % NUM_TETRS;
}
enum {
SYM_PAIR = 1,
LEFT_PAIR,
RIGHT_PAIR,
BG_PAIR,
FLASH_PAIR
};
internal int
tetr_color_pair(tetr_t tetr) {
if (tetr == J_TETR || tetr == S_TETR) {
return LEFT_PAIR;
} else if (tetr == L_TETR || tetr == Z_TETR) {
return RIGHT_PAIR;
}
return SYM_PAIR;
}
internal void
draw_piece(tetr_t tetr, int rotation, int x, int y) {
int i;
if (has_color_terminal) {
wattron(stdscr, COLOR_PAIR(tetr_color_pair(tetr)));
}
for (i = 0; i < NUM_BLOCKS; ++i) {
int ch = tetr_chars[tetr];
if (has_color_terminal) {
ch = '`';
}
mvaddch(
pieces[tetr][rotation][i].y + y,
2*(pieces[tetr][rotation][i].x) + x,
ch);
mvaddch(
pieces[tetr][rotation][i].y + y,
2*(pieces[tetr][rotation][i].x)+1 + x,
ch);
}
}
enum {
COLOR_LEFT = 8,
COLOR_RIGHT
};
internal void
new_game(game_t* game, int level) {
*game = zero_game;
memset(play_area, NO_TETR, sizeof(play_area));
game->level = level;
game->lines_until_next_level = MIN(level * LINES_PER_LEVEL + LINES_PER_LEVEL, 100);
game->drop_time = get_frames_per_cell(level);
game->tetr = random_tetr();
game->next_tetr = random_tetr();
start_new_piece(game);
}
typedef struct {
int r, g, b;
} color_t;
internal color_t
make_color(uint8 r, uint8 g, uint8 b) {
color_t c;
c.r = r * 1000 / 255;
c.g = g * 1000 / 255;
c.b = b * 1000 / 255;
return c;
}
typedef enum {
BLUE_COLOR,
CYAN_COLOR,
GREEN_COLOR,
LIGHT_GREEN_COLOR,
PURPLE_COLOR,
PINK_COLOR,
ORANGE_COLOR,
GRAY_COLOR,
MAROON_COLOR,
LIGHT_ORANGE_COLOR,
LIGHT_BLUE_COLOR,
NUM_COLORS
} color_index_t;
global_variable
color_t colors[NUM_COLORS];
typedef struct {
color_index_t left, right;
} level_colors_t;
global_variable
level_colors_t level_colors[] = {
{ BLUE_COLOR, CYAN_COLOR },
{ GREEN_COLOR, LIGHT_GREEN_COLOR },
{ PURPLE_COLOR, PINK_COLOR },
{ BLUE_COLOR, LIGHT_GREEN_COLOR },
{ PURPLE_COLOR, LIGHT_GREEN_COLOR },
{ LIGHT_GREEN_COLOR, LIGHT_BLUE_COLOR },
{ ORANGE_COLOR, GRAY_COLOR },
{ PURPLE_COLOR, MAROON_COLOR },
{ BLUE_COLOR, LIGHT_ORANGE_COLOR },
{ ORANGE_COLOR, LIGHT_ORANGE_COLOR },
};
internal void
init_colors() {
colors[BLUE_COLOR] = make_color(0x5f, 0x5f, 0xff);
colors[LIGHT_BLUE_COLOR] = make_color(0x5f, 0xaf, 0xff);
colors[CYAN_COLOR] = make_color(0x5f, 0xff, 0xff);
colors[GREEN_COLOR] = make_color(0x00, 0x5f, 0x00);
colors[LIGHT_GREEN_COLOR] = make_color(0x00, 0xd7, 0x00);
colors[PURPLE_COLOR] = make_color(0x87, 0x00, 0xaf);
colors[PINK_COLOR] = make_color(0xff, 0x00, 0xaf);
colors[ORANGE_COLOR] = make_color(0xff, 0x5f, 0x00);
colors[GRAY_COLOR] = make_color(0xb2, 0xb2, 0xb2);
colors[MAROON_COLOR] = make_color(0x87, 0x00, 0x00);
colors[LIGHT_ORANGE_COLOR] = make_color(0xff, 0xaf, 0x00);
}
internal void
set_color(int curses_color, color_t* c) {
init_color(curses_color, c->r, c->g, c->b);
}
internal void
start_new_level(game_t* game) {
if (has_color_terminal) {
set_color(COLOR_LEFT, &colors[level_colors[game->level % 10].left]);
set_color(COLOR_RIGHT, &colors[level_colors[game->level % 10].right]);
}
game->drop_time = get_frames_per_cell(game->level);
}
internal void
draw_box(int x, int y, int width, int height) {
int i;
for (i = 0; i < width; ++i) {
int row;
for (row = 0; row < height; ++row) {
mvaddch(y + row, x + i, ' ');
}
}
for (i = 0; i < width; ++i) {
mvaddch(y, x + i, '-');
mvaddch(y + height, x + i, '-');
}
for (i = 0; i < height; ++i) {
mvaddch(y + i, x, '|');
mvaddch(y + i, x + width, '|');
}
mvaddch(y, x, '+');
mvaddch(y, x + width, '+');
mvaddch(y+height, x+width, '+');
mvaddch(y+height, x, '+');
}
int main(int argc, char** argv) {
UNUSED(argc);
UNUSED(argv);
srand(time(NULL));
/* Initialize curses */
initscr();
cbreak();
keypad(stdscr, TRUE);
noecho();
curs_set(FALSE);
has_color_terminal = has_colors() && can_change_color();
timeout(0);
game_t game;
new_game(&game, 0);
uint32 top_score = 0;
bool32 is_flashing = FALSE;
uint32 flash_timer = 0;
uint32 flash_time = 40;
if (has_color_terminal) {
init_colors();
start_color();
init_pair(SYM_PAIR, COLOR_LEFT, COLOR_WHITE);
init_pair(LEFT_PAIR, COLOR_WHITE, COLOR_LEFT);
init_pair(RIGHT_PAIR, COLOR_WHITE, COLOR_RIGHT);
init_pair(BG_PAIR, COLOR_WHITE, COLOR_BLACK);
init_pair(FLASH_PAIR, COLOR_BLACK, COLOR_WHITE);
bkgd(COLOR_PAIR(BG_PAIR));
}
/* Vars */
bool32 running = TRUE;
program_state_t prog_state = PLAYING;
uint32 last_update = time_in_us();
uint32 frame_timer = 0;
uint32 clear_line_time = 30;
uint32 clear_area_time = 60;
uint32 clear_counter = 0;
uint32 num_filled_rows;
char filled_rows[play_area_rows];
start_new_level(&game);
while (running) {
/* Get Input */
int ch;
while ((ch = getch()) != -1) {
bool32 started_soft_dropping = FALSE;
switch (prog_state) {
case CLEARING_LINES:
case PLAYING: {
switch (ch) {
case 'q':
case 'Q':
running = FALSE;
continue;
case KEY_LEFT:
if (!check_collision(game.piece_shape[game.rotation], game.pos.x - 1, game.pos.y)) {
--game.pos.x;
}
break;
case KEY_RIGHT:
{
if (!check_collision(game.piece_shape[game.rotation], game.pos.x + 1, game.pos.y)) {
++game.pos.x;
}
}
break;
case 'z': case 'Z':
case 'x': case 'X':
{
int next_rotation;
if (ch == 'z' || ch == 'Z') {
next_rotation = game.rotation + 1;
if (next_rotation >= 4) {
next_rotation = 0;
}
} else {
next_rotation = game.rotation - 1;
if (next_rotation == -1) {
next_rotation = NUM_ROTATIONS - 1;
}
}
if (!check_collision(game.piece_shape[next_rotation], game.pos.x, game.pos.y)) {
game.rotation = next_rotation;
}
}
break;
case KEY_DOWN:
if (soft_drop_enabled(game.level)) {
if (!game.soft_dropping) {
started_soft_dropping = TRUE;
}
game.soft_dropping = TRUE;
}
break;
case ' ':
/* TODO: DEBUG. remove me.. */
game.drop_timer = 0;
++game.tetr;
if (game.tetr == NUM_TETRS) {
game.tetr = 0;
}
start_new_piece(&game);
break;
}
if (!started_soft_dropping) {
game.soft_dropping = FALSE;
game.soft_drop_counter = 0;
}
} break;
case CLEARING_AREA:
case GAME_OVER: {
switch (ch) {
case '\n':
prog_state = PLAYING;
new_game(&game, 0);
start_new_level(&game);
break;
case 'q':
case 'Q':
running = FALSE;
continue;
}
} break;
}
}
frame_timer += time_in_us() - last_update;
last_update = time_in_us();
if (frame_timer > MS(16)) {
/* drop piece if drop_timer expired */
/* Draw */
switch (prog_state) {
case GAME_OVER: {
} break;
case CLEARING_AREA: {
++clear_counter;
is_flashing = FALSE;
flash_timer = 0;
if (clear_counter > clear_area_time) {
prog_state = GAME_OVER;
}
} break;
case CLEARING_LINES: {
++clear_counter;
if (clear_counter > clear_line_time) {
/* copy down if filled */
int row, col;
for (row = 0; row < num_filled_rows; ++row) {
int copy_row;
int filled_row = filled_rows[row];
/* Move copy rows above filled_row down one */
for (copy_row = filled_row; copy_row > 0; --copy_row) {
memcpy(play_area[copy_row], play_area[copy_row-1], play_area_cols * sizeof(play_area[0][0]));
}
++game.lines_cleared;
}
game.score += get_score(game.level, num_filled_rows);
if (game.lines_until_next_level > num_filled_rows) {
game.lines_until_next_level -= num_filled_rows;
} else {
game.lines_until_next_level = LINES_PER_LEVEL - (num_filled_rows - game.lines_until_next_level);
++game.level;
start_new_level(&game);
}
prog_state = PLAYING;
}
} break;
case PLAYING: {
{
int time = game.soft_dropping ? SOFT_DROP_TIME : game.drop_time;
if (game.drop_timer > time) {
/* drop piece */
if (!check_collision(game.piece_shape[game.rotation], game.pos.x, game.pos.y + 1)) {
game.pos.y++;
if (game.soft_dropping) {
++game.soft_drop_counter;
}
} else {
/* land piece */
int i;
for (i = 0; i < NUM_BLOCKS; ++i) {
int row = game.piece_shape[game.rotation][i].y + game.pos.y;
int col = game.piece_shape[game.rotation][i].x + game.pos.x;
if (row < 2 || play_area[row][col] != NO_TETR) {
/* game over */
prog_state = CLEARING_AREA;
clear_counter = 0;
if (game.score > top_score) {
top_score = game.score;
}
continue;
}
play_area[row][col] = game.tetr;
}
if (game.soft_dropping) {
game.score += game.soft_drop_counter;
}
game.tetr = game.next_tetr;
game.next_tetr = random_tetr();
start_new_piece(&game);
/* check if any rows are filled */
{
int row, col;
num_filled_rows = 0;
for (row = 0; row < play_area_rows; ++row) {
bool32 filled = TRUE;
for (col = 0; col < play_area_cols; ++col) {
filled = play_area[row][col] != NO_TETR;
if (!filled) {
break;
}
}
if (filled) {
filled_rows[num_filled_rows++] = row;
}
}
if (num_filled_rows) {
prog_state = CLEARING_LINES;
clear_counter = 0;
if (num_filled_rows == 4) {
is_flashing = TRUE;
}
}
}
}
game.drop_timer = 0;
}
}
++game.drop_timer;
} break;
}
if (is_flashing) {
++flash_timer;
if (flash_timer > flash_time) {
flash_timer = 0;
is_flashing = FALSE;
}
}
erase();
{
int i;
if (has_color_terminal && is_flashing && flash_timer/6 % 2 == 0) {
int x, y, width = play_area_cols*4 + 4, height = play_area_rows+4;
wattron(stdscr, COLOR_PAIR(FLASH_PAIR));
for (y = 0; y < height; ++y) {
for (x = 0; x < width; ++x) {
if (x % 2 != y % 2) {
mvaddch(y, x, ' ');
}
}
}
}
if (has_color_terminal) {
wattron(stdscr, COLOR_PAIR(BG_PAIR));
}
draw_box(PLAY_AREA_LEFT + (play_area_cols+1)*2, PLAY_AREA_TOP + 2, 10, 5);
mvprintw(PLAY_AREA_TOP + 3, PLAY_AREA_LEFT+(play_area_cols+1)*2 + 1, "Top:");
mvprintw(PLAY_AREA_TOP + 4, PLAY_AREA_LEFT+(play_area_cols+2)*2 + 1, "%06u", top_score);
mvprintw(PLAY_AREA_TOP + 5, PLAY_AREA_LEFT+(play_area_cols+1)*2 + 1, "Score:");
mvprintw(PLAY_AREA_TOP + 6, PLAY_AREA_LEFT+(play_area_cols+2)*2 + 1, "%06u", game.score);
draw_box(PLAY_AREA_LEFT - 1, PLAY_AREA_TOP + 1, play_area_cols*2 + 1, play_area_rows - 1);
if (prog_state == PLAYING) {
draw_piece(game.tetr, game.rotation, 2*game.pos.x + PLAY_AREA_LEFT, game.pos.y + PLAY_AREA_TOP);
}
{
int y = play_area_rows/2 - 3 + PLAY_AREA_TOP;
int x = 2*play_area_cols + PLAY_AREA_LEFT + 2;
if (has_color_terminal) {
wattron(stdscr, COLOR_PAIR(BG_PAIR));
}
mvprintw(y + 1, x + 3, "Next:");
int width = 11, height = 5;
draw_box(x, y, width, height);
draw_box(x, y + height + 1, width + 2, 2);
mvprintw(y + height + 2, x + 2, "Level: %03u", game.level);
v2 offset = tetr_offset(game.next_tetr);
draw_piece(game.next_tetr, 0, x + 2, y + offset.y + 1);
}
if (has_color_terminal) {
wattron(stdscr, COLOR_PAIR(BG_PAIR));
}
for (i = 0; i < 2*play_area_cols; ++i) {
mvaddch(PLAY_AREA_TOP, PLAY_AREA_LEFT + i, ' ');
mvaddch(PLAY_AREA_TOP + 1, PLAY_AREA_LEFT + i, ' ');
}
draw_box(PLAY_AREA_LEFT - 1, PLAY_AREA_TOP - 1, play_area_cols*2 + 1, 2);
mvprintw(PLAY_AREA_TOP, PLAY_AREA_LEFT + 1, "Lines Cleared: %03u", game.lines_cleared);
int row, col;
for (row = 0; row < play_area_rows; ++row) {
for (col = 0; col < play_area_cols; ++col) {
tetr_t tetr = play_area[row][col];
if (tetr != NO_TETR) {
char ch = tetr_chars[tetr];
if (has_color_terminal) {
ch = '`';
wattron(stdscr, COLOR_PAIR(tetr_color_pair(tetr)));
}
mvaddch(PLAY_AREA_TOP + row, PLAY_AREA_LEFT + 2*col, ch);
mvaddch(PLAY_AREA_TOP + row, PLAY_AREA_LEFT + 2*col+1, ch);
}
}
}
if (prog_state == CLEARING_LINES) {
int i;
int col_center = (play_area_cols / 2);
int num_to_clear = (play_area_cols / 2) * clear_counter / clear_line_time + 1;
if (has_color_terminal) {
wattron(stdscr, COLOR_PAIR(BG_PAIR));
}
for (i = 0; i < num_filled_rows; ++i) {
row = filled_rows[i];
for (col = 0; col < num_to_clear; ++col) {
mvaddch(PLAY_AREA_TOP + row, PLAY_AREA_LEFT + 2*(col+col_center), '>');
mvaddch(PLAY_AREA_TOP + row, PLAY_AREA_LEFT + 2*(col+col_center)+1, '>');
mvaddch(PLAY_AREA_TOP + row, PLAY_AREA_LEFT + 2*(-col-1+col_center), '<');
mvaddch(PLAY_AREA_TOP + row, PLAY_AREA_LEFT + 2*(-col-1+col_center)+1, '<');
}
}
} else if (prog_state == CLEARING_AREA || prog_state == GAME_OVER) {
int num_to_clear = prog_state == CLEARING_AREA ? (play_area_rows - 2) * clear_counter / clear_area_time + 1 : (play_area_rows - 2);
if (has_color_terminal) {
wattron(stdscr, COLOR_PAIR(BG_PAIR));
}
for (row = 0; row < num_to_clear; ++row) {
for (col = 0; col < play_area_cols; ++col) {
mvaddch(PLAY_AREA_TOP + row + 2, PLAY_AREA_LEFT + 2*col, '=');
mvaddch(PLAY_AREA_TOP + row + 2, PLAY_AREA_LEFT + 2*col+1, '=');
}
}
}
}
refresh();
frame_timer = 0;
}
usleep(1);
}
endwin();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment