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
+1
View File
@@ -4,6 +4,7 @@ pipelined-cpu/*
!pipelined-cpu/tools/ !pipelined-cpu/tools/
!pipelined-cpu/programs/ !pipelined-cpu/programs/
!pipelined-cpu/create_project.tcl !pipelined-cpu/create_project.tcl
!pipelined-cpu/create_project_saolei.tcl
# 本地临时目录(仿真脚本、日志、中间文件) # 本地临时目录(仿真脚本、日志、中间文件)
_tmp/ _tmp/
+170
View File
@@ -0,0 +1,170 @@
# ============================================================================
# create_project_saolei.tcl —— 一键重建【扫雷专用】Vivado 工程
# FPGA:Nexys4DDR / xc7a100tcsg324-1(Vivado 2020.2)
#
# 与 create_project.tcl 的区别(那个是"通用/可切换程序"的脚本):
# * 只装配扫雷需要的东西:rtl/*.v + board/cpu21_board_top.v + 扫雷 ROM 头文件
# * sources_1 与 sim_1 都固定定义宏 **SAOLEI_ROM**(不用再改任何开关)
# * 工程名是独立的 pipelined-cpu-saolei,跟通用工程 pipelined-cpu 互不影响,
# 两个工程可以并存;本脚本只清理自己的生成物。
# * sim_1 顶层直接就是 tb_saolei(GUI 里 Run Simulation 就能出画面 dump)
#
# 用法(在 Vivado Tcl Shell 里):
# source create_project_saolei.tcl
# 或命令行:
# vivado -mode batch -source create_project_saolei.tcl -log create_saolei.log
#
# 前置条件:扫雷的 ROM 镜像必须先由 C 程序生成
# cd saolei
# powershell -ExecutionPolicy Bypass -File tools\build.ps1 `
# -Prefix 'C:\Users\29653\tools\riscv\xpack-riscv-none-elf-gcc-15.2.0-1\bin\riscv-none-elf-'
# → 产出 saolei/build/saolei.hex 与 pipelined-cpu/srcs/board/cpu21_saolei_rom.vh
# (改了 saolei/sw/*.c 之后要重跑这条命令,再综合/再跑仿真)
#
# 不想自动清理旧工程:把下面的 clean_first 改成 0。
# 想建完就顺手综合:把 run_synth 改成 1(2020.2 上约 1~2 分钟)。
#
# 附:没有 VGA 显示器时怎么"看/玩"扫雷
# 看:本工程的 sim_1 跑 tb_saolei,会把显存 dump 成文本;再用
# py saolei\tools\fb2png.py <dump.txt> <out.png> 还原成 640x480 画面。
# 玩:见 pipelined-cpu/tools/play_saolei.tcl(命令行交互式仿真,一步一截图)。
# ============================================================================
set origin_dir [file dirname [file normalize [info script]]]
set proj_name "pipelined-cpu-saolei"
set part_name "xc7a100tcsg324-1"
# ---------------------------------------------------------------- 自检
set saolei_rom "$origin_dir/srcs/board/cpu21_saolei_rom.vh"
set required [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
"$origin_dir/srcs/constrs/cpu21_board_top.xdc" \
"$origin_dir/srcs/tb/tb_saolei.v"]
if {[llength [glob -nocomplain "$origin_dir/srcs/rtl/*.v"]] == 0} {
lappend required "$origin_dir/srcs/rtl/*.v"
}
set missing {}
foreach f $required { if {![file exists $f]} { lappend missing $f } }
if {[llength $missing] > 0} {
puts "ERROR: 缺少下列文件,请确认 srcs/ 目录完整:"
foreach f $missing { puts " $f" }
return 1
}
if {![file exists $saolei_rom]} {
puts "ERROR: 还没有扫雷 ROM 镜像:$saolei_rom"
puts " 先在 saolei/ 目录跑 tools\\build.ps1 生成它(见本脚本头部注释)。"
return 1
}
# ---------------------------------------------------------------- 清理旧工程
# 和 create_project.tcl 同一套策略:只删生成物,srcs/ 等源文件一律不动。
set clean_first 1
set run_synth 0
set synth_jobs 4
if {$clean_first} {
set proj_items [list \
"$origin_dir/$proj_name.xpr" \
"$origin_dir/$proj_name.srcs" \
"$origin_dir/$proj_name.runs" \
"$origin_dir/$proj_name.sim" \
"$origin_dir/$proj_name.cache" \
"$origin_dir/$proj_name.hw" \
"$origin_dir/$proj_name.gen" \
"$origin_dir/$proj_name.ip_user_files" \
"$origin_dir/$proj_name.lpr" \
"$origin_dir/.Xil"]
set junk_items [list \
"$origin_dir/xsim.dir" \
"$origin_dir/webtalk.jou" \
"$origin_dir/webtalk.log" \
"$origin_dir/usage_statistics_webtalk.html" \
"$origin_dir/usage_statistics_webtalk.xml"]
foreach pat {vivado*.jou vivado*.log vivado*.str} {
foreach f [glob -nocomplain "$origin_dir/$pat"] { lappend junk_items $f }
}
set blocked {}
set removed 0
foreach {group items} [list proj $proj_items junk $junk_items] {
foreach d $items {
if {![file exists $d]} { continue }
if {[catch {file delete -force $d} err]} {
puts " WARNING: 删不掉 [file tail $d]($err)"
if {$group eq "proj"} { lappend blocked [file tail $d] }
} else {
puts " 已删除:[file tail $d]"
incr removed
}
}
}
puts "旧工程清理完成(删除 $removed 项,只动 $proj_name.* 与自己产生的临时文件)"
if {[llength $blocked] > 0} {
puts "ERROR: 这些工程目录删不掉:$blocked"
puts " 它们正被另一个 Vivado(GUI / Hardware Manager)占用,关掉后重跑。"
return 1
}
} else {
if {[file exists "$origin_dir/$proj_name.xpr"] || [file exists "$origin_dir/$proj_name.runs"]} {
puts "ERROR: 工程已存在且 clean_first = 0:$origin_dir/$proj_name.xpr"
puts " 请先删掉它,或把 clean_first 改成 1。"
return 1
}
}
# ---------------------------------------------------------------- 建工程
# 只用 -part,不依赖 Digilent 板卡文件(引脚全在 srcs/constrs/*.xdc 里)。
create_project $proj_name $origin_dir -part $part_name -force
# 设计源文件:处理器 RTL + 板级顶层 + 扫雷 ROM
add_files -norecurse -fileset sources_1 [glob "$origin_dir/srcs/rtl/*.v"]
add_files -norecurse -fileset sources_1 [list \
"$origin_dir/srcs/board/cpu21_board_top.v" \
$saolei_rom]
set_property file_type {Verilog Header} [get_files $saolei_rom]
# 关键:两个 fileset 都定义 SAOLEI_ROM,cpu21_board_top.v 的 `ifdef 链才会选中扫雷 ROM。
# (sources_1 管综合/上板,sim_1 管 GUI 仿真;sim_1 不定义的话会跑成走马灯。)
set_property verilog_define {SAOLEI_ROM} [get_filesets sources_1]
set_property verilog_define {SAOLEI_ROM} [get_filesets sim_1]
# 约束(引脚 + 时钟分频)
add_files -norecurse -fileset constrs_1 [glob "$origin_dir/srcs/constrs/*.xdc"]
# 仿真:扫雷调试台。POINTER_SAMPLE_DIV 由 tb 自己 defparam 压小,上板仍是 50000。
add_files -norecurse -fileset sim_1 [list "$origin_dir/srcs/tb/tb_saolei.v"]
# ---------------------------------------------------------------- 顶层与顺序
set_property top cpu21_board_top [get_filesets sources_1]
set_property top tb_saolei [get_filesets sim_1]
update_compile_order -fileset sources_1
update_compile_order -fileset sim_1
# GUI 仿真默认只跑 1000 ns,这里改成跑完(否则看起来像卡住)
catch { set_property -name {xsim.simulate.runtime} -value {all} -objects [get_filesets sim_1] }
# ---------------------------------------------------------------- 可选:顺手综合
if {$run_synth} {
puts "开始综合 synth_1(run_synth = 1)..."
reset_run synth_1
launch_runs synth_1 -jobs $synth_jobs
wait_on_run synth_1
if {[get_property PROGRESS [get_runs synth_1]] ne "100%"} {
puts "ERROR: 综合未完成,状态 = [get_property STATUS [get_runs synth_1]]"
return 1
}
puts "综合完成:0 error 即可 Run Implementation → Generate Bitstream。"
}
# ---------------------------------------------------------------- 结果汇总
puts ""
puts "工程已创建:$origin_dir/$proj_name.xpr"
puts " sources_1 顶层 = cpu21_board_top,程序 = 扫雷(已定义 SAOLEI_ROM)"
puts " sources_1 ROM = [file tail $saolei_rom]"
puts " sim_1 顶层 = tb_saolei(跑仿真会打印显存 dump,用 fb2png.py 还原成 PNG)"
puts " 约束 = [file tail [lindex [glob "$origin_dir/srcs/constrs/*.xdc"] 0]]"
puts " 下一步:Run Synthesis → Run Implementation → Generate Bitstream"
puts " (本工程已固定扫雷,不用再改 create_project.tcl 里的三个开关)"
puts " 上板操作:BTNU/D/L/R 移动光标,BTNC = 左键,SW3 = 插旗模式"
puts " 注意:CPU 时钟 = 100 MHz / CLK_DIV(默认 2) = 50 MHz;改分频比时"
puts " cpu21_board_top.v 的 CLK_DIV 与 cpu21_board_top.xdc 的 -divide_by 要一起改。"
+183
View File
@@ -0,0 +1,183 @@
`timescale 1ns / 1ps
// =============================================================================
// tb_saolei_play —— 没有 VGA 显示器时"在命令行里玩扫雷"用的测试平台
//
// 与 tb_saolei 的区别:没有写死的游戏脚本,改成受外部文件驱动:
//
// cmd.txt 外部(xsim 的 Tcl,见 pipelined-cpu/tools/play_saolei.tcl)往里写
// 按键字符;本 TB 每 100 us 仿真时间轮询一次,读到**一个**字符就执行
// 对应的按键脉冲,然后把文件清空。(多个动作用多次发送实现,
// play_saolei.tcl 的 k 命令会自动把 "uuc" 拆成三次发。)
// u/w 光标上移 d/s 光标下移
// l/a 光标左移 r 光标右移
// c 左键点击(BTNC)
// f 插旗模式点击(先按 SW3 再点击,点上/取消旗帜)
// p 只切换 SW3(插旗模式开关,不点击)
// g 按板上的 go 键
// z 复位(拉低 rst_n 再放开,等于重新上电)
//
// now.txt 每 1 ms 仿真时间把当前显存 dump 一次(格式同 tb_saolei):
// py saolei/tools/fb2png.py --term now.txt -> 终端里的彩色棋盘
// py saolei/tools/fb2png.py now.txt a.png -> 640x480 截图
//
// 编译(两个 -d 都要带 SAOLEI_ROM):
// xvlog -d SAOLEI_ROM -i srcs/board -i srcs/rtl srcs/rtl/*.v \
// srcs/board/cpu21_board_top.v srcs/tb/tb_saolei_play.v
// xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
// xsim saolei_play -tclbatch tools/play_saolei.tcl
//
// 说明:和 tb_saolei 一样用 POINTER_SAMPLE_DIV=50 压缩按键去抖(上板仍是 50000)。
// =============================================================================
module tb_saolei_play;
// ---- 板级顶层端口(全部由本 TB / cmd.txt 驱动,不用 add_force)----
reg clk100 = 1'b0;
reg rst_n = 1'b0;
reg go = 1'b0;
reg [ 2:0] irq_sw = 3'b000;
reg btnu = 1'b0;
reg btnd = 1'b0;
reg btnl = 1'b0;
reg btnr = 1'b0;
reg sw_flag = 1'b0;
wire [15:0] led;
wire led_we;
wire [ 7:0] an;
wire [ 6:0] seg;
wire dp;
wire [ 3:0] vga_r, vga_g, vga_b;
wire vga_hs, vga_vs;
always #5 clk100 = ~clk100;
cpu21_board_top #(
.POINTER_SAMPLE_DIV(50) // 仿真里加速按键去抖(上板默认 50000)
) dut (
.clk100 (clk100),
.rst_n (rst_n),
.go (go),
.irq_sw (irq_sw),
.btnu (btnu),
.btnd (btnd),
.btnl (btnl),
.btnr (btnr),
.sw_flag (sw_flag),
.led (led),
.led_we (led_we),
.an (an),
.seg (seg),
.dp (dp),
.vga_r (vga_r),
.vga_g (vga_g),
.vga_b (vga_b),
.vga_hs (vga_hs),
.vga_vs (vga_vs),
.ps2_clk (1'b1),
.ps2_data(1'b1)
);
// ---- 一个按键脉冲:按住 hold_us 再松开,然后留 200 us 让 CPU 消化事件 ----
task pulse;
input integer which;
input integer hold_us;
begin
case (which)
1: btnu = 1'b1;
2: btnd = 1'b1;
3: btnl = 1'b1;
4: btnr = 1'b1;
default: go = 1'b1;
endcase
#(hold_us * 1000);
case (which)
1: btnu = 1'b0;
2: btnd = 1'b0;
3: btnl = 1'b0;
4: btnr = 1'b0;
default: go = 1'b0;
endcase
#(200 * 1000);
end
endtask
// ---- 把 cmd.txt 里的一个字符变成按键动作 ----
// 注意:一次只取一个字符($fgetc),不用 $fgets/字符串,
// 这样不会踩 "reg [8*N:1] 里第一个字符落在哪个字节" 的坑。
task do_key;
input [7:0] ch;
begin
case (ch)
8'h75, 8'h77: pulse(1, 20); // u / w 上
8'h64, 8'h73: pulse(2, 20); // d / s 下
8'h6C, 8'h61: pulse(3, 20); // l / a 左
8'h72: pulse(4, 20); // r 右
8'h63: pulse(0, 30); // c 左键点击
8'h67: pulse(0, 30); // g go 键(同一次点击)
8'h66: begin // f 插旗模式点击
sw_flag = 1'b1;
#(30 * 1000);
pulse(0, 30);
#(30 * 1000);
sw_flag = 1'b0;
#(30 * 1000);
end
8'h70: begin // p 只切换插旗开关
sw_flag = ~sw_flag;
#(50 * 1000);
end
8'h7A: begin // z 复位
rst_n = 1'b0;
#(2 * 1000);
rst_n = 1'b1;
#(500 * 1000);
end
default: ; // 其它字符忽略
endcase
end
endtask
integer cfp;
integer chc;
// ---- 命令轮询:每 100 us 看一次 cmd.txt(只读第一个字符)----
initial begin
#2000;
rst_n = 1'b1; // 上电复位释放
forever begin
#(100 * 1000);
cfp = $fopen("cmd.txt", "r");
if (cfp != 0) begin
chc = $fgetc(cfp);
$fclose(cfp);
cfp = $fopen("cmd.txt", "w"); // 处理掉就清空,避免重复执行
if (cfp != 0) $fclose(cfp);
if ((chc > 0) && (chc != 8'h0A) && (chc != 8'h0D) && (chc != 8'h20))
do_key(chc[7:0]);
end
end
end
// ---- 画面 dump:每 1 ms 覆盖写一次 now.txt ----
integer df, w2, b2;
initial begin
forever begin
#(1 * 1000 * 1000);
df = $fopen("now.txt", "w");
if (df != 0) begin
$fwrite(df, "TIME %0t\n", $time);
$fwrite(df, "CURSOR %0d %0d\n",
dut.u_vga.cursor_i[12:8], dut.u_vga.cursor_i[4:0]);
$fwrite(df, "CTRL 0x%02X\n", dut.u_vga.ctrl_i[7:0]);
$fwrite(df, "FB 1200\n");
for (w2 = 0; w2 < 300; w2 = w2 + 1)
for (b2 = 0; b2 < 4; b2 = b2 + 1)
$fwrite(df, "%02X\n", dut.fb[w2][b2*8 +: 8]);
$fclose(df);
end
end
end
endmodule
+93
View File
@@ -0,0 +1,93 @@
# =============================================================================
# play_saolei.tcl —— 没有 VGA 显示器时,在 xsim 命令行里"玩"扫雷
#
# 用法(建议用 source -encoding utf-8 加载本文件,否则 xsim 默认按 GBK 读,中文注释会乱):
# cd <某个空目录,例如 _tmp/saolei_sim>
# xvlog -d SAOLEI_ROM -i <pipelined-cpu/srcs/board> -i <pipelined-cpu/srcs/rtl> \
# <pipelined-cpu/srcs/rtl/*.v> <pipelined-cpu/srcs/board/cpu21_board_top.v> \
# <pipelined-cpu/srcs/tb/tb_saolei_play.v>
# xelab -d SAOLEI_ROM -top tb_saolei_play -snapshot saolei_play
# xsim saolei_play -tclbatch <本脚本绝对路径>
#
# 然后在 xsim 提示符下敲命令(一个字母就是一步,会立刻重画棋盘):
# k c 左键点击(= 板上 BTNC)
# k u / k d / k l / k r 光标 上/下/左/右(也认 w s a d)
# k f 插旗模式点击(点一下插旗,再点取消;= 按住 SW3 再点)
# k p 只切换插旗模式开关(不点击)
# k z 复位(等于重新上电,回到开始界面)
# k uucc 可以连写:上、上、点、点
# shot 不按键,只重画一遍当前画面
# help 键位说明
#
# 原理:k 把字符写进 cmd.txt;tb_saolei_play 每 100 us 仿真时间轮询一次并执行按键,
# 同时每 1 ms 把显存 dump 到 now.txt;k 再调 fb2png.py --term 把画面画到终端。
# 仿真速度大约是 1 ms 仿真时间 = 0.5 s 墙钟,所以每步等 1~2 秒,比真实慢很多,
# 但足够用来验证"这个程序真的能玩"。
# =============================================================================
set script_dir [file dirname [file normalize [info script]]]
set fb2png [file normalize [file join $script_dir .. .. saolei tools fb2png.py]]
# 按键字符 -> 人类可读的动作(只用于回显)
array set KEYNAME {
c "左键点击" u "上" w "上" d "下" s "下"
l "左" a "左" r "右"
f "插旗点击" p "切插旗开关" g "go 键" z "复位"
}
# 每次只把一个字符写进 cmd.txt(TB 每 100 us 读走一个,读完就清空)
proc send_char {c} {
set fh [open "cmd.txt" "w"]
puts -nonewline $fh $c
close $fh
}
# 先确保 cmd.txt 存在,否则 TB 每 100 us 都会刷一条 could not be opened 警告
if {![file exists "cmd.txt"]} {
set fh [open "cmd.txt" "w"]
close $fh
}
# 把 now.txt 渲染到终端(ANSI 彩色棋盘)
proc render {} {
global fb2png
if {![file exists "now.txt"]} {
puts " (now.txt not ready yet - run a bit first)"
return
}
if {[catch { exec py $fb2png --term now.txt >@stdout 2>@stderr } err]} {
puts " render failed: $err"
puts " try manually: py \"$fb2png\" --term now.txt"
}
}
proc help {} {
puts " ---------------- SAOLEI controls (type at the xsim prompt) ----------------"
puts " k c left click (board BTNC)"
puts " k u/d/l/r move cursor up/down/left/right (w/s/a/d work too)"
puts " k f click in flag mode k p toggle flag switch only"
puts " k z reset k uucc several actions at once"
puts " shot redraw help this text quit exit"
puts " PNG screenshot: py \"$::fb2png\" now.txt shot.png"
puts " ---------------------------------------------------------------------------"
}
# one step: send the key -> advance simulation -> redraw
proc k {keys} {
set s [string map {" " "c"} [string tolower $keys]]
foreach ch [split $s ""] {
if {[string length $ch] == 0} { continue }
puts " >> $ch"
send_char $ch
run 1500 us
}
render
}
proc shot {} { render }
# ---- opening: run a little so the start screen is drawn, then show it ----
help
puts "booting... (init + draw start screen)"
run 3 ms
render
+68 -20
View File
@@ -3,6 +3,7 @@
用法: 用法:
py saolei/tools/fb2png.py <dump.txt> [输出.png] py saolei/tools/fb2png.py <dump.txt> [输出.png]
py saolei/tools/fb2png.py --term <dump.txt> # 只在终端打印彩色棋盘(交互玩扫雷用)
dump 格式(tb_saolei.v 里的 dump_fb 任务生成): dump 格式(tb_saolei.v 里的 dump_fb 任务生成):
CURSOR <row> <col> CURSOR <row> <col>
@@ -101,15 +102,77 @@ def parse_dump(path):
return cur, ctrl, fb 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(): 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__) raise SystemExit(__doc__)
src = sys.argv[1] src = args[0]
dst = sys.argv[2] if len(sys.argv) > 2 else src.rsplit(".", 1)[0] + ".png" dst = args[1] if len(args) > 1 else src.rsplit(".", 1)[0] + ".png"
cur, ctrl, fb = parse_dump(src) cur, ctrl, fb = parse_dump(src)
bg = (ctrl >> 4) & 0x7 bg = (ctrl >> 4) & 0x7
cursor_box = bool(ctrl & 1) and cur[0] is not None 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" print("光标 = (%s, %s) 光标可见 = %s 背景色 = %d"
% (cur[0], cur[1], bool(ctrl & 1), bg)) % (cur[0], cur[1], bool(ctrl & 1), bg))
@@ -135,23 +198,8 @@ def main():
write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows) write_png(dst, COLS * CELL_PX, ROWS * CELL_PX, rows)
print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX)) print("已生成 %s(%dx%d)" % (dst, COLS * CELL_PX, ROWS * CELL_PX))
# ---- 终端 ASCII 图 ---- # ---- 终端 ASCII 图(彩色版;PNG 已经写好了,这里只是方便快速看一眼)----
print("+" + "-" * COLS + "+") draw_term(fb, cur, cursor_box, dump_note(src))
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=数字")
main() main()