Hangman Game

There's a certain thing about a classic hangman game. Some kind of charm, perhaps. I find it pretty versatile, especially for remote meetings. Sometimes there's got to be some little tension-breaker among co-workers.

So I wrote this little python script to run a hangman game, getting random words from a local word_list.txt file.

There's really not much to it. It's a single python script, one text file with the words, and nothing else. All it needs is a python interpreter. Regularly, tkinter is included in python3. Some linux distributions, however, have it in a separate package. Lubuntu, for example, made me apt-get install python3-tk in order for it to work, but other than that, nothing else. I prefer tkinter over pyQt6 (although pyQt6 is prettier in my not-so-humble opinion) because I'd rather not have to deal with virtual environments.

  • Random word taken from a local `word_list.txt` (one word per line).
  • Classic ASCII-art hang-man that updates after each wrong guess.
  • Input field for single-letter guesses (case-insensitive).
  • New-Game button, win/lose messages, and a "reset" button.
  • No external image files - everything is drawn with text.








Code:
hangman.py
"""
Simple Hang-Man game with a Tkinter GUI.

by Jonathan Torres

This program is free software distributed under the GNU
General Public License, version 3, or any later version
published by the Free Software Foundation. 

"""

import random
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
from typing import List


# ASCII stages

HANGMAN_STAGES: List[str] = [
    """
     +---+
         |
         |
         |
        ===""",
    """
     +---+
     O   |
         |
         |
        ===""",
    """
     +---+
     O   |
     |   |
         |
        ===""",
    """
     +---+
     O   |
    /|   |
         |
        ===""",
    """
     +---+
     O   |
    /|\\  |
         |
        ===""",
    """
     +---+
     O   |
    /|\\  |
    /    |
        ===""",
    """
     +---+
     O   |
    /|\\  |
    / \\  |
        ===""",
]

MAX_WRONG = len(HANGMAN_STAGES) - 1


# wordfile loader helper

def load_word_list(path: Path) -> List[str]:

    if not path.is_file():
        raise FileNotFoundError(f"Word list not found: {path}")
        """hang"""
    words = []
    with path.open(encoding="utf-8") as fh:
        for line in fh:
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            words.append(line.lower())
    if not words:
        raise ValueError("The word list is empty.")
    return words


# main

class HangmanWindow:
    def __init__(self, word_list: List[str]):
        self.word_list = word_list
        self.root = tk.Tk()
        self.root.title("Hang-Man")
        self.root.geometry("300x400")
        self._setup_ui()
        self._new_game()

    def _setup_ui(self):
        self.drawing_lbl = tk.Label(
            self.root,
            font=("Courier", 12),
            justify="center"
        )

        self.word_lbl = tk.Label(
            self.root,
            font=("Sans Serif", 20, "bold")
        )

        #input field.  width 2 because otherwise it looks too small.
        self.guess_input = tk.Entry(self.root, font=("Sans Serif", 14), width=2)
        self.guess_input.bind("<Return>", lambda e: self._process_guess())

        self.wrong_lbl = tk.Label(
            self.root,
            font=("Sans Serif", 12),
            fg="red"
        )

        self.new_btn = tk.Button(self.root, text="New Game", command=self._new_game)
        self.quit_btn = tk.Button(self.root, text="Quit", command=self.root.quit)


        self.drawing_lbl.pack(pady=10)
        self.word_lbl.pack(pady=10)
        self.guess_input.pack(pady=5)
        self.wrong_lbl.pack(pady=5)
        """the"""
        btn_frame = tk.Frame(self.root)
        btn_frame.pack(pady=20, side="top", fill="none", expand=False)
        self.new_btn.pack(side="left", padx=10)
        self.quit_btn.pack(side="left", padx=10)


    # game flow helpers

    def _new_game(self):
        #new
        self.secret_word = random.choice(self.word_list)
        self.correct_set = set()
        self.wrong_set = set()
        self.wrong_cnt = 0
        self._update_ui()
        self.guess_input.config(state="normal")
        self.guess_input.delete(0, tk.END)
        self.guess_input.focus()

    def _process_guess(self):
        # check input & win/lose status
        guess = self.guess_input.get().lower()
        self.guess_input.delete(0, tk.END)

        if not guess.isalpha():
            messagebox.showinfo("Invalid", "Please type a letter (A-Z).")
            return

        if guess in self.correct_set or guess in self.wrong_set:
            messagebox.showinfo("Repeated", f"You already guessed \"{guess}\".")
            return

        if guess in self.secret_word:
            self.correct_set.add(guess)
        else:
            self.wrong_set.add(guess)
            self.wrong_cnt += 1

        self._update_ui()
        self._check_game_over()


    # refresh

    def _update_ui(self):

        self.drawing_lbl.config(text=HANGMAN_STAGES[self.wrong_cnt])
        """stickman"""
        displayed = " ".join(
            ch if ch in self.correct_set else "_" for ch in self.secret_word
        )
        self.word_lbl.config(text=displayed)

        # change color when almost lose/lose
        if self.wrong_cnt == MAX_WRONG:
            self.drawing_lbl.config(fg="red")
        elif self.wrong_cnt >= MAX_WRONG - 1:
            self.drawing_lbl.config(fg="darkred")
        else:
            self.drawing_lbl.config(fg="black")

        # display wrong letters
        if self.wrong_set:
            wrong_letters = " ".join(sorted(self.wrong_set))
            self.wrong_lbl.config(text=f"Wrong: {wrong_letters}")
        else:
            self.wrong_lbl.config(text="")


    # checks

    def _check_game_over(self):
        if self.wrong_cnt >= MAX_WRONG:
            self._end_game(False)
            return


        if all(ch in self.correct_set for ch in set(self.secret_word)):
            self._end_game(True)

    def _end_game(self, won: bool):
        self.guess_input.config(state="disabled")
        if won:
            msg = f"You guessed it! The word was \"{self.secret_word}\"."
            messagebox.showinfo("You win!", msg)
        else:

            self.drawing_lbl.config(text=HANGMAN_STAGES[-1])
            msg = f"You missed it. The word was \"{self.secret_word}\"."
            messagebox.showinfo("Game Over", msg)



# start

def main() -> None:

    script_dir = Path(__file__).parent
    words = load_word_list(script_dir / "word_list.txt")
    """for sparta!"""
    window = HangmanWindow(words)
    window.root.mainloop()


if __name__ == "__main__":
    main()

beepr

This has got to be one of the most obnoxious things I've done.

I've always had a thing for lo-fi, chip-style audio. I'd been wanting to do something of the sort for android, but I've been pretty up-to-the-neck with other stuff, so I've not had much time.

Then, just last night, as I was wrapping up another code project, I thought I might as well throw in some sort of music maker. And I tried. Boy, did I try. Working with C, though, means messing with external audio libraries or trying to work directly with pcspeaker via ioctl. Nothing worked, and I decided I hate C forever (I don't).

Good thing python simply sort of works with just about anything under the sun. So I ditched C and started over, this time using pygame, and tkinter for the gui frontend.

And it worked. I'm using the pc keyboard, but it's also drawing an on-screen keyboard so you can either clickity-click or typity-doo. The UI is nothing out of this world: just an on-screen qwerty-layout keyboard (minus controls), and a small 'screen' to display typed characters. I assigned a frequency value to each key, with 'z' being the lowest (C3), and each key after (in row order) being exactly one semitone higher, so that the entire pc keyboard covers about 3 octaves.

I tried and failed many times to generate the notes via code, so the dumbed down solution was to generate individual .wav files and have each key trigger an aplay command to play them, based on the key bindings assigned to each key. The result is an obnoxious little keyboard to annoy the crap out of anyone within hearing range.

The reason I call it obnoxious is that it's ridiculously loud, and lowering the volume does nothing to ameliorate it, but I guess that's kinda part of the charm. It's not meant to be pretty. It's supposed to be lo-fi and chip-tune-like, and forcing the laptop speakers distorts the sound a little bit, so it comes off as a sort of square wave synth sound (ish).

I won't add any pictures because the UI is really just an on-screen keyboard and is like zero-percent descriptive of what the thing does.

You'll just have to trust me.

Code:

Project Page on GitHub:

#!/usr/bin/env python3

"""
beepr v0.1 - a keyboard based synthesizer made just for fun.

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.
"""


import sys
import math
import time
import threading
import os
import tkinter as tk
from tkinter import ttk
import numpy as np
import wave
import subprocess
import tempfile
import shutil

# Configuration
SAMPLE_RATE = 44100
BASE_FREQ = 130.81  # C3
KEYS_LAYOUT = [
    "1234567890",
    "qwertyuiop",
    "asdfghjkl",
    "zxcvbnm,./"
]


# z-row (index 3) is lowest, 1-row (index 0) is highest
ALL_KEYS = KEYS_LAYOUT[3] + KEYS_LAYOUT[2] + KEYS_LAYOUT[1] + KEYS_LAYOUT[0]

class BeeprApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Beepr")
        # Set initial window size: Width x Height
        self.root.geometry("410x230")
        self.root.resizable(False, False)

        # Temp directory for wav files
        self.temp_dir = tempfile.mkdtemp(prefix="beepr_")
        self.sounds = {}
        self.generate_sounds()

        self.setup_ui()
        self.setup_bindings()

        # Pipe support
        if not sys.stdin.isatty():
            self.pipe_thread = threading.Thread(target=self.read_pipe, daemon=True)
            self.pipe_thread.start()

        self.root.protocol("WM_DELETE_WINDOW", self.on_exit)

    def generate_sounds(self):
        #Pre-generate WAV files for each key.
        duration = 0.2 #I set this to 0.2 to keep the notes short.  at 0.5 it's pretty muddy
        t = np.linspace(0, duration, int(SAMPLE_RATE * duration), False)
        
        fade_duration = int(SAMPLE_RATE * 0.05)
        fade_env = np.ones_like(t)
        fade_env[:fade_duration] = np.linspace(0, 1, fade_duration)
        fade_env[-fade_duration:] = np.linspace(1, 0, fade_duration)
        
        for i, char in enumerate(ALL_KEYS):
            freq = BASE_FREQ * (2 ** (i / 12.0))
            """tried to reduce volume by multiplying amplitude by 0.4
            (sounds just as obnoxiously loud to me anyway)"""
            tone = np.sin(2 * np.pi * freq * t) * fade_env * 0.4
            audio = (tone * 32767).astype(np.int16)
            
            wav_path = os.path.join(self.temp_dir, f"note_{i}.wav")
            with wave.open(wav_path, 'wb') as wf:
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(SAMPLE_RATE)
                wf.writeframes(audio.tobytes())
            
            self.sounds[char] = wav_path

    def setup_ui(self):
        self.display_var = tk.StringVar()
        self.display = ttk.Entry(self.root, textvariable=self.display_var, font=("Monospace", 14), state='readonly')
        self.display.pack(fill=tk.X, padx=10, pady=10)

        self.kb_frame = ttk.Frame(self.root)
        self.kb_frame.pack(padx=10, pady=10)

        self.buttons = {}
        for row_idx, row_chars in enumerate(KEYS_LAYOUT):
            row_frame = ttk.Frame(self.kb_frame)
            row_frame.pack(side=tk.TOP, anchor=tk.W)
            
            if row_idx == 1:
                ttk.Label(row_frame, text="  ").pack(side=tk.LEFT)
            elif row_idx == 2:
                ttk.Label(row_frame, text="    ").pack(side=tk.LEFT)
            elif row_idx == 3:
                ttk.Label(row_frame, text="      ").pack(side=tk.LEFT)

            for char in row_chars:
                btn = ttk.Button(row_frame, text=char, width=3,
                               command=lambda c=char: self.play_key(c))
                btn.pack(side=tk.LEFT, padx=1)
                self.buttons[char] = btn

    def setup_bindings(self):
        self.root.bind("<Key>", self.on_key_press)

    def on_key_press(self, event):
        char = event.char.lower()
        if char in self.sounds:
            self.play_key(char)

    def play_key(self, char):
        if char in self.sounds:
            #Use aplay directly cuz nothing else worked (or maybe I just suck at this)
            subprocess.Popen(["aplay", "-q", self.sounds[char]], 
                             stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

        current_text = self.display_var.get()
        if len(current_text) > 40:
            current_text = current_text[1:]
        self.display_var.set(current_text + char)
        
        if char in self.buttons:
            btn = self.buttons[char]
            btn.state(['pressed'])
            self.root.after(100, lambda: btn.state(['!pressed']))

    def read_pipe(self):
        while True:
            line = sys.stdin.readline()
            if not line:
                break
            for char in line:
                c = char.lower()
                if c in self.sounds:
                    self.root.after(0, lambda x=c: self.play_key(x))
                    time.sleep(0.15)
                elif c == ' ':
                    time.sleep(0.15)
            time.sleep(0.1)

    def on_exit(self):
        shutil.rmtree(self.temp_dir)
        self.root.destroy()

def main():
    root = tk.Tk()
    app = BeeprApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()

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