Features: Opacity, Angelhaken, Predation, wl-relative Positionen, Resize-Fix
- _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
This commit is contained in:
parent
d87088eff0
commit
0396ec83c6
1 changed files with 168 additions and 62 deletions
|
|
@ -15,8 +15,8 @@ class Config:
|
||||||
speed: float = 1.0 # global speed multiplier (TICK = 0.1 / speed)
|
speed: float = 1.0 # global speed multiplier (TICK = 0.1 / speed)
|
||||||
no_shark: bool = False
|
no_shark: bool = False
|
||||||
no_ship: bool = False
|
no_ship: bool = False
|
||||||
bloody: bool = False # shark eats fish with gore splats
|
bloody: bool = False # massacre: all predators eat fish with blood splats
|
||||||
vegan: bool = False # disable predators (shark, big_fish, fishhook)
|
vegan: bool = False # peaceful: no predators (shark, big_fish, fishhook)
|
||||||
any_key: bool = False # any key exits
|
any_key: bool = False # any key exits
|
||||||
|
|
||||||
# ─── Terminal control ────────────────────────────────────────────────────────
|
# ─── 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 _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 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 ───────────────────────────────────────────────────────────
|
# ─── Color palette ───────────────────────────────────────────────────────────
|
||||||
# RGB values matching our Homebrew init_pair fix (xterm-256 slots 16-231):
|
# RGB values matching our Homebrew init_pair fix (xterm-256 slots 16-231):
|
||||||
|
|
@ -100,23 +111,40 @@ class Canvas:
|
||||||
self._ck[row][col] = ck
|
self._ck[row][col] = ck
|
||||||
|
|
||||||
def put_sprite(self, y: int, x: int, shape: List[str],
|
def put_sprite(self, y: int, x: int, shape: List[str],
|
||||||
mask: Optional[List[str]], default_color: Optional[str]):
|
mask: Optional[List[str]], default_color: Optional[str],
|
||||||
"""Draw a sprite. Spaces and '?' characters are transparent."""
|
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
|
rows, cols = self.rows, self.cols
|
||||||
for dr, line in enumerate(shape):
|
for dr, line in enumerate(shape):
|
||||||
row = y + dr
|
row = y + dr
|
||||||
if row < 0 or row >= rows:
|
if row < 0 or row >= rows:
|
||||||
continue
|
continue
|
||||||
mline = (mask[dr] if dr < len(mask) else '') if mask else ''
|
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):
|
for dc, ch in enumerate(line):
|
||||||
if ch in (' ', '?'):
|
if ch == '?':
|
||||||
continue
|
continue
|
||||||
col = x + dc
|
col = x + dc
|
||||||
if 0 <= col < cols:
|
if 0 <= col < cols:
|
||||||
mk = mline[dc] if dc < len(mline) else ' '
|
if ch == ' ':
|
||||||
ck = mk if mk in _C else default_color
|
if opaque and interior_start <= dc <= interior_end:
|
||||||
self._ch[row][col] = ch
|
self._ch[row][col] = ' '
|
||||||
self._ck[row][col] = ck
|
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:
|
def render(self) -> str:
|
||||||
# \033[H homes the cursor; every cell is rewritten so \033[2J is not
|
# \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))
|
parts.append(RESET if ck is None else _esc(ck))
|
||||||
last_ck = ck
|
last_ck = ck
|
||||||
parts.append(self._ch[r][c])
|
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)
|
parts.append(RESET)
|
||||||
return ''.join(parts)
|
return ''.join(parts)
|
||||||
|
|
||||||
|
|
@ -178,7 +210,8 @@ class Entity:
|
||||||
death_cb: Optional[Callable] = None,
|
death_cb: Optional[Callable] = None,
|
||||||
die_time: Optional[float] = None,
|
die_time: Optional[float] = None,
|
||||||
die_frame: Optional[int] = None,
|
die_frame: Optional[int] = None,
|
||||||
entity_type: str = ''):
|
entity_type: str = '',
|
||||||
|
opaque: bool = True):
|
||||||
self.frames = frames
|
self.frames = frames
|
||||||
self.masks = masks
|
self.masks = masks
|
||||||
self.x = float(x)
|
self.x = float(x)
|
||||||
|
|
@ -190,6 +223,7 @@ class Entity:
|
||||||
self.die_time = die_time
|
self.die_time = die_time
|
||||||
self._die_frame = die_frame
|
self._die_frame = die_frame
|
||||||
self.entity_type = entity_type
|
self.entity_type = entity_type
|
||||||
|
self.opaque = opaque
|
||||||
self.curr_frame = 0.0
|
self.curr_frame = 0.0
|
||||||
self.alive = True
|
self.alive = True
|
||||||
self.height = len(frames[0]) if frames else 0
|
self.height = len(frames[0]) if frames else 0
|
||||||
|
|
@ -249,29 +283,10 @@ class Entity:
|
||||||
canvas.put_sprite(int(self.y), int(self.x),
|
canvas.put_sprite(int(self.y), int(self.x),
|
||||||
self.frames[fi],
|
self.frames[fi],
|
||||||
self.masks[fi] if self.masks and fi < len(self.masks) else None,
|
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):
|
class CrabEntity(Entity):
|
||||||
"""Crab that walks out from the castle gate and then returns."""
|
"""Crab that walks out from the castle gate and then returns."""
|
||||||
|
|
@ -310,7 +325,7 @@ class Aquarium:
|
||||||
self.canvas = Canvas(rows, cols)
|
self.canvas = Canvas(rows, cols)
|
||||||
# wl = first row of the wave area, proportional like Perl's int($lines/3).
|
# 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).
|
# 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] = []
|
self.entities: List[Entity] = []
|
||||||
|
|
||||||
fns = [self.add_whale, self.add_monster, self.add_swan,
|
fns = [self.add_whale, self.add_monster, self.add_swan,
|
||||||
|
|
@ -342,7 +357,7 @@ 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.wl = max(3, rows // 3)
|
self.wl = max(3, rows // 3 - 3)
|
||||||
self.canvas.resize(rows, cols)
|
self.canvas.resize(rows, cols)
|
||||||
self._cs_state = 0
|
self._cs_state = 0
|
||||||
self._cs_portcullis = None
|
self._cs_portcullis = None
|
||||||
|
|
@ -355,8 +370,8 @@ 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._check_predation()
|
self._check_fishing()
|
||||||
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()
|
self._update_crab_scene()
|
||||||
|
|
||||||
|
|
@ -384,6 +399,7 @@ class Aquarium:
|
||||||
cb_args=[0, 0, 0],
|
cb_args=[0, 0, 0],
|
||||||
default_color='c',
|
default_color='c',
|
||||||
die_offscreen=False,
|
die_offscreen=False,
|
||||||
|
opaque=False,
|
||||||
))
|
))
|
||||||
|
|
||||||
# ── Castle ───────────────────────────────────────────────────────────────
|
# ── Castle ───────────────────────────────────────────────────────────────
|
||||||
|
|
@ -425,6 +441,7 @@ class Aquarium:
|
||||||
cb_args=[0, 0, 0],
|
cb_args=[0, 0, 0],
|
||||||
default_color='k',
|
default_color='k',
|
||||||
die_offscreen=False,
|
die_offscreen=False,
|
||||||
|
opaque=False,
|
||||||
))
|
))
|
||||||
|
|
||||||
# ── Seaweed ──────────────────────────────────────────────────────────────
|
# ── Seaweed ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -458,6 +475,7 @@ class Aquarium:
|
||||||
die_offscreen=False,
|
die_offscreen=False,
|
||||||
death_cb=lambda e, aq: aq.add_seaweed(e),
|
death_cb=lambda e, aq: aq.add_seaweed(e),
|
||||||
die_time=die_t,
|
die_time=die_t,
|
||||||
|
opaque=False,
|
||||||
))
|
))
|
||||||
|
|
||||||
# ── Bubbles ──────────────────────────────────────────────────────────────
|
# ── Bubbles ──────────────────────────────────────────────────────────────
|
||||||
|
|
@ -478,7 +496,7 @@ class Aquarium:
|
||||||
|
|
||||||
def add_all_fish(self):
|
def add_all_fish(self):
|
||||||
underwater_rows = max(1, self.rows - (self.wl + 4))
|
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):
|
for _ in range(count):
|
||||||
self.add_fish(None)
|
self.add_fish(None)
|
||||||
|
|
||||||
|
|
@ -838,7 +856,7 @@ yywwwyyyyyyyyyyyyyyyyyyyy
|
||||||
self._add(Entity(
|
self._add(Entity(
|
||||||
frames=[shape_r if going_right else shape_l],
|
frames=[shape_r if going_right else shape_l],
|
||||||
masks=[mask_r if going_right else mask_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],
|
cb_args=[speed, 0, 0],
|
||||||
default_color='W',
|
default_color='W',
|
||||||
die_offscreen=True,
|
die_offscreen=True,
|
||||||
|
|
@ -918,7 +936,7 @@ BBBB BBBBB
|
||||||
|
|
||||||
self._add(Entity(
|
self._add(Entity(
|
||||||
frames=frames, masks=masks,
|
frames=frames, masks=masks,
|
||||||
x=x, y=0,
|
x=x, y=self.wl - 7,
|
||||||
cb_args=[speed, 0, 0, 1],
|
cb_args=[speed, 0, 0, 1],
|
||||||
default_color='W',
|
default_color='W',
|
||||||
die_offscreen=True,
|
die_offscreen=True,
|
||||||
|
|
@ -1002,7 +1020,7 @@ BBBB BBBBB
|
||||||
|
|
||||||
self._add(Entity(
|
self._add(Entity(
|
||||||
frames=shapes, masks=[mask] * 4,
|
frames=shapes, masks=[mask] * 4,
|
||||||
x=x, y=2,
|
x=x, y=self.wl - 4,
|
||||||
cb_args=[speed, 0, 0, 0.25],
|
cb_args=[speed, 0, 0, 0.25],
|
||||||
default_color='G',
|
default_color='G',
|
||||||
die_offscreen=True,
|
die_offscreen=True,
|
||||||
|
|
@ -1091,6 +1109,7 @@ BBBB BBBBB
|
||||||
default_color='Y',
|
default_color='Y',
|
||||||
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='big_fish',
|
||||||
))
|
))
|
||||||
|
|
||||||
# ── Ducks ────────────────────────────────────────────────────────────────
|
# ── Ducks ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1256,7 +1275,7 @@ yy
|
||||||
|
|
||||||
self._add(Entity(
|
self._add(Entity(
|
||||||
frames=[shape], masks=[mask],
|
frames=[shape], masks=[mask],
|
||||||
x=x, y=1,
|
x=x, y=self.wl - 6,
|
||||||
cb_args=[speed, 0, 0, 0.25],
|
cb_args=[speed, 0, 0, 0.25],
|
||||||
default_color='W',
|
default_color='W',
|
||||||
die_offscreen=True,
|
die_offscreen=True,
|
||||||
|
|
@ -1266,7 +1285,7 @@ yy
|
||||||
# ── Fishhook ─────────────────────────────────────────────────────────────
|
# ── Fishhook ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def add_fishhook(self, dead=None):
|
def add_fishhook(self, dead=None):
|
||||||
hook_shape = _p(r"""
|
hook_part = _p(r"""
|
||||||
o
|
o
|
||||||
||
|
||
|
||||||
||
|
||
|
||||||
|
|
@ -1274,18 +1293,36 @@ yy
|
||||||
\__//
|
\__//
|
||||||
`--'
|
`--'
|
||||||
""")
|
""")
|
||||||
x = random.randint(10, max(11, self.cols - 20))
|
cord = ' |' # '|' at col 7, aligns with 'o'
|
||||||
target_y = int(self.rows * 0.75)
|
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(
|
hook_mask_lines = [''] * len(hook_part) # hook chars → default_color
|
||||||
frames=[hook_shape], masks=None,
|
frames, fmasks = [], []
|
||||||
x=x, y=-4,
|
for d in range(descent + 1):
|
||||||
cb_args=[0, 1, 0],
|
n = start_d + d
|
||||||
default_color='G',
|
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,
|
die_offscreen=False,
|
||||||
target_y=target_y,
|
die_frame=int(len(frames) / fadv),
|
||||||
death_cb=lambda e, aq: aq.random_object(e),
|
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)
|
self._add(e)
|
||||||
|
|
||||||
# ── Seahorse (Seepferdchen) ───────────────────────────────────────────────
|
# ── Seahorse (Seepferdchen) ───────────────────────────────────────────────
|
||||||
|
|
@ -1442,19 +1479,63 @@ yccw
|
||||||
# ── Predation (--bloody) ─────────────────────────────────────────────────
|
# ── Predation (--bloody) ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def _check_predation(self):
|
def _check_predation(self):
|
||||||
sharks = [e for e in self.entities
|
# Shark always eats fish; in bloody mode big_fish joins the massacre.
|
||||||
if e.entity_type == 'shark' and e.alive]
|
predator_types = {'shark'}
|
||||||
fish = [e for e in self.entities
|
if self.cfg.bloody:
|
||||||
if e.entity_type == 'fish' and e.alive]
|
predator_types.add('big_fish')
|
||||||
for s in sharks:
|
predators = [e for e in self.entities
|
||||||
sx1 = int(s.x); sx2 = sx1 + s.width
|
if e.entity_type in predator_types and e.alive]
|
||||||
sy1 = int(s.y); sy2 = sy1 + s.height
|
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:
|
for f in fish:
|
||||||
fx = int(f.x) + f.width // 2
|
fx = int(f.x) + f.width // 2
|
||||||
fy = int(f.y) + f.height // 2
|
fy = int(f.y) + f.height // 2
|
||||||
if sx1 <= fx <= sx2 and sy1 <= fy <= sy2:
|
if px1 <= fx <= px2 and py1 <= fy <= py2:
|
||||||
self.add_splat(fx, fy)
|
if p.entity_type == 'shark' or self.cfg.bloody:
|
||||||
|
self.add_splat(fx, fy)
|
||||||
f.alive = False
|
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) ────────────────────────────────────────
|
# ── Crab scene (portcullis + crab) ────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -1497,7 +1578,7 @@ yccw
|
||||||
mask = [' yWy ', ' ']
|
mask = [' yWy ', ' ']
|
||||||
c = CrabEntity(
|
c = CrabEntity(
|
||||||
frames=[crab_f0, crab_f1], masks=[mask, mask],
|
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],
|
cb_args=[-1, 0, 0, 0.25],
|
||||||
default_color='y',
|
default_color='y',
|
||||||
die_offscreen=False,
|
die_offscreen=False,
|
||||||
|
|
@ -1560,6 +1641,8 @@ def main():
|
||||||
help='any key press exits (default: q/Q/Esc/Ctrl-C)')
|
help='any key press exits (default: q/Q/Esc/Ctrl-C)')
|
||||||
parser.add_argument('--debug', action='store_true',
|
parser.add_argument('--debug', action='store_true',
|
||||||
help='show terminal size / entity count overlay')
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
cfg = Config(
|
cfg = Config(
|
||||||
|
|
@ -1584,8 +1667,31 @@ def main():
|
||||||
fd = sys.stdout.fileno()
|
fd = sys.stdout.fileno()
|
||||||
# Activate alternate screen first, THEN read terminal size so the
|
# Activate alternate screen first, THEN read terminal size so the
|
||||||
# size reflects the actual pane dimensions (important in split panes).
|
# 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()
|
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)
|
aq = Aquarium(rows, cols, cfg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -1620,7 +1726,7 @@ def main():
|
||||||
f" ents={len(aq.entities)}"
|
f" ents={len(aq.entities)}"
|
||||||
f" fish_y=[{','.join(fish_ys)}] ")
|
f" fish_y=[{','.join(fish_ys)}] ")
|
||||||
frame += f"\033[1;1H\033[0;7m{dbg}\033[m"
|
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
|
elapsed = time.monotonic() - t0
|
||||||
sleep = TICK - elapsed
|
sleep = TICK - elapsed
|
||||||
|
|
@ -1631,7 +1737,7 @@ def main():
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
inp.restore()
|
inp.restore()
|
||||||
os.write(fd, (SHOW_CUR + ALT_OFF + RESET).encode())
|
_write_all(fd, (SHOW_CUR + ALT_OFF + RESET).encode())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue