Alle 12 Frames normalisiert auf 8 Zeilen (3 Fontäne + 1 Separator + 4 Körper). Vorher variierten die Frame-Höhen (6–8 Zeilen), sodass der Körper zwischen Zeilen 2/3/4 sprang. Maske hatte 7 Zeilen → bei 8-Zeilen-Frames um 1 Zeile versetzt → falsche Körperfarben. - mask_r/l: leere Trennzeile zwischen Fontänen-Farben und Körper-Farben - Fontänen-Zeilen auf genau 3 aufgefüllt (oben mit Leerstrings) - Frames direkt als Listen gebaut statt über _p()-String-Konkatenation Außerdem aus vorheriger Session: - add_all_fish: Anzahl relativ zu wl (statt hardcodiert) - main: Terminalgröße erst nach ALT_ON lesen (Split-Pane-Fix) - --debug Flag
1634 lines
49 KiB
Python
Executable file
1634 lines
49 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""asciiquarium-ng: Python port of asciiquarium with true RGB colors.
|
|
Requires no external dependencies — pure ANSI escape codes instead of Curses.
|
|
"""
|
|
|
|
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"
|
|
BOLD = "\033[1m"
|
|
HIDE_CUR = "\033[?25l"
|
|
SHOW_CUR = "\033[?25h"
|
|
ALT_ON = "\033[?1049h"
|
|
ALT_OFF = "\033[?1049l"
|
|
|
|
def _fg(r, g, b): return f"\033[38;2;{r};{g};{b}m"
|
|
def _go(row, col): return f"\033[{row+1};{col+1}H"
|
|
def term_size(): s = shutil.get_terminal_size(); return s.lines, s.columns
|
|
|
|
|
|
# ─── Color palette ───────────────────────────────────────────────────────────
|
|
# RGB values matching our Homebrew init_pair fix (xterm-256 slots 16-231):
|
|
# w=231=#ffffff r=196=#ff0000 g=46=#00ff00 b=27=#005fff
|
|
# c=51=#00ffff m=201=#ff00ff y=226=#ffff00 k=238=#444444
|
|
|
|
_C = {
|
|
'w': _fg(255,255,255), 'W': BOLD + _fg(255,255,255),
|
|
'r': _fg(255,0,0), 'R': BOLD + _fg(255,0,0),
|
|
'g': _fg(0,255,0), 'G': BOLD + _fg(0,255,0),
|
|
'b': _fg(0,95,255), 'B': BOLD + _fg(0,95,255),
|
|
'c': _fg(0,255,255), 'C': BOLD + _fg(0,255,255),
|
|
'm': _fg(255,0,255), 'M': BOLD + _fg(255,0,255),
|
|
'y': _fg(255,255,0), 'Y': BOLD + _fg(255,255,0),
|
|
'k': _fg(68,68,68), 'K': BOLD + _fg(68,68,68),
|
|
}
|
|
|
|
_NAMED = {
|
|
'white':'w', 'WHITE':'W',
|
|
'cyan':'c', 'CYAN':'C',
|
|
'green':'g', 'GREEN':'G',
|
|
'blue':'b', 'BLUE':'B',
|
|
'red':'r', 'RED':'R',
|
|
'yellow':'y', 'YELLOW':'Y',
|
|
'black':'k', 'BLACK':'k',
|
|
'magenta':'m', 'MAGENTA':'M',
|
|
}
|
|
|
|
def _esc(key: Optional[str]) -> str:
|
|
if key is None: return RESET
|
|
if key in _C: return _C[key]
|
|
k2 = _NAMED.get(key)
|
|
return _C[k2] if k2 else RESET
|
|
|
|
def _rand_color(mask_lines: List[str]) -> List[str]:
|
|
"""Replace digit placeholders 1-9 in color mask with random color letters.
|
|
Digit 4 (eye) is always 'W' (white), as in the original rand_color()."""
|
|
pool = list('cCrRyYbBgGmM')
|
|
mapping = {str(i): random.choice(pool) for i in range(1, 10)}
|
|
mapping['4'] = 'W'
|
|
return [''.join(mapping.get(ch, ch) for ch in line) for line in mask_lines]
|
|
|
|
|
|
# ─── Canvas ──────────────────────────────────────────────────────────────────
|
|
|
|
class Canvas:
|
|
def __init__(self, rows: int, cols: int):
|
|
self.rows = rows
|
|
self.cols = cols
|
|
self._ch: List[List[str]] = [[' '] * cols for _ in range(rows)]
|
|
self._ck: List[List[Optional[str]]] = [[None] * cols for _ in range(rows)]
|
|
|
|
def resize(self, rows: int, cols: int):
|
|
self.rows, self.cols = rows, cols
|
|
self._ch = [[' '] * cols for _ in range(rows)]
|
|
self._ck = [[None] * cols for _ in range(rows)]
|
|
|
|
def clear(self):
|
|
for r in range(self.rows):
|
|
self._ch[r] = [' '] * self.cols
|
|
self._ck[r] = [None] * self.cols
|
|
|
|
def put(self, row: int, col: int, ch: str, ck: Optional[str]):
|
|
if 0 <= row < self.rows and 0 <= col < self.cols:
|
|
self._ch[row][col] = ch
|
|
self._ck[row][col] = ck
|
|
|
|
def put_sprite(self, y: int, x: int, shape: List[str],
|
|
mask: Optional[List[str]], default_color: Optional[str]):
|
|
"""Draw a sprite. Spaces and '?' characters are transparent."""
|
|
rows, cols = self.rows, self.cols
|
|
for dr, line in enumerate(shape):
|
|
row = y + dr
|
|
if row < 0 or row >= rows:
|
|
continue
|
|
mline = (mask[dr] if dr < len(mask) else '') if mask else ''
|
|
for dc, ch in enumerate(line):
|
|
if ch in (' ', '?'):
|
|
continue
|
|
col = x + dc
|
|
if 0 <= col < cols:
|
|
mk = mline[dc] if dc < len(mline) else ' '
|
|
ck = mk if mk in _C else default_color
|
|
self._ch[row][col] = ch
|
|
self._ck[row][col] = ck
|
|
|
|
def render(self) -> str:
|
|
# \033[H homes the cursor; every cell is rewritten below so \033[2J is
|
|
# not needed and would push content into the scrollback on every frame.
|
|
# The very last cell (bottom-right corner) is intentionally skipped:
|
|
# writing to it would trigger auto-wrap and scroll the terminal.
|
|
last_row = self.rows - 1
|
|
last_col = self.cols - 1
|
|
parts = ["\033[H", RESET]
|
|
sentinel = object()
|
|
last_ck: object = sentinel
|
|
for r in range(self.rows):
|
|
parts.append(_go(r, 0))
|
|
col_limit = last_col if r == last_row else self.cols
|
|
for c in range(col_limit):
|
|
ck = self._ck[r][c]
|
|
if ck is not last_ck:
|
|
parts.append(RESET if ck is None else _esc(ck))
|
|
last_ck = ck
|
|
parts.append(self._ch[r][c])
|
|
parts.append(RESET)
|
|
return ''.join(parts)
|
|
|
|
|
|
# ─── Sprite helpers ──────────────────────────────────────────────────────────
|
|
|
|
def _p(s: str) -> List[str]:
|
|
"""Parse a Perl q{} heredoc: strip the mandatory leading blank line."""
|
|
lines = s.split('\n')
|
|
if lines and lines[0] == '':
|
|
lines = lines[1:]
|
|
if lines and lines[-1] == '':
|
|
lines = lines[:-1]
|
|
return lines
|
|
|
|
|
|
# ─── Entity ──────────────────────────────────────────────────────────────────
|
|
|
|
class Entity:
|
|
"""Animated, moving sprite, faithful to Term::Animation::Entity semantics.
|
|
|
|
callback_args matches the Perl convention:
|
|
[dx, dy, dz, frame_advance] — simple movement
|
|
[path_index, [list_of_steps]] — path-based movement (dolphins)
|
|
where each path step is [dx, dy, dz, frame_advance].
|
|
"""
|
|
|
|
def __init__(self,
|
|
frames: List[List[str]],
|
|
masks: Optional[List[List[str]]],
|
|
x: float, y: float,
|
|
cb_args: list,
|
|
default_color: Optional[str],
|
|
die_offscreen: bool = True,
|
|
death_cb: Optional[Callable] = None,
|
|
die_time: Optional[float] = None,
|
|
die_frame: Optional[int] = None,
|
|
entity_type: str = ''):
|
|
self.frames = frames
|
|
self.masks = masks
|
|
self.x = float(x)
|
|
self.y = float(y)
|
|
self.cb_args = list(cb_args)
|
|
self.default_color = default_color
|
|
self.die_offscreen = die_offscreen
|
|
self.death_cb = death_cb
|
|
self.die_time = die_time
|
|
self._die_frame = die_frame
|
|
self.entity_type = entity_type
|
|
self.curr_frame = 0.0
|
|
self.alive = True
|
|
self.height = len(frames[0]) if frames else 0
|
|
self.width = max((len(l) for l in frames[0]), default=0) if frames else 0
|
|
|
|
@property
|
|
def frame_idx(self) -> int:
|
|
n = len(self.frames)
|
|
return int(self.curr_frame) % n if n else 0
|
|
|
|
def _move(self):
|
|
cb = self.cb_args
|
|
# Path-based: cb_args = [path_idx, [steps]]
|
|
if len(cb) >= 2 and isinstance(cb[1], list):
|
|
idx = int(cb[0]) % len(cb[1])
|
|
step = cb[1][idx]
|
|
cb[0] = cb[0] + 1
|
|
dx = step[0] if len(step) > 0 else 0
|
|
dy = step[1] if len(step) > 1 else 0
|
|
fadv = step[3] if len(step) > 3 else 0
|
|
else:
|
|
dx = cb[0] if len(cb) > 0 else 0
|
|
dy = cb[1] if len(cb) > 1 else 0
|
|
fadv = cb[3] if len(cb) > 3 else 0
|
|
|
|
self.x += dx
|
|
self.y += dy
|
|
if fadv:
|
|
self.curr_frame = (self.curr_frame + fadv) % len(self.frames)
|
|
|
|
def update(self, aq: 'Aquarium') -> bool:
|
|
if not self.alive:
|
|
return False
|
|
|
|
if self.die_time is not None and time.time() >= self.die_time:
|
|
self.alive = False
|
|
elif self._die_frame is not None:
|
|
self._die_frame -= 1
|
|
if self._die_frame <= 0:
|
|
self.alive = False
|
|
|
|
if self.alive:
|
|
self._move()
|
|
if self.die_offscreen:
|
|
ix, iy = int(self.x), int(self.y)
|
|
if (ix >= aq.cols or ix + self.width < 0 or
|
|
iy >= aq.rows or iy + self.height < 0):
|
|
self.alive = False
|
|
|
|
if not self.alive and self.death_cb:
|
|
self.death_cb(self, aq)
|
|
|
|
return self.alive
|
|
|
|
def draw(self, canvas: Canvas):
|
|
fi = self.frame_idx
|
|
canvas.put_sprite(int(self.y), int(self.x),
|
|
self.frames[fi],
|
|
self.masks[fi] if self.masks and fi < len(self.masks) else None,
|
|
self.default_color)
|
|
|
|
|
|
class HookEntity(Entity):
|
|
"""Fishhook: descends to 75% depth, then retracts."""
|
|
|
|
def __init__(self, *args, target_y: int, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self._target_y = target_y
|
|
self._retracting = False
|
|
|
|
def _move(self):
|
|
if not self._retracting:
|
|
if int(self.y) >= self._target_y:
|
|
self._retracting = True
|
|
self.cb_args[1] = -1 # reverse: go up
|
|
else:
|
|
self.y += 1
|
|
else:
|
|
self.y -= 1
|
|
if int(self.y) < -10:
|
|
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, cfg: Optional['Config'] = None):
|
|
self.rows = rows
|
|
self.cols = cols
|
|
self.cfg = cfg or Config()
|
|
self.canvas = Canvas(rows, cols)
|
|
# wl = first row of the wave area, proportional like Perl's int($lines/3).
|
|
# wl+4 is the first fully-underwater row (4 wave rows below wl).
|
|
self.wl = max(3, rows // 3)
|
|
self.entities: List[Entity] = []
|
|
|
|
fns = [self.add_whale, self.add_monster, self.add_swan,
|
|
self.add_ducks, self.add_dolphins,
|
|
self.add_seahorse, self.add_squid, self.add_diver]
|
|
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):
|
|
self.entities.clear()
|
|
self.add_environment()
|
|
if self.rows >= 20:
|
|
self.add_castle()
|
|
self.add_all_seaweed()
|
|
self.add_all_fish()
|
|
self.random_object(None)
|
|
|
|
def resize(self, rows: int, cols: int):
|
|
self.rows, self.cols = rows, cols
|
|
self.wl = max(3, rows // 3)
|
|
self.canvas.resize(rows, cols)
|
|
self._cs_state = 0
|
|
self._cs_portcullis = None
|
|
self._setup()
|
|
|
|
def _add(self, e: Entity):
|
|
self.entities.append(e)
|
|
|
|
def random_object(self, dead):
|
|
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()
|
|
for e in self.entities:
|
|
e.draw(self.canvas)
|
|
return self.canvas.render()
|
|
|
|
# ── Environment (water lines) ─────────────────────────────────────────────
|
|
|
|
def add_environment(self):
|
|
segments = [
|
|
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',
|
|
'^^^^ ^^^ ^^^ ^^^ ^^^^ ',
|
|
'^^^^ ^^^^ ^^^ ^^ ',
|
|
'^^ ^^^^ ^^^ ^^^^^^ ',
|
|
]
|
|
repeat = self.cols // len(segments[0]) + 1
|
|
for i, seg in enumerate(segments):
|
|
self._add(Entity(
|
|
frames=[[seg * repeat]],
|
|
masks=None,
|
|
x=0, y=self.wl + i,
|
|
cb_args=[0, 0, 0],
|
|
default_color='c',
|
|
die_offscreen=False,
|
|
))
|
|
|
|
# ── Castle ───────────────────────────────────────────────────────────────
|
|
|
|
def add_castle(self):
|
|
shape = _p(r"""
|
|
T~~
|
|
|
|
|
/^\
|
|
/ \
|
|
_ _ _ / \ _ _ _
|
|
[ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ]
|
|
|_=__-_ =_|_[ ]_[ ]_|_=-___-__|
|
|
| _- = | =_ = _ |= _= |
|
|
|= -[] |- = _ = |_-=_[] |
|
|
| =_ |= - ___ | =_ = |
|
|
|= []- |- /| |\ |=_ =[] |
|
|
|- =_ | =| | | | |- = - |
|
|
|_______|__|_|_|_|__|_______|
|
|
""")
|
|
mask = _p("""
|
|
RR
|
|
|
|
yyy
|
|
y y
|
|
y y
|
|
y y
|
|
|
|
|
|
|
|
yyy
|
|
yy yy
|
|
y y y y
|
|
yyyyyyy
|
|
""")
|
|
self._add(Entity(
|
|
frames=[shape], masks=[mask],
|
|
x=self.cols - 32, y=self.rows - 13,
|
|
cb_args=[0, 0, 0],
|
|
default_color='k',
|
|
die_offscreen=False,
|
|
))
|
|
|
|
# ── Seaweed ──────────────────────────────────────────────────────────────
|
|
|
|
def add_all_seaweed(self):
|
|
for _ in range(self.cols // 15):
|
|
self.add_seaweed(None)
|
|
|
|
def add_seaweed(self, dead):
|
|
# Faithful reproduction of Perl seaweed generation:
|
|
# two frames built by alternating ( and ) per row
|
|
height = random.randint(3, 6) # int(rand(4)) + 3
|
|
raw = ['', '']
|
|
for i in range(1, height + 1):
|
|
left = i % 2
|
|
right = 1 - left
|
|
raw[left] += '(\n'
|
|
raw[right] += ' )\n'
|
|
frames = [r.split('\n')[:-1] for r in raw] # strip trailing empty
|
|
|
|
x = random.randint(1, self.cols - 3)
|
|
y = self.rows - height
|
|
anim_speed = random.uniform(0.25, 0.30)
|
|
die_t = time.time() + random.randint(8 * 60, 12 * 60)
|
|
|
|
self._add(Entity(
|
|
frames=frames, masks=None,
|
|
x=x, y=y,
|
|
cb_args=[0, 0, 0, anim_speed],
|
|
default_color='g',
|
|
die_offscreen=False,
|
|
death_cb=lambda e, aq: aq.add_seaweed(e),
|
|
die_time=die_t,
|
|
))
|
|
|
|
# ── Bubbles ──────────────────────────────────────────────────────────────
|
|
|
|
def add_bubble(self, fish: Entity):
|
|
bx = int(fish.x) + (fish.width if fish.cb_args[0] > 0 else 0)
|
|
by = int(fish.y) + fish.height // 2
|
|
self._add(Entity(
|
|
frames=[['.'], ['o'], ['O'], ['O'], ['O']],
|
|
masks=None,
|
|
x=bx, y=by,
|
|
cb_args=[0, -1, 0, 0.1],
|
|
default_color='C',
|
|
die_offscreen=True,
|
|
))
|
|
|
|
# ── Fish ─────────────────────────────────────────────────────────────────
|
|
|
|
def add_all_fish(self):
|
|
underwater_rows = max(1, self.rows - (self.wl + 4))
|
|
count = max(15, underwater_rows * self.cols // 120)
|
|
for _ in range(count):
|
|
self.add_fish(None)
|
|
|
|
def add_fish(self, dead):
|
|
# Each tuple: (shape_right, mask_right, shape_left, mask_left)
|
|
# Color mask digits: 1=body 2=dorsal 3=flippers 4=eye 5=mouth 6=tail 7=gills
|
|
fish_types = [
|
|
(_p(r"""
|
|
\
|
|
...\...,
|
|
\ /' \
|
|
>= ( ' >
|
|
/ \ / /
|
|
`"'"'/''
|
|
"""), _p("""
|
|
2
|
|
1112111
|
|
6 11 1
|
|
66 7 4 5
|
|
6 1 3 1
|
|
11111311
|
|
"""), _p(r"""
|
|
/
|
|
,.../...
|
|
/ '\ /
|
|
< ' ) =<
|
|
\ \ / \
|
|
`'\'"'"'
|
|
"""), _p("""
|
|
2
|
|
1112111
|
|
1 11 6
|
|
5 4 7 66
|
|
1 3 1 6
|
|
11311111
|
|
""")),
|
|
(_p(r"""
|
|
\
|
|
\ /--\
|
|
>= (o>
|
|
/ \__/
|
|
/
|
|
"""), _p("""
|
|
2
|
|
6 1111
|
|
66 745
|
|
6 1111
|
|
3
|
|
"""), _p(r"""
|
|
/
|
|
/--\ /
|
|
<o) =<
|
|
\__/ \
|
|
\
|
|
"""), _p("""
|
|
2
|
|
1111 6
|
|
547 66
|
|
1111 6
|
|
3
|
|
""")),
|
|
(_p(r"""
|
|
\:.
|
|
\;, ,;\\\,,
|
|
\\\;;:::::::o
|
|
///;;::::::::<
|
|
/;` ``/////``
|
|
"""), _p("""
|
|
222
|
|
666 1122211
|
|
6661111111114
|
|
66611111111115
|
|
666 113333311
|
|
"""), _p(r"""
|
|
.:/
|
|
,,///;, ,;/
|
|
o:::::::;;///
|
|
>::::::::;;\\\
|
|
''\\\\\'' ';\
|
|
"""), _p("""
|
|
222
|
|
1122211 666
|
|
4111111111666
|
|
51111111111666
|
|
113333311 666
|
|
""")),
|
|
(_p("""
|
|
__
|
|
><_'>
|
|
'
|
|
"""), _p("""
|
|
11
|
|
61145
|
|
3
|
|
"""), _p("""
|
|
__
|
|
<'_><
|
|
`
|
|
"""), _p("""
|
|
11
|
|
54116
|
|
3
|
|
""")),
|
|
(_p(r"""
|
|
..\,
|
|
>=' ('>
|
|
'''/''
|
|
"""), _p("""
|
|
1121
|
|
661 745
|
|
111311
|
|
"""), _p(r"""
|
|
,/..
|
|
<') `=<
|
|
``\```
|
|
"""), _p("""
|
|
1211
|
|
547 166
|
|
113111
|
|
""")),
|
|
(_p(r"""
|
|
\
|
|
/ \
|
|
>=_('>
|
|
\_/
|
|
/
|
|
"""), _p("""
|
|
2
|
|
1 1
|
|
661745
|
|
111
|
|
3
|
|
"""), _p(r"""
|
|
/
|
|
/ \
|
|
<')_=<
|
|
\_/
|
|
\
|
|
"""), _p("""
|
|
2
|
|
1 1
|
|
547166
|
|
111
|
|
3
|
|
""")),
|
|
(_p(r"""
|
|
,\
|
|
>=('>
|
|
'/
|
|
"""), _p("""
|
|
12
|
|
66745
|
|
13
|
|
"""), _p(r"""
|
|
/,
|
|
<')=<
|
|
\`
|
|
"""), _p("""
|
|
21
|
|
54766
|
|
31
|
|
""")),
|
|
(_p(r"""
|
|
__
|
|
\/ o\
|
|
/\__/
|
|
"""), _p("""
|
|
11
|
|
61 41
|
|
61111
|
|
"""), _p(r"""
|
|
__
|
|
/o \/
|
|
\__/\
|
|
"""), _p("""
|
|
11
|
|
14 16
|
|
11116
|
|
""")),
|
|
]
|
|
|
|
fish_num = random.randint(0, len(fish_types) - 1)
|
|
going_right = (fish_num % 2 == 0)
|
|
speed = random.uniform(0.25, 2.25) * (1 if going_right else -1)
|
|
|
|
sr, mr, sl, ml = fish_types[fish_num]
|
|
if going_right:
|
|
shape = sr
|
|
mask = _rand_color(mr)
|
|
x_offscreen = 1 - max(len(l) for l in shape)
|
|
else:
|
|
shape = sl
|
|
mask = _rand_color(ml)
|
|
x_offscreen = self.cols - 2
|
|
|
|
height = len(shape)
|
|
width = max(len(l) for l in shape)
|
|
y = random.randint(self.wl + 4, max(self.wl + 5, self.rows - height))
|
|
|
|
# Initial population (dead=None): start already on-screen so the
|
|
# aquarium is immediately populated. Respawned fish enter from edges.
|
|
if dead is None:
|
|
x = random.randint(0, max(0, self.cols - width))
|
|
else:
|
|
x = x_offscreen
|
|
|
|
def on_death(fish, aq):
|
|
if random.random() < 0.03:
|
|
aq.add_bubble(fish)
|
|
aq.add_fish(fish)
|
|
|
|
self._add(Entity(
|
|
frames=[shape], masks=[mask],
|
|
x=x, y=y,
|
|
cb_args=[speed, 0, 0],
|
|
default_color='c',
|
|
die_offscreen=True,
|
|
death_cb=on_death,
|
|
entity_type='fish',
|
|
))
|
|
|
|
# ── Splat ────────────────────────────────────────────────────────────────
|
|
|
|
def add_splat(self, x: int, y: int):
|
|
self._add(Entity(
|
|
frames=[_p("""
|
|
.
|
|
***
|
|
'
|
|
"""), _p("""
|
|
",*;`
|
|
"*,**
|
|
*"'~'
|
|
"""), _p("""
|
|
, ,
|
|
" ","'
|
|
*" *'"
|
|
" ; .
|
|
"""), _p("""
|
|
* ' , ' `
|
|
' ` * . '
|
|
' `' ",'
|
|
* ' " * .
|
|
" * ', '
|
|
""")],
|
|
masks=None,
|
|
x=x - 4, y=y - 2,
|
|
cb_args=[0, 0, 0, 0.25],
|
|
default_color='R',
|
|
die_offscreen=False,
|
|
die_frame=15,
|
|
))
|
|
|
|
# ── Shark ────────────────────────────────────────────────────────────────
|
|
|
|
def add_shark(self, dead=None):
|
|
shape_r = _p(r"""
|
|
__
|
|
( `\
|
|
,??????????????????????????) `\
|
|
;' `.????????????????????????( `\__
|
|
; `.?????????????__..---'' `~~~~-._
|
|
`. `.____...--'' (b `--._
|
|
> _.-' .(( ._ )
|
|
.`.-`--...__ .-' -.___.....-(|/|/|/|/'
|
|
;.'?????????`. ...----`.___.',,,_______......---'
|
|
'???????????'-'
|
|
""")
|
|
shape_l = _p(r"""
|
|
__
|
|
/' )
|
|
/' (??????????????????????????,
|
|
__/' )????????????????????????.' `;
|
|
_.-~~~~' ``---..__?????????????.' ;
|
|
_.--' b) ``--...____.' .'
|
|
( _. )). `-._ <
|
|
`\|\|\|\|)-.....___.- `-. __...--'-.'.
|
|
`---......_______,,,`.___.'----... .'?????????`.;
|
|
`-`???????????`
|
|
""")
|
|
mask_r = _p("""
|
|
|
|
|
|
|
|
cR
|
|
|
|
cWWWWWWWW
|
|
|
|
|
|
""")
|
|
mask_l = _p("""
|
|
|
|
|
|
|
|
|
|
Rc
|
|
|
|
WWWWWWWWc
|
|
|
|
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
speed = 2.0 * (1 if going_right else -1)
|
|
x = -53 if going_right else self.cols - 2
|
|
y = random.randint(self.wl + 4, max(self.wl + 5, self.rows - 19))
|
|
|
|
self._add(Entity(
|
|
frames=[shape_r if going_right else shape_l],
|
|
masks=[mask_r if going_right else mask_l],
|
|
x=x, y=y,
|
|
cb_args=[speed, 0, 0],
|
|
default_color='C',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
entity_type='shark',
|
|
))
|
|
|
|
# ── Ship ─────────────────────────────────────────────────────────────────
|
|
|
|
def add_ship(self, dead=None):
|
|
shape_r = _p(r"""
|
|
| | |
|
|
)_) )_) )_)
|
|
)___))___))___)\
|
|
)____)____)_____)\\\
|
|
_____|____|____|____\\\\__
|
|
\ /
|
|
""")
|
|
shape_l = _p(r"""
|
|
| | |
|
|
(_( (_( (_(
|
|
/(___((___((___(
|
|
//(_____(____(____(
|
|
__///____|____|____|_____
|
|
\ /
|
|
""")
|
|
mask_r = _p("""
|
|
y y y
|
|
|
|
w
|
|
ww
|
|
yyyyyyyyyyyyyyyyyyyywwwyy
|
|
y y
|
|
""")
|
|
mask_l = _p("""
|
|
y y y
|
|
|
|
w
|
|
ww
|
|
yywwwyyyyyyyyyyyyyyyyyyyy
|
|
y y
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
speed = 1.0 * (1 if going_right else -1)
|
|
x = -24 if going_right else self.cols - 2
|
|
|
|
self._add(Entity(
|
|
frames=[shape_r if going_right else shape_l],
|
|
masks=[mask_r if going_right else mask_l],
|
|
x=x, y=0,
|
|
cb_args=[speed, 0, 0],
|
|
default_color='W',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
))
|
|
|
|
# ── Whale ────────────────────────────────────────────────────────────────
|
|
|
|
def add_whale(self, dead=None):
|
|
body_r = """
|
|
.-----:
|
|
.' `.
|
|
,????/ (o) \\
|
|
\\`._/ ,__)
|
|
"""
|
|
body_l = """
|
|
:-----.
|
|
.' `.
|
|
/ (o) \\????,
|
|
(__, \\_.'/
|
|
"""
|
|
mask_r_str = """
|
|
C C
|
|
CCCCCCC
|
|
C C C
|
|
|
|
BBBBBBB
|
|
BB BB
|
|
B B BWB B
|
|
BBBBB BBBB
|
|
"""
|
|
mask_l_str = """
|
|
C C
|
|
CCCCCCC
|
|
C C C
|
|
|
|
BBBBBBB
|
|
BB BB
|
|
B BWB B B
|
|
BBBB BBBBB
|
|
"""
|
|
spouts = [
|
|
"\n\n :\n",
|
|
"\n\n :\n :\n",
|
|
"\n . .\n -:-\n :\n",
|
|
"\n . .\n .-:-.\n :\n",
|
|
"\n . .\n'.-:.`\n' : '\n",
|
|
"\n .- -.\n; : ;\n",
|
|
"\n\n; ;\n",
|
|
]
|
|
|
|
going_right = random.random() < 0.5
|
|
speed = 1.0 * (1 if going_right else -1)
|
|
spout_align = 11 if going_right else 1
|
|
body_str = body_r if going_right else body_l
|
|
mask_str = mask_r_str if going_right else mask_l_str
|
|
x = -18 if going_right else self.cols - 2
|
|
|
|
# 4 body lines; all frames are 8 rows: 3 spout + 1 separator + 4 body.
|
|
# This keeps the body at the same rows (4-7) in every frame so the
|
|
# whale doesn't jump, and the mask aligns correctly.
|
|
body_lines = _p(body_str)
|
|
mask_lines = _p(mask_str)
|
|
|
|
frames, masks = [], []
|
|
# 5 frames without spout: 4 empty rows + 4 body rows
|
|
for _ in range(5):
|
|
frames.append(['', '', '', ''] + body_lines)
|
|
masks.append(mask_lines)
|
|
# 7 frames with animated spout: spout padded to 3 rows, then separator, then body
|
|
for spout in spouts:
|
|
sp_lines = [' ' * spout_align + l for l in spout.strip('\n').split('\n')]
|
|
while len(sp_lines) < 3:
|
|
sp_lines = [''] + sp_lines
|
|
frames.append(sp_lines + [''] + body_lines)
|
|
masks.append(mask_lines)
|
|
|
|
self._add(Entity(
|
|
frames=frames, masks=masks,
|
|
x=x, y=0,
|
|
cb_args=[speed, 0, 0, 1],
|
|
default_color='W',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
))
|
|
|
|
# ── Monster ──────────────────────────────────────────────────────────────
|
|
|
|
def add_monster(self, dead=None):
|
|
monster_r = [_p(s) for s in [r"""
|
|
____
|
|
__??????????????????????????????????????????/ o \
|
|
/ \????????_?????????????????????_???????/ ____ >
|
|
_??????| __ |?????/ \????????_????????/ \????| |
|
|
| \?????| || |????| |?????/ \?????| |???| |
|
|
""", r"""
|
|
____
|
|
__?????????/ o \
|
|
_?????????????????????_???????/ \?????/ ____ >
|
|
_???????/ \????????_????????/ \????| __ |???| |
|
|
| \?????| |?????/ \?????| |???| || |???| |
|
|
""", r"""
|
|
____
|
|
__????????????????????/ o \
|
|
_??????????????????????_???????/ \????????_???????/ ____ >
|
|
| \??????????_????????/ \????| __ |?????/ \????| |
|
|
\ \???????/ \?????| |???| || |????| |???| |
|
|
""", r"""
|
|
____
|
|
__???????????????????????????????/ o \
|
|
_??????????_???????/ \????????_??????????????????/ ____ >
|
|
| \???????/ \????| __ |?????/ \????????_??????| |
|
|
\ \?????| |???| || |????| |?????/ \????| |
|
|
"""]]
|
|
monster_l = [_p(s) for s in [r"""
|
|
____
|
|
/ o \??????????????????????????????????????????__
|
|
< ____ \???????_?????????????????????_????????/ \
|
|
| |????/ \????????_????????/ \?????| __ |??????_
|
|
| |???| |?????/ \?????| |????| || |?????/ |
|
|
""", r"""
|
|
____
|
|
/ o \?????????__
|
|
< ____ \?????/ \???????_?????????????????????_
|
|
| |???| __ |????/ \????????_????????/ \???????_
|
|
| |???| || |???| |?????/ \?????| |?????/ |
|
|
""", r"""
|
|
____
|
|
/ o \????????????????????__
|
|
< ____ \???????_????????/ \???????_??????????????????????_
|
|
| |????/ \?????| __ |????/ \????????_??????????/ |
|
|
| |???| |????| || |???| |?????/ \???????/ /
|
|
""", r"""
|
|
____
|
|
/ o \???????????????????????????????__
|
|
< ____ \??????????????????_????????/ \???????_??????????_
|
|
| |??????_????????/ \?????| __ |????/ \???????/ |
|
|
| |????/ \?????| |????| || |???| |?????/ /
|
|
"""]]
|
|
mask_r = _p("""
|
|
|
|
W
|
|
|
|
|
|
|
|
""")
|
|
mask_l = _p("""
|
|
|
|
W
|
|
|
|
|
|
|
|
""")
|
|
|
|
going_right = random.random() < 0.5
|
|
speed = 2.0 * (1 if going_right else -1)
|
|
if going_right:
|
|
x, shapes, mask = -64, monster_r, mask_r
|
|
else:
|
|
x, shapes, mask = self.cols - 2, monster_l, mask_l
|
|
|
|
self._add(Entity(
|
|
frames=shapes, masks=[mask] * 4,
|
|
x=x, y=2,
|
|
cb_args=[speed, 0, 0, 0.25],
|
|
default_color='G',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
))
|
|
|
|
# ── Big fish ─────────────────────────────────────────────────────────────
|
|
|
|
def add_big_fish(self, dead=None):
|
|
shape_r = _p(r"""
|
|
______
|
|
`""-. `````-----.....__
|
|
`. . . `-.
|
|
: . . `.
|
|
, : . . _ :
|
|
: `. : (@) `._
|
|
`. `..' . =`-. .__)
|
|
; . = ~ : .-"
|
|
.' .'`. . . =.-' `._ .'
|
|
: .' : . .'
|
|
' .' . . . .-'
|
|
.'____....----''.'=.'
|
|
"" .'.'
|
|
''"'`
|
|
""")
|
|
shape_l = _p(r"""
|
|
______
|
|
__.....-----''''' .-""'
|
|
.-' . . .'
|
|
.' . . :
|
|
: _ . . : ,
|
|
_.' (@) : .' :
|
|
(__. .-'= . `..' .'
|
|
"-. : ~ = . ;
|
|
`. _.' `-.= . . .'`. `.
|
|
`. . : `. :
|
|
`-. . . . `. `
|
|
`.=`.``----....____`.
|
|
`.`. ""
|
|
'`"``
|
|
""")
|
|
mask_r_t = _p("""
|
|
111111
|
|
11111 11111111111111111
|
|
11 2 2 111
|
|
1 2 2 11
|
|
1 1 2 2 1 1
|
|
1 11 1 1W1 111
|
|
11 1111 2 1111 1111
|
|
1 2 1 1 1 111
|
|
11 1111 2 2 1111 111 11
|
|
1 11 1 2 11
|
|
1 11 2 2 2 111
|
|
111111111111111111111
|
|
11 1111
|
|
11111
|
|
""")
|
|
mask_l_t = _p("""
|
|
111111
|
|
11111111111111111 11111
|
|
111 2 2 11
|
|
11 2 2 1
|
|
1 1 2 2 1 1
|
|
111 1W1 1 11 1
|
|
1111 1111 2 1111 11
|
|
111 1 1 1 2 1
|
|
11 111 1111 2 2 1111 11
|
|
11 2 1 11 1
|
|
111 2 2 2 11 1
|
|
111111111111111111111
|
|
1111 11
|
|
11111
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
speed = 3.0 * (1 if going_right else -1)
|
|
if going_right:
|
|
x = -34; shape = shape_r; mask = _rand_color(mask_r_t)
|
|
else:
|
|
x = self.cols - 1; shape = shape_l; mask = _rand_color(mask_l_t)
|
|
|
|
y = random.randint(self.wl + 4, max(self.wl + 5, self.rows - 15))
|
|
self._add(Entity(
|
|
frames=[shape], masks=[mask],
|
|
x=x, y=y,
|
|
cb_args=[speed, 0, 0],
|
|
default_color='Y',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
))
|
|
|
|
# ── Ducks ────────────────────────────────────────────────────────────────
|
|
|
|
def add_ducks(self, dead=None):
|
|
ducks_r = [_p(s) for s in [r"""
|
|
_??????????_??????????_
|
|
,____(')=??,____(')=??,____(')<
|
|
\~~= ')???\~~= ')???\~~= ')
|
|
""", r"""
|
|
_??????????_??????????_
|
|
,____(')=??,____(')<??,____(')=
|
|
\~~= ')???\~~= ')???\~~= ')
|
|
""", r"""
|
|
_??????????_??????????_
|
|
,____(')<??,____(')=??,____(')=
|
|
\~~= ')???\~~= ')???\~~= ')
|
|
"""]]
|
|
ducks_l = [_p(s) for s in [r"""
|
|
_??????????_??????????_
|
|
>(')____,??=(')____,??=(')____,
|
|
(` =~~/????(` =~~/????(` =~~/
|
|
""", r"""
|
|
_??????????_??????????_
|
|
=(')____,??>(')____,??=(')____,
|
|
(` =~~/????(` =~~/????(` =~~/
|
|
""", r"""
|
|
_??????????_??????????_
|
|
=(')____,??=(')____,??>(')____,
|
|
(` =~~/????(` =~~/????(` =~~/
|
|
"""]]
|
|
mask_r = _p("""
|
|
g g g
|
|
wwwwwgcgy wwwwwgcgy wwwwwgcgy
|
|
wwww Ww wwww Ww wwww Ww
|
|
""")
|
|
mask_l = _p("""
|
|
g g g
|
|
ygcgwwwww ygcgwwwww ygcgwwwww
|
|
wW wwww wW wwww wW wwww
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
speed = 1.0 * (1 if going_right else -1)
|
|
if going_right:
|
|
x, shapes, mask = -30, ducks_r, mask_r
|
|
else:
|
|
x, shapes, mask = self.cols - 2, ducks_l, mask_l
|
|
|
|
self._add(Entity(
|
|
frames=shapes, masks=[mask] * 3,
|
|
x=x, y=self.wl,
|
|
cb_args=[speed, 0, 0, 0.25],
|
|
default_color='W',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
))
|
|
|
|
# ── Dolphins ─────────────────────────────────────────────────────────────
|
|
|
|
def add_dolphins(self, dead=None):
|
|
dolp_r = [_p(s) for s in [r"""
|
|
,
|
|
__)\
|
|
(\_.-' a`-.
|
|
(/~~````(/~^^`
|
|
""", r"""
|
|
,
|
|
(\__ __)\
|
|
(/~.'' a`-.
|
|
````\)~^^`
|
|
"""]]
|
|
dolp_l = [_p(s) for s in [r"""
|
|
,
|
|
_/(__
|
|
.-'a `-._/)
|
|
'^^~\)''''~~\)
|
|
""", r"""
|
|
,
|
|
_/(__ __/)
|
|
.-'a ``.~\)
|
|
'^^~(/''''
|
|
"""]]
|
|
mask_r = _p("""
|
|
|
|
|
|
W
|
|
""")
|
|
mask_l = _p("""
|
|
|
|
|
|
W
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
spd = 1.0 * (1 if going_right else -1)
|
|
dist = 15 * (1 if going_right else -1)
|
|
|
|
up = [spd, -0.5, 0, 0.5]
|
|
down = [spd, 0.5, 0, 0.5]
|
|
glide = [spd, 0.0, 0, 0.5]
|
|
path = [up]*14 + [glide]*2 + [down]*14 + [glide]*6
|
|
|
|
shapes = dolp_r if going_right else dolp_l
|
|
mask = mask_r if going_right else mask_l
|
|
masks = [mask] * 2
|
|
base_x = -13 if going_right else self.cols - 2
|
|
|
|
# Three dolphins offset in their path cycle (offsets 0, 12, 24).
|
|
# Relative to wl so the arc crosses the surface at any terminal size.
|
|
# Amplitude ±7: range [wl+5-7, wl+5] = [wl-2, wl+5]
|
|
# → briefly above waves and ~2 rows underwater.
|
|
# Cumulative dy at offsets 0/12/24: 0/-6/-3
|
|
start_ys = [self.wl + 5, self.wl - 1, self.wl + 2]
|
|
colors = ['b', 'B', 'C']
|
|
for i in range(3):
|
|
is_lead = (i == 2)
|
|
self._add(Entity(
|
|
frames=shapes, masks=masks,
|
|
x=base_x - dist * (2 - i),
|
|
y=start_ys[i],
|
|
cb_args=[i * 12, list(path)],
|
|
default_color=colors[i],
|
|
die_offscreen=is_lead,
|
|
death_cb=(lambda e, aq: aq.random_object(e)) if is_lead else None,
|
|
))
|
|
|
|
# ── Swan ─────────────────────────────────────────────────────────────────
|
|
|
|
def add_swan(self, dead=None):
|
|
swan_r = _p(r"""
|
|
___
|
|
,_ / _,\
|
|
| \ \( \|
|
|
| \_ \\
|
|
(_ \_) \
|
|
(\_ ` \
|
|
\ -=~ /
|
|
""")
|
|
swan_l = _p(r"""
|
|
___
|
|
/,_ \ _,
|
|
|/ )/ / |
|
|
// _/ |
|
|
/ ( / _)
|
|
/ ` _/)
|
|
\ ~=- /
|
|
""")
|
|
mask_r = _p("""
|
|
|
|
g
|
|
yy
|
|
""")
|
|
mask_l = _p("""
|
|
|
|
g
|
|
yy
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
speed = 1.0 * (1 if going_right else -1)
|
|
if going_right:
|
|
x, shape, mask = -10, swan_r, mask_r
|
|
else:
|
|
x, shape, mask = self.cols - 2, swan_l, mask_l
|
|
|
|
self._add(Entity(
|
|
frames=[shape], masks=[mask],
|
|
x=x, y=1,
|
|
cb_args=[speed, 0, 0, 0.25],
|
|
default_color='W',
|
|
die_offscreen=True,
|
|
death_cb=lambda e, aq: aq.random_object(e),
|
|
))
|
|
|
|
# ── Fishhook ─────────────────────────────────────────────────────────────
|
|
|
|
def add_fishhook(self, dead=None):
|
|
hook_shape = _p(r"""
|
|
o
|
|
||
|
|
||
|
|
/ \ ||
|
|
\__//
|
|
`--'
|
|
""")
|
|
x = random.randint(10, max(11, self.cols - 20))
|
|
target_y = int(self.rows * 0.75)
|
|
|
|
e = HookEntity(
|
|
frames=[hook_shape], masks=None,
|
|
x=x, y=-4,
|
|
cb_args=[0, 1, 0],
|
|
default_color='G',
|
|
die_offscreen=False,
|
|
target_y=target_y,
|
|
death_cb=lambda e, aq: aq.random_object(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(self.wl + 4, max(self.wl + 5, 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(self.wl + 4, max(self.wl + 5, 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),
|
|
))
|
|
|
|
# ── Diver (Taucher) ──────────────────────────────────────────────────────
|
|
|
|
def add_diver(self, dead=None):
|
|
diver_r0 = _p(r"""
|
|
O
|
|
\-|>
|
|
/\
|
|
""")
|
|
diver_r1 = _p(r"""
|
|
O
|
|
/-|>
|
|
\/
|
|
""")
|
|
diver_l0 = _p(r"""
|
|
O
|
|
<|-/
|
|
/\
|
|
""")
|
|
diver_l1 = _p(r"""
|
|
O
|
|
<|-\
|
|
\/
|
|
""")
|
|
mask_r = _p("""
|
|
y
|
|
wccy
|
|
gg
|
|
""")
|
|
mask_l = _p("""
|
|
y
|
|
yccw
|
|
gg
|
|
""")
|
|
going_right = random.random() < 0.5
|
|
speed = random.uniform(0.5, 1.0) * (1 if going_right else -1)
|
|
if going_right:
|
|
x, frames, mask = -6, [diver_r0, diver_r1], mask_r
|
|
else:
|
|
x, frames, mask = self.cols - 2, [diver_l0, diver_l1], mask_l
|
|
y = random.randint(self.wl + 4, max(self.wl + 5, self.rows - 5))
|
|
|
|
def on_death(e, aq):
|
|
aq.add_bubble(e)
|
|
aq.random_object(e)
|
|
|
|
self._add(Entity(
|
|
frames=frames, masks=[mask, mask],
|
|
x=x, y=y,
|
|
cb_args=[speed, 0, 0, 0.2],
|
|
default_color='c',
|
|
die_offscreen=True,
|
|
death_cb=on_death,
|
|
))
|
|
|
|
# ── 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()
|
|
# ASCII-only crab to avoid wide-char rendering issues
|
|
crab_f0 = ['(._.)', '/| |\\']
|
|
crab_f1 = ['(._.)', '\\| |/']
|
|
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=22, # walk further so crab is clearly visible
|
|
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 ───────────────────────────────────────────────────────────
|
|
|
|
class Input:
|
|
def __init__(self):
|
|
self._fd = sys.stdin.fileno()
|
|
self._old = termios.tcgetattr(self._fd)
|
|
tty.setraw(self._fd)
|
|
flags = fcntl.fcntl(self._fd, fcntl.F_GETFL)
|
|
fcntl.fcntl(self._fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
|
|
|
|
def key(self) -> Optional[str]:
|
|
try:
|
|
return os.read(self._fd, 1).decode('utf-8', errors='ignore')
|
|
except (BlockingIOError, OSError):
|
|
return None
|
|
|
|
def restore(self):
|
|
termios.tcsetattr(self._fd, termios.TCSADRAIN, self._old)
|
|
|
|
|
|
# ─── 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)')
|
|
parser.add_argument('--debug', action='store_true',
|
|
help='show terminal size / entity count overlay')
|
|
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,
|
|
)
|
|
|
|
inp = Input()
|
|
paused = False
|
|
needs_reset = False
|
|
|
|
def on_resize(sig, frame_):
|
|
nonlocal needs_reset
|
|
needs_reset = True
|
|
|
|
signal.signal(signal.SIGWINCH, on_resize)
|
|
|
|
fd = sys.stdout.fileno()
|
|
# Activate alternate screen first, THEN read terminal size so the
|
|
# size reflects the actual pane dimensions (important in split panes).
|
|
os.write(fd, (ALT_ON + HIDE_CUR + "\033[2J\033[H").encode())
|
|
rows, cols = term_size()
|
|
aq = Aquarium(rows, cols, cfg)
|
|
|
|
try:
|
|
TICK = 0.1 / cfg.speed
|
|
while True:
|
|
t0 = time.monotonic()
|
|
|
|
if needs_reset:
|
|
rows, cols = term_size()
|
|
aq.resize(rows, cols)
|
|
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'):
|
|
rows, cols = term_size()
|
|
aq.resize(rows, cols)
|
|
|
|
if not paused:
|
|
aq.step()
|
|
frame = aq.render()
|
|
if args.debug:
|
|
fish_ys = [f"{int(e.y)}" for e in aq.entities
|
|
if e.entity_type == 'fish'][:8]
|
|
dbg = (f" rows={aq.rows} cols={aq.cols} wl={aq.wl}"
|
|
f" ents={len(aq.entities)}"
|
|
f" fish_y=[{','.join(fish_ys)}] ")
|
|
frame += f"\033[1;1H\033[0;7m{dbg}\033[m"
|
|
os.write(fd, frame.encode())
|
|
|
|
elapsed = time.monotonic() - t0
|
|
sleep = TICK - elapsed
|
|
if sleep > 0:
|
|
time.sleep(sleep)
|
|
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
inp.restore()
|
|
os.write(fd, (SHOW_CUR + ALT_OFF + RESET).encode())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|