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.
|
||||
"""
|
||||
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue