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.
"""
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,12 +1510,14 @@ def main():
needs_reset = False
key = inp.key()
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'):
# Full redraw (like KEY_RESIZE in original)
rows, cols = term_size()
aq.resize(rows, cols)