Neue Features: --bloody, --vegan, --no-shark, --no-ship, --speed, --any-key, Seepferdchen, Tintenfisch, Krabbe am Schlosstor

- 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
This commit is contained in:
rene 2026-03-29 09:50:11 +02:00
parent 7319d26129
commit 72beb622bf

View file

@ -3,9 +3,22 @@
Requires no external dependencies pure ANSI escape codes instead of Curses. 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 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 ──────────────────────────────────────────────────────── # ─── Terminal control ────────────────────────────────────────────────────────
RESET = "\033[0m" RESET = "\033[0m"
@ -249,20 +262,59 @@ class HookEntity(Entity):
self.alive = False 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 ──────────────────────────────────────────────────────────────── # ─── Aquarium ────────────────────────────────────────────────────────────────
class 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.rows = rows
self.cols = cols self.cols = cols
self.cfg = cfg or Config()
self.canvas = Canvas(rows, cols) self.canvas = Canvas(rows, cols)
self.entities: List[Entity] = [] self.entities: List[Entity] = []
self._random_fns = [
self.add_ship, self.add_whale, self.add_monster, fns = [self.add_whale, self.add_monster, self.add_swan,
self.add_big_fish, self.add_shark, self.add_fishhook, self.add_ducks, self.add_dolphins,
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() self._setup()
def _setup(self): def _setup(self):
@ -276,6 +328,8 @@ class Aquarium:
def resize(self, rows: int, cols: int): def resize(self, rows: int, cols: int):
self.rows, self.cols = rows, cols self.rows, self.cols = rows, cols
self.canvas.resize(rows, cols) self.canvas.resize(rows, cols)
self._cs_state = 0
self._cs_portcullis = None
self._setup() self._setup()
def _add(self, e: Entity): def _add(self, e: Entity):
@ -285,7 +339,10 @@ class Aquarium:
random.choice(self._random_fns)(dead) random.choice(self._random_fns)(dead)
def step(self): def step(self):
if self.cfg.bloody:
self._check_predation()
self.entities = [e for e in self.entities if e.update(self)] self.entities = [e for e in self.entities if e.update(self)]
self._update_crab_scene()
def render(self) -> str: def render(self) -> str:
self.canvas.clear() self.canvas.clear()
@ -623,6 +680,7 @@ class Aquarium:
default_color='c', default_color='c',
die_offscreen=True, die_offscreen=True,
death_cb=on_death, death_cb=on_death,
entity_type='fish',
)) ))
# ── Splat ──────────────────────────────────────────────────────────────── # ── Splat ────────────────────────────────────────────────────────────────
@ -718,6 +776,7 @@ class Aquarium:
default_color='C', default_color='C',
die_offscreen=True, die_offscreen=True,
death_cb=lambda e, aq: aq.random_object(e), death_cb=lambda e, aq: aq.random_object(e),
entity_type='shark',
)) ))
# ── Ship ───────────────────────────────────────────────────────────────── # ── Ship ─────────────────────────────────────────────────────────────────
@ -1199,6 +1258,182 @@ yy
) )
self._add(e) 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 ─────────────────────────────────────────────────────────── # ─── Input handler ───────────────────────────────────────────────────────────
@ -1223,8 +1458,34 @@ class Input:
# ─── Main loop ─────────────────────────────────────────────────────────────── # ─── Main loop ───────────────────────────────────────────────────────────────
def main(): 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() rows, cols = term_size()
aq = Aquarium(rows, cols) aq = Aquarium(rows, cols, cfg)
inp = Input() inp = Input()
paused = False paused = False
needs_reset = False needs_reset = False
@ -1239,8 +1500,7 @@ def main():
os.write(fd, (ALT_ON + HIDE_CUR + "\033[2J\033[H").encode()) os.write(fd, (ALT_ON + HIDE_CUR + "\033[2J\033[H").encode())
try: try:
# Match original: halfdelay(1) = 0.1s tick ≈ 10 fps TICK = 0.1 / cfg.speed
TICK = 0.1
while True: while True:
t0 = time.monotonic() t0 = time.monotonic()
@ -1250,14 +1510,16 @@ def main():
needs_reset = False needs_reset = False
key = inp.key() key = inp.key()
if key in ('q', 'Q', '\x03', '\x1b'): if key is not None:
break if cfg.any_key:
elif key == 'p': break
paused = not paused if key in ('q', 'Q', '\x03', '\x1b'):
elif key in ('r', 'R'): break
# Full redraw (like KEY_RESIZE in original) elif key == 'p':
rows, cols = term_size() paused = not paused
aq.resize(rows, cols) elif key in ('r', 'R'):
rows, cols = term_size()
aq.resize(rows, cols)
if not paused: if not paused:
aq.step() aq.step()