Terminal-Screensaver: dino.py (Chrome-Dino-Runner) und pacman.py (Pac-Man-Screensaver) hinzugefuegt

This commit is contained in:
rene 2026-04-11 10:24:22 +02:00
parent 320289b38e
commit 2d64f70246
2 changed files with 994 additions and 0 deletions

461
dino.py Normal file
View file

@ -0,0 +1,461 @@
#!/usr/bin/env python3
"""dino: Chrome-style endless runner for the terminal.
SPACE / UP = jump. Press any key to start or restart. Q / Ctrl-C = quit.
No external dependencies -- pure ANSI escape codes, no Curses.
"""
import os, sys, tty, termios, fcntl, signal, time, random, shutil, argparse
from typing import List, Optional
# ── Terminal control ──────────────────────────────────────────────────────────
RESET = "\033[0m"
BOLD = "\033[1m"
HIDE_CUR = "\033[?25l"
SHOW_CUR = "\033[?25h"
ALT_ON = "\033[?1049h"
ALT_OFF = "\033[?1049l"
def _fg(r, g, b): return f"\033[38;2;{r};{g};{b}m"
def _go(r, c): return f"\033[{r+1};{c+1}H"
def term_size(): s = shutil.get_terminal_size(); return s.lines, s.columns
def _write_all(fd: int, data: bytes) -> None:
mv = memoryview(data)
off = 0
while off < len(mv):
try:
off += os.write(fd, mv[off:])
except BlockingIOError:
time.sleep(0.001)
# ── Colors ────────────────────────────────────────────────────────────────────
C_DINO = _fg(200, 100, 55) # Claude terracotta orange
C_DEAD = _fg(220, 60, 60)
C_CACTUS = _fg( 50, 170, 60)
C_BIRD = _fg( 80, 130, 220)
C_GROUND = _fg(160, 140, 90)
C_CLOUD = _fg(180, 200, 220)
C_SCORE = _fg(200, 200, 200)
C_HI = _fg(255, 220, 80)
C_TITLE = _fg(255, 255, 255)
C_SUB = _fg(150, 150, 150)
# ── Sprites ───────────────────────────────────────────────────────────────────
# Each sprite: list of strings. Spaces are transparent.
# Claude mascot (6 wide, 4 tall)
# Body: solid block, two eye-holes (spaces = transparent = dark bg)
# Legs: two stumps, alternating spread / together
DINO_RUN = [
# frame 0 legs spread
["██████",
"█ ██ █", # eyes as transparent gaps
"██████",
" █ █ "],
# frame 1 legs together
["██████",
"█ ██ █",
"██████",
" ██ "],
]
DINO_JUMP = [
"██████",
"█ ██ █",
"██████",
" ",
]
DINO_DEAD = [
"██████",
"█x██x█", # x eyes
"██████",
" ",
]
DINO_W = 6
DINO_H = 4
# Cacti (variable width, 4-5 tall)
CACTI = [
# small single (3 wide, 4 tall)
[" ^ ",
"###",
" # ",
" # "],
# tall single with left arm (4 wide, 5 tall)
[" ^ ",
"/## ",
" ## ",
" ## ",
" ## "],
# double (6 wide, 5 tall)
[" ^ ^ ",
"## ## ",
" #### ",
" ## ",
" ## "],
# triple (5 wide, 4 tall)
["^ ^ ^",
"#####",
" ### ",
" # "],
]
# Birds / pterodactyls (5 wide, 2 tall, 2 animation frames)
BIRDS = [
# frame 0: wings up
["\\-/",
" "],
# frame 1: wings level
[" v ",
"/ \\"],
]
BIRD_W = 3
BIRD_H = 2
# Clouds (8 wide, 2 tall)
CLOUD = [
" .--. ",
" / \\",
]
CLOUD_W = 8
CLOUD_H = 2
# ── Canvas ────────────────────────────────────────────────────────────────────
class Canvas:
def __init__(self, rows: int, cols: int):
self.rows, self.cols = rows, cols
self._ch: List[List[str]] = [[' '] * cols for _ in range(rows)]
self._ck: List[List[Optional[str]]] = [[None] * cols for _ in range(rows)]
def resize(self, rows: int, cols: int):
self.rows, self.cols = rows, cols
self._ch = [[' '] * cols for _ in range(rows)]
self._ck = [[None] * cols for _ in range(rows)]
def clear(self):
for r in range(self.rows):
self._ch[r] = [' '] * self.cols
self._ck[r] = [None] * self.cols
def put(self, row: int, col: int, ch: str, ck: Optional[str]):
if 0 <= row < self.rows and 0 <= col < self.cols:
self._ch[row][col] = ch
self._ck[row][col] = ck
def put_str(self, row: int, col: int, s: str, ck: Optional[str]):
for i, ch in enumerate(s):
self.put(row, col + i, ch, ck)
def put_sprite(self, y: int, x: int, lines: List[str], ck: Optional[str]):
for dy, line in enumerate(lines):
for dx, ch in enumerate(line):
if ch != ' ':
self.put(y + dy, x + dx, ch, ck)
def render(self) -> str:
col_limit = self.cols - 1
parts = ["\033[H", RESET]
sentinel = object()
last_ck: object = sentinel
for r in range(self.rows):
parts.append(_go(r, 0))
for c in range(col_limit):
ck = self._ck[r][c]
if ck is not last_ck:
parts.append(RESET if ck is None else ck)
last_ck = ck
parts.append(self._ch[r][c])
parts.append(RESET + "\033[K")
last_ck = sentinel
parts.append(RESET)
return ''.join(parts)
# ── Input ─────────────────────────────────────────────────────────────────────
class Input:
def __init__(self):
self.fd = sys.stdin.fileno()
self._old = termios.tcgetattr(self.fd)
tty.setraw(self.fd)
fl = fcntl.fcntl(self.fd, fcntl.F_GETFL)
fcntl.fcntl(self.fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
def read(self) -> bytes:
try:
return os.read(self.fd, 32)
except (BlockingIOError, OSError):
return b''
def restore(self):
termios.tcsetattr(self.fd, termios.TCSADRAIN, self._old)
# ── Obstacle ──────────────────────────────────────────────────────────────────
GRAVITY = 0.55 # acceleration (chars/tick²) — tuned for ~20 fps
JUMP_VEL = -3.0 # initial upward velocity (negative = up)
DINO_COL = 6 # dino's fixed horizontal position
class Obstacle:
def __init__(self, x: float, ground_row: int, rows: int):
self.x = x
if random.random() < 0.25:
# Bird flies at a random height
self.kind = 'bird'
self.sprite = BIRDS
self.w = BIRD_W
self.h = BIRD_H
height = random.choice([1, 2, 4])
self.row = ground_row - DINO_H + 1 - height
self.color = C_BIRD
else:
# Cactus
self.kind = 'cactus'
idx = random.randint(0, len(CACTI) - 1)
sp = CACTI[idx]
self.sprite = [sp] # wrap in list so index works like birds
self.w = max(len(l) for l in sp)
self.h = len(sp)
self.row = ground_row - self.h + 1
self.color = C_CACTUS
self._frame = 0.0
def current_frame(self) -> List[str]:
if self.kind == 'bird':
return self.sprite[int(self._frame) % len(self.sprite)]
return self.sprite[0]
def update(self, speed: float):
self.x -= speed
fadv = 0.12 if self.kind == 'bird' else 0
self._frame = (self._frame + fadv) % max(len(self.sprite), 1)
# ── Game ──────────────────────────────────────────────────────────────────────
class DinoGame:
def __init__(self):
rows, cols = term_size()
self.rows, self.cols = rows, cols
self.canvas = Canvas(rows, cols)
self.highscore = 0.0
self.state = 'title' # 'title' | 'running' | 'dead'
self._setup()
def _setup(self):
rows, cols = self.rows, self.cols
self.ground_row = rows - 3
self.dino_y = float(self.ground_row - DINO_H + 1)
self.dino_vy = 0.0
self.on_ground = True
self.score = 0.0
self.speed = 0.35
self.obstacles: List[Obstacle] = []
self.clouds: List[List[float]] = [] # [x, y]
self._frame = 0.0
self._next_obs = random.randint(30, 55)
self._death_anim = 0 # frames of death flash
# Seed some clouds
for _ in range(3):
cx = float(random.randint(10, cols - CLOUD_W - 2))
cy = float(random.randint(1, max(2, self.ground_row - DINO_H - 5)))
self.clouds.append([cx, cy])
def resize(self, rows: int, cols: int):
self.rows, self.cols = rows, cols
self.canvas.resize(rows, cols)
was_running = self.state == 'running'
self._setup()
if was_running:
self.state = 'running'
def jump(self):
if self.on_ground and self.state == 'running':
self.dino_vy = JUMP_VEL
self.on_ground = False
def step(self):
if self.state != 'running':
return
# Score & speed ramp
self.score += 0.12
self.speed = 0.35 + self.score * 0.00035
# Dino physics
if not self.on_ground:
self.dino_vy += GRAVITY * 0.28
self.dino_y += self.dino_vy * 0.28
floor = float(self.ground_row - DINO_H + 1)
if self.dino_y >= floor:
self.dino_y = floor
self.dino_vy = 0.0
self.on_ground = True
# Leg animation (only while running on ground)
if self.on_ground:
self._frame = (self._frame + 0.18) % 2
# Spawn obstacles
self._next_obs -= 1
if self._next_obs <= 0:
self.obstacles.append(
Obstacle(float(self.cols - 2), self.ground_row, self.rows))
gap = max(18, int(50 - self.score / 60))
self._next_obs = random.randint(gap, gap + 22)
# Move obstacles
for obs in self.obstacles:
obs.update(self.speed)
self.obstacles = [o for o in self.obstacles if o.x + o.w > 0]
# Move clouds (slow parallax)
for cl in self.clouds:
cl[0] -= 0.08
self.clouds = [c for c in self.clouds if c[0] + CLOUD_W > 0]
if len(self.clouds) < 4 and random.random() < 0.015:
cy = float(random.randint(1, max(2, self.ground_row - DINO_H - 5)))
self.clouds.append([float(self.cols - CLOUD_W - 1), cy])
# Collision detection (AABB with 1-char margin)
dx = DINO_COL
dy = int(self.dino_y)
for obs in self.obstacles:
ox = int(obs.x)
oy = obs.row
if (dx + DINO_W - 1 > ox + 1 and
dx + 1 < ox + obs.w - 1 and
dy + DINO_H - 1 > oy + 1 and
dy + 1 < oy + obs.h - 1):
self.state = 'dead'
if self.score > self.highscore:
self.highscore = self.score
def render(self) -> str:
rows, cols = self.rows, self.cols
canvas = self.canvas
canvas.clear()
gr = self.ground_row
# Ground line
ground_str = ('_.-' * ((cols // 3) + 2))[:cols - 1]
canvas.put_str(gr, 0, ground_str, C_GROUND)
# Clouds
for (cx, cy) in self.clouds:
canvas.put_sprite(int(cy), int(cx), CLOUD, C_CLOUD)
# ── Title screen ──────────────────────────────────────────────────────
if self.state == 'title':
# Draw a static dino on the ground
canvas.put_sprite(gr - DINO_H, cols // 2 - 2, DINO_RUN[0], C_DINO)
title = "DINO RUNNER"
sub = "SPACE / UP to start Q to quit"
canvas.put_str(rows // 2 - 2, (cols - len(title)) // 2, title, C_TITLE)
canvas.put_str(rows // 2, (cols - len(sub)) // 2, sub, C_SUB)
# ── Running / Dead ────────────────────────────────────────────────────
else:
# Obstacles
for obs in self.obstacles:
canvas.put_sprite(obs.row, int(obs.x), obs.current_frame(), obs.color)
# Dino
if self.state == 'dead':
dino_sp = DINO_DEAD
dino_c = C_DEAD
elif not self.on_ground:
dino_sp = DINO_JUMP
dino_c = C_DINO
else:
dino_sp = DINO_RUN[int(self._frame) % 2]
dino_c = C_DINO
canvas.put_sprite(int(self.dino_y), DINO_COL, dino_sp, dino_c)
# Score row
score_str = f"SCORE {int(self.score):05d}"
hi_str = f"BEST {int(self.highscore):05d}"
canvas.put_str(0, cols - len(score_str) - 2, score_str, C_SCORE)
canvas.put_str(0, cols - len(score_str) - len(hi_str) - 4,
hi_str, C_HI)
if self.state == 'dead':
msg = "GAME OVER"
sub = "SPACE to restart Q to quit"
canvas.put_str(rows // 2 - 1,
(cols - len(msg)) // 2, msg, C_DEAD)
canvas.put_str(rows // 2 + 1,
(cols - len(sub)) // 2, sub, C_SUB)
return canvas.render()
# ── Entry point ───────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description='Terminal dino runner game')
ap.add_argument('--speed', type=float, default=1.0,
help='Starting speed multiplier (default 1.0)')
args = ap.parse_args()
TICK = 0.05 # 20 fps
fd_out = sys.stdout.fileno()
inp = Input()
game = DinoGame()
game.speed *= args.speed
resize_flag = False
# Key bytes that mean "jump / start / restart"
ACTION_KEYS = {b' ', b'\r', b'\x1b[A', b'\x1b[B'} # space, enter, up, down
def on_resize(sig, frame):
nonlocal resize_flag
resize_flag = True
signal.signal(signal.SIGWINCH, on_resize)
_write_all(fd_out, (ALT_ON + HIDE_CUR).encode())
try:
while True:
t0 = time.monotonic()
if resize_flag:
resize_flag = False
r, c = term_size()
game.resize(r, c)
raw = inp.read()
if raw:
# Quit on Q, q, Ctrl-C, Escape
if any(b in raw for b in (b'q', b'Q', b'\x03', b'\x1b\x1b')):
break
# Action: start / jump / restart
acted = (b' ' in raw or b'\r' in raw
or b'\x1b[A' in raw or b'\x00' in raw)
# Arrow up is ESC [ A (3 bytes); detect as subsequence
if b'\x1b' in raw and b'A' in raw:
acted = True
if acted:
if game.state == 'title':
game.state = 'running'
elif game.state == 'dead':
game._setup()
game.state = 'running'
else:
game.jump()
game.step()
_write_all(fd_out, game.render().encode())
elapsed = time.monotonic() - t0
rem = TICK - elapsed
if rem > 0:
time.sleep(rem)
finally:
_write_all(fd_out, (ALT_OFF + SHOW_CUR + RESET).encode())
inp.restore()
if game.score > 0:
print(f"\nScore: {int(game.score)} Best: {int(game.highscore)}")
if __name__ == '__main__':
main()