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:
parent
7319d26129
commit
72beb622bf
1 changed files with 280 additions and 18 deletions
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue