diff --git a/.gitignore b/.gitignore index 8fb9056..dcba560 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ pipelined-cpu/* !pipelined-cpu/tools/ !pipelined-cpu/programs/ !pipelined-cpu/create_project.tcl +!pipelined-cpu/create_project_saolei.tcl # 本地临时目录(仿真脚本、日志、中间文件) _tmp/ diff --git a/pipelined-cpu/create_project_saolei.tcl b/pipelined-cpu/create_project_saolei.tcl new file mode 100644 index 0000000..8ab015f --- /dev/null +++ b/pipelined-cpu/create_project_saolei.tcl @@ -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 还原成 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 要一起改。" diff --git a/pipelined-cpu/srcs/tb/tb_saolei_play.v b/pipelined-cpu/srcs/tb/tb_saolei_play.v new file mode 100644 index 0000000..036c070 --- /dev/null +++ b/pipelined-cpu/srcs/tb/tb_saolei_play.v @@ -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 diff --git a/pipelined-cpu/tools/play_saolei.tcl b/pipelined-cpu/tools/play_saolei.tcl new file mode 100644 index 0000000..6a987f9 --- /dev/null +++ b/pipelined-cpu/tools/play_saolei.tcl @@ -0,0 +1,93 @@ +# ============================================================================= +# play_saolei.tcl —— 没有 VGA 显示器时,在 xsim 命令行里"玩"扫雷 +# +# 用法(建议用 source -encoding utf-8 加载本文件,否则 xsim 默认按 GBK 读,中文注释会乱): +# cd <某个空目录,例如 _tmp/saolei_sim> +# xvlog -d SAOLEI_ROM -i -i \ +# \ +# +# 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 diff --git a/saolei/tools/fb2png.py b/saolei/tools/fb2png.py index c43f665..db1c164 100644 --- a/saolei/tools/fb2png.py +++ b/saolei/tools/fb2png.py @@ -3,6 +3,7 @@ 用法: py saolei/tools/fb2png.py [输出.png] + py saolei/tools/fb2png.py --term # 只在终端打印彩色棋盘(交互玩扫雷用) dump 格式(tb_saolei.v 里的 dump_fb 任务生成): CURSOR @@ -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()