From 66671eda8b3b76739cc9c707108c271dd77b9fc6 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 29 Mar 2026 15:20:18 +0200 Subject: [PATCH 1/3] Fix: Wal springt nicht mehr, Farben korrekt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- asciiquarium_ng.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/asciiquarium_ng.py b/asciiquarium_ng.py index 7391e7d..cbd8a6b 100755 --- a/asciiquarium_ng.py +++ b/asciiquarium_ng.py @@ -473,7 +473,8 @@ class Aquarium: # ── Fish ───────────────────────────────────────────────────────────────── def add_all_fish(self): - count = max(8, (self.rows - 9) * self.cols // 200) + 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) @@ -859,6 +860,7 @@ yywwwyyyyyyyyyyyyyyyyyyyy C C CCCCCCC C C C + BBBBBBB BB BB B B BWB B @@ -868,6 +870,7 @@ BBBBB BBBB C C CCCCCCC C C C + BBBBBBB BB BB B BWB B B @@ -890,17 +893,24 @@ BBBB BBBBB 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 + # 5 frames without spout: 4 empty rows + 4 body rows for _ in range(5): - frames.append(_p('\n\n\n' + body_str)) - masks.append(_p(mask_str)) - # 7 frames with animated spout + 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.strip('\n').split('\n') - aligned = '\n'.join(' ' * spout_align + l for l in sp_lines) - frames.append(_p(aligned + '\n' + body_str)) - masks.append(_p(mask_str)) + 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, @@ -1544,6 +1554,8 @@ def main(): 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( @@ -1555,8 +1567,6 @@ def main(): any_key = args.any_key, ) - rows, cols = term_size() - aq = Aquarium(rows, cols, cfg) inp = Input() paused = False needs_reset = False @@ -1568,7 +1578,11 @@ def main(): 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 @@ -1595,6 +1609,13 @@ def main(): 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 From d87088eff0a10b8c76e4f30a3b6899419a9ad63d Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 29 Mar 2026 15:34:31 +0200 Subject: [PATCH 2/3] Fix: Pending-Wrap-Kaskade in iTerm2 behoben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit render() schrieb pro Nicht-Letzte-Zeile exakt self.cols Zeichen → Cursor landet in Pending-Wrap-Zustand. In iTerm2 löscht ein Cursor-Positionierungs- befehl (_go) diesen Zustand NICHT: das erste Zeichen der nächsten Zeile triggert den Wrap → Zeile r+1 landet auf r+2 usw. → Kaskade. Folgen: Zeilen ohne Überschreibung zeigen alte Zeichen (Fragmente oberhalb der Wasserlinie), Unterwasserinhalt verschiebt sich aus dem sichtbaren Bereich (nichts unter der Wasserlinie sichtbar). Fix: letztes Zeichen jeder Zeile grundsätzlich auslassen (col_limit = cols - 1, für alle Zeilen). Pending-Wrap tritt nie auf. Letzte Spalte bleibt leer, was für die Animation völlig akzeptabel ist. --- asciiquarium_ng.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/asciiquarium_ng.py b/asciiquarium_ng.py index cbd8a6b..86a3045 100755 --- a/asciiquarium_ng.py +++ b/asciiquarium_ng.py @@ -119,18 +119,22 @@ class Canvas: 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 + # \033[H homes the cursor; every cell is rewritten so \033[2J is not + # needed (it would push content into the scrollback on every frame). + # + # The last column of EVERY row is intentionally skipped (not just the + # last row). Writing to the last column triggers "pending-wrap" state. + # In iTerm2, cursor-position commands (_go) do NOT clear pending-wrap, + # so the first character of the next row triggers a wrap, placing it one + # row too low. This cascades: row r+1 lands at r+2, row r+2 at r+3, … + # Content below the waterline shifts off-screen; skipped rows show stale + # characters. Staying one column short prevents pending-wrap entirely. + col_limit = 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: From 0396ec83c639cb12d7fcaa36d6ec7ac0b6618665 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 29 Mar 2026 18:39:12 +0200 Subject: [PATCH 3/3] Features: Opacity, Angelhaken, Predation, wl-relative Positionen, Resize-Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _write_all(): Partial-Write-Fix (O_NONBLOCK stdin → stdout in iTerm2 PTY) - Canvas.put_sprite(): opaque-Modus — nur Interior-Spaces werden schwarz gemalt, Leading/Trailing-Spaces bleiben transparent (kein schwarzer Block hinter Tieren) - Entity: opaque=True als Default; Wellen/Schloss/Seegras explizit opaque=False - wl = max(3, rows//3 - 3): Wasserlinie 3 Zeilen höher - Fischanzahl: max(6, underwater_rows * cols // 320) - Oberflächenobjekte (Schiff, Wal, Nessi, Schwan) mit wl-relativem y — bleiben bei Resize auf der Wasseroberfläche - add_fishhook(): neu als Frame-Animation (Schnur wächst/schrumpft per Frame); weißes Schnur-Mask + gelber Haken (default_color y) - _check_fishing(): Haken erkennt Fisch-Kollision, springt zur Rückzugsphase, injiziert Fisch-Zeile in verbleibende Frames → Fisch hängt am Haken - _check_predation(): Hai frisst immer (mit Blut); --bloody aktiviert big_fish als weiteren Räuber; --vegan = keine Räuber - HookEntity entfernt (ersetzt durch Frame-Animation) - big_fish: entity_type gesetzt für Kollisionserkennung - Krabbe: y = gy+1 (eine Zeile tiefer) - Canvas.render(): \033[K am Ende jeder Zeile — löscht letzte Spalte ohne Pending-Wrap; behebt Stale-Content nach Resize - \033[r beim Start: Scroll-Region-Reset - --ruler Flag für Diagnose --- asciiquarium_ng.py | 230 +++++++++++++++++++++++++++++++++------------ 1 file changed, 168 insertions(+), 62 deletions(-) diff --git a/asciiquarium_ng.py b/asciiquarium_ng.py index 86a3045..47efc8e 100755 --- a/asciiquarium_ng.py +++ b/asciiquarium_ng.py @@ -15,8 +15,8 @@ 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) + bloody: bool = False # massacre: all predators eat fish with blood splats + vegan: bool = False # peaceful: no predators (shark, big_fish, fishhook) any_key: bool = False # any key exits # ─── Terminal control ──────────────────────────────────────────────────────── @@ -32,6 +32,17 @@ 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 +def _write_all(fd: int, data: bytes) -> None: + """Write all bytes to fd, retrying on partial writes (non-blocking TTY).""" + mv = memoryview(data) + offset = 0 + while offset < len(mv): + try: + n = os.write(fd, mv[offset:]) + offset += n + except BlockingIOError: + time.sleep(0.001) + # ─── Color palette ─────────────────────────────────────────────────────────── # RGB values matching our Homebrew init_pair fix (xterm-256 slots 16-231): @@ -100,23 +111,40 @@ class Canvas: 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.""" + mask: Optional[List[str]], default_color: Optional[str], + opaque: bool = False): + """Draw a sprite. + '?' is always transparent. + ' ' is transparent when opaque=False (background decorations). + When opaque=True: only INTERIOR spaces (between the first and last + visible char of each line) are drawn as black background. + Leading/trailing spaces stay transparent so shapes don't leave + black blocks outside their visible outline. + """ 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 '' + if opaque: + nsp = [i for i, c in enumerate(line) if c not in (' ', '?')] + interior_start = nsp[0] if nsp else -1 + interior_end = nsp[-1] if nsp else -1 for dc, ch in enumerate(line): - if ch in (' ', '?'): + if ch == '?': 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 + if ch == ' ': + if opaque and interior_start <= dc <= interior_end: + self._ch[row][col] = ' ' + self._ck[row][col] = None + else: + 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 so \033[2J is not @@ -141,6 +169,10 @@ class Canvas: parts.append(RESET if ck is None else _esc(ck)) last_ck = ck parts.append(self._ch[r][c]) + # Erase the last column with the default background (no cursor + # advance → no pending-wrap; handles content left by wider frames). + parts.append(RESET + "\033[K") + last_ck = sentinel parts.append(RESET) return ''.join(parts) @@ -178,7 +210,8 @@ class Entity: death_cb: Optional[Callable] = None, die_time: Optional[float] = None, die_frame: Optional[int] = None, - entity_type: str = ''): + entity_type: str = '', + opaque: bool = True): self.frames = frames self.masks = masks self.x = float(x) @@ -190,6 +223,7 @@ class Entity: self.die_time = die_time self._die_frame = die_frame self.entity_type = entity_type + self.opaque = opaque self.curr_frame = 0.0 self.alive = True self.height = len(frames[0]) if frames else 0 @@ -249,29 +283,10 @@ class Entity: 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) + self.default_color, + self.opaque) -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.""" @@ -310,7 +325,7 @@ class Aquarium: 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.wl = max(3, rows // 3 - 3) self.entities: List[Entity] = [] fns = [self.add_whale, self.add_monster, self.add_swan, @@ -342,7 +357,7 @@ class Aquarium: def resize(self, rows: int, cols: int): self.rows, self.cols = rows, cols - self.wl = max(3, rows // 3) + self.wl = max(3, rows // 3 - 3) self.canvas.resize(rows, cols) self._cs_state = 0 self._cs_portcullis = None @@ -355,8 +370,8 @@ class Aquarium: random.choice(self._random_fns)(dead) def step(self): - if self.cfg.bloody: - self._check_predation() + self._check_predation() + self._check_fishing() self.entities = [e for e in self.entities if e.update(self)] self._update_crab_scene() @@ -384,6 +399,7 @@ class Aquarium: cb_args=[0, 0, 0], default_color='c', die_offscreen=False, + opaque=False, )) # ── Castle ─────────────────────────────────────────────────────────────── @@ -425,6 +441,7 @@ class Aquarium: cb_args=[0, 0, 0], default_color='k', die_offscreen=False, + opaque=False, )) # ── Seaweed ────────────────────────────────────────────────────────────── @@ -458,6 +475,7 @@ class Aquarium: die_offscreen=False, death_cb=lambda e, aq: aq.add_seaweed(e), die_time=die_t, + opaque=False, )) # ── Bubbles ────────────────────────────────────────────────────────────── @@ -478,7 +496,7 @@ class Aquarium: def add_all_fish(self): underwater_rows = max(1, self.rows - (self.wl + 4)) - count = max(15, underwater_rows * self.cols // 120) + count = max(6, underwater_rows * self.cols // 320) for _ in range(count): self.add_fish(None) @@ -838,7 +856,7 @@ yywwwyyyyyyyyyyyyyyyyyyyy 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, + x=x, y=self.wl - 5, cb_args=[speed, 0, 0], default_color='W', die_offscreen=True, @@ -918,7 +936,7 @@ BBBB BBBBB self._add(Entity( frames=frames, masks=masks, - x=x, y=0, + x=x, y=self.wl - 7, cb_args=[speed, 0, 0, 1], default_color='W', die_offscreen=True, @@ -1002,7 +1020,7 @@ BBBB BBBBB self._add(Entity( frames=shapes, masks=[mask] * 4, - x=x, y=2, + x=x, y=self.wl - 4, cb_args=[speed, 0, 0, 0.25], default_color='G', die_offscreen=True, @@ -1091,6 +1109,7 @@ BBBB BBBBB default_color='Y', die_offscreen=True, death_cb=lambda e, aq: aq.random_object(e), + entity_type='big_fish', )) # ── Ducks ──────────────────────────────────────────────────────────────── @@ -1256,7 +1275,7 @@ yy self._add(Entity( frames=[shape], masks=[mask], - x=x, y=1, + x=x, y=self.wl - 6, cb_args=[speed, 0, 0, 0.25], default_color='W', die_offscreen=True, @@ -1266,7 +1285,7 @@ yy # ── Fishhook ───────────────────────────────────────────────────────────── def add_fishhook(self, dead=None): - hook_shape = _p(r""" + hook_part = _p(r""" o || || @@ -1274,18 +1293,36 @@ yy \__// `--' """) - x = random.randint(10, max(11, self.cols - 20)) - target_y = int(self.rows * 0.75) + cord = ' |' # '|' at col 7, aligns with 'o' + cord_mask = ' w' # cord is white; hook uses default_color ('y') + x = random.randint(10, max(11, self.cols - 20)) + start_d = self.wl + max_d = int(self.rows * 0.75) + descent = max(1, max_d - start_d) + fadv = 0.3 # ~1 row per 3 ticks ≈ 0.3 s/row - e = HookEntity( - frames=[hook_shape], masks=None, - x=x, y=-4, - cb_args=[0, 1, 0], - default_color='G', + hook_mask_lines = [''] * len(hook_part) # hook chars → default_color + frames, fmasks = [], [] + for d in range(descent + 1): + n = start_d + d + frames.append([cord] * n + hook_part) + fmasks.append([cord_mask] * n + hook_mask_lines) + for d in range(descent - 1, -1, -1): + n = start_d + d + frames.append([cord] * n + hook_part) + fmasks.append([cord_mask] * n + hook_mask_lines) + + e = Entity( + frames=frames, masks=fmasks, + x=x, y=0, + cb_args=[0, 0, 0, fadv], + default_color='y', # hook = yellow metal die_offscreen=False, - target_y=target_y, + die_frame=int(len(frames) / fadv), death_cb=lambda e, aq: aq.random_object(e), + entity_type='fishhook', ) + e._hook_descent = descent # needed for early retraction on catch self._add(e) # ── Seahorse (Seepferdchen) ─────────────────────────────────────────────── @@ -1442,19 +1479,63 @@ yccw # ── 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 + # Shark always eats fish; in bloody mode big_fish joins the massacre. + predator_types = {'shark'} + if self.cfg.bloody: + predator_types.add('big_fish') + predators = [e for e in self.entities + if e.entity_type in predator_types and e.alive] + fish = [e for e in self.entities + if e.entity_type == 'fish' and e.alive] + for p in predators: + px1 = int(p.x); px2 = px1 + p.width + py1 = int(p.y); py2 = py1 + p.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) + if px1 <= fx <= px2 and py1 <= fy <= py2: + if p.entity_type == 'shark' or self.cfg.bloody: + self.add_splat(fx, fy) f.alive = False + if f.death_cb: + f.death_cb(f, self) + + def _check_fishing(self): + """Fishhook catches fish; caught fish is dragged upward with the hook.""" + HOOK_PART_HEIGHT = 6 + hooks = [e for e in self.entities + if e.entity_type == 'fishhook' and e.alive] + fish = [e for e in self.entities + if e.entity_type == 'fish' and e.alive] + for h in hooks: + fi = h.frame_idx + cord_n = len(h.frames[fi]) - HOOK_PART_HEIGHT + hook_row = int(h.y) + cord_n # row of 'o' + hook_col = int(h.x) + 7 # 'o' at offset 7 + for f in fish: + fy1 = int(f.y); fy2 = fy1 + f.height + fx1 = int(f.x); fx2 = fx1 + f.width + if fy1 <= hook_row <= fy2 and fx1 <= hook_col <= fx2: + f.alive = False + if f.death_cb: + f.death_cb(f, self) + # Jump hook to retraction phase immediately + descent = getattr(h, '_hook_descent', None) + if descent is not None and int(h.curr_frame) <= descent: + h.curr_frame = float(descent) + # Inject fish line into all remaining hook frames so it + # is dragged upward locked to the hook (no separate entity). + fish_raw = (f.frames[f.frame_idx] or ['><>'])[0] or '><>' + fish_stripped = fish_raw.strip() or '><>' + # Centre the fish under the hook's 'o' (col offset 7 in the frame) + pad = max(0, 7 - len(fish_stripped) // 2) + fish_line = ' ' * pad + fish_stripped + fi = int(h.curr_frame) + for i in range(fi, len(h.frames)): + h.frames[i].append(fish_line) + if h.masks and i < len(h.masks) and h.masks[i] is not None: + h.masks[i].append('') + break # one fish per hook per tick # ── Crab scene (portcullis + crab) ──────────────────────────────────────── @@ -1497,7 +1578,7 @@ yccw mask = [' yWy ', ' '] c = CrabEntity( frames=[crab_f0, crab_f1], masks=[mask, mask], - x=float(gx), y=float(gy), + x=float(gx), y=float(gy + 1), cb_args=[-1, 0, 0, 0.25], default_color='y', die_offscreen=False, @@ -1560,6 +1641,8 @@ def main(): 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') + parser.add_argument('--ruler', action='store_true', + help='draw numbered row ruler and exit (diagnostic)') args = parser.parse_args() cfg = Config( @@ -1584,8 +1667,31 @@ def main(): 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()) + # \033[r resets any inherited scroll region to the full screen. + _write_all(fd, (ALT_ON + HIDE_CUR + "\033[r" + "\033[2J\033[H").encode()) rows, cols = term_size() + + if args.ruler: + # Draw numbered ruler for every row to verify actual terminal dimensions. + # The last visible row number tells us the real screen height. + buf = ["\033[H"] + for r in range(rows): + label = f"row {r:3d} (1-idx={r+1}) cols={cols}" + if r == rows - 1: + label = f"\033[7m LAST: row {r} / rows={rows} cols={cols} -- press any key \033[m" + buf.append(f"\033[{r+1};1H\033[0m{label}") + _write_all(fd, ''.join(buf).encode()) + try: + while True: + k = inp.key() + if k is not None: + break + time.sleep(0.05) + finally: + inp.restore() + _write_all(fd, (ALT_OFF + SHOW_CUR).encode()) + return + aq = Aquarium(rows, cols, cfg) try: @@ -1620,7 +1726,7 @@ def main(): 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()) + _write_all(fd, frame.encode()) elapsed = time.monotonic() - t0 sleep = TICK - elapsed @@ -1631,7 +1737,7 @@ def main(): pass finally: inp.restore() - os.write(fd, (SHOW_CUR + ALT_OFF + RESET).encode()) + _write_all(fd, (SHOW_CUR + ALT_OFF + RESET).encode()) if __name__ == '__main__':