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

533
pacman.py Normal file
View file

@ -0,0 +1,533 @@
#!/usr/bin/env python3
"""pacman: Pac-Man ASCII screensaver with maze. Press any key to exit.
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, Set, Tuple
# ── Terminal control ──────────────────────────────────────────────────────────
RESET = "\033[0m"
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_WALL = _fg( 30, 60, 255) # classic Pac-Man blue
C_PAC = _fg(255, 255, 0) # yellow
C_DOT = _fg(255, 220, 180) # cream
C_PILL = _fg(255, 220, 180)
C_SCORE = _fg(180, 180, 255)
C_SCARED = _fg( 20, 20, 200)
C_SCARE2 = _fg(200, 200, 255) # flashing near end
GHOST_COLORS = [
_fg(255, 40, 40), # Blinky red
_fg(255, 170, 200), # Pinky pink
_fg( 30, 210, 255), # Inky cyan
_fg(255, 175, 80), # Clyde orange
]
# ── Maze definition ───────────────────────────────────────────────────────────
# 28 cols × 14 rows. # = wall, . = dot, * = power pellet
MAZE = [
"############################",
"#............##............#",
"#.####.#####.##.#####.####.#",
"#*####.#####.##.#####.####*#",
"#.####.#####.##.#####.####.#",
"#..........................#",
"#.####.##.########.##.####.#",
"#......##....##....##......#",
"#.####.##.########.##.####.#",
"#..........................#",
"#.####.#####.##.#####.####.#",
"#*####.#####.##.#####.####*#",
"#............##............#",
"############################",
]
MAZE_H = len(MAZE) # 14
MAZE_W = len(MAZE[0]) # 28
# Box-drawing lookup: (N, S, W, E) → character
# Each wall cell gets the char that shows which sides connect to other walls.
_BOX: dict = {
(False,False,False,False): '·',
(True, False,False,False): '',
(False,True, False,False): '',
(True, True, False,False): '',
(False,False,True, False): '',
(False,False,False,True ): '',
(False,False,True, True ): '',
(True, False,True, False): '',
(True, False,False,True ): '',
(False,True, True, False): '',
(False,True, False,True ): '',
(True, True, True, False): '',
(True, True, False,True ): '',
(True, False,True, True ): '',
(False,True, True, True ): '',
(True, True, True, True ): '',
}
# ── Movement helpers ──────────────────────────────────────────────────────────
DIRS = ['R', 'L', 'U', 'D']
DELTA = {'R': (0, 1), 'L': (0, -1), 'U': (-1, 0), 'D': (1, 0)}
def is_wall(mr: int, mc: int) -> bool:
if 0 <= mr < MAZE_H and 0 <= mc < MAZE_W:
return MAZE[mr][mc] == '#'
return True
def valid_dirs(mr: int, mc: int, exclude: Optional[str] = None) -> List[str]:
return [d for d in DIRS
if d != exclude
and not is_wall(mr + DELTA[d][0], mc + DELTA[d][1])]
def try_move(mr: int, mc: int, d: str) -> Tuple[int, int, bool]:
dr, dc = DELTA[d]
nmr, nmc = mr + dr, mc + dc
if is_wall(nmr, nmc):
return mr, mc, False
return nmr, nmc, True
# ── Sprite generators ─────────────────────────────────────────────────────────
def make_pac(sw: int, sh: int, direction: str, frame: int) -> List[str]:
"""Pac-Man sprite sw×sh using /\\ for the mouth wedge.
frame 0/1 = open, frame 2 = closed (full block).
At sw=4, sh=4 (user's design):
RIGHT open LEFT open UP open DOWN open
#### #### #/\\# ####
# \\ / # #### ####
# / \\ # #### #\\/#
#### #### #### ####
"""
W = sw
mid = max(0, sw - 2)
arc = '#' * W
opn = (frame <= 1)
def right() -> List[str]:
rows = [arc] * sh
if opn:
if sh == 1:
rows[0] = '#' * (W - 1) + '>'
else:
hi = max(0, sh // 3)
lo = min(sh - 1, 2 * sh // 3)
if hi == lo and sh > 1:
lo = min(hi + 1, sh - 1)
rows[hi] = '#' + ' ' * mid + '\\'
rows[lo] = '#' + ' ' * mid + '/'
return rows
def left() -> List[str]:
rows = [arc] * sh
if opn:
if sh == 1:
rows[0] = '<' + '#' * (W - 1)
else:
hi = max(0, sh // 3)
lo = min(sh - 1, 2 * sh // 3)
if hi == lo and sh > 1:
lo = min(hi + 1, sh - 1)
rows[hi] = '/' + ' ' * mid + '#'
rows[lo] = '\\' + ' ' * mid + '#'
return rows
def up() -> List[str]:
rows = [arc] * sh
if opn:
mc = W // 2
r = list(arc)
if mc > 0: r[mc - 1] = '/'
if mc < W: r[mc] = '\\'
for i in range(mc - 1): r[i] = ' '
for i in range(mc + 1, W): r[i] = ' '
rows[0] = ''.join(r)
return rows
def down() -> List[str]:
rows = [arc] * sh
if opn:
mc = W // 2
r = list(arc)
if mc > 0: r[mc - 1] = '\\'
if mc < W: r[mc] = '/'
for i in range(mc - 1): r[i] = ' '
for i in range(mc + 1, W): r[i] = ' '
rows[-1] = ''.join(r)
return rows
return {'R': right, 'L': left, 'U': up, 'D': down}[direction]()
def make_ghost(sw: int, sh: int, scared: bool, frame: int) -> List[str]:
"""Ghost sprite sw×sh chars."""
mid = sw - 2
if sh == 1 or mid < 0:
return [('=' if scared else '&') * max(1, sw)]
eye_str = ('= =' if mid >= 5 else '==' if mid >= 2 else '=') if scared else \
('o o' if mid >= 5 else 'oo' if mid >= 2 else 'o')
eyes = eye_str.center(mid)[:mid]
top = '/' + '-' * mid + '\\'
body = '|' + eyes + '|'
pat = '/\\' if frame == 0 else '\\/'
skirt = (pat * (sw // 2 + 2))[:sw]
if sh == 2:
return [top, body]
else:
return [top, body, skirt]
# ── 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 fill_rect(self, row: int, col: int, h: int, w: int, ch: str, ck: Optional[str]):
for dr in range(h):
for dc in range(w):
self.put(row + dr, col + dc, ch, ck)
def put_sprite_solid(self, y: int, x: int, lines: List[str], ck: Optional[str]):
"""Draw sprite; interior spaces are erased (opaque body)."""
for dy, line in enumerate(lines):
vis = [i for i, c in enumerate(line) if c not in (' ', '?')]
if not vis:
continue
lo, hi = vis[0], vis[-1]
for dx, ch in enumerate(line):
if ch in (' ', '?'):
if lo < dx < hi:
self.put(y + dy, x + dx, ' ', None)
else:
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)
# ── Entities ──────────────────────────────────────────────────────────────────
class Ghost:
def __init__(self, mr: int, mc: int, color: str):
self.mr, self.mc = mr, mc
self.color = color
self.dir = random.choice(valid_dirs(mr, mc) or ['R'])
self.frame = 0
self.scared_ticks = 0
def step(self):
nmr, nmc, ok = try_move(self.mr, self.mc, self.dir)
if ok:
self.mr, self.mc = nmr, nmc
# Turn randomly or when blocked
if not ok or random.random() < 0.07:
vd = valid_dirs(self.mr, self.mc)
if vd:
self.dir = random.choice(vd)
self.frame = (self.frame + 1) % 2
if self.scared_ticks > 0:
self.scared_ticks -= 1
class Pacman:
def __init__(self, mr: int, mc: int):
self.mr, self.mc = mr, mc
self.dir = 'R'
self.frame = 0
self._fanim = 1 # mouth open/close direction
def step(self, dots: Set[Tuple[int, int]],
pills: Set[Tuple[int, int]]) -> Tuple[int, int]:
"""Move one step. Returns (score_delta, scared_bonus)."""
self.frame += self._fanim
if self.frame >= 2:
self._fanim = -1
elif self.frame <= 0:
self._fanim = 1
nmr, nmc, ok = try_move(self.mr, self.mc, self.dir)
if ok:
self.mr, self.mc = nmr, nmc
if not ok or random.random() < 0.05:
vd = valid_dirs(self.mr, self.mc)
if vd:
self.dir = random.choice(vd)
score, scared = 0, 0
pos = (self.mr, self.mc)
if pos in dots:
dots.discard(pos)
score = 10
if pos in pills:
pills.discard(pos)
score = 50
scared = 150
return score, scared
# ── Game ──────────────────────────────────────────────────────────────────────
class PacmanGame:
def __init__(self):
rows, cols = term_size()
self.rows, self.cols = rows, cols
self.canvas = Canvas(rows, cols)
self.score = 0
self.level = 1
self._scale()
self._init_level()
def _scale(self):
rows, cols = self.rows, self.cols
self.sw = max(2, min(8, (cols - 4) // MAZE_W))
self.sh = max(1, min(4, (rows - 4) // MAZE_H))
self.mleft = (cols - MAZE_W * self.sw) // 2
self.mtop = (rows - MAZE_H * self.sh) // 2 + 1 # +1 for status line
def _init_level(self):
self.dots: Set[Tuple[int, int]] = set()
self.pills: Set[Tuple[int, int]] = set()
for mr in range(MAZE_H):
for mc in range(MAZE_W):
ch = MAZE[mr][mc]
if ch == '.':
self.dots.add((mr, mc))
elif ch == '*':
self.pills.add((mr, mc))
open_cells = [(mr, mc) for mr in range(MAZE_H)
for mc in range(MAZE_W) if MAZE[mr][mc] != '#']
# Pac-Man near center
cr, cc = MAZE_H // 2, MAZE_W // 2
start = min(open_cells, key=lambda p: abs(p[0] - cr) + abs(p[1] - cc))
self.pac = Pacman(start[0], start[1])
# Ghosts spread out
spots = random.sample(open_cells, min(4, len(open_cells)))
self.ghosts = [Ghost(mr, mc, GHOST_COLORS[i % 4])
for i, (mr, mc) in enumerate(spots)]
self.scared_ticks = 0
self._ptick = 0
self._gtick = 0
def resize(self, rows: int, cols: int):
self.rows, self.cols = rows, cols
self.canvas.resize(rows, cols)
self._scale()
self._init_level()
def step(self):
self._ptick += 1
if self._ptick >= 2:
self._ptick = 0
sd, sb = self.pac.step(self.dots, self.pills)
self.score += sd
if sb:
self.scared_ticks = sb
for g in self.ghosts:
g.scared_ticks = sb
if self.scared_ticks > 0:
self.scared_ticks -= 1
self._gtick += 1
if self._gtick >= 3:
self._gtick = 0
for g in self.ghosts:
g.step()
if not self.dots and not self.pills:
self.level += 1
self._init_level()
def _tpos(self, mr: int, mc: int) -> Tuple[int, int]:
return self.mtop + mr * self.sh, self.mleft + mc * self.sw
def render(self) -> str:
sw, sh = self.sw, self.sh
canvas = self.canvas
canvas.clear()
# Status line
status = f" SCORE: {self.score:>6} LEVEL: {self.level} dots: {len(self.dots)+len(self.pills)} "
canvas.put_str(0, 0, status, C_SCORE)
# Maze: walls, dots, pills
for mr in range(MAZE_H):
for mc in range(MAZE_W):
ty, tx = self._tpos(mr, mc)
cell = MAZE[mr][mc]
if cell == '#':
n = is_wall(mr - 1, mc)
s = is_wall(mr + 1, mc)
w_n = is_wall(mr, mc - 1)
e_n = is_wall(mr, mc + 1)
ne = is_wall(mr - 1, mc + 1)
nw = is_wall(mr - 1, mc - 1)
se = is_wall(mr + 1, mc + 1)
sw_d = is_wall(mr + 1, mc - 1)
top, bot = ty, ty + sh - 1
lft, rgt = tx, tx + sw - 1
# Boundary lines only where wall meets corridor
if not n:
for dx in range(sw): canvas.put(top, tx + dx, '', C_WALL)
if not s:
for dx in range(sw): canvas.put(bot, tx + dx, '', C_WALL)
if not w_n:
for dy in range(sh): canvas.put(ty + dy, lft, '', C_WALL)
if not e_n:
for dy in range(sh): canvas.put(ty + dy, rgt, '', C_WALL)
# Outer corners (convex: two boundary edges meet)
if not n and not w_n: canvas.put(top, lft, '', C_WALL)
if not n and not e_n: canvas.put(top, rgt, '', C_WALL)
if not s and not w_n: canvas.put(bot, lft, '', C_WALL)
if not s and not e_n: canvas.put(bot, rgt, '', C_WALL)
# Inner corners (concave: both direct neighbours are walls, diagonal is corridor)
if n and w_n and not nw: canvas.put(top, lft, '', C_WALL)
if n and e_n and not ne: canvas.put(top, rgt, '', C_WALL)
if s and w_n and not sw_d: canvas.put(bot, lft, '', C_WALL)
if s and e_n and not se: canvas.put(bot, rgt, '', C_WALL)
elif (mr, mc) in self.dots:
canvas.put(ty + sh // 2, tx + sw // 2, '\u2022', C_DOT)
elif (mr, mc) in self.pills:
canvas.put(ty + sh // 2, tx + sw // 2, '\u25cf', C_PILL)
# Ghosts
for g in self.ghosts:
ty, tx = self._tpos(g.mr, g.mc)
scared = g.scared_ticks > 0
color = (C_SCARE2 if (g.scared_ticks // 5) % 2 == 0
else C_SCARED) if scared else g.color
sp = make_ghost(sw, sh, scared, g.frame)
canvas.put_sprite_solid(ty, tx, sp, color)
# Pac-Man
ty, tx = self._tpos(self.pac.mr, self.pac.mc)
sp = make_pac(sw, sh, self.pac.dir, min(self.pac.frame, 2))
canvas.put_sprite_solid(ty, tx, sp, C_PAC)
return canvas.render()
# ── Entry point ───────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description='Pac-Man ASCII screensaver')
ap.add_argument('--speed', type=float, default=1.0,
help='Speed multiplier (default 1.0)')
args = ap.parse_args()
TICK = 0.08 / args.speed
fd_out = sys.stdout.fileno()
inp = Input()
game = PacmanGame()
resize_flag = False
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)
if inp.read():
break
game.step()
_write_all(fd_out, game.render().encode())
rem = TICK - (time.monotonic() - t0)
if rem > 0:
time.sleep(rem)
finally:
_write_all(fd_out, (ALT_OFF + SHOW_CUR + RESET).encode())
inp.restore()
print(f"\nFinal score: {game.score} (Level {game.level})")
if __name__ == '__main__':
main()