Terminal-Screensaver: dino.py (Chrome-Dino-Runner) und pacman.py (Pac-Man-Screensaver) hinzugefuegt
This commit is contained in:
parent
320289b38e
commit
2d64f70246
2 changed files with 994 additions and 0 deletions
461
dino.py
Normal file
461
dino.py
Normal 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()
|
||||||
533
pacman.py
Normal file
533
pacman.py
Normal 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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue