feat(saolei): 命令行交互式玩扫雷(无需 VGA 显示器)

- srcs/tb/tb_saolei_play.v:受 cmd.txt 驱动的交互式 TB,每 100us 轮询一个按键字符,
  每 1ms 把显存 dump 到 now.txt
- tools/play_saolei.tcl:xsim 里 k c / k u / k f 一步一按键,自动把 now.txt 画到终端
- saolei/tools/fb2png.py:新增 --term,用 ANSI 24 位色在终端画 40x30 棋盘,光标格反色
- .gitignore:放行 create_project_saolei.tcl
实测:k c 进关卡、k uuuu 光标精确移动 4 格、k c 首击展开出数字 1/2
This commit is contained in:
2026-09-19 19:54:56 +08:00
parent 0908580e46
commit 132c9202f6
5 changed files with 515 additions and 20 deletions
+68 -20
View File
@@ -3,6 +3,7 @@
用法:
py saolei/tools/fb2png.py <dump.txt> [输出.png]
py saolei/tools/fb2png.py --term <dump.txt> # 只在终端打印彩色棋盘(交互玩扫雷用)
dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
CURSOR <row> <col>
@@ -101,15 +102,77 @@ def parse_dump(path):
return cur, ctrl, fb
def cell_char(g):
"""字形号 -> 一个可见字符(字形 1..8 就是数字 1..8)。"""
if g in GLYPH_CH:
return GLYPH_CH[g]
if 1 <= g <= 8:
return chr(ord('0') + g)
return ' '
def dump_note(path):
"""把 dump 开头的 TIME / CURSOR / STATS 拼成一行,当终端画面的标题。"""
note = []
try:
with open(path, encoding="utf-8", errors="replace") as f:
for line in f:
if line.startswith(("TIME", "CURSOR", "STATS")):
note.append(line.strip())
elif line.startswith("FB"):
break
except OSError:
pass
return " ".join(note)
def draw_term(fb, cur, cursor_box, note=""):
"""在终端用 ANSI 24 位颜色画 40x30 棋盘(每格一个字符),光标格反色。"""
try: # Windows 控制台默认不是 UTF-8
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
out = ["\x1b[H\x1b[2J"] # 回左上角 + 清屏(连续刷新不闪屏残留)
if note:
out.append(" " + note + "\n")
out.append(" " + "".join(str(x % 10) for x in range(COLS)) + "\n")
for y in range(ROWS):
cells = []
for x in range(COLS):
cell = fb[y * COLS + x]
g, c = cell & 0xF, (cell >> 4) & 0x7
r, gg, b = PAL[c]
ch = cell_char(g)
if cursor_box and (x, y) == cur:
cells.append("\x1b[7m\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17,
ch if ch != ' ' else '_'))
else:
cells.append("\x1b[38;2;%d;%d;%dm%s" % (r * 17, gg * 17, b * 17, ch))
out.append("%2d " % y + "".join(cells) + "\x1b[0m\n")
out.append("\x1b[0m")
out.append("图例: #=未揭开 P=旗 *=雷 X=爆炸 ?=问号 1..8=数字 空格=已揭开的空地;反色格 = 光标\n")
sys.stdout.write("".join(out))
sys.stdout.flush()
def main():
if len(sys.argv) < 2:
args = list(sys.argv[1:])
term_only = "--term" in args
if term_only:
args.remove("--term")
if not args:
raise SystemExit(__doc__)
src = sys.argv[1]
dst = sys.argv[2] if len(sys.argv) > 2 else src.rsplit(".", 1)[0] + ".png"
src = args[0]
dst = args[1] if len(args) > 1 else src.rsplit(".", 1)[0] + ".png"
cur, ctrl, fb = parse_dump(src)
bg = (ctrl >> 4) & 0x7
cursor_box = bool(ctrl & 1) and cur[0] is not None
if term_only:
draw_term(fb, cur, cursor_box, dump_note(src))
return
print("光标 = (%s, %s) 光标可见 = %s 背景色 = %d"
% (cur[0], cur[1], bool(ctrl & 1), bg))
@@ -135,23 +198,8 @@ def main():
write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows)
print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX))
# ---- 终端 ASCII 图 ----
print("+" + "-" * COLS + "+")
for y in range(ROWS):
line = ""
for x in range(COLS):
cell = fb[y * COLS + x]
g, c = cell & 0xF, (cell >> 4) & 0x7
if cursor_box and (x, y) == cur:
line += "["
elif g in GLYPH_CH:
line += GLYPH_CH[g]
else:
line += chr(ord('0') + g) # 字形 1..8 就是数字 1..8
line = line.replace("[", "[]" * 1) # 光标只提示左边界,避免打乱列数
print("|" + (line + " " * COLS)[:COLS] + "|")
print("+" + "-" * COLS + "+")
print("图例: #=未揭开 P=旗 *=雷 X=爆炸 ?=问号 -=横线 ==实心 1..8=数字")
# ---- 终端 ASCII 图(彩色版;PNG 已经写好了,这里只是方便快速看一眼)----
draw_term(fb, cur, cursor_box, dump_note(src))
main()