From 72beb622bf089347c29b9f456975ce5ca32d7a90 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 29 Mar 2026 09:50:11 +0200 Subject: [PATCH] Neue Features: --bloody, --vegan, --no-shark, --no-ship, --speed, --any-key, Seepferdchen, Tintenfisch, Krabbe am Schlosstor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLI-Parameter via argparse: --speed (TICK-basiert), --no-shark, --no-ship, --bloody (Hai frisst Fische mit Splat-Animation), --vegan (keine Räuber), --any-key (beliebige Taste beendet) - Config-Dataclass: wird durch Aquarium durchgereicht, steuert random_fns - CrabEntity: Krabbe läuft aus Schlosstor heraus und kehrt zurück - Portcullis-Animation: Falltor öffnet/schließt sich vor/nach der Krabbe - Neue Tiere: add_seahorse (Seepferdchen), add_squid (Tintenfisch) - entity_type für Fish und Shark für Kollisionserkennung --- asciiquarium_ng.py | 298 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 280 insertions(+), 18 deletions(-) diff --git a/asciiquarium_ng.py b/asciiquarium_ng.py index aa3edfe..f8c3df1 100755 --- a/asciiquarium_ng.py +++ b/asciiquarium_ng.py @@ -3,9 +3,22 @@ Requires no external dependencies — pure ANSI escape codes instead of Curses. """ -import os, sys, tty, termios, fcntl, signal, time, random, shutil +import os, sys, tty, termios, fcntl, signal, time, random, shutil, argparse +from dataclasses import dataclass from typing import Optional, Callable, List + +# ─── Configuration ─────────────────────────────────────────────────────────── + +@dataclass +class Config: + speed: float = 1.0 # global speed multiplier (TICK = 0.1 / speed) + no_shark: bool = False + no_ship: bool = False + bloody: bool = False # shark eats fish with gore splats + vegan: bool = False # disable predators (shark, big_fish, fishhook) + any_key: bool = False # any key exits + # ─── Terminal control ──────────────────────────────────────────────────────── RESET = "\033[0m" @@ -249,20 +262,59 @@ class HookEntity(Entity): self.alive = False +class CrabEntity(Entity): + """Crab that walks out from the castle gate and then returns.""" + + def __init__(self, *args, steps_out: int, **kwargs): + super().__init__(*args, **kwargs) + self._steps_out = steps_out + self._step_count = 0 + self._returning = False + self._start_x = self.x + self._orig_dx = self.cb_args[0] + + def _move(self): + if not self._returning: + self._step_count += 1 + if self._step_count >= self._steps_out: + self._returning = True + self.cb_args[0] = -self._orig_dx + else: + dx = self.cb_args[0] + if (dx > 0 and self.x >= self._start_x) or \ + (dx < 0 and self.x <= self._start_x): + self.alive = False + return + super()._move() + + # ─── Aquarium ──────────────────────────────────────────────────────────────── class Aquarium: - def __init__(self, rows: int, cols: int): + def __init__(self, rows: int, cols: int, cfg: Optional['Config'] = None): self.rows = rows self.cols = cols + self.cfg = cfg or Config() self.canvas = Canvas(rows, cols) self.entities: List[Entity] = [] - self._random_fns = [ - self.add_ship, self.add_whale, self.add_monster, - self.add_big_fish, self.add_shark, self.add_fishhook, - self.add_swan, self.add_ducks, self.add_dolphins, - ] + + fns = [self.add_whale, self.add_monster, self.add_swan, + self.add_ducks, self.add_dolphins, + self.add_seahorse, self.add_squid] + if not self.cfg.no_ship: + fns.append(self.add_ship) + if not self.cfg.no_shark and not self.cfg.vegan: + fns.append(self.add_shark) + if not self.cfg.vegan: + fns += [self.add_big_fish, self.add_fishhook] + self._random_fns = fns + + # Crab scene state machine + self._cs_state: int = 0 # 0=idle 1=opening 2=out 3=closing + self._cs_next: float = time.time() + random.uniform(90, 150) + self._cs_portcullis: Optional[Entity] = None + self._setup() def _setup(self): @@ -276,6 +328,8 @@ class Aquarium: def resize(self, rows: int, cols: int): self.rows, self.cols = rows, cols self.canvas.resize(rows, cols) + self._cs_state = 0 + self._cs_portcullis = None self._setup() def _add(self, e: Entity): @@ -285,7 +339,10 @@ class Aquarium: random.choice(self._random_fns)(dead) def step(self): + if self.cfg.bloody: + self._check_predation() self.entities = [e for e in self.entities if e.update(self)] + self._update_crab_scene() def render(self) -> str: self.canvas.clear() @@ -623,6 +680,7 @@ class Aquarium: default_color='c', die_offscreen=True, death_cb=on_death, + entity_type='fish', )) # ── Splat ──────────────────────────────────────────────────────────────── @@ -718,6 +776,7 @@ class Aquarium: default_color='C', die_offscreen=True, death_cb=lambda e, aq: aq.random_object(e), + entity_type='shark', )) # ── Ship ───────────────────────────────────────────────────────────────── @@ -1199,6 +1258,182 @@ yy ) self._add(e) + # ── Seahorse (Seepferdchen) ─────────────────────────────────────────────── + + def add_seahorse(self, dead=None): + frame0 = _p(r""" + } + {( + |) + | + (| + |) + _|_ +( ) +""") + frame1 = _p(r""" + } + {( + |) + | + (| + |( + _|_ +( ) +""") + mask0 = _p(""" + y + yy + y + g + gy + g + ggg +ygyyy +""") + mask1 = mask0 + going_right = random.random() < 0.5 + speed = random.uniform(0.3, 0.8) * (1 if going_right else -1) + x = -5 if going_right else self.cols - 2 + y = random.randint(9, max(10, self.rows - 10)) + self._add(Entity( + frames=[frame0, frame1], masks=[mask0, mask1], + x=x, y=y, + cb_args=[speed, 0, 0, 0.3], + default_color='y', + die_offscreen=True, + death_cb=lambda e, aq: aq.random_object(e), + )) + + # ── Squid / Octopus (Tintenfisch) ──────────────────────────────────────── + + def add_squid(self, dead=None): + frame_r0 = _p(r""" + __ + /oo\ +( ) + \ / + /\/\ +/\ /\ +""") + frame_r1 = _p(r""" + __ + /oo\ +( ) + \ / + /\/\ +\/ \/ +""") + frame_l0 = _p(r""" + __ + /oo\ +( ) + \ / + /\/\ +/\ /\ +""") + frame_l1 = frame_r1 + mask_r = _p(""" + mm + mWWm +mmmmmm + mmmm + mmmm +mmmmmm +""") + going_right = random.random() < 0.5 + speed = random.uniform(0.5, 1.5) * (1 if going_right else -1) + x = -8 if going_right else self.cols - 2 + y = random.randint(9, max(10, self.rows - 8)) + frames = [frame_r0, frame_r1] if going_right else [frame_l0, frame_l1] + self._add(Entity( + frames=frames, masks=[mask_r, mask_r], + x=x, y=y, + cb_args=[speed, 0, 0, 0.3], + default_color='m', + die_offscreen=True, + death_cb=lambda e, aq: aq.random_object(e), + )) + + # ── Predation (--bloody) ───────────────────────────────────────────────── + + def _check_predation(self): + sharks = [e for e in self.entities + if e.entity_type == 'shark' and e.alive] + fish = [e for e in self.entities + if e.entity_type == 'fish' and e.alive] + for s in sharks: + sx1 = int(s.x); sx2 = sx1 + s.width + sy1 = int(s.y); sy2 = sy1 + s.height + for f in fish: + fx = int(f.x) + f.width // 2 + fy = int(f.y) + f.height // 2 + if sx1 <= fx <= sx2 and sy1 <= fy <= sy2: + self.add_splat(fx, fy) + f.alive = False + + # ── Crab scene (portcullis + crab) ──────────────────────────────────────── + + def _gate_pos(self): + """Absolute (x, y) of the top-left of the portcullis opening.""" + return self.cols - 32 + 12, self.rows - 13 + 10 + + def _portcullis_entity(self, opening: bool) -> Entity: + gx, gy = self._gate_pos() + f_closed = ['|_|_|_|', '|#|#|#|'] + f_half = ['|_|_|_|', '| |'] + f_open = [' ', ' '] + if opening: + frames = ([f_closed] * 3 + [f_half] * 3 + [f_open] * 2) + else: + frames = ([f_open] * 2 + [f_half] * 3 + [f_closed] * 3) + return Entity( + frames=frames, masks=None, + x=gx, y=gy, + cb_args=[0, 0, 0, 1], + default_color='y', + die_offscreen=False, + die_frame=len(frames), + ) + + def _update_crab_scene(self): + st = self._cs_state + if st == 0: # idle + if time.time() >= self._cs_next: + e = self._portcullis_entity(opening=True) + self._cs_portcullis = e + self._add(e) + self._cs_state = 1 + elif st == 1: # opening + if self._cs_portcullis and not self._cs_portcullis.alive: + gx, gy = self._gate_pos() + crab_f0 = ['(°w°)', '/| |\\'] + crab_f1 = ['(°w°)', '\\| |/'] + mask = [' yWy ', ' '] + c = CrabEntity( + frames=[crab_f0, crab_f1], masks=[mask, mask], + x=float(gx), y=float(gy), + cb_args=[-1, 0, 0, 0.25], + default_color='y', + die_offscreen=False, + steps_out=14, + entity_type='crab', + ) + self._add(c) + self._cs_portcullis = None + self._cs_state = 2 + elif st == 2: # crab out + if not any(e.entity_type == 'crab' for e in self.entities): + e = self._portcullis_entity(opening=False) + self._cs_portcullis = e + self._add(e) + self._cs_state = 3 + elif st == 3: # closing + if self._cs_portcullis and not self._cs_portcullis.alive: + self._cs_portcullis = None + self._cs_state = 0 + self._cs_next = time.time() + random.uniform(120, 240) + # ─── Input handler ─────────────────────────────────────────────────────────── @@ -1223,8 +1458,34 @@ class Input: # ─── Main loop ─────────────────────────────────────────────────────────────── def main(): + parser = argparse.ArgumentParser( + prog='asciiquarium', + description='Aquarium animation in ASCII art (Python port)') + parser.add_argument('--speed', type=float, default=1.0, + metavar='N', help='global speed multiplier (default 1.0)') + parser.add_argument('--no-shark', action='store_true', + help='no sharks') + parser.add_argument('--no-ship', action='store_true', + help='no ships') + parser.add_argument('--bloody', action='store_true', + help='sharks eat fish with gory splats') + parser.add_argument('--vegan', action='store_true', + help='disable all predators (shark, big fish, fishhook)') + parser.add_argument('--any-key', action='store_true', + help='any key press exits (default: q/Q/Esc/Ctrl-C)') + args = parser.parse_args() + + cfg = Config( + speed = max(0.1, min(10.0, args.speed)), + no_shark = args.no_shark, + no_ship = args.no_ship, + bloody = args.bloody, + vegan = args.vegan, + any_key = args.any_key, + ) + rows, cols = term_size() - aq = Aquarium(rows, cols) + aq = Aquarium(rows, cols, cfg) inp = Input() paused = False needs_reset = False @@ -1239,8 +1500,7 @@ def main(): os.write(fd, (ALT_ON + HIDE_CUR + "\033[2J\033[H").encode()) try: - # Match original: halfdelay(1) = 0.1s tick ≈ 10 fps - TICK = 0.1 + TICK = 0.1 / cfg.speed while True: t0 = time.monotonic() @@ -1250,14 +1510,16 @@ def main(): needs_reset = False key = inp.key() - if key in ('q', 'Q', '\x03', '\x1b'): - break - elif key == 'p': - paused = not paused - elif key in ('r', 'R'): - # Full redraw (like KEY_RESIZE in original) - rows, cols = term_size() - aq.resize(rows, cols) + if key is not None: + if cfg.any_key: + break + if key in ('q', 'Q', '\x03', '\x1b'): + break + elif key == 'p': + paused = not paused + elif key in ('r', 'R'): + rows, cols = term_size() + aq.resize(rows, cols) if not paused: aq.step()