Skip to content

Instantly share code, notes, and snippets.

@rsbohn
Created March 31, 2026 07:26
Show Gist options
  • Select an option

  • Save rsbohn/57f5f6a2d0b0669b70caaf83f1b22730 to your computer and use it in GitHub Desktop.

Select an option

Save rsbohn/57f5f6a2d0b0669b70caaf83f1b22730 to your computer and use it in GitHub Desktop.
Tiny guy vs birds terminal animation
#!/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