Created
March 31, 2026 07:26
-
-
Save rsbohn/57f5f6a2d0b0669b70caaf83f1b22730 to your computer and use it in GitHub Desktop.
Tiny guy vs birds terminal animation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import curses | |
| import time | |
| # Simple terminal animation: a tiny guy slides in, hops, then battles birds. | |
| # Press 'q' to quit early. | |
| TINY_GUY_STAND = [ | |
| " o ", | |
| "/|\\", | |
| "/ \\", | |
| ] | |
| TINY_GUY_HOP = [ | |
| " o ", | |
| "/|\\", | |
| " ^ ", | |
| ] | |
| FRAME_DELAY = 0.08 | |
| HOP_DELAY = 0.12 | |
| SPARK_DELAY = 0.16 | |
| SPARK_STEPS = 4 | |
| SPARK_SHOTS = [ | |
| (0, -1), | |
| (1, -1), | |
| (-1, -1), | |
| (2, 0), | |
| (-2, 0), | |
| (1, -2), | |
| (-1, -2), | |
| (2, -1), | |
| (-2, -1), | |
| ] | |
| SPARK_SPAWN_INTERVAL = 2 | |
| BATTLE_DELAY = 0.12 | |
| BIRD_SPRITE = "<\\>" | |
| BIRD_SPAWN_INTERVAL = 6 | |
| BIRD_MOVE_INTERVAL = 2 | |
| BIRDS_PER_WAVE = 10 | |
| TOTAL_WAVES = 5 | |
| CREDIT_DELAY = 0.12 | |
| CREDITS = [ | |
| "Tiny Guy vs Birds", | |
| " ", | |
| "Starring: Tiny Guy", | |
| "Bird Squadron", | |
| "Spark Effects Team", | |
| "Thanks for watching!", | |
| ] | |
| def draw_guy(stdscr, rows, cols, x, y, pose): | |
| for i, line in enumerate(pose): | |
| row = y + i | |
| if 0 <= row < rows: | |
| stdscr.addstr(row, max(0, x), line[: max(0, cols - x)]) | |
| def draw_spark(stdscr, rows, cols, x, y, dx, dy, step): | |
| origin_x = x + 1 | |
| origin_y = y - 2 | |
| spark_x = origin_x + dx * step | |
| spark_y = origin_y + dy * step | |
| if 0 <= spark_y < rows and 0 <= spark_x < cols: | |
| stdscr.addstr(spark_y, spark_x, "*" if step % 2 == 0 else ".") | |
| def draw_bird(stdscr, rows, cols, x, y): | |
| if 0 <= y < rows: | |
| if x < cols and x + len(BIRD_SPRITE) > 0: | |
| start = max(0, x) | |
| slice_start = start - x | |
| stdscr.addstr(y, start, BIRD_SPRITE[slice_start: max(0, cols - start)]) | |
| def draw_wave_countdown(stdscr, rows, cols, wave, total_waves): | |
| row = rows - 6 | |
| if 0 <= row < rows: | |
| message = f"Waves left: {max(0, total_waves - wave)}" | |
| stdscr.addstr(row, 0, message[: max(0, cols)]) | |
| def draw_credits(stdscr, rows, cols, offset): | |
| start_col = (cols * 2) // 3 | |
| if start_col >= cols: | |
| return | |
| for index, line in enumerate(CREDITS): | |
| row = rows - offset + index | |
| if 0 <= row < rows: | |
| trimmed = line[: max(0, cols - start_col)] | |
| stdscr.addstr(row, start_col, trimmed) | |
| def run_animation(stdscr): | |
| curses.curs_set(0) | |
| stdscr.nodelay(True) | |
| stdscr.clear() | |
| rows, cols = stdscr.getmaxyx() | |
| guy_width = max(len(line) for line in TINY_GUY_STAND) | |
| guy_height = len(TINY_GUY_STAND) | |
| ground_row = min(rows - guy_height - 1, rows - guy_height) | |
| if ground_row < 0: | |
| return | |
| start_x = -guy_width | |
| end_x = max(0, min(cols - guy_width, cols // 3)) | |
| x = start_x | |
| y = ground_row | |
| # Slide in. | |
| while x < end_x: | |
| if stdscr.getch() == ord("q"): | |
| return | |
| stdscr.erase() | |
| draw_guy(stdscr, rows, cols, x, y, TINY_GUY_STAND) | |
| stdscr.refresh() | |
| time.sleep(FRAME_DELAY) | |
| x += 1 | |
| # Hop: up, down, then stand. | |
| hop_positions = [y - 1, y - 2, y - 1, y] | |
| for hop_y in hop_positions: | |
| if stdscr.getch() == ord("q"): | |
| return | |
| stdscr.erase() | |
| pose = TINY_GUY_HOP if hop_y < y else TINY_GUY_STAND | |
| draw_guy(stdscr, rows, cols, x, hop_y, pose) | |
| stdscr.refresh() | |
| time.sleep(HOP_DELAY) | |
| # Battle loop: sparks vs birds until quit. | |
| bird_y_positions = [max(0, y - 4), max(0, y - 5)] | |
| birds = [] | |
| sparks = [] | |
| shot_index = 0 | |
| frame = 0 | |
| wave = 0 | |
| birds_spawned = 0 | |
| while wave < TOTAL_WAVES: | |
| if stdscr.getch() == ord("q"): | |
| return | |
| stdscr.erase() | |
| draw_guy(stdscr, rows, cols, x, y, TINY_GUY_STAND) | |
| draw_wave_countdown(stdscr, rows, cols, wave, TOTAL_WAVES) | |
| if frame % SPARK_SPAWN_INTERVAL == 0: | |
| dx, dy = SPARK_SHOTS[shot_index] | |
| shot_index = (shot_index + 1) % len(SPARK_SHOTS) | |
| sparks.append({"dx": dx, "dy": dy, "step": 1}) | |
| for spark in sparks[:]: | |
| draw_spark(stdscr, rows, cols, x, y, spark["dx"], spark["dy"], spark["step"]) | |
| spark["step"] += 1 | |
| if spark["step"] > SPARK_STEPS: | |
| sparks.remove(spark) | |
| if birds_spawned < BIRDS_PER_WAVE and frame % BIRD_SPAWN_INTERVAL == 0: | |
| bird_row = bird_y_positions[birds_spawned % len(bird_y_positions)] | |
| birds.append({"x": cols, "y": bird_row}) | |
| birds_spawned += 1 | |
| for bird in birds[:]: | |
| draw_bird(stdscr, rows, cols, bird["x"], bird["y"]) | |
| if frame % BIRD_MOVE_INTERVAL == 0: | |
| bird["x"] -= 1 | |
| if bird["x"] + len(BIRD_SPRITE) < 0: | |
| birds.remove(bird) | |
| if birds_spawned >= BIRDS_PER_WAVE and not birds: | |
| wave += 1 | |
| birds_spawned = 0 | |
| stdscr.refresh() | |
| time.sleep(BATTLE_DELAY) | |
| frame += 1 | |
| # Roll credits after the last wave. | |
| credits_offset = 0 | |
| while credits_offset < rows + len(CREDITS): | |
| if stdscr.getch() == ord("q"): | |
| return | |
| stdscr.erase() | |
| draw_guy(stdscr, rows, cols, x, y, TINY_GUY_STAND) | |
| draw_wave_countdown(stdscr, rows, cols, TOTAL_WAVES, TOTAL_WAVES) | |
| draw_credits(stdscr, rows, cols, credits_offset) | |
| stdscr.refresh() | |
| time.sleep(CREDIT_DELAY) | |
| credits_offset += 1 | |
| # After credits, hold until quit. | |
| while True: | |
| if stdscr.getch() == ord("q"): | |
| return | |
| stdscr.erase() | |
| draw_guy(stdscr, rows, cols, x, y, TINY_GUY_STAND) | |
| draw_wave_countdown(stdscr, rows, cols, TOTAL_WAVES, TOTAL_WAVES) | |
| draw_credits(stdscr, rows, cols, rows) | |
| stdscr.refresh() | |
| time.sleep(FRAME_DELAY) | |
| def main(): | |
| curses.wrapper(run_animation) | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment