mirror of
https://github.com/curly60e/pyblock.git
synced 2026-08-19 13:18:13 +02:00
194 lines
5.9 KiB
Python
194 lines
5.9 KiB
Python
|
|
"""Visual animations for the block clock.
|
||
|
|
|
||
|
|
Mining rain, odometer digit transition, fireworks on milestones.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
from random import choice, randrange
|
||
|
|
|
||
|
|
from cfonts import render
|
||
|
|
|
||
|
|
# Halving blocks for milestone detection
|
||
|
|
HALVING_BLOCKS = {210_000 * i for i in range(1, 65)}
|
||
|
|
|
||
|
|
|
||
|
|
def is_milestone_block(height):
|
||
|
|
"""Check if block is a milestone. Returns description or None."""
|
||
|
|
if height in HALVING_BLOCKS:
|
||
|
|
return f"HALVING #{height // 210_000}"
|
||
|
|
if height % 100_000 == 0:
|
||
|
|
return f"BLOCK {height:,}"
|
||
|
|
if height % 10_000 == 0:
|
||
|
|
return f"BLOCK {height:,}"
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def mining_animation(duration=3.0):
|
||
|
|
"""Matrix-style mining rain animation for new block discovery.
|
||
|
|
|
||
|
|
Uses inline implementation to avoid import issues with terminal_matrix.
|
||
|
|
"""
|
||
|
|
cols, lines = shutil.get_terminal_size((80, 24))
|
||
|
|
chars = [chr(i) for i in range(0x30, 0x80)]
|
||
|
|
green = "\033[32m"
|
||
|
|
bright_green = "\033[1;32m"
|
||
|
|
reset = "\033[0m"
|
||
|
|
|
||
|
|
# Initialize cascades
|
||
|
|
cascades = {}
|
||
|
|
sys.stdout.write("\033[2J\033[H\x1b[?25l")
|
||
|
|
|
||
|
|
end_time = time.time() + duration
|
||
|
|
while time.time() < end_time:
|
||
|
|
# Spawn new cascades
|
||
|
|
if len(cascades) < cols // 2:
|
||
|
|
col = randrange(1, cols + 1)
|
||
|
|
if col not in cascades:
|
||
|
|
speed = randrange(1, 4)
|
||
|
|
length = randrange(4, lines // 2)
|
||
|
|
cascades[col] = {'row': 1, 'speed': speed, 'length': length}
|
||
|
|
|
||
|
|
buf = []
|
||
|
|
to_remove = []
|
||
|
|
for col, c in cascades.items():
|
||
|
|
row = c['row']
|
||
|
|
if row <= lines:
|
||
|
|
char = choice(chars)
|
||
|
|
buf.append(f"\033[{row};{col}H{bright_green}{char}")
|
||
|
|
# Dim the trail
|
||
|
|
trail_row = row - c['length']
|
||
|
|
if 1 <= trail_row <= lines:
|
||
|
|
buf.append(f"\033[{trail_row};{col}H{reset} ")
|
||
|
|
c['row'] += c['speed']
|
||
|
|
if c['row'] - c['length'] > lines:
|
||
|
|
to_remove.append(col)
|
||
|
|
|
||
|
|
for col in to_remove:
|
||
|
|
del cascades[col]
|
||
|
|
|
||
|
|
if buf:
|
||
|
|
sys.stdout.write(''.join(buf))
|
||
|
|
sys.stdout.flush()
|
||
|
|
|
||
|
|
time.sleep(0.03)
|
||
|
|
|
||
|
|
sys.stdout.write(f"\033[2J\033[H{reset}\x1b[?25l")
|
||
|
|
sys.stdout.flush()
|
||
|
|
|
||
|
|
|
||
|
|
def odometer_transition(old_height_str, new_height_str, settings, start_row):
|
||
|
|
"""Animate changing digits like a mechanical odometer.
|
||
|
|
|
||
|
|
Renders intermediate digit values at the positions that changed.
|
||
|
|
"""
|
||
|
|
colors = [settings.get('colorA', 'green'), settings.get('colorB', 'yellow')]
|
||
|
|
font = settings.get('design', 'block')
|
||
|
|
|
||
|
|
# Pad to same length
|
||
|
|
max_len = max(len(old_height_str), len(new_height_str))
|
||
|
|
old = old_height_str.zfill(max_len)
|
||
|
|
new = new_height_str.zfill(max_len)
|
||
|
|
|
||
|
|
# Find which digits changed
|
||
|
|
changed = [i for i in range(max_len) if old[i] != new[i]]
|
||
|
|
|
||
|
|
if not changed:
|
||
|
|
return
|
||
|
|
|
||
|
|
# Animate: show 3 intermediate frames
|
||
|
|
frames = 3
|
||
|
|
for frame in range(frames):
|
||
|
|
intermediate = list(old)
|
||
|
|
for i in changed:
|
||
|
|
old_d = int(old[i])
|
||
|
|
new_d = int(new[i])
|
||
|
|
# Roll through digits
|
||
|
|
step = (old_d + (frame + 1) * (new_d - old_d + 10) // (frames + 1)) % 10
|
||
|
|
if frame == frames - 1:
|
||
|
|
step = new_d
|
||
|
|
intermediate[i] = str(step)
|
||
|
|
|
||
|
|
text = ''.join(intermediate)
|
||
|
|
output = render(text, colors=colors, align='center', font=font)
|
||
|
|
lines = output.rstrip('\n').split('\n')
|
||
|
|
|
||
|
|
buf = []
|
||
|
|
for j, line in enumerate(lines):
|
||
|
|
buf.append(f"\033[{start_row + j};1H\033[2K{line}")
|
||
|
|
sys.stdout.write(''.join(buf))
|
||
|
|
sys.stdout.flush()
|
||
|
|
time.sleep(0.12)
|
||
|
|
|
||
|
|
|
||
|
|
def fireworks_animation(term_width, term_height, duration=5.0):
|
||
|
|
"""ASCII fireworks celebration for milestone blocks."""
|
||
|
|
colors = [
|
||
|
|
"\033[1;31m", # red
|
||
|
|
"\033[1;33m", # yellow
|
||
|
|
"\033[1;32m", # green
|
||
|
|
"\033[1;36m", # cyan
|
||
|
|
"\033[1;35m", # magenta
|
||
|
|
"\033[1;37m", # white
|
||
|
|
]
|
||
|
|
sparks = ['*', '.', '+', 'o', '\u2022', '\u2726', '\u2727', '\u2728']
|
||
|
|
reset = "\033[0m"
|
||
|
|
|
||
|
|
sys.stdout.write("\033[2J\033[H\x1b[?25l")
|
||
|
|
|
||
|
|
end_time = time.time() + duration
|
||
|
|
explosions = []
|
||
|
|
|
||
|
|
while time.time() < end_time:
|
||
|
|
# Spawn new explosion
|
||
|
|
if randrange(5) == 0 or not explosions:
|
||
|
|
cx = randrange(5, term_width - 5)
|
||
|
|
cy = randrange(3, term_height - 3)
|
||
|
|
color = choice(colors)
|
||
|
|
explosions.append({
|
||
|
|
'cx': cx, 'cy': cy, 'color': color,
|
||
|
|
'radius': 0, 'max_radius': randrange(3, 8),
|
||
|
|
'age': 0
|
||
|
|
})
|
||
|
|
|
||
|
|
buf = []
|
||
|
|
alive = []
|
||
|
|
for exp in explosions:
|
||
|
|
exp['age'] += 1
|
||
|
|
exp['radius'] = min(exp['radius'] + 1, exp['max_radius'])
|
||
|
|
|
||
|
|
if exp['age'] > exp['max_radius'] * 3:
|
||
|
|
# Fade: clear spark positions
|
||
|
|
for _ in range(8):
|
||
|
|
dx = randrange(-exp['max_radius'], exp['max_radius'] + 1)
|
||
|
|
dy = randrange(-exp['max_radius'] // 2, exp['max_radius'] // 2 + 1)
|
||
|
|
x = exp['cx'] + dx
|
||
|
|
y = exp['cy'] + dy
|
||
|
|
if 1 <= x <= term_width and 1 <= y <= term_height:
|
||
|
|
buf.append(f"\033[{y};{x}H ")
|
||
|
|
continue
|
||
|
|
|
||
|
|
alive.append(exp)
|
||
|
|
r = exp['radius']
|
||
|
|
for _ in range(r * 4):
|
||
|
|
dx = randrange(-r, r + 1)
|
||
|
|
dy = randrange(-r // 2, r // 2 + 1)
|
||
|
|
x = exp['cx'] + dx
|
||
|
|
y = exp['cy'] + dy
|
||
|
|
if 1 <= x <= term_width and 1 <= y <= term_height:
|
||
|
|
spark = choice(sparks)
|
||
|
|
buf.append(f"\033[{y};{x}H{exp['color']}{spark}")
|
||
|
|
|
||
|
|
explosions = alive
|
||
|
|
|
||
|
|
if buf:
|
||
|
|
sys.stdout.write(''.join(buf) + reset)
|
||
|
|
sys.stdout.flush()
|
||
|
|
|
||
|
|
time.sleep(0.08)
|
||
|
|
|
||
|
|
sys.stdout.write(f"\033[2J\033[H{reset}\x1b[?25l")
|
||
|
|
sys.stdout.flush()
|